diff --git a/simgear/misc/strutils.cxx b/simgear/misc/strutils.cxx index 18f0b10b..f20d5f68 100644 --- a/simgear/misc/strutils.cxx +++ b/simgear/misc/strutils.cxx @@ -632,6 +632,59 @@ std::string encodeHex(const unsigned char* rawBytes, unsigned int length) return hex; } +// Write an octal backslash-escaped respresentation of 'val' to 'buf'. +// +// At least 4 write positions must be available at 'buf'. The result is *not* +// null-terminated. Only the 8 least significant bits of 'val' are used; +// higher-order bits have no influence on the chars written to 'buf'. +static void writeOctalBackslashEscapedRepr(char *buf, unsigned char val) +{ + buf[0] = '\\'; + buf[1] = '0' + ((val >> 6) & 3); // 2 bits + buf[2] = '0' + ((val >> 3) & 7); // 3 bits + buf[3] = '0' + (val & 7); // 3 bits +} + +// Backslash-escape a string for C/C++ string literal syntax. +std::string escape(const std::string& s) { + string res; + char buf[4]; + + for (const char c: s) { + // We don't really *need* to special-case \a, \b, \f, \n, \r, \t and \v, + // because they could be handled like the other non-ASCII or non-printable + // characters. However, doing so will make the output string both shorter + // and more readable. + if (c == '\a') { + res += "\\a"; + } else if (c == '\b') { + res += "\\b"; + } else if (c == '\f') { + res += "\\f"; + } else if (c == '\n') { + res += "\\n"; + } else if (c == '\r') { + res += "\\r"; + } else if (c == '\t') { + res += "\\t"; + } else if (c == '\v') { + res += "\\v"; + } else if (c < 0x20 || c > 0x7e) { // non-ASCII or non-printable character + // This is fast (no memory allocation nor IOStreams needed) + writeOctalBackslashEscapedRepr(buf, static_cast(c)); + res.append(buf, 4); + } else if (c == '\\') { + res += "\\\\"; + } else if (c == '"') { + res += "\\\""; + } else { + res += c; + } + } + + return res; +} + //------------------------------------------------------------------------------ std::string unescape(const char* s) { diff --git a/simgear/misc/strutils.hxx b/simgear/misc/strutils.hxx index e49d67e3..73c1d387 100644 --- a/simgear/misc/strutils.hxx +++ b/simgear/misc/strutils.hxx @@ -230,26 +230,43 @@ namespace simgear { * malformed */ void decodeBase64(const std::string& a, std::vector& output); - + /** * convert bytes to hexadecimal equivalent */ std::string encodeHex(const std::string& bytes); - + std::string encodeHex(const unsigned char* rawBytes, unsigned int length); + /** + * Backslash-escape a string for C/C++ string literal syntax. + * + * @param s Input string. + * @return a copy of the input string with proper escaping, so that if the + * result is part of a C or C++ file and enclosed in double + * quotes, it can be used to represent a string literal that is + * equal to the input string. + * + * @note For every std::string s: unescape(escape(s)) == s + * @see unescape() + */ + std::string escape(const std::string& s); + /** * Unescape string. * * @param str String possibly containing escaped characters. * @return string with escaped characters replaced by single character * values. + * + * @note For every std::string s: unescape(escape(s)) == s + * @see escape() */ std::string unescape(const char* str); inline std::string unescape(const std::string& str) { return unescape(str.c_str()); } - + /** * Check a printf-style format string for dangerous (buffer-overflowing, * memory re-writing) format tokens. If a problematic token is diff --git a/simgear/misc/strutils_test.cxx b/simgear/misc/strutils_test.cxx index a485e432..2ffd03f4 100644 --- a/simgear/misc/strutils_test.cxx +++ b/simgear/misc/strutils_test.cxx @@ -1,15 +1,20 @@ +// -*- coding: utf-8 -*- +// // Unit tests for functions inside the strutils package -#include -#include // _set_errno() on Windows #include +#include +#include // std::move() #include // std::ifstream +#include +#include // _set_errno() on Windows #include #include #include using std::string; +using std::vector; namespace strutils = simgear::strutils; @@ -139,6 +144,40 @@ void test_split() } } +void test_escape() +{ + SG_CHECK_EQUAL(strutils::escape(""), ""); + SG_CHECK_EQUAL(strutils::escape("\\"), "\\\\"); + SG_CHECK_EQUAL(strutils::escape("\""), "\\\""); + SG_CHECK_EQUAL(strutils::escape("\\n"), "\\\\n"); + SG_CHECK_EQUAL(strutils::escape("n\\"), "n\\\\"); + SG_CHECK_EQUAL(strutils::escape(" ab\nc \\def\t\r \\ ghi\\"), + " ab\\nc \\\\def\\t\\r \\\\ ghi\\\\"); + // U+0152 is LATIN CAPITAL LIGATURE OE. The last word is Egg translated in + // French and encoded in UTF-8 ('Œuf' if you can read UTF-8). + SG_CHECK_EQUAL(strutils::escape("Un \"Bel\" '\u0152uf'"), + "Un \\\"Bel\\\" '\\305\\222uf'"); + SG_CHECK_EQUAL(strutils::escape("\a\b\f\n\r\t\v"), + "\\a\\b\\f\\n\\r\\t\\v"); + + // Test with non-printable characters + // + // - 'prefix' is an std::string that *contains* a NUL character. + // - \012 is \n (LINE FEED). + // - \037 (\x1F) is the last non-printable ASCII character before \040 (\x20), + // which is the space. + // - \176 (\x7E) is '~', the last printable ASCII character. + // - \377 is \xFF. Higher char values (> 255) are not faithfully encoded by + // strutils::escape(): only the lowest 8 bits are used; higher-order bits + // are ignored (for people who use chars with more than 8 bits...). + const string prefix = string("abc") + '\000'; + SG_CHECK_EQUAL(strutils::escape(prefix + + "\003def\012\037\040\176\177\376\377"), + "abc\\000\\003def\\n\\037 ~\\177\\376\\377"); + + SG_CHECK_EQUAL(strutils::escape(" \n\tAOa"), " \\n\\tAOa"); +} + void test_unescape() { SG_CHECK_EQUAL(strutils::unescape("\\ \\n\\t\\x41\\117a"), " \n\tAOa"); @@ -151,6 +190,28 @@ void test_unescape() SG_CHECK_EQUAL(strutils::unescape("\\xA-"), "\n-"); } +void aux_escapeAndUnescapeRoundTripTest(const string& testString) +{ + SG_CHECK_EQUAL(strutils::unescape(strutils::escape(testString)), testString); +} + +void test_escapeAndUnescapeRoundTrips() +{ + // "\0332" contains two chars: '\033' (ESC) followed by '2'. + // Ditto for "\0402": it's a space ('\040') followed by a '2'. + vector stringsToTest( + {"", "\\", "\n", "\\\\", "\"\'\?\t\rAG\v\a \b\f\\", "\x23\xf8", + "\0332", "\0402", "\uab42", "\U12345678"}); + + const string withBinary = (string("abc") + '\000' + + "\003def\012\037\040\176\177\376\377"); + stringsToTest.push_back(std::move(withBinary)); + + for (const string& s: stringsToTest) { + aux_escapeAndUnescapeRoundTripTest(s); + } +} + void test_compare_versions() { SG_CHECK_LT(strutils::compare_versions("1.0.12", "1.1"), 0); @@ -239,7 +300,9 @@ int main(int argc, char* argv[]) test_simplify(); test_to_int(); test_split(); + test_escape(); test_unescape(); + test_escapeAndUnescapeRoundTrips(); test_compare_versions(); test_md5_hex(); test_error_string();