Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
kapacity = "0.9.9-beta07"
kapacity = "0.9.9-beta08"
agp = "8.11.2"
android-compileSdk = "36"
android-minSdk = "24"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package io.github.developrofthings.kapacity.io

import io.github.developrofthings.kapacity.byte
import java.nio.ByteBuffer
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals

class ByteBufferKapacityTest {

//region ByteBuffer.get() Tests
@Test
fun testGet_exactFit() {
val buffer = ByteBuffer.wrap(byteArrayOf(1, 2, 3, 4, 5))
val dest = ByteArray(size = 5)

buffer.get(destination = dest, kapacity = 5.byte)

assertContentEquals(byteArrayOf(1, 2, 3, 4, 5), dest)
assertEquals(0, buffer.remaining()) // Buffer should be fully read
}

@Test
fun testGet_limitedByKapacity() {
val buffer = ByteBuffer.wrap(byteArrayOf(1, 2, 3, 4, 5))
val dest = ByteArray(size = 5)

// Buffer has 5, array holds 5, but we only ask for 3
buffer.get(destination = dest, kapacity = 3.byte)

// Only the first 3 slots should be filled
assertContentEquals(byteArrayOf(1, 2, 3, 0, 0), dest)
assertEquals(2, buffer.remaining()) // 2 bytes left in buffer
}

@Test
fun testGet_limitedByArraySpace() {
val buffer = ByteBuffer.wrap(byteArrayOf(1, 2, 3, 4, 5))
val dest = ByteArray(size = 2)

// Ask for 5, but array only holds 2
buffer.get(destination = dest, kapacity = 5.byte)

assertContentEquals(byteArrayOf(1, 2), dest)
assertEquals(3, buffer.remaining()) // 3 bytes left in buffer
}

@Test
fun testGet_limitedByBufferSpace() {
val buffer = ByteBuffer.wrap(byteArrayOf(1, 2))
val dest = ByteArray(size = 5)

// Ask for 5, array holds 5, but buffer only has 2
buffer.get(destination = dest, kapacity = 5.byte)

assertContentEquals(byteArrayOf(1, 2, 0, 0, 0), dest)
assertEquals(0, buffer.remaining())
}

@Test
fun testGet_withOffset() {
val buffer = ByteBuffer.wrap(byteArrayOf(9, 8, 7))
val dest = ByteArray(size = 5)

// Write starting at index 2
buffer.get(destination = dest, destinationOffset = 2, kapacity = 10.byte)

assertContentEquals(byteArrayOf(0, 0, 9, 8, 7), dest)
}
//endregion

//region ByteBuffer.put() Tests
@Test
fun testPut_exactFit() {
val buffer = ByteBuffer.allocate(5)
val source = byteArrayOf(1, 2, 3, 4, 5)

buffer.put(source = source, kapacity = 5.byte)

buffer.flip() // Prepare buffer for reading to assert contents
val result = ByteArray(5)
buffer.get(result)
assertContentEquals(byteArrayOf(1, 2, 3, 4, 5), result)
}

@Test
fun testPut_limitedByKapacity() {
val buffer = ByteBuffer.allocate(5)
val source = byteArrayOf(1, 2, 3, 4, 5)

// We have 5, buffer holds 5, but we only want to write 2
buffer.put(source = source, kapacity = 2.byte)

assertEquals(2, buffer.position()) // Position should advance by 2
}

@Test
fun testPut_limitedByBufferSpace() {
val buffer = ByteBuffer.allocate(2)
val source = byteArrayOf(1, 2, 3, 4, 5)

// Try to write 5, but buffer only holds 2
buffer.put(source = source, kapacity = 5.byte)

assertEquals(2, buffer.position()) // Should safely stop at buffer capacity
assertEquals(0, buffer.remaining())
}

@Test
fun testPut_limitedByArraySpace() {
val buffer = ByteBuffer.allocate(5)
val source = byteArrayOf(1, 2)

// Try to write 5, but array only has 2
buffer.put(source = source, kapacity = 5.byte)

assertEquals(2, buffer.position()) // Should safely stop after reading the 2 available bytes
}

@Test
fun testPut_withOffset() {
val buffer = ByteBuffer.allocate(5)
val source = byteArrayOf(0, 0, 9, 8, 7) // We want to skip the first two zeros

// Read from source starting at index 2
buffer.put(source = source, sourceOffset = 2, kapacity = 10.byte)

assertEquals(3, buffer.position()) // Should write exactly 3 bytes

buffer.flip()
val result = ByteArray(3)
buffer.get(result)
assertContentEquals(byteArrayOf(9, 8, 7), result)
}
//endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
@file:OptIn(InternalKapacityApi::class)

package io.github.developrofthings.kapacity.io

import io.github.developrofthings.kapacity.InternalKapacityApi
import io.github.developrofthings.kapacity.Kapacity
import io.github.developrofthings.kapacity.gigabyte
import io.github.developrofthings.kapacity.megabyte
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class KapacityByteBufferAllocationTest {

// --- 1. TEST THE MATH (Zero Allocation) ---
@Test
fun testCoercion_safelyClampsMassiveCapacities() {
val massiveKapacity = 3.gigabyte

// Test the internal property directly to prove the math works
// without ever touching the JVM's memory allocator.
assertEquals((Int.MAX_VALUE - 8), massiveKapacity.rawBytesCoercedToIntRange)
}

@Test
fun testCoercion_handlesNegativeOverflowGracefully() {
// If someone bypasses builders and forces a negative value
val negativeKapacity = Kapacity.fromBytes(-5000L)

// Prove it clamps to 0 to prevent NegativeArraySizeException
assertEquals(0, negativeKapacity.rawBytesCoercedToIntRange)
}

// --- 2. TEST THE ALLOCATOR (Safe Memory Bounds) ---
@Test
fun testAllocateByteBuffer_wiresCorrectly() {
// Use a safe, reasonable size (e.g., 10 Megabytes) that will
// comfortably allocate on any CI runner or Android emulator.
val safeKapacity = 10.megabyte

val buffer = safeKapacity.allocateByteBuffer()

assertEquals(safeKapacity.rawBytes, buffer.capacity().toLong())
assertTrue(buffer.hasArray())
}

@Test
fun testDirectAllocateByteBuffer_wiresCorrectly() {
val safeKapacity = 10.megabyte

val buffer = safeKapacity.directAllocateByteBuffer()

assertEquals(safeKapacity.rawBytes, buffer.capacity().toLong())
assertTrue(buffer.isDirect)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
@file:OptIn(InternalKapacityApi::class)
@file:Suppress("unused")

package io.github.developrofthings.kapacity.io

import io.github.developrofthings.kapacity.InternalKapacityApi
import io.github.developrofthings.kapacity.Kapacity
import io.github.developrofthings.kapacity.byte
import java.io.File
Expand All @@ -25,4 +29,98 @@ val Path.kapacity: Kapacity get() = this.fileSize().byte
/**
* Returns the [Kapacity] of this [ByteBuffer], calculated directly from its length on the disk.
*/
val ByteBuffer.kapacity: Kapacity get() = this.capacity().byte
val ByteBuffer.kapacity: Kapacity get() = this.capacity().byte

/**
* Allocates a new standard (heap-backed) [ByteBuffer] with a capacity equal to this [Kapacity].
*
* This buffer is allocated on the JVM heap and will be managed by the standard garbage collector.
* It is backed by a standard `ByteArray`, which can be accessed via `buffer.array()`.
*
* **Warning on Bounds:** Because [ByteBuffer] capacities are strictly indexed by [Int], the maximum
* allowed size is [Int.MAX_VALUE]` - 8` (approximately 2.14 GB). If this capacity exceeds that limit,
* the resulting buffer size will be safely truncated to [Int.MAX_VALUE] to prevent memory allocation crashes.
*
* @return A new, empty [ByteBuffer] ready for writing.
*/
fun Kapacity.allocateByteBuffer(): ByteBuffer =
ByteBuffer.allocate(/*capacity = */this.rawBytesCoercedToIntRange)

/**
* Allocates a new direct (off-heap) [ByteBuffer] with a capacity equal to this [Kapacity].
*
* Direct buffers allocate memory outside of the standard JVM heap, bypassing standard garbage collection.
* They are highly optimized for native I/O operations (like socket or file channel transfers) and JNI
* interactions, as they avoid copying data between the JVM heap and native memory.
*
* **Note:** Allocating and deallocating direct buffers is generally more expensive than standard buffers,
* so they are best suited for large, long-lived buffers.
*
* **Warning on Bounds:** Like standard buffers, the maximum allowed size is safely truncated to
* [Int.MAX_VALUE]` - 8` (approximately 2.14 GB) to prevent memory allocation crashes.
*
* @return A new, empty direct [ByteBuffer] ready for writing.
*/
fun Kapacity.directAllocateByteBuffer(): ByteBuffer =
ByteBuffer.allocateDirect(/*capacity = */this.rawBytesCoercedToIntRange)

/**
* Reads up to the specified [kapacity] of bytes from this buffer into the [destination] array.
*
* **Warning on Bounds:** This operation safely clamps the number of bytes read to prevent
* `BufferUnderflowException` and `IndexOutOfBoundsException`. The actual number of bytes transferred
* will be the minimum of: the requested [kapacity], the [ByteBuffer.remaining] bytes in this buffer, or the
* available space in the [destination] array accounting for the [destinationOffset].
*
* @param destination The byte array to write the data into.
* @param destinationOffset The index within the destination array to begin writing. Defaults to 0.
* @param kapacity The maximum amount of data to read from this buffer.
* @return This buffer, to allow for fluent method chaining.
*/
fun ByteBuffer.get(
destination: ByteArray,
destinationOffset: Int = 0,
kapacity: Kapacity,
): ByteBuffer {
val length = (destination.size - destinationOffset)
val safeLength = minOf(
a = kapacity.rawBytesCoercedToIntRange,
b = minOf(a = remaining(), b = length)
)

return this.get(
/* dst = */ destination,
/* offset = */ destinationOffset,
/* length = */ safeLength,
)
}

/**
* Writes up to the specified [kapacity] of bytes from the [source] array into this buffer.
*
* **Warning on Bounds:** This operation safely clamps the number of bytes written to prevent
* `BufferOverflowException` and `IndexOutOfBoundsException`. The actual number of bytes transferred
* will be the minimum of: the requested [kapacity], the [ByteBuffer.remaining] space in this buffer, or the
* available data in the [source] array accounting for the [sourceOffset].
*
* @param source The byte array containing the data to write.
* @param sourceOffset The index within the source array to begin reading from. Defaults to 0.
* @param kapacity The maximum amount of data to write into this buffer.
* @return This buffer, to allow for fluent method chaining.
*/
fun ByteBuffer.put(
source: ByteArray,
sourceOffset: Int = 0,
kapacity: Kapacity,
): ByteBuffer {
val length = (source.size - sourceOffset)
val safeLength = minOf(
a = kapacity.rawBytesCoercedToIntRange,
b = minOf(a = remaining(), b = length)
)
return this.put(
/* src = */ source,
/* offset = */ sourceOffset,
/* length = */ safeLength,
)
}
Loading
Loading