From ce1c02ee40eab1977d03ada56ec768774ca55341 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Sat, 17 Oct 2020 18:38:12 +0200 Subject: [PATCH] Ported ArrayBufferCursor. --- .../kotlin/world/phantasmal/lib/Constants.kt | 4 + .../lib/cursor/AbstractWritableCursor.kt | 184 ++++++++++++ .../world/phantasmal/lib/cursor/Cursor.kt | 3 +- .../phantasmal/lib/cursor/WritableCursor.kt | 85 ++++++ .../phantasmal/lib/fileFormats/ninja/Nj.kt | 18 +- .../phantasmal/lib/cursor/CursorTests.kt | 272 ++++++++++++++++++ .../lib/cursor/WritableCursorTests.kt | 217 ++++++++++++++ .../lib/cursor/AbstractArrayBufferCursor.kt | 167 +++++++++++ .../lib/cursor/ArrayBufferCursor.kt | 35 +++ .../lib/cursor/ArrayBufferCursorTests.kt | 8 + 10 files changed, 983 insertions(+), 10 deletions(-) create mode 100644 lib/src/commonMain/kotlin/world/phantasmal/lib/Constants.kt create mode 100644 lib/src/commonMain/kotlin/world/phantasmal/lib/cursor/AbstractWritableCursor.kt create mode 100644 lib/src/commonMain/kotlin/world/phantasmal/lib/cursor/WritableCursor.kt create mode 100644 lib/src/commonTest/kotlin/world/phantasmal/lib/cursor/CursorTests.kt create mode 100644 lib/src/commonTest/kotlin/world/phantasmal/lib/cursor/WritableCursorTests.kt create mode 100644 lib/src/jsMain/kotlin/world/phantasmal/lib/cursor/AbstractArrayBufferCursor.kt create mode 100644 lib/src/jsMain/kotlin/world/phantasmal/lib/cursor/ArrayBufferCursor.kt create mode 100644 lib/src/jsTest/kotlin/world/phantasmal/lib/cursor/ArrayBufferCursorTests.kt diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/Constants.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/Constants.kt new file mode 100644 index 00000000..ab5786b0 --- /dev/null +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/Constants.kt @@ -0,0 +1,4 @@ +package world.phantasmal.lib + +const val ZERO_U8: UByte = 0u +const val ZERO_U16: UShort = 0u diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/cursor/AbstractWritableCursor.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/cursor/AbstractWritableCursor.kt new file mode 100644 index 00000000..6a7a4a42 --- /dev/null +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/cursor/AbstractWritableCursor.kt @@ -0,0 +1,184 @@ +package world.phantasmal.lib.cursor + +import world.phantasmal.lib.ZERO_U16 +import world.phantasmal.lib.ZERO_U8 +import kotlin.math.min + +abstract class AbstractWritableCursor +protected constructor(protected val offset: UInt) : WritableCursor { + override var position: UInt = 0u + protected set + + override val bytesLeft: UInt + get() = size - position + + protected val absolutePosition: UInt + get() = offset + position + + override fun seek(offset: Int): WritableCursor = + seekStart((position.toInt() + offset).toUInt()) + + override fun seekStart(offset: UInt): WritableCursor { + require(offset <= size) { "Offset $offset is out of bounds." } + + position = offset + return this + } + + override fun seekEnd(offset: UInt): WritableCursor { + require(offset <= size) { "Offset $offset is out of bounds." } + + position = size - offset + return this + } + + override fun stringAscii( + maxByteLength: UInt, + nullTerminated: Boolean, + dropRemaining: Boolean, + ): String = + buildString { + for (i in 0u until maxByteLength) { + val codePoint = u8() + + if (nullTerminated && codePoint == ZERO_U8) { + if (dropRemaining) { + seek((maxByteLength - i - 1u).toInt()) + } + + break + } + + append(codePoint.toShort().toChar()) + } + } + + override fun stringUtf16( + maxByteLength: UInt, + nullTerminated: Boolean, + dropRemaining: Boolean, + ): String = + buildString { + val len = maxByteLength / 2u + + for (i in 0u until len) { + val codePoint = u16() + + if (nullTerminated && codePoint == ZERO_U16) { + if (dropRemaining) { + seek((maxByteLength - 2u * i - 2u).toInt()) + } + + break + } + + append(codePoint.toShort().toChar()) + } + } + + override fun writeU8Array(array: UByteArray): WritableCursor { + val len = array.size + requireSize(len.toUInt()) + + for (i in 0 until len) { + writeU8(array[i]) + } + + return this + } + + override fun writeU16Array(array: UShortArray): WritableCursor { + val len = array.size + requireSize(2u * len.toUInt()) + + for (i in 0 until len) { + writeU16(array[i]) + } + + return this + } + + override fun writeU32Array(array: UIntArray): WritableCursor { + val len = array.size + requireSize(4u * len.toUInt()) + + for (i in 0 until len) { + writeU32(array[i]) + } + + return this + } + + override fun writeI32Array(array: IntArray): WritableCursor { + val len = array.size + requireSize(4u * len.toUInt()) + + for (i in 0 until len) { + writeI32(array[i]) + } + + return this + } + + override fun writeCursor(other: Cursor): WritableCursor { + val size = other.bytesLeft + requireSize(size) + + for (i in 0u until (size / 4u)) { + writeU32(other.u32()) + } + + for (i in 0u until (size % 4u)) { + writeU8(other.u8()) + } + + position += size + return this + } + + override fun writeStringAscii(str: String, byteLength: UInt): WritableCursor { + requireSize(byteLength) + + val len = min(byteLength.toInt(), str.length) + + for (i in 0 until len) { + writeU8(str[i].toByte().toUByte()) + } + + val padLen = byteLength.toInt() - len + + for (i in 0 until padLen) { + writeU8(0u) + } + + return this + } + + override fun writeStringUtf16(str: String, byteLength: UInt): WritableCursor { + requireSize(byteLength) + + val maxLen = byteLength.toInt() / 2 + val len = min(maxLen, str.length) + + for (i in 0 until len) { + writeU16(str[i].toShort().toUShort()) + } + + val padLen = maxLen - len + + for (i in 0 until padLen) { + writeU16(0u) + } + + return this + } + + /** + * Throws an error if less than [size] bytes are left at [position]. + */ + protected fun requireSize(size: UInt) { + val left = this.size - position + + require(size <= left) { "$size Bytes required but only $left available." } + } +} diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/cursor/Cursor.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/cursor/Cursor.kt index 577e625d..d9adac9e 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/cursor/Cursor.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/cursor/Cursor.kt @@ -21,7 +21,8 @@ interface Cursor { /** * Seek forward or backward by a number of bytes. * - * @param offset if positive, seeks forward by offset bytes, otherwise seeks backward by -offset bytes. + * @param offset if positive, seeks forward by offset bytes, otherwise seeks backward by -offset + * bytes. */ fun seek(offset: Int): Cursor diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/cursor/WritableCursor.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/cursor/WritableCursor.kt new file mode 100644 index 00000000..260e17b2 --- /dev/null +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/cursor/WritableCursor.kt @@ -0,0 +1,85 @@ +package world.phantasmal.lib.cursor + +/** + * A cursor for reading and writing binary data. + */ +interface WritableCursor : Cursor { + override var size: UInt + + /** + * Writes an unsigned 8-bit integer and increments position by 1. + */ + fun writeU8(value: UByte): WritableCursor + + /** + * Writes an unsigned 16-bit integer and increments position by 2. + */ + fun writeU16(value: UShort): WritableCursor + + /** + * Writes an unsigned 32-bit integer and increments position by 4. + */ + fun writeU32(value: UInt): WritableCursor + + /** + * Writes a signed 8-bit integer and increments position by 1. + */ + fun writeI8(value: Byte): WritableCursor + + /** + * Writes a signed 16-bit integer and increments position by 2. + */ + fun writeI16(value: Short): WritableCursor + + /** + * Writes a signed 32-bit integer and increments position by 4. + */ + fun writeI32(value: Int): WritableCursor + + /** + * Writes a 32-bit floating point number and increments position by 4. + */ + fun writeF32(value: Float): WritableCursor + + /** + * Writes an array of unsigned 8-bit integers and increments position by the array's length. + */ + fun writeU8Array(array: UByteArray): WritableCursor + + /** + * Writes an array of unsigned 16-bit integers and increments position by twice the array's + * length. + */ + fun writeU16Array(array: UShortArray): WritableCursor + + /** + * Writes an array of unsigned 32-bit integers and increments position by four times the array's + * length. + */ + fun writeU32Array(array: UIntArray): WritableCursor + + /** + * Writes an array of signed 32-bit integers and increments position by four times the array's + * length. + */ + fun writeI32Array(array: IntArray): WritableCursor + + /** + * Writes the contents of the given cursor from its position to its end. Increments this + * cursor's and the given cursor's position by the size of the given cursor. + */ + fun writeCursor(other: Cursor): WritableCursor + + /** + * Writes [byteLength] characters of [str]. If [str] is shorter than [byteLength], nul bytes + * will be inserted until [byteLength] bytes have been written. + */ + fun writeStringAscii(str: String, byteLength: UInt): WritableCursor + + /** + * Writes characters of [str] without writing more than [byteLength] bytes. If less than + * [byteLength] bytes can be written this way, nul bytes will be inserted until [byteLength] + * bytes have been written. + */ + fun writeStringUtf16(str: String, byteLength: UInt): WritableCursor +} diff --git a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/ninja/Nj.kt b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/ninja/Nj.kt index 43a3a8c7..00f8e44b 100644 --- a/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/ninja/Nj.kt +++ b/lib/src/commonMain/kotlin/world/phantasmal/lib/fileFormats/ninja/Nj.kt @@ -1,6 +1,7 @@ package world.phantasmal.lib.fileFormats.ninja import mu.KotlinLogging +import world.phantasmal.lib.ZERO_U8 import world.phantasmal.lib.cursor.Cursor import world.phantasmal.lib.fileFormats.Vec2 import world.phantasmal.lib.fileFormats.Vec3 @@ -12,7 +13,6 @@ import kotlin.math.abs // - bump maps private val logger = KotlinLogging.logger {} -private const val ZERO_UBYTE: UByte = 0u class NjcmModel( /** @@ -357,7 +357,7 @@ private fun parseVertexChunk( flags: UByte, ): List { val boneWeightStatus = flags and 0b11u - val calcContinue = (flags and 0x80u) != ZERO_UBYTE + val calcContinue = (flags and 0x80u) != ZERO_U8 val index = cursor.u16() val vertexCount = cursor.u16() @@ -442,13 +442,13 @@ private fun parseTriangleStripChunk( chunkTypeId: UByte, flags: UByte, ): List { - val ignoreLight = (flags and 0b1u) != ZERO_UBYTE - val ignoreSpecular = (flags and 0b10u) != ZERO_UBYTE - val ignoreAmbient = (flags and 0b100u) != ZERO_UBYTE - val useAlpha = (flags and 0b1000u) != ZERO_UBYTE - val doubleSide = (flags and 0b10000u) != ZERO_UBYTE - val flatShading = (flags and 0b100000u) != ZERO_UBYTE - val environmentMapping = (flags and 0b1000000u) != ZERO_UBYTE + val ignoreLight = (flags and 0b1u) != ZERO_U8 + val ignoreSpecular = (flags and 0b10u) != ZERO_U8 + val ignoreAmbient = (flags and 0b100u) != ZERO_U8 + val useAlpha = (flags and 0b1000u) != ZERO_U8 + val doubleSide = (flags and 0b10000u) != ZERO_U8 + val flatShading = (flags and 0b100000u) != ZERO_U8 + val environmentMapping = (flags and 0b1000000u) != ZERO_U8 val userOffsetAndStripCount = cursor.u16() val userFlagsSize = (userOffsetAndStripCount.toUInt() shr 14).toInt() diff --git a/lib/src/commonTest/kotlin/world/phantasmal/lib/cursor/CursorTests.kt b/lib/src/commonTest/kotlin/world/phantasmal/lib/cursor/CursorTests.kt new file mode 100644 index 00000000..42281350 --- /dev/null +++ b/lib/src/commonTest/kotlin/world/phantasmal/lib/cursor/CursorTests.kt @@ -0,0 +1,272 @@ +package world.phantasmal.lib.cursor + +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Test suite for all [Cursor] implementations. There is a subclass of this suite for every [Cursor] + * implementation. + */ +abstract class CursorTests { + abstract fun createCursor(bytes: Array, endianness: Endianness): Cursor + + @Test + fun simple_cursor_properties_and_invariants() { + simple_cursor_properties_and_invariants(Endianness.Little) + simple_cursor_properties_and_invariants(Endianness.Big) + } + + private fun simple_cursor_properties_and_invariants(endianness: Endianness) { + val cursor = createCursor(arrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), endianness) + + for ((seek_to, expectedPos) in listOf( + 0 to 0u, + 3 to 3u, + 5 to 8u, + 2 to 10u, + -10 to 0u, + )) { + cursor.seek(seek_to) + + assertEquals(10u, cursor.size) + assertEquals(expectedPos, cursor.position) + assertEquals(cursor.position + cursor.bytesLeft, cursor.size) + assertEquals(endianness, cursor.endianness) + } + } + + @Test + fun cursor_handles_byte_order_correctly() { + cursor_handles_byte_order_correctly(Endianness.Little) + cursor_handles_byte_order_correctly(Endianness.Big) + } + + private fun cursor_handles_byte_order_correctly(endianness: Endianness) { + val cursor = createCursor(arrayOf(1, 2, 3, 4), endianness) + + if (endianness == Endianness.Little) { + assertEquals(0x04030201u, cursor.u32()) + } else { + assertEquals(0x01020304u, cursor.u32()) + } + } + + @Test + fun u8() { + testIntegerRead(1, { u8().toInt() }, Endianness.Little) + testIntegerRead(1, { u8().toInt() }, Endianness.Big) + } + + @Test + fun u16() { + testIntegerRead(2, { u16().toInt() }, Endianness.Little) + testIntegerRead(2, { u16().toInt() }, Endianness.Big) + } + + @Test + fun u32() { + testIntegerRead(4, { u32().toInt() }, Endianness.Little) + testIntegerRead(4, { u32().toInt() }, Endianness.Big) + } + + @Test + fun i8() { + testIntegerRead(1, { i8().toInt() }, Endianness.Little) + testIntegerRead(1, { i8().toInt() }, Endianness.Big) + } + + @Test + fun i16() { + testIntegerRead(2, { i16().toInt() }, Endianness.Little) + testIntegerRead(2, { i16().toInt() }, Endianness.Big) + } + + @Test + fun i32() { + testIntegerRead(4, { i32() }, Endianness.Little) + testIntegerRead(4, { i32() }, Endianness.Big) + } + + /** + * Reads two integers. + */ + private fun testIntegerRead(byteCount: Int, read: Cursor.() -> Int, endianness: Endianness) { + // Generate two numbers of the form 0x010203... + val expectedNumber1 = 0x01020304 shr (8 * (4 - byteCount)) + val expectedNumber2 = 0x05060708 shr (8 * (4 - byteCount)) + + // Put them in a byte array. + val bytes = Array(2 * byteCount) { 0 } + + for (i in 0 until byteCount) { + val shift = + if (endianness == Endianness.Little) { + 8 * i + } else { + 8 * (byteCount - i - 1) + } + + bytes[i] = (expectedNumber1 shr shift).toByte() + bytes[byteCount + i] = (expectedNumber2 shr shift).toByte() + } + + // Check that individual bytes are in the correct order when read as part of a larger + // integer. + val cursor = createCursor(bytes, endianness) + + assertEquals(expectedNumber1, cursor.read()) + assertEquals(byteCount.toUInt(), cursor.position) + + assertEquals(expectedNumber2, cursor.read()) + assertEquals(2u * byteCount.toUInt(), cursor.position) + } + + @Test + fun u8Array() { + val read: Cursor.(UInt) -> IntArray = { n -> + val arr = u8Array(n) + IntArray(n.toInt()) { arr[it].toInt() } + } + + testIntegerArrayRead(1, read, Endianness.Little) + testIntegerArrayRead(1, read, Endianness.Big) + } + + @Test + fun u16Array() { + val read: Cursor.(UInt) -> IntArray = { n -> + val arr = u16Array(n) + IntArray(n.toInt()) { arr[it].toInt() } + } + + testIntegerArrayRead(2, read, Endianness.Little) + testIntegerArrayRead(2, read, Endianness.Big) + } + + @Test + fun u32Array() { + val read: Cursor.(UInt) -> IntArray = { n -> + val arr = u32Array(n) + IntArray(n.toInt()) { arr[it].toInt() } + } + + testIntegerArrayRead(4, read, Endianness.Little) + testIntegerArrayRead(4, read, Endianness.Big) + } + + @Test + fun i32Array() { + val read: Cursor.(UInt) -> IntArray = { n -> + val arr = i32Array(n) + IntArray(n.toInt()) { arr[it] } + } + + testIntegerArrayRead(4, read, Endianness.Little) + testIntegerArrayRead(4, read, Endianness.Big) + } + + private fun testIntegerArrayRead( + byteCount: Int, + read: Cursor.(UInt) -> IntArray, + endianness: Endianness, + ) { + // Generate array of the form 1, 2, 0xFF, 4, 5, 6, 7, 8. + val bytes = Array(8 * byteCount) { 0 } + + for (i in 0 until 8) { + if (i == 2) { + for (j in 0 until byteCount) { + bytes[i * byteCount + j] = (0xff).toByte() + } + } else { + if (endianness == Endianness.Little) { + bytes[i * byteCount] = (i + 1).toByte() + } else { + bytes[i * byteCount + byteCount - 1] = (i + 1).toByte() + } + } + } + + var allOnes = 0 + repeat(byteCount) { allOnes = ((allOnes shl 8) or 0xff) } + + // Test cursor. + val cursor = createCursor(bytes, endianness) + + val array1 = cursor.read(3u) + assertEquals(1, array1[0]) + assertEquals(2, array1[1]) + assertEquals(allOnes, array1[2]) + + cursor.seekStart((2 * byteCount).toUInt()) + val array2 = cursor.read(4u) + assertEquals(allOnes, array2[0]) + assertEquals(4, array2[1]) + assertEquals(5, array2[2]) + assertEquals(6, array2[3]) + + cursor.seekStart((5 * byteCount).toUInt()) + val array3 = cursor.read(3u) + assertEquals(6, array3[0]) + assertEquals(7, array3[1]) + assertEquals(8, array3[2]) + } + + @Test + fun stringAscii() { + testStringRead(1, Cursor::stringAscii, Endianness.Little) + testStringRead(1, Cursor::stringAscii, Endianness.Big) + } + + @Test + fun stringUtf16() { + testStringRead(2, Cursor::stringUtf16, Endianness.Little) + testStringRead(2, Cursor::stringUtf16, Endianness.Big) + } + + private fun testStringRead( + byteCount: Int, + read: Cursor.( + maxByteLength: UInt, + nullTerminated: Boolean, + dropRemaining: Boolean, + ) -> String, + endianness: Endianness, + ) { + val chars = byteArrayOf(7, 65, 66, 0, (255).toByte(), 13) + val bytes = Array(chars.size * byteCount) { 0 } + + for (i in 0..chars.size) { + if (endianness == Endianness.Little) { + bytes[byteCount * i] = chars[i] + } else { + bytes[byteCount * i + byteCount - 1] = chars[i] + } + } + + val bc = byteCount.toUInt() + val cursor = createCursor(bytes, endianness) + + cursor.seekStart(bc) + assertEquals("AB", cursor.read(4u * bc, true, true)) + assertEquals(5u * bc, cursor.position) + cursor.seekStart(bc) + assertEquals("AB", cursor.read(2u * bc, true, true)) + assertEquals(3u * bc, cursor.position) + + cursor.seekStart(bc) + assertEquals("AB", cursor.read(4u * bc, true, false)) + assertEquals(4u * bc, cursor.position) + cursor.seekStart(bc) + assertEquals("AB", cursor.read(2u * bc, true, false)) + assertEquals(3u * bc, cursor.position) + + cursor.seekStart(bc) + assertEquals("AB\u0000ÿ", cursor.read(4u * bc, false, true)) + assertEquals(5u * bc, cursor.position) + + cursor.seekStart(bc) + assertEquals("AB\u0000ÿ", cursor.read(4u * bc, false, false)) + assertEquals(5u * bc, cursor.position) + } +} diff --git a/lib/src/commonTest/kotlin/world/phantasmal/lib/cursor/WritableCursorTests.kt b/lib/src/commonTest/kotlin/world/phantasmal/lib/cursor/WritableCursorTests.kt new file mode 100644 index 00000000..c74d5206 --- /dev/null +++ b/lib/src/commonTest/kotlin/world/phantasmal/lib/cursor/WritableCursorTests.kt @@ -0,0 +1,217 @@ +package world.phantasmal.lib.cursor + +import kotlin.math.abs +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +abstract class WritableCursorTests : CursorTests() { + abstract override fun createCursor(bytes: Array, endianness: Endianness): WritableCursor + + @Test + fun simple_WritableCursor_properties_and_invariants() { + simple_WritableCursor_properties_and_invariants(Endianness.Little) + simple_WritableCursor_properties_and_invariants(Endianness.Big) + } + + private fun simple_WritableCursor_properties_and_invariants(endianness: Endianness) { + val cursor = createCursor(arrayOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), endianness) + + assertEquals(0u, cursor.position) + + cursor.writeU8(99u).writeU8(99u).writeU8(99u).writeU8(99u) + cursor.seek(-1) + + assertEquals(cursor.position + cursor.bytesLeft, cursor.size) + assertEquals(10u, cursor.size) + assertEquals(3u, cursor.position) + assertEquals(7u, cursor.bytesLeft) + assertEquals(endianness, cursor.endianness) + } + + @Test + fun writeU8() { + testIntegerWrite(1, { u8().toInt() }, { writeU8(it.toUByte()) }, Endianness.Little) + testIntegerWrite(1, { u8().toInt() }, { writeU8(it.toUByte()) }, Endianness.Big) + } + + @Test + fun writeU16() { + testIntegerWrite(2, { u16().toInt() }, { writeU16(it.toUShort()) }, Endianness.Little) + testIntegerWrite(2, { u16().toInt() }, { writeU16(it.toUShort()) }, Endianness.Big) + } + + @Test + fun writeU32() { + testIntegerWrite(4, { u32().toInt() }, { writeU32(it.toUInt()) }, Endianness.Little) + testIntegerWrite(4, { u32().toInt() }, { writeU32(it.toUInt()) }, Endianness.Big) + } + + @Test + fun writeI8() { + testIntegerWrite(1, { i8().toInt() }, { writeI8(it.toByte()) }, Endianness.Little) + testIntegerWrite(1, { i8().toInt() }, { writeI8(it.toByte()) }, Endianness.Big) + } + + @Test + fun writeI16() { + testIntegerWrite(2, { i16().toInt() }, { writeI16(it.toShort()) }, Endianness.Little) + testIntegerWrite(2, { i16().toInt() }, { writeI16(it.toShort()) }, Endianness.Big) + } + + @Test + fun writeI32() { + testIntegerWrite(4, { i32() }, { writeI32(it) }, Endianness.Little) + testIntegerWrite(4, { i32() }, { writeI32(it) }, Endianness.Big) + } + + /** + * Writes and reads two integers. + */ + private fun testIntegerWrite( + byteCount: Int, + read: Cursor.() -> Int, + write: WritableCursor.(Int) -> Unit, + endianness: Endianness, + ) { + val expectedNumber1 = 0x01020304 shr (8 * (4 - byteCount)) + val expectedNumber2 = 0x05060708 shr (8 * (4 - byteCount)) + + val cursor = createCursor(Array(2 * byteCount) { 0 }, endianness) + + cursor.write(expectedNumber1) + cursor.write(expectedNumber2) + + assertEquals((2 * byteCount).toUInt(), cursor.position) + + cursor.seekStart(0u) + + assertEquals(expectedNumber1, cursor.read()) + assertEquals(expectedNumber2, cursor.read()) + } + + @Test + fun writeF32() { + writeF32(Endianness.Little) + writeF32(Endianness.Big) + } + + /** + * Writes and reads two floats. + */ + private fun writeF32(endianness: Endianness) { + val cursor = createCursor(Array(8) { 0 }, endianness) + + cursor.writeF32(1337.9001f) + cursor.writeF32(103.502f) + + assertEquals(8u, cursor.position) + + cursor.seekStart(0u) + + // The read floats won't be exactly the same as the written floats in Kotlin JS, because + // they're backed by numbers (64-bit floats). + assertTrue(abs(1337.9001f - cursor.f32()) < 0.001) + assertTrue(abs(103.502f - cursor.f32()) < 0.001) + + assertEquals(8u, cursor.position) + } + + @Test + fun writeU8Array() { + val read: Cursor.(UInt) -> IntArray = { n -> + val arr = u8Array(n) + IntArray(n.toInt()) { arr[it].toInt() } + } + val write: WritableCursor.(IntArray) -> Unit = { a -> + writeU8Array(UByteArray(a.size) { a[it].toUByte() }) + } + + testIntegerArrayWrite(1, read, write, Endianness.Little) + testIntegerArrayWrite(1, read, write, Endianness.Big) + } + + @Test + fun writeU16Array() { + val read: Cursor.(UInt) -> IntArray = { n -> + val arr = u16Array(n) + IntArray(n.toInt()) { arr[it].toInt() } + } + val write: WritableCursor.(IntArray) -> Unit = { a -> + writeU16Array(UShortArray(a.size) { a[it].toUShort() }) + } + + testIntegerArrayWrite(2, read, write, Endianness.Little) + testIntegerArrayWrite(2, read, write, Endianness.Big) + } + + @Test + fun writeU32Array() { + val read: Cursor.(UInt) -> IntArray = { n -> + val arr = u32Array(n) + IntArray(n.toInt()) { arr[it].toInt() } + } + val write: WritableCursor.(IntArray) -> Unit = { a -> + writeU32Array(UIntArray(a.size) { a[it].toUInt() }) + } + + testIntegerArrayWrite(4, read, write, Endianness.Little) + testIntegerArrayWrite(4, read, write, Endianness.Big) + } + + @Test + fun writeI32Array() { + val read: Cursor.(UInt) -> IntArray = { n -> + i32Array(n) + } + val write: WritableCursor.(IntArray) -> Unit = { a -> + writeI32Array(a) + } + + testIntegerArrayWrite(4, read, write, Endianness.Little) + testIntegerArrayWrite(4, read, write, Endianness.Big) + } + + private fun testIntegerArrayWrite( + byteCount: Int, + read: Cursor.(UInt) -> IntArray, + write: WritableCursor.(IntArray) -> Unit, + endianness: Endianness, + ) { + val testArray1 = IntArray(10) { it } + val testArray2 = IntArray(10) { it + 10 } + + val cursor = createCursor(Array(20 * byteCount) { 0 }, endianness) + + cursor.write(testArray1) + assertEquals(10u * byteCount.toUInt(), cursor.position) + + cursor.write(testArray2) + assertEquals(20u * byteCount.toUInt(), cursor.position) + + cursor.seekStart(0u) + + assertTrue(testArray1.contentEquals(cursor.read(10u))) + assertTrue(testArray2.contentEquals(cursor.read(10u))) + assertEquals(20u * byteCount.toUInt(), cursor.position) + } + + @Test + fun write_seek_backwards_then_take() { + write_seek_backwards_then_take(Endianness.Little) + write_seek_backwards_then_take(Endianness.Big) + } + + private fun write_seek_backwards_then_take(endianness: Endianness) { + val cursor = createCursor(Array(16) { 0 }, endianness) + + cursor.writeU32(1u).writeU32(2u).writeU32(3u).writeU32(4u) + cursor.seek(-8) + val newCursor = cursor.take(8u) + + assertEquals(8u, newCursor.size) + assertEquals(0u, newCursor.position) + assertEquals(3u, newCursor.u32()) + assertEquals(4u, newCursor.u32()) + } +} diff --git a/lib/src/jsMain/kotlin/world/phantasmal/lib/cursor/AbstractArrayBufferCursor.kt b/lib/src/jsMain/kotlin/world/phantasmal/lib/cursor/AbstractArrayBufferCursor.kt new file mode 100644 index 00000000..bfcb1f07 --- /dev/null +++ b/lib/src/jsMain/kotlin/world/phantasmal/lib/cursor/AbstractArrayBufferCursor.kt @@ -0,0 +1,167 @@ +package world.phantasmal.lib.cursor + +import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.DataView + +abstract class AbstractArrayBufferCursor +protected constructor(endianness: Endianness, offset: UInt) : AbstractWritableCursor(offset) { + private var littleEndian: Boolean = endianness == Endianness.Little + protected abstract val backingBuffer: ArrayBuffer + protected abstract val dv: DataView + + override var endianness: Endianness + get() = if (littleEndian) Endianness.Little else Endianness.Big + set(value) { + littleEndian = value == Endianness.Little + } + + override fun u8(): UByte { + requireSize(1u) + val r = dv.getUint8(absolutePosition.toInt()) + position++ + return r.toUByte() + } + + override fun u16(): UShort { + requireSize(2u) + val r = dv.getUint16(absolutePosition.toInt(), littleEndian) + position += 2u + return r.toUShort() + } + + override fun u32(): UInt { + requireSize(4u) + val r = dv.getUint32(absolutePosition.toInt(), littleEndian) + position += 4u + return r.toUInt() + } + + override fun i8(): Byte { + requireSize(1u) + val r = dv.getInt8(absolutePosition.toInt()) + position++ + return r + } + + override fun i16(): Short { + requireSize(2u) + val r = dv.getInt16(absolutePosition.toInt(), littleEndian) + position += 2u + return r + } + + override fun i32(): Int { + requireSize(4u) + val r = dv.getInt32(absolutePosition.toInt(), littleEndian) + position += 4u + return r + } + + override fun f32(): Float { + requireSize(4u) + val r = dv.getFloat32(absolutePosition.toInt(), littleEndian) + position += 4u + return r + } + + override fun u8Array(n: UInt): UByteArray { + requireSize(n) + + val array = UByteArray(n.toInt()) + + for (i in 0 until n.toInt()) { + array[i] = dv.getUint8(absolutePosition.toInt()).toUByte() + position++ + } + + return array + } + + override fun u16Array(n: UInt): UShortArray { + requireSize(2u * n) + + val array = UShortArray(n.toInt()) + + for (i in 0 until n.toInt()) { + array[i] = dv.getUint16(absolutePosition.toInt(), littleEndian).toUShort() + position += 2u + } + + return array + } + + override fun u32Array(n: UInt): UIntArray { + requireSize(4u * n) + + val array = UIntArray(n.toInt()) + + for (i in 0 until n.toInt()) { + array[i] = dv.getUint32(absolutePosition.toInt(), littleEndian).toUInt() + position += 4u + } + + return array + } + + override fun i32Array(n: UInt): IntArray { + requireSize(4u * n) + + val array = IntArray(n.toInt()) + + for (i in 0 until n.toInt()) { + array[i] = dv.getInt32(absolutePosition.toInt(), littleEndian) + position += 4u + } + + return array + } + + override fun writeU8(value: UByte): WritableCursor { + requireSize(1u) + dv.setUint8(absolutePosition.toInt(), value.toByte()) + position++ + return this + } + + override fun writeU16(value: UShort): WritableCursor { + requireSize(2u) + dv.setUint16(absolutePosition.toInt(), value.toShort(), littleEndian) + position += 2u + return this + } + + override fun writeU32(value: UInt): WritableCursor { + requireSize(4u) + dv.setUint32(absolutePosition.toInt(), value.toInt(), littleEndian) + position += 4u + return this + } + + override fun writeI8(value: Byte): WritableCursor { + requireSize(1u) + dv.setInt8(absolutePosition.toInt(), value) + position++ + return this + } + + override fun writeI16(value: Short): WritableCursor { + requireSize(2u) + dv.setInt16(absolutePosition.toInt(), value, littleEndian) + position += 2u + return this + } + + override fun writeI32(value: Int): WritableCursor { + requireSize(4u) + dv.setInt32(absolutePosition.toInt(), value, littleEndian) + position += 4u + return this + } + + override fun writeF32(value: Float): WritableCursor { + requireSize(4u) + dv.setFloat32(absolutePosition.toInt(), value, littleEndian) + position += 4u + return this + } +} diff --git a/lib/src/jsMain/kotlin/world/phantasmal/lib/cursor/ArrayBufferCursor.kt b/lib/src/jsMain/kotlin/world/phantasmal/lib/cursor/ArrayBufferCursor.kt new file mode 100644 index 00000000..18ea8f91 --- /dev/null +++ b/lib/src/jsMain/kotlin/world/phantasmal/lib/cursor/ArrayBufferCursor.kt @@ -0,0 +1,35 @@ +package world.phantasmal.lib.cursor + +import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.DataView + +/** + * A cursor for reading from an array buffer or part of an array buffer. + * + * @param buffer The buffer to read from. + * @param endianness Decides in which byte order multi-byte integers and floats will be interpreted. + * @param offset The start offset of the part that will be read from. + * @param size The size of the part that will be read from. + */ +class ArrayBufferCursor( + buffer: ArrayBuffer, + endianness: Endianness, + offset: UInt = 0u, + size: UInt = buffer.byteLength.toUInt() - offset, +) : AbstractArrayBufferCursor(endianness, offset) { + override val backingBuffer = buffer + override val dv = DataView(buffer, 0, buffer.byteLength) + + override var size: UInt = size + set(value) { + require(size <= backingBuffer.byteLength.toUInt() - offset) + field = value + } + + override fun take(size: UInt): ArrayBufferCursor { + val offset = offset + position + val wrapper = ArrayBufferCursor(backingBuffer, endianness, offset, size) + this.position += size + return wrapper + } +} diff --git a/lib/src/jsTest/kotlin/world/phantasmal/lib/cursor/ArrayBufferCursorTests.kt b/lib/src/jsTest/kotlin/world/phantasmal/lib/cursor/ArrayBufferCursorTests.kt new file mode 100644 index 00000000..50305b56 --- /dev/null +++ b/lib/src/jsTest/kotlin/world/phantasmal/lib/cursor/ArrayBufferCursorTests.kt @@ -0,0 +1,8 @@ +package world.phantasmal.lib.cursor + +import org.khronos.webgl.Uint8Array + +class ArrayBufferCursorTests : WritableCursorTests() { + override fun createCursor(bytes: Array, endianness: Endianness) = + ArrayBufferCursor(Uint8Array(bytes).buffer, endianness) +}