Index: third_party/protobuf/csharp/src/Google.Protobuf/JsonParser.cs |
diff --git a/third_party/protobuf/csharp/src/Google.Protobuf/JsonParser.cs b/third_party/protobuf/csharp/src/Google.Protobuf/JsonParser.cs |
index 95f9ad351ebcacc3ce6ec2cee0fd761a2baa8ca6..d738ebb04b17449cce9a263a4b267c31c0a2c358 100644 |
--- a/third_party/protobuf/csharp/src/Google.Protobuf/JsonParser.cs |
+++ b/third_party/protobuf/csharp/src/Google.Protobuf/JsonParser.cs |
@@ -168,6 +168,10 @@ namespace Google.Protobuf |
} |
var descriptor = message.Descriptor; |
var jsonFieldMap = descriptor.Fields.ByJsonName(); |
+ // All the oneof fields we've already accounted for - we can only see each of them once. |
+ // The set is created lazily to avoid the overhead of creating a set for every message |
+ // we parsed, when oneofs are relatively rare. |
+ HashSet<OneofDescriptor> seenOneofs = null; |
while (true) |
{ |
token = tokenizer.Next(); |
@@ -183,6 +187,17 @@ namespace Google.Protobuf |
FieldDescriptor field; |
if (jsonFieldMap.TryGetValue(name, out field)) |
{ |
+ if (field.ContainingOneof != null) |
+ { |
+ if (seenOneofs == null) |
+ { |
+ seenOneofs = new HashSet<OneofDescriptor>(); |
+ } |
+ if (!seenOneofs.Add(field.ContainingOneof)) |
+ { |
+ throw new InvalidProtocolBufferException($"Multiple values specified for oneof {field.ContainingOneof.Name}"); |
+ } |
+ } |
MergeField(message, field, tokenizer); |
} |
else |
@@ -200,10 +215,15 @@ namespace Google.Protobuf |
var token = tokenizer.Next(); |
if (token.Type == JsonToken.TokenType.Null) |
{ |
+ // Clear the field if we see a null token, unless it's for a singular field of type |
+ // google.protobuf.Value. |
// Note: different from Java API, which just ignores it. |
// TODO: Bring it more in line? Discuss... |
- field.Accessor.Clear(message); |
- return; |
+ if (field.IsMap || field.IsRepeated || !IsGoogleProtobufValueField(field)) |
+ { |
+ field.Accessor.Clear(message); |
+ return; |
+ } |
} |
tokenizer.PushBack(token); |
@@ -239,6 +259,10 @@ namespace Google.Protobuf |
return; |
} |
tokenizer.PushBack(token); |
+ if (token.Type == JsonToken.TokenType.Null) |
+ { |
+ throw new InvalidProtocolBufferException("Repeated field elements cannot be null"); |
+ } |
list.Add(ParseSingleValue(field, tokenizer)); |
} |
} |
@@ -270,19 +294,30 @@ namespace Google.Protobuf |
} |
object key = ParseMapKey(keyField, token.StringValue); |
object value = ParseSingleValue(valueField, tokenizer); |
- // TODO: Null handling |
+ if (value == null) |
+ { |
+ throw new InvalidProtocolBufferException("Map values must not be null"); |
+ } |
dictionary[key] = value; |
} |
} |
+ private static bool IsGoogleProtobufValueField(FieldDescriptor field) |
+ { |
+ return field.FieldType == FieldType.Message && |
+ field.MessageType.FullName == Value.Descriptor.FullName; |
+ } |
+ |
private object ParseSingleValue(FieldDescriptor field, JsonTokenizer tokenizer) |
{ |
var token = tokenizer.Next(); |
if (token.Type == JsonToken.TokenType.Null) |
{ |
- if (field.FieldType == FieldType.Message && field.MessageType.FullName == Value.Descriptor.FullName) |
+ // TODO: In order to support dynamic messages, we should really build this up |
+ // dynamically. |
+ if (IsGoogleProtobufValueField(field)) |
{ |
- return new Value { NullValue = NullValue.NULL_VALUE }; |
+ return Value.ForNull(); |
} |
return null; |
} |
@@ -339,7 +374,7 @@ namespace Google.Protobuf |
/// <exception cref="InvalidProtocolBufferException">The JSON does not represent a Protocol Buffers message correctly</exception> |
public T Parse<T>(string json) where T : IMessage, new() |
{ |
- Preconditions.CheckNotNull(json, nameof(json)); |
+ ProtoPreconditions.CheckNotNull(json, nameof(json)); |
return Parse<T>(new StringReader(json)); |
} |
@@ -352,7 +387,7 @@ namespace Google.Protobuf |
/// <exception cref="InvalidProtocolBufferException">The JSON does not represent a Protocol Buffers message correctly</exception> |
public T Parse<T>(TextReader jsonReader) where T : IMessage, new() |
{ |
- Preconditions.CheckNotNull(jsonReader, nameof(jsonReader)); |
+ ProtoPreconditions.CheckNotNull(jsonReader, nameof(jsonReader)); |
T message = new T(); |
Merge(message, jsonReader); |
return message; |
@@ -367,8 +402,8 @@ namespace Google.Protobuf |
/// <exception cref="InvalidProtocolBufferException">The JSON does not represent a Protocol Buffers message correctly</exception> |
public IMessage Parse(string json, MessageDescriptor descriptor) |
{ |
- Preconditions.CheckNotNull(json, nameof(json)); |
- Preconditions.CheckNotNull(descriptor, nameof(descriptor)); |
+ ProtoPreconditions.CheckNotNull(json, nameof(json)); |
+ ProtoPreconditions.CheckNotNull(descriptor, nameof(descriptor)); |
return Parse(new StringReader(json), descriptor); |
} |
@@ -381,8 +416,8 @@ namespace Google.Protobuf |
/// <exception cref="InvalidProtocolBufferException">The JSON does not represent a Protocol Buffers message correctly</exception> |
public IMessage Parse(TextReader jsonReader, MessageDescriptor descriptor) |
{ |
- Preconditions.CheckNotNull(jsonReader, nameof(jsonReader)); |
- Preconditions.CheckNotNull(descriptor, nameof(descriptor)); |
+ ProtoPreconditions.CheckNotNull(jsonReader, nameof(jsonReader)); |
+ ProtoPreconditions.CheckNotNull(descriptor, nameof(descriptor)); |
IMessage message = descriptor.Parser.CreateTemplate(); |
Merge(message, jsonReader); |
return message; |
@@ -464,6 +499,11 @@ namespace Google.Protobuf |
{ |
tokens.Add(token); |
token = tokenizer.Next(); |
+ |
+ if (tokenizer.ObjectDepth < typeUrlObjectDepth) |
+ { |
+ throw new InvalidProtocolBufferException("Any message with no @type"); |
+ } |
} |
// Don't add the @type property or its value to the recorded token list |
@@ -473,7 +513,7 @@ namespace Google.Protobuf |
throw new InvalidProtocolBufferException("Expected string value for Any.@type"); |
} |
string typeUrl = token.StringValue; |
- string typeName = JsonFormatter.GetTypeName(typeUrl); |
+ string typeName = Any.GetTypeName(typeUrl); |
MessageDescriptor descriptor = settings.TypeRegistry.Find(typeName); |
if (descriptor == null) |
@@ -540,17 +580,17 @@ namespace Google.Protobuf |
case FieldType.Int32: |
case FieldType.SInt32: |
case FieldType.SFixed32: |
- return ParseNumericString(keyText, int.Parse, false); |
+ return ParseNumericString(keyText, int.Parse); |
case FieldType.UInt32: |
case FieldType.Fixed32: |
- return ParseNumericString(keyText, uint.Parse, false); |
+ return ParseNumericString(keyText, uint.Parse); |
case FieldType.Int64: |
case FieldType.SInt64: |
case FieldType.SFixed64: |
- return ParseNumericString(keyText, long.Parse, false); |
+ return ParseNumericString(keyText, long.Parse); |
case FieldType.UInt64: |
case FieldType.Fixed64: |
- return ParseNumericString(keyText, ulong.Parse, false); |
+ return ParseNumericString(keyText, ulong.Parse); |
default: |
throw new InvalidProtocolBufferException("Invalid field type for map: " + field.FieldType); |
} |
@@ -561,7 +601,6 @@ namespace Google.Protobuf |
double value = token.NumberValue; |
checked |
{ |
- // TODO: Validate that it's actually an integer, possibly in terms of the textual representation? |
try |
{ |
switch (field.FieldType) |
@@ -569,16 +608,20 @@ namespace Google.Protobuf |
case FieldType.Int32: |
case FieldType.SInt32: |
case FieldType.SFixed32: |
+ CheckInteger(value); |
return (int) value; |
case FieldType.UInt32: |
case FieldType.Fixed32: |
+ CheckInteger(value); |
return (uint) value; |
case FieldType.Int64: |
case FieldType.SInt64: |
case FieldType.SFixed64: |
+ CheckInteger(value); |
return (long) value; |
case FieldType.UInt64: |
case FieldType.Fixed64: |
+ CheckInteger(value); |
return (ulong) value; |
case FieldType.Double: |
return value; |
@@ -597,20 +640,37 @@ namespace Google.Protobuf |
{ |
return float.NegativeInfinity; |
} |
- throw new InvalidProtocolBufferException("Value out of range: " + value); |
+ throw new InvalidProtocolBufferException($"Value out of range: {value}"); |
} |
return (float) value; |
+ case FieldType.Enum: |
+ CheckInteger(value); |
+ // Just return it as an int, and let the CLR convert it. |
+ // Note that we deliberately don't check that it's a known value. |
+ return (int) value; |
default: |
- throw new InvalidProtocolBufferException("Unsupported conversion from JSON number for field type " + field.FieldType); |
+ throw new InvalidProtocolBufferException($"Unsupported conversion from JSON number for field type {field.FieldType}"); |
} |
} |
catch (OverflowException) |
{ |
- throw new InvalidProtocolBufferException("Value out of range: " + value); |
+ throw new InvalidProtocolBufferException($"Value out of range: {value}"); |
} |
} |
} |
+ private static void CheckInteger(double value) |
+ { |
+ if (double.IsInfinity(value) || double.IsNaN(value)) |
+ { |
+ throw new InvalidProtocolBufferException($"Value not an integer: {value}"); |
+ } |
+ if (value != Math.Floor(value)) |
+ { |
+ throw new InvalidProtocolBufferException($"Value not an integer: {value}"); |
+ } |
+ } |
+ |
private static object ParseSingleStringValue(FieldDescriptor field, string text) |
{ |
switch (field.FieldType) |
@@ -618,47 +678,46 @@ namespace Google.Protobuf |
case FieldType.String: |
return text; |
case FieldType.Bytes: |
- return ByteString.FromBase64(text); |
+ try |
+ { |
+ return ByteString.FromBase64(text); |
+ } |
+ catch (FormatException e) |
+ { |
+ throw InvalidProtocolBufferException.InvalidBase64(e); |
+ } |
case FieldType.Int32: |
case FieldType.SInt32: |
case FieldType.SFixed32: |
- return ParseNumericString(text, int.Parse, false); |
+ return ParseNumericString(text, int.Parse); |
case FieldType.UInt32: |
case FieldType.Fixed32: |
- return ParseNumericString(text, uint.Parse, false); |
+ return ParseNumericString(text, uint.Parse); |
case FieldType.Int64: |
case FieldType.SInt64: |
case FieldType.SFixed64: |
- return ParseNumericString(text, long.Parse, false); |
+ return ParseNumericString(text, long.Parse); |
case FieldType.UInt64: |
case FieldType.Fixed64: |
- return ParseNumericString(text, ulong.Parse, false); |
+ return ParseNumericString(text, ulong.Parse); |
case FieldType.Double: |
- double d = ParseNumericString(text, double.Parse, true); |
- // double.Parse can return +/- infinity on Mono for non-infinite values which are out of range for double. |
- if (double.IsInfinity(d) && !text.Contains("Infinity")) |
- { |
- throw new InvalidProtocolBufferException("Invalid numeric value: " + text); |
- } |
+ double d = ParseNumericString(text, double.Parse); |
+ ValidateInfinityAndNan(text, double.IsPositiveInfinity(d), double.IsNegativeInfinity(d), double.IsNaN(d)); |
return d; |
case FieldType.Float: |
- float f = ParseNumericString(text, float.Parse, true); |
- // float.Parse can return +/- infinity on Mono for non-infinite values which are out of range for float. |
- if (float.IsInfinity(f) && !text.Contains("Infinity")) |
- { |
- throw new InvalidProtocolBufferException("Invalid numeric value: " + text); |
- } |
+ float f = ParseNumericString(text, float.Parse); |
+ ValidateInfinityAndNan(text, float.IsPositiveInfinity(f), float.IsNegativeInfinity(f), float.IsNaN(f)); |
return f; |
case FieldType.Enum: |
var enumValue = field.EnumType.FindValueByName(text); |
if (enumValue == null) |
{ |
- throw new InvalidProtocolBufferException("Invalid enum value: " + text + " for enum type: " + field.EnumType.FullName); |
+ throw new InvalidProtocolBufferException($"Invalid enum value: {text} for enum type: {field.EnumType.FullName}"); |
} |
// Just return it as an int, and let the CLR convert it. |
return enumValue.Number; |
default: |
- throw new InvalidProtocolBufferException("Unsupported conversion from JSON string for field type " + field.FieldType); |
+ throw new InvalidProtocolBufferException($"Unsupported conversion from JSON string for field type {field.FieldType}"); |
} |
} |
@@ -670,43 +729,53 @@ namespace Google.Protobuf |
return field.MessageType.Parser.CreateTemplate(); |
} |
- private static T ParseNumericString<T>(string text, Func<string, NumberStyles, IFormatProvider, T> parser, bool floatingPoint) |
+ private static T ParseNumericString<T>(string text, Func<string, NumberStyles, IFormatProvider, T> parser) |
{ |
- // TODO: Prohibit leading zeroes (but allow 0!) |
- // TODO: Validate handling of "Infinity" etc. (Should be case sensitive, no leading whitespace etc) |
// Can't prohibit this with NumberStyles. |
if (text.StartsWith("+")) |
{ |
- throw new InvalidProtocolBufferException("Invalid numeric value: " + text); |
+ throw new InvalidProtocolBufferException($"Invalid numeric value: {text}"); |
} |
if (text.StartsWith("0") && text.Length > 1) |
{ |
if (text[1] >= '0' && text[1] <= '9') |
{ |
- throw new InvalidProtocolBufferException("Invalid numeric value: " + text); |
+ throw new InvalidProtocolBufferException($"Invalid numeric value: {text}"); |
} |
} |
else if (text.StartsWith("-0") && text.Length > 2) |
{ |
if (text[2] >= '0' && text[2] <= '9') |
{ |
- throw new InvalidProtocolBufferException("Invalid numeric value: " + text); |
+ throw new InvalidProtocolBufferException($"Invalid numeric value: {text}"); |
} |
} |
try |
{ |
- var styles = floatingPoint |
- ? NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent |
- : NumberStyles.AllowLeadingSign; |
- return parser(text, styles, CultureInfo.InvariantCulture); |
+ return parser(text, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture); |
} |
catch (FormatException) |
{ |
- throw new InvalidProtocolBufferException("Invalid numeric value for type: " + text); |
+ throw new InvalidProtocolBufferException($"Invalid numeric value for type: {text}"); |
} |
catch (OverflowException) |
{ |
- throw new InvalidProtocolBufferException("Value out of range: " + text); |
+ throw new InvalidProtocolBufferException($"Value out of range: {text}"); |
+ } |
+ } |
+ |
+ /// <summary> |
+ /// Checks that any infinite/NaN values originated from the correct text. |
+ /// This corrects the lenient whitespace handling of double.Parse/float.Parse, as well as the |
+ /// way that Mono parses out-of-range values as infinity. |
+ /// </summary> |
+ private static void ValidateInfinityAndNan(string text, bool isPositiveInfinity, bool isNegativeInfinity, bool isNaN) |
+ { |
+ if ((isPositiveInfinity && text != "Infinity") || |
+ (isNegativeInfinity && text != "-Infinity") || |
+ (isNaN && text != "NaN")) |
+ { |
+ throw new InvalidProtocolBufferException($"Invalid numeric value: {text}"); |
} |
} |
@@ -719,7 +788,7 @@ namespace Google.Protobuf |
var match = TimestampRegex.Match(token.StringValue); |
if (!match.Success) |
{ |
- throw new InvalidProtocolBufferException("Invalid Timestamp value: " + token.StringValue); |
+ throw new InvalidProtocolBufferException($"Invalid Timestamp value: {token.StringValue}"); |
} |
var dateTime = match.Groups["datetime"].Value; |
var subseconds = match.Groups["subseconds"].Value; |
@@ -809,28 +878,24 @@ namespace Google.Protobuf |
try |
{ |
- long seconds = long.Parse(secondsText, CultureInfo.InvariantCulture); |
+ long seconds = long.Parse(secondsText, CultureInfo.InvariantCulture) * multiplier; |
int nanos = 0; |
if (subseconds != "") |
{ |
// This should always work, as we've got 1-9 digits. |
int parsedFraction = int.Parse(subseconds.Substring(1)); |
- nanos = parsedFraction * SubsecondScalingFactors[subseconds.Length]; |
+ nanos = parsedFraction * SubsecondScalingFactors[subseconds.Length] * multiplier; |
} |
- if (seconds >= Duration.MaxSeconds) |
+ if (!Duration.IsNormalized(seconds, nanos)) |
{ |
- // Allow precisely 315576000000 seconds, but prohibit even 1ns more. |
- if (seconds > Duration.MaxSeconds || nanos > 0) |
- { |
- throw new InvalidProtocolBufferException("Invalid Duration value: " + token.StringValue); |
- } |
+ throw new InvalidProtocolBufferException($"Invalid Duration value: {token.StringValue}"); |
} |
- message.Descriptor.Fields[Duration.SecondsFieldNumber].Accessor.SetValue(message, seconds * multiplier); |
- message.Descriptor.Fields[Duration.NanosFieldNumber].Accessor.SetValue(message, nanos * multiplier); |
+ message.Descriptor.Fields[Duration.SecondsFieldNumber].Accessor.SetValue(message, seconds); |
+ message.Descriptor.Fields[Duration.NanosFieldNumber].Accessor.SetValue(message, nanos); |
} |
catch (FormatException) |
{ |
- throw new InvalidProtocolBufferException("Invalid Duration value: " + token.StringValue); |
+ throw new InvalidProtocolBufferException($"Invalid Duration value: {token.StringValue}"); |
} |
} |
@@ -853,6 +918,8 @@ namespace Google.Protobuf |
private static string ToSnakeCase(string text) |
{ |
var builder = new StringBuilder(text.Length * 2); |
+ // Note: this is probably unnecessary now, but currently retained to be as close as possible to the |
+ // C++, whilst still throwing an exception on underscores. |
bool wasNotUnderscore = false; // Initialize to false for case 1 (below) |
bool wasNotCap = false; |
@@ -886,7 +953,11 @@ namespace Google.Protobuf |
else |
{ |
builder.Append(c); |
- wasNotUnderscore = c != '_'; |
+ if (c == '_') |
+ { |
+ throw new InvalidProtocolBufferException($"Invalid field mask: {text}"); |
+ } |
+ wasNotUnderscore = true; |
wasNotCap = true; |
} |
} |
@@ -940,7 +1011,7 @@ namespace Google.Protobuf |
public Settings(int recursionLimit, TypeRegistry typeRegistry) |
{ |
RecursionLimit = recursionLimit; |
- TypeRegistry = Preconditions.CheckNotNull(typeRegistry, nameof(typeRegistry)); |
+ TypeRegistry = ProtoPreconditions.CheckNotNull(typeRegistry, nameof(typeRegistry)); |
} |
} |
} |