diff --git a/CHANGELOG.md b/CHANGELOG.md index a33bb9f7..d3957ead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ ### 3.10.0 NEXT (Supreme 0.5.0 NEXT) +The public API remains unchanged, except for some methods migrating from a ByteIterator to kotlinx-io Source +and some newly added. The internals have changed substantially, however. +Be sure to match Signum versions if multiple libraries pull it in as transitive dependency. +Better safe than sorry! + +* KmmResult 1.9.0 * Introduce generic tag assertion to `Asn1Element` * Change CSR to take an actual `CryptoSignature` instead of a ByteArray * Introduce shorthand to create CSR from TbsCSR @@ -13,6 +19,11 @@ * Base OIDs on BigInteger instead of UInt * Directly support UUID-based OID creation * Implement hash-to-curve and hash-to-scalar as per RFC9380 +* Use kotlinx-io as primary source for parsing + * Base number encoding/decoding on koltinx-io + * Remove parsing from iterator + * Base ASN.1 encoding and decoding on kotlinx-io + * Remove single element decoding from Iterator ### 3.9.0 (Supreme 0.4.0) @@ -300,7 +311,7 @@ the Tag class just cannot be directly accessed from Swift and ObjC any more. * Proper BIT STRING * BitSet (100% Kotlin BitSet implementation) * Recursively parsing (and encapsulating) ASN.1 structures in OCTET Strings -* Initial pretty-printing of ASN.1 Structures +* Initial pretty-printing of ASN.1 Strucutres * Massive ASN.1 builder DSL streamlining * More convenient explicit tagging diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9ad245c0..459dd454 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ jose = "9.31" kotlinpoet = "1.16.0" runner = "1.5.2" kotest-plugin = "20240918.002009-71" -kmmresult = "1.8.0" +kmmresult = "1.9.0" [libraries] bignum = { group = "com.ionspin.kotlin", name = "bignum", version.ref = "bignum" } diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Elements.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Elements.kt index 52e6037c..fccd4a54 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Elements.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Elements.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalStdlibApi::class) + package at.asitplus.signum.indispensable.asn1 import at.asitplus.catching @@ -24,8 +26,7 @@ import kotlin.native.ObjCName */ @Serializable(with = Asn1EncodableSerializer::class) sealed class Asn1Element( - internal val tlv: TLV, - protected open val children: List? + val tag: Tag ) { override fun equals(other: Any?): Boolean { @@ -33,16 +34,10 @@ sealed class Asn1Element( if (other == null) return false if (other !is Asn1Element) return false if (tag != other.tag) return false - if (!content.contentEquals(other.content)) return false if (this is Asn1Structure && other !is Asn1Structure) return false if (this is Asn1Primitive && other !is Asn1Primitive) return false - return if (this is Asn1Primitive) { - (this.content contentEquals other.content) - } else { - this as Asn1Structure - other as Asn1Structure - children == other.children - } + return true + } companion object { @@ -68,73 +63,54 @@ sealed class Asn1Element( * For a primitive, this is just the size of the held bytes. * For a structure, it is the sum of the number of bytes needed to encode all held child nodes. */ - val length: Int by lazy { - children?.fold(0) { acc, extendedTlv -> acc + extendedTlv.overallLength } ?: tlv.contentLength - } + abstract val length: Int /** * Total number of bytes required to represent the ths element, when encoding to ASN.1. */ - val overallLength by lazy { length + tlv.tag.encodedTagLength + encodedLength.size } - - protected open val content by lazy { tlv.content } + val overallLength by lazy { length + tag.encodedTagLength + encodedLength.size } - val tag by lazy { tlv.tag } + private val derEncodedLazy = lazy { Buffer().also { encodeTo(it) }.readByteArray() } - private val derEncodedLazy = lazy { Buffer().also { it.writeAsn1Element(this) }.readByteArray() } + /** + * Lazily-evaluated DER-encoded representation of this ASN.1 element + */ val derEncoded: ByteArray by derEncodedLazy - private fun Sink.writeAsn1Element(element: Asn1Element) { - if (element.derEncodedLazy.isInitialized()) { - write(element.derEncoded) - return - } - write(element.tlv.tag.encodedTag); - write(element.encodedLength); - if (element.children != null) { // structure - element.children!!.forEach { writeAsn1Element(it) } - } else { // primitive - write(element.tlv.content) + protected abstract fun doEncode(sink: Sink) + internal fun encodeTo(sink: Sink) { + if (derEncodedLazy.isInitialized()) { + sink.write(derEncoded) + return } + doEncode(sink) } - override fun toString(): String = "(tag=${tlv.tag}" + - ", length=${length}" + - ", overallLength=${overallLength}" + - (children?.let { ", children=$children" } ?: ", content=${ - content.encodeToString(Base16 { - lineBreakInterval = 0;encodeToLowercase = false - }) - }") + - ")" + + override fun toString(): String = prettyPrintHeader(0) + contentToString() + prettyPrintTrailer(0) + + protected abstract fun contentToString(): String fun prettyPrint() = prettyPrint(0) - protected open fun prettyPrint(indent: Int): String = "(tag=${tlv.tag}" + - ", length=${length}" + - ", overallLength=${overallLength}" + - ((children?.joinToString( - prefix = ")\n" + (" " * indent) + "{\n", - separator = "\n", - postfix = "\n" + (" " * indent) + "}" - ) { it.prettyPrint(indent + 2) }) ?: ", content=${ - content.encodeToString(Base16 { - lineBreakInterval = 0;encodeToLowercase = false - }) - })") - - - protected operator fun String.times(op: Int): String { - var s = this - kotlin.repeat(op) { s += this } - return s - } + protected open fun prettyPrintHeader(indent: Int) = + "(tag=${tag}" + ", length=${length}" + ", overallLength=${overallLength}) " + + protected open fun prettyPrintTrailer(indent: Int) = "" + protected abstract fun prettyPrintContents(indent: Int): String + + internal open fun prettyPrint(indent: Int): String = + prettyPrintHeader(indent) + prettyPrintContents(indent) + prettyPrintTrailer(indent) + + + protected operator fun String.times(op: Int): String = repeat(op) + /** - * Convenience method to directly produce an HEX string of this element's ANS.1 representation + * Convenience method to directly produce an HEX string of this element's ASN.1 representation */ fun toDerHexString(lineLen: Byte? = null) = derEncoded.encodeToString(Base16 { lineLen?.let { @@ -142,11 +118,6 @@ sealed class Asn1Element( } }) - override fun hashCode(): Int { - var result = tlv.hashCode() - result = 31 * result + (children?.hashCode() ?: 0) - return result - } /** * Convenience function to cast this element to an [Asn1Primitive] @@ -215,9 +186,17 @@ sealed class Asn1Element( children, tag.tagValue, tag.tagClass, - sortChildren = isSorted - ) else Asn1CustomStructure.asPrimitive(children, tag.tagValue, tag.tagClass) + sortChildren = false, + shouldBeSorted = shouldBeSorted + ) else Asn1CustomStructure.asPrimitive( + children, + tag.tagValue, + tag.tagClass, + sortChildren = false, + shouldBeSorted = shouldBeSorted + ) } + is Asn1Primitive -> Asn1Primitive(tag without CONSTRUCTED, content) } @@ -248,6 +227,8 @@ sealed class Asn1Element( ) } + override fun hashCode(): Int = tag.hashCode() + @Serializable @ConsistentCopyVisibility @@ -256,13 +237,14 @@ sealed class Asn1Element( @Serializable(with = ByteArrayBase64Serializer::class) val encodedTag: ByteArray ) : Comparable { - - constructor(derEncoded: ByteArray) : this(derEncoded.iterator().decodeTag()) - //workaround because we cannot return two values or assign params in a destructured manner private constructor(decoded: Pair) : this(decoded.first, decoded.second) + /** + * The length (in bytes) of this tag when encoded according to DER + */ val encodedTagLength: Int = encodedTag.size + /** * Creates a copy of this tag, overriding [tagValue], but keeping [isConstructed] and [tagClass] */ @@ -458,13 +440,24 @@ sealed class Asn1Structure( * from DER-encoded structures, because this has a chance of altering the structure for non-conforming DER-encoded * structures. */ - val isSorted: Boolean = false + sortChildren: Boolean, + + /** + * Indicates whether this structure should sort their child nodes by default. This is true for SET and for + * all custom structure that enforce SET semantics. Note that it is impossible to infer this property correctly when + * parsing custom structures. Therefore, it has no impact on [equals]. + */ + val shouldBeSorted: Boolean ) : - Asn1Element(TLV(tag, byteArrayOf()), if (!isSorted) children else children.sortedBy { it.tag }) { + Asn1Element(tag) { + val children: List = if (!sortChildren) children else children.sortedBy { it.tag } - public override val children: List - get() = super.children!! + /** + * indicated whether the structure's children are actually sorted. + * This could be false for parsing non-compliant SETs, for example. + */ + val isActuallySorted: Boolean by if (sortChildren) lazyOf(true) else lazy { children.sortedBy { it.tag } == children } private var index = 0 @@ -491,6 +484,47 @@ sealed class Asn1Structure( */ fun peek() = if (!hasMoreChildren()) null else children[index] + + override val length: Int by lazy { children.fold(0) { acc, child -> acc + child.overallLength } } + + override fun doEncode(sink: Sink) { + children.let { childElems -> + sink.write(tag.encodedTag); + sink.write(encodedLength); + childElems.forEach { child -> child.encodeTo(sink) } + } + } + + override fun prettyPrintContents(indent: Int): String = + children.joinToString( + prefix = "\n" + (" " * indent) + "{\n", + separator = "\n", + postfix = "\n" + (" " * indent) + "}" + ) { it.prettyPrint(indent + 2) } + + override fun contentToString(): String { + val prefix = when { + shouldBeSorted && isActuallySorted -> "SORTED" + shouldBeSorted && !isActuallySorted -> "NON-COMPLIANT (UNSORTED)" + else -> "" + } + return "$prefix, children=$children" + } + + override fun hashCode() = 31 * super.hashCode() + children.hashCode() + + /** + * the [shouldBeSorted] flag has no bearing on equals! + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Asn1Structure) return false + if (!super.equals(other)) return false + + if (children != other.children) return false + + return true + } } /** @@ -504,7 +538,12 @@ class Asn1ExplicitlyTagged * */ internal constructor(tag: ULong, children: List) : - Asn1Structure(Tag(tag, constructed = true, tagClass = TagClass.CONTEXT_SPECIFIC), children) { + Asn1Structure( + Tag(tag, constructed = true, tagClass = TagClass.CONTEXT_SPECIFIC), + children, + sortChildren = false, + shouldBeSorted = false + ) { /** @@ -537,7 +576,7 @@ internal constructor(tag: ULong, children: List) : fun verifyTagOrNull(explicitTag: Tag) = catching { verifyTag(explicitTag) }.getOrNull() override fun toString() = "Tagged" + super.toString() - override fun prettyPrint(indent: Int) = (" " * indent) + "Tagged" + super.prettyPrint(indent + 2) + override fun prettyPrintHeader(indent: Int) = (" " * indent) + "Tagged" + super.prettyPrintHeader(indent) } /** @@ -545,7 +584,7 @@ internal constructor(tag: ULong, children: List) : * @param children the elements to put into this sequence */ class Asn1Sequence internal constructor(children: List) : - Asn1Structure(Tag.SEQUENCE, children) { + Asn1Structure(Tag.SEQUENCE, children, sortChildren = false, shouldBeSorted = false) { init { if (!tag.isConstructed) throw IllegalArgumentException("An ASN.1 Structure must have a CONSTRUCTED tag") @@ -553,29 +592,32 @@ class Asn1Sequence internal constructor(children: List) : } override fun toString() = "Sequence" + super.toString() - override fun prettyPrint(indent: Int) = (" " * indent) + "Sequence" + super.prettyPrint(indent + 2) + override fun prettyPrintHeader(indent: Int) = (" " * indent) + "Sequence" + super.prettyPrintHeader(indent) } /** * ASN1 structure (i.e. containing child nodes) with custom tag */ class Asn1CustomStructure private constructor( - tag: Tag, children: List, sortChildren: Boolean + tag: Tag, children: List, sortChildren: Boolean, shouldBeSorted: Boolean ) : - Asn1Structure(tag, children, sortChildren) { + Asn1Structure(tag, children, sortChildren, shouldBeSorted) { /** * ASN.1 CONSTRUCTED with custom tag * @param children the elements to put into this sequence * @param tag the custom tag to use * @param tagClass the tag class to use for this custom tag. defaults to [TagClass.UNIVERSAL] * @param sortChildren whether to sort the passed child nodes. defaults to false + * @param shouldBeSorted whether the child nodes of this structure should be sorted according to this structure's definition. + * Note that this information is lost when parsing custom structures! */ constructor( children: List, tag: ULong, tagClass: TagClass = TagClass.UNIVERSAL, - sortChildren: Boolean = false - ) : this(Tag(tag, constructed = true, tagClass), children, sortChildren) + sortChildren: Boolean = false, + shouldBeSorted: Boolean = false + ) : this(Tag(tag, constructed = true, tagClass), children, sortChildren, shouldBeSorted) /** * ASN.1 CONSTRUCTED with custom tag @@ -583,25 +625,37 @@ class Asn1CustomStructure private constructor( * @param tag the custom tag to use * @param tagClass the tag class to use for this custom tag. defaults to [TagClass.UNIVERSAL] * @param sortChildren whether to sort the passed child nodes. defaults to false + * @param shouldBeSorted whether the child nodes of this structure should be sorted according to this structure's definition. + * Note that this information is lost when parsing custom structures! */ constructor( children: List, tag: UByte, tagClass: TagClass = TagClass.UNIVERSAL, - sortChildren: Boolean = false - ) : this(children, tag.toULong(), tagClass, sortChildren) + sortChildren: Boolean = false, + shouldBeSorted: Boolean = false + ) : this(children, tag.toULong(), tagClass, sortChildren, shouldBeSorted) - override val content: ByteArray by lazy { + /** + * Raw byte DER-encoded representation of this custom structure's children. + * This property is `null` **unless** the `CONSTRUCTED` flag of this structure's tag is overridden to `false` + */ + val content: ByteArray? by lazy { if (!tag.isConstructed) children.fold(byteArrayOf()) { acc, asn1Element -> acc + asn1Element.derEncoded } - else super.content + else null } override fun toString() = "${tag.tagClass}" + super.toString() - override fun prettyPrint(indent: Int) = - (" " * indent) + tag.tagClass + " ${tag.tagValue} " + super.prettyPrint(indent + 2) + override fun prettyPrintHeader(indent: Int) = + (" " * indent) + tag.tagClass + + " ${tag.tagValue}" + + (if (!tag.isConstructed) " PRIMITIVE" else "") + + " (=${tag.encodedTag.encodeToString(Base16)}), length=${length}" + + ", overallLength=${overallLength}" + + content?.let { " ${it.toHexString(HexFormat.UpperCase)}" } companion object { /** @@ -609,15 +663,21 @@ class Asn1CustomStructure private constructor( * @param children the elements to put into this sequence * @param tag the custom tag to use * @param tagClass the tag class to use for this custom tag. defaults to [TagClass.UNIVERSAL] - * @param sort whether to sort the passed childr nodes. defaults to false + * @param sortChildren whether to sort the passed child nodes. defaults to false */ fun asPrimitive( children: List, tag: ULong, tagClass: TagClass = TagClass.UNIVERSAL, - sort: Boolean = false + sortChildren: Boolean = false, + shouldBeSorted: Boolean = false ) = - Asn1CustomStructure(Tag(tag, constructed = false, tagClass), children, sort) + Asn1CustomStructure( + Tag(tag, constructed = false, tagClass), + children, + sortChildren, + shouldBeSorted = shouldBeSorted + ) } } @@ -628,7 +688,7 @@ class Asn1CustomStructure private constructor( @Suppress("SERIALIZER_TYPE_INCOMPATIBLE") @Serializable(with = Asn1EncodableSerializer::class) class Asn1EncapsulatingOctetString(children: List) : - Asn1Structure(Tag.OCTET_STRING, children), + Asn1Structure(Tag.OCTET_STRING, children, sortChildren = false, shouldBeSorted = false), Asn1OctetString { override val content: ByteArray by lazy { children.fold(byteArrayOf()) { acc, asn1Element -> acc + asn1Element.derEncoded } @@ -639,8 +699,8 @@ class Asn1EncapsulatingOctetString(children: List) : override fun toString() = "OCTET STRING Encapsulating" + super.toString() - override fun prettyPrint(indent: Int) = - (" " * indent) + "OCTET STRING Encapsulating" + super.prettyPrint(indent + 2) + override fun prettyPrintHeader(indent: Int) = + (" " * indent) + "OCTET STRING Encapsulating" + super.prettyPrintHeader(indent) + content.toHexString(HexFormat.UpperCase) } /** @@ -650,13 +710,11 @@ class Asn1EncapsulatingOctetString(children: List) : class Asn1PrimitiveOctetString(content: ByteArray) : Asn1Primitive(Tag.OCTET_STRING, content), Asn1OctetString { - override val content: ByteArray get() = super.content - override fun unwrap() = this override fun toString() = "OCTET STRING " + super.toString() - override fun prettyPrint(indent: Int) = (" " * indent) + "OCTET STRING Primitive" + tlv.toString().substring(3) + override fun prettyPrintHeader(indent: Int) = (" " * indent) + "OCTET STRING" + super.prettyPrintHeader(0) } @@ -664,7 +722,7 @@ class Asn1PrimitiveOctetString(content: ByteArray) : Asn1Primitive(Tag.OCTET_STR * ASN.1 SET 0x31 ([BERTags.SET] OR [BERTags.CONSTRUCTED]) */ open class Asn1Set private constructor(children: List, dontSort: Boolean) : - Asn1Structure(Tag.SET, children, !dontSort) { + Asn1Structure(Tag.SET, children, !dontSort, shouldBeSorted = true) { /** * @param children the elements to put into this set. will be automatically sorted by tag @@ -673,13 +731,12 @@ open class Asn1Set private constructor(children: List, dontSort: Bo init { if (!tag.isConstructed) throw IllegalArgumentException("An ASN.1 Structure must have a CONSTRUCTED tag") - } override fun toString() = "Set" + super.toString() - override fun prettyPrint(indent: Int) = (" " * indent) + "Set" + super.prettyPrint(indent + 2) + override fun prettyPrintHeader(indent: Int) = (" " * indent) + "Set" + super.prettyPrintHeader(indent) companion object { /** @@ -703,24 +760,48 @@ class Asn1SetOf @Throws(Asn1Exception::class) internal constructor(children: Lis /** * ASN.1 primitive. Hold o children, but [content] under [tag] */ -open class Asn1Primitive(tag: Tag, content: ByteArray) : Asn1Element(TLV(tag, content), null) { +open class Asn1Primitive( + tag: Tag, + /** + * Raw data contained in this ASN.1 primitive in its encoded form. Requires decoding to interpret it + */ + val content: ByteArray +) : Asn1Element(tag) { init { if (tag.isConstructed) throw IllegalArgumentException("A primitive cannot have a CONSTRUCTED tag") } + override val length: Int get() = content.size + override fun doEncode(sink: Sink) { + sink.write(tag.encodedTag) + sink.write(encodedLength) + sink.write(content) + } + + override fun toString() = "Primitive" + super.toString() constructor(tagValue: ULong, content: ByteArray) : this(Tag(tagValue, false), content) constructor(tagValue: UByte, content: ByteArray) : this(tagValue.toULong(), content) - override fun prettyPrint(indent: Int) = (" " * indent) + "Primitive" + super.prettyPrint(indent) + override fun prettyPrintHeader(indent: Int) = (" " * indent) + "Primitive" + super.prettyPrintHeader(indent) + + override fun contentToString() = content.toHexString(HexFormat.UpperCase) + override fun prettyPrintContents(indent: Int) = contentToString() + + + override fun hashCode() = 31 * super.hashCode() + content.contentHashCode() + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Asn1Primitive) return false + if (!super.equals(other)) return false + + if (!content.contentEquals(other.content)) return false + + return true + } - /** - * Raw data contained in this ASN.1 primitive in its encoded form. Requires decoding to interpret it - */ - public override val content: ByteArray - get() = super.content } @@ -763,4 +844,20 @@ internal fun Int.encodeLength(): ByteArray { byteArrayOf((lengthLength or 0x80).toByte(), *length) } } +} + +@Throws(IllegalArgumentException::class) +internal fun Sink.encodeLength(len: Long): Int { + require(len >= 0) + return when { + (len < 0x80) -> writeByte(len.toByte()).run { 1 } /* short form */ + else -> { /* long form */ + val length = Buffer() + val lengthLength = length.writeMagnitudeLong(len) + check(lengthLength < 0x80) + writeByte((lengthLength or 0x80).toByte()) + length.transferTo(this) + 1 + lengthLength + } + } } \ No newline at end of file diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Exception.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Exception.kt index 6c7a11de..9df748f9 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Exception.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Exception.kt @@ -1,14 +1,18 @@ package at.asitplus.signum.indispensable.asn1 import at.asitplus.catching -import at.asitplus.wrapping +import at.asitplus.catchingUnwrappedAs open class Asn1Exception(message: String?, cause: Throwable?) : Throwable(message, cause) { constructor(message: String) : this(message, null) constructor(throwable: Throwable) : this(null, throwable) } -class Asn1TagMismatchException(val expected: Asn1Element.Tag, val actual: Asn1Element.Tag, detailedMessage: String? = null) : +class Asn1TagMismatchException( + val expected: Asn1Element.Tag, + val actual: Asn1Element.Tag, + detailedMessage: String? = null +) : Asn1Exception((detailedMessage?.let { "$it " } ?: "") + "Expected tag $expected, is: $actual") class Asn1StructuralException(message: String, cause: Throwable? = null) : Asn1Exception(message, cause) @@ -19,4 +23,4 @@ class Asn1OidException(message: String, val oid: ObjectIdentifier) : Asn1Excepti * Runs [block] inside [catching] and encapsulates any thrown exception in an [Asn1Exception] unless it already is one */ @Throws(Asn1Exception::class) -inline fun runRethrowing(block: () -> R) = wrapping(asA = ::Asn1Exception, block).getOrThrow() \ No newline at end of file +inline fun runRethrowing(block: () -> R) = catchingUnwrappedAs(::Asn1Exception, block).getOrThrow() \ No newline at end of file diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/ObjectIdentifier.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/ObjectIdentifier.kt index 95d971d7..cbe15eeb 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/ObjectIdentifier.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/ObjectIdentifier.kt @@ -5,6 +5,7 @@ import at.asitplus.signum.indispensable.asn1.encoding.decodeAsn1VarBigInt import at.asitplus.signum.indispensable.asn1.encoding.toAsn1VarInt import at.asitplus.signum.indispensable.asn1.encoding.toBigInteger import com.ionspin.kotlin.bignum.integer.BigInteger +import kotlinx.io.Buffer import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -38,14 +39,14 @@ class ObjectIdentifier @Throws(Asn1Exception::class) private constructor( throw Asn1Exception("Empty OIDs are not supported") bytes?.apply { - if(first().toUByte()>127u) throw Asn1Exception("OID top-level arc can only be number 0, 1 or 2") + if (first().toUByte() > 127u) throw Asn1Exception("OID top-level arc can only be number 0, 1 or 2") } nodes?.apply { if (size < 2) throw Asn1StructuralException("at least two nodes required!") if (first() > 2u) throw Asn1Exception("OID top-level arc can only be number 0, 1 or 2") - if(first()<2u) { + if (first() < 2u) { if (get(1) > 39u) throw Asn1Exception("Second segment must be <40") - }else { + } else { if (get(1) > 47u) throw Asn1Exception("Second segment must be <48") } forEach { if (it.isNegative) throw Asn1Exception("Negative Number encountered: $it") } @@ -79,12 +80,12 @@ class ObjectIdentifier @Throws(Asn1Exception::class) private constructor( collected += BigInteger.fromUInt(this.bytes[index].toUInt()) index++ } else { - val currentNode = mutableListOf() + val currentNode = Buffer() //todo: calculate only index and then operate on a byte view (unsafe wrapped). probably irrelevant since we'll roll our own while (this.bytes[index] < 0) { - currentNode += this.bytes[index] //+= parsed + currentNode.writeByte(this.bytes[index]) //+= parsed index++ } - currentNode += this.bytes[index] + currentNode.writeByte(this.bytes[index]) index++ collected += currentNode.decodeAsn1VarBigInt().first } diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/TLV.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/TLV.kt deleted file mode 100644 index 7656295e..00000000 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/TLV.kt +++ /dev/null @@ -1,44 +0,0 @@ -package at.asitplus.signum.indispensable.asn1 - -import io.matthewnelson.encoding.base16.Base16 -import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString - -internal data class TLV(val tag: Asn1Element.Tag, val content: ByteArray) { - - val encodedContentLength by lazy { contentLength.encodeLength() } - val contentLength: Int by lazy { content.size } - val overallLength: Int by lazy { contentLength + tag.encodedTagLength + encodedContentLength.size } - - val tagClass: TagClass get() = tag.tagClass - - val isConstructed get() = tag.isConstructed - - val encodedTag get() = tag.encodedTag - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null) return false - if (this::class != other::class) return false - - other as TLV - - if (tag != other.tag) return false - if (!content.contentEquals(other.content)) return false - - return true - } - - override fun hashCode(): Int { - var result = tag.hashCode() - result = 31 * result + content.contentHashCode() - return result - } - - override fun toString(): String { - return "TLV(tag=$tag" + - ", length=$contentLength" + - ", overallLength=$overallLength" + - ", content=${content.encodeToString(Base16)})" - } - -} \ No newline at end of file diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Decoding.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Decoding.kt index 76a62ebb..dbaa2d6a 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Decoding.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/Asn1Decoding.kt @@ -10,91 +10,162 @@ import at.asitplus.signum.indispensable.asn1.BERTags.T61_STRING import at.asitplus.signum.indispensable.asn1.BERTags.UNIVERSAL_STRING import at.asitplus.signum.indispensable.asn1.BERTags.UTF8_STRING import at.asitplus.signum.indispensable.asn1.BERTags.VISIBLE_STRING +import at.asitplus.signum.indispensable.io.wrapInUnsafeSource import com.ionspin.kotlin.bignum.integer.BigInteger import com.ionspin.kotlin.bignum.integer.util.fromTwosComplementByteArray import kotlinx.datetime.Instant +import kotlinx.io.Source +import kotlinx.io.readByteArray +import kotlinx.io.readUByte import kotlin.experimental.and /** - * Parses the provided [input] into a single [Asn1Element]. Consumes all Bytes and throws if more than one Asn.1 Structure was found or trailing bytes were detected - * @return the parsed [Asn1Element] - * - * @throws Asn1Exception on invalid input or if more than a single root structure was contained in the [input] + * Convenience wrapper around [parse], taking a [ByteArray] as [source] + * @see parse */ @Throws(Asn1Exception::class) -fun Asn1Element.Companion.parse(input: ByteIterator): Asn1Element = parseFirst(input).also { - if (input.hasNext()) throw Asn1StructuralException("Trailing bytes found after the first ASN.1 element") -} +@Deprecated( + "Use a ByteArray or (even better) a kotlinx.io Source as input when possible. This method copies all bytes from the input twice and is inefficient.", + ReplaceWith("source.readAsn1Element(); require(source.exhausted())") +) +fun Asn1Element.Companion.parse(input: ByteIterator): Asn1Element = + parse(mutableListOf().also { while (input.hasNext()) it.add(input.nextByte()) }.toByteArray()) /** - * Convenience wrapper around [parse], taking a [ByteArray] as [source] - * @see parse + * Parses the provided [input] into a single [Asn1Element]. Consumes all Bytes and throws if more than one Asn.1 Structure was found or trailing bytes were detected + * @return the parsed [Asn1Element] + * + * @throws Asn1Exception on invalid input or if more than a single root structure was contained in the [input] */ @Throws(Asn1Exception::class) -fun Asn1Element.Companion.parse(source: ByteArray): Asn1Element = parse(source.iterator()) +fun Asn1Element.Companion.parse(source: ByteArray): Asn1Element = + source.wrapInUnsafeSource().readFullyToAsn1Elements().first.let { + if (it.size != 1) + throw Asn1StructuralException("Trailing bytes found after the first ASN.1 element") + it.first() + } /** * Tries to parse the [input] into a list of [Asn1Element]s. Consumes all Bytes and throws if an invalid ASN.1 Structure is found at any point. * @return the parsed elements * * @throws Asn1Exception on invalid input or if more than a single root structure was contained in the [input] + * */ +@Deprecated( + "Use a ByteArray or (even better) a kotlinx.io Source as input when possible. This method copies all bytes from the input twice and is inefficient.", + ReplaceWith("source.readFullyToAsn1Elements()"), + DeprecationLevel.WARNING +) @Throws(Asn1Exception::class) -fun Asn1Element.Companion.parseAll(input: ByteIterator): List = input.doParseAll() +fun Asn1Element.Companion.parseAll(input: ByteIterator): List = + mutableListOf().also { while (input.hasNext()) it.add(input.nextByte()) }.toByteArray().wrapInUnsafeSource() + .readFullyToAsn1Elements().first /** * Convenience wrapper around [parseAll], taking a [ByteArray] as [source] * @see parse */ @Throws(Asn1Exception::class) -fun Asn1Element.Companion.parseAll(source: ByteArray): List = parseAll(source.iterator()) +fun Asn1Element.Companion.parseAll(source: ByteArray): List = + source.wrapInUnsafeSource().readFullyToAsn1Elements().first +/** + * Convenience wrapper around [parseFirst], taking a [ByteArray] as [source]. + * @return a pair of the first parsed [Asn1Element] mapped to the remaining bytes + * @see readAsn1Element + */ +@Throws(Asn1Exception::class) +fun Asn1Element.Companion.parseFirst(source: ByteArray): Pair = + source.wrapInUnsafeSource().readAsn1Element() + .let { Pair(it.first, source.copyOfRange(it.second.toInt(), source.size)) } + +private fun Source.doParseExactly(nBytes: Long): List = mutableListOf().also { list -> + var nBytesRead: Long = 0 + while (nBytesRead < nBytes) { + val peekTagAndLen = peekTagAndLen() + val numberOfNextBytesRead = peekTagAndLen.second + peekTagAndLen.first.length + if (nBytesRead + numberOfNextBytesRead > nBytes) break + skip(peekTagAndLen.second.toLong()) // we only peeked before, so now we need to skip, + // since we want to recycle the result below + val (elem, read) = readAsn1Element(peekTagAndLen.first, peekTagAndLen.second) + list.add(elem) + nBytesRead += read + } + require(nBytesRead == nBytes) { "Indicated length ($nBytes) does not correspond to an ASN.1 element boundary ($nBytesRead)" } +} /** - * Parses the first [Asn1Element] from [input]. - * @return the parsed [Asn1Element]. Trailing byte are left untouched and can be consumed from [input] after parsing + * Reads all parsable ASN.1 elements from this source. * - * @throws Asn1Exception on invalid input or if more than a single root structure was contained in the [input] + * @throws Asn1Exception on error if any illegal element or any trailing bytes are encountered */ -//this only makes sense until we switch to kotlinx.io @Throws(Asn1Exception::class) -fun Asn1Element.Companion.parseFirst(input: ByteIterator): Asn1Element = input.doParseSingle() +fun Source.readFullyToAsn1Elements(): Pair, Long> = mutableListOf().let { list -> + var bytesRead = 0L + while (!exhausted()) readAsn1Element().also { (elem, nBytes) -> + bytesRead += nBytes + list.add(elem) + } + Pair(list, bytesRead) +} +/** + * Reads a [TagAndLength] and the number of consumed bytes from the source without consuming it + */ +private fun Source.peekTagAndLen() = peek().readTagAndLength() /** - * Convenience wrapper around [parseFirst], taking a [ByteArray] as [source]. - * @return a pari of the first parsed [Asn1Element] mapped to the remaining bytes - * @see parse + * Decodes a single [Asn1Element] from this source. + * + * @return the decoded element and the number of bytes read from the source */ @Throws(Asn1Exception::class) -fun Asn1Element.Companion.parseFirst(source: ByteArray): Pair = - source.iterator().doParseSingle().let { Pair(it, source.copyOfRange(it.overallLength, source.size)) } +fun Source.readAsn1Element(): Pair = runRethrowing { + val (readTagAndLength, bytesRead) = readTagAndLength() + readAsn1Element(readTagAndLength, bytesRead) +} +/** + * RAW decoding of an ASN.1 element after tag and length have already been decoded and consumed from the source + */ @Throws(Asn1Exception::class) -private fun ByteIterator.doParseAll(): List = runRethrowing { - val result = mutableListOf() - while (hasNext()) result += doParseSingle() - return result -} +private fun Source.readAsn1Element(tagAndLength: TagAndLength, tagAndLengthBytes: Int): Pair = + runRethrowing { + val (tag, length) = tagAndLength + + //ASN.1 SEQUENCE + (if (tag.isSequence()) Asn1Sequence(doParseExactly(length)) + + //ASN.1 SET + else if (tag.isSet()) Asn1Set.fromPresorted(doParseExactly(length)) + + //ASN.1 TAGGED (explicitly) + else if (tag.isExplicitlyTagged) Asn1ExplicitlyTagged(tag.tagValue, doParseExactly(length)) + + //ASN.1 OCTET STRING + else if (tag == Asn1Element.Tag.OCTET_STRING) catching { + //try to decode recursively + Asn1EncapsulatingOctetString(peek().doParseExactly(length)).also { skip(length) } as Asn1Element + }.getOrElse { + //recursive decoding failed, so we interpret is as primitive + require(length <= Int.MAX_VALUE) { "Cannot read more than ${Int.MAX_VALUE} into an OCTET STRING" } + Asn1PrimitiveOctetString(readByteArray(length.toInt())) as Asn1Element + } + + //IMPLICIT-ly TAGGED ASN.1 CONSTRUCTED; we don't know if it is a SET OF, SET, SEQUENCE,… so we default to sequence semantics + else if (tag.isConstructed) Asn1CustomStructure(doParseExactly(length), tag.tagValue, tag.tagClass) -private fun ByteIterator.doParseSingle(): Asn1Element = runRethrowing { - val tlv = readTlv() - if (tlv.isSequence()) Asn1Sequence(tlv.content.iterator().doParseAll()) - else if (tlv.isSet()) Asn1Set.fromPresorted(tlv.content.iterator().doParseAll()) - else if (tlv.isExplicitlyTagged()) - Asn1ExplicitlyTagged(tlv.tag.tagValue, tlv.content.iterator().doParseAll()) - else if (tlv.tag == Asn1Element.Tag.OCTET_STRING) catching { - Asn1EncapsulatingOctetString(tlv.content.iterator().doParseAll()) as Asn1Element - }.getOrElse { Asn1PrimitiveOctetString(tlv.content) as Asn1Element } - else if (tlv.tag.isConstructed) { //custom tags, we don't know if it is a SET OF, SET, SEQUENCE,… so we default to sequence semantics - Asn1CustomStructure(tlv.content.iterator().doParseAll(), tlv.tag.tagValue, tlv.tagClass) - } else Asn1Primitive(tlv.tag, tlv.content) + //IMPLICIT-ly TAGGED ASN.1 PRIMITIVE + else { + require(length <= Int.MAX_VALUE) { "Cannot read more than ${Int.MAX_VALUE} into a primitive" } + Asn1Primitive(tag, readByteArray(length.toInt())) as Asn1Element + }) to length + tagAndLengthBytes } - private fun TLV.isSet() = tag == Asn1Element.Tag.SET - private fun TLV.isSequence() = (tag == Asn1Element.Tag.SEQUENCE) - private fun TLV.isExplicitlyTagged() = tag.isExplicitlyTagged +private fun Asn1Element.Tag.isSet() = this == Asn1Element.Tag.SET +private fun Asn1Element.Tag.isSequence() = (this == Asn1Element.Tag.SEQUENCE) /** @@ -102,7 +173,8 @@ private fun ByteIterator.doParseSingle(): Asn1Element = runRethrowing { * @throws [Asn1Exception] all sorts of exceptions on invalid input */ @Throws(Asn1Exception::class) -fun Asn1Primitive.decodeToBoolean() = runRethrowing { decode(Asn1Element.Tag.BOOL) { Boolean.decodeFromAsn1ContentBytes(it) } } +fun Asn1Primitive.decodeToBoolean() = + runRethrowing { decode(Asn1Element.Tag.BOOL) { Boolean.decodeFromAsn1ContentBytes(it) } } /** Exception-free version of [decodeToBoolean] */ fun Asn1Primitive.decodeToBooleanOrNull() = catching { decodeToBoolean() }.getOrNull() @@ -142,7 +214,8 @@ inline fun Asn1Primitive.decodeToUIntOrNull() = catching { decodeToUInt() }.getO * @throws [Asn1Exception] on invalid input */ @Throws(Asn1Exception::class) -fun Asn1Primitive.decodeToULong() = runRethrowing { decode(Asn1Element.Tag.INT) { ULong.decodeFromAsn1ContentBytes(it) } } +fun Asn1Primitive.decodeToULong() = + runRethrowing { decode(Asn1Element.Tag.INT) { ULong.decodeFromAsn1ContentBytes(it) } } /** Exception-free version of [decodeToULong] */ inline fun Asn1Primitive.decodeToULongOrNull() = catching { decodeToULong() }.getOrNull() @@ -180,13 +253,12 @@ fun Asn1Primitive.asAsn1String(): Asn1String = runRethrowing { * Decodes this [Asn1Primitive]'s content into a String. * @throws [Asn1Exception] all sorts of exceptions on invalid input */ -fun Asn1Primitive.decodeToString() = runRethrowing {asAsn1String().value} +fun Asn1Primitive.decodeToString() = runRethrowing { asAsn1String().value } /** Exception-free version of [decodeToString] */ fun Asn1Primitive.decodeToStringOrNull() = catching { decodeToString() }.getOrNull() - /** * decodes this [Asn1Primitive]'s content into an [Instant] if it is encoded as UTC TIME or GENERALIZED TIME * @@ -195,7 +267,11 @@ fun Asn1Primitive.decodeToStringOrNull() = catching { decodeToString() }.getOrNu @Throws(Asn1Exception::class) fun Asn1Primitive.decodeToInstant() = when (tag) { - Asn1Element.Tag.TIME_UTC -> decode(Asn1Element.Tag.TIME_UTC, Instant.Companion::decodeUtcTimeFromAsn1ContentBytes) + Asn1Element.Tag.TIME_UTC -> decode( + Asn1Element.Tag.TIME_UTC, + Instant.Companion::decodeUtcTimeFromAsn1ContentBytes + ) + Asn1Element.Tag.TIME_GENERALIZED -> decode( Asn1Element.Tag.TIME_GENERALIZED, Instant.Companion::decodeGeneralizedTimeFromAsn1ContentBytes @@ -354,32 +430,41 @@ fun Boolean.Companion.decodeFromAsn1ContentBytes(bytes: ByteArray): Boolean { */ fun String.Companion.decodeFromAsn1ContentBytes(bytes: ByteArray) = bytes.decodeToString() +/** + * [Asn1Element.Tag] to the decoded length + */ +private typealias TagAndLength = Pair -private fun ByteIterator.readTlv(): TLV = runRethrowing { - if (!hasNext()) throw IllegalArgumentException("Can't read TLV, input empty") +private val TagAndLength.tag: Asn1Element.Tag get() = first +private val TagAndLength.length: Long get() = second - val tag = decodeTag() - val length = decodeLength() - require(length < 1024 * 1024) { "Heap space" } - val value = ByteArray(length) { - require(hasNext()) { "Out of bytes to decode" } - nextByte() - } +/** + * Reads [TagAndLength] and the number of consumed bytes from the source + */ +private fun Source.readTagAndLength(): Pair = runRethrowing { + if (exhausted()) throw IllegalArgumentException("Can't read TLV, input empty") - return TLV(Asn1Element.Tag(tag.first,tag.second), value) + val tag = readAsn1Tag() + val length = decodeLength() + require(length.first >= 0L) { "Illegal length: $length" } + return Pair((tag to length.first), (length.second + tag.encodedTagLength)) } +/** + * Decodes the `length` of an ASN.1 element (which is preceded by its tag) from the source. + * @return the decoded length and the number of bytes consumed + */ @Throws(IllegalArgumentException::class) -private fun ByteIterator.decodeLength() = - nextByte().let { firstByte -> +private fun Source.decodeLength(): Pair = + readByte().let { firstByte -> if (firstByte.isBerShortForm()) { - firstByte.toUByte().toInt() + Pair(firstByte.toUByte().toLong(), 1) } else { // its BER long form! val numberOfLengthOctets = (firstByte byteMask 0x7F).toInt() - (0 until numberOfLengthOctets).fold(0) { acc, index -> - require(hasNext()) { "Can't decode length" } - acc + (nextByte().toUByte().toInt() shl Byte.SIZE_BITS * (numberOfLengthOctets - index - 1)) - } + (0 until numberOfLengthOctets).fold(0L) { acc, index -> + require(!exhausted()) { "Can't decode length" } + acc + (readUByte().toLong() shl Byte.SIZE_BITS * (numberOfLengthOctets - index - 1)) + }.let { Pair(it, 1 + numberOfLengthOctets) } } } @@ -387,13 +472,12 @@ private fun Byte.isBerShortForm() = this byteMask 0x80 == 0x00.toUByte() internal infix fun Byte.byteMask(mask: Int) = (this and mask.toUInt().toByte()).toUByte() -internal fun ByteIterator.decodeTag(): Pair = - nextByte().let { firstByte -> +fun Source.readAsn1Tag(): Asn1Element.Tag = + readByte().let { firstByte -> (firstByte byteMask 0x1F).let { tagNumber -> - if (tagNumber <= 30U) { - tagNumber.toULong() to byteArrayOf(firstByte) - } else { - decodeAsn1VarULong().let { (l, b) -> l to byteArrayOf(firstByte, *b) } + if (tagNumber <= 30U) Asn1Element.Tag(tagNumber.toULong(), byteArrayOf(firstByte)) + else decodeAsn1VarULong().let { (l, b) -> + Asn1Element.Tag(l, byteArrayOf(firstByte, *b)) } } } diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/NumberEncoding.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/NumberEncoding.kt index c1a1f638..1d5a4041 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/NumberEncoding.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/encoding/NumberEncoding.kt @@ -1,15 +1,28 @@ package at.asitplus.signum.indispensable.asn1.encoding import at.asitplus.catching +import at.asitplus.catchingUnwrapped import at.asitplus.signum.indispensable.asn1.ObjectIdentifier +import at.asitplus.signum.indispensable.io.appendUnsafe import at.asitplus.signum.indispensable.io.ensureSize +import at.asitplus.signum.indispensable.io.throughBuffer import com.ionspin.kotlin.bignum.integer.BigInteger import com.ionspin.kotlin.bignum.integer.Sign +import kotlinx.io.* +import kotlin.experimental.and import kotlin.experimental.or import kotlin.math.ceil import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.Uuid +internal const val UVARINT_SINGLEBYTE_MAXVALUE_UBYTE: UByte = 0x80u +internal const val UVARINT_SINGLEBYTE_MAXVALUE: Byte = 0x80.toByte() + +internal const val UVARINT_MASK: Byte = 0x7F +internal const val UVARINT_MASK_UBYTE: UByte = 0x7Fu +internal const val UVARINT_MASK_UINT: UInt = 0x7Fu +internal const val UVARINT_MASK_ULONG: ULong = 0x7FuL +internal val UVARINT_MASK_BIGINT = BigInteger.fromUByte(UVARINT_MASK_UBYTE) /** * Encode as a four-byte array @@ -225,13 +238,7 @@ fun ULong.Companion.fromTwosComplementByteArray(it: ByteArray) = when { private fun ByteArray.shiftLeftAsULong(index: Int, shift: Int) = this[index].toUByte().toULong() shl shift /** Encodes an unsigned Long to a minimum-size unsigned byte array */ -fun Long.toUnsignedByteArray(): ByteArray { - require(this >= 0) - return this.toTwosComplementByteArray().let { - if (it[0] == 0.toByte()) it.copyOfRange(1, it.size) - else it - } -} +fun Long.toUnsignedByteArray(): ByteArray = throughBuffer { it.writeMagnitudeLong(this) } /** Encodes an unsigned Int to a minimum-size unsigned byte array */ fun Int.toUnsignedByteArray() = toLong().toUnsignedByteArray() @@ -241,47 +248,14 @@ fun Int.toUnsignedByteArray() = toLong().toUnsignedByteArray() * Encodes this number using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, * while the highest bit indicates if more bytes are to come */ -fun ULong.toAsn1VarInt(): ByteArray { - if (this < 128u) return byteArrayOf(this.toByte()) //Fast case - var offset = 0 - var result = mutableListOf() - - var b0 = (this shr offset and 0x7FuL).toByte() - while ((this shr offset > 0uL) || offset == 0) { - result += b0 - offset += 7 - if (offset > (ULong.SIZE_BITS - 1)) break //End of Fahnenstange - b0 = (this shr offset and 0x7FuL).toByte() - } - return with(result) { - ByteArray(size) { fromBack(it) or asn1VarIntByteMask(it) } - } -} +fun ULong.toAsn1VarInt(): ByteArray = throughBuffer { it.writeAsn1VarInt(this) } /** * Encodes this number using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, * while the highest bit indicates if more bytes are to come */ -fun BigInteger.toAsn1VarInt(): ByteArray { - if (isZero()) return byteArrayOf(0) - require(isPositive) { "Only positive Numbers are supported" } - if (this < 128) return byteArrayOf(this.byteValue(exactRequired = true)) //Fast case - var offset = 0 - var result = mutableListOf() - - val mask = BigInteger.fromUByte(0x7Fu) - var b0 = ((this shr offset) and mask).byteValue(exactRequired = false) - while ((this shr offset > 0uL) || offset == 0) { - result += b0 - offset += 7 - if (offset > (this.bitLength() - 1)) break //End of Fahnenstange - b0 = ((this shr offset) and mask).byteValue(exactRequired = false) - } - - return with(result) { - ByteArray(size) { fromBack(it) or asn1VarIntByteMask(it) } - } -} +@Throws(IllegalArgumentException::class) +fun BigInteger.toAsn1VarInt(): ByteArray = throughBuffer { it.writeAsn1VarInt(this) } /** * Encodes this number using unsigned VarInt encoding as used within ASN.1: @@ -289,29 +263,14 @@ fun BigInteger.toAsn1VarInt(): ByteArray { * * This kind of encoding is used to encode [ObjectIdentifier] nodes and ASN.1 Tag values > 30 */ -fun UInt.toAsn1VarInt(): ByteArray { - if (this < 128u) return byteArrayOf(this.toByte()) //Fast case - var offset = 0 - var result = mutableListOf() - - var b0 = (this shr offset and 0x7Fu).toByte() - while ((this shr offset > 0u) || offset == 0) { - result += b0 - offset += 7 - if (offset > (UInt.SIZE_BITS - 1)) break //End of Fahnenstange - b0 = (this shr offset and 0x7Fu).toByte() - } +fun UInt.toAsn1VarInt(): ByteArray = throughBuffer { it.writeAsn1VarInt(this) } - return with(result) { - ByteArray(size) { fromBack(it) or asn1VarIntByteMask(it) } - } -} - -private fun MutableList.asn1VarIntByteMask(it: Int) = (if (isLastIndex(it)) 0x00 else 0x80).toByte() +private fun List.asn1VarIntByteMask(it: Int) = + (if (isLastIndex(it)) 0x00 else UVARINT_SINGLEBYTE_MAXVALUE).toByte() -private fun MutableList.isLastIndex(it: Int) = it == size - 1 +private fun List.isLastIndex(it: Int) = it == size - 1 -private fun MutableList.fromBack(it: Int) = this[size - 1 - it] +private fun List.fromBack(it: Int) = this[size - 1 - it] /** @@ -321,142 +280,256 @@ private fun MutableList.fromBack(it: Int) = this[size - 1 - it] * @return the decoded ULong and the underlying varint-encoded bytes as `ByteArray` * @throws IllegalArgumentException if the number is larger than [ULong.MAX_VALUE] */ -inline fun Iterable.decodeAsn1VarULong(): Pair = iterator().decodeAsn1VarULong() +@Throws(IllegalArgumentException::class) +fun ByteArray.decodeAsn1VarULong(): Pair = this.throughBuffer { it.decodeAsn1VarULong() } /** - * Decodes an ULong from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, + * Decodes an unsigned BigInteger from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. * - * @return the decoded ULong and the underlying varint-encoded bytes as `ByteArray` - * @throws IllegalArgumentException if the number is larger than [ULong.MAX_VALUE] + * @return the decoded unsigned BigInteger and the underlying varint-encoded bytes as `ByteArray` */ -inline fun ByteArray.decodeAsn1VarULong(): Pair = iterator().decodeAsn1VarULong() +fun ByteArray.decodeAsn1VarBigInt(): Pair = this.throughBuffer { it.decodeAsn1VarBigInt() } /** - * Decodes an ULong from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, + * Decodes an UInt from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. * - * @return the decoded ULong and the underlying varint-encoded bytes as `ByteArray` - * @throws IllegalArgumentException if the number is larger than [ULong.MAX_VALUE] + * @return the decoded UInt and the underlying varint-encoded bytes as `ByteArray` + * @throws IllegalArgumentException if the number is larger than [UInt.MAX_VALUE] */ -fun Iterator.decodeAsn1VarULong(): Pair { - var offset = 0 - var result = 0uL - val accumulator = mutableListOf() - while (hasNext()) { - val current = next().toUByte() - accumulator += current.toByte() - if (current >= 0x80.toUByte()) { - result = (current and 0x7F.toUByte()).toULong() or (result shl 7) - } else { - result = (current and 0x7F.toUByte()).toULong() or (result shl 7) - break - } - if (++offset > ceil(ULong.SIZE_BYTES.toFloat() * 8f / 7f)) throw IllegalArgumentException("Tag number too Large do decode into ULong!") - } - - return result to accumulator.toByteArray() -} +@Throws(IllegalArgumentException::class) +fun ByteArray.decodeAsn1VarUInt(): Pair = this.throughBuffer { it.decodeAsn1VarUInt() } +/** + * Converts this UUID to a BigInteger representation + */ +@OptIn(ExperimentalUuidApi::class) +fun Uuid.toBigInteger(): BigInteger = BigInteger.fromByteArray(toByteArray(), Sign.POSITIVE) /** - * Decodes an unsigned BigInteger from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, - * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. + * Tries to convert a BigInteger to a UUID. Only guaranteed to work with BigIntegers that contain the unsigned (positive) + * integer representation of a UUID, chances are high, though, that it works with random positive BigIntegers between + * 16 and 14 bytes large. * - * @return the decoded unsigned BigInteger and the underlying varint-encoded bytes as `ByteArray` + * Returns `null` if conversion fails. Never throws. */ -inline fun Iterable.decodeAsn1VarBigInt(): Pair = iterator().decodeAsn1VarBigInt() +@OptIn(ExperimentalUuidApi::class) +fun Uuid.Companion.fromBigintOrNull(bigInteger: BigInteger): Uuid? = + catchingUnwrapped { fromByteArray(bigInteger.toByteArray().ensureSize(16)) }.getOrNull() + + +///////////KTX-IO /** - * Decodes an unsigned BigInteger from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, - * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. + * Encodes this number using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, + * while the highest bit indicates if more bytes are to come * - * @return the decoded unsigned BigInteger and the underlying varint-encoded bytes as `ByteArray` + * @return the number of bytes written to the sink */ -inline fun ByteArray.decodeAsn1VarBigInt(): Pair = iterator().decodeAsn1VarBigInt() +fun Sink.writeAsn1VarInt(number: ULong) = writeAsn1VarInt(number, ULong.SIZE_BITS) +/** + * Encodes this number using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, + * while the highest bit indicates if more bytes are to come + * + * @return the number of bytes written to the sink + */ +fun Sink.writeAsn1VarInt(number: UInt) = writeAsn1VarInt(number.toULong(), UInt.SIZE_BITS) /** - * Decodes an BigInteger from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, - * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. + * Encodes this number using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, + * while the highest bit indicates if more bytes are to come * - * @return the decoded BigInteger and the underlying varint-encoded bytes as `ByteArray` - * @throws IllegalArgumentException if the number is larger than [ULong.MAX_VALUE] + * @return the number of bytes written to the sink */ -fun Iterator.decodeAsn1VarBigInt(): Pair { - var result = BigInteger.ZERO - val mask = BigInteger.fromUByte(0x7Fu) - val accumulator = mutableListOf() - while (hasNext()) { - val curByte = next() - val current = BigInteger(curByte.toUByte().toInt()) - accumulator += curByte - result = (current and mask) or (result shl 7) - if (current < 0x80.toUByte()) break +private fun Sink.writeAsn1VarInt(number: ULong, bits: Int): Int { + if(number==0uL){ //fast case + writeByte(0) + return 1 } - - return result to accumulator.toByteArray() + val numBytes = (number.bitLength + 6) / 7 // division rounding up + (numBytes - 1).downTo(0).forEach { byteIndex -> + writeUByte( + ((number shr (byteIndex * 7)).toUByte() and UVARINT_MASK_UBYTE) or + (if (byteIndex > 0) UVARINT_SINGLEBYTE_MAXVALUE_UBYTE else 0u) + ) + } + return numBytes } +/** + * the number of bits required to represent this number + */ +val ULong.bitLength inline get() = ULong.SIZE_BITS - this.countLeadingZeroBits() -//TOOD: how to not duplicate all this??? /** - * Decodes an UInt from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, - * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. + * the number of bits required to represent this number + */ +val Long.bitLength inline get() = Long.SIZE_BITS - this.countLeadingZeroBits() + +/** + * the number of bits required to represent this number + */ +val UInt.bitLength inline get() = UInt.SIZE_BITS - this.countLeadingZeroBits() + +/** + * the number of bits required to represent this number + */ +val Int.bitLength inline get() = Int.SIZE_BITS - this.countLeadingZeroBits() + +/** + * Encodes this number using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, + * while the highest bit indicates if more bytes are to come * - * @return the decoded UInt and the underlying varint-encoded bytes as `ByteArray` - * @throws IllegalArgumentException if the number is larger than [UInt.MAX_VALUE] + * @return the number of bytes written to the sink */ -inline fun Iterable.decodeAsn1VarUInt(): Pair = iterator().decodeAsn1VarUInt() +@Throws(IllegalArgumentException::class) +fun Sink.writeAsn1VarInt(number: BigInteger): Int { + if(number==BigInteger.ZERO){ //fast case + writeByte(0) + return 1 + } + require(!number.isNegative) { "Only non-negative numbers are supported" } + val numBytes = (number.bitLength() + 6) / 7 // division rounding up + (numBytes - 1).downTo(0).forEach { byteIndex -> + writeByte( + ((number shr (byteIndex * 7)).byteValue(exactRequired = false) and UVARINT_MASK) or + (if (byteIndex > 0) UVARINT_SINGLEBYTE_MAXVALUE else 0) + ) + } + //otherwise we won't ever write zero + return numBytes +} /** - * Decodes an UInt from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, - * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. + * Decodes an ASN.1 unsigned varint to an [ULong], copying all bytes from the source into a [ByteArray]. * - * @return the decoded UInt and the underlying varint-encoded bytes as `ByteArray` - * @throws IllegalArgumentException if the number is larger than [UInt.MAX_VALUE] + * @return the decoded [ULong] and the underlying varint-encoded bytes as [ByteArray] + * @throws IllegalArgumentException if the number is larger than [ULong.MAX_VALUE] */ -inline fun ByteArray.decodeAsn1VarUInt(): Pair = iterator().decodeAsn1VarUInt() +@Throws(IllegalArgumentException::class) +fun Source.decodeAsn1VarULong(): Pair = decodeAsn1VarInt(ULong.SIZE_BITS) /** - * Decodes an UInt from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, - * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. + * Decodes an ASN.1 unsigned varint to an [UInt], copying all bytes from the source into a [ByteArray]. * - * @return the decoded UInt and the underlying varint-encoded bytes as `ByteArray` + * @return the decoded [UInt] and the underlying varint-encoded bytes as [ByteArray] * @throws IllegalArgumentException if the number is larger than [UInt.MAX_VALUE] */ -fun Iterator.decodeAsn1VarUInt(): Pair { +@Throws(IllegalArgumentException::class) +fun Source.decodeAsn1VarUInt(): Pair = + decodeAsn1VarInt(UInt.SIZE_BITS).let { (n, b) -> n.toUInt() to b } + +/** + * Decodes an ASN.1 unsigned varint to an ULong allocating at most [bits] many bits . + * This function is useful as an intermediate processing step, since it also returns a [Buffer] + * holding all bytes consumed from the source. + * This operation essentially moves bytes around without copying. + * + * @return the decoded ASN.1 varint as an [ULong] and the underlying varint-encoded bytes as [Buffer] + * @throws IllegalArgumentException if the resulting number requires more than [bits] many bits to be represented + */ +@Throws(IllegalArgumentException::class) +private fun Source.decodeAsn1VarInt(bits: Int): Pair { var offset = 0 - var result = 0u - val accumulator = mutableListOf() - while (hasNext()) { - val current = next().toUByte() - accumulator += current.toByte() - if (current >= 0x80.toUByte()) { - result = (current and 0x7F.toUByte()).toUInt() or (result shl 7) + var result = 0uL + val accumulator = Buffer() + while (!exhausted()) { + val current = readUByte() + accumulator.writeUByte(current) + if (current >= UVARINT_SINGLEBYTE_MAXVALUE_UBYTE) { + result = (current and UVARINT_MASK_UBYTE).toULong() or (result shl 7) } else { - result = (current and 0x7F.toUByte()).toUInt() or (result shl 7) + result = (current and UVARINT_MASK_UBYTE).toULong() or (result shl 7) break } - if (++offset > ceil(UInt.SIZE_BYTES.toFloat() * 8f / 7f)) throw IllegalArgumentException("Tag number too Large do decode into UInt!") + if (++offset > ceil((bits * 8).toFloat() * 8f / 7f)) throw IllegalArgumentException("Number too Large do decode into $bits bits!") } - return result to accumulator.toByteArray() + return result to accumulator.readByteArray() } /** - * Converts this UUID to a BigInteger representation + * Decodes an ASN.1 unsigned varint to a [BigInteger], copying all bytes from the source into a [ByteArray]. + * + * @return the decoded [BigInteger] and the underlying varint-encoded bytes as [ByteArray] */ -@OptIn(ExperimentalUuidApi::class) -fun Uuid.toBigInteger(): BigInteger = BigInteger.fromByteArray(toByteArray(), Sign.POSITIVE) +fun Source.decodeAsn1VarBigInt(): Pair { + var result = BigInteger.ZERO + val accumulator = Buffer() + while (!exhausted()) { + val curByte = readByte() + val current = BigInteger(curByte.toUByte().toInt()) + accumulator.writeByte(curByte) + result = (current and UVARINT_MASK_BIGINT) or (result shl 7) + if (current < UVARINT_SINGLEBYTE_MAXVALUE_UBYTE) break + } + + return result to accumulator.readByteArray() +} + /** - * Tries to convert a BigInteger to a UUID. Only guaranteed to work with BigIntegers that contain the unsigned (positive) - * integer representation of a UUID, chances are high, though, that it works with random positive BigIntegers between - * 16 and 14 bytes large. + * Writes a signed long using twos-complement encoding using the fewest bytes required * - * Returns `null` if conversion fails. Never throws. + * @return the number of byte written to the sink */ -@OptIn(ExperimentalUuidApi::class) -fun Uuid.Companion.fromBigintOrNull(bigInteger: BigInteger): Uuid? = - catching { fromByteArray(bigInteger.toByteArray().ensureSize(16)) }.getOrNull() +fun Sink.writeTwosComplementLong(number: Long): Int = appendUnsafe(number.toTwosComplementByteArray()) + +/** + * Encodes an unsigned Long to a minimum-size twos-complement byte array + * @return the number of bytes written + */ +fun Sink.writeTwosComplementULong(number: ULong): Int = appendUnsafe(number.toTwosComplementByteArray()) + + +/** Encodes an unsigned Int to a minimum-size twos-complement byte array + * @return the number of bytes written to the sink + */ +fun Sink.writeTwosComplementUInt(number: UInt) = writeTwosComplementLong(number.toLong()) + +/** + * Consumes exactly [nBytes] from this source and interprets it as a signed [ULong]. + * + * @throws IllegalArgumentException if too much or too little data is present + */ +@Throws(IllegalArgumentException::class) +fun Source.readTwosComplementULong(nBytes: Int): ULong = ULong.fromTwosComplementByteArray(readByteArray(nBytes)) + + +/** + * Consumes exactly [nBytes] from this source and interprets it as a [Long]. + * + * @throws IllegalArgumentException if too much or too little data is present + */ +@Throws(IllegalArgumentException::class) +fun Source.readTwosComplementLong(nBytes: Int): Long = Long.fromTwosComplementByteArray(readByteArray(nBytes)) + + +/** + * Consumes exactly [nBytes] from this source and interprets it as a signed [Int] + * + * @throws IllegalArgumentException if too much or too little data is present + */ +@Throws(IllegalArgumentException::class) +fun Source.readTwosComplementInt(nBytes: Int): Int = Int.fromTwosComplementByteArray(readByteArray(nBytes)) + +/** + * Consumes exactly [nBytes] remaining data from this source and interprets it as a [UInt] + * + * @throws IllegalArgumentException if no or too much data is present + */ +@Throws(IllegalArgumentException::class) +fun Source.readTwosComplementUInt(nBytes: Int): UInt = UInt.fromTwosComplementByteArray(readByteArray(nBytes)) + +/** + * Encodes a positive Long to a minimum-size unsigned byte array, omitting the leading zero + * + * @throws IllegalArgumentException if [number] is negative + * @return the number of bytes written + */ +fun Sink.writeMagnitudeLong(number: Long): Int { + require(number >= 0) + return number.toTwosComplementByteArray().let { appendUnsafe(it, if (it[0] == 0.toByte()) 1 else 0) } +} \ No newline at end of file diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/io/Encoding.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/io/Encoding.kt index ae905b64..b8fad1ef 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/io/Encoding.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/io/Encoding.kt @@ -6,9 +6,7 @@ import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.base64.Base64ConfigBuilder import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString -import kotlinx.io.Buffer -import kotlinx.io.Source -import kotlinx.io.UnsafeIoApi +import kotlinx.io.* import kotlinx.io.unsafe.UnsafeBufferOperations import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationException @@ -128,10 +126,43 @@ fun ByteArray.ensureSize(size: Int): ByteArray = (this.size - size).let { toDrop @Suppress("NOTHING_TO_INLINE") inline fun ByteArray.ensureSize(size: UInt) = ensureSize(size.toInt()) -@Suppress("NOTHING_TO_INLINE") -inline fun ByteArray.asBuffer()= Buffer().also{it.write(this)} - @OptIn(UnsafeIoApi::class) internal fun ByteArray.wrapInUnsafeSource(): Source = Buffer().apply { UnsafeBufferOperations.moveToTail(this, this@wrapInUnsafeSource) } + +/** + * Directly moves the byte array to a buffer without copying. Thus, it keeps bytes managed by a Buffer accessible. + * The bytes may be overwritten through the Buffer or even recycled to be used by another buffer. + * Therefore, operating on these bytes after wrapping leads to undefined behaviour. + * [startIndex] is inclusive, [endIndex] is exclusive. + */ +@OptIn(UnsafeIoApi::class) +internal fun wrapInUnsafeSource(bytes: ByteArray, startIndex: Int = 0, endIndex: Int = bytes.size) = Buffer().apply { + require(startIndex in 0..endIndex) { "StartIndex bust be between 0 and $endIndex" } + UnsafeBufferOperations.moveToTail(this, bytes, startIndex, endIndex) +} + +/** + * Helper to create a buffer, operate on it and return its contents as a [ByteArray] + */ +internal inline fun throughBuffer(operation: (Buffer) -> Unit): ByteArray = + Buffer().also(operation).readByteArray() + +internal inline fun ByteArray.throughBuffer(operation: (Source) -> T): T = + wrapInUnsafeSource().let { operation(it) } + + +/** + * Directly appends [bytes] to this Sink's internal Buffer without copying. Thus, it keeps bytes managed by a Buffer accessible. + * The bytes may be overwritten through the Buffer or even recycled to be used by another buffer. + * Therefore, operating on these bytes after wrapping leads to undefined behaviour. + * [startIndex] is inclusive, [endIndex] is exclusive. + */ +internal fun Sink.appendUnsafe(bytes: ByteArray, startIndex: Int = 0, endIndex: Int = bytes.size): Int { + require(startIndex in 0.. { Asn1.Int(v).decodeToLong() } } } "failures: too large" - { checkAll(iterations = 5000, Arb.bigInt(128)) { - val v = BigInteger.fromLong(Long.MAX_VALUE).plus(1) - .plus(BigInteger.fromTwosComplementByteArray(it.toByteArray())) + val v = BigInteger.fromLong(Long.MAX_VALUE).plus(1).plus(BigInteger.fromTwosComplementByteArray(it.toByteArray())) shouldThrow { Asn1.Int(v).decodeToLong() } } } @@ -113,6 +157,12 @@ class Asn1EncodingTest : FreeSpec({ decoded shouldBe it Asn1.Int(it).derEncoded shouldBe ASN1Integer(it).encoded + + val toTwosComplementByteArray = it.toTwosComplementByteArray() + toTwosComplementByteArray.wrapInUnsafeSource().readTwosComplementLong(toTwosComplementByteArray.size) shouldBe it + Buffer().apply { writeTwosComplementLong(it) }.snapshot() + .toByteArray() shouldBe toTwosComplementByteArray + } } } @@ -124,7 +174,7 @@ class Asn1EncodingTest : FreeSpec({ } } "failures: too large" - { - checkAll(iterations = 5000, Arb.long(Int.MAX_VALUE.toLong() + 1.. { Asn1.Int(it).decodeToInt() } } } @@ -135,6 +185,9 @@ class Asn1EncodingTest : FreeSpec({ decoded shouldBe it Asn1.Int(it).derEncoded shouldBe ASN1Integer(it.toLong()).encoded + val twosComplementByteArray = it.toTwosComplementByteArray() + twosComplementByteArray.wrapInUnsafeSource().readTwosComplementInt(twosComplementByteArray.size) shouldBe it + twosComplementByteArray.wrapInUnsafeSource().readTwosComplementLong(twosComplementByteArray.size) shouldBe it } } } @@ -157,11 +210,30 @@ class Asn1EncodingTest : FreeSpec({ decoded shouldBe it Asn1.Int(it).derEncoded shouldBe ASN1Integer(it.toBigInteger().toJavaBigInteger()).encoded + val twosComplementByteArray = it.toTwosComplementByteArray() + twosComplementByteArray.wrapInUnsafeSource().readTwosComplementUInt(twosComplementByteArray.size) shouldBe it + twosComplementByteArray.wrapInUnsafeSource().readTwosComplementULong(twosComplementByteArray.size) shouldBe it.toULong() } } } "unsigned longs" - { + + "manual" - { + withData( + 2f.pow(24).toULong() - 1u, + 256uL, + 65555uL, + 2f.pow(24).toULong(), + 255uL, + 360uL, + 4113774321109173852uL + ) { + val bytes = (it).toTwosComplementByteArray() + bytes.wrapInUnsafeSource().readTwosComplementULong(bytes.size) shouldBe it + } + } + "failures: negative" - { checkAll(iterations = 5000, Arb.long(Long.MIN_VALUE..<0)) { shouldThrow { Asn1.Int(it).decodeToULong() } @@ -169,9 +241,12 @@ class Asn1EncodingTest : FreeSpec({ } "failures: too large" - { checkAll(iterations = 5000, Arb.bigInt(128)) { - val v = BigInteger.fromULong(ULong.MAX_VALUE).plus(1) - .plus(BigInteger.fromTwosComplementByteArray(it.toByteArray())) - shouldThrow { Asn1.Int(v).decodeToULong() } + val byteArray = it.toByteArray() + val v = BigInteger.fromULong(ULong.MAX_VALUE).plus(1).plus(BigInteger.fromTwosComplementByteArray( + byteArray + )) + val asn1Primitive = Asn1.Int(v) + shouldThrow { asn1Primitive.decodeToULong() } } } "successes" - { @@ -181,6 +256,8 @@ class Asn1EncodingTest : FreeSpec({ decoded shouldBe it Asn1.Int(it).derEncoded shouldBe ASN1Integer(it.toBigInteger().toJavaBigInteger()).encoded + val twosComplementByteArray = it.toTwosComplementByteArray() + twosComplementByteArray.wrapInUnsafeSource().readTwosComplementULong(twosComplementByteArray.size) shouldBe it } } } @@ -191,7 +268,8 @@ class Asn1EncodingTest : FreeSpec({ val certBytes = Base64.getMimeDecoder() .decode(javaClass.classLoader.getResourceAsStream("github-com.pem")!!.reader().readText()) val tree = Asn1Element.parse(certBytes) - tree.derEncoded shouldBe certBytes + withClue(certBytes.toHexString() + "\n" + tree.toDerHexString()) + { tree.derEncoded shouldBe certBytes } } @@ -200,38 +278,38 @@ class Asn1EncodingTest : FreeSpec({ val instant = Clock.System.now() val sequence = Asn1.Sequence { - +Asn1.ExplicitlyTagged(1u) { +Asn1Primitive(BERTags.BOOLEAN, byteArrayOf(0x00)) } + +ExplicitlyTagged(1u) { +Asn1Primitive(BERTags.BOOLEAN, byteArrayOf(0x00)) } +Asn1.Set { +Asn1.Sequence { +Asn1.SetOf { - +Asn1.PrintableString("World") - +Asn1.PrintableString("Hello") + +PrintableString("World") + +PrintableString("Hello") } +Asn1.Set { - +Asn1.PrintableString("World") - +Asn1.PrintableString("Hello") - +Asn1.Utf8String("!!!") + +PrintableString("World") + +PrintableString("Hello") + +Utf8String("!!!") } } } - +Asn1.Null() + +Null() +ObjectIdentifier("1.2.603.624.97") - +Asn1.Utf8String("Foo") - +Asn1.PrintableString("Bar") + +Utf8String("Foo") + +PrintableString("Bar") +Asn1.Set { +Asn1.Int(3) +Asn1.Int(-65789876543L) - +Asn1.Bool(false) - +Asn1.Bool(true) + +Bool(false) + +Bool(true) } +Asn1.Sequence { - +Asn1.Null() + +Null() +Asn1String.Numeric("12345") - +Asn1.UtcTime(instant) + +UtcTime(instant) } } Asn1Element.parse(sequence.derEncoded).derEncoded shouldBe sequence.derEncoded diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/Asn1ParserTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/Asn1ParserTest.kt index 32249431..6ad890db 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/Asn1ParserTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/Asn1ParserTest.kt @@ -1,10 +1,8 @@ package at.asitplus.signum.indispensable.asn1 -import at.asitplus.signum.indispensable.asn1.encoding.Asn1 -import at.asitplus.signum.indispensable.asn1.encoding.parse -import at.asitplus.signum.indispensable.asn1.encoding.parseAll -import at.asitplus.signum.indispensable.asn1.encoding.parseFirst -import at.asitplus.signum.indispensable.toByteArray +import at.asitplus.signum.indispensable.asn1.encoding.* +import at.asitplus.signum.indispensable.io.wrapInUnsafeSource +import at.asitplus.signum.indispensable.pki.toByteArray import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.shouldBe @@ -24,22 +22,25 @@ class Asn1ParserTest : FreeSpec({ encoded.sliceArray(seq.tag.encodedTagLength + seq.encodedLength.size until seq.derEncoded.size) "without Garbage" { - val iterator = rawChildren.iterator() - val parseFirst = Asn1Element.parseFirst(iterator) + + val (parseFirst, rest) = Asn1Element.parseFirst(rawChildren) val childIterator = seq.children.iterator() parseFirst shouldBe childIterator.next() - val bytes = iterator.toByteArray() - bytes shouldBe rawChildren.sliceArray(parseFirst.overallLength until rawChildren.size) + + rest shouldBe rawChildren.sliceArray(parseFirst.overallLength until rawChildren.size) Asn1Element.parseFirst(rawChildren).let { (elem,rest )-> elem shouldBe seq.children.first() rest shouldBe rawChildren.sliceArray(parseFirst.overallLength until rawChildren.size) } - val byteIterator = bytes.iterator() - repeat(9) { Asn1Element.parseFirst(byteIterator) shouldBe childIterator.next() } - Asn1Element.parseAll(rawChildren.iterator()) shouldBe seq.children + var byteIterator = rest + repeat(9) { + Asn1Element.parseFirst(byteIterator) + .let { (elem, residue) -> byteIterator = residue;elem } shouldBe childIterator.next() + } + Asn1Element.parseAll(rawChildren) shouldBe seq.children shouldThrow { Asn1Element.parse(rawChildren) } shouldThrow { Asn1Element.parse(rawChildren.iterator()) } @@ -48,21 +49,23 @@ class Asn1ParserTest : FreeSpec({ "with Garbage" { val garbage = Random.nextBytes(32) val withGarbage = rawChildren + garbage - val iterator = withGarbage.iterator() - val parseFirst = Asn1Element.parseFirst(iterator) + val source = withGarbage.wrapInUnsafeSource() + val (parseFirst,rest) = Asn1Element.parseFirst(withGarbage) + val firstFromSource= source.readAsn1Element().first + firstFromSource shouldBe parseFirst val childIterator = seq.children.iterator() parseFirst shouldBe childIterator.next() - val bytes = iterator.toByteArray() - bytes shouldBe withGarbage.sliceArray(parseFirst.overallLength until withGarbage.size) + + rest shouldBe withGarbage.sliceArray(parseFirst.overallLength until withGarbage.size) Asn1Element.parseFirst(withGarbage).let { (elem,rest )-> elem shouldBe seq.children.first() rest shouldBe withGarbage.sliceArray(parseFirst.overallLength until withGarbage.size) } - val byteIterator = bytes.iterator() - repeat(9) { Asn1Element.parseFirst(byteIterator) shouldBe childIterator.next() } + + repeat(9) { source.readAsn1Element().first shouldBe childIterator.next() } shouldThrow { Asn1Element.parseAll(withGarbage.iterator()) shouldBe seq.children } diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/OidTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/OidTest.kt index 869624da..b9f5c712 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/OidTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/OidTest.kt @@ -400,7 +400,7 @@ class OldOIDObjectIdentifier @Throws(Asn1Exception::class) constructor(@Transien } currentNode += rawValue[index] index++ - collected += currentNode.decodeAsn1VarBigInt().first + collected += currentNode.toByteArray().decodeAsn1VarBigInt().first } } return ObjectIdentifier(*collected.toTypedArray()) diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/TagEncodingTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/TagEncodingTest.kt index 880d3aff..c271f64b 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/TagEncodingTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/TagEncodingTest.kt @@ -9,9 +9,12 @@ import io.kotest.datatest.withData import io.kotest.matchers.shouldBe import io.kotest.property.Arb import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.positiveInt import io.kotest.property.arbitrary.uInt import io.kotest.property.arbitrary.uLong import io.kotest.property.checkAll +import kotlinx.io.Buffer +import kotlinx.io.snapshot import org.bouncycastle.asn1.ASN1Integer import org.bouncycastle.asn1.DERTaggedObject @@ -27,6 +30,12 @@ class TagEncodingTest : FreeSpec({ long shouldBe it } + "length encoding" - { + checkAll(Arb.positiveInt()) { + Buffer().apply { encodeLength(it.toLong()) }.snapshot().toByteArray() shouldBe it.encodeLength() + } + } + "Manual" - { withData(207692171uL, 128uL, 36uL, 16088548868045964978uL, 15871772363588580035uL) { it.toAsn1VarInt().decodeAsn1VarULong().first shouldBe it diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/UVarIntTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/UVarIntTest.kt index 1b9a3216..8a3236a8 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/UVarIntTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/asn1/UVarIntTest.kt @@ -1,41 +1,66 @@ package at.asitplus.signum.indispensable.asn1 -import at.asitplus.signum.indispensable.asn1.encoding.decodeAsn1VarBigInt -import at.asitplus.signum.indispensable.asn1.encoding.decodeAsn1VarUInt -import at.asitplus.signum.indispensable.asn1.encoding.decodeAsn1VarULong -import at.asitplus.signum.indispensable.asn1.encoding.toAsn1VarInt +import at.asitplus.signum.indispensable.asn1.encoding.* +import at.asitplus.signum.indispensable.io.wrapInUnsafeSource import com.ionspin.kotlin.bignum.integer.BigInteger import com.ionspin.kotlin.bignum.integer.Sign import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.shouldBe import io.kotest.property.Arb import io.kotest.property.arbitrary.bigInt import io.kotest.property.arbitrary.uInt import io.kotest.property.arbitrary.uLong import io.kotest.property.checkAll +import kotlinx.io.Buffer +import kotlinx.io.snapshot +import kotlin.math.ceil import kotlin.random.Random class UVarIntTest : FreeSpec({ + //TODO: buffer based tests with capped number of bytes test "UInts with trailing bytes" - { "manual" { - byteArrayOf(65, 0, 0, 0).decodeAsn1VarUInt().first shouldBe 65u + val src = byteArrayOf(65, 0, 0, 0) + src.decodeAsn1VarUInt().first shouldBe 65u + val buf = src.wrapInUnsafeSource() + buf.decodeAsn1VarUInt().first shouldBe 65u + repeat(3){buf.readByte() shouldBe 0.toByte()} + buf.exhausted().shouldBeTrue() } "automated -" { checkAll(Arb.uInt()) { int -> - (int.toAsn1VarInt().asList() + Random.nextBytes(8).asList()).decodeAsn1VarUInt().first shouldBe int - + val rnd = Random.nextBytes(8) + val src = int.toAsn1VarInt().asList() + rnd.asList() + src.decodeAsn1VarUInt().first shouldBe int + val buffer = src.toByteArray().wrapInUnsafeSource() + buffer.decodeAsn1VarUInt().first shouldBe int + rnd.forEach { it shouldBe buffer.readByte() } + buffer.exhausted().shouldBeTrue() } } } "ULongs with trailing bytes" - { "manual" { - byteArrayOf(65, 0, 0, 0).decodeAsn1VarULong().first shouldBe 65uL + val src = byteArrayOf(65, 0, 0, 0) + src.decodeAsn1VarULong().first shouldBe 65uL + val buf = src.wrapInUnsafeSource() + buf.decodeAsn1VarULong().first shouldBe 65uL + repeat(3){buf.readByte() shouldBe 0.toByte()} + buf.exhausted().shouldBeTrue() } "automated -" { checkAll(Arb.uLong()) { long -> - (long.toAsn1VarInt().asList() + Random.nextBytes(8).asList()).decodeAsn1VarULong().first shouldBe long + val rnd = Random.nextBytes(8) + val src = long.toAsn1VarInt().asList() + rnd.asList() + src.decodeAsn1VarULong().first shouldBe long + + val buffer = src.toByteArray().wrapInUnsafeSource() + buffer.decodeAsn1VarULong().first shouldBe long + rnd.forEach { it shouldBe buffer.readByte() } + buffer.exhausted().shouldBeTrue() } } @@ -49,8 +74,18 @@ class UVarIntTest : FreeSpec({ val bigIntVarInt = bigInteger.toAsn1VarInt() bigIntVarInt shouldBe uLongVarInt + Buffer().apply { writeAsn1VarInt(bigInteger) }.snapshot().toByteArray() shouldBe bigIntVarInt + Buffer().apply { writeAsn1VarInt(long) }.snapshot().toByteArray() shouldBe uLongVarInt + + val rnd = Random.nextBytes(8) + val src = uLongVarInt.asList() + rnd.asList() + src.decodeAsn1VarBigInt().first shouldBe bigInteger - (uLongVarInt.asList() + Random.nextBytes(8).asList()).decodeAsn1VarBigInt().first shouldBe bigInteger + + val buffer = src.toByteArray().wrapInUnsafeSource() + buffer.decodeAsn1VarBigInt().first shouldBe bigInteger + rnd.forEach { it shouldBe buffer.readByte() } + buffer.exhausted().shouldBeTrue() } } @@ -58,10 +93,127 @@ class UVarIntTest : FreeSpec({ "larger" - { checkAll(Arb.bigInt(1, 1024 * 32)) { javaBigInt -> val bigInt = BigInteger.fromByteArray(javaBigInt.toByteArray(), Sign.POSITIVE) - (bigInt.toAsn1VarInt().asList() + Random.nextBytes(33) - .asList()).decodeAsn1VarBigInt().first shouldBe bigInt + val bigIntVarint = bigInt.toAsn1VarInt() + val rnd = Random.nextBytes(33) + val src = bigIntVarint.asList() + rnd + .asList() + src.decodeAsn1VarBigInt().first shouldBe bigInt + + val buf = src.toByteArray().wrapInUnsafeSource() + buf.decodeAsn1VarBigInt().first shouldBe bigInt + rnd.forEach { it shouldBe buf.readByte() } + buf.exhausted().shouldBeTrue() } } } -}) \ No newline at end of file +}) + +//old code for regeressiontests + +/** + * Decodes an ULong from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, + * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. + * + * @return the decoded ULong and the underlying varint-encoded bytes as `ByteArray` + * @throws IllegalArgumentException if the number is larger than [ULong.MAX_VALUE] + */ +@Throws(IllegalArgumentException::class) +private inline fun Iterable.decodeAsn1VarULong(): Pair = iterator().decodeAsn1VarULong() + + +/** + * Decodes an ULong from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, + * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. + * + * @return the decoded ULong and the underlying varint-encoded bytes as `ByteArray` + * @throws IllegalArgumentException if the number is larger than [ULong.MAX_VALUE] + */ +@Throws(IllegalArgumentException::class) +private fun Iterator.decodeAsn1VarULong(): Pair { + var offset = 0 + var result = 0uL + val accumulator = mutableListOf() + while (hasNext()) { + val current = next().toUByte() + accumulator += current.toByte() + if (current >= 0x80.toUByte()) { + result = (current and 0x7F.toUByte()).toULong() or (result shl 7) + } else { + result = (current and 0x7F.toUByte()).toULong() or (result shl 7) + break + } + if (++offset > ceil(ULong.SIZE_BYTES.toFloat() * 8f / 7f)) throw IllegalArgumentException("Tag number too Large do decode into ULong!") + } + + return result to accumulator.toByteArray() +} + +/** + * Decodes an unsigned BigInteger from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, + * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. + * + * @return the decoded unsigned BigInteger and the underlying varint-encoded bytes as `ByteArray` + */ +private inline fun Iterable.decodeAsn1VarBigInt(): Pair = iterator().decodeAsn1VarBigInt() + + + +/** + * Decodes a BigInteger from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, + * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. + * + * @return the decoded BigInteger and the underlying varint-encoded bytes as `ByteArray` + */ +private fun Iterator.decodeAsn1VarBigInt(): Pair { + var result = BigInteger.ZERO + val mask = BigInteger.fromUByte(0x7Fu) + val accumulator = mutableListOf() + while (hasNext()) { + val curByte = next() + val current = BigInteger(curByte.toUByte().toInt()) + accumulator += curByte + result = (current and mask) or (result shl 7) + if (current < 0x80.toUByte()) break + } + + return result to accumulator.toByteArray() +} + + +/** + * Decodes an UInt from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, + * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. + * + * @return the decoded UInt and the underlying varint-encoded bytes as `ByteArray` + * @throws IllegalArgumentException if the number is larger than [UInt.MAX_VALUE] + */ +@Throws(IllegalArgumentException::class) +private inline fun Iterable.decodeAsn1VarUInt(): Pair = iterator().decodeAsn1VarUInt() + +/** + * Decodes an UInt from bytes using varint encoding as used within ASN.1: groups of seven bits are encoded into a byte, + * while the highest bit indicates if more bytes are to come. Trailing bytes are ignored. + * + * @return the decoded UInt and the underlying varint-encoded bytes as `ByteArray` + * @throws IllegalArgumentException if the number is larger than [UInt.MAX_VALUE] + */ +@Throws(IllegalArgumentException::class) +private fun Iterator.decodeAsn1VarUInt(): Pair { + var offset = 0 + var result = 0u + val accumulator = mutableListOf() + while (hasNext()) { + val current = next().toUByte() + accumulator += current.toByte() + if (current >= 0x80.toUByte()) { + result = (current and 0x7F.toUByte()).toUInt() or (result shl 7) + } else { + result = (current and 0x7F.toUByte()).toUInt() or (result shl 7) + break + } + if (++offset > ceil(UInt.SIZE_BYTES.toFloat() * 8f / 7f)) throw IllegalArgumentException("Tag number too Large do decode into UInt!") + } + + return result to accumulator.toByteArray() +} diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/DistinguishedNameTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/DistinguishedNameTest.kt similarity index 98% rename from indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/DistinguishedNameTest.kt rename to indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/DistinguishedNameTest.kt index df7d9b2a..d2314333 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/DistinguishedNameTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/DistinguishedNameTest.kt @@ -1,4 +1,4 @@ -package at.asitplus.signum.indispensable +package at.asitplus.signum.indispensable.pki import at.asitplus.signum.indispensable.asn1.KnownOIDs import at.asitplus.signum.indispensable.pki.AttributeTypeAndValue.* diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Pkcs10CertificationRequestJvmTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/Pkcs10CertificationRequestJvmTest.kt similarity index 99% rename from indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Pkcs10CertificationRequestJvmTest.kt rename to indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/Pkcs10CertificationRequestJvmTest.kt index 6dca3f99..493fd45b 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/Pkcs10CertificationRequestJvmTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/Pkcs10CertificationRequestJvmTest.kt @@ -1,10 +1,10 @@ -package at.asitplus.signum.indispensable +package at.asitplus.signum.indispensable.pki +import at.asitplus.signum.indispensable.* import at.asitplus.signum.indispensable.asn1.* import at.asitplus.signum.indispensable.asn1.encoding.encodeToAsn1Primitive import at.asitplus.signum.indispensable.io.ensureSize import at.asitplus.signum.indispensable.asn1.encoding.parse -import at.asitplus.signum.indispensable.pki.* import io.kotest.assertions.withClue import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.nulls.shouldNotBeNull diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertParserTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/X509CertParserTest.kt similarity index 88% rename from indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertParserTest.kt rename to indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/X509CertParserTest.kt index 8c10059d..3455a9f5 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertParserTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/X509CertParserTest.kt @@ -1,9 +1,10 @@ -package at.asitplus.signum.indispensable +package at.asitplus.signum.indispensable.pki import at.asitplus.signum.indispensable.asn1.* import at.asitplus.signum.indispensable.asn1.encoding.parse import at.asitplus.signum.indispensable.asn1.encoding.parseFirst -import at.asitplus.signum.indispensable.pki.X509Certificate +import at.asitplus.signum.indispensable.asn1.encoding.readAsn1Element +import at.asitplus.signum.indispensable.io.wrapInUnsafeSource import io.kotest.assertions.throwables.shouldThrow import io.kotest.assertions.withClue import io.kotest.core.spec.style.FreeSpec @@ -14,6 +15,7 @@ import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.base64.Base64 import io.matthewnelson.encoding.core.Decoder.Companion.decodeToByteArray import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString +import kotlinx.io.readByteArray import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject @@ -41,10 +43,10 @@ class X509CertParserTest : FreeSpec({ X509Certificate.decodeFromDer(derBytes) val garbage = Random.nextBytes(Random.nextInt(0..128)) - val input = (derBytes + garbage).iterator() - Asn1Element.parseFirst(input).let { parsed -> + val input = (derBytes + garbage).wrapInUnsafeSource() + input.readAsn1Element().let { (parsed, _) -> parsed.derEncoded shouldBe derBytes - input.toByteArray() shouldBe garbage + input.readByteArray() shouldBe garbage } } @@ -66,10 +68,10 @@ class X509CertParserTest : FreeSpec({ cert shouldBe X509Certificate.decodeFromByteArray(certBytes) val garbage = Random.nextBytes(Random.nextInt(0..128)) - val input = (certBytes + garbage).iterator() - Asn1Element.parseFirst(input).let { parsed -> + val input = (certBytes + garbage).wrapInUnsafeSource() + input.readAsn1Element().let { (parsed, _) -> parsed.derEncoded shouldBe certBytes - input.toByteArray() shouldBe garbage + input.readByteArray() shouldBe garbage } } } @@ -127,10 +129,10 @@ class X509CertParserTest : FreeSpec({ parsed shouldBe X509Certificate.decodeFromByteArray(crt.encoded) val garbage = Random.nextBytes(Random.nextInt(0..128)) - val bytes = (crt.encoded + garbage).iterator() - Asn1Element.parseFirst(bytes).let { parsed -> + val bytes = (crt.encoded + garbage).wrapInUnsafeSource() + bytes.readAsn1Element().let { (parsed,_) -> parsed.derEncoded shouldBe own - bytes.toByteArray() shouldBe garbage + bytes.readByteArray() shouldBe garbage } } } @@ -150,10 +152,10 @@ class X509CertParserTest : FreeSpec({ decoded shouldBe X509Certificate.decodeFromByteArray(it.second) val garbage = Random.nextBytes(Random.nextInt(0..128)) - val bytes = (it.second + garbage).iterator() - Asn1Element.parseFirst(bytes).let { parsed -> + val bytes = (it.second + garbage).wrapInUnsafeSource() + bytes.readAsn1Element().let { (parsed,_) -> parsed.derEncoded shouldBe it.second - bytes.toByteArray() shouldBe garbage + bytes.readByteArray() shouldBe garbage } } } @@ -194,16 +196,14 @@ class X509CertParserTest : FreeSpec({ cert.encodeToTlv().derEncoded shouldBe encodedSrc val garbage = Random.nextBytes(Random.nextInt(0..128)) - val input = (jcaCert.encoded + garbage).iterator() - Asn1Element.parseFirst(input).let { parsed -> + val input = (jcaCert.encoded + garbage).wrapInUnsafeSource() + input.readAsn1Element().let { (parsed,_) -> parsed.derEncoded shouldBe jcaCert.encoded - input.asSequence().toList().toByteArray() shouldBe garbage + input.readByteArray() shouldBe garbage } } } - } - }) diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertificateJvmTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/X509CertificateJvmTest.kt similarity index 99% rename from indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertificateJvmTest.kt rename to indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/X509CertificateJvmTest.kt index f564fa09..11b38b53 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509CertificateJvmTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/X509CertificateJvmTest.kt @@ -1,9 +1,9 @@ -package at.asitplus.signum.indispensable +package at.asitplus.signum.indispensable.pki +import at.asitplus.signum.indispensable.* import at.asitplus.signum.indispensable.asn1.* import at.asitplus.signum.indispensable.io.ensureSize import at.asitplus.signum.indispensable.asn1.encoding.parse -import at.asitplus.signum.indispensable.pki.* import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509ConversionTests.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/X509ConversionTests.kt similarity index 75% rename from indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509ConversionTests.kt rename to indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/X509ConversionTests.kt index 362204d3..f9a2e13d 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/X509ConversionTests.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/X509ConversionTests.kt @@ -1,6 +1,8 @@ -package at.asitplus.signum.indispensable +package at.asitplus.signum.indispensable.pki import at.asitplus.KmmResult +import at.asitplus.signum.indispensable.X509SignatureAlgorithm +import at.asitplus.signum.indispensable.toX509SignatureAlgorithm import io.kotest.core.spec.style.FreeSpec import io.kotest.datatest.withData import io.kotest.matchers.shouldBe