| 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
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..95f9ad351ebcacc3ce6ec2cee0fd761a2baa8ca6
|
| --- /dev/null
|
| +++ b/third_party/protobuf/csharp/src/Google.Protobuf/JsonParser.cs
|
| @@ -0,0 +1,947 @@
|
| +#region Copyright notice and license
|
| +// Protocol Buffers - Google's data interchange format
|
| +// Copyright 2015 Google Inc. All rights reserved.
|
| +// https://developers.google.com/protocol-buffers/
|
| +//
|
| +// Redistribution and use in source and binary forms, with or without
|
| +// modification, are permitted provided that the following conditions are
|
| +// met:
|
| +//
|
| +// * Redistributions of source code must retain the above copyright
|
| +// notice, this list of conditions and the following disclaimer.
|
| +// * Redistributions in binary form must reproduce the above
|
| +// copyright notice, this list of conditions and the following disclaimer
|
| +// in the documentation and/or other materials provided with the
|
| +// distribution.
|
| +// * Neither the name of Google Inc. nor the names of its
|
| +// contributors may be used to endorse or promote products derived from
|
| +// this software without specific prior written permission.
|
| +//
|
| +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
| +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
| +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
| +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
| +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
| +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
| +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
| +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
| +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
| +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
| +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
| +#endregion
|
| +
|
| +using Google.Protobuf.Reflection;
|
| +using Google.Protobuf.WellKnownTypes;
|
| +using System;
|
| +using System.Collections;
|
| +using System.Collections.Generic;
|
| +using System.Globalization;
|
| +using System.IO;
|
| +using System.Text;
|
| +using System.Text.RegularExpressions;
|
| +
|
| +namespace Google.Protobuf
|
| +{
|
| + /// <summary>
|
| + /// Reflection-based converter from JSON to messages.
|
| + /// </summary>
|
| + /// <remarks>
|
| + /// <para>
|
| + /// Instances of this class are thread-safe, with no mutable state.
|
| + /// </para>
|
| + /// <para>
|
| + /// This is a simple start to get JSON parsing working. As it's reflection-based,
|
| + /// it's not as quick as baking calls into generated messages - but is a simpler implementation.
|
| + /// (This code is generally not heavily optimized.)
|
| + /// </para>
|
| + /// </remarks>
|
| + public sealed class JsonParser
|
| + {
|
| + // Note: using 0-9 instead of \d to ensure no non-ASCII digits.
|
| + // This regex isn't a complete validator, but will remove *most* invalid input. We rely on parsing to do the rest.
|
| + private static readonly Regex TimestampRegex = new Regex(@"^(?<datetime>[0-9]{4}-[01][0-9]-[0-3][0-9]T[012][0-9]:[0-5][0-9]:[0-5][0-9])(?<subseconds>\.[0-9]{1,9})?(?<offset>(Z|[+-][0-1][0-9]:[0-5][0-9]))$", FrameworkPortability.CompiledRegexWhereAvailable);
|
| + private static readonly Regex DurationRegex = new Regex(@"^(?<sign>-)?(?<int>[0-9]{1,12})(?<subseconds>\.[0-9]{1,9})?s$", FrameworkPortability.CompiledRegexWhereAvailable);
|
| + private static readonly int[] SubsecondScalingFactors = { 0, 100000000, 100000000, 10000000, 1000000, 100000, 10000, 1000, 100, 10, 1 };
|
| + private static readonly char[] FieldMaskPathSeparators = new[] { ',' };
|
| +
|
| + private static readonly JsonParser defaultInstance = new JsonParser(Settings.Default);
|
| +
|
| + // TODO: Consider introducing a class containing parse state of the parser, tokenizer and depth. That would simplify these handlers
|
| + // and the signatures of various methods.
|
| + private static readonly Dictionary<string, Action<JsonParser, IMessage, JsonTokenizer>>
|
| + WellKnownTypeHandlers = new Dictionary<string, Action<JsonParser, IMessage, JsonTokenizer>>
|
| + {
|
| + { Timestamp.Descriptor.FullName, (parser, message, tokenizer) => MergeTimestamp(message, tokenizer.Next()) },
|
| + { Duration.Descriptor.FullName, (parser, message, tokenizer) => MergeDuration(message, tokenizer.Next()) },
|
| + { Value.Descriptor.FullName, (parser, message, tokenizer) => parser.MergeStructValue(message, tokenizer) },
|
| + { ListValue.Descriptor.FullName, (parser, message, tokenizer) =>
|
| + parser.MergeRepeatedField(message, message.Descriptor.Fields[ListValue.ValuesFieldNumber], tokenizer) },
|
| + { Struct.Descriptor.FullName, (parser, message, tokenizer) => parser.MergeStruct(message, tokenizer) },
|
| + { Any.Descriptor.FullName, (parser, message, tokenizer) => parser.MergeAny(message, tokenizer) },
|
| + { FieldMask.Descriptor.FullName, (parser, message, tokenizer) => MergeFieldMask(message, tokenizer.Next()) },
|
| + { Int32Value.Descriptor.FullName, MergeWrapperField },
|
| + { Int64Value.Descriptor.FullName, MergeWrapperField },
|
| + { UInt32Value.Descriptor.FullName, MergeWrapperField },
|
| + { UInt64Value.Descriptor.FullName, MergeWrapperField },
|
| + { FloatValue.Descriptor.FullName, MergeWrapperField },
|
| + { DoubleValue.Descriptor.FullName, MergeWrapperField },
|
| + { BytesValue.Descriptor.FullName, MergeWrapperField },
|
| + { StringValue.Descriptor.FullName, MergeWrapperField }
|
| + };
|
| +
|
| + // Convenience method to avoid having to repeat the same code multiple times in the above
|
| + // dictionary initialization.
|
| + private static void MergeWrapperField(JsonParser parser, IMessage message, JsonTokenizer tokenizer)
|
| + {
|
| + parser.MergeField(message, message.Descriptor.Fields[WrappersReflection.WrapperValueFieldNumber], tokenizer);
|
| + }
|
| +
|
| + /// <summary>
|
| + /// Returns a formatter using the default settings.
|
| + /// </summary>
|
| + public static JsonParser Default { get { return defaultInstance; } }
|
| +
|
| + private readonly Settings settings;
|
| +
|
| + /// <summary>
|
| + /// Creates a new formatted with the given settings.
|
| + /// </summary>
|
| + /// <param name="settings">The settings.</param>
|
| + public JsonParser(Settings settings)
|
| + {
|
| + this.settings = settings;
|
| + }
|
| +
|
| + /// <summary>
|
| + /// Parses <paramref name="json"/> and merges the information into the given message.
|
| + /// </summary>
|
| + /// <param name="message">The message to merge the JSON information into.</param>
|
| + /// <param name="json">The JSON to parse.</param>
|
| + internal void Merge(IMessage message, string json)
|
| + {
|
| + Merge(message, new StringReader(json));
|
| + }
|
| +
|
| + /// <summary>
|
| + /// Parses JSON read from <paramref name="jsonReader"/> and merges the information into the given message.
|
| + /// </summary>
|
| + /// <param name="message">The message to merge the JSON information into.</param>
|
| + /// <param name="jsonReader">Reader providing the JSON to parse.</param>
|
| + internal void Merge(IMessage message, TextReader jsonReader)
|
| + {
|
| + var tokenizer = JsonTokenizer.FromTextReader(jsonReader);
|
| + Merge(message, tokenizer);
|
| + var lastToken = tokenizer.Next();
|
| + if (lastToken != JsonToken.EndDocument)
|
| + {
|
| + throw new InvalidProtocolBufferException("Expected end of JSON after object");
|
| + }
|
| + }
|
| +
|
| + /// <summary>
|
| + /// Merges the given message using data from the given tokenizer. In most cases, the next
|
| + /// token should be a "start object" token, but wrapper types and nullity can invalidate
|
| + /// that assumption. This is implemented as an LL(1) recursive descent parser over the stream
|
| + /// of tokens provided by the tokenizer. This token stream is assumed to be valid JSON, with the
|
| + /// tokenizer performing that validation - but not every token stream is valid "protobuf JSON".
|
| + /// </summary>
|
| + private void Merge(IMessage message, JsonTokenizer tokenizer)
|
| + {
|
| + if (tokenizer.ObjectDepth > settings.RecursionLimit)
|
| + {
|
| + throw InvalidProtocolBufferException.JsonRecursionLimitExceeded();
|
| + }
|
| + if (message.Descriptor.IsWellKnownType)
|
| + {
|
| + Action<JsonParser, IMessage, JsonTokenizer> handler;
|
| + if (WellKnownTypeHandlers.TryGetValue(message.Descriptor.FullName, out handler))
|
| + {
|
| + handler(this, message, tokenizer);
|
| + return;
|
| + }
|
| + // Well-known types with no special handling continue in the normal way.
|
| + }
|
| + var token = tokenizer.Next();
|
| + if (token.Type != JsonToken.TokenType.StartObject)
|
| + {
|
| + throw new InvalidProtocolBufferException("Expected an object");
|
| + }
|
| + var descriptor = message.Descriptor;
|
| + var jsonFieldMap = descriptor.Fields.ByJsonName();
|
| + while (true)
|
| + {
|
| + token = tokenizer.Next();
|
| + if (token.Type == JsonToken.TokenType.EndObject)
|
| + {
|
| + return;
|
| + }
|
| + if (token.Type != JsonToken.TokenType.Name)
|
| + {
|
| + throw new InvalidOperationException("Unexpected token type " + token.Type);
|
| + }
|
| + string name = token.StringValue;
|
| + FieldDescriptor field;
|
| + if (jsonFieldMap.TryGetValue(name, out field))
|
| + {
|
| + MergeField(message, field, tokenizer);
|
| + }
|
| + else
|
| + {
|
| + // TODO: Is this what we want to do? If not, we'll need to skip the value,
|
| + // which may be an object or array. (We might want to put code in the tokenizer
|
| + // to do that.)
|
| + throw new InvalidProtocolBufferException("Unknown field: " + name);
|
| + }
|
| + }
|
| + }
|
| +
|
| + private void MergeField(IMessage message, FieldDescriptor field, JsonTokenizer tokenizer)
|
| + {
|
| + var token = tokenizer.Next();
|
| + if (token.Type == JsonToken.TokenType.Null)
|
| + {
|
| + // Note: different from Java API, which just ignores it.
|
| + // TODO: Bring it more in line? Discuss...
|
| + field.Accessor.Clear(message);
|
| + return;
|
| + }
|
| + tokenizer.PushBack(token);
|
| +
|
| + if (field.IsMap)
|
| + {
|
| + MergeMapField(message, field, tokenizer);
|
| + }
|
| + else if (field.IsRepeated)
|
| + {
|
| + MergeRepeatedField(message, field, tokenizer);
|
| + }
|
| + else
|
| + {
|
| + var value = ParseSingleValue(field, tokenizer);
|
| + field.Accessor.SetValue(message, value);
|
| + }
|
| + }
|
| +
|
| + private void MergeRepeatedField(IMessage message, FieldDescriptor field, JsonTokenizer tokenizer)
|
| + {
|
| + var token = tokenizer.Next();
|
| + if (token.Type != JsonToken.TokenType.StartArray)
|
| + {
|
| + throw new InvalidProtocolBufferException("Repeated field value was not an array. Token type: " + token.Type);
|
| + }
|
| +
|
| + IList list = (IList) field.Accessor.GetValue(message);
|
| + while (true)
|
| + {
|
| + token = tokenizer.Next();
|
| + if (token.Type == JsonToken.TokenType.EndArray)
|
| + {
|
| + return;
|
| + }
|
| + tokenizer.PushBack(token);
|
| + list.Add(ParseSingleValue(field, tokenizer));
|
| + }
|
| + }
|
| +
|
| + private void MergeMapField(IMessage message, FieldDescriptor field, JsonTokenizer tokenizer)
|
| + {
|
| + // Map fields are always objects, even if the values are well-known types: ParseSingleValue handles those.
|
| + var token = tokenizer.Next();
|
| + if (token.Type != JsonToken.TokenType.StartObject)
|
| + {
|
| + throw new InvalidProtocolBufferException("Expected an object to populate a map");
|
| + }
|
| +
|
| + var type = field.MessageType;
|
| + var keyField = type.FindFieldByNumber(1);
|
| + var valueField = type.FindFieldByNumber(2);
|
| + if (keyField == null || valueField == null)
|
| + {
|
| + throw new InvalidProtocolBufferException("Invalid map field: " + field.FullName);
|
| + }
|
| + IDictionary dictionary = (IDictionary) field.Accessor.GetValue(message);
|
| +
|
| + while (true)
|
| + {
|
| + token = tokenizer.Next();
|
| + if (token.Type == JsonToken.TokenType.EndObject)
|
| + {
|
| + return;
|
| + }
|
| + object key = ParseMapKey(keyField, token.StringValue);
|
| + object value = ParseSingleValue(valueField, tokenizer);
|
| + // TODO: Null handling
|
| + dictionary[key] = value;
|
| + }
|
| + }
|
| +
|
| + 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)
|
| + {
|
| + return new Value { NullValue = NullValue.NULL_VALUE };
|
| + }
|
| + return null;
|
| + }
|
| +
|
| + var fieldType = field.FieldType;
|
| + if (fieldType == FieldType.Message)
|
| + {
|
| + // Parse wrapper types as their constituent types.
|
| + // TODO: What does this mean for null?
|
| + if (field.MessageType.IsWrapperType)
|
| + {
|
| + field = field.MessageType.Fields[WrappersReflection.WrapperValueFieldNumber];
|
| + fieldType = field.FieldType;
|
| + }
|
| + else
|
| + {
|
| + // TODO: Merge the current value in message? (Public API currently doesn't make this relevant as we don't expose merging.)
|
| + tokenizer.PushBack(token);
|
| + IMessage subMessage = NewMessageForField(field);
|
| + Merge(subMessage, tokenizer);
|
| + return subMessage;
|
| + }
|
| + }
|
| +
|
| + switch (token.Type)
|
| + {
|
| + case JsonToken.TokenType.True:
|
| + case JsonToken.TokenType.False:
|
| + if (fieldType == FieldType.Bool)
|
| + {
|
| + return token.Type == JsonToken.TokenType.True;
|
| + }
|
| + // Fall through to "we don't support this type for this case"; could duplicate the behaviour of the default
|
| + // case instead, but this way we'd only need to change one place.
|
| + goto default;
|
| + case JsonToken.TokenType.StringValue:
|
| + return ParseSingleStringValue(field, token.StringValue);
|
| + // Note: not passing the number value itself here, as we may end up storing the string value in the token too.
|
| + case JsonToken.TokenType.Number:
|
| + return ParseSingleNumberValue(field, token);
|
| + case JsonToken.TokenType.Null:
|
| + throw new NotImplementedException("Haven't worked out what to do for null yet");
|
| + default:
|
| + throw new InvalidProtocolBufferException("Unsupported JSON token type " + token.Type + " for field type " + fieldType);
|
| + }
|
| + }
|
| +
|
| + /// <summary>
|
| + /// Parses <paramref name="json"/> into a new message.
|
| + /// </summary>
|
| + /// <typeparam name="T">The type of message to create.</typeparam>
|
| + /// <param name="json">The JSON to parse.</param>
|
| + /// <exception cref="InvalidJsonException">The JSON does not comply with RFC 7159</exception>
|
| + /// <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));
|
| + return Parse<T>(new StringReader(json));
|
| + }
|
| +
|
| + /// <summary>
|
| + /// Parses JSON read from <paramref name="jsonReader"/> into a new message.
|
| + /// </summary>
|
| + /// <typeparam name="T">The type of message to create.</typeparam>
|
| + /// <param name="jsonReader">Reader providing the JSON to parse.</param>
|
| + /// <exception cref="InvalidJsonException">The JSON does not comply with RFC 7159</exception>
|
| + /// <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));
|
| + T message = new T();
|
| + Merge(message, jsonReader);
|
| + return message;
|
| + }
|
| +
|
| + /// <summary>
|
| + /// Parses <paramref name="json"/> into a new message.
|
| + /// </summary>
|
| + /// <param name="json">The JSON to parse.</param>
|
| + /// <param name="descriptor">Descriptor of message type to parse.</param>
|
| + /// <exception cref="InvalidJsonException">The JSON does not comply with RFC 7159</exception>
|
| + /// <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));
|
| + return Parse(new StringReader(json), descriptor);
|
| + }
|
| +
|
| + /// <summary>
|
| + /// Parses JSON read from <paramref name="jsonReader"/> into a new message.
|
| + /// </summary>
|
| + /// <param name="jsonReader">Reader providing the JSON to parse.</param>
|
| + /// <param name="descriptor">Descriptor of message type to parse.</param>
|
| + /// <exception cref="InvalidJsonException">The JSON does not comply with RFC 7159</exception>
|
| + /// <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));
|
| + IMessage message = descriptor.Parser.CreateTemplate();
|
| + Merge(message, jsonReader);
|
| + return message;
|
| + }
|
| +
|
| + private void MergeStructValue(IMessage message, JsonTokenizer tokenizer)
|
| + {
|
| + var firstToken = tokenizer.Next();
|
| + var fields = message.Descriptor.Fields;
|
| + switch (firstToken.Type)
|
| + {
|
| + case JsonToken.TokenType.Null:
|
| + fields[Value.NullValueFieldNumber].Accessor.SetValue(message, 0);
|
| + return;
|
| + case JsonToken.TokenType.StringValue:
|
| + fields[Value.StringValueFieldNumber].Accessor.SetValue(message, firstToken.StringValue);
|
| + return;
|
| + case JsonToken.TokenType.Number:
|
| + fields[Value.NumberValueFieldNumber].Accessor.SetValue(message, firstToken.NumberValue);
|
| + return;
|
| + case JsonToken.TokenType.False:
|
| + case JsonToken.TokenType.True:
|
| + fields[Value.BoolValueFieldNumber].Accessor.SetValue(message, firstToken.Type == JsonToken.TokenType.True);
|
| + return;
|
| + case JsonToken.TokenType.StartObject:
|
| + {
|
| + var field = fields[Value.StructValueFieldNumber];
|
| + var structMessage = NewMessageForField(field);
|
| + tokenizer.PushBack(firstToken);
|
| + Merge(structMessage, tokenizer);
|
| + field.Accessor.SetValue(message, structMessage);
|
| + return;
|
| + }
|
| + case JsonToken.TokenType.StartArray:
|
| + {
|
| + var field = fields[Value.ListValueFieldNumber];
|
| + var list = NewMessageForField(field);
|
| + tokenizer.PushBack(firstToken);
|
| + Merge(list, tokenizer);
|
| + field.Accessor.SetValue(message, list);
|
| + return;
|
| + }
|
| + default:
|
| + throw new InvalidOperationException("Unexpected token type: " + firstToken.Type);
|
| + }
|
| + }
|
| +
|
| + private void MergeStruct(IMessage message, JsonTokenizer tokenizer)
|
| + {
|
| + var token = tokenizer.Next();
|
| + if (token.Type != JsonToken.TokenType.StartObject)
|
| + {
|
| + throw new InvalidProtocolBufferException("Expected object value for Struct");
|
| + }
|
| + tokenizer.PushBack(token);
|
| +
|
| + var field = message.Descriptor.Fields[Struct.FieldsFieldNumber];
|
| + MergeMapField(message, field, tokenizer);
|
| + }
|
| +
|
| + private void MergeAny(IMessage message, JsonTokenizer tokenizer)
|
| + {
|
| + // Record the token stream until we see the @type property. At that point, we can take the value, consult
|
| + // the type registry for the relevant message, and replay the stream, omitting the @type property.
|
| + var tokens = new List<JsonToken>();
|
| +
|
| + var token = tokenizer.Next();
|
| + if (token.Type != JsonToken.TokenType.StartObject)
|
| + {
|
| + throw new InvalidProtocolBufferException("Expected object value for Any");
|
| + }
|
| + int typeUrlObjectDepth = tokenizer.ObjectDepth;
|
| +
|
| + // The check for the property depth protects us from nested Any values which occur before the type URL
|
| + // for *this* Any.
|
| + while (token.Type != JsonToken.TokenType.Name ||
|
| + token.StringValue != JsonFormatter.AnyTypeUrlField ||
|
| + tokenizer.ObjectDepth != typeUrlObjectDepth)
|
| + {
|
| + tokens.Add(token);
|
| + token = tokenizer.Next();
|
| + }
|
| +
|
| + // Don't add the @type property or its value to the recorded token list
|
| + token = tokenizer.Next();
|
| + if (token.Type != JsonToken.TokenType.StringValue)
|
| + {
|
| + throw new InvalidProtocolBufferException("Expected string value for Any.@type");
|
| + }
|
| + string typeUrl = token.StringValue;
|
| + string typeName = JsonFormatter.GetTypeName(typeUrl);
|
| +
|
| + MessageDescriptor descriptor = settings.TypeRegistry.Find(typeName);
|
| + if (descriptor == null)
|
| + {
|
| + throw new InvalidOperationException($"Type registry has no descriptor for type name '{typeName}'");
|
| + }
|
| +
|
| + // Now replay the token stream we've already read and anything that remains of the object, just parsing it
|
| + // as normal. Our original tokenizer should end up at the end of the object.
|
| + var replay = JsonTokenizer.FromReplayedTokens(tokens, tokenizer);
|
| + var body = descriptor.Parser.CreateTemplate();
|
| + if (descriptor.IsWellKnownType)
|
| + {
|
| + MergeWellKnownTypeAnyBody(body, replay);
|
| + }
|
| + else
|
| + {
|
| + Merge(body, replay);
|
| + }
|
| + var data = body.ToByteString();
|
| +
|
| + // Now that we have the message data, we can pack it into an Any (the message received as a parameter).
|
| + message.Descriptor.Fields[Any.TypeUrlFieldNumber].Accessor.SetValue(message, typeUrl);
|
| + message.Descriptor.Fields[Any.ValueFieldNumber].Accessor.SetValue(message, data);
|
| + }
|
| +
|
| + // Well-known types end up in a property called "value" in the JSON. As there's no longer a @type property
|
| + // in the given JSON token stream, we should *only* have tokens of start-object, name("value"), the value
|
| + // itself, and then end-object.
|
| + private void MergeWellKnownTypeAnyBody(IMessage body, JsonTokenizer tokenizer)
|
| + {
|
| + var token = tokenizer.Next(); // Definitely start-object; checked in previous method
|
| + token = tokenizer.Next();
|
| + // TODO: What about an absent Int32Value, for example?
|
| + if (token.Type != JsonToken.TokenType.Name || token.StringValue != JsonFormatter.AnyWellKnownTypeValueField)
|
| + {
|
| + throw new InvalidProtocolBufferException($"Expected '{JsonFormatter.AnyWellKnownTypeValueField}' property for well-known type Any body");
|
| + }
|
| + Merge(body, tokenizer);
|
| + token = tokenizer.Next();
|
| + if (token.Type != JsonToken.TokenType.EndObject)
|
| + {
|
| + throw new InvalidProtocolBufferException($"Expected end-object token after @type/value for well-known type");
|
| + }
|
| + }
|
| +
|
| + #region Utility methods which don't depend on the state (or settings) of the parser.
|
| + private static object ParseMapKey(FieldDescriptor field, string keyText)
|
| + {
|
| + switch (field.FieldType)
|
| + {
|
| + case FieldType.Bool:
|
| + if (keyText == "true")
|
| + {
|
| + return true;
|
| + }
|
| + if (keyText == "false")
|
| + {
|
| + return false;
|
| + }
|
| + throw new InvalidProtocolBufferException("Invalid string for bool map key: " + keyText);
|
| + case FieldType.String:
|
| + return keyText;
|
| + case FieldType.Int32:
|
| + case FieldType.SInt32:
|
| + case FieldType.SFixed32:
|
| + return ParseNumericString(keyText, int.Parse, false);
|
| + case FieldType.UInt32:
|
| + case FieldType.Fixed32:
|
| + return ParseNumericString(keyText, uint.Parse, false);
|
| + case FieldType.Int64:
|
| + case FieldType.SInt64:
|
| + case FieldType.SFixed64:
|
| + return ParseNumericString(keyText, long.Parse, false);
|
| + case FieldType.UInt64:
|
| + case FieldType.Fixed64:
|
| + return ParseNumericString(keyText, ulong.Parse, false);
|
| + default:
|
| + throw new InvalidProtocolBufferException("Invalid field type for map: " + field.FieldType);
|
| + }
|
| + }
|
| +
|
| + private static object ParseSingleNumberValue(FieldDescriptor field, JsonToken token)
|
| + {
|
| + double value = token.NumberValue;
|
| + checked
|
| + {
|
| + // TODO: Validate that it's actually an integer, possibly in terms of the textual representation?
|
| + try
|
| + {
|
| + switch (field.FieldType)
|
| + {
|
| + case FieldType.Int32:
|
| + case FieldType.SInt32:
|
| + case FieldType.SFixed32:
|
| + return (int) value;
|
| + case FieldType.UInt32:
|
| + case FieldType.Fixed32:
|
| + return (uint) value;
|
| + case FieldType.Int64:
|
| + case FieldType.SInt64:
|
| + case FieldType.SFixed64:
|
| + return (long) value;
|
| + case FieldType.UInt64:
|
| + case FieldType.Fixed64:
|
| + return (ulong) value;
|
| + case FieldType.Double:
|
| + return value;
|
| + case FieldType.Float:
|
| + if (double.IsNaN(value))
|
| + {
|
| + return float.NaN;
|
| + }
|
| + if (value > float.MaxValue || value < float.MinValue)
|
| + {
|
| + if (double.IsPositiveInfinity(value))
|
| + {
|
| + return float.PositiveInfinity;
|
| + }
|
| + if (double.IsNegativeInfinity(value))
|
| + {
|
| + return float.NegativeInfinity;
|
| + }
|
| + throw new InvalidProtocolBufferException("Value out of range: " + value);
|
| + }
|
| + return (float) value;
|
| + default:
|
| + throw new InvalidProtocolBufferException("Unsupported conversion from JSON number for field type " + field.FieldType);
|
| + }
|
| + }
|
| + catch (OverflowException)
|
| + {
|
| + throw new InvalidProtocolBufferException("Value out of range: " + value);
|
| + }
|
| + }
|
| + }
|
| +
|
| + private static object ParseSingleStringValue(FieldDescriptor field, string text)
|
| + {
|
| + switch (field.FieldType)
|
| + {
|
| + case FieldType.String:
|
| + return text;
|
| + case FieldType.Bytes:
|
| + return ByteString.FromBase64(text);
|
| + case FieldType.Int32:
|
| + case FieldType.SInt32:
|
| + case FieldType.SFixed32:
|
| + return ParseNumericString(text, int.Parse, false);
|
| + case FieldType.UInt32:
|
| + case FieldType.Fixed32:
|
| + return ParseNumericString(text, uint.Parse, false);
|
| + case FieldType.Int64:
|
| + case FieldType.SInt64:
|
| + case FieldType.SFixed64:
|
| + return ParseNumericString(text, long.Parse, false);
|
| + case FieldType.UInt64:
|
| + case FieldType.Fixed64:
|
| + return ParseNumericString(text, ulong.Parse, false);
|
| + 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);
|
| + }
|
| + 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);
|
| + }
|
| + 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);
|
| + }
|
| + // 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);
|
| + }
|
| + }
|
| +
|
| + /// <summary>
|
| + /// Creates a new instance of the message type for the given field.
|
| + /// </summary>
|
| + private static IMessage NewMessageForField(FieldDescriptor field)
|
| + {
|
| + return field.MessageType.Parser.CreateTemplate();
|
| + }
|
| +
|
| + private static T ParseNumericString<T>(string text, Func<string, NumberStyles, IFormatProvider, T> parser, bool floatingPoint)
|
| + {
|
| + // 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);
|
| + }
|
| + if (text.StartsWith("0") && text.Length > 1)
|
| + {
|
| + if (text[1] >= '0' && text[1] <= '9')
|
| + {
|
| + 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);
|
| + }
|
| + }
|
| + try
|
| + {
|
| + var styles = floatingPoint
|
| + ? NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent
|
| + : NumberStyles.AllowLeadingSign;
|
| + return parser(text, styles, CultureInfo.InvariantCulture);
|
| + }
|
| + catch (FormatException)
|
| + {
|
| + throw new InvalidProtocolBufferException("Invalid numeric value for type: " + text);
|
| + }
|
| + catch (OverflowException)
|
| + {
|
| + throw new InvalidProtocolBufferException("Value out of range: " + text);
|
| + }
|
| + }
|
| +
|
| + private static void MergeTimestamp(IMessage message, JsonToken token)
|
| + {
|
| + if (token.Type != JsonToken.TokenType.StringValue)
|
| + {
|
| + throw new InvalidProtocolBufferException("Expected string value for Timestamp");
|
| + }
|
| + var match = TimestampRegex.Match(token.StringValue);
|
| + if (!match.Success)
|
| + {
|
| + throw new InvalidProtocolBufferException("Invalid Timestamp value: " + token.StringValue);
|
| + }
|
| + var dateTime = match.Groups["datetime"].Value;
|
| + var subseconds = match.Groups["subseconds"].Value;
|
| + var offset = match.Groups["offset"].Value;
|
| +
|
| + try
|
| + {
|
| + DateTime parsed = DateTime.ParseExact(
|
| + dateTime,
|
| + "yyyy-MM-dd'T'HH:mm:ss",
|
| + CultureInfo.InvariantCulture,
|
| + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
|
| + // TODO: It would be nice not to have to create all these objects... easy to optimize later though.
|
| + Timestamp timestamp = Timestamp.FromDateTime(parsed);
|
| + int nanosToAdd = 0;
|
| + if (subseconds != "")
|
| + {
|
| + // This should always work, as we've got 1-9 digits.
|
| + int parsedFraction = int.Parse(subseconds.Substring(1), CultureInfo.InvariantCulture);
|
| + nanosToAdd = parsedFraction * SubsecondScalingFactors[subseconds.Length];
|
| + }
|
| + int secondsToAdd = 0;
|
| + if (offset != "Z")
|
| + {
|
| + // This is the amount we need to *subtract* from the local time to get to UTC - hence - => +1 and vice versa.
|
| + int sign = offset[0] == '-' ? 1 : -1;
|
| + int hours = int.Parse(offset.Substring(1, 2), CultureInfo.InvariantCulture);
|
| + int minutes = int.Parse(offset.Substring(4, 2));
|
| + int totalMinutes = hours * 60 + minutes;
|
| + if (totalMinutes > 18 * 60)
|
| + {
|
| + throw new InvalidProtocolBufferException("Invalid Timestamp value: " + token.StringValue);
|
| + }
|
| + if (totalMinutes == 0 && sign == 1)
|
| + {
|
| + // This is an offset of -00:00, which means "unknown local offset". It makes no sense for a timestamp.
|
| + throw new InvalidProtocolBufferException("Invalid Timestamp value: " + token.StringValue);
|
| + }
|
| + // We need to *subtract* the offset from local time to get UTC.
|
| + secondsToAdd = sign * totalMinutes * 60;
|
| + }
|
| + // Ensure we've got the right signs. Currently unnecessary, but easy to do.
|
| + if (secondsToAdd < 0 && nanosToAdd > 0)
|
| + {
|
| + secondsToAdd++;
|
| + nanosToAdd = nanosToAdd - Duration.NanosecondsPerSecond;
|
| + }
|
| + if (secondsToAdd != 0 || nanosToAdd != 0)
|
| + {
|
| + timestamp += new Duration { Nanos = nanosToAdd, Seconds = secondsToAdd };
|
| + // The resulting timestamp after offset change would be out of our expected range. Currently the Timestamp message doesn't validate this
|
| + // anywhere, but we shouldn't parse it.
|
| + if (timestamp.Seconds < Timestamp.UnixSecondsAtBclMinValue || timestamp.Seconds > Timestamp.UnixSecondsAtBclMaxValue)
|
| + {
|
| + throw new InvalidProtocolBufferException("Invalid Timestamp value: " + token.StringValue);
|
| + }
|
| + }
|
| + message.Descriptor.Fields[Timestamp.SecondsFieldNumber].Accessor.SetValue(message, timestamp.Seconds);
|
| + message.Descriptor.Fields[Timestamp.NanosFieldNumber].Accessor.SetValue(message, timestamp.Nanos);
|
| + }
|
| + catch (FormatException)
|
| + {
|
| + throw new InvalidProtocolBufferException("Invalid Timestamp value: " + token.StringValue);
|
| + }
|
| + }
|
| +
|
| + private static void MergeDuration(IMessage message, JsonToken token)
|
| + {
|
| + if (token.Type != JsonToken.TokenType.StringValue)
|
| + {
|
| + throw new InvalidProtocolBufferException("Expected string value for Duration");
|
| + }
|
| + var match = DurationRegex.Match(token.StringValue);
|
| + if (!match.Success)
|
| + {
|
| + throw new InvalidProtocolBufferException("Invalid Duration value: " + token.StringValue);
|
| + }
|
| + var sign = match.Groups["sign"].Value;
|
| + var secondsText = match.Groups["int"].Value;
|
| + // Prohibit leading insignficant zeroes
|
| + if (secondsText[0] == '0' && secondsText.Length > 1)
|
| + {
|
| + throw new InvalidProtocolBufferException("Invalid Duration value: " + token.StringValue);
|
| + }
|
| + var subseconds = match.Groups["subseconds"].Value;
|
| + var multiplier = sign == "-" ? -1 : 1;
|
| +
|
| + try
|
| + {
|
| + long seconds = long.Parse(secondsText, CultureInfo.InvariantCulture);
|
| + 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];
|
| + }
|
| + if (seconds >= Duration.MaxSeconds)
|
| + {
|
| + // Allow precisely 315576000000 seconds, but prohibit even 1ns more.
|
| + if (seconds > Duration.MaxSeconds || nanos > 0)
|
| + {
|
| + 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);
|
| + }
|
| + catch (FormatException)
|
| + {
|
| + throw new InvalidProtocolBufferException("Invalid Duration value: " + token.StringValue);
|
| + }
|
| + }
|
| +
|
| + private static void MergeFieldMask(IMessage message, JsonToken token)
|
| + {
|
| + if (token.Type != JsonToken.TokenType.StringValue)
|
| + {
|
| + throw new InvalidProtocolBufferException("Expected string value for FieldMask");
|
| + }
|
| + // TODO: Do we *want* to remove empty entries? Probably okay to treat "" as "no paths", but "foo,,bar"?
|
| + string[] jsonPaths = token.StringValue.Split(FieldMaskPathSeparators, StringSplitOptions.RemoveEmptyEntries);
|
| + IList messagePaths = (IList) message.Descriptor.Fields[FieldMask.PathsFieldNumber].Accessor.GetValue(message);
|
| + foreach (var path in jsonPaths)
|
| + {
|
| + messagePaths.Add(ToSnakeCase(path));
|
| + }
|
| + }
|
| +
|
| + // Ported from src/google/protobuf/util/internal/utility.cc
|
| + private static string ToSnakeCase(string text)
|
| + {
|
| + var builder = new StringBuilder(text.Length * 2);
|
| + bool wasNotUnderscore = false; // Initialize to false for case 1 (below)
|
| + bool wasNotCap = false;
|
| +
|
| + for (int i = 0; i < text.Length; i++)
|
| + {
|
| + char c = text[i];
|
| + if (c >= 'A' && c <= 'Z') // ascii_isupper
|
| + {
|
| + // Consider when the current character B is capitalized:
|
| + // 1) At beginning of input: "B..." => "b..."
|
| + // (e.g. "Biscuit" => "biscuit")
|
| + // 2) Following a lowercase: "...aB..." => "...a_b..."
|
| + // (e.g. "gBike" => "g_bike")
|
| + // 3) At the end of input: "...AB" => "...ab"
|
| + // (e.g. "GoogleLAB" => "google_lab")
|
| + // 4) Followed by a lowercase: "...ABc..." => "...a_bc..."
|
| + // (e.g. "GBike" => "g_bike")
|
| + if (wasNotUnderscore && // case 1 out
|
| + (wasNotCap || // case 2 in, case 3 out
|
| + (i + 1 < text.Length && // case 3 out
|
| + (text[i + 1] >= 'a' && text[i + 1] <= 'z')))) // ascii_islower(text[i + 1])
|
| + { // case 4 in
|
| + // We add an underscore for case 2 and case 4.
|
| + builder.Append('_');
|
| + }
|
| + // ascii_tolower, but we already know that c *is* an upper case ASCII character...
|
| + builder.Append((char) (c + 'a' - 'A'));
|
| + wasNotUnderscore = true;
|
| + wasNotCap = false;
|
| + }
|
| + else
|
| + {
|
| + builder.Append(c);
|
| + wasNotUnderscore = c != '_';
|
| + wasNotCap = true;
|
| + }
|
| + }
|
| + return builder.ToString();
|
| + }
|
| + #endregion
|
| +
|
| + /// <summary>
|
| + /// Settings controlling JSON parsing.
|
| + /// </summary>
|
| + public sealed class Settings
|
| + {
|
| + /// <summary>
|
| + /// Default settings, as used by <see cref="JsonParser.Default"/>. This has the same default
|
| + /// recursion limit as <see cref="CodedInputStream"/>, and an empty type registry.
|
| + /// </summary>
|
| + public static Settings Default { get; }
|
| +
|
| + // Workaround for the Mono compiler complaining about XML comments not being on
|
| + // valid language elements.
|
| + static Settings()
|
| + {
|
| + Default = new Settings(CodedInputStream.DefaultRecursionLimit);
|
| + }
|
| +
|
| + /// <summary>
|
| + /// The maximum depth of messages to parse. Note that this limit only applies to parsing
|
| + /// messages, not collections - so a message within a collection within a message only counts as
|
| + /// depth 2, not 3.
|
| + /// </summary>
|
| + public int RecursionLimit { get; }
|
| +
|
| + /// <summary>
|
| + /// The type registry used to parse <see cref="Any"/> messages.
|
| + /// </summary>
|
| + public TypeRegistry TypeRegistry { get; }
|
| +
|
| + /// <summary>
|
| + /// Creates a new <see cref="Settings"/> object with the specified recursion limit.
|
| + /// </summary>
|
| + /// <param name="recursionLimit">The maximum depth of messages to parse</param>
|
| + public Settings(int recursionLimit) : this(recursionLimit, TypeRegistry.Empty)
|
| + {
|
| + }
|
| +
|
| + /// <summary>
|
| + /// Creates a new <see cref="Settings"/> object with the specified recursion limit and type registry.
|
| + /// </summary>
|
| + /// <param name="recursionLimit">The maximum depth of messages to parse</param>
|
| + /// <param name="typeRegistry">The type registry used to parse <see cref="Any"/> messages</param>
|
| + public Settings(int recursionLimit, TypeRegistry typeRegistry)
|
| + {
|
| + RecursionLimit = recursionLimit;
|
| + TypeRegistry = Preconditions.CheckNotNull(typeRegistry, nameof(typeRegistry));
|
| + }
|
| + }
|
| + }
|
| +}
|
|
|