diff --git a/docs/mainpage.doxygen.tmpl b/docs/mainpage.doxygen.tmpl index 1e11bc5..522f7bd 100644 --- a/docs/mainpage.doxygen.tmpl +++ b/docs/mainpage.doxygen.tmpl @@ -96,6 +96,8 @@ any data = parse_yaml(yaml_str); - added the \c YamlRpcConnection class to the YamlRpcClient module - updated the DataStreamClient module for complex types and new internal RestClient API changes (issue 2073) - updated the DataStreamUtil module for complex types + - fixed a bug deserializing single-quoted strings; also serialized \c "!number" values will always include the tag to avoid + potential future ambiguity (issue 2343) @subsection yaml051 yaml Module Version 0.5.1 diff --git a/src/QoreYamlEmitter.cpp b/src/QoreYamlEmitter.cpp index 8e26cc7..821eba1 100644 --- a/src/QoreYamlEmitter.cpp +++ b/src/QoreYamlEmitter.cpp @@ -195,5 +195,6 @@ int QoreYamlEmitter::emitValue(const DateTime &d) { else d.format(str, "Z"); + // issue #2343: to avoid ambiguity with single quoted strings, we always use the tag here return emitScalar(str, YAML_TIMESTAMP_TAG); } diff --git a/src/QoreYamlParser.cpp b/src/QoreYamlParser.cpp index 7401e89..dfd140f 100644 --- a/src/QoreYamlParser.cpp +++ b/src/QoreYamlParser.cpp @@ -213,7 +213,8 @@ static QoreNumberNode* parseNumber(const char* val, size_t len) { } // FIXME: this is still capable of false positives -static AbstractQoreNode* try_parse_number(const char* val, size_t len) { +static AbstractQoreNode* try_parse_number(const char* val, size_t len, bool no_simple_numeric = false) { + //printd(5, "try_parse_number() val: \"%s\" len: %d\n", val, (int)len); bool sign = (*val == '-' || *val == '+'); // check for @inf@ and @nan@ @@ -235,7 +236,7 @@ static AbstractQoreNode* try_parse_number(const char* val, size_t len) { return new QoreNumberNode(val, prec); } } - return 0; + return nullptr; } const char* str = val + (int)sign; @@ -266,21 +267,21 @@ static AbstractQoreNode* try_parse_number(const char* val, size_t len) { od = false; if (*str == '.') { if (dp || e || pm) - return 0; + return nullptr; dp = true; } else if (*str == 'e' || *str == 'E') { if (e || pm) - return 0; + return nullptr; e = true; } else if (*str == '+' || *str == '-') { if (pm || !e) - return 0; + return nullptr; pm = true; } else - return 0; + return nullptr; ++str; } @@ -293,6 +294,8 @@ static AbstractQoreNode* try_parse_number(const char* val, size_t len) { || (len == 20 && sign && ((*val == '+' && strcmp(val, "+9223372036854775807") <= 0) || (*val == '-' && strcmp(val, "-9223372036854775808") <= 0))))) { + if (no_simple_numeric) + return nullptr; int64 iv = strtoll(val, 0, 10); errno = 0; assert(errno != ERANGE); @@ -304,7 +307,7 @@ static AbstractQoreNode* try_parse_number(const char* val, size_t len) { return new QoreNumberNode(val); } - return new QoreFloatNode(strtod(val, 0)); + return no_simple_numeric ? nullptr : new QoreFloatNode(strtod(val, 0)); } AbstractQoreNode* QoreYamlParser::parseScalar(bool favor_string) { @@ -316,11 +319,23 @@ AbstractQoreNode* QoreYamlParser::parseScalar(bool favor_string) { //printd(5, "QoreYamlParser::parseScalar() anchor=%s tag=%s value=%s len=%d plain_implicit=%d quoted_implicit=%d style=%d\n", event.data.scalar.anchor ? event.data.scalar.anchor : (yaml_char_t*)"n/a", event.data.scalar.tag ? event.data.scalar.tag : (yaml_char_t*)"n/a", val, len, event.data.scalar.plain_implicit, event.data.scalar.quoted_implicit, event.data.scalar.style); if (!event.data.scalar.tag) { - if (favor_string || (event.data.scalar.quoted_implicit && event.data.scalar.style == YAML_DOUBLE_QUOTED_SCALAR_STYLE)) { + if (favor_string || (event.data.scalar.quoted_implicit + && (event.data.scalar.style == YAML_DOUBLE_QUOTED_SCALAR_STYLE))) { // assume it's a string return new QoreStringNode(val, len, QCS_UTF8); } + // issue #2343: could be a string, arbitrary-precision numeric or special floating-point value, or an ISO-8601 date/time value + if (event.data.scalar.quoted_implicit && event.data.scalar.style == YAML_SINGLE_QUOTED_SCALAR_STYLE) { + if (len > 9 && isdigit(val[0]) && isdigit(val[1]) && isdigit(val[2]) && isdigit(val[3]) && val[4] == '-' + && isdigit(val[5]) && isdigit(val[6]) && val[7] == '-' + && isdigit(val[8]) && isdigit(val[9])) + return parseAbsoluteDate(); + // try + AbstractQoreNode* n = try_parse_number(val, len, true); + return n ? n : new QoreStringNode(val, len, QCS_UTF8); + } + // check for boolean values if (!strcmp(val, "true")) return &True; @@ -329,14 +344,16 @@ AbstractQoreNode* QoreYamlParser::parseScalar(bool favor_string) { // check for null if (!strcmp(val, "null") || !strcmp(val, "~") || !len) - return 0; + return nullptr; // check for sqlnull if (!strcmp(val, "sqlnull")) return &Null; // check for absolute date/time values - if (isdigit(val[0]) && isdigit(val[1]) && isdigit(val[2]) && isdigit(val[3]) && val[4] == '-') + if (len > 9 && isdigit(val[0]) && isdigit(val[1]) && isdigit(val[2]) && isdigit(val[3]) && val[4] == '-' + && isdigit(val[5]) && isdigit(val[6]) && val[7] == '-' + && isdigit(val[8]) && isdigit(val[9])) return parseAbsoluteDate(); // check for relative date/time values (durations) @@ -363,6 +380,7 @@ AbstractQoreNode* QoreYamlParser::parseScalar(bool favor_string) { } const char* tag = (const char*)event.data.scalar.tag; + // FIXME: use a map here for O(ln(n)) performance if (!strcmp(tag, YAML_TIMESTAMP_TAG)) return parseAbsoluteDate(); if (!strcmp(tag, YAML_BINARY_TAG)) @@ -370,7 +388,7 @@ AbstractQoreNode* QoreYamlParser::parseScalar(bool favor_string) { if (!strcmp(tag, YAML_STR_TAG)) return new QoreStringNode(val, len, QCS_UTF8); if (!strcmp(tag, YAML_NULL_TAG)) - return 0; + return nullptr; if (!strcmp(tag, YAML_BOOL_TAG)) return parseBool(); if (!strcmp(tag, YAML_INT_TAG)) @@ -381,10 +399,12 @@ AbstractQoreNode* QoreYamlParser::parseScalar(bool favor_string) { return new DateTimeNode(val); if (!strcmp(tag, QORE_YAML_NUMBER_TAG)) return parseNumber(val, len); + if (!strcmp(tag, QORE_YAML_SQLNULL_TAG)) + return &Null; xsink->raiseException(QY_PARSE_ERR, "don't know how to parse scalar tag '%s'", tag); - return 0; + return nullptr; } QoreBoolNode* QoreYamlParser::parseBool() { diff --git a/src/yaml-module.h b/src/yaml-module.h index 867826a..1ceb503 100644 --- a/src/yaml-module.h +++ b/src/yaml-module.h @@ -211,22 +211,22 @@ class QoreYamlEmitter : public QoreYamlBase { return emit("map end"); } - DLLLOCAL int emitScalar(const QoreString &value, const char *tag, const char *anchor = 0, + DLLLOCAL int emitScalar(const QoreString &value, const char *tag, const char *anchor = nullptr, bool plain_implicit = true, bool quoted_implicit = true, yaml_scalar_style_t style = YAML_ANY_SCALAR_STYLE) { TempEncodingHelper str(&value, QCS_UTF8, xsink); if (*xsink) return -1; - if (!yaml_scalar_event_initialize(&event, (yaml_char_t *)anchor, (yaml_char_t *)tag, - (yaml_char_t *)str->getBuffer(), str->strlen(), + if (!yaml_scalar_event_initialize(&event, (yaml_char_t*)anchor, (yaml_char_t *)tag, + (yaml_char_t*)str->c_str(), str->strlen(), plain_implicit, quoted_implicit, style)) return err("unknown error initializing yaml scalar output event for yaml type '%s', value '%s'", tag, value.getBuffer()); return emit("scalar", tag); } - DLLLOCAL int emitScalar(const char *value, const char *tag, const char *anchor = 0, + DLLLOCAL int emitScalar(const char *value, const char *tag, const char *anchor = nullptr, bool plain_implicit = true, bool quoted_implicit = true, yaml_scalar_style_t style = YAML_ANY_SCALAR_STYLE) { if (!yaml_scalar_event_initialize(&event, (yaml_char_t *)anchor, (yaml_char_t *)tag, @@ -283,7 +283,8 @@ class QoreYamlEmitter : public QoreYamlBase { // append precision tmp.sprintf("{%d}", n.getPrec()); //printd(5, "yaml emit number: %s\n", tmp.getBuffer()); - return emitScalar(tmp, QORE_YAML_NUMBER_TAG); + // issue #2343: to avoid ambiguity with single quoted strings, we always use the tag here + return emitScalar(tmp, QORE_YAML_NUMBER_TAG, nullptr, false, false); } DLLLOCAL int emitValue(bool b) { diff --git a/test/yaml.qtest b/test/yaml.qtest index 7e6f527..ad07b64 100755 --- a/test/yaml.qtest +++ b/test/yaml.qtest @@ -38,6 +38,8 @@ const DATA = ( -@inf@n, @inf@, -@inf@, + "'single quoted string'", + "'1234'", ); const DATA2 = ( @@ -64,6 +66,7 @@ public class Main inherits QUnit::Test { addTestCase("Len test", \testLen()); addTestCase("Date/Time test", \testDates()); addTestCase("Structure with NaN test", \testNanStructure()); + addTestCase("single quoted strings", \testSingleQuotedStrings()); addTestCase("sql null test", \sqlNull()); # Return for compatibility with test harness that checks return value. @@ -71,9 +74,7 @@ public class Main inherits QUnit::Test { } testYaml() { - string s = make_yaml(DATA); - any l = parse_yaml(s); - testAssertion("test simple yaml", \equals(), (DATA, l)); + assertEq(DATA, parse_yaml(make_yaml(DATA))); } testOptions() { @@ -122,6 +123,13 @@ public class Main inherits QUnit::Test { assertEq(DATA2[4], l[4]); } + testSingleQuotedStrings() { + # issue 2343 + assertEq("1234", parse_yaml("'1234'")); + assertEq(500n, parse_yaml("'5e+02n{128}'")); + assertEq(2017-10-26T09:37:36.119613+02:00, parse_yaml("'2017-10-26 09:37:36.119613 +02:00'")); + } + sqlNull() { string yaml = make_yaml(NULL, EmitSqlNull); assertRegex("^sqlnull", yaml);