1 /*
2  * Copyright 2022 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
18 
19 import androidx.datastore.core.DataStore
20 import androidx.datastore.core.DataStoreFactory.create
21 import androidx.datastore.core.InterProcessCoordinator
22 import androidx.datastore.core.Storage
23 import kotlin.random.Random
24 import kotlin.reflect.KClass
25 import kotlinx.coroutines.CoroutineScope
26 
27 abstract class TestIO<F : TestFile<F>, IOE : Throwable>(getTmpDir: () -> F) {
28     private val testRoot: F =
<lambda>null29         getTmpDir().let { createNewRandomChild(parent = it, namePrefix = "datastore-testio-") }
30 
31     init {
32         testRoot.mkdirs(mustCreate = true)
33     }
34 
getStorenull35     fun getStore(
36         serializerConfig: TestingSerializerConfig,
37         scope: CoroutineScope,
38         coordinatorProducer: () -> InterProcessCoordinator,
39         futureFile: () -> F
40     ): DataStore<Byte> {
41         return create(getStorage(serializerConfig, coordinatorProducer, futureFile), scope = scope)
42     }
43 
getStoragenull44     abstract fun getStorage(
45         serializerConfig: TestingSerializerConfig,
46         coordinatorProducer: () -> InterProcessCoordinator,
47         futureFile: () -> F = { newTempFile() }
48     ): Storage<Byte>
49 
randomNamenull50     private fun randomName(prefix: String): String {
51         return prefix +
52             (0 until 15).joinToString(separator = "") {
53                 ('a' + Random.nextInt(from = 0, until = 26)).toString()
54             }
55     }
56 
createNewRandomChildnull57     protected fun createNewRandomChild(
58         parent: F,
59         namePrefix: String = "test-file-",
60     ): F {
61         while (true) {
62             val child = parent.resolve(randomName(namePrefix))
63             if (!child.exists()) {
64                 return child
65             }
66         }
67     }
68 
69     /** Returns a new file instance without creating it or its parents. */
newTempFilenull70     fun newTempFile(parentFile: F? = null, relativePath: String? = null): F {
71         val parent = parentFile ?: testRoot
72         return if (relativePath == null) {
73             createNewRandomChild(parent = parent)
74         } else {
75             parent.resolve(relativePath)
76         }
77     }
78 
ioExceptionnull79     abstract fun ioException(message: String): IOE
80 
81     abstract fun ioExceptionClass(): KClass<IOE>
82 }
83 
84 abstract class TestFile<T : TestFile<T>> {
85     /** The name of the file, including the extension */
86     abstract val name: String
87 
88     /** Get the canonical path for the file. */
89     abstract fun path(): String
90 
91     /**
92      * Deletes the file if it exists. Will return `false` if the file does not exist or cannot be
93      * deleted. (similar to File.delete)
94      */
95     abstract fun delete(): Boolean
96 
97     /** Returns true if this file/directory exists. */
98     abstract fun exists(): Boolean
99 
100     /**
101      * Creates a directory from the file.
102      *
103      * If it is a regular file, will throw. If [mustCreate] is `true` and directory already exists,
104      * will throw.
105      */
106     abstract fun mkdirs(mustCreate: Boolean = false)
107 
108     /** Returns `true` if this exists and a regular file on the filesystem. */
109     abstract fun isRegularFile(): Boolean
110 
111     /** Returns `true` if this exists and is a directory on the filesystem. */
112     abstract fun isDirectory(): Boolean
113 
114     /**
115      * Resolves the given [relative] relative to `this`. (similar to File.resolve in kotlin stdlib).
116      *
117      * Note that this path is sanitized to ensure it is not a root path (e.g. does not start with
118      * `/`)
119      */
120     fun resolve(relative: String): T {
121         return if (relative.startsWith("/")) {
122             protectedResolve(relative.substring(startIndex = 1))
123         } else {
124             protectedResolve(relative)
125         }
126     }
127 
128     /** Implemented by the subclasses to resolve child from sanitized name. */
129     abstract fun protectedResolve(relative: String): T
130 
131     /** Returns the parent file or null if this has no parent. */
132     abstract fun parentFile(): T?
133 
134     /**
135      * Writes the given [body] test into the file using the default encoding (kotlin stdlib's
136      * String.encodeToByteArray)
137      */
138     fun write(body: String) {
139         write(body.encodeToByteArray())
140     }
141 
142     /**
143      * Overrides the file with the given contents. If parent directories do not exist, they'll be
144      * created.
145      */
146     fun write(body: ByteArray) {
147         if (isDirectory()) {
148             error("Cannot write to a directory")
149         }
150         parentFile()?.mkdirs(mustCreate = false)
151         parentFile()?.mkdirs()
152         protectedWrite(body)
153     }
154 
155     /** Reads the byte contents of the file. */
156     fun readBytes(): ByteArray {
157         check(exists()) { "File does not exist" }
158         return protectedReadBytes()
159     }
160 
161     fun readText() = readBytes().decodeToString()
162 
163     /**
164      * Writes the given [body] into the file. This is called after necessary checks are done so
165      * implementers should only focus on writing the contents.
166      */
167     abstract fun protectedWrite(body: ByteArray)
168 
169     /**
170      * Reads the byte contents of the file. This is called after necessary checks are done so
171      * implementers should only focus on reading the bytes.
172      */
173     abstract fun protectedReadBytes(): ByteArray
174 }
175