[CBOR] Implement Map support for CborReader and CborWriter (#33500)
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Mon, 16 Mar 2020 22:00:12 +0000 (22:00 +0000)
committerGitHub <noreply@github.com>
Mon, 16 Mar 2020 22:00:12 +0000 (22:00 +0000)
* Implement map support for CborReader and CborWriter

* fix test naming

* move helper functions to nested type

* check for arithmetic overflow in CborReader.ReadStartMap

* throw FormatExceptions on data items whose definite length exceeds remaining buffer size

13 files changed:
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Array.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Helpers.cs [new file with mode: 0644]
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Map.cs [new file with mode: 0644]
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Array.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Helpers.cs [new file with mode: 0644]
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Map.cs [new file with mode: 0644]
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborInitialByte.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Integer.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Map.cs [new file with mode: 0644]
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Integer.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Map.cs [new file with mode: 0644]
src/libraries/System.Security.Cryptography.Encoding/tests/System.Security.Cryptography.Encoding.Tests.csproj

index 56ffd1bc9b4dcb1c30a8046961caab0f7ef45cd0..22a27e4cf9f13ec2a4a9fac8ff7b09f5cf958452 100644 (file)
@@ -25,7 +25,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         {
             byte[] encoding = hexEncoding.HexToByteArray();
             var reader = new CborReader(encoding);
-            ArrayReaderHelper.VerifyArray(reader, expectedValues);
+            Helpers.VerifyArray(reader, expectedValues);
             Assert.Equal(CborReaderState.Finished, reader.Peek());
         }
 
@@ -37,7 +37,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         {
             byte[] encoding = hexEncoding.HexToByteArray();
             var reader = new CborReader(encoding);
-            ArrayReaderHelper.VerifyArray(reader, expectedValues);
+            Helpers.VerifyArray(reader, expectedValues);
             Assert.Equal(CborReaderState.Finished, reader.Peek());
         }
 
@@ -133,9 +133,8 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         }
 
         [Theory]
-        [InlineData("81", 1, 0)]
-        [InlineData("8201", 2, 1)]
-        [InlineData("860102", 6, 2)]
+        [InlineData("821907e4", 2, 1)]
+        [InlineData("861907e41907e4", 6, 2)]
         public static void ReadArray_IncorrectDefiniteLength_ShouldThrowFormatException(string hexEncoding, int expectedLength, int actualLength)
         {
             byte[] encoding = hexEncoding.HexToByteArray();
@@ -153,9 +152,8 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         }
 
         [Theory]
-        [InlineData("81", 1, 0)]
         [InlineData("828101", 2, 1)]
-        [InlineData("8681018102", 6, 2)]
+        [InlineData("868101811907e4", 6, 2)]
         public static void ReadArray_IncorrectDefiniteLength_NestedValues_ShouldThrowFormatException(string hexEncoding, int expectedLength, int actualLength)
         {
             byte[] encoding = hexEncoding.HexToByteArray();
@@ -176,7 +174,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         }
 
         [Fact]
-        public static void BeginReadArray_EmptyBuffer_ShouldThrowFormatException()
+        public static void ReadStartArray_EmptyBuffer_ShouldThrowFormatException()
         {
             byte[] encoding = Array.Empty<byte>();
             var reader = new CborReader(encoding);
@@ -193,7 +191,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         [InlineData("a0")] // {}
         [InlineData("f97e00")] // NaN
         [InlineData("fb3ff199999999999a")] // 1.1
-        public static void BeginReadArray_InvalidType_ShouldThrowInvalidOperationException(string hexEncoding)
+        public static void ReadStartArray_InvalidType_ShouldThrowInvalidOperationException(string hexEncoding)
         {
             byte[] data = hexEncoding.HexToByteArray();
             var reader = new CborReader(data);
@@ -210,63 +208,24 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         [InlineData("9912")]
         [InlineData("9a000000")]
         [InlineData("9b00000000000000")]
-        public static void BeginReadArray_InvalidData_ShouldThrowFormatException(string hexEncoding)
+        public static void ReadStartArray_InvalidData_ShouldThrowFormatException(string hexEncoding)
         {
             byte[] data = hexEncoding.HexToByteArray();
             var reader = new CborReader(data);
 
             Assert.Throws<FormatException>(() => reader.ReadStartArray());
         }
-    }
 
-    static class ArrayReaderHelper
-    {
-        public static void VerifyArray(CborReader reader, params object[] expectedValues)
+        [Theory]
+        [InlineData("81")]
+        [InlineData("830102")]
+        [InlineData("9b7fffffffffffffff")] // long.MaxValue
+        public static void ReadStartArray_BufferTooSmall_ShouldThrowFormatException(string hexEncoding)
         {
-            Assert.Equal(CborReaderState.StartArray, reader.Peek());
-
-            ulong? length = reader.ReadStartArray();
-
-            Assert.NotNull(length);
-            Assert.Equal(expectedValues.Length, (int)length!.Value);
-
-            foreach (object value in expectedValues)
-            {
-                switch (value)
-                {
-                    case int expected:
-                        if (expected >= 0)
-                        {
-                            Assert.Equal(CborReaderState.UnsignedInteger, reader.Peek());
-                        }
-                        else
-                        {
-                            Assert.Equal(CborReaderState.NegativeInteger, reader.Peek());
-                        }
-
-                        long i = reader.ReadInt64();
-                        Assert.Equal(expected, (int)i);
-                        break;
-                    case string expected:
-                        Assert.Equal(CborReaderState.TextString, reader.Peek());
-                        string s = reader.ReadTextString();
-                        Assert.Equal(expected, s);
-                        break;
-                    case byte[] expected:
-                        Assert.Equal(CborReaderState.ByteString, reader.Peek());
-                        byte[] b = reader.ReadByteString();
-                        Assert.Equal(expected, b);
-                        break;
-                    case object[] nested:
-                        VerifyArray(reader, nested);
-                        break;
-                    default:
-                        throw new ArgumentException($"Unrecognized argument type {value.GetType()}");
-                }
-            }
+            byte[] data = hexEncoding.HexToByteArray();
+            var reader = new CborReader(data);
 
-            Assert.Equal(CborReaderState.EndArray, reader.Peek());
-            reader.ReadEndArray();
+            Assert.Throws<FormatException>(() => reader.ReadStartArray());
         }
     }
 }
diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Helpers.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Helpers.cs
new file mode 100644 (file)
index 0000000..6eba42b
--- /dev/null
@@ -0,0 +1,102 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+using System.Linq;
+using Xunit;
+
+namespace System.Security.Cryptography.Encoding.Tests.Cbor
+{
+    public partial class CborReaderTests
+    {
+        internal static class Helpers
+        {
+            public static void VerifyValue(CborReader reader, object expectedValue)
+            {
+                switch (expectedValue)
+                {
+                    case int expected:
+                        VerifyPeekInteger(reader, isUnsignedInteger: expected >= 0);
+                        long i = reader.ReadInt64();
+                        Assert.Equal(expected, (int)i);
+                        break;
+                    case long expected:
+                        VerifyPeekInteger(reader, isUnsignedInteger: expected >= 0);
+                        long l = reader.ReadInt64();
+                        Assert.Equal(expected, l);
+                        break;
+                    case ulong expected:
+                        VerifyPeekInteger(reader, isUnsignedInteger: true);
+                        ulong u = reader.ReadUInt64();
+                        Assert.Equal(expected, u);
+                        break;
+                    case string expected:
+                        Assert.Equal(CborReaderState.TextString, reader.Peek());
+                        string s = reader.ReadTextString();
+                        Assert.Equal(expected, s);
+                        break;
+                    case byte[] expected:
+                        Assert.Equal(CborReaderState.ByteString, reader.Peek());
+                        byte[] b = reader.ReadByteString();
+                        Assert.Equal(expected, b);
+                        break;
+                    case object[] nested when CborWriterTests.Helpers.IsCborMapRepresentation(nested):
+                        VerifyMap(reader, nested);
+                        break;
+                    case object[] nested:
+                        VerifyArray(reader, nested);
+                        break;
+                    default:
+                        throw new ArgumentException($"Unrecognized argument type {expectedValue.GetType()}");
+                }
+
+                static void VerifyPeekInteger(CborReader reader, bool isUnsignedInteger)
+                {
+                    CborReaderState expectedState = isUnsignedInteger ? CborReaderState.UnsignedInteger : CborReaderState.NegativeInteger;
+                    Assert.Equal(expectedState, reader.Peek());
+                }
+            }
+
+            public static void VerifyArray(CborReader reader, params object[] expectedValues)
+            {
+                Assert.Equal(CborReaderState.StartArray, reader.Peek());
+
+                ulong? length = reader.ReadStartArray();
+
+                Assert.NotNull(length);
+                Assert.Equal(expectedValues.Length, (int)length!.Value);
+
+                foreach (object value in expectedValues)
+                {
+                    VerifyValue(reader, value);
+                }
+
+                Assert.Equal(CborReaderState.EndArray, reader.Peek());
+                reader.ReadEndArray();
+            }
+
+            public static void VerifyMap(CborReader reader, params object[] expectedValues)
+            {
+                if (!CborWriterTests.Helpers.IsCborMapRepresentation(expectedValues))
+                {
+                    throw new ArgumentException($"cbor map expected values missing '{CborWriterTests.Helpers.MapPrefixIdentifier}' prefix.");
+                }
+
+                Assert.Equal(CborReaderState.StartMap, reader.Peek());
+                ulong? length = reader.ReadStartMap();
+
+                Assert.NotNull(length);
+                Assert.Equal((expectedValues.Length - 1) / 2, (int)length!.Value);
+
+                foreach (object value in expectedValues.Skip(1))
+                {
+                    VerifyValue(reader, value);
+                }
+
+                Assert.Equal(CborReaderState.EndMap, reader.Peek());
+                reader.ReadEndMap();
+            }
+        }
+    }
+}
diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Map.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Map.cs
new file mode 100644 (file)
index 0000000..c443f35
--- /dev/null
@@ -0,0 +1,286 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+using System;
+using Test.Cryptography;
+using Xunit;
+
+namespace System.Security.Cryptography.Encoding.Tests.Cbor
+{
+    public partial class CborReaderTests
+    {
+        // Data points taken from https://tools.ietf.org/html/rfc7049#appendix-A
+        // Additional pairs generated using http://cbor.me/
+
+        public const string Map = CborWriterTests.Helpers.MapPrefixIdentifier;
+
+        [Theory]
+        [InlineData(new object[] { Map }, "a0")]
+        [InlineData(new object[] { Map, 1, 2, 3, 4 }, "a201020304")]
+        [InlineData(new object[] { Map, "a", "A", "b", "B", "c", "C", "d", "D", "e", "E" }, "a56161614161626142616361436164614461656145")]
+        [InlineData(new object[] { Map, "a", "A", -1, 2, new byte[] { }, new byte[] { 1 } }, "a3616161412002404101")]
+        public static void ReadMap_SimpleValues_HappyPath(object[] expectedValues, string hexEncoding)
+        {
+            byte[] encoding = hexEncoding.HexToByteArray();
+            var reader = new CborReader(encoding);
+            Helpers.VerifyMap(reader, expectedValues);
+            Assert.Equal(CborReaderState.Finished, reader.Peek());
+        }
+
+        [Theory]
+        [InlineData(new object[] { Map, "a", 1, "b", new object[] { Map, 2, 3 } }, "a26161016162a10203")]
+        [InlineData(new object[] { Map, "a", new object[] { Map, 2, 3 }, "b", new object[] { Map, "x", -1, "y", new object[] { Map, "z", 0 } } }, "a26161a102036162a26178206179a1617a00")]
+        [InlineData(new object[] { Map, new object[] { Map, "x", 2 }, 42 }, "a1a1617802182a")] // using maps as keys
+        public static void ReadMap_NestedValues_HappyPath(object[] expectedValues, string hexEncoding)
+        {
+            byte[] encoding = hexEncoding.HexToByteArray();
+            var reader = new CborReader(encoding);
+            Helpers.VerifyMap(reader, expectedValues);
+            Assert.Equal(CborReaderState.Finished, reader.Peek());
+        }
+
+        [Theory]
+        [InlineData(new object[] { Map, "a", 1, "b", new object[] { 2, 3 } }, "a26161016162820203")]
+        [InlineData(new object[] { Map, "a", new object[] { 2, 3, "b", new object[] { Map, "x", -1, "y", new object[] { "z", 0 } } } }, "a161618402036162a2617820617982617a00")]
+        [InlineData(new object[] { "a", new object[] { Map, "b", "c" } }, "826161a161626163")]
+        [InlineData(new object[] { Map, new object[] { 1 }, 42 }, "a18101182a")] // using arrays as keys
+        public static void ReadMap_NestedListValues_HappyPath(object expectedValue, string hexEncoding)
+        {
+            byte[] encoding = hexEncoding.HexToByteArray();
+            var reader = new CborReader(encoding);
+            Helpers.VerifyValue(reader, expectedValue);
+            Assert.Equal(CborReaderState.Finished, reader.Peek());
+        }
+
+
+        [Theory]
+        [InlineData(new object[] { Map, "a", 1, "a", 2 }, "a2616101616102")]
+        public static void ReadMap_DuplicateKeys_ShouldSucceed(object[] values, string hexEncoding)
+        {
+            byte[] encoding = hexEncoding.HexToByteArray();
+            var reader = new CborReader(encoding);
+            Helpers.VerifyMap(reader, values);
+            Assert.Equal(CborReaderState.Finished, reader.Peek());
+        }
+
+        [Theory]
+        [InlineData("a0", 0)]
+        [InlineData("a10102", 1)]
+        [InlineData("a3010203040506", 3)]
+        public static void ReadMap_DefiniteLengthExceeded_ShouldThrowInvalidOperationException(string hexEncoding, int expectedLength)
+        {
+            byte[] encoding = hexEncoding.HexToByteArray();
+            var reader = new CborReader(encoding);
+
+            ulong? length = reader.ReadStartMap();
+            Assert.Equal(expectedLength, (int)length!.Value);
+
+            for (int i = 0; i < expectedLength; i++)
+            {
+                reader.ReadInt64(); // key
+                reader.ReadInt64(); // value
+            }
+
+            Assert.Throws<InvalidOperationException>(() => reader.ReadInt64());
+        }
+
+        [Theory]
+        [InlineData("a101a10101", 1)]
+        [InlineData("a301a1010102a1020203a10303", 3)]
+        public static void ReadMap_DefiniteLengthExceeded_WithNestedData_ShouldThrowInvalidOperationException(string hexEncoding, int expectedLength)
+        {
+            byte[] encoding = hexEncoding.HexToByteArray();
+            var reader = new CborReader(encoding);
+
+            ulong? length = reader.ReadStartMap();
+            Assert.Equal(expectedLength, (int)length!.Value);
+
+            for (int i = 0; i < expectedLength; i++)
+            {
+                reader.ReadInt64(); // key
+
+                // value
+                ulong? nestedLength = reader.ReadStartMap();
+                Assert.Equal(1, (int)nestedLength!.Value);
+                reader.ReadInt64();
+                reader.ReadInt64();
+                reader.ReadEndMap();
+            }
+
+            Assert.Throws<InvalidOperationException>(() => reader.ReadInt64());
+        }
+
+        [Theory]
+        [InlineData("a10101", 1)]
+        [InlineData("a3010203040506", 3)]
+        public static void ReadEndMap_DefiniteLengthNotMet_ShouldThrowInvalidOperationException(string hexEncoding, int expectedLength)
+        {
+            byte[] encoding = hexEncoding.HexToByteArray();
+            var reader = new CborReader(encoding);
+
+            ulong? length = reader.ReadStartMap();
+            Assert.Equal(expectedLength, (int)length!.Value);
+
+            for (int i = 1; i < expectedLength; i++)
+            {
+                reader.ReadInt64(); // key
+                reader.ReadInt64(); // value
+            }
+
+            Assert.Throws<InvalidOperationException>(() => reader.ReadEndMap());
+        }
+
+        [Theory]
+        [InlineData("a101a10101", 1)]
+        [InlineData("a301a1010102a10202a3a10303", 3)]
+        public static void ReadEndMap_DefiniteLengthNotMet_WithNestedData_ShouldThrowInvalidOperationException(string hexEncoding, int expectedLength)
+        {
+            byte[] encoding = hexEncoding.HexToByteArray();
+            var reader = new CborReader(encoding);
+
+            ulong? length = reader.ReadStartMap();
+            Assert.Equal(expectedLength, (int)length!.Value);
+
+            for (int i = 1; i < expectedLength; i++)
+            {
+                reader.ReadInt64(); // key
+
+                ulong? nestedLength = reader.ReadStartMap();
+                Assert.Equal(1, (int)nestedLength!.Value);
+                reader.ReadInt64();
+                reader.ReadInt64();
+                reader.ReadEndMap();
+            }
+
+            Assert.Throws<InvalidOperationException>(() => reader.ReadEndMap());
+        }
+
+        [Theory]
+        [InlineData("80", 0)]
+        [InlineData("80", 1)]
+        [InlineData("8180", 2)]
+        public static void ReadEndMap_ImbalancedCall_ShouldThrowInvalidOperationException(string hexEncoding, int depth)
+        {
+            byte[] encoding = hexEncoding.HexToByteArray();
+            var reader = new CborReader(encoding);
+            for (int i = 0; i < depth; i++)
+            {
+                reader.ReadStartArray();
+            }
+
+            Assert.Throws<InvalidOperationException>(() => reader.ReadEndMap());
+        }
+
+        [Theory]
+        [InlineData("a2011907e4", 2, 1)]
+        [InlineData("a6011a01344224031a01344224", 6, 2)]
+        public static void ReadMap_IncorrectDefiniteLength_ShouldThrowFormatException(string hexEncoding, int expectedLength, int actualLength)
+        {
+            byte[] encoding = hexEncoding.HexToByteArray();
+            var reader = new CborReader(encoding);
+
+            ulong? length = reader.ReadStartMap();
+            Assert.Equal(expectedLength, (int)length!.Value);
+
+            for (int i = 0; i < actualLength; i++)
+            {
+                reader.ReadInt64(); // key
+                reader.ReadInt64(); // value
+            }
+
+            Assert.Throws<FormatException>(() => reader.ReadInt64());
+        }
+
+        [Theory]
+        [InlineData("a201811907e4", 2, 1)]
+        [InlineData("a61907e4811907e402811907e4", 6, 2)]
+        public static void ReadMap_IncorrectDefiniteLength_NestedValues_ShouldThrowFormatException(string hexEncoding, int expectedLength, int actualLength)
+        {
+            byte[] encoding = hexEncoding.HexToByteArray();
+            var reader = new CborReader(encoding);
+
+            ulong? length = reader.ReadStartMap();
+            Assert.Equal(expectedLength, (int)length!.Value);
+
+            for (int i = 0; i < actualLength; i++)
+            {
+                reader.ReadInt64(); // key
+
+                ulong? innerLength = reader.ReadStartArray();
+                Assert.Equal(1, (int)innerLength!.Value);
+                reader.ReadInt64();
+                reader.ReadEndArray();
+            }
+
+            Assert.Throws<FormatException>(() => reader.ReadInt64());
+        }
+
+        [Fact]
+        public static void ReadStartMap_EmptyBuffer_ShouldThrowFormatException()
+        {
+            byte[] encoding = Array.Empty<byte>();
+            var reader = new CborReader(encoding);
+
+            Assert.Throws<FormatException>(() => reader.ReadStartMap());
+        }
+
+        [Theory]
+        [InlineData("00")] // 0
+        [InlineData("20")] // -1
+        [InlineData("40")] // empty byte string
+        [InlineData("60")] // empty text string
+        [InlineData("f6")] // null
+        [InlineData("80")] // []
+        [InlineData("f97e00")] // NaN
+        [InlineData("fb3ff199999999999a")] // 1.1
+        public static void ReadStartMap_InvalidType_ShouldThrowInvalidOperationException(string hexEncoding)
+        {
+            byte[] data = hexEncoding.HexToByteArray();
+            var reader = new CborReader(data);
+            Assert.Throws<InvalidOperationException>(() => reader.ReadStartMap());
+        }
+
+        [Theory]
+        // Invalid initial bytes with map major type
+        [InlineData("bc")]
+        [InlineData("bd")]
+        [InlineData("be")]
+        // valid initial bytes missing required definite length data
+        [InlineData("b8")]
+        [InlineData("b912")]
+        [InlineData("ba000000")]
+        [InlineData("bb00000000000000")]
+        public static void ReadStartMap_InvalidData_ShouldThrowFormatException(string hexEncoding)
+        {
+            byte[] data = hexEncoding.HexToByteArray();
+            var reader = new CborReader(data);
+
+            Assert.Throws<FormatException>(() => reader.ReadStartMap());
+        }
+
+        [Theory]
+        [InlineData("b1")]
+        [InlineData("b20101")]
+        [InlineData("bb7fffffffffffffff")] // long.MaxValue
+        public static void ReadStartMap_BufferTooSmall_ShouldThrowFormatException(string hexEncoding)
+        {
+            byte[] data = hexEncoding.HexToByteArray();
+            var reader = new CborReader(data);
+
+            Assert.Throws<FormatException>(() => reader.ReadStartMap());
+        }
+
+        [Theory]
+        [InlineData("bb8000000000000000")] // long.MaxValue + 1
+        [InlineData("bbffffffffffffffff")] // ulong.MaxValue
+        public static void ReadStartMap_LargeFieldCount_ShouldThrowOverflowException(string hexEncoding)
+        {
+            byte[] data = hexEncoding.HexToByteArray();
+            var reader = new CborReader(data);
+
+            Assert.Throws<OverflowException>(() => reader.ReadStartMap());
+        }
+    }
+}
index 690b58d26bf0f4c6183b946d6a1c6d7ca331dc11..e690655b340728e0d769256bc64914ba3f3dd639 100644 (file)
@@ -25,7 +25,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         {
             byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
             using var writer = new CborWriter();
-            ArrayWriterHelper.WriteArray(writer, values);
+            Helpers.WriteArray(writer, values);
             byte[] actualEncoding = writer.ToArray();
             AssertHelper.HexEqual(expectedEncoding, actualEncoding);
         }
