implement patching for indefinite-length items
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Tue, 28 Apr 2020 15:18:51 +0000 (16:18 +0100)
committerEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Wed, 29 Apr 2020 14:30:11 +0000 (15:30 +0100)
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.Tests/CborWriterTests.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 66ba5c1..756d242 100644 (file)
@@ -79,6 +79,40 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         }
 
         [Theory]
+        [InlineData(new object[] { }, "80")]
+        [InlineData(new object[] { 42 }, "81182a")]
+        [InlineData(new object[] { 1, 2, 3 }, "83010203")]
+        [InlineData(new object[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25 }, "98190102030405060708090a0b0c0d0e0f101112131415161718181819")]
+        [InlineData(new object[] { 1, -1, "", new byte[] { 7 } }, "840120604107")]
+        [InlineData(new object[] { "lorem", "ipsum", "dolor" }, "83656c6f72656d65697073756d65646f6c6f72")]
+        [InlineData(new object?[] { false, null, float.NaN, double.PositiveInfinity }, "84f4f6faffc00000fb7ff0000000000000")]
+        public static void WriteArray_IndefiniteLengthWithPatching_HappyPath(object[] values, string expectedHexEncoding)
+        {
+            byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
+
+            using var writer = new CborWriter(patchIndefiniteLengthItems: true);
+            Helpers.WriteArray(writer, values, useDefiniteLengthCollections: false);
+
+            byte[] actualEncoding = writer.GetEncoding();
+            AssertHelper.HexEqual(expectedEncoding, actualEncoding);
+        }
+
+        [Theory]
+        [InlineData(new object[] { new object[] { } }, "8180")]
+        [InlineData(new object[] { 1, new object[] { 2, 3 }, new object[] { 4, 5 } }, "8301820203820405")]
+        [InlineData(new object[] { "", new object[] { new object[] { }, new object[] { 1, new byte[] { 10 } } } }, "826082808201410a")]
+        public static void WriteArray_IndefiniteLengthWithPatching_NestedValues_HappyPath(object[] values, string expectedHexEncoding)
+        {
+            byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
+
+            using var writer = new CborWriter(patchIndefiniteLengthItems: true);
+            Helpers.WriteArray(writer, values, useDefiniteLengthCollections: false);
+
+            byte[] actualEncoding = writer.GetEncoding();
+            AssertHelper.HexEqual(expectedEncoding, actualEncoding);
+        }
+
+        [Theory]
         [InlineData(0)]
         [InlineData(1)]
         [InlineData(3)]
index 40b51de..337756a 100644 (file)
@@ -19,6 +19,8 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
             public const string EncodedPrefixIdentifier = "_encodedValue";
 
+            public const string HexByteStringIdentifier = "_hex";
+
             // Since we inject test data using attributes, meed to represent both arrays and maps using object arrays.
             // To distinguish between the two types, we prepend map representations using a string constant.
             public static bool IsCborMapRepresentation(object[] values)
@@ -38,6 +40,11 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 return values.Length == 2 && values[0] is CborTag;
             }
 
+            public static bool IsIndefiniteLengthByteString(string[] values)
+            {
+                return values.Length % 2 == 1 && values[0] == HexByteStringIdentifier;
+            }
+
             public static void WriteValue(CborWriter writer, object value, bool useDefiniteLengthCollections = true)
             {
                 switch (value)
@@ -55,6 +62,11 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                     case DateTimeOffset d: writer.WriteDateTimeOffset(d); break;
                     case byte[] b: writer.WriteByteString(b); break;
                     case byte[][] chunks: WriteChunkedByteString(writer, chunks); break;
+                    case string[] chunks when IsIndefiniteLengthByteString(chunks):
+                        byte[][] byteChunks = chunks.Skip(1).Select(ch => ch.HexToByteArray()).ToArray();
+                        WriteChunkedByteString(writer, byteChunks);
+                        break;
+
                     case string[] chunks: WriteChunkedTextString(writer, chunks); break;
                     case object[] nested when IsCborMapRepresentation(nested): WriteMap(writer, nested, useDefiniteLengthCollections); break;
                     case object[] nested when IsEncodedValueRepresentation(nested):
index ec8e271..0a97021 100644 (file)
@@ -16,6 +16,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         // Additional pairs generated using http://cbor.me/
 
         public const string Map = Helpers.MapPrefixIdentifier;
+        public const string Hex = Helpers.HexByteStringIdentifier;
 
         [Theory]
         [InlineData(new object[] { Map }, "a0")]
@@ -72,6 +73,47 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         }
 
         [Theory]
