[CBOR] Implement Write/ReadEncodedValue() methods (#34650)
authorEirik Tsarpalis <eirik.tsarpalis@gmail.com>
Wed, 8 Apr 2020 20:20:38 +0000 (21:20 +0100)
committerGitHub <noreply@github.com>
Wed, 8 Apr 2020 20:20:38 +0000 (21:20 +0100)
* Implement CBOR Write/ReadEncodedValue() methods

* Wrap utf8 encoding and decoding exceptions in ArgumentException and FormatException respectively

* Address feedback

* address feedback

* add contextual tests for WriteEncodedValue

src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.Helpers.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.SkipValue.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.String.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborReaderTests.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor.Tests/CborWriterTests.Helpers.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/CborReader.String.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborReader.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.String.cs
src/libraries/System.Security.Cryptography.Encoding/tests/Cbor/CborWriter.cs

index d1bca36..6c09b4a 100644 (file)
@@ -158,5 +158,101 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 reader.ReadEndMap();
             }
         }
+
+        public static string[] SampleCborValues =>
+            new[]
+            {
+                // numeric values
+                "01",
+                "37",
+                "3818",
+                "1818",
+                "190100",
+                "390100",
+                "1a000f4240",
+                "3affffffff",
+                // byte strings
+                "40",
+                "4401020304",
+                "5f41ab40ff",
+                // text strings
+                "60",
+                "6161",
+                "6449455446",
+                "7f62616260ff",
+                // Arrays
+                "80",
+                "840120604107",
+                "8301820203820405",
+                "9f182aff",
+                // Maps
+                "a0",
+                "a201020304",
+                "a1a1617802182a",
+                "bf01020304ff",
+                // tagged values
+                "c202",
+                "d82076687474703a2f2f7777772e6578616d706c652e636f6d",
+                // special values
+                "f4",
+                "f6",
+                "fa47c35000",
+            };
+
+        public static string[] InvalidCborValues =>
+            new[]
+            {
+                "",
+                // numeric types with missing bytes
+                "18",
+                "19ff",
+                "1affffff",
+                "1bffffffffffffff",
+                "38",
+                "39ff",
+                "3affffff",
+                "3bffffffffffffff",
+                // definite-length strings with missing bytes
+                "41",
+                "4201",
+                "61",
+                "6261",
+                // invalid utf8 strings
+                "61ff",
+                "62f090",
+                // indefinite-length strings with missing break byte
+                "5f41ab40",
+                "7f62616260",
+                // definite-length arrays with missing elements
+                "81",
+                "8201",
+                // definite-length maps with missing fields
+                "a1",
+                "a20102",
+                // maps with odd number of elements
+                "a101",
+                "a2010203",
+                "bf01ff",
+                // indefinite-length collections with missing break byte
+                "9f",
+                "9f01",
+                "bf",
+                "bf0102",
+                // tags missing data
+                "d8",
+                "d9ff",
+                "daffffff",
+                "daffffffffff",
+                // valid tag not followed by value
+                "c2",
+                // floats missing data
+                "f9ff",
+                "faffffff",
+                "fbffffffffffffff",
+                // special value missing data
+                "f8",
+                // invalid special value
+                "f81f",
+            };
     }
 }
