Skip to content

Commit ed6f833

Browse files
authored
APIs to seek a FileHandle's source or sink (square#959)
The one fancy thing we're doing here is preserving the buffered data if we're seeking forward on a source. I don't think there's a compelling equivalent for preserving buffered data when seeking backwards on a sink. In theory we could truncate the buffer to the new seek position, but this breaks if the writer doesn't overwrite the full range of what was truncated.
1 parent 0c8e9f9 commit ed6f833

File tree

2 files changed

+302
-0
lines changed

2 files changed

+302
-0
lines changed

okio/src/commonMain/kotlin/okio/FileHandle.kt

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,44 @@ abstract class FileHandle(
167167
require(source is FileHandleSource && source.fileHandle === this) {
168168
"source was not created by this FileHandle"
169169
}
170+
check(!source.closed) { "closed" }
170171

171172
return source.position - bufferSize
172173
}
173174

175+
/**
176+
* Change the position of [source] in the file to [position]. The argument [source] must be either
177+
* a source produced by this file handle, or a [BufferedSource] that directly wraps such a source.
178+
* If the parameter is a [BufferedSource], it will skip or clear buffered bytes.
179+
*/
180+
@Throws(IOException::class)
181+
fun reposition(source: Source, position: Long) {
182+
if (source is RealBufferedSource) {
183+
val fileHandleSource = source.source
184+
require(fileHandleSource is FileHandleSource && fileHandleSource.fileHandle === this) {
185+
"source was not created by this FileHandle"
186+
}
187+
check(!fileHandleSource.closed) { "closed" }
188+
189+
val bufferSize = source.buffer.size
190+
val toSkip = position - (fileHandleSource.position - bufferSize)
191+
if (toSkip in 0L until bufferSize) {
192+
// The new position requires only a buffer change.
193+
source.skip(toSkip)
194+
} else {
195+
// The new position doesn't share data with the current buffer.
196+
source.buffer.clear()
197+
fileHandleSource.position = position
198+
}
199+
} else {
200+
require(source is FileHandleSource && source.fileHandle === this) {
201+
"source was not created by this FileHandle"
202+
}
203+
check(!source.closed) { "closed" }
204+
source.position = position
205+
}
206+
}
207+
174208
/**
175209
* Returns a sink that writes to this starting at [fileOffset]. The returned sink must be closed
176210
* when it is no longer needed.
@@ -212,10 +246,36 @@ abstract class FileHandle(
212246
require(sink is FileHandleSink && sink.fileHandle === this) {
213247
"sink was not created by this FileHandle"
214248
}
249+
check(!sink.closed) { "closed" }
215250

216251
return sink.position + bufferSize
217252
}
218253

254+
/**
255+
* Change the position of [sink] in the file to [position]. The argument [sink] must be either a
256+
* sink produced by this file handle, or a [BufferedSink] that directly wraps such a sink. If the
257+
* parameter is a [BufferedSink], it emits for buffered bytes.
258+
*/
259+
@Throws(IOException::class)
260+
fun reposition(sink: Sink, position: Long) {
261+
if (sink is RealBufferedSink) {
262+
val fileHandleSink = sink.sink
263+
require(fileHandleSink is FileHandleSink && fileHandleSink.fileHandle === this) {
264+
"sink was not created by this FileHandle"
265+
}
266+
check(!fileHandleSink.closed) { "closed" }
267+
268+
sink.emit()
269+
fileHandleSink.position = position
270+
} else {
271+
require(sink is FileHandleSink && sink.fileHandle === this) {
272+
"sink was not created by this FileHandle"
273+
}
274+
check(!sink.closed) { "closed" }
275+
sink.position = position
276+
}
277+
}
278+
219279
@Throws(IOException::class)
220280
final override fun close() {
221281
synchronized(this) {

okio/src/commonTest/kotlin/okio/AbstractFileSystemTest.kt

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,72 @@ abstract class AbstractFileSystemTest(
835835
}
836836
}
837837

838+
@Test fun fileHandleSinkReposition() {
839+
if (!supportsFileHandle()) return
840+
841+
val path = base / "file-handle-sink-reposition"
842+
843+
fileSystem.openReadWrite(path).use { handle ->
844+
handle.sink().use { sink ->
845+
sink.write(Buffer().writeUtf8("abcdefghij"), 10)
846+
handle.reposition(sink, 5)
847+
assertEquals(5, handle.position(sink))
848+
sink.write(Buffer().writeUtf8("KLM"), 3)
849+
assertEquals(8, handle.position(sink))
850+
851+
handle.reposition(sink, 200)
852+
sink.write(Buffer().writeUtf8("ABCDEFGHIJ"), 10)
853+
handle.reposition(sink, 205)
854+
assertEquals(205, handle.position(sink))
855+
sink.write(Buffer().writeUtf8("klm"), 3)
856+
assertEquals(208, handle.position(sink))
857+
}
858+
859+
Buffer().also {
860+
handle.read(fileOffset = 0, sink = it, byteCount = 10)
861+
assertEquals("abcdeKLMij", it.readUtf8())
862+
}
863+
864+
Buffer().also {
865+
handle.read(fileOffset = 200, sink = it, byteCount = 15)
866+
assertEquals("ABCDEklmIJ", it.readUtf8())
867+
}
868+
}
869+
}
870+
871+
@Test fun fileHandleBufferedSinkReposition() {
872+
if (!supportsFileHandle()) return
873+
874+
val path = base / "file-handle-buffered-sink-reposition"
875+
876+
fileSystem.openReadWrite(path).use { handle ->
877+
handle.sink().buffer().use { sink ->
878+
sink.write(Buffer().writeUtf8("abcdefghij"), 10)
879+
handle.reposition(sink, 5)
880+
assertEquals(5, handle.position(sink))
881+
sink.write(Buffer().writeUtf8("KLM"), 3)
882+
assertEquals(8, handle.position(sink))
883+
884+
handle.reposition(sink, 200)
885+
sink.write(Buffer().writeUtf8("ABCDEFGHIJ"), 10)
886+
handle.reposition(sink, 205)
887+
assertEquals(205, handle.position(sink))
888+
sink.write(Buffer().writeUtf8("klm"), 3)
889+
assertEquals(208, handle.position(sink))
890+
}
891+
892+
Buffer().also {
893+
handle.read(fileOffset = 0, sink = it, byteCount = 10)
894+
assertEquals("abcdeKLMij", it.readUtf8())
895+
}
896+
897+
Buffer().also {
898+
handle.read(fileOffset = 200, sink = it, byteCount = 15)
899+
assertEquals("ABCDEklmIJ", it.readUtf8())
900+
}
901+
}
902+
}
903+
838904
@Test fun fileHandleSourceHappyPath() {
839905
if (!supportsFileHandle()) return
840906

@@ -870,6 +936,98 @@ abstract class AbstractFileSystemTest(
870936
}
871937
}
872938

939+
@Test fun fileHandleSourceReposition() {
940+
if (!supportsFileHandle()) return
941+
942+
val path = base / "file-handle-source-reposition"
943+
fileSystem.write(path) {
944+
writeUtf8("abcdefghijklmnop")
945+
}
946+
947+
fileSystem.openReadOnly(path).use { handle ->
948+
assertEquals(16L, handle.size())
949+
val buffer = Buffer()
950+
951+
handle.source().use { source ->
952+
handle.reposition(source, 12L)
953+
assertEquals(12L, handle.position(source))
954+
assertEquals(4L, source.read(buffer, 4L))
955+
assertEquals("mnop", buffer.readUtf8())
956+
assertEquals(-1L, source.read(buffer, 4L))
957+
assertEquals("", buffer.readUtf8())
958+
assertEquals(16L, handle.position(source))
959+
960+
handle.reposition(source, 0L)
961+
assertEquals(0L, handle.position(source))
962+
assertEquals(4L, source.read(buffer, 4L))
963+
assertEquals("abcd", buffer.readUtf8())
964+
assertEquals(4L, handle.position(source))
965+
966+
handle.reposition(source, 8L)
967+
assertEquals(8L, handle.position(source))
968+
assertEquals(4L, source.read(buffer, 4L))
969+
assertEquals("ijkl", buffer.readUtf8())
970+
assertEquals(12L, handle.position(source))
971+
972+
handle.reposition(source, 16L)
973+
assertEquals(16L, handle.position(source))
974+
assertEquals(-1L, source.read(buffer, 4L))
975+
assertEquals("", buffer.readUtf8())
976+
assertEquals(16L, handle.position(source))
977+
}
978+
}
979+
}
980+
981+
@Test fun fileHandleBufferedSourceReposition() {
982+
if (!supportsFileHandle()) return
983+
984+
val path = base / "file-handle-buffered-source-reposition"
985+
fileSystem.write(path) {
986+
writeUtf8("abcdefghijklmnop")
987+
}
988+
989+
fileSystem.openReadOnly(path).use { handle ->
990+
assertEquals(16L, handle.size())
991+
val buffer = Buffer()
992+
993+
handle.source().buffer().use { source ->
994+
handle.reposition(source, 12L)
995+
assertEquals(0L, source.buffer.size)
996+
assertEquals(12L, handle.position(source))
997+
assertEquals(4L, source.read(buffer, 4L))
998+
assertEquals(0L, source.buffer.size)
999+
assertEquals("mnop", buffer.readUtf8())
1000+
assertEquals(-1L, source.read(buffer, 4L))
1001+
assertEquals("", buffer.readUtf8())
1002+
assertEquals(16L, handle.position(source))
1003+
1004+
handle.reposition(source, 0L)
1005+
assertEquals(0L, source.buffer.size)
1006+
assertEquals(0L, handle.position(source))
1007+
assertEquals(4L, source.read(buffer, 4L))
1008+
assertEquals(12L, source.buffer.size) // Buffered bytes accumulated.
1009+
assertEquals("abcd", buffer.readUtf8())
1010+
assertEquals(4L, handle.position(source))
1011+
1012+
handle.reposition(source, 8L)
1013+
assertEquals(8L, source.buffer.size) // Buffered bytes preserved.
1014+
assertEquals(8L, handle.position(source))
1015+
assertEquals(4L, source.read(buffer, 4L))
1016+
assertEquals(4L, source.buffer.size)
1017+
assertEquals("ijkl", buffer.readUtf8())
1018+
assertEquals(12L, handle.position(source))
1019+
1020+
handle.reposition(source, 16L)
1021+
assertEquals(0L, source.buffer.size)
1022+
assertEquals(16L, handle.position(source))
1023+
assertEquals(-1L, source.read(buffer, 4L))
1024+
assertEquals(0L, source.buffer.size)
1025+
assertEquals("", buffer.readUtf8())
1026+
assertEquals(16L, handle.position(source))
1027+
}
1028+
}
1029+
}
1030+
8731031
@Test fun fileHandleSourceSeekBackwards() {
8741032
if (!supportsFileHandle()) return
8751033

@@ -1017,6 +1175,90 @@ abstract class AbstractFileSystemTest(
10171175
assertEquals("", path.readUtf8())
10181176
}
10191177

1178+
@Test fun sinkPositionFailsAfterClose() {
1179+
if (!supportsFileHandle()) return
1180+
1181+
val path = base / "sink-position-fails-after-close"
1182+
1183+
fileSystem.openReadWrite(path).use { handle ->
1184+
val sink = handle.sink()
1185+
sink.close()
1186+
try {
1187+
handle.position(sink)
1188+
fail()
1189+
} catch (_: IllegalStateException) {
1190+
}
1191+
try {
1192+
handle.position(sink.buffer())
1193+
fail()
1194+
} catch (_: IllegalStateException) {
1195+
}
1196+
}
1197+
}
1198+
1199+
@Test fun sinkRepositionFailsAfterClose() {
1200+
if (!supportsFileHandle()) return
1201+
1202+
val path = base / "sink-reposition-fails-after-close"
1203+
1204+
fileSystem.openReadWrite(path).use { handle ->
1205+
val sink = handle.sink()
1206+
sink.close()
1207+
try {
1208+
handle.reposition(sink, 1L)
1209+
fail()
1210+
} catch (_: IllegalStateException) {
1211+
}
1212+
try {
1213+
handle.reposition(sink.buffer(), 1L)
1214+
fail()
1215+
} catch (_: IllegalStateException) {
1216+
}
1217+
}
1218+
}
1219+
1220+
@Test fun sourcePositionFailsAfterClose() {
1221+
if (!supportsFileHandle()) return
1222+
1223+
val path = base / "source-position-fails-after-close"
1224+
1225+
fileSystem.openReadWrite(path).use { handle ->
1226+
val source = handle.source()
1227+
source.close()
1228+
try {
1229+
handle.position(source)
1230+
fail()
1231+
} catch (_: IllegalStateException) {
1232+
}
1233+
try {
1234+
handle.position(source.buffer())
1235+
fail()
1236+
} catch (_: IllegalStateException) {
1237+
}
1238+
}
1239+
}
1240+
1241+
@Test fun sourceRepositionFailsAfterClose() {
1242+
if (!supportsFileHandle()) return
1243+
1244+
val path = base / "source-reposition-fails-after-close"
1245+
1246+
fileSystem.openReadWrite(path).use { handle ->
1247+
val source = handle.source()
1248+
source.close()
1249+
try {
1250+
handle.reposition(source, 1L)
1251+
fail()
1252+
} catch (_: IllegalStateException) {
1253+
}
1254+
try {
1255+
handle.reposition(source.buffer(), 1L)
1256+
fail()
1257+
} catch (_: IllegalStateException) {
1258+
}
1259+
}
1260+
}
1261+
10201262
private fun assertClosedFailure(block: () -> Unit) {
10211263
val exception = assertFails {
10221264
block()

0 commit comments

Comments
 (0)