+        [InlineData(new object[] { Map }, "a0")]
+        [InlineData(new object[] { Map, 1, 2, 3, 4 }, "a201020304")]
+        [InlineData(new object[] { Map, "a", "A", "b", "B", "c", "C", "d", "D", "e", "E" }, "a56161614161626142616361436164614461656145")]
+        [InlineData(new object[] { Map, "a", "A", -1, 2, new byte[] { }, new byte[] { 1 } }, "a3616161412002404101")]
+        public static void WriteMap_IndefiniteLengthWithPatching_SimpleValues_HappyPath(object[] values, string expectedHexEncoding)
+        {
+            byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
+            using var writer = new CborWriter(patchIndefiniteLengthItems: true);
+            Helpers.WriteMap(writer, values, useDefiniteLengthCollections: false);
+            byte[] actualEncoding = writer.GetEncoding();
+            AssertHelper.HexEqual(expectedEncoding, actualEncoding);
+        }
+
+        [Theory]
+        [InlineData(new object[] { Map, "a", 1, "b", new object[] { Map, 2, 3 } }, "a26161016162a10203")]
+        [InlineData(new object[] { Map, "a", new object[] { Map, 2, 3 }, "b", new object[] { Map, "x", -1, "y", new object[] { Map, "z", 0 } } }, "a26161a102036162a26178206179a1617a00")]
+        [InlineData(new object[] { Map, new object[] { Map, "x", 2 }, 42 }, "a1a1617802182a")] // using maps as keys
+        public static void WriteMap_IndefiniteLengthWithPatching_NestedValues_HappyPath(object[] values, string expectedHexEncoding)
+        {
+            byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
+            using var writer = new CborWriter(patchIndefiniteLengthItems: true);
+            Helpers.WriteMap(writer, values, useDefiniteLengthCollections: false);
+            byte[] actualEncoding = writer.GetEncoding();
+            AssertHelper.HexEqual(expectedEncoding, actualEncoding);
+        }
+
+        [Theory]
+        [InlineData(new object[] { Map, 3, 4, 1, 2 }, "a201020304")]
+        [InlineData(new object[] { Map, "d", "D", "e", "E", "a", "A", "b", "B", "c", "C" }, "a56161614161626142616361436164614461656145")]
+        [InlineData(new object[] { Map, "a", "A", -1, 2, new byte[] { }, new byte[] { 1 } }, "a3200240410161616141")]
+        [InlineData(new object[] { Map, new object[] { Map, 3, 4, 1, 2 }, 0, new object[] { 1, 2, 3 }, 0, new string[] { "a", "b" }, 0, new string[] { Hex, "ab", "" }, 00 }, "a441ab00626162008301020300a20102030400")]
+        public static void WriteMap_IndefiniteLengthWithPatching_Ctap2Sorting_HappyPath(object[] values, string expectedHexEncoding)
+    {
+        byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
+        using var writer = new CborWriter(CborConformanceLevel.Ctap2Canonical, patchIndefiniteLengthItems: true);
+        Helpers.WriteMap(writer, values, useDefiniteLengthCollections: false);
+        byte[] actualEncoding = writer.GetEncoding();
+        AssertHelper.HexEqual(expectedEncoding, actualEncoding);
+    }
+
+    [Theory]
         [InlineData(new object[] { Map, "a", 1, "b", new object[] { 2, 3 } }, "a26161016162820203")]
         [InlineData(new object[] { Map, "a", new object[] { 2, 3, "b", new object[] { Map, "x", -1, "y", new object[] { "z", 0 } } } }, "a161618402036162a2617820617982617a00")]
         [InlineData(new object[] { "a", new object[] { Map, "b", "c" } }, "826161a161626163")]
index 4bd31ce..478835b 100644 (file)
@@ -32,7 +32,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         [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)
+        public static void WriteByteString_IndefiniteLength_SingleValue_HappyPath(string[] hexChunkInputs, string hexExpectedEncoding)
         {
             byte[][] chunkInputs = hexChunkInputs.Select(ch => ch.HexToByteArray()).ToArray();
             byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray();
@@ -43,6 +43,21 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         }
 
         [Theory]