index 3085636..5bd163d 100644 (file)
@@ -15,7 +15,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
     public partial class CborReaderTests
     {
         [Theory]
-        [MemberData(nameof(SampleValues))]
+        [MemberData(nameof(SkipTestInputs))]
         public static void SkipValue_RootValue_HappyPath(string hexEncoding)
         {
             byte[] encoding = hexEncoding.HexToByteArray();
@@ -26,7 +26,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         }
 
         [Theory]
-        [MemberData(nameof(SampleValues))]
+        [MemberData(nameof(SkipTestInputs))]
         public static void SkipValue_NestedValue_HappyPath(string hexEncoding)
         {
             byte[] encoding = $"8301{hexEncoding}03".HexToByteArray();
@@ -41,7 +41,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         }
 
         [Theory]
-        [MemberData(nameof(SampleValues))]
+        [MemberData(nameof(SkipTestInputs))]
         public static void SkipValue_TaggedValue_HappyPath(string hexEncoding)
         {
             byte[] encoding = $"c2{hexEncoding}".HexToByteArray();
@@ -63,11 +63,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         }
 
         [Theory]
-        [InlineData("")]
-        [InlineData("ff")]
-        [InlineData("c2")]
-        [InlineData("bf01ff")]
-        [InlineData("7f01ff")]
+        [MemberData(nameof(SkipTestInvalidCborInputs))]
         public static void SkipValue_InvalidFormat_ShouldThrowFormatException(string hexEncoding)
         {
             byte[] encoding = hexEncoding.HexToByteArray();
@@ -79,12 +75,14 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         [Theory]
         [InlineData("61ff")]
         [InlineData("62f090")]
-        public static void SkipValue_InvalidUtf8_ShouldDecoderFallbackException(string hexEncoding)
+        public static void SkipValue_InvalidUtf8_ShouldThrowFormatException(string hexEncoding)
         {
             byte[] encoding = hexEncoding.HexToByteArray();
             var reader = new CborReader(encoding);
 
-            Assert.Throws<DecoderFallbackException>(() => reader.SkipValue());
+            FormatException exn = Assert.Throws<FormatException>(() => reader.SkipValue());
+            Assert.NotNull(exn.InnerException);
+            Assert.IsType<DecoderFallbackException>(exn.InnerException);
         }
 
         [Theory]
@@ -104,39 +102,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             Assert.Equal(CborReaderState.Finished, reader.Peek());
         }
 
-        public static IEnumerable<object[]> SampleValues =>
-            new string[]
-            {
-                // numeric values
-                "01",
-                "1a000f4240",
-                "3affffffff",
-                // byte strings
-                "40",
-                "4401020304",
-                "5f41ab40ff",
-                // text strings
-                "60",
-                "6161",
-                "6449455446",
-                "7f62616260ff",
-                // Arrays
-                "80",
-                "840120604107",
-                "8301820203820405",
-                "9f182aff",
-                // Maps
-                "a0",
-                "a201020304",
-                "a1a1617802182a",
-                "bf01020304ff",
-                // tagged values
-                "c202",
-                "d82076687474703a2f2f7777772e6578616d706c652e636f6d",
-                // special values
-                "f4",
-                "f6",
-                "fa47c35000",
-            }.Select(x => new object[] { x });
+        public static IEnumerable<object[]> SkipTestInputs => SampleCborValues.Select(x => new [] { x });
+        public static IEnumerable<object[]> SkipTestInvalidCborInputs => InvalidCborValues.Select(x => new[] { x });
     }
 }
index ead9666..50ad86d 100644 (file)
@@ -486,23 +486,27 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         [Theory]
         [InlineData("61ff")]
         [InlineData("62f090")]
-        public static void ReadTextString_InvalidUnicode_ShouldThrowDecoderFallbackException(string hexEncoding)
+        public static void ReadTextString_InvalidUnicode_ShouldThrowFormatException(string hexEncoding)
         {
             byte[] data = hexEncoding.HexToByteArray();
             var reader = new CborReader(data);
-            Assert.Throws<System.Text.DecoderFallbackException>(() => reader.ReadTextString());
+            FormatException exn = Assert.Throws<FormatException>(() => reader.ReadTextString());
+            Assert.NotNull(exn.InnerException);
+            Assert.IsType<System.Text.DecoderFallbackException>(exn.InnerException);
         }
 
         [Theory]
         [InlineData("61ff")]
         [InlineData("62f090")]
