Skip to content

Commit 81ca1c4

Browse files
authored
Add AsnDecoder {Try}DecodeLength
1 parent 4965f21 commit 81ca1c4

3 files changed

Lines changed: 212 additions & 50 deletions

File tree

src/libraries/System.Formats.Asn1/ref/System.Formats.Asn1.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public AsnContentException(string? message, System.Exception? inner) { }
5555
}
5656
public static partial class AsnDecoder
5757
{
58+
public static int? DecodeLength(System.ReadOnlySpan<byte> source, System.Formats.Asn1.AsnEncodingRules ruleSet, out int bytesConsumed) { throw null; }
5859
public static byte[] ReadBitString(System.ReadOnlySpan<byte> source, System.Formats.Asn1.AsnEncodingRules ruleSet, out int unusedBitCount, out int bytesConsumed, System.Formats.Asn1.Asn1Tag? expectedTag = default(System.Formats.Asn1.Asn1Tag?)) { throw null; }
5960
public static bool ReadBoolean(System.ReadOnlySpan<byte> source, System.Formats.Asn1.AsnEncodingRules ruleSet, out int bytesConsumed, System.Formats.Asn1.Asn1Tag? expectedTag = default(System.Formats.Asn1.Asn1Tag?)) { throw null; }
6061
public static string ReadCharacterString(System.ReadOnlySpan<byte> source, System.Formats.Asn1.AsnEncodingRules ruleSet, System.Formats.Asn1.UniversalTagNumber encodingType, out int bytesConsumed, System.Formats.Asn1.Asn1Tag? expectedTag = default(System.Formats.Asn1.Asn1Tag?)) { throw null; }
@@ -74,6 +75,7 @@ public static partial class AsnDecoder
7475
public static void ReadSequence(System.ReadOnlySpan<byte> source, System.Formats.Asn1.AsnEncodingRules ruleSet, out int contentOffset, out int contentLength, out int bytesConsumed, System.Formats.Asn1.Asn1Tag? expectedTag = default(System.Formats.Asn1.Asn1Tag?)) { throw null; }
7576
public static void ReadSetOf(System.ReadOnlySpan<byte> source, System.Formats.Asn1.AsnEncodingRules ruleSet, out int contentOffset, out int contentLength, out int bytesConsumed, bool skipSortOrderValidation = false, System.Formats.Asn1.Asn1Tag? expectedTag = default(System.Formats.Asn1.Asn1Tag?)) { throw null; }
7677
public static System.DateTimeOffset ReadUtcTime(System.ReadOnlySpan<byte> source, System.Formats.Asn1.AsnEncodingRules ruleSet, out int bytesConsumed, int twoDigitYearMax = 2049, System.Formats.Asn1.Asn1Tag? expectedTag = default(System.Formats.Asn1.Asn1Tag?)) { throw null; }
78+
public static bool TryDecodeLength(System.ReadOnlySpan<byte> source, System.Formats.Asn1.AsnEncodingRules ruleSet, out int? decodedLength, out int bytesConsumed) { throw null; }
7779
public static bool TryReadBitString(System.ReadOnlySpan<byte> source, System.Span<byte> destination, System.Formats.Asn1.AsnEncodingRules ruleSet, out int unusedBitCount, out int bytesConsumed, out int bytesWritten, System.Formats.Asn1.Asn1Tag? expectedTag = default(System.Formats.Asn1.Asn1Tag?)) { throw null; }
7880
public static bool TryReadCharacterString(System.ReadOnlySpan<byte> source, System.Span<char> destination, System.Formats.Asn1.AsnEncodingRules ruleSet, System.Formats.Asn1.UniversalTagNumber encodingType, out int bytesConsumed, out int charsWritten, System.Formats.Asn1.Asn1Tag? expectedTag = default(System.Formats.Asn1.Asn1Tag?)) { throw null; }
7981
public static bool TryReadCharacterStringBytes(System.ReadOnlySpan<byte> source, System.Span<byte> destination, System.Formats.Asn1.AsnEncodingRules ruleSet, System.Formats.Asn1.Asn1Tag expectedTag, out int bytesConsumed, out int bytesWritten) { throw null; }

src/libraries/System.Formats.Asn1/src/System/Formats/Asn1/AsnDecoder.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,86 @@ private static ReadOnlySpan<byte> GetPrimitiveContentSpan(
206206
return ret;
207207
}
208208

209+
/// <summary>
210+
/// Decodes the data in <paramref name="source"/> as a length value under the specified
211+
/// encoding rules.
212+
/// </summary>
213+
/// <param name="source">The buffer containing encoded data.</param>
214+
/// <param name="ruleSet">The encoding constraints to use when interpreting the data.</param>
215+
/// <param name="bytesConsumed">
216+
/// When this method returns, the number of bytes from the beginning of <paramref name="source"/>
217+
/// that contributed to the length.
218+
/// This parameter is treated as uninitialized.
219+
/// </param>
220+
/// <returns>
221+
/// The decoded value of the length, or <see langword="null"/> if the
222+
/// encoded length represents the indefinite length.
223+
/// </returns>
224+
/// <exception cref="ArgumentOutOfRangeException">
225+
/// <paramref name="ruleSet"/> is not a known <see cref="AsnEncodingRules"/> value.
226+
/// </exception>
227+
/// <exception cref="AsnContentException">
228+
/// <paramref name="source"/> does not decode as a length under the specified encoding rules.
229+
/// </exception>
230+
/// <remarks>
231+
/// This method only processes the length portion of an ASN.1/BER Tag-Length-Value triplet,
232+
/// so <paramref name="source"/> needs to have already sliced off the encoded tag.
233+
/// </remarks>
234+
public static int? DecodeLength(
235+
ReadOnlySpan<byte> source,
236+
AsnEncodingRules ruleSet,
237+
out int bytesConsumed)
238+
{
239+
CheckEncodingRules(ruleSet);
240+
241+
// Use locals for the outs to hide the intermediate calculations from an out to a field.
242+
int? ret = ReadLength(source, ruleSet, out int read);
243+
bytesConsumed = read;
244+
return ret;
245+
}
246+
247+
/// <summary>
248+
/// Attempts to decode the data in <paramref name="source"/> as a length value under the specified
249+
/// encoding rules.
250+
/// </summary>
251+
/// <param name="source">The buffer containing encoded data.</param>
252+
/// <param name="ruleSet">The encoding constraints to use when interpreting the data.</param>
253+
/// <param name="decodedLength">
254+
/// When this method returns, the decoded value of the length, or <see langword="null"/> if the
255+
/// encoded length represents the indefinite length.
256+
/// This parameter is treated as uninitialized.
257+
/// </param>
258+
/// <param name="bytesConsumed">
259+
/// When this method returns, the number of bytes from the beginning of <paramref name="source"/>
260+
/// that contributed to the length.
261+
/// This parameter is treated as uninitialized.
262+
/// </param>
263+
/// <returns>
264+
/// <see langword="true"/> if the buffer represents a valid length under the specified encoding rules;
265+
/// otherwise, <see langword="false"/>
266+
/// </returns>
267+
/// <exception cref="ArgumentOutOfRangeException">
268+
/// <paramref name="ruleSet"/> is not a known <see cref="AsnEncodingRules"/> value.
269+
/// </exception>
270+
/// <remarks>
271+
/// This method only processes the length portion of an ASN.1/BER Tag-Length-Value triplet,
272+
/// so <paramref name="source"/> needs to have already sliced off the encoded tag.
273+
/// </remarks>
274+
public static bool TryDecodeLength(
275+
ReadOnlySpan<byte> source,
276+
AsnEncodingRules ruleSet,
277+
out int? decodedLength,
278+
out int bytesConsumed)
279+
{
280+
CheckEncodingRules(ruleSet);
281+
282+
// Use locals for the outs to hide the intermediate calculations from an out to a field.
283+
bool ret = TryReadLength(source, ruleSet, out int? decoded, out int read);
284+
bytesConsumed = read;
285+
decodedLength = decoded;
286+
return ret;
287+
}
288+
209289
private static bool TryReadLength(
210290
ReadOnlySpan<byte> source,
211291
AsnEncodingRules ruleSet,
Lines changed: 130 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,53 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Reflection;
54
using Test.Cryptography;
65
using Xunit;
76

87
namespace System.Formats.Asn1.Tests.Reader
98
{
109
public sealed class ReadLength
1110
{
12-
private delegate Asn1Tag ReadTagAndLengthDelegate(
11+
private static Asn1Tag ReadTagAndLength(
1312
ReadOnlySpan<byte> source,
1413
AsnEncodingRules ruleSet,
1514
out int? parsedLength,
16-
out int bytesRead);
15+
out int bytesRead)
16+
{
17+
Asn1Tag tag = Asn1Tag.Decode(source, out int tagLength);
18+
parsedLength = AsnDecoder.DecodeLength(source.Slice(tagLength), ruleSet, out int lengthLength);
19+
bytesRead = tagLength + lengthLength;
20+
return tag;
21+
}
22+
23+
private static bool TryReadTagAndLength(
24+
ReadOnlySpan<byte> source,
25+
AsnEncodingRules ruleSet,
26+
out Asn1Tag tag,
27+
out int? parsedLength,
28+
out int bytesRead)
29+
{
30+
Asn1Tag localTag = Asn1Tag.Decode(source, out int tagLength);
31+
32+
bool read = AsnDecoder.TryDecodeLength(
33+
source.Slice(tagLength),
34+
ruleSet,
35+
out parsedLength,
36+
out int lengthLength);
37+
38+
if (read)
39+
{
40+
tag = localTag;
41+
bytesRead = tagLength + lengthLength;
42+
}
43+
else
44+
{
45+
tag = default;
46+
bytesRead = default;
47+
}
1748

18-
private static ReadTagAndLengthDelegate ReadTagAndLength = (ReadTagAndLengthDelegate)
19-
typeof(AsnDecoder).GetMethod("ReadTagAndLength", BindingFlags.Static | BindingFlags.NonPublic)
20-
.CreateDelegate(typeof(ReadTagAndLengthDelegate));
49+
return read;
50+
}
2151

2252
[Theory]
2353
[InlineData(4, 0, "0400")]
@@ -39,6 +69,13 @@ public static void MinimalPrimitiveLength(int tagValue, int length, string input
3969
Assert.False(tag.IsConstructed, "tag.IsConstructed");
4070
Assert.Equal(tagValue, tag.TagValue);
4171
Assert.Equal(length, parsedLength.Value);
72+
73+
Assert.True(TryReadTagAndLength(inputBytes, rules, out tag, out parsedLength, out bytesRead));
74+
75+
Assert.Equal(inputBytes.Length, bytesRead);
76+
Assert.False(tag.IsConstructed, "tag.IsConstructed");
77+
Assert.Equal(tagValue, tag.TagValue);
78+
Assert.Equal(length, parsedLength.Value);
4279
}
4380
}
4481

@@ -51,10 +88,64 @@ public static void ReadWithUnknownRuleSet(int invalidRuleSetValue)
5188

5289
Assert.Throws<ArgumentOutOfRangeException>(
5390
() => new AsnReader(data, (AsnEncodingRules)invalidRuleSetValue));
91+
92+
Assert.Throws<ArgumentOutOfRangeException>(
93+
() => ReadTagAndLength(data, (AsnEncodingRules)invalidRuleSetValue, out _, out _));
94+
95+
Assert.Throws<ArgumentOutOfRangeException>(
96+
() => TryReadTagAndLength(data, (AsnEncodingRules)invalidRuleSetValue, out _, out _, out _));
97+
}
98+
99+
private static void ReadValid(
100+
ReadOnlySpan<byte> source,
101+
AsnEncodingRules ruleSet,
102+
int? expectedLength,
103+
int expectedBytesRead = -1)
104+
{
105+
if (expectedBytesRead < 0)
106+
{
107+
expectedBytesRead = source.Length;
108+
}
109+
110+
ReadTagAndLength(
111+
source,
112+
ruleSet,
113+
out int? length,
114+
out int bytesRead);
115+
116+
Assert.Equal(expectedBytesRead, bytesRead);
117+
Assert.Equal(expectedLength, length);
118+
119+
bool read = TryReadTagAndLength(
120+
source,
121+
ruleSet,
122+
out _,
123+
out length,
124+
out bytesRead);
125+
126+
Assert.True(read);
127+
Assert.Equal(expectedBytesRead, bytesRead);
128+
Assert.Equal(expectedLength, length);
129+
}
130+
131+
private static void ReadInvalid(byte[] source, AsnEncodingRules ruleSet)
132+
{
133+
Assert.Throws<AsnContentException>(
134+
() => ReadTagAndLength(source, ruleSet, out _, out _));
135+
136+
Asn1Tag tag;
137+
int? decodedLength;
138+
int bytesConsumed;
139+
140+
Assert.False(
141+
TryReadTagAndLength(source, ruleSet, out tag, out decodedLength, out bytesConsumed));
142+
143+
Assert.True(tag == default);
144+
Assert.Null(decodedLength);
145+
Assert.Equal(0, bytesConsumed);
54146
}
55147

56148
[Theory]
57-
[InlineData("")]
58149
[InlineData("05")]
59150
[InlineData("0481")]
60151
[InlineData("048201")]
@@ -64,18 +155,16 @@ public static void ReadWithInsufficientData(string inputHex)
64155
{
65156
byte[] inputData = inputHex.HexToByteArray();
66157

67-
Assert.Throws<AsnContentException>(
68-
() => ReadTagAndLength(inputData, AsnEncodingRules.DER, out _, out _));
158+
ReadInvalid(inputData, AsnEncodingRules.BER);
159+
ReadInvalid(inputData, AsnEncodingRules.CER);
160+
ReadInvalid(inputData, AsnEncodingRules.DER);
69161
}
70162

71163
[Theory]
72164
[InlineData("DER indefinite constructed", AsnEncodingRules.DER, "3080" + "0500" + "0000")]
73165
[InlineData("0xFF-BER", AsnEncodingRules.BER, "04FF")]
74166
[InlineData("0xFF-CER", AsnEncodingRules.CER, "04FF")]
75167
[InlineData("0xFF-DER", AsnEncodingRules.DER, "04FF")]
76-
[InlineData("CER definite constructed", AsnEncodingRules.CER, "30820500")]
77-
[InlineData("BER indefinite primitive", AsnEncodingRules.BER, "0480" + "0000")]
78-
[InlineData("CER indefinite primitive", AsnEncodingRules.CER, "0480" + "0000")]
79168
[InlineData("DER indefinite primitive", AsnEncodingRules.DER, "0480" + "0000")]
80169
[InlineData("DER non-minimal 0", AsnEncodingRules.DER, "048100")]
81170
[InlineData("DER non-minimal 7F", AsnEncodingRules.DER, "04817F")]
@@ -102,10 +191,28 @@ public static void InvalidLengths(
102191
{
103192
_ = description;
104193
byte[] inputData = inputHex.HexToByteArray();
105-
AsnReader reader = new AsnReader(inputData, rules);
106194

107-
Assert.Throws<AsnContentException>(
108-
() => ReadTagAndLength(inputData, rules, out _, out _));
195+
ReadInvalid(inputData, rules);
196+
}
197+
198+
[Theory]
199+
[InlineData("CER definite constructed", AsnEncodingRules.CER, 0x0500, 4, "30820500")]
200+
[InlineData("BER indefinite primitive", AsnEncodingRules.BER, null, 2, "0480" + "0000")]
201+
[InlineData("CER indefinite primitive", AsnEncodingRules.CER, null, 2, "0480" + "0000")]
202+
public static void ContextuallyInvalidLengths(
203+
string description,
204+
AsnEncodingRules rules,
205+
int? expectedLength,
206+
int expectedBytesRead,
207+
string inputHex)
208+
{
209+
// These inputs will all throw from AsnDecoder.ReadTagAndLength, but require
210+
// the tag as context.
211+
212+
_ = description;
213+
byte[] inputData = inputHex.HexToByteArray();
214+
215+
ReadValid(inputData, rules, expectedLength, expectedBytesRead);
109216
}
110217

111218
[Theory]
@@ -117,18 +224,8 @@ public static void IndefiniteLength(AsnEncodingRules ruleSet)
117224
// NULL
118225
// End-of-Contents
119226
byte[] data = { 0x30, 0x80, 0x05, 0x00, 0x00, 0x00 };
120-
AsnReader reader = new AsnReader(data, ruleSet);
121-
122-
Asn1Tag tag = ReadTagAndLength(
123-
data,
124-
ruleSet,
125-
out int? length,
126-
out int bytesRead);
127227

128-
Assert.Equal(2, bytesRead);
129-
Assert.False(length.HasValue, "length.HasValue");
130-
Assert.Equal((int)UniversalTagNumber.Sequence, tag.TagValue);
131-
Assert.True(tag.IsConstructed, "tag.IsConstructed");
228+
ReadValid(data, ruleSet, null, 2);
132229
}
133230

134231
[Theory]
@@ -138,42 +235,25 @@ public static void IndefiniteLength(AsnEncodingRules ruleSet)
138235
public static void BerNonMinimalLength(int expectedLength, string inputHex)
139236
{
140237
byte[] inputData = inputHex.HexToByteArray();
141-
AsnReader reader = new AsnReader(inputData, AsnEncodingRules.BER);
142238

143-
Asn1Tag tag = ReadTagAndLength(
144-
inputData,
145-
AsnEncodingRules.BER,
146-
out int? length,
147-
out int bytesRead);
148-
149-
Assert.Equal(inputData.Length, bytesRead);
150-
Assert.Equal(expectedLength, length.Value);
151-
// ReadTagAndLength doesn't move the _data span forward.
152-
Assert.True(reader.HasData, "reader.HasData");
239+
ReadValid(inputData, AsnEncodingRules.BER, expectedLength);
240+
ReadInvalid(inputData, AsnEncodingRules.CER);
241+
ReadInvalid(inputData, AsnEncodingRules.DER);
153242
}
154243

155244
[Theory]
156-
[InlineData(AsnEncodingRules.BER, 4, 0, 5, "0483000000" + "0500")]
157-
[InlineData(AsnEncodingRules.DER, 1, 1, 2, "0101" + "FF")]
158-
[InlineData(AsnEncodingRules.CER, 0x10, null, 2, "3080" + "0500" + "0000")]
245+
[InlineData(AsnEncodingRules.BER, 0, 5, "0483000000" + "0500")]
246+
[InlineData(AsnEncodingRules.DER, 1, 2, "0101" + "FF")]
247+
[InlineData(AsnEncodingRules.CER, null, 2, "3080" + "0500" + "0000")]
159248
public static void ReadWithDataRemaining(
160249
AsnEncodingRules ruleSet,
161-
int tagValue,
162250
int? expectedLength,
163251
int expectedBytesRead,
164252
string inputHex)
165253
{
166254
byte[] inputData = inputHex.HexToByteArray();
167255

168-
Asn1Tag tag = ReadTagAndLength(
169-
inputData,
170-
ruleSet,
171-
out int? length,
172-
out int bytesRead);
173-
174-
Assert.Equal(expectedBytesRead, bytesRead);
175-
Assert.Equal(tagValue, tag.TagValue);
176-
Assert.Equal(expectedLength, length);
256+
ReadValid(inputData, ruleSet, expectedLength, expectedBytesRead);
177257
}
178258
}
179259
}

0 commit comments

Comments
 (0)