implement writing and reading multiple root-level values
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Thu, 30 Apr 2020 18:32:22 +0000 (19:32 +0100)
committerEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Thu, 30 Apr 2020 18:32:22 +0000 (19:32 +0100)
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Array.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.Map.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.cs

index 9b2df9d..e61bda7 100644 (file)
@@ -75,19 +75,69 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         }
 
         [Fact]
-        public static void CborReader_ReadingTwoPrimitiveValues_ShouldThrowInvalidOperationException()
+        public static void Read_EmptyBuffer_ShouldThrowFormatException()
+        {
+            var reader = new CborReader(ReadOnlyMemory<byte>.Empty);
+            Assert.Throws<FormatException>(() => reader.ReadInt64());
+        }
+
+        [Fact]
+        public static void Read_BeyondEndOfFirstValue_ShouldThrowInvalidOperationException()
+        {
+            var reader = new CborReader("01".HexToByteArray());
+            reader.ReadInt64();
+            Assert.Equal(CborReaderState.Finished, reader.PeekState());
+            Assert.Throws<InvalidOperationException>(() => reader.ReadInt64());
+        }
+
+        [Fact]
+        public static void CborReader_ReadingTwoRootLevelValues_ShouldThrowInvalidOperationException()
         {
             ReadOnlyMemory<byte> buffer = new byte[] { 0, 0 };
             var reader = new CborReader(buffer);
             reader.ReadInt64();
-            Assert.Equal(CborReaderState.Finished, reader.PeekState());
 
             int bytesRemaining = reader.BytesRemaining;
+            Assert.Equal(CborReaderState.FinishedWithTrailingBytes, reader.PeekState());
             Assert.Throws<InvalidOperationException>(() => reader.ReadInt64());
             Assert.Equal(bytesRemaining, reader.BytesRemaining);
         }
 
         [Theory]
+        [InlineData(1, 2, "0101")]
+        [InlineData(10, 10, "0a0a0a0a0a0a0a0a0a0a")]
+        [InlineData(new object[] { 1, 2 }, 3, "820102820102820102")]
+        public static void CborReader_MultipleRootValuesAllowed_ReadingMultipleValues_HappyPath(object expectedValue, int repetitions, string hexEncoding)
+        {
+            var reader = new CborReader(hexEncoding.HexToByteArray(), allowMultipleRootLevelValues: true);
+
+            for (int i = 0; i < repetitions; i++)
+            {
+                Helpers.VerifyValue(reader, expectedValue);
+            }
+
+            Assert.Equal(CborReaderState.Finished, reader.PeekState());
+        }
+
+        [Fact]
+        public static void CborReader_MultipleRootValuesAllowed_ReadingBeyondEndOfBuffer_ShouldThrowInvalidOperationException()
+        {
+            string hexEncoding = "810102";
+            var reader = new CborReader(hexEncoding.HexToByteArray(), allowMultipleRootLevelValues: true);
+
+            Assert.Equal(CborReaderState.StartArray, reader.PeekState());
+            reader.ReadStartArray();
+            reader.ReadInt32();
+            reader.ReadEndArray();
+
+            Assert.Equal(CborReaderState.UnsignedInteger, reader.PeekState());
+            reader.ReadInt32();
+
+            Assert.Equal(CborReaderState.Finished, reader.PeekState());
+            Assert.Throws<InvalidOperationException>(() => reader.ReadInt32());
+        }
+
+        [Theory]
         [MemberData(nameof(EncodedValueInputs))]
         public static void ReadEncodedValue_RootValue_HappyPath(string hexEncoding)
         {
index ecfa188..e661a59 100644 (file)
@@ -39,6 +39,38 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             Assert.Equal(bytesWritten, writer.BytesWritten);
         }
 
