diff --git a/generated/google_cloud_protobuf/lib/src/protobuf.p.dart b/generated/google_cloud_protobuf/lib/src/protobuf.p.dart index 4bcf5ecb..f13c3306 100644 --- a/generated/google_cloud_protobuf/lib/src/protobuf.p.dart +++ b/generated/google_cloud_protobuf/lib/src/protobuf.p.dart @@ -199,26 +199,7 @@ class Value extends ProtoMessage { this.listValue, }) : super(fullyQualifiedName); - factory Value.fromJson(Object? json) { - switch (json) { - case null: - return Value(nullValue: NullValue.nullValue); - case double d: - return Value(numberValue: d); - case int i: - return Value(numberValue: i.toDouble()); - case String s: - return Value(stringValue: s); - case bool b: - return Value(boolValue: b); - case List l: - return Value(listValue: ListValue.fromJson(l)); - case Map m: - return Value(structValue: Struct.fromJson(m)); - default: - throw FormatException('Invalid Value: $json'); - } - } + factory Value.fromJson(Object? json) => _decodeValue(json, Set.identity()); @override Object? toJson() => @@ -549,21 +530,76 @@ class _StructHelper { static Map encode(Struct value) => value.fields.map((key, value) => MapEntry(key, value.toJson())); - static Struct decode(Object? value) { - final fields = (value as Map).map( - (key, value) => MapEntry(key, Value.fromJson(value)), - ); - return Struct(fields: fields); - } + static Struct decode(Object? value) => _decodeStruct(value, Set.identity()); } class _ListValueHelper { static List encode(ListValue value) => value.values.map((v) => v.toJson()).toList(); - static ListValue decode(Object? value) { - final values = (value as List).map(Value.fromJson).toList(); - return ListValue(values: values); + static ListValue decode(Object? value) => + _decodeListValue(value, Set.identity()); +} + +// `Value`, `Struct` and `ListValue` decode into each other, so the decoding +// functions below thread [parents] — the containers currently being decoded — +// through the recursion in order to reject cycles. +// +// `jsonDecode` cannot produce a cyclic structure, but a hand-built Dart +// collection can be cyclic, which would otherwise recurse until the stack +// overflows. + +/// Decodes [json] into a [Value]. +Value _decodeValue(Object? json, Set parents) { + switch (json) { + case null: + return Value(nullValue: NullValue.nullValue); + case double d: + return Value(numberValue: d); + case int i: + return Value(numberValue: i.toDouble()); + case String s: + return Value(stringValue: s); + case bool b: + return Value(boolValue: b); + case List l: + return Value(listValue: _decodeListValue(l, parents)); + case Map m: + return Value(structValue: _decodeStruct(m, parents)); + default: + throw FormatException('Invalid Value: $json'); + } +} + +/// Decodes [json] into a [Struct]. +Struct _decodeStruct(Object? json, Set parents) { + final map = json as Map; + if (!parents.add(map)) { + throw FormatException('Cyclic reference in Struct: $json'); + } + try { + return Struct( + fields: map.map( + (key, value) => MapEntry(key, _decodeValue(value, parents)), + ), + ); + } finally { + parents.remove(map); + } +} + +/// Decodes [json] into a [ListValue]. +ListValue _decodeListValue(Object? json, Set parents) { + final list = json as List; + if (!parents.add(list)) { + throw FormatException('Cyclic reference in ListValue: $json'); + } + try { + return ListValue( + values: [for (final value in list) _decodeValue(value, parents)], + ); + } finally { + parents.remove(list); } } diff --git a/generated/google_cloud_protobuf/test/struct_test.dart b/generated/google_cloud_protobuf/test/struct_test.dart index d81062b0..60aa52b7 100644 --- a/generated/google_cloud_protobuf/test/struct_test.dart +++ b/generated/google_cloud_protobuf/test/struct_test.dart @@ -115,4 +115,18 @@ void main() { expect(actual, ['foo', 3.0, false, true, 3.14, null]); }); }); + + test('Struct.fromJson rejects a cyclic map', () { + final map = {}; + map['x'] = map; + + expect(() => Struct.fromJson(map), throwsFormatException); + }); + + test('ListValue.fromJson rejects a cyclic list', () { + final list = []; + list.add(list); + + expect(() => ListValue.fromJson(list), throwsFormatException); + }); } diff --git a/generated/google_cloud_protobuf/test/value_test.dart b/generated/google_cloud_protobuf/test/value_test.dart index 48a43171..6e54bcf7 100644 --- a/generated/google_cloud_protobuf/test/value_test.dart +++ b/generated/google_cloud_protobuf/test/value_test.dart @@ -106,4 +106,67 @@ void main() { test('decode invalid', () { expect(() => Value.fromJson(DateTime.now()), throwsFormatException); }); + + group('decode cyclic', () { + test('map referencing itself', () { + final map = {}; + map['x'] = map; + + expect(() => Value.fromJson(map), throwsFormatException); + }); + + test('list containing itself', () { + final list = []; + list.add(list); + + expect(() => Value.fromJson(list), throwsFormatException); + }); + + test('map and list referencing each other', () { + final map = {}; + final list = [map]; + map['list'] = list; + + expect(() => Value.fromJson(map), throwsFormatException); + expect(() => Value.fromJson(list), throwsFormatException); + }); + + test('map nested below the root', () { + final inner = {}; + inner['self'] = inner; + + expect( + () => Value.fromJson({ + 'a': [ + {'b': inner}, + ], + }), + throwsFormatException, + ); + }); + }); + + group('decode repeated but acyclic', () { + test('map referenced by two keys', () { + final shared = {'a': 1}; + + final value = Value.fromJson({'x': shared, 'y': shared}); + + expect(value.toJson(), { + 'x': {'a': 1.0}, + 'y': {'a': 1.0}, + }); + }); + + test('list referenced by two elements', () { + final shared = [1, 2]; + + final value = Value.fromJson([shared, shared]); + + expect(value.toJson(), [ + [1.0, 2.0], + [1.0, 2.0], + ]); + }); + }); }