diff --git a/source/dyaml/composer.d b/source/dyaml/composer.d index c000b02..e7b083a 100644 --- a/source/dyaml/composer.d +++ b/source/dyaml/composer.d @@ -16,6 +16,7 @@ import std.array; import std.conv; import std.exception; +import std.format; import std.range; import std.typecons; @@ -357,12 +358,18 @@ merge(*pairAppender, flatten(node[0], startEvent.startMark, node[1], pairAppenderLevel + 1, nodeAppenderLevel)); } - auto numUnique = pairAppender.data.dup - .sort!((x,y) => x.key > y.key) - .uniq!((x,y) => x.key == y.key) - .walkLength; - enforce(numUnique == pairAppender.data.length, - new ComposerException("Duplicate key found in mapping", parser_.front.startMark)); + + auto sorted = pairAppender.data.dup.sort!((x,y) => x.key > y.key); + if (sorted.length) { + foreach (index, const ref value; sorted[0 .. $ - 1].enumerate) + if (value.key == sorted[index + 1].key) { + const message = () @trusted { + return format("Key '%s' appears multiple times in mapping (first: %s)", + value.key.get!string, value.key.startMark); + }(); + throw new ComposerException(message, sorted[index + 1].key.startMark); + } + } Node node = constructNode(startEvent.startMark, parser_.front.endMark, tag, pairAppender.data.dup); @@ -373,3 +380,22 @@ return node; } } + +// Provide good error message on multiple keys (which JSON supports) +@safe unittest +{ + import dyaml.loader : Loader; + + const str = `{ + "comment": "This is a common technique", + "name": "foobar", + "comment": "To write down comments pre-JSON5" +}`; + + try + auto node = Loader.fromString(str).load(); + catch (ComposerException exc) + assert(exc.message() == + "Key 'comment' appears multiple times in mapping " ~ + "(first: file ,line 2,column 5)\nfile ,line 4,column 5"); +} diff --git a/source/dyaml/emitter.d b/source/dyaml/emitter.d index d8e016c..5aafc0e 100644 --- a/source/dyaml/emitter.d +++ b/source/dyaml/emitter.d @@ -29,6 +29,7 @@ import dyaml.exception; import dyaml.linebreak; import dyaml.queue; +import dyaml.scanner; import dyaml.style; import dyaml.tagdirective; @@ -949,7 +950,7 @@ ///Prepare anchor for output. static string prepareAnchor(const string anchor) @safe in(anchor != "", "Anchor must not be empty") - in(anchor.all!(c => isAlphaNum(c) || c.among!('-', '_')), "Anchor contains invalid characters") + in(anchor.all!isNSAnchorName, "Anchor contains invalid characters") { return anchor; } diff --git a/source/dyaml/escapes.d b/source/dyaml/escapes.d index fa70eb1..36fd744 100644 --- a/source/dyaml/escapes.d +++ b/source/dyaml/escapes.d @@ -31,8 +31,8 @@ case 'f': return '\x0C'; case 'r': return '\x0D'; case 'e': return '\x1B'; - case ' ': return '\x20'; case '/': return '/'; + case ' ': return '\x20'; case '\"': return '\"'; case '\\': return '\\'; case 'N': return '\x85'; //'\u0085'; diff --git a/source/dyaml/loader.d b/source/dyaml/loader.d index 09c19db..6638dfc 100644 --- a/source/dyaml/loader.d +++ b/source/dyaml/loader.d @@ -82,8 +82,10 @@ /** Construct a Loader to load YAML from a string. * - * Params: data = String to load YAML from. The char[] version $(B will) - * overwrite its input during parsing as D:YAML reuses memory. + * Params: + * data = String to load YAML from. The char[] version $(B will) + * overwrite its input during parsing as D:YAML reuses memory. + * filename = The filename to give to the Loader, defaults to `""` * * Returns: Loader loading YAML from given string. * @@ -91,14 +93,14 @@ * * YAMLException if data could not be read (e.g. a decoding error) */ - static Loader fromString(char[] data) @safe + static Loader fromString(char[] data, string filename = "") @safe { - return Loader(cast(ubyte[])data); + return Loader(cast(ubyte[])data, filename); } /// Ditto - static Loader fromString(string data) @safe + static Loader fromString(string data, string filename = "") @safe { - return fromString(data.dup); + return fromString(data.dup, filename); } /// Load a char[]. @safe unittest diff --git a/source/dyaml/scanner.d b/source/dyaml/scanner.d index 3f0f394..17893d1 100644 --- a/source/dyaml/scanner.d +++ b/source/dyaml/scanner.d @@ -72,6 +72,8 @@ alias isFlowScalarBreakSpace = among!(' ', '\t', '\0', '\n', '\r', '\u0085', '\u2028', '\u2029', '\'', '"', '\\'); +alias isNSAnchorName = c => !c.isWhiteSpace && !c.among!('[', ']', '{', '}', ',', '\uFEFF'); + /// Marked exception thrown at scanner errors. /// /// See_Also: MarkedYAMLException @@ -763,6 +765,25 @@ reader_.sliceBuilder.write(reader_.get(length)); } + /// Scan a string. + /// + /// Assumes that the caller is building a slice in Reader, and puts the scanned + /// characters into that slice. + void scanAnchorAliasToSlice(const Mark startMark) @safe + { + size_t length; + dchar c = reader_.peek(); + while (c.isNSAnchorName) + { + c = reader_.peek(++length); + } + + enforce(length > 0, new ScannerException("While scanning an anchor or alias", + startMark, expected("a printable character besides '[', ']', '{', '}' and ','", c), reader_.mark)); + + reader_.sliceBuilder.write(reader_.get(length)); + } + /// Scan and throw away all characters until next line break. void scanToNextBreak() @safe { @@ -988,20 +1009,14 @@ Token scanAnchor(const TokenID id) @safe { const startMark = reader_.mark; - const dchar i = reader_.get(); + reader_.forward(); // The */& character was only peeked, so we drop it now reader_.sliceBuilder.begin(); - if(i == '*') { scanAlphaNumericToSlice!"an alias"(startMark); } - else { scanAlphaNumericToSlice!"an anchor"(startMark); } + scanAnchorAliasToSlice(startMark); // On error, value is discarded as we return immediately char[] value = reader_.sliceBuilder.finish(); - enum anchorCtx = "While scanning an anchor"; - enum aliasCtx = "While scanning an alias"; - enforce(reader_.peek().isWhiteSpace || - reader_.peekByte().among!('?', ':', ',', ']', '}', '%', '@'), - new ScannerException(i == '*' ? aliasCtx : anchorCtx, startMark, - expected("alphanumeric, '-' or '_'", reader_.peek()), reader_.mark)); + assert(!reader_.peek().isNSAnchorName, "Anchor/alias name not fully scanned"); if(id == TokenID.alias_) {