+        [Theory]
+        [InlineData(1, 2, "0101")]
+        [InlineData(10, 10, "0a0a0a0a0a0a0a0a0a0a")]
+        [InlineData(new object[] { 1, 2 }, 3, "820102820102820102")]
+        public static void CborWriter_MultipleRootLevelValuesAllowed_WritingMultipleRootValues_HappyPath(object value, int repetitions, string expectedHexEncoding)
+        {
+            byte[] expectedEncoding = expectedHexEncoding.HexToByteArray();
+            using var writer = new CborWriter(allowMultipleRootLevelValues: true);
+
+            for (int i = 0; i < repetitions; i++)
+            {
+                Helpers.WriteValue(writer, value);
+            }
+
+            AssertHelper.HexEqual(expectedEncoding, writer.GetEncoding());
+        }
+
+        [Fact]
+        public static void GetEncoding_MultipleRootLevelValuesAllowed_PartialRootValue_ShouldThrowInvalidOperationException()
+        {
+            using var writer = new CborWriter(allowMultipleRootLevelValues: true);
+
+            writer.WriteStartArray(1);
+            writer.WriteDouble(3.14);
+            writer.WriteEndArray();
+            writer.WriteStartArray(1);
+            writer.WriteDouble(3.14);
+            // misses writer.WriteEndArray();
+
+            Assert.Throws<InvalidOperationException>(() => writer.GetEncoding());
+        }
+
         [Fact]
         public static void BytesWritten_SingleValue_ShouldReturnBytesWritten()
         {
index 0c36b98..5fa3f11 100644 (file)
@@ -34,7 +34,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 }
 
                 AdvanceBuffer(1 + additionalBytes);
-                PushDataItem(CborMajorType.Array, (uint)arrayLength);
+                PushDataItem(CborMajorType.Array, (int)arrayLength);
                 return (int)arrayLength;
             }
         }
index 61ae773..608a6ce 100644 (file)
@@ -40,7 +40,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 }
 
                 AdvanceBuffer(1 + additionalBytes);
-                PushDataItem(CborMajorType.Map, 2 * (uint)mapSize);
+                PushDataItem(CborMajorType.Map, 2 * (int)mapSize);
                 return (int)mapSize;
             }
         }
index 6b73f94..791b2ee 100644 (file)
@@ -6,7 +6,6 @@
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
-using System.Threading;
 
 namespace System.Security.Cryptography.Encoding.Tests.Cbor
 {
@@ -33,6 +32,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         DoublePrecisionFloat,
         SpecialValue,
         Finished,
+        FinishedWithTrailingBytes,
         EndOfData,
         FormatError,
     }
@@ -43,11 +43,8 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         private ReadOnlyMemory<byte> _buffer;
         private int _bytesRead = 0;
 
-        // 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 read.
-        private uint? _remainingDataItems = 1;
         private Stack<StackFrame>? _nestedDataItems;
+        private int? _remainingDataItems; // remaining data items to read if context is definite-length collection
         private int _frameOffset = 0; // buffer offset particular to the current data item context
         private bool _isTagContext = false; // true if reader is expecting a tagged value
 
@@ -60,16 +57,19 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         // keeps a cached copy of the reader state; 'Unknown' denotes uncomputed state
         private CborReaderState _cachedState = CborReaderState.Unknown;
 
-        internal CborReader(ReadOnlyMemory<byte> buffer, CborConformanceLevel conformanceLevel = CborConformanceLevel.Lax)
+        internal CborReader(ReadOnlyMemory<byte> buffer, CborConformanceLevel conformanceLevel = CborConformanceLevel.Lax, bool allowMultipleRootLevelValues = false)
         {
             CborConformanceLevelHelpers.Validate(conformanceLevel);
 
             _originalBuffer = buffer;
             _buffer = buffer;
             ConformanceLevel = conformanceLevel;
+            AllowMultipleRootLevelValues = allowMultipleRootLevelValues;
+            _remainingDataItems = allowMultipleRootLevelValues ? null : (int?)1;
         }
 
         public CborConformanceLevel ConformanceLevel { get; }
+        public bool AllowMultipleRootLevelValues { get; }
         public int BytesRead => _bytesRead;
         public int BytesRemaining => _buffer.Length;
 
@@ -89,21 +89,31 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             {
                 if (_nestedDataItems?.Count > 0)
                 {
-                    return _nestedDataItems.Peek().MajorType switch
+                    // is at the end of a definite-length collection
+                    switch (_nestedDataItems.Peek().MajorType)
                     {
-                        CborMajorType.Array => CborReaderState.EndArray,
-                        CborMajorType.Map => CborReaderState.EndMap,
-                        _ => throw new Exception("CborReader internal error. Invalid CBOR major type pushed to stack."),
+                        case CborMajorType.Array: return CborReaderState.EndArray;
+                        case CborMajorType.Map: return CborReaderState.EndMap;
+                        default:
+                            Debug.Fail("CborReader internal error. Invalid CBOR major type pushed to stack.");
+                            throw new Exception("CborReader internal error. Invalid CBOR major type pushed to stack.");
                     };
                 }
                 else
                 {
-                    return CborReaderState.Finished;
+                    // is at the end of the root value
+                    return _buffer.IsEmpty ? CborReaderState.Finished : CborReaderState.FinishedWithTrailingBytes;
                 }
             }
 
             if (_buffer.IsEmpty)
             {
+                if (_remainingDataItems is null && (_nestedDataItems?.Count ?? 0) == 0)
+                {
+                    // is at the end of a well-defined sequence of root-level values
+                    return CborReaderState.Finished;
+                }
+
                 return CborReaderState.EndOfData;
             }
 
