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