From 4feaec5e009dc8108c0697c5bf2740e79e1c46dc Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Mon, 29 Dec 2025 19:00:00 +0100 Subject: [PATCH] Foundations(iOS Sourceset): Implement functions(some, basic) for BitSet(with passing tests that use it). --- .../vitorpamplona/quartz/utils/BitSet.ios.kt | 30 +- .../vitorpamplona/quartz/utils/io/Buffer.kt | 622 ++++++++++++ .../quartz/utils/io/ByteBuffers.kt | 892 ++++++++++++++++++ .../quartz/utils/io/CustomBitSet.kt | 474 ++++++++++ .../quartz/utils/io/Extensions.kt | 480 ++++++++++ 5 files changed, 2483 insertions(+), 15 deletions(-) create mode 100644 quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/Buffer.kt create mode 100644 quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/ByteBuffers.kt create mode 100644 quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/CustomBitSet.kt create mode 100644 quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/Extensions.kt diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/BitSet.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/BitSet.ios.kt index 0172dc0e0..c2642dde5 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/BitSet.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/BitSet.ios.kt @@ -20,32 +20,32 @@ */ package com.vitorpamplona.quartz.utils +import com.vitorpamplona.quartz.utils.io.CustomBitSet + actual class BitSet { + val nativeBitSet: CustomBitSet + actual constructor(nBits: Int) { - TODO("Not yet implemented") + nativeBitSet = CustomBitSet(nBits) + } + + constructor(bytes: ByteArray) { + nativeBitSet = CustomBitSet(bytes) } actual fun set(bitIndex: Int) { - TODO("Not yet implemented") + nativeBitSet.set(bitIndex, true) } actual fun clear(bitIndex: Int) { - TODO("Not yet implemented") + nativeBitSet.clear(bitIndex) } - actual fun get(bitIndex: Int): Boolean { - TODO("Not yet implemented") - } + actual fun get(bitIndex: Int): Boolean = nativeBitSet[bitIndex] - actual fun size(): Int { - TODO("Not yet implemented") - } + actual fun size(): Int = nativeBitSet.size() - actual fun toByteArray(): ByteArray { - TODO("Not yet implemented") - } + actual fun toByteArray(): ByteArray = nativeBitSet.toByteArray() } -actual fun bitSetValueOf(bytes: ByteArray): BitSet { - TODO("Not yet implemented") -} +actual fun bitSetValueOf(bytes: ByteArray): BitSet = BitSet(bytes) diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/Buffer.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/Buffer.kt new file mode 100644 index 000000000..67c1028f1 --- /dev/null +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/Buffer.kt @@ -0,0 +1,622 @@ +/** + * 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 + +abstract class Buffer internal constructor( + markPosition: Int, + pos: Int, + lim: Int, + cap: Int, + var order: ByteOrder = ByteOrder.LittleEndian +) { + enum class ByteOrder { LittleEndian, BigEndian } + + // Invariants: mark <= position <= limit <= capacity + var position = pos + /** + * Sets this buffer's position. If the mark is defined and larger than the + * new position then it is discarded. + * + * @param newPosition + * The new position value; must be non-negative + * and no larger than the current limit + * + * @throws IllegalArgumentException + * If the preconditions on newPosition do not hold + */ + set(newPosition) { + if (newPosition > limit || newPosition < 0) + throw IllegalArgumentException("Position $newPosition exceeds limit:$limit") + field = newPosition + if (mark > position) mark = noMark + } + + var limit = lim + /** + * Sets this buffer's limit. If the position is larger than the new limit + * then it is set to the new limit. If the mark is defined and larger than + * the new limit then it is discarded. + * + * @param newLimit + * The new limit value; must be non-negative + * and no larger than this buffer's capacity + * + * @return This buffer + * + * @throws IllegalArgumentException + * If the preconditions on newLimit do not hold + */ + set(newLimit) { + if (newLimit > capacity || newLimit < 0) + throw IllegalArgumentException("limit $newLimit exceeds capacity $capacity") + field = newLimit + if (position > limit) position = limit + if (mark > limit) mark = -1 + } + var capacity = cap + internal set + var mark = markPosition + set(value) { + if (mark < -1 || mark > position) + throw IllegalArgumentException("Mark $mark out of range of 0 to $position") + field = value + } + + /** + * Returns the number of elements between the current position and the + * limit. + * + * @return The number of elements remaining in this buffer + */ + val remaining get() = limit - position + + /** + * Tells whether there are any elements between the current position and + * the limit. + * + * @return true if, and only if, there is at least one element + * remaining in this buffer + */ + val hasRemaining get() = position < limit + + /** + * Tells whether or not this buffer is read-only. + * + * @return true if, and only if, this buffer is read-only + */ + abstract val isReadOnly: Boolean + + /** + * Subclasses provide a property that get a byte from the buffer, or put a byte in the buffer. + * Position must be incremented in both cases + */ + abstract var byte: Element + + /** + * The following functions are all usable by subclasses to encode/decode basic types + * in and Endian-aware fashion. In all cases an exception is thrown if remaining() < length + * to be read/written. Position will be incremented by the appropriate length for both gets + * and sets. + * + * For some reason, the ByteArray in Kotlin Native is the only implementation that provides + * similar function, but assumes all is Little Endian. Since these are not available in the + * common stdlib, they are not used even for little endian. + */ + + /** + * Read or write a two-byte Char at the current position. Position will increment by 2. + */ + var char: Char + get() { + val c = when (order) { + ByteOrder.LittleEndian -> ((getElementAsInt(position + 1) shl 8) or + getElementAsInt(position)).toChar() + ByteOrder.BigEndian -> ((getElementAsInt(position) shl 8) or + getElementAsInt(position + 1)).toChar() + } + position += 2 + return c + } + set(value) { + putEndian(shortToArray(value.code.toShort())) + } + + /** + * Read or write a two-byte Short at the current position. Position will increment by 2. + */ + var short: Short + get() { + if (remaining < shortLength) + throw IllegalArgumentException("Short requires $shortLength bytes. Position: $position, remaining:$remaining") + val s = getShortValue(position) + position += shortLength + return s + } + set(value) { + putEndian(shortToArray(value)) + } + + /** + * Read or write a two-byte Short at the current position. Position will increment by 2. + */ + var ushort: UShort + get() { + if (remaining < shortLength) + throw IllegalArgumentException("UShort requires $shortLength bytes. Position: $position, remaining:$remaining") + val s = getUShortValue(position) + position += shortLength + return s + } + set(value) { + putEndian(ushortToArray(value)) + } + + /** + * Read or write a four-byte Int at the current position. Position will increment by 4. + */ + var int: Int + get() { + if (remaining < intLength) + throw IllegalArgumentException("Int requires $intLength bytes. Position: $position, remaining:$remaining") + val s = getIntValue(position) + position += intLength + return s + } + set(value) { + putEndian(intToArray(value)) + } + + /** + * Read or write a two-byte Short at the current position. Position will increment by 2. + */ + var uint: UInt + get() { + if (remaining < intLength) + throw IllegalArgumentException("UInt requires $intLength bytes. Position: $position, remaining:$remaining") + val s = getUIntValue(position) + position += intLength + return s + } + set(value) { + putEndian(uintToArray(value)) + } + + /** + * Read or write a four-byte Int at the current position. Position will increment by 4. + */ + var long: Long + get() { + if (remaining < longLength) + throw IllegalArgumentException("Long requires $longLength bytes. Position: $position, remaining:$remaining") + val s = getLongValue(position) + position += longLength + return s + } + set(value) { + putEndian(longToArray(value)) + } + + /** + * Read or write a four-byte Int at the current position. Position will increment by 4. + */ + var ulong: ULong + get() { + if (remaining < longLength) + throw IllegalArgumentException("ULong requires $longLength bytes. Position: $position, remaining:$remaining") + val s = getULongValue(position) + position += longLength + return s + } + set(value) { + putEndian(ulongToArray(value)) + } + + var float: Float + get() = Float.fromBits(this.int) + set(value) { + int = value.toBits() + } + + var double: Double + get() = Double.Companion.fromBits(this.long) + set(value) { + long = value.toBits() + } + + // Creates a new buffer with the given mark, position, limit, and capacity, + // after checking invariants. + init { + if (cap < 0) throw IllegalArgumentException("Negative capacity: $cap") + capacity = cap + limit = lim + position = pos + if (mark >= 0) { + if (mark > pos) + throw IllegalArgumentException("mark > position: ($mark > $pos)") + this.mark = mark + } + } + + /** + * Clears this buffer. The position is set to zero, the limit is set to + * the capacity, and the mark is discarded. + * + * + * Invoke this method before using a sequence of channel-read or + * *put* operations to fill this buffer. For example: + * + *
+     * buf.clear();     // Prepare buffer for reading
+     * in.read(buf);    // Read data
+ * + * + * This method does not actually erase the data in the buffer, but it + * is named as if it did because it will most often be used in situations + * in which that might as well be the case. + * + * @return This buffer + */ + open fun clear() { + position = 0 + limit = capacity + mark = noMark + } + + /** + * 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 + * @param addCapacity bytes to add. Unsigned as can't be used to shrink. + */ + abstract fun expand(addCapacity: UInt) + + /** + * Flips this buffer. The limit is set to the current position and then + * the position is set to zero. If the mark is defined then it is + * discarded. + * + * + * After a sequence of channel-read or *put* operations, invoke + * this method to prepare for a sequence of channel-write or relative + * *get* operations. For example: + * + *
+     * buf.put(magic);    // Prepend header
+     * in.read(buf);      // Read data into rest of buffer
+     * buf.flip();        // Flip buffer
+     * out.write(buf);    // Write header + data to channel
+ * + * + * This method is often used in conjunction with the compact method when transferring data from + * one place to another. + * + * @return This buffer + */ + open fun flip(): Buffer { + limit = position + position = 0 + mark = noMark + return this + } + + /** + * given the specified index into the backing array, return the current Element. Do not change position + */ + fun get(index: Int): Element { + return getElementAt(index) + } + + /** + * This is used by many of the accessor properties of the various types. It must return + * a ByteArray of length bytes, containing the bytes at the current position for the + * specified length. The position will be incremented by the length. + * + * @param length must be > 0 and <= remaining(). Defaults to remaining + * + * @throws IllegalArgumentException if the length is invalid + */ + abstract fun getBytes(length: Int = remaining): Array + + /** + * Starting at the current position, copy bytes.size from the buffer into the provided array. + * If the size of the array is greater than [remaining], only [remaining] bytes are copied. + * Position is increased by the number of bytes copied. + */ + abstract fun getBytes(bytes: Array) + + /** + * given the specified index into the backing array, return the current Element. Do not change position + */ + abstract fun getElementAt(index: Int): Element + abstract fun setElementAt(index: Int, element: Element) + abstract fun getElementAsInt(index: Int): Int + abstract fun getElementAsUInt(index: Int): UInt + abstract fun getElementAsLong(index: Int): Long + abstract fun getElementAsULong(index: Int): ULong + + /** + * Convenience method Sets the limit at position + length, then sets the position + */ + fun positionLimit(position: Int, length: Int) { + if (length < 0 || position < 0 || position + length > capacity) + throw IllegalArgumentException("Position: $position + length: $length = ${position + length} must be between 0 and $capacity") + limit = position + length + this.position = position + } + + fun positionLimit(position: Short, length: Short) { + if (length < 0 || position < 0 || position + length > capacity) + throw IllegalArgumentException("Position: $position + length: $length = ${position + length} must be between 0 and $capacity") + limit = position + length + this.position = position.toInt() + } + + /** + * This is used by many of the accessor properties of the various types. It must copy the + * content of bytes into the ByteBuffer at the current position. + * The position will be incremented by the length of the array. + * + * @param bytes size of the array must be > 0 and <= remaining() + * + * @throws IllegalArgumentException if the size is invalid + */ + abstract fun put(bytes: Array) + + /** + * Resets this buffer's position to the previously-marked position. + * + * + * Invoking this method neither changes nor discards the mark's + * value. + * + * @return This buffer + * + * @throws IllegalStateException + * If the mark has not been set + */ + open fun reset() { + val m = mark + if (m < 0) throw IllegalStateException("Mark:$m must be non-negative") + position = m + } + + fun resetMark() { + mark = noMark + } + + /** + * Rewinds this buffer. The position is set to zero and the mark is + * discarded. + * + * + * Invoke this method before a sequence of channel-write or *get* + * operations, assuming that the limit has already been set + * appropriately. For example: + * + *
+     * out.write(buf);    // Write remaining data
+     * buf.rewind();      // Rewind buffer
+     * buf.get(array);    // Copy data into array
+ * + * @return This buffer + */ + open fun rewind() { + position = 0 + mark = -1 + } + + /** + * Make a new ByteBuffer containing the [remaining bytes] of this one. Length can be overrdiden to + * a shorter value than the default [remaining]. Position is unaffected + * @param length defaults to [remaining]. can be between 1 and [remaining] + */ + abstract fun slice(length: Int = remaining): ByteBufferBase + + // -- Package-private methods for bounds checking, etc. -- + /** + * Checks the current position against the limit, throwing a [ ] if it is not smaller than the limit, and then + * increments the position. + * + * @return The current position value, before it is incremented + */ + fun nextGetIndex(): Int { + if (position >= limit) throw IllegalStateException("Position:$position exceeds Limit:$limit") + return position++ + } + + fun nextGetIndex(nb: Int): Int { + if (limit - position < nb) throw IllegalStateException() + val p = position + position += nb + return p + } + + /** + * Checks the current position against the limit, and then increments the position. + * + * @return The current position value, before it is incremented + * @throws IllegalStateException if position exceeds limit + */ + fun nextPutIndex(): Int { + if (position >= limit) throw IllegalStateException("Position:$position exceeds Limit:$limit") + return position++ + } + + /** + * Checks the current position pluss the specified offset against the limit, and then increments the position. + * + * @param length position will be increased by + * @return The current position value, before it is incremented + * @throws IllegalStateException if increased position will exceed limit + */ + fun nextPutIndex(length: Int): Int { + if (limit - position < length || position < 0) + throw IllegalStateException("Limit:$limit minus position:$position less than $length") + val p = position + position += length + return p + } + + /** + * Checks the given index against the limit, throwing an [ ] if it is not smaller than the limit + * or is smaller than zero. + */ + fun checkIndex(i: Int): Int { // package-private + if (i < 0 || i >= limit) throw IndexOutOfBoundsException( + "index=$i out of bounds (limit=$limit)" + ) + return i + } + + fun checkIndex(i: Int, nb: Int): Int { // package-private + if (i < 0 || nb > limit - i) throw IndexOutOfBoundsException( + "index=$i out of bounds (limit=$limit, nb=$nb)" + ) + return i + } + + fun markValue(): Int { // package-private + return mark + } + + fun truncate() { // package-private + mark = noMark + position = 0 + limit = 0 + capacity = 0 + } + + internal fun discardMark() { // package-private + mark = -1 + } + + private fun getShortValue(index: Int): Short { + return when (order) { + ByteOrder.LittleEndian -> ((getElementAsInt(index + 1) shl 8) or getElementAsInt(index)).toShort() + ByteOrder.BigEndian -> ((getElementAsInt(index) shl 8) or getElementAsInt(index + 1)).toShort() + } + } + + fun getUShortValue(index: Int): UShort { + return when (order) { + ByteOrder.LittleEndian -> ((getElementAsUInt(index + 1) shl 8) or + getElementAsUInt(index)).toUShort() + ByteOrder.BigEndian -> ((getElementAsUInt(index) shl 8) or + getElementAsUInt(index + 1)).toUShort() + } + } + + private fun getIntValue(index: Int): Int { + return when (order) { + ByteOrder.LittleEndian -> (getElementAsInt(index + 3) shl 24) or + (getElementAsInt(index + 2) shl 16) or + (getElementAsInt(index + 1) shl 8) or + getElementAsInt(index) + ByteOrder.BigEndian -> (getElementAsInt(index) shl 24) or + (getElementAsInt(index + 1) shl 16) or + (getElementAsInt(index + 2) shl 8) or + getElementAsInt(index + 3) + } + } + + private fun getUIntValue(index: Int): UInt { + return when (order) { + ByteOrder.LittleEndian -> (getElementAsUInt(index + 3) shl 24) or + (getElementAsUInt(index + 2) shl 16) or + (getElementAsUInt(index + 1) shl 8) or + getElementAsUInt(index) + ByteOrder.BigEndian -> (getElementAsUInt(index) shl 24) or + (getElementAsUInt(index + 1) shl 16) or + (getElementAsUInt(index + 2) shl 8) or + getElementAsUInt(index + 3) + } + } + + fun getLongValue(index: Int): Long { + return when (order) { + ByteOrder.LittleEndian -> ((getElementAsLong(index + 7) shl 56) + or (getElementAsLong(index + 6) shl 48) + or (getElementAsLong(index + 5) shl 40) + or (getElementAsLong(index + 4) shl 32) + or (getElementAsLong(index + 3) shl 24) + or (getElementAsLong(index + 2) shl 16) + or (getElementAsLong(index + 1) shl 8) + or getElementAsLong(index)) + ByteOrder.BigEndian -> ((getElementAsLong(index) shl 56) + or (getElementAsLong(index + 1) shl 48) + or (getElementAsLong(index + 2) shl 40) + or (getElementAsLong(index + 3) shl 32) + or (getElementAsLong(index + 4) shl 24) + or (getElementAsLong(index + 5) shl 16) + or (getElementAsLong(index + 6) shl 8) + or getElementAsLong(index + 7)) + } + } + + private fun getULongValue(index: Int): ULong { + return when (order) { + ByteOrder.LittleEndian -> ((getElementAsULong(index + 7) shl 56) + or (getElementAsULong(index + 6) shl 48) + or (getElementAsULong(index + 5) shl 40) + or (getElementAsULong(index + 4) shl 32) + or (getElementAsULong(index + 3) shl 24) + or (getElementAsULong(index + 2) shl 16) + or (getElementAsULong(index + 1) shl 8) + or getElementAsULong(index)) + ByteOrder.BigEndian -> ((getElementAsULong(index) shl 56) + or (getElementAsULong(index + 1) shl 48) + or (getElementAsULong(index + 2) shl 40) + or (getElementAsULong(index + 3) shl 32) + or (getElementAsULong(index + 4) shl 24) + or (getElementAsULong(index + 5) shl 16) + or (getElementAsULong(index + 6) shl 8) + or getElementAsULong(index + 7)) + } + } + + /** + * Helper for property accessors that care about endian encoding. + * + * @param bytes must be in MSB to LSB order. BigEndian will put array unchanged into buffer. + * LittleEndian will put bytes in LSB to MSB order in buffer. + * + */ + abstract fun putEndian(bytes: Array) + abstract fun shortToArray(short: Short): Array + abstract fun ushortToArray(ushort: UShort): Array + abstract fun intToArray(int: Int): Array + abstract fun uintToArray(int: UInt): Array + abstract fun longToArray(long: Long): Array + abstract fun ulongToArray(uLong: ULong): Array + + companion object { + protected const val noMark = -1 + protected const val shortLength = 2 + protected const val intLength = 4 + protected const val longLength = 8 + + fun checkBounds(off: Int, len: Int, size: Int) { // package-private + if (off or len or off + len or size - (off + len) < 0) throw IndexOutOfBoundsException( + "off=$off, len=$len out of bounds (size=$size)" + ) + } + } +} \ No newline at end of file diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/ByteBuffers.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/ByteBuffers.kt new file mode 100644 index 000000000..935791c72 --- /dev/null +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/ByteBuffers.kt @@ -0,0 +1,892 @@ +/** + * 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.min + +/** + * Port of a subset of Java's ByteBuffer with full Endian support. This class should be replaceable + * by an equivalent class in kotlinx-io once that library settles. This ByteBuffer is + * implemented on a ByteArray for simplicity + */ +abstract class ByteBufferBase constructor( + capacity: Int, + order: ByteOrder = ByteOrder.LittleEndian, + override val isReadOnly: Boolean = false +) :Buffer(-1, 0, capacity, capacity, order), + Comparable> { + + 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return byteArrayOf( + (short.toInt() shr 8).toByte(), + short.toByte() + ) + } + + override fun ushortToArray(ushort: UShort): ByteArray { + return byteArrayOf( + (ushort.toUInt() shr 8).toByte(), + ushort.toByte() + ) + } + + override fun intToArray(int: Int): ByteArray { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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, + * + *

    + * + *
  1. They have the same element type,

  2. + * + *
  3. They have the same number of remaining elements, and + *

  4. + * + *
  5. The two sequences of remaining elements, considered + * independently of their starting positions, are pointwise equal. + + *

  6. + * + *