@@ -117,19 +127,21 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                     return CborReaderState.FormatError;
                 }
 
-                if (_remainingDataItems == null)
+                if (_remainingDataItems is null)
                 {
                     // stack guaranteed to be populated since root context cannot be indefinite-length
                     Debug.Assert(_nestedDataItems != null && _nestedDataItems.Count > 0);
 
-                    return _nestedDataItems.Peek().MajorType switch
+                    switch (_nestedDataItems.Peek().MajorType)
                     {
-                        CborMajorType.ByteString => CborReaderState.EndByteString,
-                        CborMajorType.TextString => CborReaderState.EndTextString,
-                        CborMajorType.Array => CborReaderState.EndArray,
-                        CborMajorType.Map when !_curentItemIsKey => CborReaderState.FormatError,
-                        CborMajorType.Map => CborReaderState.EndMap,
-                        _ => throw new Exception("CborReader internal error. Invalid CBOR major type pushed to stack."),
+                        case CborMajorType.ByteString: return CborReaderState.EndByteString;
+                        case CborMajorType.TextString: return CborReaderState.EndTextString;
+                        case CborMajorType.Array: return CborReaderState.EndArray;
+                        case CborMajorType.Map when !_curentItemIsKey: return CborReaderState.FormatError;
+                        case CborMajorType.Map: return CborReaderState.EndMap;
+                        default:
+                            Debug.Fail("CborReader internal error. Invalid CBOR major type pushed to stack.");
+                            throw new Exception("CborReader internal error. Invalid CBOR major type pushed to stack.");
                     };
                 }
                 else
@@ -138,11 +150,8 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 }
             }
 
-            if (_remainingDataItems == null)
+            if (_remainingDataItems is null && _nestedDataItems?.Count > 0)
             {
-                // stack guaranteed to be populated since root context cannot be indefinite-length
-                Debug.Assert(_nestedDataItems != null && _nestedDataItems.Count > 0);
-
                 CborMajorType parentType = _nestedDataItems.Peek().MajorType;
 
                 switch (parentType)
@@ -159,19 +168,27 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 }
             }
 
-            return initialByte.MajorType switch
+            switch (initialByte.MajorType)
             {
-                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.Simple => MapSpecialValueTagToReaderState(initialByte.AdditionalInfo),
-                _ => throw new Exception("CborReader internal error. Invalid major type."),
+                case CborMajorType.UnsignedInteger: return CborReaderState.UnsignedInteger;
+                case CborMajorType.NegativeInteger: return CborReaderState.NegativeInteger;
+                case CborMajorType.ByteString:
+                    return (initialByte.AdditionalInfo == CborAdditionalInfo.IndefiniteLength) ?
+                            CborReaderState.StartByteString :
+                            CborReaderState.ByteString;
+
+                case CborMajorType.TextString:
+                    return (initialByte.AdditionalInfo == CborAdditionalInfo.IndefiniteLength) ?
+                            CborReaderState.StartTextString :
+                            CborReaderState.TextString;
+
+                case CborMajorType.Array: return CborReaderState.StartArray;
+                case CborMajorType.Map: return CborReaderState.StartMap;
+                case CborMajorType.Tag: return CborReaderState.Tag;
+                case CborMajorType.Simple: return MapSpecialValueTagToReaderState(initialByte.AdditionalInfo);
+                default:
+                    Debug.Fail("CborReader internal error. Invalid major type.");
+                    throw new Exception("CborReader internal error. Invalid major type.");
             };
 
             static CborReaderState MapSpecialValueTagToReaderState (CborAdditionalInfo value)
