aboutsummaryrefslogtreecommitdiff
path: root/okio/src/commonMain/kotlin/okio/FileHandle.kt
diff options
context:
space:
mode:
Diffstat (limited to 'okio/src/commonMain/kotlin/okio/FileHandle.kt')
-rw-r--r--okio/src/commonMain/kotlin/okio/FileHandle.kt443
1 files changed, 443 insertions, 0 deletions
diff --git a/okio/src/commonMain/kotlin/okio/FileHandle.kt b/okio/src/commonMain/kotlin/okio/FileHandle.kt
new file mode 100644
index 00000000..d05c3c99
--- /dev/null
+++ b/okio/src/commonMain/kotlin/okio/FileHandle.kt
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2021 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package okio
+
+/**
+ * An open file for reading and writing; using either streaming and random access.
+ *
+ * Use [read] and [write] to perform one-off random-access reads and writes. Use [source], [sink],
+ * and [appendingSink] for streaming reads and writes.
+ *
+ * File handles must be closed when they are no longer needed. It is an error to read, write, or
+ * create streams after a file handle is closed. The operating system resources held by a file
+ * handle will be released once the file handle **and** all of its streams are closed.
+ *
+ * Although this class offers both reading and writing APIs, file handle instances may be
+ * read-only or write-only. For example, a handle to a file on a read-only file system will throw an
+ * exception if a write is attempted.
+ *
+ * File handles may be used by multiple threads concurrently. But the individual sources and sinks
+ * produced by a file handle are not safe for concurrent use.
+ */
+abstract class FileHandle(
+ /**
+ * True if this handle supports both reading and writing. If this is false all write operations
+ * including [write], [sink], [resize], and [flush] will all throw [IllegalStateException] if
+ * called.
+ */
+ val readWrite: Boolean,
+) : Closeable {
+ /**
+ * True once the file handle is closed. Resources should be released with [protectedClose] once
+ * this is true and [openStreamCount] is 0.
+ */
+ private var closed = false
+
+ /**
+ * Reference count of the number of open sources and sinks on this file handle. Resources should
+ * be released with [protectedClose] once this is 0 and [closed] is true.
+ */
+ private var openStreamCount = 0
+
+ val lock: Lock = newLock()
+
+ /**
+ * Reads at least 1, and up to [byteCount] bytes from this starting at [fileOffset] and copies
+ * them to [array] at [arrayOffset]. Returns the number of bytes read, or -1 if [fileOffset]
+ * equals [size].
+ */
+ @Throws(IOException::class)
+ fun read(
+ fileOffset: Long,
+ array: ByteArray,
+ arrayOffset: Int,
+ byteCount: Int,
+ ): Int {
+ lock.withLock {
+ check(!closed) { "closed" }
+ }
+ return protectedRead(fileOffset, array, arrayOffset, byteCount)
+ }
+
+ /**
+ * Reads at least 1, and up to [byteCount] bytes from this starting at [fileOffset] and appends
+ * them to [sink]. Returns the number of bytes read, or -1 if [fileOffset] equals [size].
+ */
+ @Throws(IOException::class)
+ fun read(fileOffset: Long, sink: Buffer, byteCount: Long): Long {
+ lock.withLock {
+ check(!closed) { "closed" }
+ }
+ return readNoCloseCheck(fileOffset, sink, byteCount)
+ }
+
+ /**
+ * Returns the total number of bytes in the file. This will change if the file size changes.
+ */
+ @Throws(IOException::class)
+ fun size(): Long {
+ lock.withLock {
+ check(!closed) { "closed" }
+ }
+ return protectedSize()
+ }
+
+ /**
+ * Changes the number of bytes in this file to [size]. This will remove bytes from the end if the
+ * new size is smaller. It will add `0` bytes to the end if it is larger.
+ */
+ @Throws(IOException::class)
+ fun resize(size: Long) {
+ check(readWrite) { "file handle is read-only" }
+ lock.withLock {
+ check(!closed) { "closed" }
+ }
+ return protectedResize(size)
+ }
+
+ /** Reads [byteCount] bytes from [array] and writes them to this at [fileOffset]. */
+ fun write(
+ fileOffset: Long,
+ array: ByteArray,
+ arrayOffset: Int,
+ byteCount: Int,
+ ) {
+ check(readWrite) { "file handle is read-only" }
+ lock.withLock {
+ check(!closed) { "closed" }
+ }
+ return protectedWrite(fileOffset, array, arrayOffset, byteCount)
+ }
+
+ /** Removes [byteCount] bytes from [source] and writes them to this at [fileOffset]. */
+ @Throws(IOException::class)
+ fun write(fileOffset: Long, source: Buffer, byteCount: Long) {
+ check(readWrite) { "file handle is read-only" }
+ lock.withLock {
+ check(!closed) { "closed" }
+ }
+ writeNoCloseCheck(fileOffset, source, byteCount)
+ }
+
+ /** Pushes all buffered bytes to their final destination. */
+ @Throws(IOException::class)
+ fun flush() {
+ check(readWrite) { "file handle is read-only" }
+ lock.withLock {
+ check(!closed) { "closed" }
+ }
+ return protectedFlush()
+ }
+
+ /**
+ * Returns a source that reads from this starting at [fileOffset]. The returned source must be
+ * closed when it is no longer needed.
+ */
+ @Throws(IOException::class)
+ fun source(fileOffset: Long = 0L): Source {
+ lock.withLock {
+ check(!closed) { "closed" }
+ openStreamCount++
+ }
+ return FileHandleSource(this, fileOffset)
+ }
+
+ /**
+ * Returns the position of [source] in the file. The argument [source] must be either a source
+ * produced by this file handle, or a [BufferedSource] that directly wraps such a source. If the
+ * parameter is a [BufferedSource], it adjusts for buffered bytes.
+ */
+ @Throws(IOException::class)
+ fun position(source: Source): Long {
+ var source = source
+ var bufferSize = 0L
+
+ if (source is RealBufferedSource) {
+ bufferSize = source.buffer.size
+ source = source.source
+ }
+
+ require(source is FileHandleSource && source.fileHandle === this) {
+ "source was not created by this FileHandle"
+ }
+ check(!source.closed) { "closed" }
+
+ return source.position - bufferSize
+ }
+
+ /**
+ * Change the position of [source] in the file to [position]. The argument [source] must be either
+ * a source produced by this file handle, or a [BufferedSource] that directly wraps such a source.
+ * If the parameter is a [BufferedSource], it will skip or clear buffered bytes.
+ */
+ @Throws(IOException::class)
+ fun reposition(source: Source, position: Long) {
+ if (source is RealBufferedSource) {
+ val fileHandleSource = source.source
+ require(fileHandleSource is FileHandleSource && fileHandleSource.fileHandle === this) {
+ "source was not created by this FileHandle"
+ }
+ check(!fileHandleSource.closed) { "closed" }
+
+ val bufferSize = source.buffer.size
+ val toSkip = position - (fileHandleSource.position - bufferSize)
+ if (toSkip in 0L until bufferSize) {
+ // The new position requires only a buffer change.
+ source.skip(toSkip)
+ } else {
+ // The new position doesn't share data with the current buffer.
+ source.buffer.clear()
+ fileHandleSource.position = position
+ }
+ } else {
+ require(source is FileHandleSource && source.fileHandle === this) {
+ "source was not created by this FileHandle"
+ }
+ check(!source.closed) { "closed" }
+ source.position = position
+ }
+ }
+
+ /**
+ * Returns a sink that writes to this starting at [fileOffset]. The returned sink must be closed
+ * when it is no longer needed.
+ */
+ @Throws(IOException::class)
+ fun sink(fileOffset: Long = 0L): Sink {
+ check(readWrite) { "file handle is read-only" }
+ lock.withLock {
+ check(!closed) { "closed" }
+ openStreamCount++
+ }
+ return FileHandleSink(this, fileOffset)
+ }
+
+ /**
+ * Returns a sink that writes to this starting at the end. The returned sink must be closed when
+ * it is no longer needed.
+ */
+ @Throws(IOException::class)
+ fun appendingSink(): Sink {
+ return sink(size())
+ }
+
+ /**
+ * Returns the position of [sink] in the file. The argument [sink] must be either a sink produced
+ * by this file handle, or a [BufferedSink] that directly wraps such a sink. If the parameter is a
+ * [BufferedSink], it adjusts for buffered bytes.
+ */
+ @Throws(IOException::class)
+ fun position(sink: Sink): Long {
+ var sink = sink
+ var bufferSize = 0L
+
+ if (sink is RealBufferedSink) {
+ bufferSize = sink.buffer.size
+ sink = sink.sink
+ }
+
+ require(sink is FileHandleSink && sink.fileHandle === this) {
+ "sink was not created by this FileHandle"
+ }
+ check(!sink.closed) { "closed" }
+
+ return sink.position + bufferSize
+ }
+
+ /**
+ * Change the position of [sink] in the file to [position]. The argument [sink] must be either a
+ * sink produced by this file handle, or a [BufferedSink] that directly wraps such a sink. If the
+ * parameter is a [BufferedSink], it emits for buffered bytes.
+ */
+ @Throws(IOException::class)
+ fun reposition(sink: Sink, position: Long) {
+ if (sink is RealBufferedSink) {
+ val fileHandleSink = sink.sink
+ require(fileHandleSink is FileHandleSink && fileHandleSink.fileHandle === this) {
+ "sink was not created by this FileHandle"
+ }
+ check(!fileHandleSink.closed) { "closed" }
+
+ sink.emit()
+ fileHandleSink.position = position
+ } else {
+ require(sink is FileHandleSink && sink.fileHandle === this) {
+ "sink was not created by this FileHandle"
+ }
+ check(!sink.closed) { "closed" }
+ sink.position = position
+ }
+ }
+
+ @Throws(IOException::class)
+ final override fun close() {
+ lock.withLock {
+ if (closed) return
+ closed = true
+ if (openStreamCount != 0) return
+ }
+ protectedClose()
+ }
+
+ /** Like [read] but not performing any close check. */
+ @Throws(IOException::class)
+ protected abstract fun protectedRead(
+ fileOffset: Long,
+ array: ByteArray,
+ arrayOffset: Int,
+ byteCount: Int,
+ ): Int
+
+ /** Like [write] but not performing any close check. */
+ @Throws(IOException::class)
+ protected abstract fun protectedWrite(
+ fileOffset: Long,
+ array: ByteArray,
+ arrayOffset: Int,
+ byteCount: Int,
+ )
+
+ /** Like [flush] but not performing any close check. */
+ @Throws(IOException::class)
+ protected abstract fun protectedFlush()
+
+ /** Like [resize] but not performing any close check. */
+ @Throws(IOException::class)
+ protected abstract fun protectedResize(size: Long)
+
+ /** Like [size] but not performing any close check. */
+ @Throws(IOException::class)
+ protected abstract fun protectedSize(): Long
+
+ /**
+ * Subclasses should implement this to release resources held by this file handle. It is invoked
+ * once both the file handle is closed, and also all sources and sinks produced by it are also
+ * closed.
+ */
+ @Throws(IOException::class)
+ protected abstract fun protectedClose()
+
+ private fun readNoCloseCheck(fileOffset: Long, sink: Buffer, byteCount: Long): Long {
+ require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
+
+ var currentOffset = fileOffset
+ val targetOffset = fileOffset + byteCount
+
+ while (currentOffset < targetOffset) {
+ val tail = sink.writableSegment(1)
+ val readByteCount = protectedRead(
+ fileOffset = currentOffset,
+ array = tail.data,
+ arrayOffset = tail.limit,
+ byteCount = minOf(targetOffset - currentOffset, Segment.SIZE - tail.limit).toInt(),
+ )
+
+ if (readByteCount == -1) {
+ if (tail.pos == tail.limit) {
+ // We allocated a tail segment, but didn't end up needing it. Recycle!
+ sink.head = tail.pop()
+ SegmentPool.recycle(tail)
+ }
+ if (fileOffset == currentOffset) return -1L // We wanted bytes but didn't return any.
+ break
+ }
+
+ tail.limit += readByteCount
+ currentOffset += readByteCount
+ sink.size += readByteCount
+ }
+
+ return currentOffset - fileOffset
+ }
+
+ private fun writeNoCloseCheck(fileOffset: Long, source: Buffer, byteCount: Long) {
+ checkOffsetAndCount(source.size, 0L, byteCount)
+
+ var currentOffset = fileOffset
+ val targetOffset = fileOffset + byteCount
+
+ while (currentOffset < targetOffset) {
+ val head = source.head!!
+ val toCopy = minOf(targetOffset - currentOffset, head.limit - head.pos).toInt()
+ protectedWrite(currentOffset, head.data, head.pos, toCopy)
+
+ head.pos += toCopy
+ currentOffset += toCopy
+ source.size -= toCopy
+
+ if (head.pos == head.limit) {
+ source.head = head.pop()
+ SegmentPool.recycle(head)
+ }
+ }
+ }
+
+ private class FileHandleSink(
+ val fileHandle: FileHandle,
+ var position: Long,
+ ) : Sink {
+ var closed = false
+
+ override fun write(source: Buffer, byteCount: Long) {
+ check(!closed) { "closed" }
+ fileHandle.writeNoCloseCheck(position, source, byteCount)
+ position += byteCount
+ }
+
+ override fun flush() {
+ check(!closed) { "closed" }
+ fileHandle.protectedFlush()
+ }
+
+ override fun timeout() = Timeout.NONE
+
+ override fun close() {
+ if (closed) return
+ closed = true
+ fileHandle.lock.withLock {
+ fileHandle.openStreamCount--
+ if (fileHandle.openStreamCount != 0 || !fileHandle.closed) return@close
+ }
+ fileHandle.protectedClose()
+ }
+ }
+
+ private class FileHandleSource(
+ val fileHandle: FileHandle,
+ var position: Long,
+ ) : Source {
+ var closed = false
+
+ override fun read(sink: Buffer, byteCount: Long): Long {
+ check(!closed) { "closed" }
+ val result = fileHandle.readNoCloseCheck(position, sink, byteCount)
+ if (result != -1L) position += result
+ return result
+ }
+
+ override fun timeout() = Timeout.NONE
+
+ override fun close() {
+ if (closed) return
+ closed = true
+ fileHandle.lock.withLock {
+ fileHandle.openStreamCount--
+ if (fileHandle.openStreamCount != 0 || !fileHandle.closed) return@close
+ }
+ fileHandle.protectedClose()
+ }
+ }
+}