-        public static void TryReadTextString_InvalidUnicode_ShouldThrowDecoderFallbackException(string hexEncoding)
+        public static void TryReadTextString_InvalidUnicode_ShouldThrowFormatException(string hexEncoding)
         {
             byte[] data = hexEncoding.HexToByteArray();
             char[] buffer = new char[32];
             var reader = new CborReader(data);
 
-            Assert.Throws<System.Text.DecoderFallbackException>(() => reader.TryReadTextString(buffer, out int _));
+            FormatException exn = Assert.Throws<FormatException>(() => reader.TryReadTextString(buffer, out int _));
+            Assert.NotNull(exn.InnerException);
+            Assert.IsType<System.Text.DecoderFallbackException>(exn.InnerException);
         }
 
         [Fact]
@@ -614,7 +618,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         }
 
         [Fact]
-        public static void ReadTextString_IndefiniteLengthConcatenated_InvalidUtf8Chunks_ShouldThrowDecoderFallbackException()
+        public static void ReadTextString_IndefiniteLengthConcatenated_InvalidUtf8Chunks_ShouldThrowFormatException()
         {
             // while the concatenated string is valid utf8, the individual chunks are not,
             // which is in violation of the CBOR format.
@@ -622,7 +626,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             string hexEncoding = "7f62f090628591ff";
             byte[] data = hexEncoding.HexToByteArray();
             var reader = new CborReader(data);
-            Assert.Throws<DecoderFallbackException>(() => reader.ReadTextString());
+            Assert.Throws<FormatException>(() => reader.ReadTextString());
         }
     }
 }
index 404a39b..c7d3637 100644 (file)
@@ -4,6 +4,8 @@
 
 #nullable enable
 using System;
+using System.Collections.Generic;
+using System.Linq;
 using Test.Cryptography;
 using Xunit;
 
@@ -74,5 +76,44 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             Assert.Equal(CborReaderState.Finished, reader.Peek());
             Assert.Throws<InvalidOperationException>(() => reader.ReadInt64());
         }
+
+        [Theory]
+        [MemberData(nameof(EncodedValueInputs))]
+        public static void ReadEncodedValue_RootValue_HappyPath(string hexEncoding)
+        {
+            byte[] encoding = hexEncoding.HexToByteArray();
+            var reader = new CborReader(encoding);
+
+            byte[] encodedValue = reader.ReadEncodedValue().ToArray();
+            Assert.Equal(hexEncoding, encodedValue.ByteArrayToHex().ToLower());
+        }
+
+        [Theory]
+        [MemberData(nameof(EncodedValueInputs))]
+        public static void ReadEncodedValue_NestedValue_HappyPath(string hexEncoding)
+        {
+            byte[] encoding = $"8301{hexEncoding}60".HexToByteArray();
+
+            var reader = new CborReader(encoding);
+
+            reader.ReadStartArray();
+            reader.ReadInt64();
+            byte[] encodedValue = reader.ReadEncodedValue().ToArray();
+
+            Assert.Equal(hexEncoding, encodedValue.ByteArrayToHex().ToLower());
+        }
+
+        [Theory]
+        [MemberData(nameof(EncodedValueInvalidInputs))]
+        public static void ReadEncodedValue_InvalidCbor_ShouldThrowFormatException(string hexEncoding)
+        {
+            byte[] encoding = hexEncoding.HexToByteArray();
+            var reader = new CborReader(encoding);
+
+            Assert.Throws<FormatException>(() => reader.ReadEncodedValue());
+        }
+
+        public static IEnumerable<object[]> EncodedValueInputs => CborReaderTests.SampleCborValues.Select(x => new[] { x });
+        public static IEnumerable<object[]> EncodedValueInvalidInputs => CborReaderTests.InvalidCborValues.Select(x => new[] { x });
     }
 }
index 3665045..c88944e 100644 (file)
@@ -5,6 +5,7 @@
 #nullable enable
 
 using System.Linq;
