3 * ===============================================================
4 * SourceMod (C)2004-2007 AlliedModders LLC. All rights reserved.
5 * ===============================================================
7 * This file is not open source and may not be copied without explicit
8 * written permission of AlliedModders LLC. This file may not be redistributed
9 * in whole or significant part.
10 * For information, see LICENSE.txt or http://www.sourcemod.net/license.php
21 #include "TextParsers.h"
24 TextParsers g_TextParser;
26 static int g_ini_chartable1[255] = {0};
27 static int g_ws_chartable[255] = {0};
29 bool IsWhitespace(const char *stream)
31 return g_ws_chartable[(unsigned)*stream] == 1;
34 TextParsers::TextParsers()
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_ini_chartable1[(unsigned)'/'] = 1;
44 g_ws_chartable[(unsigned)'\n'] = 1;
45 g_ws_chartable[(unsigned)'\v'] = 1;
46 g_ws_chartable[(unsigned)'\r'] = 1;
47 g_ws_chartable[(unsigned)'\t'] = 1;
48 g_ws_chartable[(unsigned)'\f'] = 1;
49 g_ws_chartable[(unsigned)' '] = 1;
52 void TextParsers::OnSourceModAllInitialized()
54 g_ShareSys.AddInterface(NULL, this);
57 unsigned int TextParsers::GetUTF8CharBytes(const char *stream)
59 return _GetUTF8CharBytes(stream);
71 bool CharStreamReader(void *stream, char *buffer, size_t maxlength, unsigned int *read)
73 CharStream *srdr = (CharStream *)stream;
75 const char *ptr = srdr->curpos;
76 for (size_t i=0; i<maxlength; i++)
85 *read = ptr - srdr->curpos;
92 SMCParseError TextParsers::ParseString_SMC(const char *stream,
93 ITextListener_SMC *smc,
97 CharStream srdr = { stream };
99 return ParseStream_SMC(&srdr, CharStreamReader, smc, line, col);
106 bool FileStreamReader(void *stream, char *buffer, size_t maxlength, unsigned int *read)
108 size_t num = fread(buffer, 1, maxlength, (FILE *)stream);
110 *read = static_cast<unsigned int>(num);
112 if (num == 0 && feof((FILE *)stream))
117 return (ferror((FILE *)stream) == 0);
120 SMCParseError TextParsers::ParseFile_SMC(const char *file, ITextListener_SMC *smc, unsigned int *line, unsigned int *col)
122 FILE *fp = fopen(file, "rt");
126 return SMCParse_StreamOpen;
129 SMCParseError result = ParseStream_SMC(fp, FileStreamReader, smc, line, col);
137 * Raw parsing of streams with helper functions
142 StringInfo() : quoted(false), ptr(NULL), end(NULL), special(false) { }
149 const char *FixupString(StringInfo &data)
162 /* A string will never have beginning whitespace because we ignore it in the stream.
163 * Furthermore, if there is trailing whitespace, the end ptr will point to it, so it is valid
164 * to overwrite! Lastly, the last character must be whitespace or a comment/invalid character.
169 /* Do some extra work on strings that have special quoted characters. */
172 char *outptr = data.ptr;
173 size_t len = data.end - data.ptr;
176 for (size_t i=0; i<len; i++)
178 if (data.ptr[i] == '\\' && i < len - 1)
180 /* Resolve the next character. */
182 if (data.ptr[i] == 'n')
185 } else if (data.ptr[i] == 't') {
187 } else if (data.ptr[i] == 'r') {
189 } else if (data.ptr[i] != '\\'
190 && data.ptr[i] != '"') {
191 /* This character is invalid, so go back one */
195 *outptr++ = data.ptr[i];
206 const char *rotate(StringInfo info[3])
208 if (info[2].ptr != NULL)
213 if (info[0].ptr != NULL)
217 info[0] = StringInfo();
223 void scrap(StringInfo info[3])
225 info[2] = StringInfo();
226 info[1] = StringInfo();
227 info[0] = StringInfo();
230 void reloc(StringInfo &data, unsigned int bytes)
242 char *lowstring(StringInfo info[3])
244 for (int i=2; i>=0; i--)
255 SMCParseError TextParsers::ParseStream_SMC(void *stream,
257 ITextListener_SMC *smc,
262 char *reparse_point = NULL;
263 char *parse_point = in_buf;
264 char *line_begin = in_buf;
266 unsigned int curline = 1;
267 unsigned int curtok = 0;
268 unsigned int curlevel = 0;
269 bool in_quote = false;
270 bool ignoring = false;
271 bool eol_comment = false;
272 bool ml_comment = false;
274 SMCParseError err = SMCParse_Okay;
278 StringInfo strings[3];
279 StringInfo emptystring;
281 smc->ReadSMC_ParseStart();
283 while (srdr(stream, parse_point, sizeof(in_buf) - (parse_point - line_begin) - 1, &read))
290 /* :TODO: do this outside of the main loop somehow
291 * This checks for BOM markings
294 in_buf[0] == (char)0xEF &&
295 in_buf[1] == (char)0xBB &&
296 in_buf[2] == (char)0xBF)
298 parse_point = &in_buf[3];
303 read += (parse_point - reparse_point);
304 parse_point = reparse_point;
305 reparse_point = NULL;
308 for (i=0; i<read; i++)
313 /* If we got a newline, there's a lot of things that could have happened in the interim.
314 * First, let's make sure the staged strings are rotated.
318 strings[0].end = &parse_point[i];
319 if (rotate(strings) != NULL)
321 err = SMCParse_InvalidTokens;
326 /* Next, let's clear some line-based values that may no longer have meaning */
329 if (ignoring && !ml_comment)
334 /* Pass the raw line onto the listener */
335 if ((res=smc->ReadSMC_RawLine(line_begin, curline)) != SMCParse_Continue)
337 err = (res == SMCParse_HaltFail) ? SMCParse_Custom : SMCParse_Okay;
341 /* Now we check the sanity of our staged strings! */
346 err = SMCParse_InvalidProperty1;
349 /* Assume the next string is a property and pass the info on. */
350 if ((res=smc->ReadSMC_KeyValue(
351 FixupString(strings[2]),
352 FixupString(strings[1]),
354 strings[1].quoted)) != SMCParse_Continue)
356 err = (res == SMCParse_HaltFail) ? SMCParse_Custom : SMCParse_Okay;
362 /* Change the states for the next line */
365 line_begin = &parse_point[i+1]; //Note: safe because this gets relocated later
366 } else if (ignoring) {
369 /* If i was 0, this case is impossible due to reparsing */
370 if ((i != 0) && c == '"' && parse_point[i-1] != '\\')
372 /* If we reached a quote in an ignore phase,
373 * we're staging a string and we must rotate it out.
378 strings[0].end = &parse_point[i];
379 strings[0].quoted = true;
380 if (rotate(strings) != NULL)
382 /* If we rotated too many strings, there was too much crap on one line */
383 err = SMCParse_InvalidTokens;
386 } else if (c == '\\') {
387 strings[0].special = true;
390 reparse_point = &parse_point[i];
394 } else if (ml_comment) {
397 /* Check if we need to get more input first */
400 reparse_point = &parse_point[i];
403 if (parse_point[i+1] == '/')
407 /* We should not be staging anything right now. */
408 assert(strings[0].ptr == NULL);
409 /* Advance the input stream so we don't choke on this token */
416 /* Check if we're whitespace or not */
417 if (!g_ws_chartable[(unsigned)c])
419 bool restage = false;
420 /* Check various special tokens:
427 if (c == ';' || c == '/')
429 /* If it's a line-based comment (that is, ; or //)
430 * we will need to scrap everything until the end of the line.
436 /* If we reached the end of the look-ahead, we need to re-check our input.
437 * Breaking out will force this to be the new reparse point!
439 reparse_point = &parse_point[i];
442 if (parse_point[i + 1] == '/')
444 /* standard comment */
448 } else if (parse_point[i+1] == '*') {
449 /* inline comment - start ignoring */
452 /* yes, we restage, meaning that:
453 * STR/ *stuff* /ING (space because ml comments don't nest in C++)
454 * will not generate 'STRING', but rather 'STR' and 'ING'.
455 * This should be a rare occurrence and is done here for convenience.
464 } else if (c == '{') {
465 /* If we are staging a string, we must rotate here */
468 /* We have unacceptable tokens on this line */
469 if (rotate(strings) != NULL)
471 err = SMCParse_InvalidSection1;
475 /* Sections must always be alone */
476 if (strings[2].ptr != NULL)
478 err = SMCParse_InvalidSection1;
480 } else if (strings[1].ptr == NULL) {
481 err = SMCParse_InvalidSection2;
484 if ((res=smc->ReadSMC_NewSection(FixupString(strings[1]), strings[1].quoted))
485 != SMCParse_Continue)
487 err = (res == SMCParse_HaltFail) ? SMCParse_Custom : SMCParse_Okay;
490 strings[1] = emptystring;
492 } else if (c == '}') {
493 /* Unlike our matching friend, this can be on the same line as something prior */
494 if (rotate(strings) != NULL)
496 err = SMCParse_InvalidSection3;
503 err = SMCParse_InvalidProperty1;
506 if ((res=smc->ReadSMC_KeyValue(
507 FixupString(strings[2]),
508 FixupString(strings[1]),
511 != SMCParse_Continue)
513 err = (res == SMCParse_HaltFail) ? SMCParse_Custom : SMCParse_Okay;
516 } else if (strings[1].ptr) {
517 err = SMCParse_InvalidSection3;
519 } else if (!curlevel) {
520 err = SMCParse_InvalidSection4;
523 /* Now it's safe to leave the section */
525 if ((res=smc->ReadSMC_LeavingSection()) != SMCParse_Continue)
527 err = (res == SMCParse_HaltFail) ? SMCParse_Custom : SMCParse_Okay;
531 } else if (c == '"') {
532 /* If we get a quote mark, we always restage, but we need to do it beforehand */
535 strings[0].end = &parse_point[i];
536 if (rotate(strings) != NULL)
538 err = SMCParse_InvalidTokens;
542 strings[0].ptr = &parse_point[i];
545 } else if (!strings[0].ptr) {
546 /* If we have no string, we must start one */
547 strings[0].ptr = &parse_point[i];
549 if (restage && strings[0].ptr)
551 strings[0].end = &parse_point[i];
552 if (rotate(strings) != NULL)
554 err = SMCParse_InvalidTokens;
559 /* If we're eating a string and get whitespace, we need to restage.
560 * (Note that if we are quoted, this is being ignored)
565 * The specification says the second string in a pair does not need to be quoted.
566 * Thus, we check if there's already a string on the stack.
567 * If there's a newline, we always rotate so the newline has an empty starter.
571 /* There's no string, so we must move this one down and eat up another */
572 strings[0].end = &parse_point[i];
574 } else if (!strings[1].quoted) {
575 err = SMCParse_InvalidTokens;
582 /* Advance which token we're on */
586 if (line_begin != in_buf)
588 /* The line buffer has advanced, so it's safe to copy N bytes back to the beginning.
589 * What's N? N is the lowest point we're currently relying on.
591 char *stage = lowstring(strings);
592 if (!stage || stage > line_begin)
596 unsigned int bytes = read - (stage - parse_point);
598 /* It is now safe to delete everything before the staged point */
599 memmove(in_buf, stage, bytes);
601 /* Calculate the number of bytes in the new buffer */
602 bytes = stage - in_buf;
603 /* Relocate all the cached pointers to our new base */
605 reloc(strings[0], bytes);
606 reloc(strings[1], bytes);
607 reloc(strings[2], bytes);
610 reparse_point -= bytes;
614 parse_point = &parse_point[read];
615 parse_point -= bytes;
617 } else if (read == sizeof(in_buf) - 1) {
618 err = SMCParse_TokenOverflow;
623 /* If we're done parsing and there are tokens left over... */
626 err = SMCParse_InvalidSection5;
628 } else if (strings[0].ptr || strings[1].ptr) {
629 err = SMCParse_InvalidTokens;
633 smc->ReadSMC_ParseEnd(false, false);
635 return SMCParse_Okay;
643 smc->ReadSMC_ParseEnd(true, (err == SMCParse_Custom));
658 bool TextParsers::ParseFile_INI(const char *file, ITextListener_INI *ini_listener, unsigned int *line, unsigned int *col)
660 FILE *fp = fopen(file, "rt");
661 unsigned int curline = 0;
676 char *ptr, *save_ptr;
684 if (fgets(buffer, sizeof(buffer), fp) == NULL)
689 //:TODO: this will only run once, so find a nice way to move it out of the while loop
690 /* If this is the first line, check the first three bytes for BOM */
692 buffer[0] == (char)0xEF &&
693 buffer[1] == (char)0xBB &&
694 buffer[2] == (char)0xBF)
696 /* We have a UTF-8 marked file... skip these bytes */
702 /***************************************************
703 * We preprocess the string before parsing tokens! *
704 ***************************************************/
706 /* First strip beginning whitespace */
707 while (*ptr != '\0' && g_ws_chartable[(unsigned)*ptr] != 0)
719 /* Now search for comment characters */
722 for (size_t i=0; i<len; i++,ptr++)
737 /* Terminate the string here */
757 /* Lastly, strip ending whitespace off */
758 for (size_t i=len-1; i>=0 && i<len; i--)
760 if (g_ws_chartable[(unsigned)ptr[i]])
774 if (!ini_listener->ReadINI_RawLine(ptr, &curtok))
781 bool invalid_tokens = false;
782 bool got_bracket = false;
783 bool extra_tokens = false;
788 for (size_t i=1; i<len; i++)
795 if (mbtowc(&wc, &ptr[i], len-i) != -1)
797 alnum = (iswalnum(wc) != 0);
798 i += _GetUTF8CharBytes(&ptr[i]) - 1;
801 alnum = (isalnum(c) != 0) || (g_ini_chartable1[(unsigned)c] != 0);
805 /* First check - is this a bracket? */
810 /* If this isn't the last character... */
819 /* n...No! Continue copying. */
820 invalid_tokens = true;
825 /* Tell the handler */
826 if (!ini_listener->ReadINI_NewSection(&ptr[1], invalid_tokens, got_bracket, extra_tokens, &curtok))
832 char *val_ptr = NULL;
834 size_t first_space = 0;
835 bool invalid_tokens = false;
836 bool equal_token = false;
841 for (size_t i=0; i<len; i++)
845 /* is this an invalid char? */
848 if (mbtowc(&wc, &ptr[i], len-i) != -1)
850 alnum = (iswalnum(wc) != 0);
851 i += _GetUTF8CharBytes(&ptr[i]) - 1;
854 alnum = (isalnum(c) != 0) || (g_ini_chartable1[(unsigned)c] != 0);
859 if (g_ws_chartable[(unsigned)c])
861 /* if it's a space, keep track of the first occurring space */
869 /* if it's an equal sign, we're done with the key */
872 /* remove excess whitespace */
873 key_ptr[first_space] = '\0';
875 /* remove the equal sign */
878 if (ptr[++i] != '\0')
880 /* If this isn't the end, set next pointer */
886 /* Mark that we got something invalid! */
887 invalid_tokens = true;
894 /* Now we need to parse the value, if any */
897 /* eat up spaces! there shouldn't be any h*/
898 while ((*val_ptr != '\0') && g_ws_chartable[(unsigned)*val_ptr] != 0)
902 if (*val_ptr == '\0')
907 /* Do we have an initial quote? If so, the parsing rules change! */
908 if (*val_ptr == '"' && *val_ptr != '\0')
910 len = strlen(val_ptr);
911 if (val_ptr[len-1] == '"')
914 val_ptr[--len] = '\0';
922 curtok = val_ptr - buffer;
923 if (!ini_listener->ReadINI_KeyValue(key_ptr, val_ptr, invalid_tokens, equal_token, quotes, &curtok))
956 const char *TextParsers::GetSMCErrorString(SMCParseError err)
958 static const char *s_errors[] =
961 "Stream failed to open",
962 "Stream returned read error",
964 "Un-quoted section has invalid tokens",
965 "Section declared without header",
966 "Section declared with unknown tokens",
967 "Section ending without a matching section beginning",
968 "Section beginning without a matching ending",
969 "Line contained too many invalid tokens",
970 "Token buffer overflowed",
971 "A property was declared outside of a section",
974 if (err < SMCParse_Okay || err > SMCParse_InvalidProperty1)
979 return s_errors[err];