blob: 8da577450382d856f7df5026e8b2f59b74742367 [file] [log] [blame]
Jon Skeetfb248822015-09-04 12:41:14 +01001#region Copyright notice and license
2// Protocol Buffers - Google's data interchange format
3// Copyright 2015 Google Inc. All rights reserved.
4// https://developers.google.com/protocol-buffers/
5//
6// Redistribution and use in source and binary forms, with or without
7// modification, are permitted provided that the following conditions are
8// met:
9//
10// * Redistributions of source code must retain the above copyright
11// notice, this list of conditions and the following disclaimer.
12// * Redistributions in binary form must reproduce the above
13// copyright notice, this list of conditions and the following disclaimer
14// in the documentation and/or other materials provided with the
15// distribution.
16// * Neither the name of Google Inc. nor the names of its
17// contributors may be used to endorse or promote products derived from
18// this software without specific prior written permission.
19//
20// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31#endregion
32
33using Google.Protobuf.Reflection;
34using Google.Protobuf.WellKnownTypes;
35using System;
36using System.Collections;
37using System.Collections.Generic;
38using System.Globalization;
39using System.IO;
40using System.Linq;
41using System.Text;
42using System.Text.RegularExpressions;
43
44namespace Google.Protobuf
45{
46 /// <summary>
47 /// Reflection-based converter from JSON to messages.
48 /// </summary>
49 /// <remarks>
50 /// <para>
51 /// Instances of this class are thread-safe, with no mutable state.
52 /// </para>
53 /// <para>
54 /// This is a simple start to get JSON parsing working. As it's reflection-based,
55 /// it's not as quick as baking calls into generated messages - but is a simpler implementation.
56 /// (This code is generally not heavily optimized.)
57 /// </para>
58 /// </remarks>
59 public sealed class JsonParser
60 {
61 // Note: using 0-9 instead of \d to ensure no non-ASCII digits.
62 // This regex isn't a complete validator, but will remove *most* invalid input. We rely on parsing to do the rest.
63 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);
64 private static readonly Regex DurationRegex = new Regex(@"^(?<sign>-)?(?<int>[0-9]{1,12})(?<subseconds>\.[0-9]{1,9})?s$", FrameworkPortability.CompiledRegexWhereAvailable);
65 private static readonly int[] SubsecondScalingFactors = { 0, 100000000, 100000000, 10000000, 1000000, 100000, 10000, 1000, 100, 10, 1 };
66 private static readonly char[] FieldMaskPathSeparators = new[] { ',' };
67
68 private static readonly JsonParser defaultInstance = new JsonParser(Settings.Default);
69
70 private static readonly Dictionary<string, Action<JsonParser, IMessage, JsonTokenizer>>
71 WellKnownTypeHandlers = new Dictionary<string, Action<JsonParser, IMessage, JsonTokenizer>>
72 {
73 { Timestamp.Descriptor.FullName, (parser, message, tokenizer) => MergeTimestamp(message, tokenizer.Next()) },
74 { Duration.Descriptor.FullName, (parser, message, tokenizer) => MergeDuration(message, tokenizer.Next()) },
75 { Value.Descriptor.FullName, (parser, message, tokenizer) => parser.MergeStructValue(message, tokenizer) },
76 { ListValue.Descriptor.FullName, (parser, message, tokenizer) => parser.MergeRepeatedField(message, message.Descriptor.Fields[ListValue.ValuesFieldNumber], tokenizer) },
77 { Struct.Descriptor.FullName, (parser, message, tokenizer) => parser.MergeStruct(message, tokenizer) },
78 { FieldMask.Descriptor.FullName, (parser, message, tokenizer) => MergeFieldMask(message, tokenizer.Next()) },
79 { Int32Value.Descriptor.FullName, MergeWrapperField },
80 { Int64Value.Descriptor.FullName, MergeWrapperField },
81 { UInt32Value.Descriptor.FullName, MergeWrapperField },
82 { UInt64Value.Descriptor.FullName, MergeWrapperField },
83 { FloatValue.Descriptor.FullName, MergeWrapperField },
84 { DoubleValue.Descriptor.FullName, MergeWrapperField },
85 { BytesValue.Descriptor.FullName, MergeWrapperField },
86 { StringValue.Descriptor.FullName, MergeWrapperField }
87 };
88
89 // Convenience method to avoid having to repeat the same code multiple times in the above
90 // dictionary initialization.
91 private static void MergeWrapperField(JsonParser parser, IMessage message, JsonTokenizer tokenizer)
92 {
93 parser.MergeField(message, message.Descriptor.Fields[Wrappers.WrapperValueFieldNumber], tokenizer);
94 }
95
96 /// <summary>
97 /// Returns a formatter using the default settings. /// </summary>
98 public static JsonParser Default { get { return defaultInstance; } }
99
100// Currently the settings are unused.
101// TODO: When we've implemented Any (and the json spec is finalized), revisit whether they're
102// needed at all.
103#pragma warning disable 0414
104 private readonly Settings settings;
105#pragma warning restore 0414
106
107 /// <summary>
108 /// Creates a new formatted with the given settings.
109 /// </summary>
110 /// <param name="settings">The settings.</param>
111 public JsonParser(Settings settings)
112 {
113 this.settings = settings;
114 }
115
116 /// <summary>
117 /// Parses <paramref name="json"/> and merges the information into the given message.
118 /// </summary>
119 /// <param name="message">The message to merge the JSON information into.</param>
120 /// <param name="json">The JSON to parse.</param>
121 internal void Merge(IMessage message, string json)
122 {
123 Merge(message, new StringReader(json));
124 }
125
126 /// <summary>
127 /// Parses JSON read from <paramref name="jsonReader"/> and merges the information into the given message.
128 /// </summary>
129 /// <param name="message">The message to merge the JSON information into.</param>
130 /// <param name="jsonReader">Reader providing the JSON to parse.</param>
131 internal void Merge(IMessage message, TextReader jsonReader)
132 {
133 var tokenizer = new JsonTokenizer(jsonReader);
134 Merge(message, tokenizer);
135 var lastToken = tokenizer.Next();
136 if (lastToken != JsonToken.EndDocument)
137 {
138 throw new InvalidProtocolBufferException("Expected end of JSON after object");
139 }
140 }
141
142 /// <summary>
143 /// Merges the given message using data from the given tokenizer. In most cases, the next
144 /// token should be a "start object" token, but wrapper types and nullity can invalidate
145 /// that assumption. This is implemented as an LL(1) recursive descent parser over the stream
146 /// of tokens provided by the tokenizer. This token stream is assumed to be valid JSON, with the
147 /// tokenizer performing that validation - but not every token stream is valid "protobuf JSON".
148 /// </summary>
149 private void Merge(IMessage message, JsonTokenizer tokenizer)
150 {
151 if (message.Descriptor.IsWellKnownType)
152 {
153 Action<JsonParser, IMessage, JsonTokenizer> handler;
154 if (WellKnownTypeHandlers.TryGetValue(message.Descriptor.FullName, out handler))
155 {
156 handler(this, message, tokenizer);
157 return;
158 }
159 // Well-known types with no special handling continue in the normal way.
160 }
161 var token = tokenizer.Next();
162 if (token.Type != JsonToken.TokenType.StartObject)
163 {
164 throw new InvalidProtocolBufferException("Expected an object");
165 }
166 var descriptor = message.Descriptor;
167 // TODO: Make this more efficient, e.g. by building it once in the descriptor.
168 // Additionally, we need to consider whether to parse field names in their original proto form,
169 // and any overrides in the descriptor. But yes, all of this should be in the descriptor somehow...
170 // the descriptor can expose the dictionary.
171 var jsonFieldMap = descriptor.Fields.InDeclarationOrder().ToDictionary(field => JsonFormatter.ToCamelCase(field.Name));
172 while (true)
173 {
174 token = tokenizer.Next();
175 if (token.Type == JsonToken.TokenType.EndObject)
176 {
177 return;
178 }
179 if (token.Type != JsonToken.TokenType.Name)
180 {
181 throw new InvalidOperationException("Unexpected token type " + token.Type);
182 }
183 string name = token.StringValue;
184 FieldDescriptor field;
185 if (jsonFieldMap.TryGetValue(name, out field))
186 {
187 MergeField(message, field, tokenizer);
188 }
189 else
190 {
191 // TODO: Is this what we want to do? If not, we'll need to skip the value,
192 // which may be an object or array. (We might want to put code in the tokenizer
193 // to do that.)
194 throw new InvalidProtocolBufferException("Unknown field: " + name);
195 }
196 }
197 }
198
199 private void MergeField(IMessage message, FieldDescriptor field, JsonTokenizer tokenizer)
200 {
201 var token = tokenizer.Next();
202 if (token.Type == JsonToken.TokenType.Null)
203 {
204 // Note: different from Java API, which just ignores it.
205 // TODO: Bring it more in line? Discuss...
206 field.Accessor.Clear(message);
207 return;
208 }
209 tokenizer.PushBack(token);
210
211 if (field.IsMap)
212 {
213 MergeMapField(message, field, tokenizer);
214 }
215 else if (field.IsRepeated)
216 {
217 MergeRepeatedField(message, field, tokenizer);
218 }
219 else
220 {
221 var value = ParseSingleValue(field, tokenizer);
222 field.Accessor.SetValue(message, value);
223 }
224 }
225
226 private void MergeRepeatedField(IMessage message, FieldDescriptor field, JsonTokenizer tokenizer)
227 {
228 var token = tokenizer.Next();
229 if (token.Type != JsonToken.TokenType.StartArray)
230 {
231 throw new InvalidProtocolBufferException("Repeated field value was not an array. Token type: " + token.Type);
232 }
233
234 IList list = (IList) field.Accessor.GetValue(message);
235 while (true)
236 {
237 token = tokenizer.Next();
238 if (token.Type == JsonToken.TokenType.EndArray)
239 {
240 return;
241 }
242 tokenizer.PushBack(token);
243 list.Add(ParseSingleValue(field, tokenizer));
244 }
245 }
246
247 private void MergeMapField(IMessage message, FieldDescriptor field, JsonTokenizer tokenizer)
248 {
249 // Map fields are always objects, even if the values are well-known types: ParseSingleValue handles those.
250 var token = tokenizer.Next();
251 if (token.Type != JsonToken.TokenType.StartObject)
252 {
253 throw new InvalidProtocolBufferException("Expected an object to populate a map");
254 }
255
256 var type = field.MessageType;
257 var keyField = type.FindFieldByNumber(1);
258 var valueField = type.FindFieldByNumber(2);
259 if (keyField == null || valueField == null)
260 {
261 throw new InvalidProtocolBufferException("Invalid map field: " + field.FullName);
262 }
263 IDictionary dictionary = (IDictionary) field.Accessor.GetValue(message);
264
265 while (true)
266 {
267 token = tokenizer.Next();
268 if (token.Type == JsonToken.TokenType.EndObject)
269 {
270 return;
271 }
272 object key = ParseMapKey(keyField, token.StringValue);
273 object value = ParseSingleValue(valueField, tokenizer);
274 // TODO: Null handling
275 dictionary[key] = value;
276 }
277 }
278
279 private object ParseSingleValue(FieldDescriptor field, JsonTokenizer tokenizer)
280 {
281 var token = tokenizer.Next();
282 if (token.Type == JsonToken.TokenType.Null)
283 {
284 if (field.FieldType == FieldType.Message && field.MessageType.FullName == Value.Descriptor.FullName)
285 {
286 return new Value { NullValue = NullValue.NULL_VALUE };
287 }
288 return null;
289 }
290
291 var fieldType = field.FieldType;
292 if (fieldType == FieldType.Message)
293 {
294 // Parse wrapper types as their constituent types.
295 // TODO: What does this mean for null?
296 // TODO: Detect this differently when we have dynamic messages, and put it in one place...
297 if (field.MessageType.IsWellKnownType && field.MessageType.File == Int32Value.Descriptor.File)
298 {
299 field = field.MessageType.Fields[Wrappers.WrapperValueFieldNumber];
300 fieldType = field.FieldType;
301 }
302 else
303 {
304 // TODO: Merge the current value in message? (Public API currently doesn't make this relevant as we don't expose merging.)
305 tokenizer.PushBack(token);
306 IMessage subMessage = NewMessageForField(field);
307 Merge(subMessage, tokenizer);
308 return subMessage;
309 }
310 }
311
312 switch (token.Type)
313 {
314 case JsonToken.TokenType.True:
315 case JsonToken.TokenType.False:
316 if (fieldType == FieldType.Bool)
317 {
318 return token.Type == JsonToken.TokenType.True;
319 }
320 // Fall through to "we don't support this type for this case"; could duplicate the behaviour of the default
321 // case instead, but this way we'd only need to change one place.
322 goto default;
323 case JsonToken.TokenType.StringValue:
324 return ParseSingleStringValue(field, token.StringValue);
325 // Note: not passing the number value itself here, as we may end up storing the string value in the token too.
326 case JsonToken.TokenType.Number:
327 return ParseSingleNumberValue(field, token);
328 case JsonToken.TokenType.Null:
329 throw new NotImplementedException("Haven't worked out what to do for null yet");
330 default:
331 throw new InvalidProtocolBufferException("Unsupported JSON token type " + token.Type + " for field type " + fieldType);
332 }
333 }
334
335 /// <summary>
336 /// Parses <paramref name="json"/> into a new message.
337 /// </summary>
338 /// <typeparam name="T">The type of message to create.</typeparam>
339 /// <param name="json">The JSON to parse.</param>
Jon Skeet0fb39c42015-11-04 11:49:15 +0000340 /// <exception cref="InvalidJsonException">The JSON does not comply with RFC 7159</exception>
341 /// <exception cref="InvalidProtocolBufferException">The JSON does not represent a Protocol Buffers message correctly</exception>
Jon Skeetfb248822015-09-04 12:41:14 +0100342 public T Parse<T>(string json) where T : IMessage, new()
343 {
344 return Parse<T>(new StringReader(json));
345 }
346
347 /// <summary>
348 /// Parses JSON read from <paramref name="jsonReader"/> into a new message.
349 /// </summary>
350 /// <typeparam name="T">The type of message to create.</typeparam>
351 /// <param name="jsonReader">Reader providing the JSON to parse.</param>
Jon Skeet0fb39c42015-11-04 11:49:15 +0000352 /// <exception cref="InvalidJsonException">The JSON does not comply with RFC 7159</exception>
353 /// <exception cref="InvalidProtocolBufferException">The JSON does not represent a Protocol Buffers message correctly</exception>
Jon Skeetfb248822015-09-04 12:41:14 +0100354 public T Parse<T>(TextReader jsonReader) where T : IMessage, new()
355 {
356 T message = new T();
357 Merge(message, jsonReader);
358 return message;
359 }
360
361 private void MergeStructValue(IMessage message, JsonTokenizer tokenizer)
362 {
363 var firstToken = tokenizer.Next();
364 var fields = message.Descriptor.Fields;
365 switch (firstToken.Type)
366 {
367 case JsonToken.TokenType.Null:
368 fields[Value.NullValueFieldNumber].Accessor.SetValue(message, 0);
369 return;
370 case JsonToken.TokenType.StringValue:
371 fields[Value.StringValueFieldNumber].Accessor.SetValue(message, firstToken.StringValue);
372 return;
373 case JsonToken.TokenType.Number:
374 fields[Value.NumberValueFieldNumber].Accessor.SetValue(message, firstToken.NumberValue);
375 return;
376 case JsonToken.TokenType.False:
377 case JsonToken.TokenType.True:
378 fields[Value.BoolValueFieldNumber].Accessor.SetValue(message, firstToken.Type == JsonToken.TokenType.True);
379 return;
380 case JsonToken.TokenType.StartObject:
381 {
382 var field = fields[Value.StructValueFieldNumber];
383 var structMessage = NewMessageForField(field);
384 tokenizer.PushBack(firstToken);
385 Merge(structMessage, tokenizer);
386 field.Accessor.SetValue(message, structMessage);
387 return;
388 }
389 case JsonToken.TokenType.StartArray:
390 {
391 var field = fields[Value.ListValueFieldNumber];
392 var list = NewMessageForField(field);
393 tokenizer.PushBack(firstToken);
394 Merge(list, tokenizer);
395 field.Accessor.SetValue(message, list);
396 return;
397 }
398 default:
399 throw new InvalidOperationException("Unexpected token type: " + firstToken.Type);
400 }
401 }
402
403 private void MergeStruct(IMessage message, JsonTokenizer tokenizer)
404 {
405 var token = tokenizer.Next();
406 if (token.Type != JsonToken.TokenType.StartObject)
407 {
408 throw new InvalidProtocolBufferException("Expected object value for Struct");
409 }
410 tokenizer.PushBack(token);
411
412 var field = message.Descriptor.Fields[Struct.FieldsFieldNumber];
413 MergeMapField(message, field, tokenizer);
414 }
415
416 #region Utility methods which don't depend on the state (or settings) of the parser.
417 private static object ParseMapKey(FieldDescriptor field, string keyText)
418 {
419 switch (field.FieldType)
420 {
421 case FieldType.Bool:
422 if (keyText == "true")
423 {
424 return true;
425 }
426 if (keyText == "false")
427 {
428 return false;
429 }
430 throw new InvalidProtocolBufferException("Invalid string for bool map key: " + keyText);
431 case FieldType.String:
432 return keyText;
433 case FieldType.Int32:
434 case FieldType.SInt32:
435 case FieldType.SFixed32:
436 return ParseNumericString(keyText, int.Parse, false);
437 case FieldType.UInt32:
438 case FieldType.Fixed32:
439 return ParseNumericString(keyText, uint.Parse, false);
440 case FieldType.Int64:
441 case FieldType.SInt64:
442 case FieldType.SFixed64:
443 return ParseNumericString(keyText, long.Parse, false);
444 case FieldType.UInt64:
445 case FieldType.Fixed64:
446 return ParseNumericString(keyText, ulong.Parse, false);
447 default:
448 throw new InvalidProtocolBufferException("Invalid field type for map: " + field.FieldType);
449 }
450 }
451
452 private static object ParseSingleNumberValue(FieldDescriptor field, JsonToken token)
453 {
454 double value = token.NumberValue;
455 checked
456 {
457 // TODO: Validate that it's actually an integer, possibly in terms of the textual representation?
458 try
459 {
460 switch (field.FieldType)
461 {
462 case FieldType.Int32:
463 case FieldType.SInt32:
464 case FieldType.SFixed32:
465 return (int) value;
466 case FieldType.UInt32:
467 case FieldType.Fixed32:
468 return (uint) value;
469 case FieldType.Int64:
470 case FieldType.SInt64:
471 case FieldType.SFixed64:
472 return (long) value;
473 case FieldType.UInt64:
474 case FieldType.Fixed64:
475 return (ulong) value;
476 case FieldType.Double:
477 return value;
478 case FieldType.Float:
479 if (double.IsNaN(value))
480 {
481 return float.NaN;
482 }
483 if (value > float.MaxValue || value < float.MinValue)
484 {
485 if (double.IsPositiveInfinity(value))
486 {
487 return float.PositiveInfinity;
488 }
489 if (double.IsNegativeInfinity(value))
490 {
491 return float.NegativeInfinity;
492 }
493 throw new InvalidProtocolBufferException("Value out of range: " + value);
494 }
495 return (float) value;
496 default:
497 throw new InvalidProtocolBufferException("Unsupported conversion from JSON number for field type " + field.FieldType);
498 }
499 }
500 catch (OverflowException)
501 {
502 throw new InvalidProtocolBufferException("Value out of range: " + value);
503 }
504 }
505 }
506
507 private static object ParseSingleStringValue(FieldDescriptor field, string text)
508 {
509 switch (field.FieldType)
510 {
511 case FieldType.String:
512 return text;
513 case FieldType.Bytes:
514 return ByteString.FromBase64(text);
515 case FieldType.Int32:
516 case FieldType.SInt32:
517 case FieldType.SFixed32:
518 return ParseNumericString(text, int.Parse, false);
519 case FieldType.UInt32:
520 case FieldType.Fixed32:
521 return ParseNumericString(text, uint.Parse, false);
522 case FieldType.Int64:
523 case FieldType.SInt64:
524 case FieldType.SFixed64:
525 return ParseNumericString(text, long.Parse, false);
526 case FieldType.UInt64:
527 case FieldType.Fixed64:
528 return ParseNumericString(text, ulong.Parse, false);
529 case FieldType.Double:
530 double d = ParseNumericString(text, double.Parse, true);
531 // double.Parse can return +/- infinity on Mono for non-infinite values which are out of range for double.
532 if (double.IsInfinity(d) && !text.Contains("Infinity"))
533 {
534 throw new InvalidProtocolBufferException("Invalid numeric value: " + text);
535 }
536 return d;
537 case FieldType.Float:
538 float f = ParseNumericString(text, float.Parse, true);
539 // float.Parse can return +/- infinity on Mono for non-infinite values which are out of range for float.
540 if (float.IsInfinity(f) && !text.Contains("Infinity"))
541 {
542 throw new InvalidProtocolBufferException("Invalid numeric value: " + text);
543 }
544 return f;
545 case FieldType.Enum:
546 var enumValue = field.EnumType.FindValueByName(text);
547 if (enumValue == null)
548 {
549 throw new InvalidProtocolBufferException("Invalid enum value: " + text + " for enum type: " + field.EnumType.FullName);
550 }
551 // Just return it as an int, and let the CLR convert it.
552 return enumValue.Number;
553 default:
554 throw new InvalidProtocolBufferException("Unsupported conversion from JSON string for field type " + field.FieldType);
555 }
556 }
557
558 /// <summary>
559 /// Creates a new instance of the message type for the given field.
560 /// This method is mostly extracted so we can replace it in one go when we work out
561 /// what we want to do instead of Activator.CreateInstance.
562 /// </summary>
563 private static IMessage NewMessageForField(FieldDescriptor field)
564 {
565 // TODO: Create an instance in a better way ?
566 // (We could potentially add a Parser property to MessageDescriptor... see issue 806.)
567 return (IMessage) Activator.CreateInstance(field.MessageType.GeneratedType);
568 }
569
570 private static T ParseNumericString<T>(string text, Func<string, NumberStyles, IFormatProvider, T> parser, bool floatingPoint)
571 {
572 // TODO: Prohibit leading zeroes (but allow 0!)
573 // TODO: Validate handling of "Infinity" etc. (Should be case sensitive, no leading whitespace etc)
574 // Can't prohibit this with NumberStyles.
575 if (text.StartsWith("+"))
576 {
577 throw new InvalidProtocolBufferException("Invalid numeric value: " + text);
578 }
579 if (text.StartsWith("0") && text.Length > 1)
580 {
581 if (text[1] >= '0' && text[1] <= '9')
582 {
583 throw new InvalidProtocolBufferException("Invalid numeric value: " + text);
584 }
585 }
586 else if (text.StartsWith("-0") && text.Length > 2)
587 {
588 if (text[2] >= '0' && text[2] <= '9')
589 {
590 throw new InvalidProtocolBufferException("Invalid numeric value: " + text);
591 }
592 }
593 try
594 {
595 var styles = floatingPoint
596 ? NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent
597 : NumberStyles.AllowLeadingSign;
598 return parser(text, styles, CultureInfo.InvariantCulture);
599 }
600 catch (FormatException)
601 {
602 throw new InvalidProtocolBufferException("Invalid numeric value for type: " + text);
603 }
604 catch (OverflowException)
605 {
606 throw new InvalidProtocolBufferException("Value out of range: " + text);
607 }
608 }
609
610 private static void MergeTimestamp(IMessage message, JsonToken token)
611 {
612 if (token.Type != JsonToken.TokenType.StringValue)
613 {
614 throw new InvalidProtocolBufferException("Expected string value for Timestamp");
615 }
616 var match = TimestampRegex.Match(token.StringValue);
617 if (!match.Success)
618 {
619 throw new InvalidProtocolBufferException("Invalid Timestamp value: " + token.StringValue);
620 }
621 var dateTime = match.Groups["datetime"].Value;
622 var subseconds = match.Groups["subseconds"].Value;
623 var offset = match.Groups["offset"].Value;
624
625 try
626 {
627 DateTime parsed = DateTime.ParseExact(
628 dateTime,
629 "yyyy-MM-dd'T'HH:mm:ss",
630 CultureInfo.InvariantCulture,
631 DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
632 // TODO: It would be nice not to have to create all these objects... easy to optimize later though.
633 Timestamp timestamp = Timestamp.FromDateTime(parsed);
634 int nanosToAdd = 0;
635 if (subseconds != "")
636 {
637 // This should always work, as we've got 1-9 digits.
638 int parsedFraction = int.Parse(subseconds.Substring(1), CultureInfo.InvariantCulture);
639 nanosToAdd = parsedFraction * SubsecondScalingFactors[subseconds.Length];
640 }
641 int secondsToAdd = 0;
642 if (offset != "Z")
643 {
644 // This is the amount we need to *subtract* from the local time to get to UTC - hence - => +1 and vice versa.
645 int sign = offset[0] == '-' ? 1 : -1;
646 int hours = int.Parse(offset.Substring(1, 2), CultureInfo.InvariantCulture);
647 int minutes = int.Parse(offset.Substring(4, 2));
648 int totalMinutes = hours * 60 + minutes;
649 if (totalMinutes > 18 * 60)
650 {
651 throw new InvalidProtocolBufferException("Invalid Timestamp value: " + token.StringValue);
652 }
653 if (totalMinutes == 0 && sign == 1)
654 {
655 // This is an offset of -00:00, which means "unknown local offset". It makes no sense for a timestamp.
656 throw new InvalidProtocolBufferException("Invalid Timestamp value: " + token.StringValue);
657 }
658 // We need to *subtract* the offset from local time to get UTC.
659 secondsToAdd = sign * totalMinutes * 60;
660 }
661 // Ensure we've got the right signs. Currently unnecessary, but easy to do.
662 if (secondsToAdd < 0 && nanosToAdd > 0)
663 {
664 secondsToAdd++;
665 nanosToAdd = nanosToAdd - Duration.NanosecondsPerSecond;
666 }
667 if (secondsToAdd != 0 || nanosToAdd != 0)
668 {
669 timestamp += new Duration { Nanos = nanosToAdd, Seconds = secondsToAdd };
670 // The resulting timestamp after offset change would be out of our expected range. Currently the Timestamp message doesn't validate this
671 // anywhere, but we shouldn't parse it.
672 if (timestamp.Seconds < Timestamp.UnixSecondsAtBclMinValue || timestamp.Seconds > Timestamp.UnixSecondsAtBclMaxValue)
673 {
674 throw new InvalidProtocolBufferException("Invalid Timestamp value: " + token.StringValue);
675 }
676 }
677 message.Descriptor.Fields[Timestamp.SecondsFieldNumber].Accessor.SetValue(message, timestamp.Seconds);
678 message.Descriptor.Fields[Timestamp.NanosFieldNumber].Accessor.SetValue(message, timestamp.Nanos);
679 }
680 catch (FormatException)
681 {
682 throw new InvalidProtocolBufferException("Invalid Timestamp value: " + token.StringValue);
683 }
684 }
685
686 private static void MergeDuration(IMessage message, JsonToken token)
687 {
688 if (token.Type != JsonToken.TokenType.StringValue)
689 {
690 throw new InvalidProtocolBufferException("Expected string value for Duration");
691 }
692 var match = DurationRegex.Match(token.StringValue);
693 if (!match.Success)
694 {
695 throw new InvalidProtocolBufferException("Invalid Duration value: " + token.StringValue);
696 }
697 var sign = match.Groups["sign"].Value;
698 var secondsText = match.Groups["int"].Value;
699 // Prohibit leading insignficant zeroes
700 if (secondsText[0] == '0' && secondsText.Length > 1)
701 {
702 throw new InvalidProtocolBufferException("Invalid Duration value: " + token.StringValue);
703 }
704 var subseconds = match.Groups["subseconds"].Value;
705 var multiplier = sign == "-" ? -1 : 1;
706
707 try
708 {
709 long seconds = long.Parse(secondsText, CultureInfo.InvariantCulture);
710 int nanos = 0;
711 if (subseconds != "")
712 {
713 // This should always work, as we've got 1-9 digits.
714 int parsedFraction = int.Parse(subseconds.Substring(1));
715 nanos = parsedFraction * SubsecondScalingFactors[subseconds.Length];
716 }
717 if (seconds >= Duration.MaxSeconds)
718 {
719 // Allow precisely 315576000000 seconds, but prohibit even 1ns more.
720 if (seconds > Duration.MaxSeconds || nanos > 0)
721 {
722 throw new InvalidProtocolBufferException("Invalid Duration value: " + token.StringValue);
723 }
724 }
725 message.Descriptor.Fields[Duration.SecondsFieldNumber].Accessor.SetValue(message, seconds * multiplier);
726 message.Descriptor.Fields[Duration.NanosFieldNumber].Accessor.SetValue(message, nanos * multiplier);
727 }
728 catch (FormatException)
729 {
730 throw new InvalidProtocolBufferException("Invalid Duration value: " + token.StringValue);
731 }
732 }
733
734 private static void MergeFieldMask(IMessage message, JsonToken token)
735 {
736 if (token.Type != JsonToken.TokenType.StringValue)
737 {
738 throw new InvalidProtocolBufferException("Expected string value for FieldMask");
739 }
740 // TODO: Do we *want* to remove empty entries? Probably okay to treat "" as "no paths", but "foo,,bar"?
741 string[] jsonPaths = token.StringValue.Split(FieldMaskPathSeparators, StringSplitOptions.RemoveEmptyEntries);
742 IList messagePaths = (IList) message.Descriptor.Fields[FieldMask.PathsFieldNumber].Accessor.GetValue(message);
743 foreach (var path in jsonPaths)
744 {
745 messagePaths.Add(ToSnakeCase(path));
746 }
747 }
748
749 // Ported from src/google/protobuf/util/internal/utility.cc
750 private static string ToSnakeCase(string text)
751 {
752 var builder = new StringBuilder(text.Length * 2);
753 bool wasNotUnderscore = false; // Initialize to false for case 1 (below)
754 bool wasNotCap = false;
755
756 for (int i = 0; i < text.Length; i++)
757 {
758 char c = text[i];
759 if (c >= 'A' && c <= 'Z') // ascii_isupper
760 {
761 // Consider when the current character B is capitalized:
762 // 1) At beginning of input: "B..." => "b..."
763 // (e.g. "Biscuit" => "biscuit")
764 // 2) Following a lowercase: "...aB..." => "...a_b..."
765 // (e.g. "gBike" => "g_bike")
766 // 3) At the end of input: "...AB" => "...ab"
767 // (e.g. "GoogleLAB" => "google_lab")
768 // 4) Followed by a lowercase: "...ABc..." => "...a_bc..."
769 // (e.g. "GBike" => "g_bike")
770 if (wasNotUnderscore && // case 1 out
771 (wasNotCap || // case 2 in, case 3 out
772 (i + 1 < text.Length && // case 3 out
773 (text[i + 1] >= 'a' && text[i + 1] <= 'z')))) // ascii_islower(text[i + 1])
774 { // case 4 in
775 // We add an underscore for case 2 and case 4.
776 builder.Append('_');
777 }
778 // ascii_tolower, but we already know that c *is* an upper case ASCII character...
779 builder.Append((char) (c + 'a' - 'A'));
780 wasNotUnderscore = true;
781 wasNotCap = false;
782 }
783 else
784 {
785 builder.Append(c);
786 wasNotUnderscore = c != '_';
787 wasNotCap = true;
788 }
789 }
790 return builder.ToString();
791 }
792 #endregion
793
794 /// <summary>
795 /// Settings controlling JSON parsing. (Currently doesn't have any actual settings, but I suspect
796 /// we'll want them for levels of strictness, descriptor pools for Any handling, etc.)
797 /// </summary>
798 public sealed class Settings
799 {
800 private static readonly Settings defaultInstance = new Settings();
801
802 // TODO: Add recursion limit.
803
804 /// <summary>
805 /// Default settings, as used by <see cref="JsonParser.Default"/>
806 /// </summary>
807 public static Settings Default { get { return defaultInstance; } }
808
809 /// <summary>
810 /// Creates a new <see cref="Settings"/> object.
811 /// </summary>
812 public Settings()
813 {
814 }
815 }
816 }
817}