+using Test.Cryptography;
 using Xunit;
 
 namespace System.Security.Cryptography.Encoding.Tests.Cbor
@@ -15,6 +16,8 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         {
             public const string MapPrefixIdentifier = "_map";
 
+            public const string EncodedPrefixIdentifier = "_encodedValue";
+
             // 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)
@@ -22,6 +25,13 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                 return values.Length % 2 == 1 && values[0] is string s && s == MapPrefixIdentifier;
             }
 
+            public static bool IsEncodedValueRepresentation(object[] values)
+            {
+                return values.Length == 2 &&
+                       values[0] is string s && s == EncodedPrefixIdentifier &&
+                       values[1] is string;
+            }
+
             public static void WriteValue(CborWriter writer, object value, bool useDefiniteLengthCollections = true)
             {
                 switch (value)
@@ -38,6 +48,11 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
                     case byte[][] chunks: WriteChunkedByteString(writer, chunks); break;
                     case string[] chunks: WriteChunkedTextString(writer, chunks); break;
                     case object[] nested when IsCborMapRepresentation(nested): WriteMap(writer, nested, useDefiniteLengthCollections); break;
+                    case object[] nested when IsEncodedValueRepresentation(nested):
+                        byte[] encodedValue = ((string)nested[1]).HexToByteArray();
+                        writer.WriteEncodedValue(encodedValue);
+                        break;
+
                     case object[] nested: WriteArray(writer, nested, useDefiniteLengthCollections); break;
                     default: throw new ArgumentException($"Unrecognized argument type {value.GetType()}");
                 };
index 9705af5..ea7465f 100644 (file)
@@ -72,12 +72,14 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         }
 
         [Fact]
-        public static void WriteTextString_InvalidUnicodeString_ShouldThrowEncoderFallbackException()
+        public static void WriteTextString_InvalidUnicodeString_ShouldThrowArgumentException()
         {
             // NB Xunit's InlineDataAttribute will corrupt string literals containing invalid unicode
             string invalidUnicodeString = "\ud800";
             using var writer = new CborWriter();
-            Assert.Throws<System.Text.EncoderFallbackException>(() => writer.WriteTextString(invalidUnicodeString));
+            ArgumentException exn = Assert.Throws<ArgumentException>(() => writer.WriteTextString(invalidUnicodeString));
+            Assert.NotNull(exn.InnerException);
+            Assert.IsType<System.Text.EncoderFallbackException>(exn.InnerException);
         }
 
         [Theory]
index 510c986..daa42a9 100644 (file)
@@ -4,6 +4,8 @@
 
 #nullable enable
 using System;
+using System.Collections.Generic;
+using System.Linq;
 using Test.Cryptography;
 using Xunit;
 
@@ -43,5 +45,118 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             writer.WriteTextString("test");
             Assert.Equal(5, writer.BytesWritten);
         }