@@ -38,7 +38,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         {
             byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
             using var writer = new CborWriter();
-            ArrayWriterHelper.WriteArray(writer, values);
+            Helpers.WriteArray(writer, values);
             byte[] actualEncoding = writer.ToArray();
             AssertHelper.HexEqual(expectedEncoding, actualEncoding);
         }
@@ -83,7 +83,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         [InlineData(1)]
         [InlineData(3)]
         [InlineData(10)]
-        public static void EndWriteArray_DefiniteLengthNotMet_ShouldThrowInvalidOperationException(int definiteLength)
+        public static void WriteEndArray_DefiniteLengthNotMet_ShouldThrowInvalidOperationException(int definiteLength)
         {
             using var writer = new CborWriter();
             writer.WriteStartArray(definiteLength);
@@ -99,7 +99,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         [InlineData(1)]
         [InlineData(3)]
         [InlineData(10)]
-        public static void EndWriteArray_DefiniteLengthNotMet_WithNestedData_ShouldThrowInvalidOperationException(int definiteLength)
+        public static void WriteEndArray_DefiniteLengthNotMet_WithNestedData_ShouldThrowInvalidOperationException(int definiteLength)
         {
             using var writer = new CborWriter();
             writer.WriteStartArray(definiteLength);
@@ -113,31 +113,43 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             Assert.Throws<InvalidOperationException>(() => writer.WriteEndArray());
         }
 
-        [Fact]
-        public static void EndWriteArray_ImbalancedCall_ShouldThrowInvalidOperationException()
+        [Theory]
+        [InlineData(0)]
+        [InlineData(1)]
+        [InlineData(3)]
+        public static void WriteEndArray_ImbalancedCall_ShouldThrowInvalidOperationException(int depth)
         {
             using var writer = new CborWriter();
+            for (int i = 0; i < depth; i++)
+            {
+                writer.WriteStartMap(1);
+            }
+
             Assert.Throws<InvalidOperationException>(() => writer.WriteEndArray());
         }
-    }
 