+        [InlineData(new string[] { }, "40")]
+        [InlineData(new string[] { "" }, "40")]
+        [InlineData(new string[] { "ab", "" }, "41ab")]
+        [InlineData(new string[] { "ab", "bc", "" }, "42abbc")]
+        public static void WriteByteString_IndefiniteLength_WithPatching_SingleValue_HappyPath(string[] hexChunkInputs, string hexExpectedEncoding)
+        {
+            byte[][] chunkInputs = hexChunkInputs.Select(ch => ch.HexToByteArray()).ToArray();
+            byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray();
+
+            using var writer = new CborWriter(patchIndefiniteLengthItems: true);
+            Helpers.WriteChunkedByteString(writer, chunkInputs);
+            AssertHelper.HexEqual(expectedEncoding, writer.GetEncoding());
+        }
+
+        [Theory]
         [InlineData("", "60")]
         [InlineData("a", "6161")]
         [InlineData("IETF", "6449455446")]
@@ -71,6 +86,19 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             AssertHelper.HexEqual(expectedEncoding, writer.GetEncoding());
         }
 
+        [Theory]
+        [InlineData(new string[] { }, "60")]
+        [InlineData(new string[] { "" }, "60")]
+        [InlineData(new string[] { "ab", "" }, "626162")]
+        [InlineData(new string[] { "ab", "bc", "" }, "6461626263")]
+        public static void WriteTextString_IndefiniteLengthWithPatching_SingleValue_HappyPath(string[] chunkInputs, string hexExpectedEncoding)
+        {
+            byte[] expectedEncoding = hexExpectedEncoding.HexToByteArray();
+            using var writer = new CborWriter(patchIndefiniteLengthItems: true);
+            Helpers.WriteChunkedTextString(writer, chunkInputs);
+            AssertHelper.HexEqual(expectedEncoding, writer.GetEncoding());
+        }
+
         [Fact]
         public static void WriteTextString_InvalidUnicodeString_ShouldThrowArgumentException()
         {
@@ -117,7 +145,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         [InlineData(nameof(CborWriter.WriteEndTextStringIndefiniteLength))]
         [InlineData(nameof(CborWriter.WriteEndArray))]
         [InlineData(nameof(CborWriter.WriteEndMap))]
-        public static void WriteByteString_IndefiteLength_NestedWrites_ShouldThrowInvalidOperationException(string opName)
+        public static void WriteByteString_IndefiniteLength_NestedWrites_ShouldThrowInvalidOperationException(string opName)
         {
             using var writer = new CborWriter();
             writer.WriteStartByteStringIndefiniteLength();
@@ -128,7 +156,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         [InlineData(nameof(CborWriter.WriteEndTextStringIndefiniteLength))]
         [InlineData(nameof(CborWriter.WriteEndArray))]
         [InlineData(nameof(CborWriter.WriteEndMap))]
-        public static void WriteByteString_IndefiteLength_ImbalancedWrites_ShouldThrowInvalidOperationException(string opName)
+        public static void WriteByteString_IndefiniteLength_ImbalancedWrites_ShouldThrowInvalidOperationException(string opName)
         {
             using var writer = new CborWriter();
             writer.WriteStartByteStringIndefiniteLength();
index 945a36a..af3a4a9 100644 (file)
@@ -23,7 +23,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         }
 
         [Fact]
-        public static void ToArray_OnInCompleteValue_ShouldThrowInvalidOperationExceptoin()
+        public static void GetEncoding_OnInCompleteValue_ShouldThrowInvalidOperationExceptoin()
         {
             using var writer = new CborWriter();
             Assert.Throws<InvalidOperationException>(() => writer.GetEncoding());
@@ -34,7 +34,9 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         {
             using var writer = new CborWriter();
             writer.WriteInt64(42);
+            int bytesWritten = writer.BytesWritten;
             Assert.Throws<InvalidOperationException>(() => writer.WriteTextString("lorem ipsum"));
+            Assert.Equal(bytesWritten, writer.BytesWritten);
         }
 
         [Fact]
index 36c4568..5bbdb21 100644 (file)
@@ -4,6 +4,7 @@
 
 #nullable enable
 using System.Buffers.Binary;
+using System.Diagnostics;
 using System.Text;
 
 namespace System.Security.Cryptography.Encoding.Tests.Cbor
@@ -18,34 +19,48 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             }
 
             WriteUnsignedInteger(CborMajorType.Array, (ulong)definiteLength);
-            PushDataItem(CborMajorType.Array, (uint)definiteLength);
+            PushDataItem(CborMajorType.Array, definiteLength);
         }
 
         public void WriteEndArray()
         {
-            bool isDefiniteLengthArray = _remainingDataItems.HasValue;
             PopDataItem(CborMajorType.Array);
-
-            if (!isDefiniteLengthArray)
-            {
-                // append break byte for indefinite-length arrays
-                EnsureWriteCapacity(1);
-                _buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte;
-            }
-
             AdvanceDataItemCounters();
         }
 
         public void WriteStartArrayIndefiniteLength()
         {
-            if (CborConformanceLevelHelpers.RequiresDefiniteLengthItems(ConformanceLevel))
+            if (!PatchIndefiniteLengthItems && CborConformanceLevelHelpers.RequiresDefiniteLengthItems(ConformanceLevel))
             {
                 throw new InvalidOperationException("Indefinite-length items are not permitted under the current conformance level.");
             }
 
             EnsureWriteCapacity(1);
             WriteInitialByte(new CborInitialByte(CborMajorType.Array, CborAdditionalInfo.IndefiniteLength));
-            PushDataItem(CborMajorType.Array, expectedNestedItems: null);
+            PushDataItem(CborMajorType.Array, definiteLength: null);
+        }
+
+        private void PatchIndefiniteLengthCollection(CborMajorType majorType, int count)
+        {
+            Debug.Assert(majorType == CborMajorType.Array || majorType == CborMajorType.Map);
+
+            int currentOffset = _offset;
+            int bytesToShift = GetIntegerEncodingLength((ulong)count) - 1;
+
+            if (bytesToShift > 0)
+            {
+                // length encoding requires more than 1 byte, need to shift encoded elements to the right
+                EnsureWriteCapacity(bytesToShift);
+
+                ReadOnlySpan<byte> elementEncoding = _buffer.AsSpan(_frameOffset, currentOffset - _frameOffset);
+                Span<byte> target = _buffer.AsSpan(_frameOffset + bytesToShift, currentOffset - _frameOffset);
+                elementEncoding.CopyTo(target);
+            }
+
+            // rewind to the start of the collection and write a new initial byte
+            _offset = _frameOffset - 1;
+            WriteUnsignedInteger(majorType, (ulong)count);
+            _offset = currentOffset + bytesToShift;
         }
     }
 }
