}
[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)]
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)
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)
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):
// 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")]
}
[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")]
[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();
}
[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")]
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()
{
[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();
[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();
}
[Fact]
- public static void ToArray_OnInCompleteValue_ShouldThrowInvalidOperationExceptoin()
+ public static void GetEncoding_OnInCompleteValue_ShouldThrowInvalidOperationExceptoin()
{
using var writer = new CborWriter();
Assert.Throws<InvalidOperationException>(() => writer.GetEncoding());
{
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]
#nullable enable
using System.Buffers.Binary;
+using System.Diagnostics;
using System.Text;
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;
}
}
}
// 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));
_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;
+ }
+ }
}
}
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;
}
#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
{
{
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();
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();
+ }
}
}
#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
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)
{
}
}
+ 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();
}
}
- 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;
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();
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
}
}
- _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.");
}
}
}
- 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;
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; }