[CBOR] Implement indefinite length writer and reader support (#33831)
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Tue, 24 Mar 2020 22:01:27 +0000 (22:01 +0000)
committerGitHub <noreply@github.com>
Tue, 24 Mar 2020 22:01:27 +0000 (22:01 +0000)
* 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.

20 files changed:
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Array.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Helpers.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Integer.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Map.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.String.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Array.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Helpers.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Map.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.String.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborInitialByte.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Array.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Integer.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Map.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.String.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Array.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Integer.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Map.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.String.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.cs

index 22a27e4cf9f13ec2a4a9fac8ff7b09f5cf958452..e836967ab882713d47a34c0c2c864b7f7b7f731f 100644 (file)
@@ -41,6 +41,21 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             Assert.Throws<InvalidOperationException>(() => 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<InvalidOperationException>(() => reader.ReadEndArray());
+        }
+
         [Theory]
         [InlineData("8101", 1)]
         [InlineData("83010203", 3)]
index 6eba42bdb83174c0934c3adbfcf84db140084b68..16f444b6946a2a970e816035cb3df211255281d8 100644 (file)
@@ -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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
     {
         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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                     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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 }
             }
 
-            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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 }
 
                 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))
                 {
index 9c42ec6fa1e4822c61071e3160e7b4cff7cd7c2b..62a04378d245d65a7769a3a69cac5e7a285c720e 100644 (file)
@@ -256,12 +256,12 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         [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<NotImplementedException>(() => reader.ReadInt64());
+            Assert.Throws<FormatException>(() => reader.ReadInt64());
         }
 
         [Fact]
index c443f354dfdd280201674e0e803494ad015a9038..8c90149c5c91fe8e78af25fa450cfd8faa86bbc6 100644 (file)
@@ -54,6 +54,19 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             Assert.Throws<FormatException>(() => 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<InvalidOperationException>(() => 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<FormatException>(() => reader.ReadEndMap());
+        }
+
         [Theory]
         [InlineData("a201811907e4", 2, 1)]
         [InlineData("a61907e4811907e402811907e4", 6, 2)]
index 10c91c674da2f419a2bbb709a3692ddcaf24a3f6..ead9666d5199c4eb1ae706eca7af897178f09dc1 100644 (file)
@@ -4,6 +4,8 @@
 
 #nullable enable
 using System;
+using System.Linq;
+using System.Text;
 using Test.Cryptography;
 using Xunit;
 
@@ -83,18 +85,145 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             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<byte> 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<char> 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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         [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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
             Assert.Throws<FormatException>(() => 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<FormatException>(() => 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<FormatException>(() => 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<FormatException>(() => reader.ReadStartByteStringIndefiniteLength());
+        }
+
+        [Fact]
+        public static void ReadByteString_IndefiniteLengthConcatenated_ContainingNestedIndefiniteLengthStrings_ShouldThrowFormatException()
+        {
+            string hexEncoding = "5f5fffff";
+            byte[] data = hexEncoding.HexToByteArray();
+            var reader = new CborReader(data);
+
+            Assert.Throws<FormatException>(() => 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<FormatException>(() => reader.ReadStartTextStringIndefiniteLength());
+        }
+
+        [Fact]
+        public static void ReadTextString_IndefiniteLengthConcatenated_ContainingNestedIndefiniteLengthStrings_ShouldThrowFormatException()
+        {
+            string hexEncoding = "7f7fffff";
+            byte[] data = hexEncoding.HexToByteArray();
+            var reader = new CborReader(data);
+
+            Assert.Throws<FormatException>(() => reader.ReadTextString());
+        }
+
+        [Fact]
+        public static void ReadByteString_IndefiniteLengthConcatenated_ContainingInvalidMajorTypes_ShouldThrowFormatException()
+        {
+            string hexEncoding = "5f4001ff";
+            byte[] data = hexEncoding.HexToByteArray();
+            var reader = new CborReader(data);
+            Assert.Throws<FormatException>(() => reader.ReadByteString());
+        }
+
+        [Fact]
+        public static void ReadTextString_IndefiniteLengthConcatenated_ContainingInvalidMajorTypes_ShouldThrowFormatException()
+        {
+            string hexEncoding = "7f6001ff";
+            byte[] data = hexEncoding.HexToByteArray();
+            var reader = new CborReader(data);
+            Assert.Throws<FormatException>(() => 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<DecoderFallbackException>(() => reader.ReadTextString());
+        }
     }
 }
index e690655b340728e0d769256bc64914ba3f3dd639..d96346b5c77dff57d37568c56901ba329779de0e 100644 (file)
@@ -43,6 +43,39 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             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)]
index e276dc962f295673b45299f2e7c25ecc948d5bdf..1aa5e94d217c040e89893c203f915074541f5390 100644 (file)
@@ -22,7 +22,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                     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<byte>()); 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}");
+                }
+            }
         }
     }
 }
