From 9ff32a2b5d3f510309534815df51b5d4105cbc40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 10 Dec 2024 14:27:11 +0200 Subject: [PATCH 01/66] added support for c# Span type by adding new methods that return Span instead of byte array and reduce allocations --- Asn1Parser/Asn1Reader.cs | 58 ++++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/Asn1Parser/Asn1Reader.cs b/Asn1Parser/Asn1Reader.cs index 0af9c60..62b4711 100644 --- a/Asn1Parser/Asn1Reader.cs +++ b/Asn1Parser/Asn1Reader.cs @@ -31,7 +31,7 @@ public class Asn1Reader { (Byte)Asn1Type.SET, (Byte)Asn1Type.SET | (Byte)Asn1Class.CONSTRUCTED ]; - readonly List _rawData = []; + Byte[] _rawData = []; readonly Dictionary _offsetMap = []; AsnInternalMap currentPosition; Int32 childCount; @@ -100,7 +100,7 @@ public Asn1Reader(Byte[] rawData) : this(rawData, 0) { } /// /// Gets the internal ASN.1 stream length in bytes. /// - public Int32 Length => _rawData.Count; + public Int32 Length => _rawData.Length; /// /// Gets next structure's offset at same level (next sibling). /// @@ -124,16 +124,14 @@ void decode(Byte[]? raw, Int32 pOffset) { IsConstructed = false; childCount = 0; if (raw != null) { - _rawData.Clear(); - _rawData.AddRange(raw); + _rawData = raw; } Offset = pOffset; Tag = _rawData[Offset]; calculateLength(); // strip possible unnecessary bytes - if (raw != null && TagLength != _rawData.Count) { - _rawData.Clear(); - _rawData.AddRange(raw.Take(TagLength).ToArray()); + if (raw != null && TagLength != _rawData.Length) { + _rawData = raw.Take(TagLength).ToArray(); } TagName = GetTagName(Tag); // 0 Tag is reserved for BER and is not available in DER @@ -148,7 +146,7 @@ void decode(Byte[]? raw, Int32 pOffset) { } if (PayloadLength == 0) { // if current node is the last node in binary data, set NextOffset to 0, this means EOF. - NextOffset = Offset + TagLength == _rawData.Count + NextOffset = Offset + TagLength == _rawData.Length ? 0 : Offset + TagLength; NextSiblingOffset = currentPosition.LevelEnd == 0 || Offset - currentPosition.LevelStart + TagLength == currentPosition.LevelEnd @@ -165,7 +163,7 @@ void decode(Byte[]? raw, Int32 pOffset) { // skip unused bits byte ? PayloadStartOffset + 1 : PayloadStartOffset - : Offset + TagLength < _rawData.Count + : Offset + TagLength < _rawData.Length ? Offset + TagLength : 0; } @@ -212,7 +210,7 @@ Boolean validateArrayBoundaries(Int64 start) { if (start > Int32.MaxValue) { return false; } - return start >= 0 && start < _rawData.Count && _rawData[(Int32)start] != 0; + return start >= 0 && start < _rawData.Length && _rawData[(Int32)start] != 0; } /// /// Checks if current primitive type is sub-typed (contains nested types) or not. @@ -288,7 +286,7 @@ void calculateLength() { /// Start offset for suggested nested type. /// Estimated full tag length for nested type. Int64 calculatePredictLength(Int64 offset) { - if (offset + 1 >= _rawData.Count || offset < 0) { + if (offset + 1 >= _rawData.Length || offset < 0) { return Int32.MaxValue; } @@ -297,7 +295,7 @@ Int64 calculatePredictLength(Int64 offset) { } Int32 lengthBytes = _rawData[(Int32)(offset + 1)] - 128; // max length can be encoded by using 4 bytes. - if (lengthBytes > 4 || offset + 2 >= _rawData.Count) { + if (lengthBytes > 4 || offset + 2 >= _rawData.Length) { return Int32.MaxValue; } Int32 pPayloadLength = _rawData[(Int32)(offset + 2)]; @@ -334,9 +332,19 @@ public Byte[] GetHeader() { for (Int32 i = 0; i < headerLength; i++) { array[i] = _rawData[Offset + i]; } + return array; } /// + /// Gets current structure header. Header contains tag and tag length byte (or bytes). + /// + /// Current structure header. Header contains tag and tag length byte (or bytes). + public ReadOnlySpan GetHeaderAsSpan() { + Int32 headerLength = PayloadStartOffset - Offset; + + return _rawData.AsSpan(Offset, headerLength); + } + /// /// Gets the byte array of the current structure's payload. /// /// Byte array of the current structure's payload @@ -348,6 +356,13 @@ public Byte[] GetPayload() { return array; } /// + /// Gets the byte array of the current structure's payload. + /// + /// Memory span of the current structure's payload. + public ReadOnlySpan GetPayloadAsSpan() { + return _rawData.AsSpan(PayloadStartOffset, PayloadLength); + } + /// /// Gets the raw data of the tag, which includes tag, length bytes and payload. /// /// A full binary copy of the tag. @@ -359,17 +374,31 @@ public Byte[] GetTagRawData() { return array; } /// + /// Gets the raw data of the tag, which includes tag, length bytes and payload. + /// + /// A full binary copy of the tag. + public ReadOnlySpan GetTagRawDataAsSpan() { + return _rawData.AsSpan(Offset, TagLength); + } + /// /// Gets a copy of internal ASN.1 stream. The size of the stream is equals to member value. /// /// A full binary copy of the internal byte stream. public Byte[] GetRawData() { - Byte[] array = new Byte[_rawData.Count]; - for (Int32 i = 0; i < _rawData.Count; i++) { + Byte[] array = new Byte[_rawData.Length]; + for (Int32 i = 0; i < _rawData.Length; i++) { array[i] = _rawData[i]; } return array; } /// + /// Gets a copy of internal ASN.1 stream. The size of the stream is equals to member value. + /// + /// A full binary copy of the internal byte stream. + public ReadOnlySpan GetRawDataAsSpan() { + return _rawData.AsSpan(); + } + /// /// Gets the count of nested nodes under node in the current position. /// /// Count of nested nodes. @@ -525,6 +554,7 @@ public Boolean Seek(Int32 newPosition) { } currentPosition = value; decode(null, newPosition); + return true; } From 5f34a083ffc0ffa906a90173a45e6ca72c9f2025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 10 Dec 2024 14:29:51 +0200 Subject: [PATCH 02/66] updated AsnFormatter by using Span and added new BinaryToString overload. --- Asn1Parser/AsnFormatter.cs | 62 +++++++++++++----- Asn1Parser/BinaryToStringFormatter.cs | 94 +++++++++++---------------- 2 files changed, 86 insertions(+), 70 deletions(-) diff --git a/Asn1Parser/AsnFormatter.cs b/Asn1Parser/AsnFormatter.cs index c9774a3..2b46815 100644 --- a/Asn1Parser/AsnFormatter.cs +++ b/Asn1Parser/AsnFormatter.cs @@ -38,20 +38,52 @@ public static class AsnFormatter { /// /// public static String BinaryToString(Byte[] rawData, EncodingType encoding = EncodingType.HexRaw, EncodingFormat format = EncodingFormat.CRLF, Int32 start = 0, Int32 count = 0, Boolean forceUpperCase = false) { + Span slice = count == 0 ? rawData.AsSpan() : rawData.AsSpan().Slice(start, count); + return BinaryToString(slice, encoding, format, forceUpperCase); + } + /// + /// Converts and formats byte array to a string. See for encoding examples. + /// + /// Byte array to format. + /// Specifies the encoding for formatting. Default is HexRaw + /// + /// Specifies the encoding options. The default behavior is to use a carriage return/line feed + /// (CR/LF) pair (0x0D/0x0A) to represent a new line. + /// + /// + /// Specifies whether the force hex octet representation in upper case. Default is lower case. + /// + /// This parameter has effect only when hex encoding is selected in the encoding parameter: + /// Hex, HexRaw, HexAddress, HexAscii + /// and HexAsciiAddress. For other values, this parameter is silently ignored. + /// + /// + /// An invalid encoding type was specified. + /// Encoded and formatted string. + /// + /// This method do not support the following encoding types: + /// + /// Binary + /// Base64Any + /// StringAny + /// HexAny + /// + /// + public static String BinaryToString(ReadOnlySpan rawData, EncodingType encoding = EncodingType.HexRaw, EncodingFormat format = EncodingFormat.CRLF, Boolean forceUpperCase = false) { if (rawData == null || rawData.Length == 0) { return String.Empty; } if (PemHeader.ContainsEncoding(encoding)) { - return BinaryToStringFormatter.ToBase64(rawData, encoding, format, start, count); + return BinaryToStringFormatter.ToBase64(rawData, encoding, format); } return encoding switch { - EncodingType.Base64 => BinaryToStringFormatter.ToBase64(rawData, encoding, format, start, count), - EncodingType.Hex => BinaryToStringFormatter.ToHex(rawData, format, start, count, forceUpperCase), - EncodingType.HexAddress => BinaryToStringFormatter.ToHexAddress(rawData, format, start, count, forceUpperCase), - EncodingType.HexAscii => BinaryToStringFormatter.ToHexAscii(rawData, format, start, count, forceUpperCase), - EncodingType.HexAsciiAddress => BinaryToStringFormatter.ToHexAddressAndAscii(rawData, format, start, count, forceUpperCase), - EncodingType.HexRaw => BinaryToStringFormatter.ToHexRaw(rawData, start, count, forceUpperCase), + EncodingType.Base64 => BinaryToStringFormatter.ToBase64(rawData, encoding, format), + EncodingType.Hex => BinaryToStringFormatter.ToHex(rawData, format, forceUpperCase), + EncodingType.HexAddress => BinaryToStringFormatter.ToHexAddress(rawData, format, forceUpperCase), + EncodingType.HexAscii => BinaryToStringFormatter.ToHexAscii(rawData, format, forceUpperCase), + EncodingType.HexAsciiAddress => BinaryToStringFormatter.ToHexAddressAndAscii(rawData, format, forceUpperCase), + EncodingType.HexRaw => BinaryToStringFormatter.ToHexRaw(rawData, forceUpperCase), _ => throw new ArgumentException("Specified encoding is invalid.") }; } @@ -91,19 +123,19 @@ public static String BinaryToString(Asn1Reader asn, EncodingType encoding = Enco return String.Empty; } if ((Int32)encoding > 20 && (Int32)encoding < 45) { - return BinaryToStringFormatter.ToBase64(asn.GetRawData(), encoding, format, asn.PayloadStartOffset, asn.PayloadLength); + return BinaryToStringFormatter.ToBase64(asn.GetRawDataAsSpan(), encoding, format); } if (PemHeader.ContainsEncoding(encoding)) { - return BinaryToStringFormatter.ToBase64(asn.GetRawData(), encoding, format, asn.PayloadStartOffset, asn.PayloadLength); + return BinaryToStringFormatter.ToBase64(asn.GetRawDataAsSpan(), encoding, format); } return encoding switch { - EncodingType.Base64 => BinaryToStringFormatter.ToBase64(asn.GetRawData(), encoding, format, asn.PayloadStartOffset, asn.PayloadLength), - EncodingType.Hex => BinaryToStringFormatter.ToHex(asn.GetRawData(), format, asn.PayloadStartOffset, asn.PayloadLength, forceUpperCase), - EncodingType.HexAddress => BinaryToStringFormatter.ToHexAddress(asn.GetRawData(), format, asn.PayloadStartOffset, asn.PayloadLength, forceUpperCase), - EncodingType.HexAscii => BinaryToStringFormatter.ToHexAscii(asn.GetRawData(), format, asn.PayloadStartOffset, asn.PayloadLength, forceUpperCase), - EncodingType.HexAsciiAddress => BinaryToStringFormatter.ToHexAddressAndAscii(asn.GetRawData(), format, asn.PayloadStartOffset, asn.PayloadLength, forceUpperCase), - EncodingType.HexRaw => BinaryToStringFormatter.ToHexRaw(asn.GetRawData(), asn.PayloadStartOffset, asn.PayloadLength, forceUpperCase), + EncodingType.Base64 => BinaryToStringFormatter.ToBase64(asn.GetRawDataAsSpan(), encoding, format), + EncodingType.Hex => BinaryToStringFormatter.ToHex(asn.GetRawDataAsSpan(), format, forceUpperCase), + EncodingType.HexAddress => BinaryToStringFormatter.ToHexAddress(asn.GetRawDataAsSpan(), format, forceUpperCase), + EncodingType.HexAscii => BinaryToStringFormatter.ToHexAscii(asn.GetRawDataAsSpan(), format, forceUpperCase), + EncodingType.HexAsciiAddress => BinaryToStringFormatter.ToHexAddressAndAscii(asn.GetRawDataAsSpan(), format, forceUpperCase), + EncodingType.HexRaw => BinaryToStringFormatter.ToHexRaw(asn.GetRawDataAsSpan(), forceUpperCase), _ => throw new ArgumentException("Specified encoding is invalid.") }; } diff --git a/Asn1Parser/BinaryToStringFormatter.cs b/Asn1Parser/BinaryToStringFormatter.cs index 12d4746..8a2aa3f 100644 --- a/Asn1Parser/BinaryToStringFormatter.cs +++ b/Asn1Parser/BinaryToStringFormatter.cs @@ -1,31 +1,27 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Text; namespace SysadminsLV.Asn1Parser; static class BinaryToStringFormatter { - public static String ToHexRaw(IReadOnlyList rawData, Int32 start, Int32 count, Boolean forceUpperCase) { - count = getCount(rawData.Count, start, count); + public static String ToHexRaw(ReadOnlySpan rawData, Boolean forceUpperCase) { var SB = new StringBuilder(); - for (Int32 i = start; i < start + count; i++) { - byteToHexOctet(SB, rawData[i], forceUpperCase); + foreach (Byte b in rawData) { + byteToHexOctet(SB, b, forceUpperCase); } + return SB.ToString(); } - public static String ToHex(IReadOnlyList rawData, EncodingFormat format, Int32 start, Int32 count, Boolean forceUpperCase) { - count = getCount(rawData.Count, start, count); + public static String ToHex(ReadOnlySpan rawData, EncodingFormat format, Boolean forceUpperCase) { var sb = new StringBuilder(); - Int32 n = 0; - for (Int32 index = start; index < start + count; index++) { - n++; + for (Int32 index = 0; index < rawData.Length; index++) { byteToHexOctet(sb, rawData[index], forceUpperCase); - if (index == start) { + if (index == 0) { sb.Append(" "); continue; } - if (n % 16 == 0) { + if ((index + 1) % 16 == 0) { + // if current octet is the last octet in a row, append EOL format switch (format) { case EncodingFormat.NOCRLF: sb.Append(" "); @@ -35,7 +31,7 @@ public static String ToHex(IReadOnlyList rawData, EncodingFormat format, I case EncodingFormat.NOCR: sb.Append("\n"); break; } - } else if (n % 8 == 0 && format != EncodingFormat.NOCRLF) { + } else if ((index + 1) % 8 == 0 && format != EncodingFormat.NOCRLF) { sb.Append(" "); } else { sb.Append(" "); @@ -44,13 +40,12 @@ public static String ToHex(IReadOnlyList rawData, EncodingFormat format, I return finalizeBinaryToString(sb, format); } - public static String ToHexAddress(IReadOnlyList rawData, EncodingFormat format, Int32 start, Int32 count, Boolean forceUpperCase) { - count = getCount(rawData.Count, start, count); + public static String ToHexAddress(ReadOnlySpan rawData, EncodingFormat format, Boolean forceUpperCase) { var sb = new StringBuilder(); - Int32 rowCount = 0, n = 0; - Int32 addrLength = getAddrLength(rawData.Count); - for (Int32 index = start; index < start + count; index++) { - if (n % 16 == 0) { + Int32 rowCount = 0; + Int32 addrLength = getAddrLength(rawData.Length); + for (Int32 index = 0; index < rawData.Length; index++) { + if (index % 16 == 0) { String hexAddress = Convert.ToString(rowCount, 16).PadLeft(addrLength, '0'); if (forceUpperCase) { hexAddress = hexAddress.ToUpper(); @@ -60,66 +55,64 @@ public static String ToHexAddress(IReadOnlyList rawData, EncodingFormat fo rowCount += 16; } byteToHexOctet(sb, rawData[index], forceUpperCase); - if (index == start) { + if (index == 0) { sb.Append(" "); - n++; continue; } - if ((n + 1) % 16 == 0) { + + if ((index + 1) % 16 == 0) { + // if current octet is the last octet in a row, append EOL format sb.Append(format == EncodingFormat.NOCR ? "\n" : "\r\n"); - } else if ((n + 1) % 8 == 0) { + } else if ((index + 1) % 8 == 0) { + // if current octet is center octet in a row, append extra space sb.Append(" "); } else { sb.Append(" "); } - n++; } return finalizeBinaryToString(sb, format); } - public static String ToHexAscii(Byte[] rawData, EncodingFormat format, Int32 start, Int32 count, Boolean forceUpperCase) { - count = getCount(rawData.Length, start, count); + public static String ToHexAscii(ReadOnlySpan rawData, EncodingFormat format, Boolean forceUpperCase) { var sb = new StringBuilder(); var ascii = new StringBuilder(8); - Int32 n = 0; - for (Int32 index = 0; index < start + count; index++) { - n++; + for (Int32 index = 0; index < rawData.Length; index++) { byteToHexOctet(sb, rawData[index], forceUpperCase); Char c = rawData[index] < 32 || rawData[index] > 126 ? '.' : (Char)rawData[index]; ascii.Append(c); - if (index == start) { + if (index == 0) { sb.Append(" "); continue; } - if (n % 16 == 0) { + if ((index + 1) % 16 == 0) { sb.Append(" "); sb.Append(ascii); ascii.Clear(); + // if current octet is the last octet in a row, append EOL format sb.Append(format == EncodingFormat.NOCR ? "\n" : "\r\n"); - } else if (n % 8 == 0) { + } else if ((index + 1) % 8 == 0) { sb.Append(" "); } else { sb.Append(" "); } // handle last byte to complete partial ASCII panel. - if (n == count) { - sb.Append(getAsciiPadding(n)); + if (index + 1 == rawData.Length) { + sb.Append(getAsciiPadding(index + 1)); sb.Append(ascii); } } return finalizeBinaryToString(sb, format); } - public static String ToHexAddressAndAscii(IReadOnlyList rawData, EncodingFormat format, Int32 start, Int32 count, Boolean forceUpperCase) { - count = getCount(rawData.Count, start, count); + public static String ToHexAddressAndAscii(ReadOnlySpan rawData, EncodingFormat format, Boolean forceUpperCase) { var sb = new StringBuilder(); var ascii = new StringBuilder(8); - Int32 addrLength = getAddrLength(rawData.Count); - Int32 rowCount = 0, n = 0; - for (Int32 index = 0; index < start + count; index++) { - if (n % 16 == 0) { + Int32 addrLength = getAddrLength(rawData.Length); + Int32 rowCount = 0; + for (Int32 index = 0; index < rawData.Length; index++) { + if (index % 16 == 0) { String hexAddress = Convert.ToString(rowCount, 16).PadLeft(addrLength, '0'); if (forceUpperCase) { hexAddress = hexAddress.ToUpper(); @@ -135,32 +128,29 @@ public static String ToHexAddressAndAscii(IReadOnlyList rawData, EncodingF ascii.Append(c); if (index == 0) { sb.Append(" "); - n++; continue; } - if ((n + 1) % 16 == 0) { + if ((index + 1) % 16 == 0) { sb.Append(" "); sb.Append(ascii); ascii.Clear(); sb.Append(format == EncodingFormat.NOCR ? "\n" : "\r\n"); - } else if ((n + 1) % 8 == 0) { + } else if ((index + 1) % 8 == 0) { sb.Append(" "); } else { sb.Append(" "); } // handle last byte to complete partial ASCII panel. - if (n + 1 == count) { + if (index + 1 == rawData.Length) { sb.Append(getAsciiPadding(index + 1)); sb.Append(ascii); } - n++; } return finalizeBinaryToString(sb, format); } - public static String ToBase64(IReadOnlyCollection rawData, EncodingType encoding, EncodingFormat format, Int32 start, Int32 count) { - count = getCount(rawData.Count, start, count); - var sb = new StringBuilder(Convert.ToBase64String(rawData.Skip(start).Take(count).ToArray())); + public static String ToBase64(ReadOnlySpan rawData, EncodingType encoding, EncodingFormat format) { + var sb = new StringBuilder(Convert.ToBase64String(rawData.ToArray())); String splitter; switch (format) { case EncodingFormat.NOCR: @@ -229,12 +219,6 @@ static String getAsciiPadding(Int32 index) { return new String(' ', (17 - remainder) * 3); } - static Int32 getCount(Int32 size, Int32 start, Int32 count) { - if (start < 0 || start >= size) { - throw new OverflowException(); - } - return count == 0 || start + count > size ? size - start : count; - } static Int32 getAddrLength(Int32 size) { Int32 div = size / 16; if (size % 16 > 0) { div++; } From 8822573aa139945a3f5028c73f1628cd519ddd40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 10 Dec 2024 14:31:49 +0200 Subject: [PATCH 03/66] added Asn1Utils.Encode overload that accepts and returns Span --- Asn1Parser/Asn1Utils.cs | 53 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/Asn1Parser/Asn1Utils.cs b/Asn1Parser/Asn1Utils.cs index 941932f..ca56be4 100644 --- a/Asn1Parser/Asn1Utils.cs +++ b/Asn1Parser/Asn1Utils.cs @@ -115,6 +115,59 @@ public static Byte[] Encode(Byte[]? rawData, Byte enclosingTag) { /// /// This method do not check whether the data in rawData is valid data for specified enclosing type. /// A byte array to wrap. + /// An enumeration of type represented as byte. + /// Wrapped encoded byte array. + /// If rawData is null, an empty tag is encoded. + public static Span Encode(ReadOnlySpan rawData, Byte enclosingTag) { + Byte[] retValue; + if (rawData == null) { + retValue = [enclosingTag, 0]; + + return retValue; + } + if (rawData.Length < 128) { + // pre-create destination array of fixed size. + retValue = new Byte[rawData.Length + 2]; + // populate TL components of TLV + retValue[0] = enclosingTag; + retValue[1] = (Byte)rawData.Length; + // copy input buffer to V component + for (Int32 index = 0; index < rawData.Length; index++) { + retValue[index + 2] = rawData[index]; + } + } else { + Byte[] lenBytes = new Byte[4]; + Int32 num = rawData.Length; + Int32 counter = 0; + while (num >= 256) { + lenBytes[counter] = (Byte)(num & 255); + num >>= 8; + counter++; + } + // 3 is: len byte and enclosing tag + // pre-create destination array of fixed size. + retValue = new Byte[rawData.Length + 3 + counter]; + // copy input buffer to V component + for (Int32 index = 0; index < rawData.Length; index++) { + retValue[index + 3 + counter] = rawData[index]; + } + // populate TL components of TLV + retValue[0] = enclosingTag; + retValue[1] = (Byte)(129 + counter); + retValue[2] = (Byte)num; + Int32 n = 3; + for (Int32 i = counter - 1; i >= 0; i--) { + retValue[n] = lenBytes[i]; + n++; + } + } + return retValue; + } + /// + /// Wraps encoded data to an ASN.1 type/structure. + /// + /// This method do not check whether the data in rawData is valid data for specified enclosing type. + /// A byte array to wrap. /// An enumeration of . /// Wrapped encoded byte array. /// If rawData is null, an empty tag is encoded. From dc1d5ad6069d1dc241761c60998d90f63abd5140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 10 Dec 2024 14:32:54 +0200 Subject: [PATCH 04/66] updated Asn1Utils by using new span-based formatter --- Asn1Parser/Asn1Utils.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Asn1Parser/Asn1Utils.cs b/Asn1Parser/Asn1Utils.cs index ca56be4..b17f330 100644 --- a/Asn1Parser/Asn1Utils.cs +++ b/Asn1Parser/Asn1Utils.cs @@ -207,21 +207,19 @@ static String decodeBitString(Asn1Reader asn) { "Unused bits: {0} : {1}", asn[asn.PayloadStartOffset], AsnFormatter.BinaryToString( - asn.GetRawData(), + asn.GetRawDataAsSpan().Slice(asn.PayloadStartOffset + 1, asn.PayloadLength - 1), EncodingType.HexRaw, - EncodingFormat.NOCRLF, - asn.PayloadStartOffset + 1, - asn.PayloadLength - 1) + EncodingFormat.NOCRLF) ); } static String decodeOctetString(Asn1Reader asn) { return AsnFormatter.BinaryToString( - asn.GetRawData(), + asn.GetRawDataAsSpan().Slice(asn.PayloadStartOffset, asn.PayloadLength), EncodingType.HexRaw, - EncodingFormat.NOCRLF, asn.PayloadStartOffset, asn.PayloadLength); + EncodingFormat.NOCRLF); } static String decodeAsciiString(Asn1Reader asn) { - return Encoding.ASCII.GetString(asn.GetRawData(), asn.PayloadStartOffset, asn.PayloadLength); + return Encoding.ASCII.GetString(asn.GetRawDataAsSpan().ToArray(), asn.PayloadStartOffset, asn.PayloadLength); } static String decodeUtcTime(Asn1Reader asn) { DateTime dt = new Asn1UtcTime(asn).Value; From b7b3b0c7ba5ce448b29cbf3732b018480075079c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 10 Dec 2024 14:36:10 +0200 Subject: [PATCH 05/66] fixed xml-docs by pointing to right overload. --- Asn1Parser/EncodingType.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Asn1Parser/EncodingType.cs b/Asn1Parser/EncodingType.cs index 918e4d2..16619c6 100644 --- a/Asn1Parser/EncodingType.cs +++ b/Asn1Parser/EncodingType.cs @@ -113,9 +113,9 @@ public enum EncodingType : UInt32 { /// Base64Header /// Base64 /// - /// BinaryToString method do not support this flag. + /// BinaryToString method do not support this flag. /// - Base64Any = 6, + Base64Any = 6, /// /// Tries the following, in order: /// @@ -123,9 +123,9 @@ public enum EncodingType : UInt32 { /// Base64 /// Binary /// - /// BinaryToString method do not support this flag. + /// BinaryToString method do not support this flag. /// - StringAny = 7, + StringAny = 7, /// /// /// Tries the following, in order: @@ -135,9 +135,9 @@ public enum EncodingType : UInt32 { /// HexRaw /// HexAscii /// - /// BinaryToString method do not support this flag. + /// BinaryToString method do not support this flag. /// - HexAny = 8, + HexAny = 8, /// /// Base64, with X.509 certificate revocation list (CRL) beginning and ending headers. /// From 7f1afe16561cd735818d5346be417e73b03d842c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 10 Dec 2024 14:40:03 +0200 Subject: [PATCH 06/66] updated xml-docs --- Asn1Parser/AsnFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Asn1Parser/AsnFormatter.cs b/Asn1Parser/AsnFormatter.cs index 2b46815..3d084d0 100644 --- a/Asn1Parser/AsnFormatter.cs +++ b/Asn1Parser/AsnFormatter.cs @@ -149,7 +149,7 @@ public static String BinaryToString(Asn1Reader asn, EncodingType encoding = Enco /// Original byte array. /// /// This method may not be fully compatible with - /// BinaryToString + /// BinaryToString /// method. /// /// If encoding parameter is set to Base64Header, the method will accept any properly formatted PEM header From deba5557b56dae21c1b5731dd5518718aa2a021c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 10 Dec 2024 14:46:17 +0200 Subject: [PATCH 07/66] added Asn1Universal.GetRawDataAsSpan method --- Asn1Parser/Universal/Asn1Universal.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Asn1Parser/Universal/Asn1Universal.cs b/Asn1Parser/Universal/Asn1Universal.cs index ac8393a..8249882 100644 --- a/Asn1Parser/Universal/Asn1Universal.cs +++ b/Asn1Parser/Universal/Asn1Universal.cs @@ -122,4 +122,11 @@ public virtual String Format(EncodingType encoding = EncodingType.Base64) { public Byte[] GetRawData() { return asnReader!.GetTagRawData(); } + /// + /// Gets the full tag raw data, including header and payload information. + /// + /// ASN.1-encoded type as span. + public Span GetRawDataAsSpan() { + return asnReader!.GetTagRawDataAsSpan(); + } } \ No newline at end of file From 60c502af618300795a524738695ee145353b2770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 10 Dec 2024 14:46:56 +0200 Subject: [PATCH 08/66] changed Span to ReadOnlySpan as it should be immutable --- Asn1Parser/Universal/Asn1Universal.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Asn1Parser/Universal/Asn1Universal.cs b/Asn1Parser/Universal/Asn1Universal.cs index 8249882..1f4039f 100644 --- a/Asn1Parser/Universal/Asn1Universal.cs +++ b/Asn1Parser/Universal/Asn1Universal.cs @@ -126,7 +126,7 @@ public Byte[] GetRawData() { /// Gets the full tag raw data, including header and payload information. /// /// ASN.1-encoded type as span. - public Span GetRawDataAsSpan() { + public ReadOnlySpan GetRawDataAsSpan() { return asnReader!.GetTagRawDataAsSpan(); } } \ No newline at end of file From f880d472dab5916770b994a00b76a97dc6c0a908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Wed, 11 Dec 2024 13:23:10 +0200 Subject: [PATCH 09/66] replaced Span with Memory --- Asn1Parser/Asn1Parser.csproj | 5 +- Asn1Parser/Asn1Reader.cs | 89 +++++++++++++++------------ Asn1Parser/Asn1Utils.cs | 6 +- Asn1Parser/AsnFormatter.cs | 16 ++--- Asn1Parser/Universal/Asn1Universal.cs | 4 +- 5 files changed, 68 insertions(+), 52 deletions(-) diff --git a/Asn1Parser/Asn1Parser.csproj b/Asn1Parser/Asn1Parser.csproj index 17d227d..178d657 100644 --- a/Asn1Parser/Asn1Parser.csproj +++ b/Asn1Parser/Asn1Parser.csproj @@ -15,7 +15,7 @@ true enable True - strongname.snk + bin\Release\netstandard2.0\SysadminsLV.Asn1Parser.xml @@ -26,4 +26,7 @@ + + + diff --git a/Asn1Parser/Asn1Reader.cs b/Asn1Parser/Asn1Reader.cs index 62b4711..2bfc98d 100644 --- a/Asn1Parser/Asn1Reader.cs +++ b/Asn1Parser/Asn1Reader.cs @@ -31,7 +31,7 @@ public class Asn1Reader { (Byte)Asn1Type.SET, (Byte)Asn1Type.SET | (Byte)Asn1Class.CONSTRUCTED ]; - Byte[] _rawData = []; + ReadOnlyMemory _rawData; readonly Dictionary _offsetMap = []; AsnInternalMap currentPosition; Int32 childCount; @@ -44,7 +44,7 @@ public class Asn1Reader { /// /// This constructor creates a copy of a current position of an existing ASN1 object. /// - public Asn1Reader(Asn1Reader asn) : this(asn.GetTagRawData()) { } + public Asn1Reader(Asn1Reader asn) : this(asn.GetTagRawDataAsMemory(), 0, true) { } /// /// Initializes a new instance of the ASN1 class by using an ASN.1 encoded byte array. /// @@ -60,17 +60,26 @@ public Asn1Reader(Asn1Reader asn) : this(asn.GetTagRawData()) { } /// required bytes from input data. /// public Asn1Reader(Byte[] rawData) : this(rawData, 0) { } + /// + /// Initializes a new instance of the ASN1 class by using an ASN.1 encoded byte array. + /// + /// ASN.1-encoded byte array. + /// + /// The data in the rawData parameter is not valid ASN sequence. + /// + /// + /// If rawData size is greater than outer structure size, constructor will take only + /// required bytes from input data. + /// + public Asn1Reader(ReadOnlyMemory rawData) : this(rawData, 0) { } - Asn1Reader(Byte[] rawData, Int32 offset) { - if (rawData == null) { - throw new ArgumentNullException(nameof(rawData)); - } + Asn1Reader(ReadOnlyMemory rawData, Int32 offset, Boolean skipCopy = false) { if (rawData.Length < 2) { throw new Win32Exception(ErrorCode.InvalidDataException); } currentPosition = new AsnInternalMap(0, 0); _offsetMap.Add(0, currentPosition); - decode(rawData, offset); + decode(rawData, offset, skipCopy); } /// @@ -118,20 +127,24 @@ public Asn1Reader(Byte[] rawData) : this(rawData, 0) { } /// /// Binary array index to access. /// index parameter is outside of binary array boundaries. - public Byte this[Int32 index] => _rawData[index]; + public Byte this[Int32 index] => _rawData.Span[index]; - void decode(Byte[]? raw, Int32 pOffset) { + void decode(ReadOnlyMemory raw, Int32 pOffset, Boolean skipCopy = false) { IsConstructed = false; childCount = 0; - if (raw != null) { - _rawData = raw; + if (!raw.IsEmpty) { + if (skipCopy) { + _rawData = raw; + } else { + _rawData = raw.ToArray(); + } } Offset = pOffset; - Tag = _rawData[Offset]; + Tag = _rawData.Span[Offset]; calculateLength(); // strip possible unnecessary bytes - if (raw != null && TagLength != _rawData.Length) { - _rawData = raw.Take(TagLength).ToArray(); + if (!raw.IsEmpty && TagLength != _rawData.Length) { + _rawData = raw.Slice(0, TagLength).ToArray(); } TagName = GetTagName(Tag); // 0 Tag is reserved for BER and is not available in DER @@ -210,7 +223,7 @@ Boolean validateArrayBoundaries(Int64 start) { if (start > Int32.MaxValue) { return false; } - return start >= 0 && start < _rawData.Length && _rawData[(Int32)start] != 0; + return start >= 0 && start < _rawData.Length && _rawData.Span[(Int32)start] != 0; } /// /// Checks if current primitive type is sub-typed (contains nested types) or not. @@ -227,7 +240,7 @@ Boolean testNestedForUniversal(Int64 start, Int32 estimatedLength) { return false; } // if current type is primitive, then nested type can be either, primitive or constructed only. - if (_rawData[(Int32)start] >= (Byte)Asn1Class.APPLICATION) { + if (_rawData.Span[(Int32)start] >= (Byte)Asn1Class.APPLICATION) { return false; } // otherwise, attempt to resolve nested type. Only single nested type is allowed for primitive types. @@ -262,20 +275,20 @@ Boolean predict(Int64 start, Int32 projectedLength, Boolean assignMap, out Int32 return sum == projectedLength; } void calculateLength() { - if (_rawData[Offset + 1] < 128) { + if (_rawData.Span[Offset + 1] < 128) { PayloadStartOffset = Offset + 2; - PayloadLength = _rawData[Offset + 1]; + PayloadLength = _rawData.Span[Offset + 1]; TagLength = PayloadLength + 2; } else { - Int32 lengthBytes = _rawData[Offset + 1] - 128; + Int32 lengthBytes = _rawData.Span[Offset + 1] - 128; // max length can be encoded by using 4 bytes. if (lengthBytes > 4) { throw new OverflowException("Data length is too large."); } PayloadStartOffset = Offset + 2 + lengthBytes; - PayloadLength = _rawData[Offset + 2]; + PayloadLength = _rawData.Span[Offset + 2]; for (Int32 i = Offset + 3; i < PayloadStartOffset; i++) { - PayloadLength = (PayloadLength << 8) | _rawData[i]; + PayloadLength = (PayloadLength << 8) | _rawData.Span[i]; } TagLength = PayloadLength + lengthBytes + 2; } @@ -290,17 +303,17 @@ Int64 calculatePredictLength(Int64 offset) { return Int32.MaxValue; } - if (_rawData[(Int32)(offset + 1)] < 128) { - return _rawData[(Int32) (offset + 1)] + 2; + if (_rawData.Span[(Int32)(offset + 1)] < 128) { + return _rawData.Span[(Int32) (offset + 1)] + 2; } - Int32 lengthBytes = _rawData[(Int32)(offset + 1)] - 128; + Int32 lengthBytes = _rawData.Span[(Int32)(offset + 1)] - 128; // max length can be encoded by using 4 bytes. if (lengthBytes > 4 || offset + 2 >= _rawData.Length) { return Int32.MaxValue; } - Int32 pPayloadLength = _rawData[(Int32)(offset + 2)]; + Int32 pPayloadLength = _rawData.Span[(Int32)(offset + 2)]; for (Int32 i = (Int32)(offset + 3); i < offset + 2 + lengthBytes; i++) { - pPayloadLength = (pPayloadLength << 8) | _rawData[i]; + pPayloadLength = (pPayloadLength << 8) | _rawData.Span[i]; } // 2 -- transitional + tag return pPayloadLength + lengthBytes + 2; @@ -330,7 +343,7 @@ public Byte[] GetHeader() { Int32 headerLength = PayloadStartOffset - Offset; Byte[] array = new Byte[headerLength]; for (Int32 i = 0; i < headerLength; i++) { - array[i] = _rawData[Offset + i]; + array[i] = _rawData.Span[Offset + i]; } return array; @@ -339,10 +352,10 @@ public Byte[] GetHeader() { /// Gets current structure header. Header contains tag and tag length byte (or bytes). /// /// Current structure header. Header contains tag and tag length byte (or bytes). - public ReadOnlySpan GetHeaderAsSpan() { + public ReadOnlyMemory GetHeaderAsMemory() { Int32 headerLength = PayloadStartOffset - Offset; - return _rawData.AsSpan(Offset, headerLength); + return _rawData.Slice(Offset, headerLength); } /// /// Gets the byte array of the current structure's payload. @@ -351,7 +364,7 @@ public ReadOnlySpan GetHeaderAsSpan() { public Byte[] GetPayload() { Byte[] array = new Byte[PayloadLength]; for (Int32 i = 0; i < PayloadLength; i++) { - array[i] = _rawData[PayloadStartOffset + i]; + array[i] = _rawData.Span[PayloadStartOffset + i]; } return array; } @@ -359,8 +372,8 @@ public Byte[] GetPayload() { /// Gets the byte array of the current structure's payload. /// /// Memory span of the current structure's payload. - public ReadOnlySpan GetPayloadAsSpan() { - return _rawData.AsSpan(PayloadStartOffset, PayloadLength); + public ReadOnlyMemory GetPayloadAsMemory() { + return _rawData.Slice(PayloadStartOffset, PayloadLength); } /// /// Gets the raw data of the tag, which includes tag, length bytes and payload. @@ -369,7 +382,7 @@ public ReadOnlySpan GetPayloadAsSpan() { public Byte[] GetTagRawData() { Byte[] array = new Byte[TagLength]; for (Int32 i = 0; i < TagLength; i++) { - array[i] = _rawData[Offset + i]; + array[i] = _rawData.Span[Offset + i]; } return array; } @@ -377,8 +390,8 @@ public Byte[] GetTagRawData() { /// Gets the raw data of the tag, which includes tag, length bytes and payload. /// /// A full binary copy of the tag. - public ReadOnlySpan GetTagRawDataAsSpan() { - return _rawData.AsSpan(Offset, TagLength); + public ReadOnlyMemory GetTagRawDataAsMemory() { + return _rawData.Slice(Offset, TagLength); } /// /// Gets a copy of internal ASN.1 stream. The size of the stream is equals to member value. @@ -387,7 +400,7 @@ public ReadOnlySpan GetTagRawDataAsSpan() { public Byte[] GetRawData() { Byte[] array = new Byte[_rawData.Length]; for (Int32 i = 0; i < _rawData.Length; i++) { - array[i] = _rawData[i]; + array[i] = _rawData.Span[i]; } return array; } @@ -395,8 +408,8 @@ public Byte[] GetRawData() { /// Gets a copy of internal ASN.1 stream. The size of the stream is equals to member value. /// /// A full binary copy of the internal byte stream. - public ReadOnlySpan GetRawDataAsSpan() { - return _rawData.AsSpan(); + public ReadOnlyMemory GetRawDataAsMemory() { + return _rawData; } /// /// Gets the count of nested nodes under node in the current position. diff --git a/Asn1Parser/Asn1Utils.cs b/Asn1Parser/Asn1Utils.cs index b17f330..d139fbf 100644 --- a/Asn1Parser/Asn1Utils.cs +++ b/Asn1Parser/Asn1Utils.cs @@ -207,19 +207,19 @@ static String decodeBitString(Asn1Reader asn) { "Unused bits: {0} : {1}", asn[asn.PayloadStartOffset], AsnFormatter.BinaryToString( - asn.GetRawDataAsSpan().Slice(asn.PayloadStartOffset + 1, asn.PayloadLength - 1), + asn.GetRawDataAsMemory().Slice(asn.PayloadStartOffset + 1, asn.PayloadLength - 1).Span, EncodingType.HexRaw, EncodingFormat.NOCRLF) ); } static String decodeOctetString(Asn1Reader asn) { return AsnFormatter.BinaryToString( - asn.GetRawDataAsSpan().Slice(asn.PayloadStartOffset, asn.PayloadLength), + asn.GetRawDataAsMemory().Slice(asn.PayloadStartOffset, asn.PayloadLength).Span, EncodingType.HexRaw, EncodingFormat.NOCRLF); } static String decodeAsciiString(Asn1Reader asn) { - return Encoding.ASCII.GetString(asn.GetRawDataAsSpan().ToArray(), asn.PayloadStartOffset, asn.PayloadLength); + return Encoding.ASCII.GetString(asn.GetRawDataAsMemory().ToArray(), asn.PayloadStartOffset, asn.PayloadLength); } static String decodeUtcTime(Asn1Reader asn) { DateTime dt = new Asn1UtcTime(asn).Value; diff --git a/Asn1Parser/AsnFormatter.cs b/Asn1Parser/AsnFormatter.cs index 3d084d0..f08b388 100644 --- a/Asn1Parser/AsnFormatter.cs +++ b/Asn1Parser/AsnFormatter.cs @@ -123,19 +123,19 @@ public static String BinaryToString(Asn1Reader asn, EncodingType encoding = Enco return String.Empty; } if ((Int32)encoding > 20 && (Int32)encoding < 45) { - return BinaryToStringFormatter.ToBase64(asn.GetRawDataAsSpan(), encoding, format); + return BinaryToStringFormatter.ToBase64(asn.GetRawDataAsMemory().Span, encoding, format); } if (PemHeader.ContainsEncoding(encoding)) { - return BinaryToStringFormatter.ToBase64(asn.GetRawDataAsSpan(), encoding, format); + return BinaryToStringFormatter.ToBase64(asn.GetRawDataAsMemory().Span, encoding, format); } return encoding switch { - EncodingType.Base64 => BinaryToStringFormatter.ToBase64(asn.GetRawDataAsSpan(), encoding, format), - EncodingType.Hex => BinaryToStringFormatter.ToHex(asn.GetRawDataAsSpan(), format, forceUpperCase), - EncodingType.HexAddress => BinaryToStringFormatter.ToHexAddress(asn.GetRawDataAsSpan(), format, forceUpperCase), - EncodingType.HexAscii => BinaryToStringFormatter.ToHexAscii(asn.GetRawDataAsSpan(), format, forceUpperCase), - EncodingType.HexAsciiAddress => BinaryToStringFormatter.ToHexAddressAndAscii(asn.GetRawDataAsSpan(), format, forceUpperCase), - EncodingType.HexRaw => BinaryToStringFormatter.ToHexRaw(asn.GetRawDataAsSpan(), forceUpperCase), + EncodingType.Base64 => BinaryToStringFormatter.ToBase64(asn.GetRawDataAsMemory().Span, encoding, format), + EncodingType.Hex => BinaryToStringFormatter.ToHex(asn.GetRawDataAsMemory().Span, format, forceUpperCase), + EncodingType.HexAddress => BinaryToStringFormatter.ToHexAddress(asn.GetRawDataAsMemory().Span, format, forceUpperCase), + EncodingType.HexAscii => BinaryToStringFormatter.ToHexAscii(asn.GetRawDataAsMemory().Span, format, forceUpperCase), + EncodingType.HexAsciiAddress => BinaryToStringFormatter.ToHexAddressAndAscii(asn.GetRawDataAsMemory().Span, format, forceUpperCase), + EncodingType.HexRaw => BinaryToStringFormatter.ToHexRaw(asn.GetRawDataAsMemory().Span, forceUpperCase), _ => throw new ArgumentException("Specified encoding is invalid.") }; } diff --git a/Asn1Parser/Universal/Asn1Universal.cs b/Asn1Parser/Universal/Asn1Universal.cs index 1f4039f..46401c0 100644 --- a/Asn1Parser/Universal/Asn1Universal.cs +++ b/Asn1Parser/Universal/Asn1Universal.cs @@ -126,7 +126,7 @@ public Byte[] GetRawData() { /// Gets the full tag raw data, including header and payload information. /// /// ASN.1-encoded type as span. - public ReadOnlySpan GetRawDataAsSpan() { - return asnReader!.GetTagRawDataAsSpan(); + public ReadOnlyMemory GetRawDataAsMemory() { + return asnReader!.GetTagRawDataAsMemory(); } } \ No newline at end of file From 6ee6eb39005b7e0ff74f21070e5cdb38cb086bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Wed, 11 Dec 2024 17:00:16 +0200 Subject: [PATCH 10/66] added faster implementation of BinaryToStringFormatter.ToBase64, which is 50-80 times faster than previous and consumes upt to 2 times less memory --- Asn1Parser/BinaryToStringFormatter.cs | 85 +++++++++++++++++---------- 1 file changed, 53 insertions(+), 32 deletions(-) diff --git a/Asn1Parser/BinaryToStringFormatter.cs b/Asn1Parser/BinaryToStringFormatter.cs index 8a2aa3f..ac97752 100644 --- a/Asn1Parser/BinaryToStringFormatter.cs +++ b/Asn1Parser/BinaryToStringFormatter.cs @@ -1,4 +1,6 @@ using System; +using System.Buffers; +using System.Buffers.Text; using System.Text; namespace SysadminsLV.Asn1Parser; @@ -150,51 +152,62 @@ public static String ToHexAddressAndAscii(ReadOnlySpan rawData, EncodingFo return finalizeBinaryToString(sb, format); } public static String ToBase64(ReadOnlySpan rawData, EncodingType encoding, EncodingFormat format) { - var sb = new StringBuilder(Convert.ToBase64String(rawData.ToArray())); - String splitter; - switch (format) { - case EncodingFormat.NOCR: - splitter = "\n"; - // Base64FormattingOptions inserts new lines at 76 position, while we need 64. - for (Int32 i = 64; i < sb.Length; i += 65) { // 64 + "\r\n" - sb.Insert(i, splitter); - } - break; - case EncodingFormat.NOCRLF: - splitter = String.Empty; - break; - default: - splitter = "\r\n"; - // Base64FormattingOptions inserts new lines at 76 position, while we need 64. - for (Int32 i = 64; i < sb.Length; i += 66) { // 64 + "\r\n" - sb.Insert(i, splitter); - } - break; - } + Int32 b64Length = Base64.GetMaxEncodedToUtf8Length(rawData.Length); + Span base64 = new Byte[b64Length]; + OperationStatus result = Base64.EncodeToUtf8(rawData, base64, out _, out _); + String eol = getEOL(format); + Int32 rowCount = (Int32)Math.Floor(b64Length / 64d); + Int32 eolCount = rowCount * eol.Length + eol.Length; + PemHeader? pem = null; switch (encoding) { case EncodingType.Base64: break; default: - finalizeBase64WithHeader(sb, encoding, splitter); + pem = getPemHeader(encoding); break; } + Int32 totalLength = b64Length + eolCount; + if (pem is not null) { + // total length is a sum of: + // - PEM header length + EOL + // - main base64 content with EOLs + // - PEM footer length + EOL + // - final EOL + totalLength = totalLength + pem.GetHeader().Length + pem.GetFooter().Length + eol.Length * 2 + 1; + } + var sb = new StringBuilder(totalLength); + // append PEM header if available + if (pem is not null) { + sb.Append(pem.GetHeader()).Append(eol); + } + // copy first full lines + for (Int32 i = 0; i < rowCount; i++) { + for (Int32 j = 0; j < 64; j++) { + sb.Append((Char)base64[i * 64 + j]); + } + sb.Append(eol); + } + for (Int32 i = rowCount * 64; i < b64Length; i++) { + sb.Append((Char)base64[i]); + } + // append PEM footer if available + if (pem is not null) { + sb.Append(eol); + sb.Append(pem.GetFooter()); + } + sb.Append(eol); - return finalizeBinaryToString(sb, format); + return sb.ToString(); } #region string finalizers - static void finalizeBase64WithHeader(StringBuilder sb, EncodingType encoding, String splitter) { - Func header, footer; + static PemHeader getPemHeader(EncodingType encoding) { if (PemHeader.ContainsEncoding(encoding)) { - PemHeader pemHeader = PemHeader.GetHeader(encoding); - header = pemHeader.GetHeader; - footer = pemHeader.GetFooter; - } else { - throw new ArgumentException("Specified encoding is not valid Base64 encoding."); + return PemHeader.GetHeader(encoding); } - sb.Insert(0, header.Invoke() + splitter); - sb.Append(splitter + footer.Invoke()); + + throw new ArgumentException("Specified encoding is not valid Base64 encoding."); } static String finalizeBinaryToString(StringBuilder sb, EncodingFormat format) { switch (format) { @@ -206,6 +219,14 @@ static String finalizeBinaryToString(StringBuilder sb, EncodingFormat format) { return sb.Append("\r\n").ToString(); } } + static String getEOL(EncodingFormat format) { + return format switch { + EncodingFormat.CRLF => "\r\n", + EncodingFormat.NOCRLF => String.Empty, + EncodingFormat.NOCR => "\n", + _ => throw new ArgumentOutOfRangeException(nameof(format), format, null) + }; + } #endregion From 4bb52b07766236d56effada1a9eb75420cdf989d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Wed, 11 Dec 2024 17:02:30 +0200 Subject: [PATCH 11/66] fixed null checking for struct, which is never null --- Asn1Parser/AsnFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Asn1Parser/AsnFormatter.cs b/Asn1Parser/AsnFormatter.cs index f08b388..31ef133 100644 --- a/Asn1Parser/AsnFormatter.cs +++ b/Asn1Parser/AsnFormatter.cs @@ -70,7 +70,7 @@ public static String BinaryToString(Byte[] rawData, EncodingType encoding = Enco /// /// public static String BinaryToString(ReadOnlySpan rawData, EncodingType encoding = EncodingType.HexRaw, EncodingFormat format = EncodingFormat.CRLF, Boolean forceUpperCase = false) { - if (rawData == null || rawData.Length == 0) { + if (rawData.IsEmpty) { return String.Empty; } if (PemHeader.ContainsEncoding(encoding)) { From 8c3c54323459ab3c2cde400a94a828f5499ff53a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Wed, 11 Dec 2024 17:03:51 +0200 Subject: [PATCH 12/66] optimized conditions and removed duplicate code --- Asn1Parser/AsnFormatter.cs | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/Asn1Parser/AsnFormatter.cs b/Asn1Parser/AsnFormatter.cs index 31ef133..e045c1e 100644 --- a/Asn1Parser/AsnFormatter.cs +++ b/Asn1Parser/AsnFormatter.cs @@ -73,12 +73,11 @@ public static String BinaryToString(ReadOnlySpan rawData, EncodingType enc if (rawData.IsEmpty) { return String.Empty; } - if (PemHeader.ContainsEncoding(encoding)) { + if (encoding == EncodingType.Base64 || PemHeader.ContainsEncoding(encoding)) { return BinaryToStringFormatter.ToBase64(rawData, encoding, format); } return encoding switch { - EncodingType.Base64 => BinaryToStringFormatter.ToBase64(rawData, encoding, format), EncodingType.Hex => BinaryToStringFormatter.ToHex(rawData, format, forceUpperCase), EncodingType.HexAddress => BinaryToStringFormatter.ToHexAddress(rawData, format, forceUpperCase), EncodingType.HexAscii => BinaryToStringFormatter.ToHexAscii(rawData, format, forceUpperCase), @@ -119,25 +118,8 @@ public static String BinaryToString(Asn1Reader asn, EncodingType encoding = Enco if (asn == null) { throw new ArgumentNullException(nameof(asn)); } - if (asn.PayloadLength == 0) { - return String.Empty; - } - if ((Int32)encoding > 20 && (Int32)encoding < 45) { - return BinaryToStringFormatter.ToBase64(asn.GetRawDataAsMemory().Span, encoding, format); - } - if (PemHeader.ContainsEncoding(encoding)) { - return BinaryToStringFormatter.ToBase64(asn.GetRawDataAsMemory().Span, encoding, format); - } - return encoding switch { - EncodingType.Base64 => BinaryToStringFormatter.ToBase64(asn.GetRawDataAsMemory().Span, encoding, format), - EncodingType.Hex => BinaryToStringFormatter.ToHex(asn.GetRawDataAsMemory().Span, format, forceUpperCase), - EncodingType.HexAddress => BinaryToStringFormatter.ToHexAddress(asn.GetRawDataAsMemory().Span, format, forceUpperCase), - EncodingType.HexAscii => BinaryToStringFormatter.ToHexAscii(asn.GetRawDataAsMemory().Span, format, forceUpperCase), - EncodingType.HexAsciiAddress => BinaryToStringFormatter.ToHexAddressAndAscii(asn.GetRawDataAsMemory().Span, format, forceUpperCase), - EncodingType.HexRaw => BinaryToStringFormatter.ToHexRaw(asn.GetRawDataAsMemory().Span, forceUpperCase), - _ => throw new ArgumentException("Specified encoding is invalid.") - }; + return BinaryToString(asn.GetTagRawDataAsMemory().Span, encoding, format, forceUpperCase); } /// /// Converts previously formatted string back to a byte array. From 864d1e48fab919f7be1d91722cfab6bd913ba35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Wed, 11 Dec 2024 17:05:50 +0200 Subject: [PATCH 13/66] added few more tests --- tests/Asn1Parser.Tests/Asn1DateTimeTests.cs | 3 +- tests/Asn1Parser.Tests/Asn1ReaderTests.cs | 15 ++++ .../BinaryToBase64StringTests.cs | 42 +++++++++ .../BinaryToHexStringTests.cs | 89 +++++++++++++++---- tests/Asn1Parser.Tests/CollectionPerfTests.cs | 1 + 5 files changed, 131 insertions(+), 19 deletions(-) create mode 100644 tests/Asn1Parser.Tests/Asn1ReaderTests.cs diff --git a/tests/Asn1Parser.Tests/Asn1DateTimeTests.cs b/tests/Asn1Parser.Tests/Asn1DateTimeTests.cs index a200747..fc9f96f 100644 --- a/tests/Asn1Parser.Tests/Asn1DateTimeTests.cs +++ b/tests/Asn1Parser.Tests/Asn1DateTimeTests.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Text; using Microsoft.VisualStudio.TestTools.UnitTesting; using SysadminsLV.Asn1Parser; @@ -66,7 +65,7 @@ static void assertDateTimeEncode(Asn1Type expectedType, Asn1DateTime adt, DateTi Assert.AreEqual((Byte)expectedType, adt.Tag); Assert.AreEqual(DateTimeKind.Local, adt.Value.Kind); - String gts = Encoding.ASCII.GetString(adt.GetRawData().Skip(2).ToArray()); + String gts = Encoding.ASCII.GetString(adt.GetRawDataAsMemory()[2..].ToArray()); Assert.AreEqual(dt, adt.Value); if (adt.ZoneInfo == null) { dt = dt.ToUniversalTime(); diff --git a/tests/Asn1Parser.Tests/Asn1ReaderTests.cs b/tests/Asn1Parser.Tests/Asn1ReaderTests.cs new file mode 100644 index 0000000..c9f912e --- /dev/null +++ b/tests/Asn1Parser.Tests/Asn1ReaderTests.cs @@ -0,0 +1,15 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SysadminsLV.Asn1Parser; + +namespace Asn1Parser.Tests; + +[TestClass] +public class Asn1ReaderTests { + [TestMethod] + public void TestTruncatedSource() { + var bb = new Byte[] { 48, 2, 5, 0 }; + var asn = new Asn1Reader(bb.AsMemory()); + bb[3] = 1; + } +} diff --git a/tests/Asn1Parser.Tests/BinaryToBase64StringTests.cs b/tests/Asn1Parser.Tests/BinaryToBase64StringTests.cs index 0d24044..857916c 100644 --- a/tests/Asn1Parser.Tests/BinaryToBase64StringTests.cs +++ b/tests/Asn1Parser.Tests/BinaryToBase64StringTests.cs @@ -8,9 +8,17 @@ namespace Asn1Parser.Tests; public class BinaryToBase64StringTests { readonly Byte[] _rawData = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; readonly String _base64; + readonly Byte[] _complexRawData; + readonly String _complexBase64 = """ + MGMxCzAJBgNVBAYTAlVTMSEwHwYDVQQKExhUaGUgR28gRGFkZHkgR3JvdXAsIElu + Yy4xMTAvBgNVBAsTKEdvIERhZGR5IENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRo + b3JpdHk= + + """; public BinaryToBase64StringTests() { _base64 = Convert.ToBase64String(_rawData); + _complexRawData = Convert.FromBase64String(_complexBase64); } [TestMethod] @@ -116,4 +124,38 @@ static void validateHeader(String pemString, String pemHeaderText) { Assert.IsTrue(pemString.StartsWith(headerString)); Assert.IsTrue(pemString.EndsWith(footerString)); } + + [TestMethod] + public void TestBase64ComplexWithCRLF() { + String str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.Base64); + Assert.AreEqual(_complexBase64, str); + } + [TestMethod] + public void TestBase64ComplexWithLF() { + String str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.Base64, EncodingFormat.NOCR); + Assert.AreEqual(_complexBase64.Replace("\r", null), str); + } + [TestMethod] + public void TestBase64ComplexWithNoEOL() { + String str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.Base64, EncodingFormat.NOCRLF); + Assert.AreEqual(_complexBase64.Replace("\r\n", null), str); + } + [TestMethod] + public void TestPemComplexWithCRLF() { + String str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.Base64Header); + String expected = "-----BEGIN CERTIFICATE-----" + "\r\n" + _complexBase64 + "-----END CERTIFICATE-----" + "\r\n"; + Assert.AreEqual(expected, str); + } + [TestMethod] + public void TestPemComplexWithLF() { + String str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.Base64Header, EncodingFormat.NOCR); + String expected = "-----BEGIN CERTIFICATE-----" + "\n" + _complexBase64.Replace("\r", null) + "-----END CERTIFICATE-----" + "\n"; + Assert.AreEqual(expected, str); + } + [TestMethod] + public void TestPemComplexWithNoEOL() { + String str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.Base64Header, EncodingFormat.NOCRLF); + String expected = "-----BEGIN CERTIFICATE-----" + _complexBase64.Replace("\r\n", null) + "-----END CERTIFICATE-----"; + Assert.AreEqual(expected, str); + } } \ No newline at end of file diff --git a/tests/Asn1Parser.Tests/BinaryToHexStringTests.cs b/tests/Asn1Parser.Tests/BinaryToHexStringTests.cs index 6909d94..1b5268d 100644 --- a/tests/Asn1Parser.Tests/BinaryToHexStringTests.cs +++ b/tests/Asn1Parser.Tests/BinaryToHexStringTests.cs @@ -1,61 +1,116 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; using SysadminsLV.Asn1Parser; namespace Asn1Parser.Tests; [TestClass] public class BinaryToHexStringTests { - readonly System.Byte[] _rawData = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; - readonly System.Byte[] _rawDataTruncated = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; - const System.String HEX = "01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10"; - const System.String TRUNCATED_HEX = "01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f"; - const System.String HEX_RAW = "0102030405060708090a0b0c0d0e0f10"; - const System.String HEX_ADDR = "0000 "; - const System.String HEX_ASCII = " ................"; - const System.String TRUNCATED_HEX_ASCII = " ..............."; + readonly Byte[] _rawData = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + readonly Byte[] _rawDataTruncated = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + readonly Byte[] _complexRawData = Convert.FromBase64String("BiQrBgEEAYI3FQiWnU2FkrJ4vZ88hejsdILO2ER6gqr3QofnxDY="); + const String HEX = "01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10"; + const String TRUNCATED_HEX = "01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f"; + const String HEX_RAW = "0102030405060708090a0b0c0d0e0f10"; + const String HEX_ADDR = "0000 "; + const String HEX_ASCII = " ................"; + const String TRUNCATED_HEX_ASCII = " ..............."; + const String COMPLEX_HEX_RAW = "06242b0601040182371508969d4d8592b278bd9f3c85e8ec7482ced8447a82aaf74287e7c436"; + const String COMPLEX_HEX = """ + 06 24 2b 06 01 04 01 82 37 15 08 96 9d 4d 85 92 + b2 78 bd 9f 3c 85 e8 ec 74 82 ce d8 44 7a 82 aa + f7 42 87 e7 c4 36 + """; + const String COMPLEX_HEX_ADDR = """ + 0000 06 24 2b 06 01 04 01 82 37 15 08 96 9d 4d 85 92 + 0010 b2 78 bd 9f 3c 85 e8 ec 74 82 ce d8 44 7a 82 aa + 0020 f7 42 87 e7 c4 36 + """; + const String COMPLEX_HEX_ASCII = """ + 06 24 2b 06 01 04 01 82 37 15 08 96 9d 4d 85 92 .$+.....7....M.. + b2 78 bd 9f 3c 85 e8 ec 74 82 ce d8 44 7a 82 aa .x..<...t...Dz.. + f7 42 87 e7 c4 36 .B...6 + """; + const String COMPLEX_HEX_ADDR_ASCII = """ + 0000 06 24 2b 06 01 04 01 82 37 15 08 96 9d 4d 85 92 .$+.....7....M.. + 0010 b2 78 bd 9f 3c 85 e8 ec 74 82 ce d8 44 7a 82 aa .x..<...t...Dz.. + 0020 f7 42 87 e7 c4 36 .B...6 + """; [TestMethod] public void TestHex() { - System.String str = AsnFormatter.BinaryToString(_rawData, EncodingType.Hex); + String str = AsnFormatter.BinaryToString(_rawData, EncodingType.Hex); Assert.AreEqual(HEX, str.TrimEnd()); str = AsnFormatter.BinaryToString(_rawData, EncodingType.Hex, forceUpperCase: true); Assert.AreEqual(HEX.ToUpper(), str.TrimEnd()); } [TestMethod] public void TestHexRaw() { - System.String str = AsnFormatter.BinaryToString(_rawData); + String str = AsnFormatter.BinaryToString(_rawData); Assert.AreEqual(HEX_RAW, str.TrimEnd()); str = AsnFormatter.BinaryToString(_rawData, forceUpperCase: true); Assert.AreEqual(HEX_RAW.ToUpper(), str.TrimEnd()); } [TestMethod] public void TestHexAddr() { - System.String str = AsnFormatter.BinaryToString(_rawData, EncodingType.HexAddress); + String str = AsnFormatter.BinaryToString(_rawData, EncodingType.HexAddress); Assert.AreEqual(HEX_ADDR + HEX, str.TrimEnd()); } [TestMethod] public void TestHexAscii() { - System.String str = AsnFormatter.BinaryToString(_rawData, EncodingType.HexAscii); + String str = AsnFormatter.BinaryToString(_rawData, EncodingType.HexAscii); Assert.AreEqual(HEX + HEX_ASCII, str.TrimEnd()); } [TestMethod] public void TestHexAddrAscii() { - System.String str = AsnFormatter.BinaryToString(_rawData, EncodingType.HexAsciiAddress); + String str = AsnFormatter.BinaryToString(_rawData, EncodingType.HexAsciiAddress); Assert.AreEqual(HEX_ADDR + HEX + HEX_ASCII, str.TrimEnd()); } [TestMethod] public void TestHexAddrTruncated() { - System.String str = AsnFormatter.BinaryToString(_rawDataTruncated, EncodingType.HexAddress); + String str = AsnFormatter.BinaryToString(_rawDataTruncated, EncodingType.HexAddress); Assert.AreEqual(HEX_ADDR + TRUNCATED_HEX, str.TrimEnd()); } [TestMethod] public void TestHexAsciiTruncated() { - System.String str = AsnFormatter.BinaryToString(_rawDataTruncated, EncodingType.HexAscii); + String str = AsnFormatter.BinaryToString(_rawDataTruncated, EncodingType.HexAscii); Assert.AreEqual(TRUNCATED_HEX + TRUNCATED_HEX_ASCII, str.TrimEnd()); } [TestMethod] public void TestHexAddrAsciiTruncated() { - System.String str = AsnFormatter.BinaryToString(_rawDataTruncated, EncodingType.HexAsciiAddress); + String str = AsnFormatter.BinaryToString(_rawDataTruncated, EncodingType.HexAsciiAddress); Assert.AreEqual(HEX_ADDR + TRUNCATED_HEX + TRUNCATED_HEX_ASCII, str.TrimEnd()); } + + [TestMethod] + public void TestComplexHexRaw() { + String str = AsnFormatter.BinaryToString(_complexRawData.AsSpan()); + Assert.AreEqual(COMPLEX_HEX_RAW, str.TrimEnd()); + str = AsnFormatter.BinaryToString(_complexRawData.AsSpan(), forceUpperCase: true); + Assert.AreEqual(COMPLEX_HEX_RAW.ToUpper(), str.TrimEnd()); + } + [TestMethod] + public void TestComplexHex() { + String str = AsnFormatter.BinaryToString(_complexRawData.AsSpan(), EncodingType.Hex); + Assert.AreEqual(COMPLEX_HEX, str.TrimEnd()); + str = AsnFormatter.BinaryToString(_complexRawData.AsSpan(), EncodingType.Hex, forceUpperCase: true); + Assert.AreEqual(COMPLEX_HEX.ToUpper(), str.TrimEnd()); + } + [TestMethod] + public void TestComplexHexAddr() { + String str = AsnFormatter.BinaryToString(_complexRawData.AsSpan(), EncodingType.HexAddress); + Assert.AreEqual(COMPLEX_HEX_ADDR, str.TrimEnd()); + str = AsnFormatter.BinaryToString(_complexRawData.AsSpan(), EncodingType.HexAddress, forceUpperCase: true); + Assert.AreEqual(COMPLEX_HEX_ADDR.ToUpper(), str.TrimEnd()); + } + [TestMethod] + public void TestComplexHexAscii() { + String str = AsnFormatter.BinaryToString(_complexRawData.AsSpan(), EncodingType.HexAscii); + Assert.AreEqual(COMPLEX_HEX_ASCII, str.TrimEnd()); + } + [TestMethod] + public void TestComplexHexAddrAscii() { + String str = AsnFormatter.BinaryToString(_complexRawData.AsSpan(), EncodingType.HexAsciiAddress); + Assert.AreEqual(COMPLEX_HEX_ADDR_ASCII, str.TrimEnd()); + } } \ No newline at end of file diff --git a/tests/Asn1Parser.Tests/CollectionPerfTests.cs b/tests/Asn1Parser.Tests/CollectionPerfTests.cs index d07de4b..2bea4f3 100644 --- a/tests/Asn1Parser.Tests/CollectionPerfTests.cs +++ b/tests/Asn1Parser.Tests/CollectionPerfTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Asn1Parser.Tests; From c1e7725510c1a1b8ff7d63181cca03fcbdecf4b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Wed, 11 Dec 2024 17:21:03 +0200 Subject: [PATCH 14/66] added quick benchmark project to test implementations. Nothing fancy there. --- Asn1Parser.sln | 7 + .../Asn1Parser.Benchmark.csproj | 34 +++++ .../Asn1ReaderBenchmark.cs | 62 +++++++++ .../CollectionPerfBenchmark.cs | 48 +++++++ tests/Asn1Parser.Benchmark/Program.cs | 13 ++ .../Properties/Resources.Designer.cs | 63 +++++++++ .../Properties/Resources.resx | 124 ++++++++++++++++++ .../Properties/Resources/msitwww2.crl | Bin 0 -> 175347 bytes .../Resources/level1m.crl | Bin 0 -> 292298 bytes .../Resources/msitwww2.crl | Bin 0 -> 175347 bytes 10 files changed, 351 insertions(+) create mode 100644 tests/Asn1Parser.Benchmark/Asn1Parser.Benchmark.csproj create mode 100644 tests/Asn1Parser.Benchmark/Asn1ReaderBenchmark.cs create mode 100644 tests/Asn1Parser.Benchmark/CollectionPerfBenchmark.cs create mode 100644 tests/Asn1Parser.Benchmark/Program.cs create mode 100644 tests/Asn1Parser.Benchmark/Properties/Resources.Designer.cs create mode 100644 tests/Asn1Parser.Benchmark/Properties/Resources.resx create mode 100644 tests/Asn1Parser.Benchmark/Properties/Resources/msitwww2.crl create mode 100644 tests/Asn1Parser.Benchmark/Resources/level1m.crl create mode 100644 tests/Asn1Parser.Benchmark/Resources/msitwww2.crl diff --git a/Asn1Parser.sln b/Asn1Parser.sln index 9a906c0..3a91e84 100644 --- a/Asn1Parser.sln +++ b/Asn1Parser.sln @@ -14,6 +14,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{CA656832 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Asn1Parser.Tests", "tests\Asn1Parser.Tests\Asn1Parser.Tests.csproj", "{739EADA6-013A-4BA3-BF41-70D667B9682A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Asn1Parser.Benchmark", "tests\Asn1Parser.Benchmark\Asn1Parser.Benchmark.csproj", "{492FCE10-32C2-4485-A529-CC08AF075D5A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -28,12 +30,17 @@ Global {739EADA6-013A-4BA3-BF41-70D667B9682A}.Debug|Any CPU.Build.0 = Debug|Any CPU {739EADA6-013A-4BA3-BF41-70D667B9682A}.Release|Any CPU.ActiveCfg = Release|Any CPU {739EADA6-013A-4BA3-BF41-70D667B9682A}.Release|Any CPU.Build.0 = Release|Any CPU + {492FCE10-32C2-4485-A529-CC08AF075D5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {492FCE10-32C2-4485-A529-CC08AF075D5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {492FCE10-32C2-4485-A529-CC08AF075D5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {492FCE10-32C2-4485-A529-CC08AF075D5A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {739EADA6-013A-4BA3-BF41-70D667B9682A} = {CA656832-2531-426B-ACF5-724FA0A6EDC7} + {492FCE10-32C2-4485-A529-CC08AF075D5A} = {CA656832-2531-426B-ACF5-724FA0A6EDC7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B1F6FDE1-A4A9-4257-82D9-2CFC87B32B09} diff --git a/tests/Asn1Parser.Benchmark/Asn1Parser.Benchmark.csproj b/tests/Asn1Parser.Benchmark/Asn1Parser.Benchmark.csproj new file mode 100644 index 0000000..7fbf07b --- /dev/null +++ b/tests/Asn1Parser.Benchmark/Asn1Parser.Benchmark.csproj @@ -0,0 +1,34 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/tests/Asn1Parser.Benchmark/Asn1ReaderBenchmark.cs b/tests/Asn1Parser.Benchmark/Asn1ReaderBenchmark.cs new file mode 100644 index 0000000..7f22a51 --- /dev/null +++ b/tests/Asn1Parser.Benchmark/Asn1ReaderBenchmark.cs @@ -0,0 +1,62 @@ +using Asn1Parser.Benchmark.Properties; +using BenchmarkDotNet.Attributes; +using SysadminsLV.Asn1Parser; + +namespace Asn1Parser.Benchmark; + + +[MemoryDiagnoser] +public class Asn1ReaderBenchmark { + readonly Asn1Reader _reader; + + public Asn1ReaderBenchmark() { + Byte[] bytes = (Byte[])Resources.ResourceManager.GetObject("MiddleSizeCRL"); ; + _reader = new Asn1Reader(bytes); + } + + [Benchmark(Baseline = true)] + public void Test1() { + _reader.BuildOffsetMap(); + do { + _reader.GetPayload(); + } while (_reader.MoveNext()); + } + [Benchmark] + public void Test2() { + _reader.BuildOffsetMap(); + do { + _reader.GetPayloadAsMemory(); + } while (_reader.MoveNext()); + } +} +[MemoryDiagnoser] +public class Asn1FormatterBenchmark { + readonly Asn1Reader _reader; + + public Asn1FormatterBenchmark() { + Byte[] bytes = (Byte[])Resources.ResourceManager.GetObject("MiddleSizeCRL"); + _reader = new Asn1Reader(bytes); + } + + [Benchmark(Baseline = true)] + public void TestArray() { + _reader.BuildOffsetMap(); + do { + AsnFormatter.BinaryToString(_reader.GetTagRawData(), EncodingType.Base64); + } while (_reader.MoveNext()); + } + [Benchmark] + public void TestSpan() { + _reader.BuildOffsetMap(); + do { + AsnFormatter.BinaryToString(_reader.GetTagRawDataAsMemory().Span, EncodingType.Base64); + } while (_reader.MoveNext()); + } + [Benchmark] + public void TestAsnReader() { + _reader.BuildOffsetMap(); + do { + AsnFormatter.BinaryToString(_reader, EncodingType.Base64); + } while (_reader.MoveNext()); + } +} \ No newline at end of file diff --git a/tests/Asn1Parser.Benchmark/CollectionPerfBenchmark.cs b/tests/Asn1Parser.Benchmark/CollectionPerfBenchmark.cs new file mode 100644 index 0000000..2602220 --- /dev/null +++ b/tests/Asn1Parser.Benchmark/CollectionPerfBenchmark.cs @@ -0,0 +1,48 @@ +using BenchmarkDotNet.Attributes; + +namespace Asn1Parser.Benchmark; + +public class CollectionPerfBenchmark { + static readonly List _list = []; + static readonly HashSet _hashSet = []; + + public CollectionPerfBenchmark() { + foreach (Byte b in Enumerable.Range(0, 31)) { + _list.Add(b); + _hashSet.Add(b); + } + } + + [Benchmark(Baseline = true)] + public void Contains11() { + _list.Contains(0); + } + [Benchmark] + public void Contains21() { + _hashSet.Contains(0); + } + [Benchmark] + public void Contains12() { + _list.Contains(20); + } + [Benchmark] + public void Contains22() { + _hashSet.Contains(20); + } + [Benchmark] + public void Contains13() { + _list.Contains(30); + } + [Benchmark] + public void Contains23() { + _hashSet.Contains(30); + } + [Benchmark] + public void Contains14() { + _list.Contains(255); + } + [Benchmark] + public void Contains24() { + _hashSet.Contains(255); + } +} \ No newline at end of file diff --git a/tests/Asn1Parser.Benchmark/Program.cs b/tests/Asn1Parser.Benchmark/Program.cs new file mode 100644 index 0000000..1a4d6d7 --- /dev/null +++ b/tests/Asn1Parser.Benchmark/Program.cs @@ -0,0 +1,13 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; + +namespace Asn1Parser.Benchmark; + +internal class Program { + static void Main(String[] args) { + IConfig config = DefaultConfig.Instance; + Summary summary = BenchmarkRunner.Run(config); + summary = BenchmarkRunner.Run(config); + } +} diff --git a/tests/Asn1Parser.Benchmark/Properties/Resources.Designer.cs b/tests/Asn1Parser.Benchmark/Properties/Resources.Designer.cs new file mode 100644 index 0000000..19a02d4 --- /dev/null +++ b/tests/Asn1Parser.Benchmark/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Asn1Parser.Benchmark.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Asn1Parser.Benchmark.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/tests/Asn1Parser.Benchmark/Properties/Resources.resx b/tests/Asn1Parser.Benchmark/Properties/Resources.resx new file mode 100644 index 0000000..4dd87ed --- /dev/null +++ b/tests/Asn1Parser.Benchmark/Properties/Resources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + Resources\msitwww2.crl;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/tests/Asn1Parser.Benchmark/Properties/Resources/msitwww2.crl b/tests/Asn1Parser.Benchmark/Properties/Resources/msitwww2.crl new file mode 100644 index 0000000000000000000000000000000000000000..8b541559b5044c7787a24516c3c2e66b912bb963 GIT binary patch literal 175347 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

Uglq+9n%Bd;F6khn+#*}A(TL)t%u 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℞V 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 Ws_6AL#(w8gGbg3A0;Ncy_5T3~CrU;D literal 0 HcmV?d00001 diff --git a/tests/Asn1Parser.Benchmark/Resources/level1m.crl b/tests/Asn1Parser.Benchmark/Resources/level1m.crl new file mode 100644 index 0000000000000000000000000000000000000000..550e3110db206b9882ffce30de020326f28f169e GIT binary patch literal 292298 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

QF)ov7>DgPC8yMYR!wJg>S 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

1T`w=Bs?QRh&$b_EY+qC?Zn8i#`pn$ z91I9H!npYhkr6qoAj#gxTN3#Vp&>?x0wcxp>t9Y0Kxo>@jbt z(9GHMrFQDsXR!zN7xwCzLB{K}f z4b=lTm=kp~vp#O!ImOjB>HY4i>1Ll;#}q4VEK!9rk`A)XJs!`EvGfn^(0;5{hd(_G z_BuvH9KLg7$}b1bed$!c7=FdI>KOi@92_O*E${lpglZQf68+w z1Yi*rc*&_v7!f%w%J(kmJXw;ZANbQ8qG6(76HR;0&?j?a119DKZ?947wiepN;0GZ3 zV;Luy>cy3my^P9MIl67RtPBwhiFnZH%}zq|)#1E9(-(N#a!wo{x$e@e7*AsiM&gmDCt z3=VEmWtVn84xCy&dBS3)@q3>o+Wyf@yi3gMvooRAE$A_2zTJh{grel6EJFca9zVp* zvsCmRd&~kvg^pL$7{pcV%rj>I#T2*sFH6sGUh)`Oa%jBdaWJvxcY+{}V2|Pd!V&l# zL=WJI@3?on)HftPBIIK4q4cM4LliPEuE0rrL+-eGT+@A`GFZwy~$ z`ikebJ$2{DF1C_Xa+KzMz;$#n@a8*L;BY3{R`>ck-nEU{nqND&N0Yv?1O+av3ExKS z=}@p%K1TCcc3t`~y)iyb5&&!@fM^9UsGpBi@0YaW{m!e~>y_ooZ~u)!uz6_s?L>JC zw?iur=O1-^H{br+-&FIvNl5jfx1|pA#cM7z#!kU@5=a#o3zsnYC=g<^+dAvsk9tZv z54M*90*WRKJWytFl#@lOBYK;)S7i)(M>s^i<{|_XC;$L&+VW%poYfrDVSCQ3rFVig zzU`1!?p(zH==QTPql}+7yf{DKu;FsBLf410*I?BIKg5g$-eBfoKq?NF4jg?|`$#RL zyAMpw@k~GhK=30KoJoGkmu=ze2R>VTeCn2-yL!+0hX@|o0H8Zct-ZtSu;OwPJ=5c@ z>~Z9WRKwOZyz(R@ZEe+6-*OFY=Q1|UX!-Y%1lt1118l#?W!dcv(fx5Z;Gv zNcsza=L8ABv&E3qzNcg5-gkMI>#|q|lX%Hk9+GCEBMn&@_2Q!ZlV1Hg&iCK;!u%}J z0IeKUCc&EBWoV_2`1RVLbBgN~ZBi2&7yvd;AdK-u4RYulsJG4ay253PjOpsq+4m^8 zK;sM7n1l!ay*y`{>ODK*%v57IyBL6X55O~WBP0thzu$`Y-S0Dx)!?F;==$L>5af{v zp)*AVX1=hsdpR?Bv9PE9z~5B$lI5svAk_mddc0u3C*j1-*Y@0eHar7aIQEEf$UdE zasz|68)rt(ia4Gvzc?i)3kR=210)^zy$d0tdy40EUuU|)mFag7O*X&@AOKW_T%tj8 zGHdpa>%2hn#bMEl`-lOmLH06;pvhfdxqZ{4oC6F)1?!EYpkZ-ZFiIq?I{Ipiv}soc zkdY|CfbXjDaz;tZa!Hh-+F30|gg6l6Ce}FkX-LSVxkRAk8jG{L8DCMA9P0W;2?Sn> zhE#6^v3|}`9qfUUL&Rhg8dD{{RfA926Y6Mnt|zdSpQ5u=eRLM!B`UeyqOK5s&l-1GhLQfRQVA zCLc>RDrxfjY_An)qQwAk0|*+5oNSx`3|GoOlzpQ&cGlA!#u1eRB^9Y#<_ zH(I7myS8#|C280ZmQ~^PE_faudvSkfbiCq({f;sRV2AOx6?2^ZTz0

3CT*#Z+* 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

Uglq+9n%Bd;F6khn+#*}A(TL)t%u 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℞V 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 Ws_6AL#(w8gGbg3A0;Ncy_5T3~CrU;D literal 0 HcmV?d00001 From e4d9b0ee62bf2adfe5874bf633202ddd64655202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Thu, 12 Dec 2024 11:49:51 +0200 Subject: [PATCH 15/66] addressed #22 --- Asn1Parser/AsnFormatter.cs | 2 +- Asn1Parser/BinaryToStringFormatter.cs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Asn1Parser/AsnFormatter.cs b/Asn1Parser/AsnFormatter.cs index e045c1e..6f696d9 100644 --- a/Asn1Parser/AsnFormatter.cs +++ b/Asn1Parser/AsnFormatter.cs @@ -82,7 +82,7 @@ public static String BinaryToString(ReadOnlySpan rawData, EncodingType enc EncodingType.HexAddress => BinaryToStringFormatter.ToHexAddress(rawData, format, forceUpperCase), EncodingType.HexAscii => BinaryToStringFormatter.ToHexAscii(rawData, format, forceUpperCase), EncodingType.HexAsciiAddress => BinaryToStringFormatter.ToHexAddressAndAscii(rawData, format, forceUpperCase), - EncodingType.HexRaw => BinaryToStringFormatter.ToHexRaw(rawData, forceUpperCase), + EncodingType.HexRaw => BinaryToStringFormatter.ToHexRaw(rawData, format, forceUpperCase), _ => throw new ArgumentException("Specified encoding is invalid.") }; } diff --git a/Asn1Parser/BinaryToStringFormatter.cs b/Asn1Parser/BinaryToStringFormatter.cs index ac97752..071ec39 100644 --- a/Asn1Parser/BinaryToStringFormatter.cs +++ b/Asn1Parser/BinaryToStringFormatter.cs @@ -6,13 +6,14 @@ namespace SysadminsLV.Asn1Parser; static class BinaryToStringFormatter { - public static String ToHexRaw(ReadOnlySpan rawData, Boolean forceUpperCase) { + public static String ToHexRaw(ReadOnlySpan rawData, EncodingFormat format, Boolean forceUpperCase) { + String eol = getEOL(format); var SB = new StringBuilder(); foreach (Byte b in rawData) { byteToHexOctet(SB, b, forceUpperCase); } - return SB.ToString(); + return SB.Append(eol).ToString(); } public static String ToHex(ReadOnlySpan rawData, EncodingFormat format, Boolean forceUpperCase) { var sb = new StringBuilder(); From 271214bf89ab899fa37e41a016b9cbe4e3ce7e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Thu, 12 Dec 2024 11:51:53 +0200 Subject: [PATCH 16/66] improved BinaryToStringFormatter.ToHexAddress performance by pre-allocating StringBuilder --- Asn1Parser/BinaryToStringFormatter.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Asn1Parser/BinaryToStringFormatter.cs b/Asn1Parser/BinaryToStringFormatter.cs index 071ec39..8a129b3 100644 --- a/Asn1Parser/BinaryToStringFormatter.cs +++ b/Asn1Parser/BinaryToStringFormatter.cs @@ -44,9 +44,13 @@ public static String ToHex(ReadOnlySpan rawData, EncodingFormat format, Bo return finalizeBinaryToString(sb, format); } public static String ToHexAddress(ReadOnlySpan rawData, EncodingFormat format, Boolean forceUpperCase) { - var sb = new StringBuilder(); Int32 rowCount = 0; Int32 addrLength = getAddrLength(rawData.Length); + String eol = format == EncodingFormat.NOCR ? "\n" : "\r\n"; + String eof = getEOL(format); + Int32 totalRows = (Int32)Math.Ceiling(rawData.Length / 16d); + Int32 bufferSize = addrLength + (52 + eol.Length) * totalRows + eof.Length; + var sb = new StringBuilder(bufferSize); for (Int32 index = 0; index < rawData.Length; index++) { if (index % 16 == 0) { String hexAddress = Convert.ToString(rowCount, 16).PadLeft(addrLength, '0'); @@ -62,10 +66,10 @@ public static String ToHexAddress(ReadOnlySpan rawData, EncodingFormat for sb.Append(" "); continue; } - + if ((index + 1) % 16 == 0) { // if current octet is the last octet in a row, append EOL format - sb.Append(format == EncodingFormat.NOCR ? "\n" : "\r\n"); + sb.Append(eol); } else if ((index + 1) % 8 == 0) { // if current octet is center octet in a row, append extra space sb.Append(" "); @@ -74,7 +78,7 @@ public static String ToHexAddress(ReadOnlySpan rawData, EncodingFormat for } } - return finalizeBinaryToString(sb, format); + return sb.Append(eof).ToString(); } public static String ToHexAscii(ReadOnlySpan rawData, EncodingFormat format, Boolean forceUpperCase) { var sb = new StringBuilder(); From 0c2761ce5254076629a6a0538138eb609ae989b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Thu, 12 Dec 2024 11:52:20 +0200 Subject: [PATCH 17/66] refactored benchmarks --- .../Asn1BinaryToStringBenchmark.cs | 46 +++++++++++++++++++ .../Asn1ReaderBenchmark.cs | 31 ------------- tests/Asn1Parser.Benchmark/Program.cs | 5 +- 3 files changed, 48 insertions(+), 34 deletions(-) create mode 100644 tests/Asn1Parser.Benchmark/Asn1BinaryToStringBenchmark.cs diff --git a/tests/Asn1Parser.Benchmark/Asn1BinaryToStringBenchmark.cs b/tests/Asn1Parser.Benchmark/Asn1BinaryToStringBenchmark.cs new file mode 100644 index 0000000..5ec8927 --- /dev/null +++ b/tests/Asn1Parser.Benchmark/Asn1BinaryToStringBenchmark.cs @@ -0,0 +1,46 @@ +using Asn1Parser.Benchmark.Properties; +using BenchmarkDotNet.Attributes; +using SysadminsLV.Asn1Parser; + +namespace Asn1Parser.Benchmark; + +[MemoryDiagnoser] +public abstract class Asn1BinaryToStringBenchmark { + readonly Asn1Reader _reader; + readonly EncodingType _encoding; + + protected Asn1BinaryToStringBenchmark(EncodingType encoding) { + Byte[] bytes = (Byte[])Resources.ResourceManager.GetObject("MiddleSizeCRL"); + _reader = new Asn1Reader(bytes); + _encoding = encoding; + } + + [Benchmark(Baseline = true)] + public void TestArray() { + _reader.BuildOffsetMap(); + do { + AsnFormatter.BinaryToString(_reader.GetTagRawData(), _encoding); + } while (_reader.MoveNext()); + } + [Benchmark] + public void TestSpan() { + _reader.BuildOffsetMap(); + do { + AsnFormatter.BinaryToString(_reader.GetTagRawDataAsMemory().Span, _encoding); + } while (_reader.MoveNext()); + } + [Benchmark] + public void TestAsnReader() { + _reader.BuildOffsetMap(); + do { + AsnFormatter.BinaryToString(_reader, _encoding); + } while (_reader.MoveNext()); + } +} + +public class Asn1FormatterToBase64Benchmark() : Asn1BinaryToStringBenchmark(EncodingType.Base64); +public class Asn1FormatterToHexRawBenchmark() : Asn1BinaryToStringBenchmark(EncodingType.HexRaw); +public class Asn1FormatterToHexBenchmark() : Asn1BinaryToStringBenchmark(EncodingType.Hex); +public class Asn1FormatterToHexAddressBenchmark() : Asn1BinaryToStringBenchmark(EncodingType.HexAddress); +public class Asn1FormatterToHexAsciiBenchmark() : Asn1BinaryToStringBenchmark(EncodingType.HexAscii); +public class Asn1FormatterToHexAddressAsciiBenchmark() : Asn1BinaryToStringBenchmark(EncodingType.HexAsciiAddress); \ No newline at end of file diff --git a/tests/Asn1Parser.Benchmark/Asn1ReaderBenchmark.cs b/tests/Asn1Parser.Benchmark/Asn1ReaderBenchmark.cs index 7f22a51..09ffa3f 100644 --- a/tests/Asn1Parser.Benchmark/Asn1ReaderBenchmark.cs +++ b/tests/Asn1Parser.Benchmark/Asn1ReaderBenchmark.cs @@ -28,35 +28,4 @@ public void Test2() { _reader.GetPayloadAsMemory(); } while (_reader.MoveNext()); } -} -[MemoryDiagnoser] -public class Asn1FormatterBenchmark { - readonly Asn1Reader _reader; - - public Asn1FormatterBenchmark() { - Byte[] bytes = (Byte[])Resources.ResourceManager.GetObject("MiddleSizeCRL"); - _reader = new Asn1Reader(bytes); - } - - [Benchmark(Baseline = true)] - public void TestArray() { - _reader.BuildOffsetMap(); - do { - AsnFormatter.BinaryToString(_reader.GetTagRawData(), EncodingType.Base64); - } while (_reader.MoveNext()); - } - [Benchmark] - public void TestSpan() { - _reader.BuildOffsetMap(); - do { - AsnFormatter.BinaryToString(_reader.GetTagRawDataAsMemory().Span, EncodingType.Base64); - } while (_reader.MoveNext()); - } - [Benchmark] - public void TestAsnReader() { - _reader.BuildOffsetMap(); - do { - AsnFormatter.BinaryToString(_reader, EncodingType.Base64); - } while (_reader.MoveNext()); - } } \ No newline at end of file diff --git a/tests/Asn1Parser.Benchmark/Program.cs b/tests/Asn1Parser.Benchmark/Program.cs index 1a4d6d7..c149845 100644 --- a/tests/Asn1Parser.Benchmark/Program.cs +++ b/tests/Asn1Parser.Benchmark/Program.cs @@ -1,5 +1,4 @@ using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; namespace Asn1Parser.Benchmark; @@ -7,7 +6,7 @@ namespace Asn1Parser.Benchmark; internal class Program { static void Main(String[] args) { IConfig config = DefaultConfig.Instance; - Summary summary = BenchmarkRunner.Run(config); - summary = BenchmarkRunner.Run(config); + //Summary summary = BenchmarkRunner.Run(config); + var summary = BenchmarkRunner.Run(config); } } From 6649e8e61ae2fd35be0979080b1cf435f2a923d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Thu, 12 Dec 2024 12:11:02 +0200 Subject: [PATCH 18/66] refactored some static methods to extension methods. --- Asn1Parser/BinaryToStringFormatter.cs | 54 +++++-------------- .../CLRExtensions/EncodingFormatExtensions.cs | 23 ++++++++ .../CLRExtensions/StringBuilderExtensions.cs | 27 ++++++++++ 3 files changed, 64 insertions(+), 40 deletions(-) create mode 100644 Asn1Parser/Utils/CLRExtensions/EncodingFormatExtensions.cs create mode 100644 Asn1Parser/Utils/CLRExtensions/StringBuilderExtensions.cs diff --git a/Asn1Parser/BinaryToStringFormatter.cs b/Asn1Parser/BinaryToStringFormatter.cs index 8a129b3..1d42150 100644 --- a/Asn1Parser/BinaryToStringFormatter.cs +++ b/Asn1Parser/BinaryToStringFormatter.cs @@ -2,23 +2,24 @@ using System.Buffers; using System.Buffers.Text; using System.Text; +using SysadminsLV.Asn1Parser.Utils.CLRExtensions; namespace SysadminsLV.Asn1Parser; static class BinaryToStringFormatter { public static String ToHexRaw(ReadOnlySpan rawData, EncodingFormat format, Boolean forceUpperCase) { - String eol = getEOL(format); - var SB = new StringBuilder(); + String eol = format.GetEndOfLine(); + var sb = new StringBuilder(); foreach (Byte b in rawData) { - byteToHexOctet(SB, b, forceUpperCase); + sb.AppendHexOctet(b, forceUpperCase); } - return SB.Append(eol).ToString(); + return sb.Append(eol).ToString(); } public static String ToHex(ReadOnlySpan rawData, EncodingFormat format, Boolean forceUpperCase) { var sb = new StringBuilder(); for (Int32 index = 0; index < rawData.Length; index++) { - byteToHexOctet(sb, rawData[index], forceUpperCase); + sb.AppendHexOctet(rawData[index], forceUpperCase); if (index == 0) { sb.Append(" "); continue; @@ -41,13 +42,13 @@ public static String ToHex(ReadOnlySpan rawData, EncodingFormat format, Bo } } - return finalizeBinaryToString(sb, format); + return sb.Append(format.GetEndOfLine()).ToString(); } public static String ToHexAddress(ReadOnlySpan rawData, EncodingFormat format, Boolean forceUpperCase) { Int32 rowCount = 0; Int32 addrLength = getAddrLength(rawData.Length); String eol = format == EncodingFormat.NOCR ? "\n" : "\r\n"; - String eof = getEOL(format); + String eof = format.GetEndOfLine(); Int32 totalRows = (Int32)Math.Ceiling(rawData.Length / 16d); Int32 bufferSize = addrLength + (52 + eol.Length) * totalRows + eof.Length; var sb = new StringBuilder(bufferSize); @@ -61,7 +62,7 @@ public static String ToHexAddress(ReadOnlySpan rawData, EncodingFormat for sb.Append(" "); rowCount += 16; } - byteToHexOctet(sb, rawData[index], forceUpperCase); + sb.AppendHexOctet(rawData[index], forceUpperCase); if (index == 0) { sb.Append(" "); continue; @@ -84,7 +85,7 @@ public static String ToHexAscii(ReadOnlySpan rawData, EncodingFormat forma var sb = new StringBuilder(); var ascii = new StringBuilder(8); for (Int32 index = 0; index < rawData.Length; index++) { - byteToHexOctet(sb, rawData[index], forceUpperCase); + sb.AppendHexOctet(rawData[index], forceUpperCase); Char c = rawData[index] < 32 || rawData[index] > 126 ? '.' : (Char)rawData[index]; @@ -111,7 +112,7 @@ public static String ToHexAscii(ReadOnlySpan rawData, EncodingFormat forma } } - return finalizeBinaryToString(sb, format); + return sb.Append(format.GetEndOfLine()).ToString(); } public static String ToHexAddressAndAscii(ReadOnlySpan rawData, EncodingFormat format, Boolean forceUpperCase) { var sb = new StringBuilder(); @@ -128,7 +129,7 @@ public static String ToHexAddressAndAscii(ReadOnlySpan rawData, EncodingFo sb.Append(" "); rowCount += 16; } - byteToHexOctet(sb, rawData[index], forceUpperCase); + sb.AppendHexOctet(rawData[index], forceUpperCase); Char c = rawData[index] < 32 || rawData[index] > 126 ? '.' : (Char)rawData[index]; @@ -154,13 +155,13 @@ public static String ToHexAddressAndAscii(ReadOnlySpan rawData, EncodingFo } } - return finalizeBinaryToString(sb, format); + return sb.Append(format.GetEndOfLine()).ToString(); } public static String ToBase64(ReadOnlySpan rawData, EncodingType encoding, EncodingFormat format) { Int32 b64Length = Base64.GetMaxEncodedToUtf8Length(rawData.Length); Span base64 = new Byte[b64Length]; OperationStatus result = Base64.EncodeToUtf8(rawData, base64, out _, out _); - String eol = getEOL(format); + String eol = format.GetEndOfLine(); Int32 rowCount = (Int32)Math.Floor(b64Length / 64d); Int32 eolCount = rowCount * eol.Length + eol.Length; PemHeader? pem = null; @@ -214,24 +215,6 @@ static PemHeader getPemHeader(EncodingType encoding) { throw new ArgumentException("Specified encoding is not valid Base64 encoding."); } - static String finalizeBinaryToString(StringBuilder sb, EncodingFormat format) { - switch (format) { - case EncodingFormat.NOCR: - return sb.Append('\n').ToString(); - case EncodingFormat.NOCRLF: - return sb.ToString().TrimEnd(); - default: - return sb.Append("\r\n").ToString(); - } - } - static String getEOL(EncodingFormat format) { - return format switch { - EncodingFormat.CRLF => "\r\n", - EncodingFormat.NOCRLF => String.Empty, - EncodingFormat.NOCR => "\n", - _ => throw new ArgumentOutOfRangeException(nameof(format), format, null) - }; - } #endregion @@ -253,15 +236,6 @@ static Int32 getAddrLength(Int32 size) { ? 4 : (h.Length % 2 == 0 ? h.Length : h.Length + 1); } - static void byteToHexOctet(StringBuilder sb, Byte b, Boolean forceUpperCase) { - sb.Append(byteToHexChar((b >> 4) & 15, forceUpperCase)); - sb.Append(byteToHexChar(b & 15, forceUpperCase)); - } - static Char byteToHexChar(Int32 b, Boolean forceUpperCase) { - return b < 10 - ? (Char)(b + 48) - : (forceUpperCase ? (Char)(b + 55) : (Char)(b + 87)); - } #endregion } \ No newline at end of file diff --git a/Asn1Parser/Utils/CLRExtensions/EncodingFormatExtensions.cs b/Asn1Parser/Utils/CLRExtensions/EncodingFormatExtensions.cs new file mode 100644 index 0000000..f608545 --- /dev/null +++ b/Asn1Parser/Utils/CLRExtensions/EncodingFormatExtensions.cs @@ -0,0 +1,23 @@ +using System; + +namespace SysadminsLV.Asn1Parser.Utils.CLRExtensions; + +///

+/// Contains extension methods for enumeration. +/// +static class EncodingFormatExtensions { + /// + /// Gets end of line implementation. + /// + /// Encoding format. + /// End of line string. Can be empty string. + /// format argument is not valid value. + public static String GetEndOfLine(this EncodingFormat format) { + return format switch { + EncodingFormat.CRLF => "\r\n", + EncodingFormat.NOCRLF => String.Empty, + EncodingFormat.NOCR => "\n", + _ => throw new ArgumentOutOfRangeException(nameof(format), format, null) + }; + } +} diff --git a/Asn1Parser/Utils/CLRExtensions/StringBuilderExtensions.cs b/Asn1Parser/Utils/CLRExtensions/StringBuilderExtensions.cs new file mode 100644 index 0000000..66e6e57 --- /dev/null +++ b/Asn1Parser/Utils/CLRExtensions/StringBuilderExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Text; + +namespace SysadminsLV.Asn1Parser.Utils.CLRExtensions; +/// +/// Contains extension methods for . +/// +internal static class StringBuilderExtensions { + /// + /// Appends byte as a two-character hex octet. If input value is less than 16, then hex string is prepended with leading zero character. + /// + /// StringBuilder instance. + /// Byte to add. + /// Specifies whether hex octet should be added in uppercase. Default is false. + /// StringBuilder instance with appended hex. + public static StringBuilder AppendHexOctet(this StringBuilder sb, Byte b, Boolean forceUpperCase = false) { + return sb + .Append(byteToHexChar((b >> 4) & 15, forceUpperCase)) + .Append(byteToHexChar(b & 15, forceUpperCase)); + } + + static Char byteToHexChar(Int32 b, Boolean forceUpperCase) { + return b < 10 + ? (Char)(b + 48) + : (forceUpperCase ? (Char)(b + 55) : (Char)(b + 87)); + } +} From e7ebea9ee8d2dcc76ee8e6035cfc44f7f1e2fa5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Thu, 12 Dec 2024 12:12:04 +0200 Subject: [PATCH 19/66] fixed class visibility --- Asn1Parser/Utils/CLRExtensions/BigIntegerExtensions.cs | 2 +- Asn1Parser/Utils/CLRExtensions/EncodingFormatExtensions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Asn1Parser/Utils/CLRExtensions/BigIntegerExtensions.cs b/Asn1Parser/Utils/CLRExtensions/BigIntegerExtensions.cs index c83d702..d6e6024 100644 --- a/Asn1Parser/Utils/CLRExtensions/BigIntegerExtensions.cs +++ b/Asn1Parser/Utils/CLRExtensions/BigIntegerExtensions.cs @@ -7,7 +7,7 @@ namespace SysadminsLV.Asn1Parser.Utils.CLRExtensions; /// /// Extension class for class. /// -static class BigIntegerExtensions { +internal static class BigIntegerExtensions { /// /// Gets a byte array in the big-endian order. /// diff --git a/Asn1Parser/Utils/CLRExtensions/EncodingFormatExtensions.cs b/Asn1Parser/Utils/CLRExtensions/EncodingFormatExtensions.cs index f608545..74bc510 100644 --- a/Asn1Parser/Utils/CLRExtensions/EncodingFormatExtensions.cs +++ b/Asn1Parser/Utils/CLRExtensions/EncodingFormatExtensions.cs @@ -5,7 +5,7 @@ namespace SysadminsLV.Asn1Parser.Utils.CLRExtensions; /// /// Contains extension methods for enumeration. /// -static class EncodingFormatExtensions { +internal static class EncodingFormatExtensions { /// /// Gets end of line implementation. /// From da53b872373e8cd42ee19241c4e065716a32946c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Thu, 20 Feb 2025 14:21:39 +0200 Subject: [PATCH 20/66] refactored classes that inherit from Asn1String by moving shared functionality into base class --- Asn1Parser/Universal/Asn1BmpString.cs | 5 ---- Asn1Parser/Universal/Asn1IA5String.cs | 5 ---- Asn1Parser/Universal/Asn1NumericString.cs | 5 ---- Asn1Parser/Universal/Asn1PrintableString.cs | 5 ---- Asn1Parser/Universal/Asn1String.cs | 24 +++++++++++++++++- Asn1Parser/Universal/Asn1TeletexString.cs | 27 ++++++++++++--------- Asn1Parser/Universal/Asn1UTF8String.cs | 5 ---- Asn1Parser/Universal/Asn1UniversalString.cs | 5 ---- Asn1Parser/Universal/Asn1VideotexString.cs | 18 ++++++-------- Asn1Parser/Universal/Asn1VisibleString.cs | 7 +----- 10 files changed, 48 insertions(+), 58 deletions(-) diff --git a/Asn1Parser/Universal/Asn1BmpString.cs b/Asn1Parser/Universal/Asn1BmpString.cs index 07d290e..b7da632 100644 --- a/Asn1Parser/Universal/Asn1BmpString.cs +++ b/Asn1Parser/Universal/Asn1BmpString.cs @@ -44,9 +44,4 @@ void m_encode(String inputString) { void m_decode(Asn1Reader asn) { Value = Encoding.BigEndianUnicode.GetString(asn.GetPayload()); } - - /// - public override String GetDisplayValue() { - return Value; - } } \ No newline at end of file diff --git a/Asn1Parser/Universal/Asn1IA5String.cs b/Asn1Parser/Universal/Asn1IA5String.cs index f40578c..c881cc0 100644 --- a/Asn1Parser/Universal/Asn1IA5String.cs +++ b/Asn1Parser/Universal/Asn1IA5String.cs @@ -61,9 +61,4 @@ void m_decode(Asn1Reader asn) { } Value = Encoding.ASCII.GetString(asn.GetPayload()); } - - /// - public override String GetDisplayValue() { - return Value; - } } \ No newline at end of file diff --git a/Asn1Parser/Universal/Asn1NumericString.cs b/Asn1Parser/Universal/Asn1NumericString.cs index 4b46a84..141afd9 100644 --- a/Asn1Parser/Universal/Asn1NumericString.cs +++ b/Asn1Parser/Universal/Asn1NumericString.cs @@ -61,9 +61,4 @@ void m_decode(Asn1Reader asn) { } Value = Encoding.ASCII.GetString(asn.GetPayload()); } - - /// - public override String GetDisplayValue() { - return Value; - } } \ No newline at end of file diff --git a/Asn1Parser/Universal/Asn1PrintableString.cs b/Asn1Parser/Universal/Asn1PrintableString.cs index ccd4522..6b2b819 100644 --- a/Asn1Parser/Universal/Asn1PrintableString.cs +++ b/Asn1Parser/Universal/Asn1PrintableString.cs @@ -73,9 +73,4 @@ static Boolean testValue(IEnumerable rawData) { List alphabet = StringUtils.GetAlphabet(TYPE); return rawData.All(alphabet.Contains); } - - /// - public override String GetDisplayValue() { - return Value; - } } \ No newline at end of file diff --git a/Asn1Parser/Universal/Asn1String.cs b/Asn1Parser/Universal/Asn1String.cs index 5608d1e..4b29464 100644 --- a/Asn1Parser/Universal/Asn1String.cs +++ b/Asn1Parser/Universal/Asn1String.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; namespace SysadminsLV.Asn1Parser.Universal; @@ -39,13 +40,34 @@ protected Asn1String(Asn1Type type) : base(type) { } ///

/// object in the position that represents ASN.1 date/time object. /// Optional expected ASN.1 type. - protected Asn1String(Asn1Reader asn, Asn1Type? type) : base(asn, type) { } + protected Asn1String(Asn1Reader asn, Asn1Type type) : base(asn, type) { + decode(asn, type); + } /// /// Gets value associated with the current object. /// public String Value { get; protected set; } = String.Empty; + void decode(Asn1Reader reader, Asn1Type type) { + ReadOnlyMemory payload = reader.GetPayloadAsMemory(); + if (!IsValidString(payload.Span)) { + throw new InvalidDataException(String.Format(InvalidType, type)); + } + + Value = Decode(payload.Span); + } + + protected virtual Boolean IsValidString(ReadOnlySpan value) { + return true; + } + protected virtual String Decode(ReadOnlySpan payload) {return String.Empty;} + + /// + public sealed override String GetDisplayValue() { + return Value; + } + /// /// Decodes any ASN.1-encoded binary string into ASN.1 string type instance. /// diff --git a/Asn1Parser/Universal/Asn1TeletexString.cs b/Asn1Parser/Universal/Asn1TeletexString.cs index 572c6c3..222fc74 100644 --- a/Asn1Parser/Universal/Asn1TeletexString.cs +++ b/Asn1Parser/Universal/Asn1TeletexString.cs @@ -22,9 +22,7 @@ public sealed class Asn1TeletexString : Asn1String { /// /// Input data contains invalid TeletexString character. /// - public Asn1TeletexString(Asn1Reader asn) : base(asn, TYPE) { - m_decode(asn); - } + public Asn1TeletexString(Asn1Reader asn) : base(asn, TYPE) { } /// /// Initializes a new instance of Asn1TeletexString from an ASN.1-encoded byte array. /// @@ -55,15 +53,22 @@ void m_encode(String inputString) { Value = inputString; Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString), TYPE))); } - void m_decode(Asn1Reader asn) { - if (asn.GetPayload().Any(b => b > 127)) { - throw new InvalidDataException(String.Format(InvalidType, TYPE.ToString())); + + protected override Boolean IsValidString(ReadOnlySpan value) { + foreach (Byte b in value) { + if (b > 127) { + return false; + } } - Value = Encoding.ASCII.GetString(asn.GetPayload()); + + return true; } - - /// - public override String GetDisplayValue() { - return Value; + protected override String Decode(ReadOnlySpan payload) { + var sb = new StringBuilder(payload.Length); + foreach (Byte b in payload) { + sb.Append((Char)b); + } + + return sb.ToString(); } } \ No newline at end of file diff --git a/Asn1Parser/Universal/Asn1UTF8String.cs b/Asn1Parser/Universal/Asn1UTF8String.cs index 8d10925..61ffc13 100644 --- a/Asn1Parser/Universal/Asn1UTF8String.cs +++ b/Asn1Parser/Universal/Asn1UTF8String.cs @@ -61,9 +61,4 @@ void m_decode(Asn1Reader asn) { static Boolean testValue(String str) { return str.All(x => Convert.ToUInt32(x) <= 255); } - - /// - public override String GetDisplayValue() { - return Value; - } } \ No newline at end of file diff --git a/Asn1Parser/Universal/Asn1UniversalString.cs b/Asn1Parser/Universal/Asn1UniversalString.cs index a31142a..c1b6235 100644 --- a/Asn1Parser/Universal/Asn1UniversalString.cs +++ b/Asn1Parser/Universal/Asn1UniversalString.cs @@ -49,9 +49,4 @@ void m_encode(String inputString) { void m_decode(Asn1Reader asn) { Value = new String(Encoding.UTF32.GetString(asn.GetPayload().Reverse().ToArray()).Reverse().ToArray()); } - - /// - public override String GetDisplayValue() { - return Value; - } } \ No newline at end of file diff --git a/Asn1Parser/Universal/Asn1VideotexString.cs b/Asn1Parser/Universal/Asn1VideotexString.cs index 557cbf1..d2fe532 100644 --- a/Asn1Parser/Universal/Asn1VideotexString.cs +++ b/Asn1Parser/Universal/Asn1VideotexString.cs @@ -21,9 +21,7 @@ public sealed class Asn1VideotexString : Asn1String { /// /// Input data contains invalid VideotexString character. /// - public Asn1VideotexString(Asn1Reader asn) : base(asn, TYPE) { - decode(asn); - } + public Asn1VideotexString(Asn1Reader asn) : base(asn, TYPE) { } /// /// Initializes a new instance of Asn1VideotexString from a ASN.1-encoded byte array. /// @@ -34,7 +32,7 @@ public Asn1VideotexString(Asn1Reader asn) : base(asn, TYPE) { /// /// Input data contains invalid VideotexString character. /// - public Asn1VideotexString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1VideotexString(Byte[] rawData) : base(new Asn1Reader(rawData), TYPE) { } /// /// Initializes a new instance of the Asn1VideotexString class from a unicode string. /// @@ -50,12 +48,12 @@ void encode(String inputString) { Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString), TYPE))); Value = inputString; } - void decode(Asn1Reader asn) { - Value = Encoding.ASCII.GetString(asn.GetPayload()); - } + protected override String Decode(ReadOnlySpan payload) { + var sb = new StringBuilder(payload.Length); + foreach (Byte b in payload) { + sb.Append((Char)b); + } - /// - public override String GetDisplayValue() { - return Value; + return sb.ToString(); } } \ No newline at end of file diff --git a/Asn1Parser/Universal/Asn1VisibleString.cs b/Asn1Parser/Universal/Asn1VisibleString.cs index 1199596..6a843ee 100644 --- a/Asn1Parser/Universal/Asn1VisibleString.cs +++ b/Asn1Parser/Universal/Asn1VisibleString.cs @@ -58,14 +58,9 @@ void m_encode(String inputString) { Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString), TYPE))); } void m_decode(Asn1Reader asn) { - if (asn.GetPayload().Any(b => b < 32 || b > 126)) { + if (asn.GetPayload().Any(b => b is < 32 or > 126)) { throw new InvalidDataException(String.Format(InvalidType, TYPE.ToString())); } Value = Encoding.ASCII.GetString(asn.GetPayload()); } - - /// - public override String GetDisplayValue() { - return Value; - } } \ No newline at end of file From 4612795b50386c8c755d1bd8b133124eb7bbf51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Thu, 20 Feb 2025 14:25:18 +0200 Subject: [PATCH 21/66] moved from .NET Standard to .NET 8.0 by keeping compat with .NET Framework 4.7.2 --- Asn1Parser/Asn1Parser.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Asn1Parser/Asn1Parser.csproj b/Asn1Parser/Asn1Parser.csproj index 178d657..e4e0267 100644 --- a/Asn1Parser/Asn1Parser.csproj +++ b/Asn1Parser/Asn1Parser.csproj @@ -1,6 +1,6 @@  - netstandard2.0;net472 + net8.0;net472 latest 1.3.0 SysadminsLV.Asn1Parser @@ -17,8 +17,8 @@ True - - bin\Release\netstandard2.0\SysadminsLV.Asn1Parser.xml + + bin\Release\net8.0\SysadminsLV.Asn1Parser.xml bin\Release\net472\SysadminsLV.Asn1Parser.xml @@ -29,4 +29,4 @@ - + \ No newline at end of file From 76d2035e677b12319b531a3cba2e6c819cba0522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 10:54:26 +0200 Subject: [PATCH 22/66] refactored Encode method --- Asn1Parser/Asn1Utils.cs | 56 ++++++++++------------------------------- 1 file changed, 13 insertions(+), 43 deletions(-) diff --git a/Asn1Parser/Asn1Utils.cs b/Asn1Parser/Asn1Utils.cs index d139fbf..0ae1d55 100644 --- a/Asn1Parser/Asn1Utils.cs +++ b/Asn1Parser/Asn1Utils.cs @@ -77,48 +77,7 @@ public static Int64 CalculatePayloadLength(Byte[] asnHeader) { /// An enumeration of type represented as byte. /// Wrapped encoded byte array. /// If rawData is null, an empty tag is encoded. - public static Byte[] Encode(Byte[]? rawData, Byte enclosingTag) { - if (rawData == null) { - return [enclosingTag, 0]; - } - Byte[] retValue; - if (rawData.Length < 128) { - retValue = new Byte[rawData.Length + 2]; - retValue[0] = enclosingTag; - retValue[1] = (Byte)rawData.Length; - rawData.CopyTo(retValue, 2); - } else { - Byte[] lenBytes = new Byte[4]; - Int32 num = rawData.Length; - Int32 counter = 0; - while (num >= 256) { - lenBytes[counter] = (Byte)(num & 255); - num >>= 8; - counter++; - } - // 3 is: len byte and enclosing tag - retValue = new Byte[rawData.Length + 3 + counter]; - rawData.CopyTo(retValue, 3 + counter); - retValue[0] = enclosingTag; - retValue[1] = (Byte)(129 + counter); - retValue[2] = (Byte)num; - Int32 n = 3; - for (Int32 i = counter - 1; i >= 0; i--) { - retValue[n] = lenBytes[i]; - n++; - } - } - return retValue; - } - /// - /// Wraps encoded data to an ASN.1 type/structure. - /// - /// This method do not check whether the data in rawData is valid data for specified enclosing type. - /// A byte array to wrap. - /// An enumeration of type represented as byte. - /// Wrapped encoded byte array. - /// If rawData is null, an empty tag is encoded. - public static Span Encode(ReadOnlySpan rawData, Byte enclosingTag) { + public static Memory Encode(ReadOnlySpan rawData, Byte enclosingTag) { Byte[] retValue; if (rawData == null) { retValue = [enclosingTag, 0]; @@ -172,10 +131,21 @@ public static Span Encode(ReadOnlySpan rawData, Byte enclosingTag) { /// Wrapped encoded byte array. /// If rawData is null, an empty tag is encoded. public static Byte[] Encode(Byte[] rawData, Asn1Type type) { + return Encode(rawData, (Byte)type).ToArray(); + } + /// + /// Wraps encoded data to an ASN.1 type/structure. + /// + /// This method do not check whether the data in rawData is valid data for specified enclosing type. + /// A byte array to wrap. + /// An enumeration of . + /// Wrapped encoded byte array. + /// If rawData is null, an empty tag is encoded. + public static Memory Encode(ReadOnlySpan rawData, Asn1Type type) { return Encode(rawData, (Byte)type); } #endregion - + #region internal public static String GetViewValue(Asn1Reader asn) { if (asn.PayloadLength == 0 && asn.Tag != (Byte)Asn1Type.NULL) { return "NULL"; } From 579f15301d150fb7652bba9475045cfe01112b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 10:58:40 +0200 Subject: [PATCH 23/66] added few more Obsolete attributes and better way to get a copy of ASN reader that points to current position. --- Asn1Parser/Asn1Reader.cs | 9 +++++++++ Asn1Parser/Universal/Asn1Universal.cs | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Asn1Parser/Asn1Reader.cs b/Asn1Parser/Asn1Reader.cs index 2bfc98d..d6e37da 100644 --- a/Asn1Parser/Asn1Reader.cs +++ b/Asn1Parser/Asn1Reader.cs @@ -44,6 +44,7 @@ public class Asn1Reader { /// /// This constructor creates a copy of a current position of an existing ASN1 object. /// + [Obsolete("Consider using 'GetReader()' method on existing instance.", true)] public Asn1Reader(Asn1Reader asn) : this(asn.GetTagRawDataAsMemory(), 0, true) { } /// /// Initializes a new instance of the ASN1 class by using an ASN.1 encoded byte array. @@ -59,6 +60,7 @@ public Asn1Reader(Asn1Reader asn) : this(asn.GetTagRawDataAsMemory(), 0, true) { /// If rawData size is greater than outer structure size, constructor will take only /// required bytes from input data. /// + [Obsolete("Use constructor that accepts 'ReadOnlyMemory' parameter.")] public Asn1Reader(Byte[] rawData) : this(rawData, 0) { } /// /// Initializes a new instance of the ASN1 class by using an ASN.1 encoded byte array. @@ -607,6 +609,13 @@ public Asn1Universal GetTagObject() { }; } /// + /// Returns a new instance of that points to current tag. This method does not allocate extra memory. + /// + /// A new instance of . + public Asn1Reader GetReader() { + return new Asn1Reader(GetRawDataAsMemory(), 0, true); + } + /// /// Recursively processes ASN tree and builds internal offset map. /// /// A number of processed ASN structures. diff --git a/Asn1Parser/Universal/Asn1Universal.cs b/Asn1Parser/Universal/Asn1Universal.cs index 46401c0..18d3dab 100644 --- a/Asn1Parser/Universal/Asn1Universal.cs +++ b/Asn1Parser/Universal/Asn1Universal.cs @@ -86,7 +86,7 @@ protected Asn1Universal(Asn1Reader asn, Asn1Type? type) { /// /// Existing object. protected void Initialize(Asn1Reader asn) { - asnReader = new Asn1Reader(asn); // do not store external ASN reader reference. + asnReader = asn.GetReader(); // do not store external ASN reader reference. Tag = asn.Tag; TagName = asn.TagName; IsContainer = asn.IsConstructed; From d3b6aa78757af0227ae51d4e03a448fb9766e3a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 11:00:39 +0200 Subject: [PATCH 24/66] updated ASN1 builder to support ReadOnlyMemory. Still need some work. --- Asn1Parser/Asn1Builder.cs | 154 +++++++++++++++++++++++++++----------- 1 file changed, 109 insertions(+), 45 deletions(-) diff --git a/Asn1Parser/Asn1Builder.cs b/Asn1Parser/Asn1Builder.cs index 5d0f95e..9b53e62 100644 --- a/Asn1Parser/Asn1Builder.cs +++ b/Asn1Parser/Asn1Builder.cs @@ -13,7 +13,7 @@ namespace SysadminsLV.Asn1Parser; /// Represents ASN.1 Distinguished Encoding Rules (DER) binary builder. /// public class Asn1Builder { - readonly List _rawData; + readonly List> _rawData; Asn1Builder() { _rawData = []; @@ -31,7 +31,7 @@ public class Asn1Builder { /// /// Current instance with added value. public Asn1Builder AddBoolean(Boolean value) { - _rawData.AddRange(new Asn1Boolean(value).GetRawData()); + _rawData.Add(new Asn1Boolean(value).GetRawDataAsMemory()); return this; } /// @@ -42,7 +42,7 @@ public Asn1Builder AddBoolean(Boolean value) { /// /// Current instance with added value. public Asn1Builder AddInteger(BigInteger value) { - _rawData.AddRange(new Asn1Integer(value).GetRawData()); + _rawData.Add(new Asn1Integer(value).GetRawDataAsMemory()); return this; } /// @@ -59,7 +59,7 @@ public Asn1Builder AddBitString(Byte[] value, Byte unusedBits) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - _rawData.AddRange(new Asn1BitString(value, unusedBits).GetRawData()); + _rawData.Add(new Asn1BitString(value, unusedBits).GetRawDataAsMemory()); return this; } /// @@ -76,7 +76,7 @@ public Asn1Builder AddBitString(Byte[] value, Boolean calculateUnusedBits = fals if (value == null) { throw new ArgumentNullException(nameof(value)); } - _rawData.AddRange(new Asn1BitString(value, calculateUnusedBits).GetRawData()); + _rawData.Add(new Asn1BitString(value, calculateUnusedBits).GetRawDataAsMemory()); return this; } /// @@ -93,7 +93,7 @@ public Asn1Builder AddOctetString(Byte[] value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - _rawData.AddRange(new Asn1OctetString(value, false).GetRawData()); + _rawData.Add(new Asn1OctetString(value, false).GetRawDataAsMemory()); return this; } /// @@ -101,7 +101,7 @@ public Asn1Builder AddOctetString(Byte[] value) { /// /// Current instance with added value. public Asn1Builder AddNull() { - _rawData.AddRange(new Asn1Null().GetRawData()); + _rawData.Add(new Asn1Null().GetRawDataAsMemory()); return this; } /// @@ -118,7 +118,7 @@ public Asn1Builder AddObjectIdentifier(Oid value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - _rawData.AddRange(new Asn1ObjectIdentifier(value).GetRawData()); + _rawData.Add(new Asn1ObjectIdentifier(value).GetRawDataAsMemory()); return this; } /// @@ -136,7 +136,7 @@ public Asn1Builder AddRelativeOid(String value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - _rawData.AddRange(new Asn1RelativeOid(value).GetRawData()); + _rawData.Add(new Asn1RelativeOid(value).GetRawDataAsMemory()); return this; } /// @@ -147,7 +147,7 @@ public Asn1Builder AddRelativeOid(String value) { /// /// Current instance with added value. public Asn1Builder AddEnumerated(UInt64 value) { - _rawData.AddRange(new Asn1Enumerated(value).GetRawData()); + _rawData.Add(new Asn1Enumerated(value).GetRawDataAsMemory()); return this; } /// @@ -164,7 +164,7 @@ public Asn1Builder AddUTF8String(String value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - _rawData.AddRange(new Asn1UTF8String(value).GetRawData()); + _rawData.Add(new Asn1UTF8String(value).GetRawDataAsMemory()); return this; } /// @@ -184,11 +184,12 @@ public Asn1Builder AddSequence(Byte[] value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - IEnumerable encoded = Asn1Utils.Encode(value, 0x30); var asn = new Asn1Reader(value); asn.BuildOffsetMap(); // if we reach this far, most likely, the data is ok. - _rawData.AddRange(encoded); + ReadOnlyMemory encoded = Asn1Utils.Encode(value, 0x30); + _rawData.Add(encoded); + return this; } /// @@ -208,11 +209,13 @@ public Asn1Builder AddSet(Byte[] value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - IEnumerable encoded = Asn1Utils.Encode(value, 0x31); + var asn = new Asn1Reader(value); asn.BuildOffsetMap(); // if we reach this far, most likely, the data is ok. - _rawData.AddRange(encoded); + ReadOnlyMemory encoded = Asn1Utils.Encode(value, 0x31); + _rawData.Add(encoded); + return this; } /// @@ -229,7 +232,7 @@ public Asn1Builder AddNumericString(String value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - _rawData.AddRange(new Asn1NumericString(value).GetRawData()); + _rawData.Add(new Asn1NumericString(value).GetRawDataAsMemory()); return this; } /// @@ -246,7 +249,7 @@ public Asn1Builder AddPrintableString(String value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - _rawData.AddRange(new Asn1PrintableString(value).GetRawData()); + _rawData.Add(new Asn1PrintableString(value).GetRawDataAsMemory()); return this; } /// @@ -263,7 +266,7 @@ public Asn1Builder AddTeletexString(String value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - _rawData.AddRange(new Asn1TeletexString(value).GetRawData()); + _rawData.Add(new Asn1TeletexString(value).GetRawDataAsMemory()); return this; } /// @@ -280,7 +283,7 @@ public Asn1Builder AddVideotexString(String value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - _rawData.AddRange(Asn1Utils.Encode(Encoding.ASCII.GetBytes(value), Asn1Type.VideotexString)); + _rawData.Add(Asn1Utils.Encode(Encoding.ASCII.GetBytes(value), Asn1Type.VideotexString)); return this; } /// @@ -297,7 +300,7 @@ public Asn1Builder AddIA5String(String value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - _rawData.AddRange(new Asn1IA5String(value).GetRawData()); + _rawData.Add(new Asn1IA5String(value).GetRawDataAsMemory()); return this; } /// @@ -308,7 +311,7 @@ public Asn1Builder AddIA5String(String value) { /// /// Current instance with added value. public Asn1Builder AddUtcTime(DateTime value) { - _rawData.AddRange(new Asn1UtcTime(value).GetRawData()); + _rawData.Add(new Asn1UtcTime(value).GetRawDataAsMemory()); return this; } /// @@ -319,7 +322,7 @@ public Asn1Builder AddUtcTime(DateTime value) { /// /// Current instance with added value. public Asn1Builder AddGeneralizedTime(DateTime value) { - _rawData.AddRange(new Asn1GeneralizedTime(value).GetRawData()); + _rawData.Add(new Asn1GeneralizedTime(value).GetRawDataAsMemory()); return this; } /// @@ -334,9 +337,9 @@ public Asn1Builder AddGeneralizedTime(DateTime value) { /// Generalized Time. /// public Asn1Builder AddRfcDateTime(DateTime value) { - _rawData.AddRange(value.Year < 2050 - ? new Asn1UtcTime(value).GetRawData() - : new Asn1GeneralizedTime(value).GetRawData()); + _rawData.Add(value.Year < 2050 + ? new Asn1UtcTime(value).GetRawDataAsMemory() + : new Asn1GeneralizedTime(value).GetRawDataAsMemory()); return this; } @@ -354,7 +357,7 @@ public Asn1Builder AddVisibleString(String value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - _rawData.AddRange(new Asn1VisibleString(value).GetRawData()); + _rawData.Add(new Asn1VisibleString(value).GetRawDataAsMemory()); return this; } /// @@ -371,7 +374,7 @@ public Asn1Builder AddUniversalString(String value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - _rawData.AddRange(new Asn1UniversalString(value).GetRawData()); + _rawData.Add(new Asn1UniversalString(value).GetRawDataAsMemory()); return this; } /// @@ -388,7 +391,7 @@ public Asn1Builder AddBMPString(String value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - _rawData.AddRange(new Asn1BMPString(value).GetRawData()); + _rawData.Add(new Asn1BMPString(value).GetRawDataAsMemory()); return this; } /// @@ -407,7 +410,7 @@ public Asn1Builder AddDerData(Byte[] value) { } var asn = new Asn1Reader(value); asn.BuildOffsetMap(); - _rawData.AddRange(value); + _rawData.Add(value); return this; } /// @@ -427,7 +430,7 @@ public Asn1Builder AddDerData(Byte[] value, Byte outerTag) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - _rawData.AddRange(Asn1Utils.Encode(value, outerTag)); + _rawData.Add(Asn1Utils.Encode(value, outerTag)); return this; } /// @@ -462,7 +465,7 @@ public Asn1Builder AddImplicit(Byte implicitTag, Byte[] value, Boolean mustEncod throw new ArgumentNullException(nameof(value)); } if (mustEncode) { - _rawData.AddRange(Asn1Utils.Encode(value, (Byte)(0x80 + implicitTag))); + _rawData.Add(Asn1Utils.Encode(value, (Byte)(0x80 + implicitTag))); } else { if (value.Length < 2) { throw new InvalidDataException(); @@ -471,7 +474,7 @@ public Asn1Builder AddImplicit(Byte implicitTag, Byte[] value, Boolean mustEncod asn.BuildOffsetMap(); Byte[] valueCopy = value.ToArray(); valueCopy[0] = (Byte)(0x80 + implicitTag); - _rawData.AddRange(valueCopy); + _rawData.Add(valueCopy); } return this; } @@ -506,13 +509,13 @@ public Asn1Builder AddExplicit(Byte explicitTag, Byte[] value, Boolean mustEncod throw new ArgumentNullException(nameof(value)); } if (mustEncode) { - _rawData.AddRange(Asn1Utils.Encode(value, (Byte)(0xa0 + explicitTag))); + _rawData.Add(Asn1Utils.Encode(value, (Byte)(0xa0 + explicitTag))); } else { var asn = new Asn1Reader(value); asn.BuildOffsetMap(); Byte[] valueCopy = value.ToArray(); valueCopy[0] = (Byte)(0xa0 + explicitTag); - _rawData.AddRange(valueCopy); + _rawData.Add(valueCopy); } return this; } @@ -532,7 +535,7 @@ public Asn1Builder AddBitString(Func selector) { throw new ArgumentNullException(nameof(selector)); } Asn1Builder b = selector(new Asn1Builder()); - _rawData.AddRange(new Asn1BitString(b._rawData.ToArray(), 0).GetRawData()); + _rawData.Add(new Asn1BitString(b.GetRawDataAsMemory().Span, 0).GetRawDataAsMemory()); return this; } /// @@ -551,7 +554,7 @@ public Asn1Builder AddOctetString(Func selector) { throw new ArgumentNullException(nameof(selector)); } Asn1Builder b = selector(new Asn1Builder()); - _rawData.AddRange(Asn1Utils.Encode(b._rawData.ToArray(), Asn1Type.OCTET_STRING)); + _rawData.Add(Asn1Utils.Encode(b.GetRawDataAsMemory().Span, (Byte)Asn1Type.OCTET_STRING)); return this; } /// @@ -567,7 +570,7 @@ public Asn1Builder AddSequence(Func selector) { throw new ArgumentNullException(nameof(selector)); } Asn1Builder b = selector(new Asn1Builder()); - _rawData.AddRange(Asn1Utils.Encode(b._rawData.ToArray(), 0x30)); + _rawData.Add(Asn1Utils.Encode(b.GetRawDataAsMemory().Span, 0x30)); return this; } /// @@ -583,7 +586,7 @@ public Asn1Builder AddSet(Func selector) { throw new ArgumentNullException(nameof(selector)); } Asn1Builder b = selector(new Asn1Builder()); - _rawData.AddRange(Asn1Utils.Encode(b._rawData.ToArray(), 0x31)); + _rawData.Add(Asn1Utils.Encode(b.GetRawDataAsMemory().Span, 0x31)); return this; } /// @@ -602,7 +605,7 @@ public Asn1Builder AddExplicit(Byte explicitTag, Func throw new ArgumentNullException(nameof(selector)); } Asn1Builder b = selector(new Asn1Builder()); - _rawData.AddRange(Asn1Utils.Encode(b._rawData.ToArray(), (Byte)(0xa0 + explicitTag))); + _rawData.Add(Asn1Utils.Encode(b.GetRawDataAsMemory().Span, (Byte)(0xa0 + explicitTag))); return this; } @@ -618,12 +621,40 @@ public Asn1Builder AddExplicit(Byte explicitTag, Func /// A new instance of ASN.1 DER builder that contains the state of the current instance. /// public Asn1Builder Encode(Byte outerTag = 0x30) { - IEnumerable encoded = GetEncoded(outerTag); + Byte[] encoded = GetEncoded(outerTag); _rawData.Clear(); - _rawData.AddRange(encoded); + _rawData.Add(encoded); return new Asn1Builder(this); } + Byte[] getEncoded(Byte outerTag, Boolean includeHeader) { + Int32 payloadLength = 0; + foreach (ReadOnlyMemory chunk in _rawData) { + payloadLength += chunk.Length; + } + Int32 headerLength = 0; + Byte[]? header = null; + if (includeHeader) { + header = Asn1Utils.GetLengthBytes(payloadLength); + headerLength = header.Length; + } + Byte[] memory = new Byte[headerLength + payloadLength]; + Int32 i = 0; + if (includeHeader) { + for (; i < header!.Length; i++) { + memory[i] = header[i]; + } + } + + foreach (ReadOnlyMemory chunk in _rawData) { + foreach (Byte b in chunk.Span) { + memory[i] = b; + i++; + } + } + + return memory; + } /// /// Gets ASN.1-encoded byte array that represents current state of builder wrapped using outer ASN.1 type. /// @@ -635,16 +666,38 @@ public Asn1Builder Encode(Byte outerTag = 0x30) { /// ASN.1-encoded byte array. /// public Byte[] GetEncoded(Byte outerTag = 0x30) { - return Asn1Utils.Encode(_rawData.ToArray(), outerTag); + return getEncoded(outerTag, true); } /// - /// Gets a raw data of the current state of the builder. + /// Gets ASN.1-encoded memory that represents current state of builder wrapped using outer ASN.1 type. + /// + /// + /// Outer type to wrap current state of builder. Outer type must not by the type that is used in primitive form only. + /// Default outer tag is constructed SEQUENCE (0x30 or decimal 48). + /// + /// + /// ASN.1-encoded memory. + /// + public ReadOnlyMemory GetEncodedAsMemory(Byte outerTag) { + return getEncoded(outerTag, true); + } + /// + /// Gets an unencoded raw data of the current state of the builder. /// /// /// Raw data. /// public Byte[] GetRawData() { - return _rawData.ToArray(); + return getEncoded(0, false); + } + /// + /// Gets an unencoded raw data of the current state of the builder. + /// + /// + /// Raw data. + /// + public ReadOnlyMemory GetRawDataAsMemory() { + return getEncoded(0, false); } /// @@ -661,7 +714,18 @@ public static Asn1Builder Create() { /// ASN.1 Builder. public static Asn1Builder Create(IEnumerable rawData) { var builder = new Asn1Builder(); - builder._rawData.AddRange(rawData); + builder._rawData.Add(rawData.ToArray()); + + return builder; + } + /// + /// Creates a default instance of Asn1Builder class from existing ASN.1-encoded data. + /// + /// ASN.1-encoded data to initialize the builder from. + /// ASN.1 Builder. + public static Asn1Builder Create(ReadOnlySpan rawData) { + var builder = new Asn1Builder(); + builder._rawData.Add(new ReadOnlyMemory(rawData.ToArray())); return builder; } From ef343f97e15b28a766f0e15ed0f1539e370d7fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 11:02:03 +0200 Subject: [PATCH 25/66] added support for ReadOnlyMemory in ASN.1 type decoders --- Asn1Parser/Universal/Asn1BitString.cs | 16 ++++++++-------- Asn1Parser/Universal/Asn1BmpString.cs | 8 ++++++++ Asn1Parser/Universal/Asn1DateTime.cs | 4 ++-- Asn1Parser/Universal/Asn1GeneralizedTime.cs | 9 +++++++++ Asn1Parser/Universal/Asn1IA5String.cs | 11 +++++++++++ Asn1Parser/Universal/Asn1NumericString.cs | 11 +++++++++++ Asn1Parser/Universal/Asn1PrintableString.cs | 11 +++++++++++ Asn1Parser/Universal/Asn1String.cs | 9 +++------ Asn1Parser/Universal/Asn1TeletexString.cs | 11 +++++++++++ Asn1Parser/Universal/Asn1UTF8String.cs | 13 ++++++++++++- Asn1Parser/Universal/Asn1Universal.cs | 3 ++- Asn1Parser/Universal/Asn1UniversalString.cs | 8 ++++++++ Asn1Parser/Universal/Asn1UtcTime.cs | 9 +++++++++ Asn1Parser/Universal/Asn1VideotexString.cs | 11 +++++++++++ Asn1Parser/Universal/Asn1VisibleString.cs | 11 +++++++++++ tests/Asn1Parser.Tests/Asn1DateTimeTests.cs | 4 ++-- tests/Asn1Parser.Tests/Asn1OidTests.cs | 2 +- tests/Asn1Parser.Tests/Asn1RelativeOidTests.cs | 2 +- 18 files changed, 131 insertions(+), 22 deletions(-) diff --git a/Asn1Parser/Universal/Asn1BitString.cs b/Asn1Parser/Universal/Asn1BitString.cs index d05b6f4..cac045e 100644 --- a/Asn1Parser/Universal/Asn1BitString.cs +++ b/Asn1Parser/Universal/Asn1BitString.cs @@ -50,7 +50,7 @@ public Asn1BitString(Byte[] valueToEncode, Boolean calculateUnusedBits) : base(T /// Raw value to encode. /// A number of unused bits in bit string. /// valueToEncode parameter is null reference. - public Asn1BitString(Byte[] valueToEncode, Byte unusedBits) : base(TYPE) { + public Asn1BitString(ReadOnlySpan valueToEncode, Byte unusedBits) : base(TYPE) { if (valueToEncode == null) { throw new ArgumentNullException(nameof(valueToEncode)); } m_encode(valueToEncode, false, unusedBits); } @@ -64,15 +64,15 @@ public Asn1BitString(Byte[] valueToEncode, Byte unusedBits) : base(TYPE) { /// public Byte[] Value { get; private set; } = []; - void m_encode(Byte[] value, Boolean calc, Byte unusedBits) { - Value = value; + void m_encode(ReadOnlySpan value, Boolean calc, Byte unusedBits) { + Value = value.ToArray(); UnusedBits = calc ? CalculateUnusedBits(value) : unusedBits; - Byte[] v = new Byte[value.Length + 1]; + Span v = new Byte[value.Length + 1]; v[0] = UnusedBits; - value.CopyTo(v, 1); - Initialize(new Asn1Reader(Asn1Utils.Encode(v, TYPE))); + value.CopyTo(v.Slice(1)); + Initialize(new Asn1Reader(Asn1Utils.Encode(v, (Byte)TYPE))); } /// @@ -94,12 +94,12 @@ public override String GetDisplayValue() { /// A byte array to process. /// The number of unused bits. /// bytes parameter is null reference. - public static Byte CalculateUnusedBits(Byte[] bytes) { + public static Byte CalculateUnusedBits(ReadOnlySpan bytes) { if (bytes == null) { throw new ArgumentNullException(nameof(bytes)); } - return CalculateUnusedBits(bytes[bytes.Length - 1]); + return CalculateUnusedBits(bytes[bytes.Length - 1]); // calculate unused bits based on last byte. } /// /// Calculates the number of bits left unused in the specified byte. diff --git a/Asn1Parser/Universal/Asn1BmpString.cs b/Asn1Parser/Universal/Asn1BmpString.cs index b7da632..a04b265 100644 --- a/Asn1Parser/Universal/Asn1BmpString.cs +++ b/Asn1Parser/Universal/Asn1BmpString.cs @@ -30,6 +30,14 @@ public Asn1BMPString(Asn1Reader asn) : base(asn, TYPE) { /// public Asn1BMPString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } /// + /// Initializes a new instance of Asn1BitString from a ASN.1-encoded byte array. + /// + /// ASN.1-encoded byte array. + /// + /// rawData is not BMPString data type. + /// + public Asn1BMPString(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } + /// /// Initializes a new instance of the Asn1BMPString class from a unicode string. /// /// A unicode string to encode. diff --git a/Asn1Parser/Universal/Asn1DateTime.cs b/Asn1Parser/Universal/Asn1DateTime.cs index cdb9146..164a71e 100644 --- a/Asn1Parser/Universal/Asn1DateTime.cs +++ b/Asn1Parser/Universal/Asn1DateTime.cs @@ -26,7 +26,7 @@ protected Asn1DateTime(Asn1Reader asn, Asn1Type type) : base(asn, type) { if (type is not (Asn1Type.UTCTime or Asn1Type.GeneralizedTime)) { throw new ArgumentException("Invalid ASN type. Must be either, UTCTime or GeneralizedTime."); } - m_decode(asn.GetTagRawData()); + m_decode(asn.GetTagRawDataAsMemory()); } protected Asn1DateTime(Asn1Type type, DateTime time, TimeZoneInfo? zone = null, Boolean preciseTime = false) : this(type) { m_encode(type, time, zone, preciseTime); @@ -51,7 +51,7 @@ void m_encode(Asn1Type type, DateTime time, TimeZoneInfo? zone, Boolean preciseT Initialize(new Asn1Reader(Asn1Utils.Encode(DateTimeUtils.Encode(time, ref zone, utcTime, preciseTime), type))); ZoneInfo = zone; } - void m_decode(Byte[] rawData) { + void m_decode(ReadOnlyMemory rawData) { var asn = new Asn1Reader(rawData); Initialize(asn); Value = DateTimeUtils.Decode(asn, out TimeZoneInfo zoneInfo); diff --git a/Asn1Parser/Universal/Asn1GeneralizedTime.cs b/Asn1Parser/Universal/Asn1GeneralizedTime.cs index 6948b28..441e31d 100644 --- a/Asn1Parser/Universal/Asn1GeneralizedTime.cs +++ b/Asn1Parser/Universal/Asn1GeneralizedTime.cs @@ -48,4 +48,13 @@ public Asn1GeneralizedTime(Asn1Reader asn) : base(asn, TYPE) { } /// The current state of ASN1 object is not Generalized Time. /// public Asn1GeneralizedTime(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + /// + /// Initializes a new instance of the Asn1GeneralizedTime class from a byte array that + /// represents encoded UTC time. + /// + /// ASN.1-encoded byte array. + /// + /// The current state of ASN1 object is not Generalized Time. + /// + public Asn1GeneralizedTime(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } } \ No newline at end of file diff --git a/Asn1Parser/Universal/Asn1IA5String.cs b/Asn1Parser/Universal/Asn1IA5String.cs index c881cc0..2d55504 100644 --- a/Asn1Parser/Universal/Asn1IA5String.cs +++ b/Asn1Parser/Universal/Asn1IA5String.cs @@ -38,6 +38,17 @@ public Asn1IA5String(Asn1Reader asn) : base(asn, TYPE) { /// public Asn1IA5String(Byte[] rawData) : this(new Asn1Reader(rawData)) { } /// + /// Initializes a new instance of Asn1IA5String from a ASN.1-encoded byte array. + /// + /// ASN.1-encoded byte array. + /// + /// rawData is not IA5String data type. + /// + /// + /// Input data contains invalid IA5String character. + /// + public Asn1IA5String(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } + /// /// Initializes a new instance of the Asn1IA5String class from a unicode string. /// /// A unicode string to encode. diff --git a/Asn1Parser/Universal/Asn1NumericString.cs b/Asn1Parser/Universal/Asn1NumericString.cs index 141afd9..a14298f 100644 --- a/Asn1Parser/Universal/Asn1NumericString.cs +++ b/Asn1Parser/Universal/Asn1NumericString.cs @@ -38,6 +38,17 @@ public Asn1NumericString(Asn1Reader asn) : base(asn, TYPE) { /// public Asn1NumericString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } /// + /// Initializes a new instance of Asn1NumericString from a ASN.1-encoded byte array. + /// + /// ASN.1-encoded byte array. + /// + /// rawData is not NumericString data type. + /// + /// + /// Input data contains invalid NumericString character. + /// + public Asn1NumericString(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } + /// /// Initializes a new instance of the Asn1NumericString class from a unicode string. /// /// A unicode string to encode. diff --git a/Asn1Parser/Universal/Asn1PrintableString.cs b/Asn1Parser/Universal/Asn1PrintableString.cs index 6b2b819..efb770c 100644 --- a/Asn1Parser/Universal/Asn1PrintableString.cs +++ b/Asn1Parser/Universal/Asn1PrintableString.cs @@ -40,6 +40,17 @@ public Asn1PrintableString(Asn1Reader asn) : base(asn, TYPE) { /// public Asn1PrintableString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } /// + /// Initializes a new instance of Asn1PrintableString from a ASN.1-encoded byte array. + /// + /// ASN.1-encoded byte array. + /// + /// rawData is not PrintableString data type. + /// + /// + /// Input data contains invalid PrintableString character. + /// + public Asn1PrintableString(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } + /// /// Initializes a new instance of the Asn1PrintableString class from a unicode string. /// /// A unicode string to encode. diff --git a/Asn1Parser/Universal/Asn1String.cs b/Asn1Parser/Universal/Asn1String.cs index 4b29464..847f7e8 100644 --- a/Asn1Parser/Universal/Asn1String.cs +++ b/Asn1Parser/Universal/Asn1String.cs @@ -83,19 +83,16 @@ public sealed override String GetDisplayValue() { /// /// Input data is not valid string type. /// - public static Asn1String DecodeAnyString(Byte[] rawData, IEnumerable? allowedStringTypes = null) { - if (rawData == null) { - throw new ArgumentNullException(nameof(rawData)); - } + public static Asn1String DecodeAnyString(ReadOnlyMemory rawData, IEnumerable? allowedStringTypes = null) { if (rawData.Length < 2) { throw new ArgumentException("Raw data must have at least tag (1 byte) and length components (1 byte) in TLV structure."); } IEnumerable asn1Types = allowedStringTypes?.ToList(); - if (asn1Types != null && !asn1Types.Contains((Asn1Type)rawData[0])) { + if (asn1Types != null && !asn1Types.Contains((Asn1Type)rawData.Span[0])) { throw new ArgumentException("Input string is not permitted by restriction."); } - var tag = (Asn1Type)(rawData[0] & (Int32)Asn1Type.TAG_MASK); + var tag = (Asn1Type)(rawData.Span[0] & (Int32)Asn1Type.TAG_MASK); return tag switch { Asn1Type.IA5String => new Asn1IA5String(rawData), Asn1Type.PrintableString => new Asn1PrintableString(rawData), diff --git a/Asn1Parser/Universal/Asn1TeletexString.cs b/Asn1Parser/Universal/Asn1TeletexString.cs index 222fc74..b0111f4 100644 --- a/Asn1Parser/Universal/Asn1TeletexString.cs +++ b/Asn1Parser/Universal/Asn1TeletexString.cs @@ -35,6 +35,17 @@ public Asn1TeletexString(Asn1Reader asn) : base(asn, TYPE) { } /// public Asn1TeletexString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } /// + /// Initializes a new instance of Asn1TeletexString from an ASN.1-encoded byte array. + /// + /// ASN.1-encoded byte array. + /// + /// rawData parameter represents different data type. + /// + /// + /// Input data contains invalid TeletexString character. + /// + public Asn1TeletexString(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } + /// /// Initializes a new instance of Asn1TeletexString from a string that contains valid /// Teletex String characters. /// diff --git a/Asn1Parser/Universal/Asn1UTF8String.cs b/Asn1Parser/Universal/Asn1UTF8String.cs index 61ffc13..64f643a 100644 --- a/Asn1Parser/Universal/Asn1UTF8String.cs +++ b/Asn1Parser/Universal/Asn1UTF8String.cs @@ -38,6 +38,17 @@ public Asn1UTF8String(Asn1Reader asn) : base(asn, TYPE) { /// public Asn1UTF8String(Byte[] rawData) : this(new Asn1Reader(rawData)) { } /// + /// Initializes a new instance of Asn1UTF8String from a ASN.1-encoded byte array. + /// + /// ASN.1-encoded byte array. + /// + /// rawData is not UTF8String data type. + /// + /// + /// Input data contains invalid UTF8String character. + /// + public Asn1UTF8String(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } + /// /// Initializes a new instance of the Asn1UTF8String class from a unicode string. /// /// A unicode string to encode. @@ -53,7 +64,7 @@ void m_encode(String inputString) { throw new InvalidDataException(String.Format(InvalidType, TYPE.ToString())); } Value = inputString; - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.UTF8.GetBytes(inputString), TYPE))); + Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.UTF8.GetBytes(inputString).AsMemory().Span, TYPE))); } void m_decode(Asn1Reader asn) { Value = Encoding.UTF8.GetString(asn.GetPayload()); diff --git a/Asn1Parser/Universal/Asn1Universal.cs b/Asn1Parser/Universal/Asn1Universal.cs index 18d3dab..4a60a93 100644 --- a/Asn1Parser/Universal/Asn1Universal.cs +++ b/Asn1Parser/Universal/Asn1Universal.cs @@ -78,7 +78,7 @@ protected Asn1Universal(Asn1Reader asn, Asn1Type? type) { /// /// Gets the full tag raw data, including header and payload information. /// - [Obsolete("Use 'GetRawData()' method instead.", true)] + [Obsolete("Use 'GetRawDataAsMemory()' method instead.", true)] public Byte[] RawData => GetRawData(); /// @@ -119,6 +119,7 @@ public virtual String Format(EncodingType encoding = EncodingType.Base64) { /// Gets the full tag raw data, including header and payload information. /// /// ASN.1-encoded type. + [Obsolete("Consider using 'GetRawDataAsMemory()' method instead.")] public Byte[] GetRawData() { return asnReader!.GetTagRawData(); } diff --git a/Asn1Parser/Universal/Asn1UniversalString.cs b/Asn1Parser/Universal/Asn1UniversalString.cs index c1b6235..bee2ab7 100644 --- a/Asn1Parser/Universal/Asn1UniversalString.cs +++ b/Asn1Parser/Universal/Asn1UniversalString.cs @@ -32,6 +32,14 @@ public Asn1UniversalString(Asn1Reader asn) : base(asn, TYPE) { /// public Asn1UniversalString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } /// + /// Initializes a new instance of Asn1UniversalString from a ASN.1-encoded byte array. + /// + /// ASN.1-encoded byte array. + /// + /// rawData is not UniversalString data type. + /// + public Asn1UniversalString(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } + /// /// Initializes a new instance of the Asn1UniversalString class from a unicode string. /// /// A unicode string to encode. diff --git a/Asn1Parser/Universal/Asn1UtcTime.cs b/Asn1Parser/Universal/Asn1UtcTime.cs index 28b8df6..e00ac86 100644 --- a/Asn1Parser/Universal/Asn1UtcTime.cs +++ b/Asn1Parser/Universal/Asn1UtcTime.cs @@ -48,4 +48,13 @@ public Asn1UtcTime(Asn1Reader asn) : base(asn, TYPE) { } /// The current state of ASN1 object is not UTC time. /// public Asn1UtcTime(Byte[] rawData) : base(new Asn1Reader(rawData), TYPE) { } + /// + /// Initializes a new instance of the Asn1UtcTime class from a byte array that + /// represents encoded UTC time. + /// + /// ASN.1-encoded byte array. + /// + /// The current state of ASN1 object is not UTC time. + /// + public Asn1UtcTime(ReadOnlyMemory rawData) : base(new Asn1Reader(rawData), TYPE) { } } \ No newline at end of file diff --git a/Asn1Parser/Universal/Asn1VideotexString.cs b/Asn1Parser/Universal/Asn1VideotexString.cs index d2fe532..cc1ed49 100644 --- a/Asn1Parser/Universal/Asn1VideotexString.cs +++ b/Asn1Parser/Universal/Asn1VideotexString.cs @@ -34,6 +34,17 @@ public Asn1VideotexString(Asn1Reader asn) : base(asn, TYPE) { } /// public Asn1VideotexString(Byte[] rawData) : base(new Asn1Reader(rawData), TYPE) { } /// + /// Initializes a new instance of Asn1VideotexString from a ASN.1-encoded byte array. + /// + /// ASN.1-encoded byte array. + /// + /// rawData is not VideotexString data type. + /// + /// + /// Input data contains invalid VideotexString character. + /// + public Asn1VideotexString(ReadOnlyMemory rawData) : base(new Asn1Reader(rawData), TYPE) { } + /// /// Initializes a new instance of the Asn1VideotexString class from a unicode string. /// /// A unicode string to encode. diff --git a/Asn1Parser/Universal/Asn1VisibleString.cs b/Asn1Parser/Universal/Asn1VisibleString.cs index 6a843ee..cc76a5d 100644 --- a/Asn1Parser/Universal/Asn1VisibleString.cs +++ b/Asn1Parser/Universal/Asn1VisibleString.cs @@ -40,6 +40,17 @@ public Asn1VisibleString(Asn1Reader asn) : base(asn, TYPE) { /// public Asn1VisibleString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } /// + /// Initializes a new instance of Asn1VisibleString from a ASN.1-encoded byte array. + /// + /// ASN.1-encoded byte array. + /// + /// rawData is not VisibleString data type. + /// + /// + /// Input data contains invalid VisibleString character. + /// + public Asn1VisibleString(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } + /// /// Initializes a new instance of the Asn1VisibleString class from a unicode string. /// /// A unicode string to encode. diff --git a/tests/Asn1Parser.Tests/Asn1DateTimeTests.cs b/tests/Asn1Parser.Tests/Asn1DateTimeTests.cs index fc9f96f..3ba6e7a 100644 --- a/tests/Asn1Parser.Tests/Asn1DateTimeTests.cs +++ b/tests/Asn1Parser.Tests/Asn1DateTimeTests.cs @@ -80,8 +80,8 @@ static void assertDateTimeEncode(Asn1Type expectedType, Asn1DateTime adt, DateTi } static void assertDateTimeDecode(Asn1Type expectedTime, Asn1DateTime adt, DateTime dt, String expectedFormat) { adt = expectedTime == Asn1Type.UTCTime - ? new Asn1UtcTime(adt.GetRawData()) - : new Asn1GeneralizedTime(adt.GetRawData()); + ? new Asn1UtcTime(adt.GetRawDataAsMemory()) + : new Asn1GeneralizedTime(adt.GetRawDataAsMemory()); assertDateTimeEncode(expectedTime, adt, dt, expectedFormat, true); } } diff --git a/tests/Asn1Parser.Tests/Asn1OidTests.cs b/tests/Asn1Parser.Tests/Asn1OidTests.cs index 1c34a29..26e9832 100644 --- a/tests/Asn1Parser.Tests/Asn1OidTests.cs +++ b/tests/Asn1Parser.Tests/Asn1OidTests.cs @@ -68,7 +68,7 @@ public void TestLargeTopArcs() { static void testOidBiDirectional(String oidString, String expectedB64) { // test OID string -> binary encoding process var oid = new Asn1ObjectIdentifier(oidString); - String encodedB64 = Convert.ToBase64String(oid.GetRawData()); + String encodedB64 = Convert.ToBase64String(oid.GetRawDataAsMemory().ToArray()); Assert.AreEqual(expectedB64, encodedB64); // test binary -> OID string decoding process oid = new Asn1ObjectIdentifier(Convert.FromBase64String(expectedB64)); diff --git a/tests/Asn1Parser.Tests/Asn1RelativeOidTests.cs b/tests/Asn1Parser.Tests/Asn1RelativeOidTests.cs index 9ae1e12..1d18ae6 100644 --- a/tests/Asn1Parser.Tests/Asn1RelativeOidTests.cs +++ b/tests/Asn1Parser.Tests/Asn1RelativeOidTests.cs @@ -40,7 +40,7 @@ public void TestRelativeOidJunk() { static void testOidBiDirectional(String oidString, String expectedB64) { // test OID string -> binary encoding process var oid = new Asn1RelativeOid(oidString); - String encodedB64 = Convert.ToBase64String(oid.GetRawData()); + String encodedB64 = Convert.ToBase64String(oid.GetRawDataAsMemory().ToArray()); Assert.AreEqual(expectedB64, encodedB64); if (oidString.StartsWith('.')) { Assert.AreEqual(oidString, oid.Value); From 72be4cbd08f7abdb2c0e133a3038b2610491decc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 11:45:32 +0200 Subject: [PATCH 26/66] added protected access to internal reader --- Asn1Parser/Universal/Asn1Universal.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Asn1Parser/Universal/Asn1Universal.cs b/Asn1Parser/Universal/Asn1Universal.cs index 4a60a93..261aabe 100644 --- a/Asn1Parser/Universal/Asn1Universal.cs +++ b/Asn1Parser/Universal/Asn1Universal.cs @@ -92,6 +92,13 @@ protected void Initialize(Asn1Reader asn) { IsContainer = asn.IsConstructed; } /// + /// Gets internal reader instance. + /// + /// ASN.1 reader. + protected Asn1Reader GetInternalReader() { + return asnReader!.GetReader(); + } + /// /// Constant string to display error message for tag mismatch exceptions. /// protected const String InvalidType = "Input data does not represent valid '{0}' type."; From 33f2f0ff24d64453d5414807257fa78680fe97ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 11:46:00 +0200 Subject: [PATCH 27/66] refactored Asn1OctetString to support ReadOnlyMemory. --- Asn1Parser/Universal/Asn1OctetString.cs | 31 ++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/Asn1Parser/Universal/Asn1OctetString.cs b/Asn1Parser/Universal/Asn1OctetString.cs index f59d878..5b58caa 100644 --- a/Asn1Parser/Universal/Asn1OctetString.cs +++ b/Asn1Parser/Universal/Asn1OctetString.cs @@ -44,12 +44,41 @@ public Asn1OctetString(Byte[] rawData, Boolean tagged) : base(TYPE) { Initialize(asn); } else { Value = rawData; - Initialize(new Asn1Reader(Asn1Utils.Encode(rawData, TYPE))); + Initialize(new Asn1Reader(Asn1Utils.Encode(rawData.AsSpan(), TYPE))); + } + } + /// + /// Initializes a new instance of Asn1NumericString from a ASN.1-encoded memory buffer. + /// + /// ASN.1-encoded memory buffer. + /// Boolean value that indicates whether the byte array in rawData parameter is encoded or not. + /// + /// rawData is not NumericString data type. + /// + /// + /// Input data contains invalid NumericString character. + /// + public Asn1OctetString(ReadOnlyMemory rawData, Boolean tagged) : base(TYPE) { + if (tagged) { + var asn = new Asn1Reader(rawData); + if (asn.Tag != Tag) { + throw new Asn1InvalidTagException(String.Format(InvalidType, TYPE.ToString())); + } + Value = asn.GetPayload(); + Initialize(asn); + } else { + Value = rawData.ToArray(); + Initialize(new Asn1Reader(Asn1Utils.Encode(rawData.Span, TYPE))); } } /// /// Gets value associated with the current object. /// + [Obsolete("Use 'GetValue()' method instead.")] public Byte[] Value { get; private set; } + + public ReadOnlyMemory GetValue() { + return GetInternalReader().GetPayloadAsMemory(); + } } \ No newline at end of file From 9989441dde752068e944fc17f3bc70941b290a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 11:46:40 +0200 Subject: [PATCH 28/66] fixed bug in Asn1Builder.getEncoded when outer tag is not copied to target memory buffer. --- Asn1Parser/Asn1Builder.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Asn1Parser/Asn1Builder.cs b/Asn1Parser/Asn1Builder.cs index 9b53e62..5c7a566 100644 --- a/Asn1Parser/Asn1Builder.cs +++ b/Asn1Parser/Asn1Builder.cs @@ -90,10 +90,21 @@ public Asn1Builder AddBitString(Byte[] value, Boolean calculateUnusedBits = fals /// /// Current instance with added value. public Asn1Builder AddOctetString(Byte[] value) { - if (value == null) { - throw new ArgumentNullException(nameof(value)); - } + return AddOctetString(value.AsMemory()); + } + /// + /// Adds ASN.1 OCTET_STRING value. + /// + /// + /// Value to encode. + /// + /// + /// value parameter is null. + /// + /// Current instance with added value. + public Asn1Builder AddOctetString(ReadOnlyMemory value) { _rawData.Add(new Asn1OctetString(value, false).GetRawDataAsMemory()); + return this; } /// @@ -641,6 +652,8 @@ Byte[] getEncoded(Byte outerTag, Boolean includeHeader) { Byte[] memory = new Byte[headerLength + payloadLength]; Int32 i = 0; if (includeHeader) { + memory[0] = outerTag; + i = 1; for (; i < header!.Length; i++) { memory[i] = header[i]; } From 8905c9b76b7267a1086897093a04222aa5505379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 11:50:29 +0200 Subject: [PATCH 29/66] typo --- Asn1Parser/Asn1Builder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Asn1Parser/Asn1Builder.cs b/Asn1Parser/Asn1Builder.cs index 5c7a566..4261020 100644 --- a/Asn1Parser/Asn1Builder.cs +++ b/Asn1Parser/Asn1Builder.cs @@ -685,7 +685,7 @@ public Byte[] GetEncoded(Byte outerTag = 0x30) { /// Gets ASN.1-encoded memory that represents current state of builder wrapped using outer ASN.1 type. /// /// - /// Outer type to wrap current state of builder. Outer type must not by the type that is used in primitive form only. + /// Outer type to wrap current state of builder. Outer type must not be the type that is used in primitive form only. /// Default outer tag is constructed SEQUENCE (0x30 or decimal 48). /// /// From ba0d21f6331d343ff7d9d8cf6694c80f813ef546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 11:55:40 +0200 Subject: [PATCH 30/66] added Obsolete attribute --- Asn1Parser/Universal/Asn1OctetString.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Asn1Parser/Universal/Asn1OctetString.cs b/Asn1Parser/Universal/Asn1OctetString.cs index 5b58caa..59f383e 100644 --- a/Asn1Parser/Universal/Asn1OctetString.cs +++ b/Asn1Parser/Universal/Asn1OctetString.cs @@ -34,6 +34,7 @@ public Asn1OctetString(Asn1Reader asn) : base(asn, TYPE) { /// /// Input data contains invalid NumericString character. /// + [Obsolete("Consider using constructor that accepts 'ReadOnlyMemory' instead.")] public Asn1OctetString(Byte[] rawData, Boolean tagged) : base(TYPE) { if (tagged) { var asn = new Asn1Reader(rawData); From 0907e20cc04c375d40def13dabe30dd41df557b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 11:56:09 +0200 Subject: [PATCH 31/66] added new overload to Create method that accepts ReadOnlyMemory --- Asn1Parser/Asn1Builder.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Asn1Parser/Asn1Builder.cs b/Asn1Parser/Asn1Builder.cs index 4261020..a1c09ad 100644 --- a/Asn1Parser/Asn1Builder.cs +++ b/Asn1Parser/Asn1Builder.cs @@ -726,10 +726,7 @@ public static Asn1Builder Create() { /// ASN.1-encoded data to initialize the builder from. /// ASN.1 Builder. public static Asn1Builder Create(IEnumerable rawData) { - var builder = new Asn1Builder(); - builder._rawData.Add(rawData.ToArray()); - - return builder; + return Create(rawData.ToArray().AsSpan()); } /// /// Creates a default instance of Asn1Builder class from existing ASN.1-encoded data. From a6f7d1c697a030a14184fdbfd3300d0da3810d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 12:29:03 +0200 Subject: [PATCH 32/66] added xml-doc for FlagsAttribute --- Asn1Parser/EncodingFormat.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Asn1Parser/EncodingFormat.cs b/Asn1Parser/EncodingFormat.cs index b4ee113..8f291ea 100644 --- a/Asn1Parser/EncodingFormat.cs +++ b/Asn1Parser/EncodingFormat.cs @@ -4,6 +4,7 @@ namespace SysadminsLV.Asn1Parser; /// /// Contains values that specify how line breaks are handled during byte array formatting. +/// This enumeration has a attribute that allows a bitwise combination of its member values. /// [Flags] public enum EncodingFormat : UInt32 { From f7c80e60a3d2b7bf5a09e6022d89082dc32f0c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 12:31:50 +0200 Subject: [PATCH 33/66] replaced Asn1Universal.IsContainer with Asn1Universal.IsConstructed --- Asn1Parser/Universal/Asn1Universal.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Asn1Parser/Universal/Asn1Universal.cs b/Asn1Parser/Universal/Asn1Universal.cs index 261aabe..362262c 100644 --- a/Asn1Parser/Universal/Asn1Universal.cs +++ b/Asn1Parser/Universal/Asn1Universal.cs @@ -13,7 +13,7 @@ public abstract class Asn1Universal { protected Asn1Universal(Asn1Type type) { Tag = (Byte)type; TagName = Asn1Reader.GetTagName(Tag); - IsContainer = (Tag & (Byte)Asn1Class.CONSTRUCTED) > 0; + IsConstructed = (Tag & (Byte)Asn1Class.CONSTRUCTED) > 0; } /// /// Initializes a new instance of Asn1Universal from an existing @@ -74,7 +74,9 @@ protected Asn1Universal(Asn1Reader asn, Asn1Type? type) { /// BMPString /// /// - public Boolean IsContainer { get; private set; } + public Boolean IsConstructed { get; private set; } + [Obsolete("Use 'IsConstructed' member instead.", true)] + public Boolean IsContainer => IsConstructed; /// /// Gets the full tag raw data, including header and payload information. /// @@ -89,7 +91,7 @@ protected void Initialize(Asn1Reader asn) { asnReader = asn.GetReader(); // do not store external ASN reader reference. Tag = asn.Tag; TagName = asn.TagName; - IsContainer = asn.IsConstructed; + IsConstructed = asn.IsConstructed; } /// /// Gets internal reader instance. From 44251eca5ba0697612665ee69b163fa6348ac007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 12:34:08 +0200 Subject: [PATCH 34/66] added missing xml-doc --- Asn1Parser/Universal/Asn1OctetString.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Asn1Parser/Universal/Asn1OctetString.cs b/Asn1Parser/Universal/Asn1OctetString.cs index 59f383e..8c5820c 100644 --- a/Asn1Parser/Universal/Asn1OctetString.cs +++ b/Asn1Parser/Universal/Asn1OctetString.cs @@ -79,6 +79,9 @@ public Asn1OctetString(ReadOnlyMemory rawData, Boolean tagged) : base(TYPE [Obsolete("Use 'GetValue()' method instead.")] public Byte[] Value { get; private set; } + /// + /// Gets value associated with the current object. + /// public ReadOnlyMemory GetValue() { return GetInternalReader().GetPayloadAsMemory(); } From b7681059785be1875fde74308be7689fd61210d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 13:03:54 +0200 Subject: [PATCH 35/66] ensure that all ASN type decoders have constructor that accepts ReadOnlyMemory and added GetValue() method to types that hold binary values --- Asn1Parser/Universal/Asn1BitString.cs | 14 +++++++++++--- Asn1Parser/Universal/Asn1BmpString.cs | 4 ++-- Asn1Parser/Universal/Asn1Boolean.cs | 10 +++++++++- Asn1Parser/Universal/Asn1Enumerated.cs | 10 +++++++++- Asn1Parser/Universal/Asn1GeneralizedTime.cs | 2 +- Asn1Parser/Universal/Asn1IA5String.cs | 4 ++-- Asn1Parser/Universal/Asn1Integer.cs | 10 +++++++++- Asn1Parser/Universal/Asn1Null.cs | 12 ++++++++++-- Asn1Parser/Universal/Asn1NumericString.cs | 4 ++-- Asn1Parser/Universal/Asn1ObjectIdentifier.cs | 12 +++++++++--- Asn1Parser/Universal/Asn1OctetString.cs | 14 +------------- Asn1Parser/Universal/Asn1PrintableString.cs | 4 ++-- Asn1Parser/Universal/Asn1RelativeOid.cs | 12 +++++++++--- Asn1Parser/Universal/Asn1TeletexString.cs | 2 +- Asn1Parser/Universal/Asn1UTF8String.cs | 2 +- Asn1Parser/Universal/Asn1UniversalString.cs | 2 +- Asn1Parser/Universal/Asn1UtcTime.cs | 2 +- Asn1Parser/Universal/Asn1VideotexString.cs | 4 ++-- Asn1Parser/Universal/Asn1VisibleString.cs | 4 ++-- .../Utils/CLRExtensions/BigIntegerExtensions.cs | 2 +- Asn1Parser/Utils/DateTimeUtils.cs | 2 +- 21 files changed, 86 insertions(+), 46 deletions(-) diff --git a/Asn1Parser/Universal/Asn1BitString.cs b/Asn1Parser/Universal/Asn1BitString.cs index cac045e..557a79a 100644 --- a/Asn1Parser/Universal/Asn1BitString.cs +++ b/Asn1Parser/Universal/Asn1BitString.cs @@ -29,7 +29,7 @@ public Asn1BitString(Asn1Reader asn) : base(asn, TYPE) { /// /// rawData is not BIT_STRING data type. /// - public Asn1BitString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1BitString(Byte[] rawData) : this(new Asn1Reader(rawData.AsMemory())) { } /// /// Initializes a new instance of Asn1BitString from a raw byte array to encode and parameter that indicates /// whether the bit length is decremented to exclude trailing zero bits. @@ -62,6 +62,7 @@ public Asn1BitString(ReadOnlySpan valueToEncode, Byte unusedBits) : base(T /// /// Gets raw value of BIT_STRING without unused bits identifier. /// + [Obsolete("Use 'GetValue()' method instead.")] public Byte[] Value { get; private set; } = []; void m_encode(ReadOnlySpan value, Boolean calc, Byte unusedBits) { @@ -72,7 +73,7 @@ void m_encode(ReadOnlySpan value, Boolean calc, Byte unusedBits) { Span v = new Byte[value.Length + 1]; v[0] = UnusedBits; value.CopyTo(v.Slice(1)); - Initialize(new Asn1Reader(Asn1Utils.Encode(v, (Byte)TYPE))); + Initialize(new Asn1Reader(Asn1Utils.Encode(v, TYPE))); } /// @@ -82,11 +83,18 @@ void m_encode(ReadOnlySpan value, Boolean calc, Byte unusedBits) { public override String GetDisplayValue() { var SB = new StringBuilder(); SB.AppendFormat("Unused bits={0}\r\n", UnusedBits); - String tempString = AsnFormatter.BinaryToString(Value, EncodingType.HexAddress); + String tempString = AsnFormatter.BinaryToString(GetValue().Span, EncodingType.HexAddress); SB.AppendFormat("{0}\r\n", tempString.Replace("\r\n", "\r\n ").TrimEnd()); return SB.ToString(); } + /// + /// Gets raw value of BIT_STRING without unused bits identifier. + /// + /// BIT_STRING value. + public ReadOnlyMemory GetValue() { + return GetInternalReader().GetPayloadAsMemory(); + } /// /// Calculates the number of bits left unused in the final byte of content. diff --git a/Asn1Parser/Universal/Asn1BmpString.cs b/Asn1Parser/Universal/Asn1BmpString.cs index a04b265..6d6dc39 100644 --- a/Asn1Parser/Universal/Asn1BmpString.cs +++ b/Asn1Parser/Universal/Asn1BmpString.cs @@ -28,7 +28,7 @@ public Asn1BMPString(Asn1Reader asn) : base(asn, TYPE) { /// /// rawData is not BMPString data type. /// - public Asn1BMPString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1BMPString(Byte[] rawData) : this(new Asn1Reader(rawData.AsMemory())) { } /// /// Initializes a new instance of Asn1BitString from a ASN.1-encoded byte array. /// @@ -47,7 +47,7 @@ public Asn1BMPString(String inputString) : base(TYPE) { void m_encode(String inputString) { Value = inputString; - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.BigEndianUnicode.GetBytes(inputString), TYPE))); + Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.BigEndianUnicode.GetBytes(inputString).AsSpan(), TYPE))); } void m_decode(Asn1Reader asn) { Value = Encoding.BigEndianUnicode.GetString(asn.GetPayload()); diff --git a/Asn1Parser/Universal/Asn1Boolean.cs b/Asn1Parser/Universal/Asn1Boolean.cs index e10d970..ad21823 100644 --- a/Asn1Parser/Universal/Asn1Boolean.cs +++ b/Asn1Parser/Universal/Asn1Boolean.cs @@ -26,7 +26,15 @@ public Asn1Boolean(Asn1Reader asn) : base(asn, TYPE) { /// /// rawData is not valid BOOLEAN data type. /// - public Asn1Boolean(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1Boolean(Byte[] rawData) : this(rawData.AsMemory()) { } + /// + /// Initializes a new instance of Asn1Boolean from a ASN.1-encoded byte array. + /// + /// ASN.1-encoded byte array. + /// + /// rawData is not valid BOOLEAN data type. + /// + public Asn1Boolean(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of the Asn1Boolean class from a boolean value. /// diff --git a/Asn1Parser/Universal/Asn1Enumerated.cs b/Asn1Parser/Universal/Asn1Enumerated.cs index 50291f0..5279054 100644 --- a/Asn1Parser/Universal/Asn1Enumerated.cs +++ b/Asn1Parser/Universal/Asn1Enumerated.cs @@ -30,7 +30,15 @@ public Asn1Enumerated(Asn1Reader asn) : base(asn, TYPE) { /// /// rawData is not valid INTEGER data type. /// - public Asn1Enumerated(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1Enumerated(Byte[] rawData) : this(rawData.AsMemory()) { } + /// + /// Initializes a new instance of Asn1Enumerated from a ASN.1-encoded byte array. + /// + /// ASN.1-encoded byte array. + /// + /// rawData is not valid INTEGER data type. + /// + public Asn1Enumerated(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of the Asn1Enumerated class from an integer value. /// diff --git a/Asn1Parser/Universal/Asn1GeneralizedTime.cs b/Asn1Parser/Universal/Asn1GeneralizedTime.cs index 441e31d..21f447c 100644 --- a/Asn1Parser/Universal/Asn1GeneralizedTime.cs +++ b/Asn1Parser/Universal/Asn1GeneralizedTime.cs @@ -47,7 +47,7 @@ public Asn1GeneralizedTime(Asn1Reader asn) : base(asn, TYPE) { } /// /// The current state of ASN1 object is not Generalized Time. /// - public Asn1GeneralizedTime(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1GeneralizedTime(Byte[] rawData) : this(rawData.AsMemory()) { } /// /// Initializes a new instance of the Asn1GeneralizedTime class from a byte array that /// represents encoded UTC time. diff --git a/Asn1Parser/Universal/Asn1IA5String.cs b/Asn1Parser/Universal/Asn1IA5String.cs index 2d55504..3ab4606 100644 --- a/Asn1Parser/Universal/Asn1IA5String.cs +++ b/Asn1Parser/Universal/Asn1IA5String.cs @@ -36,7 +36,7 @@ public Asn1IA5String(Asn1Reader asn) : base(asn, TYPE) { /// /// Input data contains invalid IA5String character. /// - public Asn1IA5String(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1IA5String(Byte[] rawData) : this(rawData.AsMemory()) { } /// /// Initializes a new instance of Asn1IA5String from a ASN.1-encoded byte array. /// @@ -64,7 +64,7 @@ void m_encode(String inputString) { throw new InvalidDataException(String.Format(InvalidType, TYPE.ToString())); } Value = inputString; - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString), TYPE))); + Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE))); } void m_decode(Asn1Reader asn) { if (asn.GetPayload().Any(b => b > 127)) { diff --git a/Asn1Parser/Universal/Asn1Integer.cs b/Asn1Parser/Universal/Asn1Integer.cs index f933723..2a63714 100644 --- a/Asn1Parser/Universal/Asn1Integer.cs +++ b/Asn1Parser/Universal/Asn1Integer.cs @@ -29,7 +29,15 @@ public Asn1Integer(Asn1Reader asn) : base(asn, TYPE) { /// /// rawData is not valid INTEGER data type. /// - public Asn1Integer(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1Integer(Byte[] rawData) : this(rawData.AsMemory()) { } + /// + /// Initializes a new instance of Asn1Integer from a ASN.1-encoded byte array. + /// + /// ASN.1-encoded byte array. + /// + /// rawData is not valid INTEGER data type. + /// + public Asn1Integer(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of the Asn1Integer class from an integer value. /// diff --git a/Asn1Parser/Universal/Asn1Null.cs b/Asn1Parser/Universal/Asn1Null.cs index 217a352..19377ee 100644 --- a/Asn1Parser/Universal/Asn1Null.cs +++ b/Asn1Parser/Universal/Asn1Null.cs @@ -13,7 +13,7 @@ public sealed class Asn1Null : Asn1Universal { /// Initializes a new instance of Asn1Null class. /// public Asn1Null() : base(TYPE) { - Initialize(new Asn1Reader([5, 0])); + Initialize(new Asn1Reader(new Byte[] { 5, 0 }.AsMemory())); } /// /// Initializes a new instance of the Asn1Null class from an @@ -33,7 +33,15 @@ public Asn1Null(Asn1Reader asn) : base(asn, TYPE) { /// /// rawData is not valid NULL data type. /// - public Asn1Null(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1Null(Byte[] rawData) : this(rawData.AsMemory()) { } + /// + /// Initializes a new instance of Asn1Null from a ASN.1-encoded byte array. + /// + /// ASN.1-encoded byte array. + /// + /// rawData is not valid NULL data type. + /// + public Asn1Null(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } void m_decode(Asn1Reader asn) { if (asn.PayloadLength > 0) { diff --git a/Asn1Parser/Universal/Asn1NumericString.cs b/Asn1Parser/Universal/Asn1NumericString.cs index a14298f..49169e9 100644 --- a/Asn1Parser/Universal/Asn1NumericString.cs +++ b/Asn1Parser/Universal/Asn1NumericString.cs @@ -36,7 +36,7 @@ public Asn1NumericString(Asn1Reader asn) : base(asn, TYPE) { /// /// Input data contains invalid NumericString character. /// - public Asn1NumericString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1NumericString(Byte[] rawData) : this(rawData.AsMemory()) { } /// /// Initializes a new instance of Asn1NumericString from a ASN.1-encoded byte array. /// @@ -64,7 +64,7 @@ void m_encode(String inputString) { throw new InvalidDataException(String.Format(InvalidType, TYPE.ToString())); } Value = inputString; - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString), TYPE))); + Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE))); } void m_decode(Asn1Reader asn) { if (asn.GetPayload().Any(b => b is < 48 or > 57 && b != 32)) { diff --git a/Asn1Parser/Universal/Asn1ObjectIdentifier.cs b/Asn1Parser/Universal/Asn1ObjectIdentifier.cs index 3a6fd71..0235cb9 100644 --- a/Asn1Parser/Universal/Asn1ObjectIdentifier.cs +++ b/Asn1Parser/Universal/Asn1ObjectIdentifier.cs @@ -32,7 +32,13 @@ public Asn1ObjectIdentifier(Asn1Reader asn) : base(asn, TYPE) { /// that represents encoded object identifier. /// /// Byte array that represents encoded object identifier. - public Asn1ObjectIdentifier(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1ObjectIdentifier(Byte[] rawData) : this(rawData.AsMemory()) { } + /// + /// Initializes a new instance of the Asn1ObjectIdentifier class from a byte array + /// that represents encoded object identifier. + /// + /// Byte array that represents encoded object identifier. + public Asn1ObjectIdentifier(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of the Asn1ObjectIdentifier class from a string /// that represents object identifier value. @@ -63,7 +69,7 @@ public Asn1ObjectIdentifier(Oid oid) : base(TYPE) { void m_encode(Oid oid) { if (String.IsNullOrWhiteSpace(oid.Value)) { - Initialize(new Asn1Reader([Tag, 0])); + Initialize(new Asn1Reader(new Byte[] { Tag, 0 }.AsMemory())); Value = new Oid(); return; } @@ -77,7 +83,7 @@ void m_encode(Oid oid) { Initialize(new Asn1Reader(Asn1Utils.Encode(encode(tokens), TYPE))); } - static Byte[] encode(IList tokens) { + static ReadOnlySpan encode(IList tokens) { var rawOid = new List(); for (Int32 tokenIndex = 0; tokenIndex < tokens.Count; tokenIndex++) { BigInteger token = tokens[tokenIndex]; diff --git a/Asn1Parser/Universal/Asn1OctetString.cs b/Asn1Parser/Universal/Asn1OctetString.cs index 8c5820c..c65c6be 100644 --- a/Asn1Parser/Universal/Asn1OctetString.cs +++ b/Asn1Parser/Universal/Asn1OctetString.cs @@ -35,19 +35,7 @@ public Asn1OctetString(Asn1Reader asn) : base(asn, TYPE) { /// Input data contains invalid NumericString character. /// [Obsolete("Consider using constructor that accepts 'ReadOnlyMemory' instead.")] - public Asn1OctetString(Byte[] rawData, Boolean tagged) : base(TYPE) { - if (tagged) { - var asn = new Asn1Reader(rawData); - if (asn.Tag != Tag) { - throw new Asn1InvalidTagException(String.Format(InvalidType, TYPE.ToString())); - } - Value = asn.GetPayload(); - Initialize(asn); - } else { - Value = rawData; - Initialize(new Asn1Reader(Asn1Utils.Encode(rawData.AsSpan(), TYPE))); - } - } + public Asn1OctetString(Byte[] rawData, Boolean tagged) : this(rawData.AsMemory(), tagged) { } /// /// Initializes a new instance of Asn1NumericString from a ASN.1-encoded memory buffer. /// diff --git a/Asn1Parser/Universal/Asn1PrintableString.cs b/Asn1Parser/Universal/Asn1PrintableString.cs index efb770c..2a326fd 100644 --- a/Asn1Parser/Universal/Asn1PrintableString.cs +++ b/Asn1Parser/Universal/Asn1PrintableString.cs @@ -38,7 +38,7 @@ public Asn1PrintableString(Asn1Reader asn) : base(asn, TYPE) { /// /// Input data contains invalid PrintableString character. /// - public Asn1PrintableString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1PrintableString(Byte[] rawData) : this(rawData.AsMemory()) { } /// /// Initializes a new instance of Asn1PrintableString from a ASN.1-encoded byte array. /// @@ -66,7 +66,7 @@ void m_encode(String inputString) { throw new InvalidDataException(String.Format(InvalidType, TYPE.ToString())); } Value = inputString; - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString), TYPE))); + Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE))); } void m_decode(Asn1Reader asn) { if (!testValue(asn.GetPayload())) { diff --git a/Asn1Parser/Universal/Asn1RelativeOid.cs b/Asn1Parser/Universal/Asn1RelativeOid.cs index a1c104d..99d13c4 100644 --- a/Asn1Parser/Universal/Asn1RelativeOid.cs +++ b/Asn1Parser/Universal/Asn1RelativeOid.cs @@ -28,7 +28,13 @@ public Asn1RelativeOid(Asn1Reader asn) : base(asn, TYPE) { /// that represents encoded relative object identifier. /// /// Byte array that represents encoded relative object identifier. - public Asn1RelativeOid(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1RelativeOid(Byte[] rawData) : this(rawData.AsMemory()) { } + /// + /// Initializes a new instance of the Asn1RelativeOid class from a byte array + /// that represents encoded relative object identifier. + /// + /// Byte array that represents encoded relative object identifier. + public Asn1RelativeOid(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of the Asn1RelativeOid class from a string /// that represents relative object identifier value. @@ -59,7 +65,7 @@ public Asn1RelativeOid(String relativeOid) : base(TYPE) { void m_encode(String oidString) { if (String.IsNullOrWhiteSpace(oidString)) { - Initialize(new Asn1Reader([Tag, 0])); + Initialize(new Asn1Reader(new Byte[] { Tag, 0 }.AsMemory())); Value = String.Empty; return; } @@ -72,7 +78,7 @@ void m_encode(String oidString) { .Select(BigInteger.Parse); Initialize(new Asn1Reader(Asn1Utils.Encode(encode(tokens), TYPE))); } - static Byte[] encode(IEnumerable tokens) { + static ReadOnlySpan encode(IEnumerable tokens) { var rawOid = new List(); foreach (BigInteger token in tokens) { rawOid.AddRange(OidUtils.EncodeOidArc(token)); diff --git a/Asn1Parser/Universal/Asn1TeletexString.cs b/Asn1Parser/Universal/Asn1TeletexString.cs index b0111f4..981fca5 100644 --- a/Asn1Parser/Universal/Asn1TeletexString.cs +++ b/Asn1Parser/Universal/Asn1TeletexString.cs @@ -33,7 +33,7 @@ public Asn1TeletexString(Asn1Reader asn) : base(asn, TYPE) { } /// /// Input data contains invalid TeletexString character. /// - public Asn1TeletexString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1TeletexString(Byte[] rawData) : this(rawData.AsMemory()) { } /// /// Initializes a new instance of Asn1TeletexString from an ASN.1-encoded byte array. /// diff --git a/Asn1Parser/Universal/Asn1UTF8String.cs b/Asn1Parser/Universal/Asn1UTF8String.cs index 64f643a..fa08087 100644 --- a/Asn1Parser/Universal/Asn1UTF8String.cs +++ b/Asn1Parser/Universal/Asn1UTF8String.cs @@ -36,7 +36,7 @@ public Asn1UTF8String(Asn1Reader asn) : base(asn, TYPE) { /// /// Input data contains invalid UTF8String character. /// - public Asn1UTF8String(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1UTF8String(Byte[] rawData) : this(rawData.AsMemory()) { } /// /// Initializes a new instance of Asn1UTF8String from a ASN.1-encoded byte array. /// diff --git a/Asn1Parser/Universal/Asn1UniversalString.cs b/Asn1Parser/Universal/Asn1UniversalString.cs index bee2ab7..8a6e036 100644 --- a/Asn1Parser/Universal/Asn1UniversalString.cs +++ b/Asn1Parser/Universal/Asn1UniversalString.cs @@ -30,7 +30,7 @@ public Asn1UniversalString(Asn1Reader asn) : base(asn, TYPE) { /// /// rawData is not UniversalString data type. /// - public Asn1UniversalString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1UniversalString(Byte[] rawData) : this(rawData.AsMemory()) { } /// /// Initializes a new instance of Asn1UniversalString from a ASN.1-encoded byte array. /// diff --git a/Asn1Parser/Universal/Asn1UtcTime.cs b/Asn1Parser/Universal/Asn1UtcTime.cs index e00ac86..a380172 100644 --- a/Asn1Parser/Universal/Asn1UtcTime.cs +++ b/Asn1Parser/Universal/Asn1UtcTime.cs @@ -47,7 +47,7 @@ public Asn1UtcTime(Asn1Reader asn) : base(asn, TYPE) { } /// /// The current state of ASN1 object is not UTC time. /// - public Asn1UtcTime(Byte[] rawData) : base(new Asn1Reader(rawData), TYPE) { } + public Asn1UtcTime(Byte[] rawData) : this(rawData.AsMemory()) { } /// /// Initializes a new instance of the Asn1UtcTime class from a byte array that /// represents encoded UTC time. diff --git a/Asn1Parser/Universal/Asn1VideotexString.cs b/Asn1Parser/Universal/Asn1VideotexString.cs index cc1ed49..4fbed56 100644 --- a/Asn1Parser/Universal/Asn1VideotexString.cs +++ b/Asn1Parser/Universal/Asn1VideotexString.cs @@ -32,7 +32,7 @@ public Asn1VideotexString(Asn1Reader asn) : base(asn, TYPE) { } /// /// Input data contains invalid VideotexString character. /// - public Asn1VideotexString(Byte[] rawData) : base(new Asn1Reader(rawData), TYPE) { } + public Asn1VideotexString(Byte[] rawData) : this(rawData.AsMemory()) { } /// /// Initializes a new instance of Asn1VideotexString from a ASN.1-encoded byte array. /// @@ -56,7 +56,7 @@ public Asn1VideotexString(String inputString) : base(TYPE) { } void encode(String inputString) { - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString), TYPE))); + Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE))); Value = inputString; } protected override String Decode(ReadOnlySpan payload) { diff --git a/Asn1Parser/Universal/Asn1VisibleString.cs b/Asn1Parser/Universal/Asn1VisibleString.cs index cc76a5d..df90bdc 100644 --- a/Asn1Parser/Universal/Asn1VisibleString.cs +++ b/Asn1Parser/Universal/Asn1VisibleString.cs @@ -38,7 +38,7 @@ public Asn1VisibleString(Asn1Reader asn) : base(asn, TYPE) { /// /// Input data contains invalid VisibleString character. /// - public Asn1VisibleString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1VisibleString(Byte[] rawData) : this(rawData.AsMemory()) { } /// /// Initializes a new instance of Asn1VisibleString from a ASN.1-encoded byte array. /// @@ -66,7 +66,7 @@ void m_encode(String inputString) { throw new InvalidDataException(String.Format(InvalidType, TYPE.ToString())); } Value = inputString; - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString), TYPE))); + Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE))); } void m_decode(Asn1Reader asn) { if (asn.GetPayload().Any(b => b is < 32 or > 126)) { diff --git a/Asn1Parser/Utils/CLRExtensions/BigIntegerExtensions.cs b/Asn1Parser/Utils/CLRExtensions/BigIntegerExtensions.cs index d6e6024..bac1021 100644 --- a/Asn1Parser/Utils/CLRExtensions/BigIntegerExtensions.cs +++ b/Asn1Parser/Utils/CLRExtensions/BigIntegerExtensions.cs @@ -13,7 +13,7 @@ internal static class BigIntegerExtensions { /// /// An class instance. /// Byte array in a big-endian order. - public static Byte[] GetAsnBytes(this BigInteger bigInteger) { + public static ReadOnlySpan GetAsnBytes(this BigInteger bigInteger) { return bigInteger.ToByteArray().Reverse().ToArray(); } } \ No newline at end of file diff --git a/Asn1Parser/Utils/DateTimeUtils.cs b/Asn1Parser/Utils/DateTimeUtils.cs index 7402242..aef9014 100644 --- a/Asn1Parser/Utils/DateTimeUtils.cs +++ b/Asn1Parser/Utils/DateTimeUtils.cs @@ -7,7 +7,7 @@ namespace SysadminsLV.Asn1Parser.Utils; static class DateTimeUtils { - public static Byte[] Encode(DateTime time, ref TimeZoneInfo? zone, Boolean UTC, Boolean usePrecise) { + public static ReadOnlySpan Encode(DateTime time, ref TimeZoneInfo? zone, Boolean UTC, Boolean usePrecise) { String suffix = String.Empty; String preValue; String format = UTC From 9ca789aa464c9fd2df665f385a00f3b3b1075658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 13:48:47 +0200 Subject: [PATCH 36/66] added new EncodeAsReader method which eliminates duplicate reader instanitations. --- Asn1Parser/Asn1Utils.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Asn1Parser/Asn1Utils.cs b/Asn1Parser/Asn1Utils.cs index 0ae1d55..48cb1fb 100644 --- a/Asn1Parser/Asn1Utils.cs +++ b/Asn1Parser/Asn1Utils.cs @@ -144,6 +144,28 @@ public static Byte[] Encode(Byte[] rawData, Asn1Type type) { public static Memory Encode(ReadOnlySpan rawData, Asn1Type type) { return Encode(rawData, (Byte)type); } + /// + /// Wraps encoded data to an ASN.1 type/structure and returns ASN.1 reader instance. + /// + /// This method do not check whether the data in rawData is valid data for specified enclosing type. + /// A byte array to wrap. + /// An enumeration of . + /// ASN.1 reader that represents encoded type. + /// If rawData is null, an empty tag is encoded. + public static Asn1Reader EncodeAsReader(ReadOnlySpan rawData, Asn1Type type) { + return new Asn1Reader(Encode(rawData, (Byte)type)); + } + /// + /// Wraps encoded data to an ASN.1 type/structure and returns ASN.1 reader instance. + /// + /// This method do not check whether the data in rawData is valid data for specified enclosing type. + /// A byte array to wrap. + /// An enumeration of . + /// ASN.1 reader that represents encoded type. + /// If rawData is null, an empty tag is encoded. + public static Asn1Reader EncodeAsReader(ReadOnlySpan rawData, Byte type) { + return new Asn1Reader(Encode(rawData, type)); + } #endregion #region internal From 08db27986e40d9d8a985479da515458727a3be4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 14:09:28 +0200 Subject: [PATCH 37/66] deprecated Asn1Reader(Byte[]) ctor and updated usages --- Asn1Parser/Asn1Builder.cs | 87 ++++++++++++++------ Asn1Parser/Asn1Reader.cs | 16 ---- Asn1Parser/Universal/Asn1BitString.cs | 10 ++- Asn1Parser/Universal/Asn1BmpString.cs | 4 +- Asn1Parser/Universal/Asn1Boolean.cs | 2 +- Asn1Parser/Universal/Asn1DateTime.cs | 2 +- Asn1Parser/Universal/Asn1Enumerated.cs | 2 +- Asn1Parser/Universal/Asn1IA5String.cs | 2 +- Asn1Parser/Universal/Asn1Integer.cs | 2 +- Asn1Parser/Universal/Asn1NumericString.cs | 2 +- Asn1Parser/Universal/Asn1ObjectIdentifier.cs | 2 +- Asn1Parser/Universal/Asn1OctetString.cs | 2 +- Asn1Parser/Universal/Asn1PrintableString.cs | 2 +- Asn1Parser/Universal/Asn1RelativeOid.cs | 2 +- Asn1Parser/Universal/Asn1TeletexString.cs | 2 +- Asn1Parser/Universal/Asn1UTF8String.cs | 2 +- Asn1Parser/Universal/Asn1UniversalString.cs | 6 +- Asn1Parser/Universal/Asn1VideotexString.cs | 2 +- Asn1Parser/Universal/Asn1VisibleString.cs | 2 +- 19 files changed, 91 insertions(+), 60 deletions(-) diff --git a/Asn1Parser/Asn1Builder.cs b/Asn1Parser/Asn1Builder.cs index a1c09ad..207edd7 100644 --- a/Asn1Parser/Asn1Builder.cs +++ b/Asn1Parser/Asn1Builder.cs @@ -72,7 +72,7 @@ public Asn1Builder AddBitString(Byte[] value, Byte unusedBits) { /// Indicates whether unused bits should be calculated. If set to false, unused bits value is set to zero. /// /// Current instance with added value. - public Asn1Builder AddBitString(Byte[] value, Boolean calculateUnusedBits = false) { + public Asn1Builder AddBitString(ReadOnlySpan value, Boolean calculateUnusedBits = false) { if (value == null) { throw new ArgumentNullException(nameof(value)); } @@ -191,15 +191,29 @@ public Asn1Builder AddUTF8String(String value) { /// /// In the current implementation, SEQUENCE is encoded using constructed form only. /// + [Obsolete] public Asn1Builder AddSequence(Byte[] value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - var asn = new Asn1Reader(value); - asn.BuildOffsetMap(); - // if we reach this far, most likely, the data is ok. - ReadOnlyMemory encoded = Asn1Utils.Encode(value, 0x30); - _rawData.Add(encoded); + + return AddSequence(value.AsSpan()); + } + /// + /// Adds ASN.1 SEQUENCE value. + /// + /// + /// Value to encode. + /// + /// + /// value parameter is null. + /// + /// Current instance with added value. + /// + /// In the current implementation, SEQUENCE is encoded using constructed form only. + /// + public Asn1Builder AddSequence(ReadOnlySpan value) { + _rawData.Add(Asn1Utils.Encode(value, 0x30)); return this; } @@ -221,11 +235,23 @@ public Asn1Builder AddSet(Byte[] value) { throw new ArgumentNullException(nameof(value)); } - var asn = new Asn1Reader(value); - asn.BuildOffsetMap(); - // if we reach this far, most likely, the data is ok. - ReadOnlyMemory encoded = Asn1Utils.Encode(value, 0x31); - _rawData.Add(encoded); + return AddSet(value.AsSpan()); + } + /// + /// Adds ASN.1 SET value. + /// + /// + /// Value to encode. + /// + /// + /// value parameter is null. + /// + /// Current instance with added value. + /// + /// In the current implementation, SET is encoded using constructed form only. + /// + public Asn1Builder AddSet(ReadOnlySpan value) { + _rawData.Add(Asn1Utils.Encode(value, 0x31)); return this; } @@ -419,9 +445,24 @@ public Asn1Builder AddDerData(Byte[] value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - var asn = new Asn1Reader(value); + + return AddDerData(value.AsSpan()); + } + /// + /// Adds arbitrary ASN.1-encoded data. + /// + /// + /// Value to encode. + /// + /// + /// value parameter is null. + /// + /// Current instance with added value. + public Asn1Builder AddDerData(ReadOnlySpan value) { + var asn = new Asn1Reader(value.ToArray()); asn.BuildOffsetMap(); - _rawData.Add(value); + _rawData.Add(asn.GetRawDataAsMemory()); + return this; } /// @@ -471,7 +512,7 @@ public Asn1Builder AddDerData(Byte[] value, Byte outerTag) { /// not. If mustEncode parameter is set to false and value passed in /// value parameter is untagged, an exception will be thrown. /// - public Asn1Builder AddImplicit(Byte implicitTag, Byte[] value, Boolean mustEncode) { + public Asn1Builder AddImplicit(Byte implicitTag, ReadOnlySpan value, Boolean mustEncode) { if (value == null) { throw new ArgumentNullException(nameof(value)); } @@ -481,11 +522,11 @@ public Asn1Builder AddImplicit(Byte implicitTag, Byte[] value, Boolean mustEncod if (value.Length < 2) { throw new InvalidDataException(); } - var asn = new Asn1Reader(value); + Byte[] buff = value.ToArray(); + var asn = new Asn1Reader(buff); asn.BuildOffsetMap(); - Byte[] valueCopy = value.ToArray(); - valueCopy[0] = (Byte)(0x80 + implicitTag); - _rawData.Add(valueCopy); + buff[0] = (Byte)(0x80 + implicitTag); + _rawData.Add(buff); } return this; } @@ -515,18 +556,18 @@ public Asn1Builder AddImplicit(Byte implicitTag, Byte[] value, Boolean mustEncod /// If mustEncode parameter is set to false and value passed in value parameter /// is untagged, invalid type will be produced. /// - public Asn1Builder AddExplicit(Byte explicitTag, Byte[] value, Boolean mustEncode) { + public Asn1Builder AddExplicit(Byte explicitTag, ReadOnlySpan value, Boolean mustEncode) { if (value == null) { throw new ArgumentNullException(nameof(value)); } if (mustEncode) { _rawData.Add(Asn1Utils.Encode(value, (Byte)(0xa0 + explicitTag))); } else { - var asn = new Asn1Reader(value); + Byte[] buff = value.ToArray(); + var asn = new Asn1Reader(buff); asn.BuildOffsetMap(); - Byte[] valueCopy = value.ToArray(); - valueCopy[0] = (Byte)(0xa0 + explicitTag); - _rawData.Add(valueCopy); + buff[0] = (Byte)(0xa0 + explicitTag); + _rawData.Add(buff); } return this; } diff --git a/Asn1Parser/Asn1Reader.cs b/Asn1Parser/Asn1Reader.cs index d6e37da..06b9958 100644 --- a/Asn1Parser/Asn1Reader.cs +++ b/Asn1Parser/Asn1Reader.cs @@ -50,22 +50,6 @@ public Asn1Reader(Asn1Reader asn) : this(asn.GetTagRawDataAsMemory(), 0, true) { /// Initializes a new instance of the ASN1 class by using an ASN.1 encoded byte array. /// /// ASN.1-encoded byte array. - /// - /// rawData parameter is null reference. - /// - /// - /// The data in the rawData parameter is not valid ASN sequence. - /// - /// - /// If rawData size is greater than outer structure size, constructor will take only - /// required bytes from input data. - /// - [Obsolete("Use constructor that accepts 'ReadOnlyMemory' parameter.")] - public Asn1Reader(Byte[] rawData) : this(rawData, 0) { } - /// - /// Initializes a new instance of the ASN1 class by using an ASN.1 encoded byte array. - /// - /// ASN.1-encoded byte array. /// /// The data in the rawData parameter is not valid ASN sequence. /// diff --git a/Asn1Parser/Universal/Asn1BitString.cs b/Asn1Parser/Universal/Asn1BitString.cs index 557a79a..e9347c4 100644 --- a/Asn1Parser/Universal/Asn1BitString.cs +++ b/Asn1Parser/Universal/Asn1BitString.cs @@ -29,7 +29,7 @@ public Asn1BitString(Asn1Reader asn) : base(asn, TYPE) { /// /// rawData is not BIT_STRING data type. /// - public Asn1BitString(Byte[] rawData) : this(new Asn1Reader(rawData.AsMemory())) { } + public Asn1BitString(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of Asn1BitString from a raw byte array to encode and parameter that indicates /// whether the bit length is decremented to exclude trailing zero bits. @@ -39,8 +39,10 @@ public Asn1BitString(Byte[] rawData) : this(new Asn1Reader(rawData.AsMemory())) /// True if the bit length is decremented to exclude trailing zero bits. Otherwise False. /// /// valueToEncode parameter is null reference. - public Asn1BitString(Byte[] valueToEncode, Boolean calculateUnusedBits) : base(TYPE) { - if (valueToEncode == null) { throw new ArgumentNullException(nameof(valueToEncode)); } + public Asn1BitString(ReadOnlySpan valueToEncode, Boolean calculateUnusedBits) : base(TYPE) { + if (valueToEncode == null) { + throw new ArgumentNullException(nameof(valueToEncode)); + } m_encode(valueToEncode, calculateUnusedBits, 0); } /// @@ -73,7 +75,7 @@ void m_encode(ReadOnlySpan value, Boolean calc, Byte unusedBits) { Span v = new Byte[value.Length + 1]; v[0] = UnusedBits; value.CopyTo(v.Slice(1)); - Initialize(new Asn1Reader(Asn1Utils.Encode(v, TYPE))); + Initialize(Asn1Utils.EncodeAsReader(v, TYPE)); } /// diff --git a/Asn1Parser/Universal/Asn1BmpString.cs b/Asn1Parser/Universal/Asn1BmpString.cs index 6d6dc39..8a51e61 100644 --- a/Asn1Parser/Universal/Asn1BmpString.cs +++ b/Asn1Parser/Universal/Asn1BmpString.cs @@ -28,7 +28,7 @@ public Asn1BMPString(Asn1Reader asn) : base(asn, TYPE) { /// /// rawData is not BMPString data type. /// - public Asn1BMPString(Byte[] rawData) : this(new Asn1Reader(rawData.AsMemory())) { } + public Asn1BMPString(Byte[] rawData) : this(rawData.AsMemory()) { } /// /// Initializes a new instance of Asn1BitString from a ASN.1-encoded byte array. /// @@ -47,7 +47,7 @@ public Asn1BMPString(String inputString) : base(TYPE) { void m_encode(String inputString) { Value = inputString; - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.BigEndianUnicode.GetBytes(inputString).AsSpan(), TYPE))); + Initialize(Asn1Utils.EncodeAsReader(Encoding.BigEndianUnicode.GetBytes(inputString).AsSpan(), TYPE)); } void m_decode(Asn1Reader asn) { Value = Encoding.BigEndianUnicode.GetString(asn.GetPayload()); diff --git a/Asn1Parser/Universal/Asn1Boolean.cs b/Asn1Parser/Universal/Asn1Boolean.cs index ad21823..00ebfa0 100644 --- a/Asn1Parser/Universal/Asn1Boolean.cs +++ b/Asn1Parser/Universal/Asn1Boolean.cs @@ -50,7 +50,7 @@ public Asn1Boolean(Boolean fValue) : base(TYPE) { void m_encode(Boolean fValue) { Value = fValue; Byte value = (Byte)(fValue ? 255 : 0); - Initialize(new Asn1Reader(Asn1Utils.Encode([value], TYPE))); + Initialize(Asn1Utils.EncodeAsReader([value], TYPE)); } void m_decode(Asn1Reader asn) { Value = asn[asn.PayloadStartOffset] > 0; diff --git a/Asn1Parser/Universal/Asn1DateTime.cs b/Asn1Parser/Universal/Asn1DateTime.cs index 164a71e..2943d42 100644 --- a/Asn1Parser/Universal/Asn1DateTime.cs +++ b/Asn1Parser/Universal/Asn1DateTime.cs @@ -48,7 +48,7 @@ void m_encode(Asn1Type type, DateTime time, TimeZoneInfo? zone, Boolean preciseT : TimeZoneInfo.ConvertTimeToUtc(time, zone).ToLocalTime(); Value = time; Boolean utcTime = type == Asn1Type.UTCTime; - Initialize(new Asn1Reader(Asn1Utils.Encode(DateTimeUtils.Encode(time, ref zone, utcTime, preciseTime), type))); + Initialize(Asn1Utils.EncodeAsReader(DateTimeUtils.Encode(time, ref zone, utcTime, preciseTime), type)); ZoneInfo = zone; } void m_decode(ReadOnlyMemory rawData) { diff --git a/Asn1Parser/Universal/Asn1Enumerated.cs b/Asn1Parser/Universal/Asn1Enumerated.cs index 5279054..fe0e81d 100644 --- a/Asn1Parser/Universal/Asn1Enumerated.cs +++ b/Asn1Parser/Universal/Asn1Enumerated.cs @@ -54,7 +54,7 @@ public Asn1Enumerated(UInt64 inputInteger) : base(TYPE) { void m_encode(BigInteger inputInteger) { Value = (UInt64)inputInteger; - Initialize(new Asn1Reader(Asn1Utils.Encode(inputInteger.GetAsnBytes(), TYPE))); + Initialize(Asn1Utils.EncodeAsReader(inputInteger.GetAsnBytes(), TYPE)); } void m_decode(Asn1Reader asn) { var value = new BigInteger(asn.GetPayload().Reverse().ToArray()); diff --git a/Asn1Parser/Universal/Asn1IA5String.cs b/Asn1Parser/Universal/Asn1IA5String.cs index 3ab4606..6487839 100644 --- a/Asn1Parser/Universal/Asn1IA5String.cs +++ b/Asn1Parser/Universal/Asn1IA5String.cs @@ -64,7 +64,7 @@ void m_encode(String inputString) { throw new InvalidDataException(String.Format(InvalidType, TYPE.ToString())); } Value = inputString; - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE))); + Initialize(Asn1Utils.EncodeAsReader(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE)); } void m_decode(Asn1Reader asn) { if (asn.GetPayload().Any(b => b > 127)) { diff --git a/Asn1Parser/Universal/Asn1Integer.cs b/Asn1Parser/Universal/Asn1Integer.cs index 2a63714..57cf304 100644 --- a/Asn1Parser/Universal/Asn1Integer.cs +++ b/Asn1Parser/Universal/Asn1Integer.cs @@ -53,7 +53,7 @@ public Asn1Integer(BigInteger inputInteger) : base(TYPE) { void m_encode(BigInteger inputInteger) { Value = inputInteger; - Initialize(new Asn1Reader(Asn1Utils.Encode(inputInteger.GetAsnBytes(), TYPE))); + Initialize(Asn1Utils.EncodeAsReader(inputInteger.GetAsnBytes(), TYPE)); } void m_decode(Asn1Reader asn) { Value = new BigInteger(asn.GetPayload().Reverse().ToArray()); diff --git a/Asn1Parser/Universal/Asn1NumericString.cs b/Asn1Parser/Universal/Asn1NumericString.cs index 49169e9..cee3aba 100644 --- a/Asn1Parser/Universal/Asn1NumericString.cs +++ b/Asn1Parser/Universal/Asn1NumericString.cs @@ -64,7 +64,7 @@ void m_encode(String inputString) { throw new InvalidDataException(String.Format(InvalidType, TYPE.ToString())); } Value = inputString; - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE))); + Initialize(Asn1Utils.EncodeAsReader(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE)); } void m_decode(Asn1Reader asn) { if (asn.GetPayload().Any(b => b is < 48 or > 57 && b != 32)) { diff --git a/Asn1Parser/Universal/Asn1ObjectIdentifier.cs b/Asn1Parser/Universal/Asn1ObjectIdentifier.cs index 0235cb9..cbe1e3d 100644 --- a/Asn1Parser/Universal/Asn1ObjectIdentifier.cs +++ b/Asn1Parser/Universal/Asn1ObjectIdentifier.cs @@ -80,7 +80,7 @@ void m_encode(Oid oid) { throw new InvalidDataException(String.Format(InvalidType, TYPE.ToString())); } Value = oid; - Initialize(new Asn1Reader(Asn1Utils.Encode(encode(tokens), TYPE))); + Initialize(Asn1Utils.EncodeAsReader(encode(tokens), TYPE)); } static ReadOnlySpan encode(IList tokens) { diff --git a/Asn1Parser/Universal/Asn1OctetString.cs b/Asn1Parser/Universal/Asn1OctetString.cs index c65c6be..4e13cb1 100644 --- a/Asn1Parser/Universal/Asn1OctetString.cs +++ b/Asn1Parser/Universal/Asn1OctetString.cs @@ -57,7 +57,7 @@ public Asn1OctetString(ReadOnlyMemory rawData, Boolean tagged) : base(TYPE Initialize(asn); } else { Value = rawData.ToArray(); - Initialize(new Asn1Reader(Asn1Utils.Encode(rawData.Span, TYPE))); + Initialize(Asn1Utils.EncodeAsReader(rawData.Span, TYPE)); } } diff --git a/Asn1Parser/Universal/Asn1PrintableString.cs b/Asn1Parser/Universal/Asn1PrintableString.cs index 2a326fd..748d4a1 100644 --- a/Asn1Parser/Universal/Asn1PrintableString.cs +++ b/Asn1Parser/Universal/Asn1PrintableString.cs @@ -66,7 +66,7 @@ void m_encode(String inputString) { throw new InvalidDataException(String.Format(InvalidType, TYPE.ToString())); } Value = inputString; - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE))); + Initialize(Asn1Utils.EncodeAsReader(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE)); } void m_decode(Asn1Reader asn) { if (!testValue(asn.GetPayload())) { diff --git a/Asn1Parser/Universal/Asn1RelativeOid.cs b/Asn1Parser/Universal/Asn1RelativeOid.cs index 99d13c4..19558a8 100644 --- a/Asn1Parser/Universal/Asn1RelativeOid.cs +++ b/Asn1Parser/Universal/Asn1RelativeOid.cs @@ -76,7 +76,7 @@ void m_encode(String oidString) { IEnumerable tokens = oidString .Split(['.'], StringSplitOptions.RemoveEmptyEntries) .Select(BigInteger.Parse); - Initialize(new Asn1Reader(Asn1Utils.Encode(encode(tokens), TYPE))); + Initialize(Asn1Utils.EncodeAsReader(encode(tokens), TYPE)); } static ReadOnlySpan encode(IEnumerable tokens) { var rawOid = new List(); diff --git a/Asn1Parser/Universal/Asn1TeletexString.cs b/Asn1Parser/Universal/Asn1TeletexString.cs index 981fca5..fced4ad 100644 --- a/Asn1Parser/Universal/Asn1TeletexString.cs +++ b/Asn1Parser/Universal/Asn1TeletexString.cs @@ -62,7 +62,7 @@ void m_encode(String inputString) { throw new InvalidDataException(String.Format(InvalidType, TYPE.ToString())); } Value = inputString; - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString), TYPE))); + Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE))); } protected override Boolean IsValidString(ReadOnlySpan value) { diff --git a/Asn1Parser/Universal/Asn1UTF8String.cs b/Asn1Parser/Universal/Asn1UTF8String.cs index fa08087..db865ff 100644 --- a/Asn1Parser/Universal/Asn1UTF8String.cs +++ b/Asn1Parser/Universal/Asn1UTF8String.cs @@ -64,7 +64,7 @@ void m_encode(String inputString) { throw new InvalidDataException(String.Format(InvalidType, TYPE.ToString())); } Value = inputString; - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.UTF8.GetBytes(inputString).AsMemory().Span, TYPE))); + Initialize(Asn1Utils.EncodeAsReader(Encoding.UTF8.GetBytes(inputString).AsMemory().Span, TYPE)); } void m_decode(Asn1Reader asn) { Value = Encoding.UTF8.GetString(asn.GetPayload()); diff --git a/Asn1Parser/Universal/Asn1UniversalString.cs b/Asn1Parser/Universal/Asn1UniversalString.cs index 8a6e036..e6a4af4 100644 --- a/Asn1Parser/Universal/Asn1UniversalString.cs +++ b/Asn1Parser/Universal/Asn1UniversalString.cs @@ -52,7 +52,11 @@ public Asn1UniversalString(String inputString) : base(TYPE) { void m_encode(String inputString) { Value = inputString; - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.UTF32.GetBytes(inputString.Reverse().ToArray()).Reverse().ToArray(), TYPE))); + Initialize(Asn1Utils.EncodeAsReader(Encoding.UTF32.GetBytes(inputString.Reverse().ToArray()) + .Reverse() + .ToArray() + .AsSpan(), + TYPE)); } void m_decode(Asn1Reader asn) { Value = new String(Encoding.UTF32.GetString(asn.GetPayload().Reverse().ToArray()).Reverse().ToArray()); diff --git a/Asn1Parser/Universal/Asn1VideotexString.cs b/Asn1Parser/Universal/Asn1VideotexString.cs index 4fbed56..d8eb8bf 100644 --- a/Asn1Parser/Universal/Asn1VideotexString.cs +++ b/Asn1Parser/Universal/Asn1VideotexString.cs @@ -56,7 +56,7 @@ public Asn1VideotexString(String inputString) : base(TYPE) { } void encode(String inputString) { - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE))); + Initialize(Asn1Utils.EncodeAsReader(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE)); Value = inputString; } protected override String Decode(ReadOnlySpan payload) { diff --git a/Asn1Parser/Universal/Asn1VisibleString.cs b/Asn1Parser/Universal/Asn1VisibleString.cs index df90bdc..3cf030a 100644 --- a/Asn1Parser/Universal/Asn1VisibleString.cs +++ b/Asn1Parser/Universal/Asn1VisibleString.cs @@ -66,7 +66,7 @@ void m_encode(String inputString) { throw new InvalidDataException(String.Format(InvalidType, TYPE.ToString())); } Value = inputString; - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE))); + Initialize(Asn1Utils.EncodeAsReader(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE)); } void m_decode(Asn1Reader asn) { if (asn.GetPayload().Any(b => b is < 32 or > 126)) { From bf9272ca4b17606d7b33771b6f70170576d07fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 14:31:24 +0200 Subject: [PATCH 38/66] removed all methods that accept byte arrays as parameter --- Asn1Parser/Asn1Builder.cs | 85 ++++----------------------------------- 1 file changed, 7 insertions(+), 78 deletions(-) diff --git a/Asn1Parser/Asn1Builder.cs b/Asn1Parser/Asn1Builder.cs index 207edd7..2c9b782 100644 --- a/Asn1Parser/Asn1Builder.cs +++ b/Asn1Parser/Asn1Builder.cs @@ -55,7 +55,7 @@ public Asn1Builder AddInteger(BigInteger value) { /// Unused bits in bit string. This value must fall in range between 0 and 7. /// /// Current instance with added value. - public Asn1Builder AddBitString(Byte[] value, Byte unusedBits) { + public Asn1Builder AddBitString(ReadOnlySpan value, Byte unusedBits) { if (value == null) { throw new ArgumentNullException(nameof(value)); } @@ -89,19 +89,6 @@ public Asn1Builder AddBitString(ReadOnlySpan value, Boolean calculateUnuse /// value parameter is null. /// /// Current instance with added value. - public Asn1Builder AddOctetString(Byte[] value) { - return AddOctetString(value.AsMemory()); - } - /// - /// Adds ASN.1 OCTET_STRING value. - /// - /// - /// Value to encode. - /// - /// - /// value parameter is null. - /// - /// Current instance with added value. public Asn1Builder AddOctetString(ReadOnlyMemory value) { _rawData.Add(new Asn1OctetString(value, false).GetRawDataAsMemory()); @@ -191,27 +178,6 @@ public Asn1Builder AddUTF8String(String value) { /// /// In the current implementation, SEQUENCE is encoded using constructed form only. /// - [Obsolete] - public Asn1Builder AddSequence(Byte[] value) { - if (value == null) { - throw new ArgumentNullException(nameof(value)); - } - - return AddSequence(value.AsSpan()); - } - /// - /// Adds ASN.1 SEQUENCE value. - /// - /// - /// Value to encode. - /// - /// - /// value parameter is null. - /// - /// Current instance with added value. - /// - /// In the current implementation, SEQUENCE is encoded using constructed form only. - /// public Asn1Builder AddSequence(ReadOnlySpan value) { _rawData.Add(Asn1Utils.Encode(value, 0x30)); @@ -230,26 +196,6 @@ public Asn1Builder AddSequence(ReadOnlySpan value) { /// /// In the current implementation, SET is encoded using constructed form only. /// - public Asn1Builder AddSet(Byte[] value) { - if (value == null) { - throw new ArgumentNullException(nameof(value)); - } - - return AddSet(value.AsSpan()); - } - /// - /// Adds ASN.1 SET value. - /// - /// - /// Value to encode. - /// - /// - /// value parameter is null. - /// - /// Current instance with added value. - /// - /// In the current implementation, SET is encoded using constructed form only. - /// public Asn1Builder AddSet(ReadOnlySpan value) { _rawData.Add(Asn1Utils.Encode(value, 0x31)); @@ -320,7 +266,7 @@ public Asn1Builder AddVideotexString(String value) { if (value == null) { throw new ArgumentNullException(nameof(value)); } - _rawData.Add(Asn1Utils.Encode(Encoding.ASCII.GetBytes(value), Asn1Type.VideotexString)); + _rawData.Add(Asn1Utils.Encode(Encoding.ASCII.GetBytes(value).AsSpan(), Asn1Type.VideotexString)); return this; } /// @@ -441,23 +387,6 @@ public Asn1Builder AddBMPString(String value) { /// value parameter is null. /// /// Current instance with added value. - public Asn1Builder AddDerData(Byte[] value) { - if (value == null) { - throw new ArgumentNullException(nameof(value)); - } - - return AddDerData(value.AsSpan()); - } - /// - /// Adds arbitrary ASN.1-encoded data. - /// - /// - /// Value to encode. - /// - /// - /// value parameter is null. - /// - /// Current instance with added value. public Asn1Builder AddDerData(ReadOnlySpan value) { var asn = new Asn1Reader(value.ToArray()); asn.BuildOffsetMap(); @@ -478,11 +407,9 @@ public Asn1Builder AddDerData(ReadOnlySpan value) { /// value parameter is null. /// /// Current instance with added value. - public Asn1Builder AddDerData(Byte[] value, Byte outerTag) { - if (value == null) { - throw new ArgumentNullException(nameof(value)); - } - _rawData.Add(Asn1Utils.Encode(value, outerTag)); + public Asn1Builder AddDerData(ReadOnlyMemory value, Byte outerTag) { + _rawData.Add(Asn1Utils.Encode(value.Span, outerTag)); + return this; } /// @@ -680,6 +607,8 @@ public Asn1Builder Encode(Byte outerTag = 0x30) { } Byte[] getEncoded(Byte outerTag, Boolean includeHeader) { + // we do all this complexity to allocate only one buffer of exact required size + // and avoid dynamic List resize overhead Int32 payloadLength = 0; foreach (ReadOnlyMemory chunk in _rawData) { payloadLength += chunk.Length; From 745dbfd746fdcf719a38c8655df1c0888a821714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 14:31:47 +0200 Subject: [PATCH 39/66] moved xmld-doc to appropriate class --- Asn1Parser/Asn1Reader.cs | 8 -------- Asn1Parser/Asn1Utils.cs | 9 +++++++++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Asn1Parser/Asn1Reader.cs b/Asn1Parser/Asn1Reader.cs index 06b9958..f63bcf2 100644 --- a/Asn1Parser/Asn1Reader.cs +++ b/Asn1Parser/Asn1Reader.cs @@ -12,14 +12,6 @@ namespace SysadminsLV.Asn1Parser; /// Provides a set of properties and generic methods to work with ASN.1 structures in Distinguished Encoding /// Rules (DER) encoding. /// -/// -/// Static methods of this class provides an encoders and decoders for the generic .NET types and unmanaged -/// structures. -/// Static methods (except Encode) strictly verify -/// whether the encoded or source data is valid for the specific ASN.1 type. If the data is not appropriate -/// for the method, it throws -/// -/// public class Asn1Reader { // a list of primitive tags. Source: http://en.wikipedia.org/wiki/Distinguished_Encoding_Rules#DER_encoding // although we actively do lookups, it is NOT recommended to use sets (HashSet), because at current collection size diff --git a/Asn1Parser/Asn1Utils.cs b/Asn1Parser/Asn1Utils.cs index 48cb1fb..e5dd030 100644 --- a/Asn1Parser/Asn1Utils.cs +++ b/Asn1Parser/Asn1Utils.cs @@ -7,6 +7,15 @@ namespace SysadminsLV.Asn1Parser; /// /// Contains utility methods for ASN.1 data. /// +/// +/// Static methods of this class provides an encoders and decoders for the generic .NET types and unmanaged +/// structures. +/// Static methods (except Encode(ReadOnlySpan<Byte>, Byte) +/// and Encode(ReadOnlySpan<Byte>, Asn1Type)) strictly verify +/// whether the encoded or source data is valid for the specific ASN.1 type. If the data is not appropriate +/// for the method, it throws +/// +/// public static class Asn1Utils { #region ASN.1 helper methods /// From e013f772f626fd892e88378d0a83bf654861936e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 14:33:13 +0200 Subject: [PATCH 40/66] refactored Asn1Utils by ensuring that methods accept ReadOnlySpan as parameter. Two methods that use pure byte array are left for compatibility purposes. --- Asn1Parser/Asn1Utils.cs | 48 ++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/Asn1Parser/Asn1Utils.cs b/Asn1Parser/Asn1Utils.cs index e5dd030..e8e13bb 100644 --- a/Asn1Parser/Asn1Utils.cs +++ b/Asn1Parser/Asn1Utils.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Text; using SysadminsLV.Asn1Parser.Universal; @@ -57,7 +58,7 @@ public static Byte[] GetLengthBytes(Int32 payloadLength) { /// asnHeader parameter length is more than 4 bytes or is invalid value. /// /// ASN.1 payload length in bytes. - public static Int64 CalculatePayloadLength(Byte[] asnHeader) { + public static Int64 CalculatePayloadLength(ReadOnlySpan asnHeader) { if (asnHeader == null) { throw new ArgumentNullException(nameof(asnHeader)); } @@ -83,10 +84,34 @@ public static Int64 CalculatePayloadLength(Byte[] asnHeader) { /// /// This method do not check whether the data in rawData is valid data for specified enclosing type. /// A byte array to wrap. + /// An enumeration of . + /// Wrapped encoded byte array. + /// If rawData is null, an empty tag is encoded. + [Obsolete("Consider the use of other overloads that accept 'ReadOnlySpan' as a parameter.")] + public static Byte[] Encode(Byte[] rawData, Asn1Type type) { + return Encode(rawData.AsSpan(), type).ToArray(); + } + /// + /// Wraps encoded data to an ASN.1 type/structure. + /// + /// This method do not check whether the data in rawData is valid data for specified enclosing type. + /// A byte array to wrap. + /// An enumeration of . + /// Wrapped encoded byte array. + /// If rawData is null, an empty tag is encoded. + [Obsolete("Consider the use of other overloads that accept 'ReadOnlySpan' as a parameter.")] + public static Byte[] Encode(Byte[] rawData, Byte enclosingTag) { + return Encode(rawData.AsSpan(), enclosingTag).ToArray(); + } + /// + /// Wraps encoded data to an ASN.1 type/structure. + /// + /// This method do not check whether the data in rawData is valid data for specified enclosing type. + /// A byte array to wrap. /// An enumeration of type represented as byte. /// Wrapped encoded byte array. /// If rawData is null, an empty tag is encoded. - public static Memory Encode(ReadOnlySpan rawData, Byte enclosingTag) { + public static ReadOnlyMemory Encode(ReadOnlySpan rawData, Byte enclosingTag) { Byte[] retValue; if (rawData == null) { retValue = [enclosingTag, 0]; @@ -139,18 +164,7 @@ public static Memory Encode(ReadOnlySpan rawData, Byte enclosingTag) /// An enumeration of . /// Wrapped encoded byte array. /// If rawData is null, an empty tag is encoded. - public static Byte[] Encode(Byte[] rawData, Asn1Type type) { - return Encode(rawData, (Byte)type).ToArray(); - } - /// - /// Wraps encoded data to an ASN.1 type/structure. - /// - /// This method do not check whether the data in rawData is valid data for specified enclosing type. - /// A byte array to wrap. - /// An enumeration of . - /// Wrapped encoded byte array. - /// If rawData is null, an empty tag is encoded. - public static Memory Encode(ReadOnlySpan rawData, Asn1Type type) { + public static ReadOnlyMemory Encode(ReadOnlySpan rawData, Asn1Type type) { return Encode(rawData, (Byte)type); } /// @@ -169,11 +183,11 @@ public static Asn1Reader EncodeAsReader(ReadOnlySpan rawData, Asn1Type typ /// /// This method do not check whether the data in rawData is valid data for specified enclosing type. /// A byte array to wrap. - /// An enumeration of . + /// An enumeration of . /// ASN.1 reader that represents encoded type. /// If rawData is null, an empty tag is encoded. - public static Asn1Reader EncodeAsReader(ReadOnlySpan rawData, Byte type) { - return new Asn1Reader(Encode(rawData, type)); + public static Asn1Reader EncodeAsReader(ReadOnlySpan rawData, Byte enclosingTag) { + return new Asn1Reader(Encode(rawData, enclosingTag)); } #endregion From 9a20e1eb3eb4eea05d143f831533ed83f360bd66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Tue, 25 Feb 2025 15:09:26 +0200 Subject: [PATCH 41/66] removed constructors from ASN.1 decoders that accept pure byte arrays and updated xml-docs --- Asn1Parser/AsnFormatter.cs | 34 -------------------- Asn1Parser/Universal/Asn1BitString.cs | 8 ++--- Asn1Parser/Universal/Asn1BmpString.cs | 12 ++----- Asn1Parser/Universal/Asn1Boolean.cs | 12 ++----- Asn1Parser/Universal/Asn1Enumerated.cs | 12 ++----- Asn1Parser/Universal/Asn1GeneralizedTime.cs | 13 ++------ Asn1Parser/Universal/Asn1IA5String.cs | 15 ++------- Asn1Parser/Universal/Asn1Integer.cs | 12 ++----- Asn1Parser/Universal/Asn1Null.cs | 12 ++----- Asn1Parser/Universal/Asn1NumericString.cs | 15 ++------- Asn1Parser/Universal/Asn1ObjectIdentifier.cs | 10 ++---- Asn1Parser/Universal/Asn1OctetString.cs | 13 -------- Asn1Parser/Universal/Asn1PrintableString.cs | 15 ++------- Asn1Parser/Universal/Asn1RelativeOid.cs | 10 ++---- Asn1Parser/Universal/Asn1TeletexString.cs | 17 ++-------- Asn1Parser/Universal/Asn1UTF8String.cs | 15 ++------- Asn1Parser/Universal/Asn1UniversalString.cs | 12 ++----- Asn1Parser/Universal/Asn1UtcTime.cs | 13 ++------ Asn1Parser/Universal/Asn1VideotexString.cs | 15 ++------- Asn1Parser/Universal/Asn1VisibleString.cs | 15 ++------- 20 files changed, 39 insertions(+), 241 deletions(-) diff --git a/Asn1Parser/AsnFormatter.cs b/Asn1Parser/AsnFormatter.cs index 6f696d9..c5e9fa3 100644 --- a/Asn1Parser/AsnFormatter.cs +++ b/Asn1Parser/AsnFormatter.cs @@ -7,40 +7,6 @@ namespace SysadminsLV.Asn1Parser; /// This class contains methods to convert Base64, Hex and Binary strings to byte array and vice versa. /// public static class AsnFormatter { - /// - /// Converts and formats byte array to a string. See for encoding examples. - /// - /// Byte array to format. - /// Specifies the encoding for formatting. Default is HexRaw - /// - /// Specifies the encoding options. The default behavior is to use a carriage return/line feed - /// (CR/LF) pair (0x0D/0x0A) to represent a new line. - /// - /// Specifies the start position of the byte array to format. Default is zero. - /// Specifies how many bytes must be formatted. If zero, entire byte array will be encoded. - /// - /// Specifies whether the force hex octet representation in upper case. Default is lower case. - /// - /// This parameter has effect only when hex encoding is selected in the encoding parameter: - /// Hex, HexRaw, HexAddress, HexAscii - /// and HexAsciiAddress. For other values, this parameter is silently ignored. - /// - /// - /// An invalid encoding type was specified. - /// Encoded and formatted string. - /// - /// This method do not support the following encoding types: - /// - /// Binary - /// Base64Any - /// StringAny - /// HexAny - /// - /// - public static String BinaryToString(Byte[] rawData, EncodingType encoding = EncodingType.HexRaw, EncodingFormat format = EncodingFormat.CRLF, Int32 start = 0, Int32 count = 0, Boolean forceUpperCase = false) { - Span slice = count == 0 ? rawData.AsSpan() : rawData.AsSpan().Slice(start, count); - return BinaryToString(slice, encoding, format, forceUpperCase); - } /// /// Converts and formats byte array to a string. See for encoding examples. /// diff --git a/Asn1Parser/Universal/Asn1BitString.cs b/Asn1Parser/Universal/Asn1BitString.cs index e9347c4..de74ee6 100644 --- a/Asn1Parser/Universal/Asn1BitString.cs +++ b/Asn1Parser/Universal/Asn1BitString.cs @@ -23,15 +23,15 @@ public Asn1BitString(Asn1Reader asn) : base(asn, TYPE) { Value = asn.GetPayload().Skip(1).ToArray(); } /// - /// Initializes a new instance of Asn1BitString from a ASN.1-encoded byte array. + /// Initializes a new instance of Asn1BitString from an ASN.1-encoded memory buffer. /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// rawData is not BIT_STRING data type. /// public Asn1BitString(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// - /// Initializes a new instance of Asn1BitString from a raw byte array to encode and parameter that indicates + /// Initializes a new instance of Asn1BitString from a raw memory buffer to encode and parameter that indicates /// whether the bit length is decremented to exclude trailing zero bits. /// /// Raw value to encode. @@ -46,7 +46,7 @@ public Asn1BitString(ReadOnlySpan valueToEncode, Boolean calculateUnusedBi m_encode(valueToEncode, calculateUnusedBits, 0); } /// - /// Initializes a new instance of Asn1BitString from a raw byte array to encode and a number of unused bits + /// Initializes a new instance of Asn1BitString from a raw memory buffer to encode and a number of unused bits /// in a current bit string. /// /// Raw value to encode. diff --git a/Asn1Parser/Universal/Asn1BmpString.cs b/Asn1Parser/Universal/Asn1BmpString.cs index 8a51e61..344d47f 100644 --- a/Asn1Parser/Universal/Asn1BmpString.cs +++ b/Asn1Parser/Universal/Asn1BmpString.cs @@ -22,17 +22,9 @@ public Asn1BMPString(Asn1Reader asn) : base(asn, TYPE) { m_decode(asn); } /// - /// Initializes a new instance of Asn1BitString from a ASN.1-encoded byte array. + /// Initializes a new instance of Asn1BitString from a ASN.1-encoded memory buffer. /// - /// ASN.1-encoded byte array. - /// - /// rawData is not BMPString data type. - /// - public Asn1BMPString(Byte[] rawData) : this(rawData.AsMemory()) { } - /// - /// Initializes a new instance of Asn1BitString from a ASN.1-encoded byte array. - /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// rawData is not BMPString data type. /// diff --git a/Asn1Parser/Universal/Asn1Boolean.cs b/Asn1Parser/Universal/Asn1Boolean.cs index 00ebfa0..ebd2645 100644 --- a/Asn1Parser/Universal/Asn1Boolean.cs +++ b/Asn1Parser/Universal/Asn1Boolean.cs @@ -20,17 +20,9 @@ public Asn1Boolean(Asn1Reader asn) : base(asn, TYPE) { m_decode(asn); } /// - /// Initializes a new instance of Asn1Boolean from a ASN.1-encoded byte array. + /// Initializes a new instance of Asn1Boolean from a ASN.1-encoded memory buffer. /// - /// ASN.1-encoded byte array. - /// - /// rawData is not valid BOOLEAN data type. - /// - public Asn1Boolean(Byte[] rawData) : this(rawData.AsMemory()) { } - /// - /// Initializes a new instance of Asn1Boolean from a ASN.1-encoded byte array. - /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// rawData is not valid BOOLEAN data type. /// diff --git a/Asn1Parser/Universal/Asn1Enumerated.cs b/Asn1Parser/Universal/Asn1Enumerated.cs index fe0e81d..78fc38b 100644 --- a/Asn1Parser/Universal/Asn1Enumerated.cs +++ b/Asn1Parser/Universal/Asn1Enumerated.cs @@ -24,17 +24,9 @@ public Asn1Enumerated(Asn1Reader asn) : base(asn, TYPE) { m_decode(asn); } /// - /// Initializes a new instance of Asn1Enumerated from a ASN.1-encoded byte array. + /// Initializes a new instance of Asn1Enumerated from a ASN.1-encoded memory buffer. /// - /// ASN.1-encoded byte array. - /// - /// rawData is not valid INTEGER data type. - /// - public Asn1Enumerated(Byte[] rawData) : this(rawData.AsMemory()) { } - /// - /// Initializes a new instance of Asn1Enumerated from a ASN.1-encoded byte array. - /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// rawData is not valid INTEGER data type. /// diff --git a/Asn1Parser/Universal/Asn1GeneralizedTime.cs b/Asn1Parser/Universal/Asn1GeneralizedTime.cs index 21f447c..84a4c83 100644 --- a/Asn1Parser/Universal/Asn1GeneralizedTime.cs +++ b/Asn1Parser/Universal/Asn1GeneralizedTime.cs @@ -40,19 +40,10 @@ public Asn1GeneralizedTime(DateTime time, TimeZoneInfo? zone = null, Boolean pre /// public Asn1GeneralizedTime(Asn1Reader asn) : base(asn, TYPE) { } /// - /// Initializes a new instance of the Asn1GeneralizedTime class from a byte array that + /// Initializes a new instance of the Asn1GeneralizedTime class from a memory buffer that /// represents encoded UTC time. /// - /// ASN.1-encoded byte array. - /// - /// The current state of ASN1 object is not Generalized Time. - /// - public Asn1GeneralizedTime(Byte[] rawData) : this(rawData.AsMemory()) { } - /// - /// Initializes a new instance of the Asn1GeneralizedTime class from a byte array that - /// represents encoded UTC time. - /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// The current state of ASN1 object is not Generalized Time. /// diff --git a/Asn1Parser/Universal/Asn1IA5String.cs b/Asn1Parser/Universal/Asn1IA5String.cs index 6487839..89537d3 100644 --- a/Asn1Parser/Universal/Asn1IA5String.cs +++ b/Asn1Parser/Universal/Asn1IA5String.cs @@ -27,20 +27,9 @@ public Asn1IA5String(Asn1Reader asn) : base(asn, TYPE) { m_decode(asn); } /// - /// Initializes a new instance of Asn1IA5String from a ASN.1-encoded byte array. + /// Initializes a new instance of Asn1IA5String from a ASN.1-encoded memory buffer. /// - /// ASN.1-encoded byte array. - /// - /// rawData is not IA5String data type. - /// - /// - /// Input data contains invalid IA5String character. - /// - public Asn1IA5String(Byte[] rawData) : this(rawData.AsMemory()) { } - /// - /// Initializes a new instance of Asn1IA5String from a ASN.1-encoded byte array. - /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// rawData is not IA5String data type. /// diff --git a/Asn1Parser/Universal/Asn1Integer.cs b/Asn1Parser/Universal/Asn1Integer.cs index 57cf304..f831d0e 100644 --- a/Asn1Parser/Universal/Asn1Integer.cs +++ b/Asn1Parser/Universal/Asn1Integer.cs @@ -23,17 +23,9 @@ public Asn1Integer(Asn1Reader asn) : base(asn, TYPE) { m_decode(asn); } /// - /// Initializes a new instance of Asn1Integer from a ASN.1-encoded byte array. + /// Initializes a new instance of Asn1Integer from a ASN.1-encoded memory buffer. /// - /// ASN.1-encoded byte array. - /// - /// rawData is not valid INTEGER data type. - /// - public Asn1Integer(Byte[] rawData) : this(rawData.AsMemory()) { } - /// - /// Initializes a new instance of Asn1Integer from a ASN.1-encoded byte array. - /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// rawData is not valid INTEGER data type. /// diff --git a/Asn1Parser/Universal/Asn1Null.cs b/Asn1Parser/Universal/Asn1Null.cs index 19377ee..1716936 100644 --- a/Asn1Parser/Universal/Asn1Null.cs +++ b/Asn1Parser/Universal/Asn1Null.cs @@ -27,17 +27,9 @@ public Asn1Null(Asn1Reader asn) : base(asn, TYPE) { m_decode(asn); } /// - /// Initializes a new instance of Asn1Null from a ASN.1-encoded byte array. + /// Initializes a new instance of Asn1Null from a ASN.1-encoded memory buffer. /// - /// ASN.1-encoded byte array. - /// - /// rawData is not valid NULL data type. - /// - public Asn1Null(Byte[] rawData) : this(rawData.AsMemory()) { } - /// - /// Initializes a new instance of Asn1Null from a ASN.1-encoded byte array. - /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// rawData is not valid NULL data type. /// diff --git a/Asn1Parser/Universal/Asn1NumericString.cs b/Asn1Parser/Universal/Asn1NumericString.cs index cee3aba..077a72f 100644 --- a/Asn1Parser/Universal/Asn1NumericString.cs +++ b/Asn1Parser/Universal/Asn1NumericString.cs @@ -27,20 +27,9 @@ public Asn1NumericString(Asn1Reader asn) : base(asn, TYPE) { m_decode(asn); } /// - /// Initializes a new instance of Asn1NumericString from a ASN.1-encoded byte array. + /// Initializes a new instance of Asn1NumericString from a ASN.1-encoded memory buffer. /// - /// ASN.1-encoded byte array. - /// - /// rawData is not NumericString data type. - /// - /// - /// Input data contains invalid NumericString character. - /// - public Asn1NumericString(Byte[] rawData) : this(rawData.AsMemory()) { } - /// - /// Initializes a new instance of Asn1NumericString from a ASN.1-encoded byte array. - /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// rawData is not NumericString data type. /// diff --git a/Asn1Parser/Universal/Asn1ObjectIdentifier.cs b/Asn1Parser/Universal/Asn1ObjectIdentifier.cs index cbe1e3d..922819a 100644 --- a/Asn1Parser/Universal/Asn1ObjectIdentifier.cs +++ b/Asn1Parser/Universal/Asn1ObjectIdentifier.cs @@ -28,16 +28,10 @@ public Asn1ObjectIdentifier(Asn1Reader asn) : base(asn, TYPE) { Value = new Oid(decode(asn)); } /// - /// Initializes a new instance of the Asn1ObjectIdentifier class from a byte array + /// Initializes a new instance of the Asn1ObjectIdentifier class from a memory buffer /// that represents encoded object identifier. /// - /// Byte array that represents encoded object identifier. - public Asn1ObjectIdentifier(Byte[] rawData) : this(rawData.AsMemory()) { } - /// - /// Initializes a new instance of the Asn1ObjectIdentifier class from a byte array - /// that represents encoded object identifier. - /// - /// Byte array that represents encoded object identifier. + /// Memory buffer that represents encoded object identifier. public Asn1ObjectIdentifier(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of the Asn1ObjectIdentifier class from a string diff --git a/Asn1Parser/Universal/Asn1OctetString.cs b/Asn1Parser/Universal/Asn1OctetString.cs index 4e13cb1..7c5bb48 100644 --- a/Asn1Parser/Universal/Asn1OctetString.cs +++ b/Asn1Parser/Universal/Asn1OctetString.cs @@ -24,19 +24,6 @@ public Asn1OctetString(Asn1Reader asn) : base(asn, TYPE) { Value = asn.GetPayload(); } /// - /// Initializes a new instance of Asn1NumericString from a ASN.1-encoded byte array. - /// - /// ASN.1-encoded byte array. - /// Boolean value that indicates whether the byte array in rawData parameter is encoded or not. - /// - /// rawData is not NumericString data type. - /// - /// - /// Input data contains invalid NumericString character. - /// - [Obsolete("Consider using constructor that accepts 'ReadOnlyMemory' instead.")] - public Asn1OctetString(Byte[] rawData, Boolean tagged) : this(rawData.AsMemory(), tagged) { } - /// /// Initializes a new instance of Asn1NumericString from a ASN.1-encoded memory buffer. /// /// ASN.1-encoded memory buffer. diff --git a/Asn1Parser/Universal/Asn1PrintableString.cs b/Asn1Parser/Universal/Asn1PrintableString.cs index 748d4a1..e62d41d 100644 --- a/Asn1Parser/Universal/Asn1PrintableString.cs +++ b/Asn1Parser/Universal/Asn1PrintableString.cs @@ -29,20 +29,9 @@ public Asn1PrintableString(Asn1Reader asn) : base(asn, TYPE) { m_decode(asn); } /// - /// Initializes a new instance of Asn1PrintableString from a ASN.1-encoded byte array. + /// Initializes a new instance of Asn1PrintableString from a ASN.1-encoded memory buffer. /// - /// ASN.1-encoded byte array. - /// - /// rawData is not PrintableString data type. - /// - /// - /// Input data contains invalid PrintableString character. - /// - public Asn1PrintableString(Byte[] rawData) : this(rawData.AsMemory()) { } - /// - /// Initializes a new instance of Asn1PrintableString from a ASN.1-encoded byte array. - /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// rawData is not PrintableString data type. /// diff --git a/Asn1Parser/Universal/Asn1RelativeOid.cs b/Asn1Parser/Universal/Asn1RelativeOid.cs index 19558a8..ee6634d 100644 --- a/Asn1Parser/Universal/Asn1RelativeOid.cs +++ b/Asn1Parser/Universal/Asn1RelativeOid.cs @@ -24,16 +24,10 @@ public Asn1RelativeOid(Asn1Reader asn) : base(asn, TYPE) { Value = decode(asn); } /// - /// Initializes a new instance of the Asn1RelativeOid class from a byte array + /// Initializes a new instance of the Asn1RelativeOid class from a memory buffer /// that represents encoded relative object identifier. /// - /// Byte array that represents encoded relative object identifier. - public Asn1RelativeOid(Byte[] rawData) : this(rawData.AsMemory()) { } - /// - /// Initializes a new instance of the Asn1RelativeOid class from a byte array - /// that represents encoded relative object identifier. - /// - /// Byte array that represents encoded relative object identifier. + /// Memory buffer that represents encoded relative object identifier. public Asn1RelativeOid(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of the Asn1RelativeOid class from a string diff --git a/Asn1Parser/Universal/Asn1TeletexString.cs b/Asn1Parser/Universal/Asn1TeletexString.cs index fced4ad..cc6c225 100644 --- a/Asn1Parser/Universal/Asn1TeletexString.cs +++ b/Asn1Parser/Universal/Asn1TeletexString.cs @@ -15,7 +15,7 @@ public sealed class Asn1TeletexString : Asn1String { /// /// Initializes a new instance of Asn1TeletexString from an ASN reader object. /// - /// ASN.1-encoded byte array. + /// Existing object. /// /// Current position in the ASN.1 object is not TeletexString. /// @@ -24,20 +24,9 @@ public sealed class Asn1TeletexString : Asn1String { /// public Asn1TeletexString(Asn1Reader asn) : base(asn, TYPE) { } /// - /// Initializes a new instance of Asn1TeletexString from an ASN.1-encoded byte array. + /// Initializes a new instance of Asn1TeletexString from an ASN.1-encoded memory buffer. /// - /// ASN.1-encoded byte array. - /// - /// rawData parameter represents different data type. - /// - /// - /// Input data contains invalid TeletexString character. - /// - public Asn1TeletexString(Byte[] rawData) : this(rawData.AsMemory()) { } - /// - /// Initializes a new instance of Asn1TeletexString from an ASN.1-encoded byte array. - /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// rawData parameter represents different data type. /// diff --git a/Asn1Parser/Universal/Asn1UTF8String.cs b/Asn1Parser/Universal/Asn1UTF8String.cs index db865ff..7d5e4f3 100644 --- a/Asn1Parser/Universal/Asn1UTF8String.cs +++ b/Asn1Parser/Universal/Asn1UTF8String.cs @@ -27,20 +27,9 @@ public Asn1UTF8String(Asn1Reader asn) : base(asn, TYPE) { m_decode(asn); } /// - /// Initializes a new instance of Asn1UTF8String from a ASN.1-encoded byte array. + /// Initializes a new instance of Asn1UTF8String from a ASN.1-encoded memory buffer. /// - /// ASN.1-encoded byte array. - /// - /// rawData is not UTF8String data type. - /// - /// - /// Input data contains invalid UTF8String character. - /// - public Asn1UTF8String(Byte[] rawData) : this(rawData.AsMemory()) { } - /// - /// Initializes a new instance of Asn1UTF8String from a ASN.1-encoded byte array. - /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// rawData is not UTF8String data type. /// diff --git a/Asn1Parser/Universal/Asn1UniversalString.cs b/Asn1Parser/Universal/Asn1UniversalString.cs index e6a4af4..0ffdbe0 100644 --- a/Asn1Parser/Universal/Asn1UniversalString.cs +++ b/Asn1Parser/Universal/Asn1UniversalString.cs @@ -24,17 +24,9 @@ public Asn1UniversalString(Asn1Reader asn) : base(asn, TYPE) { m_decode(asn); } /// - /// Initializes a new instance of Asn1UniversalString from a ASN.1-encoded byte array. + /// Initializes a new instance of Asn1UniversalString from a ASN.1-encoded memory buffer. /// - /// ASN.1-encoded byte array. - /// - /// rawData is not UniversalString data type. - /// - public Asn1UniversalString(Byte[] rawData) : this(rawData.AsMemory()) { } - /// - /// Initializes a new instance of Asn1UniversalString from a ASN.1-encoded byte array. - /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// rawData is not UniversalString data type. /// diff --git a/Asn1Parser/Universal/Asn1UtcTime.cs b/Asn1Parser/Universal/Asn1UtcTime.cs index a380172..fce3068 100644 --- a/Asn1Parser/Universal/Asn1UtcTime.cs +++ b/Asn1Parser/Universal/Asn1UtcTime.cs @@ -40,19 +40,10 @@ public Asn1UtcTime(DateTime time, TimeZoneInfo? zone = null, Boolean preciseTime /// public Asn1UtcTime(Asn1Reader asn) : base(asn, TYPE) { } /// - /// Initializes a new instance of the Asn1UtcTime class from a byte array that + /// Initializes a new instance of the Asn1UtcTime class from a memory buffer that /// represents encoded UTC time. /// - /// ASN.1-encoded byte array. - /// - /// The current state of ASN1 object is not UTC time. - /// - public Asn1UtcTime(Byte[] rawData) : this(rawData.AsMemory()) { } - /// - /// Initializes a new instance of the Asn1UtcTime class from a byte array that - /// represents encoded UTC time. - /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// The current state of ASN1 object is not UTC time. /// diff --git a/Asn1Parser/Universal/Asn1VideotexString.cs b/Asn1Parser/Universal/Asn1VideotexString.cs index d8eb8bf..2015272 100644 --- a/Asn1Parser/Universal/Asn1VideotexString.cs +++ b/Asn1Parser/Universal/Asn1VideotexString.cs @@ -23,20 +23,9 @@ public sealed class Asn1VideotexString : Asn1String { /// public Asn1VideotexString(Asn1Reader asn) : base(asn, TYPE) { } /// - /// Initializes a new instance of Asn1VideotexString from a ASN.1-encoded byte array. + /// Initializes a new instance of Asn1VideotexString from a ASN.1-encoded memory buffer. /// - /// ASN.1-encoded byte array. - /// - /// rawData is not VideotexString data type. - /// - /// - /// Input data contains invalid VideotexString character. - /// - public Asn1VideotexString(Byte[] rawData) : this(rawData.AsMemory()) { } - /// - /// Initializes a new instance of Asn1VideotexString from a ASN.1-encoded byte array. - /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// rawData is not VideotexString data type. /// diff --git a/Asn1Parser/Universal/Asn1VisibleString.cs b/Asn1Parser/Universal/Asn1VisibleString.cs index 3cf030a..cc41f79 100644 --- a/Asn1Parser/Universal/Asn1VisibleString.cs +++ b/Asn1Parser/Universal/Asn1VisibleString.cs @@ -29,20 +29,9 @@ public Asn1VisibleString(Asn1Reader asn) : base(asn, TYPE) { m_decode(asn); } /// - /// Initializes a new instance of Asn1VisibleString from a ASN.1-encoded byte array. + /// Initializes a new instance of Asn1VisibleString from a ASN.1-encoded memory buffer. /// - /// ASN.1-encoded byte array. - /// - /// rawData is not VisibleString data type. - /// - /// - /// Input data contains invalid VisibleString character. - /// - public Asn1VisibleString(Byte[] rawData) : this(rawData.AsMemory()) { } - /// - /// Initializes a new instance of Asn1VisibleString from a ASN.1-encoded byte array. - /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// rawData is not VisibleString data type. /// From 21297f7fbf25dcfdc427af8d266d337da668b86f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Wed, 26 Feb 2025 11:23:46 +0200 Subject: [PATCH 42/66] - removed redundant null checks - updated null check syntax --- Asn1Parser/Asn1Builder.cs | 51 ++++++-------------- Asn1Parser/Asn1Reader.cs | 5 +- Asn1Parser/Asn1Utils.cs | 8 +-- Asn1Parser/AsnFormatter.cs | 18 +++---- Asn1Parser/Universal/Asn1BitString.cs | 10 ---- Asn1Parser/Universal/Asn1DateTime.cs | 2 +- Asn1Parser/Universal/Asn1ObjectIdentifier.cs | 2 +- Asn1Parser/Universal/Asn1RelativeOid.cs | 2 +- Asn1Parser/Universal/Asn1String.cs | 2 +- Asn1Parser/Universal/Asn1Universal.cs | 6 +-- Asn1Parser/Utils/DateTimeUtils.cs | 2 +- tests/Asn1Parser.Tests/Asn1DateTimeTests.cs | 4 +- 12 files changed, 37 insertions(+), 75 deletions(-) diff --git a/Asn1Parser/Asn1Builder.cs b/Asn1Parser/Asn1Builder.cs index 2c9b782..6d8f15a 100644 --- a/Asn1Parser/Asn1Builder.cs +++ b/Asn1Parser/Asn1Builder.cs @@ -56,9 +56,6 @@ public Asn1Builder AddInteger(BigInteger value) { /// /// Current instance with added value. public Asn1Builder AddBitString(ReadOnlySpan value, Byte unusedBits) { - if (value == null) { - throw new ArgumentNullException(nameof(value)); - } _rawData.Add(new Asn1BitString(value, unusedBits).GetRawDataAsMemory()); return this; } @@ -73,9 +70,6 @@ public Asn1Builder AddBitString(ReadOnlySpan value, Byte unusedBits) { /// /// Current instance with added value. public Asn1Builder AddBitString(ReadOnlySpan value, Boolean calculateUnusedBits = false) { - if (value == null) { - throw new ArgumentNullException(nameof(value)); - } _rawData.Add(new Asn1BitString(value, calculateUnusedBits).GetRawDataAsMemory()); return this; } @@ -91,7 +85,6 @@ public Asn1Builder AddBitString(ReadOnlySpan value, Boolean calculateUnuse /// Current instance with added value. public Asn1Builder AddOctetString(ReadOnlyMemory value) { _rawData.Add(new Asn1OctetString(value, false).GetRawDataAsMemory()); - return this; } /// @@ -113,7 +106,7 @@ public Asn1Builder AddNull() { /// /// Current instance with added value. public Asn1Builder AddObjectIdentifier(Oid value) { - if (value == null) { + if (value is null) { throw new ArgumentNullException(nameof(value)); } _rawData.Add(new Asn1ObjectIdentifier(value).GetRawDataAsMemory()); @@ -131,7 +124,7 @@ public Asn1Builder AddObjectIdentifier(Oid value) { /// Specified value doesn't represent valid decimal-dot format. /// Current instance with added value. public Asn1Builder AddRelativeOid(String value) { - if (value == null) { + if (value is null) { throw new ArgumentNullException(nameof(value)); } _rawData.Add(new Asn1RelativeOid(value).GetRawDataAsMemory()); @@ -159,7 +152,7 @@ public Asn1Builder AddEnumerated(UInt64 value) { /// /// Current instance with added value. public Asn1Builder AddUTF8String(String value) { - if (value == null) { + if (value is null) { throw new ArgumentNullException(nameof(value)); } _rawData.Add(new Asn1UTF8String(value).GetRawDataAsMemory()); @@ -212,7 +205,7 @@ public Asn1Builder AddSet(ReadOnlySpan value) { /// /// Current instance with added value. public Asn1Builder AddNumericString(String value) { - if (value == null) { + if (value is null) { throw new ArgumentNullException(nameof(value)); } _rawData.Add(new Asn1NumericString(value).GetRawDataAsMemory()); @@ -229,7 +222,7 @@ public Asn1Builder AddNumericString(String value) { /// /// Current instance with added value. public Asn1Builder AddPrintableString(String value) { - if (value == null) { + if (value is null) { throw new ArgumentNullException(nameof(value)); } _rawData.Add(new Asn1PrintableString(value).GetRawDataAsMemory()); @@ -246,7 +239,7 @@ public Asn1Builder AddPrintableString(String value) { /// /// Current instance with added value. public Asn1Builder AddTeletexString(String value) { - if (value == null) { + if (value is null) { throw new ArgumentNullException(nameof(value)); } _rawData.Add(new Asn1TeletexString(value).GetRawDataAsMemory()); @@ -263,7 +256,7 @@ public Asn1Builder AddTeletexString(String value) { /// /// Current instance with added value. public Asn1Builder AddVideotexString(String value) { - if (value == null) { + if (value is null) { throw new ArgumentNullException(nameof(value)); } _rawData.Add(Asn1Utils.Encode(Encoding.ASCII.GetBytes(value).AsSpan(), Asn1Type.VideotexString)); @@ -280,7 +273,7 @@ public Asn1Builder AddVideotexString(String value) { /// /// Current instance with added value. public Asn1Builder AddIA5String(String value) { - if (value == null) { + if (value is null) { throw new ArgumentNullException(nameof(value)); } _rawData.Add(new Asn1IA5String(value).GetRawDataAsMemory()); @@ -337,7 +330,7 @@ public Asn1Builder AddRfcDateTime(DateTime value) { /// /// Current instance with added value. public Asn1Builder AddVisibleString(String value) { - if (value == null) { + if (value is null) { throw new ArgumentNullException(nameof(value)); } _rawData.Add(new Asn1VisibleString(value).GetRawDataAsMemory()); @@ -354,7 +347,7 @@ public Asn1Builder AddVisibleString(String value) { /// /// Current instance with added value. public Asn1Builder AddUniversalString(String value) { - if (value == null) { + if (value is null) { throw new ArgumentNullException(nameof(value)); } _rawData.Add(new Asn1UniversalString(value).GetRawDataAsMemory()); @@ -371,7 +364,7 @@ public Asn1Builder AddUniversalString(String value) { /// /// Current instance with added value. public Asn1Builder AddBMPString(String value) { - if (value == null) { + if (value is null) { throw new ArgumentNullException(nameof(value)); } _rawData.Add(new Asn1BMPString(value).GetRawDataAsMemory()); @@ -424,9 +417,6 @@ public Asn1Builder AddDerData(ReadOnlyMemory value, Byte outerTag) { /// /// Specifies if data in value parameter must be encoded or not. See Remarks for more details. /// - /// - /// value parameter is null. - /// /// /// value is not encoded. /// @@ -440,9 +430,6 @@ public Asn1Builder AddDerData(ReadOnlyMemory value, Byte outerTag) { /// value parameter is untagged, an exception will be thrown. /// public Asn1Builder AddImplicit(Byte implicitTag, ReadOnlySpan value, Boolean mustEncode) { - if (value == null) { - throw new ArgumentNullException(nameof(value)); - } if (mustEncode) { _rawData.Add(Asn1Utils.Encode(value, (Byte)(0x80 + implicitTag))); } else { @@ -469,9 +456,6 @@ public Asn1Builder AddImplicit(Byte implicitTag, ReadOnlySpan value, Boole /// /// Specifies if data in value parameter must be encoded or not. See Remarks for more details. /// - /// - /// value parameter is null. - /// /// /// value is not encoded. /// @@ -484,9 +468,6 @@ public Asn1Builder AddImplicit(Byte implicitTag, ReadOnlySpan value, Boole /// is untagged, invalid type will be produced. /// public Asn1Builder AddExplicit(Byte explicitTag, ReadOnlySpan value, Boolean mustEncode) { - if (value == null) { - throw new ArgumentNullException(nameof(value)); - } if (mustEncode) { _rawData.Add(Asn1Utils.Encode(value, (Byte)(0xa0 + explicitTag))); } else { @@ -510,7 +491,7 @@ public Asn1Builder AddExplicit(Byte explicitTag, ReadOnlySpan value, Boole /// In the current implementation, constructed BIT_STRING is encoded using primitive form. /// public Asn1Builder AddBitString(Func selector) { - if (selector == null) { + if (selector is null) { throw new ArgumentNullException(nameof(selector)); } Asn1Builder b = selector(new Asn1Builder()); @@ -529,7 +510,7 @@ public Asn1Builder AddBitString(Func selector) { /// In the current implementation, constructed OCTET_STRING is encoded using primitive form. /// public Asn1Builder AddOctetString(Func selector) { - if (selector == null) { + if (selector is null) { throw new ArgumentNullException(nameof(selector)); } Asn1Builder b = selector(new Asn1Builder()); @@ -545,7 +526,7 @@ public Asn1Builder AddOctetString(Func selector) { /// /// Current instance with added value. public Asn1Builder AddSequence(Func selector) { - if (selector == null) { + if (selector is null) { throw new ArgumentNullException(nameof(selector)); } Asn1Builder b = selector(new Asn1Builder()); @@ -561,7 +542,7 @@ public Asn1Builder AddSequence(Func selector) { /// /// Current instance with added value. public Asn1Builder AddSet(Func selector) { - if (selector == null) { + if (selector is null) { throw new ArgumentNullException(nameof(selector)); } Asn1Builder b = selector(new Asn1Builder()); @@ -580,7 +561,7 @@ public Asn1Builder AddSet(Func selector) { /// /// Current instance with added value. public Asn1Builder AddExplicit(Byte explicitTag, Func selector) { - if (selector == null) { + if (selector is null) { throw new ArgumentNullException(nameof(selector)); } Asn1Builder b = selector(new Asn1Builder()); diff --git a/Asn1Parser/Asn1Reader.cs b/Asn1Parser/Asn1Reader.cs index f63bcf2..6f74d4b 100644 --- a/Asn1Parser/Asn1Reader.cs +++ b/Asn1Parser/Asn1Reader.cs @@ -297,7 +297,7 @@ Int64 calculatePredictLength(Int64 offset) { return pPayloadLength + lengthBytes + 2; } void moveAndExpectTypes(Func action, params Byte[] expectedTypes) { - if (expectedTypes == null) { + if (expectedTypes is null) { throw new ArgumentNullException(nameof(expectedTypes)); } var set = new HashSet(); @@ -537,9 +537,6 @@ public void MoveNextSiblingAndExpectTags(params Asn1Type[] expectedTags) { /// method calls are not necessary. /// public Boolean Seek(Int32 newPosition) { - if (_offsetMap == null) { - throw new InvalidOperationException(); - } if (!_offsetMap.TryGetValue(newPosition, out AsnInternalMap value)) { return false; } diff --git a/Asn1Parser/Asn1Utils.cs b/Asn1Parser/Asn1Utils.cs index e8e13bb..233b73d 100644 --- a/Asn1Parser/Asn1Utils.cs +++ b/Asn1Parser/Asn1Utils.cs @@ -51,17 +51,11 @@ public static Byte[] GetLengthBytes(Int32 payloadLength) { /// Calculates the ASN.1 payload length from a given ASN.1 length header. /// /// A byte array that represents ASN.1 length header - /// - /// asnHeader parameter is null. - /// /// /// asnHeader parameter length is more than 4 bytes or is invalid value. /// /// ASN.1 payload length in bytes. public static Int64 CalculatePayloadLength(ReadOnlySpan asnHeader) { - if (asnHeader == null) { - throw new ArgumentNullException(nameof(asnHeader)); - } if (asnHeader.Length == 0) { return 0; } @@ -113,7 +107,7 @@ public static Byte[] Encode(Byte[] rawData, Byte enclosingTag) { /// If rawData is null, an empty tag is encoded. public static ReadOnlyMemory Encode(ReadOnlySpan rawData, Byte enclosingTag) { Byte[] retValue; - if (rawData == null) { + if (rawData.Length == 0) { retValue = [enclosingTag, 0]; return retValue; diff --git a/Asn1Parser/AsnFormatter.cs b/Asn1Parser/AsnFormatter.cs index c5e9fa3..7a1f7a1 100644 --- a/Asn1Parser/AsnFormatter.cs +++ b/Asn1Parser/AsnFormatter.cs @@ -81,7 +81,7 @@ public static String BinaryToString(ReadOnlySpan rawData, EncodingType enc /// /// public static String BinaryToString(Asn1Reader asn, EncodingType encoding = EncodingType.HexRaw, EncodingFormat format = EncodingFormat.CRLF, Boolean forceUpperCase = false) { - if (asn == null) { + if (asn is null) { throw new ArgumentNullException(nameof(asn)); } @@ -127,7 +127,7 @@ public static Byte[] StringToBinary(String input, EncodingType encoding = Encodi } - if (rawData == null) { + if (rawData is null) { throw new InvalidDataException("The data is invalid."); } return rawData; @@ -143,31 +143,31 @@ public static EncodingType TestInputString(String input) { Byte[]? rawBytes; foreach (PemHeader pemHeader in PemHeader.GetPemHeaders()) { rawBytes = StringToBinaryFormatter.FromBase64Header(input, pemHeader.GetHeader(), pemHeader.GetFooter()); - if (rawBytes != null) { + if (rawBytes is not null) { return pemHeader.Encoding; } } rawBytes = StringToBinaryFormatter.FromBase64Header(input); - if (rawBytes != null) { + if (rawBytes is not null) { return EncodingType.Base64Header; } rawBytes = StringToBinaryFormatter.FromBase64(input); - if (rawBytes != null) { + if (rawBytes is not null) { return EncodingType.Base64; } rawBytes = StringToBinaryFormatter.FromHexAddr(input); - if (rawBytes != null) { + if (rawBytes is not null) { return EncodingType.HexAddress; } rawBytes = StringToBinaryFormatter.FromHexAddrAscii(input); - if (rawBytes != null) { + if (rawBytes is not null) { return EncodingType.HexAsciiAddress; } rawBytes = StringToBinaryFormatter.FromHex(input); - if (rawBytes != null) { + if (rawBytes is not null) { return EncodingType.Hex; } rawBytes = StringToBinaryFormatter.FromHexAscii(input); - return rawBytes != null ? EncodingType.HexAscii : EncodingType.Binary; + return rawBytes is not null ? EncodingType.HexAscii : EncodingType.Binary; } } \ No newline at end of file diff --git a/Asn1Parser/Universal/Asn1BitString.cs b/Asn1Parser/Universal/Asn1BitString.cs index de74ee6..cbaf172 100644 --- a/Asn1Parser/Universal/Asn1BitString.cs +++ b/Asn1Parser/Universal/Asn1BitString.cs @@ -38,11 +38,7 @@ public Asn1BitString(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData /// /// True if the bit length is decremented to exclude trailing zero bits. Otherwise False. /// - /// valueToEncode parameter is null reference. public Asn1BitString(ReadOnlySpan valueToEncode, Boolean calculateUnusedBits) : base(TYPE) { - if (valueToEncode == null) { - throw new ArgumentNullException(nameof(valueToEncode)); - } m_encode(valueToEncode, calculateUnusedBits, 0); } /// @@ -51,9 +47,7 @@ public Asn1BitString(ReadOnlySpan valueToEncode, Boolean calculateUnusedBi /// /// Raw value to encode. /// A number of unused bits in bit string. - /// valueToEncode parameter is null reference. public Asn1BitString(ReadOnlySpan valueToEncode, Byte unusedBits) : base(TYPE) { - if (valueToEncode == null) { throw new ArgumentNullException(nameof(valueToEncode)); } m_encode(valueToEncode, false, unusedBits); } @@ -105,10 +99,6 @@ public ReadOnlyMemory GetValue() { /// The number of unused bits. /// bytes parameter is null reference. public static Byte CalculateUnusedBits(ReadOnlySpan bytes) { - if (bytes == null) { - throw new ArgumentNullException(nameof(bytes)); - } - return CalculateUnusedBits(bytes[bytes.Length - 1]); // calculate unused bits based on last byte. } /// diff --git a/Asn1Parser/Universal/Asn1DateTime.cs b/Asn1Parser/Universal/Asn1DateTime.cs index 2943d42..1321333 100644 --- a/Asn1Parser/Universal/Asn1DateTime.cs +++ b/Asn1Parser/Universal/Asn1DateTime.cs @@ -43,7 +43,7 @@ protected Asn1DateTime(Asn1Type type, DateTime time, TimeZoneInfo? zone = null, void m_encode(Asn1Type type, DateTime time, TimeZoneInfo? zone, Boolean preciseTime) { zone = DateTimeUtils.CoerceTimeZone(zone); - time = zone == null + time = zone is null ? DateTime.SpecifyKind(time, DateTimeKind.Local) : TimeZoneInfo.ConvertTimeToUtc(time, zone).ToLocalTime(); Value = time; diff --git a/Asn1Parser/Universal/Asn1ObjectIdentifier.cs b/Asn1Parser/Universal/Asn1ObjectIdentifier.cs index 922819a..5ac90e0 100644 --- a/Asn1Parser/Universal/Asn1ObjectIdentifier.cs +++ b/Asn1Parser/Universal/Asn1ObjectIdentifier.cs @@ -50,7 +50,7 @@ public Asn1ObjectIdentifier(String oid) : this(new Oid(oid)) { } /// The string is not valid object identifier. /// The string is too large. public Asn1ObjectIdentifier(Oid oid) : base(TYPE) { - if (oid == null) { + if (oid is null) { throw new ArgumentNullException(nameof(oid)); } m_encode(oid); diff --git a/Asn1Parser/Universal/Asn1RelativeOid.cs b/Asn1Parser/Universal/Asn1RelativeOid.cs index ee6634d..ce7da22 100644 --- a/Asn1Parser/Universal/Asn1RelativeOid.cs +++ b/Asn1Parser/Universal/Asn1RelativeOid.cs @@ -41,7 +41,7 @@ public Asn1RelativeOid(ReadOnlyMemory rawData) : this(new Asn1Reader(rawDa /// The string is too large. /// Maximum relative object identifier string is 8kb. public Asn1RelativeOid(String relativeOid) : base(TYPE) { - if (relativeOid == null) { + if (relativeOid is null) { throw new ArgumentNullException(nameof(relativeOid)); } m_encode(relativeOid); diff --git a/Asn1Parser/Universal/Asn1String.cs b/Asn1Parser/Universal/Asn1String.cs index 847f7e8..cacfc45 100644 --- a/Asn1Parser/Universal/Asn1String.cs +++ b/Asn1Parser/Universal/Asn1String.cs @@ -89,7 +89,7 @@ public static Asn1String DecodeAnyString(ReadOnlyMemory rawData, IEnumerab } IEnumerable asn1Types = allowedStringTypes?.ToList(); - if (asn1Types != null && !asn1Types.Contains((Asn1Type)rawData.Span[0])) { + if (asn1Types is not null && !asn1Types.Contains((Asn1Type)rawData.Span[0])) { throw new ArgumentException("Input string is not permitted by restriction."); } var tag = (Asn1Type)(rawData.Span[0] & (Int32)Asn1Type.TAG_MASK); diff --git a/Asn1Parser/Universal/Asn1Universal.cs b/Asn1Parser/Universal/Asn1Universal.cs index 362262c..98c672f 100644 --- a/Asn1Parser/Universal/Asn1Universal.cs +++ b/Asn1Parser/Universal/Asn1Universal.cs @@ -23,7 +23,7 @@ protected Asn1Universal(Asn1Type type) { /// ASN.1 type. /// asn parameter is null reference. protected Asn1Universal(Asn1Reader asn, Asn1Type? type) { - if (asn == null) { + if (asn is null) { throw new ArgumentNullException(nameof(asn)); } if (type.HasValue && asn.Tag != (Byte)type.Value) { @@ -110,7 +110,7 @@ protected Asn1Reader GetInternalReader() { /// /// Decoded type value. public virtual String GetDisplayValue() { - return asnReader == null + return asnReader is null ? String.Empty : AsnFormatter.BinaryToString(asnReader, EncodingType.HexRaw, EncodingFormat.NOCRLF); } @@ -120,7 +120,7 @@ public virtual String GetDisplayValue() { /// Specifies the output encoding. /// Encoded text value. public virtual String Format(EncodingType encoding = EncodingType.Base64) { - return asnReader == null + return asnReader is null ? String.Empty : AsnFormatter.BinaryToString(asnReader, encoding); } diff --git a/Asn1Parser/Utils/DateTimeUtils.cs b/Asn1Parser/Utils/DateTimeUtils.cs index aef9014..704c255 100644 --- a/Asn1Parser/Utils/DateTimeUtils.cs +++ b/Asn1Parser/Utils/DateTimeUtils.cs @@ -20,7 +20,7 @@ public static ReadOnlySpan Encode(DateTime time, ref TimeZoneInfo? zone, B suffix += (time.Millisecond / 1000d).ToString(CultureInfo.InvariantCulture).Substring(1); } zone = CoerceTimeZone(zone); - if (zone == null) { + if (zone is null) { preValue = time.ToUniversalTime().ToString(format) + suffix + "Z"; } else { suffix += zone.BaseUtcOffset is { Hours: >= 0, Minutes: >= 0 } diff --git a/tests/Asn1Parser.Tests/Asn1DateTimeTests.cs b/tests/Asn1Parser.Tests/Asn1DateTimeTests.cs index 3ba6e7a..8981be6 100644 --- a/tests/Asn1Parser.Tests/Asn1DateTimeTests.cs +++ b/tests/Asn1Parser.Tests/Asn1DateTimeTests.cs @@ -67,12 +67,12 @@ static void assertDateTimeEncode(Asn1Type expectedType, Asn1DateTime adt, DateTi String gts = Encoding.ASCII.GetString(adt.GetRawDataAsMemory()[2..].ToArray()); Assert.AreEqual(dt, adt.Value); - if (adt.ZoneInfo == null) { + if (adt.ZoneInfo is null) { dt = dt.ToUniversalTime(); } Assert.AreEqual(dt.ToString(expectedFormat), gts); if (!decode) { - if (adt.ZoneInfo == null) { + if (adt.ZoneInfo is null) { dt = dt.ToLocalTime(); } assertDateTimeDecode(expectedType, adt, dt, expectedFormat); From 0979941e62779857452547b6fcb085f0971259dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Wed, 26 Feb 2025 11:34:18 +0200 Subject: [PATCH 43/66] adjusted null syntax --- Asn1Parser/Asn1InvalidTagException.cs | 1 + Asn1Parser/Asn1Reader.cs | 2 +- Asn1Parser/Asn1Utils.cs | 5 +++-- Asn1Parser/AsnFormatter.cs | 2 +- Asn1Parser/Universal/Asn1DateTime.cs | 2 +- Asn1Parser/Universal/Asn1ObjectIdentifier.cs | 2 +- Asn1Parser/Universal/Asn1String.cs | 2 +- 7 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Asn1Parser/Asn1InvalidTagException.cs b/Asn1Parser/Asn1InvalidTagException.cs index 0deb5df..8cb1db9 100644 --- a/Asn1Parser/Asn1InvalidTagException.cs +++ b/Asn1Parser/Asn1InvalidTagException.cs @@ -39,6 +39,7 @@ public Asn1InvalidTagException(String message, Exception innerException) : base( /// The object that holds the serialized object data. /// The contextual information about the source or destination. /// This constructor is called during deserialization to reconstitute the exception object transmitted over a stream. + [Obsolete("This overload is obsolete by SYSLIB0051.", true)] public Asn1InvalidTagException(SerializationInfo info, StreamingContext context) : base(info, context) { } /// /// Gets the offset at which invalid ASN tag appear. diff --git a/Asn1Parser/Asn1Reader.cs b/Asn1Parser/Asn1Reader.cs index 6f74d4b..c14174c 100644 --- a/Asn1Parser/Asn1Reader.cs +++ b/Asn1Parser/Asn1Reader.cs @@ -537,7 +537,7 @@ public void MoveNextSiblingAndExpectTags(params Asn1Type[] expectedTags) { /// method calls are not necessary. /// public Boolean Seek(Int32 newPosition) { - if (!_offsetMap.TryGetValue(newPosition, out AsnInternalMap value)) { + if (!_offsetMap.TryGetValue(newPosition, out AsnInternalMap? value)) { return false; } currentPosition = value; diff --git a/Asn1Parser/Asn1Utils.cs b/Asn1Parser/Asn1Utils.cs index 233b73d..cbbdba2 100644 --- a/Asn1Parser/Asn1Utils.cs +++ b/Asn1Parser/Asn1Utils.cs @@ -187,10 +187,11 @@ public static Asn1Reader EncodeAsReader(ReadOnlySpan rawData, Byte enclosi #region internal public static String GetViewValue(Asn1Reader asn) { - if (asn.PayloadLength == 0 && asn.Tag != (Byte)Asn1Type.NULL) { return "NULL"; } + if (asn.PayloadLength == 0 && asn.Tag != (Byte)Asn1Type.NULL) { + return "NULL"; + } return asn.Tag switch { - (Byte)Asn1Type.BOOLEAN => new Asn1Boolean(asn).Value.ToString(), (Byte)Asn1Type.INTEGER => new Asn1Integer(asn).Value.ToString(), (Byte)Asn1Type.BIT_STRING => decodeBitString(asn), diff --git a/Asn1Parser/AsnFormatter.cs b/Asn1Parser/AsnFormatter.cs index 7a1f7a1..d1b53fa 100644 --- a/Asn1Parser/AsnFormatter.cs +++ b/Asn1Parser/AsnFormatter.cs @@ -105,7 +105,7 @@ public static String BinaryToString(Asn1Reader asn, EncodingType encoding = Enco /// /// public static Byte[] StringToBinary(String input, EncodingType encoding = EncodingType.Base64) { - Byte[] rawData; + Byte[]? rawData; if (PemHeader.ContainsEncoding(encoding)) { var pemHeader = PemHeader.GetHeader(encoding); rawData = StringToBinaryFormatter.FromBase64Header(input, pemHeader.GetHeader(), pemHeader.GetFooter()); diff --git a/Asn1Parser/Universal/Asn1DateTime.cs b/Asn1Parser/Universal/Asn1DateTime.cs index 1321333..5077bd0 100644 --- a/Asn1Parser/Universal/Asn1DateTime.cs +++ b/Asn1Parser/Universal/Asn1DateTime.cs @@ -54,7 +54,7 @@ void m_encode(Asn1Type type, DateTime time, TimeZoneInfo? zone, Boolean preciseT void m_decode(ReadOnlyMemory rawData) { var asn = new Asn1Reader(rawData); Initialize(asn); - Value = DateTimeUtils.Decode(asn, out TimeZoneInfo zoneInfo); + Value = DateTimeUtils.Decode(asn, out TimeZoneInfo? zoneInfo); ZoneInfo = zoneInfo; } diff --git a/Asn1Parser/Universal/Asn1ObjectIdentifier.cs b/Asn1Parser/Universal/Asn1ObjectIdentifier.cs index 5ac90e0..7001cf1 100644 --- a/Asn1Parser/Universal/Asn1ObjectIdentifier.cs +++ b/Asn1Parser/Universal/Asn1ObjectIdentifier.cs @@ -170,7 +170,7 @@ static Boolean validateOidString(String oid, out List tokens) { /// public override String GetDisplayValue() { return String.IsNullOrEmpty(Value.FriendlyName) - ? Value.Value + ? Value.Value! : $"{Value.FriendlyName} ({Value.Value})"; } } \ No newline at end of file diff --git a/Asn1Parser/Universal/Asn1String.cs b/Asn1Parser/Universal/Asn1String.cs index cacfc45..2efd522 100644 --- a/Asn1Parser/Universal/Asn1String.cs +++ b/Asn1Parser/Universal/Asn1String.cs @@ -88,7 +88,7 @@ public static Asn1String DecodeAnyString(ReadOnlyMemory rawData, IEnumerab throw new ArgumentException("Raw data must have at least tag (1 byte) and length components (1 byte) in TLV structure."); } - IEnumerable asn1Types = allowedStringTypes?.ToList(); + IEnumerable? asn1Types = allowedStringTypes?.ToList(); if (asn1Types is not null && !asn1Types.Contains((Asn1Type)rawData.Span[0])) { throw new ArgumentException("Input string is not permitted by restriction."); } From 1938428d225bd6e34bb79519476673968a9c5329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Wed, 26 Feb 2025 11:50:55 +0200 Subject: [PATCH 44/66] fixed raw data source --- Asn1Parser/Asn1Reader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Asn1Parser/Asn1Reader.cs b/Asn1Parser/Asn1Reader.cs index c14174c..ba38a6b 100644 --- a/Asn1Parser/Asn1Reader.cs +++ b/Asn1Parser/Asn1Reader.cs @@ -586,7 +586,7 @@ public Asn1Universal GetTagObject() { /// /// A new instance of . public Asn1Reader GetReader() { - return new Asn1Reader(GetRawDataAsMemory(), 0, true); + return new Asn1Reader(GetTagRawDataAsMemory(), 0, true); } /// /// Recursively processes ASN tree and builds internal offset map. From 5ce482bfc6a2b113b211dc0d470046eafda5d4c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Wed, 26 Feb 2025 11:51:24 +0200 Subject: [PATCH 45/66] clarified the meaning of Asn1Reader.GetReader() method --- Asn1Parser/Asn1Reader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Asn1Parser/Asn1Reader.cs b/Asn1Parser/Asn1Reader.cs index ba38a6b..827f090 100644 --- a/Asn1Parser/Asn1Reader.cs +++ b/Asn1Parser/Asn1Reader.cs @@ -582,7 +582,7 @@ public Asn1Universal GetTagObject() { }; } /// - /// Returns a new instance of that points to current tag. This method does not allocate extra memory. + /// Returns a new instance of that is sourced from the current tag. /// /// A new instance of . public Asn1Reader GetReader() { From 071f4c46259a9071532bbdf83fbeebb9afa04c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Wed, 26 Feb 2025 11:51:43 +0200 Subject: [PATCH 46/66] added Asn1Reader.Clone() method --- Asn1Parser/Asn1Reader.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Asn1Parser/Asn1Reader.cs b/Asn1Parser/Asn1Reader.cs index 827f090..f147e9d 100644 --- a/Asn1Parser/Asn1Reader.cs +++ b/Asn1Parser/Asn1Reader.cs @@ -589,6 +589,29 @@ public Asn1Reader GetReader() { return new Asn1Reader(GetTagRawDataAsMemory(), 0, true); } /// + /// Returns a cloned instance of current object and current state. + /// + /// A cloned instance of . + public Asn1Reader Clone() { + var reader = new Asn1Reader(GetRawDataAsMemory(), 0, true) { + currentPosition = new AsnInternalMap(currentPosition.LevelStart, currentPosition.LevelEnd), + childCount = childCount, + Tag = Tag, + TagName = TagName, + TagLength = TagLength, + PayloadStartOffset = PayloadStartOffset, + PayloadLength = PayloadLength, + NextSiblingOffset = NextSiblingOffset, + NextOffset = NextOffset, + IsConstructed = IsConstructed + }; + foreach (KeyValuePair mapEntry in _offsetMap) { + reader._offsetMap.Add(mapEntry.Key, mapEntry.Value); + } + + return reader; + } + /// /// Recursively processes ASN tree and builds internal offset map. /// /// A number of processed ASN structures. From 7b81f7fd406bcaeb802216c0d04adf6a4ca04f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Wed, 26 Feb 2025 11:59:13 +0200 Subject: [PATCH 47/66] optimized Asn1Reader.Clone() method --- Asn1Parser/Asn1Reader.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Asn1Parser/Asn1Reader.cs b/Asn1Parser/Asn1Reader.cs index f147e9d..f0a4c44 100644 --- a/Asn1Parser/Asn1Reader.cs +++ b/Asn1Parser/Asn1Reader.cs @@ -27,7 +27,7 @@ public class Asn1Reader { readonly Dictionary _offsetMap = []; AsnInternalMap currentPosition; Int32 childCount; - + /// /// Initializes a new instance of the ASN1 class from an existing /// ASN1 object. @@ -59,6 +59,12 @@ public Asn1Reader(ReadOnlyMemory rawData) : this(rawData, 0) { } _offsetMap.Add(0, currentPosition); decode(rawData, offset, skipCopy); } + // this constructor is used for clone purposes only. + Asn1Reader(ReadOnlyMemory rawData, AsnInternalMap position, Int32 childCount) { + _rawData = rawData; + currentPosition = new AsnInternalMap(position.LevelStart, position.LevelEnd); + this.childCount = childCount; + } /// /// Gets current position in the byte array stored in current data source. @@ -593,9 +599,7 @@ public Asn1Reader GetReader() { /// /// A cloned instance of . public Asn1Reader Clone() { - var reader = new Asn1Reader(GetRawDataAsMemory(), 0, true) { - currentPosition = new AsnInternalMap(currentPosition.LevelStart, currentPosition.LevelEnd), - childCount = childCount, + var reader = new Asn1Reader(_rawData, currentPosition, childCount) { Tag = Tag, TagName = TagName, TagLength = TagLength, From 95d2df60cffbd92ec4780ec7fb03416552421fcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Wed, 26 Feb 2025 12:38:10 +0200 Subject: [PATCH 48/66] updated .nuspec file --- nuget/SysadminsLV.Asn1Parser.nuspec | 46 ++++++++++++++++------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/nuget/SysadminsLV.Asn1Parser.nuspec b/nuget/SysadminsLV.Asn1Parser.nuspec index 471c119..286367f 100644 --- a/nuget/SysadminsLV.Asn1Parser.nuspec +++ b/nuget/SysadminsLV.Asn1Parser.nuspec @@ -1,24 +1,28 @@ - - SysadminsLV.Asn1Parser - $version$ - Vadims Podans, PKI Solutions LLC - false - MS-PL - - https://github.com/PKISolutions/Asn1DerParser.NET - ASN.1 binary parser and utility set for binary data encoded with Distinguished Encoding Rules (DER). - None. - Copyright © Sysadmins LV 2012-$year$ - ASN.1 ASN ASN1 - - - - - - - - - + + SysadminsLV.Asn1Parser + $version$ + Vadims Podans, PKI Solutions LLC + false + MS-PL + + https://github.com/PKISolutions/Asn1DerParser.NET + ASN.1 binary parser and utility set for binary data encoded with Distinguished Encoding Rules (DER). + None. + Copyright © Sysadmins LV 2012-$year$ + ASN.1 ASN ASN1 + + + + + + + + + + + + + From af7b40b61ff5da69d0967f446bee1c57b90ebaf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Wed, 26 Feb 2025 12:48:54 +0200 Subject: [PATCH 49/66] shortened .NET Framework version --- nuget/SysadminsLV.Asn1Parser.nuspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nuget/SysadminsLV.Asn1Parser.nuspec b/nuget/SysadminsLV.Asn1Parser.nuspec index 286367f..bd43450 100644 --- a/nuget/SysadminsLV.Asn1Parser.nuspec +++ b/nuget/SysadminsLV.Asn1Parser.nuspec @@ -13,7 +13,7 @@ Copyright © Sysadmins LV 2012-$year$ ASN.1 ASN ASN1 - + From 4d35b7e8a4b263c88f61bdcdf61d7208bdfc6e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Wed, 26 Feb 2025 13:28:19 +0200 Subject: [PATCH 50/66] bump to v2.0.0 --- Asn1Parser/Asn1Parser.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Asn1Parser/Asn1Parser.csproj b/Asn1Parser/Asn1Parser.csproj index e4e0267..47ffc50 100644 --- a/Asn1Parser/Asn1Parser.csproj +++ b/Asn1Parser/Asn1Parser.csproj @@ -2,7 +2,7 @@ net8.0;net472 latest - 1.3.0 + 2.0.0 SysadminsLV.Asn1Parser SysadminsLV.Asn1Parser From 2d5a638607784fa877b6d4f6263f32fdeb44f8e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Fri, 28 Feb 2025 13:12:39 +0200 Subject: [PATCH 51/66] removed Asn1Builder.Create(IEnumerable) method --- Asn1Parser/Asn1Builder.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Asn1Parser/Asn1Builder.cs b/Asn1Parser/Asn1Builder.cs index 6d8f15a..7a17efa 100644 --- a/Asn1Parser/Asn1Builder.cs +++ b/Asn1Parser/Asn1Builder.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Numerics; using System.Security.Cryptography; using System.Text; @@ -676,14 +675,6 @@ public static Asn1Builder Create() { /// /// ASN.1-encoded data to initialize the builder from. /// ASN.1 Builder. - public static Asn1Builder Create(IEnumerable rawData) { - return Create(rawData.ToArray().AsSpan()); - } - /// - /// Creates a default instance of Asn1Builder class from existing ASN.1-encoded data. - /// - /// ASN.1-encoded data to initialize the builder from. - /// ASN.1 Builder. public static Asn1Builder Create(ReadOnlySpan rawData) { var builder = new Asn1Builder(); builder._rawData.Add(new ReadOnlyMemory(rawData.ToArray())); From 53ff12617f32614a2423eb4566543644774be7f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Mon, 3 Mar 2025 11:18:39 +0200 Subject: [PATCH 52/66] fixed ASN.1 builder encoder bugs --- Asn1Parser/Asn1Builder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Asn1Parser/Asn1Builder.cs b/Asn1Parser/Asn1Builder.cs index 7a17efa..b53b7ee 100644 --- a/Asn1Parser/Asn1Builder.cs +++ b/Asn1Parser/Asn1Builder.cs @@ -597,16 +597,16 @@ Byte[] getEncoded(Byte outerTag, Boolean includeHeader) { Byte[]? header = null; if (includeHeader) { header = Asn1Utils.GetLengthBytes(payloadLength); - headerLength = header.Length; + headerLength = header.Length + 1; } Byte[] memory = new Byte[headerLength + payloadLength]; Int32 i = 0; if (includeHeader) { memory[0] = outerTag; - i = 1; for (; i < header!.Length; i++) { - memory[i] = header[i]; + memory[i + 1] = header[i]; } + i++; } foreach (ReadOnlyMemory chunk in _rawData) { From b2de43cba735c0de3c15ba6666450f85dc331cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Mon, 3 Mar 2025 11:35:56 +0200 Subject: [PATCH 53/66] fixed bug in Asn1BitString.GetValue() which returns unused bit byte --- Asn1Parser/Universal/Asn1BitString.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Asn1Parser/Universal/Asn1BitString.cs b/Asn1Parser/Universal/Asn1BitString.cs index cbaf172..dcec6bc 100644 --- a/Asn1Parser/Universal/Asn1BitString.cs +++ b/Asn1Parser/Universal/Asn1BitString.cs @@ -89,7 +89,7 @@ public override String GetDisplayValue() { /// /// BIT_STRING value. public ReadOnlyMemory GetValue() { - return GetInternalReader().GetPayloadAsMemory(); + return GetInternalReader().GetPayloadAsMemory().Slice(1); } /// From 9c09c8b72727fb299139f57195bfd70b1d26dedd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Mon, 3 Mar 2025 12:34:26 +0200 Subject: [PATCH 54/66] fixed bug when Convert.ToUInt64 fails to convert BigInteger, because Convert.ToUInt64 expects IConvertible which isn't implemented by BigInteger. --- Asn1Parser/Universal/Asn1Enumerated.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Asn1Parser/Universal/Asn1Enumerated.cs b/Asn1Parser/Universal/Asn1Enumerated.cs index 78fc38b..96cd5bc 100644 --- a/Asn1Parser/Universal/Asn1Enumerated.cs +++ b/Asn1Parser/Universal/Asn1Enumerated.cs @@ -53,6 +53,6 @@ void m_decode(Asn1Reader asn) { if (value > UInt64.MaxValue) { throw new InvalidDataException(String.Format(InvalidType, TYPE.ToString())); } - Value = Convert.ToUInt64(value); + Value = (UInt64)value; } } \ No newline at end of file From 9c1e59ebe04a385d3b64a26a16d9492633c47ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Mon, 3 Mar 2025 13:37:42 +0200 Subject: [PATCH 55/66] added missing default value for Asn1Builder.GetEncodedAsMemory() method which defaults to Constructed SEQUENCE --- Asn1Parser/Asn1Builder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Asn1Parser/Asn1Builder.cs b/Asn1Parser/Asn1Builder.cs index b53b7ee..092be1c 100644 --- a/Asn1Parser/Asn1Builder.cs +++ b/Asn1Parser/Asn1Builder.cs @@ -641,7 +641,7 @@ public Byte[] GetEncoded(Byte outerTag = 0x30) { /// /// ASN.1-encoded memory. /// - public ReadOnlyMemory GetEncodedAsMemory(Byte outerTag) { + public ReadOnlyMemory GetEncodedAsMemory(Byte outerTag = 0x30) { return getEncoded(outerTag, true); } /// From 00275fde830215a3cd33c1eed8f726156c447d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Mon, 3 Mar 2025 13:39:07 +0200 Subject: [PATCH 56/66] added some unit tests for Asn1Builder class --- .../Asn1BuilderContextSpecificTests.cs | 33 ++++++ .../Builder/Asn1BuilderTestBase.cs | 90 ++++++++++++++++ .../Builder/Asn1BuilderTests.cs | 102 ++++++++++++++++++ 3 files changed, 225 insertions(+) create mode 100644 tests/Asn1Parser.Tests/Builder/Asn1BuilderContextSpecificTests.cs create mode 100644 tests/Asn1Parser.Tests/Builder/Asn1BuilderTestBase.cs create mode 100644 tests/Asn1Parser.Tests/Builder/Asn1BuilderTests.cs diff --git a/tests/Asn1Parser.Tests/Builder/Asn1BuilderContextSpecificTests.cs b/tests/Asn1Parser.Tests/Builder/Asn1BuilderContextSpecificTests.cs new file mode 100644 index 0000000..3280524 --- /dev/null +++ b/tests/Asn1Parser.Tests/Builder/Asn1BuilderContextSpecificTests.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SysadminsLV.Asn1Parser; +using SysadminsLV.Asn1Parser.Universal; + +namespace Asn1Parser.Tests.Builder; + +[TestClass] +public class Asn1BuilderContextSpecificTests { + static Asn1Builder BuildTestContextSpecific() { + return Asn1Builder.Create() + .AddImplicit(0, [1, 2], true) + .AddImplicit(1, new Asn1IA5String("a").GetRawDataAsMemory().Span, false) + .AddExplicit(0, builder => builder.AddInteger(5)) + .AddExplicit(1, new Asn1Integer(5).GetRawDataAsMemory().Span, true) + .AddExplicit(2, [48, 3, 2, 1, 5], false); + } + + [TestMethod] + public void TestContextSpecific() { + Asn1Builder builder = BuildTestContextSpecific(); + + ReadOnlyMemory encoded = builder.GetEncodedAsMemory(); + var reader = new Asn1Reader(encoded); + Assert.AreEqual(reader.Tag, 0x30); + + Asn1BuilderTestBase.AssertContextSpecific(reader, 0x80, [0x80, 2, 1, 2]); + Asn1BuilderTestBase.AssertContextSpecific(reader, 0x81, [0x81, 1, 0x61]); + Asn1BuilderTestBase.AssertContextSpecific(reader, 0xa0, [0xa0, 3, 2, 1, 5]); + Asn1BuilderTestBase.AssertContextSpecific(reader, 0xa1, [0xa1, 3, 2, 1, 5], useSibling: true); + Asn1BuilderTestBase.AssertContextSpecific(reader, 0xa2, [0xa2, 3, 2, 1, 5], useSibling: true); + } +} \ No newline at end of file diff --git a/tests/Asn1Parser.Tests/Builder/Asn1BuilderTestBase.cs b/tests/Asn1Parser.Tests/Builder/Asn1BuilderTestBase.cs new file mode 100644 index 0000000..403b23c --- /dev/null +++ b/tests/Asn1Parser.Tests/Builder/Asn1BuilderTestBase.cs @@ -0,0 +1,90 @@ +using System; +using System.Numerics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SysadminsLV.Asn1Parser; +using SysadminsLV.Asn1Parser.Universal; + +namespace Asn1Parser.Tests.Builder; + +public static class Asn1BuilderTestBase { + static void moveNext(Asn1Reader reader, Asn1Type type, Boolean useSibling) { + if (useSibling) { + reader.MoveNextSiblingAndExpectTags(type); + } else { + reader.MoveNextAndExpectTags(type); + } + } + + public static void AssertBoolean(Asn1Reader reader, Boolean expectedValue, Boolean useSibling = false) { + moveNext(reader, Asn1Type.BOOLEAN, useSibling); + var tag = (Asn1Boolean)reader.GetTagObject(); + Assert.AreEqual(expectedValue, tag.Value); + } + public static void AssertInteger(Asn1Reader reader, BigInteger expectedValue, Boolean useSibling = false) { + moveNext(reader, Asn1Type.INTEGER, useSibling); + var tag = (Asn1Integer)reader.GetTagObject(); + Assert.AreEqual(expectedValue, tag.Value); + } + public static void AssertBitStringSimple(Asn1Reader reader, ReadOnlySpan expectedValue, Byte expectedUnusedBits, Boolean useSibling = false) { + moveNext(reader, Asn1Type.BIT_STRING, useSibling); + var tag = (Asn1BitString)reader.GetTagObject(); + Assert.AreEqual(expectedUnusedBits, tag.UnusedBits); + Assert.IsTrue(expectedValue.SequenceEqual(tag.GetValue().Span)); + } + public static void AssertBitString(Asn1Reader reader, Boolean useSibling = false) { + moveNext(reader, Asn1Type.BIT_STRING, useSibling); + var tag = (Asn1BitString)reader.GetTagObject(); + Assert.AreEqual(0, tag.UnusedBits); + } + public static void AssertOctetString(Asn1Reader reader, ReadOnlySpan expectedValue, Boolean useSibling = false) { + moveNext(reader, Asn1Type.OCTET_STRING, useSibling); + var tag = (Asn1OctetString)reader.GetTagObject(); + Assert.IsTrue(expectedValue.SequenceEqual(tag.GetValue().Span)); + } + public static void AssertOctetString(Asn1Reader reader, Boolean useSibling = false) { + moveNext(reader, Asn1Type.OCTET_STRING, useSibling); + } + public static void AssertNull(Asn1Reader reader, Boolean useSibling = false) { + moveNext(reader, Asn1Type.NULL, useSibling); + var tag = (Asn1Null)reader.GetTagObject(); + Assert.AreEqual(2, tag.GetRawDataAsMemory().Length); + } + public static void AssertObjectIdentifier(Asn1Reader reader, String expectedValue, Boolean useSibling = false) { + moveNext(reader, Asn1Type.OBJECT_IDENTIFIER, useSibling); + var tag = (Asn1ObjectIdentifier)reader.GetTagObject(); + Assert.AreEqual(expectedValue, tag.Value.Value); + } + public static void AssertEnumerated(Asn1Reader reader, UInt64 expectedValue, Boolean useSibling = false) { + moveNext(reader, Asn1Type.ENUMERATED, useSibling); + var tag = (Asn1Enumerated)reader.GetTagObject(); + Assert.AreEqual(expectedValue, tag.Value); + } + public static void AssertString(Asn1Reader reader, Asn1Type type, String expectedValue, Boolean useSibling = false) { + moveNext(reader, type, useSibling); + var tag = (Asn1String)reader.GetTagObject(); + Assert.AreEqual(expectedValue, tag.Value); + } + public static void AssertRelativeOid(Asn1Reader reader, String expectedValue, Boolean useSibling = false) { + moveNext(reader, Asn1Type.RELATIVE_OID, useSibling); + var tag = (Asn1RelativeOid)reader.GetTagObject(); + Assert.AreEqual(expectedValue, tag.Value); + } + public static void AssertSequence(Asn1Reader reader, Boolean useSibling = false) { + moveNext(reader, Asn1Type.SEQUENCE | (Asn1Type)Asn1Class.CONSTRUCTED, useSibling); + } + public static void AssertSet(Asn1Reader reader, Boolean useSibling = false) { + moveNext(reader, Asn1Type.SET | (Asn1Type)Asn1Class.CONSTRUCTED, useSibling); + } + public static void AssertDateTime(Asn1Reader reader, DateTime expected, Boolean useUtc, Boolean useSibling = false) { + moveNext(reader, useUtc + ? Asn1Type.UTCTime + : Asn1Type.GeneralizedTime, useSibling); + var tag = (Asn1DateTime)reader.GetTagObject(); + Assert.AreEqual(expected, tag.Value); + } + public static void AssertContextSpecific(Asn1Reader reader, Byte expectedTag, ReadOnlySpan expectedBytes, Boolean useSibling = false) { + moveNext(reader, (Asn1Type)expectedTag, useSibling); + Asn1Universal tag = reader.GetTagObject(); + Assert.IsTrue(expectedBytes.SequenceEqual(tag.GetRawDataAsMemory().Span)); + } +} \ No newline at end of file diff --git a/tests/Asn1Parser.Tests/Builder/Asn1BuilderTests.cs b/tests/Asn1Parser.Tests/Builder/Asn1BuilderTests.cs new file mode 100644 index 0000000..218a7b0 --- /dev/null +++ b/tests/Asn1Parser.Tests/Builder/Asn1BuilderTests.cs @@ -0,0 +1,102 @@ +using System; +using System.Security.Cryptography; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SysadminsLV.Asn1Parser; +using SysadminsLV.Asn1Parser.Universal; + +namespace Asn1Parser.Tests.Builder; + +[TestClass] +public class Asn1BuilderTests { + const String EXPECTED_ENCODED = """ + MIIBGQEB/wEBAAIBBQMCBLADBAACAQEEAgECBAMCAQQFAAYIKwYBBQUHAwEKAQMM + ClVURjhTdHJpbmcNAQUwBgIBAQIBAjADAgEFMQYCAQECAQIxAwIBBRIHNTU1IDU1 + NRMPUHJpbnRhYmxlU3RyaW5nFA1UZWxldGV4U3RyaW5nFQ5WaWRlb3RleFN0cmlu + ZxYJSUE1U3RyaW5nFw0yNTAxMDExODAwMDBaGA8yMDI1MDEwMTE4MDAwMFoaDVZp + c2libGVTdHJpbmccPAAAAFUAAABuAAAAaQAAAHYAAABlAAAAcgAAAHMAAABhAAAA + bAAAAFMAAAB0AAAAcgAAAGkAAABuAAAAZx4SAEIATQBQAFMAdAByAGkAbgBn + """; + static Asn1Builder BuildTestSimple() { + return Asn1Builder.Create() + .AddBoolean(true) + .AddBoolean(false) + .AddInteger(5) + .AddBitString([0xb0], true) + .AddBitString(builder => builder.AddInteger(1)) + .AddOctetString(new Byte[] { 1, 2 }) + .AddOctetString(builder => builder.AddInteger(4)) + .AddNull() + .AddObjectIdentifier(new Oid("1.3.6.1.5.5.7.3.1")) + .AddEnumerated(3) + .AddUTF8String("UTF8String") + .AddRelativeOid(".5") + .AddSequence(builder => builder.AddInteger(1).AddInteger(2)) + .AddSequence(new Asn1Integer(5).GetRawDataAsMemory().Span) + .AddSet(builder => builder.AddInteger(1).AddInteger(2)) + .AddSet(new Asn1Integer(5).GetRawDataAsMemory().Span) + .AddNumericString("555 555") + .AddPrintableString("PrintableString") + .AddTeletexString("TeletexString") + .AddVideotexString("VideotexString") + .AddIA5String("IA5String") + .AddUtcTime(DateTime.Parse("2025-01-01 20:00:00")) + .AddGeneralizedTime(DateTime.Parse("2025-01-01 20:00:00")) + .AddVisibleString("VisibleString") + .AddUniversalString("UniversalString") + .AddBMPString("BMPString"); + } + + [TestMethod] + public void TestBuilderSimple() { + Asn1Builder builder = BuildTestSimple(); + ReadOnlyMemory encoded = builder.GetEncodedAsMemory(); + + String base64 = Convert.ToBase64String(encoded.Span); + Assert.AreEqual(EXPECTED_ENCODED.Replace("\r\n", null), base64.Replace("\r\n", null)); + + // read envelope + var reader = new Asn1Reader(encoded); + Assert.AreEqual(reader.Tag, 0x30); + + Asn1BuilderTestBase.AssertBoolean(reader, true); + Asn1BuilderTestBase.AssertBoolean(reader, false); + Asn1BuilderTestBase.AssertInteger(reader, 5); + Asn1BuilderTestBase.AssertBitStringSimple(reader, [0xb0], 4); + Asn1BuilderTestBase.AssertBitString(reader); + Asn1Reader nestedReader = reader.GetReader(); + Asn1BuilderTestBase.AssertInteger(nestedReader, 1); + Asn1BuilderTestBase.AssertOctetString(reader, [1, 2], useSibling: true); + Asn1BuilderTestBase.AssertOctetString(reader); + nestedReader = reader.GetReader(); + Asn1BuilderTestBase.AssertInteger(nestedReader, 4); + Asn1BuilderTestBase.AssertNull(reader, useSibling: true); + Asn1BuilderTestBase.AssertObjectIdentifier(reader, "1.3.6.1.5.5.7.3.1"); + Asn1BuilderTestBase.AssertEnumerated(reader, 3); + Asn1BuilderTestBase.AssertString(reader, Asn1Type.UTF8String, Asn1Type.UTF8String.ToString()); + Asn1BuilderTestBase.AssertRelativeOid(reader, ".5"); + Asn1BuilderTestBase.AssertSequence(reader); + nestedReader = reader.GetReader(); + Asn1BuilderTestBase.AssertInteger(nestedReader, 1); + Asn1BuilderTestBase.AssertInteger(nestedReader, 2); + Asn1BuilderTestBase.AssertSequence(reader, useSibling: true); + nestedReader = reader.GetReader(); + Asn1BuilderTestBase.AssertInteger(nestedReader, 5); + Asn1BuilderTestBase.AssertSet(reader, useSibling: true); + nestedReader = reader.GetReader(); + Asn1BuilderTestBase.AssertInteger(nestedReader, 1); + Asn1BuilderTestBase.AssertInteger(nestedReader, 2); + Asn1BuilderTestBase.AssertSet(reader, useSibling: true); + nestedReader = reader.GetReader(); + Asn1BuilderTestBase.AssertInteger(nestedReader, 5); + Asn1BuilderTestBase.AssertString(reader, Asn1Type.NumericString, "555 555", useSibling: true); + Asn1BuilderTestBase.AssertString(reader, Asn1Type.PrintableString, Asn1Type.PrintableString.ToString()); + Asn1BuilderTestBase.AssertString(reader, Asn1Type.TeletexString, Asn1Type.TeletexString.ToString()); + Asn1BuilderTestBase.AssertString(reader, Asn1Type.VideotexString, Asn1Type.VideotexString.ToString()); + Asn1BuilderTestBase.AssertString(reader, Asn1Type.IA5String, Asn1Type.IA5String.ToString()); + Asn1BuilderTestBase.AssertDateTime(reader, DateTime.Parse("2025-01-01 20:00:00"), true); + Asn1BuilderTestBase.AssertDateTime(reader, DateTime.Parse("2025-01-01 20:00:00"), false); + Asn1BuilderTestBase.AssertString(reader, Asn1Type.VisibleString, Asn1Type.VisibleString.ToString()); + Asn1BuilderTestBase.AssertString(reader, Asn1Type.UniversalString, Asn1Type.UniversalString.ToString()); + Asn1BuilderTestBase.AssertString(reader, Asn1Type.BMPString, Asn1Type.BMPString.ToString()); + } +} From 100dc87865e3bf0fcf997cf15c24f7b5b2c25757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Mon, 3 Mar 2025 17:54:36 +0200 Subject: [PATCH 57/66] added Asn1Universal.GetPayloadAsMemory() method. --- Asn1Parser/Universal/Asn1Universal.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Asn1Parser/Universal/Asn1Universal.cs b/Asn1Parser/Universal/Asn1Universal.cs index 98c672f..cca4292 100644 --- a/Asn1Parser/Universal/Asn1Universal.cs +++ b/Asn1Parser/Universal/Asn1Universal.cs @@ -139,4 +139,11 @@ public Byte[] GetRawData() { public ReadOnlyMemory GetRawDataAsMemory() { return asnReader!.GetTagRawDataAsMemory(); } + /// + /// Gets the memory buffer that holds tag payload, excluding tag and length bytes. + /// + /// Raw payload value excluding tag and length bytes. + public ReadOnlyMemory GetPayloadAsMemory() { + return asnReader!.GetPayloadAsMemory(); + } } \ No newline at end of file From 8ab6f2c2be36f00a77f6d611996133a297eab1e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Thu, 6 Mar 2025 13:11:55 +0200 Subject: [PATCH 58/66] Significantly reworked Base64 to string conversion which addresses #24 and clarifies the behavior of conversion rules and expectations. Added unit tests to cover `AsnFormatter.TestInputString` for Base64 encodings --- Asn1Parser/AsnFormatter.cs | 14 ++- Asn1Parser/StringToBinaryFormatter.cs | 115 +++++++++++++----- .../Base64StringToBinaryTests.cs | 35 +++++- 3 files changed, 128 insertions(+), 36 deletions(-) diff --git a/Asn1Parser/AsnFormatter.cs b/Asn1Parser/AsnFormatter.cs index d1b53fa..d0b2045 100644 --- a/Asn1Parser/AsnFormatter.cs +++ b/Asn1Parser/AsnFormatter.cs @@ -100,7 +100,7 @@ public static String BinaryToString(Asn1Reader asn, EncodingType encoding = Enco /// BinaryToString /// method. /// - /// If encoding parameter is set to Base64Header, the method will accept any properly formatted PEM header + /// If encoding parameter is set to Base64Header, the method will accept any PEM header /// and footer. /// /// @@ -108,10 +108,9 @@ public static Byte[] StringToBinary(String input, EncodingType encoding = Encodi Byte[]? rawData; if (PemHeader.ContainsEncoding(encoding)) { var pemHeader = PemHeader.GetHeader(encoding); - rawData = StringToBinaryFormatter.FromBase64Header(input, pemHeader.GetHeader(), pemHeader.GetFooter()); + rawData = StringToBinaryFormatter.FromBase64Header(input, pemHeader.Header); } else { rawData = encoding switch { - EncodingType.Binary => StringToBinaryFormatter.FromBinary(input), EncodingType.Base64 => StringToBinaryFormatter.FromBase64(input), EncodingType.Base64Any => StringToBinaryFormatter.FromBase64Any(input), @@ -139,15 +138,20 @@ public static Byte[] StringToBinary(String input, EncodingType encoding = Encodi /// /// Resolved input string format. If format cannot be determined, Binary type is returned. /// + /// + /// Method returns Base64Header when input string contains + /// -----BEGIN ...----- PEM header and -----END ...----- PEM footer which is not + /// supported by enumeration. + /// public static EncodingType TestInputString(String input) { Byte[]? rawBytes; foreach (PemHeader pemHeader in PemHeader.GetPemHeaders()) { - rawBytes = StringToBinaryFormatter.FromBase64Header(input, pemHeader.GetHeader(), pemHeader.GetFooter()); + rawBytes = StringToBinaryFormatter.FromBase64Header(input, pemHeader.Header); if (rawBytes is not null) { return pemHeader.Encoding; } } - rawBytes = StringToBinaryFormatter.FromBase64Header(input); + rawBytes = StringToBinaryFormatter.FromBase64Header(input, String.Empty, true); if (rawBytes is not null) { return EncodingType.Base64Header; } diff --git a/Asn1Parser/StringToBinaryFormatter.cs b/Asn1Parser/StringToBinaryFormatter.cs index 2ad0f77..39ec2f6 100644 --- a/Asn1Parser/StringToBinaryFormatter.cs +++ b/Asn1Parser/StringToBinaryFormatter.cs @@ -1,11 +1,16 @@ using System; using System.Collections.Generic; -using System.Linq; namespace SysadminsLV.Asn1Parser; static class StringToBinaryFormatter { static readonly Char[] _delimiters = [' ', '-', ':', '\t', '\n', '\r']; + /// + /// Converts the specified string, which encodes binary data as base-64 digits, to + /// an equivalent 8-bit unsigned integer array. + /// + /// The string to convert. + /// An array of 8-bit unsigned integers that is equivalent to input. public static Byte[]? FromBase64(String input) { try { return Convert.FromBase64String(input.Trim()); @@ -13,41 +18,75 @@ static class StringToBinaryFormatter { return null; } } - // accept any header, not only certificate - public static Byte[]? FromBase64Header(String input) { - const String header = "-----BEGIN "; - const String footer = "-----END "; - return FromBase64Header(input, header, footer, true); - } - public static Byte[]? FromBase64Header(String input, String header, String footer, Boolean skipHeaderValidation = false) { - if (skipHeaderValidation && (!input.ToUpper().Contains(header) || !input.Contains(footer))) { + /// + /// Attempts to convert the specified string, which encodes binary data as base-64 digits with PEM header and footer, + /// to an equivalent 8-bit unsigned integer array. + /// + /// The string to convert. + /// + /// Specifies the header name. This parameter MUS NOT include 5-dash sequence and BEGIN/END constants. + /// This parameter is ignored if skipHeaderValidation parameter is set to true. + /// + /// + /// Skips strict PEM header and footer check. By default, this method checks for exact PEM header and footer + /// specified in header parameter, which includes only name, without fixed (5-dash sequences, + /// BEGIN and END constants). + /// + /// When this parameter is set to true, then headerName parameter is ignored and method will + /// attempt to find any properly PEM formatted header and footer, even if PEM header and footer names don't match. + /// For example, the following sequence would also be considered valid: + /// + /// -----BEGIN CERTIFICATE----- + /// MIIDBjCCAm8CAQAwcTERMA8GA1UEAxMIcXV1eC5jb20xDzANBgNVBAsTBkJyYWlu + /// czEWMBQGA1UEChMNRGV2ZWxvcE1lbnRvcjERMA8GA1UEBxMIVG9ycmFuY2UxEzAR + /// BgNVBAgTCkNhbGlmb3JuaWExCzAJBgNVBAYTAlVTMIGfMA0GCSqGSIb3DQEBAQUA + /// <...> + /// -----END X509 CRL----- + /// + /// + /// + /// An array of 8-bit unsigned integers that is equivalent to input, or null if no valid PEM header was found. + public static Byte[]? FromBase64Header(String input, String headerName, Boolean skipHeaderValidation = false) { + String header, footer; + if (skipHeaderValidation) { + header = "-----BEGIN "; + footer = "-----END "; + } else { + header = $"-----BEGIN {headerName.Trim()}-----"; + footer = $"-----END {headerName.Trim()}-----"; + } + // search for header (uppercase) start position + Int32 start = input.IndexOf(header, StringComparison.Ordinal); + if (start < 0) { + // if we don't find header, then it is not valid PEM header. End here. return null; } - Int32 start = input.IndexOf(header, StringComparison.Ordinal) + 10; - Int32 headerEndPos = input.IndexOf("-----", start, StringComparison.Ordinal) + 5; - Int32 footerStartPos = input.IndexOf(footer, StringComparison.Ordinal); + Int32 headerEndPos = skipHeaderValidation + // 10 is the length of mandatory '-----BEGIN' header part. Seek after mandatory part and look where + // terminating 5-dash sequence begins and append the length of 5-dash. This is the end of header and + // where body begins. + ? input.IndexOf("-----", start + 10, StringComparison.Ordinal) + 5 + : start + header.Length; + // search for footer starting with header end position. Empty + Int32 footerStartPos = input.IndexOf(footer, headerEndPos, StringComparison.Ordinal); + if (footerStartPos < 0) { + // we found PEM header, but no PEM footer, or the order is wrong (footer defined before header) + return null; + } + try { return Convert.FromBase64String(input.Substring(headerEndPos, footerStartPos - headerEndPos)); } catch { return null; } } - public static Byte[]? FromBase64Request(String input) { - String header; - String footer; - if (input.ToUpper().Contains(PemHeader.PEM_HEADER_REQ_NEW.GetHeader())) { - header = PemHeader.PEM_HEADER_REQ_NEW.GetHeader(); - footer = PemHeader.PEM_HEADER_REQ_NEW.GetFooter(); - } else if (input.ToUpper().Contains(PemHeader.PEM_HEADER_REQ.GetHeader())) { - header = PemHeader.PEM_HEADER_REQ.GetHeader(); - footer = PemHeader.PEM_HEADER_REQ.GetFooter(); - } else { - return null; - } - - return FromBase64Header(input, header, footer, true); - } + /// + /// Attempts to convert input string into a 8bit byte array. This method converts each character of input + /// string to its 8bit numerical representation. + /// + /// String to convert. + /// Decoded byte array. This method returns null if input string contains non-8bit characters. public static Byte[]? FromBinary(String input) { Byte[] rawBytes = new Byte[input.Length]; for (Int32 i = 0; i < input.Length; i++) { @@ -321,11 +360,29 @@ static class StringToBinaryFormatter { return bytes.ToArray(); } + /// + /// Attempts to convert the specified string, which encodes binary data as base64 digits with + /// or without PEM header and footer, to an equivalent 8-bit unsigned integer array. + /// + /// Base64 formatted string with optional PEM header and footer. + /// + /// An array of 8-bit unsigned integers that is equivalent to input, or null if input string doesn't represent + /// valid Base64 or PEM formatted string. + /// public static Byte[]? FromBase64Any(String input) { - return FromBase64Header(input) ?? FromBase64(input); + if (input.Contains("-----BEGIN ")) { + // if input string contains PEM begin sequence, then try only Base64 with header and footer + // without strict validation. + return FromBase64Header(input, String.Empty, true); + } + // if there is no PEM found, then try raw base64. + return FromBase64(input); } public static Byte[] FromStringAny(String input) { - return FromBase64Header(input) ?? FromBase64(input) ?? input.Select(Convert.ToByte).ToArray(); + if (input.Contains("-----BEGIN ")) { + return FromBase64Header(input, String.Empty, true); + } + return FromBase64(input) ?? FromBinary(input); } public static Byte[]? FromHexAny(String input) { return FromHexAddr(input) ?? diff --git a/tests/Asn1Parser.Tests/Base64StringToBinaryTests.cs b/tests/Asn1Parser.Tests/Base64StringToBinaryTests.cs index b417707..1e97f3c 100644 --- a/tests/Asn1Parser.Tests/Base64StringToBinaryTests.cs +++ b/tests/Asn1Parser.Tests/Base64StringToBinaryTests.cs @@ -50,7 +50,23 @@ public void TestBase64ToBinary() { [TestMethod] public void TestBase64HeaderToBinaryStrictValid() { foreach (EncodingType encoding in _b64Encodings) { - Byte[] actual = AsnFormatter.StringToBinary(AsnFormatter.BinaryToString(_rawData, encoding), encoding); + String input = AsnFormatter.BinaryToString(_rawData, encoding); + Byte[] actual = AsnFormatter.StringToBinary(input, encoding); + + EncodingType expected; + switch (encoding) { + case EncodingType.Base64Header: + expected = EncodingType.PemCert; + break; + case EncodingType.Base64RequestHeader: + expected = EncodingType.PemNewReq; + break; + default: + expected = encoding; + break; + } + EncodingType suggestedEncoding = AsnFormatter.TestInputString(input); + Assert.AreEqual(expected, suggestedEncoding); validateBinary(actual); } } @@ -70,5 +86,20 @@ void validateBinary(Byte[] actual) { Assert.IsTrue(_rawData.SequenceEqual(actual)); } - + [TestMethod] + public void TestMismatchHeaderAndFooter() { + String pem = $""" + -----BEGIN CERTIFICATE----- + {_base64} + -----END PKCS7----- + """; + EncodingType encoding = AsnFormatter.TestInputString(pem); + Assert.AreEqual(EncodingType.Base64Header, encoding); + } + [TestMethod] + public void TestInvalidBase64() { + String base64 = "Xblue"; + EncodingType encoding = AsnFormatter.TestInputString(base64); + Assert.AreEqual(EncodingType.Binary, encoding); + } } \ No newline at end of file From 2703e5b346c2653f92ac7603d88818d041f06e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Thu, 6 Mar 2025 13:12:20 +0200 Subject: [PATCH 59/66] removed redundant `.AsSpan()` calls. --- tests/Asn1Parser.Tests/BinaryToHexStringTests.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/Asn1Parser.Tests/BinaryToHexStringTests.cs b/tests/Asn1Parser.Tests/BinaryToHexStringTests.cs index 1b5268d..07903e4 100644 --- a/tests/Asn1Parser.Tests/BinaryToHexStringTests.cs +++ b/tests/Asn1Parser.Tests/BinaryToHexStringTests.cs @@ -84,33 +84,33 @@ public void TestHexAddrAsciiTruncated() { [TestMethod] public void TestComplexHexRaw() { - String str = AsnFormatter.BinaryToString(_complexRawData.AsSpan()); + String str = AsnFormatter.BinaryToString(_complexRawData); Assert.AreEqual(COMPLEX_HEX_RAW, str.TrimEnd()); - str = AsnFormatter.BinaryToString(_complexRawData.AsSpan(), forceUpperCase: true); + str = AsnFormatter.BinaryToString(_complexRawData, forceUpperCase: true); Assert.AreEqual(COMPLEX_HEX_RAW.ToUpper(), str.TrimEnd()); } [TestMethod] public void TestComplexHex() { - String str = AsnFormatter.BinaryToString(_complexRawData.AsSpan(), EncodingType.Hex); + String str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.Hex); Assert.AreEqual(COMPLEX_HEX, str.TrimEnd()); - str = AsnFormatter.BinaryToString(_complexRawData.AsSpan(), EncodingType.Hex, forceUpperCase: true); + str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.Hex, forceUpperCase: true); Assert.AreEqual(COMPLEX_HEX.ToUpper(), str.TrimEnd()); } [TestMethod] public void TestComplexHexAddr() { - String str = AsnFormatter.BinaryToString(_complexRawData.AsSpan(), EncodingType.HexAddress); + String str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.HexAddress); Assert.AreEqual(COMPLEX_HEX_ADDR, str.TrimEnd()); - str = AsnFormatter.BinaryToString(_complexRawData.AsSpan(), EncodingType.HexAddress, forceUpperCase: true); + str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.HexAddress, forceUpperCase: true); Assert.AreEqual(COMPLEX_HEX_ADDR.ToUpper(), str.TrimEnd()); } [TestMethod] public void TestComplexHexAscii() { - String str = AsnFormatter.BinaryToString(_complexRawData.AsSpan(), EncodingType.HexAscii); + String str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.HexAscii); Assert.AreEqual(COMPLEX_HEX_ASCII, str.TrimEnd()); } [TestMethod] public void TestComplexHexAddrAscii() { - String str = AsnFormatter.BinaryToString(_complexRawData.AsSpan(), EncodingType.HexAsciiAddress); + String str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.HexAsciiAddress); Assert.AreEqual(COMPLEX_HEX_ADDR_ASCII, str.TrimEnd()); } } \ No newline at end of file From 8f36ccc430cc480eb713001b9c5575aae184908d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Thu, 6 Mar 2025 13:51:39 +0200 Subject: [PATCH 60/66] Refactored BinaryToHexStringTests --- .../BinaryToHexStringTests.cs | 84 +++++++++++-------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/tests/Asn1Parser.Tests/BinaryToHexStringTests.cs b/tests/Asn1Parser.Tests/BinaryToHexStringTests.cs index 07903e4..87b65a7 100644 --- a/tests/Asn1Parser.Tests/BinaryToHexStringTests.cs +++ b/tests/Asn1Parser.Tests/BinaryToHexStringTests.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using Microsoft.VisualStudio.TestTools.UnitTesting; using SysadminsLV.Asn1Parser; @@ -39,78 +40,89 @@ 0020 f7 42 87 e7 c4 36 .B...6 [TestMethod] public void TestHex() { - String str = AsnFormatter.BinaryToString(_rawData, EncodingType.Hex); - Assert.AreEqual(HEX, str.TrimEnd()); - str = AsnFormatter.BinaryToString(_rawData, EncodingType.Hex, forceUpperCase: true); - Assert.AreEqual(HEX.ToUpper(), str.TrimEnd()); + runTest(_rawData, EncodingType.Hex, HEX, true); } [TestMethod] public void TestHexRaw() { - String str = AsnFormatter.BinaryToString(_rawData); - Assert.AreEqual(HEX_RAW, str.TrimEnd()); - str = AsnFormatter.BinaryToString(_rawData, forceUpperCase: true); - Assert.AreEqual(HEX_RAW.ToUpper(), str.TrimEnd()); + runTest(_rawData, EncodingType.HexRaw, HEX_RAW, true); } [TestMethod] public void TestHexAddr() { - String str = AsnFormatter.BinaryToString(_rawData, EncodingType.HexAddress); - Assert.AreEqual(HEX_ADDR + HEX, str.TrimEnd()); + runTest(_rawData, EncodingType.HexAddress, HEX_ADDR + HEX, true); } [TestMethod] public void TestHexAscii() { - String str = AsnFormatter.BinaryToString(_rawData, EncodingType.HexAscii); - Assert.AreEqual(HEX + HEX_ASCII, str.TrimEnd()); + runTest(_rawData, EncodingType.HexAscii, HEX + HEX_ASCII, true); } [TestMethod] public void TestHexAddrAscii() { - String str = AsnFormatter.BinaryToString(_rawData, EncodingType.HexAsciiAddress); - Assert.AreEqual(HEX_ADDR + HEX + HEX_ASCII, str.TrimEnd()); + runTest(_rawData, EncodingType.HexAsciiAddress, HEX_ADDR + HEX + HEX_ASCII, true); } [TestMethod] public void TestHexAddrTruncated() { - String str = AsnFormatter.BinaryToString(_rawDataTruncated, EncodingType.HexAddress); - Assert.AreEqual(HEX_ADDR + TRUNCATED_HEX, str.TrimEnd()); + runTest(_rawDataTruncated, EncodingType.HexAddress, HEX_ADDR + TRUNCATED_HEX, true); } [TestMethod] public void TestHexAsciiTruncated() { - String str = AsnFormatter.BinaryToString(_rawDataTruncated, EncodingType.HexAscii); - Assert.AreEqual(TRUNCATED_HEX + TRUNCATED_HEX_ASCII, str.TrimEnd()); + runTest(_rawDataTruncated, EncodingType.HexAscii, TRUNCATED_HEX + TRUNCATED_HEX_ASCII, true); } [TestMethod] public void TestHexAddrAsciiTruncated() { - String str = AsnFormatter.BinaryToString(_rawDataTruncated, EncodingType.HexAsciiAddress); - Assert.AreEqual(HEX_ADDR + TRUNCATED_HEX + TRUNCATED_HEX_ASCII, str.TrimEnd()); + runTest(_rawDataTruncated, EncodingType.HexAsciiAddress, HEX_ADDR + TRUNCATED_HEX + TRUNCATED_HEX_ASCII, true); } [TestMethod] public void TestComplexHexRaw() { - String str = AsnFormatter.BinaryToString(_complexRawData); - Assert.AreEqual(COMPLEX_HEX_RAW, str.TrimEnd()); - str = AsnFormatter.BinaryToString(_complexRawData, forceUpperCase: true); - Assert.AreEqual(COMPLEX_HEX_RAW.ToUpper(), str.TrimEnd()); + runTest(_complexRawData, EncodingType.HexRaw, COMPLEX_HEX_RAW, true); } [TestMethod] public void TestComplexHex() { - String str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.Hex); - Assert.AreEqual(COMPLEX_HEX, str.TrimEnd()); - str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.Hex, forceUpperCase: true); - Assert.AreEqual(COMPLEX_HEX.ToUpper(), str.TrimEnd()); + runTest(_complexRawData, EncodingType.Hex, COMPLEX_HEX, true); } [TestMethod] public void TestComplexHexAddr() { - String str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.HexAddress); - Assert.AreEqual(COMPLEX_HEX_ADDR, str.TrimEnd()); - str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.HexAddress, forceUpperCase: true); - Assert.AreEqual(COMPLEX_HEX_ADDR.ToUpper(), str.TrimEnd()); + runTest(_complexRawData, EncodingType.HexAddress, COMPLEX_HEX_ADDR, true); } [TestMethod] public void TestComplexHexAscii() { - String str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.HexAscii); - Assert.AreEqual(COMPLEX_HEX_ASCII, str.TrimEnd()); + runTest(_complexRawData, EncodingType.HexAscii, COMPLEX_HEX_ASCII, false); } [TestMethod] public void TestComplexHexAddrAscii() { - String str = AsnFormatter.BinaryToString(_complexRawData, EncodingType.HexAsciiAddress); - Assert.AreEqual(COMPLEX_HEX_ADDR_ASCII, str.TrimEnd()); + runTest(_complexRawData, EncodingType.HexAsciiAddress, COMPLEX_HEX_ADDR_ASCII, false); } + + void runTest(Byte[] rawData, EncodingType encoding, String expected, Boolean testUppercase) { + String str = AsnFormatter.BinaryToString(rawData, encoding); + Assert.AreEqual(expected, str.TrimEnd()); + EncodingType testedEncoding = AsnFormatter.TestInputString(str); + EncodingType capiEncoding = tryCapiDecode(str); + // commented for now. Need research for these tests + //Assert.AreEqual(encoding, testedEncoding); + //Assert.AreEqual(capiEncoding, testedEncoding); + if (testUppercase) { + str = AsnFormatter.BinaryToString(rawData, encoding, forceUpperCase: true); + Assert.AreEqual(expected.ToUpper(), str.TrimEnd()); + } + } + + EncodingType tryCapiDecode(String s) { + UInt32 pcbBinary = 0; + if (!CryptStringToBinary(s, s.Length, 0x7, null, ref pcbBinary, out UInt32 pdwSkip, out EncodingType pdwFlags)) { + Console.WriteLine(Marshal.GetLastWin32Error()); + return EncodingType.Binary; + } + + return pdwFlags; + } + [DllImport("crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)] + static extern Boolean CryptStringToBinary( + [In] String pszString, + [In] Int32 cchString, + [In] UInt32 dwFlags, + [In] Byte[]? pbBinary, + [In, Out] ref UInt32 pcbBinary, + [Out] out UInt32 pdwSkip, + [Out] out EncodingType pdwFlags + ); } \ No newline at end of file From c6bdf9b4e98aff32a5de29ceb92d81db69d3074f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Fri, 14 Mar 2025 17:34:33 +0200 Subject: [PATCH 61/66] added Asn1BitString tests --- tests/Asn1Parser.Tests/Asn1BitStringTests.cs | 36 ++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/Asn1Parser.Tests/Asn1BitStringTests.cs diff --git a/tests/Asn1Parser.Tests/Asn1BitStringTests.cs b/tests/Asn1Parser.Tests/Asn1BitStringTests.cs new file mode 100644 index 0000000..0880e1a --- /dev/null +++ b/tests/Asn1Parser.Tests/Asn1BitStringTests.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SysadminsLV.Asn1Parser.Universal; + +namespace Asn1Parser.Tests; +[TestClass] +public class Asn1BitStringTests { + [TestMethod] + public void TestUnusedBitsCalculator() { + Byte unusedBits = Asn1BitString.CalculateUnusedBits([0x1]); + Assert.AreEqual(0, unusedBits); + unusedBits = Asn1BitString.CalculateUnusedBits([0x2]); + Assert.AreEqual(1, unusedBits); + unusedBits = Asn1BitString.CalculateUnusedBits([0x4]); + Assert.AreEqual(2, unusedBits); + unusedBits = Asn1BitString.CalculateUnusedBits([0x8]); + Assert.AreEqual(3, unusedBits); + unusedBits = Asn1BitString.CalculateUnusedBits([0x10]); + Assert.AreEqual(4, unusedBits); + unusedBits = Asn1BitString.CalculateUnusedBits([0x20]); + Assert.AreEqual(5, unusedBits); + unusedBits = Asn1BitString.CalculateUnusedBits([0x40]); + Assert.AreEqual(6, unusedBits); + unusedBits = Asn1BitString.CalculateUnusedBits([0x80]); + Assert.AreEqual(7, unusedBits); + } + [TestMethod] + public void TestBitStringDecodeFromByteArray() { + Byte[] array = [3, 2, 5, 0xa0]; + var bitString = new Asn1BitString(array); + Assert.AreEqual(5, bitString.UnusedBits); + Assert.IsTrue(array.AsSpan().Slice(3,1).SequenceEqual(bitString.GetValue().Span)); + Assert.IsTrue(array.AsSpan().Slice(2, 2).SequenceEqual(bitString.GetPayloadAsMemory().Span)); + Assert.IsTrue(array.AsSpan().SequenceEqual(bitString.GetRawDataAsMemory().Span)); + } +} From 60cccfba94cbfd1387fd98ec5543d6dc8be4376c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Fri, 14 Mar 2025 17:51:34 +0200 Subject: [PATCH 62/66] Added Asn1Utils.GetLengthBytesAsMemory which is identical to Asn1Utils.GetLengthBytes. --- Asn1Parser/Asn1Utils.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Asn1Parser/Asn1Utils.cs b/Asn1Parser/Asn1Utils.cs index cbbdba2..cc75075 100644 --- a/Asn1Parser/Asn1Utils.cs +++ b/Asn1Parser/Asn1Utils.cs @@ -48,6 +48,14 @@ public static Byte[] GetLengthBytes(Int32 payloadLength) { return retValue; } /// + /// Generates tag length header for specified size. + /// + /// A projected tag length. + /// Encoded tag length header. Return value do not contain tag and payload. + public static ReadOnlyMemory GetLengthBytesAsMemory(Int32 payloadLength) { + return GetLengthBytes(payloadLength); + } + /// /// Calculates the ASN.1 payload length from a given ASN.1 length header. /// /// A byte array that represents ASN.1 length header From 5041af1918bd40de7cdfecf1c82653a42aa7182b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Fri, 14 Mar 2025 18:04:28 +0200 Subject: [PATCH 63/66] fixed #25 Related Work Items: #2, #25 --- Asn1Parser/Asn1Utils.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Asn1Parser/Asn1Utils.cs b/Asn1Parser/Asn1Utils.cs index cc75075..cafb2b1 100644 --- a/Asn1Parser/Asn1Utils.cs +++ b/Asn1Parser/Asn1Utils.cs @@ -58,7 +58,7 @@ public static ReadOnlyMemory GetLengthBytesAsMemory(Int32 payloadLength) { /// /// Calculates the ASN.1 payload length from a given ASN.1 length header. /// - /// A byte array that represents ASN.1 length header + /// A byte array that represents ASN.1 length header excluding tag and payload. /// /// asnHeader parameter length is more than 4 bytes or is invalid value. /// @@ -67,7 +67,7 @@ public static Int64 CalculatePayloadLength(ReadOnlySpan asnHeader) { if (asnHeader.Length == 0) { return 0; } - if (asnHeader[0] < 127) { + if (asnHeader[0] <= 127) { return asnHeader[0]; } Int32 lengthBytes = asnHeader[0] - 128; From b603668eb23eb1e3ef9c8538b0c2b61311186de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Fri, 14 Mar 2025 18:20:34 +0200 Subject: [PATCH 64/66] significantly simplified Asn1Utils.Encode method. --- Asn1Parser/Asn1Utils.cs | 53 +++++++++++------------------------------ 1 file changed, 14 insertions(+), 39 deletions(-) diff --git a/Asn1Parser/Asn1Utils.cs b/Asn1Parser/Asn1Utils.cs index cafb2b1..e636879 100644 --- a/Asn1Parser/Asn1Utils.cs +++ b/Asn1Parser/Asn1Utils.cs @@ -114,48 +114,23 @@ public static Byte[] Encode(Byte[] rawData, Byte enclosingTag) { /// Wrapped encoded byte array. /// If rawData is null, an empty tag is encoded. public static ReadOnlyMemory Encode(ReadOnlySpan rawData, Byte enclosingTag) { - Byte[] retValue; if (rawData.Length == 0) { - retValue = [enclosingTag, 0]; - - return retValue; + return new Byte[] { enclosingTag, 0 }; + } + ReadOnlySpan pbHeader = GetLengthBytesAsMemory(rawData.Length).Span; + // copy T component to destination array. + Byte[] retValue = new Byte[1 + pbHeader.Length + rawData.Length]; + // copy L component to destination array + retValue[0] = enclosingTag; + for (Int32 i = 0; i < pbHeader.Length; i++) { + retValue[i + 1] = pbHeader[i]; } - if (rawData.Length < 128) { - // pre-create destination array of fixed size. - retValue = new Byte[rawData.Length + 2]; - // populate TL components of TLV - retValue[0] = enclosingTag; - retValue[1] = (Byte)rawData.Length; - // copy input buffer to V component - for (Int32 index = 0; index < rawData.Length; index++) { - retValue[index + 2] = rawData[index]; - } - } else { - Byte[] lenBytes = new Byte[4]; - Int32 num = rawData.Length; - Int32 counter = 0; - while (num >= 256) { - lenBytes[counter] = (Byte)(num & 255); - num >>= 8; - counter++; - } - // 3 is: len byte and enclosing tag - // pre-create destination array of fixed size. - retValue = new Byte[rawData.Length + 3 + counter]; - // copy input buffer to V component - for (Int32 index = 0; index < rawData.Length; index++) { - retValue[index + 3 + counter] = rawData[index]; - } - // populate TL components of TLV - retValue[0] = enclosingTag; - retValue[1] = (Byte)(129 + counter); - retValue[2] = (Byte)num; - Int32 n = 3; - for (Int32 i = counter - 1; i >= 0; i--) { - retValue[n] = lenBytes[i]; - n++; - } + Int32 shift = 1 + pbHeader.Length; + // copy V component to destination array + for (Int32 i = 0; i < rawData.Length; i++) { + retValue[i + shift] = rawData[i]; } + return retValue; } /// From e99580165500f1b75f48cd396987330bd4a0881a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Fri, 14 Mar 2025 18:22:20 +0200 Subject: [PATCH 65/66] minor refactoring --- Asn1Parser/Asn1Utils.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Asn1Parser/Asn1Utils.cs b/Asn1Parser/Asn1Utils.cs index e636879..7bfb5da 100644 --- a/Asn1Parser/Asn1Utils.cs +++ b/Asn1Parser/Asn1Utils.cs @@ -122,10 +122,11 @@ public static ReadOnlyMemory Encode(ReadOnlySpan rawData, Byte enclo Byte[] retValue = new Byte[1 + pbHeader.Length + rawData.Length]; // copy L component to destination array retValue[0] = enclosingTag; + Int32 shift = 1; for (Int32 i = 0; i < pbHeader.Length; i++) { - retValue[i + 1] = pbHeader[i]; + retValue[i + shift] = pbHeader[i]; } - Int32 shift = 1 + pbHeader.Length; + shift += pbHeader.Length; // copy V component to destination array for (Int32 i = 0; i < rawData.Length; i++) { retValue[i + shift] = rawData[i]; From 510686534c36eb679455434a94334303b2557918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vadims=20Pod=C4=81ns?= Date: Fri, 14 Mar 2025 18:32:55 +0200 Subject: [PATCH 66/66] added unit tests for core methods of Asn1Utils class. --- tests/Asn1Parser.Tests/Asn1UtilsTests.cs | 72 ++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/Asn1Parser.Tests/Asn1UtilsTests.cs diff --git a/tests/Asn1Parser.Tests/Asn1UtilsTests.cs b/tests/Asn1Parser.Tests/Asn1UtilsTests.cs new file mode 100644 index 0000000..bc28cd1 --- /dev/null +++ b/tests/Asn1Parser.Tests/Asn1UtilsTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SysadminsLV.Asn1Parser; + +namespace Asn1Parser.Tests; + +[TestClass] +public class Asn1UtilsTests { + [TestMethod] + public void TestGetLengthBytes() { + ReadOnlyMemory lenBytes = Asn1Utils.GetLengthBytesAsMemory(0); + Assert.AreEqual(1, lenBytes.Length); + Assert.IsTrue(lenBytes.Span.SequenceEqual(new Byte[] { 0 })); + + lenBytes = Asn1Utils.GetLengthBytesAsMemory(127); + Assert.AreEqual(1, lenBytes.Length); + Assert.IsTrue(lenBytes.Span.SequenceEqual(new Byte[] { 127 })); + + lenBytes = Asn1Utils.GetLengthBytesAsMemory(128); + Assert.AreEqual(2, lenBytes.Length); + Assert.IsTrue(lenBytes.Span.SequenceEqual(new Byte[] { 129, 128 })); + + lenBytes = Asn1Utils.GetLengthBytesAsMemory(255); + Assert.AreEqual(2, lenBytes.Length); + Assert.IsTrue(lenBytes.Span.SequenceEqual(new Byte[] { 129, 255 })); + + lenBytes = Asn1Utils.GetLengthBytesAsMemory(256); + Assert.AreEqual(3, lenBytes.Length); + Assert.IsTrue(lenBytes.Span.SequenceEqual(new Byte[] { 130, 1, 0 })); + + lenBytes = Asn1Utils.GetLengthBytesAsMemory(100000); + Assert.AreEqual(4, lenBytes.Length); + Assert.IsTrue(lenBytes.Span.SequenceEqual(new Byte[] { 131, 1, 134, 160 })); + } + + [TestMethod] + public void TestCalculatePayloadLength() { + Int64 length = Asn1Utils.CalculatePayloadLength([0]); + Assert.AreEqual(0, length); + + length = Asn1Utils.CalculatePayloadLength([127]); + Assert.AreEqual(127, length); + + length = Asn1Utils.CalculatePayloadLength([129, 128]); + Assert.AreEqual(128, length); + + length = Asn1Utils.CalculatePayloadLength([129, 255]); + Assert.AreEqual(255, length); + + length = Asn1Utils.CalculatePayloadLength([130, 1, 0]); + Assert.AreEqual(256, length); + + length = Asn1Utils.CalculatePayloadLength([131, 1, 134, 160]); + Assert.AreEqual(100000, length); + } + + [TestMethod] + public void TestEncode() { + Byte[] array = new Byte[130]; + for (Int32 i = 0; i < array.Length; i++) { + array[i] = (Byte)i; + } + + ReadOnlyMemory encoded = Asn1Utils.Encode(array.AsSpan(), Asn1Type.OCTET_STRING); + var list = new List([4, 129, 130]); + list.AddRange(array); + + Assert.AreEqual(list.Count, encoded.Length); + Assert.IsTrue(encoded.Span.SequenceEqual(list.ToArray())); + } +}