+
+        [Theory]
+        [MemberData(nameof(EncodedValueInputs))]
+        public static void WriteEncodedValue_RootValue_HappyPath(string hexEncodedValue)
+        {
+            byte[] encodedValue = hexEncodedValue.HexToByteArray();
+
+            using var writer = new CborWriter();
+            writer.WriteEncodedValue(encodedValue);
+
+            string hexResult = writer.ToArray().ByteArrayToHex();
+            Assert.Equal(hexEncodedValue, hexResult.ToLower());
+        }
+
+        [Theory]
+        [MemberData(nameof(EncodedValueInputs))]
+        public static void WriteEncodedValue_NestedValue_HappyPath(string hexEncodedValue)
+        {
+            byte[] encodedValue = hexEncodedValue.HexToByteArray();
+
+            using var writer = new CborWriter();
+            writer.WriteStartArray(3);
+            writer.WriteInt64(1);
+            writer.WriteEncodedValue(encodedValue);
+            writer.WriteTextString("");
+            writer.WriteEndArray();
+
+            string hexResult = writer.ToArray().ByteArrayToHex();
+            Assert.Equal("8301" + hexEncodedValue + "60", hexResult.ToLower());
+        }
+
+        public const string Enc = Helpers.EncodedPrefixIdentifier;
+
+        [Theory]
+        [InlineData(new object[] { new object[] { Enc, "8101" } }, true, "818101")]
+        [InlineData(new object[] { new object[] { Enc, "8101" } }, false, "9f8101ff")]
+        [InlineData(new object[] { Map, new object[] { Enc, "8101" }, 42 }, true, "a18101182a")]
+        [InlineData(new object[] { Map, new object[] { Enc, "8101" }, 42 }, false, "bf8101182aff")]
+        [InlineData(new object[] { Map, 42, new object[] { Enc, "8101" } }, true, "a1182a8101")]
+        [InlineData(new object[] { Map, 42, new object[] { Enc, "8101" } }, false, "bf182a8101ff")]
+
+        public static void WriteEncodedValue_ContextScenaria_HappyPath(object value, bool useDefiniteLength, string hexExpectedEncoding)
+        {
+            using var writer = new CborWriter();
+
+            Helpers.WriteValue(writer, value, useDefiniteLengthCollections: useDefiniteLength);
+
+            string hexEncoding = writer.ToArray().ByteArrayToHex().ToLower();
+            Assert.Equal(hexExpectedEncoding, hexEncoding);
+        }
+
+        [Fact]
+        public static void WriteEncodedValue_IndefiniteLengthTextString_HappyPath()
+        {
+            using var writer = new CborWriter();
+
+            writer.WriteStartTextStringIndefiniteLength();
+            writer.WriteTextString("foo");
+            writer.WriteEncodedValue("63626172".HexToByteArray());
+            writer.WriteEndTextStringIndefiniteLength();
+
+            byte[] encoding = writer.ToArray();
+            Assert.Equal("7f63666f6f63626172ff", encoding.ByteArrayToHex().ToLower());
+        }
+
+        [Fact]
+        public static void WriteEncodedValue_IndefiniteLengthByteString_HappyPath()
+        {
+            using var writer = new CborWriter();
+
+            writer.WriteStartByteStringIndefiniteLength();
+            writer.WriteByteString(new byte[] { 1, 1, 1 });
+            writer.WriteEncodedValue("43020202".HexToByteArray());
+            writer.WriteEndByteStringIndefiniteLength();
+
+            byte[] encoding = writer.ToArray();
+            Assert.Equal("5f4301010143020202ff", encoding.ByteArrayToHex().ToLower());
+        }
+
+        [Fact]
+        public static void WriteEncodedValue_BadIndefiniteLengthStringValue_ShouldThrowInvalidOperationException()
+        {
+            using var writer = new CborWriter();
+            writer.WriteStartTextStringIndefiniteLength();
+            Assert.Throws<InvalidOperationException>(() => writer.WriteEncodedValue(new byte[] { 0x01 }));
+        }
+
+        [Fact]
+        public static void WriteEncodedValue_AtEndOfDefiniteLengthCollection_ShouldThrowInvalidOperationException()
+        {
+            using var writer = new CborWriter();
+            writer.WriteInt64(0);
+            Assert.Throws<InvalidOperationException>(() => writer.WriteEncodedValue(new byte[] { 0x01 }));
+        }
+
+        [Theory]
+        [MemberData(nameof(EncodedValueBadInputs))]
+        public static void WriteEncodedValue_InvalidCbor_ShouldThrowArgumentException(string hexEncodedInput)
+        {
+            byte[] encodedInput = hexEncodedInput.HexToByteArray();
+            using var writer = new CborWriter();
+            Assert.Throws<ArgumentException>(() => writer.WriteEncodedValue(encodedInput));
+        }
+
+        [Fact]
+        public static void WriteEncodedValue_ValidPayloadWithTrailingBytes_ShouldThrowArgumentException()
+        {
+            using var writer = new CborWriter();
+            Assert.Throws<ArgumentException>(() => writer.WriteEncodedValue(new byte[] { 0x01, 0x01 }));
+        }
+
+        public static IEnumerable<object[]> EncodedValueInputs => CborReaderTests.SampleCborValues.Select(x => new [] { x });
+        public static IEnumerable<object[]> EncodedValueBadInputs => CborReaderTests.InvalidCborValues.Select(x => new[] { x });
     }
 }