-    static class ArrayWriterHelper
-    {
-        public static void WriteArray(CborWriter writer, params object[] values)
+        [Theory]
+        [InlineData(0)]
+        [InlineData(1)]
+        [InlineData(3)]
+        public static void WriteEndArray_AfterStartMap_ShouldThrowInvalidOperationException(int depth)
         {
-            writer.WriteStartArray(values.Length);
-            foreach (object value in values)
+            using var writer = new CborWriter();
+
+            for (int i = 0; i < depth; i++)
             {
-                switch (value)
+                if (i % 2 == 0)
+                {
+                    writer.WriteStartArray(1);
+                }
+                else
                 {
-                    case int i: writer.WriteInt64(i); break;
-                    case string s: writer.WriteTextString(s); break;
-                    case byte[] b: writer.WriteByteString(b); break;
-                    case object[] nested: ArrayWriterHelper.WriteArray(writer, nested); break;
-                    default: throw new ArgumentException($"Unrecognized argument type {value.GetType()}");
-                };
+                    writer.WriteStartMap(1);
+                }
             }
-            writer.WriteEndArray();
+
+            writer.WriteStartMap(definiteLength: 0);
+            Assert.Throws<InvalidOperationException>(() => writer.WriteEndArray());
         }
     }
 }
diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Helpers.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Helpers.cs
new file mode 100644 (file)
index 0000000..e276dc9
--- /dev/null
@@ -0,0 +1,68 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+
+using System.Linq;
+using Xunit;
+
+namespace System.Security.Cryptography.Encoding.Tests.Cbor
+{
+    public partial class CborWriterTests
+    {
+        internal static class Helpers
+        {
+            public const string MapPrefixIdentifier = "_map";
+
+            // Since we inject test data using attributes, meed to represent both arrays and maps using object arrays.
+            // To distinguish between the two types, we prepend map representations using a string constant.
+            public static bool IsCborMapRepresentation(object[] values)
+            {
+                return values.Length % 2 == 1 && values[0] is string s && s == MapPrefixIdentifier;
+            }
+
+            public static void WriteValue(CborWriter writer, object value)
+            {
+                switch (value)
+                {
+                    case int i: writer.WriteInt64(i); break;
+                    case long i: writer.WriteInt64(i); break;
+                    case ulong i: writer.WriteUInt64(i); break;
+                    case string s: writer.WriteTextString(s); break;
+                    case byte[] b: writer.WriteByteString(b); break;
+                    case object[] nested when IsCborMapRepresentation(nested): WriteMap(writer, nested); break;
+                    case object[] nested: WriteArray(writer, nested); break;
+                    default: throw new ArgumentException($"Unrecognized argument type {value.GetType()}");
+                };
+            }
+
+            public static void WriteArray(CborWriter writer, params object[] values)
+            {
+                writer.WriteStartArray(values.Length);
+                foreach (object value in values)
+                {
+                    WriteValue(writer, value);
+                }
+                writer.WriteEndArray();
+            }
+
+            public static void WriteMap(CborWriter writer, params object[] keyValuePairs)
+            {
+                if (!IsCborMapRepresentation(keyValuePairs))
+                {
+                    throw new ArgumentException($"CBOR map representation must contain odd number of elements prepended with a '{MapPrefixIdentifier}' constant.");
+                }
+
+                writer.WriteStartMap(keyValuePairs.Length / 2);
+
+                foreach (object value in keyValuePairs.Skip(1))
+                {
+                    WriteValue(writer, value);
+                }
+
+                writer.WriteEndMap();
+            }
+        }
+    }
+}
diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Map.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Map.cs
new file mode 100644 (file)
index 0000000..46c54b0
--- /dev/null
@@ -0,0 +1,194 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+using System;
+using Test.Cryptography;
+using Xunit;
+//using static W = System.Security.Cryptography.Encoding.Tests.Cbor.CborWriterHelpers;
+
+namespace System.Security.Cryptography.Encoding.Tests.Cbor
+{
+    public partial class CborWriterTests
+    {
+        // Data points taken from https://tools.ietf.org/html/rfc7049#appendix-A
+        // Additional pairs generated using http://cbor.me/
+
+        public const string Map = Helpers.MapPrefixIdentifier;
+
+        [Theory]
+        [InlineData(new object[] { Map }, "a0")]
+        [InlineData(new object[] { Map, 1, 2, 3, 4 }, "a201020304")]
+        [InlineData(new object[] { Map, "a", "A", "b", "B", "c", "C", "d", "D", "e", "E" }, "a56161614161626142616361436164614461656145")]
+        [InlineData(new object[] { Map, "a", "A", -1, 2, new byte[] { }, new byte[] { 1 } }, "a3616161412002404101")]
+        public static void WriteMap_SimpleValues_HappyPath(object[] values, string expectedHexEncoding)
+        {
+            byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
+            using var writer = new CborWriter();
+            Helpers.WriteMap(writer, values);
+            byte[] actualEncoding = writer.ToArray();
+            AssertHelper.HexEqual(expectedEncoding, actualEncoding);
+        }
+
+        [Theory]
+        [InlineData(new object[] { Map, "a", 1, "b", new object[] { Map, 2, 3 } }, "a26161016162a10203")]
+        [InlineData(new object[] { Map, "a", new object[] { Map, 2, 3 }, "b", new object[] { Map, "x", -1, "y", new object[] { Map, "z", 0 } } }, "a26161a102036162a26178206179a1617a00")]
+        [InlineData(new object[] { Map, new object[] { Map, "x", 2 }, 42 }, "a1a1617802182a")] // using maps as keys
+        public static void WriteMap_NestedValues_HappyPath(object[] values, string expectedHexEncoding)
+        {
+            byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
+            using var writer = new CborWriter();
+            Helpers.WriteMap(writer, values);
+            byte[] actualEncoding = writer.ToArray();
+            AssertHelper.HexEqual(expectedEncoding, actualEncoding);
+        }
+
+        [Theory]
+        [InlineData(new object[] { Map, "a", 1, "b", new object[] { 2, 3 } }, "a26161016162820203")]
+        [InlineData(new object[] { Map, "a", new object[] { 2, 3, "b", new object[] { Map, "x", -1, "y", new object[] { "z", 0 } } } }, "a161618402036162a2617820617982617a00")]
+        [InlineData(new object[] { "a", new object[] { Map, "b", "c" } }, "826161a161626163")]
+        [InlineData(new object[] { Map, new object[] { 1 }, 42 }, "a18101182a")] // using arrays as keys
+        public static void WriteMap_NestedListValues_HappyPath(object value, string expectedHexEncoding)
+        {
+            byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
+            using var writer = new CborWriter();
+            Helpers.WriteValue(writer, value);
+            byte[] actualEncoding = writer.ToArray();
+            AssertHelper.HexEqual(expectedEncoding, actualEncoding);
+        }
+
+        [Theory]
+        [InlineData(new object[] { Map, "a", 1, "a", 2 }, "a2616101616102")]
+        public static void WriteMap_DuplicateKeys_ShouldSucceed(object[] values, string expectedHexEncoding)
+        {
+            byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
+            using var writer = new CborWriter();
+            Helpers.WriteMap(writer, values);
+            byte[] actualEncoding = writer.ToArray();
+            AssertHelper.HexEqual(expectedEncoding, actualEncoding);
+        }
+
+        [Theory]
+        [InlineData(0)]
+        [InlineData(1)]
+        [InlineData(3)]
+        [InlineData(10)]
+        public static void WriteMap_DefiniteLengthExceeded_ShouldThrowInvalidOperationException(int definiteLength)
+        {
+            using var writer = new CborWriter();
+            writer.WriteStartMap(definiteLength);
+            for (int i = 0; i < definiteLength; i++)
+            {
+                writer.WriteTextString($"key_{i}");
+                writer.WriteInt64(i);
+            }
+
+            Assert.Throws<InvalidOperationException>(() => writer.WriteInt64(0));
+        }
+
+        [Theory]
+        [InlineData(0)]
+        [InlineData(1)]
+        [InlineData(3)]
+        [InlineData(10)]
+        public static void WriteMap_DefiniteLengthExceeded_WithNestedData_ShouldThrowInvalidOperationException(int definiteLength)
+        {
+            using var writer = new CborWriter();
+            writer.WriteStartMap(definiteLength);
+            for (int i = 0; i < definiteLength; i++)
+            {
+                writer.WriteTextString($"key_{i}");
+                writer.WriteStartMap(definiteLength: 1);
+                writer.WriteInt64(i);
+                writer.WriteInt64(i);
+                writer.WriteEndMap();
+            }
+
+            Assert.Throws<InvalidOperationException>(() => writer.WriteInt64(0));
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(3)]
+        [InlineData(10)]
+        public static void EndWriteMap_DefiniteLengthNotMet_ShouldThrowInvalidOperationException(int definiteLength)
+        {
+            using var writer = new CborWriter();
+            writer.WriteStartMap(definiteLength);
+            for (int i = 1; i < definiteLength; i++)
+            {
+                writer.WriteTextString($"key_{i}");
+                writer.WriteInt64(i);
+            }
+
+            Assert.Throws<InvalidOperationException>(() => writer.WriteEndMap());
+        }
+
+        [Theory]
+        [InlineData(1)]
+        [InlineData(3)]
+        [InlineData(10)]
+        public static void EndWriteMap_DefiniteLengthNotMet_WithNestedData_ShouldThrowInvalidOperationException(int definiteLength)
+        {
+            using var writer = new CborWriter();
+            writer.WriteStartMap(definiteLength);
+            for (int i = 1; i < definiteLength; i++)
+            {
+                writer.WriteTextString($"key_{i}");
+                writer.WriteStartMap(definiteLength: 1);
+                writer.WriteInt64(i);
+                writer.WriteInt64(i);
+                writer.WriteEndMap();
+            }
+
+            Assert.Throws<InvalidOperationException>(() => writer.WriteEndMap());
+        }
+
+        [Fact]
+        public static void EndWriteMap_ImbalancedCall_ShouldThrowInvalidOperationException()
+        {
+            using var writer = new CborWriter();
+            Assert.Throws<InvalidOperationException>(() => writer.WriteEndMap());
+        }
+
+        [Theory]
+        [InlineData(0)]
+        [InlineData(1)]
+        [InlineData(3)]
+        public static void WriteEndMap_ImbalancedCall_ShouldThrowInvalidOperationException(int depth)
+        {
+            using var writer = new CborWriter();
+            for (int i = 0; i < depth; i++)
+            {
+                writer.WriteStartArray(1);
+            }
+
+            Assert.Throws<InvalidOperationException>(() => writer.WriteEndMap());
+        }
+
+        [Theory]
+        [InlineData(0)]
+        [InlineData(1)]
+        [InlineData(3)]
+        public static void WriteEndMap_AfterStartArray_ShouldThrowInvalidOperationException(int depth)
+        {
+            using var writer = new CborWriter();
+
+            for (int i = 0; i < depth; i++)
+            {
+                if (i % 2 == 0)
+                {
+                    writer.WriteStartArray(1);
+                }
+                else
+                {
+                    writer.WriteStartMap(1);
+                }
+            }
+
+            writer.WriteStartArray(definiteLength: 0);
+            Assert.Throws<InvalidOperationException>(() => writer.WriteEndMap());
+        }
+    }
+}
index 90dc83bc0f68e6819b51b414bbe319f493a59658..ef82fb49075193f0de73cc40f71aaa975420e004 100644 (file)
@@ -20,10 +20,10 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
     internal enum CborAdditionalInfo : byte
     {
-        UnsignedInteger8BitEncoding = 24,
-        UnsignedInteger16BitEncoding = 25,
-        UnsignedInteger32BitEncoding = 26,
-        UnsignedInteger64BitEncoding = 27,
+        Unsigned8BitIntegerEncoding = 24,
+        Unsigned16BitIntegerEncoding = 25,
+        Unsigned32BitIntegerEncoding = 26,
+        Unsigned64BitIntegerEncoding = 27,
         IndefiniteLength = 31,
     }
 