index 4db3193..0310be5 100644 (file)
@@ -45,7 +45,7 @@ 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)
         {
-            if (value < 24)
+            if (value < (byte)CborAdditionalInfo.Additional8BitData)
             {
                 EnsureWriteCapacity(1);
                 WriteInitialByte(new CborInitialByte(type, (CborAdditionalInfo)value));
@@ -78,5 +78,29 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 _offset += 8;
             }
         }
+
+        private int GetIntegerEncodingLength(ulong value)
+        {
+            if (value < (byte)CborAdditionalInfo.Additional8BitData)
+            {
+                return 1;
+            }
+            else if (value <= byte.MaxValue)
+            {
+                return 2;
+            }
+            else if (value <= ushort.MaxValue)
+            {
+                return 3;
+            }
+            else if (value <= uint.MaxValue)
+            {
+                return 5;
+            }
+            else
+            {
+                return 9;
+            }
+        }
     }
 }
index df6a485..b29f6bb 100644 (file)
@@ -15,46 +15,36 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
         public void WriteStartMap(int definiteLength)
         {
-            if (definiteLength < 0)
+            if (definiteLength < 0 || definiteLength > int.MaxValue / 2)
             {
-                throw new ArgumentOutOfRangeException(nameof(definiteLength), "must be non-negative integer.");
+                throw new ArgumentOutOfRangeException(nameof(definiteLength));
             }
 
             WriteUnsignedInteger(CborMajorType.Map, (ulong)definiteLength);
-            PushDataItem(CborMajorType.Map, 2 * (uint)definiteLength);
+            PushDataItem(CborMajorType.Map, definiteLength: checked(2 * definiteLength));
         }
 
         public void WriteEndMap()
         {
-            if (_currentValueOffset != null)
+            if (_itemsWritten % 2 == 1)
             {
                 throw new InvalidOperationException("CBOR Map types require an even number of key/value combinations");
             }
 
-            bool isDefiniteLengthMap = _remainingDataItems.HasValue;
-
             PopDataItem(CborMajorType.Map);
-
-            if (!isDefiniteLengthMap)
-            {
-                // append break byte
-                EnsureWriteCapacity(1);
-                _buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte;
-            }
-
             AdvanceDataItemCounters();
         }
 
         public void WriteStartMapIndefiniteLength()
         {
-            if (CborConformanceLevelHelpers.RequiresDefiniteLengthItems(ConformanceLevel))
+            if (!PatchIndefiniteLengthItems && CborConformanceLevelHelpers.RequiresDefiniteLengthItems(ConformanceLevel))
             {
                 throw new InvalidOperationException("Indefinite-length items are not permitted under the current conformance level.");
             }
 
             EnsureWriteCapacity(1);
             WriteInitialByte(new CborInitialByte(CborMajorType.Map, CborAdditionalInfo.IndefiniteLength));
-            PushDataItem(CborMajorType.Map, expectedNestedItems: null);
+            PushDataItem(CborMajorType.Map, definiteLength: null);
             _currentKeyOffset = _offset;
             _currentValueOffset = null;
         }
