1 /*
2  * Copyright (C) 2020 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 android.util;
18 
19 import static org.junit.Assert.assertArrayEquals;
20 import static org.junit.Assert.assertTrue;
21 
22 import android.app.Instrumentation;
23 import android.content.Context;
24 
25 import androidx.annotation.NonNull;
26 import androidx.annotation.Nullable;
27 import androidx.test.platform.app.InstrumentationRegistry;
28 
29 import org.junit.After;
30 import org.junit.Before;
31 import org.junit.Test;
32 import org.junit.runner.RunWith;
33 import org.junit.runners.Parameterized;
34 
35 import java.io.ByteArrayOutputStream;
36 import java.io.File;
37 import java.io.FileInputStream;
38 import java.io.FileNotFoundException;
39 import java.io.FileOutputStream;
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.nio.charset.StandardCharsets;
43 
44 @RunWith(Parameterized.class)
45 public class AtomicFileTest {
46     private static final String BASE_NAME = "base";
47     private static final String NEW_NAME = BASE_NAME + ".new";
48     private static final String LEGACY_BACKUP_NAME = BASE_NAME + ".bak";
49     // The string isn't actually used, but we just need a different identifier.
50     private static final String BASE_NAME_DIRECTORY = BASE_NAME + ".dir";
51 
52     private enum WriteAction {
53         FINISH,
54         FAIL,
55         ABORT,
56         READ_FINISH
57     }
58 
59     private static final byte[] BASE_BYTES = "base".getBytes(StandardCharsets.UTF_8);
60     private static final byte[] EXISTING_NEW_BYTES = "unnew".getBytes(StandardCharsets.UTF_8);
61     private static final byte[] NEW_BYTES = "new".getBytes(StandardCharsets.UTF_8);
62     private static final byte[] LEGACY_BACKUP_BYTES = "bak".getBytes(StandardCharsets.UTF_8);
63 
64     // JUnit wants every parameter to be used so make it happy.
65     @Parameterized.Parameter()
66     public String mUnusedTestName;
67     @Nullable
68     @Parameterized.Parameter(1)
69     public String[] mExistingFileNames;
70     @Nullable
71     @Parameterized.Parameter(2)
72     public WriteAction mWriteAction;
73     @Nullable
74     @Parameterized.Parameter(3)
75     public byte[] mExpectedBytes;
76 
77     private final Instrumentation mInstrumentation =
78             InstrumentationRegistry.getInstrumentation();
79     private final Context mContext = mInstrumentation.getContext();
80 
81     private final File mDirectory = mContext.getFilesDir();
82     private final File mBaseFile = new File(mDirectory, BASE_NAME);
83     private final File mNewFile = new File(mDirectory, NEW_NAME);
84     private final File mLegacyBackupFile = new File(mDirectory, LEGACY_BACKUP_NAME);
85 
86     @Parameterized.Parameters(name = "{0}")
data()87     public static Object[][] data() {
88         return new Object[][] {
89                 // Standard tests.
90                 { "none + none = none", null, null, null },
91                 { "none + finish = new", null, WriteAction.FINISH, NEW_BYTES },
92                 { "none + fail = none", null, WriteAction.FAIL, null },
93                 { "none + abort = none", null, WriteAction.ABORT, null },
94                 { "base + none = base", new String[] { BASE_NAME }, null, BASE_BYTES },
95                 { "base + finish = new", new String[] { BASE_NAME }, WriteAction.FINISH,
96                         NEW_BYTES },
97                 { "base + fail = base", new String[] { BASE_NAME }, WriteAction.FAIL, BASE_BYTES },
98                 { "base + abort = base", new String[] { BASE_NAME }, WriteAction.ABORT,
99                         BASE_BYTES },
100                 { "new + none = none", new String[] { NEW_NAME }, null, null },
101                 { "new + finish = new", new String[] { NEW_NAME }, WriteAction.FINISH, NEW_BYTES },
102                 { "new + fail = none", new String[] { NEW_NAME }, WriteAction.FAIL, null },
103                 { "new + abort = none", new String[] { NEW_NAME }, WriteAction.ABORT, null },
104                 { "bak + none = bak", new String[] { LEGACY_BACKUP_NAME }, null,
105                         LEGACY_BACKUP_BYTES },
106                 { "bak + finish = new", new String[] { LEGACY_BACKUP_NAME }, WriteAction.FINISH,
107                         NEW_BYTES },
108                 { "bak + fail = bak", new String[] { LEGACY_BACKUP_NAME }, WriteAction.FAIL,
109                         LEGACY_BACKUP_BYTES },
110                 { "bak + abort = bak", new String[] { LEGACY_BACKUP_NAME }, WriteAction.ABORT,
111                         LEGACY_BACKUP_BYTES },
112                 { "base & new + none = base", new String[] { BASE_NAME, NEW_NAME }, null,
113                         BASE_BYTES },
114                 { "base & new + finish = new", new String[] { BASE_NAME, NEW_NAME },
115                         WriteAction.FINISH, NEW_BYTES },
116                 { "base & new + fail = base", new String[] { BASE_NAME, NEW_NAME },
117                         WriteAction.FAIL, BASE_BYTES },
118                 { "base & new + abort = base", new String[] { BASE_NAME, NEW_NAME },
119                         WriteAction.ABORT, BASE_BYTES },
120                 { "base & bak + none = bak", new String[] { BASE_NAME, LEGACY_BACKUP_NAME }, null,
121                         LEGACY_BACKUP_BYTES },
122                 { "base & bak + finish = new", new String[] { BASE_NAME, LEGACY_BACKUP_NAME },
123                         WriteAction.FINISH, NEW_BYTES },
124                 { "base & bak + fail = bak", new String[] { BASE_NAME, LEGACY_BACKUP_NAME },
125                         WriteAction.FAIL, LEGACY_BACKUP_BYTES },
126                 { "base & bak + abort = bak", new String[] { BASE_NAME, LEGACY_BACKUP_NAME },
127                         WriteAction.ABORT, LEGACY_BACKUP_BYTES },
128                 { "new & bak + none = bak", new String[] { NEW_NAME, LEGACY_BACKUP_NAME }, null,
129                         LEGACY_BACKUP_BYTES },
130                 { "new & bak + finish = new", new String[] { NEW_NAME, LEGACY_BACKUP_NAME },
131                         WriteAction.FINISH, NEW_BYTES },
132                 { "new & bak + fail = bak", new String[] { NEW_NAME, LEGACY_BACKUP_NAME },
133                         WriteAction.FAIL, LEGACY_BACKUP_BYTES },
134                 { "new & bak + abort = bak", new String[] { NEW_NAME, LEGACY_BACKUP_NAME },
135                         WriteAction.ABORT, LEGACY_BACKUP_BYTES },
136                 { "base & new & bak + none = bak",
137                         new String[] { BASE_NAME, NEW_NAME, LEGACY_BACKUP_NAME }, null,
138                         LEGACY_BACKUP_BYTES },
139                 { "base & new & bak + finish = new",
140                         new String[] { BASE_NAME, NEW_NAME, LEGACY_BACKUP_NAME },
141                         WriteAction.FINISH, NEW_BYTES },
142                 { "base & new & bak + fail = bak",
143                         new String[] { BASE_NAME, NEW_NAME, LEGACY_BACKUP_NAME }, WriteAction.FAIL,
144                         LEGACY_BACKUP_BYTES },
145                 { "base & new & bak + abort = bak",
146                         new String[] { BASE_NAME, NEW_NAME, LEGACY_BACKUP_NAME }, WriteAction.ABORT,
147                         LEGACY_BACKUP_BYTES },
148                 // Compatibility when there is a directory in the place of base file, by replacing
149                 // no base with base.dir.
150                 { "base.dir + none = none", new String[] { BASE_NAME_DIRECTORY }, null, null },
151                 { "base.dir + finish = new", new String[] { BASE_NAME_DIRECTORY },
152                         WriteAction.FINISH, NEW_BYTES },
153                 { "base.dir + fail = none", new String[] { BASE_NAME_DIRECTORY }, WriteAction.FAIL,
154                         null },
155                 { "base.dir + abort = none", new String[] { BASE_NAME_DIRECTORY },
156                         WriteAction.ABORT, null },
157                 { "base.dir & new + none = none", new String[] { BASE_NAME_DIRECTORY, NEW_NAME },
158                         null, null },
159                 { "base.dir & new + finish = new", new String[] { BASE_NAME_DIRECTORY, NEW_NAME },
160                         WriteAction.FINISH, NEW_BYTES },
161                 { "base.dir & new + fail = none", new String[] { BASE_NAME_DIRECTORY, NEW_NAME },
162                         WriteAction.FAIL, null },
163                 { "base.dir & new + abort = none", new String[] { BASE_NAME_DIRECTORY, NEW_NAME },
164                         WriteAction.ABORT, null },
165                 { "base.dir & bak + none = bak",
166                         new String[] { BASE_NAME_DIRECTORY, LEGACY_BACKUP_NAME }, null,
167                         LEGACY_BACKUP_BYTES },
168                 { "base.dir & bak + finish = new",
169                         new String[] { BASE_NAME_DIRECTORY, LEGACY_BACKUP_NAME },
170                         WriteAction.FINISH, NEW_BYTES },
171                 { "base.dir & bak + fail = bak",
172                         new String[] { BASE_NAME_DIRECTORY, LEGACY_BACKUP_NAME }, WriteAction.FAIL,
173                         LEGACY_BACKUP_BYTES },
174                 { "base.dir & bak + abort = bak",
175                         new String[] { BASE_NAME_DIRECTORY, LEGACY_BACKUP_NAME }, WriteAction.ABORT,
176                         LEGACY_BACKUP_BYTES },
177                 { "base.dir & new & bak + none = bak",
178                         new String[] { BASE_NAME_DIRECTORY, NEW_NAME, LEGACY_BACKUP_NAME }, null,
179                         LEGACY_BACKUP_BYTES },
180                 { "base.dir & new & bak + finish = new",
181                         new String[] { BASE_NAME_DIRECTORY, NEW_NAME, LEGACY_BACKUP_NAME },
182                         WriteAction.FINISH, NEW_BYTES },
183                 { "base.dir & new & bak + fail = bak",
184                         new String[] { BASE_NAME_DIRECTORY, NEW_NAME, LEGACY_BACKUP_NAME },
185                         WriteAction.FAIL, LEGACY_BACKUP_BYTES },
186                 { "base.dir & new & bak + abort = bak",
187                         new String[] { BASE_NAME_DIRECTORY, NEW_NAME, LEGACY_BACKUP_NAME },
188                         WriteAction.ABORT, LEGACY_BACKUP_BYTES },
189                 // Compatibility when openRead() is called between startWrite() and finishWrite() -
190                 // the write should still succeed if it's the first write.
191                 { "none + read & finish = new", null, WriteAction.READ_FINISH, NEW_BYTES },
192         };
193     }
194 
195     @Before
196     @After
deleteFiles()197     public void deleteFiles() {
198         mBaseFile.delete();
199         mNewFile.delete();
200         mLegacyBackupFile.delete();
201     }
202 
203     @Test
testAtomicFile()204     public void testAtomicFile() throws Exception {
205         if (mExistingFileNames != null) {
206             for (String fileName : mExistingFileNames) {
207                 switch (fileName) {
208                     case BASE_NAME:
209                         writeBytes(mBaseFile, BASE_BYTES);
210                         break;
211                     case NEW_NAME:
212                         writeBytes(mNewFile, EXISTING_NEW_BYTES);
213                         break;
214                     case LEGACY_BACKUP_NAME:
215                         writeBytes(mLegacyBackupFile, LEGACY_BACKUP_BYTES);
216                         break;
217                     case BASE_NAME_DIRECTORY:
218                         assertTrue(mBaseFile.mkdir());
219                         break;
220                     default:
221                         throw new AssertionError(fileName);
222                 }
223             }
224         }
225 
226         AtomicFile atomicFile = new AtomicFile(mBaseFile);
227         if (mWriteAction != null) {
228             try (FileOutputStream outputStream = atomicFile.startWrite()) {
229                 outputStream.write(NEW_BYTES);
230                 switch (mWriteAction) {
231                     case FINISH:
232                         atomicFile.finishWrite(outputStream);
233                         break;
234                     case FAIL:
235                         atomicFile.failWrite(outputStream);
236                         break;
237                     case ABORT:
238                         // Neither finishing nor failing is called upon abort.
239                         break;
240                     case READ_FINISH:
241                         // We are only using this action when there is no base file.
242                         assertThrows(FileNotFoundException.class, atomicFile::openRead);
243                         atomicFile.finishWrite(outputStream);
244                         break;
245                     default:
246                         throw new AssertionError(mWriteAction);
247                 }
248             }
249         }
250 
251         if (mExpectedBytes != null) {
252             try (FileInputStream inputStream = atomicFile.openRead()) {
253                 assertArrayEquals(mExpectedBytes, readAllBytes(inputStream));
254             }
255         } else {
256             assertThrows(FileNotFoundException.class, atomicFile::openRead);
257         }
258     }
259 
writeBytes(@onNull File file, @NonNull byte[] bytes)260     private static void writeBytes(@NonNull File file, @NonNull byte[] bytes) throws IOException {
261         try (FileOutputStream outputStream = new FileOutputStream(file)) {
262             outputStream.write(bytes);
263         }
264     }
265 
266     // InputStream.readAllBytes() is introduced in Java 9. Our files are small enough so that a
267     // naive implementation is okay.
readAllBytes(@onNull InputStream inputStream)268     private static byte[] readAllBytes(@NonNull InputStream inputStream) throws IOException {
269         try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
270             int b;
271             while ((b = inputStream.read()) != -1) {
272                 outputStream.write(b);
273             }
274             return outputStream.toByteArray();
275         }
276     }
277 
278     @NonNull
assertThrows(@onNull Class<T> expectedType, @NonNull ThrowingRunnable runnable)279     public static <T extends Throwable> T assertThrows(@NonNull Class<T> expectedType,
280             @NonNull ThrowingRunnable runnable) {
281         try {
282             runnable.run();
283         } catch (Throwable t) {
284             if (!expectedType.isInstance(t)) {
285                 sneakyThrow(t);
286             }
287             //noinspection unchecked
288             return (T) t;
289         }
290         throw new AssertionError(String.format("Expected %s wasn't thrown",
291                 expectedType.getSimpleName()));
292     }
293 
sneakyThrow(@onNull Throwable throwable)294     private static <T extends Throwable> void sneakyThrow(@NonNull Throwable throwable) throws T {
295         //noinspection unchecked
296         throw (T) throwable;
297     }
298 
299     private interface ThrowingRunnable {
run()300         void run() throws Throwable;
301     }
302 }
303