From 435b9080120d648d4c4dd54f31be77ddbbb3c7af Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Tue, 21 Apr 2020 13:57:02 +0100 Subject: [PATCH] implement writing sorted keys in maps --- .../tests/Cbor.Tests/CborWriterTests.Map.cs | 94 ++++++++++- .../tests/Cbor/CborInitialByte.cs | 7 + .../tests/Cbor/CborWriter.Array.cs | 4 +- .../tests/Cbor/CborWriter.Map.cs | 181 ++++++++++++++++++++- .../tests/Cbor/CborWriter.String.cs | 4 +- .../tests/Cbor/CborWriter.cs | 77 +++++++-- 6 files changed, 345 insertions(+), 22 deletions(-) diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Map.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Map.cs index a23ddcb..915ff78 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Map.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Map.cs @@ -86,6 +86,82 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor } [Theory] + [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, 3, 3, 2, 2, 1, 1 }, "a3030302020101")] + [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, 3, 3, 2, 2, 1, 1 }, "a3010102020303")] + [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, 3, 3, 2, 2, 1, 1 }, "a3010102020303")] + // indefinite length string payload + [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, "b", 0, 2, 0, "a", 0, new string[] { "c", "" }, 0, 1, 0 }, "a561620002006161007f616360ff000100")] + [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, "b", 0, 2, 0, "a", 0, new string[] { "c", "" }, 0, 1, 0 }, "a5010002006161006162007f616360ff00")] + [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, "b", 0, 2, 0, "a", 0, new string[] { "c", "" }, 0, 1, 0 }, "a5010002006161006162007f616360ff00")] + // CBOR sorting rules do not match canonical string sorting + [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, "aa", 0, "z", 0 }, "a262616100617a00")] + [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, "aa", 0, "z", 0 }, "a2617a0062616100")] + [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, "aa", 0, "z", 0 }, "a2617a0062616100")] + // Test case distinguishing between RFC7049 and CTAP2 sorting rules + [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, "", 0, 255, 0 }, "a2600018ff00")] + [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, "", 0, 255, 0 }, "a2600018ff00")] + [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, "", 0, 255, 0 }, "a218ff006000")] + internal static void WriteMap_SimpleValues_ShouldSortKeysAccordingToConformanceLevel(CborConformanceLevel level, object value, string expectedHexEncoding) + { + byte[] expectedEncoding = expectedHexEncoding.HexToByteArray(); + using var writer = new CborWriter(level); + Helpers.WriteValue(writer, value); + byte[] actualEncoding = writer.ToArray(); + AssertHelper.HexEqual(expectedEncoding, actualEncoding); + } + + [Theory] + [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, -1, 0, new object[] { Map, 3, 3, 2, 2, 1, 1 }, 0, "a", 0, 256, 0 }, "a42000a30303020201010061610019010000")] + [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, -1, 0, new object[] { Map, 3, 3, 2, 2, 1, 1 }, 0, "a", 0, 256, 0 }, "a4200061610019010000a301010202030300")] + [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, -1, 0, new object[] { Map, 3, 3, 2, 2, 1, 1 }, 0, "a", 0, 256, 0 }, "a4190100002000616100a301010202030300")] + internal static void WriteMap_NestedValues_ShouldSortKeysAccordingToConformanceLevel(CborConformanceLevel level, object value, string expectedHexEncoding) + { + byte[] expectedEncoding = expectedHexEncoding.HexToByteArray(); + using var writer = new CborWriter(level); + Helpers.WriteValue(writer, value); + byte[] actualEncoding = writer.ToArray(); + AssertHelper.HexEqual(expectedEncoding, actualEncoding); + } + + [Theory] + [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, 3, 3, 2, 2, 1, 1 }, "bf030302020101ff")] + [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, 3, 3, 2, 2, 1, 1 }, "bf010102020303ff")] + [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, 3, 3, 2, 2, 1, 1 }, "bf010102020303ff")] + // indefinite length string payload + [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, "b", 0, 2, 0, "a", 0, new string[] { "c", "" }, 0, 1, 0 }, "bf61620002006161007f616360ff000100ff")] + [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, "b", 0, 2, 0, "a", 0, new string[] { "c", "" }, 0, 1, 0 }, "bf010002006161006162007f616360ff00ff")] + [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, "b", 0, 2, 0, "a", 0, new string[] { "c", "" }, 0, 1, 0 }, "bf010002006161006162007f616360ff00ff")] + // CBOR sorting rules do not match canonical string sorting + [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, "aa", 0, "z", 0 }, "bf62616100617a00ff")] + [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, "aa", 0, "z", 0 }, "bf617a0062616100ff")] + [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, "aa", 0, "z", 0 }, "bf617a0062616100ff")] + // Test case distinguishing between RFC7049 and CTAP2 sorting rules + [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, "", 0, 255, 0 }, "bf600018ff00ff")] + [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, "", 0, 255, 0 }, "bf600018ff00ff")] + [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, "", 0, 255, 0 }, "bf18ff006000ff")] + internal static void WriteMap_SimpleValues_IndefiniteLength_ShouldSortKeysAccordingToConformanceLevel(CborConformanceLevel level, object value, string expectedHexEncoding) + { + byte[] expectedEncoding = expectedHexEncoding.HexToByteArray(); + using var writer = new CborWriter(level); + Helpers.WriteValue(writer, value, useDefiniteLengthCollections: false); + byte[] actualEncoding = writer.ToArray(); + AssertHelper.HexEqual(expectedEncoding, actualEncoding); + } + + [Theory] + [InlineData(CborConformanceLevel.NoConformance, new object[] { Map, -1, 0, new object[] { Map, 3, 3, 2, 2, 1, 1 }, 0, "a", 0, 256, 0 }, "bf2000bf030302020101ff0061610019010000ff")] + [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { Map, -1, 0, new object[] { Map, 3, 3, 2, 2, 1, 1 }, 0, "a", 0, 256, 0 }, "bf200061610019010000bf010102020303ff00ff")] + [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { Map, -1, 0, new object[] { Map, 3, 3, 2, 2, 1, 1 }, 0, "a", 0, 256, 0 }, "bf190100002000616100bf010102020303ff00ff")] + internal static void WriteMap_NestedValues_IndefiniteLength_ShouldSortKeysAccordingToConformanceLevel(CborConformanceLevel level, object value, string expectedHexEncoding) + { + byte[] expectedEncoding = expectedHexEncoding.HexToByteArray(); + using var writer = new CborWriter(level); + Helpers.WriteValue(writer, value, useDefiniteLengthCollections: false); + byte[] actualEncoding = writer.ToArray(); + AssertHelper.HexEqual(expectedEncoding, actualEncoding); + } + + [Theory] [InlineData(new object[] { Map, "a", 1, "a", 2 }, "a2616101616102")] public static void WriteMap_DuplicateKeys_ShouldSucceed(object[] values, string expectedHexEncoding) { @@ -97,6 +173,22 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor } [Theory] + [InlineData(CborConformanceLevel.Rfc7049Canonical, 42)] + [InlineData(CborConformanceLevel.Ctap2Canonical, 42)] + [InlineData(CborConformanceLevel.Rfc7049Canonical, "foobar")] + [InlineData(CborConformanceLevel.Ctap2Canonical, "foobar")] + [InlineData(CborConformanceLevel.Rfc7049Canonical, new object[] { new string[] { "x", "y" } })] + [InlineData(CborConformanceLevel.Ctap2Canonical, new object[] { new string[] { "x", "y" } })] + internal static void WriteMap_DuplicateKeys_StrictConformance_ShouldFail(CborConformanceLevel level, object dupeKey) + { + using var writer = new CborWriter(level); + writer.WriteStartMap(2); + Helpers.WriteValue(writer, dupeKey); + writer.WriteInt32(0); + Assert.Throws(() => Helpers.WriteValue(writer, dupeKey)); + } + + [Theory] [InlineData(0)] [InlineData(1)] [InlineData(3)] @@ -176,7 +268,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor [InlineData(0)] [InlineData(3)] [InlineData(10)] - public static void EndWriteMap_IndefiniteLength_EvenItems_ShouldThrowInvalidOperationException(int length) + public static void EndWriteMap_IndefiniteLength_OddItems_ShouldThrowInvalidOperationException(int length) { using var writer = new CborWriter(); writer.WriteStartMapIndefiniteLength(); diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborInitialByte.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborInitialByte.cs index 185097e..40a8432 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborInitialByte.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborInitialByte.cs @@ -6,6 +6,13 @@ using System.Diagnostics; namespace System.Security.Cryptography.Encoding.Tests.Cbor { + internal enum CborConformanceLevel + { + NoConformance = 0, + Rfc7049Canonical = 1, + Ctap2Canonical = 2, + } + internal enum CborMajorType : byte { UnsignedInteger = 0, diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Array.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Array.cs index 39b54f2..4ef072b 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Array.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Array.cs @@ -18,7 +18,6 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor } WriteUnsignedInteger(CborMajorType.Array, (ulong)definiteLength); - AdvanceDataItemCounters(); PushDataItem(CborMajorType.Array, (uint)definiteLength); } @@ -33,13 +32,14 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor EnsureWriteCapacity(1); _buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte; } + + AdvanceDataItemCounters(); } public void WriteStartArrayIndefiniteLength() { EnsureWriteCapacity(1); WriteInitialByte(new CborInitialByte(CborMajorType.Array, CborAdditionalInfo.IndefiniteLength)); - AdvanceDataItemCounters(); PushDataItem(CborMajorType.Array, expectedNestedItems: null); } } diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Map.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Map.cs index 2ebe219..fe80513 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Map.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.Map.cs @@ -3,12 +3,16 @@ // See the LICENSE file in the project root for more information. #nullable enable -using System.Text; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; namespace System.Security.Cryptography.Encoding.Tests.Cbor { internal partial class CborWriter { + private KeyEncodingComparer? _comparer; + public void WriteStartMap(int definiteLength) { if (definiteLength < 0) @@ -17,13 +21,12 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor } WriteUnsignedInteger(CborMajorType.Map, (ulong)definiteLength); - AdvanceDataItemCounters(); PushDataItem(CborMajorType.Map, 2 * (uint)definiteLength); } public void WriteEndMap() { - if (!_isEvenNumberOfDataItemsWritten) + if (_currentValueOffset != null) { throw new InvalidOperationException("CBOR Map types require an even number of key/value combinations"); } @@ -38,14 +41,184 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor EnsureWriteCapacity(1); _buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte; } + + AdvanceDataItemCounters(); } public void WriteStartMapIndefiniteLength() { EnsureWriteCapacity(1); WriteInitialByte(new CborInitialByte(CborMajorType.Map, CborAdditionalInfo.IndefiniteLength)); - AdvanceDataItemCounters(); PushDataItem(CborMajorType.Map, expectedNestedItems: null); + _currentKeyOffset = _offset; + _currentValueOffset = null; + } + + // + // Map encoding conformance + // + + private bool ConformanceRequiresSortedKeys() + { + return ConformanceLevel switch + { + CborConformanceLevel.Rfc7049Canonical => true, + CborConformanceLevel.Ctap2Canonical => true, + CborConformanceLevel.NoConformance => false, + _ => false, + }; + } + + private SortedSet<(int offset, int keyLength, int keyValueLength)> GetKeyValueEncodingRanges() + { + // TODO consider pooling set allocations? + + if (_keyValueEncodingRanges == null) + { + _comparer ??= new KeyEncodingComparer(this); + return _keyValueEncodingRanges = new SortedSet<(int offset, int keyLength, int keyValueLength)>(_comparer); + } + + return _keyValueEncodingRanges; + } + + private void HandleKeyWritten() + { + Debug.Assert(_currentKeyOffset != null && _currentValueOffset == null); + + _currentValueOffset = _offset; + + if (ConformanceRequiresSortedKeys()) + { + // check for key uniqueness + SortedSet<(int offset, int keyLength, int keyValueLength)> ranges = GetKeyValueEncodingRanges(); + + (int offset, int keyLength, int valueLength) currentKeyRange = + (_currentKeyOffset.Value, + _currentValueOffset.Value - _currentKeyOffset.Value, + 0); + + if (ranges.Contains(currentKeyRange)) + { + // TODO: check if rollback is necessary here + throw new InvalidOperationException("Duplicate key encoding in CBOR map."); + } + } + } + + private void HandleValueWritten() + { + Debug.Assert(_currentKeyOffset != null && _currentValueOffset != null); + + if (ConformanceRequiresSortedKeys()) + { + Debug.Assert(_keyValueEncodingRanges != null); + + (int offset, int keyLength, int keyValueLength) currentKeyRange = + (_currentKeyOffset.Value, + _currentValueOffset.Value - _currentKeyOffset.Value, + _offset - _currentKeyOffset.Value); + + _keyValueEncodingRanges.Add(currentKeyRange); + } + + // reset state + _currentKeyOffset = _offset; + _currentValueOffset = null; + } + + private void SortKeyValuePairEncodings() + { + if (_keyValueEncodingRanges == null) + { + return; + } + + int totalMapPayloadEncodingLength = _offset - _frameOffset; + byte[] tempBuffer = s_bufferPool.Rent(totalMapPayloadEncodingLength); + Span tmpSpan = tempBuffer.AsSpan(0, totalMapPayloadEncodingLength); + + // copy sorted ranges to temporary buffer + Span s = tmpSpan; + foreach((int, int, int) range in _keyValueEncodingRanges) + { + ReadOnlySpan kvEnc = GetKeyValueEncoding(range); + kvEnc.CopyTo(s); + s = s.Slice(kvEnc.Length); + } + Debug.Assert(s.IsEmpty); + + // now copy back to the original buffer segment + tmpSpan.CopyTo(_buffer.AsSpan(_frameOffset, totalMapPayloadEncodingLength)); + + s_bufferPool.Return(tempBuffer, clearArray: true); + } + + private ReadOnlySpan GetKeyEncoding((int offset, int keyLength, int valueLength) keyValueRange) + { + return _buffer.AsSpan(keyValueRange.offset, keyValueRange.keyLength); + } + + private ReadOnlySpan GetKeyValueEncoding((int offset, int keyLength, int keyValueLength) keyValueRange) + { + return _buffer.AsSpan(keyValueRange.offset, keyValueRange.keyValueLength); + } + + private static int CompareEncodings(ReadOnlySpan left, ReadOnlySpan right, CborConformanceLevel level) + { + Debug.Assert(!left.IsEmpty && !right.IsEmpty); + + switch (level) + { + case CborConformanceLevel.Rfc7049Canonical: + // Implements key sorting according to + // https://tools.ietf.org/html/rfc7049#section-3.9 + + if (left.Length != right.Length) + { + return left.Length - right.Length; + } + + return left.SequenceCompareTo(right); + + case CborConformanceLevel.Ctap2Canonical: + // Implements key sorting according to + // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#message-encoding + + int leftMt = (int)new CborInitialByte(left[0]).MajorType; + int rightMt = (int)new CborInitialByte(right[0]).MajorType; + + if (leftMt != rightMt) + { + return leftMt - rightMt; + } + + if (left.Length != right.Length) + { + return left.Length - right.Length; + } + + return left.SequenceCompareTo(right); + + default: + Debug.Fail("Invalid conformance level used in encoding sort."); + throw new Exception("Invalid conformance level used in encoding sort."); + } + } + + private class KeyEncodingComparer : IComparer<(int, int, int)> + { + private readonly CborWriter _writer; + + public KeyEncodingComparer(CborWriter writer) + { + _writer = writer; + } + + public int Compare((int, int, int) x, (int, int, int) y) + { + return CompareEncodings(_writer.GetKeyEncoding(x), _writer.GetKeyEncoding(y), _writer.ConformanceLevel); + } } } } diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.String.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.String.cs index 733b281..2a36f37 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.String.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.String.cs @@ -46,7 +46,6 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor { EnsureWriteCapacity(1); WriteInitialByte(new CborInitialByte(CborMajorType.ByteString, CborAdditionalInfo.IndefiniteLength)); - AdvanceDataItemCounters(); PushDataItem(CborMajorType.ByteString, expectedNestedItems: null); } @@ -56,13 +55,13 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor // append break byte EnsureWriteCapacity(1); _buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte; + AdvanceDataItemCounters(); } public void WriteStartTextStringIndefiniteLength() { EnsureWriteCapacity(1); WriteInitialByte(new CborInitialByte(CborMajorType.TextString, CborAdditionalInfo.IndefiniteLength)); - AdvanceDataItemCounters(); PushDataItem(CborMajorType.TextString, expectedNestedItems: null); } @@ -72,6 +71,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor // append break byte EnsureWriteCapacity(1); _buffer[_offset++] = CborInitialByte.IndefiniteLengthBreakByte; + AdvanceDataItemCounters(); } } } diff --git a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.cs b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.cs index ff2d81f..2fd2e50 100644 --- a/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.cs +++ b/src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.cs @@ -21,15 +21,21 @@ 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 bool _isEvenNumberOfDataItemsWritten = true; // required for indefinite-length map writes - private Stack<(CborMajorType type, bool isEvenNumberOfDataItemsWritten, uint? remainingDataItems)>? _nestedDataItemStack; + private Stack? _nestedDataItemStack; + 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 - public CborWriter() - { + // Map-specific bookkeeping + private int? _currentKeyOffset = null; + private int? _currentValueOffset = null; + private SortedSet<(int offset, int keyLength, int keyValueLength)>? _keyValueEncodingRanges = null; + public CborWriter(CborConformanceLevel conformanceLevel = CborConformanceLevel.NoConformance) + { + ConformanceLevel = conformanceLevel; } + 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 && (_nestedDataItemStack?.Count ?? 0) == 0; @@ -105,10 +111,13 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor private void PushDataItem(CborMajorType type, uint? expectedNestedItems) { - _nestedDataItemStack ??= new Stack<(CborMajorType, bool, uint?)>(); - _nestedDataItemStack.Push((type, _isEvenNumberOfDataItemsWritten, _remainingDataItems)); + _nestedDataItemStack ??= new Stack(); + _nestedDataItemStack.Push(new StackFrame(type, _frameOffset, _remainingDataItems, _currentKeyOffset, _currentValueOffset, _keyValueEncodingRanges)); + _frameOffset = _offset; _remainingDataItems = expectedNestedItems; - _isEvenNumberOfDataItemsWritten = true; + _currentKeyOffset = (type == CborMajorType.Map) ? new int?(_offset) : null; + _currentValueOffset = null; + _keyValueEncodingRanges = null; } private void PopDataItem(CborMajorType expectedType) @@ -118,9 +127,9 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor throw new InvalidOperationException("No active CBOR nested data item to pop"); } - (CborMajorType actualType, bool isEvenNumberOfDataItemsWritten, uint? remainingItems) = _nestedDataItemStack.Peek(); + StackFrame frame = _nestedDataItemStack.Peek(); - if (expectedType != actualType) + if (expectedType != frame.MajorType) { throw new InvalidOperationException("Unexpected major type in nested CBOR data item."); } @@ -135,16 +144,35 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor throw new InvalidOperationException("Definite-length nested CBOR data item is incomplete."); } + if (expectedType == CborMajorType.Map && ConformanceRequiresSortedKeys()) + { + SortKeyValuePairEncodings(); + } + _nestedDataItemStack.Pop(); - _remainingDataItems = remainingItems; - _isEvenNumberOfDataItemsWritten = isEvenNumberOfDataItemsWritten; + _frameOffset = frame.FrameOffset; + _remainingDataItems = frame.RemainingDataItems; + _currentKeyOffset = frame.CurrentKeyOffset; + _currentValueOffset = frame.CurrentValueOffset; + _keyValueEncodingRanges = frame.KeyValueEncodingRanges; } private void AdvanceDataItemCounters() { _remainingDataItems--; _isTagContext = false; - _isEvenNumberOfDataItemsWritten = !_isEvenNumberOfDataItemsWritten; + + if (_currentKeyOffset != null) // this is a map context + { + if (_currentValueOffset == null) + { + HandleKeyWritten(); + } + else + { + HandleValueWritten(); + } + } } private void WriteInitialByte(CborInitialByte initialByte) @@ -156,7 +184,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor if (_nestedDataItemStack != null && _nestedDataItemStack.Count > 0) { - CborMajorType parentType = _nestedDataItemStack.Peek().type; + CborMajorType parentType = _nestedDataItemStack.Peek().MajorType; switch (parentType) { @@ -206,5 +234,28 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor return (_offset == 0) ? Array.Empty() : _buffer.AsSpan(0, _offset).ToArray(); } + + private readonly struct StackFrame + { + public StackFrame(CborMajorType type, int frameOffset, uint? remainingDataItems, + int? currentKeyOffset, int? currentValueOffset, + SortedSet<(int, int, int)>? keyValueEncodingRanges) + { + MajorType = type; + FrameOffset = frameOffset; + RemainingDataItems = remainingDataItems; + CurrentKeyOffset = currentKeyOffset; + CurrentValueOffset = currentValueOffset; + KeyValueEncodingRanges = keyValueEncodingRanges; + } + + public CborMajorType MajorType { get; } + public int FrameOffset { get; } + public uint? RemainingDataItems { get; } + + public int? CurrentKeyOffset { get; } + public int? CurrentValueOffset { get; } + public SortedSet<(int, int, int)>? KeyValueEncodingRanges { get; } + } } } -- 2.7.4