From f7e7ec6f5552d6a6444aad61426f08b7ed37116f Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 16 Mar 2020 22:00:12 +0000 Subject: [PATCH] [CBOR] Implement Map support for CborReader and CborWriter (#33500) * 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 --- .../tests/Cbor.Tests/CborReaderTests.Array.cs | 73 ++---- .../tests/Cbor.Tests/CborReaderTests.Helpers.cs | 102 ++++++++ .../tests/Cbor.Tests/CborReaderTests.Map.cs | 286 +++++++++++++++++++++ .../tests/Cbor.Tests/CborWriterTests.Array.cs | 52 ++-- .../tests/Cbor.Tests/CborWriterTests.Helpers.cs | 68 +++++ .../tests/Cbor.Tests/CborWriterTests.Map.cs | 194 ++++++++++++++ .../tests/Cbor/CborInitialByte.cs | 8 +- .../tests/Cbor/CborReader.Integer.cs | 10 +- .../tests/Cbor/CborReader.Map.cs | 33 +++ .../tests/Cbor/CborReader.cs | 5 + .../tests/Cbor/CborWriter.Integer.cs | 8 +- .../tests/Cbor/CborWriter.Map.cs | 28 ++ ...tem.Security.Cryptography.Encoding.Tests.csproj | 6 + 13 files changed, 783 insertions(+), 90 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Helpers.cs create mode 100644 src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Map.cs create mode 100644 src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Helpers.cs create mode 100644 src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Map.cs create mode 100644 src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Map.cs create mode 100644 src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Map.cs diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Array.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Array.cs index 56ffd1b..22a27e4 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Array.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Array.cs @@ -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(); 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(() => 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(() => 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 index 0000000..6eba42b --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Helpers.cs @@ -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 index 0000000..c443f35 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Map.cs @@ -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(() => 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(() => 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(() => 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(() => 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(() => 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(() => 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(() => reader.ReadInt64()); + } + + [Fact] + public static void ReadStartMap_EmptyBuffer_ShouldThrowFormatException() + { + byte[] encoding = Array.Empty(); + var reader = new CborReader(encoding); + + Assert.Throws(() => 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(() => 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(() => 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(() => 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(() => reader.ReadStartMap()); + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Array.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Array.cs index 690b58d..e690655 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Array.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Array.cs @@ -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(() => 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(() => 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(() => 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 index 0000000..e276dc9 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Helpers.cs @@ -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 index 0000000..46c54b0 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Map.cs @@ -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(() => 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(() => 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(() => 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(() => writer.WriteEndMap()); + } + + [Fact] + public static void EndWriteMap_ImbalancedCall_ShouldThrowInvalidOperationException() + { + using var writer = new CborWriter(); + Assert.Throws(() => 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(() => 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(() => writer.WriteEndMap()); + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborInitialByte.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborInitialByte.cs index 90dc83b..ef82fb4 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborInitialByte.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborInitialByte.cs @@ -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, } diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Integer.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Integer.cs index 3b5cf34..a3bb7b8 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Integer.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Integer.cs @@ -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 index 0000000..aaca54e --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Map.cs @@ -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); + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.cs index 73621ba..83673c8 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.cs @@ -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; diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Integer.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Integer.cs index 6505249..f79cbd9 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Integer.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Integer.cs @@ -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 index 0000000..6304c7a --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Map.cs @@ -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); + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/System.Security.Cryptography.Encoding.Tests.csproj b/src/libraries/System.Security.Cryptography.Encoding/tests/System.Security.Cryptography.Encoding.Tests.csproj index a397df2..28af930 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/System.Security.Cryptography.Encoding.Tests.csproj +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/System.Security.Cryptography.Encoding.Tests.csproj @@ -48,15 +48,21 @@ + + + + + + -- 2.7.4