index ce8977e..4594c4a 100644 (file)
@@ -4,7 +4,10 @@
 
 #nullable enable
 using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.Diagnostics;
 using System.Text;
+using System.Threading;
 
 namespace System.Security.Cryptography.Encoding.Tests.Cbor
 {
@@ -12,11 +15,22 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
     {
         private static readonly System.Text.Encoding s_utf8Encoding = new System.Text.UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
 
+        // keeps track of chunk offsets for written indefinite-length string ranges
+        private List<(int Offset, int Length)>? _currentIndefiniteLengthStringRanges = null;
+
         // Implements major type 2 encoding per https://tools.ietf.org/html/rfc7049#section-2.1
         public void WriteByteString(ReadOnlySpan<byte> value)
         {
             WriteUnsignedInteger(CborMajorType.ByteString, (ulong)value.Length);
             EnsureWriteCapacity(value.Length);
+
+            if (PatchIndefiniteLengthItems && IsMajorTypeContext(CborMajorType.ByteString))
+            {
+                // operation is writing chunk of an indefinite-length string
+                Debug.Assert(_currentIndefiniteLengthStringRanges != null);
+                _currentIndefiniteLengthStringRanges.Add((_offset, value.Length));
+            }
+
             value.CopyTo(_buffer.AsSpan(_offset));
             _offset += value.Length;
             AdvanceDataItemCounters();
@@ -37,51 +51,99 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
             WriteUnsignedInteger(CborMajorType.TextString, (ulong)length);
             EnsureWriteCapacity(length);
-            s_utf8Encoding.GetBytes(value, _buffer.AsSpan(_offset));
+
+            if (PatchIndefiniteLengthItems && IsMajorTypeContext(CborMajorType.TextString))
+            {
+                // operation is writing chunk of an indefinite-length string
+                Debug.Assert(_currentIndefiniteLengthStringRanges != null);
+                _currentIndefiniteLengthStringRanges.Add((_offset, value.Length));
+            }
+
+            s_utf8Encoding.GetBytes(value, _buffer.AsSpan(_offset, length));
             _offset += length;
             AdvanceDataItemCounters();
         }
 
         public void WriteStartByteStringIndefiniteLength()
         {
-            if (CborConformanceLevelHelpers.RequiresDefiniteLengthItems(ConformanceLevel))
+            if (PatchIndefiniteLengthItems)
+            {
+                _currentIndefiniteLengthStringRanges ??= new List<(int, int)>();
+            }
+            else if (CborConformanceLevelHelpers.RequiresDefiniteLengthItems(ConformanceLevel))
             {
                 throw new InvalidOperationException("Indefinite-length items are not permitted under the current conformance level.");
             }
 
             EnsureWriteCapacity(1);
             WriteInitialByte(new CborInitialByte(CborMajorType.ByteString, CborAdditionalInfo.IndefiniteLength));
-            PushDataItem(CborMajorType.ByteString, expectedNestedItems: null);
+            PushDataItem(CborMajorType.ByteString, definiteLength: null);
         }
 
         public void WriteEndByteStringIndefiniteLength()
         {
             PopDataItem(CborMajorType.ByteString);
-            // append break byte
-            EnsureWriteCapacity(1);
-            _buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte;
             AdvanceDataItemCounters();
         }
 
         public void WriteStartTextStringIndefiniteLength()
         {
-            if (CborConformanceLevelHelpers.RequiresDefiniteLengthItems(ConformanceLevel))
+            if (PatchIndefiniteLengthItems)
+            {
+                _currentIndefiniteLengthStringRanges ??= new List<(int, int)>();
+            }
+            else if (CborConformanceLevelHelpers.RequiresDefiniteLengthItems(ConformanceLevel))
             {
                 throw new InvalidOperationException("Indefinite-length items are not permitted under the current conformance level.");
             }
 
             EnsureWriteCapacity(1);
             WriteInitialByte(new CborInitialByte(CborMajorType.TextString, CborAdditionalInfo.IndefiniteLength));
-            PushDataItem(CborMajorType.TextString, expectedNestedItems: null);
+            PushDataItem(CborMajorType.TextString, definiteLength: null);
         }
 
         public void WriteEndTextStringIndefiniteLength()
         {
             PopDataItem(CborMajorType.TextString);
-            // append break byte
-            EnsureWriteCapacity(1);
-            _buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte;
             AdvanceDataItemCounters();
         }
+
+        private void PatchIndefiniteLengthString(CborMajorType type)
+        {
+            Debug.Assert(type == CborMajorType.ByteString || type == CborMajorType.TextString);
+            Debug.Assert(_currentIndefiniteLengthStringRanges != null);
+
+            int currentOffset = _offset;
+
+            // calculate the definite length of the concatenated string
+            int definiteLength = 0;
+            foreach ((int _, int length) in _currentIndefiniteLengthStringRanges)
+            {
+                definiteLength += length;
+            }
+
+            // copy chunks to a temporary buffer
+            byte[] tempBuffer = s_bufferPool.Rent(definiteLength);
+            Span<byte> tempSpan = tempBuffer.AsSpan(0, definiteLength);
+
+            Span<byte> s = tempSpan;
+            foreach ((int offset, int length) in _currentIndefiniteLengthStringRanges)
+            {
+                _buffer.AsSpan(offset, length).CopyTo(s);
+                s = s.Slice(length);
+            }
+            Debug.Assert(s.IsEmpty);
+
+            // write back to the original buffer
+            _offset = _frameOffset - 1;
+            WriteUnsignedInteger(type, (ulong)definiteLength);
+            tempSpan.CopyTo(_buffer.AsSpan(_offset, definiteLength));
+            _offset += definiteLength;
+
+            // zero out excess bytes & other cleanups
+            _buffer.AsSpan(_offset, currentOffset - _offset).Fill(0);
+            s_bufferPool.Return(tempBuffer, clearArray: true);
+            _currentIndefiniteLengthStringRanges.Clear();
+        }
     }
 }
