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

View File

@@ -9,6 +9,7 @@ dependencies {
implementation deps.junit5Api implementation deps.junit5Api
implementation deps.junit5JupiterEngine implementation deps.junit5JupiterEngine
implementation deps.junitPlatformConsole implementation deps.junitPlatformConsole
implementation deps.okioFakeFileSystem
implementation project(':okhttp') implementation project(':okhttp')
implementation project(':okhttp-brotli') 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-brotli')
testImplementation project(':okhttp-dnsoverhttps') testImplementation project(':okhttp-dnsoverhttps')
testImplementation project(':okhttp-sse') testImplementation project(':okhttp-sse')
testImplementation deps.okioFakeFileSystem
testImplementation deps.conscrypt testImplementation deps.conscrypt
testImplementation deps.junit testImplementation deps.junit
testImplementation deps.junit5Api testImplementation deps.junit5Api

View File

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

View File

@@ -21,10 +21,12 @@ import okhttp3.Response
import okhttp3.internal.connection.Exchange import okhttp3.internal.connection.Exchange
import okhttp3.internal.connection.RealCall import okhttp3.internal.connection.RealCall
import okhttp3.internal.connection.RealConnection import okhttp3.internal.connection.RealConnection
import okhttp3.internal.io.FileSystem import okio.ExperimentalFileSystem
import java.io.File 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) return Cache(file, maxSize, fileSystem)
} }

View File

