1
0
mirror of https://github.com/square/okhttp.git synced 2025-08-07 12:42:57 +03:00

Okio Filesystem (#6500)

* Testing okio

* Working tests

* Working tests

* Working tests

* Working tests

* okio 3

* Fix dependencies

* File system

* Cleanup

* Cleanup

* Cleanup

* Cache fixes

* Cache fixes

* Review comments

* Cleanup

* Cleanup

* Build fixes
This commit is contained in:
Yuri Schimke
2021-02-06 13:49:05 +00:00
committed by GitHub
parent c667a56468
commit 6e8aa12dd6
18 changed files with 710 additions and 1092 deletions

View File

@@ -16,7 +16,7 @@ buildscript {
'junit5': '5.7.0',
'kotlin': '1.4.30',
'moshi': '1.11.0',
'okio': '2.9.0',
'okio': '3.0.0-alpha.1',
'ktlint': '0.38.0',
'picocli': '4.5.1',
'openjsse': '1.1.0',
@@ -50,6 +50,7 @@ buildscript {
'moshi': "com.squareup.moshi:moshi:${versions.moshi}",
'moshiKotlin': "com.squareup.moshi:moshi-kotlin-codegen:${versions.moshi}",
'okio': "com.squareup.okio:okio:${versions.okio}",
'okioFakeFileSystem': "com.squareup.okio:okio-fakefilesystem:${versions.okio}",
'openjsse': "org.openjsse:openjsse:${versions.openjsse}",
'bnd': "biz.aQute.bnd:biz.aQute.bnd.gradle:${versions.bnd}",
'bndResolve': "biz.aQute.bnd:biz.aQute.resolve:${versions.bnd}",
@@ -110,6 +111,13 @@ allprojects {
project.ext.artifactId = rootProject.ext.publishedArtifactId(project)
version = '5.0.0-SNAPSHOT'
repositories {
mavenCentral()
maven { url 'https://dl.bintray.com/kotlin/dokka' }
maven { url 'https://kotlin.bintray.com/kotlinx/' }
google()
}
task downloadDependencies() {
description 'Download all dependencies to the Gradle cache'
doLast {
@@ -358,7 +366,7 @@ def alpnBootVersion() {
return alpnBootVersionForPatchVersion(javaVersion, patchVersion)
}
def alpnBootVersionForPatchVersion(String javaVersion, int patchVersion) {
static def alpnBootVersionForPatchVersion(String javaVersion, int patchVersion) {
// https://www.eclipse.org/jetty/documentation/current/alpn-chapter.html#alpn-versions
switch (patchVersion) {
case 0..24:

View File

@@ -9,6 +9,7 @@ dependencies {
implementation deps.junit5Api
implementation deps.junit5JupiterEngine
implementation deps.junitPlatformConsole
implementation deps.okioFakeFileSystem
implementation project(':okhttp')
implementation project(':okhttp-brotli')

View File

@@ -1,116 +0,0 @@
/*
* Copyright (C) 2015 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 okhttp3.internal.io
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.util.IdentityHashMap
import okio.Buffer
import okio.ForwardingSink
import okio.ForwardingSource
import okio.Sink
import okio.Source
import okhttp3.TestUtil.isDescendentOf
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
/** A simple file system where all files are held in memory. Not safe for concurrent use. */
class InMemoryFileSystem : FileSystem, AfterEachCallback {
val files = mutableMapOf<File, Buffer>()
private val openSources = IdentityHashMap<Source, File>()
private val openSinks = IdentityHashMap<Sink, File>()
override fun afterEach(context: ExtensionContext?) {
ensureResourcesClosed()
}
fun ensureResourcesClosed() {
val openResources = mutableListOf<String>()
for (file in openSources.values) {
openResources.add("Source for $file")
}
for (file in openSinks.values) {
openResources.add("Sink for $file")
}
check(openResources.isEmpty()) {
"Resources acquired but not closed:\n * ${openResources.joinToString(separator = "\n * ")}"
}
}
@Throws(FileNotFoundException::class)
override fun source(file: File): Source {
val result = files[file] ?: throw FileNotFoundException()
val source: Source = result.clone()
openSources[source] = file
return object : ForwardingSource(source) {
override fun close() {
openSources.remove(source)
super.close()
}
}
}
@Throws(FileNotFoundException::class)
override fun sink(file: File) = sink(file, false)
@Throws(FileNotFoundException::class)
override fun appendingSink(file: File) = sink(file, true)
private fun sink(file: File, appending: Boolean): Sink {
var result: Buffer? = null
if (appending) {
result = files[file]
}
if (result == null) {
result = Buffer()
}
files[file] = result
val sink: Sink = result
openSinks[sink] = file
return object : ForwardingSink(sink) {
override fun close() {
openSinks.remove(sink)
super.close()
}
}
}
@Throws(IOException::class)
override fun delete(file: File) {
files.remove(file)
}
override fun exists(file: File) = files.containsKey(file)
override fun size(file: File) = files[file]?.size ?: 0L
@Throws(IOException::class)
override fun rename(from: File, to: File) {
files[to] = files.remove(from) ?: throw FileNotFoundException()
}
@Throws(IOException::class)
override fun deleteContents(directory: File) {
val i = files.keys.iterator()
while (i.hasNext()) {
val file = i.next()
if (file.isDescendentOf(directory)) i.remove()
}
}
override fun toString() = "InMemoryFileSystem"
}

View File

@@ -1,104 +0,0 @@
/*
* Copyright (C) 2020 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 okhttp3.internal.io
import java.io.File
import java.util.Collections
import okhttp3.TestUtil.isDescendentOf
import okio.ForwardingSink
import okio.ForwardingSource
import okio.IOException
import okio.Sink
import okio.Source
/**
* Emulate Windows file system limitations on any file system. In particular, Windows will throw an
* [IOException] when asked to delete or rename an open file.
*/
class WindowsFileSystem(val delegate: FileSystem) : FileSystem {
/** Guarded by itself. */
private val openFiles = Collections.synchronizedList(mutableListOf<File>())
override fun source(file: File): Source = FileSource(file, delegate.source(file))
override fun sink(file: File): Sink = FileSink(file, delegate.sink(file))
override fun appendingSink(file: File): Sink = FileSink(file, delegate.appendingSink(file))
override fun delete(file: File) {
val fileOpen = file in openFiles
if (fileOpen) throw IOException("file is open $file")
delegate.delete(file)
}
override fun exists(file: File) = delegate.exists(file)
override fun size(file: File) = delegate.size(file)
override fun rename(from: File, to: File) {
val fromOpen = from in openFiles
if (fromOpen) throw IOException("file is open $from")
val toOpen = to in openFiles
if (toOpen) throw IOException("file is open $to")
delegate.rename(from, to)
}
override fun deleteContents(directory: File) {
val openChild = synchronized(openFiles) {
openFiles.firstOrNull { it.isDescendentOf(directory) }
}
if (openChild != null) throw IOException("file is open $openChild")
delegate.deleteContents(directory)
}
private inner class FileSink(val file: File, delegate: Sink) : ForwardingSink(delegate) {
var closed = false
init {
openFiles += file
}
override fun close() {
if (!closed) {
closed = true
val removed = openFiles.remove(file)
check(removed)
}
delegate.close()
}
}
private inner class FileSource(val file: File, delegate: Source) : ForwardingSource(delegate) {
var closed = false
init {
openFiles += file
}
override fun close() {
if (!closed) {
closed = true
val removed = openFiles.remove(file)
check(removed)
}
delegate.close()
}
}
override fun toString() = "$delegate for Windows™"
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright (C) 2020 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 okhttp3.okio
import okio.ExperimentalFileSystem
import okio.FileSystem
import okio.ForwardingFileSystem
import okio.Path
import okio.Sink
import okio.Source
@OptIn(ExperimentalFileSystem::class)
class LoggingFilesystem(fileSystem: FileSystem) : ForwardingFileSystem(fileSystem) {
fun log(line: String) {
println(line)
}
override fun appendingSink(file: Path): Sink {
log("appendingSink($file)")
return super.appendingSink(file)
}
override fun atomicMove(source: Path, target: Path) {
log("atomicMove($source, $target)")
super.atomicMove(source, target)
}
override fun createDirectory(dir: Path) {
log("createDirectory($dir)")
super.createDirectory(dir)
}
override fun delete(path: Path) {
log("delete($path)")
super.delete(path)
}
override fun sink(path: Path): Sink {
log("sink($path)")
return super.sink(path)
}
override fun source(file: Path): Source {
log("source($file)")
return super.source(file)
}
}

View File

@@ -82,6 +82,7 @@ dependencies {
testImplementation project(':okhttp-brotli')
testImplementation project(':okhttp-dnsoverhttps')
testImplementation project(':okhttp-sse')
testImplementation deps.okioFakeFileSystem
testImplementation deps.conscrypt
testImplementation deps.junit
testImplementation deps.junit5Api

View File

@@ -25,7 +25,6 @@ import okhttp3.internal.closeQuietly
import okhttp3.internal.concurrent.TaskRunner
import okhttp3.internal.http.HttpMethod
import okhttp3.internal.http.StatusLine
import okhttp3.internal.io.FileSystem
import okhttp3.internal.platform.Platform
import okhttp3.internal.platform.Platform.Companion.WARN
import okhttp3.internal.toLongOrDefault
@@ -35,8 +34,12 @@ import okio.BufferedSource
import okio.ByteString.Companion.decodeBase64
import okio.ByteString.Companion.encodeUtf8
import okio.ByteString.Companion.toByteString
import okio.ExperimentalFileSystem
import okio.FileSystem
import okio.ForwardingSink
import okio.ForwardingSource
import okio.Path
import okio.Path.Companion.toOkioPath
import okio.Sink
import okio.Source
import okio.buffer
@@ -50,14 +53,6 @@ import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.util.NoSuchElementException
import java.util.TreeSet
import kotlin.collections.ArrayList
import kotlin.collections.List
import kotlin.collections.MutableIterator
import kotlin.collections.MutableSet
import kotlin.collections.Set
import kotlin.collections.emptyList
import kotlin.collections.emptySet
import kotlin.collections.none
/**
* Caches HTTP and HTTPS responses to the filesystem so they may be reused, saving time and
@@ -148,18 +143,20 @@ import kotlin.collections.none
*
* [rfc_7234]: http://tools.ietf.org/html/rfc7234
*/
class Cache internal constructor(
directory: File,
@OptIn(ExperimentalFileSystem::class)
class Cache
internal constructor(
directory: Path,
maxSize: Long,
fileSystem: FileSystem
) : Closeable, Flushable {
internal val cache = DiskLruCache(
fileSystem = fileSystem,
directory = directory,
appVersion = VERSION,
valueCount = ENTRY_COUNT,
maxSize = maxSize,
taskRunner = TaskRunner.INSTANCE
fileSystem = fileSystem,
directory = directory,
appVersion = VERSION,
valueCount = ENTRY_COUNT,
maxSize = maxSize,
taskRunner = TaskRunner.INSTANCE
)
// read and write statistics, all guarded by 'this'.
@@ -173,7 +170,10 @@ class Cache internal constructor(
get() = cache.isClosed()
/** Create a cache of at most [maxSize] bytes in [directory]. */
constructor(directory: File, maxSize: Long) : this(directory, maxSize, FileSystem.SYSTEM)
@OptIn(ExperimentalFileSystem::class)
constructor(directory: File, maxSize: Long) : this(
directory.toOkioPath(), maxSize, FileSystem.SYSTEM
)
internal fun get(request: Request): Response? {
val key = key(request.url)
@@ -365,14 +365,18 @@ class Cache internal constructor(
}
@get:JvmName("directory") val directory: File
get() = cache.directory.toFile()
@get:JvmName("directoryPath") val directoryPath: Path
get() = cache.directory
@JvmName("-deprecated_directory")
@Deprecated(
message = "moved to val",
replaceWith = ReplaceWith(expression = "directory"),
level = DeprecationLevel.ERROR)
fun directory(): File = cache.directory
message = "moved to val",
replaceWith = ReplaceWith(expression = "directory"),
level = DeprecationLevel.ERROR
)
fun directory(): File = cache.directory.toFile()
@Synchronized internal fun trackResponse(cacheStrategy: CacheStrategy) {
requestCount++
@@ -575,27 +579,27 @@ class Cache internal constructor(
sink.writeDecimalLong(varyHeaders.size.toLong()).writeByte('\n'.toInt())
for (i in 0 until varyHeaders.size) {
sink.writeUtf8(varyHeaders.name(i))
.writeUtf8(": ")
.writeUtf8(varyHeaders.value(i))
.writeByte('\n'.toInt())
.writeUtf8(": ")
.writeUtf8(varyHeaders.value(i))
.writeByte('\n'.toInt())
}
sink.writeUtf8(StatusLine(protocol, code, message).toString()).writeByte('\n'.toInt())
sink.writeDecimalLong((responseHeaders.size + 2).toLong()).writeByte('\n'.toInt())
for (i in 0 until responseHeaders.size) {
sink.writeUtf8(responseHeaders.name(i))
.writeUtf8(": ")
.writeUtf8(responseHeaders.value(i))
.writeByte('\n'.toInt())
.writeUtf8(": ")
.writeUtf8(responseHeaders.value(i))
.writeByte('\n'.toInt())
}
sink.writeUtf8(SENT_MILLIS)
.writeUtf8(": ")
.writeDecimalLong(sentRequestMillis)
.writeByte('\n'.toInt())
.writeUtf8(": ")
.writeDecimalLong(sentRequestMillis)
.writeByte('\n'.toInt())
sink.writeUtf8(RECEIVED_MILLIS)
.writeUtf8(": ")
.writeDecimalLong(receivedResponseMillis)
.writeByte('\n'.toInt())
.writeUtf8(": ")
.writeDecimalLong(receivedResponseMillis)
.writeByte('\n'.toInt())
if (isHttps) {
sink.writeByte('\n'.toInt())
@@ -643,29 +647,29 @@ class Cache internal constructor(
fun matches(request: Request, response: Response): Boolean {
return url == request.url &&
requestMethod == request.method &&
varyMatches(response, varyHeaders, request)
requestMethod == request.method &&
varyMatches(response, varyHeaders, request)
}
fun response(snapshot: DiskLruCache.Snapshot): Response {
val contentType = responseHeaders["Content-Type"]
val contentLength = responseHeaders["Content-Length"]
val cacheRequest = Request.Builder()
.url(url)
.method(requestMethod, null)
.headers(varyHeaders)
.build()
.url(url)
.method(requestMethod, null)
.headers(varyHeaders)
.build()
return Response.Builder()
.request(cacheRequest)
.protocol(protocol)
.code(code)
.message(message)
.headers(responseHeaders)
.body(CacheResponseBody(snapshot, contentType, contentLength))
.handshake(handshake)
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(receivedResponseMillis)
.build()
.request(cacheRequest)
.protocol(protocol)
.code(code)
.message(message)
.headers(responseHeaders)
.body(CacheResponseBody(snapshot, contentType, contentLength))
.handshake(handshake)
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(receivedResponseMillis)
.build()
}
companion object {

View File

@@ -21,10 +21,12 @@ import okhttp3.Response
import okhttp3.internal.connection.Exchange
import okhttp3.internal.connection.RealCall
import okhttp3.internal.connection.RealConnection
import okhttp3.internal.io.FileSystem
import java.io.File
import okio.ExperimentalFileSystem
import okio.FileSystem
import okio.Path
fun buildCache(file: File, maxSize: Long, fileSystem: FileSystem): Cache {
@OptIn(ExperimentalFileSystem::class)
fun buildCache(file: Path, maxSize: Long, fileSystem: FileSystem): Cache {
return Cache(file, maxSize, fileSystem)
}

View File

@@ -17,8 +17,27 @@
package okhttp3.internal
import okhttp3.EventListener
import okhttp3.Headers
import okhttp3.Headers.Companion.headersOf
import okhttp3.HttpUrl
import okhttp3.OkHttp
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.internal.http2.Header
import okio.Buffer
import okio.BufferedSink
import okio.BufferedSource
import okio.ByteString.Companion.decodeHex
import okio.ExperimentalFileSystem
import okio.FileSystem
import okio.Options
import okio.Path
import okio.Source
import java.io.Closeable
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InterruptedIOException
import java.net.InetSocketAddress
@@ -38,24 +57,6 @@ import java.util.concurrent.ThreadFactory
import java.util.concurrent.TimeUnit
import kotlin.text.Charsets.UTF_32BE
import kotlin.text.Charsets.UTF_32LE
import okhttp3.Call
import okhttp3.EventListener
import okhttp3.Headers
import okhttp3.Headers.Companion.headersOf
import okhttp3.HttpUrl
import okhttp3.OkHttp
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.internal.http2.Header
import okhttp3.internal.io.FileSystem
import okio.Buffer
import okio.BufferedSink
import okio.BufferedSource
import okio.ByteString.Companion.decodeHex
import okio.Options
import okio.Source
@JvmField
val EMPTY_BYTE_ARRAY = ByteArray(0)
@@ -530,7 +531,8 @@ fun ServerSocket.closeQuietly() {
*
* @param file a file in the directory to check. This file shouldn't already exist!
*/
fun FileSystem.isCivilized(file: File): Boolean {
@OptIn(ExperimentalFileSystem::class)
fun FileSystem.isCivilized(file: Path): Boolean {
sink(file).use {
try {
delete(file)
@@ -542,6 +544,45 @@ fun FileSystem.isCivilized(file: File): Boolean {
return false
}
/** Delete file we expect but don't require to exist. */
@OptIn(ExperimentalFileSystem::class)
fun FileSystem.deleteIfExists(path: Path) {
try {
delete(path)
} catch (fnfe: FileNotFoundException) {
return
}
}
/**
* Tolerant delete, try to clear as many files as possible even after a failure.
*/
@OptIn(ExperimentalFileSystem::class)
fun FileSystem.deleteContents(directory: Path) {
var exception: IOException? = null
val files = try {
list(directory)
} catch (fnfe: FileNotFoundException) {
return
}
for (file in files) {
try {
if (metadata(file).isDirectory) {
deleteContents(file)
}
delete(file)
} catch (ioe: IOException) {
if (exception == null) {
exception = ioe
}
}
}
if (exception != null) {
throw exception
}
}
fun Long.toHexString(): String = java.lang.Long.toHexString(this)
fun Int.toHexString(): String = Integer.toHexString(this)

View File

@@ -20,16 +20,27 @@ import okhttp3.internal.cache.DiskLruCache.Editor
import okhttp3.internal.closeQuietly
import okhttp3.internal.concurrent.Task
import okhttp3.internal.concurrent.TaskRunner
import okhttp3.internal.io.FileSystem
import okhttp3.internal.deleteContents
import okhttp3.internal.deleteIfExists
import okhttp3.internal.isCivilized
import okhttp3.internal.okHttpName
import okhttp3.internal.platform.Platform
import okhttp3.internal.platform.Platform.Companion.WARN
import okio.*
import java.io.*
import okio.BufferedSink
import okio.ExperimentalFileSystem
import okio.FileNotFoundException
import okio.FileSystem
import okio.ForwardingFileSystem
import okio.ForwardingSource
import okio.Path
import okio.Sink
import okio.Source
import okio.blackholeSink
import okio.buffer
import java.io.Closeable
import java.io.EOFException
import java.io.Flushable
import java.io.IOException
import java.util.*
/**
* A cache that uses a bounded amount of space on a filesystem. Each cache entry has a string key
@@ -74,11 +85,12 @@ import java.util.*
* @param valueCount the number of values per cache entry. Must be positive.
* @param maxSize the maximum number of bytes this cache should use to store.
*/
@OptIn(ExperimentalFileSystem::class)
class DiskLruCache(
internal val fileSystem: FileSystem,
fileSystem: FileSystem,
/** Returns the directory where this cache stores its data. */
val directory: File,
val directory: Path,
private val appVersion: Int,
@@ -90,6 +102,18 @@ class DiskLruCache(
/** Used for asynchronous journal rebuilds. */
taskRunner: TaskRunner
) : Closeable, Flushable {
internal val fileSystem: FileSystem = object : ForwardingFileSystem(fileSystem) {
override fun sink(file: Path): Sink {
file.parent?.let {
// TODO from okhttp3.internal.io.FileSystem
if (!exists(it)) {
createDirectories(it)
}
}
return super.sink(file)
}
}
/** The maximum number of bytes that this cache should use to store its data. */
@get:Synchronized @set:Synchronized var maxSize: Long = maxSize
set(value) {
@@ -139,9 +163,9 @@ class DiskLruCache(
* compaction; that file should be deleted if it exists when the cache is opened.
*/
private val journalFile: File
private val journalFileTmp: File
private val journalFileBackup: File
private val journalFile: Path
private val journalFileTmp: Path
private val journalFileBackup: Path
private var size: Long = 0L
private var journalWriter: BufferedSink? = null
internal val lruEntries = LinkedHashMap<String, Entry>(0, 0.75f, true)
@@ -195,9 +219,9 @@ class DiskLruCache(
require(maxSize > 0L) { "maxSize <= 0" }
require(valueCount > 0) { "valueCount <= 0" }
this.journalFile = File(directory, JOURNAL_FILE)
this.journalFileTmp = File(directory, JOURNAL_FILE_TEMP)
this.journalFileBackup = File(directory, JOURNAL_FILE_BACKUP)
this.journalFile = directory / JOURNAL_FILE
this.journalFileTmp = directory / JOURNAL_FILE_TEMP
this.journalFileBackup = directory / JOURNAL_FILE_BACKUP
}
@Synchronized @Throws(IOException::class)
@@ -214,7 +238,7 @@ class DiskLruCache(
if (fileSystem.exists(journalFile)) {
fileSystem.delete(journalFileBackup)
} else {
fileSystem.rename(journalFileBackup, journalFile)
fileSystem.atomicMove(journalFileBackup, journalFile)
}
}
@@ -250,12 +274,12 @@ class DiskLruCache(
@Throws(IOException::class)
private fun readJournal() {
fileSystem.source(journalFile).buffer().use { source ->
val magic = source.readUtf8LineStrict()
val version = source.readUtf8LineStrict()
val appVersionString = source.readUtf8LineStrict()
val valueCountString = source.readUtf8LineStrict()
val blank = source.readUtf8LineStrict()
fileSystem.read(journalFile) {
val magic = readUtf8LineStrict()
val version = readUtf8LineStrict()
val appVersionString = readUtf8LineStrict()
val valueCountString = readUtf8LineStrict()
val blank = readUtf8LineStrict()
if (MAGIC != magic ||
VERSION_1 != version ||
@@ -269,7 +293,7 @@ class DiskLruCache(
var lineCount = 0
while (true) {
try {
readJournalLine(source.readUtf8LineStrict())
readJournalLine(readUtf8LineStrict())
lineCount++
} catch (_: EOFException) {
break // End of journal.
@@ -279,7 +303,7 @@ class DiskLruCache(
redundantOpCount = lineCount - lruEntries.size
// If we ended on a truncated line, rebuild the journal before appending to it.
if (!source.exhausted()) {
if (!exhausted()) {
rebuildJournal()
} else {
journalWriter = newJournalWriter()
@@ -348,7 +372,7 @@ class DiskLruCache(
*/
@Throws(IOException::class)
private fun processJournal() {
fileSystem.delete(journalFileTmp)
fileSystem.deleteIfExists(journalFileTmp)
val i = lruEntries.values.iterator()
while (i.hasNext()) {
val entry = i.next()
@@ -359,8 +383,8 @@ class DiskLruCache(
} else {
entry.currentEditor = null
for (t in 0 until valueCount) {
fileSystem.delete(entry.cleanFiles[t])
fileSystem.delete(entry.dirtyFiles[t])
fileSystem.deleteIfExists(entry.cleanFiles[t])
fileSystem.deleteIfExists(entry.dirtyFiles[t])
}
i.remove()
}
@@ -375,32 +399,34 @@ class DiskLruCache(
internal fun rebuildJournal() {
journalWriter?.close()
fileSystem.sink(journalFileTmp).buffer().use { sink ->
sink.writeUtf8(MAGIC).writeByte('\n'.toInt())
sink.writeUtf8(VERSION_1).writeByte('\n'.toInt())
sink.writeDecimalLong(appVersion.toLong()).writeByte('\n'.toInt())
sink.writeDecimalLong(valueCount.toLong()).writeByte('\n'.toInt())
sink.writeByte('\n'.toInt())
fileSystem.write(journalFileTmp) {
writeUtf8(MAGIC).writeByte('\n'.toInt())
writeUtf8(VERSION_1).writeByte('\n'.toInt())
writeDecimalLong(appVersion.toLong()).writeByte('\n'.toInt())
writeDecimalLong(valueCount.toLong()).writeByte('\n'.toInt())
writeByte('\n'.toInt())
for (entry in lruEntries.values) {
if (entry.currentEditor != null) {
sink.writeUtf8(DIRTY).writeByte(' '.toInt())
sink.writeUtf8(entry.key)
sink.writeByte('\n'.toInt())
writeUtf8(DIRTY).writeByte(' '.toInt())
writeUtf8(entry.key)
writeByte('\n'.toInt())
} else {
sink.writeUtf8(CLEAN).writeByte(' '.toInt())
sink.writeUtf8(entry.key)
entry.writeLengths(sink)
sink.writeByte('\n'.toInt())
writeUtf8(CLEAN).writeByte(' '.toInt())
writeUtf8(entry.key)
entry.writeLengths(this)
writeByte('\n'.toInt())
}
}
}
if (fileSystem.exists(journalFile)) {
fileSystem.rename(journalFile, journalFileBackup)
fileSystem.atomicMove(journalFile, journalFileBackup)
fileSystem.atomicMove(journalFileTmp, journalFile)
fileSystem.deleteIfExists(journalFileBackup)
} else {
fileSystem.atomicMove(journalFileTmp, journalFile)
}
fileSystem.rename(journalFileTmp, journalFile)
fileSystem.delete(journalFileBackup)
journalWriter = newJournalWriter()
hasJournalErrors = false
@@ -519,14 +545,15 @@ class DiskLruCache(
if (success && !entry.zombie) {
if (fileSystem.exists(dirty)) {
val clean = entry.cleanFiles[i]
fileSystem.rename(dirty, clean)
fileSystem.atomicMove(dirty, clean)
val oldLength = entry.lengths[i]
val newLength = fileSystem.size(clean)
// TODO check null behaviour
val newLength = fileSystem.metadata(clean).size ?: 0
entry.lengths[i] = newLength
size = size - oldLength + newLength
}
} else {
fileSystem.delete(dirty)
fileSystem.deleteIfExists(dirty)
}
}
@@ -613,7 +640,7 @@ class DiskLruCache(
entry.currentEditor?.detach() // Prevent the edit from completing normally.
for (i in 0 until valueCount) {
fileSystem.delete(entry.cleanFiles[i])
fileSystem.deleteIfExists(entry.cleanFiles[i])
size -= entry.lengths[i]
entry.lengths[i] = 0
}
@@ -916,8 +943,8 @@ class DiskLruCache(
/** Lengths of this entry's files. */
internal val lengths: LongArray = LongArray(valueCount)
internal val cleanFiles = mutableListOf<File>()
internal val dirtyFiles = mutableListOf<File>()
internal val cleanFiles = mutableListOf<Path>()
internal val dirtyFiles = mutableListOf<Path>()
/** True if this entry has ever been published. */
internal var readable: Boolean = false
@@ -946,9 +973,9 @@ class DiskLruCache(
val truncateTo = fileBuilder.length
for (i in 0 until valueCount) {
fileBuilder.append(i)
cleanFiles += File(directory, fileBuilder.toString())
cleanFiles += directory / fileBuilder.toString()
fileBuilder.append(".tmp")
dirtyFiles += File(directory, fileBuilder.toString())
dirtyFiles += directory / fileBuilder.toString()
fileBuilder.setLength(truncateTo)
}
}

View File

@@ -1,149 +0,0 @@
/*
* Copyright (C) 2015 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 okhttp3.internal.io
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import okio.Sink
import okio.Source
import okio.appendingSink
import okio.sink
import okio.source
/**
* Access to read and write files on a hierarchical data store. Most callers should use the
* [SYSTEM] implementation, which uses the host machine's local file system. Alternate
* implementations may be used to inject faults (for testing) or to transform stored data (to add
* encryption, for example).
*
* All operations on a file system are racy. For example, guarding a call to [source] with
* [exists] does not guarantee that [FileNotFoundException] will not be thrown. The
* file may be moved between the two calls!
*
* This interface is less ambitious than [java.nio.file.FileSystem] introduced in Java 7.
* It lacks important features like file watching, metadata, permissions, and disk space
* information. In exchange for these limitations, this interface is easier to implement and works
* on all versions of Java and Android.
*/
interface FileSystem {
companion object {
/** The host machine's local file system. */
@JvmField
val SYSTEM: FileSystem = SystemFileSystem()
private class SystemFileSystem : FileSystem {
@Throws(FileNotFoundException::class)
override fun source(file: File): Source = file.source()
@Throws(FileNotFoundException::class)
override fun sink(file: File): Sink {
return try {
file.sink()
} catch (_: FileNotFoundException) {
// Maybe the parent directory doesn't exist? Try creating it first.
file.parentFile.mkdirs()
file.sink()
}
}
@Throws(FileNotFoundException::class)
override fun appendingSink(file: File): Sink {
return try {
file.appendingSink()
} catch (_: FileNotFoundException) {
// Maybe the parent directory doesn't exist? Try creating it first.
file.parentFile.mkdirs()
file.appendingSink()
}
}
@Throws(IOException::class)
override fun delete(file: File) {
// If delete() fails, make sure it's because the file didn't exist!
if (!file.delete() && file.exists()) {
throw IOException("failed to delete $file")
}
}
override fun exists(file: File): Boolean = file.exists()
override fun size(file: File): Long = file.length()
@Throws(IOException::class)
override fun rename(from: File, to: File) {
delete(to)
if (!from.renameTo(to)) {
throw IOException("failed to rename $from to $to")
}
}
@Throws(IOException::class)
override fun deleteContents(directory: File) {
val files = directory.listFiles() ?: throw IOException("not a readable directory: $directory")
for (file in files) {
if (file.isDirectory) {
deleteContents(file)
}
if (!file.delete()) {
throw IOException("failed to delete $file")
}
}
}
override fun toString() = "FileSystem.SYSTEM"
}
}
/** Reads from [file]. */
@Throws(FileNotFoundException::class)
fun source(file: File): Source
/**
* Writes to [file], discarding any data already present. Creates parent directories if
* necessary.
*/
@Throws(FileNotFoundException::class)
fun sink(file: File): Sink
/**
* Writes to [file], appending if data is already present. Creates parent directories if
* necessary.
*/
@Throws(FileNotFoundException::class)
fun appendingSink(file: File): Sink
/** Deletes [file] if it exists. Throws if the file exists and cannot be deleted. */
@Throws(IOException::class)
fun delete(file: File)
/** Returns true if [file] exists on the file system. */
fun exists(file: File): Boolean
/** Returns the number of bytes stored in [file], or 0 if it does not exist. */
fun size(file: File): Long
/** Renames [from] to [to]. Throws if the file cannot be renamed. */
@Throws(IOException::class)
fun rename(from: File, to: File)
/**
* Recursively delete the contents of [directory]. Throws an IOException if any file could
* not be deleted, or if `dir` is not a readable directory.
*/
@Throws(IOException::class)
fun deleteContents(directory: File)
}

View File

@@ -18,15 +18,17 @@ package okhttp3
import mockwebserver3.MockResponse
import mockwebserver3.MockWebServer
import okhttp3.internal.buildCache
import okhttp3.internal.io.InMemoryFileSystem
import okhttp3.okio.LoggingFilesystem
import okhttp3.testing.PlatformRule
import okhttp3.tls.internal.TlsUtil.localhost
import okio.ExperimentalFileSystem
import okio.Path.Companion.toPath
import okio.fakefilesystem.FakeFileSystem
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
import java.io.File
import java.net.CookieManager
import java.net.ResponseCache
import java.text.DateFormat
@@ -38,12 +40,20 @@ import java.util.concurrent.TimeUnit
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLSession
@OptIn(ExperimentalFileSystem::class)
class CacheCorruptionTest(
var server: MockWebServer
) {
@JvmField @RegisterExtension var fileSystem = InMemoryFileSystem()
@JvmField @RegisterExtension val clientTestRule = OkHttpClientTestRule()
@JvmField @RegisterExtension val platform = PlatformRule()
@OptIn(ExperimentalFileSystem::class)
var fileSystem = FakeFileSystem()
@JvmField
@RegisterExtension
val clientTestRule = OkHttpClientTestRule()
@JvmField
@RegisterExtension
val platform = PlatformRule()
private val handshakeCertificates = localhost()
private lateinit var client: OkHttpClient
@@ -52,25 +62,29 @@ class CacheCorruptionTest(
HostnameVerifier { _: String?, _: SSLSession? -> true }
private val cookieManager = CookieManager()
@BeforeEach fun setUp() {
@BeforeEach
fun setUp() {
platform.assumeNotOpenJSSE()
platform.assumeNotBouncyCastle()
server.protocolNegotiationEnabled = false
cache = buildCache(File("/cache/"), Int.MAX_VALUE.toLong(), fileSystem)
val loggingFileSystem = LoggingFilesystem(fileSystem)
cache = buildCache("/cache/".toPath(), Int.MAX_VALUE.toLong(), loggingFileSystem)
client = clientTestRule.newClientBuilder()
.cache(cache)
.cookieJar(JavaNetCookieJar(cookieManager))
.build()
}
@AfterEach fun tearDown() {
@AfterEach
fun tearDown() {
ResponseCache.setDefault(null)
if (this::cache.isInitialized) {
cache.delete()
}
}
@Test fun corruptedCipher() {
@Test
fun corruptedCipher() {
val response = testCorruptingCache {
corruptMetadata {
// mess with cipher suite
@@ -86,7 +100,8 @@ class CacheCorruptionTest(
assertThat(response.handshake?.cipherSuite?.javaName).startsWith("SLT_")
}
@Test fun truncatedMetadataEntry() {
@Test
fun truncatedMetadataEntry() {
val response = testCorruptingCache {
corruptMetadata {
// truncate metadata to 1/4 of length
@@ -115,30 +130,41 @@ class CacheCorruptionTest(
}
private fun corruptMetadata(corruptor: (String) -> String) {
val metadataFile = fileSystem.files.keys.find { it.name.endsWith(".0") }
val metadataBuffer = fileSystem.files[metadataFile]
val metadataFile = fileSystem.allPaths.find {
it.name.endsWith(".0")
}
val contents = metadataBuffer!!.peek().readUtf8()
if (metadataFile != null) {
val contents = fileSystem.read(metadataFile) {
readUtf8()
}
metadataBuffer.clear()
metadataBuffer.writeUtf8(corruptor(contents))
fileSystem.write(metadataFile) {
writeUtf8(corruptor(contents))
}
}
}
private fun testCorruptingCache(corruptor: () -> Unit): Response {
server.useHttps(handshakeCertificates.sslSocketFactory(), false)
server.enqueue(MockResponse()
server.enqueue(
MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setBody("ABC.1"))
server.enqueue(MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setBody("ABC.2"))
.setBody("ABC.1")
)
server.enqueue(
MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setBody("ABC.2")
)
client = client.newBuilder()
.sslSocketFactory(
handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager)
.hostnameVerifier(NULL_HOSTNAME_VERIFIER)
.build()
.sslSocketFactory(
handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager
)
.hostnameVerifier(NULL_HOSTNAME_VERIFIER)
.build()
val request: Request = Request.Builder().url(server.url("/")).build()
val response1: Response = client.newCall(request).execute()
val bodySource = response1.body!!.source()

View File

@@ -38,7 +38,6 @@ import mockwebserver3.MockResponse;
import mockwebserver3.MockWebServer;
import mockwebserver3.RecordedRequest;
import okhttp3.internal.Internal;
import okhttp3.internal.io.InMemoryFileSystem;
import okhttp3.internal.platform.Platform;
import okhttp3.testing.PlatformRule;
import okhttp3.tls.HandshakeCertificates;
@@ -47,6 +46,8 @@ import okio.BufferedSink;
import okio.BufferedSource;
import okio.GzipSink;
import okio.Okio;
import okio.Path;
import okio.fakefilesystem.FakeFileSystem;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
@@ -64,7 +65,7 @@ import static org.junit.jupiter.api.Assertions.fail;
public final class CacheTest {
private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = (name, session) -> true;
@RegisterExtension public InMemoryFileSystem fileSystem = new InMemoryFileSystem();
public FakeFileSystem fileSystem = new FakeFileSystem();
@RegisterExtension public final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule();
@RegisterExtension public final PlatformRule platform = new PlatformRule();
@@ -83,7 +84,7 @@ public final class CacheTest {
platform.assumeNotBouncyCastle();
server.setProtocolNegotiationEnabled(false);
cache = new Cache(new File("/cache/"), Integer.MAX_VALUE, fileSystem);
cache = new Cache(Path.get("/cache/"), Integer.MAX_VALUE, fileSystem);
client = clientTestRule.newClientBuilder()
.cache(cache)
.cookieJar(new JavaNetCookieJar(cookieManager))
@@ -2061,10 +2062,11 @@ public final class CacheTest {
+ "2\n"
+ "\n"
+ "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
writeFile(cache.directory(), urlKey + ".0", entryMetadata);
writeFile(cache.directory(), urlKey + ".1", entryBody);
writeFile(cache.directory(), "journal", journalBody);
cache = new Cache(cache.directory(), Integer.MAX_VALUE, fileSystem);
fileSystem.createDirectory(cache.directoryPath());
writeFile(cache.directoryPath(), urlKey + ".0", entryMetadata);
writeFile(cache.directoryPath(), urlKey + ".1", entryBody);
writeFile(cache.directoryPath(), "journal", journalBody);
cache = new Cache(Path.get(cache.directory().getPath()), Integer.MAX_VALUE, fileSystem);
client = client.newBuilder()
.cache(cache)
.build();
@@ -2110,11 +2112,12 @@ public final class CacheTest {
+ "\n"
+ "DIRTY " + urlKey + "\n"
+ "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
writeFile(cache.directory(), urlKey + ".0", entryMetadata);
writeFile(cache.directory(), urlKey + ".1", entryBody);
writeFile(cache.directory(), "journal", journalBody);
fileSystem.createDirectory(cache.directoryPath());
writeFile(cache.directoryPath(), urlKey + ".0", entryMetadata);
writeFile(cache.directoryPath(), urlKey + ".1", entryBody);
writeFile(cache.directoryPath(), "journal", journalBody);
cache.close();
cache = new Cache(cache.directory(), Integer.MAX_VALUE, fileSystem);
cache = new Cache(Path.get(cache.directory().getPath()), Integer.MAX_VALUE, fileSystem);
client = client.newBuilder()
.cache(cache)
.build();
@@ -2160,11 +2163,12 @@ public final class CacheTest {
+ "\n"
+ "DIRTY " + urlKey + "\n"
+ "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
writeFile(cache.directory(), urlKey + ".0", entryMetadata);
writeFile(cache.directory(), urlKey + ".1", entryBody);
writeFile(cache.directory(), "journal", journalBody);
fileSystem.createDirectory(cache.directoryPath());
writeFile(cache.directoryPath(), urlKey + ".0", entryMetadata);
writeFile(cache.directoryPath(), urlKey + ".1", entryBody);
writeFile(cache.directoryPath(), "journal", journalBody);
cache.close();
cache = new Cache(cache.directory(), Integer.MAX_VALUE, fileSystem);
cache = new Cache(Path.get(cache.directory().getPath()), Integer.MAX_VALUE, fileSystem);
client = client.newBuilder()
.cache(cache)
.build();
@@ -2197,11 +2201,12 @@ public final class CacheTest {
+ "\n"
+ "DIRTY " + urlKey + "\n"
+ "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
writeFile(cache.directory(), urlKey + ".0", entryMetadata);
writeFile(cache.directory(), urlKey + ".1", entryBody);
writeFile(cache.directory(), "journal", journalBody);
fileSystem.createDirectory(cache.directoryPath());
writeFile(cache.directoryPath(), urlKey + ".0", entryMetadata);
writeFile(cache.directoryPath(), urlKey + ".1", entryBody);
writeFile(cache.directoryPath(), "journal", journalBody);
cache.close();
cache = new Cache(cache.directory(), Integer.MAX_VALUE, fileSystem);
cache = new Cache(Path.get(cache.directory().getPath()), Integer.MAX_VALUE, fileSystem);
client = client.newBuilder()
.cache(cache)
.build();
@@ -2496,8 +2501,8 @@ public final class CacheTest {
return client.newCall(request).execute();
}
private void writeFile(File directory, String file, String content) throws IOException {
BufferedSink sink = Okio.buffer(fileSystem.sink(new File(directory, file)));
private void writeFile(Path directory, String file, String content) throws IOException {
BufferedSink sink = Okio.buffer(fileSystem.sink(directory.resolve(file)));
sink.writeUtf8(content);
sink.close();
}

View File

@@ -15,7 +15,6 @@
*/
package okhttp3;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -69,7 +68,7 @@ import okhttp3.internal.DoubleInetAddressDns;
import okhttp3.internal.RecordingOkAuthenticator;
import okhttp3.internal.Util;
import okhttp3.internal.http.RecordingProxySelector;
import okhttp3.internal.io.InMemoryFileSystem;
import okhttp3.okio.LoggingFilesystem;
import okhttp3.testing.Flaky;
import okhttp3.testing.PlatformRule;
import okhttp3.tls.HandshakeCertificates;
@@ -80,6 +79,8 @@ import okio.BufferedSource;
import okio.ForwardingSource;
import okio.GzipSink;
import okio.Okio;
import okio.Path;
import okio.fakefilesystem.FakeFileSystem;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
@@ -104,7 +105,7 @@ import static org.junit.jupiter.api.Assumptions.assumeFalse;
@Timeout(30)
public final class CallTest {
@RegisterExtension final PlatformRule platform = new PlatformRule();
@RegisterExtension final InMemoryFileSystem fileSystem = new InMemoryFileSystem();
final FakeFileSystem fileSystem = new FakeFileSystem();
@RegisterExtension final OkHttpClientTestRule clientTestRule = new OkHttpClientTestRule();
@RegisterExtension final TestLogHandler testLogHandler = new TestLogHandler(OkHttpClient.class);
@@ -116,7 +117,7 @@ public final class CallTest {
.eventListenerFactory(clientTestRule.wrap(listener))
.build();
private RecordingCallback callback = new RecordingCallback();
private Cache cache = new Cache(new File("/cache/"), Integer.MAX_VALUE, fileSystem);
private Cache cache = new Cache(Path.get("/cache"), Integer.MAX_VALUE, new LoggingFilesystem(fileSystem));
public CallTest(MockWebServer server, MockWebServer server2) {
this.server = server;
@@ -129,7 +130,8 @@ public final class CallTest {
}
@AfterEach public void tearDown() throws Exception {
cache.delete();
cache.close();
fileSystem.checkNoOpenFiles();
}
@Test public void get() throws Exception {

File diff suppressed because it is too large Load Diff

View File

@@ -1,114 +0,0 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* 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 okhttp3.internal.io;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;
import okio.Buffer;
import okio.ForwardingSink;
import okio.Sink;
import okio.Source;
public final class FaultyFileSystem implements FileSystem {
private final FileSystem delegate;
private final Set<File> writeFaults = new LinkedHashSet<>();
private final Set<File> deleteFaults = new LinkedHashSet<>();
private final Set<File> renameFaults = new LinkedHashSet<>();
public FaultyFileSystem(FileSystem delegate) {
this.delegate = delegate;
}
public void setFaultyWrite(File file, boolean faulty) {
if (faulty) {
writeFaults.add(file);
} else {
writeFaults.remove(file);
}
}
public void setFaultyDelete(File file, boolean faulty) {
if (faulty) {
deleteFaults.add(file);
} else {
deleteFaults.remove(file);
}
}
public void setFaultyRename(File file, boolean faulty) {
if (faulty) {
renameFaults.add(file);
} else {
renameFaults.remove(file);
}
}
@Override public Source source(File file) throws FileNotFoundException {
return delegate.source(file);
}
@Override public Sink sink(File file) throws FileNotFoundException {
return new FaultySink(delegate.sink(file), file);
}
@Override public Sink appendingSink(File file) throws FileNotFoundException {
return new FaultySink(delegate.appendingSink(file), file);
}
@Override public void delete(File file) throws IOException {
if (deleteFaults.contains(file)) throw new IOException("boom!");
delegate.delete(file);
}
@Override public boolean exists(File file) {
return delegate.exists(file);
}
@Override public long size(File file) {
return delegate.size(file);
}
@Override public void rename(File from, File to) throws IOException {
if (renameFaults.contains(from) || renameFaults.contains(to)) throw new IOException("boom!");
delegate.rename(from, to);
}
@Override public void deleteContents(File directory) throws IOException {
if (deleteFaults.contains(directory)) throw new IOException("boom!");
delegate.deleteContents(directory);
}
private class FaultySink extends ForwardingSink {
private final File file;
public FaultySink(Sink delegate, File file) {
super(delegate);
this.file = file;
}
@Override public void write(Buffer source, long byteCount) throws IOException {
if (writeFaults.contains(file)) throw new IOException("boom!");
super.write(source, byteCount);
}
}
@Override public String toString() {
return "Faulty " + delegate;
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* 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 okhttp3.internal.io
import okio.Buffer
import okio.ExperimentalFileSystem
import okio.FileSystem
import okio.ForwardingFileSystem
import okio.ForwardingSink
import okio.Path
import okio.Sink
import java.io.IOException
import java.util.LinkedHashSet
@OptIn(ExperimentalFileSystem::class)
class FaultyFileSystem constructor(delegate: FileSystem?) : ForwardingFileSystem(delegate!!) {
private val writeFaults: MutableSet<Path> = LinkedHashSet()
private val deleteFaults: MutableSet<Path> = LinkedHashSet()
private val renameFaults: MutableSet<Path> = LinkedHashSet()
fun setFaultyWrite(file: Path, faulty: Boolean) {
if (faulty) {
writeFaults.add(file)
} else {
writeFaults.remove(file)
}
}
fun setFaultyDelete(file: Path, faulty: Boolean) {
if (faulty) {
deleteFaults.add(file)
} else {
deleteFaults.remove(file)
}
}
fun setFaultyRename(file: Path, faulty: Boolean) {
if (faulty) {
renameFaults.add(file)
} else {
renameFaults.remove(file)
}
}
@Throws(IOException::class)
override fun atomicMove(source: Path, target: Path) {
if (renameFaults.contains(source) || renameFaults.contains(target)) throw IOException("boom!")
super.atomicMove(source, target)
}
@Throws(IOException::class)
override fun delete(path: Path) {
if (deleteFaults.contains(path)) throw IOException("boom!")
super.delete(path)
}
@Throws(IOException::class)
override fun deleteRecursively(fileOrDirectory: Path) {
if (deleteFaults.contains(fileOrDirectory)) throw IOException("boom!")
super.deleteRecursively(fileOrDirectory)
}
override fun appendingSink(file: Path): Sink = FaultySink(super.appendingSink(file), file)
override fun sink(file: Path): Sink = FaultySink(super.sink(file), file)
inner class FaultySink(sink: Sink, private val file: Path) : ForwardingSink(sink) {
override fun write(source: Buffer, byteCount: Long) {
if (writeFaults.contains(file)) {
throw IOException("boom!")
} else {
super.write(source, byteCount)
}
}
}
}

View File

@@ -1,175 +0,0 @@
/*
* Copyright (C) 2020 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 okhttp3.internal.io
import okhttp3.SimpleProvider
import okhttp3.TestUtil
import okio.buffer
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.io.TempDir
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ArgumentsSource
import java.io.File
import java.io.IOException
class FileSystemParamProvider: SimpleProvider() {
override fun arguments() = listOf(
FileSystem.SYSTEM to TestUtil.windows,
InMemoryFileSystem() to false,
WindowsFileSystem(FileSystem.SYSTEM) to true,
WindowsFileSystem(InMemoryFileSystem()) to true
)
}
/**
* Test that our file system abstraction is consistent and sufficient for OkHttp's needs. We're
* particularly interested in what happens when open files are moved or deleted on Windows.
*/
class FileSystemTest {
@TempDir lateinit var temporaryFolder: File
private lateinit var fileSystem: FileSystem
private var windows: Boolean = false
internal fun setUp(fileSystem: FileSystem, windows: Boolean) {
this.fileSystem = fileSystem
this.windows = windows
}
@ParameterizedTest
@ArgumentsSource(FileSystemParamProvider::class)
fun `delete open for writing fails on Windows`(
parameters: Pair<FileSystem, Boolean>
) {
setUp(parameters.first, parameters.second)
val file = File(temporaryFolder, "file.txt")
expectIOExceptionOnWindows {
fileSystem.sink(file).use {
fileSystem.delete(file)
}
}
}
@ParameterizedTest
@ArgumentsSource(FileSystemParamProvider::class)
fun `delete open for appending fails on Windows`(
parameters: Pair<FileSystem, Boolean>
) {
setUp(parameters.first, parameters.second)
val file = File(temporaryFolder, "file.txt")
file.write("abc")
expectIOExceptionOnWindows {
fileSystem.appendingSink(file).use {
fileSystem.delete(file)
}
}
}
@ParameterizedTest
@ArgumentsSource(FileSystemParamProvider::class)
fun `delete open for reading fails on Windows`(
parameters: Pair<FileSystem, Boolean>
) {
setUp(parameters.first, parameters.second)
val file = File(temporaryFolder, "file.txt")
file.write("abc")
expectIOExceptionOnWindows {
fileSystem.source(file).use {
fileSystem.delete(file)
}
}
}
@ParameterizedTest
@ArgumentsSource(FileSystemParamProvider::class)
fun `rename target exists succeeds on all platforms`(
parameters: Pair<FileSystem, Boolean>
) {
setUp(parameters.first, parameters.second)
val from = File(temporaryFolder, "from.txt")
val to = File(temporaryFolder, "to.txt")
from.write("source file")
to.write("target file")
fileSystem.rename(from, to)
}
@ParameterizedTest
@ArgumentsSource(FileSystemParamProvider::class)
fun `rename source is open fails on Windows`(
parameters: Pair<FileSystem, Boolean>
) {
setUp(parameters.first, parameters.second)
val from = File(temporaryFolder, "from.txt")
val to = File(temporaryFolder, "to.txt")
from.write("source file")
to.write("target file")
expectIOExceptionOnWindows {
fileSystem.source(from).use {
fileSystem.rename(from, to)
}
}
}
@ParameterizedTest
@ArgumentsSource(FileSystemParamProvider::class)
fun `rename target is open fails on Windows`(
parameters: Pair<FileSystem, Boolean>
) {
setUp(parameters.first, parameters.second)
val from = File(temporaryFolder, "from.txt")
val to = File(temporaryFolder, "to.txt")
from.write("source file")
to.write("target file")
expectIOExceptionOnWindows {
fileSystem.source(to).use {
fileSystem.rename(from, to)
}
}
}
@ParameterizedTest
@ArgumentsSource(FileSystemParamProvider::class)
fun `delete contents of parent of file open for reading fails on Windows`(
parameters: Pair<FileSystem, Boolean>
) {
setUp(parameters.first, parameters.second)
val parentA = File(temporaryFolder, "a").also { it.mkdirs() }
val parentAB = File(parentA, "b")
val parentABC = File(parentAB, "c")
val file = File(parentABC, "file.txt")
file.write("child file")
expectIOExceptionOnWindows {
fileSystem.source(file).use {
fileSystem.deleteContents(parentA)
}
}
}
private fun File.write(content: String) {
fileSystem.sink(this).buffer().use {
it.writeUtf8(content)
}
}
private fun expectIOExceptionOnWindows(block: () -> Unit) {
try {
block()
assertThat(windows).isFalse()
} catch (_: IOException) {
assertThat(windows).isTrue()
}
}
}