From c3f7da2008a4c7508fac1c4ff4722ca85a2214fe Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 30 Mar 2020 21:49:09 +0100 Subject: [PATCH] [CBOR] Implement tag and special value support for CborWriter and CborReader (#34046) * Implement tag support for CborWriter and CborReader * Implement CBOR special value support * add nested special value tests * implement half-precision float decoding; address feedback * address style * remove dead code * add checks for CBOR tags in indefinite-length collections * Update src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Integer.cs Co-Authored-By: Jeremy Barton Co-authored-by: Jeremy Barton --- .../tests/Cbor.Tests/CborReaderTests.Array.cs | 2 + .../Cbor.Tests/CborReaderTests.Helpers.cs | 23 +- .../Cbor.Tests/CborReaderTests.Integer.cs | 114 +++++++++ .../Cbor.Tests/CborReaderTests.Special.cs | 233 ++++++++++++++++++ .../tests/Cbor.Tests/CborReaderTests.cs | 2 +- .../tests/Cbor.Tests/CborWriterTests.Array.cs | 2 + .../Cbor.Tests/CborWriterTests.Helpers.cs | 4 + .../Cbor.Tests/CborWriterTests.Integer.cs | 67 ++++- .../Cbor.Tests/CborWriterTests.Special.cs | 83 +++++++ .../tests/Cbor/CborInitialByte.cs | 12 +- .../tests/Cbor/CborReader.Array.cs | 4 +- .../tests/Cbor/CborReader.Integer.cs | 28 ++- .../tests/Cbor/CborReader.Map.cs | 4 +- .../tests/Cbor/CborReader.Special.cs | 162 ++++++++++++ .../tests/Cbor/CborReader.String.cs | 20 +- .../tests/Cbor/CborReader.cs | 46 +++- .../tests/Cbor/CborTag.cs | 39 +++ .../tests/Cbor/CborWriter.Array.cs | 14 +- .../tests/Cbor/CborWriter.Integer.cs | 20 +- .../tests/Cbor/CborWriter.Map.cs | 15 +- .../tests/Cbor/CborWriter.Special.cs | 58 +++++ .../tests/Cbor/CborWriter.String.cs | 16 +- .../tests/Cbor/CborWriter.cs | 19 +- ...ecurity.Cryptography.Encoding.Tests.csproj | 5 + 24 files changed, 923 insertions(+), 69 deletions(-) create mode 100644 src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Special.cs create mode 100644 src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Special.cs create mode 100644 src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Special.cs create mode 100644 src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborTag.cs create mode 100644 src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Special.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 e836967ab88271..5f479b6a085641 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 @@ -21,6 +21,7 @@ public partial class CborReaderTests [InlineData(new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 }, "98190102030405060708090a0b0c0d0e0f101112131415161718181819")] [InlineData(new object[] { 1, -1, "", new byte[] { 7 } }, "840120604107")] [InlineData(new object[] { "lorem", "ipsum", "dolor" }, "83656c6f72656d65697073756d65646f6c6f72")] + [InlineData(new object?[] { false, null, float.NaN, double.PositiveInfinity }, "84f4f6faffc00000fb7ff0000000000000")] public static void ReadArray_SimpleValues_HappyPath(object[] expectedValues, string hexEncoding) { byte[] encoding = hexEncoding.HexToByteArray(); @@ -48,6 +49,7 @@ public static void ReadArray_NestedValues_HappyPath(object[] expectedValues, str [InlineData(new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 }, "9f0102030405060708090a0b0c0d0e0f101112131415161718181819ff")] [InlineData(new object[] { 1, -1, "", new byte[] { 7 } }, "9f0120604107ff")] [InlineData(new object[] { "lorem", "ipsum", "dolor" }, "9f656c6f72656d65697073756d65646f6c6f72ff")] + [InlineData(new object?[] { false, null, float.NaN, double.PositiveInfinity }, "9ff4f6faffc00000fb7ff0000000000000ff")] public static void ReadArray_IndefiniteLength_HappyPath(object[] expectedValues, string hexEncoding) { byte[] encoding = hexEncoding.HexToByteArray(); 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 index 16f444b6946a2a..d1bca366b3494c 100644 --- 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 @@ -17,6 +17,15 @@ public static void VerifyValue(CborReader reader, object expectedValue, bool exp { switch (expectedValue) { + case null: + Assert.Equal(CborReaderState.Null, reader.Peek()); + reader.ReadNull(); + break; + case bool expected: + Assert.Equal(CborReaderState.Boolean, reader.Peek()); + bool b = reader.ReadBoolean(); + Assert.Equal(expected, b); + break; case int expected: VerifyPeekInteger(reader, isUnsignedInteger: expected >= 0); long i = reader.ReadInt64(); @@ -32,6 +41,16 @@ public static void VerifyValue(CborReader reader, object expectedValue, bool exp ulong u = reader.ReadUInt64(); Assert.Equal(expected, u); break; + case float expected: + Assert.Equal(CborReaderState.SinglePrecisionFloat, reader.Peek()); + float f = reader.ReadSingle(); + Assert.Equal(expected, f); + break; + case double expected: + Assert.Equal(CborReaderState.DoublePrecisionFloat, reader.Peek()); + double d = reader.ReadDouble(); + Assert.Equal(expected, d); + break; case string expected: Assert.Equal(CborReaderState.TextString, reader.Peek()); string s = reader.ReadTextString(); @@ -39,8 +58,8 @@ public static void VerifyValue(CborReader reader, object expectedValue, bool exp break; case byte[] expected: Assert.Equal(CborReaderState.ByteString, reader.Peek()); - byte[] b = reader.ReadByteString(); - Assert.Equal(expected.ByteArrayToHex(), b.ByteArrayToHex()); + byte[] bytes = reader.ReadByteString(); + Assert.Equal(expected.ByteArrayToHex(), bytes.ByteArrayToHex()); break; case string[] expectedChunks: Assert.Equal(CborReaderState.StartTextString, reader.Peek()); diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Integer.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Integer.cs index 62a04378d245d6..3542d3412ec00c 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Integer.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Integer.cs @@ -100,6 +100,49 @@ public static void ReadCborNegativeIntegerEncoding_SingleValue_HappyPath(ulong e Assert.Equal(CborReaderState.Finished, reader.Peek()); } + [Theory] + [InlineData(2, 2, "c202")] + [InlineData(0, "2013-03-21T20:04:00Z", "c074323031332d30332d32315432303a30343a30305a")] + [InlineData(1, 1363896240, "c11a514b67b0")] + [InlineData(23, new byte[] { 1, 2, 3, 4 }, "d74401020304")] + [InlineData(32, "http://www.example.com", "d82076687474703a2f2f7777772e6578616d706c652e636f6d")] + [InlineData(int.MaxValue, 2, "da7fffffff02")] + [InlineData(ulong.MaxValue, new object[] { 1, 2 }, "dbffffffffffffffff820102")] + public static void ReadTag_SingleValue_HappyPath(ulong expectedTag, object expectedValue, string hexEncoding) + { + byte[] encoding = hexEncoding.HexToByteArray(); + var reader = new CborReader(encoding); + + Assert.Equal(CborReaderState.Tag, reader.Peek()); + CborTag tag = reader.ReadTag(); + Assert.Equal(expectedTag, (ulong)tag); + + Helpers.VerifyValue(reader, expectedValue); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + + [Theory] + [InlineData(new ulong[] { 1, 2, 3 }, 2, "c1c2c302")] + [InlineData(new ulong[] { 0, 0, 0 }, "2013-03-21T20:04:00Z", "c0c0c074323031332d30332d32315432303a30343a30305a")] + [InlineData(new ulong[] { int.MaxValue, ulong.MaxValue }, 1363896240, "da7fffffffdbffffffffffffffff1a514b67b0")] + [InlineData(new ulong[] { 23, 24, 100 }, new byte[] { 1, 2, 3, 4 }, "d7d818d8644401020304")] + [InlineData(new ulong[] { 32, 1, 1 }, new object[] { 1, "lorem ipsum" }, "d820c1c182016b6c6f72656d20697073756d")] + public static void ReadTag_NestedTags_HappyPath(ulong[] expectedTags, object expectedValue, string hexEncoding) + { + byte[] encoding = hexEncoding.HexToByteArray(); + var reader = new CborReader(encoding); + + foreach (ulong expectedTag in expectedTags) + { + Assert.Equal(CborReaderState.Tag, reader.Peek()); + CborTag tag = reader.ReadTag(); + Assert.Equal(expectedTag, (ulong)tag); + } + + Helpers.VerifyValue(reader, expectedValue); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + [Theory] // all possible definite-length encodings for the value 23 [InlineData("17")] @@ -156,6 +199,16 @@ public static void ReadUInt64_OutOfRangeValues_ShouldThrowOverflowException(stri Assert.Throws(() => reader.ReadUInt64()); } + [Theory] + [InlineData("c2")] + public static void ReadTag_NoSubsequentData_ShouldPeekEndOfData(string hexEncoding) + { + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + reader.ReadTag(); + Assert.Equal(CborReaderState.EndOfData, reader.Peek()); + } + [Theory] [InlineData("40")] // empty text string [InlineData("60")] // empty byte string @@ -173,6 +226,67 @@ public static void ReadInt64_InvalidTypes_ShouldThrowInvalidOperationException(s Assert.Equal("Data item major type mismatch.", exn.Message); } + [Theory] + [InlineData("40")] // empty text string + [InlineData("60")] // empty byte string + [InlineData("f6")] // null + [InlineData("80")] // [] + [InlineData("a0")] // {} + [InlineData("f97e00")] // NaN + [InlineData("fb3ff199999999999a")] // 1.1 + public static void ReadTag_InvalidTypes_ShouldThrowInvalidOperationException(string hexEncoding) + { + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + InvalidOperationException exn = Assert.Throws(() => reader.ReadTag()); + + Assert.Equal("Data item major type mismatch.", exn.Message); + } + + [Fact] + public static void ReadTag_NestedTagWithMissingPayload_ShouldThrowFormatException() + { + byte[] data = "9fc2ff".HexToByteArray(); + var reader = new CborReader(data); + + reader.ReadStartArray(); + reader.ReadTag(); + Assert.Equal(CborReaderState.FormatError, reader.Peek()); + Assert.Throws(() => reader.ReadEndArray()); + } + + [Theory] + [InlineData("8201c202")] // definite length array + [InlineData("9f01c202ff")] // equivalent indefinite-length array + public static void ReadTag_CallingEndReadArrayPrematurely_ShouldThrowInvalidOperationException(string hexEncoding) + { + // encoding is valid CBOR, so should not throw FormatException + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + + reader.ReadStartArray(); + reader.ReadInt64(); + reader.ReadTag(); + Assert.Equal(CborReaderState.UnsignedInteger, reader.Peek()); + Assert.Throws(() => reader.ReadEndArray()); + } + + [Theory] + [InlineData("a102c202")] // definite length map + [InlineData("bf02c202ff")] // equivalent indefinite-length map + public static void ReadTag_CallingEndReadMapPrematurely_ShouldThrowInvalidOperationException(string hexEncoding) + { + // encoding is valid CBOR, so should not throw FormatException + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + + reader.ReadStartMap(); + reader.ReadInt64(); + reader.ReadTag(); + Assert.Equal(CborReaderState.UnsignedInteger, reader.Peek()); + Assert.Throws(() => reader.ReadEndArray()); + } + [Theory] [InlineData("40")] // empty byte string [InlineData("60")] // empty text string diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Special.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Special.cs new file mode 100644 index 00000000000000..f33caae979f9d3 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Special.cs @@ -0,0 +1,233 @@ +// 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/ + + [Theory] + [InlineData(100000.0, "fa47c35000")] + [InlineData(3.4028234663852886e+38, "fa7f7fffff")] + [InlineData(float.PositiveInfinity, "fa7f800000")] + [InlineData(float.NegativeInfinity, "faff800000")] + [InlineData(float.NaN, "fa7fc00000")] + internal static void ReadSingle_SingleValue_HappyPath(float expectedResult, string hexEncoding) + { + byte[] encoding = hexEncoding.HexToByteArray(); + var reader = new CborReader(encoding); + Assert.Equal(CborReaderState.SinglePrecisionFloat, reader.Peek()); + float actualResult = reader.ReadSingle(); + Assert.Equal(expectedResult, actualResult); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + + [Theory] + [InlineData(1.1, "fb3ff199999999999a")] + [InlineData(1.0e+300, "fb7e37e43c8800759c")] + [InlineData(-4.1, "fbc010666666666666")] + [InlineData(3.1415926, "fb400921fb4d12d84a")] + [InlineData(double.PositiveInfinity, "fb7ff0000000000000")] + [InlineData(double.NegativeInfinity, "fbfff0000000000000")] + [InlineData(double.NaN, "fb7ff8000000000000")] + internal static void ReadDouble_SingleValue_HappyPath(double expectedResult, string hexEncoding) + { + byte[] encoding = hexEncoding.HexToByteArray(); + var reader = new CborReader(encoding); + Assert.Equal(CborReaderState.DoublePrecisionFloat, reader.Peek()); + double actualResult = reader.ReadDouble(); + Assert.Equal(expectedResult, actualResult); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + + [Theory] + [InlineData(100000.0, "fa47c35000")] + [InlineData(3.4028234663852886e+38, "fa7f7fffff")] + [InlineData(double.PositiveInfinity, "fa7f800000")] + [InlineData(double.NegativeInfinity, "faff800000")] + [InlineData(double.NaN, "fa7fc00000")] + internal static void ReadDouble_SinglePrecisionValue_ShouldCoerceToDouble(double expectedResult, string hexEncoding) + { + byte[] encoding = hexEncoding.HexToByteArray(); + var reader = new CborReader(encoding); + Assert.Equal(CborReaderState.SinglePrecisionFloat, reader.Peek()); + double actualResult = reader.ReadDouble(); + Assert.Equal(expectedResult, actualResult); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + + [Theory] + [InlineData(0.0, "f90000")] + [InlineData(-0.0, "f98000")] + [InlineData(1.0, "f93c00")] + [InlineData(1.5, "f93e00")] + [InlineData(65504.0, "f97bff")] + [InlineData(5.960464477539063e-8, "f90001")] + [InlineData(0.00006103515625, "f90400")] + [InlineData(-4.0, "f9c400")] + [InlineData(double.PositiveInfinity, "f97c00")] + [InlineData(double.NaN, "f97e00")] + [InlineData(double.NegativeInfinity, "f9fc00")] + internal static void ReadDouble_HalfPrecisionValue_ShouldCoerceToDouble(double expectedResult, string hexEncoding) + { + byte[] encoding = hexEncoding.HexToByteArray(); + var reader = new CborReader(encoding); + Assert.Equal(CborReaderState.HalfPrecisionFloat, reader.Peek()); + double actualResult = reader.ReadDouble(); + Assert.Equal(expectedResult, actualResult); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + + [Theory] + [InlineData(0.0, "f90000")] + [InlineData(-0.0, "f98000")] + [InlineData(1.0, "f93c00")] + [InlineData(1.5, "f93e00")] + [InlineData(65504.0, "f97bff")] + [InlineData(5.960464477539063e-8, "f90001")] + [InlineData(0.00006103515625, "f90400")] + [InlineData(-4.0, "f9c400")] + [InlineData(float.PositiveInfinity, "f97c00")] + [InlineData(float.NaN, "f97e00")] + [InlineData(float.NegativeInfinity, "f9fc00")] + internal static void ReadSingle_HalfPrecisionValue_ShouldCoerceToSingle(float expectedResult, string hexEncoding) + { + byte[] encoding = hexEncoding.HexToByteArray(); + var reader = new CborReader(encoding); + Assert.Equal(CborReaderState.HalfPrecisionFloat, reader.Peek()); + float actualResult = reader.ReadSingle(); + Assert.Equal(expectedResult, actualResult); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + + [Fact] + internal static void ReadNull_SingleValue_HappyPath() + { + byte[] encoding = "f6".HexToByteArray(); + var reader = new CborReader(encoding); + Assert.Equal(CborReaderState.Null, reader.Peek()); + reader.ReadNull(); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + + [Theory] + [InlineData(false, "f4")] + [InlineData(true, "f5")] + internal static void ReadBoolean_SingleValue_HappyPath(bool expectedResult, string hexEncoding) + { + byte[] encoding = hexEncoding.HexToByteArray(); + var reader = new CborReader(encoding); + Assert.Equal(CborReaderState.Boolean, reader.Peek()); + bool actualResult = reader.ReadBoolean(); + Assert.Equal(expectedResult, actualResult); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + + [Theory] + [InlineData((CborSpecialValue)0, "e0")] + [InlineData(CborSpecialValue.False, "f4")] + [InlineData(CborSpecialValue.True, "f5")] + [InlineData(CborSpecialValue.Null, "f6")] + [InlineData(CborSpecialValue.Undefined, "f7")] + [InlineData((CborSpecialValue)32, "f820")] + [InlineData((CborSpecialValue)255, "f8ff")] + internal static void ReadSpecialValue_SingleValue_HappyPath(CborSpecialValue expectedResult, string hexEncoding) + { + byte[] encoding = hexEncoding.HexToByteArray(); + var reader = new CborReader(encoding); + CborSpecialValue actualResult = reader.ReadSpecialValue(); + Assert.Equal(expectedResult, actualResult); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + + [Theory] + [InlineData("01")] // integer + [InlineData("40")] // empty text string + [InlineData("60")] // empty byte string + [InlineData("80")] // [] + [InlineData("a0")] // {} + [InlineData("c202")] // tagged value + public static void ReadSpecialValue_InvalidTypes_ShouldThrowInvalidOperationException(string hexEncoding) + { + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + InvalidOperationException exn = Assert.Throws(() => reader.ReadSpecialValue()); + + Assert.Equal("Data item major type mismatch.", exn.Message); + } + + [Theory] + [InlineData("01")] // integer + [InlineData("40")] // empty text string + [InlineData("60")] // empty byte string + [InlineData("80")] // [] + [InlineData("a0")] // {} + [InlineData("f97e00")] // NaN + [InlineData("f6")] // null + [InlineData("fb3ff199999999999a")] // 1.1 + [InlineData("c202")] // tagged value + public static void ReadBoolean_InvalidTypes_ShouldThrowInvalidOperationException(string hexEncoding) + { + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + Assert.Throws(() => reader.ReadBoolean()); + } + + [Theory] + [InlineData("01")] // integer + [InlineData("40")] // empty text string + [InlineData("60")] // empty byte string + [InlineData("80")] // [] + [InlineData("a0")] // {} + [InlineData("f4")] // false + [InlineData("f97e00")] // NaN + [InlineData("fb3ff199999999999a")] // 1.1 + [InlineData("c202")] // tagged value + public static void ReadNull_InvalidTypes_ShouldThrowInvalidOperationException(string hexEncoding) + { + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + Assert.Throws(() => reader.ReadNull()); + } + + [Theory] + [InlineData("01")] // integer + [InlineData("40")] // empty text string + [InlineData("60")] // empty byte string + [InlineData("80")] // [] + [InlineData("a0")] // {} + [InlineData("f6")] // null + [InlineData("f4")] // false + [InlineData("c202")] // tagged value + public static void ReadSingle_InvalidTypes_ShouldThrowInvalidOperationException(string hexEncoding) + { + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + Assert.Throws(() => reader.ReadSingle()); + } + + [Theory] + [InlineData("01")] // integer + [InlineData("40")] // empty text string + [InlineData("60")] // empty byte string + [InlineData("80")] // [] + [InlineData("a0")] // {} + [InlineData("f6")] // null + [InlineData("f4")] // false + [InlineData("c202")] // tagged value + public static void ReadDouble_InvalidTypes_ShouldThrowInvalidOperationException(string hexEncoding) + { + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + Assert.Throws(() => reader.ReadDouble()); + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.cs index 28a1a0384d7971..404a39bfee6273 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.cs @@ -57,7 +57,7 @@ public static void BytesRemaining_SingleRead_ShouldReturnRemainingBytes() [InlineData(CborMajorType.Array, CborReaderState.StartArray)] [InlineData(CborMajorType.Map, CborReaderState.StartMap)] [InlineData(CborMajorType.Tag, CborReaderState.Tag)] - [InlineData(CborMajorType.Special, CborReaderState.Special)] + [InlineData(CborMajorType.Special, CborReaderState.SpecialValue)] internal static void Peek_SingleByteBuffer_ShouldReturnExpectedState(CborMajorType majorType, CborReaderState expectedResult) { ReadOnlyMemory buffer = new byte[] { (byte)((byte)majorType << 5) }; 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 d96346b5c77dff..c9529164982f29 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 @@ -21,6 +21,7 @@ public partial class CborWriterTests [InlineData(new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 }, "98190102030405060708090a0b0c0d0e0f101112131415161718181819")] [InlineData(new object[] { 1, -1, "", new byte[] { 7 } }, "840120604107")] [InlineData(new object[] { "lorem", "ipsum", "dolor" }, "83656c6f72656d65697073756d65646f6c6f72")] + [InlineData(new object?[] { false, null, float.NaN, double.PositiveInfinity }, "84f4f6faffc00000fb7ff0000000000000")] public static void WriteArray_SimpleValues_HappyPath(object[] values, string expectedHexEncoding) { byte[] expectedEncoding = expectedHexEncoding.HexToByteArray(); @@ -50,6 +51,7 @@ public static void WriteArray_NestedValues_HappyPath(object[] values, string exp [InlineData(new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 }, "9f0102030405060708090a0b0c0d0e0f101112131415161718181819ff")] [InlineData(new object[] { 1, -1, "", new byte[] { 7 } }, "9f0120604107ff")] [InlineData(new object[] { "lorem", "ipsum", "dolor" }, "9f656c6f72656d65697073756d65646f6c6f72ff")] + [InlineData(new object?[] { false, null, float.NaN, double.PositiveInfinity }, "9ff4f6faffc00000fb7ff0000000000000ff")] public static void WriteArray_IndefiniteLength_HappyPath(object[] values, string expectedHexEncoding) { byte[] expectedEncoding = expectedHexEncoding.HexToByteArray(); 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 index 1aa5e94d217c04..366504508b1c17 100644 --- 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 @@ -26,9 +26,13 @@ public static void WriteValue(CborWriter writer, object value, bool useDefiniteL { switch (value) { + case null: writer.WriteNull(); break; + case bool b: writer.WriteBoolean(b); break; case int i: writer.WriteInt64(i); break; case long i: writer.WriteInt64(i); break; case ulong i: writer.WriteUInt64(i); break; + case float f: writer.WriteSingle(f); break; + case double d: writer.WriteDouble(d); break; case string s: writer.WriteTextString(s); break; case byte[] b: writer.WriteByteString(b); break; case byte[][] chunks: WriteChunkedByteString(writer, chunks); break; diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Integer.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Integer.cs index d3ab3d55733a7f..1996e9e0adc25b 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Integer.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Integer.cs @@ -43,7 +43,7 @@ public partial class CborWriterTests [InlineData(-2 - uint.MaxValue, "3b0000000100000000")] [InlineData(long.MinValue, "3b7fffffffffffffff")] [InlineData(long.MaxValue, "1b7fffffffffffffff")] - public static void Int64Writer_SingleValue_HappyPath(long input, string hexExpectedEncoding) + public static void WriteInt64_SingleValue_HappyPath(long input, string hexExpectedEncoding) { byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray(); using var writer = new CborWriter(); @@ -70,14 +70,77 @@ public static void Int64Writer_SingleValue_HappyPath(long input, string hexExpec [InlineData((ulong)uint.MaxValue + 1, "1b0000000100000000")] [InlineData(long.MaxValue, "1b7fffffffffffffff")] [InlineData(ulong.MaxValue, "1bffffffffffffffff")] - public static void UInt64Writer_SingleValue_HappyPath(ulong input, string hexExpectedEncoding) + public static void WriteUInt64_SingleValue_HappyPath(ulong input, string hexExpectedEncoding) { byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray(); using var writer = new CborWriter(); writer.WriteUInt64(input); AssertHelper.HexEqual(expectedEncoding, writer.ToArray()); } + + [Theory] + [InlineData(2, 2, "c202")] + [InlineData(0, "2013-03-21T20:04:00Z", "c074323031332d30332d32315432303a30343a30305a")] + [InlineData(1, 1363896240, "c11a514b67b0")] + [InlineData(23, new byte[] { 1, 2, 3, 4 }, "d74401020304")] + [InlineData(32, "http://www.example.com", "d82076687474703a2f2f7777772e6578616d706c652e636f6d")] + [InlineData(int.MaxValue, 2, "da7fffffff02")] + [InlineData(ulong.MaxValue, new object[] { 1, 2 }, "dbffffffffffffffff820102")] + public static void WriteTag_SingleValue_HappyPath(ulong tag, object value, string hexExpectedEncoding) + { + byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray(); + using var writer = new CborWriter(); + writer.WriteTag((CborTag)tag); + Helpers.WriteValue(writer, value); + AssertHelper.HexEqual(expectedEncoding, writer.ToArray()); + } + + [Theory] + [InlineData(new ulong[] { 1, 2, 3 }, 2, "c1c2c302")] + [InlineData(new ulong[] { 0, 0, 0 }, "2013-03-21T20:04:00Z", "c0c0c074323031332d30332d32315432303a30343a30305a")] + [InlineData(new ulong[] { int.MaxValue, ulong.MaxValue }, 1363896240, "da7fffffffdbffffffffffffffff1a514b67b0")] + [InlineData(new ulong[] { 23, 24, 100 }, new byte[] { 1, 2, 3, 4 }, "d7d818d8644401020304")] + [InlineData(new ulong[] { 32, 1, 1 }, new object[] { 1, "lorem ipsum" }, "d820c1c182016b6c6f72656d20697073756d")] + public static void WriteTag_NestedTags_HappyPath(ulong[] tags, object value, string hexExpectedEncoding) + { + byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray(); + using var writer = new CborWriter(); + foreach (var tag in tags) + { + writer.WriteTag((CborTag)tag); + } + Helpers.WriteValue(writer, value); + AssertHelper.HexEqual(expectedEncoding, writer.ToArray()); + } + + [Theory] + [InlineData(new ulong[] { 2 })] + [InlineData(new ulong[] { 1, 2, 3 })] + public static void WriteTag_NoValue_ShouldThrowInvalidOperationException(ulong[] tags) + { + using var writer = new CborWriter(); + + foreach (ulong tag in tags) + { + writer.WriteTag((CborTag)tag); + } + + InvalidOperationException exn = Assert.Throws(() => writer.ToArray()); + + Assert.Equal("Buffer contains incomplete CBOR document.", exn.Message); + } + + [Fact] + public static void WriteTag_NoValueInNestedContext_ShouldThrowInvalidOperationException() + { + using var writer = new CborWriter(); + + writer.WriteStartArrayIndefiniteLength(); + writer.WriteTag(CborTag.Uri); + Assert.Throws(() => writer.WriteEndArray()); + } } + internal static class AssertHelper { /// diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Special.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Special.cs new file mode 100644 index 00000000000000..8b228267cb6c25 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Special.cs @@ -0,0 +1,83 @@ +// 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 CborWriterTests + { + // Data points taken from https://tools.ietf.org/html/rfc7049#appendix-A + // Additional pairs generated using http://cbor.me/ + + [Theory] + [InlineData(100000.0, "fa47c35000")] + [InlineData(3.4028234663852886e+38, "fa7f7fffff")] + [InlineData(float.PositiveInfinity, "fa7f800000")] + [InlineData(float.NegativeInfinity, "faff800000")] + [InlineData(float.NaN, "faffc00000")] + internal static void WriteSingle_SingleValue_HappyPath(float input, string hexExpectedEncoding) + { + byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray(); + using var writer = new CborWriter(); + writer.WriteSingle(input); + AssertHelper.HexEqual(expectedEncoding, writer.ToArray()); + } + + [Theory] + [InlineData(1.1, "fb3ff199999999999a")] + [InlineData(1.0e+300, "fb7e37e43c8800759c")] + [InlineData(-4.1, "fbc010666666666666")] + [InlineData(3.1415926, "fb400921fb4d12d84a")] + [InlineData(double.PositiveInfinity, "fb7ff0000000000000")] + [InlineData(double.NegativeInfinity, "fbfff0000000000000")] + [InlineData(double.NaN, "fbfff8000000000000")] + internal static void WriteDouble_SingleValue_HappyPath(double input, string hexExpectedEncoding) + { + byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray(); + using var writer = new CborWriter(); + writer.WriteDouble(input); + AssertHelper.HexEqual(expectedEncoding, writer.ToArray()); + } + + [Fact] + internal static void WriteNull_SingleValue_HappyPath() + { + byte[] expectedEncoding = "f6".HexToByteArray(); + using var writer = new CborWriter(); + writer.WriteNull(); + AssertHelper.HexEqual(expectedEncoding, writer.ToArray()); + } + + [Theory] + [InlineData(false, "f4")] + [InlineData(true, "f5")] + internal static void WriteBoolean_SingleValue_HappyPath(bool input, string hexExpectedEncoding) + { + byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray(); + using var writer = new CborWriter(); + writer.WriteBoolean(input); + AssertHelper.HexEqual(expectedEncoding, writer.ToArray()); + } + + [Theory] + [InlineData((CborSpecialValue)0, "e0")] + [InlineData(CborSpecialValue.False, "f4")] + [InlineData(CborSpecialValue.True, "f5")] + [InlineData(CborSpecialValue.Null, "f6")] + [InlineData(CborSpecialValue.Undefined, "f7")] + [InlineData((CborSpecialValue)32, "f820")] + [InlineData((CborSpecialValue)255, "f8ff")] + internal static void WriteSpecialValue_SingleValue_HappyPath(CborSpecialValue input, string hexExpectedEncoding) + { + byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray(); + using var writer = new CborWriter(); + writer.WriteSpecialValue(input); + AssertHelper.HexEqual(expectedEncoding, writer.ToArray()); + } + } +} 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 b2dd4621add61a..772f2ee384e223 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,14 @@ internal enum CborMajorType : byte internal enum CborAdditionalInfo : byte { - Unsigned8BitIntegerEncoding = 24, - Unsigned16BitIntegerEncoding = 25, - Unsigned32BitIntegerEncoding = 26, - Unsigned64BitIntegerEncoding = 27, + SpecialValueFalse = 20, + SpecialValueTrue = 21, + SpecialValueNull = 22, + + Additional8BitData = 24, + Additional16BitData = 25, + Additional32BitData = 26, + Additional64BitData = 27, IndefiniteLength = 31, } diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Array.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Array.cs index 7ad19430a03887..bfa415a7f38204 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Array.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Array.cs @@ -16,7 +16,7 @@ internal partial class CborReader if (header.AdditionalInfo == CborAdditionalInfo.IndefiniteLength) { AdvanceBuffer(1); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); PushDataItem(CborMajorType.Array, null); return null; } @@ -24,7 +24,7 @@ internal partial class CborReader { ulong arrayLength = ReadUnsignedInteger(_buffer.Span, header, out int additionalBytes); AdvanceBuffer(1 + additionalBytes); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); PushDataItem(CborMajorType.Array, arrayLength); return arrayLength; } 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 ef9ebb185d5bea..d3f3cc91eba501 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 @@ -18,7 +18,7 @@ public ulong ReadUInt64() case CborMajorType.UnsignedInteger: ulong value = ReadUnsignedInteger(_buffer.Span, header, out int additionalBytes); AdvanceBuffer(1 + additionalBytes); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); return value; case CborMajorType.NegativeInteger: @@ -42,13 +42,13 @@ public long ReadInt64() case CborMajorType.UnsignedInteger: value = checked((long)ReadUnsignedInteger(_buffer.Span, header, out additionalBytes)); AdvanceBuffer(1 + additionalBytes); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); return value; case CborMajorType.NegativeInteger: value = checked(-1 - (long)ReadUnsignedInteger(_buffer.Span, header, out additionalBytes)); AdvanceBuffer(1 + additionalBytes); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); return value; default: @@ -56,6 +56,16 @@ public long ReadInt64() } } + public CborTag ReadTag() + { + CborInitialByte header = PeekInitialByte(expectedType: CborMajorType.Tag); + ulong tag = ReadUnsignedInteger(_buffer.Span, header, out int additionalBytes); + AdvanceBuffer(1 + additionalBytes); + // NB tag reads do not advance data item counters + _isTagContext = true; + return (CborTag)tag; + } + // Returns the next CBOR negative integer encoding according to // https://tools.ietf.org/html/rfc7049#section-2.1 public ulong ReadCborNegativeIntegerEncoding() @@ -63,7 +73,7 @@ public ulong ReadCborNegativeIntegerEncoding() CborInitialByte header = PeekInitialByte(expectedType: CborMajorType.NegativeInteger); ulong value = ReadUnsignedInteger(_buffer.Span, header, out int additionalBytes); AdvanceBuffer(1 + additionalBytes); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); return value; } @@ -72,26 +82,26 @@ private static ulong ReadUnsignedInteger(ReadOnlySpan buffer, CborInitialB { switch (header.AdditionalInfo) { - case CborAdditionalInfo x when (x < CborAdditionalInfo.Unsigned8BitIntegerEncoding): + case CborAdditionalInfo x when (x < CborAdditionalInfo.Additional8BitData): additionalBytes = 0; return (ulong)x; - case CborAdditionalInfo.Unsigned8BitIntegerEncoding: + case CborAdditionalInfo.Additional8BitData: EnsureBuffer(buffer, 2); additionalBytes = 1; return buffer[1]; - case CborAdditionalInfo.Unsigned16BitIntegerEncoding: + case CborAdditionalInfo.Additional16BitData: EnsureBuffer(buffer, 3); additionalBytes = 2; return BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(1)); - case CborAdditionalInfo.Unsigned32BitIntegerEncoding: + case CborAdditionalInfo.Additional32BitData: EnsureBuffer(buffer, 5); additionalBytes = 4; return BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(1)); - case CborAdditionalInfo.Unsigned64BitIntegerEncoding: + case CborAdditionalInfo.Additional64BitData: EnsureBuffer(buffer, 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 index 4c800055f8e88a..7d18195bb5821c 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Map.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Map.cs @@ -16,7 +16,7 @@ internal partial class CborReader if (header.AdditionalInfo == CborAdditionalInfo.IndefiniteLength) { AdvanceBuffer(1); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); PushDataItem(CborMajorType.Map, null); return null; } @@ -30,7 +30,7 @@ internal partial class CborReader } AdvanceBuffer(1 + additionalBytes); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); PushDataItem(CborMajorType.Map, 2 * mapSize); return mapSize; } diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Special.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Special.cs new file mode 100644 index 00000000000000..adbc97752d1a1f --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Special.cs @@ -0,0 +1,162 @@ +// 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. + +using System.Buffers.Binary; + +namespace System.Security.Cryptography.Encoding.Tests.Cbor +{ + internal partial class CborReader + { + public float ReadSingle() + { + CborInitialByte header = PeekInitialByte(expectedType: CborMajorType.Special); + ReadOnlySpan buffer = _buffer.Span; + float result; + + switch (header.AdditionalInfo) + { + case CborAdditionalInfo.Additional16BitData: + EnsureBuffer(buffer, 3); + result = (float)ReadHalfBigEndian(buffer.Slice(1)); + AdvanceBuffer(3); + AdvanceDataItemCounters(); + return result; + + case CborAdditionalInfo.Additional32BitData: + EnsureBuffer(buffer, 5); + result = BinaryPrimitives.ReadSingleBigEndian(buffer.Slice(1)); + AdvanceBuffer(5); + AdvanceDataItemCounters(); + return result; + + case CborAdditionalInfo.Additional64BitData: + throw new InvalidOperationException("Attempting to read double-precision floating point encoding as single-precision."); + + default: + throw new InvalidOperationException("CBOR data item does not encode a floating point number."); + + } + } + + public double ReadDouble() + { + CborInitialByte header = PeekInitialByte(expectedType: CborMajorType.Special); + ReadOnlySpan buffer = _buffer.Span; + double result; + + switch (header.AdditionalInfo) + { + case CborAdditionalInfo.Additional16BitData: + EnsureBuffer(buffer, 3); + result = ReadHalfBigEndian(buffer.Slice(1)); + AdvanceBuffer(3); + AdvanceDataItemCounters(); + return result; + + case CborAdditionalInfo.Additional32BitData: + EnsureBuffer(buffer, 5); + result = BinaryPrimitives.ReadSingleBigEndian(buffer.Slice(1)); + AdvanceBuffer(5); + AdvanceDataItemCounters(); + return result; + + case CborAdditionalInfo.Additional64BitData: + EnsureBuffer(buffer, 9); + result = BinaryPrimitives.ReadDoubleBigEndian(buffer.Slice(1)); + AdvanceBuffer(9); + AdvanceDataItemCounters(); + return result; + + default: + throw new InvalidOperationException("CBOR data item does not encode a floating point number."); + } + } + + public bool ReadBoolean() + { + CborInitialByte header = PeekInitialByte(expectedType: CborMajorType.Special); + + switch (header.AdditionalInfo) + { + case CborAdditionalInfo.SpecialValueFalse: + AdvanceBuffer(1); + AdvanceDataItemCounters(); + return false; + case CborAdditionalInfo.SpecialValueTrue: + AdvanceBuffer(1); + AdvanceDataItemCounters(); + return true; + default: + throw new InvalidOperationException("CBOR data item does not encode a boolean value."); + } + } + + public void ReadNull() + { + CborInitialByte header = PeekInitialByte(expectedType: CborMajorType.Special); + + switch (header.AdditionalInfo) + { + case CborAdditionalInfo.SpecialValueNull: + AdvanceBuffer(1); + AdvanceDataItemCounters(); + return; + default: + throw new InvalidOperationException("CBOR data item does not encode a null value."); + } + } + + public CborSpecialValue ReadSpecialValue() + { + CborInitialByte header = PeekInitialByte(expectedType: CborMajorType.Special); + + switch (header.AdditionalInfo) + { + case CborAdditionalInfo info when (byte)info < 24: + AdvanceBuffer(1); + AdvanceDataItemCounters(); + return (CborSpecialValue)header.AdditionalInfo; + case CborAdditionalInfo.Additional8BitData: + EnsureBuffer(2); + byte value = _buffer.Span[1]; + + if (value < 32) + { + throw new FormatException("Two-byte CBOR special value must be between 32 and 255."); + } + + AdvanceBuffer(2); + AdvanceDataItemCounters(); + return (CborSpecialValue)value; + default: + throw new InvalidOperationException("CBOR data item does not encode a special value."); + } + } + + // half-precision float decoder adapted from https://tools.ietf.org/html/rfc7049#appendix-D + private static double ReadHalfBigEndian(ReadOnlySpan buffer) + { + int half = (buffer[0] << 8) + buffer[1]; + bool isNegative = (half >> 15) != 0; + int exp = (half >> 10) & 0x1f; + int mant = half & 0x3ff; + double value; + + if (exp == 0) + { + value = mant * 5.9604644775390625e-08 /* precomputed 2^-24 */; + } + else if (exp != 31) + { + value = (mant + 1024) * Math.Pow(2, exp - 25); + } + else + { + value = (mant == 0) ? double.PositiveInfinity : double.NaN; + } + + return isNegative ? -value : value; + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.String.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.String.cs index 0093a8247e30f7..639d84ef1270a4 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.String.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.String.cs @@ -27,7 +27,7 @@ public byte[] ReadByteString() byte[] result = new byte[length]; _buffer.Slice(1 + additionalBytes, length).CopyTo(result); AdvanceBuffer(1 + additionalBytes + length); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); return result; } @@ -51,7 +51,7 @@ public bool TryReadByteString(Span destination, out int bytesWritten) _buffer.Span.Slice(1 + additionalBytes, length).CopyTo(destination); AdvanceBuffer(1 + additionalBytes + length); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); bytesWritten = length; return true; @@ -72,7 +72,7 @@ public string ReadTextString() ReadOnlySpan encodedString = _buffer.Span.Slice(1 + additionalBytes, length); string result = s_utf8Encoding.GetString(encodedString); AdvanceBuffer(1 + additionalBytes + length); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); return result; } @@ -98,7 +98,7 @@ public bool TryReadTextString(Span destination, out int charsWritten) s_utf8Encoding.GetChars(encodedSlice, destination); AdvanceBuffer(1 + additionalBytes + byteLength); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); charsWritten = charLength; return true; } @@ -112,7 +112,7 @@ public void ReadStartTextStringIndefiniteLength() throw new InvalidOperationException("CBOR text string is not of indefinite length."); } - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); AdvanceBuffer(1); PushDataItem(CborMajorType.TextString, expectedNestedItems: null); @@ -134,7 +134,7 @@ public void ReadStartByteStringIndefiniteLength() throw new InvalidOperationException("CBOR text string is not of indefinite length."); } - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); AdvanceBuffer(1); PushDataItem(CborMajorType.ByteString, expectedNestedItems: null); @@ -167,7 +167,7 @@ private bool TryReadChunkedByteStringConcatenated(Span destination, out in bytesWritten = concatenatedBufferSize; AdvanceBuffer(encodingLength); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); ReturnRangeList(ranges); return true; } @@ -197,7 +197,7 @@ private bool TryReadChunkedTextStringConcatenated(Span destination, out in charsWritten = concatenatedStringSize; AdvanceBuffer(encodingLength); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); ReturnRangeList(ranges); return true; } @@ -218,7 +218,7 @@ private byte[] ReadChunkedByteStringConcatenated() Debug.Assert(target.IsEmpty); AdvanceBuffer(encodingLength); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); ReturnRangeList(ranges); return output; } @@ -237,7 +237,7 @@ private string ReadChunkedTextStringConcatenated() string output = string.Create(concatenatedStringSize, (ranges, _buffer), BuildString); AdvanceBuffer(encodingLength); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); ReturnRangeList(ranges); return output; 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 fb61dda27af972..5e93218bfc0f14 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.cs @@ -25,7 +25,12 @@ internal enum CborReaderState EndArray, EndMap, Tag, - Special, + Null, + Boolean, + HalfPrecisionFloat, + SinglePrecisionFloat, + DoublePrecisionFloat, + SpecialValue, Finished, FormatError, EndOfData, @@ -42,6 +47,7 @@ internal partial class CborReader private ulong? _remainingDataItems = 1; private bool _isEvenNumberOfDataItemsRead = true; // required for indefinite-length map writes private Stack<(CborMajorType type, bool isEvenNumberOfDataItemsWritten, ulong? remainingDataItems)>? _nestedDataItemStack; + private bool _isTagContext = false; // true if reader is expecting a tagged value // stores a reusable List allocation for keeping ranges in the buffer private List<(int offset, int length)>? _rangeListAllocation = null; @@ -82,6 +88,12 @@ public CborReaderState Peek() if (initialByte.InitialByte == CborInitialByte.IndefiniteLengthBreakByte) { + if (_isTagContext) + { + // indefinite-length collection has ended without providing value for tag + return CborReaderState.FormatError; + } + if (_remainingDataItems == null) { // stack guaranteed to be populated since root context cannot be indefinite-length @@ -135,9 +147,31 @@ public CborReaderState Peek() CborMajorType.Array => CborReaderState.StartArray, CborMajorType.Map => CborReaderState.StartMap, CborMajorType.Tag => CborReaderState.Tag, - CborMajorType.Special => CborReaderState.Special, + CborMajorType.Special => MapSpecialValueTagToReaderState(initialByte.AdditionalInfo), _ => CborReaderState.FormatError, }; + + static CborReaderState MapSpecialValueTagToReaderState (CborAdditionalInfo value) + { + // https://tools.ietf.org/html/rfc7049#section-2.3 + + switch (value) + { + case CborAdditionalInfo.SpecialValueNull: + return CborReaderState.Null; + case CborAdditionalInfo.SpecialValueFalse: + case CborAdditionalInfo.SpecialValueTrue: + return CborReaderState.Boolean; + case CborAdditionalInfo.Additional16BitData: + return CborReaderState.HalfPrecisionFloat; + case CborAdditionalInfo.Additional32BitData: + return CborReaderState.SinglePrecisionFloat; + case CborAdditionalInfo.Additional64BitData: + return CborReaderState.DoublePrecisionFloat; + default: + return CborReaderState.SpecialValue; + } + } } private CborInitialByte PeekInitialByte() @@ -233,15 +267,21 @@ private void PopDataItem(CborMajorType expectedType) throw new InvalidOperationException("Definite-length nested CBOR data item is incomplete."); } + if (_isTagContext) + { + throw new FormatException("CBOR tag should be followed by a data item."); + } + _nestedDataItemStack.Pop(); _remainingDataItems = remainingItems; _isEvenNumberOfDataItemsRead = isEvenNumberOfDataItemsWritten; } - private void DecrementRemainingItemCount() + private void AdvanceDataItemCounters() { _remainingDataItems--; _isEvenNumberOfDataItemsRead = !_isEvenNumberOfDataItemsRead; + _isTagContext = false; } private void AdvanceBuffer(int length) diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborTag.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborTag.cs new file mode 100644 index 00000000000000..f7d4ca1f270e76 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborTag.cs @@ -0,0 +1,39 @@ +// 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. + +namespace System.Security.Cryptography.Encoding.Tests +{ + // https://tools.ietf.org/html/rfc7049#section-2.4 + internal enum CborTag : ulong + { + DateTimeString = 0, + EpochDateTime = 1, + PositiveBigNum = 2, + NegativeBigNum = 3, + DecimalFraction = 4, + BigFloat = 5, + + Base64UrlLaterEncoding = 21, + Base64StringLaterEncoding = 22, + Base16StringLaterEncoding = 23, + EncodedCborDataItem = 24, + + Uri = 32, + Base64Url = 33, + Base64 = 34, + Regex = 35, + MimeMessage = 36, + + SelfDescribingCbor = 55799, + } + + // https://tools.ietf.org/html/rfc7049#section-2.3 + internal enum CborSpecialValue : byte + { + False = 20, + True = 21, + Null = 22, + Undefined = 23, + } +} diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Array.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Array.cs index 97e7f4ff3a609c..39b54f28460b81 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Array.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Array.cs @@ -18,26 +18,28 @@ public void WriteStartArray(int definiteLength) } WriteUnsignedInteger(CborMajorType.Array, (ulong)definiteLength); + AdvanceDataItemCounters(); PushDataItem(CborMajorType.Array, (uint)definiteLength); } public void WriteEndArray() { - if (!_remainingDataItems.HasValue) + bool isDefiniteLengthArray = _remainingDataItems.HasValue; + PopDataItem(CborMajorType.Array); + + if (!isDefiniteLengthArray) { - // indefinite-length map, add break byte + // append break byte for indefinite-length arrays EnsureWriteCapacity(1); - WriteInitialByte(new CborInitialByte(CborInitialByte.IndefiniteLengthBreakByte)); + _buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte; } - - PopDataItem(CborMajorType.Array); } public void WriteStartArrayIndefiniteLength() { EnsureWriteCapacity(1); WriteInitialByte(new CborInitialByte(CborMajorType.Array, CborAdditionalInfo.IndefiniteLength)); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); PushDataItem(CborMajorType.Array, expectedNestedItems: null); } } 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 75cd10d55a5778..6351306183840c 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 @@ -12,6 +12,7 @@ internal partial class CborWriter public void WriteUInt64(ulong value) { WriteUnsignedInteger(CborMajorType.UnsignedInteger, value); + AdvanceDataItemCounters(); } // Implements major type 0,1 encoding per https://tools.ietf.org/html/rfc7049#section-2.1 @@ -26,6 +27,15 @@ public void WriteInt64(long value) { WriteUnsignedInteger(CborMajorType.UnsignedInteger, (ulong)value); } + + AdvanceDataItemCounters(); + } + + public void WriteTag(CborTag tag) + { + WriteUnsignedInteger(CborMajorType.Tag, (ulong)tag); + // NB tag writes do not advance data item counters + _isTagContext = true; } // Unsigned integer encoding https://tools.ietf.org/html/rfc7049#section-2.1 @@ -39,32 +49,30 @@ private void WriteUnsignedInteger(CborMajorType type, ulong value) else if (value <= byte.MaxValue) { EnsureWriteCapacity(2); - WriteInitialByte(new CborInitialByte(type, CborAdditionalInfo.Unsigned8BitIntegerEncoding)); + WriteInitialByte(new CborInitialByte(type, CborAdditionalInfo.Additional8BitData)); _buffer[_offset++] = (byte)value; } else if (value <= ushort.MaxValue) { EnsureWriteCapacity(3); - WriteInitialByte(new CborInitialByte(type, CborAdditionalInfo.Unsigned16BitIntegerEncoding)); + WriteInitialByte(new CborInitialByte(type, CborAdditionalInfo.Additional16BitData)); BinaryPrimitives.WriteUInt16BigEndian(_buffer.AsSpan(_offset), (ushort)value); _offset += 2; } else if (value <= uint.MaxValue) { EnsureWriteCapacity(5); - WriteInitialByte(new CborInitialByte(type, CborAdditionalInfo.Unsigned32BitIntegerEncoding)); + WriteInitialByte(new CborInitialByte(type, CborAdditionalInfo.Additional32BitData)); BinaryPrimitives.WriteUInt32BigEndian(_buffer.AsSpan(_offset), (uint)value); _offset += 4; } else { EnsureWriteCapacity(9); - WriteInitialByte(new CborInitialByte(type, CborAdditionalInfo.Unsigned64BitIntegerEncoding)); + WriteInitialByte(new CborInitialByte(type, CborAdditionalInfo.Additional64BitData)); BinaryPrimitives.WriteUInt64BigEndian(_buffer.AsSpan(_offset), value); _offset += 8; } - - DecrementRemainingItemCount(); } } } 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 index 98147689d3546e..2ebe21918578cf 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Map.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Map.cs @@ -17,6 +17,7 @@ public void WriteStartMap(int definiteLength) } WriteUnsignedInteger(CborMajorType.Map, (ulong)definiteLength); + AdvanceDataItemCounters(); PushDataItem(CborMajorType.Map, 2 * (uint)definiteLength); } @@ -27,21 +28,23 @@ public void WriteEndMap() throw new InvalidOperationException("CBOR Map types require an even number of key/value combinations"); } - if (!_remainingDataItems.HasValue) + bool isDefiniteLengthMap = _remainingDataItems.HasValue; + + PopDataItem(CborMajorType.Map); + + if (!isDefiniteLengthMap) { - // indefinite-length map, add break byte + // append break byte EnsureWriteCapacity(1); - WriteInitialByte(new CborInitialByte(CborInitialByte.IndefiniteLengthBreakByte)); + _buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte; } - - PopDataItem(CborMajorType.Map); } public void WriteStartMapIndefiniteLength() { EnsureWriteCapacity(1); WriteInitialByte(new CborInitialByte(CborMajorType.Map, CborAdditionalInfo.IndefiniteLength)); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); PushDataItem(CborMajorType.Map, expectedNestedItems: null); } } diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Special.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Special.cs new file mode 100644 index 00000000000000..20184b5b1f6f0f --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Special.cs @@ -0,0 +1,58 @@ +// 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. + +using System.Buffers.Binary; + +namespace System.Security.Cryptography.Encoding.Tests.Cbor +{ + internal partial class CborWriter + { + // Implements https://tools.ietf.org/html/rfc7049#section-2.3 + + public void WriteSingle(float value) + { + EnsureWriteCapacity(5); + WriteInitialByte(new CborInitialByte(CborMajorType.Special, CborAdditionalInfo.Additional32BitData)); + BinaryPrimitives.WriteSingleBigEndian(_buffer.AsSpan(_offset), value); + _offset += 4; + AdvanceDataItemCounters(); + } + + public void WriteDouble(double value) + { + EnsureWriteCapacity(9); + WriteInitialByte(new CborInitialByte(CborMajorType.Special, CborAdditionalInfo.Additional64BitData)); + BinaryPrimitives.WriteDoubleBigEndian(_buffer.AsSpan(_offset), value); + _offset += 8; + AdvanceDataItemCounters(); + } + + public void WriteBoolean(bool value) + { + WriteSpecialValue(value ? CborSpecialValue.True : CborSpecialValue.False); + } + + public void WriteNull() + { + WriteSpecialValue(CborSpecialValue.Null); + } + + public void WriteSpecialValue(CborSpecialValue value) + { + if ((byte)value < 24) + { + EnsureWriteCapacity(1); + WriteInitialByte(new CborInitialByte(CborMajorType.Special, (CborAdditionalInfo)value)); + } + else + { + EnsureWriteCapacity(2); + WriteInitialByte(new CborInitialByte(CborMajorType.Special, CborAdditionalInfo.Additional8BitData)); + _buffer[_offset++] = (byte)value; + } + + AdvanceDataItemCounters(); + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.String.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.String.cs index bd9517718971a1..45083ccf7b907c 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.String.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.String.cs @@ -19,6 +19,7 @@ public void WriteByteString(ReadOnlySpan value) EnsureWriteCapacity(value.Length); value.CopyTo(_buffer.AsSpan(_offset)); _offset += value.Length; + AdvanceDataItemCounters(); } // Implements major type 3 encoding per https://tools.ietf.org/html/rfc7049#section-2.1 @@ -29,36 +30,39 @@ public void WriteTextString(ReadOnlySpan value) EnsureWriteCapacity(length); s_utf8Encoding.GetBytes(value, _buffer.AsSpan(_offset)); _offset += length; + AdvanceDataItemCounters(); } public void WriteStartByteStringIndefiniteLength() { EnsureWriteCapacity(1); WriteInitialByte(new CborInitialByte(CborMajorType.ByteString, CborAdditionalInfo.IndefiniteLength)); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); PushDataItem(CborMajorType.ByteString, expectedNestedItems: null); } public void WriteEndByteStringIndefiniteLength() { - EnsureWriteCapacity(1); - WriteInitialByte(new CborInitialByte(CborInitialByte.IndefiniteLengthBreakByte)); PopDataItem(CborMajorType.ByteString); + // append break byte + EnsureWriteCapacity(1); + _buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte; } public void WriteStartTextStringIndefiniteLength() { EnsureWriteCapacity(1); WriteInitialByte(new CborInitialByte(CborMajorType.TextString, CborAdditionalInfo.IndefiniteLength)); - DecrementRemainingItemCount(); + AdvanceDataItemCounters(); PushDataItem(CborMajorType.TextString, expectedNestedItems: null); } public void WriteEndTextStringIndefiniteLength() { - EnsureWriteCapacity(1); - WriteInitialByte(new CborInitialByte(CborInitialByte.IndefiniteLengthBreakByte)); PopDataItem(CborMajorType.TextString); + // append break byte + EnsureWriteCapacity(1); + _buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte; } } } diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.cs index 316c10b92399ab..2316a701626b7b 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.cs @@ -23,6 +23,7 @@ internal partial class CborWriter : IDisposable private uint? _remainingDataItems = 1; private bool _isEvenNumberOfDataItemsWritten = true; // required for indefinite-length map writes private Stack<(CborMajorType type, bool isEvenNumberOfDataItemsWritten, uint? remainingDataItems)>? _nestedDataItemStack; + private bool _isTagContext = false; // true if writer is expecting a tagged value public CborWriter() { @@ -81,6 +82,11 @@ private void PopDataItem(CborMajorType expectedType) throw new InvalidOperationException("Unexpected major type in nested CBOR data item."); } + if (_isTagContext) + { + throw new InvalidOperationException("Tagged CBOR value context is incomplete."); + } + if (_remainingDataItems > 0) { throw new InvalidOperationException("Definite-length nested CBOR data item is incomplete."); @@ -91,9 +97,10 @@ private void PopDataItem(CborMajorType expectedType) _isEvenNumberOfDataItemsWritten = isEvenNumberOfDataItemsWritten; } - private void DecrementRemainingItemCount() + private void AdvanceDataItemCounters() { _remainingDataItems--; + _isTagContext = false; _isEvenNumberOfDataItemsWritten = !_isEvenNumberOfDataItemsWritten; } @@ -104,13 +111,6 @@ private void WriteInitialByte(CborInitialByte initialByte) throw new InvalidOperationException("Adding a CBOR data item to the current context exceeds its definite length."); } - if (_remainingDataItems.HasValue && initialByte.InitialByte == CborInitialByte.IndefiniteLengthBreakByte) - { - throw new InvalidOperationException("Cannot write CBOR break byte in definite-length contexts"); - } - - // TODO check for tag state - if (_nestedDataItemStack != null && _nestedDataItemStack.Count > 0) { CborMajorType parentType = _nestedDataItemStack.Peek().type; @@ -120,8 +120,7 @@ private void WriteInitialByte(CborInitialByte initialByte) // indefinite-length string contexts do not permit nesting case CborMajorType.ByteString: case CborMajorType.TextString: - if (initialByte.InitialByte == CborInitialByte.IndefiniteLengthBreakByte || - initialByte.MajorType == parentType && + if (initialByte.MajorType == parentType && initialByte.AdditionalInfo != CborAdditionalInfo.IndefiniteLength) { break; 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 87109c395eaaf7..1cf426ea78b709 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 @@ -50,11 +50,15 @@ + + + + @@ -63,6 +67,7 @@ +