@@ -219,7 +236,12 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
             if (_buffer.IsEmpty)
             {
-                throw new FormatException("unexpected end of buffer.");
+                if (_remainingDataItems is null && _bytesRead > 0 && (_nestedDataItems?.Count ?? 0) == 0)
+                {
+                    throw new InvalidOperationException("No remaining root-level CBOR data items in the buffer.");
+                }
+
+                throw new FormatException("Unexpected end of buffer.");
             }
 
             var result = new CborInitialByte(_buffer.Span[0]);
@@ -269,7 +291,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             }
         }
 
-        private void PushDataItem(CborMajorType type, uint? expectedNestedItems)
+        private void PushDataItem(CborMajorType type, int? expectedNestedItems)
         {
             var frame = new StackFrame(
                 type: type,
@@ -284,7 +306,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             _nestedDataItems ??= new Stack<StackFrame>();
             _nestedDataItems.Push(frame);
 
-            _remainingDataItems = checked((uint?)expectedNestedItems);
+            _remainingDataItems = expectedNestedItems;
             _frameOffset = _bytesRead;
             _isTagContext = false;
             _currentKeyOffset = (type == CborMajorType.Map) ? (int?)_bytesRead : null;
@@ -389,7 +411,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
         private readonly struct StackFrame
         {
-            public StackFrame(CborMajorType type, int frameOffset, uint? remainingDataItems,
+            public StackFrame(CborMajorType type, int frameOffset, int? remainingDataItems,
                               int? currentKeyOffset, bool currentItemIsKey,
                               (int Offset, int Length)? previousKeyRange, HashSet<(int Offset, int Length)>? previousKeyRanges)
             {
@@ -405,7 +427,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
             public CborMajorType MajorType { get; }
             public int FrameOffset { get; }
-            public uint? RemainingDataItems { get; }
+            public int? RemainingDataItems { get; }
 
             public int? CurrentKeyOffset { get; }
             public bool CurrentItemIsKey { get; }
@@ -418,7 +440,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         // reader is within the original context in which the checkpoint was created
         private readonly struct Checkpoint
         {
-            public Checkpoint(int bytesRead, int stackDepth, int frameOffset, uint? remainingDataItems,
+            public Checkpoint(int bytesRead, int stackDepth, int frameOffset, int? remainingDataItems,
                               int? currentKeyOffset, bool currentItemIsKey, (int Offset, int Length)? previousKeyRange,
                               HashSet<(int Offset, int Length)>? previousKeyRanges)
             {
@@ -436,7 +458,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             public int BytesRead { get; }
             public int StackDepth { get; }
             public int FrameOffset { get; }
-            public uint? RemainingDataItems { get; }
+            public int? RemainingDataItems { get; }
 
             public int? CurrentKeyOffset { get; }
             public bool CurrentItemIsKey { get; }
index a9654cc..63ccff2 100644 (file)
@@ -20,10 +20,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
         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 int? _definiteLength = 1;
+        private int? _definiteLength; // predetermined definite-length of current data item context
         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
@@ -32,7 +29,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         private int? _currentValueOffset = null;
         private HashSet<KeyValueEncodingRange>? _keyValueEncodingRanges = null;
 
-        public CborWriter(CborConformanceLevel conformanceLevel = CborConformanceLevel.Lax, bool encodeIndefiniteLengths = false)
+        public CborWriter(CborConformanceLevel conformanceLevel = CborConformanceLevel.Lax, bool encodeIndefiniteLengths = false, bool allowMultipleRootLevelValues = false)
         {
             CborConformanceLevelHelpers.Validate(conformanceLevel);
 
@@ -43,13 +40,16 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
             ConformanceLevel = conformanceLevel;
             EncodeIndefiniteLengths = encodeIndefiniteLengths;
+            AllowMultipleRootLevelValues = allowMultipleRootLevelValues;
+            _definiteLength = allowMultipleRootLevelValues ? null : (int?)1;
         }
 
-        public bool EncodeIndefiniteLengths { get; }
         public CborConformanceLevel ConformanceLevel { get; }
+        public bool EncodeIndefiniteLengths { get; }
+        public bool AllowMultipleRootLevelValues { get; }
         public int BytesWritten => _offset;
         // Returns true iff a complete CBOR document has been written to buffer
-        public bool IsWriteCompleted => (_nestedDataItems?.Count ?? 0) == 0 && _itemsWritten == _definiteLength;
+        public bool IsWriteCompleted => (_nestedDataItems?.Count ?? 0) == 0 && _itemsWritten > 0;
 
         public void WriteEncodedValue(ReadOnlyMemory<byte> encodedValue)
         {