From 8b08294271ee2479a86b7a94d17a802f09f8e96f Mon Sep 17 00:00:00 2001 From: Stanislav Gatev Date: Mon, 1 May 2023 19:03:24 +0200 Subject: [PATCH] Add methods that construct read-only messages This patch adds the following methods to `GeneratedMessage`: * `mergeFromBufferFrozen` * `mergeFromCodedBufferReaderFrozen` * `mergeFromJsonFrozen` * `mergeFromJsonMapFrozen` All of them add to the current message by deserializing the provided data and mark the new parts of the message (including the message itself) as read-only. Note that sub-messages that are already part of the current message are not marked as read-only. Closes #627 --- protobuf/lib/meta.dart | 4 + protobuf/lib/src/protobuf/coded_buffer.dart | 87 +++-- .../lib/src/protobuf/generated_message.dart | 86 ++++- protobuf/lib/src/protobuf/json.dart | 21 +- protobuf/lib/src/protobuf/pb_map.dart | 3 +- protobuf/test/readonly_message_test.dart | 332 ++++++++++++------ 6 files changed, 392 insertions(+), 141 deletions(-) diff --git a/protobuf/lib/meta.dart b/protobuf/lib/meta.dart index 519bbd052..e69e54554 100644 --- a/protobuf/lib/meta.dart +++ b/protobuf/lib/meta.dart @@ -39,9 +39,13 @@ const GeneratedMessage_reservedNames = [ 'isFrozen', 'isInitialized', 'mergeFromBuffer', + 'mergeFromBufferFrozen', 'mergeFromCodedBufferReader', + 'mergeFromCodedBufferReaderFrozen', 'mergeFromJson', + 'mergeFromJsonFrozen', 'mergeFromJsonMap', + 'mergeFromJsonMapFrozen', 'mergeFromMessage', 'mergeFromProto3Json', 'mergeUnknownFields', diff --git a/protobuf/lib/src/protobuf/coded_buffer.dart b/protobuf/lib/src/protobuf/coded_buffer.dart index 76d95edc6..5090814c9 100644 --- a/protobuf/lib/src/protobuf/coded_buffer.dart +++ b/protobuf/lib/src/protobuf/coded_buffer.dart @@ -30,12 +30,15 @@ void _writeToCodedBufferWriter(_FieldSet fs, CodedBufferWriter out) { } void _mergeFromCodedBufferReader(BuilderInfo meta, _FieldSet fs, - CodedBufferReader input, ExtensionRegistry registry) { + CodedBufferReader input, ExtensionRegistry registry, bool frozen) { ArgumentError.checkNotNull(registry); fs._ensureWritable(); while (true) { var tag = input.readTag(); - if (tag == 0) return; + if (tag == 0) { + fs._frozenState = frozen; + return; + } var wireType = tag & 0x7; var tagNumber = tag >> 3; @@ -44,6 +47,7 @@ void _mergeFromCodedBufferReader(BuilderInfo meta, _FieldSet fs, if (fi == null || !_wireTypeMatches(fi.type, wireType)) { if (!fs._ensureUnknownFields().mergeFieldFromBuffer(tag, input)) { + fs._frozenState = frozen; return; } continue; @@ -128,83 +132,100 @@ void _mergeFromCodedBufferReader(BuilderInfo meta, _FieldSet fs, } break; case PbFieldType._REPEATED_BOOL: - _readPackable(meta, fs, input, wireType, fi, input.readBool); + _readPackable(meta, fs, input, wireType, fi, input.readBool, frozen); break; case PbFieldType._REPEATED_BYTES: - fs - ._ensureRepeatedField(meta, fi) - .add(Uint8List.fromList(input.readBytes())); + var list = fs._ensureRepeatedField(meta, fi); + list.add(Uint8List.fromList(input.readBytes())); + if (list is PbList) { + list._isReadOnly = frozen; + } break; case PbFieldType._REPEATED_STRING: - fs._ensureRepeatedField(meta, fi).add(input.readString()); + var list = fs._ensureRepeatedField(meta, fi); + list.add(input.readString()); + if (list is PbList) { + list._isReadOnly = frozen; + } break; case PbFieldType._REPEATED_FLOAT: - _readPackable(meta, fs, input, wireType, fi, input.readFloat); + _readPackable(meta, fs, input, wireType, fi, input.readFloat, frozen); break; case PbFieldType._REPEATED_DOUBLE: - _readPackable(meta, fs, input, wireType, fi, input.readDouble); + _readPackable(meta, fs, input, wireType, fi, input.readDouble, frozen); break; case PbFieldType._REPEATED_ENUM: _readPackableToListEnum( - meta, fs, input, wireType, fi, tagNumber, registry); + meta, fs, input, wireType, fi, tagNumber, registry, frozen); break; case PbFieldType._REPEATED_GROUP: var subMessage = meta._makeEmptyMessage(tagNumber, registry); input.readGroup(tagNumber, subMessage, registry); - fs._ensureRepeatedField(meta, fi).add(subMessage); + var list = fs._ensureRepeatedField(meta, fi); + list.add(subMessage); + if (list is PbList) { + list._isReadOnly = frozen; + } break; case PbFieldType._REPEATED_INT32: - _readPackable(meta, fs, input, wireType, fi, input.readInt32); + _readPackable(meta, fs, input, wireType, fi, input.readInt32, frozen); break; case PbFieldType._REPEATED_INT64: - _readPackable(meta, fs, input, wireType, fi, input.readInt64); + _readPackable(meta, fs, input, wireType, fi, input.readInt64, frozen); break; case PbFieldType._REPEATED_SINT32: - _readPackable(meta, fs, input, wireType, fi, input.readSint32); + _readPackable(meta, fs, input, wireType, fi, input.readSint32, frozen); break; case PbFieldType._REPEATED_SINT64: - _readPackable(meta, fs, input, wireType, fi, input.readSint64); + _readPackable(meta, fs, input, wireType, fi, input.readSint64, frozen); break; case PbFieldType._REPEATED_UINT32: - _readPackable(meta, fs, input, wireType, fi, input.readUint32); + _readPackable(meta, fs, input, wireType, fi, input.readUint32, frozen); break; case PbFieldType._REPEATED_UINT64: - _readPackable(meta, fs, input, wireType, fi, input.readUint64); + _readPackable(meta, fs, input, wireType, fi, input.readUint64, frozen); break; case PbFieldType._REPEATED_FIXED32: - _readPackable(meta, fs, input, wireType, fi, input.readFixed32); + _readPackable(meta, fs, input, wireType, fi, input.readFixed32, frozen); break; case PbFieldType._REPEATED_FIXED64: - _readPackable(meta, fs, input, wireType, fi, input.readFixed64); + _readPackable(meta, fs, input, wireType, fi, input.readFixed64, frozen); break; case PbFieldType._REPEATED_SFIXED32: - _readPackable(meta, fs, input, wireType, fi, input.readSfixed32); + _readPackable( + meta, fs, input, wireType, fi, input.readSfixed32, frozen); break; case PbFieldType._REPEATED_SFIXED64: - _readPackable(meta, fs, input, wireType, fi, input.readSfixed64); + _readPackable( + meta, fs, input, wireType, fi, input.readSfixed64, frozen); break; case PbFieldType._REPEATED_MESSAGE: var subMessage = meta._makeEmptyMessage(tagNumber, registry); input.readMessage(subMessage, registry); - fs._ensureRepeatedField(meta, fi).add(subMessage); + var list = fs._ensureRepeatedField(meta, fi); + list.add(subMessage); + if (list is PbList) { + list._isReadOnly = frozen; + } break; case PbFieldType._MAP: final mapFieldInfo = fi as MapFieldInfo; final mapEntryMeta = mapFieldInfo.mapEntryBuilderInfo; - fs - ._ensureMapField(meta, mapFieldInfo) - ._mergeEntry(mapEntryMeta, input, registry); + var map = fs._ensureMapField(meta, mapFieldInfo); + map._mergeEntry(mapEntryMeta, input, registry); + map._isReadonly = frozen; break; default: throw 'Unknown field type $fieldType'; } } + fs._frozenState = frozen; } void _readPackable(BuilderInfo meta, _FieldSet fs, CodedBufferReader input, - int wireType, FieldInfo fi, Function() readFunc) { + int wireType, FieldInfo fi, Function() readFunc, bool frozen) { void readToList(List list) => list.add(readFunc()); - _readPackableToList(meta, fs, input, wireType, fi, readToList); + _readPackableToList(meta, fs, input, wireType, fi, readToList, frozen); } void _readPackableToListEnum( @@ -214,7 +235,8 @@ void _readPackableToListEnum( int wireType, FieldInfo fi, int tagNumber, - ExtensionRegistry registry) { + ExtensionRegistry registry, + bool frozen) { void readToList(List list) { var rawValue = input.readEnum(); var value = meta._decodeEnum(tagNumber, registry, rawValue); @@ -226,7 +248,7 @@ void _readPackableToListEnum( } } - _readPackableToList(meta, fs, input, wireType, fi, readToList); + _readPackableToList(meta, fs, input, wireType, fi, readToList, frozen); } void _readPackableToList( @@ -235,7 +257,8 @@ void _readPackableToList( CodedBufferReader input, int wireType, FieldInfo fi, - Function(List) readToList) { + Function(List) readToList, + bool frozen) { var list = fs._ensureRepeatedField(meta, fi); if (wireType == WIRETYPE_LENGTH_DELIMITED) { @@ -249,4 +272,8 @@ void _readPackableToList( // Not packed. readToList(list); } + + if (list is PbList) { + list._isReadOnly = frozen; + } } diff --git a/protobuf/lib/src/protobuf/generated_message.dart b/protobuf/lib/src/protobuf/generated_message.dart index d30f9de9c..2d8dfd37e 100644 --- a/protobuf/lib/src/protobuf/generated_message.dart +++ b/protobuf/lib/src/protobuf/generated_message.dart @@ -171,10 +171,37 @@ abstract class GeneratedMessage { void writeToCodedBufferWriter(CodedBufferWriter output) => _writeToCodedBufferWriter(_fieldSet, output); + /// Merges serialized protocol buffer data into this message. + /// + /// For each field in [input] that is already present in this message: + /// + /// * If it's a repeated field, this appends to the end of our list. + /// * Else, if it's a scalar, this overwrites our field. + /// * Else, (it's a non-repeated sub-message), this recursively merges into + /// the existing sub-message. void mergeFromCodedBufferReader(CodedBufferReader input, [ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY]) { final meta = _fieldSet._meta; - _mergeFromCodedBufferReader(meta, _fieldSet, input, extensionRegistry); + _mergeFromCodedBufferReader( + meta, _fieldSet, input, extensionRegistry, false); + } + + /// Merges serialized protocol buffer data into this message. + /// + /// For each field in [input] that is already present in this message: + /// + /// * If it's a repeated field, this appends to the end of our list. + /// * Else, if it's a scalar, this overwrites our field. + /// * Else, (it's a non-repeated sub-message), this recursively merges into + /// the existing sub-message. + /// + /// NOTE: Sub-messages that are already part of this message are not marked as + /// read-only. + void mergeFromCodedBufferReaderFrozen(CodedBufferReader input, + [ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY]) { + final meta = _fieldSet._meta; + _mergeFromCodedBufferReader( + meta, _fieldSet, input, extensionRegistry, true); } /// Merges serialized protocol buffer data into this message. @@ -189,7 +216,28 @@ abstract class GeneratedMessage { [ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY]) { var codedInput = CodedBufferReader(input); final meta = _fieldSet._meta; - _mergeFromCodedBufferReader(meta, _fieldSet, codedInput, extensionRegistry); + _mergeFromCodedBufferReader( + meta, _fieldSet, codedInput, extensionRegistry, false); + codedInput.checkLastTagWas(0); + } + + /// Merges serialized protocol buffer data into this message and makes it read-only. + /// + /// For each field in [input] that is already present in this message: + /// + /// * If it's a repeated field, this appends to the end of our list. + /// * Else, if it's a scalar, this overwrites our field. + /// * Else, (it's a non-repeated sub-message), this recursively merges into + /// the existing sub-message. + /// + /// NOTE: Sub-messages that are already part of this message are not marked as + /// read-only. + void mergeFromBufferFrozen(List input, + [ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY]) { + var codedInput = CodedBufferReader(input); + final meta = _fieldSet._meta; + _mergeFromCodedBufferReader( + meta, _fieldSet, codedInput, extensionRegistry, true); codedInput.checkLastTagWas(0); } @@ -279,7 +327,25 @@ abstract class GeneratedMessage { /// on the Dart VM for a slight speedup. final Map jsonMap = jsonDecode(data, reviver: _emptyReviver); - _mergeFromJsonMap(_fieldSet, jsonMap, extensionRegistry); + _mergeFromJsonMap(_fieldSet, jsonMap, extensionRegistry, false); + } + + /// Merges field values from [data], a JSON object, encoded as described by + /// [GeneratedMessage.writeToJson] and marks the message read-only. + /// + /// For the proto3 JSON format use: [mergeFromProto3Json]. + /// + /// NOTE: Sub-messages that are already part of this message are not marked as + /// read-only. + void mergeFromJsonFrozen(String data, + [ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY]) { + /// Disable lazy creation of Dart objects for a dart2js speedup. + /// This is a slight regression on the Dart VM. + /// TODO(skybrian) we could skip the reviver if we're running + /// on the Dart VM for a slight speedup. + final Map jsonMap = + jsonDecode(data, reviver: _emptyReviver); + _mergeFromJsonMap(_fieldSet, jsonMap, extensionRegistry, true); } static Object? _emptyReviver(Object? k, Object? v) => v; @@ -289,7 +355,19 @@ abstract class GeneratedMessage { /// The encoding is described in [GeneratedMessage.writeToJson]. void mergeFromJsonMap(Map json, [ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY]) { - _mergeFromJsonMap(_fieldSet, json, extensionRegistry); + _mergeFromJsonMap(_fieldSet, json, extensionRegistry, false); + } + + /// Merges field values from a JSON object represented as a Dart map + /// and marks the message read-only. + /// + /// The encoding is described in [GeneratedMessage.writeToJson]. + /// + /// NOTE: Sub-messages that are already part of this message are not marked as + /// read-only. + void mergeFromJsonMapFrozen(Map json, + [ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY]) { + _mergeFromJsonMap(_fieldSet, json, extensionRegistry, true); } /// Adds an extension field value to a repeated field. diff --git a/protobuf/lib/src/protobuf/json.dart b/protobuf/lib/src/protobuf/json.dart index 2ed29621a..ba1005698 100644 --- a/protobuf/lib/src/protobuf/json.dart +++ b/protobuf/lib/src/protobuf/json.dart @@ -94,8 +94,8 @@ Map _writeToJsonMap(_FieldSet fs) { // Merge fields from a previously decoded JSON object. // (Called recursively on nested messages.) -void _mergeFromJsonMap( - _FieldSet fs, Map json, ExtensionRegistry? registry) { +void _mergeFromJsonMap(_FieldSet fs, Map json, + ExtensionRegistry? registry, bool frozen) { fs._ensureWritable(); final keys = json.keys; final meta = fs._meta; @@ -107,18 +107,19 @@ void _mergeFromJsonMap( if (fi == null) continue; // Unknown tag; skip } if (fi.isMapField) { - _appendJsonMap( - meta, fs, json[key], fi as MapFieldInfo, registry); + _appendJsonMap(meta, fs, json[key], fi as MapFieldInfo, + registry, frozen); } else if (fi.isRepeated) { - _appendJsonList(meta, fs, json[key], fi, registry); + _appendJsonList(meta, fs, json[key], fi, registry, frozen); } else { _setJsonField(meta, fs, json[key], fi, registry); } } + fs._frozenState = frozen; } void _appendJsonList(BuilderInfo meta, _FieldSet fs, List jsonList, - FieldInfo fi, ExtensionRegistry? registry) { + FieldInfo fi, ExtensionRegistry? registry, bool frozen) { final repeated = fi._ensureRepeatedField(meta, fs); // Micro optimization. Using "for in" generates the following and iterator // alloc: @@ -134,10 +135,13 @@ void _appendJsonList(BuilderInfo meta, _FieldSet fs, List jsonList, convertedValue ??= fi.defaultEnumValue; repeated.add(convertedValue); } + if (repeated is PbList) { + repeated._isReadOnly = frozen; + } } void _appendJsonMap(BuilderInfo meta, _FieldSet fs, List jsonList, - MapFieldInfo fi, ExtensionRegistry? registry) { + MapFieldInfo fi, ExtensionRegistry? registry, bool frozen) { final entryMeta = fi.mapEntryBuilderInfo; final map = fi._ensureMapField(meta, fs) as PbMap; for (var jsonEntryDynamic in jsonList) { @@ -163,6 +167,7 @@ void _appendJsonMap(BuilderInfo meta, _FieldSet fs, List jsonList, convertedValue ??= fi.defaultEnumValue; map[convertedKey] = convertedValue; } + map._isReadonly = frozen; } void _setJsonField(BuilderInfo meta, _FieldSet fs, json, FieldInfo fi, @@ -279,7 +284,7 @@ dynamic _convertJsonValue(BuilderInfo meta, _FieldSet fs, value, int tagNumber, if (value is Map) { final messageValue = value as Map; var subMessage = meta._makeEmptyMessage(tagNumber, registry); - _mergeFromJsonMap(subMessage._fieldSet, messageValue, registry); + _mergeFromJsonMap(subMessage._fieldSet, messageValue, registry, false); return subMessage; } expectedType = 'nested message or group'; diff --git a/protobuf/lib/src/protobuf/pb_map.dart b/protobuf/lib/src/protobuf/pb_map.dart index 4ec9b3f53..60927ebb7 100644 --- a/protobuf/lib/src/protobuf/pb_map.dart +++ b/protobuf/lib/src/protobuf/pb_map.dart @@ -100,7 +100,8 @@ class PbMap extends MapBase { var oldLimit = input._currentLimit; input._currentLimit = input._bufferPos + length; final entryFieldSet = _FieldSet(null, mapEntryMeta, null); - _mergeFromCodedBufferReader(mapEntryMeta, entryFieldSet, input, registry); + _mergeFromCodedBufferReader( + mapEntryMeta, entryFieldSet, input, registry, false); input.checkLastTagWas(0); input._currentLimit = oldLimit; var key = diff --git a/protobuf/test/readonly_message_test.dart b/protobuf/test/readonly_message_test.dart index ef365b993..4435b3d55 100644 --- a/protobuf/test/readonly_message_test.dart +++ b/protobuf/test/readonly_message_test.dart @@ -3,7 +3,13 @@ // BSD-style license that can be found in the LICENSE file. import 'package:protobuf/protobuf.dart' - show BuilderInfo, GeneratedMessage, PbFieldType, UnknownFieldSetField; + show + BuilderInfo, + CodedBufferReader, + CodedBufferWriter, + GeneratedMessage, + PbFieldType, + UnknownFieldSetField; import 'package:test/test.dart'; Matcher throwsError(Type expectedType, Matcher expectedMessage) => @@ -47,62 +53,140 @@ class Rec extends GeneratedMessage { } } +T viaFreeze(T message) { + return message..freeze(); +} + +T viaMergeFromBufferFrozen(T message, T out) { + var bytes = message.writeToBuffer(); + out..mergeFromBufferFrozen(bytes); + return out; +} + +T viaMergeFromCodedBufferReaderFrozen( + T message, T out) { + var writer = CodedBufferWriter(); + message.writeToCodedBufferWriter(writer); + out..mergeFromCodedBufferReaderFrozen(CodedBufferReader(writer.toBuffer())); + return out; +} + +T viaMergeFromJsonFrozen(T message, T out) { + out..mergeFromJsonFrozen(message.writeToJson()); + return out; +} + +T viaMergeFromJsonMapFrozen(T message, T out) { + out..mergeFromJsonMapFrozen(message.writeToJsonMap()); + return out; +} + void main() { test('can write a read-only message', () { - expect(Rec.getDefault().writeToBuffer(), []); - expect(Rec.getDefault().writeToJson(), '{}'); + for (var create in [ + () => Rec.getDefault(), + () => viaFreeze(Rec()), + () => viaMergeFromBufferFrozen(Rec(), Rec()), + () => viaMergeFromCodedBufferReaderFrozen(Rec(), Rec()), + () => viaMergeFromJsonFrozen(Rec(), Rec()), + () => viaMergeFromJsonMapFrozen(Rec(), Rec()), + ]) { + expect(create().writeToBuffer(), []); + expect(create().writeToJson(), '{}'); + } }); test("can't merge to a read-only message", () { - expect( - () => Rec.getDefault().mergeFromJson('{"1":1}'), - throwsError(UnsupportedError, - equals('Attempted to change a read-only message (rec)'))); + for (var create in [ + () => Rec.getDefault(), + () => viaFreeze(Rec()), + () => viaMergeFromBufferFrozen(Rec(), Rec()), + () => viaMergeFromCodedBufferReaderFrozen(Rec(), Rec()), + () => viaMergeFromJsonFrozen(Rec(), Rec()), + () => viaMergeFromJsonMapFrozen(Rec(), Rec()), + ]) { + expect( + () => create().mergeFromJson('{"1":1}'), + throwsError(UnsupportedError, + equals('Attempted to change a read-only message (rec)'))); + } }); test("can't set a field on a read-only message", () { - expect( - () => Rec.getDefault().setField(1, 456), - throwsError(UnsupportedError, - equals('Attempted to change a read-only message (rec)'))); + for (var create in [ + () => Rec.getDefault(), + () => viaFreeze(Rec()), + () => viaMergeFromBufferFrozen(Rec(), Rec()), + () => viaMergeFromCodedBufferReaderFrozen(Rec(), Rec()), + () => viaMergeFromJsonFrozen(Rec(), Rec()), + () => viaMergeFromJsonMapFrozen(Rec(), Rec()), + ]) { + expect( + () => create().setField(1, 456), + throwsError(UnsupportedError, + equals('Attempted to change a read-only message (rec)'))); + } }); test("can't clear a read-only message", () { - expect( - () => Rec.getDefault().clear(), - throwsError(UnsupportedError, - equals('Attempted to change a read-only message (rec)'))); + for (var create in [ + () => Rec.getDefault(), + () => viaFreeze(Rec()), + () => viaMergeFromBufferFrozen(Rec(), Rec()), + () => viaMergeFromCodedBufferReaderFrozen(Rec(), Rec()), + () => viaMergeFromJsonFrozen(Rec(), Rec()), + () => viaMergeFromJsonMapFrozen(Rec(), Rec()), + ]) { + expect( + () => create().clear(), + throwsError(UnsupportedError, + equals('Attempted to change a read-only message (rec)'))); + } }); test("can't clear a field on a read-only message", () { - expect( - () => Rec.getDefault().clearField(1), - throwsError(UnsupportedError, - equals('Attempted to change a read-only message (rec)'))); + for (var create in [ + () => Rec.getDefault(), + () => viaFreeze(Rec()), + () => viaMergeFromBufferFrozen(Rec(), Rec()), + () => viaMergeFromCodedBufferReaderFrozen(Rec(), Rec()), + () => viaMergeFromJsonFrozen(Rec(), Rec()), + () => viaMergeFromJsonMapFrozen(Rec(), Rec()), + ]) { + expect( + () => create().clearField(1), + throwsError(UnsupportedError, + equals('Attempted to change a read-only message (rec)'))); + } }); test("can't modify repeated fields on a read-only message", () { expect(() => Rec.getDefault().sub.add(Rec.create()), throwsError(UnsupportedError, contains('add'))); - var r = Rec.create() - ..ints.add(10) - ..freeze(); - expect(() => r.ints.clear(), - throwsError(UnsupportedError, equals("'clear' on a read-only list"))); - expect( - () => r.ints[0] = 2, - throwsError( - UnsupportedError, equals("'set element' on a read-only list"))); - expect(() => r.sub.add(Rec.create()), - throwsError(UnsupportedError, equals("'add' on a read-only list"))); - - r = Rec.create() - ..sub.add(Rec.create()) - ..freeze(); - expect(() => r.sub.add(Rec.create()), - throwsError(UnsupportedError, equals("'add' on a read-only list"))); - expect(() => r.ints.length = 20, - throwsError(UnsupportedError, contains('length'))); + + for (var create in [ + (Rec r) => viaFreeze(r), + (Rec r) => viaMergeFromBufferFrozen(r, Rec()), + (Rec r) => viaMergeFromCodedBufferReaderFrozen(r, Rec()), + (Rec r) => viaMergeFromJsonFrozen(r, Rec()), + (Rec r) => viaMergeFromJsonMapFrozen(r, Rec()), + ]) { + var r = create(Rec.create()..ints.add(10)); + expect(() => r.ints.clear(), + throwsError(UnsupportedError, equals("'clear' on a read-only list"))); + expect( + () => r.ints[0] = 2, + throwsError( + UnsupportedError, equals("'set element' on a read-only list"))); + expect(() => r.sub.add(Rec.create()), + throwsError(UnsupportedError, equals("'add' on a read-only list"))); + + r = create(Rec.create()..sub.add(Rec.create())); + expect(() => r.sub.add(Rec.create()), + throwsError(UnsupportedError, equals("'add' on a read-only list"))); + expect(() => r.ints.length = 20, + throwsError(UnsupportedError, contains('length'))); + } }); test("can't modify sub-messages on a read-only message", () { @@ -115,82 +199,122 @@ void main() { () => subMessage.value = 2, throwsError(UnsupportedError, equals('Attempted to change a read-only message (rec)'))); + + // This test does not apply to the merging utilities because they + // freeze a message that is constructed through deserialization. }); test("can't modify unknown fields on a read-only message", () { - expect( - () => Rec.getDefault().unknownFields.clear(), - throwsError( - UnsupportedError, - equals( - 'Attempted to call clear on a read-only message (UnknownFieldSet)'))); + for (var create in [ + () => Rec.getDefault(), + () => viaFreeze(Rec()), + () => viaMergeFromBufferFrozen(Rec(), Rec()), + () => viaMergeFromCodedBufferReaderFrozen(Rec(), Rec()), + () => viaMergeFromJsonFrozen(Rec(), Rec()), + () => viaMergeFromJsonMapFrozen(Rec(), Rec()), + ]) { + expect( + () => create().unknownFields.clear(), + throwsError( + UnsupportedError, + equals( + 'Attempted to call clear on a read-only message (UnknownFieldSet)'))); + } }); test('can rebuild a frozen message with merge', () { - final orig = Rec.create() - ..value = 10 - ..freeze(); - final rebuilt = orig.copyWith((m) => m.mergeFromJson('{"1": 7}')); - expect(identical(orig, rebuilt), false); - expect(orig.value, 10); - expect(rebuilt.value, 7); + for (var create in [ + (Rec r) => viaFreeze(r), + (Rec r) => viaMergeFromBufferFrozen(r, Rec()), + (Rec r) => viaMergeFromCodedBufferReaderFrozen(r, Rec()), + (Rec r) => viaMergeFromJsonFrozen(r, Rec()), + (Rec r) => viaMergeFromJsonMapFrozen(r, Rec()), + ]) { + final orig = create(Rec.create()..value = 10); + final rebuilt = orig.copyWith((m) => m.mergeFromJson('{"1": 7}')); + expect(identical(orig, rebuilt), false); + expect(orig.value, 10); + expect(rebuilt.value, 7); + } }); test('can set a field while rebuilding a frozen message', () { - final orig = Rec.create() - ..value = 10 - ..freeze(); - final rebuilt = orig.copyWith((m) => m.value = 7); - expect(identical(orig, rebuilt), false); - expect(orig.value, 10); - expect(rebuilt.value, 7); + for (var create in [ + (Rec r) => viaFreeze(r), + (Rec r) => viaMergeFromBufferFrozen(r, Rec()), + (Rec r) => viaMergeFromCodedBufferReaderFrozen(r, Rec()), + (Rec r) => viaMergeFromJsonFrozen(r, Rec()), + (Rec r) => viaMergeFromJsonMapFrozen(r, Rec()), + ]) { + final orig = create(Rec.create()..value = 10); + final rebuilt = orig.copyWith((m) => m.value = 7); + expect(identical(orig, rebuilt), false); + expect(orig.value, 10); + expect(rebuilt.value, 7); + } }); test('can clear while rebuilding a frozen message', () { - final orig = Rec.create() - ..value = 10 - ..freeze(); - final rebuilt = orig.copyWith((m) => m.clear()); - expect(identical(orig, rebuilt), false); - expect(orig.value, 10); - expect(orig.hasValue(), true); - expect(rebuilt.hasValue(), false); + for (var create in [ + (Rec r) => viaFreeze(r), + (Rec r) => viaMergeFromBufferFrozen(r, Rec()), + (Rec r) => viaMergeFromCodedBufferReaderFrozen(r, Rec()), + (Rec r) => viaMergeFromJsonFrozen(r, Rec()), + (Rec r) => viaMergeFromJsonMapFrozen(r, Rec()), + ]) { + final orig = create(Rec.create()..value = 10); + final rebuilt = orig.copyWith((m) => m.clear()); + expect(identical(orig, rebuilt), false); + expect(orig.value, 10); + expect(orig.hasValue(), true); + expect(rebuilt.hasValue(), false); + } }); test('can clear a field while rebuilding a frozen message', () { - final orig = Rec.create() - ..value = 10 - ..freeze(); - final rebuilt = orig.copyWith((m) => m.clearField(1)); - expect(identical(orig, rebuilt), false); - expect(orig.value, 10); - expect(orig.hasValue(), true); - expect(rebuilt.hasValue(), false); + for (var create in [ + (Rec r) => viaFreeze(r), + (Rec r) => viaMergeFromBufferFrozen(r, Rec()), + (Rec r) => viaMergeFromCodedBufferReaderFrozen(r, Rec()), + (Rec r) => viaMergeFromJsonFrozen(r, Rec()), + (Rec r) => viaMergeFromJsonMapFrozen(r, Rec()), + ]) { + final orig = create(Rec.create()..value = 10); + final rebuilt = orig.copyWith((m) => m.clearField(1)); + expect(identical(orig, rebuilt), false); + expect(orig.value, 10); + expect(orig.hasValue(), true); + expect(rebuilt.hasValue(), false); + } }); test('can modify repeated fields while rebuilding a frozen message', () { - var orig = Rec.create() - ..ints.add(10) - ..freeze(); - var rebuilt = orig.copyWith((m) => m.ints.add(12)); - expect(identical(orig, rebuilt), false); - expect(orig.ints, [10]); - expect(rebuilt.ints, [10, 12]); + for (var create in [ + (Rec r) => viaFreeze(r), + (Rec r) => viaMergeFromBufferFrozen(r, Rec()), + (Rec r) => viaMergeFromCodedBufferReaderFrozen(r, Rec()), + (Rec r) => viaMergeFromJsonFrozen(r, Rec()), + (Rec r) => viaMergeFromJsonMapFrozen(r, Rec()), + ]) { + var orig = create(Rec.create()..ints.add(10)); + var rebuilt = orig.copyWith((m) => m.ints.add(12)); + expect(identical(orig, rebuilt), false); + expect(orig.ints, [10]); + expect(rebuilt.ints, [10, 12]); - rebuilt = orig.copyWith((m) => m.ints.clear()); - expect(orig.ints, [10]); - expect(rebuilt.ints, []); + rebuilt = orig.copyWith((m) => m.ints.clear()); + expect(orig.ints, [10]); + expect(rebuilt.ints, []); - rebuilt = orig.copyWith((m) => m.ints[0] = 2); - expect(orig.ints, [10]); - expect(rebuilt.ints, [2]); + rebuilt = orig.copyWith((m) => m.ints[0] = 2); + expect(orig.ints, [10]); + expect(rebuilt.ints, [2]); - orig = Rec.create() - ..sub.add(Rec.create()) - ..freeze(); - rebuilt = orig.copyWith((m) => m.sub.add(Rec.create())); - expect(orig.sub.length, 1); - expect(rebuilt.sub.length, 2); + orig = create(Rec.create()..sub.add(Rec.create())); + rebuilt = orig.copyWith((m) => m.sub.add(Rec.create())); + expect(orig.sub.length, 1); + expect(rebuilt.sub.length, 2); + } }); test('cannot modify sub-messages while rebuilding a frozen message', () { @@ -215,13 +339,25 @@ void main() { expect(identical(subMessage, rebuilt.sub[0].sub[0]), false); expect(orig.sub[0].sub[0].value, 1); expect(rebuilt.sub[0].sub[0].value, 2); + + // This test does not apply to the merging utilities because they + // freeze a message that is constructed through deserialization. }); test('can modify unknown fields while rebuilding a frozen message', () { - final orig = Rec.create() - ..unknownFields.addField(20, UnknownFieldSetField()..fixed32s.add(1)); - final rebuilt = orig.copyWith((m) => m.unknownFields.clear()); - expect(orig.unknownFields.hasField(20), true); - expect(rebuilt.unknownFields.hasField(20), false); + for (var create in [ + (Rec r) => viaFreeze(r), + (Rec r) => viaMergeFromBufferFrozen(r, Rec()), + (Rec r) => viaMergeFromCodedBufferReaderFrozen(r, Rec()), + ]) { + final orig = create(Rec.create() + ..unknownFields.addField(20, UnknownFieldSetField()..fixed32s.add(1))); + final rebuilt = orig.copyWith((m) => m.unknownFields.clear()); + expect(orig.unknownFields.hasField(20), true); + expect(rebuilt.unknownFields.hasField(20), false); + } + + // This test does not apply to the JSON merging utilities because they + // don't serialize unknown fields. }); }