index 71f63cd..2bb7ca8 100644 (file)
@@ -5,6 +5,8 @@
 #nullable enable
 using System.Buffers;
 using System.Collections.Generic;
+using System.Diagnostics;
+using System.Drawing.Text;
 using System.Threading;
 
 namespace System.Security.Cryptography.Encoding.Tests.Cbor
@@ -17,30 +19,33 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         private byte[] _buffer = null!;
         private int _offset = 0;
 
+        private Stack<StackFrame>? _nestedDataItems;
+
         // remaining number of data items in current cbor context
         // with null representing indefinite length data items.
         // The root context ony permits one data item to be written.
-        private uint? _remainingDataItems = 1;
-        private Stack<StackFrame>? _nestedDataItems;
+        private int? _definiteLength = 1;
+        private int _itemsWritten = 0; // number of items written in the current context
         private int _frameOffset = 0; // buffer offset particular to the current data item context
         private bool _isTagContext = false; // true if writer is expecting a tagged value
-
         // Map-specific bookkeeping
         private int? _currentKeyOffset = null;
         private int? _currentValueOffset = null;
         private SortedSet<(int Offset, int KeyLength, int TotalLength)>? _keyValueEncodingRanges = null;
 
-        public CborWriter(CborConformanceLevel conformanceLevel = CborConformanceLevel.Lax)
+        public CborWriter(CborConformanceLevel conformanceLevel = CborConformanceLevel.Lax, bool patchIndefiniteLengthItems = false)
         {
             CborConformanceLevelHelpers.Validate(conformanceLevel);
 
             ConformanceLevel = conformanceLevel;
+            PatchIndefiniteLengthItems = patchIndefiniteLengthItems;
         }
 
