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",
+ };
}
}
public partial class CborReaderTests
{
[Theory]
- [MemberData(nameof(SampleValues))]
+ [MemberData(nameof(SkipTestInputs))]
public static void SkipValue_RootValue_HappyPath(string hexEncoding)
{
byte[] encoding = hexEncoding.HexToByteArray();
}
[Theory]
- [MemberData(nameof(SampleValues))]
+ [MemberData(nameof(SkipTestInputs))]
public static void SkipValue_NestedValue_HappyPath(string hexEncoding)
{
byte[] encoding = $"8301{hexEncoding}03".HexToByteArray();
}
[Theory]
- [MemberData(nameof(SampleValues))]
+ [MemberData(nameof(SkipTestInputs))]
public static void SkipValue_TaggedValue_HappyPath(string hexEncoding)
{
byte[] encoding = $"c2{hexEncoding}".HexToByteArray();
}
[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();
[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]
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 });
}
}
[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]
}
[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.
string hexEncoding = "7f62f090628591ff";
byte[] data = hexEncoding.HexToByteArray();
var reader = new CborReader(data);
- Assert.Throws<DecoderFallbackException>(() => reader.ReadTextString());
+ Assert.Throws<FormatException>(() => reader.ReadTextString());
}
}
}
#nullable enable
using System;
+using System.Collections.Generic;
+using System.Linq;
using Test.Cryptography;
using Xunit;
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 });
}
}
#nullable enable
using System.Linq;
+using Test.Cryptography;
using Xunit;
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)
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)
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()}");
};
}
[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]
#nullable enable
using System;
+using System.Collections.Generic;
+using System.Linq;
using Test.Cryptography;
using Xunit;
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 });
}
}
#nullable enable
using System.Collections.Generic;
using System.Diagnostics;
+using System.Text;
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;
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;
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)
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);
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);
+ }
+ }
}
}
}
}
+ 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)
// 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));
#nullable enable
using System.Buffers;
using System.Collections.Generic;
-using System.Diagnostics;
+using System.Threading;
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();
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()