diff --git a/amxmodx/AMBuilder b/amxmodx/AMBuilder index 16125cdc..1665ec89 100644 --- a/amxmodx/AMBuilder +++ b/amxmodx/AMBuilder @@ -94,6 +94,8 @@ binary.sources = [ 'CDataPack.cpp', 'datapacks.cpp', 'stackstructs.cpp', + 'CTextParsers.cpp', + 'textparse.cpp', ] if builder.target_platform == 'windows': diff --git a/amxmodx/CTextParsers.cpp b/amxmodx/CTextParsers.cpp new file mode 100644 index 00000000..4a3f63b1 --- /dev/null +++ b/amxmodx/CTextParsers.cpp @@ -0,0 +1,1113 @@ +/** +* vim: set ts=4 : +* ============================================================================= +* SourceMod +* Copyright (C) 2004-2008 AlliedModders LLC. All rights reserved. +* ============================================================================= +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the GNU General Public License, version 3.0, as published by the +* Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +* details. +* +* You should have received a copy of the GNU General Public License along with +* this program. If not, see . +* +* As a special exception, AlliedModders LLC gives you permission to link the +* code of this program (as well as its derivative works) to "Half-Life 2," the +* "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software +* by the Valve Corporation. You must obey the GNU General Public License in +* all respects for all other code used. Additionally, AlliedModders LLC grants +* this exception to all derivative works. AlliedModders LLC defines further +* exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), +* or . +* +* Version: $Id$ +*/ + +#include "amxmodx.h" +#include "CTextParsers.h" +/* +#include +#include +#include +#include +#include +#include +#include */ + +TextParsers g_TextParser; +ITextParsers *textparsers = &g_TextParser; + +static int g_ini_chartable1[255] = { 0 }; +static int g_ws_chartable[255] = { 0 }; + +bool TextParsers::IsWhitespace(const char *stream) +{ + return g_ws_chartable[(unsigned char)*stream] == 1; +} + +TextParsers::TextParsers() +{ + g_ini_chartable1[(unsigned)'_'] = 1; + g_ini_chartable1[(unsigned)'-'] = 1; + g_ini_chartable1[(unsigned)','] = 1; + g_ini_chartable1[(unsigned)'+'] = 1; + g_ini_chartable1[(unsigned)'.'] = 1; + g_ini_chartable1[(unsigned)'$'] = 1; + g_ini_chartable1[(unsigned)'?'] = 1; + g_ini_chartable1[(unsigned)'/'] = 1; + g_ws_chartable[(unsigned)'\n'] = 1; + g_ws_chartable[(unsigned)'\v'] = 1; + g_ws_chartable[(unsigned)'\r'] = 1; + g_ws_chartable[(unsigned)'\t'] = 1; + g_ws_chartable[(unsigned)'\f'] = 1; + g_ws_chartable[(unsigned)' '] = 1; +} + +/* +void TextParsers::OnSourceModAllInitialized() +{ + sharesys->AddInterface(NULL, this); +}*/ + +unsigned int TextParsers::GetUTF8CharBytes(const char *stream) +{ + return _GetUTF8CharBytes(stream); +} + +/** +* File streams +*/ + +bool FileStreamReader(void *stream, char *buffer, size_t maxlength, unsigned int *read) +{ + size_t num = fread(buffer, 1, maxlength, (FILE *)stream); + + *read = static_cast(num); + + if (num == 0 && feof((FILE *)stream)) + { + return true; + } + + return (ferror((FILE *)stream) == 0); +} + +SMCError TextParsers::ParseFile_SMC(const char *file, ITextListener_SMC *smc, SMCStates *states) +{ + FILE *fp = fopen(file, "rt"); + + if (!fp) + { + if (states != NULL) + { + states->line = 0; + states->col = 0; + } + return SMCError_StreamOpen; + } + + SMCError result = ParseStream_SMC(fp, FileStreamReader, smc, states); + + fclose(fp); + + return result; +} + +SMCError TextParsers::ParseSMCFile(const char *file, + ITextListener_SMC *smc_listener, + SMCStates *states, + char *buffer, + size_t maxsize) +{ + const char *errstr; + FILE *fp = fopen(file, "rt"); + + if (fp == NULL) + { + char error[256] = "unknown"; + if (states != NULL) + { + states->line = 0; + states->col = 0; + } + /*libsys->GetPlatformError(error, sizeof(error));*/ + snprintf(buffer, maxsize, "File could not be opened: %s", error); + return SMCError_StreamOpen; + } + + SMCError result = ParseStream_SMC(fp, FileStreamReader, smc_listener, states); + + fclose(fp); + + errstr = GetSMCErrorString(result); + snprintf(buffer, maxsize, "%s", errstr != NULL ? errstr : "Unknown error"); + + return result; +} + +struct RawStream +{ + const char *stream; + size_t length; + size_t pos; +}; + +bool RawStreamReader(void *stream, char *buffer, size_t maxlength, unsigned int *read) +{ + RawStream *rs = (RawStream *)stream; + + if (rs->pos >= rs->length) + { + return false; + } + + size_t remaining = rs->length - rs->pos; + + /* Use the smaller of the two */ + size_t copy = (remaining > maxlength) ? maxlength : remaining; + + memcpy(buffer, &rs->stream[rs->pos], copy); + rs->pos += copy; + *read = copy; + assert(rs->pos <= rs->length); + + return true; +} + +SMCError TextParsers::ParseSMCStream(const char *stream, + size_t length, + ITextListener_SMC *smc_listener, + SMCStates *states, + char *buffer, + size_t maxsize) +{ + RawStream rs; + SMCError result; + + rs.stream = stream; + rs.length = length; + rs.pos = 0; + + result = ParseStream_SMC(&rs, RawStreamReader, smc_listener, states); + + const char *errstr = GetSMCErrorString(result); + snprintf(buffer, maxsize, "%s", errstr != NULL ? errstr : "Unknown error"); + + return result; +} + +/** +* Raw parsing of streams with helper functions +*/ + +struct StringInfo +{ + StringInfo() : quoted(false), ptr(NULL), end(NULL), special(false) { } + bool quoted; + char *ptr; + char *end; + bool special; +}; + +const char *FixupString(StringInfo &data) +{ + if (!data.ptr) + { + return NULL; + } + + if (data.quoted) + { + data.ptr++; + } +#if defined _DEBUG + else { + /* A string will never have beginning whitespace because we ignore it in the stream. + * Furthermore, if there is trailing whitespace, the end ptr will point to it, so it is valid + * to overwrite! Lastly, the last character must be whitespace or a comment/invalid character. + */ + } +#endif + + /* Do some extra work on strings that have special quoted characters. */ + if (data.special) + { + char *outptr = data.ptr; + size_t len = data.end - data.ptr; + if (len >= 2) + { + for (size_t i = 0; i= 0; i--) + { + if (info[i].ptr) + { + return info[i].ptr; + } + } + + return NULL; +} + +SMCError TextParsers::ParseStream_SMC(void *stream, + STREAMREADER srdr, + ITextListener_SMC *smc, + SMCStates *pStates) +{ + char *reparse_point = NULL; + char in_buf[4096]; + char *parse_point = in_buf; + char *line_begin = in_buf; + unsigned int read; + unsigned int curlevel = 0; + bool in_quote = false; + bool ignoring = false; + bool eol_comment = false; + bool ml_comment = false; + unsigned int i; + SMCError err = SMCError_Okay; + SMCResult res; + SMCStates states; + char c; + + StringInfo strings[3]; + StringInfo emptystring; + + states.line = 1; + states.col = 0; + + smc->ReadSMC_ParseStart(); + + /** + * The stream reader reads in as much as it can fill the buffer with. + * It then processes the buffer. If the buffer cannot be fully processed, for example, + * a line is left hanging with no newline, then the contents of the buffer is shifted + * down, and the buffer is filled from the stream reader again. + * + * What makes this particularly annoying is that we cache pointers everywhere, so when + * the shifting process takes place, all those pointers must be shifted as well. + */ + while (srdr(stream, parse_point, sizeof(in_buf)-(parse_point - in_buf) - 1, &read)) + { + if (!read) + { + break; + } + + /* Check for BOM markings, which is only relevant on the first line. + * Not worth it, but it could be moved out of the loop. + */ + if (states.line == 1 && + in_buf[0] == (char)0xEF && + in_buf[1] == (char)0xBB && + in_buf[2] == (char)0xBF) + { + /* Move EVERYTHING down :\ */ + memmove(in_buf, &in_buf[3], read - 3); + read -= 3; + } + + if (reparse_point) + { + read += (parse_point - reparse_point); + parse_point = reparse_point; + reparse_point = NULL; + } + + for (i = 0; iReadSMC_RawLine(&states, line_begin)) != SMCResult_Continue) + { + err = (res == SMCResult_HaltFail) ? SMCError_Custom : SMCError_Okay; + goto failed; + } + parse_point[i] = '\n'; + + /* Now we check the sanity of our staged strings! */ + if (strings[2].ptr) + { + if (!curlevel) + { + err = SMCError_InvalidProperty1; + goto failed; + } + /* Assume the next string is a property and pass the info on. */ + if ((res = smc->ReadSMC_KeyValue( + &states, + FixupString(strings[2]), + FixupString(strings[1]))) != SMCResult_Continue) + { + err = (res == SMCResult_HaltFail) ? SMCError_Custom : SMCError_Okay; + goto failed; + } + scrap(strings); + } + + /* Change the states for the next line */ + states.col = 0; + states.line++; + line_begin = &parse_point[i + 1]; //Note: safe because this gets relocated later + } + else if (ignoring) + { + if (in_quote) + { + /* If i was 0, we could have reparsed, so make sure there's no buffer underrun */ + if ((&parse_point[i] != in_buf) && c == '"' && parse_point[i - 1] != '\\') + { + /* If we reached a quote in an ignore phase, + * we're staging a string and we must rotate it out. + */ + in_quote = false; + ignoring = false; + /* Set our info */ + strings[0].end = &parse_point[i]; + strings[0].quoted = true; + if (rotate(strings) != NULL) + { + /* If we rotated too many strings, there was too much crap on one line */ + err = SMCError_InvalidTokens; + goto failed; + } + } + else if (c == '\\') + { + strings[0].special = true; + if (i == (read - 1)) + { + reparse_point = &parse_point[i]; + break; + } + } + } + else if (ml_comment) + { + if (c == '*') + { + /* Check if we need to get more input first */ + if (i == read - 1) + { + reparse_point = &parse_point[i]; + break; + } + if (parse_point[i + 1] == '/') + { + ml_comment = false; + ignoring = false; + /* We should not be staging anything right now. */ + assert(strings[0].ptr == NULL); + /* Advance the input stream so we don't choke on this token */ + i++; + states.col++; + } + } + } + } + else + { + /* Check if we're whitespace or not */ + if (!g_ws_chartable[(unsigned char)c]) + { + bool restage = false; + /* Check various special tokens: + * ; + * // + * / * + * { + * } + */ + if (c == ';' || c == '/') + { + /* If it's a line-based comment (that is, ; or //) + * we will need to scrap everything until the end of the line. + */ + if (c == '/') + { + if (i == read - 1) + { + /* If we reached the end of the look-ahead, we need to re-check our input. + * Breaking out will force this to be the new reparse point! + */ + reparse_point = &parse_point[i]; + break; + } + if (parse_point[i + 1] == '/') + { + /* standard comment */ + ignoring = true; + eol_comment = true; + restage = true; + } + else if (parse_point[i + 1] == '*') + { + /* inline comment - start ignoring */ + ignoring = true; + ml_comment = true; + /* yes, we restage, meaning that: + * STR/ *stuff* /ING (space because ml comments don't nest in C++) + * will not generate 'STRING', but rather 'STR' and 'ING'. + * This should be a rare occurrence and is done here for convenience. + */ + restage = true; + } + } + else + { + ignoring = true; + eol_comment = true; + restage = true; + } + } + else if (c == '{') + { + /* If we are staging a string, we must rotate here */ + if (strings[0].ptr) + { + /* We have unacceptable tokens on this line */ + if (rotate(strings) != NULL) + { + err = SMCError_InvalidSection1; + goto failed; + } + } + /* Sections must always be alone */ + if (strings[2].ptr != NULL) + { + err = SMCError_InvalidSection1; + goto failed; + } + else if (strings[1].ptr == NULL) + { + err = SMCError_InvalidSection2; + goto failed; + } + if ((res = smc->ReadSMC_NewSection(&states, FixupString(strings[1]))) + != SMCResult_Continue) + { + err = (res == SMCResult_HaltFail) ? SMCError_Custom : SMCError_Okay; + goto failed; + } + strings[1] = emptystring; + curlevel++; + } + else if (c == '}') + { + /* Unlike our matching friend, this can be on the same line as something prior */ + if (rotate(strings) != NULL) + { + err = SMCError_InvalidSection3; + goto failed; + } + if (strings[2].ptr) + { + if (!curlevel) + { + err = SMCError_InvalidProperty1; + goto failed; + } + if ((res = smc->ReadSMC_KeyValue( + &states, + FixupString(strings[2]), + FixupString(strings[1]))) + != SMCResult_Continue) + { + err = (res == SMCResult_HaltFail) ? SMCError_Custom : SMCError_Okay; + goto failed; + } + } + else if (strings[1].ptr) + { + err = SMCError_InvalidSection3; + goto failed; + } + else if (!curlevel) + { + err = SMCError_InvalidSection4; + goto failed; + } + /* Now it's safe to leave the section */ + scrap(strings); + if ((res = smc->ReadSMC_LeavingSection(&states)) != SMCResult_Continue) + { + err = (res == SMCResult_HaltFail) ? SMCError_Custom : SMCError_Okay; + goto failed; + } + curlevel--; + } + else if (c == '"') + { + /* If we get a quote mark, we always restage, but we need to do it beforehand */ + if (strings[0].ptr) + { + strings[0].end = &parse_point[i]; + if (rotate(strings) != NULL) + { + err = SMCError_InvalidTokens; + goto failed; + } + } + strings[0].ptr = &parse_point[i]; + in_quote = true; + ignoring = true; + } + else if (!strings[0].ptr) + { + /* If we have no string, we must start one */ + strings[0].ptr = &parse_point[i]; + } + if (restage && strings[0].ptr) + { + strings[0].end = &parse_point[i]; + if (rotate(strings) != NULL) + { + err = SMCError_InvalidTokens; + goto failed; + } + } + } + else + { + /* If we're eating a string and get whitespace, we need to restage. + * (Note that if we are quoted, this is being ignored) + */ + if (strings[0].ptr) + { + /* + * The specification says the second string in a pair does not need to be quoted. + * Thus, we check if there's already a string on the stack. + * If there's a newline, we always rotate so the newline has an empty starter. + */ + if (!strings[1].ptr) + { + /* There's no string, so we must move this one down and eat up another */ + strings[0].end = &parse_point[i]; + rotate(strings); + } + else if (!strings[1].quoted) + { + err = SMCError_InvalidTokens; + goto failed; + } + } + } + } + + /* Advance which token we're on */ + states.col++; + } + + if (line_begin != in_buf) + { + /* The line buffer has advanced, so it's safe to copy N bytes back to the beginning. + * What's N? N is the lowest point we're currently relying on. + */ + char *stage = lowstring(strings); + if (!stage || stage > line_begin) + { + stage = line_begin; + } + unsigned int bytes = read - (stage - parse_point); + + /* It is now safe to delete everything before the staged point */ + memmove(in_buf, stage, bytes); + + /* Calculate the number of bytes in the new buffer */ + bytes = stage - in_buf; + /* Relocate all the cached pointers to our new base */ + line_begin -= bytes; + reloc(strings[0], bytes); + reloc(strings[1], bytes); + reloc(strings[2], bytes); + if (reparse_point) + { + reparse_point -= bytes; + } + if (parse_point) + { + parse_point = &parse_point[read]; + parse_point -= bytes; + } + } + else if (read == sizeof(in_buf)-1) + { + err = SMCError_TokenOverflow; + goto failed; + } + } + + /* If we're done parsing and there are tokens left over... */ + if (curlevel) + { + err = SMCError_InvalidSection5; + goto failed; + } + else if (strings[0].ptr || strings[1].ptr) + { + err = SMCError_InvalidTokens; + goto failed; + } + + smc->ReadSMC_ParseEnd(false, false); + + if (pStates != NULL) + { + *pStates = states; + } + + return SMCError_Okay; + +failed: + if (pStates != NULL) + { + *pStates = states; + } + + smc->ReadSMC_ParseEnd(true, (err == SMCError_Custom)); + + return err; +} + + +/** +* INI parser +*/ + +bool TextParsers::ParseFile_INI(const char *file, ITextListener_INI *ini_listener, unsigned int *line, unsigned int *col) +{ + FILE *fp = fopen(file, "rt"); + unsigned int curline = 0; + unsigned int curtok; + size_t len; + + if (!fp) + { + if (line) + { + *line = 0; + } + + return false; + } + + char buffer[2048]; + char *ptr, *save_ptr; + bool in_quote; + + while (!feof(fp)) + { + curline++; + curtok = 0; + buffer[0] = '\0'; + if (fgets(buffer, sizeof(buffer), fp) == NULL) + { + break; + } + + //:TODO: this will only run once, so find a nice way to move it out of the while loop + /* If this is the first line, check the first three bytes for BOM */ + if (curline == 1 && + buffer[0] == (char)0xEF && + buffer[1] == (char)0xBB && + buffer[2] == (char)0xBF) + { + /* We have a UTF-8 marked file... skip these bytes */ + ptr = &buffer[3]; + } + else { + ptr = buffer; + } + + /*************************************************** + * We preprocess the string before parsing tokens! * + ***************************************************/ + + /* First strip beginning whitespace */ + while (*ptr != '\0' && g_ws_chartable[(unsigned char)*ptr] != 0) + { + ptr++; + } + + len = strlen(ptr); + + if (!len) + { + continue; + } + + /* Now search for comment characters */ + in_quote = false; + save_ptr = ptr; + for (size_t i = 0; iReadINI_RawLine(ptr, &curtok)) + { + goto event_failed; + } + + if (*ptr == '[') + { + bool invalid_tokens = false; + bool got_bracket = false; + bool extra_tokens = false; + char c; + bool alnum; + wchar_t wc; + + for (size_t i = 1; iReadINI_NewSection(&ptr[1], invalid_tokens, got_bracket, extra_tokens, &curtok)) + { + goto event_failed; + } + } + else { + char *key_ptr = ptr; + char *val_ptr = NULL; + char c; + size_t first_space = 0; + bool invalid_tokens = false; + bool equal_token = false; + bool quotes = false; + bool alnum; + wchar_t wc; + + for (size_t i = 0; iReadINI_KeyValue(key_ptr, val_ptr, invalid_tokens, equal_token, quotes, &curtok)) + { + curtok = 0; + goto event_failed; + } + } + } + + if (line) + { + *line = curline; + } + + fclose(fp); + + return true; + +event_failed: + if (line) + { + *line = curline; + } + + if (col) + { + *col = curtok; + } + + fclose(fp); + + return false; +} + +const char *TextParsers::GetSMCErrorString(SMCError err) +{ + static const char *s_errors[] = + { + NULL, + "Stream failed to open", + "Stream returned read error", + NULL, + "Un-quoted section has invalid tokens", + "Section declared without header", + "Section declared with unknown tokens", + "Section ending without a matching section beginning", + "Section beginning without a matching ending", + "Line contained too many invalid tokens", + "Token buffer overflowed", + "A property was declared outside of a section", + }; + + if (err < SMCError_Okay || err > SMCError_InvalidProperty1) + { + return NULL; + } + + return s_errors[err]; +} diff --git a/amxmodx/CTextParsers.h b/amxmodx/CTextParsers.h new file mode 100644 index 00000000..45ebe52c --- /dev/null +++ b/amxmodx/CTextParsers.h @@ -0,0 +1,90 @@ +/** +* vim: set ts=4 : +* ============================================================================= +* SourceMod +* Copyright (C) 2004-2008 AlliedModders LLC. All rights reserved. +* ============================================================================= +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the GNU General Public License, version 3.0, as published by the +* Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +* details. +* +* You should have received a copy of the GNU General Public License along with +* this program. If not, see . +* +* As a special exception, AlliedModders LLC gives you permission to link the +* code of this program (as well as its derivative works) to "Half-Life 2," the +* "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software +* by the Valve Corporation. You must obey the GNU General Public License in +* all respects for all other code used. Additionally, AlliedModders LLC grants +* this exception to all derivative works. AlliedModders LLC defines further +* exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), +* or . +* +* Version: $Id$ +*/ + +#ifndef _INCLUDE_SOURCEMOD_TEXTPARSERS_H_ +#define _INCLUDE_SOURCEMOD_TEXTPARSERS_H_ + +#include +#include + +using namespace SourceMod; + +/** +* @param void * IN: Stream pointer +* @param char * IN/OUT: Stream buffer +* @param size_t IN: Maximum size of buffer +* @param unsigned int * OUT: Number of bytes read (0 = end of stream) +* @return True on success, false on failure +*/ +typedef bool(*STREAMREADER)(void *, char *, size_t, unsigned int *); + +class TextParsers : public ITextParsers +{ +public: + TextParsers(); +public: + bool ParseFile_INI(const char *file, + ITextListener_INI *ini_listener, + unsigned int *line, + unsigned int *col); + + SMCError ParseFile_SMC(const char *file, + ITextListener_SMC *smc_listener, + SMCStates *states); + + SMCError ParseSMCFile(const char *file, + ITextListener_SMC *smc_listener, + SMCStates *states, + char *buffer, + size_t maxsize); + + SMCError ParseSMCStream(const char *stream, + size_t length, + ITextListener_SMC *smc_listener, + SMCStates *states, + char *buffer, + size_t maxsize); + + unsigned int GetUTF8CharBytes(const char *stream); + + const char *GetSMCErrorString(SMCError err); + bool IsWhitespace(const char *stream); +private: + SMCError ParseStream_SMC(void *stream, + STREAMREADER srdr, + ITextListener_SMC *smc, + SMCStates *states); +}; + +extern TextParsers g_TextParser; + +#endif //_INCLUDE_SOURCEMOD_TEXTPARSERS_H_ + diff --git a/amxmodx/amxmodx.h b/amxmodx/amxmodx.h index c23ec57b..6b9973ca 100755 --- a/amxmodx/amxmodx.h +++ b/amxmodx/amxmodx.h @@ -64,6 +64,7 @@ extern AMX_NATIVE_INFO vector_Natives[]; extern AMX_NATIVE_INFO g_SortNatives[]; extern AMX_NATIVE_INFO g_DataStructNatives[]; extern AMX_NATIVE_INFO g_StackNatives[]; +extern AMX_NATIVE_INFO g_TextParserNatives[]; #if defined(_WIN32) #define DLLOAD(path) (DLHANDLE)LoadLibrary(path) diff --git a/amxmodx/meta_api.cpp b/amxmodx/meta_api.cpp index 75693f6d..c54ea4b3 100755 --- a/amxmodx/meta_api.cpp +++ b/amxmodx/meta_api.cpp @@ -30,6 +30,7 @@ #include #include "trie_natives.h" #include "CDataPack.h" +#include "textparse.h" plugin_info_t Plugin_info = { @@ -395,6 +396,7 @@ int C_Spawn(edict_t *pent) g_TrieHandles.clear(); g_TrieSnapshotHandles.clear(); g_DataPackHandles.clear(); + g_TextParsersHandles.clear(); char map_pluginsfile_path[256]; char prefixed_map_pluginsfile[256]; diff --git a/amxmodx/modules.cpp b/amxmodx/modules.cpp index 00639297..3929cb2e 100755 --- a/amxmodx/modules.cpp +++ b/amxmodx/modules.cpp @@ -552,7 +552,8 @@ int set_amxnatives(AMX* amx, char error[128]) amx_Register(amx, trie_Natives, -1); amx_Register(amx, g_DatapackNatives, -1); amx_Register(amx, g_StackNatives, -1); - + amx_Register(amx, g_TextParserNatives, -1); + //we're not actually gonna check these here anymore amx->flags |= AMX_FLAG_PRENIT; diff --git a/amxmodx/msvc10/amxmodx_mm.vcxproj b/amxmodx/msvc10/amxmodx_mm.vcxproj index 1da903e8..069d2c7a 100644 --- a/amxmodx/msvc10/amxmodx_mm.vcxproj +++ b/amxmodx/msvc10/amxmodx_mm.vcxproj @@ -306,6 +306,7 @@ + @@ -335,6 +336,7 @@ All + @@ -370,6 +372,7 @@ + diff --git a/amxmodx/msvc10/amxmodx_mm.vcxproj.filters b/amxmodx/msvc10/amxmodx_mm.vcxproj.filters index 06c8962c..67096bb7 100644 --- a/amxmodx/msvc10/amxmodx_mm.vcxproj.filters +++ b/amxmodx/msvc10/amxmodx_mm.vcxproj.filters @@ -173,9 +173,15 @@ Source Files - + Source Files + + + Source Files + + + Source Files @@ -317,6 +323,18 @@ Header Files +<<<<<<< HEAD +======= + + Header Files + + + Header Files + + + Header Files + +>>>>>>> Introduce TextParser API. diff --git a/amxmodx/textparse.cpp b/amxmodx/textparse.cpp new file mode 100644 index 00000000..0ed02579 --- /dev/null +++ b/amxmodx/textparse.cpp @@ -0,0 +1,226 @@ +/** +* vim: set ts=4 : +* ============================================================================= +* SourceMod +* Copyright (C) 2004-2008 AlliedModders LLC. All rights reserved. +* ============================================================================= +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the GNU General Public License, version 3.0, as published by the +* Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +* details. +* +* You should have received a copy of the GNU General Public License along with +* this program. If not, see . +* +* As a special exception, AlliedModders LLC gives you permission to link the +* code of this program (as well as its derivative works) to "Half-Life 2," the +* "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software +* by the Valve Corporation. You must obey the GNU General Public License in +* all respects for all other code used. Additionally, AlliedModders LLC grants +* this exception to all derivative works. AlliedModders LLC defines further +* exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), +* or . +* +* Version: $Id$ +*/ + +#include "amxmodx.h" +#include +#include + +TextParserHandles g_TextParsersHandles; + +static cell AMX_NATIVE_CALL SMC_CreateParser(AMX *amx, cell *params) +{ + return static_cast(g_TextParsersHandles.create()); +} + +static cell AMX_NATIVE_CALL SMC_SetParseStart(AMX *amx, cell *params) +{ + ParseInfo *p = g_TextParsersHandles.lookup(params[1]); + + if (p == NULL) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid map handle provided (%d)", params[1]); + return 0; + } + + int length; + const char* functionName = get_amxstring(amx, params[2], 0, length); + int function = registerSPForwardByName(amx, functionName, FP_CELL, FP_DONE); + + if (function == -1) + { + LogError(amx, AMX_ERR_NATIVE, "Function is not present (function \"%s\") (plugin \"%s\")", functionName, g_plugins.findPluginFast(amx)->getName()); + return 0; + } + + p->parse_start = function; + + return 1; +} + +static cell AMX_NATIVE_CALL SMC_SetParseEnd(AMX *amx, cell *params) +{ + ParseInfo *p = g_TextParsersHandles.lookup(params[1]); + + if (p == NULL) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid map handle provided (%d)", params[1]); + return 0; + } + + int length; + const char* funcName = get_amxstring(amx, params[2], 0, length); + int func = registerSPForwardByName(amx, funcName, FP_CELL, FP_DONE); + + if (func == -1) + { + LogError(amx, AMX_ERR_NATIVE, "Function is not present (function \"%s\") (plugin \"%s\")", funcName, g_plugins.findPluginFast(amx)->getName()); + return 0; + } + + p->parse_end = func; + + return 1; +} + +static cell AMX_NATIVE_CALL SMC_SetReaders(AMX *amx, cell *params) +{ + ParseInfo *p = g_TextParsersHandles.lookup(params[1]); + + if (p == NULL) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid map handle provided (%d)", params[1]); + return 0; + } + + int length; + const char* NewSectionFuncName = get_amxstring(amx, params[2], 0, length); + const char* KeyValueFuncName = get_amxstring(amx, params[3], 1, length); + const char* EndSectionFuncName = get_amxstring(amx, params[4], 2, length); + + int NewSectionFunc = registerSPForwardByName(amx, NewSectionFuncName, FP_CELL, FP_STRING, FP_DONE); + if (NewSectionFunc == -1) + { + LogError(amx, AMX_ERR_NATIVE, "Function is not present (function \"%s\") (plugin \"%s\")", NewSectionFuncName, g_plugins.findPluginFast(amx)->getName()); + return 0; + } + + int KeyValueFunc = registerSPForwardByName(amx, KeyValueFuncName, FP_CELL, FP_STRING, FP_STRING, FP_DONE); + if (KeyValueFunc == -1) + { + LogError(amx, AMX_ERR_NATIVE, "Function is not present (function \"%s\") (plugin \"%s\")", KeyValueFuncName, g_plugins.findPluginFast(amx)->getName()); + return 0; + } + + int EndSectionFunc = registerSPForwardByName(amx, EndSectionFuncName, FP_CELL, FP_DONE); + if (EndSectionFunc == -1) + { + LogError(amx, AMX_ERR_NATIVE, "Function is not present (function \"%s\") (plugin \"%s\")", EndSectionFuncName, g_plugins.findPluginFast(amx)->getName()); + return 0; + } + + p->new_section = NewSectionFunc; + p->key_value = KeyValueFunc; + p->end_section = EndSectionFunc; + + return 1; +} + +static cell AMX_NATIVE_CALL SMC_SetRawLine(AMX *amx, cell *params) +{ + ParseInfo *p = g_TextParsersHandles.lookup(params[1]); + + if (p == NULL) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid map handle provided (%d)", params[1]); + return 0; + } + + int length; + const char* funcName = get_amxstring(amx, params[2], 0, length); + + int func = registerSPForwardByName(amx, funcName, FP_CELL, FP_STRING, FP_CELL, FP_DONE); + if (func == -1) + { + LogError(amx, AMX_ERR_NATIVE, "Function is not present (function \"%s\") (plugin \"%s\")", funcName, g_plugins.findPluginFast(amx)->getName()); + return 0; + } + + p->raw_line = func; + + return 1; +} + +static cell AMX_NATIVE_CALL SMC_ParseFile(AMX *amx, cell *params) +{ + ParseInfo *p = g_TextParsersHandles.lookup(params[1]); + + if (p == NULL) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid map handle provided (%d)", params[1]); + return 0; + } + + int length; + const char *file = build_pathname("%s", get_amxstring(amx, params[2], 0, length)); + + SMCStates states; + SMCError p_err = textparsers->ParseFile_SMC(file, p, &states); + + *get_amxaddr(amx, params[3]) = states.line; + *get_amxaddr(amx, params[4]) = states.col; + + return (cell)p_err; +} + +static cell AMX_NATIVE_CALL SMC_GetErrorString(AMX *amx, cell *params) +{ + const char *str = textparsers->GetSMCErrorString((SMCError)params[1]); + + if (!str) + { + return 0; + } + + return set_amxstring(amx, params[2], str, params[3]); +} + +static cell AMX_NATIVE_CALL SMC_DestroyParser(AMX *amx, cell *params) +{ + cell *ptr = get_amxaddr(amx, params[1]); + + ParseInfo *p = g_TextParsersHandles.lookup(*ptr); + + if (p == NULL) + { + return 0; + } + + if (g_TextParsersHandles.destroy(*ptr)) + { + *ptr = 0; + return 1; + } + + return 0; +} + +AMX_NATIVE_INFO g_TextParserNatives[] = +{ + { "SMC_CreateParser" , SMC_CreateParser }, + { "SMC_ParseFile" , SMC_ParseFile }, + { "SMC_GetErrorString", SMC_GetErrorString }, + { "SMC_SetParseStart" , SMC_SetParseStart }, + { "SMC_SetParseEnd" , SMC_SetParseEnd }, + { "SMC_SetReaders" , SMC_SetReaders }, + { "SMC_SetRawLine" , SMC_SetRawLine }, + { "SMC_DestroyParser" , SMC_DestroyParser }, + { NULL, NULL }, +}; diff --git a/amxmodx/textparse.h b/amxmodx/textparse.h new file mode 100644 index 00000000..cdb3ec02 --- /dev/null +++ b/amxmodx/textparse.h @@ -0,0 +1,191 @@ +/** +* vim: set ts=4 : +* ============================================================================= +* SourceMod +* Copyright (C) 2004-2008 AlliedModders LLC. All rights reserved. +* ============================================================================= +* +* This program is free software; you can redistribute it and/or modify it under +* the terms of the GNU General Public License, version 3.0, as published by the +* Free Software Foundation. +* +* This program is distributed in the hope that it will be useful, but WITHOUT +* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +* details. +* +* You should have received a copy of the GNU General Public License along with +* this program. If not, see . +* +* As a special exception, AlliedModders LLC gives you permission to link the +* code of this program (as well as its derivative works) to "Half-Life 2," the +* "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software +* by the Valve Corporation. You must obey the GNU General Public License in +* all respects for all other code used. Additionally, AlliedModders LLC grants +* this exception to all derivative works. AlliedModders LLC defines further +* exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), +* or . +* +* Version: $Id$ +*/ + +#ifndef _INCLUDE_TEXTPARSE_H_ +#define _INCLUDE_TEXTPARSE_H_ + +#include "amxmodx.h" +#include "CTextParsers.h" + +class ParseInfo : public ITextListener_SMC +{ +public: + ParseInfo() + { + parse_start = -1; + parse_end = -1; + new_section = -1; + key_value = -1; + end_section = -1; + raw_line = -1; + handle = -1; + } + ~ParseInfo() {} +public: + void ReadSMC_ParseStart() + { + if (parse_start != -1) + { + executeForwards(parse_start, handle); + } + } + + void ReadSMC_ParseEnd(bool halted, bool failed) + { + if (parse_end != -1) + { + executeForwards(parse_end, handle, halted ? 1 : 0, failed ? 1 : 0); + } + } + + SMCResult ReadSMC_NewSection(const SMCStates *states, const char *name) + { + if (new_section != -1) + { + return (SMCResult)executeForwards(new_section, handle, name); + } + + return SMCResult_Continue; + } + + SMCResult ReadSMC_KeyValue(const SMCStates *states, const char *key, const char *value) + { + if (key_value != -1) + { + return (SMCResult)executeForwards(key_value, handle, key, value); + } + + return SMCResult_Continue; + } + + SMCResult ReadSMC_LeavingSection(const SMCStates *states) + { + if (end_section != -1) + { + return (SMCResult)executeForwards(end_section, handle, handle); + } + + return SMCResult_Continue; + } + + SMCResult ReadSMC_RawLine(const SMCStates *states, const char *line) + { + if (raw_line != -1) + { + return (SMCResult)executeForwards(raw_line, handle, line, states->line); + } + + return SMCResult_Continue; + } +public: + int parse_start; + int parse_end; + int new_section; + int key_value; + int end_section; + int raw_line; + int handle; +}; + +template +class TextParserHandles +{ +private: + ke::Vector m_textparsers; + +public: + TextParserHandles() { } + ~TextParserHandles() + { + this->clear(); + } + + void clear() + { + for (size_t i = 0; i < m_textparsers.length(); i++) + { + if (m_textparsers[i] != NULL) + { + delete m_textparsers[i]; + } + } + + m_textparsers.clear(); + } + T *lookup(int handle) + { + handle--; + + if (handle < 0 || handle >= static_cast(m_textparsers.length())) + { + return NULL; + } + + return m_textparsers[handle]; + } + int create() + { + for (size_t i = 0; i < m_textparsers.length(); i++) + { + if (m_textparsers[i] == NULL) + { + // reuse handle + m_textparsers[i] = new T; + + return static_cast(i)+1; + } + } + m_textparsers.append(new T); + return m_textparsers.length(); + } + bool destroy(int handle) + { + handle--; + + if (handle < 0 || handle >= static_cast(m_textparsers.length())) + { + return false; + } + + if (m_textparsers[handle] == NULL) + { + return false; + } + delete m_textparsers[handle]; + m_textparsers[handle] = NULL; + + return true; + } +}; + +extern TextParserHandles g_TextParsersHandles; + +#endif // _INCLUDE_TEXTPARSE_H_ \ No newline at end of file diff --git a/plugins/include/textparse.inc b/plugins/include/textparse.inc new file mode 100644 index 00000000..2ae996c5 --- /dev/null +++ b/plugins/include/textparse.inc @@ -0,0 +1,223 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * SourceMod (C)2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This file is part of the SourceMod/SourcePawn SDK. + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + * + * Version: $Id$ + */ + +#if defined _textparse_included + #endinput +#endif +#define _textparse_included + + +/** + * Everything below describes the SMC Parse, or "SourceMod Configuration" format. + * This parser is entirely event based. You must hook events to receive data. + * The file format itself is nearly identical to Valve's KeyValues format. + */ + +/** + * Parse result directive. + */ +enum SMCResult +{ + SMCParse_Continue, /**< Continue parsing */ + SMCParse_Halt, /**< Stop parsing here */ + SMCParse_HaltFail /**< Stop parsing and return failure */ +}; + +/** + * Parse error codes. + */ +enum SMCError +{ + SMCError_Okay = 0, /**< No error */ + SMCError_StreamOpen, /**< Stream failed to open */ + SMCError_StreamError, /**< The stream died... somehow */ + SMCError_Custom, /**< A custom handler threw an error */ + SMCError_InvalidSection1, /**< A section was declared without quotes, and had extra tokens */ + SMCError_InvalidSection2, /**< A section was declared without any header */ + SMCError_InvalidSection3, /**< A section ending was declared with too many unknown tokens */ + SMCError_InvalidSection4, /**< A section ending has no matching beginning */ + SMCError_InvalidSection5, /**< A section beginning has no matching ending */ + SMCError_InvalidTokens, /**< There were too many unidentifiable strings on one line */ + SMCError_TokenOverflow, /**< The token buffer overflowed */ + SMCError_InvalidProperty1, /**< A property was declared outside of any section */ +}; + +enum TextParser +{ + Invalid_TextPaser = 0 +}; + +/** + * Creates a new SMC file format parser. + * This is used to set parse hooks. + * + * @return A new Handle to an SMC Parse structure. + */ +native TextParser:SMC_CreateParser(); + +/** + * Parses an SMC file. + * + * @param smc A Handle to an SMC Parse structure. + * @param file A string containing the file path. + * @param line An optional by reference cell to store the last line number read. + * @param col An optional by reference cell to store the last column number read. + + * @return An SMCParseError result. + * @error Invalid or corrupt Handle. + */ +native SMCError:SMC_ParseFile(TextParser:smc, const file[], &line = 0, &col = 0); + +/** + * Gets an error string for an SMCError code. + * + * @note SMCError_Okay returns false. + * @note SMCError_Custom (which is thrown on SMCParse_HaltFail) returns false. + * + * @param error The SMCParseError code. + * @param buffer A string buffer for the error (contents undefined on failure). + * @param buf_max The maximum size of the buffer. + * + * @return True on success, false otherwise. + */ +native bool:SMC_GetErrorString(SMCError:error, buffer[], buf_max); + +/** + * Sets the SMC_ParseStart function of a parse Handle. + * Below is the prototype of callback.: + * + * - ParseStart: + * Called when parsing is started. + * + * @param smc The SMC Parse Handle. + * @noreturn + * + * public SMC_ParseStart(TextParser:smc) + * + * @param smc Handle to an SMC Parse. + * @param func A ParseStart callback. + * + * @noreturn + * @error Invalid or corrupt Handle. + */ +native SMC_SetParseStart(TextParser:smc, const func[]); + +/** + * Sets the SMC_ParseEnd of a parse handle. + * Below is the prototype of callback.: + * + * - ParseEnd: + * Called when parsing is halted. + * + * @param smc The SMC Parse Handle. + * @param halted True if abnormally halted, false otherwise. + * @param failed True if parsing failed, false otherwise. + * @noreturn + * + * public SMC_ParseEnd(TextParser:smc, bool:halted, bool:failed) + * + * @param smc Handle to an SMC Parse. + * @param func A ParseEnd callback.. + * + * @noreturn + * @error Invalid or corrupt Handle. + */ +native SMC_SetParseEnd(TextParser:smc, const func[]); + +/** + * Sets the three main reader functions. + * Below are the different prototypes of callbacks: + * + * - NewSection: + * Called when the parser finds the end of the current section. + * @note Enclosing quotes are always stripped. + * + * @param smc The SMC Parse Handle. + * @param name String containing section name. + * @return An SMCResult action to take. + * + * public SMCResult:SMC_NewSection(TextParser:smc, const name[]) + * + * - KeyValue: + * Called when the parser finds a new key/value pair. + * @note Enclosing quotes are always stripped. + * + * @param smc The SMC Parse Handle. + * @param key String containing key name. + * @param value String containing value name. + * @return An SMCResult action to take. + * + * public SMCResult:SMC_KeyValue(TextParser:smc, const key[], const value[]) + * + * - EndSection: + * Called when the parser finds the end of the current section. + * + * @param smc The SMC Parse Handle. + * + * public SMCResult:SMC_EndSection(TextParser:smc) + * - + * @param smc An SMC parse Handle. + * @param ns A NewSection callback. + * @param kv A KeyValue callback. + * @param es An EndSection callback. + * + * @noreturn + */ +native SMC_SetReaders(TextParser:smc, const nsFunc[], const kvFunc[], const esFunc[]); + +/** + * Sets a raw line reader on an SMC parser Handle. + * Below is the prototype of callback.: + * + * - RawLine: + * Called whenever a raw line is read. + * + * @param smc The SMC Parse Handle. + * @param line A string containing the raw line from the file. + * @param lineno The line number it occurs on. + * @return An SMCResult action to take. + * + * public SMCResult:SMC_RawLine(TextParser:smc, const line[], lineno) + * + * @param smc Handle to an SMC Parse. + * @param func A RawLine callback.. + * @noreturn + */ +native SMC_SetRawLine(TextParser:smc, const func[]); + +/** + * Disposes of a text parser. + * + * @param smc Handle to an SMC Parse. + * @return True if disposed, false otherwise. + */ +native SMC_DestroyParser(&TextParser:smc); \ No newline at end of file diff --git a/plugins/testsuite/textparse_test.sma b/plugins/testsuite/textparse_test.sma new file mode 100644 index 00000000..d5ab402c --- /dev/null +++ b/plugins/testsuite/textparse_test.sma @@ -0,0 +1,116 @@ +#include +#include + +new SuccessCount; +new Trie:ExpectedKVData; + +public plugin_init() +{ + register_concmd("textparse_vdf", "ConsoleCommand_TextParseVDF"); + register_clcmd("textparse_ini", "ServerCommand_TextParseINI"); +} + +/** + * VDF Config format + */ + +public ConsoleCommand_TextParseVDF() +{ + server_print("Testing text parser with VDF config file format..."); + + new const configFile[] = "addons/amxmodx/scripting/testsuite/textparse_test.cfg"; + + ExpectedKVData = TrieCreate(); + TrieSetString(ExpectedKVData, "Logging", "on"); + TrieSetString(ExpectedKVData, "LogMode", "daily"); + TrieSetString(ExpectedKVData, "ServerLang", "en"); + TrieSetString(ExpectedKVData, "PublicChatTrigger", "!"); + TrieSetString(ExpectedKVData, "SilentChatTrigger", "/"); + TrieSetString(ExpectedKVData, "SilentFailSuppress", "no"); + TrieSetString(ExpectedKVData, "PassInfoVar", "_password"); + TrieSetString(ExpectedKVData, "MenuItemSound", "buttons/button14.wav"); + TrieSetString(ExpectedKVData, "MenuExitSound", "buttons/combine_button7.wav"); + TrieSetString(ExpectedKVData, "MenuExitBackSound", "buttons/combine_button7.wav"); + TrieSetString(ExpectedKVData, "AllowClLanguageVar", "On"); + TrieSetString(ExpectedKVData, "DisableAutoUpdate", "no"); + TrieSetString(ExpectedKVData, "ForceRestartAfterUpdate", "no"); + TrieSetString(ExpectedKVData, "AutoUpdateURL", "http://update.sourcemod.net/update/"); + TrieSetString(ExpectedKVData, "DebugSpew", "no"); + TrieSetString(ExpectedKVData, "SteamAuthstringValidation", "yes"); + TrieSetString(ExpectedKVData, "BlockBadPlugins", "yes"); + TrieSetString(ExpectedKVData, "SlowScriptTimeout", "8"); + + new const expectedSectionCount = 2; // Include start and end. + new const expectedStartEndCount = 2; + new const expectedKeyValueCount = TrieGetSize(ExpectedKVData); + new const expectedLineCount = file_size(configFile, .flag = 1) - 1; + + new TextParser:parser = SMC_CreateParser(); + + SMC_SetReaders(parser, "ReadCore_NewSection", "ReadCore_KeyValue", "ReadCore_EndSection"); + SMC_SetParseStart(parser, "ReadCore_ParseStart"); + SMC_SetRawLine(parser, "ReadCore_CurrentLine"); + SMC_SetParseEnd(parser, "ReadCore_ParseEnd"); + + new line, col; + new SMCError:err = SMC_ParseFile_VDF(parser, configFile, line, col); + + if (err != SMCError_Okay) + { + new buffer[64]; + server_print("%s", SMC_GetErrorString(err, buffer, charsmax(buffer)) ? buffer : "Fatal parse error"); + } + + if (line == expectedLineCount + 1 && col == 2) + { + ++SuccessCount; + } + + SMC_DestroyParser(parser); + + server_print("^tTests successful: %d/%d", SuccessCount, expectedStartEndCount + expectedSectionCount + expectedKeyValueCount + expectedLineCount + 1); +} + +public ReadCore_ParseStart(TextParser:smc) +{ + ++SuccessCount; +} + +public ReadCore_NewSection(TextParser:smc, const name[]) +{ + ++SuccessCount; +} + +public ReadCore_KeyValue(TextParser:smc, const key[], const value[]) +{ + new buffer[128]; + if (TrieGetString(ExpectedKVData, key, buffer, charsmax(buffer)) && equal(value, buffer)) + { + ++SuccessCount; + } +} + +public ReadCore_EndSection(TextParser:smc) +{ + ++SuccessCount; +} + +public ReadCore_CurrentLine(TextParser:smc, const line[], lineno) +{ + ++SuccessCount; +} + +public ReadCore_ParseEnd(TextParser:smc) +{ + ++SuccessCount; +} + + + +/** + * INI Config format + */ +public ServerCommand_TextParseINI() +{ + +} diff --git a/public/ITextParsers.h b/public/ITextParsers.h new file mode 100644 index 00000000..9dff6692 --- /dev/null +++ b/public/ITextParsers.h @@ -0,0 +1,450 @@ +/** + * vim: set ts=4 : + * ============================================================================= + * SourceMod + * Copyright (C) 2004-2008 AlliedModders LLC. All rights reserved. + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + * + * As a special exception, AlliedModders LLC gives you permission to link the + * code of this program (as well as its derivative works) to "Half-Life 2," the + * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software + * by the Valve Corporation. You must obey the GNU General Public License in + * all respects for all other code used. Additionally, AlliedModders LLC grants + * this exception to all derivative works. AlliedModders LLC defines further + * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007), + * or . + * + * Version: $Id$ + */ + +#ifndef _INCLUDE_SOURCEMOD_TEXTPARSERS_INTERFACE_H_ +#define _INCLUDE_SOURCEMOD_TEXTPARSERS_INTERFACE_H_ + +/** + * @file ITextParsers.h + * @brief Defines various text/file parsing functions, as well as UTF-8 support code. + */ +namespace SourceMod +{ + + #define SMINTERFACE_TEXTPARSERS_NAME "ITextParsers" + #define SMINTERFACE_TEXTPARSERS_VERSION 4 + + /** + * The INI file format is defined as: + * WHITESPACE: 0x20, \n, \t, \r + * IDENTIFIER: A-Z a-z 0-9 _ - , + . $ ? / + * STRING: Any set of symbols + * + * Basic syntax is comprised of SECTIONs. + * A SECTION is defined as: + * [SECTIONNAME] + * OPTION + * OPTION + * OPTION... + * + * SECTIONNAME is an IDENTIFIER. + * OPTION can be repeated any number of times, once per line. + * OPTION is defined as one of: + * KEY = "VALUE" + * KEY = VALUE + * KEY + * Where KEY is an IDENTIFIER and VALUE is a STRING. + * + * WHITESPACE should always be omitted. + * COMMENTS should be stripped, and are defined as text occurring in: + * ; + * + * Example file below. Note that + * The second line is technically invalid. The event handler + * must decide whether this should be allowed. + * --FILE BELOW-- + * [gaben] + * hi = clams + * bye = "NO CLAMS" + * + * [valve] + * cannot + * maintain + * products + */ + + /** + * @brief Contains parse events for INI files. + */ + class ITextListener_INI + { + public: + /** + * @brief Returns version number. + */ + virtual unsigned int GetTextParserVersion1() + { + return SMINTERFACE_TEXTPARSERS_VERSION; + } + public: + /** + * @brief Called when a new section is encountered in an INI file. + * + * @param section Name of section in between the [ and ] characters. + * @param invalid_tokens True if invalid tokens were detected in the name. + * @param close_bracket True if a closing bracket was detected, false otherwise. + * @param extra_tokens True if extra tokens were detected on the line. + * @param curtok Contains current token in the line where the section name starts. + * You can add to this offset when failing to point to a token. + * @return True to keep parsing, false otherwise. + */ + virtual bool ReadINI_NewSection(const char *section, + bool invalid_tokens, + bool close_bracket, + bool extra_tokens, + unsigned int *curtok) + { + return true; + } + + /** + * @brief Called when encountering a key/value pair in an INI file. + * + * @param key Name of key. + * @param value String containing value (with quotes stripped, if any). + * @param invalid_tokens Whether or not the key contained invalid tokens. + * @param equal_token There was an '=' sign present (in case the value is missing). + * @param quotes Whether value was enclosed in quotes. + * @param curtok Contains the token index of the start of the value string. + * This can be changed when returning false. + * @return True to keep parsing, false otherwise. + */ + virtual bool ReadINI_KeyValue(const char *key, + const char *value, + bool invalid_tokens, + bool equal_token, + bool quotes, + unsigned int *curtok) + { + return true; + } + + /** + * @brief Called after a line has been preprocessed, if it has text. + * + * @param line Contents of line. + * @param curtok Pointer to optionally store failed position in string. + * @return True to keep parsing, false otherwise. + */ + virtual bool ReadINI_RawLine(const char *line, unsigned int *curtok) + { + return true; + } + }; + + /** + * :TODO: write this in CFG (context free grammar) format so it makes sense + * + * The SMC file format is defined as: + * WHITESPACE: 0x20, \n, \t, \r + * IDENTIFIER: Any ASCII character EXCLUDING ", {, }, ;, //, / *, or WHITESPACE. + * STRING: Any set of symbols enclosed in quotes. + * Note: if a STRING does not have quotes, it is parsed as an IDENTIFIER. + * + * Basic syntax is comprised of SECTIONBLOCKs. + * A SECTIONBLOCK defined as: + * + * SECTIONNAME + * { + * OPTION + * } + * + * OPTION can be repeated any number of times inside a SECTIONBLOCK. + * A new line will terminate an OPTION, but there can be more than one OPTION per line. + * OPTION is defined any of: + * "KEY" "VALUE" + * SECTIONBLOCK + * + * SECTIONNAME, KEY, VALUE, and SINGLEKEY are strings + * SECTIONNAME cannot have trailing characters if quoted, but the quotes can be optionally removed. + * If SECTIONNAME is not enclosed in quotes, the entire sectionname string is used (minus surrounding whitespace). + * If KEY is not enclosed in quotes, the key is terminated at first whitespace. + * If VALUE is not properly enclosed in quotes, the entire value string is used (minus surrounding whitespace). + * The VALUE may have inner quotes, but the key string may not. + * + * For an example, see configs/permissions.cfg + * + * WHITESPACE should be ignored. + * Comments are text occurring inside the following tokens, and should be stripped + * unless they are inside literal strings: + * ; + * // + * / * */ + + /** + * @brief Lists actions to take when an SMC parse hook is done. + */ + enum SMCResult + { + SMCResult_Continue, /**< Continue parsing */ + SMCResult_Halt, /**< Stop parsing here */ + SMCResult_HaltFail /**< Stop parsing and return SMCError_Custom */ + }; + + /** + * @brief Lists error codes possible from parsing an SMC file. + */ + enum SMCError + { + SMCError_Okay = 0, /**< No error */ + SMCError_StreamOpen, /**< Stream failed to open */ + SMCError_StreamError, /**< The stream died... somehow */ + SMCError_Custom, /**< A custom handler threw an error */ + SMCError_InvalidSection1, /**< A section was declared without quotes, and had extra tokens */ + SMCError_InvalidSection2, /**< A section was declared without any header */ + SMCError_InvalidSection3, /**< A section ending was declared with too many unknown tokens */ + SMCError_InvalidSection4, /**< A section ending has no matching beginning */ + SMCError_InvalidSection5, /**< A section beginning has no matching ending */ + SMCError_InvalidTokens, /**< There were too many unidentifiable strings on one line */ + SMCError_TokenOverflow, /**< The token buffer overflowed */ + SMCError_InvalidProperty1, /**< A property was declared outside of any section */ + }; + + /** + * @brief States for line/column + */ + struct SMCStates + { + unsigned int line; /**< Current line */ + unsigned int col; /**< Current col */ + }; + + /** + * @brief Describes the events available for reading an SMC stream. + */ + class ITextListener_SMC + { + public: + /** + * @brief Returns version number. + */ + virtual unsigned int GetTextParserVersion2() + { + return SMINTERFACE_TEXTPARSERS_VERSION; + } + public: + /** + * @brief Called when starting parsing. + */ + virtual void ReadSMC_ParseStart() + { + }; + + /** + * @brief Called when ending parsing. + * + * @param halted True if abnormally halted, false otherwise. + * @param failed True if parsing failed, false otherwise. + */ + virtual void ReadSMC_ParseEnd(bool halted, bool failed) + { + } + + /** + * @brief Called when entering a new section + * + * @param states Parsing states. + * @param name Name of section, with the colon omitted. + * @return SMCResult directive. + */ + virtual SMCResult ReadSMC_NewSection(const SMCStates *states, const char *name) + { + return SMCResult_Continue; + } + + /** + * @brief Called when encountering a key/value pair in a section. + * + * @param states Parsing states. + * @param key Key string. + * @param value Value string. If no quotes were specified, this will be NULL, + * and key will contain the entire string. + * @return SMCResult directive. + */ + virtual SMCResult ReadSMC_KeyValue(const SMCStates *states, const char *key, const char *value) + { + return SMCResult_Continue; + } + + /** + * @brief Called when leaving the current section. + * + * @param states Parsing states. + * @return SMCResult directive. + */ + virtual SMCResult ReadSMC_LeavingSection(const SMCStates *states) + { + return SMCResult_Continue; + } + + /** + * @brief Called after an input line has been preprocessed. + * + * @param states Parsing states. + * @param line Contents of the line, null terminated at the position + * of the newline character (thus, no newline will exist). + * @return SMCResult directive. + */ + virtual SMCResult ReadSMC_RawLine(const SMCStates *states, const char *line) + { + return SMCResult_Continue; + } + }; + + /** + * @brief Contains various text stream parsing functions. + */ + class ITextParsers /*: public SMInterface*/ + { + public: + virtual const char *GetInterfaceName() + { + return SMINTERFACE_TEXTPARSERS_NAME; + } + virtual unsigned int GetInterfaceVersion() + { + return SMINTERFACE_TEXTPARSERS_VERSION; + } + virtual bool IsVersionCompatible(unsigned int version) + { + if (version < 2) + { + return false; + } + + return true; + /*return SMInterface::IsVersionCompatible(version);*/ + } + public: + /** + * @brief Parses an INI-format file. + * + * @param file Path to file. + * @param ini_listener Event handler for reading file. + * @param line If non-NULL, will contain last line parsed (0 if file could not be opened). + * @param col If non-NULL, will contain last column parsed (undefined if file could not be opened). + * @return True if parsing succeeded, false if file couldn't be opened or there was a syntax error. + */ + virtual bool ParseFile_INI(const char *file, + ITextListener_INI *ini_listener, + unsigned int *line, + unsigned int *col) =0; + + /** + * @brief Parses an SMC-format text file. + * Note that the parser makes every effort to obey broken syntax. + * For example, if an open brace is missing, but the section name has a colon, + * it will let you know. It is up to the event handlers to decide whether to be strict or not. + * + * @param file Path to file. + * @param smc_listener Event handler for reading file. + * @param states Optional pointer to store last known states. + * @return An SMCError result code. + */ + virtual SMCError ParseFile_SMC(const char *file, + ITextListener_SMC *smc_listener, + SMCStates *states) =0; + + /** + * @brief Converts an SMCError to a string. + * + * @param err SMCError. + * @return String error message, or NULL if none. + */ + virtual const char *GetSMCErrorString(SMCError err) =0; + + public: + /** + * @brief Returns the number of bytes that a multi-byte character contains in a UTF-8 stream. + * If the current character is not multi-byte, the function returns 1. + * + * @param stream Pointer to multi-byte ANSI character string. + * @return Number of bytes in current character. + */ + virtual unsigned int GetUTF8CharBytes(const char *stream) =0; + + /** + * @brief Returns whether the first multi-byte character in the given stream + * is a whitespace character. + * + * @param stream Pointer to multi-byte character string. + * @return True if first character is whitespace, false otherwise. + */ + virtual bool IsWhitespace(const char *stream) =0; + + /** + * @brief Same as ParseFile_SMC, but with an extended error buffer. + * + * @param file Path to file. + * @param smc_listener Event handler for reading file. + * @param states Optional pointer to store last known states. + * @param buffer Error message buffer. + * @param maxsize Maximum size of the error buffer. + * @return Error code. + */ + virtual SMCError ParseSMCFile(const char *file, + ITextListener_SMC *smc_listener, + SMCStates *states, + char *buffer, + size_t maxsize) =0; + + /** + * @brief Parses a raw UTF8 stream as an SMC file. + * + * @param stream Memory containing data. + * @param length Number of bytes in the stream. + * @param smc_listener Event handler for reading file. + * @param states Optional pointer to store last known states. + * @param buffer Error message buffer. + * @param maxsize Maximum size of the error buffer. + * @return Error code. + */ + virtual SMCError ParseSMCStream(const char *stream, + size_t length, + ITextListener_SMC *smc_listener, + SMCStates *states, + char *buffer, + size_t maxsize) =0; + }; + + inline unsigned int _GetUTF8CharBytes(const char *stream) + { + unsigned char c = *(unsigned char *)stream; + if (c & (1<<7)) + { + if (c & (1<<5)) + { + if (c & (1<<4)) + { + return 4; + } + return 3; + } + return 2; + } + return 1; + } +} + +extern SourceMod::ITextParsers *textparsers; + +#endif //_INCLUDE_SOURCEMOD_TEXTPARSERS_INTERFACE_H_ +