> {
+ abstract var buf: Array
+ protected set
+
+ private var offset = 0
+ val contentBytes get() = buf
+
+ // The following properties offer simple encode/decode operations as indicated by the ByteOrder
+ // currently in effect. Properties are defined for many basic types
+
+ /**
+ * This property offers byte-level read/write. get returns the byte at the current position,
+ * and increments then position by one. set changes the byte at the current position, then
+ * increments the position by one.
+ *
+ * @throws IllegalStateException if the position is already at the limit
+ */
+ override var byte: Element
+ get() {
+ checkPosition()
+ return getElementAt(position++)
+ }
+ set(value) {
+ checkPosition()
+ setElementAt(position++, value)
+ }
+
+ /**
+ * Compacts this buffer (optional operation).
+ *
+ * The bytes between the buffer's current position and its limit,
+ * if any, are copied to the beginning of the buffer. That is, the
+ * byte at index p = position() is copied
+ * to index zero, the byte at index p + 1 is copied
+ * to index one, and so forth until the byte at index
+ * limit() - 1 is copied to index
+ * n = limit() - 1 - p.
+ * The buffer's position is then set to n+1 and its limit is set to
+ * its capacity. The mark, if defined, is discarded.
+ *
+ *
The buffer's position is set to the number of bytes copied,
+ * rather than to zero, so that an invocation of this method can be
+ * followed immediately by an invocation of another relative put
+ * method.
+ *
+
+ *
+ * Invoke this method after writing data from a buffer in case the
+ * write was incomplete. The following loop, for example, copies bytes
+ * from one channel to another via the buffer buf:
+ *
+ *
{@code
+ * buf.clear(); // Prepare buffer for use
+ * while (in.read(buf) >= 0 || buf.position != 0) {
+ * buf.flip();
+ * out.write(buf);
+ * buf.compact(); // In case of partial write
+ * }
+ * }
+ *
+ * @return This buffer
+ */
+ fun compact() {
+ var destIndex = 0
+ for (i in position until position + limit) {
+ setElementAt(destIndex++, getElementAt(i))
+ }
+ position = destIndex
+ limit = capacity
+ resetMark()
+ }
+
+ /**
+ * Compares this buffer to another.
+ *
+ * Two byte buffers are compared by comparing their sequences of
+ * remaining elements lexicographically, without regard to the starting
+ * position of each sequence within its corresponding buffer.
+ *
+ * @return A negative integer, zero, or a positive integer as this buffer
+ * is less than, equal to, or greater than the given buffer
+ */
+ override fun compareTo(other: ByteBufferBase): Int {
+ val n: Int = position + min(remaining, other.remaining)
+ var i: Int = position
+ var j: Int = other.position
+ while (i < n) {
+ val r = compareElement(getElementAt(i), other.getElementAt(j))
+ if (r != 0) return r
+ i++
+ j++
+ }
+ return 0
+ }
+
+ abstract fun compareElement(
+ element: Element,
+ other: Element,
+ ): Int
+
+ /**
+ * The contents of the current buffer are replaced with the contents of the source. Position,
+ * limit, capacity, order, and mark will all be set to same as the source.
+ */
+ fun copy(source: ByteBufferBase) {
+ buf = source.buf
+ position = source.position
+ limit = source.limit
+ mark = source.mark
+ order = source.order
+ }
+
+ /**
+ * Similar to [ByteArray] copyInto
+ * @param destination Array to receive copy
+ * @param destinationOffset index into destination where copy will start
+ * @param startIndex index in source array where copy will start
+ * @param endIndex index in source that copy will end, exclusive. [endIndex - startIndex] will
+ * be number of bytes copied.
+ */
+ abstract fun copyInto(
+ destination: Array,
+ destinationOffset: Int = 0,
+ startIndex: Int = 0,
+ endIndex: Int,
+ )
+
+ /**
+ * Starting at the current position, copy bytes into the specified destination array. Increment
+ * position by the number of bytes retrieved.
+ *
+ * @param destination
+ * The array with sufficient capacity into which bytes will be written
+ * @param destinationOffset
+ * The offset into the destination at which the copy will start
+ * @param length
+ * The number of bytes to be copied
+ * @return This buffer
+ */
+ fun fillArray(
+ destination: Array,
+ destinationOffset: Int = 0,
+ length: Int,
+ ) {
+ checkBounds(destinationOffset, length, length)
+ if (length > remaining) {
+ throw IllegalStateException("Copying length:$length is more than remaining:$remaining")
+ }
+ copyInto(destination, destinationOffset, position, position + length)
+ position += length
+ }
+
+ /**
+ * This method transfers the bytes remaining in the given source
+ * buffer into this buffer. If there are more bytes remaining in the
+ * source buffer than in this buffer, that is, if
+ * src.remaining() > remaining(),
+ * then no bytes are transferred and an IllegalStateException is thrown
+ *
+ *
+ * Otherwise, this method copies
+ * n = src.remaining() bytes from the given
+ * buffer into this buffer, starting at each buffer's current position.
+ * The positions of both buffers are then incremented by n.
+ */
+ open fun put(source: ByteBufferBase) {
+ if (source == this) {
+ throw IllegalArgumentException("Cannot copy ByteBuffer to itself")
+ }
+ val sourceBytes = source.remaining
+ if (sourceBytes > remaining) {
+ throw IllegalArgumentException("Remaining source bytes:$sourceBytes exceeds destination remaining:$remaining")
+ }
+
+ source.fillArray(buf, position, sourceBytes)
+ position += sourceBytes
+ }
+
+ /**
+ * The following are all private functions
+ */
+ private fun checkPosition(forLength: Int = 1) {
+ if (position + forLength > limit) {
+ throw IllegalStateException()
+ }
+ }
+}
+
+/**
+ * Enhance byte array implementing the Buffer interface which provides position/limit/remaining/capacity tracking, and
+ * support for either little endian or big endian encoding of basic numeric types. See [ByteBufferBase] and [Buffer] for
+ * more details.
+ * @param capacity starting size of buffer in bytes
+ * @param order defaults to little endian encoding of numeric types
+ * @param isReadOnly true if for some reason opeations that change content should throw an exception
+ * @param buf defaults to a ByteArray of specified [capacity]
+ */
+class ByteBuffer(
+ capacity: Int,
+ order: ByteOrder = ByteOrder.LittleEndian,
+ isReadOnly: Boolean = false,
+ override var buf: ByteArray = ByteArray(capacity),
+) : ByteBufferBase(capacity, order, isReadOnly) {
+ /**
+ * Construct a ByteBuffer from an existing ByteArray
+ * @param bytes becomes the buffer content (not a copy). capacity is set to the ByteArray size, position is set to
+ * zero
+ * @param order defaults to little endian encoding of numeric types
+ */
+ constructor(bytes: ByteArray, order: ByteOrder = ByteOrder.LittleEndian) :
+ this(bytes.size, order, false, bytes)
+
+ override fun flip(): ByteBuffer {
+ super.flip()
+ return this
+ }
+
+ override fun getElementAt(index: Int): Byte = buf[index]
+
+ override fun setElementAt(
+ index: Int,
+ element: Byte,
+ ) {
+ buf[index] = element
+ }
+
+ /**
+ * gets one byte at the current position without changing the position. The byte is treated as
+ * unsigned, so the returned Int will always be positive.
+ * @param index indicates which element in the current array to retrieve
+ * @return INt will not have it's high order bits set.
+ */
+ override fun getElementAsInt(index: Int): Int = buf toPosInt index
+
+ /**
+ * gets one byte at the current position without changing the position, and return a UInt.
+ * The byte is treated as unsigned.
+ * @param index indicates which element in the current array to retrieve
+ * @return UINt will not have it's high order bits set.
+ */
+ override fun getElementAsUInt(index: Int): UInt = buf toPosUInt index
+
+ /**
+ * gets one byte at the current position without changing the position. The byte is treated as
+ * unsigned, so the returned Long will always be positive.
+ * @param index indicates which element in the current array to retrieve
+ * @return Long will not have it's high order bits set.
+ */
+ override fun getElementAsLong(index: Int): Long = buf toPosLong index
+
+ /**
+ * gets one byte at the current position without changing the position. The byte is treated as
+ * unsigned, so the returned ULong will always be positive.
+ * @param index indicates which element in the current array to retrieve
+ * @return ULong will not have it's high order bits set.
+ */
+ override fun getElementAsULong(index: Int): ULong = buf toPosULong index
+
+ override fun getBytes(length: Int): ByteArray {
+ val l = min(remaining, length)
+ val a = ByteArray(l)
+ buf.copyInto(a, 0, position, position + l)
+ position += l
+ return a
+ }
+
+ override fun getBytes(bytes: ByteArray) {
+ val l = min(remaining, bytes.size)
+ buf.copyInto(bytes, 0, position, position + l)
+ position += l
+ }
+
+ override fun put(bytes: ByteArray) {
+ val l = min(remaining, bytes.size)
+ bytes.copyInto(buf, position, 0, l)
+ position += l
+ }
+
+ override fun putEndian(bytes: ByteArray) {
+ if (order == ByteOrder.LittleEndian) {
+ bytes.reverse()
+ }
+ put(bytes)
+ }
+
+ override fun shortToArray(short: Short): ByteArray =
+ byteArrayOf(
+ (short.toInt() shr 8).toByte(),
+ short.toByte(),
+ )
+
+ override fun ushortToArray(ushort: UShort): ByteArray =
+ byteArrayOf(
+ (ushort.toUInt() shr 8).toByte(),
+ ushort.toByte(),
+ )
+
+ override fun intToArray(int: Int): ByteArray =
+ byteArrayOf(
+ (int shr 24 and 0xff).toByte(),
+ (int shr 16 and 0xff).toByte(),
+ (int shr 8 and 0xff).toByte(),
+ (int and 0xff).toByte(),
+ )
+
+ override fun uintToArray(int: UInt): ByteArray =
+ byteArrayOf(
+ (int shr 24 and 0xffu).toByte(),
+ (int shr 16 and 0xffu).toByte(),
+ (int shr 8 and 0xffu).toByte(),
+ (int and 0xffu).toByte(),
+ )
+
+ override fun longToArray(long: Long): ByteArray =
+ byteArrayOf(
+ (long shr 56 and 0xff).toByte(),
+ (long shr 48 and 0xff).toByte(),
+ (long shr 40 and 0xff).toByte(),
+ (long shr 32 and 0xff).toByte(),
+ (long shr 24 and 0xff).toByte(),
+ (long shr 16 and 0xff).toByte(),
+ (long shr 8 and 0xff).toByte(),
+ (long and 0xff).toByte(),
+ )
+
+ override fun ulongToArray(uLong: ULong): ByteArray =
+ byteArrayOf(
+ (uLong shr 56 and 0xffu).toByte(),
+ (uLong shr 48 and 0xffu).toByte(),
+ (uLong shr 40 and 0xffu).toByte(),
+ (uLong shr 32 and 0xffu).toByte(),
+ (uLong shr 24 and 0xffu).toByte(),
+ (uLong shr 16 and 0xffu).toByte(),
+ (uLong shr 8 and 0xffu).toByte(),
+ (uLong and 0xffu).toByte(),
+ )
+
+ /**
+ * Similar to [ByteArray] copyInto
+ * @param destination Array to receive copy
+ * @param destinationOffset index into destination where copy will start
+ * @param startIndex index in source array where copy will start
+ * @param endIndex index in source that copy will end, exclusive. [endIndex - startIndex] will
+ * be number of bytes copied.
+ */
+ override fun copyInto(
+ destination: ByteArray,
+ destinationOffset: Int,
+ startIndex: Int,
+ endIndex: Int,
+ ) {
+ buf.copyInto(destination, destinationOffset, startIndex, endIndex)
+ }
+
+ override fun compareElement(
+ element: Byte,
+ other: Byte,
+ ): Int = element.compareTo(other)
+
+ /**
+ * Increase size of buffer. Capacity and content are increased. Position is unchanged. if limit
+ * currently set to capacity, it will be set to the new capacity. All data is retained from
+ * previous buffer, including bytes between old limit and capacity.
+ * @param addCapacity bytes to add. Unsigned as can't be used to shrink.
+ */
+ override fun expand(addCapacity: UInt) {
+ val changeLimit = limit == capacity
+ capacity += addCapacity.toInt()
+ val newBuf = ByteArray(capacity)
+ buf.copyInto(newBuf, 0, 0)
+ buf = newBuf
+ if (changeLimit) limit = capacity
+ }
+
+ /**
+ * Appends a buffer starting at its position, to the end of this buffer,
+ * starting at position of this buffer for appendBuffer remaining size. New position will
+ * be at new limit. If the contents must be expanded (usually is), then a new byteArray is allocated.
+ * position is moved to the new limit.
+ * @param appendBuffer buffer to be appended, starting at its poition for remaining bytes
+ * @return this
+ */
+ fun expand(appendBuffer: ByteBuffer) {
+ val newLimit = position + appendBuffer.remaining
+ if (newLimit > capacity) {
+ val oldBuf = buf
+ buf = ByteArray(newLimit)
+ oldBuf.copyInto(buf, 0, 0, position)
+ }
+ appendBuffer.contentBytes.copyInto(
+ buf,
+ position,
+ appendBuffer.position,
+ appendBuffer.remaining,
+ )
+ capacity = newLimit
+ limit = newLimit
+ position = limit
+ }
+
+ fun get(
+ destination: ByteArray,
+ destinationOffset: Int = 0,
+ size: Int = destination.size,
+ ) {
+ super.fillArray(destination, destinationOffset, size)
+ }
+
+ /**
+ * Copies specified bytes from source to this buffer, starting at position, for the
+ * specified length. Position is incremented by the length. Any bounds violation throws
+ * and IllegalArgumentException
+ *
+ * @param source byte array to write from, will be unchanged
+ * @param sourceOffset starting offset in source, defaults to 0
+ * @param length number of bytes to copy, defaults to size of source
+ * @return this
+ * @throws IllegalArgumentException on bounds violation
+ */
+ fun putBytes(
+ source: ByteArray,
+ sourceOffset: Int = 0,
+ length: Int = source.size,
+ ) {
+ checkBounds(sourceOffset, length, length)
+ if (length > remaining) {
+ throw IllegalArgumentException("Length:$length exceeds remaining:$remaining")
+ }
+ source.copyInto(buf, position, sourceOffset, length)
+ position += length
+ }
+
+ /**
+ * Make a new ByteBuffer containing the [remaining bytes] of this one. Length can be overridden to
+ * a shorter value than the default [remaining]. If length is > [remaining], [remaining] is used.
+ *
+ * Position in this ByteBuffer is unaffected. Position in new returned ByteBuffer is 0.
+ *
+ * @param length defaults to [remaining]. can be between 1 and [remaining]
+ */
+ override fun slice(length: Int): ByteBuffer {
+ val l = min(remaining, length)
+ val bytes = ByteArray(l)
+ buf.copyInto(bytes, 0, position, position + l)
+ return ByteBuffer(bytes, this.order)
+ }
+
+ /**
+ * Convert from a [ByteBuffer] to a [UByteBuffer], retaining the same capacity, position, limit
+ * and contents
+ */
+ fun toUByteBuffer(): UByteBuffer {
+ val uBuf = UByteBuffer(capacity, order, isReadOnly, contentBytes.toUByteArray())
+ uBuf.positionLimit(position, remaining)
+ return uBuf
+ }
+
+ override fun toString(): String =
+ buildString {
+ append("Position: $position, limit: $limit, remaining: $remaining. Content: 0x")
+ for (i in position until limit) {
+ append("${contentBytes[i].toString(16).padStart(2, '0')} ")
+ }
+ }
+
+ /**
+ * Tells whether or not this buffer is equal to another object.
+ *
+ * Two byte buffers are equal if, and only if,
+ *
+ *
+ *
+ * They have the same element type,
+ *
+ * They have the same number of remaining elements, and
+ *
+ *
+ * The two sequences of remaining elements, considered
+ * independently of their starting positions, are pointwise equal.
+
+ *
+ *
+ *
+ *
+ * A byte buffer is not equal to any other type of object.
+ *
+ * @param other The object to which this buffer is to be compared
+ *
+ * @return true if, and only if, this buffer is equal to the
+ * given object
+ */
+ override fun equals(other: Any?): Boolean {
+ if (other == null) return false
+ if (this === other) return true
+ if (other !is ByteBuffer) return false
+ if (remaining != other.remaining) return false
+ var i = limit - 1
+ var j = other.limit - 1
+ while (i >= position) {
+ if (buf[i--] != other.buf[j--]) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns the current hash code of this buffer.
+ *
+ * The hash code of a byte buffer depends only upon its remaining
+ * elements; that is, upon the elements from position() up to, and
+ * including, the element at limit() - 1.
+ *
+ *
Because buffer hash codes are content-dependent, it is inadvisable
+ * to use buffers as keys in hash maps or similar data structures unless it
+ * is known that their contents will not change.
+ *
+ * @return The current hash code of this buffer
+ */
+ override fun hashCode(): Int {
+ var h = 1
+ val p: Int = position
+ for (i in limit - 1 downTo p) h = 31 * h + buf[i].toInt()
+ return h
+ }
+}
+
+class UByteBuffer(
+ capacity: Int,
+ order: ByteOrder = ByteOrder.LittleEndian,
+ isReadOnly: Boolean = false,
+ override var buf: UByteArray = UByteArray(capacity),
+) : ByteBufferBase(capacity, order, isReadOnly) {
+ constructor(bytes: UByteArray, order: ByteOrder = ByteOrder.LittleEndian) :
+ this(bytes.size, order, false, bytes)
+
+ /**
+ * Similar to [ByteArray] copyInto
+ * @param destination Array to receive copy
+ * @param destinationOffset index into destination where copy will start
+ * @param startIndex index in source array where copy will start
+ * @param endIndex index in source that copy will end, exclusive. [endIndex - startIndex] will
+ * be number of bytes copied.
+ */
+ override fun copyInto(
+ destination: UByteArray,
+ destinationOffset: Int,
+ startIndex: Int,
+ endIndex: Int,
+ ) {
+ buf.copyInto(destination, destinationOffset, startIndex, endIndex)
+ }
+
+ override fun compareElement(
+ element: UByte,
+ other: UByte,
+ ): Int = element.compareTo(other)
+
+ /**
+ * Increase size of buffer. Capacity and content are increased. Position is unchanged. if limit
+ * currently set to capacity, it will be set to the new capacity. All data is retained from
+ * previous buffer, including bytes between old limit and capacity.
+ * @param addCapacity bytes to add. Unsigned as can't be used to shrink.
+ */
+ override fun expand(addCapacity: UInt) {
+ val changeLimit = limit == capacity
+ capacity += addCapacity.toInt()
+ val newBuf = UByteArray(capacity)
+ buf.copyInto(newBuf, 0, 0)
+ buf = newBuf
+ if (changeLimit) limit = capacity
+ }
+
+ /**
+ * Appends a buffer starting at its position, to the end of this buffer,
+ * starting at position of this buffer for appendBuffer remaining size. New position will
+ * be at new limit. If the contents must be expanded (usually is), then a new byteArray is allocated.
+ * position is moved to the new limit.
+ * @param appendBuffer buffer to be appended, starting at its poition for remaining bytes
+ * @return this
+ */
+ fun expand(appendBuffer: UByteBuffer) {
+ val newLimit = position + appendBuffer.remaining
+ if (newLimit > capacity) {
+ val oldBuf = buf
+ buf = UByteArray(newLimit)
+ oldBuf.copyInto(buf, 0, 0, position)
+ }
+ appendBuffer.contentBytes.copyInto(
+ buf,
+ position,
+ appendBuffer.position,
+ appendBuffer.remaining,
+ )
+ capacity = newLimit
+ limit = newLimit
+ position = limit
+ }
+
+ override fun flip(): UByteBuffer {
+ super.flip()
+ return this
+ }
+
+ override fun getElementAt(index: Int): UByte = buf[index]
+
+ /**
+ * gets one byte at the current position without changing the position. The byte is treated as
+ * unsigned, so the returned Int will always be positive.
+ * @param index indicates which element in the current array to retrieve
+ * @return Int will not have it's high order bits set.
+ */
+ override fun getElementAsInt(index: Int): Int = buf toPosInt index
+
+ /**
+ * gets one byte at the current position without changing the position. The byte is treated as
+ * unsigned, so the returned UInt will always be positive.
+ * @param index indicates which element in the current array to retrieve
+ * @return UInt will not have it's high order bits set.
+ */
+ override fun getElementAsUInt(index: Int): UInt = buf toPosUInt index
+
+ /**
+ * gets one byte at the current position without changing the position. The byte is treated as
+ * unsigned, so the returned Long will always be positive.
+ * @param index indicates which element in the current array to retrieve
+ * @return Long will not have it's high order bits set.
+ */
+ override fun getElementAsLong(index: Int): Long = buf toPosLong index
+
+ /**
+ * gets one byte at the current position without changing the position. The byte is treated as
+ * unsigned, so the returned ULong will always be positive.
+ * @param index indicates which element in the current array to retrieve
+ * @return ULong will not have it's high order bits set.
+ */
+ override fun getElementAsULong(index: Int): ULong = buf toPosULong index
+
+ fun get(
+ destination: UByteArray,
+ destinationOffset: Int = 0,
+ size: Int = destination.size,
+ ) {
+ super.fillArray(destination, destinationOffset, size)
+ }
+
+ override fun getBytes(length: Int): UByteArray {
+ val l = min(remaining, length)
+ val a = UByteArray(l)
+ buf.copyInto(a, 0, position, position + l)
+ position += l
+ return a
+ }
+
+ override fun getBytes(bytes: UByteArray) {
+ val l = min(remaining, bytes.size)
+ buf.copyInto(bytes, 0, position, position + l)
+ position += l
+ }
+
+ override fun put(bytes: UByteArray) {
+ val l = min(remaining, bytes.size)
+ bytes.copyInto(buf, position, 0, l)
+ position += l
+ }
+
+ /**
+ * Copies specified bytes from source to this buffer, starting at position, for the
+ * specified length. Position is incremented by the length. Any bounds violation throws
+ * an IllegalArgumentException
+ *
+ * @param source byte array to write from, will be unchanged
+ * @param sourceOffset starting offset in source, defaults to 0
+ * @param length number of bytes to copy, defaults to size of source
+ * @return this
+ * @throws IllegalArgumentException on bounds violation
+ */
+ fun putBytes(
+ source: UByteArray,
+ sourceOffset: Int = 0,
+ length: Int = source.size,
+ ) {
+ checkBounds(sourceOffset, length, length)
+ if (length > remaining) {
+ throw IllegalArgumentException("Length:$length exceeds remaining:$remaining")
+ }
+ for (i in sourceOffset until sourceOffset + length) {
+ byte = source[i]
+ }
+ }
+
+ override fun putEndian(bytes: UByteArray) {
+ if (order == ByteOrder.LittleEndian) {
+ bytes.reverse()
+ }
+ put(bytes)
+ }
+
+ override fun setElementAt(
+ index: Int,
+ element: UByte,
+ ) {
+ buf[index] = element
+ }
+
+ /**
+ * Make a new ByteBuffer containing the [remaining bytes] of this one. Length can be overriden to
+ * a shorter value than the default [remaining]. Position is unaffected
+ * @param length defaults to [remaining]. can be between 1 and [remaining]
+ */
+ override fun slice(length: Int): UByteBuffer {
+ val bytes = UByteArray(length)
+ buf.copyInto(bytes, 0, position, position + length)
+ return UByteBuffer(bytes, this.order)
+ }
+
+ override fun shortToArray(short: Short): UByteArray =
+ ubyteArrayOf(
+ (short.toInt() shr 8).toUByte(),
+ short.toUByte(),
+ )
+
+ override fun ushortToArray(ushort: UShort): UByteArray =
+ ubyteArrayOf(
+ (ushort.toUInt() shr 8).toUByte(),
+ ushort.toUByte(),
+ )
+
+ override fun intToArray(int: Int): UByteArray =
+ ubyteArrayOf(
+ (int shr 24 and 0xff).toUByte(),
+ (int shr 16 and 0xff).toUByte(),
+ (int shr 8 and 0xff).toUByte(),
+ (int and 0xff).toUByte(),
+ )
+
+ override fun uintToArray(int: UInt): UByteArray =
+ ubyteArrayOf(
+ (int shr 24 and 0xffu).toUByte(),
+ (int shr 16 and 0xffu).toUByte(),
+ (int shr 8 and 0xffu).toUByte(),
+ (int and 0xffu).toUByte(),
+ )
+
+ override fun longToArray(long: Long): UByteArray =
+ ubyteArrayOf(
+ (long shr 56 and 0xff).toUByte(),
+ (long shr 48 and 0xff).toUByte(),
+ (long shr 40 and 0xff).toUByte(),
+ (long shr 32 and 0xff).toUByte(),
+ (long shr 24 and 0xff).toUByte(),
+ (long shr 16 and 0xff).toUByte(),
+ (long shr 8 and 0xff).toUByte(),
+ (long and 0xff).toUByte(),
+ )
+
+ override fun ulongToArray(uLong: ULong): UByteArray =
+ ubyteArrayOf(
+ (uLong shr 56 and 0xffu).toUByte(),
+ (uLong shr 48 and 0xffu).toUByte(),
+ (uLong shr 40 and 0xffu).toUByte(),
+ (uLong shr 32 and 0xffu).toUByte(),
+ (uLong shr 24 and 0xffu).toUByte(),
+ (uLong shr 16 and 0xffu).toUByte(),
+ (uLong shr 8 and 0xffu).toUByte(),
+ (uLong and 0xffu).toUByte(),
+ )
+
+ /**
+ * Convert from a [UByteBuffer] to a [ByteBuffer], retaining the same capacity, position, limit
+ * and contents
+ */
+ fun toByteBuffer(): ByteBuffer {
+ val uBuf = ByteBuffer(capacity, order, isReadOnly, contentBytes.toByteArray())
+ uBuf.positionLimit(position, remaining)
+ return uBuf
+ }
+
+ override fun toString(): String =
+ buildString {
+ append("Position: $position, limit: $limit, remaining: $remaining. Content: 0x")
+ for (i in position until limit) {
+ append("${contentBytes[i].toString(16).padStart(2, '0')} ")
+ }
+ }
+
+ /**
+ * Tells whether or not this buffer is equal to another object.
+ *
+ * Two byte buffers are equal if, and only if,
+ *
+ *
+ *
+ * They have the same element type,
+ *
+ * They have the same number of remaining elements, and
+ *
+ *
+ * The two sequences of remaining elements, considered
+ * independently of their starting positions, are pointwise equal.
+
+ *
+ *
+ *
+ *
+ * A byte buffer is not equal to any other type of object.
+ *
+ * @param other The object to which this buffer is to be compared
+ *
+ * @return true if, and only if, this buffer is equal to the
+ * given object
+ */
+ override fun equals(other: Any?): Boolean {
+ if (other == null) return false
+ if (this === other) return true
+ if (other !is UByteBuffer) return false
+ if (remaining != other.remaining) return false
+ var i = limit - 1
+ var j = other.limit - 1
+ while (i >= position) {
+ if (buf[i--] != other.buf[j--]) return false
+ }
+ return true
+ }
+
+ /**
+ * Returns the current hash code of this buffer.
+ *
+ * The hash code of a byte buffer depends only upon its remaining
+ * elements; that is, upon the elements from position() up to, and
+ * including, the element at limit() - 1.
+ *
+ *
Because buffer hash codes are content-dependent, it is inadvisable
+ * to use buffers as keys in hash maps or similar data structures unless it
+ * is known that their contents will not change.
+ *
+ * @return The current hash code of this buffer
+ */
+ override fun hashCode(): Int {
+ var h = 1
+ val p: Int = position
+ for (i in limit - 1 downTo p) h = 31 * h + buf[i].toInt()
+ return h
+ }
+}
diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/CustomBitSet.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/CustomBitSet.kt
new file mode 100644
index 000000000..b148010c2
--- /dev/null
+++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/CustomBitSet.kt
@@ -0,0 +1,392 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.utils.io
+// Credits: skolson, from https://github.com/skolson/KmpIO
+
+import kotlin.math.max
+
+class CustomBitSet(
+ val numberOfBits: Int,
+) {
+ private var words =
+ LongArray(wordIndex(numberOfBits - 1) + 1) { 0L }
+ private val wordsInUse: Int
+ get() {
+ var i = words.size - 1
+ while (i >= 0) {
+ if (words[i] != 0L) break
+ i--
+ }
+ return i + 1
+ }
+
+ val length: Int
+ get() {
+ if (wordsInUse == 0) {
+ return 0
+ }
+
+ return BITS_PER_WORD * (wordsInUse - 1) +
+ (BITS_PER_WORD - numberOfLeadingZeros(words[wordsInUse - 1]))
+ }
+
+ constructor(bytes: ByteArray, bitsCount: Int = bytes.size * 8) : this(bitsCount) {
+ transformBuffer(ByteBuffer(bytes))
+ }
+
+// constructor(buffer: ByteBuffer, bitsCount: Int = buffer.remaining * 8) : this(bitsCount) {
+// transformBuffer(buffer)
+// }
+//
+ private fun transformBuffer(buffer: ByteBuffer) {
+ words = LongArray((buffer.remaining + 7) / 8) { 0 }
+ var i = 0
+ while (buffer.remaining >= 8) words[i++] = buffer.long
+
+ var j = 0
+ while (buffer.remaining > 0) {
+ words[i] = words[i] or ((buffer.byte.toLong() and 0xffL) shl (8 * j))
+ j++
+ }
+ }
+
+ val empty: Boolean
+ get() {
+ return wordsInUse == 0
+ }
+
+ fun toByteArray(): ByteArray {
+ val n = wordsInUse
+ if (n == 0) return ByteArray(0)
+ var len = 8 * (n - 1)
+
+ var x = words[n - 1]
+ while (x != 0L) {
+ len++
+ x = x ushr 8
+ }
+
+ val sz = if (numberOfBits % 8 > 0) (numberOfBits / 8) + 1 else numberOfBits / 8
+ val bytes = ByteArray(sz)
+ val bb = ByteBuffer(bytes)
+ bb.order = Buffer.ByteOrder.LittleEndian
+ for (i in 0 until n - 1) bb.long = words[i]
+ x = words[n - 1]
+ while (x != 0L) {
+ bb.byte = (x and 0xff).toByte()
+ x = x ushr 8
+ }
+ return bytes
+ }
+
+ fun flip(bitIndex: Int) {
+ if (bitIndex < 0) throw IndexOutOfBoundsException("bitIndex < 0: $bitIndex")
+ val wordIndex = wordIndex(bitIndex)
+ expandTo(wordIndex)
+ words[wordIndex] = words[wordIndex] xor (1L shl bitIndex)
+ checkInvariants()
+ }
+
+ fun flip(
+ fromIndex: Int,
+ toIndex: Int,
+ ) {
+ checkRange(fromIndex, toIndex)
+ if (fromIndex == toIndex) return
+ val startWordIndex: Int = wordIndex(fromIndex)
+ val endWordIndex: Int = wordIndex(toIndex - 1)
+ expandTo(endWordIndex)
+ val firstWordMask: Long = WORD_MASK shl fromIndex
+ val lastWordMask: Long = WORD_MASK ushr -toIndex
+ if (startWordIndex == endWordIndex) {
+ // Case 1: One word
+ words[startWordIndex] =
+ words[startWordIndex] xor (firstWordMask and lastWordMask)
+ } else {
+ // Case 2: Multiple words
+ // Handle first word
+ words[startWordIndex] = words[startWordIndex] xor firstWordMask
+
+ // Handle intermediate words, if any
+ for (i in startWordIndex + 1 until endWordIndex) {
+ words[i] =
+ words[i] xor WORD_MASK
+ }
+
+ // Handle last word
+ words[endWordIndex] = words[endWordIndex] xor lastWordMask
+ }
+ checkInvariants()
+ }
+
+ operator fun get(bitIndex: Int): Boolean {
+ if (bitIndex < 0) throw IndexOutOfBoundsException("bitIndex < 0: $bitIndex")
+
+ checkInvariants()
+
+ val wordIndex = wordIndex(bitIndex)
+ return (
+ wordIndex < wordsInUse &&
+ words[wordIndex] and (1L shl bitIndex) != 0L
+ )
+ }
+
+ operator fun set(
+ bitIndex: Int,
+ on: Boolean,
+ ) {
+ if (!on) {
+ clear(bitIndex)
+ return
+ }
+ if (bitIndex < 0) throw IndexOutOfBoundsException("bitIndex < 0: $bitIndex")
+ val wordIndex: Int = wordIndex(bitIndex)
+ expandTo(wordIndex)
+ words[wordIndex] = words[wordIndex] or (1L shl bitIndex) // Restores invariants
+ checkInvariants()
+ }
+
+ fun iterateSetBits(
+ startIndex: Int = 0,
+ onSetBit: (Int) -> Boolean,
+ ): Int {
+ var index = nextSetBit(startIndex)
+ var count = 0
+ while (index >= 0) {
+ count++
+ if (!onSetBit(index)) break
+ index = nextSetBit(index + 1)
+ }
+ return count
+ }
+
+ fun nextSetBit(fromIndex: Int): Int {
+ if (fromIndex < 0) throw IndexOutOfBoundsException("fromIndex < 0: $fromIndex")
+ checkInvariants()
+ var u = wordIndex(fromIndex)
+ if (u >= wordsInUse) return -1
+ var word = words[u] and (WORD_MASK shl fromIndex)
+ while (true) {
+ if (word != 0L) return u * BITS_PER_WORD + numberOfTrailingZeros(word)
+ if (++u == wordsInUse) return -1
+ word = words[u]
+ }
+ }
+
+ fun iterateClearBits(
+ startIndex: Int = 0,
+ onClearedBit: (Int) -> Boolean,
+ ): Int {
+ var index = nextClearBit(startIndex)
+ var count = 0
+ while (index < numberOfBits) {
+ count++
+ if (!onClearedBit(index)) break
+ index = nextClearBit(index + 1)
+ }
+ return count
+ }
+
+ fun nextClearBit(fromIndex: Int): Int {
+ if (fromIndex < 0) throw IndexOutOfBoundsException("fromIndex < 0: $fromIndex")
+ checkInvariants()
+ var u = wordIndex(fromIndex)
+ if (u >= wordsInUse) return fromIndex
+ var word = words[u].inv() and (WORD_MASK shl fromIndex)
+ while (true) {
+ if (word != 0L) return u * BITS_PER_WORD + numberOfTrailingZeros(word)
+ if (++u == wordsInUse) return wordsInUse * BITS_PER_WORD
+ word = words[u].inv()
+ }
+ }
+
+ fun clear(bitIndex: Int) {
+ if (bitIndex < 0) throw IndexOutOfBoundsException("bitIndex < 0: $bitIndex")
+ val wordIndex = wordIndex(bitIndex)
+ if (wordIndex >= wordsInUse) return
+ words[wordIndex] = words[wordIndex] and (1L shl bitIndex).inv()
+ checkInvariants()
+ }
+
+ fun clear() {
+ for (i in words.indices) words[i] = 0
+ }
+
+ private constructor(longArray: LongArray) : this(0) {
+ words = longArray
+ }
+
+ private fun ensureCapacity(wordsRequired: Int) {
+ if (words.size < wordsRequired) {
+ // Allocate larger of doubled size or required size
+ val request: Int = max(2 * words.size, wordsRequired)
+ words = words.copyOf(request)
+ }
+ }
+
+ private fun expandTo(wordIndex: Int) {
+ val wordsRequired = wordIndex + 1
+ if (wordsInUse < wordsRequired) {
+ ensureCapacity(wordsRequired)
+ }
+ }
+
+ private fun checkInvariants() {
+ if (wordsInUse == 0 || words[wordsInUse - 1] != 0L) {
+ if (wordsInUse >= 0 && wordsInUse <= words.size) {
+ if (wordsInUse == words.size || words[wordsInUse] == 0L) {
+ return
+ }
+ }
+ }
+ throw IllegalStateException("CustomBitSet check failed. wordsInUse:$wordsInUse, words:${words.size}")
+ }
+
+ override fun toString(): String {
+ var text = ""
+ var count = 0
+ iterateSetBits {
+ text = "$text, $it"
+ count++ < 50
+ }
+ return text
+ }
+
+ fun size(): Int = words.size * BITS_PER_WORD
+
+ companion object {
+ private const val ADDRESS_BITS_PER_WORD = 6
+ private const val BITS_PER_WORD = 1 shl ADDRESS_BITS_PER_WORD
+ private const val BIT_INDEX_MASK = BITS_PER_WORD - 1
+ private const val WORD_MASK = -0x1L
+
+ private fun wordIndex(bitIndex: Int): Int = bitIndex shr ADDRESS_BITS_PER_WORD
+
+ fun numberOfLeadingZeros(i: Long): Int {
+ // HD, Figure 5-6
+ if (i == 0L) return 64
+ var n = 1
+ var x = (i ushr 32).toInt()
+ if (x == 0) {
+ n += 32
+ x = i.toInt()
+ }
+ if (x ushr 16 == 0) {
+ n += 16
+ x = x shl 16
+ }
+ if (x ushr 24 == 0) {
+ n += 8
+ x = x shl 8
+ }
+ if (x ushr 28 == 0) {
+ n += 4
+ x = x shl 4
+ }
+ if (x ushr 30 == 0) {
+ n += 2
+ x = x shl 2
+ }
+ n -= x ushr 31
+ return n
+ }
+
+ fun numberOfTrailingZeros(i: Long): Int {
+ // HD, Figure 5-14
+ var x: Int
+ var y: Int
+ if (i == 0L) return 64
+ var n = 63
+ y = i.toInt()
+ if (y != 0) {
+ n -= 32
+ x = y
+ } else {
+ x = (i ushr 32).toInt()
+ }
+ y = x shl 16
+ if (y != 0) {
+ n -= 16
+ x = y
+ }
+ y = x shl 8
+ if (y != 0) {
+ n -= 8
+ x = y
+ }
+ y = x shl 4
+ if (y != 0) {
+ n -= 4
+ x = y
+ }
+ y = x shl 2
+ if (y != 0) {
+ n -= 2
+ x = y
+ }
+ return n - (x shl 1 ushr 31)
+ }
+
+ fun create(longs: LongArray): CustomBitSet {
+ var n: Int
+ n = longs.size
+ while (n > 0 && longs[n - 1] == 0L) {
+ n--
+ }
+ return CustomBitSet(longs.copyOf(n))
+ }
+
+ private fun checkRange(
+ fromIndex: Int,
+ toIndex: Int,
+ ) {
+ if (fromIndex < 0) throw IndexOutOfBoundsException("fromIndex < 0: $fromIndex")
+ if (toIndex < 0) throw IndexOutOfBoundsException("toIndex < 0: $toIndex")
+ if (fromIndex > toIndex) {
+ throw IndexOutOfBoundsException(
+ "fromIndex: " + fromIndex +
+ " > toIndex: " + toIndex,
+ )
+ }
+ }
+
+ fun create(bbIn: ByteBuffer): CustomBitSet {
+ val bb = bbIn.slice()
+ bb.order = Buffer.ByteOrder.LittleEndian
+ var n = bb.remaining
+ while (n > 0 && bb.getElementAsInt(n - 1) == 0) {
+ n--
+ }
+ val words = LongArray((n + 7) / 8)
+ bb.limit = n
+ var i = 0
+ while (bb.remaining >= 8) words[i++] = bb.long
+
+ val remaining: Int = bb.remaining
+ var j = 0
+ while (j < remaining) {
+ words[i] = words[i] or ((bb.byte.toLong() and 0xffL) shl (8 * j))
+ j++
+ }
+ return CustomBitSet(words)
+ }
+ }
+}
diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/Extensions.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/Extensions.kt
new file mode 100644
index 000000000..75d892e59
--- /dev/null
+++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/Extensions.kt
@@ -0,0 +1,489 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.utils.io
+// Credits: skolson, from https://github.com/skolson/KmpIO
+
+/**
+ * Simple extension to translate a ByteArray to a hex string
+ * @param startIndex index in array to start, defaults to zero
+ * @param length number of bytes to turn to hex
+ * @return String of size [2 * length], all lower case
+ * @throws IndexOutOfBoundsException if argument(s) specified are wrong
+ */
+fun ByteArray.toHex(
+ startIndex: Int = 0,
+ length: Int = size,
+): String {
+ val hexChars = "0123456789abcdef"
+ val result = StringBuilder(length * 2)
+ for (i in startIndex until startIndex + length) {
+ result.append(hexChars[(this[i].toInt() and 0xF0).ushr(4)])
+ result.append(hexChars[this[i].toInt() and 0x0F])
+ }
+ return result.toString()
+}
+
+/**
+ * Convenience method, forces a Byte to an Int while stripping all sign bits. the Byte becomes
+ * the LSB of the Int produced. For example, byte of 0xFF becomes 0x000000FF or 255 as the result Int.
+ */
+infix fun ByteArray.toPosInt(index: Int): Int = this[index].toInt() and 0xFF
+
+/**
+ * Convenience method, forces a Byte to an UInt while stripping all sign bits. the Byte becomes
+ * the LSB of the Int produced. For example, byte of 0xFF becomes 0x000000FF or 255 as the result Int.
+ */
+infix fun ByteArray.toPosUInt(index: Int): UInt = this[index].toUInt() and 0xFFu
+
+/**
+ * Convenience method, forces a UByte to an Int while stripping all sign bits. the Byte becomes
+ * the LSB of the Int produced. For example, byte of 0xFF becomes 0x000000FF or 255 as the result Int.
+ */
+infix fun UByteArray.toPosInt(index: Int): Int = this[index].toInt() and 0xFF
+
+/**
+ * Convenience method, forces a Byte to an UInt while stripping all sign bits. the Byte becomes
+ * the LSB of the Int produced. For example, byte of 0xFF becomes 0x000000FF or 255 as the result Int.
+ */
+infix fun UByteArray.toPosUInt(index: Int): UInt = this[index].toUInt() and 0xFFu
+
+/**
+ * Convenience method, forces a Byte to a Long while stripping all sign bits. the Byte becomes
+ * the LSB of the Long produced. For example, byte of 0xFF becomes 0x00000000000000FF or 255 as
+ * the result Long.
+ */
+infix fun ByteArray.toPosLong(index: Int): Long = this[index].toLong() and 0xFF
+
+/**
+ * Convenience method, forces a Byte to a ULong while stripping all sign bits. the Byte becomes
+ * the LSB of the Long produced. For example, byte of 0xFF becomes 0x00000000000000FF or 255 as
+ * the result Long.
+ */
+infix fun ByteArray.toPosULong(index: Int): ULong = this[index].toULong() and 0xFFu
+
+/**
+ * Convenience method, forces a UByte to a Long while stripping all sign bits. the Byte becomes
+ * the LSB of the Long produced. For example, byte of 0xFF becomes 0x00000000000000FF or 255 as
+ * the result Int.
+ */
+infix fun UByteArray.toPosLong(index: Int): Long = this[index].toLong() and 0xFF
+
+/**
+ * Convenience method, forces a UByte to a ULong while using only the LSB.
+ * For example, byte of 0xFF becomes 0x00000000000000FF or 255 as the result ULong.
+ */
+infix fun UByteArray.toPosULong(index: Int): ULong = this[index].toULong() and 0xFFu
+
+// These endian-aware extensions functions assist with retrieving Short, UShort, Int, UInt, Long, Ulong, Float,
+// and Double values from ByteArray and UByteArray. For some reason Kotlin only offers these for Little
+// Endian (you have to do your own reverse() for BigEndian) and only in Kotlin Native.
+
+/**
+ * Change one byte into an Int with toPosInt, then Binary Shift left the specified number of times.
+ * @param index of byte to change to an Int.
+ * @param shift number of times value is binary shifted left.
+ * @return resulting Int
+ */
+fun ByteArray.toIntShl(
+ index: Int,
+ shift: Int = 0,
+): Int = this toPosInt index shl shift
+
+/**
+ * Change one byte into an Int with toPosInt, then Binary Shift left the specified number of times.
+ * @param index of byte to change to an Int.
+ * @param shift number of times value is binary shifted left.
+ * @return result as UInt
+ */
+fun ByteArray.toUIntShl(
+ index: Int,
+ shift: Int = 0,
+): UInt = this toPosUInt index shl shift
+
+/**
+ * Change one byte into a Long, after the byte value is Binary Shifted left the specified number of times.
+ * @param index of byte to change to an Long.
+ * @param shift number of times value is binary shifted left.
+ * @return resulting Long
+ */
+fun ByteArray.toLongShl(
+ index: Int,
+ shift: Int = 0,
+): Long = this toPosLong index shl shift
+
+/**
+ * Change one byte into a ULong, after the byte value is Binary Shifted left the specified number of times.
+ * @param index of byte to change to an Long.
+ * @param shift number of times value is binary shifted left.
+ * @return resulting ULong
+ */
+fun ByteArray.toULongShl(
+ index: Int,
+ shift: Int = 0,
+): ULong = this toPosULong index shl shift
+
+/**
+ * starting at the specified index, change bytes at index and index+1 to a Short. Both LittleEndian
+ * and BigEndian encoding schemes are supported.
+ * @param index where two bytes to be converted to short start
+ * @param littleEndian defaults to true for LittleEndian, or false for BigEndian
+ * @return the resulting Short
+ */
+fun ByteArray.getShortAt(
+ index: Int,
+ littleEndian: Boolean = true,
+): Short =
+ if (littleEndian) {
+ (toIntShl(index + 1, 8) or toIntShl(index)).toShort()
+ } else {
+ (toIntShl(index, 8) or toIntShl(index + 1)).toShort()
+ }
+
+/**
+ * starting at the specified index, change bytes at index and index+1 to a UShort. Both LittleEndian
+ * and BigEndian encoding schemes are supported.
+ * @param index where two bytes to be converted to UShort start
+ * @param littleEndian defaults to true for LittleEndian, or false for BigEndian
+ * @return the resulting UShort
+ */
+fun ByteArray.getUShortAt(
+ index: Int,
+ littleEndian: Boolean = true,
+): UShort =
+ if (littleEndian) {
+ (toUIntShl(index + 1, 8) or toUIntShl(index)).toUShort()
+ } else {
+ (toUIntShl(index, 8) or toUIntShl(index + 1)).toUShort()
+ }
+
+/**
+ * starting at the specified index, change bytes at index, index+1, index+2, and index+3 to an Int.
+ * Both LittleEndian and BigEndian encoding schemes are supported.
+ * @param index where four bytes to be converted to Int start
+ * @param littleEndian defaults to true for LittleEndian, or false for BigEndian
+ * @return the resulting Int
+ */
+fun ByteArray.getIntAt(
+ index: Int,
+ littleEndian: Boolean = true,
+): Int =
+ if (littleEndian) {
+ toIntShl(index + 3, 24) or
+ toIntShl(index + 2, 16) or
+ toIntShl(index + 1, 8) or
+ toIntShl(index)
+ } else {
+ toIntShl(index, 24) or
+ toIntShl(index + 1, 16) or
+ toIntShl(index + 2, 8) or
+ toIntShl(index + 3)
+ }
+
+/**
+ * starting at the specified index, change bytes at index, index+1, index+2, and index+3 to an UInt.
+ * Both LittleEndian and BigEndian encoding schemes are supported.
+ * @param index where four bytes to be converted to UInt start
+ * @param littleEndian defaults to true for LittleEndian, or false for BigEndian
+ * @return the resulting UInt
+ */
+fun ByteArray.getUIntAt(
+ index: Int,
+ littleEndian: Boolean = true,
+): UInt =
+ if (littleEndian) {
+ toUIntShl(index + 3, 24) or
+ toUIntShl(index + 2, 16) or
+ toUIntShl(index + 1, 8) or
+ toUIntShl(index)
+ } else {
+ toUIntShl(index, 24) or
+ toUIntShl(index + 1, 16) or
+ toUIntShl(index + 2, 8) or
+ toUIntShl(index + 3)
+ }
+
+/**
+ * Starting at the specified index, change bytes at [index..index+7] to a Long.
+ * Both LittleEndian and BigEndian encoding schemes are supported.
+ * @param index where eight bytes to be converted to Long start
+ * @param littleEndian defaults to true for LittleEndian, or false for BigEndian
+ * @return the resulting Long
+ */
+fun ByteArray.getLongAt(
+ index: Int,
+ littleEndian: Boolean = true,
+): Long =
+ if (littleEndian) {
+ toLongShl(index + 7, 56) or
+ toLongShl(index + 6, 48) or
+ toLongShl(index + 5, 40) or
+ toLongShl(index + 4, 32) or
+ toLongShl(index + 3, 24) or
+ toLongShl(index + 2, 16) or
+ toLongShl(index + 1, 8) or
+ toLongShl(index)
+ } else {
+ toLongShl(index, 56) or
+ toLongShl(index + 1, 48) or
+ toLongShl(index + 2, 40) or
+ toLongShl(index + 3, 32) or
+ toLongShl(index + 4, 24) or
+ toLongShl(index + 5, 16) or
+ toLongShl(index + 6, 8) or
+ toLongShl(index + 7)
+ }
+
+/**
+ * Starting at the specified index, change bytes at [index..index+7] to a ULong.
+ * Both LittleEndian and BigEndian encoding schemes are supported.
+ * @param index where eight bytes to be converted to ULong start
+ * @param littleEndian defaults to true for LittleEndian, or false for BigEndian
+ * @return the resulting ULong
+ */
+fun ByteArray.getULongAt(
+ index: Int,
+ littleEndian: Boolean = true,
+): ULong =
+ if (littleEndian) {
+ toULongShl(index + 7, 56) or
+ toULongShl(index + 6, 48) or
+ toULongShl(index + 5, 40) or
+ toULongShl(index + 4, 32) or
+ toULongShl(index + 3, 24) or
+ toULongShl(index + 2, 16) or
+ toULongShl(index + 1, 8) or
+ toULongShl(index)
+ } else {
+ toULongShl(index, 56) or
+ toULongShl(index + 1, 48) or
+ toULongShl(index + 2, 40) or
+ toULongShl(index + 3, 32) or
+ toULongShl(index + 4, 24) or
+ toULongShl(index + 5, 16) or
+ toULongShl(index + 6, 8) or
+ toULongShl(index + 7)
+ }
+
+/**
+ * Simple extension to translate a ByteArray to a hex string
+ * @param startIndex index in array to start, defaults to zero
+ * @param length number of bytes to turn to hex
+ * @return String of size [2 * length], all lower case
+ * @throws IndexOutOfBoundsException if argument(s) specified are wrong
+ */
+fun UByteArray.toHex(
+ startIndex: Int = 0,
+ length: Int = size,
+): String {
+ val hexChars = "0123456789abcdef"
+ val result = StringBuilder(length * 2)
+ for (i in startIndex until startIndex + length) {
+ result.append(hexChars[(this[i].toInt() and 0xF0).ushr(4)])
+ result.append(hexChars[this[i].toInt() and 0x0F])
+ }
+ return result.toString()
+}
+
+/**
+ * Change one byte into an Int, after the byte value is Binary Shifted left the specified number of times.
+ * @param index of byte to change to an Int.
+ * @param shift number of times value is binary shifted left.
+ * @return resulting Int
+ */
+fun UByteArray.toIntShl(
+ index: Int,
+ shift: Int = 0,
+): Int = this toPosInt index shl shift
+
+/**
+ * Change one byte into an UInt, after the byte value is Binary Shifted left the specified number of times.
+ * @param index of byte to change to an Int.
+ * @param shift number of times value is binary shifted left.
+ * @return resulting UInt
+ */
+fun UByteArray.toUIntShl(
+ index: Int,
+ shift: Int = 0,
+): UInt = this toPosUInt index shl shift
+
+/**
+ * Change one byte into a Long, after the byte value is Binary Shifted left the specified number of times.
+ * @param index of byte to change to an Long.
+ * @param shift number of times value is binary shifted left.
+ * @return resulting Long
+ */
+fun UByteArray.toLongShl(
+ index: Int,
+ shift: Int = 0,
+): Long = this toPosLong index shl shift
+
+/**
+ * Change one byte into a ULong, after the byte value is Binary Shifted left the specified number of times.
+ * @param index of byte to change to an Long.
+ * @param shift number of times value is binary shifted left.
+ * @return resulting ULong
+ */
+fun UByteArray.toULongShl(
+ index: Int,
+ shift: Int = 0,
+): ULong = this toPosULong index shl shift
+
+/**
+ * starting at the specified index, change bytes at index and index+1 to a Short. Both LittleEndian
+ * and BigEndian encoding schemes are supported.
+ * @param index where two bytes to be converted to short start
+ * @param littleEndian defaults to true for LittleEndian, or false for BigEndian
+ * @return the resulting Short
+ */
+fun UByteArray.getShortAt(
+ index: Int,
+ littleEndian: Boolean = true,
+): Short =
+ if (littleEndian) {
+ (toIntShl(index + 1, 8) or toIntShl(index)).toShort()
+ } else {
+ (toIntShl(index, 8) or toIntShl(index + 1)).toShort()
+ }
+
+/**
+ * starting at the specified index, change bytes at index and index+1 to a UShort. Both LittleEndian
+ * and BigEndian encoding schemes are supported.
+ * @param index where two bytes to be converted to UShort start
+ * @param littleEndian defaults to true for LittleEndian, or false for BigEndian
+ * @return the resulting UShort
+ */
+fun UByteArray.getUShortAt(
+ index: Int,
+ littleEndian: Boolean = true,
+): UShort =
+ if (littleEndian) {
+ (toUIntShl(index + 1, 8) or toUIntShl(index)).toUShort()
+ } else {
+ (toUIntShl(index, 8) or toUIntShl(index + 1)).toUShort()
+ }
+
+/**
+ * starting at the specified index, change bytes at index, index+1, index+2, and index+3 to an Int.
+ * Both LittleEndian and BigEndian encoding schemes are supported.
+ * @param index where four bytes to be converted to Int start
+ * @param littleEndian defaults to true for LittleEndian, or false for BigEndian
+ * @return the resulting Int
+ */
+fun UByteArray.getIntAt(
+ index: Int,
+ littleEndian: Boolean = true,
+): Int =
+ if (littleEndian) {
+ toIntShl(index + 3, 24) or
+ toIntShl(index + 2, 16) or
+ toIntShl(index + 1, 8) or
+ toIntShl(index)
+ } else {
+ toIntShl(index, 24) or
+ toIntShl(index + 1, 16) or
+ toIntShl(index + 2, 8) or
+ toIntShl(index + 3)
+ }
+
+/**
+ * starting at the specified index, change bytes at index, index+1, index+2, and index+3 to an UInt.
+ * Both LittleEndian and BigEndian encoding schemes are supported.
+ * @param index where four bytes to be converted to UInt start
+ * @param littleEndian defaults to true for LittleEndian, or false for BigEndian
+ * @return the resulting UInt
+ */
+fun UByteArray.getUIntAt(
+ index: Int,
+ littleEndian: Boolean = true,
+): UInt =
+ if (littleEndian) {
+ toUIntShl(index + 3, 24) or
+ toUIntShl(index + 2, 16) or
+ toUIntShl(index + 1, 8) or
+ toUIntShl(index)
+ } else {
+ toUIntShl(index, 24) or
+ toUIntShl(index + 1, 16) or
+ toUIntShl(index + 2, 8) or
+ toUIntShl(index + 3)
+ }
+
+/**
+ * Starting at the specified index, change bytes at [index..index+7] to a Long.
+ * Both LittleEndian and BigEndian encoding schemes are supported.
+ * @param index where eight bytes to be converted to Long start
+ * @param littleEndian defaults to true for LittleEndian, or false for BigEndian
+ * @return the resulting Long
+ */
+fun UByteArray.getLongAt(
+ index: Int,
+ littleEndian: Boolean = true,
+): Long =
+ if (littleEndian) {
+ toLongShl(index + 7, 56) or
+ toLongShl(index + 6, 48) or
+ toLongShl(index + 5, 40) or
+ toLongShl(index + 4, 32) or
+ toLongShl(index + 3, 24) or
+ toLongShl(index + 2, 16) or
+ toLongShl(index + 1, 8) or
+ toLongShl(index)
+ } else {
+ toLongShl(index, 56) or
+ toLongShl(index + 1, 48) or
+ toLongShl(index + 2, 40) or
+ toLongShl(index + 3, 32) or
+ toLongShl(index + 4, 24) or
+ toLongShl(index + 5, 16) or
+ toLongShl(index + 6, 8) or
+ toLongShl(index + 7)
+ }
+
+/**
+ * Starting at the specified index, change bytes at [index..index+7] to a ULong.
+ * Both LittleEndian and BigEndian encoding schemes are supported.
+ * @param index where eight bytes to be converted to ULong start
+ * @param littleEndian defaults to true for LittleEndian, or false for BigEndian
+ * @return the resulting ULong
+ */
+fun UByteArray.getULongAt(
+ index: Int,
+ littleEndian: Boolean = true,
+): ULong =
+ if (littleEndian) {
+ toULongShl(index + 7, 56) or
+ toULongShl(index + 6, 48) or
+ toULongShl(index + 5, 40) or
+ toULongShl(index + 4, 32) or
+ toULongShl(index + 3, 24) or
+ toULongShl(index + 2, 16) or
+ toULongShl(index + 1, 8) or
+ toULongShl(index)
+ } else {
+ toULongShl(index, 56) or
+ toULongShl(index + 1, 48) or
+ toULongShl(index + 2, 40) or
+ toULongShl(index + 3, 32) or
+ toULongShl(index + 4, 24) or
+ toULongShl(index + 5, 16) or
+ toULongShl(index + 6, 8) or
+ toULongShl(index + 7)
+ }
diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt
index f1990a734..52b65b39e 100644
--- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt
+++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt
@@ -20,37 +20,62 @@
*/
package com.vitorpamplona.quartz.utils.mac
+import dev.whyoleg.cryptography.CryptographyProvider
+import dev.whyoleg.cryptography.algorithms.HMAC
+import dev.whyoleg.cryptography.algorithms.SHA256
+import dev.whyoleg.cryptography.algorithms.SHA512
+import dev.whyoleg.cryptography.providers.apple.Apple
+
actual class MacInstance actual constructor(
algorithm: String,
key: ByteArray,
) {
+ private val cryptoProvider = CryptographyProvider.Apple
+
+ private var internalMacInstance: HMAC.Key =
+ cryptoProvider
+ .get(HMAC)
+ .keyDecoder(digestForAlgorithm(algorithm))
+ .decodeFromByteArrayBlocking(HMAC.Key.Format.RAW, key)
+
+ private var hmacSignFunction = internalMacInstance.signatureGenerator().createSignFunction()
+
actual fun init(
key: ByteArray,
algorithm: String,
) {
- TODO("Not yet implemented")
+ internalMacInstance =
+ cryptoProvider
+ .get(HMAC)
+ .keyDecoder(digestForAlgorithm(algorithm))
+ .decodeFromByteArrayBlocking(HMAC.Key.Format.RAW, key)
+
+ hmacSignFunction = internalMacInstance.signatureGenerator().createSignFunction()
}
- actual fun getMacLength(): Int {
- TODO("Not yet implemented")
- }
+ actual fun getMacLength(): Int = hmacSignFunction.signIntoByteArray(internalMacInstance.encodeToByteArrayBlocking(HMAC.Key.Format.RAW))
actual fun update(array: ByteArray) {
- TODO("Not yet implemented")
+ hmacSignFunction.update(array)
}
actual fun update(byte: Byte) {
- TODO("Not yet implemented")
+ hmacSignFunction.update(byteArrayOf(byte))
}
- actual fun doFinal(): ByteArray {
- TODO("Not yet implemented")
- }
+ actual fun doFinal(): ByteArray = hmacSignFunction.signToByteArray()
actual fun doFinal(
output: ByteArray,
offset: Int,
) {
- TODO("Not yet implemented")
+ hmacSignFunction.signIntoByteArray(output, offset)
}
+
+ private fun digestForAlgorithm(algorithm: String) =
+ when (algorithm) {
+ "HmacSHA256" -> SHA256
+ "HmacSHA512" -> SHA512
+ else -> error("Algorithm is not yet supported.")
+ }
}
diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.kt
index 493c189e2..00c7bda27 100644
--- a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.kt
+++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/TestResourceLoader.kt
@@ -20,37 +20,30 @@
*/
package com.vitorpamplona.quartz
-import kotlinx.cinterop.BetaInteropApi
+import dev.whyoleg.cryptography.CryptographyProviderApi
+import dev.whyoleg.cryptography.providers.base.toByteArray
import kotlinx.cinterop.ExperimentalForeignApi
-import platform.Foundation.NSBundle
+import kotlinx.cinterop.toKString
+import kotlinx.io.files.FileNotFoundException
+import platform.Foundation.NSData
import platform.Foundation.NSString
import platform.Foundation.NSUTF8StringEncoding
+import platform.Foundation.dataWithContentsOfFile
import platform.Foundation.stringWithContentsOfFile
-import platform.darwin.NSObject
-import platform.darwin.NSObjectMeta
+import platform.posix.getenv
actual class TestResourceLoader {
- @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
+ @OptIn(ExperimentalForeignApi::class)
actual fun loadString(file: String): String {
- // Split the filename and extension (e.g., "data.json" -> "data", "json")
- val basename = file.substringBeforeLast(".")
- val extension = file.substringAfterLast(".", "")
-
- // Locate the file in the main application bundle
- val path =
- NSBundle.mainBundle.pathForResource(basename, ofType = extension)
- ?: throw IllegalArgumentException("Resource not found: $file")
-
- // Read the file content as a UTF-8 string
- return NSString
- .stringWithContentsOfFile(
- path = path,
- encoding = NSUTF8StringEncoding,
- error = null,
- ).toString()
+ val resourceDir = getenv("TEST_RESOURCES_ROOT")?.toKString()
+ val filePath = "$resourceDir/$file"
+ return NSString.stringWithContentsOfFile(filePath, encoding = NSUTF8StringEncoding, error = null) as String
}
- private class BundleMarker : NSObject() {
- companion object : NSObjectMeta()
+ @OptIn(ExperimentalForeignApi::class, CryptographyProviderApi::class)
+ fun loadFileData(file: String): ByteArray {
+ val resourceDir = getenv("TEST_RESOURCES_ROOT")?.toKString()
+ val filePath = "$resourceDir/$file"
+ return NSData.dataWithContentsOfFile(filePath)?.toByteArray() ?: throw FileNotFoundException("Resource $file was not found.")
}
}
diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip04Dm/EncryptionTest.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip04Dm/EncryptionTest.kt
new file mode 100644
index 000000000..b34458b6b
--- /dev/null
+++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip04Dm/EncryptionTest.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip04Dm
+
+import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
+import com.vitorpamplona.quartz.nip01Core.core.toHexKey
+import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
+import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo
+import com.vitorpamplona.quartz.nip04Dm.crypto.Encryption
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+class EncryptionTest {
+ private val nip04 = Encryption()
+
+ val sk1 = "91ba716fa9e7ea2fcbad360cf4f8e0d312f73984da63d90f524ad61a6a1e7dbe".hexToByteArray()
+ val sk2 = "96f6fa197aa07477ab88f6981118466ae3a982faab8ad5db9d5426870c73d220".hexToByteArray()
+ val pk1 = Nip01.pubKeyCreate(sk1)
+ val pk2 = Nip01.pubKeyCreate(sk2)
+
+ val expectedShared = "7ce22696eb0e303ddaa491bdf2a56b79d249f2d861b8e012a933e01dc4beba81"
+
+ @Test
+ fun conversationKeyTest() {
+ assertEquals(
+ expectedShared,
+ nip04.computeSharedSecret(sk2, pk1).toHexKey(),
+ )
+
+ assertEquals(
+ expectedShared,
+ nip04.computeSharedSecret(sk1, pk2).toHexKey(),
+ )
+ }
+
+ @Test
+ fun encryptDecryptTest() {
+ val message = "testing"
+ val cipher = nip04.encrypt(message, sk2, pk1)
+
+ assertEquals(message, nip04.decrypt(cipher, sk2, pk1))
+ assertEquals(message, nip04.decrypt(cipher, sk1, pk2))
+
+ val cipher2 = nip04.encrypt(message, sk1, pk2)
+
+ assertEquals(message, nip04.decrypt(cipher2, sk2, pk1))
+ assertEquals(message, nip04.decrypt(cipher2, sk1, pk2))
+ }
+
+ @Test
+ fun decryptTest() {
+ val cipher = "zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ=="
+
+ assertEquals("nanana", nip04.decrypt(cipher, nip04.computeSharedSecret(sk2, pk1)))
+ assertEquals("nanana", nip04.decrypt(cipher, nip04.computeSharedSecret(sk1, pk2)))
+ }
+
+ @Test
+ fun decryptLargePayloadTest() {
+ val ciphertext =
+ "6f8dMstm+udOu7yipSn33orTmwQpWbtfuY95NH+eTU1kArysWJIDkYgI2D25EAGIDJsNd45jOJ2NbVOhFiL3ZP/NWsTwXokk34iyHyA/lkjzugQ1bHXoMD1fP/Ay4hB4al1NHb8HXHKZaxPrErwdRDb8qa/I6dXb/1xxyVvNQBHHvmsM5yIFaPwnCN1DZqXf2KbTA/Ekz7Hy+7R+Sy3TXLQDFpWYqykppkXc7Fs0qSuPRyxz5+anuN0dxZa9GTwTEnBrZPbthKkNRrvZMdTGJ6WumOh9aUq8OJJWy9aOgsXvs7qjN1UqcCqQqYaVnEOhCaqWNDsVtsFrVDj+SaLIBvCiomwF4C4nIgngJ5I69tx0UNI0q+ZnvOGQZ7m1PpW2NYP7Yw43HJNdeUEQAmdCPnh/PJwzLTnIxHmQU7n7SPlMdV0SFa6H8y2HHvex697GAkyE5t8c2uO24OnqIwF1tR3blIqXzTSRl0GA6QvrSj2p4UtnWjvF7xT7RiIEyTtgU/AsihTrXyXzWWZaIBJogpgw6erlZqWjCH7sZy/WoGYEiblobOAqMYxax6vRbeuGtoYksr/myX+x9rfLrYuoDRTw4woXOLmMrrj+Mf0TbAgc3SjdkqdsPU1553rlSqIEZXuFgoWmxvVQDtekgTYyS97G81TDSK9nTJT5ilku8NVq2LgtBXGwsNIw/xekcOUzJke3kpnFPutNaexR1VF3ohIuqRKYRGcd8ADJP2lfwMcaGRiplAmFoaVS1YUhQwYFNq9rMLf7YauRGV4BJg/t9srdGxf5RoKCvRo+XM/nLxxysTR9MVaEP/3lDqjwChMxs+eWfLHE5vRWV8hUEqdrWNZV29gsx5nQpzJ4PARGZVu310pQzc6JAlc2XAhhFk6RamkYJnmCSMnb/RblzIATBi2kNrCVAlaXIon188inB62rEpZGPkRIP7PUfu27S/elLQHBHeGDsxOXsBRo1gl3te+raoBHsxo6zvRnYbwdAQa5taDE63eh+fT6kFI+xYmXNAQkU8Dp0MVhEh4JQI06Ni/AKrvYpC95TXXIphZcF+/Pv/vaGkhG2X9S3uhugwWK?iv=2vWkOQQi0WynNJz/aZ4k2g=="
+
+ val expected = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
+
+ assertEquals(expected, nip04.decrypt(ciphertext, nip04.computeSharedSecret(sk2, pk1)))
+ assertEquals(expected, nip04.decrypt(ciphertext, nip04.computeSharedSecret(sk1, pk2)))
+ }
+
+ @Test
+ fun isNIP04Encode() {
+ assertTrue(EncryptedInfo.isNIP04("Xj/oZZolaItdyQ5v7xYFpA==?iv=+a6zagBp+mr5m1aFbHQ8lA=="))
+ assertTrue(EncryptedInfo.isNIP04("zJxfaJ32rN5Dg1ODjOlEew==?iv=EV5bUjcc4OX2Km/zPp4ndQ=="))
+ assertTrue(
+ EncryptedInfo.isNIP04("6f8dMstm+udOu7yipSn33orTmwQpWbtfuY95NH+eTU1kArysWJIDkYgI2D25EAGIDJsNd45jOJ2NbVOhFiL3ZP/NWsTwXokk34iyHyA/lkjzugQ1bHXoMD1fP/Ay4hB4al1NHb8HXHKZaxPrErwdRDb8qa/I6dXb/1xxyVvNQBHHvmsM5yIFaPwnCN1DZqXf2KbTA/Ekz7Hy+7R+Sy3TXLQDFpWYqykppkXc7Fs0qSuPRyxz5+anuN0dxZa9GTwTEnBrZPbthKkNRrvZMdTGJ6WumOh9aUq8OJJWy9aOgsXvs7qjN1UqcCqQqYaVnEOhCaqWNDsVtsFrVDj+SaLIBvCiomwF4C4nIgngJ5I69tx0UNI0q+ZnvOGQZ7m1PpW2NYP7Yw43HJNdeUEQAmdCPnh/PJwzLTnIxHmQU7n7SPlMdV0SFa6H8y2HHvex697GAkyE5t8c2uO24OnqIwF1tR3blIqXzTSRl0GA6QvrSj2p4UtnWjvF7xT7RiIEyTtgU/AsihTrXyXzWWZaIBJogpgw6erlZqWjCH7sZy/WoGYEiblobOAqMYxax6vRbeuGtoYksr/myX+x9rfLrYuoDRTw4woXOLmMrrj+Mf0TbAgc3SjdkqdsPU1553rlSqIEZXuFgoWmxvVQDtekgTYyS97G81TDSK9nTJT5ilku8NVq2LgtBXGwsNIw/xekcOUzJke3kpnFPutNaexR1VF3ohIuqRKYRGcd8ADJP2lfwMcaGRiplAmFoaVS1YUhQwYFNq9rMLf7YauRGV4BJg/t9srdGxf5RoKCvRo+XM/nLxxysTR9MVaEP/3lDqjwChMxs+eWfLHE5vRWV8hUEqdrWNZV29gsx5nQpzJ4PARGZVu310pQzc6JAlc2XAhhFk6RamkYJnmCSMnb/RblzIATBi2kNrCVAlaXIon188inB62rEpZGPkRIP7PUfu27S/elLQHBHeGDsxOXsBRo1gl3te+raoBHsxo6zvRnYbwdAQa5taDE63eh+fT6kFI+xYmXNAQkU8Dp0MVhEh4JQI06Ni/AKrvYpC95TXXIphZcF+/Pv/vaGkhG2X9S3uhugwWK?iv=2vWkOQQi0WynNJz/aZ4k2g=="),
+ )
+ }
+
+ @Test
+ fun isNIP04EncodeWithBug() {
+ assertTrue(
+ EncryptedInfo.isNIP04(
+ "QOAYBWa88ConWs2C4kSvNqAcowCtg0ZRtAl7FyLSv9VMaJH4oCiDx0h8VLBnV97HdE4lv" +
+ "TW7AYC1eEw8/t1dbe0qRc3XrOt7MrPAO8yqpy1/3lFB1+10kip0+KdgT8Quvv02wTP8Dqi" +
+ "xpr2fliAIG2ONvDn+O5V0q9aVUN9HitgL/myTyR0T42edmxWeZoMBEOKvJyO80FekSsgVL" +
+ "ASafA/T5z4xs8oG88pSe9wSbSsw0xNjJeh3xLRCLuEuA9KI8hQ1Ys9nEax2UlaB/IL3o77" +
+ "OwBL+rrdUbNHTxYifgygRhg3BaXMsXRFNJbqYeMaRaNbvHkLVAQV2jLY4P/cKHBjEcTC/f" +
+ "lrCc2NCYF34rOQUY5EJVnFzM8qYVw6xNupBHTS7WFx1r60cPjG19P/+yoiTZ6bPdHTU0X2" +
+ "t64ovF2YWUq6/iKAclMaZDhWfrKqf82e62oIff55WQw2bw8A/jtBQVCf66EtEJ2OSFxNaZ" +
+ "rO+A4oLkHDCnAV+6fYzwo89gPOvORcVvSvg55yGiBFUZx9EHS6kdH1SU80/Mbxe2oI=" +
+ "?iv=gxz9pUFJFZHuV+D+hgKEOw==-null",
+ ),
+ )
+ }
+}
diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip04Dm/Nip04Test.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip04Dm/Nip04Test.kt
new file mode 100644
index 000000000..baa144ebe
--- /dev/null
+++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip04Dm/Nip04Test.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip04Dm
+
+import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
+import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class Nip04Test {
+ @Test
+ fun encryptDecryptNIP4Test() {
+ val msg = "Hi"
+
+ val privateKey = Nip01.privKeyCreate()
+ val publicKey = Nip01.pubKeyCreate(privateKey)
+
+ val encrypted = Nip04.encrypt(msg, privateKey, publicKey)
+ val decrypted = Nip04.decrypt(encrypted, privateKey, publicKey)
+
+ assertEquals(msg, decrypted)
+ }
+
+ @Test
+ fun encryptSharedSecretDecryptNIP4Test() {
+ val msg = "Hi"
+
+ val privateKey = Nip01.privKeyCreate()
+ val publicKey = Nip01.pubKeyCreate(privateKey)
+
+ val encrypted = Nip04.encrypt(msg, privateKey, publicKey)
+ val decrypted = Nip04.decrypt(encrypted, privateKey, publicKey)
+
+ assertEquals(msg, decrypted)
+ }
+}
diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/AESGCMTest.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/AESGCMTest.kt
new file mode 100644
index 000000000..e41063c7b
--- /dev/null
+++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/AESGCMTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip17Dm
+
+import com.vitorpamplona.quartz.TestResourceLoader
+import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
+import com.vitorpamplona.quartz.utils.ciphers.AESGCM
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class AESGCMTest {
+ val decryptionNonce = "01e77c94bd5aba3e3cbb69594e7ba07c"
+ val decryptionKey = "c128ecffab90ee7810e3df08e7fb2cc39a8d40f24201f48b2b36e23b34ac50ee"
+
+ val cipher =
+ AESGCM(
+ decryptionKey.hexToByteArray(),
+ decryptionNonce.encodeToByteArray(),
+ )
+
+ @Test
+ fun encryptDecrypt() {
+ val encrypted = cipher.encrypt("Testing".encodeToByteArray())
+ val decrypted = cipher.decrypt(encrypted)
+
+ assertEquals("Testing", decrypted.decodeToString())
+ }
+
+ @Test
+ fun imageTest() {
+ val image =
+ TestResourceLoader().loadFileData("ovxxk2vz.jpg")
+
+ val decrypted = cipher.decrypt(image)
+
+ assertEquals(44201, decrypted.size)
+ }
+
+ @Test
+ fun videoTest2() {
+ val myCipher =
+ AESGCM(
+ "373d19850ebc8ed5b0fefcca5cd6f27fde9cb6ac54fd32f6b4fad9d68ebe8ee0".hexToByteArray(),
+ "95e67b6874784a54299b58b8990499bd".hexToByteArray(),
+ )
+
+ val encrypted =
+ TestResourceLoader().loadFileData("trouble_video")
+
+ val decrypted = myCipher.decrypt(encrypted)
+
+ assertEquals(1277122, decrypted.size)
+ }
+}
diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/ChatroomKeyTest.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/ChatroomKeyTest.kt
new file mode 100644
index 000000000..bc2dfa1c7
--- /dev/null
+++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip17Dm/ChatroomKeyTest.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.quartz.nip17Dm
+
+import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
+import kotlinx.collections.immutable.persistentSetOf
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class ChatroomKeyTest {
+ @Test
+ fun testEquals() {
+ val k1 = ChatroomKey(persistentSetOf("Key1", "Key2"))
+ val k2 = ChatroomKey(persistentSetOf("Key1", "Key2"))
+
+ assertEquals(k1, k2)
+ assertEquals(k1.hashCode(), k2.hashCode())
+ }
+}
diff --git a/quartz/src/iosTest/resources/ovxxk2vz.jpg b/quartz/src/iosTest/resources/ovxxk2vz.jpg
new file mode 100644
index 000000000..bc96672af
Binary files /dev/null and b/quartz/src/iosTest/resources/ovxxk2vz.jpg differ
diff --git a/quartz/src/iosTest/resources/trouble_video b/quartz/src/iosTest/resources/trouble_video
new file mode 100644
index 000000000..401d5e1df
Binary files /dev/null and b/quartz/src/iosTest/resources/trouble_video differ
diff --git a/quartz/src/jvmTest/java/com/vitorpamplona/quartz/TestResourceLoader.kt b/quartz/src/jvmTest/java/com/vitorpamplona/quartz/TestResourceLoader.kt
index fe331449b..bb522a8b7 100644
--- a/quartz/src/jvmTest/java/com/vitorpamplona/quartz/TestResourceLoader.kt
+++ b/quartz/src/jvmTest/java/com/vitorpamplona/quartz/TestResourceLoader.kt
@@ -23,8 +23,8 @@ package com.vitorpamplona.quartz
actual class TestResourceLoader {
actual fun loadString(file: String): String =
this@TestResourceLoader
- .javaClass.classLoader!!
- .getResourceAsStream(file)
- .bufferedReader()
- .use { it.readText() }
+ .javaClass.classLoader
+ ?.getResourceAsStream(file)
+ ?.bufferedReader()
+ ?.use { it.readText() } ?: throw IllegalArgumentException("Resource not found: $file")
}
diff --git a/quartz/src/swift/swiftbridge/Rfc3986UriBridge.swift b/quartz/src/swift/swiftbridge/Rfc3986UriBridge.swift
new file mode 100644
index 000000000..bafd34a5b
--- /dev/null
+++ b/quartz/src/swift/swiftbridge/Rfc3986UriBridge.swift
@@ -0,0 +1,23 @@
+//
+// Created by NullDev on 31/12/2025.
+//
+
+import Foundation
+import RFC_3986
+
+@objcMembers public class Rfc3986UriBridge: NSObject {
+ public func normalizeUrl(url: String) throws -> String {
+ let uri = try RFC_3986.URI(url)
+ let normalized = uri.normalized()
+ return normalized.value
+ }
+
+ public func isUrlValid(url: String) -> Bool {
+ return RFC_3986.isValidURI(url)
+ }
+
+ public func hostFromUri(url: String) throws -> String {
+ let actualUri = try RFC_3986.URI(url)
+ return actualUri.host!
+ }
+}
diff --git a/quartz/src/swift/swiftbridge/UrlDetector.swift b/quartz/src/swift/swiftbridge/UrlDetector.swift
new file mode 100644
index 000000000..e4927c516
--- /dev/null
+++ b/quartz/src/swift/swiftbridge/UrlDetector.swift
@@ -0,0 +1,21 @@
+//
+// Created by NullDev on 20/01/2026.
+//
+
+import Foundation
+
+@objcMembers public class UrlDetector: NSObject {
+ public func findURLs(text: String) -> [String] {
+ var links = [String]()
+ let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
+ let matches = detector.matches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count))
+
+ for match in matches {
+ guard let range = Range(match.range, in: text) else { continue }
+ let url = text[range]
+ links.append(String(url))
+ }
+
+ return links
+ }
+}
\ No newline at end of file