index d8a0ead..6d39c7b 100644 (file)
@@ -5,6 +5,7 @@
 #nullable enable
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.Text;
 
 namespace System.Security.Cryptography.Encoding.Tests.Cbor
 {
@@ -70,7 +71,17 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             int length = checked((int)ReadUnsignedInteger(_buffer.Span, header, out int additionalBytes));
             EnsureBuffer(1 + additionalBytes + length);
             ReadOnlySpan<byte> encodedString = _buffer.Span.Slice(1 + additionalBytes, length);
-            string result = s_utf8Encoding.GetString(encodedString);
+
+            string result;
+            try
+            {
+                result = s_utf8Encoding.GetString(encodedString);
+            }
+            catch (DecoderFallbackException e)
+            {
+                throw new FormatException("Text string payload is not a valid UTF8 string.", e);
+            }
+
             AdvanceBuffer(1 + additionalBytes + length);
             AdvanceDataItemCounters();
             return result;
@@ -89,7 +100,9 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             EnsureBuffer(1 + additionalBytes + byteLength);
 
             ReadOnlySpan<byte> encodedSlice = _buffer.Span.Slice(1 + additionalBytes, byteLength);
-            int charLength = s_utf8Encoding.GetCharCount(encodedSlice);
+
+            int charLength = ValidateUtf8AndGetCharCount(encodedSlice);
+
             if (charLength > destination.Length)
             {
                 charsWritten = 0;
@@ -180,7 +193,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             int concatenatedStringSize = 0;
             foreach ((int o, int l) in ranges)
             {
-                concatenatedStringSize += s_utf8Encoding.GetCharCount(buffer.Slice(o, l));
+                concatenatedStringSize += ValidateUtf8AndGetCharCount(buffer.Slice(o, l));
             }
 
             if (concatenatedStringSize > destination.Length)
@@ -231,7 +244,7 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
             foreach ((int o, int l) in ranges)
             {
-                concatenatedStringSize += s_utf8Encoding.GetCharCount(buffer.Slice(o, l));
+                concatenatedStringSize += ValidateUtf8AndGetCharCount(buffer.Slice(o, l));
             }
 
             string output = string.Create(concatenatedStringSize, (ranges, _buffer), BuildString);
@@ -310,11 +323,23 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             if (type == CborMajorType.TextString)
             {
                 ReadOnlySpan<byte> encodedSlice = buffer.Slice(1 + additionalBytes, byteLength);
-                s_utf8Encoding.GetCharCount(encodedSlice);
+                ValidateUtf8AndGetCharCount(encodedSlice);
             }
 
             AdvanceBuffer(1 + additionalBytes + byteLength);
             AdvanceDataItemCounters();
         }
+
+        private int ValidateUtf8AndGetCharCount(ReadOnlySpan<byte> buffer)
+        {
+            try
+            {
+                return s_utf8Encoding.GetCharCount(buffer);
+            }
+            catch (DecoderFallbackException e)
+            {
+                throw new FormatException("Text string payload is not a valid UTF8 string.", e);
+            }
+        }
     }
 }
index ee8bed1..f55a1d0 100644 (file)
@@ -174,6 +174,19 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
             }
         }
 
