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..3d508899 --- /dev/null +++ b/amxmodx/CTextParsers.cpp @@ -0,0 +1,1130 @@ +/** +* 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));*/ + UTIL_Format(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); + UTIL_Format(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); + UTIL_Format(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; + } + + ini_listener->ReadINI_ParseStart(); + + 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; + } + + if (col) + { + *col = curtok; + } + + fclose(fp); + + ini_listener->ReadINI_ParseEnd(false); + + return true; + +event_failed: + if (line) + { + *line = curline; + } + + if (col) + { + *col = curtok; + } + + fclose(fp); + + ini_listener->ReadINI_ParseEnd(true); + + 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/Makefile b/amxmodx/Makefile index fc136574..10aae948 100755 --- a/amxmodx/Makefile +++ b/amxmodx/Makefile @@ -21,7 +21,8 @@ OBJECTS = meta_api.cpp CFile.cpp CVault.cpp vault.cpp float.cpp file.cpp modules CMenu.cpp util.cpp amx.cpp amxdbg.cpp natives.cpp newmenus.cpp debugger.cpp \ optimizer.cpp format.cpp messages.cpp libraries.cpp vector.cpp sorting.cpp \ nongpl_matches.cpp CFlagManager.cpp datastructs.cpp \ - trie_natives.cpp CDataPack.cpp datapacks.cpp stackstructs.cpp + trie_natives.cpp CDataPack.cpp datapacks.cpp stackstructs.cpp \ + CTextParsers.cpp textparse.cpp \ ############################################## ### CONFIGURE ANY OTHER FLAGS/OPTIONS HERE ### 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..43b7cb13 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 @@ + @@ -389,6 +392,7 @@ + @@ -401,6 +405,8 @@ + + diff --git a/amxmodx/msvc10/amxmodx_mm.vcxproj.filters b/amxmodx/msvc10/amxmodx_mm.vcxproj.filters index 06c8962c..3b1d4395 100644 --- a/amxmodx/msvc10/amxmodx_mm.vcxproj.filters +++ b/amxmodx/msvc10/amxmodx_mm.vcxproj.filters @@ -177,6 +177,12 @@ Source Files + + Source Files + + + Source Files + @@ -317,6 +323,12 @@ Header Files + + Header Files + + + Header Files + @@ -405,6 +417,12 @@ Pawn Includes + + Pawn Includes + + + Pawn Includes + diff --git a/amxmodx/textparse.cpp b/amxmodx/textparse.cpp new file mode 100644 index 00000000..d566221a --- /dev/null +++ b/amxmodx/textparse.cpp @@ -0,0 +1,394 @@ +/** + * vim: set ts=4 sw=4 tw=99 noet: + * + * AMX Mod X, based on AMX Mod by Aleksander Naszko ("OLO"). + * Copyright (C) The AMX Mod X Development Team. + * + * This software is licensed under the GNU General Public License, version 3 or higher. + * Additional exceptions apply. For full license details, see LICENSE.txt or visit: + * https://alliedmods.net/amxmodx-license + */ + +#include "amxmodx.h" +#include +#include + +TextParserHandles g_TextParsersHandles; + +cell createParser() +{ + return g_TextParsersHandles.create(); +} + +cell destroyParser(cell *handle) +{ + ParseInfo *p = g_TextParsersHandles.lookup(*handle); + + if (p == NULL) + { + return 0; + } + + if (g_TextParsersHandles.destroy(*handle)) + { + *handle = 0; + return 1; + } + + return 0; +} + + +/** + * SMC CONFIG. + */ + +// native SMCParser:SMC_CreateParser(); +static cell AMX_NATIVE_CALL SMC_CreateParser(AMX *amx, cell *params) +{ + return createParser(); +} + +// native SMC_SetParseStart(SMCParser:handle, const func[]); +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 SMC parse handle (%d)", params[1]); + return 0; + } + + int length = 0; + const char *funcName = NULL; + + if ((funcName = get_amxstring(amx, params[2], 0, length)) && length) + { + p->parse_start = registerSPForwardByName(amx, funcName, FP_CELL, FP_DONE); + } + + if (p->parse_start == -1) + { + LogError(amx, AMX_ERR_NATIVE, "Function is not present (function \"%s\") (plugin \"%s\")", funcName, g_plugins.findPluginFast(amx)->getName()); + return 0; + } + + return 1; +} + +// native SMC_SetParseEnd(SMCParser:handle, const func[]); +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 SMC parse handle (%d)", params[1]); + return 0; + } + + int length = 0; + const char *funcName = NULL; + + if ((funcName = get_amxstring(amx, params[2], 0, length)) && length) + { + p->parse_end = registerSPForwardByName(amx, funcName, FP_CELL, FP_CELL, FP_CELL, FP_DONE); + } + + if (p->parse_end == -1) + { + LogError(amx, AMX_ERR_NATIVE, "Function is not present (function \"%s\") (plugin \"%s\")", funcName, g_plugins.findPluginFast(amx)->getName()); + return 0; + } + + return 1; +} + +// native SMC_SetReaders(SMCParser:smc, const kvFunc[], const nsFunc[] = "", const esFunc[] = ""); +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 SMC parse handle (%d)", params[1]); + return 0; + } + + int kvLength = 0, nsLength = 0, esLength = 0; + const char *funcName = NULL; + + if ((funcName = get_amxstring(amx, params[2], 0, kvLength)) && kvLength) + { + p->key_value = registerSPForwardByName(amx, funcName, FP_CELL, FP_STRING, FP_STRING, FP_DONE); + } + + if (kvLength && (funcName = get_amxstring(amx, params[3], 1, nsLength)) && nsLength) + { + p->new_section = registerSPForwardByName(amx, funcName, FP_CELL, FP_STRING, FP_DONE); + } + + if (kvLength && (funcName = get_amxstring(amx, params[4], 2, esLength)) && esLength) + { + p->end_section = registerSPForwardByName(amx, funcName, FP_CELL, FP_DONE); + } + + if (p->key_value == -1 || (nsLength && p->new_section == -1) || (esLength && p->end_section == -1)) + { + LogError(amx, AMX_ERR_NATIVE, "Function is not present (function \"%s\") (plugin \"%s\")", funcName, g_plugins.findPluginFast(amx)->getName()); + return 0; + } + + return 1; +} + +// native SMC_SetRawLine(SMCParser:handle, const func[]); +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 SMC parse handle (%d)", params[1]); + return 0; + } + + int length = 0; + const char *funcName = NULL; + + if ((funcName = get_amxstring(amx, params[2], 0, length)) && length) + { + p->raw_line = registerSPForwardByName(amx, funcName, FP_CELL, FP_STRING, FP_CELL, FP_DONE); + } + + if (p->raw_line == -1) + { + LogError(amx, AMX_ERR_NATIVE, "Function is not present (function \"%s\") (plugin \"%s\")", funcName, g_plugins.findPluginFast(amx)->getName()); + return 0; + } + + return 1; +} + +// native SMCError:SMC_ParseFile(SMCParser:handle, const file[], &line = 0, &col = 0); +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 SMC parse handle (%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 static_cast(p_err); +} + +// native bool:SMC_GetErrorString(SMCError:error, buffer[], buf_max); +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]); +} + +// native SMC_DestroyParser(&SMCParser:handle); +static cell AMX_NATIVE_CALL SMC_DestroyParser(AMX *amx, cell *params) +{ + return destroyParser(get_amxaddr(amx, params[1])); +} + + +/** + * INI CONFIG + */ + +// native INIParser:INI_CreateParser(); +static cell AMX_NATIVE_CALL INI_CreateParser(AMX *amx, cell *params) +{ + return createParser(); +} + +// native bool:INI_ParseFile(INIParser:handle, const file[], &line = 0, &col = 0); +static cell AMX_NATIVE_CALL INI_ParseFile(AMX *amx, cell *params) +{ + ParseInfo *p = g_TextParsersHandles.lookup(params[1]); + + if (p == NULL) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid INI parse handle (%d)", params[1]); + return 0; + } + + int length; + const char *file = build_pathname("%s", get_amxstring(amx, params[2], 0, length)); + + unsigned int line, col; + bool result = textparsers->ParseFile_INI(file, p, &line, &col); + + *get_amxaddr(amx, params[3]) = line; + *get_amxaddr(amx, params[4]) = col; + + return result; +} + +// native INI_SetParseStart(INIParser:handle, const func[]); +static cell AMX_NATIVE_CALL INI_SetParseStart(AMX *amx, cell *params) +{ + ParseInfo *p = g_TextParsersHandles.lookup(params[1]); + + if (p == NULL) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid INI parse handle (%d)", params[1]); + return 0; + } + + int length = 0; + const char *funcName = NULL; + + if ((funcName = get_amxstring(amx, params[2], 0, length)) && length) + { + p->parse_start = registerSPForwardByName(amx, funcName, FP_CELL, FP_DONE); + } + + if (p->parse_start == -1) + { + LogError(amx, AMX_ERR_NATIVE, "Function is not present (function \"%s\") (plugin \"%s\")", funcName, g_plugins.findPluginFast(amx)->getName()); + return 0; + } + + return 1; +} + +// native INI_SetParseEnd(INIParser:handle, const func[]); +static cell AMX_NATIVE_CALL INI_SetParseEnd(AMX *amx, cell *params) +{ + ParseInfo *p = g_TextParsersHandles.lookup(params[1]); + + if (p == NULL) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid INI parse handle (%d)", params[1]); + return 0; + } + + int length = 0; + const char *funcName = NULL; + + if ((funcName = get_amxstring(amx, params[2], 0, length))) + { + p->parse_end = registerSPForwardByName(amx, funcName, FP_CELL, FP_CELL, FP_CELL, FP_DONE); + } + + if (p->parse_end == -1) + { + LogError(amx, AMX_ERR_NATIVE, "Function is not present (function \"%s\") (plugin \"%s\")", funcName, g_plugins.findPluginFast(amx)->getName()); + return 0; + } + + return 1; +} + +// native INI_SetReaders(INIParser:smc, const kvFunc[], const nsFunc[] = "" ); +static cell AMX_NATIVE_CALL INI_SetReaders(AMX *amx, cell *params) +{ + ParseInfo *p = g_TextParsersHandles.lookup(params[1]); + + if (p == NULL) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid INI parse handle (%d)", params[1]); + return 0; + } + + int kvLength = 0, nsLength = 0; + const char *funcName = NULL; + + if ((funcName = get_amxstring(amx, params[2], 0, kvLength)) && kvLength) + { + p->key_value = registerSPForwardByName(amx, funcName, FP_CELL, FP_STRING, FP_STRING, FP_CELL, FP_CELL, FP_CELL, FP_CELL, FP_DONE); + } + + if (kvLength && (funcName = get_amxstring(amx, params[3], 1, nsLength)) && nsLength) + { + p->new_section = registerSPForwardByName(amx, funcName, FP_CELL, FP_STRING, FP_CELL, FP_CELL, FP_CELL, FP_CELL, FP_DONE); + } + + if (p->key_value == -1 || (nsLength && p->new_section == -1)) + { + LogError(amx, AMX_ERR_NATIVE, "Function is not present (function \"%s\") (plugin \"%s\")", funcName, g_plugins.findPluginFast(amx)->getName()); + return 0; + } + + return 1; +} + +// native INI_SetRawLine(INIParser:handle, const func[]); +static cell AMX_NATIVE_CALL INI_SetRawLine(AMX *amx, cell *params) +{ + ParseInfo *p = g_TextParsersHandles.lookup(params[1]); + + if (p == NULL) + { + LogError(amx, AMX_ERR_NATIVE, "Invalid INI parse handle (%d)", params[1]); + return 0; + } + + int length = 0; + const char *funcName = NULL; + + if ((funcName = get_amxstring(amx, params[2], 0, length)) && length) + { + p->raw_line = registerSPForwardByName(amx, funcName, FP_CELL, FP_STRING, FP_CELL, FP_CELL, FP_DONE); + } + + if (p->raw_line == -1) + { + LogError(amx, AMX_ERR_NATIVE, "Function is not present (function \"%s\") (plugin \"%s\")", funcName, g_plugins.findPluginFast(amx)->getName()); + return 0; + } + + return 1; +} + +// native INI_DestroyParser(&INIParser:handle); +static cell AMX_NATIVE_CALL INI_DestroyParser(AMX *amx, cell *params) +{ + return destroyParser(get_amxaddr(amx, params[1])); +} + + +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 }, + + { "INI_CreateParser" , INI_CreateParser }, + { "INI_ParseFile" , INI_ParseFile }, + { "INI_SetParseStart" , INI_SetParseStart }, + { "INI_SetParseEnd" , INI_SetParseEnd }, + { "INI_SetReaders" , INI_SetReaders }, + { "INI_SetRawLine" , INI_SetRawLine }, + { "INI_DestroyParser" , INI_DestroyParser }, + + { NULL, NULL }, +}; diff --git a/amxmodx/textparse.h b/amxmodx/textparse.h new file mode 100644 index 00000000..0d844f30 --- /dev/null +++ b/amxmodx/textparse.h @@ -0,0 +1,207 @@ +/** + * vim: set ts=4 sw=4 tw=99 noet: + * + * AMX Mod X, based on AMX Mod by Aleksander Naszko ("OLO"). + * Copyright (C) The AMX Mod X Development Team. + * + * This software is licensed under the GNU General Public License, version 3 or higher. + * Additional exceptions apply. For full license details, see LICENSE.txt or visit: + * https://alliedmods.net/amxmodx-license + */ + +#ifndef _INCLUDE_TEXTPARSE_H_ +#define _INCLUDE_TEXTPARSE_H_ + +#include "amxmodx.h" +#include "CTextParsers.h" + +class ParseInfo : + public ITextListener_SMC, + public ITextListener_INI +{ +public: + ParseInfo() + { + parse_start = -1; + parse_end = -1; + new_section = -1; + key_value = -1; + end_section = -1; + raw_line = -1; + handle = -1; + } + +public: + + /** + * SMC CONFIG. + */ + + 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); + + 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; + } + + + /** + * INI CONFIG. + */ + + void ReadINI_ParseStart() + { + if (parse_start != -1) + executeForwards(parse_start, handle); + } + + void ReadINI_ParseEnd(bool halted) + { + if (parse_end != -1) + executeForwards(parse_end, handle, halted ? 1 : 0); + } + + bool ReadINI_NewSection(const char *section, bool invalid_tokens, bool close_bracket, bool extra_tokens, unsigned int *curtok) + { + if (new_section != -1) + return executeForwards(new_section, handle, section, invalid_tokens, close_bracket, extra_tokens, *curtok) > 0 ? true : false; + + return true; + } + + bool ReadINI_KeyValue(const char *key, const char *value, bool invalid_tokens, bool equal_token, bool quotes, unsigned int *curtok) + { + if (key_value != -1) + return executeForwards(key_value, handle, key, value, invalid_tokens, equal_token, quotes, *curtok) > 0 ? true : false; + + return true; + } + + bool ReadINI_RawLine(const char *line, unsigned int *curtok) + { + if (raw_line != -1) + return executeForwards(raw_line, handle, line, *curtok) > 0 ? true : false; + + return true; + } +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/amxmodx.inc b/plugins/include/amxmodx.inc index 7720ada8..394eef01 100755 --- a/plugins/include/amxmodx.inc +++ b/plugins/include/amxmodx.inc @@ -27,6 +27,8 @@ #include #include #include +#include +#include /* Function is called just after server activation. * Good place for configuration loading, commands and cvars registration. */ diff --git a/plugins/include/textparse_ini.inc b/plugins/include/textparse_ini.inc new file mode 100644 index 00000000..eb96b532 --- /dev/null +++ b/plugins/include/textparse_ini.inc @@ -0,0 +1,206 @@ +// vim: set ts=4 sw=4 tw=99 noet: +// +// AMX Mod X, based on AMX Mod by Aleksander Naszko ("OLO"). +// Copyright (C) The AMX Mod X Development Team. +// +// This software is licensed under the GNU General Public License, version 3 or higher. +// Additional exceptions apply. For full license details, see LICENSE.txt or visit: +// https://alliedmods.net/amxmodx-license + +// +// INI Parser Functions +// + +#if defined _textparse_ini_included + #endinput +#endif +#define _textparse_ini_included + +/** + * This parser API is entirely event based. + * You must hook events to receive data. + */ + +/** + * 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 + */ + +/** + * Parser invalid code. + */ +enum INIParser +{ + Invalid_INIParser = 0 +}; + +/** + * Creates a new INI parser. + * This is used to set parse hooks. + * + * @return A new handle to an INI Parse structure. + */ +native INIParser:INI_CreateParser(); + +/** + * Disposes of an INI parser. + * + * @param handle Handle to an INI Parse structure. + * + * @return True if disposed, false otherwise. + */ +native INI_DestroyParser(&INIParser:handle); + +/** + * Parses an INI config file. + * + * @param handle A handle to an INI 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 bool:INI_ParseFile(INIParser:handle, const file[], &line = 0, &col = 0); + +/** + * Sets the INI_ParseStart function of a parse handle. + * + * @note Below is the prototype of callback: + * - + * Called when parsing is started. + * + * @param handle A handle to an INI Parse structure. + * + * @noreturn + * + * public OnParseStart(INIParser:handle) + * - + * @param handle Handle to an INI Parse structure. + * @param func A ParseStart callback. + * + * @noreturn + * @error Invalid or corrupt handle. + */ +native INI_SetParseStart(INIParser:handle, const func[]); + +/** + * Sets the INI_ParseEnd function of a parse handle. + * + * @note Below is the prototype of callback: + * - + * Called when parsing is halted. + * + * @param handle A handle to an INI Parse structure. + * @param halted True if abnormally halted, false otherwise. + * + * @noreturn + * + * public OnParseEnd(INIParser:handle, bool:halted) + * - + * @param handle Handle to an INI Parse structure. + * @param func A ParseEnd callback. + * + * @noreturn + * @error Invalid or corrupt handle. + */ +native INI_SetParseEnd(INIParser:handle, const func[]); + +/** + * Sets the two main reader functions. + * + * @note Below is the prototype of callback: + * - + * NewSection: + * Called when the parser finds a new section. + * + * @param handle Handle to an INI Parse structure. + * @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. + * + * public bool:OnNewSection(INIParser:handle, const section[], bool:invalid_tokens, bool:close_bracket, bool:extra_tokens, curtok) + * + * KeyValue: + * Called when the parser finds a new key/value pair. + * + * @param handle Handle to an INI Parse structure. + * @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. + * + * public bool:OnKeyValue(INIParser:handle, const key[], const value[], bool:invalid_tokens, bool:equal_token, bool:quotes, curtok) + * - + * @param handle Handle to an INI Parse structure. + * @param kv A KeyValue callback. + * @param ns An optional NewSection callback. + * + * @noreturn + */ +native INI_SetReaders(INIParser:smc, const kvFunc[], const nsFunc[] = "" ); + +/** + * Sets a raw line reader on an INI parser handle. + * + * @note Below is the prototype of callback: + * - + * Called whenever a raw line is read. + * + * @param handle The INI Parse handle. + * @param line Contents of line. + * @param lineno The line number it occurs on. + * @param curtok Pointer to optionally store failed position in string. + * + * @return True to keep parsing, false otherwise. + * + * public bool:OnRawLine(INIParser:smc, const line[], lineno, curtok) + * + * @param handle Handle to an INI Parse structure. + * @param func A RawLine callback. + * + * @noreturn + */ +native INI_SetRawLine(INIParser:handle, const func[]); diff --git a/plugins/include/textparse_smc.inc b/plugins/include/textparse_smc.inc new file mode 100644 index 00000000..5e54af18 --- /dev/null +++ b/plugins/include/textparse_smc.inc @@ -0,0 +1,254 @@ +// vim: set ts=4 sw=4 tw=99 noet: +// +// AMX Mod X, based on AMX Mod by Aleksander Naszko ("OLO"). +// Copyright (C) The AMX Mod X Development Team. +// +// This software is licensed under the GNU General Public License, version 3 or higher. +// Additional exceptions apply. For full license details, see LICENSE.txt or visit: +// https://alliedmods.net/amxmodx-license + +// +// SMC Parser Functions +// + +#if defined _textparse_smc_included + #endinput +#endif +#define _textparse_smc_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 (also known as VDF). + */ + +/** + * 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 scripting/testsuite/textparse_test.cfg + * + * WHITESPACE should be ignored. + * Comments are text occurring inside the following tokens, and should be stripped + * unless they are inside literal strings: + * ; + * // + * / * * / + */ + +/** + * Parser invalid code. + */ +enum SMCParser +{ + Invalid_SMCParser = 0 +}; + +/** + * 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 */ +}; + +/** + * Creates a new SMC parser. + * This is used to set parse hooks. + * + * @return A new handle to an SMC Parse structure. + */ +native SMCParser:SMC_CreateParser(); + +/** + * Disposes of an SMC parser. + * + * @param handle Handle to an SMC Parse structure. + * + * @return True if disposed, false otherwise. + */ +native SMC_DestroyParser(&SMCParser:handle); + +/** + * Parses a config file. + * + * @param handle 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(SMCParser:handle, const file[], &line = 0, &col = 0); + +/** + * Sets the SMC_ParseStart function of a parse handle. + * + * @note Below is the prototype of callback: + * - + * Called when parsing is started. + * + * @param handle Handle to an SMC Parse structure. + * + * @noreturn + * + * public OnParseStart(SMCParser:handle) + * - + * @param handle Handle to an SMC Parse structure. + * @param func A ParseStart callback. + * + * @noreturn + * @error Invalid or corrupt handle. + */ +native SMC_SetParseStart(SMCParser:handle, const func[]); + +/** + * Sets the SMC_ParseEnd function of a parse handle. + * + * @note Below is the prototype of callback: + * - + * Called when parsing is halted. + * + * @param handle Handle to an SMC Parse structure. + * @param halted True if abnormally halted, false otherwise. + * @param failed True if parsing failed, false otherwise. + * + * @noreturn + * + * public OnParseEnd(SMCParser:handle, bool:halted, bool:failed) + * - + * @param handle Handle to an SMC Parse structure. + * @param func A ParseEnd callback. + * + * @noreturn + * @error Invalid or corrupt handle. + */ +native SMC_SetParseEnd(SMCParser:handle, const func[]); + +/** + * Sets the three main reader functions. + * + * @note Enclosing quotes are always stripped. + * @note Below is the prototype of callbacks: + * - + * NewSection: + * Called when the parser finds a new section or sub-section. + * + * @param handle Handle to an SMC Parse structure. + * @param name String containing section name. + * + * @return An SMCResult action to take. + * + * public SMCResult:OnNewSection(SMCParser:handle, const name[]) + * + * KeyValue: + * Called when the parser finds a new key/value pair. + * + * @param handle Handle to an SMC Parse structure. + * @param key String containing key name. + * @param value String containing value name. + * + * @return An SMCResult action to take. + * + * public SMCResult:OnKeyValue(SMCParser:handle, const key[], const value[]) + * + * EndSection: + * Called when the parser finds the end of the current section. + * + * @param handle Handle to an SMC Parse structure. + * + * @return An SMCResult action to take. + * + * public SMCResult:OnEndSection(SMCParser:handle) + * - + * @param handle Handle to an SMC Parse structure. + * @param kv A KeyValue callback. + * @param ns An optional NewSection callback. + * @param es An optional EndSection callback. + * + * @noreturn + */ +native SMC_SetReaders(SMCParser:smc, const kvFunc[], const nsFunc[] = "", const esFunc[] = ""); + +/** + * Sets a raw line reader on an text parser handle. + * + * @note Below is the prototype of callbacks: + * - + * Called whenever a raw line is read. + * + * @param handle Handle to an SMC Parse structure. + * @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(SMCParser:handle, const line[], lineno) + * - + * @param handle Handle to an SMC Parse structure. + * @param func A RawLine callback. + * + * @noreturn + */ +native SMC_SetRawLine(SMCParser:handle, const func[]); + +/** + * 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); diff --git a/plugins/testsuite/textparse_test.cfg b/plugins/testsuite/textparse_test.cfg new file mode 100644 index 00000000..5bf0cea0 --- /dev/null +++ b/plugins/testsuite/textparse_test.cfg @@ -0,0 +1,135 @@ +/** + * This file is used to set various options that are important to SourceMod's core. + * If this file is missing or an option in this file is missing, then the default values will be used. + */ +"Core" +{ + /** + * This option determines if SourceMod logging is enabled. + * + * "on" - Logging is enabled (default) + * "off" - Logging is disabled + */ + "Logging" "on" + + /** + * This option determines how SourceMod logging should be handled. + * + * "daily" - New log file is created for each day (default) + * "map" - New log file is created for each map change + * "game" - Use game's log files + */ + "LogMode" "daily" + + /** + * Language that multilingual enabled plugins and extensions will use to print messages. + * Only languages listed in languages.cfg are valid. + * + * The default value is "en" + */ + "ServerLang" "en" + + /** + * String to use as the public chat trigger. Set an empty string to disable. + */ + "PublicChatTrigger" "!" + + /** + * String to use as the silent chat trigger. Set an empty string to disable. + */ + "SilentChatTrigger" "/" + + /** + * If a say command is a silent chat trigger, and is used by an admin, + * but it does not evaluate to an actual command, it will be displayed + * publicly. This setting allows you to suppress accidental typings. + * + * The default value is "no". A value of "yes" will supress. + */ + "SilentFailSuppress" "no" + + /** + * Password setinfo key that clients must set. You must change this in order for + * passwords to work, for security reasons. + */ + "PassInfoVar" "_password" + + /** + * Specifies the sound that gets played when an item is selected from a menu. + */ + "MenuItemSound" "buttons/button14.wav" + + /** + * Specifies the sound that gets played when an "Exit" button is selected + * from a menu. + */ + "MenuExitSound" "buttons/combine_button7.wav" + + /** + * Specifies the sound that gets played when an "Exit Back" button is selected + * from a menu. This is the special "Back" button that is intended to roll back + * to a previous menu. + */ + "MenuExitBackSound" "buttons/combine_button7.wav" + + /** + * Enables or disables whether SourceMod reads a client's cl_language cvar to set + * their language for server-side phrase translation. + * + * "on" - Translate using the client's language (default) + * "off" - Translate using default server's language + */ + "AllowClLanguageVar" "On" + + /** + * Enables or Disables SourceMod's automatic gamedata updating. + * + * The default value is "no". A value of "yes" will block the Auto Updater. + */ + "DisableAutoUpdate" "no" + + /** + * If set to yes, a successful gamedata update will attempt to restart SourceMod. + * SourceMod is unloaded and reloaded, and the map is changed to the current map. + * Since gamedata updates occur when the server loads, impact should be minimal. + * But to be safe, this option is disabled by default. + */ + "ForceRestartAfterUpdate" "no" + + /** + * URL to use for retrieving update information. + * SSL is not yet supported. + */ + "AutoUpdateURL" "http://update.sourcemod.net/update/" + + /** + * Whether to show debug spew. + * Currently this will log details about the gamedata updating process. + */ + "DebugSpew" "no" + + /** + * If set to yes, SourceMod will validate steamid auth strings with the Steam backend before giving out admin access. + * This can prevent malicious users from impersonating admins with stolen Steam apptickets. + * If Steam is down, admins will not be authenticated until Steam comes back up. + * This option increases the security of your server, but is still experimental. + */ + "SteamAuthstringValidation" "yes" + + /** + * Enables or disables whether SourceMod blocks known or potentially malicious plugins from loading. + * It is STRONGLY advised that this is left enabled, there have been cases in the past with plugins that + * allow anyone to delete files on the server, gain full rcon control, etc. + * + * "yes" - Block malware or illegal plugins from loading (default) + * "no" - Warn about malware or illegal plugins loading + */ + "BlockBadPlugins" "yes" + + /** + * If a plugin takes too long to execute, hanging or freezing the game server in the process, + * SourceMod will attempt to terminate that plugin after the specified timeout length has + * passed. You can disable this feature by setting the value to "0". + */ + "SlowScriptTimeout" "8" +} \ No newline at end of file diff --git a/plugins/testsuite/textparse_test.ini b/plugins/testsuite/textparse_test.ini new file mode 100644 index 00000000..88078331 --- /dev/null +++ b/plugins/testsuite/textparse_test.ini @@ -0,0 +1,68 @@ +;CSDM Configuration File +; Default settings by BAILOPAN + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;You must be running the Main plugin for this section +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +[settings] + +;Sets whether CSDM is enabled or not. +enabled = 1 + +;Sets whether or not players should be stripped of weapons on round start +; (excludes knife) +strip_weapons = 1 + +;Sets how long weapons should stay on the ground for after being dropped +;in seconds. note that enabling this can create lots of lag for clients +; AND server. 0 is immediate, -1 is infinite. +weapons_stay = 0 + +;Sets the spawn mode. +; "none" - users spawn at normal map spawn points +; "preset" - csdm_spawn_preset.amxx required, uses predefined spawns in config files +; -- others may be supplied by 3rd party plugins +spawnmode = preset + +;Sets whether the bomb is removed +remove_bomb = 1 + +;Sets the spawn waiting time +spawn_wait_time = 0.75 + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;You must be running the protection plugin for this section +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +[protection] ; some comment + +;Is spawn protection enabled? +enabled = 1 + +;Colors of glow shell, leave this in quotes +;The digits are R, G, B, A where A is the alpha transparency +; (as A gets higher, the glow shell is thicker) +colors = "0 255 0 200" + +;Number of seconds someone is respawned for. +time = 2 + +;;;;;;;;;;;;;;;; +;;WEAPON MENUS;; +;;;;;;;;;;;;;;;; + +;Format for weapon menus is: +;shortname "Display Name" menupage +;Change the '1' to a '0' to block the weapon +;Removing or moving things +; from the list will change the order of the menus! + +[secondary] gabe // < just to test param. +usp USP 1 +glock18 Glock 1 +deagle Deagle 1 + +;List weapons here the bots can randomly have +;The short name must match one in the list above +[botsecondary +deagle +usp diff --git a/plugins/testsuite/textparse_test.sma b/plugins/testsuite/textparse_test.sma new file mode 100644 index 00000000..64e6ce82 --- /dev/null +++ b/plugins/testsuite/textparse_test.sma @@ -0,0 +1,253 @@ +/** + * vim: set ts=4 sw=4 tw=99 noet: + * + * AMX Mod X, based on AMX Mod by Aleksander Naszko ("OLO"). + * Copyright (C) The AMX Mod X Development Team. + * + * This software is licensed under the GNU General Public License, version 3 or higher. + * Additional exceptions apply. For full license details, see LICENSE.txt or visit: + * https://alliedmods.net/amxmodx-license + */ + +#include + +new SuccessCount; +new Trie:ExpectedKVData; +new bool:Debug; + +public plugin_init() +{ + register_concmd("textparse", "ConsoleCommand_TextParse"); + + Debug = !!(plugin_flags() & AMX_FLAG_DEBUG); +} + +public ConsoleCommand_TextParse() +{ + InitializeTextParseSMC(); + InitializeTextParseINI(); +} + +/** + * SMC Config format + */ + +InitializeTextParseSMC() +{ + SuccessCount = 0; + + server_print("Testing text parser with SMC 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 SMCParser:parser = SMC_CreateParser(); + + SMC_SetReaders(parser, "ReadCore_KeyValue", "ReadCore_NewSection", "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(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; + } + + server_print("^tTests successful: %d/%d", SuccessCount, expectedStartEndCount + expectedSectionCount + expectedKeyValueCount + expectedLineCount + 1); + + SMC_DestroyParser(parser); + TrieDestroy(ExpectedKVData); + SuccessCount = 0; +} + +public ReadCore_ParseStart(SMCParser:handle) +{ + Debug && server_print("ReadCore_ParseStart"); + ++SuccessCount; +} + +public ReadCore_NewSection(SMCParser:handle, const name[]) +{ + Debug && server_print("^tReadCore_NewSection - %s", name); + ++SuccessCount; +} + +public ReadCore_KeyValue(SMCParser:handle, const key[], const value[]) +{ + Debug && server_print("^t^tReadCore_KeyValue - %-32s %s", key, value); + + new buffer[128]; + if (TrieGetString(ExpectedKVData, key, buffer, charsmax(buffer)) && equal(value, buffer)) + { + ++SuccessCount; + } +} + +public ReadCore_EndSection(SMCParser:handle) +{ + Debug && server_print("^tReadCore_EndSection"); + ++SuccessCount; +} + +public ReadCore_CurrentLine(SMCParser:handle, const line[], lineno) +{ + //Debug && server_print("^t^tReadCore_CurrentLine - %s", line); + ++SuccessCount; +} + +public ReadCore_ParseEnd(SMCParser:handle, bool:halted, bool:failed) +{ + Debug &&server_print("ReadCore_ParseEnd - halted: %s, failed: %s", halted ? "yes" : "no", failed ? "yes" : "no"); + ++SuccessCount; +} + + +/** + * INI Config format + */ + +public InitializeTextParseINI() +{ + SuccessCount = 0; + + server_print("Testing text parser with INI config file format..."); + + new const configFile[] = "addons/amxmodx/scripting/testsuite/textparse_test.ini"; + + ExpectedKVData = TrieCreate(); + TrieSetString(ExpectedKVData, "settings", ""); + TrieSetString(ExpectedKVData, "enabled", "1"); + TrieSetString(ExpectedKVData, "strip_weapons", "1"); + TrieSetString(ExpectedKVData, "weapons_stay", "0"); + TrieSetString(ExpectedKVData, "spawnmode", "preset"); + TrieSetString(ExpectedKVData, "remove_bomb", "1"); + TrieSetString(ExpectedKVData, "spawn_wait_time", "0.75"); + TrieSetString(ExpectedKVData, "protection", ""); + TrieSetString(ExpectedKVData, "colors", "0 255 0 200"); + TrieSetString(ExpectedKVData, "time", "time"); + TrieSetString(ExpectedKVData, "secondary", ""); + TrieSetString(ExpectedKVData, "usp USP 1", ""); + TrieSetString(ExpectedKVData, "glock18 Glock 1", ""); + TrieSetString(ExpectedKVData, "deagle Deagle 1", ""); + TrieSetString(ExpectedKVData, "botsecondary", ""); + TrieSetString(ExpectedKVData, "deagle", ""); + TrieSetString(ExpectedKVData, "usp", ""); + + new const expectedSectionCount = 4; + new const expectedStartEndCount = 2; + new const expectedKeyValueCount = TrieGetSize(ExpectedKVData) - expectedSectionCount; + new const expectedLineCount = TrieGetSize(ExpectedKVData); // This doesn't include blanck/comments line. + + new INIParser:parser = INI_CreateParser(); + + INI_SetReaders(parser, "ReadCSDM_KeyValue", "ReadCSDM_NewSection"); + INI_SetParseStart(parser, "ReadCSDM_ParseStart"); + INI_SetRawLine(parser, "ReadCSDM_CurrentLine"); + INI_SetParseEnd(parser, "ReadCSDM_ParseEnd"); + + new line, col; + new bool:result = INI_ParseFile(parser, configFile, line, col); + + if (!result) + { + server_print("^tFatal parse error"); + } + + if (line == expectedLineCount + 1) + { + ++SuccessCount; + } + + server_print("^tTests successful: %d/%d", SuccessCount, expectedStartEndCount + expectedSectionCount + expectedKeyValueCount + expectedLineCount + 1); + + INI_DestroyParser(parser); + TrieDestroy(ExpectedKVData); +} + +public ReadCSDM_ParseStart(INIParser:handle) +{ + Debug && server_print("ReadCSDM_ParseStart"); + ++SuccessCount; +} + +public ReadCSDM_NewSection(INIParser:handle, const section[], bool:invalid_tokens, bool:close_bracket, bool:extra_tokens, curtok) +{ + Debug && server_print("^tReadCSDM_NewSection - [%s] (invalid_tokens: '%s', close_bracked: '%s', extra_tokens: '%s')", section, invalid_tokens ? "yes" : "no", close_bracket ? "yes" : "no", extra_tokens ? "yes" : "no"); + + if (TrieKeyExists(ExpectedKVData, section)) + { + if ((equal(section, "secondary") && !extra_tokens) || + (equal(section, "botsecondary") && close_bracket)) + { + return true; + } + + ++SuccessCount; + } + + return true; +} + +public bool:ReadCSDM_KeyValue(INIParser:handle, const key[], const value[], bool:invalid_tokens, bool:equal_token, bool:quotes, curtok) +{ + Debug && server_print("^t^tReadCSDM_KeyValue - %-32s %s", key, value); + + new buffer[128]; + if (TrieGetString(ExpectedKVData, key, buffer, charsmax(buffer)) && equal(value, buffer)) + { + if (equal(key, "colors") && !quotes) + { + return true; + } + + ++SuccessCount; + } + + return true; +} + +public bool:ReadCSDM_CurrentLine(INIParser:handle, const line[], curtok) +{ + //Debug && server_print("^t^tReadCSDM_CurrentLine - %s", line); + ++SuccessCount; + return true; +} + +public ReadCSDM_ParseEnd(INIParser:handle, bool:halted, bool:failed) +{ + Debug && server_print("ReadCSDM_ParseStart"); + ++SuccessCount; +} \ No newline at end of file diff --git a/public/ITextParsers.h b/public/ITextParsers.h new file mode 100644 index 00000000..0f88be9e --- /dev/null +++ b/public/ITextParsers.h @@ -0,0 +1,458 @@ +/** + * 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 starting parsing. + */ + virtual void ReadINI_ParseStart() + { + }; + + /** + * @brief Called when ending parsing. + * + * @param halted True if abnormally halted, false otherwise. + */ + virtual void ReadINI_ParseEnd(bool halted) + { + } + + /** + * @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_ + diff --git a/support/PackageScript b/support/PackageScript index f3524d0d..8fa1e99f 100644 --- a/support/PackageScript +++ b/support/PackageScript @@ -235,6 +235,9 @@ scripting_files = [ 'testsuite/trietest.sma', 'testsuite/utf8test.sma', 'testsuite/stacktest.sma', + 'testsuite/textparse_test.sma', + 'testsuite/textparse_test.cfg', + 'testsuite/textparse_test.ini', 'include/amxconst.inc', 'include/amxmisc.inc', 'include/amxmodx.inc', @@ -289,6 +292,8 @@ scripting_files = [ 'include/newmenus.inc', 'include/sorting.inc', 'include/sqlx.inc', + 'include/textparse_ini.inc', + 'include/textparse_smc.inc', 'include/time.inc', 'include/vector.inc', ]