blob: b8edff8bd43de46a15e72869129e781531ba2ea8 [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of protobuf;
Object? _writeToProto3Json(FieldSet fs, TypeRegistry typeRegistry) {
String? convertToMapKey(dynamic key, int keyType) {
var baseType = PbFieldType.baseType(keyType);
assert(!isRepeatedFieldType(keyType));
switch (baseType) {
case PbFieldType.BOOL_BIT:
return key ? 'true' : 'false';
case PbFieldType.STRING_BIT:
return key;
case PbFieldType.UINT64_BIT:
return (key as Int64).toStringUnsigned();
case PbFieldType.INT32_BIT:
case PbFieldType.SINT32_BIT:
case PbFieldType.UINT32_BIT:
case PbFieldType.FIXED32_BIT:
case PbFieldType.SFIXED32_BIT:
case PbFieldType.INT64_BIT:
case PbFieldType.SINT64_BIT:
case PbFieldType.SFIXED64_BIT:
case PbFieldType.FIXED64_BIT:
return key.toString();
default:
throw StateError('Not a valid key type $keyType');
}
}
Object? valueToProto3Json(dynamic fieldValue, int? fieldType) {
if (fieldValue == null) return null;
if (isGroupOrMessageFieldType(fieldType!)) {
return _writeToProto3Json(
(fieldValue as GeneratedMessage).fieldSet, typeRegistry);
} else if (isEnumFieldType(fieldType)) {
return (fieldValue as ProtobufEnum).name;
} else {
var baseType = PbFieldType.baseType(fieldType);
switch (baseType) {
case PbFieldType.BOOL_BIT:
return fieldValue ? true : false;
case PbFieldType.STRING_BIT:
return fieldValue;
case PbFieldType.INT32_BIT:
case PbFieldType.SINT32_BIT:
case PbFieldType.UINT32_BIT:
case PbFieldType.FIXED32_BIT:
case PbFieldType.SFIXED32_BIT:
return fieldValue;
case PbFieldType.INT64_BIT:
case PbFieldType.SINT64_BIT:
case PbFieldType.SFIXED64_BIT:
case PbFieldType.FIXED64_BIT:
return fieldValue.toString();
case PbFieldType.FLOAT_BIT:
case PbFieldType.DOUBLE_BIT:
double value = fieldValue;
if (value.isNaN) return 'NaN';
if (value.isInfinite) {
if (value.isNegative) {
return '-Infinity';
} else {
return 'Infinity';
}
}
return value;
case PbFieldType.UINT64_BIT:
return (fieldValue as Int64).toStringUnsigned();
case PbFieldType.BYTES_BIT:
return base64Encode(fieldValue);
default:
throw StateError(
'Invariant violation: unexpected value type $fieldType');
}
}
}
final info = fs.meta;
if (info.toProto3Json != null) {
return info.toProto3Json!(fs._message!, typeRegistry);
}
var result = <String, dynamic>{};
for (var fieldInfo in fs.infosSortedByTag) {
var value = fs.values[fieldInfo.index!];
if (value == null || (value is List && value.isEmpty)) {
continue; // It's missing, repeated, or an empty byte array.
}
dynamic jsonValue;
if (fieldInfo.isMapField) {
jsonValue = (value as PbMap).map((key, entryValue) {
var mapEntryInfo = fieldInfo as MapFieldInfo;
return MapEntry(convertToMapKey(key, mapEntryInfo.keyFieldType!),
valueToProto3Json(entryValue, mapEntryInfo.valueFieldType));
});
} else if (fieldInfo.isRepeated) {
jsonValue = (value as PbListBase)
.map((element) => valueToProto3Json(element, fieldInfo.type))
.toList();
} else {
jsonValue = valueToProto3Json(value, fieldInfo.type);
}
result[fieldInfo.name] = jsonValue;
}
// Extensions and unknown fields are not encoded by proto3 JSON.
return result;
}
/// TODO(paulberry): find a better home for this?
extension _FindFirst<E> on Iterable<E> {
E? findFirst(bool Function(E) test) {
for (var element in this) {
if (test(element)) return element;
}
return null;
}
}
void _mergeFromProto3Json(
Object? json,
FieldSet fieldSet,
TypeRegistry typeRegistry,
bool ignoreUnknownFields,
bool supportNamesWithUnderscores,
bool permissiveEnums) {
var context = JsonParsingContext(
ignoreUnknownFields, supportNamesWithUnderscores, permissiveEnums);
void recursionHelper(Object? json, FieldSet fieldSet) {
int tryParse32Bit(String s) {
return int.tryParse(s) ??
(throw context.parseException('expected integer', s));
}
int check32BitSigned(int n) {
if (n < -2147483648 || n > 2147483647) {
throw context.parseException('expected 32 bit unsigned integer', n);
}
return n;
}
int check32BitUnsigned(int n) {
if (n < 0 || n > 0xFFFFFFFF) {
throw context.parseException('expected 32 bit unsigned integer', n);
}
return n;
}
Int64 tryParse64Bit(String s) {
Int64 result;
try {
result = Int64.parseInt(s);
} on FormatException {
throw context.parseException('expected integer', json);
}
return result;
}
Object? convertProto3JsonValue(Object? value, FieldInfo fieldInfo) {
if (value == null) {
return fieldInfo.makeDefault!();
}
var fieldType = fieldInfo.type;
switch (PbFieldType.baseType(fieldType)) {
case PbFieldType.BOOL_BIT:
if (value is bool) {
return value;
}
throw context.parseException('Expected bool value', json);
case PbFieldType.BYTES_BIT:
if (value is String) {
Uint8List result;
try {
result = base64Decode(value);
} on FormatException {
throw context.parseException(
'Expected bytes encoded as base64 String', json);
}
return result;
}
throw context.parseException(
'Expected bytes encoded as base64 String', value);
case PbFieldType.STRING_BIT:
if (value is String) {
return value;
}
throw context.parseException('Expected String value', value);
case PbFieldType.FLOAT_BIT:
case PbFieldType.DOUBLE_BIT:
if (value is double) {
return value;
} else if (value is num) {
return value.toDouble();
} else if (value is String) {
return double.tryParse(value) ??
(throw context.parseException(
'Expected String to encode a double', value));
}
throw context.parseException(
'Expected a double represented as a String or number', value);
case PbFieldType.ENUM_BIT:
if (value is String) {
// TODO(sigurdm): Do we want to avoid linear search here? Measure...
final result = permissiveEnums
? fieldInfo.enumValues!
.findFirst((e) => permissiveCompare(e.name, value))
: fieldInfo.enumValues!.findFirst((e) => e.name == value);
if ((result != null) || ignoreUnknownFields) return result;
throw context.parseException('Unknown enum value', value);
} else if (value is int) {
return fieldInfo.valueOf!(value) ??
(ignoreUnknownFields
? null
: (throw context.parseException(
'Unknown enum value', value)));
}
throw context.parseException(
'Expected enum as a string or integer', value);
case PbFieldType.UINT32_BIT:
int result;
if (value is int) {
result = value;
} else if (value is String) {
result = tryParse32Bit(value);
} else {
throw context.parseException(
'Expected int or stringified int', value);
}
return check32BitUnsigned(result);
case PbFieldType.INT32_BIT:
case PbFieldType.SINT32_BIT:
case PbFieldType.FIXED32_BIT:
case PbFieldType.SFIXED32_BIT:
int result;
if (value is int) {
result = value;
} else if (value is String) {
result = tryParse32Bit(value);
} else {
throw context.parseException(
'Expected int or stringified int', value);
}
check32BitSigned(result);
return result;
case PbFieldType.UINT64_BIT:
Int64 result;
if (value is int) {
result = Int64(value);
} else if (value is String) {
result = tryParse64Bit(value);
} else {
throw context.parseException(
'Expected int or stringified int', value);
}
return result;
case PbFieldType.INT64_BIT:
case PbFieldType.SINT64_BIT:
case PbFieldType.FIXED64_BIT:
case PbFieldType.SFIXED64_BIT:
if (value is int) return Int64(value);
if (value is String) {
Int64 result;
try {
result = Int64.parseInt(value);
} on FormatException {
throw context.parseException(
'Expected int or stringified int', value);
}
return result;
}
throw context.parseException(
'Expected int or stringified int', value);
case PbFieldType.GROUP_BIT:
case PbFieldType.MESSAGE_BIT:
var subMessage = fieldInfo.subBuilder!();
recursionHelper(value, subMessage.fieldSet);
return subMessage;
default:
throw StateError('Unknown type $fieldType');
}
}
Object decodeMapKey(String key, int fieldType) {
switch (PbFieldType.baseType(fieldType)) {
case PbFieldType.BOOL_BIT:
switch (key) {
case 'true':
return true;
case 'false':
return false;
default:
throw context.parseException(
'Wrong boolean key, should be one of ("true", "false")', key);
}
// ignore: dead_code
throw StateError('(Should have been) unreachable statement');
case PbFieldType.STRING_BIT:
return key;
case PbFieldType.UINT64_BIT:
// TODO(sigurdm): We do not throw on negative values here.
// That would probably require going via bignum.
return tryParse64Bit(key);
case PbFieldType.INT64_BIT:
case PbFieldType.SINT64_BIT:
case PbFieldType.SFIXED64_BIT:
case PbFieldType.FIXED64_BIT:
return tryParse64Bit(key);
case PbFieldType.INT32_BIT:
case PbFieldType.SINT32_BIT:
case PbFieldType.FIXED32_BIT:
case PbFieldType.SFIXED32_BIT:
return check32BitSigned(tryParse32Bit(key));
case PbFieldType.UINT32_BIT:
return check32BitUnsigned(tryParse32Bit(key));
default:
throw StateError('Not a valid key type $fieldType');
}
}
if (json == null) {
// `null` represents the default value. Do nothing more.
return;
}
final info = fieldSet.meta;
final wellKnownConverter = info.fromProto3Json;
if (wellKnownConverter != null) {
wellKnownConverter(fieldSet._message!, json, typeRegistry, context);
} else {
if (json is Map) {
final byName = info.byName;
json.forEach((key, Object? value) {
if (key is! String) {
throw context.parseException('Key was not a String', key);
}
context.addMapIndex(key);
var fieldInfo = byName[key];
if (fieldInfo == null && supportNamesWithUnderscores) {
// We don't optimize for field names with underscores, instead do a
// linear search for the index.
fieldInfo = byName.values
.findFirst((FieldInfo info) => info.protoName == key);
}
if (fieldInfo == null) {
if (ignoreUnknownFields) {
return;
} else {
throw context.parseException('Unknown field name \'$key\'', key);
}
}
if (isMapFieldType(fieldInfo.type)) {
if (value is Map) {
final mapFieldInfo = fieldInfo as MapFieldInfo<dynamic, dynamic>;
final Map fieldValues = fieldSet.ensureMapField(info, fieldInfo);
value.forEach((subKey, subValue) {
if (subKey is! String) {
throw context.parseException('Expected a String key', subKey);
}
context.addMapIndex(subKey);
fieldValues[decodeMapKey(subKey, mapFieldInfo.keyFieldType!)] =
convertProto3JsonValue(
subValue, mapFieldInfo.valueFieldInfo);
context.popIndex();
});
} else {
throw context.parseException('Expected a map', value);
}
} else if (isRepeatedFieldType(fieldInfo.type)) {
if (value == null) {
// `null` is accepted as the empty list [].
fieldSet.ensureRepeatedField(info, fieldInfo);
} else if (value is List) {
var values = fieldSet.ensureRepeatedField(info, fieldInfo);
for (var i = 0; i < value.length; i++) {
final entry = value[i];
context.addListIndex(i);
values.add(convertProto3JsonValue(entry, fieldInfo));
context.popIndex();
}
} else {
throw context.parseException('Expected a list', value);
}
} else if (isGroupOrMessageFieldType(fieldInfo.type)) {
// TODO(sigurdm) consider a cleaner separation between parsing and
// merging.
var parsedSubMessage =
convertProto3JsonValue(value, fieldInfo) as GeneratedMessage;
GeneratedMessage? original = fieldSet.values[fieldInfo.index!];
if (original == null) {
fieldSet._setNonExtensionFieldUnchecked(
info, fieldInfo, parsedSubMessage);
} else {
original.mergeFromMessage(parsedSubMessage);
}
} else {
fieldSet.setFieldUnchecked(
info, fieldInfo, convertProto3JsonValue(value, fieldInfo));
}
context.popIndex();
});
} else {
throw context.parseException('Expected JSON object', json);
}
}
}
recursionHelper(json, fieldSet);
}