diff --git a/core/api/kotlinx-io-core.api b/core/api/kotlinx-io-core.api index 2fb84fbd6..60f0e5afd 100644 --- a/core/api/kotlinx-io-core.api +++ b/core/api/kotlinx-io-core.api @@ -43,6 +43,10 @@ public final class kotlinx/io/Buffer : kotlinx/io/Sink, kotlinx/io/Source { public fun writeShort (S)V } +public final class kotlinx/io/BufferKt { + public static final synthetic fun seek (Lkotlinx/io/Buffer;JLkotlin/jvm/functions/Function2;)Ljava/lang/Object; +} + public final class kotlinx/io/BuffersJvmKt { public static final fun asByteChannel (Lkotlinx/io/Buffer;)Ljava/nio/channels/ByteChannel; public static final fun copyTo (Lkotlinx/io/Buffer;Ljava/io/OutputStream;JJ)V @@ -106,11 +110,13 @@ public final class kotlinx/io/Segment { public final synthetic fun getLimit ()I public final synthetic fun getNext ()Lkotlinx/io/Segment; public final synthetic fun getPos ()I + public final synthetic fun getPrev ()Lkotlinx/io/Segment; public final synthetic fun getRemainingCapacity ()I public final synthetic fun getSize ()I public final synthetic fun setLimit (I)V public final synthetic fun setNext (Lkotlinx/io/Segment;)V public final synthetic fun setPos (I)V + public final synthetic fun setPrev (Lkotlinx/io/Segment;)V public final synthetic fun writeBackData ([BI)V } @@ -282,12 +288,31 @@ public final class kotlinx/io/files/PathsKt { public static final fun sourceDeprecated (Lkotlinx/io/files/Path;)Lkotlinx/io/Source; } +public abstract interface class kotlinx/io/unsafe/BufferIterationContext : kotlinx/io/unsafe/SegmentReadContext { + public abstract fun next (Lkotlinx/io/Segment;)Lkotlinx/io/Segment; +} + +public abstract interface class kotlinx/io/unsafe/SegmentReadContext { + public abstract fun getUnchecked (Lkotlinx/io/Segment;I)B +} + +public abstract interface class kotlinx/io/unsafe/SegmentWriteContext { + public abstract fun setUnchecked (Lkotlinx/io/Segment;IB)V + public abstract fun setUnchecked (Lkotlinx/io/Segment;IBB)V + public abstract fun setUnchecked (Lkotlinx/io/Segment;IBBB)V + public abstract fun setUnchecked (Lkotlinx/io/Segment;IBBBB)V +} + public final class kotlinx/io/unsafe/UnsafeBufferOperations { public static final field INSTANCE Lkotlinx/io/unsafe/UnsafeBufferOperations; public final fun getMaxSafeWriteCapacity ()I + public final fun iterate (Lkotlinx/io/Buffer;JLkotlin/jvm/functions/Function3;)V + public final fun iterate (Lkotlinx/io/Buffer;Lkotlin/jvm/functions/Function2;)V public final fun moveToTail (Lkotlinx/io/Buffer;[BII)V public static synthetic fun moveToTail$default (Lkotlinx/io/unsafe/UnsafeBufferOperations;Lkotlinx/io/Buffer;[BIIILjava/lang/Object;)V + public final fun readFromHead (Lkotlinx/io/Buffer;Lkotlin/jvm/functions/Function2;)V public final fun readFromHead (Lkotlinx/io/Buffer;Lkotlin/jvm/functions/Function3;)V + public final fun writeToTail (Lkotlinx/io/Buffer;ILkotlin/jvm/functions/Function2;)V public final fun writeToTail (Lkotlinx/io/Buffer;ILkotlin/jvm/functions/Function3;)V } @@ -297,3 +322,10 @@ public final class kotlinx/io/unsafe/UnsafeBufferOperationsJvmKt { public static final fun writeToTail (Lkotlinx/io/unsafe/UnsafeBufferOperations;Lkotlinx/io/Buffer;ILkotlin/jvm/functions/Function1;)V } +public final class kotlinx/io/unsafe/UnsafeBufferOperationsKt { + public static final synthetic fun getBufferIterationContextImpl ()Lkotlinx/io/unsafe/BufferIterationContext; + public static final synthetic fun getSegmentReadContextImpl ()Lkotlinx/io/unsafe/SegmentReadContext; + public static final synthetic fun getSegmentWriteContextImpl ()Lkotlinx/io/unsafe/SegmentWriteContext; + public static final synthetic fun withData (Lkotlinx/io/unsafe/SegmentReadContext;Lkotlinx/io/Segment;Lkotlin/jvm/functions/Function3;)V +} + diff --git a/core/api/kotlinx-io-core.klib.api b/core/api/kotlinx-io-core.klib.api index 78a89aa59..e83252e36 100644 --- a/core/api/kotlinx-io-core.klib.api +++ b/core/api/kotlinx-io-core.klib.api @@ -7,6 +7,18 @@ // - Show declarations: true // Library unique name: +abstract interface kotlinx.io.unsafe/BufferIterationContext : kotlinx.io.unsafe/SegmentReadContext { // kotlinx.io.unsafe/BufferIterationContext|null[0] + abstract fun next(kotlinx.io/Segment): kotlinx.io/Segment? // kotlinx.io.unsafe/BufferIterationContext.next|next(kotlinx.io.Segment){}[0] +} +abstract interface kotlinx.io.unsafe/SegmentReadContext { // kotlinx.io.unsafe/SegmentReadContext|null[0] + abstract fun getUnchecked(kotlinx.io/Segment, kotlin/Int): kotlin/Byte // kotlinx.io.unsafe/SegmentReadContext.getUnchecked|getUnchecked(kotlinx.io.Segment;kotlin.Int){}[0] +} +abstract interface kotlinx.io.unsafe/SegmentWriteContext { // kotlinx.io.unsafe/SegmentWriteContext|null[0] + abstract fun setUnchecked(kotlinx.io/Segment, kotlin/Int, kotlin/Byte) // kotlinx.io.unsafe/SegmentWriteContext.setUnchecked|setUnchecked(kotlinx.io.Segment;kotlin.Int;kotlin.Byte){}[0] + abstract fun setUnchecked(kotlinx.io/Segment, kotlin/Int, kotlin/Byte, kotlin/Byte) // kotlinx.io.unsafe/SegmentWriteContext.setUnchecked|setUnchecked(kotlinx.io.Segment;kotlin.Int;kotlin.Byte;kotlin.Byte){}[0] + abstract fun setUnchecked(kotlinx.io/Segment, kotlin/Int, kotlin/Byte, kotlin/Byte, kotlin/Byte) // kotlinx.io.unsafe/SegmentWriteContext.setUnchecked|setUnchecked(kotlinx.io.Segment;kotlin.Int;kotlin.Byte;kotlin.Byte;kotlin.Byte){}[0] + abstract fun setUnchecked(kotlinx.io/Segment, kotlin/Int, kotlin/Byte, kotlin/Byte, kotlin/Byte, kotlin/Byte) // kotlinx.io.unsafe/SegmentWriteContext.setUnchecked|setUnchecked(kotlinx.io.Segment;kotlin.Int;kotlin.Byte;kotlin.Byte;kotlin.Byte;kotlin.Byte){}[0] +} abstract interface kotlinx.io/RawSink : kotlin/AutoCloseable { // kotlinx.io/RawSink|null[0] abstract fun close() // kotlinx.io/RawSink.close|close(){}[0] abstract fun flush() // kotlinx.io/RawSink.flush|flush(){}[0] @@ -100,6 +112,9 @@ final class kotlinx.io/Segment { // kotlinx.io/Segment|null[0] final var pos // kotlinx.io/Segment.pos|{}pos[0] final fun (): kotlin/Int // kotlinx.io/Segment.pos.|(){}[0] final fun (kotlin/Int) // kotlinx.io/Segment.pos.|(kotlin.Int){}[0] + final var prev // kotlinx.io/Segment.prev|{}prev[0] + final fun (): kotlinx.io/Segment? // kotlinx.io/Segment.prev.|(){}[0] + final fun (kotlinx.io/Segment?) // kotlinx.io/Segment.prev.|(kotlinx.io.Segment?){}[0] } final fun (kotlinx.io.files/Path).kotlinx.io.files/sink(): kotlinx.io/Sink // kotlinx.io.files/sink|sink@kotlinx.io.files.Path(){}[0] final fun (kotlinx.io.files/Path).kotlinx.io.files/source(): kotlinx.io/Source // kotlinx.io.files/source|source@kotlinx.io.files.Path(){}[0] @@ -163,10 +178,16 @@ final fun kotlinx.io.files/Path(kotlin/String): kotlinx.io.files/Path // kotlinx final fun kotlinx.io.files/Path(kotlin/String, kotlin/Array...): kotlinx.io.files/Path // kotlinx.io.files/Path|Path(kotlin.String;kotlin.Array...){}[0] final fun kotlinx.io.files/Path(kotlinx.io.files/Path, kotlin/Array...): kotlinx.io.files/Path // kotlinx.io.files/Path|Path(kotlinx.io.files.Path;kotlin.Array...){}[0] final fun kotlinx.io/discardingSink(): kotlinx.io/RawSink // kotlinx.io/discardingSink|discardingSink(){}[0] +final inline fun (kotlinx.io.unsafe/SegmentReadContext).kotlinx.io.unsafe/withData(kotlinx.io/Segment, kotlin/Function3) // kotlinx.io.unsafe/withData|withData@kotlinx.io.unsafe.SegmentReadContext(kotlinx.io.Segment;kotlin.Function3){}[0] final inline fun (kotlinx.io/Sink).kotlinx.io/writeToInternalBuffer(kotlin/Function1) // kotlinx.io/writeToInternalBuffer|writeToInternalBuffer@kotlinx.io.Sink(kotlin.Function1){}[0] +final inline fun <#A: kotlin/Any?> (kotlinx.io/Buffer).kotlinx.io/seek(kotlin/Long, kotlin/Function2): #A // kotlinx.io/seek|seek@kotlinx.io.Buffer(kotlin.Long;kotlin.Function2){0ยง}[0] final object kotlinx.io.unsafe/UnsafeBufferOperations { // kotlinx.io.unsafe/UnsafeBufferOperations|null[0] final fun moveToTail(kotlinx.io/Buffer, kotlin/ByteArray, kotlin/Int = ..., kotlin/Int = ...) // kotlinx.io.unsafe/UnsafeBufferOperations.moveToTail|moveToTail(kotlinx.io.Buffer;kotlin.ByteArray;kotlin.Int;kotlin.Int){}[0] + final inline fun iterate(kotlinx.io/Buffer, kotlin/Function2) // kotlinx.io.unsafe/UnsafeBufferOperations.iterate|iterate(kotlinx.io.Buffer;kotlin.Function2){}[0] + final inline fun iterate(kotlinx.io/Buffer, kotlin/Long, kotlin/Function3) // kotlinx.io.unsafe/UnsafeBufferOperations.iterate|iterate(kotlinx.io.Buffer;kotlin.Long;kotlin.Function3){}[0] + final inline fun readFromHead(kotlinx.io/Buffer, kotlin/Function2) // kotlinx.io.unsafe/UnsafeBufferOperations.readFromHead|readFromHead(kotlinx.io.Buffer;kotlin.Function2){}[0] final inline fun readFromHead(kotlinx.io/Buffer, kotlin/Function3) // kotlinx.io.unsafe/UnsafeBufferOperations.readFromHead|readFromHead(kotlinx.io.Buffer;kotlin.Function3){}[0] + final inline fun writeToTail(kotlinx.io/Buffer, kotlin/Int, kotlin/Function2) // kotlinx.io.unsafe/UnsafeBufferOperations.writeToTail|writeToTail(kotlinx.io.Buffer;kotlin.Int;kotlin.Function2){}[0] final inline fun writeToTail(kotlinx.io/Buffer, kotlin/Int, kotlin/Function3) // kotlinx.io.unsafe/UnsafeBufferOperations.writeToTail|writeToTail(kotlinx.io.Buffer;kotlin.Int;kotlin.Function3){}[0] final val maxSafeWriteCapacity // kotlinx.io.unsafe/UnsafeBufferOperations.maxSafeWriteCapacity|{}maxSafeWriteCapacity[0] final fun (): kotlin/Int // kotlinx.io.unsafe/UnsafeBufferOperations.maxSafeWriteCapacity.|(){}[0] @@ -177,6 +198,12 @@ final val kotlinx.io.files/SystemPathSeparator // kotlinx.io.files/SystemPathSep final fun (): kotlin/Char // kotlinx.io.files/SystemPathSeparator.|(){}[0] final val kotlinx.io.files/SystemTemporaryDirectory // kotlinx.io.files/SystemTemporaryDirectory|{}SystemTemporaryDirectory[0] final fun (): kotlinx.io.files/Path // kotlinx.io.files/SystemTemporaryDirectory.|(){}[0] +final val kotlinx.io.unsafe/BufferIterationContextImpl // kotlinx.io.unsafe/BufferIterationContextImpl|{}BufferIterationContextImpl[0] + final fun (): kotlinx.io.unsafe/BufferIterationContext // kotlinx.io.unsafe/BufferIterationContextImpl.|(){}[0] +final val kotlinx.io.unsafe/SegmentReadContextImpl // kotlinx.io.unsafe/SegmentReadContextImpl|{}SegmentReadContextImpl[0] + final fun (): kotlinx.io.unsafe/SegmentReadContext // kotlinx.io.unsafe/SegmentReadContextImpl.|(){}[0] +final val kotlinx.io.unsafe/SegmentWriteContextImpl // kotlinx.io.unsafe/SegmentWriteContextImpl|{}SegmentWriteContextImpl[0] + final fun (): kotlinx.io.unsafe/SegmentWriteContext // kotlinx.io.unsafe/SegmentWriteContextImpl.|(){}[0] open annotation class kotlinx.io/DelicateIoApi : kotlin/Annotation { // kotlinx.io/DelicateIoApi|null[0] constructor () // kotlinx.io/DelicateIoApi.|(){}[0] } diff --git a/core/common/src/Buffer.kt b/core/common/src/Buffer.kt index 40cef38e7..c7e8153ad 100644 --- a/core/common/src/Buffer.kt +++ b/core/common/src/Buffer.kt @@ -708,6 +708,8 @@ public class Buffer : Source, Sink { * Invoke `lambda` with the segment and offset at `fromIndex`. Searches from the front or the back * depending on what's closer to `fromIndex`. */ +@PublishedApi +@JvmSynthetic internal inline fun Buffer.seek( fromIndex: Long, lambda: (Segment?, Long) -> T diff --git a/core/common/src/Segment.kt b/core/common/src/Segment.kt index f72e4deb1..79055d981 100644 --- a/core/common/src/Segment.kt +++ b/core/common/src/Segment.kt @@ -73,7 +73,9 @@ public class Segment { internal var next: Segment? = null /** Previous segment in the list, or null. */ - @JvmField + @PublishedApi + @get:JvmSynthetic + @set:JvmSynthetic internal var prev: Segment? = null private constructor() { @@ -239,6 +241,38 @@ public class Segment { @Suppress("UNUSED_PARAMETER") internal fun writeBackData(data: ByteArray, bytesToCommit: Int): Unit = Unit + internal fun getUnchecked(index: Int): Byte { + return data[pos + index] + } + + internal fun setUnchecked(index: Int, value: Byte) { + data[limit + index] = value + } + + internal fun setUnchecked(index: Int, b0: Byte, b1: Byte) { + val d = data + val l = limit + d[l + index] = b0 + d[l + index + 1] = b1 + } + + internal fun setUnchecked(index: Int, b0: Byte, b1: Byte, b2: Byte) { + val d = data + val l = limit + d[l + index] = b0 + d[l + index + 1] = b1 + d[l + index + 2] = b2 + } + + internal fun setUnchecked(index: Int, b0: Byte, b1: Byte, b2: Byte, b3: Byte) { + val d = data + val l = limit + d[l + index] = b0 + d[l + index + 1] = b1 + d[l + index + 2] = b2 + d[l + index + 3] = b3 + } + internal companion object { /** The size of all segments in bytes. */ internal const val SIZE = 8192 diff --git a/core/common/src/unsafe/UnsafeBufferOperations.kt b/core/common/src/unsafe/UnsafeBufferOperations.kt index 545e62ade..a31f9ca12 100644 --- a/core/common/src/unsafe/UnsafeBufferOperations.kt +++ b/core/common/src/unsafe/UnsafeBufferOperations.kt @@ -6,6 +6,7 @@ package kotlinx.io.unsafe import kotlinx.io.* +import kotlin.jvm.JvmSynthetic @UnsafeIoApi public object UnsafeBufferOperations { @@ -92,6 +93,41 @@ public object UnsafeBufferOperations { buffer.skip(bytesRead.toLong()) } + /** + * Provides read-only access to the data from the head of a [buffer] by calling the [readAction] on head's data and + * optionally consumes the data at the end of the action. + * + * The [readAction] receives the buffer head data and an instance of [SegmentReadContext] allowing to read data from + * the segment. + * The head segment is provided for read-only purposes, updating it may affect buffer's data + * and may lead to undefined behavior when performed outside the provided range. + * + * The [readAction] should return the number of bytes consumed, the buffer's size will be decreased by that value, + * and data from the consumed prefix will be no longer available for read. + * If the operation does not consume anything, the action should return `0`. + * It's considered an error to return a negative value or a value exceeding the [Segment.size]. + * + * Both [readAction] arguments are valid only within [readAction] scope, + * it's an error to store and reuse it later. + * + * If the buffer is empty, [IllegalArgumentException] will be thrown. + * + * @throws IllegalStateException when [readAction] returns negative value or a values exceeding + * the [Segment.size] value. + * @throws IllegalArgumentException when the [buffer] is empty. + * + * @sample kotlinx.io.samples.unsafe.UnsafeBufferOperationsSamples.readUleb128 + */ + public inline fun readFromHead(buffer: Buffer, readAction: (SegmentReadContext, Segment) -> Int) { + require(!buffer.exhausted()) { "Buffer is empty" } + val head = buffer.head!! + val bytesRead = readAction(SegmentReadContextImpl, head) + if (bytesRead < 0) throw IllegalStateException("Returned negative read bytes count") + if (bytesRead == 0) return + if (bytesRead > head.size) throw IllegalStateException("Returned too many bytes") + buffer.skip(bytesRead.toLong()) + } + /** * Provides write access to the buffer, allowing to write data * into a not yet committed portion of the buffer's tail using a [writeAction]. @@ -154,4 +190,295 @@ public object UnsafeBufferOperations { buffer.recycleTail() } } + + /** + * Provides write access to the [buffer], allowing to write data + * into a not yet committed portion of the buffer's tail using a [writeAction]. + * + * The [writeAction] receives the segment to write data into and an instance of [SegmentWriteContext] allowing to + * write data. + * + * It's guaranteed that the [Segment.remainingCapacity] for the provided segment is at least [minimumCapacity], + * but if the [minimumCapacity] bytes could not be provided for writing, + * the method will throw [IllegalStateException]. + * It is safe to use any [minimumCapacity] value below [maxSafeWriteCapacity], but unless exact minimum number of + * available bytes is required, it's recommended to use `1` as [minimumCapacity] value. + * + * The value returned by the [writeAction] denotes the number of bytes successfully written to the buffer. + * If no data was written, `0` should be returned. + * It's an error to return a negative value or a value exceeding the [Segment.remainingCapacity]. + * + * Both [writeAction] arguments are valid only within [writeAction] scope, + * it's an error to store and reuse it later. + * + * @throws IllegalStateException when [minimumCapacity] is too large and could not be fulfilled. + * @throws IllegalStateException when [writeAction] returns a negative value or a value exceeding + * the [Segment.remainingCapacity] value for the provided segment. + * + * @sample kotlinx.io.samples.unsafe.UnsafeBufferOperationsSamples.writeUleb128 + */ + public inline fun writeToTail( + buffer: Buffer, + minimumCapacity: Int, + writeAction: (SegmentWriteContext, Segment) -> Int + ) { + val tail = buffer.writableSegment(minimumCapacity) + val bytesWritten = writeAction(SegmentWriteContextImpl, tail) + + // fast path + if (bytesWritten == minimumCapacity) { + tail.limit += bytesWritten + buffer.sizeMut += bytesWritten + return + } + + check(bytesWritten in 0..tail.remainingCapacity) { + "Invalid number of bytes written: $bytesWritten. Should be in 0..${tail.remainingCapacity}" + } + if (bytesWritten != 0) { + tail.limit += bytesWritten + buffer.sizeMut += bytesWritten + return + } + + if (tail.isEmpty()) { + buffer.recycleTail() + } + } + + /** + * Provides access to [buffer] segments starting from the head. + * + * [iterationAction] is invoked with an instance of [BufferIterationContext] + * allowing to iterate over [buffer]'s segments + * and a reference to [buffer]'s head segment, which could be null in case of an empty buffer. + * + * It's considered an error to use a [BufferIterationContext] or a [Segment] instances outside the scope of + * the [iterationAction]. + * + * Both [iterationAction] arguments are valid only within [iterationAction] scope, + * it's an error to store and reuse it later. + * + * @param buffer a buffer to iterate over + * @param iterationAction a callback to invoke with the head reference and an iteration context instance + * + * @sample kotlinx.io.samples.unsafe.UnsafeReadWriteSamplesJvm.messageDigest + * @sample kotlinx.io.samples.unsafe.UnsafeBufferOperationsSamples.crc32Unsafe + */ + public inline fun iterate(buffer: Buffer, iterationAction: (BufferIterationContext, Segment?) -> Unit) { + iterationAction(BufferIterationContextImpl, buffer.head) + } + + /** + * Provides access to [buffer] segments starting from a segment spanning over a specified [offset]. + * + * [iterationAction] is invoked with an instance of [BufferIterationContext] + * allowing to iterate over [buffer]'s segments, a reference to a [buffer]'s segment, + * an offset corresponding to the beginning of the segment. + * + * If the [buffer] is empty, [iterationAction] will be invoked with a null segment. + * + * To locate [buffer]'s [offset]'th byte within the supplied segment, one has to subtract [offset] from the supplied + * offset value. + * + * All [iterationAction] arguments are valid only within [iterationAction] scope, + * it's an error to store and reuse it later. + * + * @param buffer a buffer to iterate over + * @param iterationAction a callback to invoke with an iteration context instance, a segment reference and + * an offset corresponding to the segment's beginning. + * + * @throws IllegalArgumentException when [offset] is negative + * @throws IndexOutOfBoundsException when [offset] is greater or equal to [Buffer.size] + */ + public inline fun iterate( + buffer: Buffer, offset: Long, + iterationAction: (BufferIterationContext, Segment?, Long) -> Unit + ) { + require(offset >= 0) { "Offset must be non-negative: $offset" } + if (offset >= buffer.size) { + throw IndexOutOfBoundsException("Offset should be less than buffer's size (${buffer.size}): $offset") + } + + buffer.seek(offset) { s, o -> + iterationAction(BufferIterationContextImpl, s, o) + } + } +} + +/** + * Provides read access to [Segment]'s data. + */ +@UnsafeIoApi +public interface SegmentReadContext { + /** + * Reads [offset]'s byte from [segment]. + * + * This operation does not perform any checks, and it's caller's responsibility to ensure + * that [offset] is between `0` and [Segment.size]. + * + * @param segment a segment to read from + * @param offset an offset into segment data + * + * @sample kotlinx.io.samples.unsafe.UnsafeBufferOperationsSamples.crc32GetUnchecked + * + */ + public fun getUnchecked(segment: Segment, offset: Int): Byte +} + +/** + * Provides read-only access to [segment]'s data by passing it to [readAction] along with two indices, startIndex + * and endIndex, denoting a readable portion of the array. + * + * It's considered an error to read data outside of that range. + * The data array is provided for read-only purposes, updating it may affect segment's data + * and may lead to undefined behavior when performed outside the provided range. + * + * The data is passed to the [readAction] directly from the segment's internal storage without copying on + * the best effort basis, meaning that there are no strong zero-copy guarantees + * and the copy will be created if it could not be omitted. + * + * @param segment a segment to access data from + * @param readAction an action to invoke on segment's data + * + * @sample kotlinx.io.samples.unsafe.UnsafeReadWriteSamplesJvm.messageDigest + * @sample kotlinx.io.samples.unsafe.UnsafeBufferOperationsSamples.crc32Unsafe + */ +@UnsafeIoApi +@JvmSynthetic +public inline fun SegmentReadContext.withData(segment: Segment, readAction: (ByteArray, Int, Int) -> Unit) { + readAction(segment.dataAsByteArray(true), segment.pos, segment.limit) +} + +/** + * Provides write access to [Segment]'s data. + */ +@UnsafeIoApi +public interface SegmentWriteContext { + /** + * Writes [value] to an uncommitted portion of the [segment] at [offset]. + * + * This operation does not perform any checks, and it's caller's responsibility to ensure + * that [offset] is between `0` and [Segment.remainingCapacity]. + * + * Writing outside that range is considered an error and may lead to data corruption. + * + * @param segment a segment to write into + * @param offset an offset inside [segment]'s uncommitted area to write to + * @param value a value to be written + * + * @sample kotlinx.io.samples.unsafe.UnsafeBufferOperationsSamples.writeUleb128 + */ + public fun setUnchecked(segment: Segment, offset: Int, value: Byte) + + /** + * Writes two bytes, [b0] and [b1] to an uncommitted portion of the [segment] starting at [offset]. + * + * The [b0] is always written at `offset` and [b1] is written at `offset + 1`. + * + * This operation does not perform any checks, and it's caller's responsibility to ensure + * that [offset] is between `0` and [Segment.remainingCapacity] - 1. + * + * Writing outside that range is considered an error and may lead to data corruption. + * + * @param segment a segment to write into + * @param offset an offset inside [segment]'s uncommitted area to write to + * @param b0 a first byte to be written + * @param b1 a second byte to be written + */ + public fun setUnchecked(segment: Segment, offset: Int, b0: Byte, b1: Byte) + + /** + * Writes three bytes, [b0], [b1] and [b2] to an uncommitted portion of the [segment] starting at [offset]. + * + * The [b0] is always written at `offset`, [b1] is written at `offset + 1` and [b2] is written at `offset + 2`. + * + * This operation does not perform any checks, and it's caller's responsibility to ensure + * that [offset] is between `0` and [Segment.remainingCapacity] - 2. + * + * Writing outside that range is considered an error and may lead to data corruption. + * + * @param segment a segment to write into + * @param offset an offset inside [segment]'s uncommitted area to write to + * @param b0 a first byte to be written + * @param b1 a second byte to be written + * @param b2 a third byte to be written + */ + public fun setUnchecked(segment: Segment, offset: Int, b0: Byte, b1: Byte, b2: Byte) + + /** + * Writes three bytes, [b0], [b1], [b2] and [b3] to an uncommitted portion of the [segment] starting at [offset]. + * + * The [b0] is always written at `offset`, [b1] is written at `offset + 1`, + * [b2] is written at `offset + 2`, and [b3] is written at `offset + 3`. + * + * This operation does not perform any checks, and it's caller's responsibility to ensure + * that [offset] is between `0` and [Segment.remainingCapacity] - 3. + * + * Writing outside that range is considered an error and may lead to data corruption. + * + * @param segment a segment to write into + * @param offset an offset inside [segment]'s uncommitted area to write to + * @param b0 a first byte to be written + * @param b1 a second byte to be written + * @param b2 a third byte to be written + * @param b3 a fourth byte to be written + * + */ + public fun setUnchecked(segment: Segment, offset: Int, b0: Byte, b1: Byte, b2: Byte, b3: Byte) +} + +/** + * Allows iterating over [Buffer]'s segments and reading its data. + */ +@UnsafeIoApi +public interface BufferIterationContext : SegmentReadContext { + /** + * Return a segment succeeding [segment] in the buffer, or `null`, if there is no such segment (meaning that the + * [segment] is [Buffer]'s tail). + * + * @param segment a segment for which a successor needs to be found + * + * @sample kotlinx.io.samples.unsafe.UnsafeReadWriteSamplesJvm.messageDigest + * @sample kotlinx.io.samples.unsafe.UnsafeBufferOperationsSamples.crc32Unsafe + */ + public fun next(segment: Segment): Segment? +} + +@UnsafeIoApi +@PublishedApi +@get:JvmSynthetic +internal val SegmentReadContextImpl: SegmentReadContext = object : SegmentReadContext { + override fun getUnchecked(segment: Segment, offset: Int): Byte = segment.getUnchecked(offset) +} + +@UnsafeIoApi +@PublishedApi +@get:JvmSynthetic +internal val SegmentWriteContextImpl: SegmentWriteContext = object : SegmentWriteContext { + override fun setUnchecked(segment: Segment, offset: Int, value: Byte) { + segment.setUnchecked(offset, value) + } + + override fun setUnchecked(segment: Segment, offset: Int, b0: Byte, b1: Byte) { + segment.setUnchecked(offset, b0, b1) + } + + override fun setUnchecked(segment: Segment, offset: Int, b0: Byte, b1: Byte, b2: Byte) { + segment.setUnchecked(offset, b0, b1, b2) + } + + override fun setUnchecked(segment: Segment, offset: Int, b0: Byte, b1: Byte, b2: Byte, b3: Byte) { + segment.setUnchecked(offset, b0, b1, b2, b3) + } +} + +@UnsafeIoApi +@PublishedApi +@get:JvmSynthetic +internal val BufferIterationContextImpl: BufferIterationContext = object : BufferIterationContext { + override fun next(segment: Segment): Segment? = segment.next + + override fun getUnchecked(segment: Segment, offset: Int): Byte = + SegmentReadContextImpl.getUnchecked(segment, offset) } diff --git a/core/common/test/samples/unsafe/unsafeSamples.kt b/core/common/test/samples/unsafe/unsafeSamples.kt index 72b6e4968..5f3007558 100644 --- a/core/common/test/samples/unsafe/unsafeSamples.kt +++ b/core/common/test/samples/unsafe/unsafeSamples.kt @@ -5,16 +5,17 @@ package kotlinx.io.samples.unsafe -import kotlinx.io.Buffer -import kotlinx.io.UnsafeIoApi -import kotlinx.io.readString +import kotlinx.io.* +import kotlinx.io.bytestring.ByteString import kotlinx.io.unsafe.UnsafeBufferOperations +import kotlinx.io.unsafe.withData import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.math.min import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertTrue class UnsafeBufferOperationsSamples { @OptIn(UnsafeIoApi::class) @@ -98,4 +99,236 @@ class UnsafeBufferOperationsSamples { assertEquals(0xDEC0DEDu, buffer.readULeb128()) assertEquals((-1).toULong(), buffer.readULeb128()) } + + @OptIn(UnsafeIoApi::class) + @Test + fun readUleb128() { + // Decode unsigned integer encoded using unsigned LEB128 format: + // https://en.wikipedia.org/wiki/LEB128#Unsigned_LEB128 + fun Buffer.readULEB128UInt(): UInt { + var result = 0u + var shift = 0 + var finished = false + while (!finished) { // Read until a number is fully fetched + if (exhausted()) throw EOFException() + // Pick the first segment and read from it either until the segment exhausted, + // or the number if finished. + UnsafeBufferOperations.readFromHead(this) { readCtx, segment -> + // Iterate over every byte contained in the segment + for (offset in 0..< segment.size) { + if (shift > 28) throw NumberFormatException("Overflow") + // Read the byte at the offset + val b = readCtx.getUnchecked(segment, offset) + val lsb = b.toUInt() and 0x7fu + result = result or (lsb shl shift) + shift += 7 + if (b >= 0) { + finished = true + // We're done, return how many bytes were consumed from the segment + return@readFromHead offset + 1 + } + } + // We read all the data from segment, but not finished yet. + // Return segment.size to indicate that the head segment was consumed in full. + segment.size + } + } + return result + } + + val buffer = Buffer().also { it.write(ByteString(0xe5.toByte(), 0x8e.toByte(), 0x26)) } + assertEquals(624485u, buffer.readULEB128UInt()) + assertTrue(buffer.exhausted()) + + buffer.write(ByteArray(8191)) + buffer.write(ByteString(0xe5.toByte(), 0x8e.toByte(), 0x26)) + buffer.skip(8191) + assertEquals(624485u, buffer.readULEB128UInt()) + assertTrue(buffer.exhausted()) + } + + @OptIn(UnsafeIoApi::class) + @Test + fun writeUleb128() { + // Encode an unsigned integer using unsigned LEB128 format: + // https://en.wikipedia.org/wiki/LEB128#Unsigned_LEB128 + fun Buffer.writeULEB128(value: UInt) { + // In the worst case, int will be encoded using 5 bytes + val minCapacity = 5 + // Acquire a segment that can fit at least 5 bytes + UnsafeBufferOperations.writeToTail(this, minCapacity) { ctx, segment -> + // Count how many bytes were actually written + var bytesWritten = 0 + var remainingBits = value + do { + var b = remainingBits and 0x7fu + remainingBits = remainingBits shr 7 + if (remainingBits != 0u) { + b = 0x80u or b + } + // Append a byte to the segment + ctx.setUnchecked(segment, bytesWritten++, b.toByte()) + } while (remainingBits != 0u) + // Return how many bytes were actually written + bytesWritten + } + } + + val buffer = Buffer() + buffer.writeULEB128(624485u) + assertEquals(ByteString(0xe5.toByte(), 0x8e.toByte(), 0x26), buffer.readByteString()) + } + + @OptIn(UnsafeIoApi::class) + private fun Buffer.writeULEB128(value: UInt) { + // update buffer's state after writing all bytes + + val minCapacity = 5 // in the worst case, int will be encoded using 5 bytes + UnsafeBufferOperations.writeToTail(this, minCapacity) { ctx, seg -> + var bytesWritten = 0 + var remainingBits = value + do { + var b = remainingBits and 0x7fu + remainingBits = remainingBits shr 7 + if (remainingBits != 0u) { + b = 0x80u or b + } + ctx.setUnchecked(seg, bytesWritten++, b.toByte()) + } while (remainingBits != 0u) + // return how many bytes were actually written + bytesWritten + } + } + + @OptIn(ExperimentalUnsignedTypes::class, UnsafeIoApi::class) + @Test + fun writeUleb128Array() { + // Encode multiple unsigned integers using unsigned LEB128 format: + // https://en.wikipedia.org/wiki/LEB128#Unsigned_LEB128 + fun Buffer.writeULEB128(data: UIntArray) { + // Encode array length + writeULEB128(data.size.toUInt()) + + var index = 0 + while (index < data.size) { + val value = data[index++] + // optimize small values encoding: anything below 127 will be encoded using a single byte anyway + if (value < 0x80u) { + // we need a space for a single byte, but if there's more - we'll try to fill it + UnsafeBufferOperations.writeToTail(this, 1) { ctx, seg -> + var bytesWritten = 0 + ctx.setUnchecked(seg, bytesWritten++, value.toByte()) + + // let's save as much succeeding small values as possible + val remainingDataLength = data.size - index + val remainingCapacity = seg.remainingCapacity - 1 + for (i in 0 until min(remainingDataLength, remainingCapacity)) { + val b = data[index] + if (b >= 0x80u) break + ctx.setUnchecked(seg, bytesWritten++, b.toByte()) + index++ + } + bytesWritten + } + } else { + writeULEB128(value) + } + } + } + + val buffer = Buffer() + val data = UIntArray(10) { it.toUInt() } + buffer.writeULEB128(data) + assertEquals(ByteString(10, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9), buffer.readByteString()) + } + + @Test + @OptIn(ExperimentalUnsignedTypes::class) + fun crc32Unsafe() { + fun generateCrc32Table(): UIntArray { + val table = UIntArray(256) + for (idx in table.indices) { + table[idx] = idx.toUInt() + for (bit in 8 downTo 1) { + table[idx] = if (table[idx] % 2U == 0U) { + table[idx].shr(1) + } else { + table[idx].shr(1).xor(0xEDB88320U) + } + } + } + return table + } + + val crc32Table = generateCrc32Table() + + // Compute a CRC32 checksum over Buffer's content without consuming it + @OptIn(UnsafeIoApi::class) + fun Buffer.crc32(): UInt { + var crc32 = 0xffffffffU + // Iterate over segments starting from buffer's head + UnsafeBufferOperations.iterate(this) { ctx, head -> + var currentSegment = head + // If a current segment is null, it means we ran out of segments. + while (currentSegment != null) { + // Get data from a segment + ctx.withData(currentSegment) { data, startIndex, endIndex -> + for (idx in startIndex.. + var currentSegment = head + while (currentSegment != null) { + // Get data from a segment + for (offset in 0.. + assertNull(head) + } + } + + @Test + fun singleSegment() { + UnsafeBufferOperations.iterate(Buffer().also { it.writeByte(1) }) { ctx, head -> + assertNotNull(head) + assertEquals(1, head.size) + + assertNull(ctx.next(head)) + } + } + + @Test + fun multipleSegments() { + val buffer = Buffer() + + val expectedSegments = 10 + for (i in 0 ..< expectedSegments) { + UnsafeBufferOperations.moveToTail(buffer, byteArrayOf(i.toByte())) + } + + val storedBytes = ByteArray(expectedSegments) + + UnsafeBufferOperations.iterate(buffer) { ctx, head -> + var idx = 0 + var seg = head + + do { + seg!! + + assertTrue(idx < expectedSegments) + storedBytes[idx++] = ctx.getUnchecked(seg, 0) + seg = ctx.next(seg) + } while (seg != null) + } + + assertArrayEquals(ByteArray(expectedSegments) { it.toByte() }, storedBytes) + } + + @Test + fun acquireDataDuringIteration() { + val buffer = Buffer().also { it.writeString("hello buffer") } + + val expectedSize = buffer.size + + UnsafeBufferOperations.iterate(buffer) { ctx, head -> + assertNotNull(head) + + ctx.withData(head) { data, startIndex, endIndex -> + assertEquals("hello buffer", data.decodeToString(startIndex, endIndex)) + } + } + + assertEquals(expectedSize, buffer.size) + } + + @Test + fun seekStartOffsets() { + val firstSegmentData = "hello buffer".encodeToByteArray() + val buffer = Buffer().also { it.write(firstSegmentData) } + + UnsafeBufferOperations.iterate(buffer, offset = 6) { _, segment, offset -> + assertNotNull(segment) + assertEquals(0, offset) + } + + val secondSegmentData = "; that's a second segment".encodeToByteArray() + UnsafeBufferOperations.moveToTail(buffer, secondSegmentData) + val thirdSegmentData = "; that's a third segment".encodeToByteArray() + UnsafeBufferOperations.moveToTail(buffer, thirdSegmentData) + + val startOfSegmentOffsets = longArrayOf( + 0, + firstSegmentData.size.toLong(), + (firstSegmentData.size + secondSegmentData.size).toLong(), + (firstSegmentData.size + secondSegmentData.size + thirdSegmentData.size).toLong() + ) + + val segmentsData = arrayOf(firstSegmentData, secondSegmentData, thirdSegmentData) + + for (limitIdx in 1 ..< startOfSegmentOffsets.size) { + val startOffset = startOfSegmentOffsets[limitIdx - 1] + val limitOffset = startOfSegmentOffsets[limitIdx] + + for (offset in startOffset ..< limitOffset) { + UnsafeBufferOperations.iterate(buffer, offset) { ctx, seg, o -> + assertNotNull(seg) + assertEquals(startOffset, o) + + ctx.withData(seg) { data, startIndex, endIndex -> + val slice = data.sliceArray(startIndex ..< endIndex) + assertArrayEquals(segmentsData[limitIdx - 1], slice) + } + } + } + } + } + + @Test + fun seekOutOfBounds() { + assertFailsWith { + UnsafeBufferOperations.iterate(Buffer(), offset = -1) { _, _, _ -> fail() } + } + assertFailsWith { + UnsafeBufferOperations.iterate(Buffer(), offset = 0) { _, _, _ -> fail() } + } + assertFailsWith { + UnsafeBufferOperations.iterate(Buffer(), offset = 9001) { _, _, _ -> fail() } + } + + val buffer = Buffer().also { it.writeInt(42) } + assertFailsWith { + UnsafeBufferOperations.iterate(buffer, offset = -1) { _, _, _ -> fail() } + } + assertFailsWith { + UnsafeBufferOperations.iterate(buffer, offset = 4) { _, _, _ -> fail() } + } + } +} diff --git a/core/common/test/unsafe/UnsafeBufferOperationsReadTest.kt b/core/common/test/unsafe/UnsafeBufferOperationsReadTest.kt index fb3a938e1..64c941c6d 100644 --- a/core/common/test/unsafe/UnsafeBufferOperationsReadTest.kt +++ b/core/common/test/unsafe/UnsafeBufferOperationsReadTest.kt @@ -53,6 +53,10 @@ class UnsafeBufferOperationsReadTest { val buffer = Buffer().apply { writeInt(42) } UnsafeBufferOperations.readFromHead(buffer) { _, _, _ -> 0 } assertEquals(42, buffer.readInt()) + + buffer.writeInt(42) + UnsafeBufferOperations.readFromHead(buffer) { _, _ -> 0 } + assertEquals(42, buffer.readInt()) } @Test @@ -62,6 +66,10 @@ class UnsafeBufferOperationsReadTest { endIndex - startIndex } assertTrue(buffer.exhausted()) + + buffer.writeString("hello world") + UnsafeBufferOperations.readFromHead(buffer) { _, seg -> seg.size } + assertTrue(buffer.exhausted()) } @Test @@ -70,6 +78,10 @@ class UnsafeBufferOperationsReadTest { assertFailsWith { UnsafeBufferOperations.readFromHead(buffer) { _, _, _ -> 0 } } + + assertFailsWith { + UnsafeBufferOperations.readFromHead(buffer) { _, _ -> 0 } + } } @Test @@ -91,6 +103,24 @@ class UnsafeBufferOperationsReadTest { assertEquals(extraBytesCount, buffer.size.toInt()) } + @Test + fun readFromTheSegmentEndUsingCtx() { + val segmentSize = UnsafeBufferOperations.maxSafeWriteCapacity + val extraBytesCount = 128 + val bytesToSkip = segmentSize - 2 + + val buffer = Buffer().apply { write(ByteArray(segmentSize + extraBytesCount) { 0xff.toByte() }) } + buffer.skip(bytesToSkip.toLong()) + val head = buffer.head!! + assertEquals(bytesToSkip, head.pos) + + UnsafeBufferOperations.readFromHead(buffer) { _, seg -> + assertEquals(2, seg.size) + 2 + } + + assertEquals(extraBytesCount, buffer.size.toInt()) + } @Test fun returnIllegalReadCount() { @@ -101,10 +131,20 @@ class UnsafeBufferOperationsReadTest { } assertEquals(4L, buffer.size) + assertFailsWith { + UnsafeBufferOperations.readFromHead(buffer) { _, _ -> -1 } + } + assertEquals(4L, buffer.size) + assertFailsWith { UnsafeBufferOperations.readFromHead(buffer) { _, f, t -> (t - f + 1) } } assertEquals(4L, buffer.size) + + assertFailsWith { + UnsafeBufferOperations.readFromHead(buffer) { _, seg -> seg.remainingCapacity + 1 } + } + assertEquals(4L, buffer.size) } @Test @@ -118,5 +158,12 @@ class UnsafeBufferOperationsReadTest { } } assertEquals(buffer.size, sizeBeforeRead) + + assertFailsWith { + UnsafeBufferOperations.readFromHead(buffer) { _, _ -> + throw TestException() + } + } + assertEquals(buffer.size, sizeBeforeRead) } } diff --git a/core/common/test/unsafe/UnsafeBufferOperationsWriteTest.kt b/core/common/test/unsafe/UnsafeBufferOperationsWriteTest.kt index a5c1b6a3d..164c312d2 100644 --- a/core/common/test/unsafe/UnsafeBufferOperationsWriteTest.kt +++ b/core/common/test/unsafe/UnsafeBufferOperationsWriteTest.kt @@ -24,6 +24,12 @@ class UnsafeBufferOperationsWriteTest { assertEquals(buffer.head!!.data.size, endIndex) 0 } + + UnsafeBufferOperations.writeToTail(buffer, 1) { _, segment -> + // Unsafe check, head is not committed yet + assertSame(buffer.head, segment) + 0 + } } @Test @@ -48,20 +54,29 @@ class UnsafeBufferOperationsWriteTest { UnsafeBufferOperations.writeToTail(buffer, 1) { _, _, _ -> 0 } assertTrue(buffer.exhausted()) + UnsafeBufferOperations.writeToTail(buffer, 1) { _, _ -> 0 } + assertTrue(buffer.exhausted()) + buffer.writeInt(42) UnsafeBufferOperations.writeToTail(buffer, 1) { _, _, _ -> 0 } assertEquals(4, buffer.size) + UnsafeBufferOperations.writeToTail(buffer, 1) { _, _ -> 0 } + assertEquals(4, buffer.size) + buffer.write(ByteArray(Segment.SIZE - 4)) UnsafeBufferOperations.writeToTail(buffer, 1) { _, _, _ -> 0 } assertEquals(Segment.SIZE.toLong(), buffer.size) + + UnsafeBufferOperations.writeToTail(buffer, 1) { _, _ -> 0 } + assertEquals(Segment.SIZE.toLong(), buffer.size) } @Test fun writeWholeBuffer() { val buffer = Buffer() UnsafeBufferOperations.writeToTail(buffer, 1) { data, from, to -> - for (idx in from ..< to) { + for (idx in from.. + ctx.setUnchecked(segment, 0, 1) + ctx.setUnchecked(segment, 1, 2) + 2 + } + + assertArrayEquals(byteArrayOf(1, 2), buffer.readByteArray()) + } + @Test fun requireToManyBytes() { val buffer = Buffer() @@ -77,6 +105,11 @@ class UnsafeBufferOperationsWriteTest { UnsafeBufferOperations.writeToTail(buffer, 100500) { _, _, _ -> 0 } } assertTrue(buffer.exhausted()) + + assertFailsWith { + UnsafeBufferOperations.writeToTail(buffer, 100500) { _, _ -> 0 } + } + assertTrue(buffer.exhausted()) } @Test @@ -98,6 +131,25 @@ class UnsafeBufferOperationsWriteTest { assertArrayEquals(byteArrayOf(42, 43), buffer.readByteArray()) } + @Test + fun writeToTheEndOfABufferUsingCtx() { + val buffer = Buffer().apply { write(ByteArray(Segment.SIZE - 1)) } + UnsafeBufferOperations.writeToTail(buffer, 1) { ctx, seg -> + assertEquals(1, seg.remainingCapacity) + ctx.setUnchecked(seg, 0, 42) + 1 + } + assertEquals(Segment.SIZE, buffer.size.toInt()) + UnsafeBufferOperations.writeToTail(buffer, 1) { ctx, seg -> + ctx.setUnchecked(seg, 0, 43) + 1 + } + assertEquals(Segment.SIZE + 1, buffer.size.toInt()) + + buffer.skip(Segment.SIZE - 1L) + assertArrayEquals(byteArrayOf(42, 43), buffer.readByteArray()) + } + @Test fun returnIllegalWriteCount() { val buffer = Buffer() @@ -116,6 +168,31 @@ class UnsafeBufferOperationsWriteTest { assertTrue(buffer.exhausted()) } + @Test + fun writeMultipleBytes() { + val buffer = Buffer() + + UnsafeBufferOperations.writeToTail(buffer, 10) { ctx, tail -> + ctx.setUnchecked(tail, 0, 1) + ctx.setUnchecked(tail, 1, 2, 3) + ctx.setUnchecked(tail, 3, 4, 5, 6) + ctx.setUnchecked(tail, 6, 7, 8, 9, 10) + 10 + } + + assertArrayEquals(ByteArray(10) { (it + 1).toByte() }, buffer.readByteArray()) + + // check overlapping writes + UnsafeBufferOperations.writeToTail(buffer, 4) { ctx, tail -> + ctx.setUnchecked(tail, 0, 11, 11, 11, 11) + ctx.setUnchecked(tail, 1, 10, 10, 10) + ctx.setUnchecked(tail, 2, 9, 9) + ctx.setUnchecked(tail, 3, 8) + 4 + } + assertArrayEquals(byteArrayOf(11, 10, 9, 8), buffer.readByteArray()) + } + @Test fun resetWriteOnException() { val buffer = Buffer() @@ -127,6 +204,14 @@ class UnsafeBufferOperationsWriteTest { } assertTrue(buffer.exhausted()) + + assertFailsWith { + UnsafeBufferOperations.writeToTail(buffer, 2) { _, _ -> + throw TestException() + } + } + + assertTrue(buffer.exhausted()) } @Test diff --git a/core/jvm/test/samples/unsafeAccessSamplesJvm.kt b/core/jvm/test/samples/unsafeAccessSamplesJvm.kt index 053801f7e..8255df72d 100644 --- a/core/jvm/test/samples/unsafeAccessSamplesJvm.kt +++ b/core/jvm/test/samples/unsafeAccessSamplesJvm.kt @@ -7,16 +7,18 @@ package kotlinx.io.samples.unsafe import kotlinx.io.Buffer import kotlinx.io.UnsafeIoApi +import kotlinx.io.bytestring.ByteString +import kotlinx.io.bytestring.toHexString +import kotlinx.io.bytestring.unsafe.UnsafeByteStringApi +import kotlinx.io.bytestring.unsafe.UnsafeByteStringOperations import kotlinx.io.readString -import kotlinx.io.unsafe.UnsafeBufferOperations -import kotlinx.io.unsafe.readBulk -import kotlinx.io.unsafe.readFromHead -import kotlinx.io.unsafe.writeToTail +import kotlinx.io.unsafe.* import kotlinx.io.writeString import java.nio.ByteBuffer import java.nio.channels.FileChannel import java.nio.file.Files import java.nio.file.StandardOpenOption +import java.security.MessageDigest import kotlin.random.Random import kotlin.test.Test import kotlin.test.assertEquals @@ -99,4 +101,29 @@ class UnsafeReadWriteSamplesJvm { assertEquals(64 * 1024, channel.size()) } } + + @OptIn(UnsafeByteStringApi::class, ExperimentalStdlibApi::class) + @Test + fun messageDigest() { + fun Buffer.digest(algorithm: String): ByteString { + val md = MessageDigest.getInstance(algorithm) + // iterate over all segment and update data + UnsafeBufferOperations.iterate(this) { ctx, head -> + var segment = head + // when segment is null, we reached the end of a buffer + while (segment != null) { + // access segment data without copying it + ctx.withData(segment) { data, startIndex, endIndex -> + md.update(data, startIndex, endIndex - startIndex) + } + // advance to the next segment + segment = ctx.next(segment) + } + } + return UnsafeByteStringOperations.wrapUnsafe(md.digest()) + } + + val buffer = Buffer().also { it.writeString("hello world") } + assertEquals("5eb63bbbe01eeed093cb22bb8f5acdc3", buffer.digest("MD5").toHexString()) + } }