1 /* 2 * Copyright 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.datastore.core.okio 18 19 import kotlin.jvm.JvmName 20 import okio.FileHandle 21 import okio.FileMetadata 22 import okio.FileNotFoundException 23 import okio.FileSystem 24 import okio.Path 25 import okio.Sink 26 import okio.Source 27 28 /** 29 * Used to test the race condition that may occur during [OkioReadScope.readData] when a file does 30 * not exist during a call to read but is actually being created in different process slightly 31 * later. 32 * 33 * This class wraps a [FileSystem] and overrides the [source] function to throw a 34 * [FileNotFoundException] at the first read attempt, but to return the read value as expected in 35 * the second read attempt. 36 */ 37 class OkioFakeFileSystem(@get:JvmName("delegate") val delegate: FileSystem) : FileSystem() { 38 private var fileReadAttempt = 0 39 appendingSinknull40 override fun appendingSink(file: Path, mustExist: Boolean): Sink { 41 return delegate.appendingSink(file, mustExist) 42 } 43 atomicMovenull44 override fun atomicMove(source: Path, target: Path) { 45 delegate.atomicMove(source, target) 46 } 47 canonicalizenull48 override fun canonicalize(path: Path): Path { 49 return delegate.canonicalize(path) 50 } 51 createDirectorynull52 override fun createDirectory(dir: Path, mustCreate: Boolean) { 53 delegate.createDirectory(dir, mustCreate) 54 } 55 createSymlinknull56 override fun createSymlink(source: Path, target: Path) { 57 delegate.createSymlink(source, target) 58 } 59 deletenull60 override fun delete(path: Path, mustExist: Boolean) { 61 delegate.delete(path, mustExist) 62 } 63 listnull64 override fun list(dir: Path): List<Path> { 65 return delegate.list(dir) 66 } 67 listOrNullnull68 override fun listOrNull(dir: Path): List<Path>? { 69 return delegate.listOrNull(dir) 70 } 71 metadataOrNullnull72 override fun metadataOrNull(path: Path): FileMetadata? { 73 return delegate.metadataOrNull(path) 74 } 75 openReadOnlynull76 override fun openReadOnly(file: Path): FileHandle { 77 return delegate.openReadOnly(file) 78 } 79 openReadWritenull80 override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle { 81 return delegate.openReadWrite(file, mustCreate, mustExist) 82 } 83 sinknull84 override fun sink(file: Path, mustCreate: Boolean): Sink { 85 return delegate.sink(file, mustCreate) 86 } 87 sourcenull88 override fun source(file: Path): Source { 89 // This function is invoked during fileSystem.read(). We change the behaviour to throw 90 // an exception in the first read attempt, and return the correct value in the second to 91 // mimic the race condition. 92 fileReadAttempt++ 93 return if (fileReadAttempt <= 1) { 94 // First attempt should throw. 95 throw FileNotFoundException("Intentional failure to mimic race condition.") 96 } else { 97 // Second attempt onwards returns read value as expected, if the file exists. Throws an 98 // [IOException] otherwise. 99 delegate.source(file) 100 } 101 } 102 } 103