Merge pull request #675 from jtattermusch/signing

Add ReleaseSigned configuration for C#
diff --git a/csharp/protos/extest/unittest_issues.proto b/csharp/protos/extest/unittest_issues.proto
index b66da47..376eee8 100644
--- a/csharp/protos/extest/unittest_issues.proto
+++ b/csharp/protos/extest/unittest_issues.proto
@@ -97,7 +97,10 @@
   // ordering.

   // TestFieldOrderings in unittest_proto3.proto is similar,

   // but doesn't include oneofs.

-  // TODO: Consider adding 

+  // TODO: Consider adding oneofs to TestFieldOrderings, although

+  // that will require fixing other tests in multiple platforms.

+  // Alternatively, consider just adding this to

+  // unittest_proto3.proto if multiple platforms want it.

   

   int32 plain_int32 = 4;

 

diff --git a/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs b/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs
index ac33a75..f6e6488 100644
--- a/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs
+++ b/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs
@@ -34,6 +34,7 @@
 using Google.Protobuf.TestProtos;
 using NUnit.Framework;
 using UnitTest.Issues.TestProtos;
+using Google.Protobuf.WellKnownTypes;
 
 namespace Google.Protobuf
 {
@@ -310,6 +311,83 @@
             AssertJson("{ 'plainString': 'plain', 'o1String': '', 'plainInt32': 10, 'o2Int32': 0 }", formatter.Format(message));
         }
 
+        [Test]
+        public void TimestampStandalone()
+        {
+            Assert.AreEqual("1970-01-01T00:00:00Z", new Timestamp().ToString());
+            Assert.AreEqual("1970-01-01T00:00:00.100Z", new Timestamp { Nanos = 100000000 }.ToString());
+            Assert.AreEqual("1970-01-01T00:00:00.120Z", new Timestamp { Nanos = 120000000 }.ToString());
+            Assert.AreEqual("1970-01-01T00:00:00.123Z", new Timestamp { Nanos = 123000000 }.ToString());
+            Assert.AreEqual("1970-01-01T00:00:00.123400Z", new Timestamp { Nanos = 123400000 }.ToString());
+            Assert.AreEqual("1970-01-01T00:00:00.123450Z", new Timestamp { Nanos = 123450000 }.ToString());
+            Assert.AreEqual("1970-01-01T00:00:00.123456Z", new Timestamp { Nanos = 123456000 }.ToString());
+            Assert.AreEqual("1970-01-01T00:00:00.123456700Z", new Timestamp { Nanos = 123456700 }.ToString());
+            Assert.AreEqual("1970-01-01T00:00:00.123456780Z", new Timestamp { Nanos = 123456780 }.ToString());
+            Assert.AreEqual("1970-01-01T00:00:00.123456789Z", new Timestamp { Nanos = 123456789 }.ToString());
+
+            // One before and one after the Unix epoch
+            Assert.AreEqual("1673-06-19T12:34:56Z",
+                new DateTime(1673, 6, 19, 12, 34, 56, DateTimeKind.Utc).ToTimestamp().ToString());
+            Assert.AreEqual("2015-07-31T10:29:34Z",
+                new DateTime(2015, 7, 31, 10, 29, 34, DateTimeKind.Utc).ToTimestamp().ToString());
+        }
+
+        [Test]
+        public void TimestampField()
+        {
+            var message = new TestWellKnownTypes { TimestampField = new Timestamp() };
+            AssertJson("{ 'timestampField': '1970-01-01T00:00:00Z' }", JsonFormatter.Default.Format(message));
+        }
+
+        [Test]
+        [TestCase(0, 0, "0s")]
+        [TestCase(1, 0, "1s")]
+        [TestCase(-1, 0, "-1s")]
+        [TestCase(0, 100000000, "0.100s")]
+        [TestCase(0, 120000000, "0.120s")]
+        [TestCase(0, 123000000, "0.123s")]
+        [TestCase(0, 123400000, "0.123400s")]
+        [TestCase(0, 123450000, "0.123450s")]
+        [TestCase(0, 123456000, "0.123456s")]
+        [TestCase(0, 123456700, "0.123456700s")]
+        [TestCase(0, 123456780, "0.123456780s")]
+        [TestCase(0, 123456789, "0.123456789s")]
+        [TestCase(0, -100000000, "-0.100s")]
+        [TestCase(1, 100000000, "1.100s")]
+        [TestCase(-1, -100000000, "-1.100s")]
+        // Non-normalized examples
+        [TestCase(1, 2123456789, "3.123456789s")]
+        [TestCase(1, -100000000, "0.900s")]
+        public void DurationStandalone(long seconds, int nanoseconds, string expected)
+        {
+            Assert.AreEqual(expected, new Duration { Seconds = seconds, Nanos = nanoseconds }.ToString());
+        }
+
+        [Test]
+        public void DurationField()
+        {
+            var message = new TestWellKnownTypes { DurationField = new Duration() };
+            AssertJson("{ 'durationField': '0s' }", JsonFormatter.Default.Format(message));
+        }
+
+        [Test]
+        public void StructSample()
+        {
+            var message = new Struct
+            {
+                Fields =
+                {
+                    { "a", new Value { NullValue = new NullValue() } },
+                    { "b", new Value { BoolValue = false } },
+                    { "c", new Value { NumberValue = 10.5 } },
+                    { "d", new Value { StringValue = "text" } },
+                    { "e", new Value { ListValue = new ListValue { Values = { new Value { StringValue = "t1" }, new Value { NumberValue = 5 } } } } },
+                    { "f", new Value { StructValue = new Struct { Fields = { { "nested", new Value { StringValue = "value" } } } } } }
+                }
+            };
+            AssertJson("{ 'a': null, 'b': false, 'c': 10.5, 'd': 'text', 'e': [ 't1', 5 ], 'f': { 'nested': 'value' } }", message.ToString());
+        }
+
         /// <summary>
         /// Checks that the actual JSON is the same as the expected JSON - but after replacing
         /// all apostrophes in the expected JSON with double quotes. This basically makes the tests easier