+        public bool PatchIndefiniteLengthItems { get; }
         public CborConformanceLevel ConformanceLevel { get; }
         public int BytesWritten => _offset;
         // Returns true iff a complete CBOR document has been written to buffer
-        public bool IsWriteCompleted => _remainingDataItems == 0 && (_nestedDataItems?.Count ?? 0) == 0;
+        public bool IsWriteCompleted => (_nestedDataItems?.Count ?? 0) == 0 && _itemsWritten == _definiteLength;
 
         public void WriteEncodedValue(ReadOnlyMemory<byte> encodedValue)
         {
@@ -85,6 +90,35 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             }
         }
 
+        public byte[] GetEncoding() => GetSpanEncoding().ToArray();
+
+        public bool TryWriteEncoding(Span<byte> destination, out int bytesWritten)
+        {
+            ReadOnlySpan<byte> encoding = GetSpanEncoding();
+
+            if (encoding.Length > destination.Length)
+            {
+                bytesWritten = 0;
+                return false;
+            }
+
+            encoding.CopyTo(destination);
+            bytesWritten = encoding.Length;
+            return true;
+        }
+
+        private ReadOnlySpan<byte> GetSpanEncoding()
+        {
+            CheckDisposed();
+
+            if (!IsWriteCompleted)
+            {
+                throw new InvalidOperationException("Buffer contains incomplete CBOR document.");
+            }
+
+            return (_offset == 0) ? ReadOnlySpan<byte>.Empty : new ReadOnlySpan<byte>(_buffer, 0, _offset);
+        }
+
         private void EnsureWriteCapacity(int pendingCount)
         {
             CheckDisposed();
@@ -111,12 +145,13 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             }
         }
 
-        private void PushDataItem(CborMajorType type, uint? expectedNestedItems)
+        private void PushDataItem(CborMajorType type, int? definiteLength)
         {
             _nestedDataItems ??= new Stack<StackFrame>();
-            _nestedDataItems.Push(new StackFrame(type, _frameOffset, _remainingDataItems, _currentKeyOffset, _currentValueOffset, _keyValueEncodingRanges));
+            _nestedDataItems.Push(new StackFrame(type, _frameOffset, _definiteLength, _itemsWritten, _currentKeyOffset, _currentValueOffset, _keyValueEncodingRanges));
             _frameOffset = _offset;
-            _remainingDataItems = expectedNestedItems;
+            _definiteLength = definiteLength;
+            _itemsWritten = 0;
             _currentKeyOffset = (type == CborMajorType.Map) ? (int?)_offset : null;
             _currentValueOffset = null;
             _keyValueEncodingRanges = null;
@@ -124,9 +159,11 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
         private void PopDataItem(CborMajorType expectedType)
         {
+            // Validate that the pop operation can be performed
+
             if (_nestedDataItems is null || _nestedDataItems.Count == 0)
             {
-                throw new InvalidOperationException("No active CBOR nested data item to pop");
+                throw new InvalidOperationException("No active CBOR nested data item to pop.");
             }
 
             StackFrame frame = _nestedDataItems.Peek();
@@ -141,24 +178,39 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 throw new InvalidOperationException("Tagged CBOR value context is incomplete.");
             }
 
-            if (_remainingDataItems > 0)
+            if (_definiteLength - _itemsWritten > 0)
             {
                 throw new InvalidOperationException("Definite-length nested CBOR data item is incomplete.");
             }
 
+            // Perform encoding fixups that require the current context and must be done before popping
+            // NB key sorting must happen before indefinite-length patching
+
             if (expectedType == CborMajorType.Map && CborConformanceLevelHelpers.RequiresSortedKeys(ConformanceLevel))
             {
                 SortKeyValuePairEncodings();
             }
 
+            if (_definiteLength == null)
+            {
+                CompleteIndefiniteLengthCollection(expectedType);
+            }
+
+            // pop writer state
             _nestedDataItems.Pop();
             _frameOffset = frame.FrameOffset;
-            _remainingDataItems = frame.RemainingDataItems;
+            _definiteLength = frame.DefiniteLength;
+            _itemsWritten = frame.ItemsWritten;
             _currentKeyOffset = frame.CurrentKeyOffset;
             _currentValueOffset = frame.CurrentValueOffset;
             _keyValueEncodingRanges = frame.KeyValueEncodingRanges;
         }
 