+ * + *

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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return ubyteArrayOf( + (short.toInt() shr 8).toUByte(), + short.toUByte() + ) + } + + override fun ushortToArray(ushort: UShort): UByteArray { + return ubyteArrayOf( + (ushort.toUInt() shr 8).toUByte(), + ushort.toUByte() + ) + } + + override fun intToArray(int: Int): UByteArray { + return 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 { + return 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 { + return 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 { + return 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 { + return 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, + * + *

    + * + *
  1. They have the same element type,

  2. + * + *
  3. They have the same number of remaining elements, and + *

  4. + * + *
  5. The two sequences of remaining elements, considered + * independently of their starting positions, are pointwise equal. + + *

  6. + * + *
+ * + *

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 + } +} \ No newline at end of file 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..961062d1b --- /dev/null +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/CustomBitSet.kt @@ -0,0 +1,474 @@ +/** + * 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 + +import kotlin.math.max + +/** + * Most of the functionality of a java.util.BitSet + */ +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])) + } + + /** + * Initializes a new bit set containing all the bits in the given byte + * buffer between its position and limit. + * + *

The byte buffer is not modified by this method, and no + * reference to the buffer is retained by the bit set. + * + * @param bytes a byte array containing a little-endian representation + * of a sequence of bits, to be + * used as the initial bits of the new bit set + */ + constructor(bytes: ByteArray, bitsCount: Int = bytes.size * 8) : this(bitsCount) { + transformBuffer(ByteBuffer(bytes)) + } + + /** + * Initializes a new bit set containing all the bits in the given byte + * buffer between its position and limit. + * + *

The byte buffer is not modified by this method, and no + * reference to the buffer is retained by the bit set. + * + * @param buffer a byte buffer containing a little-endian representation + * of a sequence of bits between its position and limit, to be + * used as the initial bits of the new bit set + */ + constructor(buffer: ByteBuffer, bitsCount: Int = buffer.remaining * 8) : this(bitsCount) { + transformBuffer(buffer) + } + + /** + * Initializes a new bit set containing all the bits in the given byte + * buffer between its position and limit. + * + *

The byte buffer content is not modified by this method, but the position ill be set to limit. + * Bitmask is treated as LittleEndian for the bytes containing the mask. + * + * @param buffer a byte buffer containing a little-endian representation + * of a sequence of bits between its position and limit, to be + * used as the initial bits of the new bit set + */ + 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 + } + + /** + * Sets the bit at the specified index to the complement of its + * current value. + * + * @param bitIndex the index of the bit to flip + * @throws IndexOutOfBoundsException if the specified index is negative + */ + 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() + } + + /** + * Sets each bit from the specified `fromIndex` (inclusive) to the + * specified `toIndex` (exclusive) to the complement of its current + * value. + * + * @param fromIndex index of the first bit to flip + * @param toIndex index after the last bit to flip + * @throws IndexOutOfBoundsException if `fromIndex` is negative, + * or `toIndex` is negative, or `fromIndex` is + * larger than `toIndex` + * @since 1.4 + */ + 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() + } + + /** + * find all set bits at or after startIndex. For each bit, invoke the lambda passing the index + * of the set bit. The lamda can return true to continue, or false to stop iterating. + * + * @return number of bits set + */ + 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] + } + } + + /** + * find all set bits at or after startIndex. For each bit, invoke the lambda passing the index + * of the set bit. The lamda can return true to continue, or false to stop iterating. + * + * @return number of bits set + */ + 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() + } + } + + /** + * Sets the bit specified by the index to `false`. + * + * @param bitIndex the index of the bit to be cleared + * @throws IndexOutOfBoundsException if the specified index is negative + */ + 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() + } + + /** + * Sets all of the bits in this CustomBitSet to `false`. + */ + fun clear() { + for (i in 0..words.size) 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 + } + + /** + * Returns the number of bits of space actually in use by this + * `BitSet` to represent bit values. + * The maximum element in the set is the size - 1st element. + * + * @return the number of bits currently in this bit set + */ + 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..79aee7018 --- /dev/null +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/io/Extensions.kt @@ -0,0 +1,480 @@ +/** + * 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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 { + return 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) +} \ No newline at end of file