diff options
Diffstat (limited to 'csharp/src/Google.Protobuf')
8 files changed, 261 insertions, 128 deletions
diff --git a/csharp/src/Google.Protobuf/Collections/RepeatedField.cs b/csharp/src/Google.Protobuf/Collections/RepeatedField.cs index e3f65afe..1cde03bc 100644 --- a/csharp/src/Google.Protobuf/Collections/RepeatedField.cs +++ b/csharp/src/Google.Protobuf/Collections/RepeatedField.cs @@ -34,7 +34,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Text; -using Google.Protobuf.Compatibility; namespace Google.Protobuf.Collections { @@ -96,8 +95,8 @@ namespace Google.Protobuf.Collections // iteration. uint tag = input.LastTag; var reader = codec.ValueReader; - // Value types can be packed or not. - if (typeof(T).IsValueType() && WireFormat.GetTagWireType(tag) == WireFormat.WireType.LengthDelimited) + // Non-nullable value types can be packed or not. + if (FieldCodec<T>.IsPackedRepeatedField(tag)) { int length = input.ReadLength(); if (length > 0) @@ -134,7 +133,7 @@ namespace Google.Protobuf.Collections return 0; } uint tag = codec.Tag; - if (typeof(T).IsValueType() && WireFormat.GetTagWireType(tag) == WireFormat.WireType.LengthDelimited) + if (codec.PackedRepeatedField) { int dataSize = CalculatePackedDataSize(codec); return CodedOutputStream.ComputeRawVarint32Size(tag) + @@ -186,7 +185,7 @@ namespace Google.Protobuf.Collections } var writer = codec.ValueWriter; var tag = codec.Tag; - if (typeof(T).IsValueType() && WireFormat.GetTagWireType(tag) == WireFormat.WireType.LengthDelimited) + if (codec.PackedRepeatedField) { // Packed primitive type uint size = (uint)CalculatePackedDataSize(codec); diff --git a/csharp/src/Google.Protobuf/FieldCodec.cs b/csharp/src/Google.Protobuf/FieldCodec.cs index c2b8d268..98313088 100644 --- a/csharp/src/Google.Protobuf/FieldCodec.cs +++ b/csharp/src/Google.Protobuf/FieldCodec.cs @@ -30,6 +30,7 @@ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #endregion +using Google.Protobuf.Compatibility; using Google.Protobuf.WellKnownTypes; using System; using System.Collections.Generic; @@ -329,17 +330,24 @@ namespace Google.Protobuf } /// <summary> + /// <para> /// An encode/decode pair for a single field. This effectively encapsulates /// all the information needed to read or write the field value from/to a coded /// stream. + /// </para> + /// <para> + /// This class is public and has to be as it is used by generated code, but its public + /// API is very limited - just what the generated code needs to call directly. + /// </para> /// </summary> /// <remarks> - /// This never writes default values to the stream, and is not currently designed - /// to play well with packed arrays. + /// This never writes default values to the stream, and does not address "packedness" + /// in repeated fields itself, other than to know whether or not the field *should* be packed. /// </remarks> public sealed class FieldCodec<T> { private static readonly T DefaultDefault; + private static readonly bool TypeSupportsPacking = typeof(T).IsValueType() && Nullable.GetUnderlyingType(typeof(T)) == null; static FieldCodec() { @@ -354,75 +362,31 @@ namespace Google.Protobuf // Otherwise it's the default value of the CLR type } - private readonly Func<CodedInputStream, T> reader; - private readonly Action<CodedOutputStream, T> writer; - private readonly Func<T, int> sizeCalculator; - private readonly uint tag; - private readonly int tagSize; - private readonly int fixedSize; - // Default value for this codec. Usually the same for every instance of the same type, but - // for string/ByteString wrapper fields the codec's default value is null, whereas for - // other string/ByteString fields it's "" or ByteString.Empty. - private readonly T defaultValue; + internal static bool IsPackedRepeatedField(uint tag) => + TypeSupportsPacking && WireFormat.GetTagWireType(tag) == WireFormat.WireType.LengthDelimited; - internal FieldCodec( - Func<CodedInputStream, T> reader, - Action<CodedOutputStream, T> writer, - Func<T, int> sizeCalculator, - uint tag) : this(reader, writer, sizeCalculator, tag, DefaultDefault) - { - } - - internal FieldCodec( - Func<CodedInputStream, T> reader, - Action<CodedOutputStream, T> writer, - Func<T, int> sizeCalculator, - uint tag, - T defaultValue) - { - this.reader = reader; - this.writer = writer; - this.sizeCalculator = sizeCalculator; - this.fixedSize = 0; - this.tag = tag; - this.defaultValue = defaultValue; - tagSize = CodedOutputStream.ComputeRawVarint32Size(tag); - } - - internal FieldCodec( - Func<CodedInputStream, T> reader, - Action<CodedOutputStream, T> writer, - int fixedSize, - uint tag) - { - this.reader = reader; - this.writer = writer; - this.sizeCalculator = _ => fixedSize; - this.fixedSize = fixedSize; - this.tag = tag; - tagSize = CodedOutputStream.ComputeRawVarint32Size(tag); - } + internal bool PackedRepeatedField { get; } /// <summary> - /// Returns the size calculator for just a value. + /// Returns a delegate to write a value (unconditionally) to a coded output stream. /// </summary> - internal Func<T, int> ValueSizeCalculator { get { return sizeCalculator; } } + internal Action<CodedOutputStream, T> ValueWriter { get; } /// <summary> - /// Returns a delegate to write a value (unconditionally) to a coded output stream. + /// Returns the size calculator for just a value. /// </summary> - internal Action<CodedOutputStream, T> ValueWriter { get { return writer; } } + internal Func<T, int> ValueSizeCalculator { get; } /// <summary> /// Returns a delegate to read a value from a coded input stream. It is assumed that /// the stream is already positioned on the appropriate tag. /// </summary> - internal Func<CodedInputStream, T> ValueReader { get { return reader; } } + internal Func<CodedInputStream, T> ValueReader { get; } /// <summary> /// Returns the fixed size for an entry, or 0 if sizes vary. /// </summary> - internal int FixedSize { get { return fixedSize; } } + internal int FixedSize { get; } /// <summary> /// Gets the tag of the codec. @@ -430,15 +394,54 @@ namespace Google.Protobuf /// <value> /// The tag of the codec. /// </value> - public uint Tag { get { return tag; } } + internal uint Tag { get; } /// <summary> - /// Gets the default value of the codec's type. + /// Default value for this codec. Usually the same for every instance of the same type, but + /// for string/ByteString wrapper fields the codec's default value is null, whereas for + /// other string/ByteString fields it's "" or ByteString.Empty. /// </summary> /// <value> /// The default value of the codec's type. /// </value> - public T DefaultValue { get { return defaultValue; } } + internal T DefaultValue { get; } + + private readonly int tagSize; + + internal FieldCodec( + Func<CodedInputStream, T> reader, + Action<CodedOutputStream, T> writer, + int fixedSize, + uint tag) : this(reader, writer, _ => fixedSize, tag) + { + FixedSize = fixedSize; + } + + internal FieldCodec( + Func<CodedInputStream, T> reader, + Action<CodedOutputStream, T> writer, + Func<T, int> sizeCalculator, + uint tag) : this(reader, writer, sizeCalculator, tag, DefaultDefault) + { + } + + internal FieldCodec( + Func<CodedInputStream, T> reader, + Action<CodedOutputStream, T> writer, + Func<T, int> sizeCalculator, + uint tag, + T defaultValue) + { + ValueReader = reader; + ValueWriter = writer; + ValueSizeCalculator = sizeCalculator; + FixedSize = 0; + Tag = tag; + DefaultValue = defaultValue; + tagSize = CodedOutputStream.ComputeRawVarint32Size(tag); + // Detect packed-ness once, so we can check for it within RepeatedField<T>. + PackedRepeatedField = IsPackedRepeatedField(tag); + } /// <summary> /// Write a tag and the given value, *if* the value is not the default. @@ -447,8 +450,8 @@ namespace Google.Protobuf { if (!IsDefault(value)) { - output.WriteTag(tag); - writer(output, value); + output.WriteTag(Tag); + ValueWriter(output, value); } } @@ -457,23 +460,14 @@ namespace Google.Protobuf /// </summary> /// <param name="input">The input stream to read from.</param> /// <returns>The value read from the stream.</returns> - public T Read(CodedInputStream input) - { - return reader(input); - } + public T Read(CodedInputStream input) => ValueReader(input); /// <summary> /// Calculates the size required to write the given value, with a tag, /// if the value is not the default. /// </summary> - public int CalculateSizeWithTag(T value) - { - return IsDefault(value) ? 0 : sizeCalculator(value) + tagSize; - } + public int CalculateSizeWithTag(T value) => IsDefault(value) ? 0 : ValueSizeCalculator(value) + tagSize; - private bool IsDefault(T value) - { - return EqualityComparer<T>.Default.Equals(value, defaultValue); - } + private bool IsDefault(T value) => EqualityComparer<T>.Default.Equals(value, DefaultValue); } } diff --git a/csharp/src/Google.Protobuf/InvalidProtocolBufferException.cs b/csharp/src/Google.Protobuf/InvalidProtocolBufferException.cs index cacda648..eeb0f13a 100644 --- a/csharp/src/Google.Protobuf/InvalidProtocolBufferException.cs +++ b/csharp/src/Google.Protobuf/InvalidProtocolBufferException.cs @@ -30,6 +30,7 @@ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#endregion
+using System;
using System.IO;
namespace Google.Protobuf
@@ -45,6 +46,11 @@ namespace Google.Protobuf {
}
+ internal InvalidProtocolBufferException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
internal static InvalidProtocolBufferException MoreDataAvailable()
{
return new InvalidProtocolBufferException(
@@ -82,6 +88,11 @@ namespace Google.Protobuf "Protocol message contained an invalid tag (zero).");
}
+ internal static InvalidProtocolBufferException InvalidBase64(Exception innerException)
+ {
+ return new InvalidProtocolBufferException("Invalid base64 data", innerException);
+ }
+
internal static InvalidProtocolBufferException InvalidEndTag()
{
return new InvalidProtocolBufferException(
diff --git a/csharp/src/Google.Protobuf/JsonFormatter.cs b/csharp/src/Google.Protobuf/JsonFormatter.cs index bde17903..61961c4c 100644 --- a/csharp/src/Google.Protobuf/JsonFormatter.cs +++ b/csharp/src/Google.Protobuf/JsonFormatter.cs @@ -205,11 +205,6 @@ namespace Google.Protobuf { continue; } - // Omit awkward (single) values such as unknown enum values - if (!field.IsRepeated && !field.IsMap && !CanWriteSingleValue(value)) - { - continue; - } // Okay, all tests complete: let's write the field value... if (!first) @@ -224,6 +219,31 @@ namespace Google.Protobuf return !first; } + /// <summary> + /// Camel-case converter with added strictness for field mask formatting. + /// </summary> + /// <exception cref="InvalidOperationException">The field mask is invalid for JSON representation</exception> + private static string ToCamelCaseForFieldMask(string input) + { + for (int i = 0; i < input.Length; i++) + { + char c = input[i]; + if (c >= 'A' && c <= 'Z') + { + throw new InvalidOperationException($"Invalid field mask to be converted to JSON: {input}"); + } + if (c == '_' && i < input.Length - 1) + { + char next = input[i + 1]; + if (next < 'a' || next > 'z') + { + throw new InvalidOperationException($"Invalid field mask to be converted to JSON: {input}"); + } + } + } + return ToCamelCase(input); + } + // Converted from src/google/protobuf/util/internal/utility.cc ToCamelCase // TODO: Use the new field in FieldDescriptor. internal static string ToCamelCase(string input) @@ -372,7 +392,14 @@ namespace Google.Protobuf } else if (value is System.Enum) { - WriteString(builder, value.ToString()); + if (System.Enum.IsDefined(value.GetType(), value)) + { + WriteString(builder, value.ToString()); + } + else + { + WriteValue(builder, (int) value); + } } else if (value is float || value is double) { @@ -485,13 +512,14 @@ namespace Google.Protobuf int nanos = (int) value.Descriptor.Fields[Timestamp.NanosFieldNumber].Accessor.GetValue(value); long seconds = (long) value.Descriptor.Fields[Timestamp.SecondsFieldNumber].Accessor.GetValue(value); - // Even if the original message isn't using the built-in classes, we can still build one... and then - // rely on it being normalized. - Timestamp normalized = Timestamp.Normalize(seconds, nanos); + // Even if the original message isn't using the built-in classes, we can still build one... and its + // conversion will check whether or not it's normalized. + // TODO: Perhaps the diagnostic-only formatter should not throw for non-normalized values? + Timestamp ts = new Timestamp { Seconds = seconds, Nanos = nanos }; // Use .NET's formatting for the value down to the second, including an opening double quote (as it's a string value) - DateTime dateTime = normalized.ToDateTime(); + DateTime dateTime = ts.ToDateTime(); builder.Append(dateTime.ToString("yyyy'-'MM'-'dd'T'HH:mm:ss", CultureInfo.InvariantCulture)); - AppendNanoseconds(builder, Math.Abs(normalized.Nanos)); + AppendNanoseconds(builder, Math.Abs(ts.Nanos)); builder.Append("Z\""); } @@ -502,25 +530,29 @@ namespace Google.Protobuf int nanos = (int) value.Descriptor.Fields[Duration.NanosFieldNumber].Accessor.GetValue(value); long seconds = (long) value.Descriptor.Fields[Duration.SecondsFieldNumber].Accessor.GetValue(value); + // TODO: Perhaps the diagnostic-only formatter should not throw for non-normalized values? // Even if the original message isn't using the built-in classes, we can still build one... and then // rely on it being normalized. - Duration normalized = Duration.Normalize(seconds, nanos); + if (!Duration.IsNormalized(seconds, nanos)) + { + throw new InvalidOperationException("Non-normalized duration value"); + } // The seconds part will normally provide the minus sign if we need it, but not if it's 0... - if (normalized.Seconds == 0 && normalized.Nanos < 0) + if (seconds == 0 && nanos < 0) { builder.Append('-'); } - builder.Append(normalized.Seconds.ToString("d", CultureInfo.InvariantCulture)); - AppendNanoseconds(builder, Math.Abs(normalized.Nanos)); + builder.Append(seconds.ToString("d", CultureInfo.InvariantCulture)); + AppendNanoseconds(builder, Math.Abs(nanos)); builder.Append("s\""); } private void WriteFieldMask(StringBuilder builder, IMessage value) { IList paths = (IList) value.Descriptor.Fields[FieldMask.PathsFieldNumber].Accessor.GetValue(value); - WriteString(builder, string.Join(",", paths.Cast<string>().Select(ToCamelCase))); + WriteString(builder, string.Join(",", paths.Cast<string>().Select(ToCamelCaseForFieldMask))); } private void WriteAny(StringBuilder builder, IMessage value) @@ -598,15 +630,15 @@ namespace Google.Protobuf // Output to 3, 6 or 9 digits. if (nanos % 1000000 == 0) { - builder.Append((nanos / 1000000).ToString("d", CultureInfo.InvariantCulture)); + builder.Append((nanos / 1000000).ToString("d3", CultureInfo.InvariantCulture)); } else if (nanos % 1000 == 0) { - builder.Append((nanos / 1000).ToString("d", CultureInfo.InvariantCulture)); + builder.Append((nanos / 1000).ToString("d6", CultureInfo.InvariantCulture)); } else { - builder.Append(nanos.ToString("d", CultureInfo.InvariantCulture)); + builder.Append(nanos.ToString("d9", CultureInfo.InvariantCulture)); } } } @@ -674,10 +706,6 @@ namespace Google.Protobuf bool first = true; foreach (var value in list) { - if (!CanWriteSingleValue(value)) - { - continue; - } if (!first) { builder.Append(PropertySeparator); @@ -695,10 +723,6 @@ namespace Google.Protobuf // This will box each pair. Could use IDictionaryEnumerator, but that's ugly in terms of disposal. foreach (DictionaryEntry pair in dictionary) { - if (!CanWriteSingleValue(pair.Value)) - { - continue; - } if (!first) { builder.Append(PropertySeparator); diff --git a/csharp/src/Google.Protobuf/JsonParser.cs b/csharp/src/Google.Protobuf/JsonParser.cs index 300a66b4..c07b16ea 100644 --- a/csharp/src/Google.Protobuf/JsonParser.cs +++ b/csharp/src/Google.Protobuf/JsonParser.cs @@ -168,6 +168,10 @@ namespace Google.Protobuf } var descriptor = message.Descriptor; var jsonFieldMap = descriptor.Fields.ByJsonName(); + // All the oneof fields we've already accounted for - we can only see each of them once. + // The set is created lazily to avoid the overhead of creating a set for every message + // we parsed, when oneofs are relatively rare. + HashSet<OneofDescriptor> seenOneofs = null; while (true) { token = tokenizer.Next(); @@ -183,6 +187,17 @@ namespace Google.Protobuf FieldDescriptor field; if (jsonFieldMap.TryGetValue(name, out field)) { + if (field.ContainingOneof != null) + { + if (seenOneofs == null) + { + seenOneofs = new HashSet<OneofDescriptor>(); + } + if (!seenOneofs.Add(field.ContainingOneof)) + { + throw new InvalidProtocolBufferException($"Multiple values specified for oneof {field.ContainingOneof.Name}"); + } + } MergeField(message, field, tokenizer); } else @@ -200,10 +215,15 @@ namespace Google.Protobuf var token = tokenizer.Next(); if (token.Type == JsonToken.TokenType.Null) { + // Clear the field if we see a null token, unless it's for a singular field of type + // google.protobuf.Value. // Note: different from Java API, which just ignores it. // TODO: Bring it more in line? Discuss... - field.Accessor.Clear(message); - return; + if (field.IsMap || field.IsRepeated || !IsGoogleProtobufValueField(field)) + { + field.Accessor.Clear(message); + return; + } } tokenizer.PushBack(token); @@ -239,6 +259,10 @@ namespace Google.Protobuf return; } tokenizer.PushBack(token); + if (token.Type == JsonToken.TokenType.Null) + { + throw new InvalidProtocolBufferException("Repeated field elements cannot be null"); + } list.Add(ParseSingleValue(field, tokenizer)); } } @@ -270,19 +294,30 @@ namespace Google.Protobuf } object key = ParseMapKey(keyField, token.StringValue); object value = ParseSingleValue(valueField, tokenizer); - // TODO: Null handling + if (value == null) + { + throw new InvalidProtocolBufferException("Map values must not be null"); + } dictionary[key] = value; } } + private static bool IsGoogleProtobufValueField(FieldDescriptor field) + { + return field.FieldType == FieldType.Message && + field.MessageType.FullName == Value.Descriptor.FullName; + } + private object ParseSingleValue(FieldDescriptor field, JsonTokenizer tokenizer) { var token = tokenizer.Next(); if (token.Type == JsonToken.TokenType.Null) { - if (field.FieldType == FieldType.Message && field.MessageType.FullName == Value.Descriptor.FullName) + // TODO: In order to support dynamic messages, we should really build this up + // dynamically. + if (IsGoogleProtobufValueField(field)) { - return new Value { NullValue = NullValue.NULL_VALUE }; + return Value.ForNull(); } return null; } @@ -464,6 +499,11 @@ namespace Google.Protobuf { tokens.Add(token); token = tokenizer.Next(); + + if (tokenizer.ObjectDepth < typeUrlObjectDepth) + { + throw new InvalidProtocolBufferException("Any message with no @type"); + } } // Don't add the @type property or its value to the recorded token list @@ -603,6 +643,11 @@ namespace Google.Protobuf throw new InvalidProtocolBufferException($"Value out of range: {value}"); } return (float) value; + case FieldType.Enum: + CheckInteger(value); + // Just return it as an int, and let the CLR convert it. + // Note that we deliberately don't check that it's a known value. + return (int) value; default: throw new InvalidProtocolBufferException($"Unsupported conversion from JSON number for field type {field.FieldType}"); } @@ -633,7 +678,14 @@ namespace Google.Protobuf case FieldType.String: return text; case FieldType.Bytes: - return ByteString.FromBase64(text); + try + { + return ByteString.FromBase64(text); + } + catch (FormatException e) + { + throw InvalidProtocolBufferException.InvalidBase64(e); + } case FieldType.Int32: case FieldType.SInt32: case FieldType.SFixed32: @@ -826,28 +878,24 @@ namespace Google.Protobuf try { - long seconds = long.Parse(secondsText, CultureInfo.InvariantCulture); + long seconds = long.Parse(secondsText, CultureInfo.InvariantCulture) * multiplier; int nanos = 0; if (subseconds != "") { // This should always work, as we've got 1-9 digits. int parsedFraction = int.Parse(subseconds.Substring(1)); - nanos = parsedFraction * SubsecondScalingFactors[subseconds.Length]; + nanos = parsedFraction * SubsecondScalingFactors[subseconds.Length] * multiplier; } - if (seconds >= Duration.MaxSeconds) + if (!Duration.IsNormalized(seconds, nanos)) { - // Allow precisely 315576000000 seconds, but prohibit even 1ns more. - if (seconds > Duration.MaxSeconds || nanos > 0) - { - throw new InvalidProtocolBufferException("Invalid Duration value: " + token.StringValue); - } + throw new InvalidProtocolBufferException($"Invalid Duration value: {token.StringValue}"); } - message.Descriptor.Fields[Duration.SecondsFieldNumber].Accessor.SetValue(message, seconds * multiplier); - message.Descriptor.Fields[Duration.NanosFieldNumber].Accessor.SetValue(message, nanos * multiplier); + message.Descriptor.Fields[Duration.SecondsFieldNumber].Accessor.SetValue(message, seconds); + message.Descriptor.Fields[Duration.NanosFieldNumber].Accessor.SetValue(message, nanos); } catch (FormatException) { - throw new InvalidProtocolBufferException("Invalid Duration value: " + token.StringValue); + throw new InvalidProtocolBufferException($"Invalid Duration value: {token.StringValue}"); } } @@ -870,6 +918,8 @@ namespace Google.Protobuf private static string ToSnakeCase(string text) { var builder = new StringBuilder(text.Length * 2); + // Note: this is probably unnecessary now, but currently retained to be as close as possible to the + // C++, whilst still throwing an exception on underscores. bool wasNotUnderscore = false; // Initialize to false for case 1 (below) bool wasNotCap = false; @@ -903,7 +953,11 @@ namespace Google.Protobuf else { builder.Append(c); - wasNotUnderscore = c != '_'; + if (c == '_') + { + throw new InvalidProtocolBufferException($"Invalid field mask: {text}"); + } + wasNotUnderscore = true; wasNotCap = true; } } diff --git a/csharp/src/Google.Protobuf/Reflection/MessageDescriptor.cs b/csharp/src/Google.Protobuf/Reflection/MessageDescriptor.cs index d1732b2e..f43803a6 100644 --- a/csharp/src/Google.Protobuf/Reflection/MessageDescriptor.cs +++ b/csharp/src/Google.Protobuf/Reflection/MessageDescriptor.cs @@ -92,11 +92,22 @@ namespace Google.Protobuf.Reflection new FieldDescriptor(field, file, this, index, generatedCodeInfo?.PropertyNames[index])); fieldsInNumberOrder = new ReadOnlyCollection<FieldDescriptor>(fieldsInDeclarationOrder.OrderBy(field => field.FieldNumber).ToArray()); // TODO: Use field => field.Proto.JsonName when we're confident it's appropriate. (And then use it in the formatter, too.) - jsonFieldMap = new ReadOnlyDictionary<string, FieldDescriptor>(fieldsInNumberOrder.ToDictionary(field => JsonFormatter.ToCamelCase(field.Name))); + jsonFieldMap = CreateJsonFieldMap(fieldsInNumberOrder); file.DescriptorPool.AddSymbol(this); Fields = new FieldCollection(this); } + private static ReadOnlyDictionary<string, FieldDescriptor> CreateJsonFieldMap(IList<FieldDescriptor> fields) + { + var map = new Dictionary<string, FieldDescriptor>(); + foreach (var field in fields) + { + map[JsonFormatter.ToCamelCase(field.Name)] = field; + map[field.Name] = field; + } + return new ReadOnlyDictionary<string, FieldDescriptor>(map); + } + /// <summary> /// The brief name of the descriptor's target. /// </summary> @@ -255,9 +266,10 @@ namespace Google.Protobuf.Reflection // TODO: consider making this public in the future. (Being conservative for now...) /// <value> - /// Returns a read-only dictionary mapping the field names in this message as they're used + /// Returns a read-only dictionary mapping the field names in this message as they're available /// in the JSON representation to the field descriptors. For example, a field <c>foo_bar</c> - /// in the message would result in an entry with a key <c>fooBar</c>. + /// in the message would result two entries, one with a key <c>fooBar</c> and one with a key + /// <c>foo_bar</c>, both referring to the same field. /// </value> internal IDictionary<string, FieldDescriptor> ByJsonName() => messageDescriptor.jsonFieldMap; diff --git a/csharp/src/Google.Protobuf/WellKnownTypes/DurationPartial.cs b/csharp/src/Google.Protobuf/WellKnownTypes/DurationPartial.cs index 324f48fc..b8eba9d3 100644 --- a/csharp/src/Google.Protobuf/WellKnownTypes/DurationPartial.cs +++ b/csharp/src/Google.Protobuf/WellKnownTypes/DurationPartial.cs @@ -57,15 +57,38 @@ namespace Google.Protobuf.WellKnownTypes /// </summary> public const long MinSeconds = -315576000000L; + internal const int MaxNanoseconds = NanosecondsPerSecond - 1; + internal const int MinNanoseconds = -NanosecondsPerSecond + 1; + + internal static bool IsNormalized(long seconds, int nanoseconds) + { + // Simple boundaries + if (seconds < MinSeconds || seconds > MaxSeconds || + nanoseconds < MinNanoseconds || nanoseconds > MaxNanoseconds) + { + return false; + } + // We only have a problem is one is strictly negative and the other is + // strictly positive. + return Math.Sign(seconds) * Math.Sign(nanoseconds) != -1; + } + + /// <summary> /// Converts this <see cref="Duration"/> to a <see cref="TimeSpan"/>. /// </summary> /// <remarks>If the duration is not a precise number of ticks, it is truncated towards 0.</remarks> /// <returns>The value of this duration, as a <c>TimeSpan</c>.</returns> + /// <exception cref="InvalidOperationException">This value isn't a valid normalized duration, as + /// described in the documentation.</exception> public TimeSpan ToTimeSpan() { checked { + if (!IsNormalized(Seconds, Nanos)) + { + throw new InvalidOperationException("Duration was not a valid normalized duration"); + } long ticks = Seconds * TimeSpan.TicksPerSecond + Nanos / NanosecondsPerTick; return TimeSpan.FromTicks(ticks); } diff --git a/csharp/src/Google.Protobuf/WellKnownTypes/TimestampPartial.cs b/csharp/src/Google.Protobuf/WellKnownTypes/TimestampPartial.cs index d284acd6..7c50a3d7 100644 --- a/csharp/src/Google.Protobuf/WellKnownTypes/TimestampPartial.cs +++ b/csharp/src/Google.Protobuf/WellKnownTypes/TimestampPartial.cs @@ -37,9 +37,17 @@ namespace Google.Protobuf.WellKnownTypes public partial class Timestamp { private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - private static readonly long BclSecondsAtUnixEpoch = UnixEpoch.Ticks / TimeSpan.TicksPerSecond; - internal static readonly long UnixSecondsAtBclMinValue = -BclSecondsAtUnixEpoch; - internal static readonly long UnixSecondsAtBclMaxValue = (DateTime.MaxValue.Ticks / TimeSpan.TicksPerSecond) - BclSecondsAtUnixEpoch; + // Constants determined programmatically, but then hard-coded so they can be constant expressions. + private const long BclSecondsAtUnixEpoch = 62135596800; + internal const long UnixSecondsAtBclMaxValue = 253402300799; + internal const long UnixSecondsAtBclMinValue = -BclSecondsAtUnixEpoch; + internal const int MaxNanos = Duration.NanosecondsPerSecond - 1; + + private bool IsNormalized => + Nanos >= 0 && + Nanos <= MaxNanos && + Seconds >= UnixSecondsAtBclMinValue && + Seconds <= UnixSecondsAtBclMaxValue; /// <summary> /// Returns the difference between one <see cref="Timestamp"/> and another, as a <see cref="Duration"/>. @@ -99,8 +107,14 @@ namespace Google.Protobuf.WellKnownTypes /// <see cref="DateTime"/> value precisely on a second. /// </remarks> /// <returns>This timestamp as a <c>DateTime</c>.</returns> + /// <exception cref="InvalidOperationException">The timestamp contains invalid values; either it is + /// incorrectly normalized or is outside the valid range.</exception> public DateTime ToDateTime() { + if (!IsNormalized) + { + throw new InvalidOperationException(@"Timestamp contains invalid values: Seconds={Seconds}; Nanos={Nanos}"); + } return UnixEpoch.AddSeconds(Seconds).AddTicks(Nanos / Duration.NanosecondsPerTick); } @@ -114,6 +128,8 @@ namespace Google.Protobuf.WellKnownTypes /// <see cref="DateTimeOffset"/> value precisely on a second. /// </remarks> /// <returns>This timestamp as a <c>DateTimeOffset</c>.</returns> + /// <exception cref="InvalidOperationException">The timestamp contains invalid values; either it is + /// incorrectly normalized or is outside the valid range.</exception> public DateTimeOffset ToDateTimeOffset() { return new DateTimeOffset(ToDateTime(), TimeSpan.Zero); |