diff --git a/cache/src/main/java/dev/openrs2/cache/BufferedFileChannel.kt b/cache/src/main/java/dev/openrs2/cache/BufferedFileChannel.kt index c011d99c..2c41b64b 100644 --- a/cache/src/main/java/dev/openrs2/cache/BufferedFileChannel.kt +++ b/cache/src/main/java/dev/openrs2/cache/BufferedFileChannel.kt @@ -1,53 +1,300 @@ package dev.openrs2.cache import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufAllocator import java.io.Closeable import java.io.EOFException import java.io.Flushable import java.nio.channels.FileChannel +import kotlin.math.max +import kotlin.math.min -// TODO(gpe): actually implement buffering class BufferedFileChannel( - private val channel: FileChannel + private val channel: FileChannel, + readBufferSize: Int, + writeBufferSize: Int, + alloc: ByteBufAllocator = ByteBufAllocator.DEFAULT ) : Flushable, Closeable { + private var size = channel.size() + + private val readBuffer = alloc.buffer(readBufferSize, readBufferSize) + private var readPos = -1L + + private val writeBuffer = alloc.buffer(writeBufferSize, writeBufferSize) + private var writePos = -1L + fun read(pos: Long, dest: ByteBuf, len: Int) { + require(pos >= 0) require(len <= dest.writableBytes()) + val originalDestIndex = dest.writerIndex() + var off = pos var remaining = len - while (remaining > 0) { - val n = dest.writeBytes(channel, off, remaining) + /* + * Service the whole read from the write buffer, if we can. This code + * isn't necessary, but it is more optimal than following the whole + * sequence of reads below. + */ + val writeLen = writeBuffer.readableBytes() + if (writePos != -1L && off >= writePos && off + remaining <= writePos + writeLen) { + val copyOff = (off - writePos).toInt() + dest.writeBytes(writeBuffer, copyOff, remaining) + return + } + + // Service the first part of the read from the read buffer. + if (readPos != -1L && off >= readPos && off < readPos + readBuffer.readableBytes()) { + val copyOff = (off - readPos).toInt() + val copyLen = min(readBuffer.readableBytes() - copyOff, remaining) + + dest.writeBytes(readBuffer, copyOff, copyLen) + + off += copyLen + remaining -= copyLen + } + + if (remaining > readBuffer.capacity()) { + /* + * If the remaining part of the read is larger than the read + * buffer, read directly from the file into the destination buffer. + */ + while (remaining > 0) { + val n = dest.writeBytes(channel, off, remaining) + if (n == -1) { + break + } + off += n + remaining -= n + } + } else if (remaining > 0) { + /* + * Otherwise clear and repopulate the entire read buffer from the + * current position, then copy into the destination buffer. + */ + fill(off) + + val copyLen = min(readBuffer.readableBytes(), remaining) + + dest.writeBytes(readBuffer, 0, copyLen) + + off += copyLen + remaining -= copyLen + } + + if (writePos != -1L) { + /* + * If an unflushed write extended the length of the file, fill in + * the gap between the current position and the write position with + * zeroes to reflect what the filesystem would do. + */ + if (off < writePos && remaining > 0) { + val zeroLen = min((writePos - off).toInt(), remaining) + + dest.writeZero(zeroLen) + + off += zeroLen + remaining -= zeroLen + } + + /* + * If a subset of the write buffer overlaps with a subset of the + * destination buffer, overwrite that subset of the destination + * buffer with the write buffer as the write buffer must take + * precedence over the read buffer. + */ + val start = if (writePos >= pos && writePos < pos + len) { + writePos + } else if (pos >= writePos && pos < writePos + writeLen) { + pos + } else { + -1L + } + + val end = if (writePos + writeLen > pos && writePos + writeLen <= pos + len) { + writePos + writeLen + } else if (pos + len > writePos && pos + len <= writePos + writeLen) { + pos + len + } else { + -1L + } + + if (start != -1L && end != -1L && start < end) { + val destIndex = originalDestIndex + (start - pos).toInt() + + val copyOff = (start - writePos).toInt() + val copyLen = (end - start).toInt() + + dest.setBytes(destIndex, writeBuffer, copyOff, copyLen) + + /* + * If we filled in any remaining bytes in the destination + * buffer from the write buffer then adjust the indexes to take + * that into account. + */ + if (end > off) { + val n = (end - off).toInt() + + dest.writerIndex(dest.writerIndex() + n) + + off += n + remaining -= n + } + } + } + + if (remaining > 0) { + throw EOFException() + } + } + + private fun fill(pos: Long) { + require(pos >= 0) + + readBuffer.clear() + readPos = pos + + var off = pos + while (readBuffer.isWritable) { + val n = readBuffer.writeBytes(channel, off, readBuffer.writableBytes()) if (n == -1) { - throw EOFException() + break } off += n - remaining -= n } } fun write(pos: Long, src: ByteBuf, len: Int) { + require(pos >= 0) + require(len <= src.readableBytes()) + + size = max(size, pos + len.toLong()) + + var off = pos + var remaining = len + + /* + * If the start of the write doesn't overlap with the write buffer, + * flush the existing write buffer. + */ + if (writePos != -1L && (off < writePos || off > writePos + writeBuffer.readableBytes())) { + flush() + } + + /* + * If the start of the write does overlap with the write buffer + * (implicit due to the if condition and flush() call above) and the + * end of the write runs beyond the end of the write buffer, overwrite + * the relevant part of the write buffer with the start of the source + * buffer and then flush the whole write buffer. + */ + if (writePos != -1L && off + remaining > writePos + writeBuffer.capacity()) { + val copyOff = (off - writePos).toInt() + val copyLen = writeBuffer.capacity() - copyOff + + src.readBytes(writeBuffer, copyOff, copyLen) + + off += copyLen + remaining -= copyLen + + writeBuffer.writerIndex(writeBuffer.capacity()) + + flush() + } + + if (remaining > writeBuffer.capacity()) { + /* + * If the remaining part of the write is longer than the write + * buffer, write directly to the underlying file. + */ + val originalSrcIndex = src.readerIndex() + writeFully(off, src, remaining) + + /* + * If the write overlaps with the read buffer, update the relevant + * portion of the read buffer. (As we bypassed the write buffer, we + * can't rely on the write buffer taking precedence over the read + * buffer.) + */ + val readLen = readBuffer.readableBytes() + + val start = if (off >= readPos && off < readPos + readLen) { + off + } else if (readPos >= off && readPos < off + remaining) { + readPos + } else { + -1L + } + + val end = if (off + remaining > readPos && off + remaining <= readPos + readLen) { + off + remaining + } else if (readPos + readLen > off && readPos + readLen <= off + remaining) { + readPos + readLen + } else { + -1L + } + + if (start != -1L && end != -1L && start < end) { + val srcIndex = originalSrcIndex + (start - off).toInt() + + val copyOff = (start - readPos).toInt() + val copyLen = (end - start).toInt() + + src.getBytes(srcIndex, readBuffer, copyOff, copyLen) + } + } else if (remaining > 0) { + // Otherwise write to the write buffer. + if (writePos == -1L) { + writePos = off + } + + val copyOff = (off - writePos).toInt() + + src.readBytes(writeBuffer, copyOff, remaining) + + off += remaining + + // Increase write buffer length if necessary. + val newWriteLen = (off - writePos).toInt() + if (newWriteLen > writeBuffer.readableBytes()) { + writeBuffer.writerIndex(newWriteLen) + } + } + } + + private fun writeFully(pos: Long, src: ByteBuf, len: Int) { + require(pos >= 0) require(len <= src.readableBytes()) var off = pos var remaining = len while (remaining > 0) { - val n = src.readBytes(channel, off, remaining) + val n = src.readBytes(channel, off, len) off += n remaining -= n } } fun size(): Long { - return channel.size() + return size } override fun flush() { - // empty + if (writePos != -1L) { + writeFully(writePos, writeBuffer, writeBuffer.readableBytes()) + writeBuffer.clear() + writePos = -1L + } } override fun close() { + flush() + channel.close() + + readBuffer.release() + writeBuffer.release() } } diff --git a/cache/src/main/java/dev/openrs2/cache/DiskStore.kt b/cache/src/main/java/dev/openrs2/cache/DiskStore.kt index d722b4c0..df721916 100644 --- a/cache/src/main/java/dev/openrs2/cache/DiskStore.kt +++ b/cache/src/main/java/dev/openrs2/cache/DiskStore.kt @@ -109,7 +109,12 @@ class DiskStore private constructor( return index } - val newIndex = BufferedFileChannel(FileChannel.open(indexPath(root, archive), CREATE, READ, WRITE)) + val newIndex = BufferedFileChannel( + FileChannel.open(indexPath(root, archive), CREATE, READ, WRITE), + INDEX_BUFFER_SIZE, + INDEX_BUFFER_SIZE, + alloc + ) indexes[archive] = newIndex return newIndex } @@ -444,6 +449,8 @@ class DiskStore private constructor( private const val MAX_BLOCK = (1 shl 24) - 1 private val TEMP_BUFFER_SIZE = max(INDEX_ENTRY_SIZE, max(BLOCK_HEADER_SIZE, EXTENDED_BLOCK_HEADER_SIZE)) + private const val INDEX_BUFFER_SIZE = INDEX_ENTRY_SIZE * 1000 + private const val DATA_BUFFER_SIZE = BLOCK_SIZE * 10 private fun dataPath(root: Path): Path { return root.resolve("main_file_cache.dat2") @@ -454,22 +461,42 @@ class DiskStore private constructor( } fun open(root: Path, alloc: ByteBufAllocator = ByteBufAllocator.DEFAULT): Store { - val data = BufferedFileChannel(FileChannel.open(dataPath(root), READ, WRITE)) + val data = BufferedFileChannel( + FileChannel.open(dataPath(root), READ, WRITE), + DATA_BUFFER_SIZE, + DATA_BUFFER_SIZE, + alloc + ) + val archives = Array(Store.MAX_ARCHIVE + 1) { archive -> val path = indexPath(root, archive) if (Files.exists(path)) { - BufferedFileChannel(FileChannel.open(path, READ, WRITE)) + BufferedFileChannel( + FileChannel.open(path, READ, WRITE), + INDEX_BUFFER_SIZE, + INDEX_BUFFER_SIZE, + alloc + ) } else { null } } + return DiskStore(root, data, archives, alloc) } fun create(root: Path, alloc: ByteBufAllocator = ByteBufAllocator.DEFAULT): Store { Files.createDirectories(root) - val data = BufferedFileChannel(FileChannel.open(dataPath(root), CREATE, READ, WRITE)) + + val data = BufferedFileChannel( + FileChannel.open(dataPath(root), CREATE, READ, WRITE), + DATA_BUFFER_SIZE, + DATA_BUFFER_SIZE, + alloc + ) + val archives = Array(Store.MAX_ARCHIVE + 1) { null } + return DiskStore(root, data, archives, alloc) } } diff --git a/cache/src/test/java/dev/openrs2/cache/BufferedFileChannelTest.kt b/cache/src/test/java/dev/openrs2/cache/BufferedFileChannelTest.kt new file mode 100644 index 00000000..01774fd3 --- /dev/null +++ b/cache/src/test/java/dev/openrs2/cache/BufferedFileChannelTest.kt @@ -0,0 +1,559 @@ +package dev.openrs2.cache + +import com.google.common.jimfs.Configuration +import com.google.common.jimfs.Jimfs +import dev.openrs2.buffer.use +import io.netty.buffer.Unpooled +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.io.EOFException +import java.nio.channels.FileChannel +import java.nio.file.Files +import java.nio.file.StandardOpenOption.CREATE +import java.nio.file.StandardOpenOption.READ +import java.nio.file.StandardOpenOption.WRITE +import kotlin.test.assertEquals + +object BufferedFileChannelTest { + @Test + fun testEmpty() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + BufferedFileChannel(FileChannel.open(path, CREATE, READ, WRITE), 8, 8).use { channel -> + assertEquals(0, channel.size()) + } + } + } + + @Test + fun testBufferedWrite() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + BufferedFileChannel(FileChannel.open(path, CREATE, READ, WRITE), 8, 8).use { channel -> + Unpooled.wrappedBuffer("OpenRS2".toByteArray()).use { buf -> + channel.write(0, buf, buf.readableBytes()) + } + + assertEquals(7, channel.size()) + } + + Unpooled.wrappedBuffer(Files.readAllBytes(path)).use { actual -> + Unpooled.wrappedBuffer("OpenRS2".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + + @Test + fun testBufferedWriteOverlapStart() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + BufferedFileChannel(FileChannel.open(path, CREATE, READ, WRITE), 8, 8).use { channel -> + Unpooled.wrappedBuffer("OpenRS2".toByteArray()).use { buf -> + channel.write(1, buf.slice(), buf.readableBytes()) + channel.write(0, buf.slice(), buf.readableBytes()) + } + } + + Unpooled.wrappedBuffer(Files.readAllBytes(path)).use { actual -> + Unpooled.wrappedBuffer("OpenRS22".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + + @Test + fun testBufferedWriteOverlapEnd() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + BufferedFileChannel(FileChannel.open(path, CREATE, READ, WRITE), 8, 8).use { channel -> + Unpooled.wrappedBuffer("OpenRS2".toByteArray()).use { buf -> + channel.write(0, buf.slice(), buf.readableBytes()) + channel.write(1, buf.slice(), buf.readableBytes()) + } + } + + Unpooled.wrappedBuffer(Files.readAllBytes(path)).use { actual -> + Unpooled.wrappedBuffer("OOpenRS2".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + + @Test + fun testBufferedWriteAdjacent() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + BufferedFileChannel(FileChannel.open(path, CREATE, READ, WRITE), 8, 8).use { channel -> + Unpooled.wrappedBuffer("OpenRS2".toByteArray()).use { buf -> + channel.write(0, buf.slice(), buf.readableBytes()) + channel.write(7, buf.slice(), buf.readableBytes()) + } + } + + Unpooled.wrappedBuffer(Files.readAllBytes(path)).use { actual -> + Unpooled.wrappedBuffer("OpenRS2OpenRS2".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + + @Test + fun testBufferedWriteNoOverlap() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + BufferedFileChannel(FileChannel.open(path, CREATE, READ, WRITE), 8, 8).use { channel -> + Unpooled.wrappedBuffer("OpenRS2".toByteArray()).use { buf -> + channel.write(0, buf.slice(), buf.readableBytes()) + channel.write(8, buf.slice(), buf.readableBytes()) + } + } + + Unpooled.wrappedBuffer(Files.readAllBytes(path)).use { actual -> + Unpooled.wrappedBuffer("OpenRS2\u0000OpenRS2".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + + @Test + fun testUnbufferedWrite() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + BufferedFileChannel(FileChannel.open(path, CREATE, READ, WRITE), 8, 8).use { channel -> + Unpooled.wrappedBuffer("Hello, world!".toByteArray()).use { buf -> + channel.write(0, buf, buf.readableBytes()) + } + + assertEquals(13, channel.size()) + } + + Unpooled.wrappedBuffer(Files.readAllBytes(path)).use { actual -> + Unpooled.wrappedBuffer("Hello, world!".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + + @Test + fun testBufferedRead() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + Files.write(path, "OpenRS2OpenRS2".toByteArray()) + + BufferedFileChannel(FileChannel.open(path, READ), 8, 8).use { channel -> + Unpooled.buffer(7, 7).use { actual -> + Unpooled.wrappedBuffer("OpenRS2".toByteArray()).use { expected -> + channel.read(0, actual, actual.writableBytes()) + assertEquals(expected, actual) + + actual.clear() + + channel.read(7, actual, actual.writableBytes()) + assertEquals(expected, actual) + } + } + } + } + } + + @Test + fun testUnbufferedRead() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + Files.write(path, "OpenRS2OpenRS2".toByteArray()) + + BufferedFileChannel(FileChannel.open(path, READ), 8, 8).use { channel -> + Unpooled.buffer(14, 14).use { actual -> + Unpooled.wrappedBuffer("OpenRS2OpenRS2".toByteArray()).use { expected -> + channel.read(0, actual, actual.writableBytes()) + assertEquals(expected, actual) + } + } + } + } + } + + @Test + fun testBufferedEof() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + BufferedFileChannel(FileChannel.open(path, CREATE, READ, WRITE), 8, 8).use { channel -> + Unpooled.buffer(1, 1).use { buf -> + assertThrows { + channel.read(0, buf, buf.writableBytes()) + } + } + } + } + } + + @Test + fun testUnbufferedEof() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + Files.write(path, "OpenRS2OpenRS2".toByteArray()) + + BufferedFileChannel(FileChannel.open(path, READ), 8, 8).use { channel -> + Unpooled.buffer(15, 15).use { buf -> + assertThrows { + channel.read(0, buf, buf.writableBytes()) + } + } + } + } + } + + @Test + fun testZeroExtension() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + BufferedFileChannel(FileChannel.open(path, CREATE, READ, WRITE), 8, 8).use { channel -> + Unpooled.wrappedBuffer(byteArrayOf(1)).use { buf -> + channel.write(7, buf, buf.readableBytes()) + } + + Unpooled.buffer(8, 8).use { actual -> + channel.read(0, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer(byteArrayOf(0, 0, 0, 0, 0, 0, 0, 1)).use { expected -> + assertEquals(expected, actual) + } + } + } + } + } + + @Test + fun testBufferedWriteThenRead() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + BufferedFileChannel(FileChannel.open(path, CREATE, READ, WRITE), 8, 8).use { channel -> + Unpooled.wrappedBuffer("OpenRS2".toByteArray()).use { buf -> + channel.write(0, buf, buf.readableBytes()) + } + + Unpooled.buffer(7, 7).use { actual -> + channel.read(0, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("OpenRS2".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + } + + @Test + fun testBufferedWriteThenReadSubsetStart() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + BufferedFileChannel(FileChannel.open(path, CREATE, READ, WRITE), 8, 8).use { channel -> + Unpooled.wrappedBuffer("OpenRS2".toByteArray()).use { buf -> + channel.write(0, buf, buf.readableBytes()) + } + + Unpooled.buffer(6, 6).use { actual -> + channel.read(0, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("OpenRS".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + } + + @Test + fun testBufferedWriteThenReadSubsetEnd() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + BufferedFileChannel(FileChannel.open(path, CREATE, READ, WRITE), 8, 8).use { channel -> + Unpooled.wrappedBuffer("OpenRS2".toByteArray()).use { buf -> + channel.write(0, buf, buf.readableBytes()) + } + + Unpooled.buffer(6, 6).use { actual -> + channel.read(1, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("penRS2".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + } + + @Test + fun testBufferedWriteThenReadSuperset() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + Files.write(path, "OpenRS2OpenRS2".toByteArray()) + + BufferedFileChannel(FileChannel.open(path, CREATE, READ, WRITE), 8, 8).use { channel -> + Unpooled.wrappedBuffer("Hello".toByteArray()).use { buf -> + channel.write(4, buf, buf.readableBytes()) + } + + Unpooled.buffer(14, 14).use { actual -> + channel.read(0, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("OpenHelloenRS2".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + } + + @Test + fun testBufferedWriteThenReadSupersetStart() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + Files.write(path, "OpenRS2OpenRS2".toByteArray()) + + BufferedFileChannel(FileChannel.open(path, CREATE, READ, WRITE), 8, 8).use { channel -> + Unpooled.wrappedBuffer("Hello".toByteArray()).use { buf -> + channel.write(4, buf, buf.readableBytes()) + } + + Unpooled.buffer(7, 7).use { actual -> + channel.read(0, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("OpenHel".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + } + + @Test + fun testBufferedWriteThenReadSupersetEnd() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + Files.write(path, "OpenRS2OpenRS2".toByteArray()) + + BufferedFileChannel(FileChannel.open(path, CREATE, READ, WRITE), 8, 8).use { channel -> + Unpooled.wrappedBuffer("Hello".toByteArray()).use { buf -> + channel.write(4, buf, buf.readableBytes()) + } + + Unpooled.buffer(7, 7).use { actual -> + channel.read(7, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("loenRS2".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + } + + @Test + fun testBufferedWriteThenReadNoOverlap() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + Files.write(path, "OpenRS2OpenRS2".toByteArray()) + + BufferedFileChannel(FileChannel.open(path, READ, WRITE), 4, 8).use { channel -> + Unpooled.wrappedBuffer("Hello".toByteArray()).use { buf -> + channel.write(4, buf, buf.readableBytes()) + } + + Unpooled.buffer(4, 4).use { actual -> + channel.read(0, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("Open".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + + Unpooled.buffer(5, 5).use { actual -> + channel.read(9, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("enRS2".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + } + + @Test + fun testBufferedReadThenWrite() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + Files.write(path, "OpenRS2OpenRS2".toByteArray()) + + BufferedFileChannel(FileChannel.open(path, READ, WRITE), 4, 0).use { channel -> + Unpooled.buffer(4, 4).use { actual -> + channel.read(4, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("RS2O".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + + Unpooled.wrappedBuffer("ABCD".toByteArray()).use { buf -> + channel.write(4, buf.slice(), buf.readableBytes()) + } + + Unpooled.buffer(4, 4).use { actual -> + channel.read(4, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("ABCD".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + } + + @Test + fun testBufferedReadThenWriteSubsetStart() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + Files.write(path, "OpenRS2OpenRS2".toByteArray()) + + BufferedFileChannel(FileChannel.open(path, READ, WRITE), 4, 0).use { channel -> + Unpooled.buffer(4, 4).use { actual -> + channel.read(4, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("RS2O".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + + Unpooled.wrappedBuffer("ABC".toByteArray()).use { buf -> + channel.write(4, buf.slice(), buf.readableBytes()) + } + + Unpooled.buffer(4, 4).use { actual -> + channel.read(4, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("ABCO".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + } + + @Test + fun testBufferedReadThenWriteSubsetEnd() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + Files.write(path, "OpenRS2OpenRS2".toByteArray()) + + BufferedFileChannel(FileChannel.open(path, READ, WRITE), 4, 0).use { channel -> + Unpooled.buffer(4, 4).use { actual -> + channel.read(4, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("RS2O".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + + Unpooled.wrappedBuffer("BCD".toByteArray()).use { buf -> + channel.write(5, buf.slice(), buf.readableBytes()) + } + + Unpooled.buffer(4, 4).use { actual -> + channel.read(4, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("RBCD".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + } + + @Test + fun testBufferedReadThenWriteSupersetStart() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + Files.write(path, "OpenRS2OpenRS2".toByteArray()) + + BufferedFileChannel(FileChannel.open(path, READ, WRITE), 4, 0).use { channel -> + Unpooled.buffer(4, 4).use { actual -> + channel.read(4, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("RS2O".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + + Unpooled.wrappedBuffer("ZABCD".toByteArray()).use { buf -> + channel.write(3, buf.slice(), buf.readableBytes()) + } + + Unpooled.buffer(4, 4).use { actual -> + channel.read(4, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("ABCD".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + } + + @Test + fun testBufferedReadThenWriteSupersetEnd() { + Jimfs.newFileSystem(Configuration.unix()).use { fs -> + val path = fs.getPath("/test.dat") + + Files.write(path, "OpenRS2OpenRS2".toByteArray()) + + BufferedFileChannel(FileChannel.open(path, READ, WRITE), 4, 0).use { channel -> + Unpooled.buffer(4, 4).use { actual -> + channel.read(4, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("RS2O".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + + Unpooled.wrappedBuffer("ABCDZ".toByteArray()).use { buf -> + channel.write(4, buf.slice(), buf.readableBytes()) + } + + Unpooled.buffer(4, 4).use { actual -> + channel.read(4, actual, actual.writableBytes()) + + Unpooled.wrappedBuffer("ABCD".toByteArray()).use { expected -> + assertEquals(expected, actual) + } + } + } + } + } +}