-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Description
Background and motivation
Today .NET has two ways to read ASN.1 encoded data.
AsnDecoder, which is a stateless decoder that can read slices of data and tell you how much is read. It operates onReadOnlySpan<byte>.AsnReaderwhich is a "higher abstracted", stateful reader. It operating onReadOnlyMemory<byte>and works on top ofAsnDecoder.
If you want a stateful decoder that works on ReadOnlySpan<byte>, you have to build it yourself on top of AsnDecoder.
Within .NET Libraries, that is exactly what we have done with the internal AsnValueReader.
This type is very useful as it makes working with ReadOnlySpan<byte> of ASN.1 encoded data easier. Because our own public APIs want to accept ReadOnlySpan<byte>, we need a way to read ReadOnlySpan<byte> of ASN.1 data. Other library authors are in a similar situation. If they want to expose ReadOnlySpan<byte> APIs and then interpret it as ASN.1, they are forced to use AsnDecoder, which is low level, or build their own abstraction.
The .NET Libraries in fact vastly prefer to use the internal AsnValueReader instead of the public AsnReader. This to me indicates there is a gap in the public API.
I propose then that we expose an AsnValueReader. Its API shape is identical to AsnReader, save for the fact that it accepts and returns ReadOnlySpan<byte> instead of ReadOnlyMemory<byte>, does not need a Clone, and it returns AsnValueReader for child readers.
API Proposal
namespace System.Formats.Asn1;
public partial ref struct AsnValueReader {
public AsnValueReader(ReadOnlySpan<byte> data, AsnEncodingRules ruleSet, AsnReaderOptions options = default);
public readonly bool HasData { get; }
public readonly AsnEncodingRules RuleSet { get; }
// Clone is not needed since AsnValueReader is a struct and can be cloned with copy-by-value.
// We can have it for consistency sake, if we really want.
//public AsnValueReader Clone();
public readonly ReadOnlySpan<byte> PeekContentBytes();
public readonly ReadOnlySpan<byte> PeekEncodedValue();
public readonly Asn1Tag PeekTag();
public readonly void ThrowIfNotEmpty();
public byte[] ReadBitString(out int unusedBitCount, Asn1Tag? expectedTag = default);
public bool TryReadPrimitiveBitString(out int unusedBitCount, out ReadOnlySpan<byte> value, Asn1Tag? expectedTag = default);
public bool TryReadBitString(Span<byte> destination, out int unusedBitCount, out int bytesWritten, Asn1Tag? expectedTag = default);
public bool ReadBoolean(Asn1Tag? expectedTag = default);
public string ReadCharacterString(UniversalTagNumber encodingType, Asn1Tag? expectedTag = default);
public bool TryReadCharacterString(Span<char> destination, UniversalTagNumber encodingType, out int charsWritten, Asn1Tag? expectedTag = default);
public bool TryReadCharacterStringBytes(Span<byte> destination, Asn1Tag expectedTag, out int bytesWritten);
public bool TryReadPrimitiveCharacterStringBytes(Asn1Tag expectedTag, out ReadOnlySpan<byte> contents);
public ReadOnlySpan<byte> ReadEncodedValue();
public ReadOnlySpan<byte> ReadEnumeratedBytes(Asn1Tag? expectedTag = default);
public Enum ReadEnumeratedValue(Type enumType, Asn1Tag? expectedTag = default);
public TEnum ReadEnumeratedValue<TEnum>(Asn1Tag? expectedTag = default) where TEnum : Enum;
public DateTimeOffset ReadGeneralizedTime(Asn1Tag? expectedTag = default);
public BigInteger ReadInteger(Asn1Tag? expectedTag = default);
public ReadOnlySpan<byte> ReadIntegerBytes(Asn1Tag? expectedTag = default);
public bool TryReadInt32(out int value, Asn1Tag? expectedTag = default);
public bool TryReadInt64(out long value, Asn1Tag? expectedTag = default);
[CLSCompliantAttribute(false)]
public bool TryReadUInt32(out uint value, Asn1Tag? expectedTag = default);
[CLSCompliantAttribute(false)]
public bool TryReadUInt64(out ulong value, Asn1Tag? expectedTag = default);
public BitArray ReadNamedBitList(Asn1Tag? expectedTag = default);
public Enum ReadNamedBitListValue(Type flagsEnumType, Asn1Tag? expectedTag = default);
public TFlagsEnum ReadNamedBitListValue<TFlagsEnum>(Asn1Tag? expectedTag = default) where TFlagsEnum : Enum;
public void ReadNull(Asn1Tag? expectedTag = default);
public string ReadObjectIdentifier(Asn1Tag? expectedTag = default);
public byte[] ReadOctetString(Asn1Tag? expectedTag = default);
public bool TryReadOctetString(Span<byte> destination, out int bytesWritten, Asn1Tag? expectedTag = default);
public bool TryReadPrimitiveOctetString(out ReadOnlySpan<byte> contents, Asn1Tag? expectedTag = default);
public AsnValueReader ReadSequence(Asn1Tag? expectedTag = default);
public AsnValueReader ReadSetOf(bool skipSortOrderValidation, Asn1Tag? expectedTag = default);
public AsnValueReader ReadSetOf(Asn1Tag? expectedTag = default);
public DateTimeOffset ReadUtcTime(int twoDigitYearMax, Asn1Tag? expectedTag = default);
public DateTimeOffset ReadUtcTime(Asn1Tag? expectedTag = default);
}API Usage
It will be used the same as AsnReader.
Alternative Designs
I don't feel strongly about the name. We could call it AsnValueReader, ValueAsnReader, AsnRefReader, etc.
If we want to keep the "reader" APIs on AsnReader, we could instead introduce static APIs on it that accept a ref struct. Something like
public ref struct AsnValue {
public AsnValue(ReadOnlySpan<byte> data, AsnEncodingRules ruleSet, AsnReaderOptions options = default);
public bool HasData { get; }
public AsnEncodingRules RuleSet { get; }
}
public partial class AsnReader { // Or on AsnDecoder
public static ReadOnlySpan<byte> PeekContentBytes(ref readonly AsnValue asnValue); // Peek can be readonly
public static string ReadObjectIdentifier(ref AsnValue asnValue, Asn1Tag? expectedTag = default);
// Returns a new AsnValue which is a slice into the sequence
public static AsnValue ReadSequence(ref AsnValue value, Asn1Tag? expectedTag = default);
}
// etcI don't love this because it does not meaningfully improve the situation over AsnDecoder, and it would make the sub-readers like ReadSequence be a little awkward. We have instance methods for a reason, so we should use them.
Risks
The largest risk with this is that since it is a mutable struct, the ergonomics of this make it a tad easy to clone it unintentionally. Such example might be forgetting to pass it by ref to a method, or the method having an API shape that forces the compiler to make a hidden copy. For example:
AsnValueReader reader; // Set up the reader
ConsumeSequence(reader);
// oops - reader is still unmodified by ConsumeSequence because we passed it by value
static void ConsumeSequence(AsnValueReader reader)
{
AsnValueReader seq = reader.ReadSequence();
// Do stuff
}I think this risk is acceptable.