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:
12
build.gradle
12
build.gradle
@@ -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:
|
||||
|
@@ -9,6 +9,7 @@ dependencies {
|
||||
implementation deps.junit5Api
|
||||
implementation deps.junit5JupiterEngine
|
||||
implementation deps.junitPlatformConsole
|
||||
implementation deps.okioFakeFileSystem
|
||||
|
||||
implementation project(':okhttp')
|
||||
implementation project(':okhttp-brotli')
|
||||
|
@@ -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"
|
||||
}
|
@@ -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™"
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
@@ -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()
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
89
okhttp/src/test/java/okhttp3/internal/io/FaultyFileSystem.kt
Normal file
89
okhttp/src/test/java/okhttp3/internal/io/FaultyFileSystem.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user