diff --git a/csharp/src/Google.Protobuf/JsonFormatter.cs b/csharp/src/Google.Protobuf/JsonFormatter.cs
index d9783fc..3b25beb 100644
--- a/csharp/src/Google.Protobuf/JsonFormatter.cs
+++ b/csharp/src/Google.Protobuf/JsonFormatter.cs
@@ -122,10 +122,14 @@
         {
             Preconditions.CheckNotNull(message, "message");
             StringBuilder builder = new StringBuilder();
-            // TODO(jonskeet): Handle well-known types here.
-            // Our reflection support needs improving so that we can get at the descriptor
-            // to find out whether *this* message is a well-known type.
-            WriteMessage(builder, message);
+            if (message.Descriptor.IsWellKnownType)
+            {
+                WriteWellKnownTypeValue(builder, message.Descriptor, message, false);
+            }
+            else
+            {
+                WriteMessage(builder, message);
+            }
             return builder.ToString();
         }
 
@@ -356,7 +360,7 @@
                 case FieldType.Group: // Never expect to get this, but...
                     if (descriptor.MessageType.IsWellKnownType)
                     {
-                        WriteWellKnownTypeValue(builder, descriptor, value);
+                        WriteWellKnownTypeValue(builder, descriptor.MessageType, value, true);
                     }
                     else
                     {
@@ -370,20 +374,193 @@
 
         /// <summary>
         /// Central interception point for well-known type formatting. Any well-known types which
-        /// don't need special handling can fall back to WriteMessage.
+        /// don't need special handling can fall back to WriteMessage. We avoid assuming that the
+        /// values are using the embedded well-known types, in order to allow for dynamic messages
+        /// in the future.
         /// </summary>
-        private void WriteWellKnownTypeValue(StringBuilder builder, FieldDescriptor descriptor, object value)
+        private void WriteWellKnownTypeValue(StringBuilder builder, MessageDescriptor descriptor, object value, bool inField)
         {
+            if (value == null)
+            {
+                WriteNull(builder);
+                return;
+            }
             // For wrapper types, the value will be the (possibly boxed) "native" value,
             // so we can write it as if we were unconditionally writing the Value field for the wrapper type.
-            if (descriptor.MessageType.File == Int32Value.Descriptor.File && value != null)
+            if (descriptor.File == Int32Value.Descriptor.File)
             {
-                WriteSingleValue(builder, descriptor.MessageType.FindFieldByNumber(1), value);
+                WriteSingleValue(builder, descriptor.FindFieldByNumber(1), value);
+                return;
+            }
+            if (descriptor.FullName == Timestamp.Descriptor.FullName)
+            {
+                MaybeWrapInString(builder, value, WriteTimestamp, inField);
+                return;
+            }
+            if (descriptor.FullName == Duration.Descriptor.FullName)
+            {
+                MaybeWrapInString(builder, value, WriteDuration, inField);
+                return;
+            }
+            if (descriptor.FullName == Struct.Descriptor.FullName)
+            {
+                WriteStruct(builder, (IMessage) value);
+                return;
+            }
+            if (descriptor.FullName == ListValue.Descriptor.FullName)
+            {
+                var fieldAccessor = descriptor.Fields[ListValue.ValuesFieldNumber].Accessor;
+                WriteList(builder, fieldAccessor, (IList) fieldAccessor.GetValue(value));
+                return;
+            }
+            if (descriptor.FullName == Value.Descriptor.FullName)
+            {
+                WriteStructFieldValue(builder, (IMessage) value);
                 return;
             }
             WriteMessage(builder, (IMessage) value);
         }
 
+        /// <summary>
+        /// Some well-known types end up as string values... so they need wrapping in quotes, but only
+        /// when they're being used as fields within another message.
+        /// </summary>
+        private void MaybeWrapInString(StringBuilder builder, object value, Action<StringBuilder, IMessage> action, bool inField)
+        {
+            if (inField)
+            {
+                builder.Append('"');
+                action(builder, (IMessage) value);
+                builder.Append('"');
+            }
+            else
+            {
+                action(builder, (IMessage) value);
+            }
+        }
+
+        private void WriteTimestamp(StringBuilder builder, IMessage value)
+        {
+            // TODO: In the common case where this *is* using the built-in Timestamp type, we could
+            // avoid all the reflection at this point, by casting to Timestamp. In the interests of
+            // avoiding subtle bugs, don't do that until we've implemented DynamicMessage so that we can prove
+            // it still works in that case.
+            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);
+            // 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();
+            builder.Append(dateTime.ToString("yyyy'-'MM'-'dd'T'HH:mm:ss", CultureInfo.InvariantCulture));
+            AppendNanoseconds(builder, Math.Abs(normalized.Nanos));
+            builder.Append('Z');
+        }
+
+        private void WriteDuration(StringBuilder builder, IMessage value)
+        {
+            // TODO: Same as for WriteTimestamp
+            int nanos = (int) value.Descriptor.Fields[Duration.NanosFieldNumber].Accessor.GetValue(value);
+            long seconds = (long) value.Descriptor.Fields[Duration.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.
+            Duration normalized = Duration.Normalize(seconds, nanos);
+
+            // 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)
+            {
+                builder.Append('-');
+            }
+
+            builder.Append(normalized.Seconds.ToString("d", CultureInfo.InvariantCulture));
+            AppendNanoseconds(builder, Math.Abs(normalized.Nanos));
+            builder.Append('s');
+        }
+
+        /// <summary>
+        /// Appends a number of nanoseconds to a StringBuilder. Either 0 digits are added (in which
+        /// case no "." is appended), or 3 6 or 9 digits.
+        /// </summary>
+        private static void AppendNanoseconds(StringBuilder builder, int nanos)
+        {
+            if (nanos != 0)
+            {
+                builder.Append('.');
+                // Output to 3, 6 or 9 digits.
+                if (nanos % 1000000 == 0)
+                {
+                    builder.Append((nanos / 1000000).ToString("d", CultureInfo.InvariantCulture));
+                }
+                else if (nanos % 1000 == 0)
+                {
+                    builder.Append((nanos / 1000).ToString("d", CultureInfo.InvariantCulture));
+                }
+                else
+                {
+                    builder.Append(nanos.ToString("d", CultureInfo.InvariantCulture));
+                }
+            }
+        }
+
+        private void WriteStruct(StringBuilder builder, IMessage message)
+        {
+            builder.Append("{ ");
+            IDictionary fields = (IDictionary) message.Descriptor.Fields[Struct.FieldsFieldNumber].Accessor.GetValue(message);
+            bool first = true;
+            foreach (DictionaryEntry entry in fields)
+            {
+                string key = (string) entry.Key;
+                IMessage value = (IMessage) entry.Value;
+                if (string.IsNullOrEmpty(key) || value == null)
+                {
+                    throw new InvalidOperationException("Struct fields cannot have an empty key or a null value.");
+                }
+
+                if (!first)
+                {
+                    builder.Append(", ");
+                }
+                WriteString(builder, key);
+                builder.Append(": ");
+                WriteStructFieldValue(builder, value);
+                first = false;
+            }
+            builder.Append(first ? "}" : " }");
+        }
+
+        private void WriteStructFieldValue(StringBuilder builder, IMessage message)
+        {
+            var specifiedField = message.Descriptor.Oneofs[0].Accessor.GetCaseFieldDescriptor(message);
+            if (specifiedField == null)
+            {
+                throw new InvalidOperationException("Value message must contain a value for the oneof.");
+            }
+
+            object value = specifiedField.Accessor.GetValue(message);
+            
+            switch (specifiedField.FieldNumber)
+            {
+                case Value.BoolValueFieldNumber:
+                case Value.StringValueFieldNumber:
+                case Value.NumberValueFieldNumber:
+                    WriteSingleValue(builder, specifiedField, value);
+                    return;
+                case Value.StructValueFieldNumber:
+                case Value.ListValueFieldNumber:
+                    // Structs and ListValues are nested messages, and already well-known types.
+                    var nestedMessage = (IMessage) specifiedField.Accessor.GetValue(message);
+                    WriteWellKnownTypeValue(builder, nestedMessage.Descriptor, nestedMessage, true);
+                    return;
+                case Value.NullValueFieldNumber:
+                    WriteNull(builder);
+                    return;
+                default:
+                    throw new InvalidOperationException("Unexpected case in struct field: " + specifiedField.FieldNumber);
+            }
+        }
+
         private void WriteList(StringBuilder builder, IFieldAccessor accessor, IList list)
         {
             builder.Append("[ ");
diff --git a/csharp/src/Google.Protobuf/WellKnownTypes/TimestampPartial.cs b/csharp/src/Google.Protobuf/WellKnownTypes/TimestampPartial.cs
index f682d09..1aa392c 100644
--- a/csharp/src/Google.Protobuf/WellKnownTypes/TimestampPartial.cs
+++ b/csharp/src/Google.Protobuf/WellKnownTypes/TimestampPartial.cs
@@ -147,7 +147,7 @@
             return FromDateTime(dateTimeOffset.UtcDateTime);
         }
 
-        private static Timestamp Normalize(long seconds, int nanoseconds)
+        internal static Timestamp Normalize(long seconds, int nanoseconds)
         {
             int extraSeconds = nanoseconds / Duration.NanosecondsPerSecond;
             seconds += extraSeconds;