Skip to content

Commit

Permalink
[CBOR] Add a CborReader.SkipValue() method (#34477)
Browse files Browse the repository at this point in the history
* Add a CborReader.SkipValue() method

* revert reader state format error changes

* refactor skip from recursive to imperative

* use non-allocating skips for string data items

* add a test for extremely nested values

* remove blank line

* consolidate SkipString() methods
  • Loading branch information
eiriktsarpalis authored Apr 3, 2020
1 parent c64c31a commit 223b843
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Test.Cryptography;
using Xunit;

namespace System.Security.Cryptography.Encoding.Tests.Cbor
{
public partial class CborReaderTests
{
[Theory]
[MemberData(nameof(SampleValues))]
public static void SkipValue_RootValue_HappyPath(string hexEncoding)
{
byte[] encoding = hexEncoding.HexToByteArray();
var reader = new CborReader(encoding);

reader.SkipValue();
Assert.Equal(CborReaderState.Finished, reader.Peek());
}

[Theory]
[MemberData(nameof(SampleValues))]
public static void SkipValue_NestedValue_HappyPath(string hexEncoding)
{
byte[] encoding = $"8301{hexEncoding}03".HexToByteArray();
var reader = new CborReader(encoding);

reader.ReadStartArray();
reader.ReadInt64();
reader.SkipValue();
reader.ReadInt64();
reader.ReadEndArray();
Assert.Equal(CborReaderState.Finished, reader.Peek());
}

[Theory]
[MemberData(nameof(SampleValues))]
public static void SkipValue_TaggedValue_HappyPath(string hexEncoding)
{
byte[] encoding = $"c2{hexEncoding}".HexToByteArray();
var reader = new CborReader(encoding);

reader.ReadTag();
reader.SkipValue();
Assert.Equal(CborReaderState.Finished, reader.Peek());
}

[Fact]
public static void SkipValue_NotAtValue_ShouldThrowInvalidOperationException()
{
byte[] encoding = "80".HexToByteArray();
var reader = new CborReader(encoding);

reader.ReadStartArray();
Assert.Throws<InvalidOperationException>(() => reader.SkipValue());
}

[Theory]
[InlineData("")]
[InlineData("ff")]
[InlineData("c2")]
[InlineData("bf01ff")]
[InlineData("7f01ff")]
public static void SkipValue_InvalidFormat_ShouldThrowFormatException(string hexEncoding)
{
byte[] encoding = hexEncoding.HexToByteArray();
var reader = new CborReader(encoding);

Assert.Throws<FormatException>(() => reader.SkipValue());
}

[Theory]
[InlineData("61ff")]
[InlineData("62f090")]
public static void SkipValue_InvalidUtf8_ShouldDecoderFallbackException(string hexEncoding)
{
byte[] encoding = hexEncoding.HexToByteArray();
var reader = new CborReader(encoding);

Assert.Throws<DecoderFallbackException>(() => reader.SkipValue());
}

[Theory]
[InlineData(50_000)]
public static void SkipValue_ExtremelyNestedValues_ShouldNotStackOverflow(int depth)
{
// Construct a valid CBOR encoding with extreme nesting:
// defines a tower of `depth` nested singleton arrays,
// with the innermost array containing zero.
byte[] encoding = new byte[depth + 1];
encoding.AsSpan(0, depth).Fill(0x81); // array of length 1
encoding[depth] = 0;

var reader = new CborReader(encoding);

reader.SkipValue();
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 });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace System.Security.Cryptography.Encoding.Tests.Cbor
{
internal partial class CborReader
{
public void SkipValue()
{
int depth = 0;

do
{
SkipNextNode(ref depth);
} while (depth > 0);
}

private void SkipNextNode(ref int depth)
{
CborReaderState state;

// peek, skipping any tags we might encounter
while ((state = Peek()) == CborReaderState.Tag)
{
ReadTag();
}

switch (state)
{
case CborReaderState.UnsignedInteger:
ReadUInt64();
break;

case CborReaderState.NegativeInteger:
ReadCborNegativeIntegerEncoding();
break;

case CborReaderState.ByteString:
SkipString(type: CborMajorType.ByteString);
break;

case CborReaderState.TextString:
SkipString(type: CborMajorType.TextString);
break;

case CborReaderState.StartByteString:
ReadStartByteStringIndefiniteLength();
depth++;
break;

case CborReaderState.EndByteString:
ValidatePop(state, depth);
ReadEndByteStringIndefiniteLength();
depth--;
break;

case CborReaderState.StartTextString:
ReadStartTextStringIndefiniteLength();
depth++;
break;

case CborReaderState.EndTextString:
ValidatePop(state, depth);
ReadEndTextStringIndefiniteLength();
depth--;
break;

case CborReaderState.StartArray:
ReadStartArray();
depth++;
break;

case CborReaderState.EndArray:
ValidatePop(state, depth);
ReadEndArray();
depth--;
break;

case CborReaderState.StartMap:
ReadStartMap();
depth++;
break;

case CborReaderState.EndMap:
ValidatePop(state, depth);
ReadEndMap();
depth--;
break;

case CborReaderState.HalfPrecisionFloat:
case CborReaderState.SinglePrecisionFloat:
case CborReaderState.DoublePrecisionFloat:
ReadDouble();
break;

case CborReaderState.Null:
case CborReaderState.Boolean:
case CborReaderState.SpecialValue:
ReadSpecialValue();
break;

case CborReaderState.EndOfData:
throw new FormatException("Unexpected end of buffer.");
case CborReaderState.FormatError:
throw new FormatException("Invalid CBOR format.");

default:
throw new InvalidOperationException($"Unexpected CBOR reader state {state}.");
}

// guards against cases where the caller attempts to skip when reader is not positioned at the start of a value
static void ValidatePop(CborReaderState state, int depth)
{
if (depth == 0)
{
throw new InvalidOperationException($"Reader state {state} is not at start of a data item.");
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -295,5 +295,26 @@ static CborInitialByte ReadNextInitialByte(ReadOnlySpan<byte> buffer, CborMajorT
return cib;
}
}

// SkipValue() helper: reads a cbor string without allocating or copying to a buffer
// NB this only handles definite-length chunks
private void SkipString(CborMajorType type)
{
CborInitialByte header = PeekInitialByte(expectedType: type);

ReadOnlySpan<byte> buffer = _buffer.Span;
int byteLength = checked((int)ReadUnsignedInteger(buffer, header, out int additionalBytes));
EnsureBuffer(1 + additionalBytes + byteLength);

// force any utf8 decoding errors if text string
if (type == CborMajorType.TextString)
{
ReadOnlySpan<byte> encodedSlice = buffer.Slice(1 + additionalBytes, byteLength);
s_utf8Encoding.GetCharCount(encodedSlice);
}

AdvanceBuffer(1 + additionalBytes + byteLength);
AdvanceDataItemCounters();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ internal enum CborReaderState
DoublePrecisionFloat,
SpecialValue,
Finished,
FormatError,
EndOfData,
FormatError,
}

internal partial class CborReader
Expand Down Expand Up @@ -148,7 +148,7 @@ public CborReaderState Peek()
CborMajorType.Map => CborReaderState.StartMap,
CborMajorType.Tag => CborReaderState.Tag,
CborMajorType.Special => MapSpecialValueTagToReaderState(initialByte.AdditionalInfo),
_ => CborReaderState.FormatError,
_ => throw new Exception("CborReader internal error. Invalid major type."),
};

static CborReaderState MapSpecialValueTagToReaderState (CborAdditionalInfo value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@
<Compile Include="Asn1\Writer\WriteUtf8String.cs" />
<Compile Include="Cbor.Tests\CborReaderTests.Helpers.cs" />
<Compile Include="Cbor.Tests\CborReaderTests.Map.cs" />
<Compile Include="Cbor.Tests\CborReaderTests.SkipValue.cs" />
<Compile Include="Cbor.Tests\CborReaderTests.Special.cs" />
<Compile Include="Cbor.Tests\CborWriterTests.Helpers.cs" />
<Compile Include="Cbor.Tests\CborReaderTests.Array.cs" />
<Compile Include="Cbor.Tests\CborWriterTests.Array.cs" />
<Compile Include="Cbor.Tests\CborWriterTests.cs" />
<Compile Include="Cbor.Tests\CborWriterTests.Map.cs" />
<Compile Include="Cbor.Tests\CborWriterTests.Special.cs" />
<Compile Include="Cbor\CborReader.SkipValue.cs" />
<Compile Include="Cbor\CborReader.Special.cs" />
<Compile Include="Cbor\CborTag.cs" />
<Compile Include="Cbor\CborInitialByte.cs" />
Expand Down

0 comments on commit 223b843

Please sign in to comment.