From 5c8cad2b40fbe311da121060790461771091c844 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 24 Mar 2020 22:01:27 +0000 Subject: [PATCH] [CBOR] Implement indefinite length writer and reader support (#33831) * Implement indefinite length writes * add indefinite-length cbor reader support * Use CborReaderState.FormatError in Peek() instead of throwing exceptions. * use verbose naming for indefinite-length write methods * address feedback * implement concatenation logic for indefinite-length string readers * add tests for nested indefinite-length strings * fix naming issues * check that TryReadString() methods are idempotent on failed reads. * use string.Create instead of char buffer; share single range list allocation * only clear List if it is guaranteed to be reused * move field to top of main CborReader source file. --- .../tests/Cbor.Tests/CborReaderTests.Array.cs | 49 +++ .../Cbor.Tests/CborReaderTests.Helpers.cs | 61 +++- .../Cbor.Tests/CborReaderTests.Integer.cs | 4 +- .../tests/Cbor.Tests/CborReaderTests.Map.cs | 66 ++++ .../Cbor.Tests/CborReaderTests.String.cs | 289 +++++++++++++++++- .../tests/Cbor.Tests/CborWriterTests.Array.cs | 33 ++ .../Cbor.Tests/CborWriterTests.Helpers.cs | 75 ++++- .../tests/Cbor.Tests/CborWriterTests.Map.cs | 47 +++ .../Cbor.Tests/CborWriterTests.String.cs | 82 +++++ .../tests/Cbor/CborInitialByte.cs | 2 + .../tests/Cbor/CborReader.Array.cs | 37 ++- .../tests/Cbor/CborReader.Integer.cs | 31 +- .../tests/Cbor/CborReader.Map.cs | 47 ++- .../tests/Cbor/CborReader.String.cs | 237 +++++++++++++- .../tests/Cbor/CborReader.cs | 159 ++++++++-- .../tests/Cbor/CborWriter.Array.cs | 15 + .../tests/Cbor/CborWriter.Integer.cs | 29 +- .../tests/Cbor/CborWriter.Map.cs | 20 ++ .../tests/Cbor/CborWriter.String.cs | 30 ++ .../tests/Cbor/CborWriter.cs | 73 +++-- 20 files changed, 1262 insertions(+), 124 deletions(-) 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 22a27e4cf9f13..e836967ab8827 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 @@ -41,6 +41,21 @@ public static void ReadArray_NestedValues_HappyPath(object[] expectedValues, str Assert.Equal(CborReaderState.Finished, reader.Peek()); } + [Theory] + [InlineData(new object[] { }, "9fff")] + [InlineData(new object[] { 42 }, "9f182aff")] + [InlineData(new object[] { 1, 2, 3 }, "9f010203ff")] + [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")] + public static void ReadArray_IndefiniteLength_HappyPath(object[] expectedValues, string hexEncoding) + { + byte[] encoding = hexEncoding.HexToByteArray(); + var reader = new CborReader(encoding); + Helpers.VerifyArray(reader, expectedValues, expectDefiniteLengthCollections: false); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + [Theory] [InlineData("80", 0)] [InlineData("8101", 1)] @@ -83,6 +98,40 @@ public static void ReadArray_DefiniteLengthExceeded_WithNestedData_ShouldThrowIn Assert.Throws(() => reader.ReadInt64()); } + [Theory] + [InlineData("9f")] + [InlineData("9f01")] + [InlineData("9f0102")] + public static void ReadArray_IndefiniteLength_MissingBreakByte_ShouldReportEndOfData(string hexEncoding) + { + byte[] encoding = hexEncoding.HexToByteArray(); + var reader = new CborReader(encoding); + reader.ReadStartArray(); + while (reader.Peek() == CborReaderState.UnsignedInteger) + { + reader.ReadInt64(); + } + + Assert.Equal(CborReaderState.EndOfData, reader.Peek()); + } + + [Theory] + [InlineData("9f01ff", 1)] + [InlineData("9f0102ff", 2)] + [InlineData("9f010203ff", 3)] + public static void ReadArray_IndefiniteLength_PrematureEndArrayCall_ShouldThrowInvalidOperationException(string hexEncoding, int length) + { + byte[] encoding = hexEncoding.HexToByteArray(); + var reader = new CborReader(encoding); + reader.ReadStartArray(); + for (int i = 1; i < length; i++) + { + reader.ReadInt64(); + } + + Assert.Throws(() => reader.ReadEndArray()); + } + [Theory] [InlineData("8101", 1)] [InlineData("83010203", 3)] 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 6eba42bdb8317..16f444b6946a2 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 @@ -4,6 +4,7 @@ #nullable enable using System.Linq; +using Test.Cryptography; using Xunit; namespace System.Security.Cryptography.Encoding.Tests.Cbor @@ -12,7 +13,7 @@ public partial class CborReaderTests { internal static class Helpers { - public static void VerifyValue(CborReader reader, object expectedValue) + public static void VerifyValue(CborReader reader, object expectedValue, bool expectDefiniteLengthCollections = true) { switch (expectedValue) { @@ -39,13 +40,38 @@ public static void VerifyValue(CborReader reader, object expectedValue) case byte[] expected: Assert.Equal(CborReaderState.ByteString, reader.Peek()); byte[] b = reader.ReadByteString(); - Assert.Equal(expected, b); + Assert.Equal(expected.ByteArrayToHex(), b.ByteArrayToHex()); break; + case string[] expectedChunks: + Assert.Equal(CborReaderState.StartTextString, reader.Peek()); + reader.ReadStartTextStringIndefiniteLength(); + foreach(string expectedChunk in expectedChunks) + { + Assert.Equal(CborReaderState.TextString, reader.Peek()); + string chunk = reader.ReadTextString(); + Assert.Equal(expectedChunk, chunk); + } + Assert.Equal(CborReaderState.EndTextString, reader.Peek()); + reader.ReadEndTextStringIndefiniteLength(); + break; + case byte[][] expectedChunks: + Assert.Equal(CborReaderState.StartByteString, reader.Peek()); + reader.ReadStartByteStringIndefiniteLength(); + foreach (byte[] expectedChunk in expectedChunks) + { + Assert.Equal(CborReaderState.ByteString, reader.Peek()); + byte[] chunk = reader.ReadByteString(); + Assert.Equal(expectedChunk.ByteArrayToHex(), chunk.ByteArrayToHex()); + } + Assert.Equal(CborReaderState.EndByteString, reader.Peek()); + reader.ReadEndByteStringIndefiniteLength(); + break; + case object[] nested when CborWriterTests.Helpers.IsCborMapRepresentation(nested): - VerifyMap(reader, nested); + VerifyMap(reader, nested, expectDefiniteLengthCollections); break; case object[] nested: - VerifyArray(reader, nested); + VerifyArray(reader, nested, expectDefiniteLengthCollections); break; default: throw new ArgumentException($"Unrecognized argument type {expectedValue.GetType()}"); @@ -58,14 +84,21 @@ static void VerifyPeekInteger(CborReader reader, bool isUnsignedInteger) } } - public static void VerifyArray(CborReader reader, params object[] expectedValues) + public static void VerifyArray(CborReader reader, object[] expectedValues, bool expectDefiniteLengthCollections = true) { Assert.Equal(CborReaderState.StartArray, reader.Peek()); ulong? length = reader.ReadStartArray(); - Assert.NotNull(length); - Assert.Equal(expectedValues.Length, (int)length!.Value); + if (expectDefiniteLengthCollections) + { + Assert.NotNull(length); + Assert.Equal(expectedValues.Length, (int)length!.Value); + } + else + { + Assert.Null(length); + } foreach (object value in expectedValues) { @@ -76,7 +109,7 @@ public static void VerifyArray(CborReader reader, params object[] expectedValues reader.ReadEndArray(); } - public static void VerifyMap(CborReader reader, params object[] expectedValues) + public static void VerifyMap(CborReader reader, object[] expectedValues, bool expectDefiniteLengthCollections = true) { if (!CborWriterTests.Helpers.IsCborMapRepresentation(expectedValues)) { @@ -84,10 +117,18 @@ public static void VerifyMap(CborReader reader, params object[] expectedValues) } Assert.Equal(CborReaderState.StartMap, reader.Peek()); + ulong? length = reader.ReadStartMap(); - Assert.NotNull(length); - Assert.Equal((expectedValues.Length - 1) / 2, (int)length!.Value); + if (expectDefiniteLengthCollections) + { + Assert.NotNull(length); + Assert.Equal((expectedValues.Length - 1) / 2, (int)length!.Value); + } + else + { + Assert.Null(length); + } foreach (object value in expectedValues.Skip(1)) { 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 9c42ec6fa1e48..62a04378d245d 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 @@ -256,12 +256,12 @@ public static void ReadCborNegativeIntegerEncoding_InvalidData_ShouldThrowFormat [Theory] [InlineData("1f")] [InlineData("3f")] - public static void ReadInt64_IndefiniteLengthIntegers_ShouldThrowNotImplementedException(string hexEncoding) + public static void ReadInt64_IndefiniteLengthIntegers_ShouldThrowFormatException(string hexEncoding) { byte[] data = hexEncoding.HexToByteArray(); var reader = new CborReader(data); - Assert.Throws(() => reader.ReadInt64()); + Assert.Throws(() => reader.ReadInt64()); } [Fact] 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 index c443f354dfdd2..8c90149c5c91f 100644 --- 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 @@ -54,6 +54,19 @@ public static void ReadMap_NestedListValues_HappyPath(object expectedValue, stri Assert.Equal(CborReaderState.Finished, reader.Peek()); } + [Theory] + [InlineData(new object[] { Map }, "bfff")] + [InlineData(new object[] { Map, 1, 2, 3, 4 }, "bf01020304ff")] + [InlineData(new object[] { Map, "a", "A", "b", "B", "c", "C", "d", "D", "e", "E" }, "bf6161614161626142616361436164614461656145ff")] + [InlineData(new object[] { Map, "a", "A", -1, 2, new byte[] { }, new byte[] { 1 } }, "bf616161412002404101ff")] + public static void ReadMap_IndefiniteLength_SimpleValues_HappyPath(object[] exoectedValues, string hexEncoding) + { + byte[] encoding = hexEncoding.HexToByteArray(); + var reader = new CborReader(encoding); + Helpers.VerifyMap(reader, exoectedValues, expectDefiniteLengthCollections: false); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + [Theory] [InlineData(new object[] { Map, "a", 1, "a", 2 }, "a2616101616102")] @@ -193,6 +206,59 @@ public static void ReadMap_IncorrectDefiniteLength_ShouldThrowFormatException(st Assert.Throws(() => reader.ReadInt64()); } + [Theory] + [InlineData("bf")] + [InlineData("bf0102")] + [InlineData("bf01020304")] + public static void ReadMap_IndefiniteLength_MissingBreakByte_ShouldReportEndOfData(string hexEncoding) + { + byte[] encoding = hexEncoding.HexToByteArray(); + var reader = new CborReader(encoding); + reader.ReadStartMap(); + while (reader.Peek() == CborReaderState.UnsignedInteger) + { + reader.ReadInt64(); + } + + Assert.Equal(CborReaderState.EndOfData, reader.Peek()); + } + + [Theory] + [InlineData("bf0102ff", 1)] + [InlineData("bf01020304ff", 2)] + [InlineData("bf010203040506ff", 3)] + public static void ReadMap_IndefiniteLength_PrematureEndArrayCall_ShouldThrowInvalidOperationException(string hexEncoding, int length) + { + byte[] encoding = hexEncoding.HexToByteArray(); + var reader = new CborReader(encoding); + reader.ReadStartMap(); + for (int i = 1; i < length; i++) + { + reader.ReadInt64(); + } + + Assert.Equal(CborReaderState.UnsignedInteger, reader.Peek()); + Assert.Throws(() => reader.ReadEndMap()); + } + + [Theory] + [InlineData("bf01ff", 1)] + [InlineData("bf010203ff", 3)] + [InlineData("bf0102030405ff", 5)] + public static void ReadMap_IndefiniteLength_OddKeyValuePairs_ShouldThrowFormatException(string hexEncoding, int length) + { + byte[] encoding = hexEncoding.HexToByteArray(); + var reader = new CborReader(encoding); + reader.ReadStartMap(); + for (int i = 0; i < length; i++) + { + reader.ReadInt64(); + } + + Assert.Equal(CborReaderState.FormatError, reader.Peek()); // don't want this to fail + Assert.Throws(() => reader.ReadEndMap()); + } + [Theory] [InlineData("a201811907e4", 2, 1)] [InlineData("a61907e4811907e402811907e4", 6, 2)] diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.String.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.String.cs index 10c91c674da2f..ead9666d5199c 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.String.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.String.cs @@ -4,6 +4,8 @@ #nullable enable using System; +using System.Linq; +using System.Text; using Test.Cryptography; using Xunit; @@ -83,18 +85,145 @@ public static void TryReadTextString_SingleValue_HappyPath(string expectedValue, Assert.Equal(CborReaderState.Finished, reader.Peek()); } + [Theory] + [InlineData(new string[] { }, "5fff")] + [InlineData(new string[] { "" }, "5f40ff")] + [InlineData(new string[] { "ab", "" }, "5f41ab40ff")] + [InlineData(new string[] { "ab", "bc", "" }, "5f41ab41bc40ff")] + public static void ReadByteString_IndefiniteLength_SingleValue_HappyPath(string[] expectedHexValues, string hexEncoding) + { + byte[] data = hexEncoding.HexToByteArray(); + byte[][] expectedValues = expectedHexValues.Select(x => x.HexToByteArray()).ToArray(); + var reader = new CborReader(data); + Helpers.VerifyValue(reader, expectedValues); + } + + [Theory] + [InlineData("", "5fff")] + [InlineData("", "5f40ff")] + [InlineData("ab", "5f41ab40ff")] + [InlineData("abbc", "5f41ab41bc40ff")] + public static void ReadByteString_IndefiniteLengthConcatenated_SingleValue_HappyPath(string expectedHexValue, string hexEncoding) + { + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + Assert.Equal(CborReaderState.StartByteString, reader.Peek()); + byte[] actualValue = reader.ReadByteString(); + Assert.Equal(expectedHexValue.ToUpper(), actualValue.ByteArrayToHex()); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + + [Theory] + [InlineData("", "5fff")] + [InlineData("", "5f40ff")] + [InlineData("ab", "5f41ab40ff")] + [InlineData("abbc", "5f41ab41bc40ff")] + public static void TryReadByteString_IndefiniteLengthConcatenated_SingleValue_HappyPath(string expectedHexValue, string hexEncoding) + { + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + Assert.Equal(CborReaderState.StartByteString, reader.Peek()); + + Span buffer = new byte[32]; + bool result = reader.TryReadByteString(buffer, out int bytesWritten); + + Assert.True(result); + Assert.Equal(expectedHexValue.Length / 2, bytesWritten); + Assert.Equal(expectedHexValue.ToUpper(), buffer.Slice(0, bytesWritten).ByteArrayToHex()); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + + [Fact] + public static void ReadByteString_IndefiniteLengthConcatenated_NestedValues_HappyPath() + { + string hexEncoding = "825f41ab40ff5f41ab40ff"; + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + reader.ReadStartArray(); + Assert.Equal("AB", reader.ReadByteString().ByteArrayToHex()); + Assert.Equal("AB", reader.ReadByteString().ByteArrayToHex()); + reader.ReadEndArray(); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + + [Theory] + [InlineData(new string[] { }, "7fff")] + [InlineData(new string[] { "" }, "7f60ff")] + [InlineData(new string[] { "ab", "" }, "7f62616260ff")] + [InlineData(new string[] { "ab", "bc", "" }, "7f62616262626360ff")] + public static void ReadTextString_IndefiniteLength_SingleValue_HappyPath(string[] expectedValues, string hexEncoding) + { + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + Helpers.VerifyValue(reader, expectedValues); + } + + [Theory] + [InlineData("", "7fff")] + [InlineData("", "7f60ff")] + [InlineData("ab", "7f62616260ff")] + [InlineData("abbc", "7f62616262626360ff")] + public static void ReadTextString_IndefiniteLengthConcatenated_SingleValue_HappyPath(string expectedValue, string hexEncoding) + { + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + Assert.Equal(CborReaderState.StartTextString, reader.Peek()); + string actualValue = reader.ReadTextString(); + Assert.Equal(expectedValue, actualValue); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + + [Fact] + public static void ReadTextString_IndefiniteLengthConcatenated_NestedValues_HappyPath() + { + string hexEncoding = "827f62616260ff7f62616260ff"; + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + reader.ReadStartArray(); + Assert.Equal("ab", reader.ReadTextString()); + Assert.Equal("ab", reader.ReadTextString()); + reader.ReadEndArray(); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + + [Theory] + [InlineData("", "7fff")] + [InlineData("", "7f60ff")] + [InlineData("ab", "7f62616260ff")] + [InlineData("abbc", "7f62616262626360ff")] + public static void TryReadTextString_IndefiniteLengthConcatenated_SingleValue__HappyPath(string expectedValue, string hexEncoding) + { + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + Assert.Equal(CborReaderState.StartTextString, reader.Peek()); + + Span buffer = new char[32]; + bool result = reader.TryReadTextString(buffer, out int charsWritten); + + Assert.True(result); + Assert.Equal(expectedValue.Length, charsWritten); + Assert.Equal(expectedValue, new string(buffer.Slice(0, charsWritten))); + Assert.Equal(CborReaderState.Finished, reader.Peek()); + } + [Theory] [InlineData("01020304", "4401020304")] [InlineData("ffffffffffffffffffffffffffff", "4effffffffffffffffffffffffffff")] public static void TryReadByteString_BufferTooSmall_ShouldReturnFalse(string actualValue, string hexEncoding) { - byte[] buffer = new byte[actualValue.Length / 2 - 1]; + byte[] buffer = new byte[actualValue.Length / 2]; byte[] encoding = hexEncoding.HexToByteArray(); var reader = new CborReader(encoding); - bool result = reader.TryReadByteString(buffer, out int bytesWritten); + bool result = reader.TryReadByteString(buffer.AsSpan(1), out int bytesWritten); Assert.False(result); Assert.Equal(0, bytesWritten); Assert.All(buffer, (b => Assert.Equal(0, b))); + + // ensure that reader is still able to complete the read operation if a large enough buffer is supplied subsequently + result = reader.TryReadByteString(buffer, out bytesWritten); + Assert.True(result); + Assert.Equal(buffer.Length, bytesWritten); + Assert.Equal(actualValue.ToUpper(), buffer.ByteArrayToHex()); } [Theory] @@ -107,13 +236,63 @@ public static void TryReadByteString_BufferTooSmall_ShouldReturnFalse(string act [InlineData("\ud800\udd51", "64f0908591")] public static void TryReadTextString_BufferTooSmall_ShouldReturnFalse(string actualValue, string hexEncoding) { - char[] buffer = new char[actualValue.Length - 1]; + char[] buffer = new char[actualValue.Length]; byte[] encoding = hexEncoding.HexToByteArray(); var reader = new CborReader(encoding); - bool result = reader.TryReadTextString(buffer, out int charsWritten); + bool result = reader.TryReadTextString(buffer.AsSpan(1), out int charsWritten); Assert.False(result); Assert.Equal(0, charsWritten); Assert.All(buffer, (b => Assert.Equal(0, '\0'))); + + // ensure that reader is still able to complete the read operation if a large enough buffer is supplied subsequently + result = reader.TryReadTextString(buffer, out charsWritten); + Assert.True(result); + Assert.Equal(actualValue.Length, charsWritten); + Assert.Equal(actualValue, new string(buffer.AsSpan(0, charsWritten))); + } + + [Theory] + [InlineData("ab", "5f41ab40ff")] + [InlineData("abbc", "5f41ab41bc40ff")] + public static void TryReadByteString_IndefiniteLengthConcatenated_BufferTooSmall_ShouldReturnFalse(string expectedHexValue, string hexEncoding) + { + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + + byte[] buffer = new byte[expectedHexValue.Length / 2]; + bool result = reader.TryReadByteString(buffer.AsSpan(1), out int bytesWritten); + + Assert.False(result); + Assert.Equal(0, bytesWritten); + Assert.All(buffer, (b => Assert.Equal(0, b))); + + // ensure that reader is still able to complete the read operation if a large enough buffer is supplied subsequently + result = reader.TryReadByteString(buffer, out bytesWritten); + Assert.True(result); + Assert.Equal(buffer.Length, bytesWritten); + Assert.Equal(expectedHexValue.ToUpper(), buffer.ByteArrayToHex()); + } + + [Theory] + [InlineData("ab", "7f62616260ff")] + [InlineData("abbc", "7f62616262626360ff")] + public static void TryReadTextString_IndefiniteLengthConcatenated_BufferTooSmall_ShouldReturnFalse(string expectedValue, string hexEncoding) + { + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + + char[] buffer = new char[expectedValue.Length]; + bool result = reader.TryReadTextString(buffer.AsSpan(1), out int charsWritten); + + Assert.False(result); + Assert.Equal(0, charsWritten); + Assert.All(buffer, (b => Assert.Equal(0, '\0'))); + + // ensure that reader is still able to perform the read operation if a large enough buffer is supplied subsequently + result = reader.TryReadTextString(buffer, out charsWritten); + Assert.True(result); + Assert.Equal(expectedValue.Length, charsWritten); + Assert.Equal(expectedValue, new string(buffer.AsSpan(0, charsWritten))); } [Theory] @@ -343,5 +522,107 @@ public static void ReadByteString_EmptyBuffer_ShouldThrowFormatException() Assert.Throws(() => reader.ReadByteString()); } + + [Fact] + public static void ReadByteString_IndefiniteLength_ContainingInvalidMajorTypes_ShouldThrowFormatException() + { + string hexEncoding = "5f4001ff"; + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + reader.ReadStartByteStringIndefiniteLength(); + reader.ReadByteString(); + + Assert.Equal(CborReaderState.FormatError, reader.Peek()); + // throws FormatException even if it's the right major type we're trying to read + Assert.Throws(() => reader.ReadInt64()); + } + + [Fact] + public static void ReadTextString_IndefiniteLength_ContainingInvalidMajorTypes_ShouldThrowFormatException() + { + string hexEncoding = "7f6001ff"; + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + reader.ReadStartTextStringIndefiniteLength(); + reader.ReadTextString(); + + Assert.Equal(CborReaderState.FormatError, reader.Peek()); + // throws FormatException even if it's the right major type we're trying to read + Assert.Throws(() => reader.ReadInt64()); + } + + [Fact] + public static void ReadByteString_IndefiniteLength_ContainingNestedIndefiniteLengthStrings_ShouldThrowFormatException() + { + string hexEncoding = "5f5fffff"; + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + + reader.ReadStartByteStringIndefiniteLength(); + + Assert.Throws(() => reader.ReadStartByteStringIndefiniteLength()); + } + + [Fact] + public static void ReadByteString_IndefiniteLengthConcatenated_ContainingNestedIndefiniteLengthStrings_ShouldThrowFormatException() + { + string hexEncoding = "5f5fffff"; + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + + Assert.Throws(() => reader.ReadByteString()); + } + + [Fact] + public static void ReadTextString_IndefiniteLength_ContainingNestedIndefiniteLengthStrings_ShouldThrowFormatException() + { + string hexEncoding = "7f7fffff"; + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + + reader.ReadStartTextStringIndefiniteLength(); + + Assert.Throws(() => reader.ReadStartTextStringIndefiniteLength()); + } + + [Fact] + public static void ReadTextString_IndefiniteLengthConcatenated_ContainingNestedIndefiniteLengthStrings_ShouldThrowFormatException() + { + string hexEncoding = "7f7fffff"; + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + + Assert.Throws(() => reader.ReadTextString()); + } + + [Fact] + public static void ReadByteString_IndefiniteLengthConcatenated_ContainingInvalidMajorTypes_ShouldThrowFormatException() + { + string hexEncoding = "5f4001ff"; + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + Assert.Throws(() => reader.ReadByteString()); + } + + [Fact] + public static void ReadTextString_IndefiniteLengthConcatenated_ContainingInvalidMajorTypes_ShouldThrowFormatException() + { + string hexEncoding = "7f6001ff"; + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + Assert.Throws(() => reader.ReadTextString()); + } + + [Fact] + public static void ReadTextString_IndefiniteLengthConcatenated_InvalidUtf8Chunks_ShouldThrowDecoderFallbackException() + { + // while the concatenated string is valid utf8, the individual chunks are not, + // which is in violation of the CBOR format. + + string hexEncoding = "7f62f090628591ff"; + byte[] data = hexEncoding.HexToByteArray(); + var reader = new CborReader(data); + Assert.Throws(() => reader.ReadTextString()); + } } } 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 e690655b34072..d96346b5c77df 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 @@ -43,6 +43,39 @@ public static void WriteArray_NestedValues_HappyPath(object[] values, string exp AssertHelper.HexEqual(expectedEncoding, actualEncoding); } + [Theory] + [InlineData(new object[] { }, "9fff")] + [InlineData(new object[] { 42 }, "9f182aff")] + [InlineData(new object[] { 1, 2, 3 }, "9f010203ff")] + [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")] + public static void WriteArray_IndefiniteLength_HappyPath(object[] values, string expectedHexEncoding) + { + byte[] expectedEncoding = expectedHexEncoding.HexToByteArray(); + + using var writer = new CborWriter(); + Helpers.WriteArray(writer, values, useDefiniteLengthCollections: false); + + byte[] actualEncoding = writer.ToArray(); + AssertHelper.HexEqual(expectedEncoding, actualEncoding); + } + + [Theory] + [InlineData(new object[] { new object[] { } }, "9f9fffff")] + [InlineData(new object[] { 1, new object[] { 2, 3 }, new object[] { 4, 5 } }, "9f019f0203ff9f0405ffff")] + [InlineData(new object[] { "", new object[] { new object[] { }, new object[] { 1, new byte[] { 10 } } } }, "9f609f9fff9f01410affffff")] + public static void WriteArray_IndefiniteLength_NestedValues_HappyPath(object[] values, string expectedHexEncoding) + { + byte[] expectedEncoding = expectedHexEncoding.HexToByteArray(); + + using var writer = new CborWriter(); + Helpers.WriteArray(writer, values, useDefiniteLengthCollections: false); + + byte[] actualEncoding = writer.ToArray(); + AssertHelper.HexEqual(expectedEncoding, actualEncoding); + } + [Theory] [InlineData(0)] [InlineData(1)] 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 e276dc962f295..1aa5e94d217c0 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 @@ -22,7 +22,7 @@ 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) + public static void WriteValue(CborWriter writer, object value, bool useDefiniteLengthCollections = true) { switch (value) { @@ -31,38 +31,95 @@ public static void WriteValue(CborWriter writer, object value) 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; + case byte[][] chunks: WriteChunkedByteString(writer, chunks); break; + case string[] chunks: WriteChunkedTextString(writer, chunks); break; + case object[] nested when IsCborMapRepresentation(nested): WriteMap(writer, nested, useDefiniteLengthCollections); break; + case object[] nested: WriteArray(writer, nested, useDefiniteLengthCollections); break; default: throw new ArgumentException($"Unrecognized argument type {value.GetType()}"); }; } - public static void WriteArray(CborWriter writer, params object[] values) + public static void WriteArray(CborWriter writer, object[] values, bool useDefiniteLengthCollections = true) { - writer.WriteStartArray(values.Length); + if (useDefiniteLengthCollections) + { + writer.WriteStartArray(values.Length); + } + else + { + writer.WriteStartArrayIndefiniteLength(); + } + foreach (object value in values) { - WriteValue(writer, value); + WriteValue(writer, value, useDefiniteLengthCollections); } + writer.WriteEndArray(); } - public static void WriteMap(CborWriter writer, params object[] keyValuePairs) + public static void WriteMap(CborWriter writer, object[] keyValuePairs, bool useDefiniteLengthCollections = true) { 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); + if (useDefiniteLengthCollections) + { + writer.WriteStartMap(keyValuePairs.Length / 2); + } + else + { + writer.WriteStartMapIndefiniteLength(); + } foreach (object value in keyValuePairs.Skip(1)) { - WriteValue(writer, value); + WriteValue(writer, value, useDefiniteLengthCollections); } writer.WriteEndMap(); } + + public static void WriteChunkedByteString(CborWriter writer, byte[][] chunks) + { + writer.WriteStartByteStringIndefiniteLength(); + foreach (byte[] chunk in chunks) + { + writer.WriteByteString(chunk); + } + writer.WriteEndByteStringIndefiniteLength(); + } + + public static void WriteChunkedTextString(CborWriter writer, string[] chunks) + { + writer.WriteStartTextStringIndefiniteLength(); + foreach (string chunk in chunks) + { + writer.WriteTextString(chunk); + } + writer.WriteEndTextStringIndefiniteLength(); + } + + public static void ExecOperation(CborWriter writer, string op) + { + switch (op) + { + case nameof(writer.WriteInt64): writer.WriteInt64(42); break; + case nameof(writer.WriteByteString): writer.WriteByteString(Array.Empty()); break; + case nameof(writer.WriteTextString): writer.WriteTextString(""); break; + case nameof(writer.WriteStartTextStringIndefiniteLength): writer.WriteStartTextStringIndefiniteLength(); break; + case nameof(writer.WriteStartByteStringIndefiniteLength): writer.WriteStartByteStringIndefiniteLength(); break; + case nameof(writer.WriteStartArray): writer.WriteStartArrayIndefiniteLength(); break; + case nameof(writer.WriteStartMap): writer.WriteStartMapIndefiniteLength(); break; + case nameof(writer.WriteEndByteStringIndefiniteLength): writer.WriteEndByteStringIndefiniteLength(); break; + case nameof(writer.WriteEndTextStringIndefiniteLength): writer.WriteEndTextStringIndefiniteLength(); break; + case nameof(writer.WriteEndArray): writer.WriteEndArray(); break; + case nameof(writer.WriteEndMap): writer.WriteEndMap(); break; + default: throw new Exception($"Unrecognized CborWriter operation name {op}"); + } + } } } } 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 index 46c54b0d8124d..a23ddcb68984a 100644 --- 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 @@ -44,6 +44,33 @@ public static void WriteMap_NestedValues_HappyPath(object[] values, string expec AssertHelper.HexEqual(expectedEncoding, actualEncoding); } + [Theory] + [InlineData(new object[] { Map }, "bfff")] + [InlineData(new object[] { Map, 1, 2, 3, 4 }, "bf01020304ff")] + [InlineData(new object[] { Map, "a", "A", "b", "B", "c", "C", "d", "D", "e", "E" }, "bf6161614161626142616361436164614461656145ff")] + [InlineData(new object[] { Map, "a", "A", -1, 2, new byte[] { }, new byte[] { 1 } }, "bf616161412002404101ff")] + public static void WriteMap_IndefiniteLength_SimpleValues_HappyPath(object[] values, string expectedHexEncoding) + { + byte[] expectedEncoding = expectedHexEncoding.HexToByteArray(); + using var writer = new CborWriter(); + Helpers.WriteMap(writer, values, useDefiniteLengthCollections: false); + byte[] actualEncoding = writer.ToArray(); + AssertHelper.HexEqual(expectedEncoding, actualEncoding); + } + + [Theory] + [InlineData(new object[] { Map, "a", 1, "b", new object[] { Map, 2, 3 } }, "bf6161016162bf0203ffff")] + [InlineData(new object[] { Map, "a", new object[] { Map, 2, 3 }, "b", new object[] { Map, "x", -1, "y", new object[] { Map, "z", 0 } } }, "bf6161bf0203ff6162bf6178206179bf617a00ffffff")] + [InlineData(new object[] { Map, new object[] { Map, "x", 2 }, 42 }, "bfbf617802ff182aff")] // using maps as keys + public static void WriteMap_IndefiniteLength_NestedValues_HappyPath(object[] values, string expectedHexEncoding) + { + byte[] expectedEncoding = expectedHexEncoding.HexToByteArray(); + using var writer = new CborWriter(); + Helpers.WriteMap(writer, values, useDefiniteLengthCollections: false); + 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")] @@ -145,6 +172,26 @@ public static void EndWriteMap_DefiniteLengthNotMet_WithNestedData_ShouldThrowIn Assert.Throws(() => writer.WriteEndMap()); } + [Theory] + [InlineData(0)] + [InlineData(3)] + [InlineData(10)] + public static void EndWriteMap_IndefiniteLength_EvenItems_ShouldThrowInvalidOperationException(int length) + { + using var writer = new CborWriter(); + writer.WriteStartMapIndefiniteLength(); + + for (int i = 1; i < length; i++) + { + writer.WriteTextString($"key_{i}"); + writer.WriteInt64(i); + } + + writer.WriteInt64(0); + + Assert.Throws(() => writer.WriteEndMap()); + } + [Fact] public static void EndWriteMap_ImbalancedCall_ShouldThrowInvalidOperationException() { diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.String.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.String.cs index d5f5b219f5e21..9705af552b677 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.String.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.String.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using System.Linq; using Test.Cryptography; using Xunit; @@ -26,6 +27,21 @@ public static void WriteByteString_SingleValue_HappyPath(string hexInput, string AssertHelper.HexEqual(expectedEncoding, writer.ToArray()); } + [Theory] + [InlineData(new string[] { }, "5fff")] + [InlineData(new string[] { "" }, "5f40ff")] + [InlineData(new string[] { "ab", "" }, "5f41ab40ff")] + [InlineData(new string[] { "ab", "bc", "" }, "5f41ab41bc40ff")] + public static void WriteByteString_IndefiteLength_SingleValue_HappyPath(string[] hexChunkInputs, string hexExpectedEncoding) + { + byte[][] chunkInputs = hexChunkInputs.Select(ch => ch.HexToByteArray()).ToArray(); + byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray(); + + using var writer = new CborWriter(); + Helpers.WriteChunkedByteString(writer, chunkInputs); + AssertHelper.HexEqual(expectedEncoding, writer.ToArray()); + } + [Theory] [InlineData("", "60")] [InlineData("a", "6161")] @@ -42,6 +58,19 @@ public static void WriteTextString_SingleValue_HappyPath(string input, string he AssertHelper.HexEqual(expectedEncoding, writer.ToArray()); } + [Theory] + [InlineData(new string[] { }, "7fff")] + [InlineData(new string[] { "" }, "7f60ff")] + [InlineData(new string[] { "ab", "" }, "7f62616260ff")] + [InlineData(new string[] { "ab", "bc", "" }, "7f62616262626360ff")] + public static void WriteTextString_IndefiniteLength_SingleValue_HappyPath(string[] chunkInputs, string hexExpectedEncoding) + { + byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray(); + using var writer = new CborWriter(); + Helpers.WriteChunkedTextString(writer, chunkInputs); + AssertHelper.HexEqual(expectedEncoding, writer.ToArray()); + } + [Fact] public static void WriteTextString_InvalidUnicodeString_ShouldThrowEncoderFallbackException() { @@ -50,5 +79,58 @@ public static void WriteTextString_InvalidUnicodeString_ShouldThrowEncoderFallba using var writer = new CborWriter(); Assert.Throws(() => writer.WriteTextString(invalidUnicodeString)); } + + [Theory] + [InlineData(nameof(CborWriter.WriteInt64))] + [InlineData(nameof(CborWriter.WriteByteString))] + [InlineData(nameof(CborWriter.WriteStartTextStringIndefiniteLength))] + [InlineData(nameof(CborWriter.WriteStartByteStringIndefiniteLength))] + [InlineData(nameof(CborWriter.WriteStartArray))] + [InlineData(nameof(CborWriter.WriteStartMap))] + public static void WriteTextString_IndefiniteLength_NestedWrites_ShouldThrowInvalidOperationException(string opName) + { + using var writer = new CborWriter(); + writer.WriteStartTextStringIndefiniteLength(); + Assert.Throws(() => Helpers.ExecOperation(writer, opName)); + } + + [Theory] + [InlineData(nameof(CborWriter.WriteEndByteStringIndefiniteLength))] + [InlineData(nameof(CborWriter.WriteEndArray))] + [InlineData(nameof(CborWriter.WriteEndMap))] + public static void WriteTextString_IndefiniteLength_ImbalancedWrites_ShouldThrowInvalidOperationException(string opName) + { + using var writer = new CborWriter(); + writer.WriteStartTextStringIndefiniteLength(); + Assert.Throws(() => Helpers.ExecOperation(writer, opName)); + } + + [Theory] + [InlineData(nameof(CborWriter.WriteInt64))] + [InlineData(nameof(CborWriter.WriteTextString))] + [InlineData(nameof(CborWriter.WriteStartTextStringIndefiniteLength))] + [InlineData(nameof(CborWriter.WriteStartByteStringIndefiniteLength))] + [InlineData(nameof(CborWriter.WriteStartArray))] + [InlineData(nameof(CborWriter.WriteStartMap))] + [InlineData(nameof(CborWriter.WriteEndTextStringIndefiniteLength))] + [InlineData(nameof(CborWriter.WriteEndArray))] + [InlineData(nameof(CborWriter.WriteEndMap))] + public static void WriteByteString_IndefiteLength_NestedWrites_ShouldThrowInvalidOperationException(string opName) + { + using var writer = new CborWriter(); + writer.WriteStartByteStringIndefiniteLength(); + Assert.Throws(() => Helpers.ExecOperation(writer, opName)); + } + + [Theory] + [InlineData(nameof(CborWriter.WriteEndTextStringIndefiniteLength))] + [InlineData(nameof(CborWriter.WriteEndArray))] + [InlineData(nameof(CborWriter.WriteEndMap))] + public static void WriteByteString_IndefiteLength_ImbalancedWrites_ShouldThrowInvalidOperationException(string opName) + { + using var writer = new CborWriter(); + writer.WriteStartByteStringIndefiniteLength(); + Assert.Throws(() => Helpers.ExecOperation(writer, opName)); + } } } 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 ef82fb4907519..b2dd4621add61 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborInitialByte.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborInitialByte.cs @@ -30,7 +30,9 @@ internal enum CborAdditionalInfo : byte /// Represents the Cbor Data item initial byte structure internal readonly struct CborInitialByte { + public const byte IndefiniteLengthBreakByte = 0xff; private const byte AdditionalInformationMask = 0b000_11111; + public byte InitialByte { get; } internal CborInitialByte(CborMajorType majorType, CborAdditionalInfo additionalInfo) 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 34f5c5b18a8dc..7ad19430a0388 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 @@ -12,17 +12,42 @@ internal partial class CborReader public ulong? ReadStartArray() { CborInitialByte header = PeekInitialByte(expectedType: CborMajorType.Array); - ulong arrayLength = checked((ulong)ReadUnsignedInteger(header, out int additionalBytes)); - AdvanceBuffer(1 + additionalBytes); - _remainingDataItems--; - PushDataItem(CborMajorType.Array, arrayLength); - return arrayLength; + if (header.AdditionalInfo == CborAdditionalInfo.IndefiniteLength) + { + AdvanceBuffer(1); + DecrementRemainingItemCount(); + PushDataItem(CborMajorType.Array, null); + return null; + } + else + { + ulong arrayLength = ReadUnsignedInteger(_buffer.Span, header, out int additionalBytes); + AdvanceBuffer(1 + additionalBytes); + DecrementRemainingItemCount(); + PushDataItem(CborMajorType.Array, arrayLength); + return arrayLength; + } } public void ReadEndArray() { - PopDataItem(expectedType: CborMajorType.Array); + if (_remainingDataItems == null) + { + CborInitialByte value = PeekInitialByte(); + + if (value.InitialByte != CborInitialByte.IndefiniteLengthBreakByte) + { + throw new InvalidOperationException("Not at end of indefinite-length array."); + } + + PopDataItem(expectedType: CborMajorType.Array); + AdvanceBuffer(1); + } + else + { + PopDataItem(expectedType: CborMajorType.Array); + } } } } 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 a3bb7b879fa24..ef9ebb185d5be 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 @@ -16,9 +16,9 @@ public ulong ReadUInt64() switch (header.MajorType) { case CborMajorType.UnsignedInteger: - ulong value = ReadUnsignedInteger(header, out int additionalBytes); + ulong value = ReadUnsignedInteger(_buffer.Span, header, out int additionalBytes); AdvanceBuffer(1 + additionalBytes); - _remainingDataItems--; + DecrementRemainingItemCount(); return value; case CborMajorType.NegativeInteger: @@ -40,15 +40,15 @@ public long ReadInt64() switch (header.MajorType) { case CborMajorType.UnsignedInteger: - value = checked((long)ReadUnsignedInteger(header, out additionalBytes)); + value = checked((long)ReadUnsignedInteger(_buffer.Span, header, out additionalBytes)); AdvanceBuffer(1 + additionalBytes); - _remainingDataItems--; + DecrementRemainingItemCount(); return value; case CborMajorType.NegativeInteger: - value = checked(-1 - (long)ReadUnsignedInteger(header, out additionalBytes)); + value = checked(-1 - (long)ReadUnsignedInteger(_buffer.Span, header, out additionalBytes)); AdvanceBuffer(1 + additionalBytes); - _remainingDataItems--; + DecrementRemainingItemCount(); return value; default: @@ -61,17 +61,15 @@ public long ReadInt64() public ulong ReadCborNegativeIntegerEncoding() { CborInitialByte header = PeekInitialByte(expectedType: CborMajorType.NegativeInteger); - ulong value = ReadUnsignedInteger(header, out int additionalBytes); + ulong value = ReadUnsignedInteger(_buffer.Span, header, out int additionalBytes); AdvanceBuffer(1 + additionalBytes); - _remainingDataItems--; + DecrementRemainingItemCount(); return value; } // Unsigned integer decoding https://tools.ietf.org/html/rfc7049#section-2.1 - private ulong ReadUnsignedInteger(CborInitialByte header, out int additionalBytes) + private static ulong ReadUnsignedInteger(ReadOnlySpan buffer, CborInitialByte header, out int additionalBytes) { - ReadOnlySpan buffer = _buffer.Span; - switch (header.AdditionalInfo) { case CborAdditionalInfo x when (x < CborAdditionalInfo.Unsigned8BitIntegerEncoding): @@ -79,28 +77,25 @@ private ulong ReadUnsignedInteger(CborInitialByte header, out int additionalByte return (ulong)x; case CborAdditionalInfo.Unsigned8BitIntegerEncoding: - EnsureBuffer(2); + EnsureBuffer(buffer, 2); additionalBytes = 1; return buffer[1]; case CborAdditionalInfo.Unsigned16BitIntegerEncoding: - EnsureBuffer(3); + EnsureBuffer(buffer, 3); additionalBytes = 2; return BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(1)); case CborAdditionalInfo.Unsigned32BitIntegerEncoding: - EnsureBuffer(5); + EnsureBuffer(buffer, 5); additionalBytes = 4; return BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(1)); case CborAdditionalInfo.Unsigned64BitIntegerEncoding: - EnsureBuffer(9); + EnsureBuffer(buffer, 9); additionalBytes = 8; return BinaryPrimitives.ReadUInt64BigEndian(buffer.Slice(1)); - case CborAdditionalInfo.IndefiniteLength: - throw new NotImplementedException("indefinite length support"); - default: throw new FormatException("initial byte contains invalid integer encoding data"); } 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 aaca54eb8b356..4c800055f8e88 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 @@ -12,22 +12,53 @@ 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) + if (header.AdditionalInfo == CborAdditionalInfo.IndefiniteLength) { - throw new OverflowException("Read CBOR map field count exceeds supported size."); + AdvanceBuffer(1); + DecrementRemainingItemCount(); + PushDataItem(CborMajorType.Map, null); + return null; } + else + { + ulong mapSize = ReadUnsignedInteger(_buffer.Span, header, out int additionalBytes); + + if (mapSize > long.MaxValue) + { + throw new OverflowException("Read CBOR map field count exceeds supported size."); + } - PushDataItem(CborMajorType.Map, 2 * arrayLength); - return arrayLength; + AdvanceBuffer(1 + additionalBytes); + DecrementRemainingItemCount(); + PushDataItem(CborMajorType.Map, 2 * mapSize); + return mapSize; + } } public void ReadEndMap() { - PopDataItem(expectedType: CborMajorType.Map); + if (_remainingDataItems == null) + { + CborInitialByte value = PeekInitialByte(); + + if (value.InitialByte != CborInitialByte.IndefiniteLengthBreakByte) + { + throw new InvalidOperationException("Not at end of indefinite-length map."); + } + + if (!_isEvenNumberOfDataItemsRead) + { + throw new FormatException("CBOR Map types require an even number of key/value combinations"); + } + + PopDataItem(expectedType: CborMajorType.Map); + AdvanceBuffer(1); + } + else + { + PopDataItem(expectedType: CborMajorType.Map); + } } } } 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 dd16a9a4e09fd..0093a8247e30f 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 @@ -2,7 +2,9 @@ // 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; +#nullable enable +using System.Collections.Generic; +using System.Diagnostics; namespace System.Security.Cryptography.Encoding.Tests.Cbor { @@ -14,19 +16,31 @@ internal partial class CborReader public byte[] ReadByteString() { CborInitialByte header = PeekInitialByte(expectedType: CborMajorType.ByteString); - int length = checked((int)ReadUnsignedInteger(header, out int additionalBytes)); + + if (header.AdditionalInfo == CborAdditionalInfo.IndefiniteLength) + { + return ReadChunkedByteStringConcatenated(); + } + + int length = checked((int)ReadUnsignedInteger(_buffer.Span, header, out int additionalBytes)); EnsureBuffer(1 + additionalBytes + length); byte[] result = new byte[length]; _buffer.Slice(1 + additionalBytes, length).CopyTo(result); AdvanceBuffer(1 + additionalBytes + length); - _remainingDataItems--; + DecrementRemainingItemCount(); return result; } public bool TryReadByteString(Span destination, out int bytesWritten) { CborInitialByte header = PeekInitialByte(expectedType: CborMajorType.ByteString); - int length = checked((int)ReadUnsignedInteger(header, out int additionalBytes)); + + if (header.AdditionalInfo == CborAdditionalInfo.IndefiniteLength) + { + return TryReadChunkedByteStringConcatenated(destination, out bytesWritten); + } + + int length = checked((int)ReadUnsignedInteger(_buffer.Span, header, out int additionalBytes)); EnsureBuffer(1 + additionalBytes + length); if (length > destination.Length) @@ -37,7 +51,7 @@ public bool TryReadByteString(Span destination, out int bytesWritten) _buffer.Span.Slice(1 + additionalBytes, length).CopyTo(destination); AdvanceBuffer(1 + additionalBytes + length); - _remainingDataItems--; + DecrementRemainingItemCount(); bytesWritten = length; return true; @@ -47,19 +61,31 @@ public bool TryReadByteString(Span destination, out int bytesWritten) public string ReadTextString() { CborInitialByte header = PeekInitialByte(expectedType: CborMajorType.TextString); - int length = checked((int)ReadUnsignedInteger(header, out int additionalBytes)); + + if (header.AdditionalInfo == CborAdditionalInfo.IndefiniteLength) + { + return ReadChunkedTextStringConcatenated(); + } + + int length = checked((int)ReadUnsignedInteger(_buffer.Span, header, out int additionalBytes)); EnsureBuffer(1 + additionalBytes + length); ReadOnlySpan encodedString = _buffer.Span.Slice(1 + additionalBytes, length); string result = s_utf8Encoding.GetString(encodedString); AdvanceBuffer(1 + additionalBytes + length); - _remainingDataItems--; + DecrementRemainingItemCount(); return result; } public bool TryReadTextString(Span destination, out int charsWritten) { CborInitialByte header = PeekInitialByte(expectedType: CborMajorType.TextString); - int byteLength = checked((int)ReadUnsignedInteger(header, out int additionalBytes)); + + if (header.AdditionalInfo == CborAdditionalInfo.IndefiniteLength) + { + return TryReadChunkedTextStringConcatenated(destination, out charsWritten); + } + + int byteLength = checked((int)ReadUnsignedInteger(_buffer.Span, header, out int additionalBytes)); EnsureBuffer(1 + additionalBytes + byteLength); ReadOnlySpan encodedSlice = _buffer.Span.Slice(1 + additionalBytes, byteLength); @@ -72,9 +98,202 @@ public bool TryReadTextString(Span destination, out int charsWritten) s_utf8Encoding.GetChars(encodedSlice, destination); AdvanceBuffer(1 + additionalBytes + byteLength); - _remainingDataItems--; + DecrementRemainingItemCount(); charsWritten = charLength; return true; } + + public void ReadStartTextStringIndefiniteLength() + { + CborInitialByte header = PeekInitialByte(expectedType: CborMajorType.TextString); + + if (header.AdditionalInfo != CborAdditionalInfo.IndefiniteLength) + { + throw new InvalidOperationException("CBOR text string is not of indefinite length."); + } + + DecrementRemainingItemCount(); + AdvanceBuffer(1); + + PushDataItem(CborMajorType.TextString, expectedNestedItems: null); + } + + public void ReadEndTextStringIndefiniteLength() + { + ReadNextIndefiniteLengthBreakByte(); + PopDataItem(CborMajorType.TextString); + AdvanceBuffer(1); + } + + public void ReadStartByteStringIndefiniteLength() + { + CborInitialByte header = PeekInitialByte(expectedType: CborMajorType.ByteString); + + if (header.AdditionalInfo != CborAdditionalInfo.IndefiniteLength) + { + throw new InvalidOperationException("CBOR text string is not of indefinite length."); + } + + DecrementRemainingItemCount(); + AdvanceBuffer(1); + + PushDataItem(CborMajorType.ByteString, expectedNestedItems: null); + } + + public void ReadEndByteStringIndefiniteLength() + { + ReadNextIndefiniteLengthBreakByte(); + PopDataItem(CborMajorType.ByteString); + AdvanceBuffer(1); + } + + private bool TryReadChunkedByteStringConcatenated(Span destination, out int bytesWritten) + { + List<(int offset, int length)> ranges = ReadChunkedStringRanges(CborMajorType.ByteString, out int encodingLength, out int concatenatedBufferSize); + + if (concatenatedBufferSize > destination.Length) + { + bytesWritten = 0; + return false; + } + + ReadOnlySpan source = _buffer.Span; + + foreach ((int o, int l) in ranges) + { + source.Slice(o, l).CopyTo(destination); + destination = destination.Slice(l); + } + + bytesWritten = concatenatedBufferSize; + AdvanceBuffer(encodingLength); + DecrementRemainingItemCount(); + ReturnRangeList(ranges); + return true; + } + + private bool TryReadChunkedTextStringConcatenated(Span destination, out int charsWritten) + { + List<(int offset, int length)> ranges = ReadChunkedStringRanges(CborMajorType.TextString, out int encodingLength, out int _); + ReadOnlySpan buffer = _buffer.Span; + + int concatenatedStringSize = 0; + foreach ((int o, int l) in ranges) + { + concatenatedStringSize += s_utf8Encoding.GetCharCount(buffer.Slice(o, l)); + } + + if (concatenatedStringSize > destination.Length) + { + charsWritten = 0; + return false; + } + + foreach ((int o, int l) in ranges) + { + s_utf8Encoding.GetChars(buffer.Slice(o, l), destination); + destination = destination.Slice(l); + } + + charsWritten = concatenatedStringSize; + AdvanceBuffer(encodingLength); + DecrementRemainingItemCount(); + ReturnRangeList(ranges); + return true; + } + + private byte[] ReadChunkedByteStringConcatenated() + { + List<(int offset, int length)> ranges = ReadChunkedStringRanges(CborMajorType.ByteString, out int encodingLength, out int concatenatedBufferSize); + var output = new byte[concatenatedBufferSize]; + + ReadOnlySpan source = _buffer.Span; + Span target = output; + + foreach ((int o, int l) in ranges) + { + source.Slice(o, l).CopyTo(target); + target = target.Slice(l); + } + + Debug.Assert(target.IsEmpty); + AdvanceBuffer(encodingLength); + DecrementRemainingItemCount(); + ReturnRangeList(ranges); + return output; + } + + private string ReadChunkedTextStringConcatenated() + { + List<(int offset, int length)> ranges = ReadChunkedStringRanges(CborMajorType.TextString, out int encodingLength, out int concatenatedBufferSize); + ReadOnlySpan buffer = _buffer.Span; + int concatenatedStringSize = 0; + + foreach ((int o, int l) in ranges) + { + concatenatedStringSize += s_utf8Encoding.GetCharCount(buffer.Slice(o, l)); + } + + string output = string.Create(concatenatedStringSize, (ranges, _buffer), BuildString); + + AdvanceBuffer(encodingLength); + DecrementRemainingItemCount(); + ReturnRangeList(ranges); + return output; + + static void BuildString(Span target, (List<(int offset, int length)> ranges, ReadOnlyMemory source) input) + { + ReadOnlySpan source = input.source.Span; + + foreach ((int o, int l) in input.ranges) + { + s_utf8Encoding.GetChars(source.Slice(o, l), target); + target = target.Slice(l); + } + + Debug.Assert(target.IsEmpty); + } + } + + // reads a buffer starting with an indefinite-length string, + // performing validation and returning a list of ranges containing the individual chunk payloads + private List<(int offset, int length)> ReadChunkedStringRanges(CborMajorType type, out int encodingLength, out int concatenatedBufferSize) + { + var ranges = AcquireRangeList(); + ReadOnlySpan buffer = _buffer.Span; + concatenatedBufferSize = 0; + + int i = 1; // skip the indefinite-length initial byte + CborInitialByte nextInitialByte = ReadNextInitialByte(buffer.Slice(i), type); + + while (nextInitialByte.InitialByte != CborInitialByte.IndefiniteLengthBreakByte) + { + checked + { + int chunkLength = (int)ReadUnsignedInteger(buffer.Slice(i), nextInitialByte, out int additionalBytes); + ranges.Add((i + 1 + additionalBytes, chunkLength)); + i += 1 + additionalBytes + chunkLength; + concatenatedBufferSize += chunkLength; + } + + nextInitialByte = ReadNextInitialByte(buffer.Slice(i), type); + } + + encodingLength = i + 1; // include the break byte + return ranges; + + static CborInitialByte ReadNextInitialByte(ReadOnlySpan buffer, CborMajorType expectedType) + { + EnsureBuffer(buffer, 1); + var cib = new CborInitialByte(buffer[0]); + + if (cib.InitialByte != CborInitialByte.IndefiniteLengthBreakByte && cib.MajorType != expectedType) + { + throw new FormatException("Indefinite-length CBOR string containing invalid data item."); + } + + return cib; + } + } } } 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 83673c8124708..fb61dda27af97 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.cs @@ -4,6 +4,8 @@ #nullable enable using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; namespace System.Security.Cryptography.Encoding.Tests.Cbor { @@ -14,13 +16,18 @@ internal enum CborReaderState NegativeInteger, ByteString, TextString, + StartTextString, + StartByteString, StartArray, StartMap, + EndTextString, + EndByteString, EndArray, EndMap, Tag, Special, Finished, + FormatError, EndOfData, } @@ -33,7 +40,11 @@ internal partial class CborReader // with null representing indefinite length data items. // The root context ony permits one data item to be read. private ulong? _remainingDataItems = 1; - private Stack<(CborMajorType type, ulong? remainingDataItems)>? _nestedDataItemStack; + private bool _isEvenNumberOfDataItemsRead = true; // required for indefinite-length map writes + private Stack<(CborMajorType type, bool isEvenNumberOfDataItemsWritten, ulong? remainingDataItems)>? _nestedDataItemStack; + + // stores a reusable List allocation for keeping ranges in the buffer + private List<(int offset, int length)>? _rangeListAllocation = null; internal CborReader(ReadOnlyMemory buffer) { @@ -45,11 +56,6 @@ internal CborReader(ReadOnlyMemory buffer) public CborReaderState Peek() { - if (_remainingDataItems is null) - { - throw new NotImplementedException("indefinite length collections"); - } - if (_remainingDataItems == 0) { if (_nestedDataItemStack?.Count > 0) @@ -72,19 +78,65 @@ public CborReaderState Peek() return CborReaderState.EndOfData; } - CborInitialByte initialByte = new CborInitialByte(_buffer.Span[0]); + var initialByte = new CborInitialByte(_buffer.Span[0]); + + if (initialByte.InitialByte == CborInitialByte.IndefiniteLengthBreakByte) + { + if (_remainingDataItems == null) + { + // stack guaranteed to be populated since root context cannot be indefinite-length + Debug.Assert(_nestedDataItemStack != null && _nestedDataItemStack.Count > 0); + + return _nestedDataItemStack.Peek().type switch + { + CborMajorType.ByteString => CborReaderState.EndByteString, + CborMajorType.TextString => CborReaderState.EndTextString, + CborMajorType.Array => CborReaderState.EndArray, + CborMajorType.Map when !_isEvenNumberOfDataItemsRead => CborReaderState.FormatError, + CborMajorType.Map => CborReaderState.EndMap, + _ => throw new Exception("CborReader internal error. Invalid CBOR major type pushed to stack."), + }; + } + else + { + return CborReaderState.FormatError; + } + } + + if (_remainingDataItems == null) + { + // stack guaranteed to be populated since root context cannot be indefinite-length + Debug.Assert(_nestedDataItemStack != null && _nestedDataItemStack.Count > 0); + + CborMajorType parentType = _nestedDataItemStack.Peek().type; + + switch (parentType) + { + case CborMajorType.ByteString: + case CborMajorType.TextString: + // indefinite length string contexts can only contain data items of same major type + if (initialByte.MajorType != parentType) + { + return CborReaderState.FormatError; + } + + break; + } + } return initialByte.MajorType switch { CborMajorType.UnsignedInteger => CborReaderState.UnsignedInteger, CborMajorType.NegativeInteger => CborReaderState.NegativeInteger, + CborMajorType.ByteString when initialByte.AdditionalInfo == CborAdditionalInfo.IndefiniteLength => CborReaderState.StartByteString, CborMajorType.ByteString => CborReaderState.ByteString, + CborMajorType.TextString when initialByte.AdditionalInfo == CborAdditionalInfo.IndefiniteLength => CborReaderState.StartTextString, CborMajorType.TextString => CborReaderState.TextString, CborMajorType.Array => CborReaderState.StartArray, CborMajorType.Map => CborReaderState.StartMap, CborMajorType.Tag => CborReaderState.Tag, CborMajorType.Special => CborReaderState.Special, - _ => throw new FormatException("Invalid CBOR major type"), + _ => CborReaderState.FormatError, }; } @@ -100,7 +152,31 @@ private CborInitialByte PeekInitialByte() throw new FormatException("unexpected end of buffer."); } - return new CborInitialByte(_buffer.Span[0]); + var result = new CborInitialByte(_buffer.Span[0]); + + // TODO check for tag state + + if (_nestedDataItemStack != null && _nestedDataItemStack.Count > 0) + { + CborMajorType parentType = _nestedDataItemStack.Peek().type; + + switch (parentType) + { + // indefinite-length string contexts do not permit nesting + case CborMajorType.ByteString: + case CborMajorType.TextString: + if (result.InitialByte == CborInitialByte.IndefiniteLengthBreakByte || + result.MajorType == parentType && + result.AdditionalInfo != CborAdditionalInfo.IndefiniteLength) + { + break; + } + + throw new FormatException("Indefinite-length CBOR string containing invalid data item."); + } + } + + return result; } private CborInitialByte PeekInitialByte(CborMajorType expectedType) @@ -115,6 +191,16 @@ private CborInitialByte PeekInitialByte(CborMajorType expectedType) return result; } + private void ReadNextIndefiniteLengthBreakByte() + { + CborInitialByte result = PeekInitialByte(); + + if (result.InitialByte != CborInitialByte.IndefiniteLengthBreakByte) + { + throw new InvalidOperationException("Next data item is not indefinite-length break byte."); + } + } + private void PushDataItem(CborMajorType type, ulong? expectedNestedItems) { if (expectedNestedItems > (ulong)_buffer.Length) @@ -122,37 +208,40 @@ private void PushDataItem(CborMajorType type, ulong? expectedNestedItems) throw new FormatException("Insufficient buffer size for declared definite length in CBOR data item."); } - _nestedDataItemStack ??= new Stack<(CborMajorType, ulong?)>(); - _nestedDataItemStack.Push((type, _remainingDataItems)); + _nestedDataItemStack ??= new Stack<(CborMajorType, bool, ulong?)>(); + _nestedDataItemStack.Push((type, _isEvenNumberOfDataItemsRead, _remainingDataItems)); _remainingDataItems = expectedNestedItems; + _isEvenNumberOfDataItemsRead = true; } private void PopDataItem(CborMajorType expectedType) { - if (_remainingDataItems == null) - { - throw new NotImplementedException("Indefinite-length data items"); - } - - if (_remainingDataItems > 0) - { - throw new InvalidOperationException("Definite-length nested CBOR data item is incomplete."); - } - if (_nestedDataItemStack is null || _nestedDataItemStack.Count == 0) { throw new InvalidOperationException("No active CBOR nested data item to pop"); } - (CborMajorType actualType, ulong? remainingItems) = _nestedDataItemStack.Peek(); + (CborMajorType actualType, bool isEvenNumberOfDataItemsWritten, ulong? remainingItems) = _nestedDataItemStack.Peek(); if (expectedType != actualType) { throw new InvalidOperationException("Unexpected major type in nested CBOR data item."); } + if (_remainingDataItems > 0) + { + throw new InvalidOperationException("Definite-length nested CBOR data item is incomplete."); + } + _nestedDataItemStack.Pop(); _remainingDataItems = remainingItems; + _isEvenNumberOfDataItemsRead = isEvenNumberOfDataItemsWritten; + } + + private void DecrementRemainingItemCount() + { + _remainingDataItems--; + _isEvenNumberOfDataItemsRead = !_isEvenNumberOfDataItemsRead; } private void AdvanceBuffer(int length) @@ -168,5 +257,31 @@ private void EnsureBuffer(int length) throw new FormatException("Unexpected end of buffer."); } } + + private static void EnsureBuffer(ReadOnlySpan buffer, int requiredLength) + { + if (buffer.Length < requiredLength) + { + throw new FormatException("Unexpected end of buffer."); + } + } + + private List<(int offset, int length)> AcquireRangeList() + { + List<(int offset, int length)>? ranges = Interlocked.Exchange(ref _rangeListAllocation, null); + + if (ranges != null) + { + ranges.Clear(); + return ranges; + } + + return new List<(int, int)>(); + } + + private void ReturnRangeList(List<(int offset, int length)> ranges) + { + _rangeListAllocation = ranges; + } } } 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 64676a183689c..97e7f4ff3a609 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 @@ -23,7 +23,22 @@ public void WriteStartArray(int definiteLength) public void WriteEndArray() { + if (!_remainingDataItems.HasValue) + { + // indefinite-length map, add break byte + EnsureWriteCapacity(1); + WriteInitialByte(new CborInitialByte(CborInitialByte.IndefiniteLengthBreakByte)); + } + PopDataItem(CborMajorType.Array); } + + public void WriteStartArrayIndefiniteLength() + { + EnsureWriteCapacity(1); + WriteInitialByte(new CborInitialByte(CborMajorType.Array, CborAdditionalInfo.IndefiniteLength)); + DecrementRemainingItemCount(); + 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 f79cbd97be20d..75cd10d55a577 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 @@ -31,43 +31,40 @@ public void WriteInt64(long value) // Unsigned integer encoding https://tools.ietf.org/html/rfc7049#section-2.1 private void WriteUnsignedInteger(CborMajorType type, ulong value) { - EnsureCanWriteNewDataItem(); - if (value < 24) { EnsureWriteCapacity(1); - _buffer[_offset++] = new CborInitialByte(type, (CborAdditionalInfo)value).InitialByte; + WriteInitialByte(new CborInitialByte(type, (CborAdditionalInfo)value)); } else if (value <= byte.MaxValue) { EnsureWriteCapacity(2); - _buffer[_offset] = new CborInitialByte(type, CborAdditionalInfo.Unsigned8BitIntegerEncoding).InitialByte; - _buffer[_offset + 1] = (byte)value; - _offset += 2; + WriteInitialByte(new CborInitialByte(type, CborAdditionalInfo.Unsigned8BitIntegerEncoding)); + _buffer[_offset++] = (byte)value; } else if (value <= ushort.MaxValue) { EnsureWriteCapacity(3); - _buffer[_offset] = new CborInitialByte(type, CborAdditionalInfo.Unsigned16BitIntegerEncoding).InitialByte; - BinaryPrimitives.WriteUInt16BigEndian(_buffer.AsSpan(_offset + 1), (ushort)value); - _offset += 3; + WriteInitialByte(new CborInitialByte(type, CborAdditionalInfo.Unsigned16BitIntegerEncoding)); + BinaryPrimitives.WriteUInt16BigEndian(_buffer.AsSpan(_offset), (ushort)value); + _offset += 2; } else if (value <= uint.MaxValue) { EnsureWriteCapacity(5); - _buffer[_offset] = new CborInitialByte(type, CborAdditionalInfo.Unsigned32BitIntegerEncoding).InitialByte; - BinaryPrimitives.WriteUInt32BigEndian(_buffer.AsSpan(_offset + 1), (uint)value); - _offset += 5; + WriteInitialByte(new CborInitialByte(type, CborAdditionalInfo.Unsigned32BitIntegerEncoding)); + BinaryPrimitives.WriteUInt32BigEndian(_buffer.AsSpan(_offset), (uint)value); + _offset += 4; } else { EnsureWriteCapacity(9); - _buffer[_offset] = new CborInitialByte(type, CborAdditionalInfo.Unsigned64BitIntegerEncoding).InitialByte; - BinaryPrimitives.WriteUInt64BigEndian(_buffer.AsSpan(_offset + 1), value); - _offset += 9; + WriteInitialByte(new CborInitialByte(type, CborAdditionalInfo.Unsigned64BitIntegerEncoding)); + BinaryPrimitives.WriteUInt64BigEndian(_buffer.AsSpan(_offset), value); + _offset += 8; } - _remainingDataItems--; + 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 6304c7af19c0f..98147689d3546 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 @@ -22,7 +22,27 @@ public void WriteStartMap(int definiteLength) public void WriteEndMap() { + if (!_isEvenNumberOfDataItemsWritten) + { + throw new InvalidOperationException("CBOR Map types require an even number of key/value combinations"); + } + + if (!_remainingDataItems.HasValue) + { + // indefinite-length map, add break byte + EnsureWriteCapacity(1); + WriteInitialByte(new CborInitialByte(CborInitialByte.IndefiniteLengthBreakByte)); + } + PopDataItem(CborMajorType.Map); } + + public void WriteStartMapIndefiniteLength() + { + EnsureWriteCapacity(1); + WriteInitialByte(new CborInitialByte(CborMajorType.Map, CborAdditionalInfo.IndefiniteLength)); + DecrementRemainingItemCount(); + PushDataItem(CborMajorType.Map, expectedNestedItems: null); + } } } 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 6963176fc2cfa..bd9517718971a 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 @@ -30,5 +30,35 @@ public void WriteTextString(ReadOnlySpan value) s_utf8Encoding.GetBytes(value, _buffer.AsSpan(_offset)); _offset += length; } + + public void WriteStartByteStringIndefiniteLength() + { + EnsureWriteCapacity(1); + WriteInitialByte(new CborInitialByte(CborMajorType.ByteString, CborAdditionalInfo.IndefiniteLength)); + DecrementRemainingItemCount(); + PushDataItem(CborMajorType.ByteString, expectedNestedItems: null); + } + + public void WriteEndByteStringIndefiniteLength() + { + EnsureWriteCapacity(1); + WriteInitialByte(new CborInitialByte(CborInitialByte.IndefiniteLengthBreakByte)); + PopDataItem(CborMajorType.ByteString); + } + + public void WriteStartTextStringIndefiniteLength() + { + EnsureWriteCapacity(1); + WriteInitialByte(new CborInitialByte(CborMajorType.TextString, CborAdditionalInfo.IndefiniteLength)); + DecrementRemainingItemCount(); + PushDataItem(CborMajorType.TextString, expectedNestedItems: null); + } + + public void WriteEndTextStringIndefiniteLength() + { + EnsureWriteCapacity(1); + WriteInitialByte(new CborInitialByte(CborInitialByte.IndefiniteLengthBreakByte)); + PopDataItem(CborMajorType.TextString); + } } } 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 339a8860bd5dc..316c10b92399a 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.cs @@ -21,7 +21,8 @@ internal partial class CborWriter : IDisposable // with null representing indefinite length data items. // The root context ony permits one data item to be written. private uint? _remainingDataItems = 1; - private Stack<(CborMajorType type, uint? remainingDataItems)>? _nestedDataItemStack; + private bool _isEvenNumberOfDataItemsWritten = true; // required for indefinite-length map writes + private Stack<(CborMajorType type, bool isEvenNumberOfDataItemsWritten, uint? remainingDataItems)>? _nestedDataItemStack; public CborWriter() { @@ -58,26 +59,26 @@ private void EnsureWriteCapacity(int pendingCount) } } - private void EnsureCanWriteNewDataItem() - { - if (_remainingDataItems == 0) - { - throw new InvalidOperationException("Adding a CBOR data item to the current context exceeds its definite length."); - } - } - private void PushDataItem(CborMajorType type, uint? expectedNestedItems) { - _nestedDataItemStack ??= new Stack<(CborMajorType, uint?)>(); - _nestedDataItemStack.Push((type, _remainingDataItems)); + _nestedDataItemStack ??= new Stack<(CborMajorType, bool, uint?)>(); + _nestedDataItemStack.Push((type, _isEvenNumberOfDataItemsWritten, _remainingDataItems)); _remainingDataItems = expectedNestedItems; + _isEvenNumberOfDataItemsWritten = true; } private void PopDataItem(CborMajorType expectedType) { - if (_remainingDataItems == null) + if (_nestedDataItemStack is null || _nestedDataItemStack.Count == 0) { - throw new NotImplementedException("Indefinite-length data items"); + throw new InvalidOperationException("No active CBOR nested data item to pop"); + } + + (CborMajorType actualType, bool isEvenNumberOfDataItemsWritten, uint? remainingItems) = _nestedDataItemStack.Peek(); + + if (expectedType != actualType) + { + throw new InvalidOperationException("Unexpected major type in nested CBOR data item."); } if (_remainingDataItems > 0) @@ -85,20 +86,52 @@ private void PopDataItem(CborMajorType expectedType) throw new InvalidOperationException("Definite-length nested CBOR data item is incomplete."); } - if (_nestedDataItemStack is null || _nestedDataItemStack.Count == 0) + _nestedDataItemStack.Pop(); + _remainingDataItems = remainingItems; + _isEvenNumberOfDataItemsWritten = isEvenNumberOfDataItemsWritten; + } + + private void DecrementRemainingItemCount() + { + _remainingDataItems--; + _isEvenNumberOfDataItemsWritten = !_isEvenNumberOfDataItemsWritten; + } + + private void WriteInitialByte(CborInitialByte initialByte) + { + if (_remainingDataItems == 0) { - throw new InvalidOperationException("No active CBOR nested data item to pop"); + 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"); } - (CborMajorType actualType, uint? remainingItems) = _nestedDataItemStack.Peek(); + // TODO check for tag state - if (expectedType != actualType) + if (_nestedDataItemStack != null && _nestedDataItemStack.Count > 0) { - throw new InvalidOperationException("Unexpected major type in nested CBOR data item."); + CborMajorType parentType = _nestedDataItemStack.Peek().type; + + switch (parentType) + { + // indefinite-length string contexts do not permit nesting + case CborMajorType.ByteString: + case CborMajorType.TextString: + if (initialByte.InitialByte == CborInitialByte.IndefiniteLengthBreakByte || + initialByte.MajorType == parentType && + initialByte.AdditionalInfo != CborAdditionalInfo.IndefiniteLength) + { + break; + } + + throw new InvalidOperationException("Cannot nest data items in indefinite-length CBOR string contexts."); + } } - _nestedDataItemStack.Pop(); - _remainingDataItems = remainingItems; + _buffer[_offset++] = initialByte.InitialByte; } private void CheckDisposed()