diff --git a/Sources/Plasma/FeatureLib/pfConsoleCore/CMakeLists.txt b/Sources/Plasma/FeatureLib/pfConsoleCore/CMakeLists.txt index 4b318c8a57..ea3bab9e24 100644 --- a/Sources/Plasma/FeatureLib/pfConsoleCore/CMakeLists.txt +++ b/Sources/Plasma/FeatureLib/pfConsoleCore/CMakeLists.txt @@ -3,12 +3,14 @@ set(pfConsoleCore_SOURCES pfConsoleCommandsCore.cpp pfConsoleContext.cpp pfConsoleEngine.cpp + pfConsoleParser.cpp ) set(pfConsoleCore_HEADERS pfConsoleCmd.h pfConsoleContext.h pfConsoleEngine.h + pfConsoleParser.h ) plasma_library(pfConsoleCore SOURCES ${pfConsoleCore_SOURCES} ${pfConsoleCore_HEADERS}) diff --git a/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleCmd.cpp b/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleCmd.cpp index 9b406fac79..1f32908bf4 100644 --- a/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleCmd.cpp +++ b/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleCmd.cpp @@ -47,6 +47,7 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com #include "pfConsoleCmd.h" +#include #include ////////////////////////////////////////////////////////////////////////////// @@ -134,6 +135,15 @@ void pfConsoleCmdGroup::AddSubGroup( pfConsoleCmdGroup *group ) fBaseCmdGroupRef++; } +ST::string pfConsoleCmdGroup::GetFullName() +{ + if (fParentGroup == nullptr || fParentGroup == GetBaseGroup()) { + return fName; + } else { + return ST::format("{}.{}", fParentGroup->GetFullName(), fName); + } +} + //// FindCommand ///////////////////////////////////////////////////////////// // No longer recursive. @@ -446,6 +456,15 @@ void pfConsoleCmd::Unlink() *fPrevPtr = fNext; } +ST::string pfConsoleCmd::GetFullName() +{ + if (fParentGroup == pfConsoleCmdGroup::GetBaseGroup()) { + return fName; + } else { + return ST::format("{}.{}", fParentGroup->GetFullName(), fName); + } +} + //// GetSigEntry ///////////////////////////////////////////////////////////// uint8_t pfConsoleCmd::GetSigEntry(size_t i) diff --git a/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleCmd.h b/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleCmd.h index ad660321d6..70d6ebd6f7 100644 --- a/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleCmd.h +++ b/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleCmd.h @@ -95,6 +95,7 @@ class pfConsoleCmdGroup pfConsoleCmdGroup *GetNext() { return fNext; } ST::string GetName() { return fName; } + ST::string GetFullName(); pfConsoleCmdGroup *GetParent() { return fParentGroup; } static pfConsoleCmdGroup *GetBaseGroup(); @@ -262,6 +263,7 @@ class pfConsoleCmd pfConsoleCmd *GetNext() { return fNext; } ST::string GetName() { return fName; } + ST::string GetFullName(); ST::string GetHelp() { return fHelpString; } ST::string GetSignature(); diff --git a/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleEngine.cpp b/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleEngine.cpp index b89b9a3575..9018ac112f 100644 --- a/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleEngine.cpp +++ b/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleEngine.cpp @@ -48,69 +48,15 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com #include "pfConsoleEngine.h" #include "pfConsoleCmd.h" #include "pfConsoleContext.h" +#include "pfConsoleParser.h" #include #include #include -#include #include "plFile/plEncryptedStream.h" -const int32_t pfConsoleEngine::fMaxNumParams = 16; - -static const char kTokenSeparators[] = " =\r\n\t,"; -static const char kTokenGrpSeps[] = " =\r\n._\t,"; - -//WARNING: Potentially increments the pointer passed to it. -static const char *console_strtok( char *&line, bool haveCommand ) -{ - char *begin = line; - - while (*begin && isspace(static_cast(*begin))) - ++begin; - - for (line = begin; *line; ++line) { - if (!haveCommand) { - for (const char *sep = kTokenGrpSeps; *sep; ++sep) { - if (*line == *sep) { - *line = 0; - while (*++line && (*line == *sep)) - /* skip duplicate delimiters */; - return begin; - } - } - } else { - if (*begin == '"' || *begin == '\'') { - // Handle strings as a single token - char *endptr = strchr(line + 1, *line); - if (endptr == nullptr) { - // Bad string token sentry - return "\xFF"; - } - *endptr = 0; - line = endptr + 1; - return begin + 1; - } - for (const char *sep = kTokenSeparators; *sep; ++sep) { - if (*line == *sep) { - *line = 0; - while (*++line && (*line == *sep)) - /* skip duplicate delimiters */; - return begin; - } - } - } - } - - if (begin == line) - return nullptr; - - line = line + strlen(line); - return begin; -} - - //// Constructor & Destructor //////////////////////////////////////////////// pfConsoleEngine::pfConsoleEngine() @@ -125,48 +71,22 @@ pfConsoleEngine::~pfConsoleEngine() bool pfConsoleEngine::PrintCmdHelp(const ST::string& name, void (*PrintFn)(const ST::string&)) { - pfConsoleCmd *cmd; - pfConsoleCmdGroup *group, *subGrp; - const char *ptr; - - // console_strtok requires a writable C string... - ST::char_buffer nameBuf = name.to_utf8(); - char* namePtr = nameBuf.data(); - - /// Scan for subgroups. This can be an empty loop - group = pfConsoleCmdGroup::GetBaseGroup(); - ptr = console_strtok(namePtr, false); - while (ptr != nullptr) - { - // Take this token and check to see if it's a group - if ((subGrp = group->FindSubGroupNoCase(ptr)) != nullptr) - group = subGrp; - else - break; - - ptr = console_strtok(namePtr, false); - } - - if (ptr == nullptr) - { - if (group == nullptr) - { - fErrorMsg = ST_LITERAL("Invalid command syntax"); - return false; - } + pfConsoleParser parser(name); + auto [group, token] = parser.ParseGroupAndName(); + if (!token) { // Print help for this group if( group == pfConsoleCmdGroup::GetBaseGroup() ) PrintFn(ST_LITERAL("Base commands and groups:")); else PrintFn(ST::format("Group {}:", group->GetName())); PrintFn(ST_LITERAL(" Subgroups:")); - for (subGrp = group->GetFirstSubGroup(); subGrp != nullptr; subGrp = subGrp->GetNext()) + for (pfConsoleCmdGroup* subGrp = group->GetFirstSubGroup(); subGrp != nullptr; subGrp = subGrp->GetNext()) { PrintFn(ST::format(" {}", subGrp->GetName())); } PrintFn(ST_LITERAL(" Commands:")); - for (cmd = group->GetFirstCommand(); cmd != nullptr; cmd = cmd->GetNext()) + for (pfConsoleCmd* cmd = group->GetFirstCommand(); cmd != nullptr; cmd = cmd->GetNext()) { PrintFn(ST::format(" {}: {}", cmd->GetName(), cmd->GetHelp().before_first('\n'))); } @@ -175,7 +95,7 @@ bool pfConsoleEngine::PrintCmdHelp(const ST::string& name, void (*PrintFn)(const } /// OK, so what we found wasn't a group. Which means we need a command... - cmd = group->FindCommandNoCase( ptr ); + pfConsoleCmd* cmd = group->FindCommandNoCase(*token); if (cmd == nullptr) { fErrorMsg = ST_LITERAL("Invalid syntax: command not found"); @@ -194,36 +114,8 @@ bool pfConsoleEngine::PrintCmdHelp(const ST::string& name, void (*PrintFn)(const ST::string pfConsoleEngine::GetCmdSignature(const ST::string& name) { - pfConsoleCmd *cmd; - pfConsoleCmdGroup *group, *subGrp; - const char *ptr; - - // console_strtok requires a writable C string... - ST::char_buffer nameBuf = name.to_utf8(); - char* namePtr = nameBuf.data(); - - /// Scan for subgroups. This can be an empty loop - group = pfConsoleCmdGroup::GetBaseGroup(); - ptr = console_strtok(namePtr, false); - while (ptr != nullptr) - { - // Take this token and check to see if it's a group - if ((subGrp = group->FindSubGroupNoCase(ptr)) != nullptr) - group = subGrp; - else - break; - - ptr = console_strtok(namePtr, false); - } - - if (ptr == nullptr) - { - fErrorMsg = ST_LITERAL("Invalid command syntax"); - return {}; - } - - /// OK, so what we found wasn't a group. Which means we need a command... - cmd = group->FindCommandNoCase( ptr ); + pfConsoleParser parser(name); + pfConsoleCmd* cmd = parser.ParseCommand(); if (cmd == nullptr) { fErrorMsg = ST_LITERAL("Invalid syntax: command not found"); @@ -244,8 +136,6 @@ void DummyPrintFn(const ST::string& line) bool pfConsoleEngine::ExecuteFile(const plFileName &fileName) { - int line; - std::unique_ptr stream = plEncryptedStream::OpenEncryptedFile(fileName); if( !stream ) @@ -260,7 +150,7 @@ bool pfConsoleEngine::ExecuteFile(const plFileName &fileName) } ST::string string; - for (line = 1; stream->ReadLn(string); line++) + for (int line = 1; stream->ReadLn(string); line++) { fLastErrorLine = string; @@ -283,39 +173,8 @@ bool pfConsoleEngine::ExecuteFile(const plFileName &fileName) bool pfConsoleEngine::RunCommand(const ST::string& line, void (*PrintFn)(const ST::string&)) { - pfConsoleCmd *cmd; - pfConsoleCmdGroup *group, *subGrp; - int32_t numParams, i, numQuotedParams = 0; - pfConsoleCmdParam paramArray[ fMaxNumParams + 1 ]; - const char *ptr; - bool valid = true; - - // console_strtok requires a writable C string... - ST::char_buffer lineBuf = line.to_utf8(); - char* linePtr = lineBuf.data(); - - /// Loop #1: Scan for subgroups. This can be an empty loop - group = pfConsoleCmdGroup::GetBaseGroup(); - ptr = console_strtok(linePtr, false); - while (ptr != nullptr) - { - // Take this token and check to see if it's a group - if ((subGrp = group->FindSubGroupNoCase(ptr)) != nullptr) - group = subGrp; - else - break; - - ptr = console_strtok(linePtr, false); - } - - if (ptr == nullptr) - { - fErrorMsg = ST_LITERAL("Invalid command syntax"); - return false; - } - - /// OK, so what we found wasn't a group. Which means we need a command next - cmd = group->FindCommandNoCase( ptr ); + pfConsoleParser parser(line); + pfConsoleCmd* cmd = parser.ParseCommand(); if (cmd == nullptr) { fErrorMsg = ST_LITERAL("Invalid syntax: command not found"); @@ -326,45 +185,18 @@ bool pfConsoleEngine::RunCommand(const ST::string& line, void (*PrintFn)(const S /// tokenizing (with the new separators now, mind you) and turn them into /// params - for( numParams = numQuotedParams = 0; numParams < fMaxNumParams - && (ptr = console_strtok(linePtr, true)) != nullptr - && valid; numParams++ ) - { - if( ptr[ 0 ] == '\xFF' ) - { - fErrorMsg = ST_LITERAL("Invalid syntax: unterminated quoted parameter"); - return false; - } - - // Special case for context variables--if we're specifying one, we want to just grab - // the value of it and return that instead - valid = false; - if( ptr[ 0 ] == '$' ) - { - pfConsoleContext &context = pfConsoleContext::GetRootContext(); - - // Potential variable, see if we can find it - hsSsize_t idx = context.FindVar( ptr + 1 ); - if( idx == -1 ) - { - fErrorMsg = ST_LITERAL("Invalid console variable name"); - } - else - { - paramArray[ numParams ] = context.GetVarValue( idx ); - valid = true; - } - } - - if( !valid ) - valid = IConvertToParam(cmd->GetSigEntry(numParams), ptr, ¶mArray[numParams]); + auto argTokens = parser.ParseArguments(); + if (!argTokens) { + fErrorMsg = ST::format("Invalid syntax: {}", parser.GetErrorMsg()); + return false; } - for( i = numParams; i < fMaxNumParams + 1; i++ ) - paramArray[ i ].SetNone(); - if (!valid || (cmd->GetSigEntry(numParams) != pfConsoleCmd::kAny && - cmd->GetSigEntry(numParams) != pfConsoleCmd::kNone)) - { + auto params = IResolveParams(cmd, std::move(*argTokens)); + + if (!params || ( + cmd->GetSigEntry(params->size()) != pfConsoleCmd::kAny + && cmd->GetSigEntry(params->size()) != pfConsoleCmd::kNone + )) { // Print help string and return fErrorMsg.clear(); // Printed on next line PrintFn(ST_LITERAL("Invalid parameters to command")); @@ -373,7 +205,7 @@ bool pfConsoleEngine::RunCommand(const ST::string& line, void (*PrintFn)(const S } /// Execute it and return - cmd->Execute( numParams, paramArray, PrintFn ); + cmd->Execute(params->size(), params->data(), PrintFn); return true; } @@ -381,53 +213,83 @@ bool pfConsoleEngine::RunCommand(const ST::string& line, void (*PrintFn)(const S // Converts a null-terminated string representing a parameter to a // pfConsoleCmdParam argument. -bool pfConsoleEngine::IConvertToParam(uint8_t type, ST::string string, pfConsoleCmdParam *param) +std::optional pfConsoleEngine::IConvertToParam(uint8_t type, ST::string string) { if( type == pfConsoleCmd::kNone ) - return false; + return {}; + pfConsoleCmdParam param; if( type == pfConsoleCmd::kAny ) { /// Want "any" - param->SetAny(std::move(string)); + param.SetAny(std::move(string)); } else if( type == pfConsoleCmd::kString ) { /// Want just a string - param->SetString(std::move(string)); + param.SetString(std::move(string)); } else if( type == pfConsoleCmd::kFloat ) { ST::conversion_result res; float value = string.to_float(res); if (!res.ok() || !res.full_match()) { - return false; + return {}; } - param->SetFloat(value); + param.SetFloat(value); } else if( type == pfConsoleCmd::kInt ) { ST::conversion_result res; int value = string.to_int(res, 10); if (!res.ok() || !res.full_match()) { - return false; + return {}; } - param->SetInt(value); + param.SetInt(value); } else if( type == pfConsoleCmd::kBool ) { if (string.compare_i("t") == 0) { - param->SetBool(true); + param.SetBool(true); } else if (string.compare_i("f") == 0) { - param->SetBool(false); + param.SetBool(false); } else { - param->SetBool(string.to_bool()); + param.SetBool(string.to_bool()); } } - return true; + return param; +} + +std::optional> pfConsoleEngine::IResolveParams(pfConsoleCmd* cmd, std::vector argTokens) +{ + std::vector params; + for (ST::string& argString : argTokens) { + // Special case for context variables--if we're specifying one, + // we want to just grab the value of it and return that instead + if (argString.starts_with("$")) { + pfConsoleContext& context = pfConsoleContext::GetRootContext(); + + // Potential variable, see if we can find it + hsSsize_t idx = context.FindVar(argString.substr(1)); + if (idx == -1) { + fErrorMsg = ST_LITERAL("Invalid console variable name"); + } else { + params.push_back(context.GetVarValue(idx)); + continue; + } + } + + auto param = IConvertToParam(cmd->GetSigEntry(params.size()), std::move(argString)); + if (!param) { + return {}; + } + params.emplace_back(std::move(*param)); + } + + return params; } //// FindPartialCmd ////////////////////////////////////////////////////////// @@ -448,31 +310,19 @@ ST::string pfConsoleEngine::FindPartialCmd(const ST::string& line, bool findAgai /// New search ST::string_stream newStr; - // console_strtok requires a writable C string... - ST::char_buffer lineBuf = line.to_utf8(); - char* linePtr = lineBuf.data(); - - /// Loop #1: Scan for subgroups. This can be an empty loop - pfConsoleCmdGroup* group = pfConsoleCmdGroup::GetBaseGroup(); - const char* ptr = console_strtok(linePtr, false); - while (ptr != nullptr) - { - // Take this token and check to see if it's a group - pfConsoleCmdGroup* subGrp = group->FindSubGroupNoCase(ptr, 0, nullptr); - if (subGrp == nullptr) { - break; - } - group = subGrp; - newStr << group->GetName() << '.'; - ptr = console_strtok(linePtr, false); + pfConsoleParser parser(line); + auto [group, token] = parser.ParseGroupAndName(); + + // Add group name to replacement line. + if (group != pfConsoleCmdGroup::GetBaseGroup()) { + newStr << group->GetFullName() << '.'; } - if (ptr != nullptr) - { + if (token) { // Still got at least one token left. Try to match to either // a partial group or a partial command - pfConsoleCmdGroup* subGrp = group->FindSubGroupNoCase(ptr, pfConsoleCmdGroup::kFindPartial, lastGroup); + pfConsoleCmdGroup* subGrp = group->FindSubGroupNoCase(*token, pfConsoleCmdGroup::kFindPartial, lastGroup); if (subGrp != nullptr) { lastGroup = group = subGrp; @@ -480,7 +330,7 @@ ST::string pfConsoleEngine::FindPartialCmd(const ST::string& line, bool findAgai } else { - pfConsoleCmd* cmd = group->FindCommandNoCase(ptr, pfConsoleCmdGroup::kFindPartial, lastCmd); + pfConsoleCmd* cmd = group->FindCommandNoCase(*token, pfConsoleCmdGroup::kFindPartial, lastCmd); if (cmd == nullptr) return {}; @@ -492,8 +342,7 @@ ST::string pfConsoleEngine::FindPartialCmd(const ST::string& line, bool findAgai if( preserveParams ) { /// Preserve the rest of the string after the matched command - if (linePtr != nullptr) - newStr << linePtr; + newStr.append(parser.fTokenizer.fPos, parser.fTokenizer.fEnd - parser.fTokenizer.fPos); } return newStr.to_string(); @@ -512,23 +361,8 @@ ST::string pfConsoleEngine::FindNestedPartialCmd(const ST::string& line, uint32_ if (cmd == nullptr) return {}; - /// Recurse back up and get the group hierarchy - std::vector reverseParts {cmd->GetName()}; - pfConsoleCmdGroup* group = cmd->GetParent(); - while (group != nullptr && group != pfConsoleCmdGroup::GetBaseGroup()) { - reverseParts.emplace_back(group->GetName()); - group = group->GetParent(); - } - ST::string_stream name; - for (auto it = reverseParts.crbegin(); it != reverseParts.crend(); ++it) { - name << *it; - if (it + 1 == reverseParts.crend()) { - name << ' '; - } else { - name << '.'; - } - } + name << cmd->GetFullName() << ' '; if( preserveParams ) { diff --git a/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleEngine.h b/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleEngine.h index 0b3c25421b..969f2f2af3 100644 --- a/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleEngine.h +++ b/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleEngine.h @@ -57,21 +57,22 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com #include "HeadSpin.h" +#include #include +#include class plFileName; //// pfConsoleEngine Class Definition //////////////////////////////////////// +class pfConsoleCmd; class pfConsoleCmdParam; class pfConsoleCmdGroup; class pfConsoleEngine { private: - - static const int32_t fMaxNumParams; - - bool IConvertToParam(uint8_t type, ST::string string, pfConsoleCmdParam *param); + std::optional IConvertToParam(uint8_t type, ST::string string); + std::optional> IResolveParams(pfConsoleCmd* cmd, std::vector argTokens); ST::string fErrorMsg; ST::string fLastErrorLine; diff --git a/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleParser.cpp b/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleParser.cpp new file mode 100644 index 0000000000..8c1a0957ea --- /dev/null +++ b/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleParser.cpp @@ -0,0 +1,159 @@ +/*==LICENSE==* + +CyanWorlds.com Engine - MMOG client, server and tools +Copyright (C) 2011 Cyan Worlds, Inc. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +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 . + +Additional permissions under GNU GPL version 3 section 7 + +If you modify this Program, or any covered work, by linking or +combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK, +NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent +JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK +(or a modified version of those libraries), +containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA, +PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG +JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the +licensors of this Program grant you additional +permission to convey the resulting work. Corresponding Source for a +non-source form of such a combination shall include the source code for +the parts of OpenSSL and IJG JPEG Library used as well as that of the covered +work. + +You can contact Cyan Worlds, Inc. by email legal@cyan.com + or by snail mail at: + Cyan Worlds, Inc. + 14617 N Newport Hwy + Mead, WA 99021 + +*==LICENSE==*/ + +#include "pfConsoleParser.h" + +#include + +#include "pfConsoleCmd.h" + +static const char kTokenSeparators[] = " =\r\n\t,"; +static const char kTokenGrpSeps[] = " =\r\n._\t,"; + +std::optional pfConsoleTokenizer::NextNamePart() +{ + const char* begin = fPos; + + while (begin != fEnd && isspace(static_cast(*begin))) + ++begin; + + for (fPos = begin; fPos != fEnd; ++fPos) { + for (const char *sep = kTokenGrpSeps; *sep; ++sep) { + if (*fPos == *sep) { + const char* end = fPos; + while (fPos != fEnd && (*fPos == *sep)) { + // skip duplicate delimiters + ++fPos; + } + return ST::string::from_utf8(begin, end - begin); + } + } + } + + if (begin == fPos) { + fErrorMsg.clear(); + return {}; + } + + return begin; +} + +std::optional pfConsoleTokenizer::NextArgument() +{ + const char* begin = fPos; + + while (begin != fEnd && isspace(static_cast(*begin))) + ++begin; + + for (fPos = begin; fPos != fEnd; ++fPos) { + if (*begin == '"' || *begin == '\'') { + // Handle strings as a single token + ++begin; + const char* end = begin; + while (*end != *fPos) { + ++end; + if (end == fEnd) { + fErrorMsg = ST_LITERAL("unterminated quoted parameter"); + return {}; + } + } + fPos = end + 1; + return ST::string::from_utf8(begin, end - begin); + } + for (const char *sep = kTokenSeparators; *sep; ++sep) { + if (*fPos == *sep) { + const char* end = fPos; + while (fPos != fEnd && (*fPos == *sep)) { + // skip duplicate delimiters + ++fPos; + } + return ST::string::from_utf8(begin, end - begin); + } + } + } + + if (begin == fPos) { + fErrorMsg.clear(); + return {}; + } + + return begin; +} + +std::tuple> pfConsoleParser::ParseGroupAndName() +{ + pfConsoleCmdGroup* group = pfConsoleCmdGroup::GetBaseGroup(); + auto token = fTokenizer.NextNamePart(); + while (token) { + // Take this token and check to see if it's a group + pfConsoleCmdGroup* subGrp = group->FindSubGroupNoCase(*token); + if (subGrp == nullptr) { + return {group, token}; + } + group = subGrp; + token = fTokenizer.NextNamePart(); + } + + return {group, token}; +} + +pfConsoleCmd* pfConsoleParser::ParseCommand() +{ + auto [group, token] = ParseGroupAndName(); + if (!token) { + return nullptr; + } + return group->FindCommandNoCase(*token); +} + +std::optional> pfConsoleParser::ParseArguments() +{ + std::vector args; + while (auto token = fTokenizer.NextArgument()) { + args.emplace_back(std::move(*token)); + } + if (!fTokenizer.fErrorMsg.empty()) { + // Parse error in argument + return {}; + } + return args; +} diff --git a/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleParser.h b/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleParser.h new file mode 100644 index 0000000000..8a8680ee20 --- /dev/null +++ b/Sources/Plasma/FeatureLib/pfConsoleCore/pfConsoleParser.h @@ -0,0 +1,108 @@ +/*==LICENSE==* + +CyanWorlds.com Engine - MMOG client, server and tools +Copyright (C) 2011 Cyan Worlds, Inc. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +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 . + +Additional permissions under GNU GPL version 3 section 7 + +If you modify this Program, or any covered work, by linking or +combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK, +NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent +JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK +(or a modified version of those libraries), +containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA, +PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG +JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the +licensors of this Program grant you additional +permission to convey the resulting work. Corresponding Source for a +non-source form of such a combination shall include the source code for +the parts of OpenSSL and IJG JPEG Library used as well as that of the covered +work. + +You can contact Cyan Worlds, Inc. by email legal@cyan.com + or by snail mail at: + Cyan Worlds, Inc. + 14617 N Newport Hwy + Mead, WA 99021 + +*==LICENSE==*/ + +#ifndef _pfConsolePareser_h +#define _pfConsolePareser_h + +#include "HeadSpin.h" + +#include +#include +#include +#include + +class pfConsoleCmd; +class pfConsoleCmdGroup; + +class pfConsoleTokenizer +{ +public: + ST::string::const_iterator fPos; + ST::string::const_iterator fEnd; + ST::string fErrorMsg; + + pfConsoleTokenizer(ST::string::const_iterator begin, ST::string::const_iterator end) : + fPos(begin), fEnd(end), fErrorMsg() + {} + pfConsoleTokenizer(const ST::string& line) : pfConsoleTokenizer(line.begin(), line.end()) {} + + // Parse the next command name or argument token from the input line. + // On success, returns the parsed token. + // (Note that a successfully parsed token may be an empty string, e. g. from an empty pair of quotes!) + // If the next token couldn't be parsed (e. g. quote not closed), + // returns an empty std::optional and sets fErrorMsg to a non-empty string. + // If there are no further tokens in the line, + // returns an empty std::optional and sets fErrorMsg to an empty string. + std::optional NextNamePart(); + std::optional NextArgument(); +}; + +class pfConsoleParser +{ +public: + pfConsoleTokenizer fTokenizer; + + pfConsoleParser(ST::string::const_iterator begin, ST::string::const_iterator end) : fTokenizer(begin, end) {} + pfConsoleParser(const ST::string& line) : pfConsoleParser(line.begin(), line.end()) {} + + ST::string GetErrorMsg() const { return fTokenizer.fErrorMsg; } + + // Parse the command name part of the line as far as possible. + // This consumes name part tokens and uses them to look up a command group + // until a token is encountered that isn't a known group name. + // Returns the found group and the first non-group token + // (which may be an empty std::optional if the end of the line was reached). + std::tuple> ParseGroupAndName(); + + // Parse the command name part of the line. + // Returns the command corresponding to that name, + // or nullptr if no matching command was found. + pfConsoleCmd* ParseCommand(); + + // Parse the remainder of the line as command arguments. + // On success, returns the parsed arguments with any surrounding quotes removed. + // If any of the arguments couldn't be parsed, + // returns an empty std::optional (call GetErrorMsg for an error message). + std::optional> ParseArguments(); +}; + +#endif // _pfConsolePareser_h diff --git a/Sources/Tests/FeatureTests/CMakeLists.txt b/Sources/Tests/FeatureTests/CMakeLists.txt index 05beac89e9..c42dd271e7 100644 --- a/Sources/Tests/FeatureTests/CMakeLists.txt +++ b/Sources/Tests/FeatureTests/CMakeLists.txt @@ -6,4 +6,5 @@ include_directories("${PLASMA_SOURCE_ROOT}/PubUtilLib/inc") include_directories("${PLASMA_SOURCE_ROOT}/FeatureLib") include_directories("${PLASMA_SOURCE_ROOT}/FeatureLib/inc") +add_subdirectory(pfConsoleCoreTest) add_subdirectory(pfPythonTest) diff --git a/Sources/Tests/FeatureTests/pfConsoleCoreTest/CMakeLists.txt b/Sources/Tests/FeatureTests/pfConsoleCoreTest/CMakeLists.txt new file mode 100644 index 0000000000..c71cbeb391 --- /dev/null +++ b/Sources/Tests/FeatureTests/pfConsoleCoreTest/CMakeLists.txt @@ -0,0 +1,13 @@ +set(pfConsoleCore_SOURCES + test_pfConsoleParser.cpp + test_pfConsoleTokenizer.cpp +) + +plasma_test(test_pfConsoleCore SOURCES ${pfConsoleCore_SOURCES}) +target_link_libraries( + test_pfConsoleCore + PRIVATE + CoreLib + pfConsoleCore + gtest_main +) diff --git a/Sources/Tests/FeatureTests/pfConsoleCoreTest/test_pfConsoleParser.cpp b/Sources/Tests/FeatureTests/pfConsoleCoreTest/test_pfConsoleParser.cpp new file mode 100644 index 0000000000..e1826eaa75 --- /dev/null +++ b/Sources/Tests/FeatureTests/pfConsoleCoreTest/test_pfConsoleParser.cpp @@ -0,0 +1,310 @@ +/*==LICENSE==* + +CyanWorlds.com Engine - MMOG client, server and tools +Copyright (C) 2011 Cyan Worlds, Inc. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +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 . + +Additional permissions under GNU GPL version 3 section 7 + +If you modify this Program, or any covered work, by linking or +combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK, +NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent +JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK +(or a modified version of those libraries), +containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA, +PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG +JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the +licensors of this Program grant you additional +permission to convey the resulting work. Corresponding Source for a +non-source form of such a combination shall include the source code for +the parts of OpenSSL and IJG JPEG Library used as well as that of the covered +work. + +You can contact Cyan Worlds, Inc. by email legal@cyan.com + or by snail mail at: + Cyan Worlds, Inc. + 14617 N Newport Hwy + Mead, WA 99021 + +*==LICENSE==*/ + +#include +#include + +#include "pfConsoleCore/pfConsoleCmd.h" +#include "pfConsoleCore/pfConsoleParser.h" + +using namespace ST::literals; + +// Define a few test groups and commands so that we don't have to pull in +// all the real console commands just to test the parsing. + +PF_CONSOLE_BASE_CMD(TestBaseCmd, "", "") +{} + +PF_CONSOLE_GROUP(TestGroup) + +PF_CONSOLE_CMD(TestGroup, SubCmd, "", "") +{} + +PF_CONSOLE_SUBGROUP(TestGroup, SubGroup) + +PF_CONSOLE_CMD(TestGroup_SubGroup, SubSubCmd, "", "") +{} + +// Top-level command outside of any group + +TEST(pfConsoleParser, ParseBaseCommand) +{ + ST::string string = "TestBaseCmd"_st; + pfConsoleParser parser(string); + + auto [group, token] = parser.ParseGroupAndName(); + EXPECT_EQ(group, pfConsoleCmdGroup::GetBaseGroup()); + EXPECT_EQ(token, "TestBaseCmd"_st); + EXPECT_EQ(parser.fTokenizer.fPos, string.end()); + + pfConsoleParser parser2(string); + auto cmd = parser2.ParseCommand(); + EXPECT_EQ(cmd, &conCmd_TestBaseCmd); + auto args = parser2.ParseArguments(); + EXPECT_TRUE(args); + EXPECT_TRUE(args->empty()); +} + +TEST(pfConsoleParser, ParseBaseCommandArgs) +{ + ST::string string = "TestBaseCmd arg1 arg2"_st; + pfConsoleParser parser(string); + + auto [group, token] = parser.ParseGroupAndName(); + EXPECT_EQ(group, pfConsoleCmdGroup::GetBaseGroup()); + EXPECT_EQ(token, "TestBaseCmd"_st); + EXPECT_EQ(parser.fTokenizer.fPos, string.begin() + sizeof("TestBaseCmd ") - 1); + + pfConsoleParser parser2(string); + auto cmd = parser2.ParseCommand(); + EXPECT_EQ(cmd, &conCmd_TestBaseCmd); + auto args = parser2.ParseArguments(); + EXPECT_TRUE(args); + EXPECT_EQ(args->size(), 2); + EXPECT_EQ((*args)[0], "arg1"_st); + EXPECT_EQ((*args)[1], "arg2"_st); +} + +// Top-level group + +TEST(pfConsoleParser, ParseBaseGroup) +{ + ST::string string = "TestGroup"_st; + pfConsoleParser parser(string); + + auto [group, token] = parser.ParseGroupAndName(); + EXPECT_EQ(group, &conGroup_TestGroup); + EXPECT_FALSE(token); + EXPECT_EQ(parser.fTokenizer.fPos, string.end()); + + pfConsoleParser parser2(string); + auto cmd = parser2.ParseCommand(); + EXPECT_EQ(cmd, nullptr); +} + +// Command inside top-level group + +TEST(pfConsoleParser, ParseSubCommand) +{ + ST::string string = "TestGroup.SubCmd"_st; + pfConsoleParser parser(string); + + auto [group, token] = parser.ParseGroupAndName(); + EXPECT_EQ(group, &conGroup_TestGroup); + EXPECT_EQ(token, "SubCmd"_st); + EXPECT_EQ(parser.fTokenizer.fPos, string.end()); + + pfConsoleParser parser2(string); + auto cmd = parser2.ParseCommand(); + EXPECT_EQ(cmd, &conCmd_TestGroup_SubCmd); + auto args = parser2.ParseArguments(); + EXPECT_TRUE(args); + EXPECT_TRUE(args->empty()); +} + +TEST(pfConsoleParser, ParseSubCommandArgs) +{ + ST::string string = "TestGroup.SubCmd arg1 arg2"_st; + pfConsoleParser parser(string); + + auto [group, token] = parser.ParseGroupAndName(); + EXPECT_EQ(group, &conGroup_TestGroup); + EXPECT_EQ(token, "SubCmd"_st); + EXPECT_EQ(parser.fTokenizer.fPos, string.begin() + sizeof("TestGroup.SubCmd ") - 1); + + pfConsoleParser parser2(string); + auto cmd = parser2.ParseCommand(); + EXPECT_EQ(cmd, &conCmd_TestGroup_SubCmd); + auto args = parser2.ParseArguments(); + EXPECT_TRUE(args); + EXPECT_EQ(args->size(), 2); + EXPECT_EQ((*args)[0], "arg1"_st); + EXPECT_EQ((*args)[1], "arg2"_st); +} + +TEST(pfConsoleParser, ParseSubCommandSpace) +{ + ST::string string = "TestGroup SubCmd"_st; + pfConsoleParser parser(string); + + auto [group, token] = parser.ParseGroupAndName(); + EXPECT_EQ(group, &conGroup_TestGroup); + EXPECT_EQ(token, "SubCmd"_st); + EXPECT_EQ(parser.fTokenizer.fPos, string.end()); + + pfConsoleParser parser2(string); + auto cmd = parser2.ParseCommand(); + EXPECT_EQ(cmd, &conCmd_TestGroup_SubCmd); + auto args = parser2.ParseArguments(); + EXPECT_TRUE(args); + EXPECT_TRUE(args->empty()); +} + +TEST(pfConsoleParser, ParseSubCommandSpaceArgs) +{ + ST::string string = "TestGroup SubCmd arg1 arg2"_st; + pfConsoleParser parser(string); + + auto [group, token] = parser.ParseGroupAndName(); + EXPECT_EQ(group, &conGroup_TestGroup); + EXPECT_EQ(token, "SubCmd"_st); + EXPECT_EQ(parser.fTokenizer.fPos, string.begin() + sizeof("TestGroup SubCmd ") - 1); + + pfConsoleParser parser2(string); + auto cmd = parser2.ParseCommand(); + EXPECT_EQ(cmd, &conCmd_TestGroup_SubCmd); + auto args = parser2.ParseArguments(); + EXPECT_TRUE(args); + EXPECT_EQ(args->size(), 2); + EXPECT_EQ((*args)[0], "arg1"_st); + EXPECT_EQ((*args)[1], "arg2"_st); +} + +// Subgroup inside other group + +TEST(pfConsoleParser, ParseSubGroup) +{ + ST::string string = "TestGroup.SubGroup"_st; + pfConsoleParser parser(string); + + auto [group, token] = parser.ParseGroupAndName(); + EXPECT_EQ(group, &conGroup_TestGroup_SubGroup); + EXPECT_FALSE(token); + EXPECT_EQ(parser.fTokenizer.fPos, string.end()); + + pfConsoleParser parser2(string); + auto cmd = parser2.ParseCommand(); + EXPECT_EQ(cmd, nullptr); +} + +TEST(pfConsoleParser, ParseSubGroupSpace) +{ + ST::string string = "TestGroup SubGroup"_st; + pfConsoleParser parser(string); + + auto [group, token] = parser.ParseGroupAndName(); + EXPECT_EQ(group, &conGroup_TestGroup_SubGroup); + EXPECT_FALSE(token); + EXPECT_EQ(parser.fTokenizer.fPos, string.end()); + + pfConsoleParser parser2(string); + auto cmd = parser2.ParseCommand(); + EXPECT_EQ(cmd, nullptr); +} + +// Command inside subgroup + +TEST(pfConsoleParser, ParseSubSubCommand) +{ + ST::string string = "TestGroup.SubGroup.SubSubCmd"_st; + pfConsoleParser parser(string); + + auto [group, token] = parser.ParseGroupAndName(); + EXPECT_EQ(group, &conGroup_TestGroup_SubGroup); + EXPECT_EQ(token, "SubSubCmd"_st); + EXPECT_EQ(parser.fTokenizer.fPos, string.end()); + + pfConsoleParser parser2(string); + auto cmd = parser2.ParseCommand(); + EXPECT_EQ(cmd, &conCmd_TestGroup_SubGroup_SubSubCmd); + auto args = parser2.ParseArguments(); + EXPECT_TRUE(args); + EXPECT_TRUE(args->empty()); +} + +TEST(pfConsoleParser, ParseSubSubCommandArgs) +{ + ST::string string = "TestGroup.SubGroup.SubSubCmd arg1 arg2"_st; + pfConsoleParser parser(string); + + auto [group, token] = parser.ParseGroupAndName(); + EXPECT_EQ(group, &conGroup_TestGroup_SubGroup); + EXPECT_EQ(token, "SubSubCmd"_st); + EXPECT_EQ(parser.fTokenizer.fPos, string.begin() + sizeof("TestGroup.SubGroup.SubSubCmd ") - 1); + + pfConsoleParser parser2(string); + auto cmd = parser2.ParseCommand(); + EXPECT_EQ(cmd, &conCmd_TestGroup_SubGroup_SubSubCmd); + auto args = parser2.ParseArguments(); + EXPECT_TRUE(args); + EXPECT_EQ(args->size(), 2); + EXPECT_EQ((*args)[0], "arg1"_st); + EXPECT_EQ((*args)[1], "arg2"_st); +} + +TEST(pfConsoleParser, ParseSubSubCommandSpaces) +{ + ST::string string = "TestGroup SubGroup SubSubCmd"_st; + pfConsoleParser parser(string); + + auto [group, token] = parser.ParseGroupAndName(); + EXPECT_EQ(group, &conGroup_TestGroup_SubGroup); + EXPECT_EQ(token, "SubSubCmd"_st); + EXPECT_EQ(parser.fTokenizer.fPos, string.end()); + + pfConsoleParser parser2(string); + auto cmd = parser2.ParseCommand(); + EXPECT_EQ(cmd, &conCmd_TestGroup_SubGroup_SubSubCmd); + auto args = parser2.ParseArguments(); + EXPECT_TRUE(args); + EXPECT_TRUE(args->empty()); +} + +TEST(pfConsoleParser, ParseSubSubCommandSpacesArgs) +{ + ST::string string = "TestGroup SubGroup SubSubCmd arg1 arg2"_st; + pfConsoleParser parser(string); + + auto [group, token] = parser.ParseGroupAndName(); + EXPECT_EQ(group, &conGroup_TestGroup_SubGroup); + EXPECT_EQ(token, "SubSubCmd"_st); + EXPECT_EQ(parser.fTokenizer.fPos, string.begin() + sizeof("TestGroup SubGroup SubSubCmd ") - 1); + + pfConsoleParser parser2(string); + auto cmd = parser2.ParseCommand(); + EXPECT_EQ(cmd, &conCmd_TestGroup_SubGroup_SubSubCmd); + auto args = parser2.ParseArguments(); + EXPECT_TRUE(args); + EXPECT_EQ(args->size(), 2); + EXPECT_EQ((*args)[0], "arg1"_st); + EXPECT_EQ((*args)[1], "arg2"_st); +} diff --git a/Sources/Tests/FeatureTests/pfConsoleCoreTest/test_pfConsoleTokenizer.cpp b/Sources/Tests/FeatureTests/pfConsoleCoreTest/test_pfConsoleTokenizer.cpp new file mode 100644 index 0000000000..3752517370 --- /dev/null +++ b/Sources/Tests/FeatureTests/pfConsoleCoreTest/test_pfConsoleTokenizer.cpp @@ -0,0 +1,456 @@ +/*==LICENSE==* + +CyanWorlds.com Engine - MMOG client, server and tools +Copyright (C) 2011 Cyan Worlds, Inc. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +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 . + +Additional permissions under GNU GPL version 3 section 7 + +If you modify this Program, or any covered work, by linking or +combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK, +NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent +JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK +(or a modified version of those libraries), +containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA, +PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG +JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the +licensors of this Program grant you additional +permission to convey the resulting work. Corresponding Source for a +non-source form of such a combination shall include the source code for +the parts of OpenSSL and IJG JPEG Library used as well as that of the covered +work. + +You can contact Cyan Worlds, Inc. by email legal@cyan.com + or by snail mail at: + Cyan Worlds, Inc. + 14617 N Newport Hwy + Mead, WA 99021 + +*==LICENSE==*/ + +#include +#include + +#include "pfConsoleCore/pfConsoleParser.h" + +using namespace ST::literals; + +// Tokenize command name, 1 token + +TEST(pfConsoleTokenizer, TokenizeCommandNameSingle) +{ + ST::string string = "SampleCmd1"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextNamePart(); + EXPECT_EQ(token1, "SampleCmd1"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token2 = tokenizer.NextNamePart(); + EXPECT_FALSE(token2); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +TEST(pfConsoleTokenizer, TokenizeCommandNameSingleWhitespace) +{ + ST::string string = " SampleCmd1 "_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextNamePart(); + EXPECT_EQ(token1, "SampleCmd1"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token2 = tokenizer.NextNamePart(); + EXPECT_FALSE(token2); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +// Tokenize command name, 2 tokens + +TEST(pfConsoleTokenizer, TokenizeCommandNameDot) +{ + ST::string string = "App.Quit"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextNamePart(); + EXPECT_EQ(token1, "App"_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("App.") - 1); + + auto token2 = tokenizer.NextNamePart(); + EXPECT_EQ(token2, "Quit"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token3 = tokenizer.NextNamePart(); + EXPECT_FALSE(token3); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +TEST(pfConsoleTokenizer, TokenizeCommandNameUnderscore) +{ + ST::string string = "App_Quit"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextNamePart(); + EXPECT_EQ(token1, "App"_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("App_") - 1); + + auto token2 = tokenizer.NextNamePart(); + EXPECT_EQ(token2, "Quit"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token3 = tokenizer.NextNamePart(); + EXPECT_FALSE(token3); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +TEST(pfConsoleTokenizer, TokenizeCommandNameSpace) +{ + ST::string string = "App Quit"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextNamePart(); + EXPECT_EQ(token1, "App"_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("App ") - 1); + + auto token2 = tokenizer.NextNamePart(); + EXPECT_EQ(token2, "Quit"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token3 = tokenizer.NextNamePart(); + EXPECT_FALSE(token3); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +// Tokenize command name, 3 tokens + +TEST(pfConsoleTokenizer, TokenizeCommandNameDots) +{ + ST::string string = "Graphics.Renderer.SetYon"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextNamePart(); + EXPECT_EQ(token1, "Graphics"_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("Graphics.") - 1); + + auto token2 = tokenizer.NextNamePart(); + EXPECT_EQ(token2, "Renderer"_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("Graphics.Renderer.") - 1); + + auto token3 = tokenizer.NextNamePart(); + EXPECT_EQ(token3, "SetYon"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token4 = tokenizer.NextNamePart(); + EXPECT_FALSE(token4); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +TEST(pfConsoleTokenizer, TokenizeCommandNameUnderscores) +{ + ST::string string = "Graphics_Renderer_SetYon"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextNamePart(); + EXPECT_EQ(token1, "Graphics"_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("Graphics_") - 1); + + auto token2 = tokenizer.NextNamePart(); + EXPECT_EQ(token2, "Renderer"_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("Graphics_Renderer_") - 1); + + auto token3 = tokenizer.NextNamePart(); + EXPECT_EQ(token3, "SetYon"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token4 = tokenizer.NextNamePart(); + EXPECT_FALSE(token4); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +TEST(pfConsoleTokenizer, TokenizeCommandNameSpaces) +{ + ST::string string = "Graphics Renderer SetYon"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextNamePart(); + EXPECT_EQ(token1, "Graphics"_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("Graphics ") - 1); + + auto token2 = tokenizer.NextNamePart(); + EXPECT_EQ(token2, "Renderer"_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("Graphics Renderer ") - 1); + + auto token3 = tokenizer.NextNamePart(); + EXPECT_EQ(token3, "SetYon"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token4 = tokenizer.NextNamePart(); + EXPECT_FALSE(token4); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +// Tokenize arguments, 1 token + +TEST(pfConsoleTokenizer, TokenizeArgumentsSingle) +{ + ST::string string = "arg"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextArgument(); + EXPECT_EQ(token1, "arg"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token2 = tokenizer.NextArgument(); + EXPECT_FALSE(token2); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +TEST(pfConsoleTokenizer, TokenizeArgumentsSingleWhitespace) +{ + ST::string string = " arg "_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextArgument(); + EXPECT_EQ(token1, "arg"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token2 = tokenizer.NextArgument(); + EXPECT_FALSE(token2); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +TEST(pfConsoleTokenizer, TokenizeArgumentsSingleUnderscore) +{ + ST::string string = "arg_test"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextArgument(); + EXPECT_EQ(token1, "arg_test"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token2 = tokenizer.NextArgument(); + EXPECT_FALSE(token2); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +TEST(pfConsoleTokenizer, TokenizeArgumentsSingleDoubleQuote) +{ + ST::string string = "\"(Default Device)\""_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextArgument(); + EXPECT_EQ(token1, "(Default Device)"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token2 = tokenizer.NextArgument(); + EXPECT_FALSE(token2); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +TEST(pfConsoleTokenizer, TokenizeArgumentsSingleSingleQuote) +{ + ST::string string = "'(Default Device)'"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextArgument(); + EXPECT_EQ(token1, "(Default Device)"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token2 = tokenizer.NextArgument(); + EXPECT_FALSE(token2); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +TEST(pfConsoleTokenizer, TokenizeArgumentsSingleDoubleQuoteUnclosed) +{ + ST::string string = "\"(Default Device)"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextArgument(); + EXPECT_FALSE(token1); + EXPECT_FALSE(tokenizer.fErrorMsg.empty()); +} + +TEST(pfConsoleTokenizer, TokenizeArgumentsSingleSingleQuoteUnclosed) +{ + ST::string string = "'(Default Device)"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextArgument(); + EXPECT_FALSE(token1); + EXPECT_FALSE(tokenizer.fErrorMsg.empty()); +} + +// Tokenize arguments, 2 tokens + +TEST(pfConsoleTokenizer, TokenizeArgumentsPair) +{ + ST::string string = "arg1 arg2"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextArgument(); + EXPECT_EQ(token1, "arg1"_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("arg1 ") - 1); + + auto token2 = tokenizer.NextArgument(); + EXPECT_EQ(token2, "arg2"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token3 = tokenizer.NextArgument(); + EXPECT_FALSE(token3); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +TEST(pfConsoleTokenizer, TokenizeArgumentsPairWhitespace) +{ + ST::string string = " arg1 arg2 "_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextArgument(); + EXPECT_EQ(token1, "arg1"_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof(" arg1 ") - 1); + + auto token2 = tokenizer.NextArgument(); + EXPECT_EQ(token2, "arg2"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token3 = tokenizer.NextArgument(); + EXPECT_FALSE(token3); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +TEST(pfConsoleTokenizer, TokenizeArgumentsPairComma) +{ + ST::string string = "arg1, arg2"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextArgument(); + EXPECT_EQ(token1, "arg1"_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("arg1,") - 1); + + auto token2 = tokenizer.NextArgument(); + EXPECT_EQ(token2, "arg2"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token3 = tokenizer.NextArgument(); + EXPECT_FALSE(token3); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +TEST(pfConsoleTokenizer, TokenizeArgumentsPairMixedQuotes) +{ + ST::string string = "\"argument '1'\" 'argument \"2\"'"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextArgument(); + EXPECT_EQ(token1, "argument '1'"_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("\"argument '1'\"") - 1); + + auto token2 = tokenizer.NextArgument(); + EXPECT_EQ(token2, "argument \"2\""_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token3 = tokenizer.NextArgument(); + EXPECT_FALSE(token3); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +// Tokenize arguments, 3 tokens + +TEST(pfConsoleTokenizer, TokenizeArgumentsTriple) +{ + ST::string string = "1.2 3.4 5.6"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextArgument(); + EXPECT_EQ(token1, "1.2"_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("1.2 ") - 1); + + auto token2 = tokenizer.NextArgument(); + EXPECT_EQ(token2, "3.4"_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("1.2 3.4 ") - 1); + + auto token3 = tokenizer.NextArgument(); + EXPECT_EQ(token3, "5.6"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token4 = tokenizer.NextArgument(); + EXPECT_FALSE(token4); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +TEST(pfConsoleTokenizer, TokenizeArgumentsTripleCommas) +{ + ST::string string = "1.2, 3.4, 5.6"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextArgument(); + EXPECT_EQ(token1, "1.2"_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("1.2,") - 1); + + auto token2 = tokenizer.NextArgument(); + EXPECT_EQ(token2, "3.4"_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("1.2, 3.4,") - 1); + + auto token3 = tokenizer.NextArgument(); + EXPECT_EQ(token3, "5.6"_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token4 = tokenizer.NextArgument(); + EXPECT_FALSE(token4); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +} + +TEST(pfConsoleTokenizer, TokenizeArgumentsTripleEmptyQuotes) +{ + ST::string string = "'' \"\" ''"_st; + pfConsoleTokenizer tokenizer(string); + + auto token1 = tokenizer.NextArgument(); + EXPECT_EQ(token1, ""_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("''") - 1); + + auto token2 = tokenizer.NextArgument(); + EXPECT_EQ(token2, ""_st); + EXPECT_EQ(tokenizer.fPos, string.begin() + sizeof("'' \"\"") - 1); + + auto token3 = tokenizer.NextArgument(); + EXPECT_EQ(token3, ""_st); + EXPECT_EQ(tokenizer.fPos, string.end()); + + auto token4 = tokenizer.NextArgument(); + EXPECT_FALSE(token4); + EXPECT_TRUE(tokenizer.fErrorMsg.empty()); + EXPECT_EQ(tokenizer.fPos, string.end()); +}