diff --git a/src/libprojectM/MilkdropPresetFactory/CMakeLists.txt b/src/libprojectM/MilkdropPresetFactory/CMakeLists.txt index b1af79c38..5e2e1362e 100644 --- a/src/libprojectM/MilkdropPresetFactory/CMakeLists.txt +++ b/src/libprojectM/MilkdropPresetFactory/CMakeLists.txt @@ -27,8 +27,8 @@ add_library(MilkdropPresetFactory OBJECT Param.cpp Param.hpp ParamUtils.hpp - Parser.cpp - Parser.hpp + OldParser.cpp + OldParser.hpp PerFrameEqn.cpp PerFrameEqn.hpp PerPixelEqn.cpp @@ -37,6 +37,8 @@ add_library(MilkdropPresetFactory OBJECT PerPointEqn.hpp PresetFrameIO.cpp PresetFrameIO.hpp + FileParser.cpp + FileParser.hpp ) target_include_directories(MilkdropPresetFactory diff --git a/src/libprojectM/MilkdropPresetFactory/FileParser.cpp b/src/libprojectM/MilkdropPresetFactory/FileParser.cpp new file mode 100644 index 000000000..334cd544a --- /dev/null +++ b/src/libprojectM/MilkdropPresetFactory/FileParser.cpp @@ -0,0 +1,220 @@ +#include "FileParser.hpp" + +#include +#include +#include +#include + +bool FileParser::Read(const std::string& presetFile) +{ + std::ifstream presetStream(presetFile.c_str(), std::ios_base::in | std::ios_base::binary); + if (!presetStream.good()) + { + return false; + } + + presetStream.seekg(0, presetStream.end); + auto fileSize = presetStream.tellg(); + presetStream.seekg(0, presetStream.beg); + + if (fileSize > maxFileSize) + { + return false; + } + + std::vector presetFileContents(fileSize); + presetStream.read(presetFileContents.data(), fileSize); + + if (presetStream.fail() || presetStream.bad()) + { + return false; + } + + presetStream.close(); + + size_t startPos{ 0 }; //!< Starting position of current line + size_t pos{ 0 }; //!< Current read position + + auto parseLineIfDataAvailable = [this, &pos, &startPos, &presetFileContents]() + { + if (pos > startPos) + { + auto beg = presetFileContents.begin(); + std::string line(beg + startPos, beg + pos); + ParseLine(line); + } + }; + + while (pos < presetFileContents.size()) + { + switch (presetFileContents[pos]) + { + case '\r': + case '\n': + // EOL, skip over CRLF + parseLineIfDataAvailable(); + startPos = pos + 1; + break; + + case '\0': + // Null char is not expected. Could be a random binary file. + return false; + } + + ++pos; + } + + parseLineIfDataAvailable(); + + return !m_presetValues.empty(); +} + +std::string FileParser::GetCode(const std::string& keyPrefix) const +{ + std::stringstream code; //!< The parsed code + std::string key(keyPrefix.length() + 5, '\0'); //!< Allocate a string that can hold up to 5 digits. + + key.replace(0, keyPrefix.length(), keyPrefix); + + for (int index{ 1 }; index <= 99999; ++index) + { + key.replace(keyPrefix.length(), 5, std::to_string(index)); + if (m_presetValues.find(key) == m_presetValues.end()) + { + break; + } + + auto line = m_presetValues.at(key); + StripComment(line); + Trim(line); + + if (!line.empty()) + { + if (line.at(0) == '`') + { + // Append newline in shader code + line.erase(0, 1); + code << line << std::endl; + } + else + { + // Append a space in equation code + code << line << " "; + } + } + } + + auto codeStr = code.str(); + StripMultilineComment(codeStr); + + return codeStr; +} + +int FileParser::GetInt(const std::string& key, int defaultValue) +{ + if (m_presetValues.find(key) != m_presetValues.end()) + { + try + { + return std::stoi(m_presetValues.at(key)); + } + catch (std::logic_error& ex) + { + } + } + + return defaultValue; +} + +float FileParser::GetFloat(const std::string& key, float defaultValue) +{ + if (m_presetValues.find(key) != m_presetValues.end()) + { + try + { + return std::stof(m_presetValues.at(key)); + } + catch (std::logic_error& ex) + { + } + } + + return defaultValue; +} + +int FileParser::GetBool(const std::string& key, bool defaultValue) +{ + return GetInt(key, static_cast(defaultValue)) > 0; +} + +const std::map& FileParser::PresetValues() const +{ + return m_presetValues; +} + +void FileParser::ParseLine(const std::string& line) +{ + // Search for first delimiter, either space or equal + auto varNameDelimiterPos = line.find_first_of(" ="); + + if (varNameDelimiterPos >= line.length() || varNameDelimiterPos == 0) + { + // Empty line, delimiter at end of line or no delimiter found, skip. + return; + } + + std::string varName(line.begin(), line.begin() + varNameDelimiterPos); + std::string value(line.begin() + varNameDelimiterPos + 1, line.end()); + + // Only add first occurrence to mimic Milkdrop behaviour + if (!varName.empty() && !value.empty() && m_presetValues.find(varName) == m_presetValues.end()) + { + m_presetValues.emplace(std::move(varName), std::move(value)); + } +} + +void FileParser::StripComment(std::string& line) +{ + auto commentPos = line.find("//"); + if (commentPos != std::string::npos) + { + line.resize(commentPos); + } + + // While not documented, Milkdrop also considers "\\" to be a comment. + commentPos = line.find("\\\\"); + if (commentPos != std::string::npos) + { + line.resize(commentPos); + } +} + +void FileParser::StripMultilineComment(std::string& code) +{ + size_t commentPos; + while((commentPos = code.find("/*")) != std::string::npos) + { + auto endPos = code.find("*/"); + if (endPos != std::string::npos && endPos > commentPos) + { + code.erase(commentPos, endPos - commentPos + 2); + } + else + { + code.erase(commentPos, code.size() - commentPos); + } + } +} + +void FileParser::Trim(std::string& line) +{ + if (line.empty()) + { + return; + } + + line.erase(line.begin(), std::find_if(line.begin(), line.end(), + std::not1(std::ptr_fun(std::isspace)))); + line.erase(std::find_if(line.rbegin(), line.rend(), + std::not1(std::ptr_fun(std::isspace))).base(), line.end()); +} diff --git a/src/libprojectM/MilkdropPresetFactory/FileParser.hpp b/src/libprojectM/MilkdropPresetFactory/FileParser.hpp new file mode 100644 index 000000000..17e00cee8 --- /dev/null +++ b/src/libprojectM/MilkdropPresetFactory/FileParser.hpp @@ -0,0 +1,127 @@ +#pragma once + +#include +#include + +/** + * @brief Milkdrop preset file parser + * + * Reads in the file as key/value pairs, where the key is either separated from the value by an equal sign or a space. + * Lines not matching this pattern are simply ignored, e.g. the [preset00] INI section. + * + * Values and code blocks can easily be accessed via the helper functions. It is also possible to access the parsed + * map contents directly if required. + */ +class FileParser +{ +public: + using ValueMap = std::map; //!< A map with key/value pairs, each representing one line in the preset file. + + static constexpr std::streamsize maxFileSize = 0x100000; //!< Maximum size of a preset file. Used for sanity checks. + + /** + * @brief Reads the preset file into an internal map to prepare for parsing. + * @return True if the file was parsed successfully, false if an error occurred or no line could be parsed. + */ + [[nodiscard]] bool Read(const std::string& presetFile); + + /** + * @brief Returns a block of code, ready for parsing or use in shader compilation. + * + * Shaders have a "`" prepended on each line. If a line starts with this character, it's stripped and a newline + * character is added at the end of each line. Equations are returned as a single, long line. + * + * The function appends numbers to the prefix, starting with 1, and stops when a key is missing. This is following + * Milkdrop's behaviour, so any gap in numbers will essentially cut off all code after the gap. + * + * Comments starting with // or \\\\ will be stripped until the end of each line in both equations and shader code. + * + * @param keyPrefix The key prefix for the code block to be returned. + * @return The code that was parsed from the given prefix. Empty if no code was found. + */ + [[nodiscard]] std::string GetCode(const std::string& keyPrefix) const; + + /** + * @brief Returns the given key value as an integer. + * + * Returns the default value if no value can be parsed or the key doesn't exist. + * + * Any additional text after the number, e.g. a comment, is ignored. + * + * @param key The key to retrieve the value from. + * @param defaultValue The default value to return if key is not found. + * @return The converted value or the default value. + */ + [[nodiscard]] int GetInt(const std::string& key, int defaultValue); + + /** + * @brief Returns the given key value as a floating-point value. + * + * Returns the default value if no value can be parsed or the key doesn't exist. + * + * Any additional text after the number, e.g. a comment, is ignored. + * + * @param key The key to retrieve the value from. + * @param defaultValue The default value to return if key is not found. + * @return The converted value or the default value. + */ + [[nodiscard]] float GetFloat(const std::string& key, float defaultValue); + + /** + * @brief Returns the given key value as a boolean. + * + * Returns the default value if no value can be parsed or the key doesn't exist. + * + * Any additional text after the number, e.g. a comment, is ignored. + * + * @param key The key to retrieve the value from. + * @param defaultValue The default value to return if key is not found. + * @return True if the value is non-zero, false otherwise. + */ + [[nodiscard]] int GetBool(const std::string& key, bool defaultValue); + + /** + * @brief Returns a reference to the internal value map. + * @return A reference to the internal value map. + */ + const ValueMap& PresetValues() const; + +protected: + /** + * @brief Parses a single line and stores the result in the value map. + * + * The function doesn't really care about invalid lines with random text or comments. The first "word" + * is added as key to the map, but will not be used afterwards. + * + * @param line The line to parse. + */ + void ParseLine(const std::string& line); + + /** + * @brief Strips an end-of-line comment from the given line. + * @param[in,out] line The code line to be stripped of a comment. + */ + static void StripComment(std::string& line); + + /** + * @brief Strips all multi-line comments from the given code. + * + * This is also an undocumented feature of Milkdrop's equation parser. We could do it in the parser, + * but is doesn't hurt to generally remove such comments. + * + * @param[in,out] code The code to be stripped of all multi-line comments. + */ + static void StripMultilineComment(std::string& code); + + + /** + * @brief Trims any leading/trailing whitespace from the given line. + * @param[in,out] line The code line to be trimmed. + */ + static void Trim(std::string& line); + +private: + ValueMap m_presetValues; //!< Map with preset keys and their value. +}; + + diff --git a/src/libprojectM/MilkdropPresetFactory/MilkdropPreset.cpp b/src/libprojectM/MilkdropPresetFactory/MilkdropPreset.cpp index 04b714785..cd3b8fa25 100755 --- a/src/libprojectM/MilkdropPresetFactory/MilkdropPreset.cpp +++ b/src/libprojectM/MilkdropPresetFactory/MilkdropPreset.cpp @@ -32,7 +32,7 @@ #include #include "MilkdropPreset.hpp" -#include "Parser.hpp" +#include "OldParser.hpp" #include "ParamUtils.hpp" #include "InitCondUtils.hpp" #include "fatal.h" @@ -538,12 +538,12 @@ int MilkdropPreset::readIn(std::istream& fs) /* Parse any comments (aka "[preset00]") */ /* We don't do anything with this info so it's okay if it's missing */ - if (Parser::parse_top_comment(fs) == PROJECTM_SUCCESS) + if (OldParser::parse_top_comment(fs) == PROJECTM_SUCCESS) { /* Parse the preset name and a left bracket */ char tmp_name[maxTokenSize]; - if (Parser::parse_preset_name(fs, tmp_name) < 0) + if (OldParser::parse_preset_name(fs, tmp_name) < 0) { std::cerr << "[Preset::readIn] loading of preset name failed" << std::endl; fs.seekg(0); @@ -563,7 +563,7 @@ int MilkdropPreset::readIn(std::istream& fs) // Loop through each line in file, trying to successfully parse the file. // If a line does not parse correctly, keep trucking along to next line. int retval; - while ((retval = Parser::parse_line(fs, this)) != EOF) + while ((retval = OldParser::parse_line(fs, this)) != EOF) { if (retval == PROJECTM_PARSE_ERROR) { diff --git a/src/libprojectM/MilkdropPresetFactory/Parser.cpp b/src/libprojectM/MilkdropPresetFactory/OldParser.cpp similarity index 95% rename from src/libprojectM/MilkdropPresetFactory/Parser.cpp rename to src/libprojectM/MilkdropPresetFactory/OldParser.cpp index 29b1e26f8..98f40b62b 100755 --- a/src/libprojectM/MilkdropPresetFactory/Parser.cpp +++ b/src/libprojectM/MilkdropPresetFactory/OldParser.cpp @@ -30,6 +30,7 @@ #include "InitCond.hpp" #include "MilkdropPresetFactory.hpp" #include "Param.hpp" +#include "OldParser.hpp" #include "ParamUtils.hpp" #include "PerFrameEqn.hpp" #include "PerPixelEqn.hpp" @@ -50,24 +51,24 @@ /* Grabs the next token from the file. The second argument points to the raw string */ -line_mode_t Parser::line_mode; -CustomWave *Parser::current_wave; -CustomShape *Parser::current_shape; -int Parser::string_line_buffer_index; -char Parser::string_line_buffer[stringLineSize]; -unsigned int Parser::line_count; -int Parser::per_frame_eqn_count; -int Parser::per_frame_init_eqn_count; -int Parser::last_custom_wave_id; -int Parser::last_custom_shape_id; -char Parser::last_eqn_type[maxTokenSize+1]; -int Parser::last_token_size; +line_mode_t OldParser::line_mode; +CustomWave *OldParser::current_wave; +CustomShape *OldParser::current_shape; +int OldParser::string_line_buffer_index; +char OldParser::string_line_buffer[stringLineSize]; +unsigned int OldParser::line_count; +int OldParser::per_frame_eqn_count; +int OldParser::per_frame_init_eqn_count; +int OldParser::last_custom_wave_id; +int OldParser::last_custom_shape_id; +char OldParser::last_eqn_type[maxTokenSize + 1]; +int OldParser::last_token_size; -std::string Parser::lastLinePrefix(""); +std::string OldParser::lastLinePrefix(""); -bool Parser::tokenWrapAroundEnabled(false); +bool OldParser::tokenWrapAroundEnabled(false); -token_t Parser::parseToken(std::istream & fs, char * string) +token_t OldParser::parseToken(std::istream & fs, char * string) { int c; @@ -255,7 +256,7 @@ token_t Parser::parseToken(std::istream & fs, char * string) /* Parse input in the form of "exp, exp, exp, ...)" Returns a general expression list */ -Expr **Parser::parse_prefix_args(std::istream & fs, int num_args, MilkdropPreset * preset) +Expr **OldParser::parse_prefix_args(std::istream & fs, int num_args, MilkdropPreset * preset) { int i, j; @@ -294,7 +295,7 @@ Expr **Parser::parse_prefix_args(std::istream & fs, int num_args, MilkdropPrese } /* Parses a comment at the top of the file. Stops when left bracket is found */ -int Parser::parse_top_comment(std::istream & fs) +int OldParser::parse_top_comment(std::istream & fs) { char string[maxTokenSize]; @@ -314,7 +315,7 @@ int Parser::parse_top_comment(std::istream & fs) /* Right Bracket is parsed by this function. puts a new string into name */ -int Parser::parse_preset_name(std::istream & fs, char * name) +int OldParser::parse_preset_name(std::istream & fs, char * name) { token_t token; @@ -330,7 +331,7 @@ int Parser::parse_preset_name(std::istream & fs, char * name) /* Parses per pixel equations */ -int Parser::parse_per_pixel_eqn(std::istream & fs, MilkdropPreset * preset, char * init_string) +int OldParser::parse_per_pixel_eqn(std::istream & fs, MilkdropPreset * preset, char * init_string) { @@ -372,7 +373,7 @@ int Parser::parse_per_pixel_eqn(std::istream & fs, MilkdropPreset * preset, cha } /* Parses an equation line, this function is way too big, should add some helper functions */ -int Parser::parse_line(std::istream & fs, MilkdropPreset * preset) +int OldParser::parse_line(std::istream & fs, MilkdropPreset * preset) { char eqn_string[maxTokenSize]; @@ -725,7 +726,7 @@ int Parser::parse_line(std::istream & fs, MilkdropPreset * preset) /* Parses a general expression, this function is the meat of the parser */ -Expr * Parser::_parse_gen_expr ( std::istream & fs, TreeExpr * tree_expr, MilkdropPreset * preset) +Expr * OldParser::_parse_gen_expr (std::istream & fs, TreeExpr * tree_expr, MilkdropPreset * preset) { int i; char string[maxTokenSize]; @@ -994,7 +995,7 @@ Expr * Parser::_parse_gen_expr ( std::istream & fs, TreeExpr * tree_expr, Milkd } -Expr * Parser::parse_gen_expr ( std::istream & fs, TreeExpr * tree_expr, MilkdropPreset * preset) +Expr * OldParser::parse_gen_expr (std::istream & fs, TreeExpr * tree_expr, MilkdropPreset * preset) { Expr *gen_expr = _parse_gen_expr( fs, tree_expr, preset ); if (nullptr == gen_expr) @@ -1008,7 +1009,7 @@ Expr * Parser::parse_gen_expr ( std::istream & fs, TreeExpr * tree_expr, Milkdr /* Inserts expressions into tree according to operator precedence. If root is null, a new tree is created, with infix_op as only element */ -TreeExpr * Parser::insert_infix_op(InfixOp * infix_op, TreeExpr **root) +TreeExpr * OldParser::insert_infix_op(InfixOp * infix_op, TreeExpr **root) { TreeExpr * new_root; @@ -1058,7 +1059,7 @@ TreeExpr * Parser::insert_infix_op(InfixOp * infix_op, TreeExpr **root) } -TreeExpr * Parser::insert_gen_expr(Expr * gen_expr, TreeExpr ** root) +TreeExpr * OldParser::insert_gen_expr(Expr * gen_expr, TreeExpr ** root) { TreeExpr * new_root; @@ -1090,7 +1091,7 @@ TreeExpr * Parser::insert_gen_expr(Expr * gen_expr, TreeExpr ** root) } /* A recursive helper function to insert general expression elements into the operator tree */ -int Parser::insert_gen_rec(Expr * gen_expr, TreeExpr * root) +int OldParser::insert_gen_rec(Expr * gen_expr, TreeExpr * root) { /* Trivial Case: root is null */ @@ -1137,7 +1138,7 @@ int Parser::insert_gen_rec(Expr * gen_expr, TreeExpr * root) /* A recursive helper function to insert infix arguments by operator precedence */ -int Parser::insert_infix_rec(InfixOp * infix_op, TreeExpr * root) +int OldParser::insert_infix_rec(InfixOp * infix_op, TreeExpr * root) { /* Shouldn't happen, implies a parse error */ @@ -1191,7 +1192,7 @@ int Parser::insert_infix_rec(InfixOp * infix_op, TreeExpr * root) } /* Parses an infix operator */ -Expr * Parser::parse_infix_op(std::istream & fs, token_t token, TreeExpr * tree_expr, MilkdropPreset * preset) +Expr * OldParser::parse_infix_op(std::istream & fs, token_t token, TreeExpr * tree_expr, MilkdropPreset * preset) { Expr * gen_expr; @@ -1253,7 +1254,7 @@ Expr * Parser::parse_infix_op(std::istream & fs, token_t token, TreeExpr * tree } /* Parses an integer, checks for +/- prefix */ -int Parser::parse_int(std::istream & fs, int * int_ptr) +int OldParser::parse_int(std::istream & fs, int * int_ptr) { char string[maxTokenSize]; @@ -1298,7 +1299,7 @@ int Parser::parse_int(std::istream & fs, int * int_ptr) } /* Parses a floating point number */ -int Parser::string_to_float(char * string, float * float_ptr) +int OldParser::string_to_float(char * string, float * float_ptr) { if (*string == 0) @@ -1316,7 +1317,7 @@ int Parser::string_to_float(char * string, float * float_ptr) } /* Parses a floating point number */ -int Parser::parse_float(std::istream & fs, float * float_ptr) +int OldParser::parse_float(std::istream & fs, float * float_ptr) { char string[maxTokenSize]; @@ -1360,7 +1361,7 @@ int Parser::parse_float(std::istream & fs, float * float_ptr) } /* Parses a per frame equation. That is, interprets a stream of data as a per frame equation */ -PerFrameEqn* Parser::parse_per_frame_eqn(std::istream& fs, int index, MilkdropPreset* preset) +PerFrameEqn * OldParser::parse_per_frame_eqn(std::istream & fs, int index, MilkdropPreset * preset) { Param* param{ nullptr }; @@ -1441,7 +1442,7 @@ PerFrameEqn* Parser::parse_per_frame_eqn(std::istream& fs, int index, MilkdropPr } /* Parses an 'implicit' per frame equation. That is, interprets a stream of data as a per frame equation without a prefix */ -PerFrameEqn * Parser::parse_implicit_per_frame_eqn(std::istream & fs, char * param_string, int index, MilkdropPreset * preset) +PerFrameEqn * OldParser::parse_implicit_per_frame_eqn(std::istream & fs, char * param_string, int index, MilkdropPreset * preset) { Param* param{ nullptr }; @@ -1494,7 +1495,7 @@ PerFrameEqn * Parser::parse_implicit_per_frame_eqn(std::istream & fs, char * pa } /* Parses an initial condition */ -InitCond * Parser::parse_init_cond(std::istream & fs, char * name, MilkdropPreset * preset) +InitCond * OldParser::parse_init_cond(std::istream & fs, char * name, MilkdropPreset * preset) { Param * param; @@ -1575,14 +1576,14 @@ InitCond * Parser::parse_init_cond(std::istream & fs, char * name, MilkdropPres } -void Parser::parse_string_block(std::istream & fs, std::string * out_string) { +void OldParser::parse_string_block(std::istream & fs, std::string * out_string) { std::set skipList; skipList.insert('`'); readStringUntil(fs, out_string, false, skipList); } -InitCond * Parser::parse_per_frame_init_eqn(std::istream & fs, MilkdropPreset * preset, std::map * database) +InitCond * OldParser::parse_per_frame_init_eqn(std::istream & fs, MilkdropPreset * preset, std::map * database) { char name[maxTokenSize]; @@ -1678,7 +1679,7 @@ InitCond * Parser::parse_per_frame_init_eqn(std::istream & fs, MilkdropPreset * return init_cond; } -bool Parser::scanForComment(std::istream & fs) { +bool OldParser::scanForComment(std::istream & fs) { int c; c = fs.get(); @@ -1704,7 +1705,7 @@ bool Parser::scanForComment(std::istream & fs) { } } -void Parser::readStringUntil(std::istream & fs, std::string * out_buffer, bool wrapAround, const std::set & skipList) { +void OldParser::readStringUntil(std::istream & fs, std::string * out_buffer, bool wrapAround, const std::set & skipList) { int c; @@ -1795,7 +1796,7 @@ void Parser::readStringUntil(std::istream & fs, std::string * out_buffer, bool w } -int Parser::parse_wavecode(char * token, std::istream & fs, MilkdropPreset * preset) +int OldParser::parse_wavecode(char * token, std::istream & fs, MilkdropPreset * preset) { char * var_string; @@ -1893,7 +1894,7 @@ int Parser::parse_wavecode(char * token, std::istream & fs, MilkdropPreset * pr return PROJECTM_SUCCESS; } -int Parser::parse_shapecode(char * token, std::istream & fs, MilkdropPreset * preset) +int OldParser::parse_shapecode(char * token, std::istream & fs, MilkdropPreset * preset) { char * var_string; @@ -2012,7 +2013,7 @@ int Parser::parse_shapecode(char * token, std::istream & fs, MilkdropPreset * p } -int Parser::parse_wavecode_prefix(char * token, int * id, char ** var_string) +int OldParser::parse_wavecode_prefix(char * token, int * id, char ** var_string) { int len, i; @@ -2057,7 +2058,7 @@ int Parser::parse_wavecode_prefix(char * token, int * id, char ** var_string) } -int Parser::parse_shapecode_prefix(char * token, int * id, char ** var_string) +int OldParser::parse_shapecode_prefix(char * token, int * id, char ** var_string) { int len, i; @@ -2101,7 +2102,7 @@ int Parser::parse_shapecode_prefix(char * token, int * id, char ** var_string) } -int Parser::parse_wave_prefix(char * token, int * id, char ** eqn_string) +int OldParser::parse_wave_prefix(char * token, int * id, char ** eqn_string) { int len, i; @@ -2144,7 +2145,7 @@ int Parser::parse_wave_prefix(char * token, int * id, char ** eqn_string) } -int Parser::parse_shape_prefix(char * token, int * id, char ** eqn_string) +int OldParser::parse_shape_prefix(char * token, int * id, char ** eqn_string) { int len, i; @@ -2187,7 +2188,7 @@ int Parser::parse_shape_prefix(char * token, int * id, char ** eqn_string) } /* Parses custom wave equations */ -int Parser::parse_wave(char * token, std::istream & fs, MilkdropPreset * preset) +int OldParser::parse_wave(char * token, std::istream & fs, MilkdropPreset * preset) { int id; @@ -2215,7 +2216,7 @@ int Parser::parse_wave(char * token, std::istream & fs, MilkdropPreset * preset } -int Parser::parse_wave_helper(std::istream & fs, MilkdropPreset * preset, int id, char * eqn_type, char * init_string) +int OldParser::parse_wave_helper(std::istream & fs, MilkdropPreset * preset, int id, char * eqn_type, char * init_string) { Param * param; @@ -2368,7 +2369,7 @@ int Parser::parse_wave_helper(std::istream & fs, MilkdropPreset * preset, int } /* Parses custom shape equations */ -int Parser::parse_shape(char * token, std::istream & fs, MilkdropPreset * preset) +int OldParser::parse_shape(char * token, std::istream & fs, MilkdropPreset * preset) { int id; @@ -2423,7 +2424,7 @@ int Parser::parse_shape(char * token, std::istream & fs, MilkdropPreset * prese Returns -1 if syntax error */ -int Parser::get_string_prefix_len(char * string) +int OldParser::get_string_prefix_len(char * string) { int i = 0; @@ -2461,7 +2462,7 @@ int Parser::get_string_prefix_len(char * string) return i; } -int Parser::parse_shape_per_frame_init_eqn(std::istream & fs, CustomShape * custom_shape, MilkdropPreset * preset) +int OldParser::parse_shape_per_frame_init_eqn(std::istream & fs, CustomShape * custom_shape, MilkdropPreset * preset) { InitCond * init_cond; @@ -2483,7 +2484,7 @@ int Parser::parse_shape_per_frame_init_eqn(std::istream & fs, CustomShape * cus return PROJECTM_SUCCESS; } -int Parser::parse_shape_per_frame_eqn(std::istream & fs, CustomShape * custom_shape, MilkdropPreset * preset) +int OldParser::parse_shape_per_frame_eqn(std::istream & fs, CustomShape * custom_shape, MilkdropPreset * preset) { Param* param{ nullptr }; @@ -2574,7 +2575,7 @@ int Parser::parse_shape_per_frame_eqn(std::istream & fs, CustomShape * custom_sh return PROJECTM_SUCCESS; } -int Parser::parse_wave_per_frame_eqn(std::istream & fs, CustomWave * custom_wave, MilkdropPreset * preset) +int OldParser::parse_wave_per_frame_eqn(std::istream & fs, CustomWave * custom_wave, MilkdropPreset * preset) { Param* param{ nullptr }; @@ -2673,7 +2674,7 @@ int Parser::parse_wave_per_frame_eqn(std::istream & fs, CustomWave * custom_wav } -bool Parser::wrapsToNextLine(const std::string & str) { +bool OldParser::wrapsToNextLine(const std::string & str) { std::size_t lastLineEndIndex = lastLinePrefix.find_last_not_of("0123456789"); diff --git a/src/libprojectM/MilkdropPresetFactory/Parser.hpp b/src/libprojectM/MilkdropPresetFactory/OldParser.hpp similarity index 99% rename from src/libprojectM/MilkdropPresetFactory/Parser.hpp rename to src/libprojectM/MilkdropPresetFactory/OldParser.hpp index 88e288b6c..1921037b7 100755 --- a/src/libprojectM/MilkdropPresetFactory/Parser.hpp +++ b/src/libprojectM/MilkdropPresetFactory/OldParser.hpp @@ -129,7 +129,7 @@ class PerFrameEqn; class MilkdropPreset; class TreeExpr; -class Parser { +class OldParser { public: static std::string lastLinePrefix; static line_mode_t line_mode; diff --git a/src/libprojectM/TestRunner.cpp b/src/libprojectM/TestRunner.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/tests/FileParserTest.cpp b/tests/FileParserTest.cpp new file mode 100644 index 000000000..776a70298 --- /dev/null +++ b/tests/FileParserTest.cpp @@ -0,0 +1,332 @@ +#include + +#include + +static constexpr auto fileParserTestDataPath{ PROJECTM_TEST_DATA_DIR "/FileParser/" }; + +/** + * Class to make protected function accessible to tests. + */ +class FileParserMock : public FileParser +{ +public: + static void StripComment(std::string& line) + { + FileParser::StripComment(line); + } + + static void StripMultilineComment(std::string& code) + { + FileParser::StripMultilineComment(code); + } + + static void Trim(std::string& line) + { + FileParser::Trim(line); + } +}; + +TEST(FileParser, ReadEmptyFile) +{ + FileParser parser; + ASSERT_FALSE(parser.Read(std::string(fileParserTestDataPath) + "parser-empty.milk")); +} + +TEST(FileParser, ReadFileWithNullByte) +{ + FileParser parser; + ASSERT_FALSE(parser.Read(std::string(fileParserTestDataPath) + "parser-nullbyte.milk")); +} + +TEST(FileParser, ReadSimpleFile) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-simple.milk")); +} + +TEST(FileParser, GetRawPresetValues) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-simple.milk")); + + const auto& values = parser.PresetValues(); + + EXPECT_FALSE(values.empty()); +} + +TEST(FileParser, EmptyValue) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-simple.milk")); + + const auto& values = parser.PresetValues(); + + // Lines with empty values should be ignored + EXPECT_TRUE(values.find("empty_value") == values.end()); +} + +TEST(FileParser, EmptyKey) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-simple.milk")); + + const auto& values = parser.PresetValues(); + + // Lines with empty key should be ignored + ASSERT_TRUE(values.find("value_with_space") != values.end()); + EXPECT_EQ(values.at("value_with_space"), "123"); +} + +TEST(FileParser, ValueWithSpaceDelimiter) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-simple.milk")); + + const auto& values = parser.PresetValues(); + + // Lines with empty key should be ignored + EXPECT_TRUE(values.find("empty_key") == values.end()); +} + +TEST(FileParser, ReadFileWithRepeatedKey) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-repeatedkey.milk")); + + const auto& values = parser.PresetValues(); + + ASSERT_TRUE(values.find("warp") != values.end()); + EXPECT_EQ(values.at("warp"), "0"); +} + +TEST(FileParser, GetCode) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-code.milk")); + + auto code = parser.GetCode("per_frame_"); + EXPECT_EQ(code, "r=1.0; g=1.0; b=1.0; "); +} + +TEST(FileParser, GetCodeWithGap) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-code.milk")); + + auto code = parser.GetCode("per_frame_gap_"); + EXPECT_EQ(code, "r=1.0; g=1.0; "); +} + +TEST(FileParser, GetCodeWithRepeatedLine) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-code.milk")); + + auto code = parser.GetCode("per_frame_repeat_"); + EXPECT_EQ(code, "r=1.0; g=1.0; b=1.0; "); +} + +TEST(FileParser, GetCodeTrimmed) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-code.milk")); + + auto code = parser.GetCode("per_frame_trim_"); + EXPECT_EQ(code, "r = 1.0; g = 1.0; b = 1.0; "); +} + +TEST(FileParser, GetCodeMultilineComment) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-code.milk")); + + auto code = parser.GetCode("multiline_comment_"); + EXPECT_EQ(code, "r = 1.0; b = 1.0; "); +} + +TEST(FileParser, GetCodeShaderSyntax) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-code.milk")); + + auto code = parser.GetCode("warp_"); + EXPECT_EQ(code, "r=1.0;\ng=1.0;\nb=1.0;\n"); +} + +TEST(FileParser, GetIntValid) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-valueconversion.milk")); + + EXPECT_EQ(parser.GetInt("nVideoEchoOrientation", 0), 3); +} + +TEST(FileParser, GetIntInvalid) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-valueconversion.milk")); + + EXPECT_EQ(parser.GetInt("nSomeWeirdStuff", 123), 123); +} + +TEST(FileParser, GetIntDefault) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-valueconversion.milk")); + + EXPECT_EQ(parser.GetInt("RandomKey", 123), 123); +} + +TEST(FileParser, GetFloatValid) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-valueconversion.milk")); + + EXPECT_FLOAT_EQ(parser.GetFloat("fVideoEchoAlpha", 0), 0.5f); +} + +TEST(FileParser, GetFloatInvalid) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-valueconversion.milk")); + + EXPECT_FLOAT_EQ(parser.GetFloat("fSomeWeirdStuff", 123.0f), 123.0f); +} + +TEST(FileParser, GetFloatDefault) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-valueconversion.milk")); + + EXPECT_FLOAT_EQ(parser.GetFloat("RandomKey", 123.0f), 123.0f); +} + +TEST(FileParser, GetBooleanValid) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-valueconversion.milk")); + + EXPECT_EQ(parser.GetBool("bAdditiveWaves", false), true); +} + +TEST(FileParser, GetBooleanInvalid) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-valueconversion.milk")); + + EXPECT_EQ(parser.GetBool("bSomeWeirdStuff", true), true); +} + +TEST(FileParser, GetBooleanDefault) +{ + FileParser parser; + ASSERT_TRUE(parser.Read(std::string(fileParserTestDataPath) + "parser-valueconversion.milk")); + + EXPECT_EQ(parser.GetBool("RandomKey", true), true); +} + +TEST(FileParser, StripCommentBegin) +{ + std::string line{ "// Full line comment" }; + FileParserMock::StripComment(line); + ASSERT_EQ(line, ""); +} + +TEST(FileParser, StripCommentLeadingWhitespace) +{ + std::string line{ " // Full line comment" }; + FileParserMock::StripComment(line); + ASSERT_EQ(line, " "); +} + +TEST(FileParser, StripCommentLeadingText) +{ + std::string line{ "1.005// Some value" }; + FileParserMock::StripComment(line); + ASSERT_EQ(line, "1.005"); +} + +TEST(FileParser, StripCommentEmptyComment) +{ + std::string line{ "1.005//" }; + FileParserMock::StripComment(line); + ASSERT_EQ(line, "1.005"); +} + +TEST(FileParser, StripMultiLineCommentOnlyComment) +{ + std::string code{ "/* Full line comment */" }; + FileParserMock::StripMultilineComment(code); + ASSERT_EQ(code, ""); +} + +TEST(FileParser, StripMultiLineCommentMiddleComment) +{ + std::string code{ "Some /* Middle line comment */ Text" }; + FileParserMock::StripMultilineComment(code); + ASSERT_EQ(code, "Some Text"); +} + +TEST(FileParser, StripMultiLineCommentMultipleComments) +{ + std::string code{ "Some /* Middle */ More /* line */ Nice /* comment */ Text" }; + FileParserMock::StripMultilineComment(code); + ASSERT_EQ(code, "Some More Nice Text"); +} + +TEST(FileParser, StripMultiLineCommentWithLinebreak) +{ + // Mot really a usecase as newlines are stripped, but should work. + std::string code{ "/* Multi\nline\ncomment */" }; + FileParserMock::StripMultilineComment(code); + ASSERT_EQ(code, ""); +} + +TEST(FileParser, StripMultiLineCommentWithWrongTerminatorBeginning) +{ + std::string code{ "*/ Text /* Comment with termination in front" }; + FileParserMock::StripMultilineComment(code); + ASSERT_EQ(code, "*/ Text "); +} + +TEST(FileParser, StripMultiLineCommentWithWrongTerminatorEnd) +{ + std::string code{ "Text /* Comment with two terminators */ */" }; + FileParserMock::StripMultilineComment(code); + ASSERT_EQ(code, "Text */"); +} + +TEST(FileParser, StripMultiLineCommentWithoutTerminator) +{ + std::string code{ "Text /* Comment without termination" }; + FileParserMock::StripMultilineComment(code); + ASSERT_EQ(code, "Text "); +} + +TEST(FileParser, TrimFront) +{ + std::string line{ " TEXT TEXT" }; + FileParserMock::Trim(line); + ASSERT_EQ(line, "TEXT TEXT"); +} + +TEST(FileParser, TrimBack) +{ + std::string line{ "TEXT TEXT " }; + FileParserMock::Trim(line); + ASSERT_EQ(line, "TEXT TEXT"); +} + +TEST(FileParser, TrimBoth) +{ + std::string line{ " TEXT TEXT " }; + FileParserMock::Trim(line); + ASSERT_EQ(line, "TEXT TEXT"); +} + +TEST(FileParser, TrimOtherWhitespace) +{ + std::string line{ "\t \v TEXT TEXT \r \n \f" }; + FileParserMock::Trim(line); + ASSERT_EQ(line, "TEXT TEXT"); +} diff --git a/tests/data/FileParser/parser-code.milk b/tests/data/FileParser/parser-code.milk new file mode 100644 index 000000000..430c52aa7 --- /dev/null +++ b/tests/data/FileParser/parser-code.milk @@ -0,0 +1,34 @@ +// Very simple multi-line equation +per_frame_1=r=1.0; +per_frame_2=g=1.0; +per_frame_3=b=1.0; + +// A gap in numbering should terminate parsing +per_frame_gap_1=r=1.0; +per_frame_gap_2=g=1.0; +per_frame_gap_4=b=1.0; + +// Line number 2 is duplicated, should only use the first one +per_frame_repeat_1=r=1.0; +per_frame_repeat_2=g=1.0; +per_frame_repeat_2=pi=3.141; +per_frame_repeat_3=b=1.0; + +// Some different comment formats used plus a lot of whitespace. +// Also contains an equation that spans two line. +per_frame_trim_1= r = 1.0; +per_frame_trim_2= g = 1.0; +per_frame_trim_3=//****************** Comment format often seen in equations +per_frame_trim_4= // Empty line with comment +per_frame_trim_5= b = \\Continued line with backslash comment +per_frame_trim_6= 1.0; + +// Comment spanning multiple lines, starting/ending at arbitrary positions +multiline_comment_1=r = 1.0; /* Comment... +multiline_comment_2=g = 1.0; +multiline_comment_3=... ends here */b = 1.0; + +// Shader syntax +warp_1=`r=1.0; +warp_2=`g=1.0; +warp_3=`b=1.0; diff --git a/tests/data/FileParser/parser-empty.milk b/tests/data/FileParser/parser-empty.milk new file mode 100644 index 000000000..e69de29bb diff --git a/tests/data/FileParser/parser-nullbyte.milk b/tests/data/FileParser/parser-nullbyte.milk new file mode 100644 index 000000000..a8f388867 Binary files /dev/null and b/tests/data/FileParser/parser-nullbyte.milk differ diff --git a/tests/data/FileParser/parser-repeatedkey.milk b/tests/data/FileParser/parser-repeatedkey.milk new file mode 100644 index 000000000..f150fc0ed --- /dev/null +++ b/tests/data/FileParser/parser-repeatedkey.milk @@ -0,0 +1,5 @@ +// Key repeated several times. Only the first should be used. +[preset00] +warp=0 +warp=1 +warp=2 diff --git a/tests/data/FileParser/parser-simple.milk b/tests/data/FileParser/parser-simple.milk new file mode 100644 index 000000000..e42868b1e --- /dev/null +++ b/tests/data/FileParser/parser-simple.milk @@ -0,0 +1,7 @@ +// Some simple assignments, with empty key or value and a space instead of an equal sign. +[preset00] +warp=0 +empty_value= +=empty_key +value_with_space 123 +// No newline at end of file! \ No newline at end of file diff --git a/tests/data/FileParser/parser-valueconversion.milk b/tests/data/FileParser/parser-valueconversion.milk new file mode 100644 index 000000000..34f7bcd49 --- /dev/null +++ b/tests/data/FileParser/parser-valueconversion.milk @@ -0,0 +1,8 @@ +// Used to test the value conversion functions for init params. +[preset00] +fVideoEchoAlpha=0.500 pounds +fSomeWeirdStuff=X +nVideoEchoOrientation=3 // Have a comment. +nSomeWeirdStuff=X +bAdditiveWaves=1 +bSomeWeirdStuff=X \ No newline at end of file