xref: /aosp_15_r20/external/okio/okio/src/commonMain/kotlin/okio/FileHandle.kt (revision f9742813c14b702d71392179818a9e591da8620c)
1 /*
2  * Copyright (C) 2021 Square, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package okio
17 
18 /**
19  * An open file for reading and writing; using either streaming and random access.
20  *
21  * Use [read] and [write] to perform one-off random-access reads and writes. Use [source], [sink],
22  * and [appendingSink] for streaming reads and writes.
23  *
24  * File handles must be closed when they are no longer needed. It is an error to read, write, or
25  * create streams after a file handle is closed. The operating system resources held by a file
26  * handle will be released once the file handle **and** all of its streams are closed.
27  *
28  * Although this class offers both reading and writing APIs, file handle instances may be
29  * read-only or write-only. For example, a handle to a file on a read-only file system will throw an
30  * exception if a write is attempted.
31  *
32  * File handles may be used by multiple threads concurrently. But the individual sources and sinks
33  * produced by a file handle are not safe for concurrent use.
34  */
35 abstract class FileHandle(
36   /**
37    * True if this handle supports both reading and writing. If this is false all write operations
38    * including [write], [sink], [resize], and [flush] will all throw [IllegalStateException] if
39    * called.
40    */
41   val readWrite: Boolean,
42 ) : Closeable {
43   /**
44    * True once the file handle is closed. Resources should be released with [protectedClose] once
45    * this is true and [openStreamCount] is 0.
46    */
47   private var closed = false
48 
49   /**
50    * Reference count of the number of open sources and sinks on this file handle. Resources should
51    * be released with [protectedClose] once this is 0 and [closed] is true.
52    */
53   private var openStreamCount = 0
54 
55   val lock: Lock = newLock()
56 
57   /**
58    * Reads at least 1, and up to [byteCount] bytes from this starting at [fileOffset] and copies
59    * them to [array] at [arrayOffset]. Returns the number of bytes read, or -1 if [fileOffset]
60    * equals [size].
61    */
62   @Throws(IOException::class)
readnull63   fun read(
64     fileOffset: Long,
65     array: ByteArray,
66     arrayOffset: Int,
67     byteCount: Int,
68   ): Int {
69     lock.withLock {
70       check(!closed) { "closed" }
71     }
72     return protectedRead(fileOffset, array, arrayOffset, byteCount)
73   }
74 
75   /**
76    * Reads at least 1, and up to [byteCount] bytes from this starting at [fileOffset] and appends
77    * them to [sink]. Returns the number of bytes read, or -1 if [fileOffset] equals [size].
78    */
79   @Throws(IOException::class)
readnull80   fun read(fileOffset: Long, sink: Buffer, byteCount: Long): Long {
81     lock.withLock {
82       check(!closed) { "closed" }
83     }
84     return readNoCloseCheck(fileOffset, sink, byteCount)
85   }
86 
87   /**
88    * Returns the total number of bytes in the file. This will change if the file size changes.
89    */
90   @Throws(IOException::class)
sizenull91   fun size(): Long {
92     lock.withLock {
93       check(!closed) { "closed" }
94     }
95     return protectedSize()
96   }
97 
98   /**
99    * Changes the number of bytes in this file to [size]. This will remove bytes from the end if the
100    * new size is smaller. It will add `0` bytes to the end if it is larger.
101    */
102   @Throws(IOException::class)
resizenull103   fun resize(size: Long) {
104     check(readWrite) { "file handle is read-only" }
105     lock.withLock {
106       check(!closed) { "closed" }
107     }
108     return protectedResize(size)
109   }
110 
111   /** Reads [byteCount] bytes from [array] and writes them to this at [fileOffset]. */
writenull112   fun write(
113     fileOffset: Long,
114     array: ByteArray,
115     arrayOffset: Int,
116     byteCount: Int,
117   ) {
118     check(readWrite) { "file handle is read-only" }
119     lock.withLock {
120       check(!closed) { "closed" }
121     }
122     return protectedWrite(fileOffset, array, arrayOffset, byteCount)
123   }
124 
125   /** Removes [byteCount] bytes from [source] and writes them to this at [fileOffset]. */
126   @Throws(IOException::class)
writenull127   fun write(fileOffset: Long, source: Buffer, byteCount: Long) {
128     check(readWrite) { "file handle is read-only" }
129     lock.withLock {
130       check(!closed) { "closed" }
131     }
132     writeNoCloseCheck(fileOffset, source, byteCount)
133   }
134 
135   /** Pushes all buffered bytes to their final destination. */
136   @Throws(IOException::class)
flushnull137   fun flush() {
138     check(readWrite) { "file handle is read-only" }
139     lock.withLock {
140       check(!closed) { "closed" }
141     }
142     return protectedFlush()
143   }
144 
145   /**
146    * Returns a source that reads from this starting at [fileOffset]. The returned source must be
147    * closed when it is no longer needed.
148    */
149   @Throws(IOException::class)
sourcenull150   fun source(fileOffset: Long = 0L): Source {
151     lock.withLock {
152       check(!closed) { "closed" }
153       openStreamCount++
154     }
155     return FileHandleSource(this, fileOffset)
156   }
157 
158   /**
159    * Returns the position of [source] in the file. The argument [source] must be either a source
160    * produced by this file handle, or a [BufferedSource] that directly wraps such a source. If the
161    * parameter is a [BufferedSource], it adjusts for buffered bytes.
162    */
163   @Throws(IOException::class)
positionnull164   fun position(source: Source): Long {
165     var source = source
166     var bufferSize = 0L
167 
168     if (source is RealBufferedSource) {
169       bufferSize = source.buffer.size
170       source = source.source
171     }
172 
173     require(source is FileHandleSource && source.fileHandle === this) {
174       "source was not created by this FileHandle"
175     }
176     check(!source.closed) { "closed" }
177 
178     return source.position - bufferSize
179   }
180 
181   /**
182    * Change the position of [source] in the file to [position]. The argument [source] must be either
183    * a source produced by this file handle, or a [BufferedSource] that directly wraps such a source.
184    * If the parameter is a [BufferedSource], it will skip or clear buffered bytes.
185    */
186   @Throws(IOException::class)
repositionnull187   fun reposition(source: Source, position: Long) {
188     if (source is RealBufferedSource) {
189       val fileHandleSource = source.source
190       require(fileHandleSource is FileHandleSource && fileHandleSource.fileHandle === this) {
191         "source was not created by this FileHandle"
192       }
193       check(!fileHandleSource.closed) { "closed" }
194 
195       val bufferSize = source.buffer.size
196       val toSkip = position - (fileHandleSource.position - bufferSize)
197       if (toSkip in 0L until bufferSize) {
198         // The new position requires only a buffer change.
199         source.skip(toSkip)
200       } else {
201         // The new position doesn't share data with the current buffer.
202         source.buffer.clear()
203         fileHandleSource.position = position
204       }
205     } else {
206       require(source is FileHandleSource && source.fileHandle === this) {
207         "source was not created by this FileHandle"
208       }
209       check(!source.closed) { "closed" }
210       source.position = position
211     }
212   }
213 
214   /**
215    * Returns a sink that writes to this starting at [fileOffset]. The returned sink must be closed
216    * when it is no longer needed.
217    */
218   @Throws(IOException::class)
sinknull219   fun sink(fileOffset: Long = 0L): Sink {
220     check(readWrite) { "file handle is read-only" }
221     lock.withLock {
222       check(!closed) { "closed" }
223       openStreamCount++
224     }
225     return FileHandleSink(this, fileOffset)
226   }
227 
228   /**
229    * Returns a sink that writes to this starting at the end. The returned sink must be closed when
230    * it is no longer needed.
231    */
232   @Throws(IOException::class)
appendingSinknull233   fun appendingSink(): Sink {
234     return sink(size())
235   }
236 
237   /**
238    * Returns the position of [sink] in the file. The argument [sink] must be either a sink produced
239    * by this file handle, or a [BufferedSink] that directly wraps such a sink. If the parameter is a
240    * [BufferedSink], it adjusts for buffered bytes.
241    */
242   @Throws(IOException::class)
positionnull243   fun position(sink: Sink): Long {
244     var sink = sink
245     var bufferSize = 0L
246 
247     if (sink is RealBufferedSink) {
248       bufferSize = sink.buffer.size
249       sink = sink.sink
250     }
251 
252     require(sink is FileHandleSink && sink.fileHandle === this) {
253       "sink was not created by this FileHandle"
254     }
255     check(!sink.closed) { "closed" }
256 
257     return sink.position + bufferSize
258   }
259 
260   /**
261    * Change the position of [sink] in the file to [position]. The argument [sink] must be either a
262    * sink produced by this file handle, or a [BufferedSink] that directly wraps such a sink. If the
263    * parameter is a [BufferedSink], it emits for buffered bytes.
264    */
265   @Throws(IOException::class)
repositionnull266   fun reposition(sink: Sink, position: Long) {
267     if (sink is RealBufferedSink) {
268       val fileHandleSink = sink.sink
269       require(fileHandleSink is FileHandleSink && fileHandleSink.fileHandle === this) {
270         "sink was not created by this FileHandle"
271       }
272       check(!fileHandleSink.closed) { "closed" }
273 
274       sink.emit()
275       fileHandleSink.position = position
276     } else {
277       require(sink is FileHandleSink && sink.fileHandle === this) {
278         "sink was not created by this FileHandle"
279       }
280       check(!sink.closed) { "closed" }
281       sink.position = position
282     }
283   }
284 
285   @Throws(IOException::class)
closenull286   final override fun close() {
287     lock.withLock {
288       if (closed) return
289       closed = true
290       if (openStreamCount != 0) return
291     }
292     protectedClose()
293   }
294 
295   /** Like [read] but not performing any close check. */
296   @Throws(IOException::class)
protectedReadnull297   protected abstract fun protectedRead(
298     fileOffset: Long,
299     array: ByteArray,
300     arrayOffset: Int,
301     byteCount: Int,
302   ): Int
303 
304   /** Like [write] but not performing any close check. */
305   @Throws(IOException::class)
306   protected abstract fun protectedWrite(
307     fileOffset: Long,
308     array: ByteArray,
309     arrayOffset: Int,
310     byteCount: Int,
311   )
312 
313   /** Like [flush] but not performing any close check. */
314   @Throws(IOException::class)
315   protected abstract fun protectedFlush()
316 
317   /** Like [resize] but not performing any close check. */
318   @Throws(IOException::class)
319   protected abstract fun protectedResize(size: Long)
320 
321   /** Like [size] but not performing any close check. */
322   @Throws(IOException::class)
323   protected abstract fun protectedSize(): Long
324 
325   /**
326    * Subclasses should implement this to release resources held by this file handle. It is invoked
327    * once both the file handle is closed, and also all sources and sinks produced by it are also
328    * closed.
329    */
330   @Throws(IOException::class)
331   protected abstract fun protectedClose()
332 
333   private fun readNoCloseCheck(fileOffset: Long, sink: Buffer, byteCount: Long): Long {
334     require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
335 
336     var currentOffset = fileOffset
337     val targetOffset = fileOffset + byteCount
338 
339     while (currentOffset < targetOffset) {
340       val tail = sink.writableSegment(1)
341       val readByteCount = protectedRead(
342         fileOffset = currentOffset,
343         array = tail.data,
344         arrayOffset = tail.limit,
345         byteCount = minOf(targetOffset - currentOffset, Segment.SIZE - tail.limit).toInt(),
346       )
347 
348       if (readByteCount == -1) {
349         if (tail.pos == tail.limit) {
350           // We allocated a tail segment, but didn't end up needing it. Recycle!
351           sink.head = tail.pop()
352           SegmentPool.recycle(tail)
353         }
354         if (fileOffset == currentOffset) return -1L // We wanted bytes but didn't return any.
355         break
356       }
357 
358       tail.limit += readByteCount
359       currentOffset += readByteCount
360       sink.size += readByteCount
361     }
362 
363     return currentOffset - fileOffset
364   }
365 
writeNoCloseChecknull366   private fun writeNoCloseCheck(fileOffset: Long, source: Buffer, byteCount: Long) {
367     checkOffsetAndCount(source.size, 0L, byteCount)
368 
369     var currentOffset = fileOffset
370     val targetOffset = fileOffset + byteCount
371 
372     while (currentOffset < targetOffset) {
373       val head = source.head!!
374       val toCopy = minOf(targetOffset - currentOffset, head.limit - head.pos).toInt()
375       protectedWrite(currentOffset, head.data, head.pos, toCopy)
376 
377       head.pos += toCopy
378       currentOffset += toCopy
379       source.size -= toCopy
380 
381       if (head.pos == head.limit) {
382         source.head = head.pop()
383         SegmentPool.recycle(head)
384       }
385     }
386   }
387 
388   private class FileHandleSink(
389     val fileHandle: FileHandle,
390     var position: Long,
391   ) : Sink {
392     var closed = false
393 
writenull394     override fun write(source: Buffer, byteCount: Long) {
395       check(!closed) { "closed" }
396       fileHandle.writeNoCloseCheck(position, source, byteCount)
397       position += byteCount
398     }
399 
flushnull400     override fun flush() {
401       check(!closed) { "closed" }
402       fileHandle.protectedFlush()
403     }
404 
timeoutnull405     override fun timeout() = Timeout.NONE
406 
407     override fun close() {
408       if (closed) return
409       closed = true
410       fileHandle.lock.withLock {
411         fileHandle.openStreamCount--
412         if (fileHandle.openStreamCount != 0 || !fileHandle.closed) return@close
413       }
414       fileHandle.protectedClose()
415     }
416   }
417 
418   private class FileHandleSource(
419     val fileHandle: FileHandle,
420     var position: Long,
421   ) : Source {
422     var closed = false
423 
readnull424     override fun read(sink: Buffer, byteCount: Long): Long {
425       check(!closed) { "closed" }
426       val result = fileHandle.readNoCloseCheck(position, sink, byteCount)
427       if (result != -1L) position += result
428       return result
429     }
430 
timeoutnull431     override fun timeout() = Timeout.NONE
432 
433     override fun close() {
434       if (closed) return
435       closed = true
436       fileHandle.lock.withLock {
437         fileHandle.openStreamCount--
438         if (fileHandle.openStreamCount != 0 || !fileHandle.closed) return@close
439       }
440       fileHandle.protectedClose()
441     }
442   }
443 }
444