diff --git a/include/loot/metadata/file.h b/include/loot/metadata/file.h index 0b9dd21c..59e0c4f4 100644 --- a/include/loot/metadata/file.h +++ b/include/loot/metadata/file.h @@ -47,7 +47,8 @@ public: * @param name * The filename of the file. * @param display - * The name to be displayed for the file in messages. + * The name to be displayed for the file in messages, formatted using + * GitHub Flavored Markdown. * @param condition * The File's condition string. * @return A File object. @@ -77,7 +78,12 @@ public: /** * Get the display name of the file. - * @return The file's display name. + * + * If the File was constructed with an empty display string, the name field + * will be returned instead, with any `ASCII punctuation characters + * `_ escaped. + * Escaping is not performed if returning the value of the display string. + * @return The file's display name or filename. */ LOOT_API std::string GetDisplayName() const; diff --git a/src/api/helpers/text.cpp b/src/api/helpers/text.cpp index a5cac26b..2f1e1843 100644 --- a/src/api/helpers/text.cpp +++ b/src/api/helpers/text.cpp @@ -71,6 +71,13 @@ const std::vector versionRegexes({ regex::ECMAScript | regex::icase), }); +// As defined by . +const regex asciiPunctuationCharacters("([!\"#$%&'()*+,\\-./:;<=>?@\\[\\\\\\]^_`{|}~])"); + +std::string EscapeMarkdownASCIIPunctuation(const std::string& text) { + return std::regex_replace(text, asciiPunctuationCharacters, "\\$1"); +} + std::vector ExtractBashTags(const std::string& description) { std::vector tags; diff --git a/src/api/helpers/text.h b/src/api/helpers/text.h index c0729c97..5db702f3 100644 --- a/src/api/helpers/text.h +++ b/src/api/helpers/text.h @@ -32,6 +32,8 @@ #include "loot/metadata/tag.h" namespace loot { +std::string EscapeMarkdownASCIIPunctuation(const std::string& text); + std::vector ExtractBashTags(const std::string& description); std::optional ExtractVersion(const std::string& text); diff --git a/src/api/metadata/file.cpp b/src/api/metadata/file.cpp index 5976b566..59ac9518 100644 --- a/src/api/metadata/file.cpp +++ b/src/api/metadata/file.cpp @@ -65,10 +65,11 @@ bool File::operator==(const File& rhs) const { Filename File::GetName() const { return name_; } std::string File::GetDisplayName() const { - if (display_.empty()) - return std::string(name_); - else - return display_; + if (display_.empty()) { + return EscapeMarkdownASCIIPunctuation(std::string(name_)); + } + + return display_; } bool operator!=(const File& lhs, const File& rhs) { return !(lhs == rhs); } diff --git a/src/api/metadata/yaml/file.h b/src/api/metadata/yaml/file.h index 4ba02d84..fce37cfa 100644 --- a/src/api/metadata/yaml/file.h +++ b/src/api/metadata/yaml/file.h @@ -31,6 +31,7 @@ #include #include "loot/metadata/file.h" +#include "api/helpers/text.h" namespace YAML { template<> @@ -42,8 +43,11 @@ struct convert { if (rhs.IsConditional()) node["condition"] = rhs.GetCondition(); - if (rhs.GetDisplayName() != std::string(rhs.GetName())) + auto escapedName = + loot::EscapeMarkdownASCIIPunctuation(std::string(rhs.GetName())); + if (rhs.GetDisplayName() != escapedName) { node["display"] = rhs.GetDisplayName(); + } return node; } @@ -83,9 +87,11 @@ struct convert { }; inline Emitter& operator<<(Emitter& out, const loot::File& rhs) { + auto escapedName = loot::EscapeMarkdownASCIIPunctuation(std::string(rhs.GetName())); + if (!rhs.IsConditional() && (rhs.GetDisplayName().empty() || - rhs.GetDisplayName() == std::string(rhs.GetName()))) + rhs.GetDisplayName() == escapedName)) out << YAML::SingleQuoted << std::string(rhs.GetName()); else { out << BeginMap << Key << "name" << Value << YAML::SingleQuoted @@ -95,7 +101,7 @@ inline Emitter& operator<<(Emitter& out, const loot::File& rhs) { out << Key << "condition" << Value << YAML::SingleQuoted << rhs.GetCondition(); - if (rhs.GetDisplayName() != std::string(rhs.GetName())) + if (rhs.GetDisplayName() != escapedName) out << Key << "display" << Value << YAML::SingleQuoted << rhs.GetDisplayName(); diff --git a/src/tests/api/internals/metadata/file_test.h b/src/tests/api/internals/metadata/file_test.h index e4070b8c..95fa445a 100644 --- a/src/tests/api/internals/metadata/file_test.h +++ b/src/tests/api/internals/metadata/file_test.h @@ -275,6 +275,31 @@ TEST( EXPECT_TRUE(file2 >= file1); } +TEST(File, getDisplayNameShouldReturnDisplayStringIfItIsNotEmpty) { + File file("name", "display"); + + EXPECT_EQ("display", file.GetDisplayName()); +} + +TEST(File, getDisplayNameShouldReturnNameStringIfDisplayStringIsEmpty) { + File file("name", ""); + + EXPECT_EQ("name", file.GetDisplayName()); +} +TEST(File, getDisplayNameShouldNotEscapeASCIIPunctuationInDisplayString) { + auto display = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; + File file("name", display); + + EXPECT_EQ(display, file.GetDisplayName()); +} + +TEST(File, getDisplayNameShouldEscapeASCIIPunctuationInNameString) { + File file("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"); + + EXPECT_EQ(R"raw(\!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~)raw", + file.GetDisplayName()); +} + TEST(File, emittingAsYamlShouldSingleQuoteValues) { File file("name1", "display1", "condition1"); YAML::Emitter emitter; @@ -287,19 +312,21 @@ TEST(File, emittingAsYamlShouldSingleQuoteValues) { } TEST(File, emittingAsYamlShouldOutputAsAScalarIfOnlyTheNameStringIsNotEmpty) { - File file("name1"); + File file("file.esp"); YAML::Emitter emitter; emitter << file; EXPECT_EQ("'" + std::string(file.GetName()) + "'", emitter.c_str()); } -TEST(File, emittingAsYamlShouldOmitDisplayFieldIfItMatchesTheNameField) { - File file("name1", "name1"); +TEST( + File, + emittingAsYamlShouldOmitDisplayFieldIfItMatchesTheNameFieldAfterEscapingASCIIPunctuation) { + File file("file.esp", "file\\.esp"); YAML::Emitter emitter; emitter << file; - EXPECT_EQ("'" + std::string(file.GetName()) + "'", emitter.c_str()); + EXPECT_STREQ("'file.esp'", emitter.c_str()); } TEST(File, emittingAsYamlShouldOmitAnEmptyConditionString) { @@ -323,7 +350,7 @@ TEST(File, encodingAsYamlShouldStoreDataCorrectly) { } TEST(File, encodingAsYamlShouldOmitEmptyFields) { - File file("name1"); + File file("file.esp"); YAML::Node node; node = file; @@ -332,8 +359,10 @@ TEST(File, encodingAsYamlShouldOmitEmptyFields) { EXPECT_FALSE(node["condition"]); } -TEST(File, encodingAsYamlShouldOmitDisplayFieldIfItMatchesTheNameField) { - File file("name1", "name1"); +TEST( + File, + encodingAsYamlShouldOmitDisplayFieldIfItMatchesTheNameFieldAfterEscapingASCIIPunctuation) { + File file("file.esp", "file\\.esp"); YAML::Node node; node = file;