index 46c54b0d8124dc212402ed2a57b24aca92621dd3..a23ddcb68984a9c6b3daecad6375fc7ab2fdbecf 100644 (file)
@@ -44,6 +44,33 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             Assert.Throws<InvalidOperationException>(() => 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<InvalidOperationException>(() => writer.WriteEndMap());
+        }
+
         [Fact]
         public static void EndWriteMap_ImbalancedCall_ShouldThrowInvalidOperationException()
         {
index d5f5b219f5e21615583e59264f36af80b718b493..9705af552b677d24b29a39b62bf850641f148c08 100644 (file)
@@ -4,6 +4,7 @@
 
 #nullable enable
 using System;
+using System.Linq;
 using Test.Cryptography;
 using Xunit;
 
@@ -26,6 +27,21 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             using var writer = new CborWriter();
             Assert.Throws<System.Text.EncoderFallbackException>(() => 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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<InvalidOperationException>(() => Helpers.ExecOperation(writer, opName));
+        }
     }
 }
index ef82fb49075193f0de73cc40f71aaa975420e004..b2dd4621add61a9a8cd16e14ffb14e6c67a0a184 100644 (file)
@@ -30,7 +30,9 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
     /// 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)
index 34f5c5b18a8dc7efe719ee255e27ad5e5705efdc..7ad19430a0388717f7234243960ee63f8d6d93af 100644 (file)
@@ -12,17 +12,42 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         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);
+            }
         }
     }
 }
index a3bb7b879fa24cb1bab3f92e433b6be3ccc94772..ef9ebb185d5bea18e2cc3bcd4f9b22fc0be39efc 100644 (file)
@@ -16,9 +16,9 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         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<byte> buffer, CborInitialByte header, out int additionalBytes)
         {
-            ReadOnlySpan<byte> buffer = _buffer.Span;
-
             switch (header.AdditionalInfo)
             {
                 case CborAdditionalInfo x when (x < CborAdditionalInfo.Unsigned8BitIntegerEncoding):
@@ -79,28 +77,25 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                     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");
             }
index aaca54eb8b35624f1f8acc905f994e3b7734e015..4c800055f8e88a51a724c57fa89aabe87b2cee8d 100644 (file)
@@ -12,22 +12,53 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         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);
+            }
         }
     }
 }
index dd16a9a4e09fdf35589479616c0b8cbf602bb612..0093a8247e30f72fe1182116dcf5a6cdca16405b 100644 (file)
@@ -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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         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<byte> 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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
             _buffer.Span.Slice(1 + additionalBytes, length).CopyTo(destination);
             AdvanceBuffer(1 + additionalBytes + length);
-            _remainingDataItems--;
+            DecrementRemainingItemCount();
 
             bytesWritten = length;
             return true;
@@ -47,19 +61,31 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         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<byte> 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<char> 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<byte> encodedSlice = _buffer.Span.Slice(1 + additionalBytes, byteLength);
@@ -72,9 +98,202 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
             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<byte> 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<byte> 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<char> destination, out int charsWritten)
+        {
+            List<(int offset, int length)> ranges = ReadChunkedStringRanges(CborMajorType.TextString, out int encodingLength, out int _);
+            ReadOnlySpan<byte> 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<byte> source = _buffer.Span;
+            Span<byte> 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<byte> 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<char> target, (List<(int offset, int length)> ranges, ReadOnlyMemory<byte> source) input)
+            {
+                ReadOnlySpan<byte> 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<byte> 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<byte> 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;
+            }
+        }
     }
 }
index 83673c81247085ed4d4e4f91f430284951178423..fb61dda27af9723334d01b4d139fb37416f65354 100644 (file)
@@ -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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         NegativeInteger,
         ByteString,
         TextString,
+        StartTextString,
+        StartByteString,
         StartArray,
         StartMap,
+        EndTextString,
+        EndByteString,
         EndArray,
         EndMap,
         Tag,
         Special,
         Finished,
+        FormatError,
         EndOfData,
     }
 
@@ -33,7 +40,11 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         // 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<byte> buffer)
         {
@@ -45,11 +56,6 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
         public CborReaderState Peek()
         {
-            if (_remainingDataItems is null)
-            {
-                throw new NotImplementedException("indefinite length collections");
-            }
-
             if (_remainingDataItems == 0)
             {
                 if (_nestedDataItemStack?.Count > 0)
@@ -72,19 +78,65 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 throw new FormatException("Unexpected end of buffer.");
             }
         }
+
+        private static void EnsureBuffer(ReadOnlySpan<byte> 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;
+        }
     }
 }
index 64676a183689ce2b44871b8f3b18816bfd7f07dd..97e7f4ff3a609c0893bf47bd486d8987ffbf194c 100644 (file)
@@ -23,7 +23,22 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
         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);
+        }
     }
 }
index f79cbd97be20d167eb2de73f88b45d702673bda9..75cd10d55a5778509376f97069326a2066416fb3 100644 (file)
@@ -31,43 +31,40 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         // 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();
         }
     }
 }
index 6304c7af19c0ffdfb6d4641012b1e679c7ad040b..98147689d3546e7b597e9b72890ea7786f632a65 100644 (file)
@@ -22,7 +22,27 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
         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);
+        }
     }
 }
index 6963176fc2cfaca4a128607480e264cde2301c05..bd9517718971a1a4aabc95f948896b925303ebf2 100644 (file)
@@ -30,5 +30,35 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             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);
+        }
     }
 }
index 339a8860bd5dc3d15bf414959c1fbe12b2211cdc..316c10b92399ab0587a674390c07543b05ceae1e 100644 (file)
@@ -21,7 +21,8 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         // 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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             }
         }
 
-        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 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 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()