@@ -17,8 +17,27 @@
package okhttp3.internal 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.Closeable
import java.io.File import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.io.InterruptedIOException import java.io.InterruptedIOException
import java.net.InetSocketAddress import java.net.InetSocketAddress
@@ -38,24 +57,6 @@ import java.util.concurrent.ThreadFactory
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.text.Charsets.UTF_32BE import kotlin.text.Charsets.UTF_32BE
import kotlin.text.Charsets.UTF_32LE 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 @JvmField
val EMPTY_BYTE_ARRAY = ByteArray(0) 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! * @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 { sink(file).use {
try { try {
delete(file) delete(file)
@@ -542,6 +544,45 @@ fun FileSystem.isCivilized(file: File): Boolean {
return false 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 Long.toHexString(): String = java.lang.Long.toHexString(this)
fun Int.toHexString(): String = Integer.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.closeQuietly
import okhttp3.internal.concurrent.Task import okhttp3.internal.concurrent.Task
import okhttp3.internal.concurrent.TaskRunner 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.isCivilized
import okhttp3.internal.okHttpName import okhttp3.internal.okHttpName
import okhttp3.internal.platform.Platform import okhttp3.internal.platform.Platform
import okhttp3.internal.platform.Platform.Companion.WARN import okhttp3.internal.platform.Platform.Companion.WARN
import okio.* import okio.BufferedSink
import java.io.* 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.EOFException
import java.io.Flushable
import java.io.IOException 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 * 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 valueCount the number of values per cache entry. Must be positive.
* @param maxSize the maximum number of bytes this cache should use to store. * @param maxSize the maximum number of bytes this cache should use to store.
*/ */
@OptIn(ExperimentalFileSystem::class)
class DiskLruCache( class DiskLruCache(
internal val fileSystem: FileSystem, fileSystem: FileSystem,
/** Returns the directory where this cache stores its data. */ /** Returns the directory where this cache stores its data. */
val directory: File, val directory: Path,
private val appVersion: Int, private val appVersion: Int,
@@ -90,6 +102,18 @@ class DiskLruCache(
/** Used for asynchronous journal rebuilds. */ /** Used for asynchronous journal rebuilds. */
taskRunner: TaskRunner taskRunner: TaskRunner
) : Closeable, Flushable { ) : 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. */ /** The maximum number of bytes that this cache should use to store its data. */
@get:Synchronized @set:Synchronized var maxSize: Long = maxSize @get:Synchronized @set:Synchronized var maxSize: Long = maxSize
set(value) { set(value) {
@@ -139,9 +163,9 @@ class DiskLruCache(
* compaction; that file should be deleted if it exists when the cache is opened. * compaction; that file should be deleted if it exists when the cache is opened.
*/ */
private val journalFile: File private val journalFile: Path
private val journalFileTmp: File private val journalFileTmp: Path
private val journalFileBackup: File private val journalFileBackup: Path
private var size: Long = 0L private var size: Long = 0L
private var journalWriter: BufferedSink? = null private var journalWriter: BufferedSink? = null
internal val lruEntries = LinkedHashMap<String, Entry>(0, 0.75f, true) internal val lruEntries = LinkedHashMap<String, Entry>(0, 0.75f, true)
@@ -195,9 +219,9 @@ class DiskLruCache(
require(maxSize > 0L) { "maxSize <= 0" } require(maxSize > 0L) { "maxSize <= 0" }
require(valueCount > 0) { "valueCount <= 0" } require(valueCount > 0) { "valueCount <= 0" }
this.journalFile = File(directory, JOURNAL_FILE) this.journalFile = directory / JOURNAL_FILE
this.journalFileTmp = File(directory, JOURNAL_FILE_TEMP) this.journalFileTmp = directory / JOURNAL_FILE_TEMP
this.journalFileBackup = File(directory, JOURNAL_FILE_BACKUP) this.journalFileBackup = directory / JOURNAL_FILE_BACKUP
} }
@Synchronized @Throws(IOException::class) @Synchronized @Throws(IOException::class)
@@ -214,7 +238,7 @@ class DiskLruCache(
if (fileSystem.exists(journalFile)) { if (fileSystem.exists(journalFile)) {
fileSystem.delete(journalFileBackup) fileSystem.delete(journalFileBackup)
} else { } else {
fileSystem.rename(journalFileBackup, journalFile) fileSystem.atomicMove(journalFileBackup, journalFile)
} }
} }
@@ -250,12 +274,12 @@ class DiskLruCache(
@Throws(IOException::class) @Throws(IOException::class)
private fun readJournal() { private fun readJournal() {
fileSystem.source(journalFile).buffer().use { source -> fileSystem.read(journalFile) {
val magic = source.readUtf8LineStrict() val magic = readUtf8LineStrict()
val version = source.readUtf8LineStrict() val version = readUtf8LineStrict()
val appVersionString = source.readUtf8LineStrict() val appVersionString = readUtf8LineStrict()
val valueCountString = source.readUtf8LineStrict() val valueCountString = readUtf8LineStrict()
val blank = source.readUtf8LineStrict() val blank = readUtf8LineStrict()
if (MAGIC != magic || if (MAGIC != magic ||
VERSION_1 != version || VERSION_1 != version ||
@@ -269,7 +293,7 @@ class DiskLruCache(
var lineCount = 0 var lineCount = 0
while (true) { while (true) {
try { try {
readJournalLine(source.readUtf8LineStrict()) readJournalLine(readUtf8LineStrict())
lineCount++ lineCount++
} catch (_: EOFException) { } catch (_: EOFException) {
break // End of journal. break // End of journal.
@@ -279,7 +303,7 @@ class DiskLruCache(
redundantOpCount = lineCount - lruEntries.size redundantOpCount = lineCount - lruEntries.size
// If we ended on a truncated line, rebuild the journal before appending to it. // If we ended on a truncated line, rebuild the journal before appending to it.
if (!source.exhausted()) { if (!exhausted()) {
rebuildJournal() rebuildJournal()
} else { } else {
journalWriter = newJournalWriter() journalWriter = newJournalWriter()
@@ -348,7 +372,7 @@ class DiskLruCache(
*/ */
@Throws(IOException::class) @Throws(IOException::class)
private fun processJournal() { private fun processJournal() {
fileSystem.delete(journalFileTmp) fileSystem.deleteIfExists(journalFileTmp)
val i = lruEntries.values.iterator() val i = lruEntries.values.iterator()
while (i.hasNext()) { while (i.hasNext()) {
val entry = i.next() val entry = i.next()
@@ -359,8 +383,8 @@ class DiskLruCache(
} else { } else {
entry.currentEditor = null entry.currentEditor = null
for (t in 0 until valueCount) { for (t in 0 until valueCount) {
fileSystem.delete(entry.cleanFiles[t]) fileSystem.deleteIfExists(entry.cleanFiles[t])
fileSystem.delete(entry.dirtyFiles[t]) fileSystem.deleteIfExists(entry.dirtyFiles[t])
} }
i.remove() i.remove()
} }
@@ -375,32 +399,34 @@ class DiskLruCache(
internal fun rebuildJournal() { internal fun rebuildJournal() {
journalWriter?.close() journalWriter?.close()
fileSystem.sink(journalFileTmp).buffer().use { sink -> fileSystem.write(journalFileTmp) {
sink.writeUtf8(MAGIC).writeByte('\n'.toInt()) writeUtf8(MAGIC).writeByte('\n'.toInt())
sink.writeUtf8(VERSION_1).writeByte('\n'.toInt()) writeUtf8(VERSION_1).writeByte('\n'.toInt())
sink.writeDecimalLong(appVersion.toLong()).writeByte('\n'.toInt()) writeDecimalLong(appVersion.toLong()).writeByte('\n'.toInt())
sink.writeDecimalLong(valueCount.toLong()).writeByte('\n'.toInt()) writeDecimalLong(valueCount.toLong()).writeByte('\n'.toInt())
sink.writeByte('\n'.toInt()) writeByte('\n'.toInt())
for (entry in lruEntries.values) { for (entry in lruEntries.values) {
if (entry.currentEditor != null) { if (entry.currentEditor != null) {
sink.writeUtf8(DIRTY).writeByte(' '.toInt()) writeUtf8(DIRTY).writeByte(' '.toInt())
sink.writeUtf8(entry.key) writeUtf8(entry.key)
sink.writeByte('\n'.toInt()) writeByte('\n'.toInt())
} else { } else {
sink.writeUtf8(CLEAN).writeByte(' '.toInt()) writeUtf8(CLEAN).writeByte(' '.toInt())
sink.writeUtf8(entry.key) writeUtf8(entry.key)
entry.writeLengths(sink) entry.writeLengths(this)
sink.writeByte('\n'.toInt()) writeByte('\n'.toInt())
} }
} }
} }
if (fileSystem.exists(journalFile)) { 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() journalWriter = newJournalWriter()
hasJournalErrors = false hasJournalErrors = false
@@ -519,14 +545,15 @@ class DiskLruCache(
if (success && !entry.zombie) { if (success && !entry.zombie) {
if (fileSystem.exists(dirty)) { if (fileSystem.exists(dirty)) {
val clean = entry.cleanFiles[i] val clean = entry.cleanFiles[i]
fileSystem.rename(dirty, clean) fileSystem.atomicMove(dirty, clean)
val oldLength = entry.lengths[i] 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 entry.lengths[i] = newLength
size = size - oldLength + newLength size = size - oldLength + newLength
} }
} else { } else {
fileSystem.delete(dirty) fileSystem.deleteIfExists(dirty)
} }
} }
@@ -613,7 +640,7 @@ class DiskLruCache(
entry.currentEditor?.detach() // Prevent the edit from completing normally. entry.currentEditor?.detach() // Prevent the edit from completing normally.
for (i in 0 until valueCount) { for (i in 0 until valueCount) {
fileSystem.delete(entry.cleanFiles[i]) fileSystem.deleteIfExists(entry.cleanFiles[i])
size -= entry.lengths[i] size -= entry.lengths[i]
entry.lengths[i] = 0 entry.lengths[i] = 0
} }
@@ -916,8 +943,8 @@ class DiskLruCache(
/** Lengths of this entry's files. */ /** Lengths of this entry's files. */
internal val lengths: LongArray = LongArray(valueCount) internal val lengths: LongArray = LongArray(valueCount)
internal val cleanFiles = mutableListOf<File>() internal val cleanFiles = mutableListOf<Path>()
internal val dirtyFiles = mutableListOf<File>() internal val dirtyFiles = mutableListOf<Path>()
/** True if this entry has ever been published. */ /** True if this entry has ever been published. */
internal var readable: Boolean = false internal var readable: Boolean = false
@@ -946,9 +973,9 @@ class DiskLruCache(
val truncateTo = fileBuilder.length val truncateTo = fileBuilder.length
for (i in 0 until valueCount) { for (i in 0 until valueCount) {
fileBuilder.append(i) fileBuilder.append(i)
cleanFiles += File(directory, fileBuilder.toString()) cleanFiles += directory / fileBuilder.toString()
fileBuilder.append(".tmp") fileBuilder.append(".tmp")
dirtyFiles += File(directory, fileBuilder.toString()) dirtyFiles += directory / fileBuilder.toString()
fileBuilder.setLength(truncateTo) 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.MockResponse
import mockwebserver3.MockWebServer import mockwebserver3.MockWebServer
import okhttp3.internal.buildCache import okhttp3.internal.buildCache
import okhttp3.internal.io.InMemoryFileSystem import okhttp3.okio.LoggingFilesystem
import okhttp3.testing.PlatformRule import okhttp3.testing.PlatformRule
import okhttp3.tls.internal.TlsUtil.localhost 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.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension import org.junit.jupiter.api.extension.RegisterExtension
import java.io.File
import java.net.CookieManager import java.net.CookieManager
import java.net.ResponseCache import java.net.ResponseCache
import java.text.DateFormat import java.text.DateFormat
@@ -38,12 +40,20 @@ import java.util.concurrent.TimeUnit
import javax.net.ssl.HostnameVerifier import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLSession import javax.net.ssl.SSLSession
@OptIn(ExperimentalFileSystem::class)
class CacheCorruptionTest( class CacheCorruptionTest(
var server: MockWebServer var server: MockWebServer
) { ) {
@JvmField @RegisterExtension var fileSystem = InMemoryFileSystem() @OptIn(ExperimentalFileSystem::class)
@JvmField @RegisterExtension val clientTestRule = OkHttpClientTestRule() var fileSystem = FakeFileSystem()
@JvmField @RegisterExtension val platform = PlatformRule()
@JvmField
@RegisterExtension
val clientTestRule = OkHttpClientTestRule()
@JvmField
@RegisterExtension
val platform = PlatformRule()
private val handshakeCertificates = localhost() private val handshakeCertificates = localhost()
private lateinit var client: OkHttpClient private lateinit var client: OkHttpClient
@@ -52,25 +62,29 @@ class CacheCorruptionTest(
HostnameVerifier { _: String?, _: SSLSession? -> true } HostnameVerifier { _: String?, _: SSLSession? -> true }
private val cookieManager = CookieManager() private val cookieManager = CookieManager()
@BeforeEach fun setUp() { @BeforeEach
fun setUp() {
platform.assumeNotOpenJSSE() platform.assumeNotOpenJSSE()
platform.assumeNotBouncyCastle() platform.assumeNotBouncyCastle()
server.protocolNegotiationEnabled = false 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() client = clientTestRule.newClientBuilder()
.cache(cache) .cache(cache)
.cookieJar(JavaNetCookieJar(cookieManager)) .cookieJar(JavaNetCookieJar(cookieManager))
.build() .build()
} }
@AfterEach fun tearDown() { @AfterEach
fun tearDown() {
ResponseCache.setDefault(null) ResponseCache.setDefault(null)
if (this::cache.isInitialized) { if (this::cache.isInitialized) {
cache.delete() cache.delete()
} }
} }
@Test fun corruptedCipher() { @Test
fun corruptedCipher() {
val response = testCorruptingCache { val response = testCorruptingCache {
corruptMetadata { corruptMetadata {
// mess with cipher suite // mess with cipher suite
@@ -86,7 +100,8 @@ class CacheCorruptionTest(
assertThat(response.handshake?.cipherSuite?.javaName).startsWith("SLT_") assertThat(response.handshake?.cipherSuite?.javaName).startsWith("SLT_")
} }
@Test fun truncatedMetadataEntry() { @Test
fun truncatedMetadataEntry() {
val response = testCorruptingCache { val response = testCorruptingCache {
corruptMetadata { corruptMetadata {
// truncate metadata to 1/4 of length // truncate metadata to 1/4 of length
@@ -115,30 +130,41 @@ class CacheCorruptionTest(
} }
private fun corruptMetadata(corruptor: (String) -> String) { private fun corruptMetadata(corruptor: (String) -> String) {
val metadataFile = fileSystem.files.keys.find { it.name.endsWith(".0") } val metadataFile = fileSystem.allPaths.find {
val metadataBuffer = fileSystem.files[metadataFile] it.name.endsWith(".0")
}
val contents = metadataBuffer!!.peek().readUtf8() if (metadataFile != null) {
val contents = fileSystem.read(metadataFile) {
readUtf8()
}
metadataBuffer.clear() fileSystem.write(metadataFile) {
metadataBuffer.writeUtf8(corruptor(contents)) writeUtf8(corruptor(contents))
}
}
} }
private fun testCorruptingCache(corruptor: () -> Unit): Response { private fun testCorruptingCache(corruptor: () -> Unit): Response {
server.useHttps(handshakeCertificates.sslSocketFactory(), false) server.useHttps(handshakeCertificates.sslSocketFactory(), false)
server.enqueue(MockResponse() server.enqueue(
MockResponse()
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setBody("ABC.1")) .setBody("ABC.1")
server.enqueue(MockResponse() )
.addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) server.enqueue(
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) MockResponse()
.setBody("ABC.2")) .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setBody("ABC.2")
)
client = client.newBuilder() client = client.newBuilder()
.sslSocketFactory( .sslSocketFactory(
handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager) handshakeCertificates.sslSocketFactory(), handshakeCertificates.trustManager
.hostnameVerifier(NULL_HOSTNAME_VERIFIER) )
.build() .hostnameVerifier(NULL_HOSTNAME_VERIFIER)
.build()
val request: Request = Request.Builder().url(server.url("/")).build() val request: Request = Request.Builder().url(server.url("/")).build()
val response1: Response = client.newCall(request).execute() val response1: Response = client.newCall(request).execute()
val bodySource = response1.body!!.source() val bodySource = response1.body!!.source()

View File

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

View File

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