Skip to content

Commit 50057c0

Browse files
authored
Throw FormatException for recursive collections (#2376)
Closes #2375, closes #2373 The semantics of Maps with recursive keys are defined in YAML but incompatible with Dart. In YAML a key which is itself a map uses deep/structural equality for it's identity. In Dart a key used in a Map must have a temporally consistent `hashCode`, but at the moment a map is inserted into itself as a key that hashCode would need to change. Since we cannot cleanly handle the full semantics, and since we do not need the ability to recursively nest collections for any Dart or Flutter uses of the package, we prefer to not support them at all than to support them with subtly inconsistent semantics.
1 parent 3edb61f commit 50057c0

3 files changed

Lines changed: 45 additions & 26 deletions

File tree

pkgs/yaml/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
## 3.1.4-wip
22

3-
* Fix a stack overflow in `deepHashCode` when handling self-referential lists.
3+
* Throw a `FormatException` when parsing self-referential collections instead of
4+
a `StackOverflow`. Yaml definitions with collections nested within themselves
5+
are unsupported.
46

57
## 3.1.3
68

pkgs/yaml/lib/src/loader.dart

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ class Loader {
2929
/// Aliases by the alias name.
3030
final _aliases = <String, YamlNode>{};
3131

32+
/// Anchors that are currently being resolved.
33+
final _activeAnchors = <String>{};
34+
3235
/// The span of the entire stream emitted so far.
3336
FileSpan get span => _span;
3437
FileSpan _span;
@@ -103,7 +106,13 @@ class Loader {
103106
/// Composes a node corresponding to an alias.
104107
YamlNode _loadAlias(AliasEvent event) {
105108
var alias = _aliases[event.name];
106-
if (alias != null) return alias;
109+
if (alias != null) {
110+
if (_activeAnchors.contains(event.name)) {
111+
throw YamlException(
112+
'Self-referential collections are not supported.', event.span);
113+
}
114+
return alias;
115+
}
107116

108117
throw YamlException('Undefined alias.', event.span);
109118
}
@@ -135,10 +144,16 @@ class Loader {
135144
var node = YamlList.internal(children, firstEvent.span, firstEvent.style);
136145
_registerAnchor(firstEvent.anchor, node);
137146

138-
var event = _parser.parse();
139-
while (event.type != EventType.sequenceEnd) {
140-
children.add(_loadNode(event));
147+
Event event;
148+
try {
149+
if (firstEvent.anchor case final anchor?) _activeAnchors.add(anchor);
141150
event = _parser.parse();
151+
while (event.type != EventType.sequenceEnd) {
152+
children.add(_loadNode(event));
153+
event = _parser.parse();
154+
}
155+
} finally {
156+
if (firstEvent.anchor case final anchor?) _activeAnchors.remove(anchor);
142157
}
143158

144159
setSpan(node, firstEvent.span.expand(event.span));
@@ -157,16 +172,22 @@ class Loader {
157172
var node = YamlMap.internal(children, firstEvent.span, firstEvent.style);
158173
_registerAnchor(firstEvent.anchor, node);
159174

160-
var event = _parser.parse();
161-
while (event.type != EventType.mappingEnd) {
162-
var key = _loadNode(event);
163-
var value = _loadNode(_parser.parse());
164-
if (children.containsKey(key)) {
165-
throw YamlException('Duplicate mapping key.', key.span);
166-
}
167-
168-
children[key] = value;
175+
Event event;
176+
try {
177+
if (firstEvent.anchor case final anchor?) _activeAnchors.add(anchor);
169178
event = _parser.parse();
179+
while (event.type != EventType.mappingEnd) {
180+
var key = _loadNode(event);
181+
var value = _loadNode(_parser.parse());
182+
if (children.containsKey(key)) {
183+
throw YamlException('Duplicate mapping key.', key.span);
184+
}
185+
186+
children[key] = value;
187+
event = _parser.parse();
188+
}
189+
} finally {
190+
if (firstEvent.anchor case final anchor?) _activeAnchors.remove(anchor);
170191
}
171192

172193
setSpan(node, firstEvent.span.expand(event.span));

pkgs/yaml/test/yaml_test.dart

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,19 +1122,15 @@ void main() {
11221122
expect(anchorList, same(aliasList));
11231123
});
11241124

1125-
test('self-referential list does not cause stack overflow in deepHashCode',
1126-
() {
1127-
var doc = loadYaml(cleanUpLiteral('''
1125+
test('self-referential list throws FormatException', () {
1126+
expect(() => loadYaml(cleanUpLiteral('''
11281127
? &anchor [*anchor]
1129-
: value'''));
1130-
expect(doc, isNotNull);
1131-
var key = doc.keys.first;
1132-
expect(key, isA<YamlList>());
1133-
expect(key[0], same(key));
1134-
1135-
var map = deepEqualsMap();
1136-
map[key] = 'value';
1137-
expect(map[key], 'value');
1128+
: value''')), throwsA(isA<FormatException>()));
1129+
});
1130+
1131+
test('self-referential map throws FormatException', () {
1132+
expect(() => loadYaml('&map { *map : *map }'),
1133+
throwsA(isA<FormatException>()));
11381134
});
11391135

11401136
test('[Example 7.1]', () {

0 commit comments

Comments
 (0)