+        private bool IsMajorTypeContext(CborMajorType type)
+        {
+            return _nestedDataItems?.Count > 0 && _nestedDataItems.Peek().MajorType == type;
+        }
+
         private void AdvanceDataItemCounters()
         {
             if (_currentKeyOffset != null) // this is a map context
@@ -173,13 +225,13 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 }
             }
 
-            _remainingDataItems--;
+            _itemsWritten++;
             _isTagContext = false;
         }
 
         private void WriteInitialByte(CborInitialByte initialByte)
         {
-            if (_remainingDataItems == 0)
+            if (_definiteLength - _itemsWritten == 0)
             {
                 throw new InvalidOperationException("Adding a CBOR data item to the current context exceeds its definite length.");
             }
@@ -225,44 +277,47 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             }
         }
 
-        public byte[] GetEncoding() => GetSpanEncoding().ToArray();
-
-        public bool TryWriteEncoding(Span<byte> destination, out int bytesWritten)
+        private void CompleteIndefiniteLengthCollection(CborMajorType type)
         {
-            ReadOnlySpan<byte> encoding = GetSpanEncoding();
+            Debug.Assert(_definiteLength == null);
 
-            if (encoding.Length > destination.Length)
+            if (PatchIndefiniteLengthItems)
             {
-                bytesWritten = 0;
-                return false;
+                switch (type)
+                {
+                    case CborMajorType.ByteString:
+                    case CborMajorType.TextString:
+                        PatchIndefiniteLengthString(type);
+                        break;
+                    case CborMajorType.Array:
+                        PatchIndefiniteLengthCollection(CborMajorType.Array, _itemsWritten);
+                        break;
+                    case CborMajorType.Map:
+                        Debug.Assert(_itemsWritten % 2 == 0);
+                        PatchIndefiniteLengthCollection(CborMajorType.Map, _itemsWritten / 2);
+                        break;
+                    default:
+                        Debug.Fail("Invalid CBOR major type pushed to stack.");
+                        throw new Exception("CborReader internal error. Invalid CBOR major type pushed to stack.");
+                }
             }
-
-            encoding.CopyTo(destination);
-            bytesWritten = encoding.Length;
-            return true;
-        }
-
-        private ReadOnlySpan<byte> GetSpanEncoding()
-        {
-            CheckDisposed();
-
-            if (!IsWriteCompleted)
+            else
             {
-                throw new InvalidOperationException("Buffer contains incomplete CBOR document.");
+                // no patching, so just append the break byte at the end of the existing encoding
+                EnsureWriteCapacity(1);
+                _buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte;
             }
-
-            return (_offset == 0) ? ReadOnlySpan<byte>.Empty : new ReadOnlySpan<byte>(_buffer, 0, _offset);
         }
 
         private readonly struct StackFrame
         {
-            public StackFrame(CborMajorType type, int frameOffset, uint? remainingDataItems,
-                              int? currentKeyOffset, int? currentValueOffset,
-                              SortedSet<(int, int, int)>? keyValueEncodingRanges)
+            public StackFrame(CborMajorType type, int frameOffset, int? definiteLength, int itemsWritten,
+                              int? currentKeyOffset, int? currentValueOffset, SortedSet<(int, int, int)>? keyValueEncodingRanges)
             {
                 MajorType = type;
                 FrameOffset = frameOffset;
-                RemainingDataItems = remainingDataItems;
+                DefiniteLength = definiteLength;
+                ItemsWritten = itemsWritten;
                 CurrentKeyOffset = currentKeyOffset;
                 CurrentValueOffset = currentValueOffset;
                 KeyValueEncodingRanges = keyValueEncodingRanges;
@@ -270,7 +325,8 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
             public CborMajorType MajorType { get; }
             public int FrameOffset { get; }
-            public uint? RemainingDataItems { get; }
+            public int? DefiniteLength { get; }
+            public int ItemsWritten { get; }
 
             public int? CurrentKeyOffset { get; }
             public int? CurrentValueOffset { get; }