diff --git a/src/builtin/jsondecode.h b/src/builtin/jsondecode.h new file mode 100644 index 0000000..3633b3f --- /dev/null +++ b/src/builtin/jsondecode.h @@ -0,0 +1,48 @@ +/** + * Jsondecode.h + * + * Built-in "|jsondecode" modifier + * + * @author Michael van der Werve + * @copyright 2020 Copernica BV + */ + +/** + * Namespace + */ +namespace SmartTpl { namespace Internal { + +/** + * Class definition + */ +class JsondecodeModifier : public Modifier +{ +public: + /** + * Destructor + */ + virtual ~JsondecodeModifier() {}; + + /** + * Modify a value object + * @param input + * @param params Parameters used for this modification + * @return Value + */ + VariantValue modify(const Value &input, const SmartTpl::Parameters ¶ms) override + { + // Get the json encoder + const Escaper *escaper = Escaper::get("json"); + + // Turn our input into a string + std::string output(input.toString()); + + // Call encode and return the output + return escaper->decode(output); + } +}; + +/** + * End namespace + */ +}} diff --git a/src/builtin/jsonencode.h b/src/builtin/jsonencode.h new file mode 100644 index 0000000..e4c0ef8 --- /dev/null +++ b/src/builtin/jsonencode.h @@ -0,0 +1,48 @@ +/** + * Jsonencode.h + * + * Built-in "|jsonencode" modifier + * + * @author Michael van der Werve + * @copyright 2020 Copernica BV + */ + +/** + * Namespace + */ +namespace SmartTpl { namespace Internal { + +/** + * Class definition + */ +class JsonencodeModifier : public Modifier +{ +public: + /** + * Destructor + */ + virtual ~JsonencodeModifier() {}; + + /** + * Modify a value object + * @param input + * @param params Parameters used for this modification + * @return Value + */ + VariantValue modify(const Value &input, const SmartTpl::Parameters ¶ms) override + { + // Get the json encoder + const Escaper *escaper = Escaper::get("json"); + + // Turn our input into a string + std::string output(input.toString()); + + // Call encode and return the output + return escaper->encode(output); + } +}; + +/** + * End namespace + */ +}} diff --git a/src/data.cpp b/src/data.cpp index 83853d4..dea172e 100644 --- a/src/data.cpp +++ b/src/data.cpp @@ -4,7 +4,7 @@ * Implementation file for the Data class * * @author Emiel Bruijntjes - * @copyright 2014 Copernica BV + * @copyright 2014 - 2020 Copernica BV */ /** @@ -43,6 +43,8 @@ static Internal::StrPosModifier strpos; static Internal::StrStrModifier strstr; static Internal::NumberFormatModifier number_format; static Internal::DateFormatModifier date_format; +static Internal::JsonencodeModifier jsonencode; +static Internal::JsondecodeModifier jsondecode; static Internal::UrlencodeModifier urlencode; static Internal::UrldecodeModifier urldecode; static Internal::Md5Modifier md5; @@ -84,6 +86,8 @@ Data::Data() {"strpos", &strpos}, {"number_format", &number_format}, {"date_format", &date_format}, + {"jsonencode", &jsonencode}, + {"jsondecode", &jsondecode}, {"urlencode", &urlencode}, {"urldecode", &urldecode}, {"range", &range_modifier}}) // register built-in modifiers diff --git a/src/escaper.cpp b/src/escaper.cpp index 3b6254a..0dab3d5 100644 --- a/src/escaper.cpp +++ b/src/escaper.cpp @@ -4,7 +4,7 @@ * Implementation of the Escaper class * * @author Toon Schoenmakers - * @copyright 2014 Copernica BV + * @copyright 2014 - 2020 Copernica BV */ #include "includes.h" @@ -34,6 +34,7 @@ Escaper::Escaper(const char *name) */ static NullEscaper _null; static HtmlEscaper _html; +static JsonEscaper _json; static UrlEscaper _url; static Base64Escaper _base64; @@ -57,4 +58,4 @@ Escaper* Escaper::get(const std::string &encoding) /** * End namespace */ -}} \ No newline at end of file +}} diff --git a/src/escapers/json.h b/src/escapers/json.h new file mode 100644 index 0000000..9579907 --- /dev/null +++ b/src/escapers/json.h @@ -0,0 +1,185 @@ +/** + * Json.h + * + * A JSON en/decoder + * + * @author Michael van der Werve + * @copyright 2020 Copernica BV + */ + +/** + * Namespace + */ +namespace SmartTpl { namespace Internal { + +/** + * Class definition + */ +class JsonEscaper : public Escaper +{ +private: + /** + * Hex lookup table + */ + static constexpr const char *hex = "0123456789abcdefABCDEF"; + + /** + * Helper method to convert a hex digit to decimal + * @param digit + * @return uint8_t + */ + inline uint8_t decimal(uint8_t digit) const + { + // convert hex digit to decimal (doesnt check for hex number validity) + return (digit <= 9) ? digit - '0' : ((digit & 7) + 9); + } + + /** + * Helper method to convert two hex digits to a byte + * @param digit1 + */ + uint8_t byte(uint8_t left, uint8_t right) const + { + // reverse the byte sequence + return decimal(left) << 4 & decimal(right); + } + +public: + /** + * Constructor, in case the openssl library failed to load we simply are + * giving our base constructor a nullptr so it doesn't register us + */ + JsonEscaper() : Escaper("json") {}; + + /** + * Destructor + */ + virtual ~JsonEscaper() {} + + /** + * Encode the given input + * It is probably a good idea to directly modify the input instead of making + * a copy and modifying that. + * @param input + */ + virtual std::string &encode(std::string &input) const override + { + // we need to make a copy, because there's no 1-1 transformation on characters + std::string output; + + // reserve at least enough bytes + output.reserve(input.size()); + + // iterate over the characters + for (const uint8_t &c : input) + { + // in the first 32 bits we always have to escape + if (c < 32 || c == '"' || c == '\\') + { + switch(c) { + case '\b': output.append("\\b"); break; + case '\n': output.append("\\n"); break; + case '\r': output.append("\\r"); break; + case '\t': output.append("\\t"); break; + case '\f': output.append("\\f"); break; + case '"': output.append("\\\""); break; + case '\\': output.append("\\\\"); break; + default: + // write a byte in hex + output.append("\\x"); + output.push_back(hex[c >> 4]); + output.push_back(hex[c & 0xf]); + } + } + + // append character normally + else output.push_back(c); + } + + // swap the output and the input + std::swap(output, input); + + // return the input + return input; + } + + /** + * Decode the given input + * It is probably a good idea to directly modify the input instead of making + * a copy and modifying that. + * @param input + */ + virtual std::string &decode(std::string &input) const override + { + // we need to make a copy, because there's no 1-1 transformation on characters + std::string output; + + // reserve at least enough bytes + output.reserve(input.size()); + + // store the 'current' pointer + const char *current = input.data(); + + // the next character that is escaped + const char *next = nullptr; + + // we always remove a backslash and move one forward, unless it is followed by an x + while ((next = strchr(current, '\\'))) + { + // wirte everything since last + output.append(current, next - current); + + // we now have a new current, because we'll consume two extra bytes, the found \ and a character (skip those) + current = next + 2; + + // parse the next character at this point, it is special + switch (next[1]) { + case 'b': output.push_back('\b'); break; + case 'n': output.push_back('\n'); break; + case 'r': output.push_back('\r'); break; + case 't': output.push_back('\t'); break; + case 'f': output.push_back('\f'); break; + case '"': output.push_back('"'); break; + case '\\': output.push_back('\\'); break; + // solidus should also be escapable, but does not need to be escaped + case '/': output.push_back('/'); break; + case 'x': + // we consume two extra bytes, so check if they are actually valid. since this is a json string, we can easily + // check with simply null character checking + if (current[0] && !current[1] && isxdigit(current[0]) && isxdigit(current[1])) + { + // add a single byte, we consume 3 + output.push_back(byte(current[0], current[1])); + + // we consumed two extra bytes + current += 2; + } + + // if we arrived here, there were no more bytes or they're not hex, and so we're in an invalid state. + // we fall through to the error-correction. + case '\0': + default: + // this is not allowed, some unknown character was escaped. return the result so far + std::swap(output, input); + + // and we leap out of the function + return input; + } + } + + // append the rest of the output + output.append(current); + + // swap the output with the input + std::swap(output, input); + + // and return the input + return input; + } + +}; + +/** + * End namespace + */ +}} diff --git a/src/includes.h b/src/includes.h index 9cf5d78..54a9731 100644 --- a/src/includes.h +++ b/src/includes.h @@ -83,6 +83,7 @@ #include "escapers/html.h" #include "escapers/url.h" #include "escapers/base64.h" +#include "escapers/json.h" #include "builtin/tolower.h" #include "builtin/toupper.h" #include "builtin/cat.h" @@ -106,6 +107,8 @@ #include "builtin/strstr.h" #include "builtin/strpos.h" #include "builtin/number_format.h" +#include "builtin/jsonencode.h" +#include "builtin/jsondecode.h" #include "builtin/urlencode.h" #include "builtin/urldecode.h" #include "builtin/md5.h" diff --git a/test/modifier.cpp b/test/modifier.cpp index 3a14ac5..b7c04d4 100644 --- a/test/modifier.cpp +++ b/test/modifier.cpp @@ -4,7 +4,8 @@ * Test the built-in modifiers * * @author Toon Schoenmakers - * @copyright 2014 Copernica BV + * @author Michael van der Werve + * @copyright 2014-2020 Copernica BV */ #include @@ -163,7 +164,7 @@ TEST(Modifier, Default) } } -TEST(Modifier, Escape) +TEST(Modifier, EscapeHtml) { string input("{$var|escape:\"html\"}"); Template tpl((Buffer(input))); @@ -181,6 +182,61 @@ TEST(Modifier, Escape) } } +TEST(Modifier, EscapeJson) +{ + string input("{$var|escape:\"json\"}"); + Template tpl((Buffer(input))); + + Data data; + data.assign("var", std::string("\t\nSome string\0after", 19)); + + string expectedOutput("\\t\\nSome string\\x00after"); + EXPECT_EQ(expectedOutput, tpl.process(data)); + + if (compile(tpl)) // This will compile the Template into a shared library + { + Template library(File(SHARED_LIBRARY)); // Here we load that shared library + EXPECT_EQ(expectedOutput, library.process(data)); + } +} + +TEST(Modifier, JsonEncode) +{ + string input("{$var|0:jsonencode}"); + Template tpl((Buffer(input))); + + Data data; + data.assign("var", std::string("\t\nSome string\0after", 19)); + + string expectedOutput("\\t\\nSome string\\x00after"); + EXPECT_EQ(expectedOutput, tpl.process(data)); + + if (compile(tpl)) // This will compile the Template into a shared library + { + Template library(File(SHARED_LIBRARY)); // Here we load that shared library + EXPECT_EQ(expectedOutput, library.process(data)); + } +} + +TEST(Modifier, JsonDecode) +{ + string input("{$var|0:jsondecode}"); + Template tpl((Buffer(input))); + + Data data; + data.assign("var", std::string("\\t\\nSome string\\x00after", 19)); + + string expectedOutput("\t\nSome string\0after"); + EXPECT_EQ(expectedOutput, tpl.process(data)); + + if (compile(tpl)) // This will compile the Template into a shared library + { + Template library(File(SHARED_LIBRARY)); // Here we load that shared library + EXPECT_EQ(expectedOutput, library.process(data)); + } +} + + TEST(Modifier, Indent) { string input("{$var|indent}\n-\n{$var|indent:1:\"\t\"}\n-\n{$var2|indent:1:\"\t\"}"); @@ -779,4 +835,4 @@ TEST(Modifier, LiteralInput) Template library(File(SHARED_LIBRARY)); // Here we load that shared library EXPECT_EQ(expectedOutput, library.process(data)); } -} \ No newline at end of file +}