index 3b5cf349e46ae7db80057930a5144d629f031888..a3bb7b879fa24cb1bab3f92e433b6be3ccc94772 100644 (file)
@@ -74,26 +74,26 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
             switch (header.AdditionalInfo)
             {
-                case CborAdditionalInfo x when (x < CborAdditionalInfo.UnsignedInteger8BitEncoding):
+                case CborAdditionalInfo x when (x < CborAdditionalInfo.Unsigned8BitIntegerEncoding):
                     additionalBytes = 0;
                     return (ulong)x;
 
-                case CborAdditionalInfo.UnsignedInteger8BitEncoding:
+                case CborAdditionalInfo.Unsigned8BitIntegerEncoding:
                     EnsureBuffer(2);
                     additionalBytes = 1;
                     return buffer[1];
 
-                case CborAdditionalInfo.UnsignedInteger16BitEncoding:
+                case CborAdditionalInfo.Unsigned16BitIntegerEncoding:
                     EnsureBuffer(3);
                     additionalBytes = 2;
                     return BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(1));
 
-                case CborAdditionalInfo.UnsignedInteger32BitEncoding:
+                case CborAdditionalInfo.Unsigned32BitIntegerEncoding:
                     EnsureBuffer(5);
                     additionalBytes = 4;
                     return BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(1));
 
