Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 188 additions & 24 deletions base/src/commonMain/kotlin/bytes/ByteArrayBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.splendo.kaluga.base.utils.MedFloat16
import com.splendo.kaluga.base.utils.MedFloat32
import com.splendo.kaluga.base.utils.UInt24
import kotlin.experimental.or
import kotlin.math.min

/**
* Builds a [ByteArray] from primary types
Expand Down Expand Up @@ -164,15 +165,23 @@ interface ByteArrayBuilder {
/**
* Builds a [ByteArray] using a [ByteArrayBuilder]
* @param order the [ByteOrder] in which to add to the [ByteArray]. This is the default order in which elements will be encoded.
* @param expectedSize the initial size the ByteArray will use to approximate its final size.
* @param block the building block using [ByteArrayBuilder] to build the array.
* @return the built [ByteArray]
*/
fun buildByteArray(order: ByteOrder = ByteOrder.LEAST_SIGNIFICANT_FIRST, block: ByteArrayBuilder.() -> Unit) = ByteArrayBuilderImpl(
fun buildByteArray(order: ByteOrder = ByteOrder.LEAST_SIGNIFICANT_FIRST, expectedSize: Int = Long.SIZE_BYTES, block: ByteArrayBuilder.() -> Unit) = ByteArrayBuilderImpl(
expectedSize,
order,
).apply(block).build()

private class ByteArrayBuilderImpl(override val byteOrder: ByteOrder) : ByteArrayBuilder {
var bytes = byteArrayOf()
private class ByteArrayBuilderImpl(expectedSize: Int, override val byteOrder: ByteOrder) : ByteArrayBuilder {

init {
require(expectedSize > 0) { "buildByteArray must have an expected size larger than 0" }
}
private val completedChunks = mutableListOf<ByteArray>()
var currentChunk = ByteArray(expectedSize)
var currentByteOffset = 0
var currentByte: Byte = 0
var currentBit = 0

Expand All @@ -187,92 +196,247 @@ private class ByteArrayBuilderImpl(override val byteOrder: ByteOrder) : ByteArra
}

override fun add(byte: Byte) {
add(byteArrayOf(byte))
add(
Byte.SIZE_BYTES,
generateIntoMethod = { currentChunk[it] = byte },
generateMethod = { byteArrayOf(byte) },
)
}

override fun add(short: Short, order: ByteOrder) {
add(short.toByteArray(order))
add(
Short.SIZE_BYTES,
generateIntoMethod = { short.copyIntoByteArray(currentChunk, it, order) },
generateMethod = { short.toByteArray(order) },
)
}

override fun add(int24: Int24, order: ByteOrder) {
add(int24.toByteArray(order))
add(
Int24.SIZE_BYTES,
generateIntoMethod = { int24.copyIntoByteArray(currentChunk, it, order) },
generateMethod = { int24.toByteArray(order) },
)
}

override fun add(int: Int, order: ByteOrder) {
add(int.toByteArray(order))
add(
Int.SIZE_BYTES,
generateIntoMethod = { int.copyIntoByteArray(currentChunk, it, order) },
generateMethod = { int.toByteArray(order) },
)
}

override fun add(long: Long, order: ByteOrder) {
add(long.toByteArray(order))
add(
Long.SIZE_BYTES,
generateIntoMethod = { long.copyIntoByteArray(currentChunk, it, order) },
generateMethod = { long.toByteArray(order) },
)
}

override fun add(float: Float, order: ByteOrder) {
add(float.toByteArray(order))
add(
Float.SIZE_BYTES,
generateIntoMethod = { float.copyIntoByteArray(currentChunk, it, order) },
generateMethod = { float.toByteArray(order) },
)
}

override fun add(double: Double, order: ByteOrder) {
add(double.toByteArray(order))
add(
Double.SIZE_BYTES,
generateIntoMethod = { double.copyIntoByteArray(currentChunk, it, order) },
generateMethod = { double.toByteArray(order) },
)
}

override fun add(medFloat16: MedFloat16) {
add(medFloat16.toByteArray())
add(
2,
generateIntoMethod = { medFloat16.copyIntoByteArray(currentChunk, it) },
generateMethod = { medFloat16.toByteArray() },
)
}

override fun add(medFloat32: MedFloat32) {
add(medFloat32.toByteArray())
add(
4,
generateIntoMethod = { medFloat32.copyIntoByteArray(currentChunk, it) },
generateMethod = { medFloat32.toByteArray() },
)
}

override fun add(uByte: UByte) {
add(uByte.toByteArray())
add(
UByte.SIZE_BYTES,
generateIntoMethod = { currentChunk[it] = uByte.toByte() },
generateMethod = { uByte.toByteArray() },
)
}

override fun add(uShort: UShort, order: ByteOrder) {
add(uShort.toByteArray(order))
add(
UShort.SIZE_BYTES,
generateIntoMethod = { uShort.copyIntoByteArray(currentChunk, it, order) },
generateMethod = { uShort.toByteArray(order) },
)
}

override fun add(uInt: UInt, order: ByteOrder) {
add(uInt.toByteArray(order))
add(
UInt.SIZE_BYTES,
generateIntoMethod = { uInt.copyIntoByteArray(currentChunk, it, order) },
generateMethod = { uInt.toByteArray(order) },
)
}

override fun add(uLong: ULong, order: ByteOrder) {
add(uLong.toByteArray(order))
add(
ULong.SIZE_BYTES,
generateIntoMethod = { uLong.copyIntoByteArray(currentChunk, it, order) },
generateMethod = { uLong.toByteArray(order) },
)
}

override fun add(uInt24: UInt24, order: ByteOrder) {
add(uInt24.toByteArray(order))
add(
UInt24.SIZE_BYTES,
generateIntoMethod = { uInt24.copyIntoByteArray(currentChunk, it, order) },
generateMethod = { uInt24.toByteArray(order) },
)
}

override fun add(string: String, settings: StringEncodingSettings, order: ByteOrder) {
add(string.toByteArray(settings, order))
}

override fun add(char: Char, encoding: Encoding, order: ByteOrder) {
add(char.toString(), StringEncodingSettings(StringEncodingSettings.NoMarking, encoding), order)
add(
encoding.byteSize,
generateIntoMethod = { encoding.copyCharIntoByteArray(char, currentChunk, it, order) },
generateMethod = { encoding.encodeChar(char, order) },
)
}

override fun add(bytes: ByteArray) {
add(
bytes.size,
generateIntoMethod = { bytes.copyInto(currentChunk, it) },
generateMethod = { bytes },
)
}

private inline fun add(expectedSize: Int, generateIntoMethod: (Int) -> Unit, generateMethod: () -> ByteArray) {
if (currentBit > 0) {
addCurrentByte()
}
when (byteOrder) {
ByteOrder.MOST_SIGNIFICANT_FIRST -> this.bytes = bytes + this.bytes
ByteOrder.LEAST_SIGNIFICANT_FIRST -> this.bytes += bytes

val remaining = currentChunk.size - currentByteOffset
when {
expectedSize in 1..remaining -> {
generateIntoMethod(
when (byteOrder) {
ByteOrder.MOST_SIGNIFICANT_FIRST -> remaining - expectedSize
ByteOrder.LEAST_SIGNIFICANT_FIRST -> currentByteOffset
},
)
currentByteOffset += expectedSize
}

else -> {
val fullSegment = generateMethod()
val splitIndex = min(fullSegment.size, remaining)
when (byteOrder) {
ByteOrder.LEAST_SIGNIFICANT_FIRST -> {
fullSegment.copyInto(currentChunk, currentByteOffset, 0, splitIndex)
currentByteOffset += splitIndex
if (splitIndex < fullSegment.size) {
completedChunks.add(currentChunk)
completedChunks.add(fullSegment.copyInto(ByteArray(fullSegment.size - splitIndex), 0, splitIndex))
currentChunk = ByteArray(currentChunk.size * 2)
currentByteOffset = 0
}
}

ByteOrder.MOST_SIGNIFICANT_FIRST -> {
fullSegment.copyInto(currentChunk, remaining - splitIndex, fullSegment.size - splitIndex)
currentByteOffset += splitIndex
if (splitIndex < fullSegment.size) {
completedChunks.add(currentChunk)
completedChunks.add(fullSegment.copyInto(ByteArray(fullSegment.size - splitIndex), endIndex = fullSegment.size - splitIndex))
currentChunk = ByteArray(currentChunk.size * 2)
currentByteOffset = 0
}
}
}
}
}
checkChunkCompleted()
}

private fun addCurrentByte() {
when (byteOrder) {
ByteOrder.MOST_SIGNIFICANT_FIRST -> this.bytes = byteArrayOf(currentByte) + this.bytes
ByteOrder.LEAST_SIGNIFICANT_FIRST -> this.bytes += currentByte
ByteOrder.MOST_SIGNIFICANT_FIRST -> currentChunk[currentChunk.size - currentByteOffset - 1] = currentByte
ByteOrder.LEAST_SIGNIFICANT_FIRST -> currentChunk[currentByteOffset] = currentByte
}
currentByteOffset++
currentByte = 0
currentBit = 0
checkChunkCompleted()
}

private fun checkChunkCompleted() {
if (currentByteOffset == currentChunk.size) {
completedChunks.add(currentChunk)
currentChunk = ByteArray(currentChunk.size * 2)
currentByteOffset = 0
}
}

fun build(): ByteArray {
if (currentBit > 0) {
addCurrentByte()
}
return bytes

return if (completedChunks.isEmpty()) {
if (currentByteOffset == 0) {
byteArrayOf()
} else {
when (byteOrder) {
ByteOrder.MOST_SIGNIFICANT_FIRST -> currentChunk.sliceArray(currentChunk.size - currentByteOffset..<currentChunk.size)
ByteOrder.LEAST_SIGNIFICANT_FIRST -> currentChunk.sliceArray(0..<currentByteOffset)
}
}
} else {
val totalSize = completedChunks.sumOf { it.size } + currentByteOffset
val bytes = ByteArray(totalSize)
val lastChunkOffset = completedChunks.fold(0) { sumOfOffset, chunk ->
when (byteOrder) {
ByteOrder.MOST_SIGNIFICANT_FIRST -> {
chunk.copyInto(bytes, totalSize - sumOfOffset - chunk.size)
}

ByteOrder.LEAST_SIGNIFICANT_FIRST -> {
chunk.copyInto(bytes, sumOfOffset)
}
}
sumOfOffset + chunk.size
}

if (currentByteOffset > 0) {
when (byteOrder) {
ByteOrder.MOST_SIGNIFICANT_FIRST -> {
currentChunk.copyInto(bytes, startIndex = currentChunk.size - currentByteOffset)
}

ByteOrder.LEAST_SIGNIFICANT_FIRST -> {
currentChunk.copyInto(bytes, lastChunkOffset, endIndex = currentByteOffset)
}
}
}

bytes
}
}
}
38 changes: 36 additions & 2 deletions base/src/commonMain/kotlin/bytes/CharExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package com.splendo.kaluga.base.bytes
import com.splendo.kaluga.base.bytes.Encoding.ASCII
import com.splendo.kaluga.base.bytes.Encoding.UTF_16
import com.splendo.kaluga.base.bytes.Encoding.UTF_8
import kotlin.coroutines.coroutineContext

/**
* Character encoding
Expand Down Expand Up @@ -48,10 +49,33 @@ enum class Encoding(val byteSize: Int) {
* @param char the [Char] to encode.
* @param byteOrder the [ByteOrder] to use. For [Encoding] where [Encoding.byteSize] is 1, this can be ignored.
*/
fun Encoding.encodeChar(char: Char, byteOrder: ByteOrder) = when (this) {
fun Encoding.encodeChar(char: Char, byteOrder: ByteOrder): ByteArray = when (this) {
UTF_8 -> char.toString().encodeToByteArray()
UTF_16 -> char.toUTF16(byteOrder)
ASCII -> char.toAscii()
ASCII -> byteArrayOf(char.toAscii())
}

/**
* Encodes a [Char] using the given [Encoding] and [ByteOrder] and copies it into a [ByteArray] at a given offset.
* @param char the [Char] to encode.
* @param array the [ByteArray] to copy the encoded data into.
* @param offset the offset at which to copy the encoded data.
* @param byteOrder the [ByteOrder] in which the [Char] is encoded. For [Encoding] where [Encoding.byteSize] is 1, this can be ignored.
* @throws IllegalArgumentException if [array] is not is not large enough to hold [Encoding.byteSize] bytes at the [offset].
* @return the encoded [ByteArray].
*/
fun Encoding.copyCharIntoByteArray(char: Char, array: ByteArray, offset: Int = 0, byteOrder: ByteOrder): ByteArray {
require(array.size > byteSize + offset) { "Cannot copy into ByteArray. Must be at least ${offset + byteSize} long" }
return when (this) {
UTF_8 -> char.toString().encodeToByteArray().copyInto(array, offset)

UTF_16 -> char.copyUTF16IntoByteArray(array, offset, byteOrder)

ASCII -> {
array[offset] = char.toAscii()
array
}
}
}

/**
Expand All @@ -61,6 +85,16 @@ fun Encoding.encodeChar(char: Char, byteOrder: ByteOrder) = when (this) {
*/
fun Char.toUTF16(byteOrder: ByteOrder): ByteArray = code.toUShort().toByteArray(byteOrder)

/**
* Encodes a [Char] and copies it into a [ByteArray] at a given offset in UTF-16 using the given [ByteOrder].
* @param array the [ByteArray] to copy the encoded data into.
* @param offset the offset at which to copy the encoded data.
* @param byteOrder the [ByteOrder] to use.
* @throws IllegalArgumentException if [array] is not is not large enough to hold 2 bytes at the [offset].
* @return the [ByteArray] representing the [Char] in UTF-16.
*/
fun Char.copyUTF16IntoByteArray(array: ByteArray, offset: Int = 0, byteOrder: ByteOrder) = code.toUShort().copyIntoByteArray(array, offset, byteOrder)

/**
* Encodes a [Char] to a [Byte] in ASCII.
* @throws IllegalArgumentException if the character cannot be represented in ASCII. Use [Char.toAsciiOrNull] to get a non-throwing variant
Expand Down
10 changes: 10 additions & 0 deletions base/src/commonMain/kotlin/bytes/DoubleExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,13 @@ fun ByteArray.decodeDouble(octetIndex: Int, byteOrder: ByteOrder): Double = Doub
* @return [ByteArray] representing the [Double]
*/
fun Double.toByteArray(byteOrder: ByteOrder) = toRawBits().toByteArray(byteOrder)

/**
* Encodes this [Double] and copies it into a [ByteArray] at a given offset.
* @param array the [ByteArray] to copy the encoded data into.
* @param offset the offset at which to copy the encoded data.
* @param byteOrder the [ByteOrder] in which the [Double] is encoded
* @throws IllegalArgumentException if [array] is not is not large enough to hold 8 bytes at the [offset].
* @return the encoded [ByteArray].
*/
fun Double.copyIntoByteArray(array: ByteArray, offset: Int = 0, byteOrder: ByteOrder): ByteArray = toRawBits().copyIntoByteArray(array, offset, byteOrder)
10 changes: 10 additions & 0 deletions base/src/commonMain/kotlin/bytes/FloatExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,13 @@ fun ByteArray.decodeFloat(octetIndex: Int, byteOrder: ByteOrder): Float = Float.
* @return [ByteArray] representing the [Float]
*/
fun Float.toByteArray(byteOrder: ByteOrder) = toRawBits().toByteArray(byteOrder)

/**
* Encodes this [Float] and copies it into a [ByteArray] at a given offset.
* @param array the [ByteArray] to copy the encoded data into.
* @param offset the offset at which to copy the encoded data.
* @param byteOrder the [ByteOrder] in which the [Float] is encoded
* @throws IllegalArgumentException if [array] is not is not large enough to hold 4 bytes at the [offset].
* @return the encoded [ByteArray].
*/
fun Float.copyIntoByteArray(array: ByteArray, offset: Int = 0, byteOrder: ByteOrder): ByteArray = toRawBits().copyIntoByteArray(array, offset, byteOrder)
Loading
Loading