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/Asn1Parser/Asn1Builder.cs b/Asn1Parser/Asn1Builder.cs index 5d0f95e..092be1c 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; @@ -13,7 +12,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 +30,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 +41,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; } /// @@ -55,11 +54,8 @@ 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) { - if (value == null) { - throw new ArgumentNullException(nameof(value)); - } - _rawData.AddRange(new Asn1BitString(value, unusedBits).GetRawData()); + public Asn1Builder AddBitString(ReadOnlySpan value, Byte unusedBits) { + _rawData.Add(new Asn1BitString(value, unusedBits).GetRawDataAsMemory()); return this; } /// @@ -72,11 +68,8 @@ 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) { - if (value == null) { - throw new ArgumentNullException(nameof(value)); - } - _rawData.AddRange(new Asn1BitString(value, calculateUnusedBits).GetRawData()); + public Asn1Builder AddBitString(ReadOnlySpan value, Boolean calculateUnusedBits = false) { + _rawData.Add(new Asn1BitString(value, calculateUnusedBits).GetRawDataAsMemory()); return this; } /// @@ -89,11 +82,8 @@ public Asn1Builder AddBitString(Byte[] value, Boolean calculateUnusedBits = fals /// value parameter is null. /// /// Current instance with added value. - public Asn1Builder AddOctetString(Byte[] value) { - if (value == null) { - throw new ArgumentNullException(nameof(value)); - } - _rawData.AddRange(new Asn1OctetString(value, false).GetRawData()); + public Asn1Builder AddOctetString(ReadOnlyMemory value) { + _rawData.Add(new Asn1OctetString(value, false).GetRawDataAsMemory()); return this; } /// @@ -101,7 +91,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; } /// @@ -115,10 +105,10 @@ 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.AddRange(new Asn1ObjectIdentifier(value).GetRawData()); + _rawData.Add(new Asn1ObjectIdentifier(value).GetRawDataAsMemory()); return this; } /// @@ -133,10 +123,10 @@ 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.AddRange(new Asn1RelativeOid(value).GetRawData()); + _rawData.Add(new Asn1RelativeOid(value).GetRawDataAsMemory()); return this; } /// @@ -147,7 +137,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; } /// @@ -161,10 +151,10 @@ 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.AddRange(new Asn1UTF8String(value).GetRawData()); + _rawData.Add(new Asn1UTF8String(value).GetRawDataAsMemory()); return this; } /// @@ -180,15 +170,9 @@ public Asn1Builder AddUTF8String(String value) { /// /// In the current implementation, SEQUENCE is encoded using constructed form only. /// - 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); + public Asn1Builder AddSequence(ReadOnlySpan value) { + _rawData.Add(Asn1Utils.Encode(value, 0x30)); + return this; } /// @@ -204,15 +188,9 @@ public Asn1Builder AddSequence(Byte[] 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)); - } - 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); + public Asn1Builder AddSet(ReadOnlySpan value) { + _rawData.Add(Asn1Utils.Encode(value, 0x31)); + return this; } /// @@ -226,10 +204,10 @@ public Asn1Builder AddSet(Byte[] value) { /// /// Current instance with added value. public Asn1Builder AddNumericString(String value) { - if (value == null) { + if (value is null) { throw new ArgumentNullException(nameof(value)); } - _rawData.AddRange(new Asn1NumericString(value).GetRawData()); + _rawData.Add(new Asn1NumericString(value).GetRawDataAsMemory()); return this; } /// @@ -243,10 +221,10 @@ 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.AddRange(new Asn1PrintableString(value).GetRawData()); + _rawData.Add(new Asn1PrintableString(value).GetRawDataAsMemory()); return this; } /// @@ -260,10 +238,10 @@ 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.AddRange(new Asn1TeletexString(value).GetRawData()); + _rawData.Add(new Asn1TeletexString(value).GetRawDataAsMemory()); return this; } /// @@ -277,10 +255,10 @@ 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.AddRange(Asn1Utils.Encode(Encoding.ASCII.GetBytes(value), Asn1Type.VideotexString)); + _rawData.Add(Asn1Utils.Encode(Encoding.ASCII.GetBytes(value).AsSpan(), Asn1Type.VideotexString)); return this; } /// @@ -294,10 +272,10 @@ 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.AddRange(new Asn1IA5String(value).GetRawData()); + _rawData.Add(new Asn1IA5String(value).GetRawDataAsMemory()); return this; } /// @@ -308,7 +286,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 +297,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 +312,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; } @@ -351,10 +329,10 @@ 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.AddRange(new Asn1VisibleString(value).GetRawData()); + _rawData.Add(new Asn1VisibleString(value).GetRawDataAsMemory()); return this; } /// @@ -368,10 +346,10 @@ 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.AddRange(new Asn1UniversalString(value).GetRawData()); + _rawData.Add(new Asn1UniversalString(value).GetRawDataAsMemory()); return this; } /// @@ -385,10 +363,10 @@ 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.AddRange(new Asn1BMPString(value).GetRawData()); + _rawData.Add(new Asn1BMPString(value).GetRawDataAsMemory()); return this; } /// @@ -401,13 +379,11 @@ 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)); - } - var asn = new Asn1Reader(value); + public Asn1Builder AddDerData(ReadOnlySpan value) { + var asn = new Asn1Reader(value.ToArray()); asn.BuildOffsetMap(); - _rawData.AddRange(value); + _rawData.Add(asn.GetRawDataAsMemory()); + return this; } /// @@ -423,11 +399,9 @@ public Asn1Builder AddDerData(Byte[] 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.AddRange(Asn1Utils.Encode(value, outerTag)); + public Asn1Builder AddDerData(ReadOnlyMemory value, Byte outerTag) { + _rawData.Add(Asn1Utils.Encode(value.Span, outerTag)); + return this; } /// @@ -442,9 +416,6 @@ public Asn1Builder AddDerData(Byte[] 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. /// @@ -457,21 +428,18 @@ 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) { - if (value == null) { - throw new ArgumentNullException(nameof(value)); - } + public Asn1Builder AddImplicit(Byte implicitTag, ReadOnlySpan value, Boolean mustEncode) { 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(); } - 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.AddRange(valueCopy); + buff[0] = (Byte)(0x80 + implicitTag); + _rawData.Add(buff); } return this; } @@ -487,9 +455,6 @@ public Asn1Builder AddImplicit(Byte implicitTag, Byte[] value, Boolean mustEncod /// /// Specifies if data in value parameter must be encoded or not. See Remarks for more details. /// - /// - /// value parameter is null. - /// /// /// value is not encoded. /// @@ -501,18 +466,15 @@ 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) { - if (value == null) { - throw new ArgumentNullException(nameof(value)); - } + public Asn1Builder AddExplicit(Byte explicitTag, ReadOnlySpan value, Boolean mustEncode) { if (mustEncode) { - _rawData.AddRange(Asn1Utils.Encode(value, (Byte)(0xa0 + explicitTag))); + _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.AddRange(valueCopy); + buff[0] = (Byte)(0xa0 + explicitTag); + _rawData.Add(buff); } return this; } @@ -528,11 +490,11 @@ public Asn1Builder AddExplicit(Byte explicitTag, Byte[] value, Boolean mustEncod /// 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()); - _rawData.AddRange(new Asn1BitString(b._rawData.ToArray(), 0).GetRawData()); + _rawData.Add(new Asn1BitString(b.GetRawDataAsMemory().Span, 0).GetRawDataAsMemory()); return this; } /// @@ -547,11 +509,11 @@ 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()); - _rawData.AddRange(Asn1Utils.Encode(b._rawData.ToArray(), Asn1Type.OCTET_STRING)); + _rawData.Add(Asn1Utils.Encode(b.GetRawDataAsMemory().Span, (Byte)Asn1Type.OCTET_STRING)); return this; } /// @@ -563,11 +525,11 @@ 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()); - _rawData.AddRange(Asn1Utils.Encode(b._rawData.ToArray(), 0x30)); + _rawData.Add(Asn1Utils.Encode(b.GetRawDataAsMemory().Span, 0x30)); return this; } /// @@ -579,11 +541,11 @@ 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()); - _rawData.AddRange(Asn1Utils.Encode(b._rawData.ToArray(), 0x31)); + _rawData.Add(Asn1Utils.Encode(b.GetRawDataAsMemory().Span, 0x31)); return this; } /// @@ -598,11 +560,11 @@ 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()); - _rawData.AddRange(Asn1Utils.Encode(b._rawData.ToArray(), (Byte)(0xa0 + explicitTag))); + _rawData.Add(Asn1Utils.Encode(b.GetRawDataAsMemory().Span, (Byte)(0xa0 + explicitTag))); return this; } @@ -618,12 +580,44 @@ 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) { + // 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; + } + Int32 headerLength = 0; + Byte[]? header = null; + if (includeHeader) { + header = Asn1Utils.GetLengthBytes(payloadLength); + headerLength = header.Length + 1; + } + Byte[] memory = new Byte[headerLength + payloadLength]; + Int32 i = 0; + if (includeHeader) { + memory[0] = outerTag; + for (; i < header!.Length; i++) { + memory[i + 1] = header[i]; + } + 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 +629,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 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 be 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 = 0x30) { + return getEncoded(outerTag, true); } /// - /// Gets a raw data of the current state of the builder. + /// 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); } /// @@ -659,9 +675,9 @@ public static Asn1Builder Create() { /// /// ASN.1-encoded data to initialize the builder from. /// ASN.1 Builder. - public static Asn1Builder Create(IEnumerable rawData) { + public static Asn1Builder Create(ReadOnlySpan rawData) { var builder = new Asn1Builder(); - builder._rawData.AddRange(rawData); + builder._rawData.Add(new ReadOnlyMemory(rawData.ToArray())); return builder; } 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/Asn1Parser.csproj b/Asn1Parser/Asn1Parser.csproj index 17d227d..47ffc50 100644 --- a/Asn1Parser/Asn1Parser.csproj +++ b/Asn1Parser/Asn1Parser.csproj @@ -1,8 +1,8 @@  - netstandard2.0;net472 + net8.0;net472 latest - 1.3.0 + 2.0.0 SysadminsLV.Asn1Parser SysadminsLV.Asn1Parser @@ -15,10 +15,10 @@ true enable True - strongname.snk + - - bin\Release\netstandard2.0\SysadminsLV.Asn1Parser.xml + + bin\Release\net8.0\SysadminsLV.Asn1Parser.xml bin\Release\net472\SysadminsLV.Asn1Parser.xml @@ -26,4 +26,7 @@ - + + + + \ No newline at end of file diff --git a/Asn1Parser/Asn1Reader.cs b/Asn1Parser/Asn1Reader.cs index 0af9c60..f0a4c44 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 @@ -31,11 +23,11 @@ public class Asn1Reader { (Byte)Asn1Type.SET, (Byte)Asn1Type.SET | (Byte)Asn1Class.CONSTRUCTED ]; - readonly List _rawData = []; + ReadOnlyMemory _rawData; readonly Dictionary _offsetMap = []; AsnInternalMap currentPosition; Int32 childCount; - + /// /// Initializes a new instance of the ASN1 class from an existing /// ASN1 object. @@ -44,14 +36,12 @@ public class Asn1Reader { /// /// This constructor creates a copy of a current position of an existing ASN1 object. /// - public Asn1Reader(Asn1Reader asn) : this(asn.GetTagRawData()) { } + [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. /// /// ASN.1-encoded byte array. - /// - /// rawData parameter is null reference. - /// /// /// The data in the rawData parameter is not valid ASN sequence. /// @@ -59,18 +49,21 @@ public Asn1Reader(Asn1Reader asn) : this(asn.GetTagRawData()) { } /// If rawData size is greater than outer structure size, constructor will take only /// required bytes from input data. /// - public Asn1Reader(Byte[] rawData) : this(rawData, 0) { } + 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); + } + // 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; } /// @@ -100,7 +93,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). /// @@ -118,22 +111,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.Clear(); - _rawData.AddRange(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.Count) { - _rawData.Clear(); - _rawData.AddRange(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 @@ -148,7 +143,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 +160,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 +207,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.Span[(Int32)start] != 0; } /// /// Checks if current primitive type is sub-typed (contains nested types) or not. @@ -229,7 +224,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. @@ -264,20 +259,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; } @@ -288,27 +283,27 @@ 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; } - 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.Count) { + 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; } void moveAndExpectTypes(Func action, params Byte[] expectedTypes) { - if (expectedTypes == null) { + if (expectedTypes is null) { throw new ArgumentNullException(nameof(expectedTypes)); } var set = new HashSet(); @@ -332,44 +327,75 @@ 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; } /// + /// 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 ReadOnlyMemory GetHeaderAsMemory() { + Int32 headerLength = PayloadStartOffset - Offset; + + return _rawData.Slice(Offset, headerLength); + } + /// /// Gets the byte array of the current structure's payload. /// /// Byte array of the current structure's payload 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; } /// + /// Gets the byte array of the current structure's payload. + /// + /// Memory span of the current structure's payload. + public ReadOnlyMemory GetPayloadAsMemory() { + return _rawData.Slice(PayloadStartOffset, PayloadLength); + } + /// /// Gets the raw data of the tag, which includes tag, length bytes and payload. /// /// A full binary copy of the tag. 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; } /// + /// Gets the raw data of the tag, which includes tag, length bytes and payload. + /// + /// A full binary copy of the tag. + 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. /// /// 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++) { - array[i] = _rawData[i]; + Byte[] array = new Byte[_rawData.Length]; + for (Int32 i = 0; i < _rawData.Length; i++) { + array[i] = _rawData.Span[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 ReadOnlyMemory GetRawDataAsMemory() { + return _rawData; + } + /// /// Gets the count of nested nodes under node in the current position. /// /// Count of nested nodes. @@ -517,14 +543,12 @@ 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)) { + if (!_offsetMap.TryGetValue(newPosition, out AsnInternalMap? value)) { return false; } currentPosition = value; decode(null, newPosition); + return true; } @@ -564,6 +588,34 @@ public Asn1Universal GetTagObject() { }; } /// + /// Returns a new instance of that is sourced from the current tag. + /// + /// A new instance of . + 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(_rawData, currentPosition, 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. diff --git a/Asn1Parser/Asn1Utils.cs b/Asn1Parser/Asn1Utils.cs index 941932f..7bfb5da 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; @@ -7,6 +8,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 /// @@ -38,24 +48,26 @@ 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 - /// - /// asnHeader parameter is null. - /// + /// 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. /// /// ASN.1 payload length in bytes. - public static Int64 CalculatePayloadLength(Byte[] asnHeader) { - if (asnHeader == null) { - throw new ArgumentNullException(nameof(asnHeader)); - } + 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; @@ -74,40 +86,52 @@ 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 Byte[] Encode(Byte[]? rawData, Byte enclosingTag) { - if (rawData == null) { - return [enclosingTag, 0]; + public static ReadOnlyMemory Encode(ReadOnlySpan rawData, Byte enclosingTag) { + if (rawData.Length == 0) { + return new Byte[] { 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++; - } + 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; + Int32 shift = 1; + for (Int32 i = 0; i < pbHeader.Length; i++) { + retValue[i + shift] = pbHeader[i]; } + shift += pbHeader.Length; + // copy V component to destination array + for (Int32 i = 0; i < rawData.Length; i++) { + retValue[i + shift] = rawData[i]; + } + return retValue; } /// @@ -118,17 +142,40 @@ public static Byte[] Encode(Byte[]? 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) { + public static ReadOnlyMemory 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 enclosingTag) { + return new Asn1Reader(Encode(rawData, enclosingTag)); + } #endregion - + #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), @@ -154,21 +201,19 @@ static String decodeBitString(Asn1Reader asn) { "Unused bits: {0} : {1}", asn[asn.PayloadStartOffset], AsnFormatter.BinaryToString( - asn.GetRawData(), + asn.GetRawDataAsMemory().Slice(asn.PayloadStartOffset + 1, asn.PayloadLength - 1).Span, EncodingType.HexRaw, - EncodingFormat.NOCRLF, - asn.PayloadStartOffset + 1, - asn.PayloadLength - 1) + EncodingFormat.NOCRLF) ); } static String decodeOctetString(Asn1Reader asn) { return AsnFormatter.BinaryToString( - asn.GetRawData(), + asn.GetRawDataAsMemory().Slice(asn.PayloadStartOffset, asn.PayloadLength).Span, 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.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 c9774a3..d0b2045 100644 --- a/Asn1Parser/AsnFormatter.cs +++ b/Asn1Parser/AsnFormatter.cs @@ -16,8 +16,6 @@ public static class AsnFormatter { /// 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. /// @@ -37,21 +35,20 @@ public static class AsnFormatter { /// HexAny /// /// - public static String BinaryToString(Byte[] rawData, EncodingType encoding = EncodingType.HexRaw, EncodingFormat format = EncodingFormat.CRLF, Int32 start = 0, Int32 count = 0, Boolean forceUpperCase = false) { - if (rawData == null || rawData.Length == 0) { + public static String BinaryToString(ReadOnlySpan rawData, EncodingType encoding = EncodingType.HexRaw, EncodingFormat format = EncodingFormat.CRLF, Boolean forceUpperCase = false) { + if (rawData.IsEmpty) { return String.Empty; } - if (PemHeader.ContainsEncoding(encoding)) { - return BinaryToStringFormatter.ToBase64(rawData, encoding, format, start, count); + if (encoding == EncodingType.Base64 || PemHeader.ContainsEncoding(encoding)) { + 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.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, format, forceUpperCase), _ => throw new ArgumentException("Specified encoding is invalid.") }; } @@ -84,28 +81,11 @@ public static String BinaryToString(Byte[] rawData, EncodingType encoding = Enco /// /// 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)); } - if (asn.PayloadLength == 0) { - return String.Empty; - } - if ((Int32)encoding > 20 && (Int32)encoding < 45) { - return BinaryToStringFormatter.ToBase64(asn.GetRawData(), encoding, format, asn.PayloadStartOffset, asn.PayloadLength); - } - if (PemHeader.ContainsEncoding(encoding)) { - return BinaryToStringFormatter.ToBase64(asn.GetRawData(), encoding, format, asn.PayloadStartOffset, asn.PayloadLength); - } - 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), - _ => throw new ArgumentException("Specified encoding is invalid.") - }; + return BinaryToString(asn.GetTagRawDataAsMemory().Span, encoding, format, forceUpperCase); } /// /// Converts previously formatted string back to a byte array. @@ -117,21 +97,20 @@ 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 + /// If encoding parameter is set to Base64Header, the method will accept any PEM header /// and footer. /// /// 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()); + 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), @@ -147,7 +126,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; @@ -159,35 +138,40 @@ 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()); - if (rawBytes != null) { + rawBytes = StringToBinaryFormatter.FromBase64Header(input, pemHeader.Header); + if (rawBytes is not null) { return pemHeader.Encoding; } } - rawBytes = StringToBinaryFormatter.FromBase64Header(input); - if (rawBytes != null) { + rawBytes = StringToBinaryFormatter.FromBase64Header(input, String.Empty, true); + 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/BinaryToStringFormatter.cs b/Asn1Parser/BinaryToStringFormatter.cs index 12d4746..1d42150 100644 --- a/Asn1Parser/BinaryToStringFormatter.cs +++ b/Asn1Parser/BinaryToStringFormatter.cs @@ -1,31 +1,31 @@ using System; -using System.Collections.Generic; -using System.Linq; +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(IReadOnlyList rawData, Int32 start, Int32 count, Boolean forceUpperCase) { - count = getCount(rawData.Count, start, count); - var SB = new StringBuilder(); - for (Int32 i = start; i < start + count; i++) { - byteToHexOctet(SB, rawData[i], forceUpperCase); + public static String ToHexRaw(ReadOnlySpan rawData, EncodingFormat format, Boolean forceUpperCase) { + String eol = format.GetEndOfLine(); + var sb = new StringBuilder(); + foreach (Byte b in rawData) { + sb.AppendHexOctet(b, forceUpperCase); } - return SB.ToString(); + + return sb.Append(eol).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++; - byteToHexOctet(sb, rawData[index], forceUpperCase); - if (index == start) { + for (Int32 index = 0; index < rawData.Length; index++) { + sb.AppendHexOctet(rawData[index], forceUpperCase); + 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,22 +35,25 @@ 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(" "); } } - return finalizeBinaryToString(sb, format); + return sb.Append(format.GetEndOfLine()).ToString(); } - public static String ToHexAddress(IReadOnlyList rawData, EncodingFormat format, Int32 start, Int32 count, Boolean forceUpperCase) { - count = getCount(rawData.Count, start, count); - 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) { + 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 = format.GetEndOfLine(); + 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'); if (forceUpperCase) { hexAddress = hexAddress.ToUpper(); @@ -59,67 +62,65 @@ public static String ToHexAddress(IReadOnlyList rawData, EncodingFormat fo sb.Append(" "); rowCount += 16; } - byteToHexOctet(sb, rawData[index], forceUpperCase); - if (index == start) { + sb.AppendHexOctet(rawData[index], forceUpperCase); + if (index == 0) { sb.Append(" "); - n++; continue; } - if ((n + 1) % 16 == 0) { - sb.Append(format == EncodingFormat.NOCR ? "\n" : "\r\n"); - } else if ((n + 1) % 8 == 0) { + + if ((index + 1) % 16 == 0) { + // if current octet is the last octet in a row, append EOL format + sb.Append(eol); + } 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); + return sb.Append(eof).ToString(); } - 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++; - byteToHexOctet(sb, rawData[index], forceUpperCase); + for (Int32 index = 0; index < rawData.Length; index++) { + sb.AppendHexOctet(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); + return sb.Append(format.GetEndOfLine()).ToString(); } - 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(); @@ -128,93 +129,91 @@ public static String ToHexAddressAndAscii(IReadOnlyList rawData, EncodingF 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]; 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); + return sb.Append(format.GetEndOfLine()).ToString(); } - 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())); - 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; - } + 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 = format.GetEndOfLine(); + 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."); - } - sb.Insert(0, header.Invoke() + splitter); - sb.Append(splitter + footer.Invoke()); - } - 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(); + return PemHeader.GetHeader(encoding); } + + throw new ArgumentException("Specified encoding is not valid Base64 encoding."); } #endregion @@ -229,12 +228,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++; } @@ -243,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/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 { 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. /// 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/Asn1Parser/Universal/Asn1BitString.cs b/Asn1Parser/Universal/Asn1BitString.cs index d05b6f4..dcec6bc 100644 --- a/Asn1Parser/Universal/Asn1BitString.cs +++ b/Asn1Parser/Universal/Asn1BitString.cs @@ -23,35 +23,31 @@ 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(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + 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. /// /// 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) { 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. /// A number of unused bits in bit string. - /// valueToEncode parameter is null reference. - public Asn1BitString(Byte[] valueToEncode, Byte unusedBits) : base(TYPE) { - if (valueToEncode == null) { throw new ArgumentNullException(nameof(valueToEncode)); } + public Asn1BitString(ReadOnlySpan valueToEncode, Byte unusedBits) : base(TYPE) { m_encode(valueToEncode, false, unusedBits); } @@ -62,17 +58,18 @@ public Asn1BitString(Byte[] valueToEncode, Byte unusedBits) : base(TYPE) { /// /// Gets raw value of BIT_STRING without unused bits identifier. /// + [Obsolete("Use 'GetValue()' method instead.")] 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(Asn1Utils.EncodeAsReader(v, TYPE)); } /// @@ -82,11 +79,18 @@ void m_encode(Byte[] 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().Slice(1); + } /// /// Calculates the number of bits left unused in the final byte of content. @@ -94,12 +98,8 @@ 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) { - if (bytes == null) { - throw new ArgumentNullException(nameof(bytes)); - } - - return CalculateUnusedBits(bytes[bytes.Length - 1]); + public static Byte CalculateUnusedBits(ReadOnlySpan bytes) { + 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 07d290e..344d47f 100644 --- a/Asn1Parser/Universal/Asn1BmpString.cs +++ b/Asn1Parser/Universal/Asn1BmpString.cs @@ -22,13 +22,13 @@ 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. + /// ASN.1-encoded memory buffer. /// /// rawData is not BMPString data type. /// - public Asn1BMPString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1BMPString(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of the Asn1BMPString class from a unicode string. /// @@ -39,14 +39,9 @@ public Asn1BMPString(String inputString) : base(TYPE) { void m_encode(String inputString) { Value = inputString; - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.BigEndianUnicode.GetBytes(inputString), TYPE))); + Initialize(Asn1Utils.EncodeAsReader(Encoding.BigEndianUnicode.GetBytes(inputString).AsSpan(), TYPE)); } 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/Asn1Boolean.cs b/Asn1Parser/Universal/Asn1Boolean.cs index e10d970..ebd2645 100644 --- a/Asn1Parser/Universal/Asn1Boolean.cs +++ b/Asn1Parser/Universal/Asn1Boolean.cs @@ -20,13 +20,13 @@ 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. + /// ASN.1-encoded memory buffer. /// /// rawData is not valid BOOLEAN data type. /// - public Asn1Boolean(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1Boolean(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of the Asn1Boolean class from a boolean value. /// @@ -42,7 +42,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 cdb9146..5077bd0 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); @@ -43,18 +43,18 @@ 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; 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(Byte[] rawData) { + 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/Asn1Enumerated.cs b/Asn1Parser/Universal/Asn1Enumerated.cs index 50291f0..96cd5bc 100644 --- a/Asn1Parser/Universal/Asn1Enumerated.cs +++ b/Asn1Parser/Universal/Asn1Enumerated.cs @@ -24,13 +24,13 @@ 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. + /// ASN.1-encoded memory buffer. /// /// rawData is not valid INTEGER data type. /// - public Asn1Enumerated(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1Enumerated(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of the Asn1Enumerated class from an integer value. /// @@ -46,13 +46,13 @@ 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()); 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 diff --git a/Asn1Parser/Universal/Asn1GeneralizedTime.cs b/Asn1Parser/Universal/Asn1GeneralizedTime.cs index 6948b28..84a4c83 100644 --- a/Asn1Parser/Universal/Asn1GeneralizedTime.cs +++ b/Asn1Parser/Universal/Asn1GeneralizedTime.cs @@ -40,12 +40,12 @@ 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. + /// ASN.1-encoded memory buffer. /// /// The current state of ASN1 object is not Generalized Time. /// - public Asn1GeneralizedTime(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + 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 f40578c..89537d3 100644 --- a/Asn1Parser/Universal/Asn1IA5String.cs +++ b/Asn1Parser/Universal/Asn1IA5String.cs @@ -27,16 +27,16 @@ 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. + /// ASN.1-encoded memory buffer. /// /// rawData is not IA5String data type. /// /// /// Input data contains invalid IA5String character. /// - public Asn1IA5String(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1IA5String(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of the Asn1IA5String class from a unicode string. /// @@ -53,7 +53,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(Asn1Utils.EncodeAsReader(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE)); } void m_decode(Asn1Reader asn) { if (asn.GetPayload().Any(b => b > 127)) { @@ -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/Asn1Integer.cs b/Asn1Parser/Universal/Asn1Integer.cs index f933723..f831d0e 100644 --- a/Asn1Parser/Universal/Asn1Integer.cs +++ b/Asn1Parser/Universal/Asn1Integer.cs @@ -23,13 +23,13 @@ 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. + /// ASN.1-encoded memory buffer. /// /// rawData is not valid INTEGER data type. /// - public Asn1Integer(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1Integer(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of the Asn1Integer class from an integer value. /// @@ -45,7 +45,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/Asn1Null.cs b/Asn1Parser/Universal/Asn1Null.cs index 217a352..1716936 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 @@ -27,13 +27,13 @@ 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. + /// ASN.1-encoded memory buffer. /// /// rawData is not valid NULL data type. /// - public Asn1Null(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + 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 4b46a84..077a72f 100644 --- a/Asn1Parser/Universal/Asn1NumericString.cs +++ b/Asn1Parser/Universal/Asn1NumericString.cs @@ -27,16 +27,16 @@ 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. + /// ASN.1-encoded memory buffer. /// /// rawData is not NumericString data type. /// /// /// Input data contains invalid NumericString character. /// - public Asn1NumericString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1NumericString(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of the Asn1NumericString class from a unicode string. /// @@ -53,7 +53,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(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)) { @@ -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/Asn1ObjectIdentifier.cs b/Asn1Parser/Universal/Asn1ObjectIdentifier.cs index 3a6fd71..7001cf1 100644 --- a/Asn1Parser/Universal/Asn1ObjectIdentifier.cs +++ b/Asn1Parser/Universal/Asn1ObjectIdentifier.cs @@ -28,11 +28,11 @@ 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(new Asn1Reader(rawData)) { } + /// 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 /// that represents object identifier value. @@ -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); @@ -63,7 +63,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; } @@ -74,10 +74,10 @@ 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 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]; @@ -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/Asn1OctetString.cs b/Asn1Parser/Universal/Asn1OctetString.cs index f59d878..7c5bb48 100644 --- a/Asn1Parser/Universal/Asn1OctetString.cs +++ b/Asn1Parser/Universal/Asn1OctetString.cs @@ -24,9 +24,9 @@ public Asn1OctetString(Asn1Reader asn) : base(asn, TYPE) { Value = asn.GetPayload(); } /// - /// 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. + /// 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. @@ -34,7 +34,7 @@ public Asn1OctetString(Asn1Reader asn) : base(asn, TYPE) { /// /// Input data contains invalid NumericString character. /// - public Asn1OctetString(Byte[] rawData, Boolean tagged) : base(TYPE) { + public Asn1OctetString(ReadOnlyMemory rawData, Boolean tagged) : base(TYPE) { if (tagged) { var asn = new Asn1Reader(rawData); if (asn.Tag != Tag) { @@ -43,13 +43,21 @@ public Asn1OctetString(Byte[] rawData, Boolean tagged) : base(TYPE) { Value = asn.GetPayload(); Initialize(asn); } else { - Value = rawData; - Initialize(new Asn1Reader(Asn1Utils.Encode(rawData, TYPE))); + Value = rawData.ToArray(); + Initialize(Asn1Utils.EncodeAsReader(rawData.Span, TYPE)); } } /// /// Gets value associated with the current object. /// + [Obsolete("Use 'GetValue()' method instead.")] public Byte[] Value { get; private set; } + + /// + /// Gets value associated with the current object. + /// + public ReadOnlyMemory GetValue() { + return GetInternalReader().GetPayloadAsMemory(); + } } \ No newline at end of file diff --git a/Asn1Parser/Universal/Asn1PrintableString.cs b/Asn1Parser/Universal/Asn1PrintableString.cs index ccd4522..e62d41d 100644 --- a/Asn1Parser/Universal/Asn1PrintableString.cs +++ b/Asn1Parser/Universal/Asn1PrintableString.cs @@ -29,16 +29,16 @@ 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. + /// ASN.1-encoded memory buffer. /// /// rawData is not PrintableString data type. /// /// /// Input data contains invalid PrintableString character. /// - public Asn1PrintableString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1PrintableString(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of the Asn1PrintableString class from a unicode string. /// @@ -55,7 +55,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(Asn1Utils.EncodeAsReader(Encoding.ASCII.GetBytes(inputString).AsSpan(), TYPE)); } void m_decode(Asn1Reader asn) { if (!testValue(asn.GetPayload())) { @@ -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/Asn1RelativeOid.cs b/Asn1Parser/Universal/Asn1RelativeOid.cs index a1c104d..ce7da22 100644 --- a/Asn1Parser/Universal/Asn1RelativeOid.cs +++ b/Asn1Parser/Universal/Asn1RelativeOid.cs @@ -24,11 +24,11 @@ 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(new Asn1Reader(rawData)) { } + /// 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 /// that represents relative object identifier value. @@ -41,7 +41,7 @@ public Asn1RelativeOid(Byte[] rawData) : this(new Asn1Reader(rawData)) { } /// 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); @@ -59,7 +59,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; } @@ -70,9 +70,9 @@ 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 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/Asn1String.cs b/Asn1Parser/Universal/Asn1String.cs index 5608d1e..2efd522 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. /// @@ -61,19 +83,16 @@ protected Asn1String(Asn1Reader asn, Asn1Type? type) : base(asn, type) { } /// /// 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])) { + 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."); } - 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 572c6c3..cc6c225 100644 --- a/Asn1Parser/Universal/Asn1TeletexString.cs +++ b/Asn1Parser/Universal/Asn1TeletexString.cs @@ -15,27 +15,25 @@ 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. /// /// /// 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. + /// Initializes a new instance of Asn1TeletexString from an ASN.1-encoded memory buffer. /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// rawData parameter represents different data type. /// /// /// Input data contains invalid TeletexString character. /// - public Asn1TeletexString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1TeletexString(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of Asn1TeletexString from a string that contains valid /// Teletex String characters. @@ -53,17 +51,24 @@ 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)) { - 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..7d5e4f3 100644 --- a/Asn1Parser/Universal/Asn1UTF8String.cs +++ b/Asn1Parser/Universal/Asn1UTF8String.cs @@ -27,16 +27,16 @@ 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. + /// ASN.1-encoded memory buffer. /// /// rawData is not UTF8String data type. /// /// /// Input data contains invalid UTF8String character. /// - public Asn1UTF8String(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1UTF8String(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of the Asn1UTF8String class from a unicode string. /// @@ -53,7 +53,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(Asn1Utils.EncodeAsReader(Encoding.UTF8.GetBytes(inputString).AsMemory().Span, TYPE)); } void m_decode(Asn1Reader asn) { Value = Encoding.UTF8.GetString(asn.GetPayload()); @@ -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/Asn1Universal.cs b/Asn1Parser/Universal/Asn1Universal.cs index ac8393a..cca4292 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 @@ -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) { @@ -74,11 +74,13 @@ 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. /// - [Obsolete("Use 'GetRawData()' method instead.", true)] + [Obsolete("Use 'GetRawDataAsMemory()' method instead.", true)] public Byte[] RawData => GetRawData(); /// @@ -86,10 +88,17 @@ 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; + IsConstructed = 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. @@ -101,7 +110,7 @@ protected void Initialize(Asn1Reader asn) { /// /// Decoded type value. public virtual String GetDisplayValue() { - return asnReader == null + return asnReader is null ? String.Empty : AsnFormatter.BinaryToString(asnReader, EncodingType.HexRaw, EncodingFormat.NOCRLF); } @@ -111,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); } @@ -119,7 +128,22 @@ 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(); } + /// + /// Gets the full tag raw data, including header and payload information. + /// + /// ASN.1-encoded type as span. + 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 diff --git a/Asn1Parser/Universal/Asn1UniversalString.cs b/Asn1Parser/Universal/Asn1UniversalString.cs index a31142a..0ffdbe0 100644 --- a/Asn1Parser/Universal/Asn1UniversalString.cs +++ b/Asn1Parser/Universal/Asn1UniversalString.cs @@ -24,13 +24,13 @@ 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. + /// ASN.1-encoded memory buffer. /// /// rawData is not UniversalString data type. /// - public Asn1UniversalString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1UniversalString(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of the Asn1UniversalString class from a unicode string. /// @@ -44,14 +44,13 @@ 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()); } - - /// - public override String GetDisplayValue() { - return Value; - } } \ No newline at end of file diff --git a/Asn1Parser/Universal/Asn1UtcTime.cs b/Asn1Parser/Universal/Asn1UtcTime.cs index 28b8df6..fce3068 100644 --- a/Asn1Parser/Universal/Asn1UtcTime.cs +++ b/Asn1Parser/Universal/Asn1UtcTime.cs @@ -40,12 +40,12 @@ 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. + /// ASN.1-encoded memory buffer. /// /// The current state of ASN1 object is not UTC time. /// - public Asn1UtcTime(Byte[] rawData) : base(new Asn1Reader(rawData), TYPE) { } + 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 557cbf1..2015272 100644 --- a/Asn1Parser/Universal/Asn1VideotexString.cs +++ b/Asn1Parser/Universal/Asn1VideotexString.cs @@ -21,20 +21,18 @@ 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. + /// Initializes a new instance of Asn1VideotexString from a ASN.1-encoded memory buffer. /// - /// ASN.1-encoded byte array. + /// ASN.1-encoded memory buffer. /// /// rawData is not VideotexString data type. /// /// /// Input data contains invalid VideotexString character. /// - public Asn1VideotexString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1VideotexString(ReadOnlyMemory rawData) : base(new Asn1Reader(rawData), TYPE) { } /// /// Initializes a new instance of the Asn1VideotexString class from a unicode string. /// @@ -47,15 +45,15 @@ public Asn1VideotexString(String inputString) : base(TYPE) { } void encode(String inputString) { - Initialize(new Asn1Reader(Asn1Utils.Encode(Encoding.ASCII.GetBytes(inputString), TYPE))); + Initialize(Asn1Utils.EncodeAsReader(Encoding.ASCII.GetBytes(inputString).AsSpan(), 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..cc41f79 100644 --- a/Asn1Parser/Universal/Asn1VisibleString.cs +++ b/Asn1Parser/Universal/Asn1VisibleString.cs @@ -29,16 +29,16 @@ 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. + /// ASN.1-encoded memory buffer. /// /// rawData is not VisibleString data type. /// /// /// Input data contains invalid VisibleString character. /// - public Asn1VisibleString(Byte[] rawData) : this(new Asn1Reader(rawData)) { } + public Asn1VisibleString(ReadOnlyMemory rawData) : this(new Asn1Reader(rawData)) { } /// /// Initializes a new instance of the Asn1VisibleString class from a unicode string. /// @@ -55,17 +55,12 @@ 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(Asn1Utils.EncodeAsReader(Encoding.ASCII.GetBytes(inputString).AsSpan(), 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 diff --git a/Asn1Parser/Utils/CLRExtensions/BigIntegerExtensions.cs b/Asn1Parser/Utils/CLRExtensions/BigIntegerExtensions.cs index c83d702..bac1021 100644 --- a/Asn1Parser/Utils/CLRExtensions/BigIntegerExtensions.cs +++ b/Asn1Parser/Utils/CLRExtensions/BigIntegerExtensions.cs @@ -7,13 +7,13 @@ 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. /// /// 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/CLRExtensions/EncodingFormatExtensions.cs b/Asn1Parser/Utils/CLRExtensions/EncodingFormatExtensions.cs new file mode 100644 index 0000000..74bc510 --- /dev/null +++ b/Asn1Parser/Utils/CLRExtensions/EncodingFormatExtensions.cs @@ -0,0 +1,23 @@ +using System; + +namespace SysadminsLV.Asn1Parser.Utils.CLRExtensions; + +/// +/// Contains extension methods for enumeration. +/// +internal 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)); + } +} diff --git a/Asn1Parser/Utils/DateTimeUtils.cs b/Asn1Parser/Utils/DateTimeUtils.cs index 7402242..704c255 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 @@ -20,7 +20,7 @@ public static Byte[] Encode(DateTime time, ref TimeZoneInfo? zone, Boolean UTC, 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/nuget/SysadminsLV.Asn1Parser.nuspec b/nuget/SysadminsLV.Asn1Parser.nuspec index 471c119..bd43450 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 + + + + + + + + + + + + + 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/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..09ffa3f --- /dev/null +++ b/tests/Asn1Parser.Benchmark/Asn1ReaderBenchmark.cs @@ -0,0 +1,31 @@ +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()); + } +} \ 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..c149845 --- /dev/null +++ b/tests/Asn1Parser.Benchmark/Program.cs @@ -0,0 +1,12 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Running; + +namespace Asn1Parser.Benchmark; + +internal class Program { + static void Main(String[] args) { + IConfig config = DefaultConfig.Instance; + //Summary summary = BenchmarkRunner.Run(config); + var 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 0000000..8b54155 Binary files /dev/null and b/tests/Asn1Parser.Benchmark/Properties/Resources/msitwww2.crl differ diff --git a/tests/Asn1Parser.Benchmark/Resources/level1m.crl b/tests/Asn1Parser.Benchmark/Resources/level1m.crl new file mode 100644 index 0000000..550e311 Binary files /dev/null and b/tests/Asn1Parser.Benchmark/Resources/level1m.crl differ diff --git a/tests/Asn1Parser.Benchmark/Resources/msitwww2.crl b/tests/Asn1Parser.Benchmark/Resources/msitwww2.crl new file mode 100644 index 0000000..8b54155 Binary files /dev/null and b/tests/Asn1Parser.Benchmark/Resources/msitwww2.crl differ 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)); + } +} diff --git a/tests/Asn1Parser.Tests/Asn1DateTimeTests.cs b/tests/Asn1Parser.Tests/Asn1DateTimeTests.cs index a200747..8981be6 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,14 +65,14 @@ 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) { + 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); @@ -81,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/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/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); 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())); + } +} 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 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..87b65a7 100644 --- a/tests/Asn1Parser.Tests/BinaryToHexStringTests.cs +++ b/tests/Asn1Parser.Tests/BinaryToHexStringTests.cs @@ -1,61 +1,128 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Runtime.InteropServices; +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); - 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() { - System.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() { - System.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() { - System.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() { - System.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() { - System.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() { - System.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() { - System.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() { + runTest(_complexRawData, EncodingType.HexRaw, COMPLEX_HEX_RAW, true); + } + [TestMethod] + public void TestComplexHex() { + runTest(_complexRawData, EncodingType.Hex, COMPLEX_HEX, true); + } + [TestMethod] + public void TestComplexHexAddr() { + runTest(_complexRawData, EncodingType.HexAddress, COMPLEX_HEX_ADDR, true); + } + [TestMethod] + public void TestComplexHexAscii() { + runTest(_complexRawData, EncodingType.HexAscii, COMPLEX_HEX_ASCII, false); + } + [TestMethod] + public void TestComplexHexAddrAscii() { + 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 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()); + } +} 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;