2 * ===============================================================
3 * SourceMod (C)2004-2007 AlliedModders LLC. All rights reserved.
4 * ===============================================================
6 * This file is not open source and may not be copied without explicit
7 * written permission of AlliedModders LLC. This file may not be redistributed
8 * in whole or significant part.
9 * For information, see LICENSE.txt or http://www.sourcemod.net/license.php
20 #include "TextParsers.h"
23 TextParsers g_TextParser;
25 static int g_ini_chartable1[255] = {0};
26 static int g_ws_chartable[255] = {0};
28 bool IsWhitespace(const char *stream)
30 return g_ws_chartable[(unsigned)*stream] == 1;
33 TextParsers::TextParsers()
35 g_ini_chartable1[(unsigned)'_'] = 1;
36 g_ini_chartable1[(unsigned)'-'] = 1;
37 g_ini_chartable1[(unsigned)','] = 1;
38 g_ini_chartable1[(unsigned)'+'] = 1;
39 g_ini_chartable1[(unsigned)'.'] = 1;
40 g_ini_chartable1[(unsigned)'$'] = 1;
41 g_ini_chartable1[(unsigned)'?'] = 1;
42 g_ini_chartable1[(unsigned)'/'] = 1;
43 g_ws_chartable[(unsigned)'\n'] = 1;
44 g_ws_chartable[(unsigned)'\v'] = 1;
45 g_ws_chartable[(unsigned)'\r'] = 1;
46 g_ws_chartable[(unsigned)'\t'] = 1;
47 g_ws_chartable[(unsigned)'\f'] = 1;
48 g_ws_chartable[(unsigned)' '] = 1;
51 void TextParsers::OnSourceModAllInitialized()
53 g_ShareSys.AddInterface(NULL, this);
56 unsigned int TextParsers::GetUTF8CharBytes(const char *stream)
58 return _GetUTF8CharBytes(stream);
70 bool CharStreamReader(void *stream, char *buffer, size_t maxlength, unsigned int *read)
72 CharStream *srdr = (CharStream *)stream;
74 const char *ptr = srdr->curpos;
75 for (size_t i=0; i<maxlength; i++)
84 *read = ptr - srdr->curpos;
91 SMCParseError TextParsers::ParseString_SMC(const char *stream,
92 ITextListener_SMC *smc,
96 CharStream srdr = { stream };
98 return ParseStream_SMC(&srdr, CharStreamReader, smc, line, col);
105 bool FileStreamReader(void *stream, char *buffer, size_t maxlength, unsigned int *read)
107 size_t num = fread(buffer, 1, maxlength, (FILE *)stream);
109 *read = static_cast<unsigned int>(num);
111 if (num == 0 && feof((FILE *)stream))
116 return (ferror((FILE *)stream) == 0);
119 SMCParseError TextParsers::ParseFile_SMC(const char *file, ITextListener_SMC *smc, unsigned int *line, unsigned int *col)
121 FILE *fp = fopen(file, "rt");
125 return SMCParse_StreamOpen;
128 SMCParseError result = ParseStream_SMC(fp, FileStreamReader, smc, line, col);
136 * Raw parsing of streams with helper functions
141 StringInfo() : quoted(false), ptr(NULL), end(NULL), special(false) { }
148 const char *FixupString(StringInfo &data)
161 /* A string will never have beginning whitespace because we ignore it in the stream.
162 * Furthermore, if there is trailing whitespace, the end ptr will point to it, so it is valid
163 * to overwrite! Lastly, the last character must be whitespace or a comment/invalid character.
170 //:TODO: this string has special tokens in it, like \, and we must
171 //resolve these before passing the string back to the app
179 const char *rotate(StringInfo info[3])
181 if (info[2].ptr != NULL)
186 if (info[0].ptr != NULL)
190 info[0] = StringInfo();
196 void scrap(StringInfo info[3])
198 info[2] = StringInfo();
199 info[1] = StringInfo();
200 info[0] = StringInfo();
203 void reloc(StringInfo &data, unsigned int bytes)
215 char *lowstring(StringInfo info[3])
217 for (int i=2; i>=0; i--)
228 SMCParseError TextParsers::ParseStream_SMC(void *stream,
230 ITextListener_SMC *smc,
235 char *reparse_point = NULL;
236 char *parse_point = in_buf;
237 char *line_begin = in_buf;
239 unsigned int curline = 1;
240 unsigned int curtok = 0;
241 unsigned int curlevel = 0;
242 bool in_quote = false;
243 bool ignoring = false;
244 bool eol_comment = false;
245 bool ml_comment = false;
247 SMCParseError err = SMCParse_Okay;
251 StringInfo strings[3];
252 StringInfo emptystring;
254 smc->ReadSMC_ParseStart();
256 while (srdr(stream, parse_point, sizeof(in_buf) - (parse_point - line_begin) - 1, &read))
263 /* :TODO: do this outside of the main loop somehow
264 * This checks for BOM markings
267 in_buf[0] == (char)0xEF &&
268 in_buf[1] == (char)0xBB &&
269 in_buf[2] == (char)0xBF)
271 parse_point = &in_buf[3];
276 read += (parse_point - reparse_point);
277 parse_point = reparse_point;
278 reparse_point = NULL;
281 for (i=0; i<read; i++)
286 /* If we got a newline, there's a lot of things that could have happened in the interim.
287 * First, let's make sure the staged strings are rotated.
291 strings[0].end = &parse_point[i];
292 if (rotate(strings) != NULL)
294 err = SMCParse_InvalidTokens;
299 /* Next, let's clear some line-based values that may no longer have meaning */
302 if (ignoring && !ml_comment)
307 /* Pass the raw line onto the listener */
308 if ((res=smc->ReadSMC_RawLine(line_begin, curline)) != SMCParse_Continue)
310 err = (res == SMCParse_HaltFail) ? SMCParse_Custom : SMCParse_Okay;
314 /* Now we check the sanity of our staged strings! */
319 err = SMCParse_InvalidProperty1;
322 /* Assume the next string is a property and pass the info on. */
323 if ((res=smc->ReadSMC_KeyValue(
324 FixupString(strings[2]),
325 FixupString(strings[1]),
327 strings[1].quoted)) != SMCParse_Continue)
329 err = (res == SMCParse_HaltFail) ? SMCParse_Custom : SMCParse_Okay;
335 /* Change the states for the next line */
338 line_begin = &parse_point[i+1]; //Note: safe because this gets relocated later
339 } else if (ignoring) {
342 /* If i was 0, this case is impossible due to reparsing */
343 if ((i != 0) && c == '"' && parse_point[i-1] != '\\')
345 /* If we reached a quote in an ignore phase,
346 * we're staging a string and we must rotate it out.
351 strings[0].end = &parse_point[i];
352 strings[0].quoted = true;
353 if (rotate(strings) != NULL)
355 /* If we rotated too many strings, there was too much crap on one line */
356 err = SMCParse_InvalidTokens;
359 } else if (c == '\\' && i == (read - 1)) {
360 strings[0].special = true;
361 reparse_point = &parse_point[i];
364 } else if (ml_comment) {
367 /* Check if we need to get more input first */
370 reparse_point = &parse_point[i];
373 if (parse_point[i+1] == '/')
377 /* We should not be staging anything right now. */
378 assert(strings[0].ptr == NULL);
379 /* Advance the input stream so we don't choke on this token */
386 /* Check if we're whitespace or not */
387 if (!g_ws_chartable[(unsigned)c])
389 bool restage = false;
390 /* Check various special tokens:
397 if (c == ';' || c == '/')
399 /* If it's a line-based comment (that is, ; or //)
400 * we will need to scrap everything until the end of the line.
406 /* If we reached the end of the look-ahead, we need to re-check our input.
407 * Breaking out will force this to be the new reparse point!
409 reparse_point = &reparse_point[i];
412 if (parse_point[i + 1] == '/')
414 /* standard comment */
418 } else if (parse_point[i+1] == '*') {
419 /* inline comment - start ignoring */
422 /* yes, we restage, meaning that:
423 * STR/ *stuff* /ING (space because ml comments don't nest in C++)
424 * will not generate 'STRING', but rather 'STR' and 'ING'.
425 * This should be a rare occurrence and is done here for convenience.
434 } else if (c == '{') {
435 /* If we are staging a string, we must rotate here */
438 /* We have unacceptable tokens on this line */
439 if (rotate(strings) != NULL)
441 err = SMCParse_InvalidSection1;
445 /* Sections must always be alone */
446 if (strings[2].ptr != NULL)
448 err = SMCParse_InvalidSection1;
450 } else if (strings[1].ptr == NULL) {
451 err = SMCParse_InvalidSection2;
454 if ((res=smc->ReadSMC_NewSection(FixupString(strings[1]), strings[1].quoted))
455 != SMCParse_Continue)
457 err = (res == SMCParse_HaltFail) ? SMCParse_Custom : SMCParse_Okay;
460 strings[1] = emptystring;
462 } else if (c == '}') {
463 /* Unlike our matching friend, this can be on the same line as something prior */
464 if (rotate(strings) != NULL)
466 err = SMCParse_InvalidSection3;
473 err = SMCParse_InvalidProperty1;
476 if ((res=smc->ReadSMC_KeyValue(
477 FixupString(strings[2]),
478 FixupString(strings[1]),
481 != SMCParse_Continue)
483 err = (res == SMCParse_HaltFail) ? SMCParse_Custom : SMCParse_Okay;
486 } else if (strings[1].ptr) {
487 err = SMCParse_InvalidSection3;
489 } else if (!curlevel) {
490 err = SMCParse_InvalidSection4;
493 /* Now it's safe to leave the section */
495 if ((res=smc->ReadSMC_LeavingSection()) != SMCParse_Continue)
497 err = (res == SMCParse_HaltFail) ? SMCParse_Custom : SMCParse_Okay;
501 } else if (c == '"') {
502 /* If we get a quote mark, we always restage, but we need to do it beforehand */
505 strings[0].end = &parse_point[i];
506 if (rotate(strings) != NULL)
508 err = SMCParse_InvalidTokens;
512 strings[0].ptr = &parse_point[i];
515 } else if (!strings[0].ptr) {
516 /* If we have no string, we must start one */
517 strings[0].ptr = &parse_point[i];
519 if (restage && strings[0].ptr)
521 strings[0].end = &parse_point[i];
522 if (rotate(strings) != NULL)
524 err = SMCParse_InvalidTokens;
529 /* If we're eating a string and get whitespace, we need to restage.
530 * (Note that if we are quoted, this is being ignored)
535 * The specification says the second string in a pair does not need to be quoted.
536 * Thus, we check if there's already a string on the stack.
537 * If there's a newline, we always rotate so the newline has an empty starter.
541 /* There's no string, so we must move this one down and eat up another */
542 strings[0].end = &parse_point[i];
544 } else if (!strings[1].quoted) {
545 err = SMCParse_InvalidTokens;
552 /* Advance which token we're on */
556 if (line_begin != in_buf)
558 /* The line buffer has advanced, so it's safe to copy N bytes back to the beginning.
559 * What's N? N is the lowest point we're currently relying on.
561 char *stage = lowstring(strings);
562 if (!stage || stage > line_begin)
566 unsigned int bytes = read - (stage - parse_point);
568 /* It is now safe to delete everything before the staged point */
569 memmove(in_buf, stage, bytes);
571 /* Calculate the number of bytes in the new buffer */
572 bytes = stage - in_buf;
573 /* Relocate all the cached pointers to our new base */
575 reloc(strings[0], bytes);
576 reloc(strings[1], bytes);
577 reloc(strings[2], bytes);
580 reparse_point -= bytes;
584 parse_point = &parse_point[read];
585 parse_point -= bytes;
587 } else if (read == sizeof(in_buf) - 1) {
588 err = SMCParse_TokenOverflow;
593 /* If we're done parsing and there are tokens left over... */
596 err = SMCParse_InvalidSection5;
598 } else if (strings[0].ptr || strings[1].ptr) {
599 err = SMCParse_InvalidTokens;
603 smc->ReadSMC_ParseEnd(false, false);
605 return SMCParse_Okay;
613 smc->ReadSMC_ParseEnd(true, (err == SMCParse_Custom));
628 bool TextParsers::ParseFile_INI(const char *file, ITextListener_INI *ini_listener, unsigned int *line, unsigned int *col)
630 FILE *fp = fopen(file, "rt");
631 unsigned int curline = 0;
646 char *ptr, *save_ptr;
654 if (fgets(buffer, sizeof(buffer), fp) == NULL)
659 //:TODO: this will only run once, so find a nice way to move it out of the while loop
660 /* If this is the first line, check the first three bytes for BOM */
662 buffer[0] == (char)0xEF &&
663 buffer[1] == (char)0xBB &&
664 buffer[2] == (char)0xBF)
666 /* We have a UTF-8 marked file... skip these bytes */
672 /***************************************************
673 * We preprocess the string before parsing tokens! *
674 ***************************************************/
676 /* First strip beginning whitespace */
677 while (*ptr != '\0' && g_ws_chartable[(unsigned)*ptr] != 0)
689 /* Now search for comment characters */
692 for (size_t i=0; i<len; i++,ptr++)
707 /* Terminate the string here */
727 /* Lastly, strip ending whitespace off */
728 for (size_t i=len-1; i>=0 && i<len; i--)
730 if (g_ws_chartable[(unsigned)ptr[i]])
744 if (!ini_listener->ReadINI_RawLine(ptr, &curtok))
751 bool invalid_tokens = false;
752 bool got_bracket = false;
753 bool extra_tokens = false;
758 for (size_t i=1; i<len; i++)
765 if (mbtowc(&wc, &ptr[i], len-i) != -1)
767 alnum = (iswalnum(wc) != 0);
768 i += _GetUTF8CharBytes(&ptr[i]) - 1;
771 alnum = (isalnum(c) != 0) || (g_ini_chartable1[(unsigned)c] != 0);
775 /* First check - is this a bracket? */
780 /* If this isn't the last character... */
789 /* n...No! Continue copying. */
790 invalid_tokens = true;
795 /* Tell the handler */
796 if (!ini_listener->ReadINI_NewSection(&ptr[1], invalid_tokens, got_bracket, extra_tokens, &curtok))
802 char *val_ptr = NULL;
804 size_t first_space = 0;
805 bool invalid_tokens = false;
806 bool equal_token = false;
811 for (size_t i=0; i<len; i++)
815 /* is this an invalid char? */
818 if (mbtowc(&wc, &ptr[i], len-i) != -1)
820 alnum = (iswalnum(wc) != 0);
821 i += _GetUTF8CharBytes(&ptr[i]) - 1;
824 alnum = (isalnum(c) != 0) || (g_ini_chartable1[(unsigned)c] != 0);
829 if (g_ws_chartable[(unsigned)c])
831 /* if it's a space, keep track of the first occurring space */
839 /* if it's an equal sign, we're done with the key */
842 /* remove excess whitespace */
843 key_ptr[first_space] = '\0';
845 /* remove the equal sign */
848 if (ptr[++i] != '\0')
850 /* If this isn't the end, set next pointer */
856 /* Mark that we got something invalid! */
857 invalid_tokens = true;
864 /* Now we need to parse the value, if any */
867 /* eat up spaces! there shouldn't be any h*/
868 while ((*val_ptr != '\0') && g_ws_chartable[(unsigned)*val_ptr] != 0)
872 if (*val_ptr == '\0')
877 /* Do we have an initial quote? If so, the parsing rules change! */
878 if (*val_ptr == '"' && *val_ptr != '\0')
880 len = strlen(val_ptr);
881 if (val_ptr[len-1] == '"')
884 val_ptr[--len] = '\0';
892 curtok = val_ptr - buffer;
893 if (!ini_listener->ReadINI_KeyValue(key_ptr, val_ptr, invalid_tokens, equal_token, quotes, &curtok))
926 const char *TextParsers::GetSMCErrorString(SMCParseError err)
928 static const char *s_errors[] =
931 "Stream failed to open",
932 "Stream returned read error",
934 "Un-quoted section has invalid tokens",
935 "Section declared without header",
936 "Section declared with unknown tokens",
937 "Section ending without a matching section beginning",
938 "Section beginning without a matching ending",
939 "Line contained too many invalid tokens",
940 "Token buffer overflowed",
941 "A property was declared outside of a section",
944 if (err < SMCParse_Okay || err > SMCParse_InvalidProperty1)
949 return s_errors[err];