-                case CborAdditionalInfo.UnsignedInteger64BitEncoding:
+                case CborAdditionalInfo.Unsigned64BitIntegerEncoding:
                     EnsureBuffer(9);
                     additionalBytes = 8;
                     return BinaryPrimitives.ReadUInt64BigEndian(buffer.Slice(1));
diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Map.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Map.cs
new file mode 100644 (file)
index 0000000..aaca54e
--- /dev/null
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+using System.Buffers.Binary;
+
+namespace System.Security.Cryptography.Encoding.Tests.Cbor
+{
+    internal partial class CborReader
+    {
+        public ulong? ReadStartMap()
+        {
+            CborInitialByte header = PeekInitialByte(expectedType: CborMajorType.Map);
+            ulong arrayLength = checked((ulong)ReadUnsignedInteger(header, out int additionalBytes));
+            AdvanceBuffer(1 + additionalBytes);
+            _remainingDataItems--;
+
+            if (arrayLength > long.MaxValue)
+            {
+                throw new OverflowException("Read CBOR map field count exceeds supported size.");
+            }
+
+            PushDataItem(CborMajorType.Map, 2 * arrayLength);
+            return arrayLength;
+        }
+
+        public void ReadEndMap()
+        {
+            PopDataItem(expectedType: CborMajorType.Map);
+        }
+    }
+}
index 73621ba1564be71aad28c9538daf8c4fa1861840..83673c81247085ed4d4e4f91f430284951178423 100644 (file)
@@ -117,6 +117,11 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
         private void PushDataItem(CborMajorType type, ulong? expectedNestedItems)
         {
+            if (expectedNestedItems > (ulong)_buffer.Length)
+            {
+                throw new FormatException("Insufficient buffer size for declared definite length in CBOR data item.");
+            }
+
             _nestedDataItemStack ??= new Stack<(CborMajorType, ulong?)>();
             _nestedDataItemStack.Push((type, _remainingDataItems));
             _remainingDataItems = expectedNestedItems;
index 650524919398dd43119c853d62c6b8888dbd731e..f79cbd97be20d167eb2de73f88b45d702673bda9 100644 (file)
@@ -41,28 +41,28 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             else if (value <= byte.MaxValue)
             {
                 EnsureWriteCapacity(2);
-                _buffer[_offset] = new CborInitialByte(type, CborAdditionalInfo.UnsignedInteger8BitEncoding).InitialByte;
+                _buffer[_offset] = new CborInitialByte(type, CborAdditionalInfo.Unsigned8BitIntegerEncoding).InitialByte;
                 _buffer[_offset + 1] = (byte)value;
                 _offset += 2;
             }
             else if (value <= ushort.MaxValue)
             {
                 EnsureWriteCapacity(3);
-                _buffer[_offset] = new CborInitialByte(type, CborAdditionalInfo.UnsignedInteger16BitEncoding).InitialByte;
+                _buffer[_offset] = new CborInitialByte(type, CborAdditionalInfo.Unsigned16BitIntegerEncoding).InitialByte;
                 BinaryPrimitives.WriteUInt16BigEndian(_buffer.AsSpan(_offset + 1), (ushort)value);
                 _offset += 3;
             }
             else if (value <= uint.MaxValue)
             {
                 EnsureWriteCapacity(5);
-                _buffer[_offset] = new CborInitialByte(type, CborAdditionalInfo.UnsignedInteger32BitEncoding).InitialByte;
+                _buffer[_offset] = new CborInitialByte(type, CborAdditionalInfo.Unsigned32BitIntegerEncoding).InitialByte;
                 BinaryPrimitives.WriteUInt32BigEndian(_buffer.AsSpan(_offset + 1), (uint)value);
                 _offset += 5;
             }
             else
             {
                 EnsureWriteCapacity(9);
-                _buffer[_offset] = new CborInitialByte(type, CborAdditionalInfo.UnsignedInteger64BitEncoding).InitialByte;
+                _buffer[_offset] = new CborInitialByte(type, CborAdditionalInfo.Unsigned64BitIntegerEncoding).InitialByte;
                 BinaryPrimitives.WriteUInt64BigEndian(_buffer.AsSpan(_offset + 1), value);
                 _offset += 9;
             }
diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Map.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Map.cs
new file mode 100644 (file)
index 0000000..6304c7a
--- /dev/null
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+using System.Text;
+
+namespace System.Security.Cryptography.Encoding.Tests.Cbor
+{
+    internal partial class CborWriter
+    {
+        public void WriteStartMap(int definiteLength)
+        {
+            if (definiteLength < 0)
+            {
+                throw new ArgumentOutOfRangeException(nameof(definiteLength), "must be non-negative integer.");
+            }
+
+            WriteUnsignedInteger(CborMajorType.Map, (ulong)definiteLength);
+            PushDataItem(CborMajorType.Map, 2 * (uint)definiteLength);
+        }
+
+        public void WriteEndMap()
+        {
+            PopDataItem(CborMajorType.Map);
+        }
+    }
+}
index a397df219aac19893e13ca8ceaee9740ce17b970..28af9302b239f19950fc437823d83d9705ced5db 100644 (file)
     <Compile Include="Asn1\Writer\WriteObjectIdentifier.cs" />
     <Compile Include="Asn1\Writer\WriteUtcTime.cs" />
     <Compile Include="Asn1\Writer\WriteUtf8String.cs" />
+    <Compile Include="Cbor.Tests\CborReaderTests.Helpers.cs" />
+    <Compile Include="Cbor.Tests\CborReaderTests.Map.cs" />
+    <Compile Include="Cbor.Tests\CborWriterTests.Helpers.cs" />
     <Compile Include="Cbor.Tests\CborReaderTests.Array.cs" />
     <Compile Include="Cbor.Tests\CborWriterTests.Array.cs" />
     <Compile Include="Cbor.Tests\CborWriterTests.cs" />
+    <Compile Include="Cbor.Tests\CborWriterTests.Map.cs" />
     <Compile Include="Cbor\CborInitialByte.cs" />
     <Compile Include="Cbor\CborReader.Array.cs" />
     <Compile Include="Cbor\CborReader.cs" />
     <Compile Include="Cbor\CborReader.Integer.cs" />
+    <Compile Include="Cbor\CborReader.Map.cs" />
     <Compile Include="Cbor\CborReader.String.cs" />
     <Compile Include="Cbor\CborWriter.Array.cs" />
+    <Compile Include="Cbor\CborWriter.Map.cs" />
     <Compile Include="Cbor\CborWriter.String.cs" />
     <Compile Include="Cbor\CborWriter.cs" />
     <Compile Include="Cbor\CborWriter.Integer.cs" />