#Region "Microsoft.VisualBasic::9553a8c545a433ed8b4bb0982c1dd284, Google.Protobuf\JSON\JsonParser.vb" ' Author: ' ' asuka (amethyst.asuka@gcmodeller.org) ' xie (genetics@smrucc.org) ' xieguigang (xie.guigang@live.com) ' ' Copyright (c) 2018 GPL3 Licensed ' ' ' GNU GENERAL PUBLIC LICENSE (GPL3) ' ' ' This program is free software: you can redistribute it and/or modify ' it under the terms of the GNU General Public License as published by ' the Free Software Foundation, either version 3 of the License, or ' (at your option) any later version. ' ' This program is distributed in the hope that it will be useful, ' but WITHOUT ANY WARRANTY; without even the implied warranty of ' MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ' GNU General Public License for more details. ' ' You should have received a copy of the GNU General Public License ' along with this program. If not, see . ' /********************************************************************************/ ' Summaries: ' Class JsonParser ' ' Properties: [Default] ' ' Constructor: (+1 Overloads) Sub New ' ' Function: IsGoogleProtobufValueField, NewMessageForField, (+4 Overloads) Parse, ParseMapKey, ParseNumericString ' ParseSingleNumberValue, ParseSingleStringValue, ParseSingleValue, ToSnakeCase ' ' Sub: CheckInteger, (+3 Overloads) Merge, MergeAny, MergeDuration, MergeField ' MergeFieldMask, MergeMapField, MergeRepeatedField, MergeStruct, MergeStructValue ' MergeTimestamp, MergeWellKnownTypeAnyBody, MergeWrapperField, ValidateInfinityAndNan ' Class Settings ' ' Properties: [Default], RecursionLimit, TypeRegistry ' ' Constructor: (+3 Overloads) Sub New ' ' ' ' ' /********************************************************************************/ #End Region #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. #End Region Imports Google.Protobuf.Reflection Imports Google.Protobuf.WellKnownTypes Imports System Imports System.Collections Imports System.Collections.Generic Imports System.Globalization Imports System.IO Imports System.Text Imports System.Text.RegularExpressions Namespace Google.Protobuf ''' ''' Reflection-based converter from JSON to messages. ''' ''' ''' ''' Instances of this class are thread-safe, with no mutable state. ''' ''' ''' 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.) ''' ''' Public NotInheritable 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 Shared ReadOnly TimestampRegex As Regex = New Regex("^(?[0-9]{4}-[01][0-9]-[0-3][0-9]T[012][0-9]:[0-5][0-9]:[0-5][0-9])(?\.[0-9]{1,9})?(?(Z|[+-][0-1][0-9]:[0-5][0-9]))$", CompiledRegexWhereAvailable) Private Shared ReadOnly DurationRegex As Regex = New Regex("^(?-)?(?[0-9]{1,12})(?\.[0-9]{1,9})?s$", CompiledRegexWhereAvailable) Private Shared ReadOnly SubsecondScalingFactors As Integer() = {0, 100000000, 100000000, 10000000, 1000000, 100000, 10000, 1000, 100, 10, 1} Private Shared ReadOnly FieldMaskPathSeparators As Char() = {","c} Private Shared ReadOnly defaultInstance As JsonParser = 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 Shared ReadOnly WellKnownTypeHandlers As Dictionary(Of String, Action(Of JsonParser, IMessage, JsonTokenizer)) = New Dictionary(Of String, Action(Of JsonParser, IMessage, JsonTokenizer)) From { {Timestamp.DescriptorProp.FullName, Sub(parser, message, tokenizer) MergeTimestamp(message, tokenizer.Next())}, {Duration.DescriptorProp.FullName, Sub(parser, message, tokenizer) MergeDuration(message, tokenizer.Next())}, {Value.DescriptorProp.FullName, Sub(parser, message, tokenizer) parser.MergeStructValue(message, tokenizer)}, {ListValue.DescriptorProp.FullName, Sub(parser, message, tokenizer) parser.MergeRepeatedField(message, message.Descriptor.Fields(ListValue.ValuesFieldNumber), tokenizer)}, {Struct.DescriptorProp.FullName, Sub(parser, message, tokenizer) parser.MergeStruct(message, tokenizer)}, {Any.DescriptorProp.FullName, Sub(parser, message, tokenizer) parser.MergeAny(message, tokenizer)}, {FieldMask.DescriptorProp.FullName, Sub(parser, message, tokenizer) MergeFieldMask(message, tokenizer.Next())}, {Int32Value.DescriptorProp.FullName, AddressOf MergeWrapperField}, {Int64Value.DescriptorProp.FullName, AddressOf MergeWrapperField}, {UInt32Value.DescriptorProp.FullName, AddressOf MergeWrapperField}, {UInt64Value.DescriptorProp.FullName, AddressOf MergeWrapperField}, {FloatValue.DescriptorProp.FullName, AddressOf MergeWrapperField}, {DoubleValue.DescriptorProp.FullName, AddressOf MergeWrapperField}, {BytesValue.DescriptorProp.FullName, AddressOf MergeWrapperField}, {StringValue.DescriptorProp.FullName, AddressOf MergeWrapperField}, {BoolValue.DescriptorProp.FullName, AddressOf MergeWrapperField} } ' Convenience method to avoid having to repeat the same code multiple times in the above ' dictionary initialization. Private Shared Sub MergeWrapperField(parser As JsonParser, message As IMessage, tokenizer As JsonTokenizer) parser.MergeField(message, message.Descriptor.Fields(WrapperValueFieldNumber), tokenizer) End Sub ''' ''' Returns a formatter using the default settings. ''' Public Shared ReadOnly Property [Default] As JsonParser Get Return defaultInstance End Get End Property Private ReadOnly settingsField As Settings ''' ''' Creates a new formatted with the given settings. ''' ''' The settings. Public Sub New(settings As Settings) settingsField = settings End Sub ''' ''' Parses and merges the information into the given message. ''' ''' The message to merge the JSON information into. ''' The JSON to parse. Friend Sub Merge(message As IMessage, json As String) Merge(message, New StringReader(json)) End Sub ''' ''' Parses JSON read from and merges the information into the given message. ''' ''' The message to merge the JSON information into. ''' Reader providing the JSON to parse. Friend Sub Merge(message As IMessage, jsonReader As TextReader) Dim tokenizer = JsonTokenizer.FromTextReader(jsonReader) Merge(message, tokenizer) Dim lastToken = tokenizer.Next() If lastToken IsNot JsonToken.EndDocument Then Throw New InvalidProtocolBufferException("Expected end of JSON after object") End If End Sub ''' ''' 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". ''' Private Sub Merge(message As IMessage, tokenizer As JsonTokenizer) If tokenizer.ObjectDepth > settingsField.RecursionLimit Then Throw InvalidProtocolBufferException.JsonRecursionLimitExceeded() End If If message.Descriptor.IsWellKnownType Then Dim handler As Action(Of JsonParser, IMessage, JsonTokenizer) If WellKnownTypeHandlers.TryGetValue(message.Descriptor.FullName, handler) Then handler(Me, message, tokenizer) Return End If ' Well-known types with no special handling continue in the normal way. End If Dim token = tokenizer.Next() If token.Type <> JsonToken.TokenType.StartObject Then Throw New InvalidProtocolBufferException("Expected an object") End If Dim descriptor = message.Descriptor Dim 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. Dim seenOneofs As HashSet(Of OneofDescriptor) = Nothing While True token = tokenizer.Next() If token.Type = JsonToken.TokenType.EndObject Then Return End If If token.Type <> JsonToken.TokenType.Name Then Throw New InvalidOperationException("Unexpected token type " & token.Type) End If Dim name = token.StringValue Dim field As FieldDescriptor If jsonFieldMap.TryGetValue(name, field) Then If field.ContainingOneof IsNot Nothing Then If seenOneofs Is Nothing Then seenOneofs = New HashSet(Of OneofDescriptor)() End If If Not seenOneofs.Add(field.ContainingOneof) Then Throw New InvalidProtocolBufferException($"Multiple values specified for oneof {field.ContainingOneof.Name}") End If End If 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) End If End While End Sub Private Sub MergeField(message As IMessage, field As FieldDescriptor, tokenizer As JsonTokenizer) Dim token = tokenizer.Next() If token.Type = JsonToken.TokenType.Null Then ' 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... If field.IsMap OrElse field.IsRepeated OrElse Not IsGoogleProtobufValueField(field) Then field.Accessor.Clear(message) Return End If End If tokenizer.PushBack(token) If field.IsMap Then MergeMapField(message, field, tokenizer) ElseIf field.IsRepeated Then MergeRepeatedField(message, field, tokenizer) Else Dim value = ParseSingleValue(field, tokenizer) field.Accessor.SetValue(message, value) End If End Sub Private Sub MergeRepeatedField(message As IMessage, field As FieldDescriptor, tokenizer As JsonTokenizer) Dim token = tokenizer.Next() If token.Type <> JsonToken.TokenType.StartArray Then Throw New InvalidProtocolBufferException("Repeated field value was not an array. Token type: " & token.Type) End If Dim list = CType(field.Accessor.GetValue(message), IList) While True token = tokenizer.Next() If token.Type = JsonToken.TokenType.EndArray Then Return End If tokenizer.PushBack(token) If token.Type = JsonToken.TokenType.Null Then Throw New InvalidProtocolBufferException("Repeated field elements cannot be null") End If list.Add(ParseSingleValue(field, tokenizer)) End While End Sub Private Sub MergeMapField(message As IMessage, field As FieldDescriptor, tokenizer As JsonTokenizer) ' Map fields are always objects, even if the values are well-known types: ParseSingleValue handles those. Dim token = tokenizer.Next() If token.Type <> JsonToken.TokenType.StartObject Then Throw New InvalidProtocolBufferException("Expected an object to populate a map") End If Dim type = field.MessageType Dim keyField = type.FindFieldByNumber(1) Dim valueField = type.FindFieldByNumber(2) If keyField Is Nothing OrElse valueField Is Nothing Then Throw New InvalidProtocolBufferException("Invalid map field: " & field.FullName) End If Dim dictionary = CType(field.Accessor.GetValue(message), IDictionary) While True token = tokenizer.Next() If token.Type = JsonToken.TokenType.EndObject Then Return End If Dim key = ParseMapKey(keyField, token.StringValue) Dim value = ParseSingleValue(valueField, tokenizer) If value Is Nothing Then Throw New InvalidProtocolBufferException("Map values must not be null") End If dictionary(key) = value End While End Sub Private Shared Function IsGoogleProtobufValueField(field As FieldDescriptor) As Boolean Return field.FieldType = FieldType.Message AndAlso Equals(field.MessageType.FullName, Value.DescriptorProp.FullName) End Function Private Function ParseSingleValue(field As FieldDescriptor, tokenizer As JsonTokenizer) As Object Dim token = tokenizer.Next() If token.Type = JsonToken.TokenType.Null Then ' TODO: In order to support dynamic messages, we should really build this up ' dynamically. If IsGoogleProtobufValueField(field) Then Return Value.ForNull() End If Return Nothing End If Dim fieldType = field.FieldType If fieldType = FieldType.Message Then ' Parse wrapper types as their constituent types. ' TODO: What does this mean for null? If field.MessageType.IsWrapperType Then field = field.MessageType.Fields(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) Dim subMessage = NewMessageForField(field) Merge(subMessage, tokenizer) Return subMessage End If End If Select Case token.Type Case JsonToken.TokenType.True, JsonToken.TokenType.False If fieldType = FieldType.Bool Then Return token.Type = JsonToken.TokenType.True End If ' 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 _Select0_CaseDefault 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") Case Else _Select0_CaseDefault: Throw New InvalidProtocolBufferException("Unsupported JSON token type " & token.Type & " for field type " & fieldType) End Select End Function ''' ''' Parses into a new message. ''' ''' The type of message to create. ''' The JSON to parse. ''' The JSON does not comply with RFC 7159 ''' The JSON does not represent a Protocol Buffers message correctly Public Function Parse(Of T As {IMessage, New})(json As String) As T CheckNotNull(json, NameOf(json)) Return Parse(Of T)(New StringReader(json)) End Function ''' ''' Parses JSON read from into a new message. ''' ''' The type of message to create. ''' Reader providing the JSON to parse. ''' The JSON does not comply with RFC 7159 ''' The JSON does not represent a Protocol Buffers message correctly Public Function Parse(Of T As {IMessage, New})(jsonReader As TextReader) As T CheckNotNull(jsonReader, NameOf(jsonReader)) Dim message As T = New T() Merge(message, jsonReader) Return message End Function ''' ''' Parses into a new message. ''' ''' The JSON to parse. ''' Descriptor of message type to parse. ''' The JSON does not comply with RFC 7159 ''' The JSON does not represent a Protocol Buffers message correctly Public Function Parse(json As String, descriptor As MessageDescriptor) As IMessage CheckNotNull(json, NameOf(json)) CheckNotNull(descriptor, NameOf(descriptor)) Return Parse(New StringReader(json), descriptor) End Function ''' ''' Parses JSON read from into a new message. ''' ''' Reader providing the JSON to parse. ''' Descriptor of message type to parse. ''' The JSON does not comply with RFC 7159 ''' The JSON does not represent a Protocol Buffers message correctly Public Function Parse(jsonReader As TextReader, descriptor As MessageDescriptor) As IMessage CheckNotNull(jsonReader, NameOf(jsonReader)) CheckNotNull(descriptor, NameOf(descriptor)) Dim message As IMessage = descriptor.Parser.CreateTemplate() Merge(message, jsonReader) Return message End Function Private Sub MergeStructValue(message As IMessage, tokenizer As JsonTokenizer) Dim firstToken = tokenizer.Next() Dim fields = message.Descriptor.Fields Select Case 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, JsonToken.TokenType.True fields(Value.BoolValueFieldNumber).Accessor.SetValue(message, firstToken.Type = JsonToken.TokenType.True) Return Case JsonToken.TokenType.StartObject Dim field = fields(Value.StructValueFieldNumber) Dim structMessage = NewMessageForField(field) tokenizer.PushBack(firstToken) Merge(structMessage, tokenizer) field.Accessor.SetValue(message, structMessage) Return Case JsonToken.TokenType.StartArray Dim field = fields(Value.ListValueFieldNumber) Dim list = NewMessageForField(field) tokenizer.PushBack(firstToken) Merge(list, tokenizer) field.Accessor.SetValue(message, list) Return Case Else Throw New InvalidOperationException("Unexpected token type: " & firstToken.Type) End Select End Sub Private Sub MergeStruct(message As IMessage, tokenizer As JsonTokenizer) Dim token = tokenizer.Next() If token.Type <> JsonToken.TokenType.StartObject Then Throw New InvalidProtocolBufferException("Expected object value for Struct") End If tokenizer.PushBack(token) Dim field = message.Descriptor.Fields(Struct.FieldsFieldNumber) MergeMapField(message, field, tokenizer) End Sub Private Sub MergeAny(message As IMessage, tokenizer As JsonTokenizer) ' 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. Dim tokens = New List(Of JsonToken)() Dim token = tokenizer.Next() If token.Type <> JsonToken.TokenType.StartObject Then Throw New InvalidProtocolBufferException("Expected object value for Any") End If Dim 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 OrElse Not Equals(token.StringValue, JsonFormatter.AnyTypeUrlField) OrElse tokenizer.ObjectDepth <> typeUrlObjectDepth tokens.Add(token) token = tokenizer.Next() If tokenizer.ObjectDepth < typeUrlObjectDepth Then Throw New InvalidProtocolBufferException("Any message with no @type") End If End While ' Don't add the @type property or its value to the recorded token list token = tokenizer.Next() If token.Type <> JsonToken.TokenType.StringValue Then Throw New InvalidProtocolBufferException("Expected string value for Any.@type") End If Dim typeUrl = token.StringValue Dim typeName = Any.GetTypeName(typeUrl) Dim descriptor = settingsField.TypeRegistry.Find(typeName) If descriptor Is Nothing Then Throw New InvalidOperationException($"Type registry has no descriptor for type name '{typeName}'") End If ' 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. Dim replay = JsonTokenizer.FromReplayedTokens(tokens, tokenizer) Dim body = descriptor.Parser.CreateTemplate() If descriptor.IsWellKnownType Then MergeWellKnownTypeAnyBody(body, replay) Else Merge(body, replay) End If Dim 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) End Sub ' 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 Sub MergeWellKnownTypeAnyBody(body As IMessage, tokenizer As JsonTokenizer) Dim 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 OrElse Not Equals(token.StringValue, JsonFormatter.AnyWellKnownTypeValueField) Then Throw New InvalidProtocolBufferException($"Expected '{JsonFormatter.AnyWellKnownTypeValueField}' property for well-known type Any body") End If Merge(body, tokenizer) token = tokenizer.Next() If token.Type <> JsonToken.TokenType.EndObject Then Throw New InvalidProtocolBufferException($"Expected end-object token after @type/value for well-known type") End If End Sub #Region "Utility methods which don't depend on the state (or settings) of the parser." Private Shared Function ParseMapKey(field As FieldDescriptor, keyText As String) As Object Select Case field.FieldType Case FieldType.Bool If Equals(keyText, "true") Then Return True End If If Equals(keyText, "false") Then Return False End If Throw New InvalidProtocolBufferException("Invalid string for bool map key: " & keyText) Case FieldType.String Return keyText Case FieldType.Int32, FieldType.SInt32, FieldType.SFixed32 Return ParseNumericString(keyText, New Func(Of String, NumberStyles, IFormatProvider, Integer)(AddressOf Integer.Parse)) Case FieldType.UInt32, FieldType.Fixed32 Return ParseNumericString(keyText, New Func(Of String, NumberStyles, IFormatProvider, UInteger)(AddressOf UInteger.Parse)) Case FieldType.Int64, FieldType.SInt64, FieldType.SFixed64 Return ParseNumericString(keyText, New Func(Of String, NumberStyles, IFormatProvider, Long)(AddressOf Long.Parse)) Case FieldType.UInt64, FieldType.Fixed64 Return ParseNumericString(keyText, New Func(Of String, NumberStyles, IFormatProvider, ULong)(AddressOf ULong.Parse)) Case Else Throw New InvalidProtocolBufferException("Invalid field type for map: " & field.FieldType) End Select End Function Private Shared Function ParseSingleNumberValue(field As FieldDescriptor, token As JsonToken) As Object Dim value = token.NumberValue ' BEGIN TODO : Visual Basic does not support checked statements! Try Select Case field.FieldType Case FieldType.Int32, FieldType.SInt32, FieldType.SFixed32 CheckInteger(value) Return CInt(value) Case FieldType.UInt32, FieldType.Fixed32 CheckInteger(value) Return CUInt(value) Case FieldType.Int64, FieldType.SInt64, FieldType.SFixed64 CheckInteger(value) Return CLng(value) Case FieldType.UInt64, FieldType.Fixed64 CheckInteger(value) Return CULng(value) Case FieldType.Double Return value Case FieldType.Float If Double.IsNaN(value) Then Return Single.NaN End If If value > Single.MaxValue OrElse value < Single.MinValue Then If Double.IsPositiveInfinity(value) Then Return Single.PositiveInfinity End If If Double.IsNegativeInfinity(value) Then Return Single.NegativeInfinity End If Throw New InvalidProtocolBufferException($"Value out of range: {value}") End If Return CSng(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 CInt(value) Case Else Throw New InvalidProtocolBufferException($"Unsupported conversion from JSON number for field type {field.FieldType}") End Select Catch __unusedOverflowException1__ As OverflowException Throw New InvalidProtocolBufferException($"Value out of range: {value}") End Try ' END TODO : Visual Basic does not support checked statements! End Function Private Shared Sub CheckInteger(value As Double) If Double.IsInfinity(value) OrElse Double.IsNaN(value) Then Throw New InvalidProtocolBufferException($"Value not an integer: {value}") End If If value <> Math.Floor(value) Then Throw New InvalidProtocolBufferException($"Value not an integer: {value}") End If End Sub Private Shared Function ParseSingleStringValue(field As FieldDescriptor, text As String) As Object Select Case field.FieldType Case FieldType.String Return text Case FieldType.Bytes Try Return ByteString.FromBase64(text) Catch e As FormatException Throw InvalidProtocolBufferException.InvalidBase64(e) End Try Case FieldType.Int32, FieldType.SInt32, FieldType.SFixed32 Return ParseNumericString(text, New Func(Of String, NumberStyles, IFormatProvider, Integer)(AddressOf Integer.Parse)) Case FieldType.UInt32, FieldType.Fixed32 Return ParseNumericString(text, New Func(Of String, NumberStyles, IFormatProvider, UInteger)(AddressOf UInteger.Parse)) Case FieldType.Int64, FieldType.SInt64, FieldType.SFixed64 Return ParseNumericString(text, New Func(Of String, NumberStyles, IFormatProvider, Long)(AddressOf Long.Parse)) Case FieldType.UInt64, FieldType.Fixed64 Return ParseNumericString(text, New Func(Of String, NumberStyles, IFormatProvider, ULong)(AddressOf ULong.Parse)) Case FieldType.Double Dim d As Double = ParseNumericString(text, New Func(Of String, NumberStyles, IFormatProvider, Double)(AddressOf Double.Parse)) ValidateInfinityAndNan(text, Double.IsPositiveInfinity(d), Double.IsNegativeInfinity(d), Double.IsNaN(d)) Return d Case FieldType.Float Dim f As Single = ParseNumericString(text, New Func(Of String, NumberStyles, IFormatProvider, Single)(AddressOf Single.Parse)) ValidateInfinityAndNan(text, Single.IsPositiveInfinity(f), Single.IsNegativeInfinity(f), Single.IsNaN(f)) Return f Case FieldType.Enum Dim enumValue = field.EnumType.FindValueByName(text) If enumValue Is Nothing Then Throw New InvalidProtocolBufferException($"Invalid enum value: {text} for enum type: {field.EnumType.FullName}") End If ' Just return it as an int, and let the CLR convert it. Return enumValue.Number Case Else Throw New InvalidProtocolBufferException($"Unsupported conversion from JSON string for field type {field.FieldType}") End Select End Function ''' ''' Creates a new instance of the message type for the given field. ''' Private Shared Function NewMessageForField(field As FieldDescriptor) As IMessage Return field.MessageType.Parser.CreateTemplate() End Function Private Shared Function ParseNumericString(Of T)(text As String, parser As Func(Of String, NumberStyles, IFormatProvider, T)) As T ' Can't prohibit this with NumberStyles. If text.StartsWith("+") Then Throw New InvalidProtocolBufferException($"Invalid numeric value: {text}") End If If text.StartsWith("0") AndAlso text.Length > 1 Then If text(1) >= "0"c AndAlso text(1) <= "9"c Then Throw New InvalidProtocolBufferException($"Invalid numeric value: {text}") End If ElseIf text.StartsWith("-0") AndAlso text.Length > 2 Then If text(2) >= "0"c AndAlso text(2) <= "9"c Then Throw New InvalidProtocolBufferException($"Invalid numeric value: {text}") End If End If Try Return parser(text, NumberStyles.AllowLeadingSign Or NumberStyles.AllowDecimalPoint Or NumberStyles.AllowExponent, CultureInfo.InvariantCulture) Catch __unusedFormatException1__ As FormatException Throw New InvalidProtocolBufferException($"Invalid numeric value for type: {text}") Catch __unusedOverflowException2__ As OverflowException Throw New InvalidProtocolBufferException($"Value out of range: {text}") End Try End Function ''' ''' 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. ''' Private Shared Sub ValidateInfinityAndNan(text As String, isPositiveInfinity As Boolean, isNegativeInfinity As Boolean, isNaN As Boolean) If isPositiveInfinity AndAlso Not Equals(text, "Infinity") OrElse isNegativeInfinity AndAlso Not Equals(text, "-Infinity") OrElse isNaN AndAlso Not Equals(text, "NaN") Then Throw New InvalidProtocolBufferException($"Invalid numeric value: {text}") End If End Sub Private Shared Sub MergeTimestamp(message As IMessage, token As JsonToken) If token.Type <> JsonToken.TokenType.StringValue Then Throw New InvalidProtocolBufferException("Expected string value for Timestamp") End If Dim match = TimestampRegex.Match(token.StringValue) If Not match.Success Then Throw New InvalidProtocolBufferException($"Invalid Timestamp value: {token.StringValue}") End If Dim dateTime = match.Groups("datetime").Value Dim subseconds = match.Groups("subseconds").Value Dim offset = match.Groups("offset").Value Try Dim parsed = Date.ParseExact(dateTime, "yyyy-MM-dd'T'HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal Or DateTimeStyles.AdjustToUniversal) ' TODO: It would be nice not to have to create all these objects... easy to optimize later though. Dim timestamp As Timestamp = Timestamp.FromDateTime(parsed) Dim nanosToAdd = 0 If Not Equals(subseconds, "") Then ' This should always work, as we've got 1-9 digits. Dim parsedFraction = Integer.Parse(subseconds.Substring(1), CultureInfo.InvariantCulture) nanosToAdd = parsedFraction * SubsecondScalingFactors(subseconds.Length) End If Dim secondsToAdd = 0 If Not Equals(offset, "Z") Then ' This is the amount we need to *subtract* from the local time to get to UTC - hence - => +1 and vice versa. Dim sign = If(offset(0) = "-"c, 1, -1) Dim hours = Integer.Parse(offset.Substring(1, 2), CultureInfo.InvariantCulture) Dim minutes = Integer.Parse(offset.Substring(4, 2)) Dim totalMinutes = hours * 60 + minutes If totalMinutes > 18 * 60 Then Throw New InvalidProtocolBufferException("Invalid Timestamp value: " & token.StringValue) End If If totalMinutes = 0 AndAlso sign = 1 Then ' 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) End If ' We need to *subtract* the offset from local time to get UTC. secondsToAdd = sign * totalMinutes * 60 End If ' Ensure we've got the right signs. Currently unnecessary, but easy to do. If secondsToAdd < 0 AndAlso nanosToAdd > 0 Then secondsToAdd += 1 nanosToAdd = nanosToAdd - Duration.NanosecondsPerSecond End If If secondsToAdd <> 0 OrElse nanosToAdd <> 0 Then timestamp += New Duration With { .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 OrElse timestamp.Seconds > Timestamp.UnixSecondsAtBclMaxValue Then Throw New InvalidProtocolBufferException("Invalid Timestamp value: " & token.StringValue) End If End If message.Descriptor.Fields(Timestamp.SecondsFieldNumber).Accessor.SetValue(message, timestamp.Seconds) message.Descriptor.Fields(Timestamp.NanosFieldNumber).Accessor.SetValue(message, timestamp.Nanos) Catch __unusedFormatException1__ As FormatException Throw New InvalidProtocolBufferException("Invalid Timestamp value: " & token.StringValue) End Try End Sub Private Shared Sub MergeDuration(message As IMessage, token As JsonToken) If token.Type <> JsonToken.TokenType.StringValue Then Throw New InvalidProtocolBufferException("Expected string value for Duration") End If Dim match = DurationRegex.Match(token.StringValue) If Not match.Success Then Throw New InvalidProtocolBufferException("Invalid Duration value: " & token.StringValue) End If Dim sign = match.Groups("sign").Value Dim secondsText = match.Groups("int").Value ' Prohibit leading insignficant zeroes If secondsText(0) = "0"c AndAlso secondsText.Length > 1 Then Throw New InvalidProtocolBufferException("Invalid Duration value: " & token.StringValue) End If Dim subseconds = match.Groups("subseconds").Value Dim multiplier = If(Equals(sign, "-"), -1, 1) Try Dim seconds = Long.Parse(secondsText, CultureInfo.InvariantCulture) * multiplier Dim nanos = 0 If Not Equals(subseconds, "") Then ' This should always work, as we've got 1-9 digits. Dim parsedFraction = Integer.Parse(subseconds.Substring(1)) nanos = parsedFraction * SubsecondScalingFactors(subseconds.Length) * multiplier End If If Not Duration.IsNormalized(seconds, nanos) Then Throw New InvalidProtocolBufferException($"Invalid Duration value: {token.StringValue}") End If message.Descriptor.Fields(Duration.SecondsFieldNumber).Accessor.SetValue(message, seconds) message.Descriptor.Fields(Duration.NanosFieldNumber).Accessor.SetValue(message, nanos) Catch __unusedFormatException1__ As FormatException Throw New InvalidProtocolBufferException($"Invalid Duration value: {token.StringValue}") End Try End Sub Private Shared Sub MergeFieldMask(message As IMessage, token As JsonToken) If token.Type <> JsonToken.TokenType.StringValue Then Throw New InvalidProtocolBufferException("Expected string value for FieldMask") End If ' TODO: Do we *want* to remove empty entries? Probably okay to treat "" as "no paths", but "foo,,bar"? Dim jsonPaths = token.StringValue.Split(FieldMaskPathSeparators, StringSplitOptions.RemoveEmptyEntries) Dim messagePaths = CType(message.Descriptor.Fields(FieldMask.PathsFieldNumber).Accessor.GetValue(message), IList) For Each path In jsonPaths messagePaths.Add(ToSnakeCase(path)) Next End Sub ' Ported from src/google/protobuf/util/internal/utility.cc Private Shared Function ToSnakeCase(text As String) As String Dim 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. Dim wasNotUnderscore = False ' Initialize to false for case 1 (below) Dim wasNotCap = False For i = 0 To text.Length - 1 Dim c = text(i) If c >= "A"c AndAlso c <= "Z"c Then ' 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 AndAlso (wasNotCap OrElse i + 1 < text.Length AndAlso text(i + 1) >= "a"c AndAlso text(i + 1) <= "z"c) Then ' case 1 out ' case 2 in, case 3 out ' case 3 out ' ascii_islower(text[i + 1]) ' case 4 in ' We add an underscore for case 2 and case 4. builder.Append("_"c) End If ' ascii_tolower, but we already know that c *is* an upper case ASCII character... builder.Append(Microsoft.VisualBasic.ChrW(AscW(c) + AscW("a"c) - AscW("A"c))) wasNotUnderscore = True wasNotCap = False Else builder.Append(c) If c = "_"c Then Throw New InvalidProtocolBufferException($"Invalid field mask: {text}") End If wasNotUnderscore = True wasNotCap = True End If Next Return builder.ToString() End Function #End Region ''' ''' Settings controlling JSON parsing. ''' Public NotInheritable Class Settings ''' ''' Default settings, as used by . This has the same default ''' recursion limit as , and an empty type registry. ''' Public Shared ReadOnly Property [Default] As Settings ' Workaround for the Mono compiler complaining about XML comments not being on ' valid language elements. Shared Sub New() [Default] = New Settings(CodedInputStream.DefaultRecursionLimit) End Sub ''' ''' 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. ''' Public ReadOnly Property RecursionLimit As Integer ''' ''' The type registry used to parse messages. ''' Public ReadOnly Property TypeRegistry As TypeRegistry ''' ''' Creates a new object with the specified recursion limit. ''' ''' The maximum depth of messages to parse Public Sub New(recursionLimit As Integer) Me.New(recursionLimit, TypeRegistry.Empty) End Sub ''' ''' Creates a new object with the specified recursion limit and type registry. ''' ''' The maximum depth of messages to parse ''' The type registry used to parse messages Public Sub New(recursionLimit As Integer, typeRegistry As TypeRegistry) Me.RecursionLimit = recursionLimit Me.TypeRegistry = CheckNotNull(typeRegistry, NameOf(typeRegistry)) End Sub End Class End Class End Namespace