blob: 8f08cd0d43bdb582eff3d6c09429a9e51aa358bf [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) {
int baseType = PbFieldType._baseType(keyType);
assert(!_isRepeated(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 (_isGroupOrMessage(fieldType)) {
return _writeToProto3Json(
(fieldValue as GeneratedMessage)._fieldSet, typeRegistry);
} else if (_isEnum(fieldType)) {
return (fieldValue as ProtobufEnum).name;
} else {
int 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');
}
}
}
if (fs._meta.toProto3Json != null) {
return fs._meta.toProto3Json(fs._message, typeRegistry);
}
Map<String, dynamic> result = <String, dynamic>{};
for (FieldInfo 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) {
MapFieldInfo mapEntryInfo = fieldInfo as MapFieldInfo;
return MapEntry(convertToMapKey(key, mapEntryInfo.keyFieldType),
valueToProto3Json(entryValue, mapEntryInfo.valueFieldType));
});
} else if (fieldInfo.isRepeated) {
jsonValue = (value as PbList)
.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;
}
void _mergeFromProto3Json(
Object json,
_FieldSet fieldSet,
TypeRegistry typeRegistry,
bool ignoreUnknownFields,
bool supportNamesWithUnderscores,
bool permissiveEnums) {
JsonParsingContext 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();
}
int 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.firstWhere(
(e) => _permissiveCompare(e.name, value),
orElse: () => null)
: fieldInfo.enumValues
.firstWhere((e) => e.name == value, orElse: () => null);
if (result != null) return result;
throw context.parseException('Unknown enum value', value);
} else if (value is int) {
return fieldInfo.valueOf(value) ??
(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:
GeneratedMessage 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;
}
BuilderInfo info = fieldSet._meta;
final wellKnownConverter = info.fromProto3Json;
if (wellKnownConverter != null) {
wellKnownConverter(fieldSet._message, json, typeRegistry, context);
} else {
if (json is Map) {
Map<String, FieldInfo> byName = info.byName;
json.forEach((key, value) {
if (key is! String) {
throw context.parseException('Key was not a String', key);
}
context.addMapIndex(key);
FieldInfo 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.firstWhere(
(FieldInfo info) => info.protoName == key,
orElse: () => null);
}
if (fieldInfo == null) {
if (ignoreUnknownFields) {
return;
} else {
throw context.parseException('Unknown field name \'$key\'', key);
}
}
if (_isMapField(fieldInfo.type)) {
if (value is Map) {
MapFieldInfo mapFieldInfo = fieldInfo;
Map fieldValues = fieldSet._ensureMapField(fieldInfo);
value.forEach((subKey, subValue) {
if (subKey is! String) {
throw context.parseException('Expected a String key', subKey);
}
context.addMapIndex(subKey);
final result = fieldValues[
decodeMapKey(subKey, mapFieldInfo.keyFieldType)] =
convertProto3JsonValue(
subValue, mapFieldInfo.valueFieldInfo);
context.popIndex();
return result;
});
} else {
throw context.parseException('Expected a map', value);
}
} else if (_isRepeated(fieldInfo.type)) {
if (value == null) {
// `null` is accepted as the empty list [].
fieldSet._ensureRepeatedField(fieldInfo);
} else if (value is List) {
List values = fieldSet._ensureRepeatedField(fieldInfo);
for (int 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 (_isGroupOrMessage(fieldInfo.type)) {
// TODO(sigurdm) consider a cleaner separation between parsing and merging.
GeneratedMessage parsedSubMessage =
convertProto3JsonValue(value, fieldInfo);
GeneratedMessage original = fieldSet._values[fieldInfo.index];
if (original == null) {
fieldSet._values[fieldInfo.index] = parsedSubMessage;
} else {
original.mergeFromMessage(parsedSubMessage);
}
} else {
fieldSet._setFieldUnchecked(
fieldInfo, convertProto3JsonValue(value, fieldInfo));
}
context.popIndex();
});
} else {
throw context.parseException('Expected JSON object', json);
}
}
}
recursionHelper(json, fieldSet);
}
bool _isAsciiLetter(int char) {
const lowerA = 97;
const lowerZ = 122;
const capitalA = 65;
char |= lowerA ^ capitalA;
return lowerA <= char && char <= lowerZ;
}
/// Returns true if [a] and [b] are the same ignoring case and all instances of
/// `-` and `_`.
bool _permissiveCompare(String a, String b) {
const dash = 45;
const underscore = 95;
// Enum names are always ascii.
int i = 0;
int j = 0;
outer:
while (i < a.length && j < b.length) {
int ca = a.codeUnitAt(i);
if (ca == dash || ca == underscore) {
i++;
continue;
}
int cb = b.codeUnitAt(j);
while (cb == dash || cb == underscore) {
j++;
if (j == b.length) break outer;
cb = b.codeUnitAt(j);
}
if (ca != cb && (ca ^ cb != 0x20 || !_isAsciiLetter(ca))) return false;
i++;
j++;
}
return true;
}