+        public ReadOnlyMemory<byte> ReadEncodedValue()
+        {
+            // keep a snapshot of the initial buffer state
+            ReadOnlyMemory<byte> initialBuffer = _buffer;
+            int initialBytesRead = _bytesRead;
+
+            // call skip to read and validate the next value
+            SkipValue();
+
+            // return the slice corresponding to the consumed value
+            return initialBuffer.Slice(0, _bytesRead - initialBytesRead);
+        }
+
         private CborInitialByte PeekInitialByte()
         {
             if (_remainingDataItems == 0)
index 45083cc..733b281 100644 (file)
@@ -25,7 +25,16 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         // Implements major type 3 encoding per https://tools.ietf.org/html/rfc7049#section-2.1
         public void WriteTextString(ReadOnlySpan<char> value)
         {
-            int length = s_utf8Encoding.GetByteCount(value);
+            int length;
+            try
+            {
+                length = s_utf8Encoding.GetByteCount(value);
+            }
+            catch (EncoderFallbackException e)
+            {
+                throw new ArgumentException("Provided text string is not valid UTF8.", e);
+            }
+
             WriteUnsignedInteger(CborMajorType.TextString, (ulong)length);
             EnsureWriteCapacity(length);
             s_utf8Encoding.GetBytes(value, _buffer.AsSpan(_offset));
index 2316a70..ff2d81f 100644 (file)
@@ -5,7 +5,7 @@
 #nullable enable
 using System.Buffers;
 using System.Collections.Generic;
-using System.Diagnostics;
+using System.Threading;
 
 namespace System.Security.Cryptography.Encoding.Tests.Cbor
 {
@@ -34,6 +34,49 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
         // Returns true iff a complete CBOR document has been written to buffer
         public bool IsWriteCompleted => _remainingDataItems == 0 && (_nestedDataItemStack?.Count ?? 0) == 0;
 
+        public void WriteEncodedValue(ReadOnlyMemory<byte> encodedValue)
+        {
+            ValidateEncoding(encodedValue);
+            ReadOnlySpan<byte> encodedValueSpan = encodedValue.Span;
+            EnsureWriteCapacity(encodedValueSpan.Length);
+
+            // even though the encoding might be valid CBOR, it might not be valid within the current writer context.
+            // E.g. we're at the end of a definite-length collection or writing integers in an indefinite-length string.
+            // For this reason we write the initial byte separately and perform the usual validation.
+            CborInitialByte initialByte = new CborInitialByte(encodedValueSpan[0]);
+            WriteInitialByte(initialByte);
+
+            // now copy any remaining bytes
+            encodedValueSpan = encodedValueSpan.Slice(1);
+
+            if (!encodedValueSpan.IsEmpty)
+            {
+                encodedValueSpan.CopyTo(_buffer.AsSpan(_offset));
+                _offset += encodedValueSpan.Length;
+            }
+
+            AdvanceDataItemCounters();
+
+            static void ValidateEncoding(ReadOnlyMemory<byte> encodedValue)
+            {
+                var reader = new CborReader(encodedValue);
+
+                try
+                {
+                    reader.SkipValue();
+                }
+                catch (FormatException e)
+                {
+                    throw new ArgumentException("Payload is not a valid CBOR value.", e);
+                }
+
+                if (reader.BytesRemaining != 0)
+                {
+                    throw new ArgumentException("Payload is not a valid CBOR value.");
+                }
+            }
+        }
+
         private void EnsureWriteCapacity(int pendingCount)
         {
             CheckDisposed();
@@ -143,13 +186,13 @@ namespace System.Security.Cryptography.Encoding.Tests.Cbor
 
         public void Dispose()
         {
-            if (_buffer != null)
+            byte[]? buffer = Interlocked.Exchange(ref _buffer, null!);
+
+            if (buffer != null)
             {
-                s_bufferPool.Return(_buffer, clearArray: true);
-                _buffer = null!;
+                s_bufferPool.Return(buffer, clearArray: true);
+                _offset = -1;
             }
-
-            _offset = -1;
         }
 
         public byte[] ToArray()