1 /*
2  * Copyright 2018 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 com.android.server.pm.dex;
18 
19 import static com.android.server.pm.dex.PackageDynamicCodeLoading.MAX_FILES_PER_OWNER;
20 import static com.android.server.pm.dex.PackageDynamicCodeLoading.escape;
21 import static com.android.server.pm.dex.PackageDynamicCodeLoading.unescape;
22 
23 import static com.google.common.truth.Truth.assertThat;
24 
25 import static org.junit.Assert.assertEquals;
26 import static org.junit.Assert.assertSame;
27 import static org.junit.Assert.assertTrue;
28 import static org.testng.Assert.assertFalse;
29 import static org.testng.Assert.assertThrows;
30 
31 import static java.nio.charset.StandardCharsets.UTF_8;
32 
33 import android.platform.test.annotations.Presubmit;
34 
35 import androidx.test.filters.SmallTest;
36 import androidx.test.runner.AndroidJUnit4;
37 
38 import com.android.server.pm.dex.PackageDynamicCodeLoading.DynamicCodeFile;
39 import com.android.server.pm.dex.PackageDynamicCodeLoading.PackageDynamicCode;
40 
41 import com.google.common.collect.ImmutableMap;
42 import com.google.common.collect.ImmutableSet;
43 
44 import org.junit.Test;
45 import org.junit.runner.RunWith;
46 
47 import java.io.ByteArrayInputStream;
48 import java.io.ByteArrayOutputStream;
49 import java.io.IOException;
50 import java.io.OutputStream;
51 import java.util.Map;
52 import java.util.Objects;
53 import java.util.Set;
54 
55 @Presubmit
56 @RunWith(AndroidJUnit4.class)
57 @SmallTest
58 public class PackageDynamicCodeLoadingTests {
59 
60     // Deliberately making a copy here since we're testing identity and
61     // string literals have a tendency to be identical.
62     private static final String TRIVIAL_STRING = new String("hello/world");
63     private static final Entry[] NO_ENTRIES = {};
64     private static final String[] NO_PACKAGES = {};
65 
66     @Test
testRecord()67     public void testRecord() {
68         Entry[] entries = {
69                 new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1"),
70                 new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package2"),
71                 new Entry("owning.package1", "/path/file2", 'D', 5, "loading.package1"),
72                 new Entry("owning.package2", "/path/file3", 'D', 0, "loading.package2"),
73         };
74 
75         PackageDynamicCodeLoading info = makePackageDcl(entries);
76         assertHasEntries(info, entries);
77     }
78 
79     @Test
testRecord_returnsHasChanged()80     public void testRecord_returnsHasChanged() {
81         Entry owner1Path1Loader1 =
82                 new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1");
83         Entry owner1Path1Loader2 =
84                 new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package2");
85         Entry owner1Path2Loader1 =
86                 new Entry("owning.package1", "/path/file2", 'D', 10, "loading.package1");
87         Entry owner2Path1Loader1 =
88                 new Entry("owning.package2", "/path/file1", 'D', 10, "loading.package2");
89 
90         PackageDynamicCodeLoading info = new PackageDynamicCodeLoading();
91 
92         assertTrue(record(info, owner1Path1Loader1));
93         assertFalse(record(info, owner1Path1Loader1));
94 
95         assertTrue(record(info, owner1Path1Loader2));
96         assertFalse(record(info, owner1Path1Loader2));
97 
98         assertTrue(record(info, owner1Path2Loader1));
99         assertFalse(record(info, owner1Path2Loader1));
100 
101         assertTrue(record(info, owner2Path1Loader1));
102         assertFalse(record(info, owner2Path1Loader1));
103 
104         assertHasEntries(info,
105                 owner1Path1Loader1, owner1Path1Loader2, owner1Path2Loader1, owner2Path1Loader1);
106     }
107 
108     @Test
testRecord_changeUserForFile_ignored()109     public void testRecord_changeUserForFile_ignored() {
110         Entry entry1 = new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1");
111         Entry entry2 = new Entry("owning.package1", "/path/file1", 'D', 20, "loading.package1");
112 
113         PackageDynamicCodeLoading info = makePackageDcl(entry1);
114 
115         assertThat(record(info, entry2)).isFalse();
116         assertHasEntries(info, entry1);
117     }
118 
119     @Test
testRecord_badFileType_throws()120     public void testRecord_badFileType_throws() {
121         Entry entry = new Entry("owning.package", "/path/file", 'Z', 10, "loading.package");
122         PackageDynamicCodeLoading info = new PackageDynamicCodeLoading();
123 
124         assertThrows(() -> record(info, entry));
125     }
126 
127     @Test
testRecord_tooManyFiles_ignored()128     public void testRecord_tooManyFiles_ignored() {
129         PackageDynamicCodeLoading info = new PackageDynamicCodeLoading();
130         int tooManyFiles = MAX_FILES_PER_OWNER + 1;
131         for (int i = 1; i <= tooManyFiles; i++) {
132             Entry entry = new Entry("owning.package", "/path/file" + i, 'D', 10, "loading.package");
133             boolean added = record(info, entry);
134             Set<Entry> entries = entriesFrom(info);
135             if (i < tooManyFiles) {
136                 assertThat(entries).contains(entry);
137                 assertTrue(added);
138             } else {
139                 assertThat(entries).doesNotContain(entry);
140                 assertFalse(added);
141             }
142         }
143     }
144 
145     @Test
testClear()146     public void testClear() {
147         Entry[] entries = {
148                 new Entry("owner1", "file1", 'D', 10, "loader1"),
149                 new Entry("owner2", "file2", 'D', 20, "loader2"),
150         };
151         PackageDynamicCodeLoading info = makePackageDcl(entries);
152 
153         info.clear();
154         assertHasEntries(info, NO_ENTRIES);
155     }
156 
157     @Test
testRemovePackage_present()158     public void testRemovePackage_present() {
159         Entry other = new Entry("other", "file", 'D', 0, "loader");
160         Entry[] entries = {
161                 new Entry("owner", "file1", 'D', 10, "loader1"),
162                 new Entry("owner", "file2", 'D', 20, "loader2"),
163                 other
164         };
165         PackageDynamicCodeLoading info = makePackageDcl(entries);
166 
167         assertTrue(info.removePackage("owner"));
168         assertHasEntries(info, other);
169         assertHasPackages(info, "other");
170     }
171 
172     @Test
testRemovePackage_notPresent()173     public void testRemovePackage_notPresent() {
174         Entry[] entries = { new Entry("owner", "file", 'D', 0, "loader") };
175         PackageDynamicCodeLoading info = makePackageDcl(entries);
176 
177         assertFalse(info.removePackage("other"));
178         assertHasEntries(info, entries);
179     }
180 
181     @Test
testRemoveUserPackage_notPresent()182     public void testRemoveUserPackage_notPresent() {
183         Entry[] entries = { new Entry("owner", "file", 'D', 0, "loader") };
184         PackageDynamicCodeLoading info = makePackageDcl(entries);
185 
186         assertFalse(info.removeUserPackage("other", 0));
187         assertHasEntries(info, entries);
188     }
189 
190     @Test
testRemoveUserPackage_presentWithNoOtherUsers()191     public void testRemoveUserPackage_presentWithNoOtherUsers() {
192         Entry other = new Entry("other", "file", 'D', 0, "loader");
193         Entry[] entries = {
194                 new Entry("owner", "file1", 'D', 0, "loader1"),
195                 new Entry("owner", "file2", 'D', 0, "loader2"),
196                 other
197         };
198         PackageDynamicCodeLoading info = makePackageDcl(entries);
199 
200         assertTrue(info.removeUserPackage("owner", 0));
201         assertHasEntries(info, other);
202         assertHasPackages(info, "other");
203     }
204 
205     @Test
testRemoveUserPackage_presentWithUsers()206     public void testRemoveUserPackage_presentWithUsers() {
207         Entry other = new Entry("owner", "file", 'D', 1, "loader");
208         Entry[] entries = {
209                 new Entry("owner", "file1", 'D', 0, "loader1"),
210                 new Entry("owner", "file2", 'D', 0, "loader2"),
211                 other
212         };
213         PackageDynamicCodeLoading info = makePackageDcl(entries);
214 
215         assertTrue(info.removeUserPackage("owner", 0));
216         assertHasEntries(info, other);
217     }
218 
219     @Test
testRemoveFile_present()220     public void testRemoveFile_present() {
221         Entry[] entries = {
222                 new Entry("package1", "file1", 'D', 0, "loader1"),
223                 new Entry("package1", "file2", 'D', 0, "loader1"),
224                 new Entry("package2", "file1", 'D', 0, "loader2"),
225         };
226         Entry[] expectedSurvivors = {
227                 new Entry("package1", "file2", 'D', 0, "loader1"),
228                 new Entry("package2", "file1", 'D', 0, "loader2"),
229         };
230         PackageDynamicCodeLoading info = makePackageDcl(entries);
231 
232         assertTrue(info.removeFile("package1", "file1", 0));
233         assertHasEntries(info, expectedSurvivors);
234     }
235 
236     @Test
testRemoveFile_onlyEntry()237     public void testRemoveFile_onlyEntry() {
238         Entry[] entries = {
239                 new Entry("package1", "file1", 'D', 0, "loader1"),
240         };
241         PackageDynamicCodeLoading info = makePackageDcl(entries);
242 
243         assertTrue(info.removeFile("package1", "file1", 0));
244         assertHasEntries(info, NO_ENTRIES);
245         assertHasPackages(info, NO_PACKAGES);
246     }
247 
248     @Test
testRemoveFile_notPresent()249     public void testRemoveFile_notPresent() {
250         Entry[] entries = {
251                 new Entry("package1", "file2", 'D', 0, "loader1"),
252         };
253         PackageDynamicCodeLoading info = makePackageDcl(entries);
254 
255         assertFalse(info.removeFile("package1", "file1", 0));
256         assertHasEntries(info, entries);
257     }
258 
259     @Test
testRemoveFile_wrongUser()260     public void testRemoveFile_wrongUser() {
261         Entry[] entries = {
262                 new Entry("package1", "file1", 'D', 10, "loader1"),
263         };
264         PackageDynamicCodeLoading info = makePackageDcl(entries);
265 
266         assertFalse(info.removeFile("package1", "file1", 0));
267         assertHasEntries(info, entries);
268     }
269 
270     @Test
testSyncData()271     public void testSyncData() {
272         Map<String, Set<Integer>> packageToUsersMap = ImmutableMap.of(
273                 "package1", ImmutableSet.of(10, 20),
274                 "package2", ImmutableSet.of(20));
275 
276         Entry[] entries = {
277                 new Entry("deleted.packaged", "file1", 'D', 10, "package1"),
278                 new Entry("package1", "file2", 'D', 20, "package2"),
279                 new Entry("package1", "file3", 'D', 10, "package2"),
280                 new Entry("package1", "file3", 'D', 10, "deleted.package"),
281                 new Entry("package2", "file4", 'D', 20, "deleted.package"),
282         };
283 
284         Entry[] expectedSurvivors = {
285                 new Entry("package1", "file2", 'D', 20, "package2"),
286         };
287 
288         PackageDynamicCodeLoading info = makePackageDcl(entries);
289         info.syncData(packageToUsersMap);
290         assertHasEntries(info, expectedSurvivors);
291         assertHasPackages(info, "package1");
292     }
293 
294     @Test
testRead_onlyHeader_emptyResult()295     public void testRead_onlyHeader_emptyResult() throws Exception {
296         assertHasEntries(read("DCL1"), NO_ENTRIES);
297     }
298 
299     @Test
testRead_noHeader_throws()300     public void testRead_noHeader_throws() {
301         assertThrows(IOException.class, () -> read(""));
302     }
303 
304     @Test
testRead_wrongHeader_throws()305     public void testRead_wrongHeader_throws() {
306         assertThrows(IOException.class, () -> read("DCL2"));
307     }
308 
309     @Test
testRead_oneEntry()310     public void testRead_oneEntry() throws Exception {
311         String inputText = ""
312                 + "DCL1\n"
313                 + "P:owning.package\n"
314                 + "D:10:loading.package:/path/fi\\\\le\n";
315         assertHasEntries(read(inputText),
316                 new Entry("owning.package", "/path/fi\\le", 'D', 10, "loading.package"));
317     }
318 
319     @Test
testRead_emptyPackage()320     public void testRead_emptyPackage() throws Exception {
321         String inputText = ""
322                 + "DCL1\n"
323                 + "P:owning.package\n";
324         PackageDynamicCodeLoading info = read(inputText);
325         assertHasEntries(info, NO_ENTRIES);
326         assertHasPackages(info, NO_PACKAGES);
327     }
328 
329     @Test
testRead_complex()330     public void testRead_complex() throws Exception {
331         String inputText = ""
332                 + "DCL1\n"
333                 + "P:owning.package1\n"
334                 + "D:10:loading.package1,loading.package2:/path/file1\n"
335                 + "D:5:loading.package1:/path/file2\n"
336                 + "P:owning.package2\n"
337                 + "D:0:loading.package2:/path/file3";
338         assertHasEntries(read(inputText),
339                 new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1"),
340                 new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package2"),
341                 new Entry("owning.package1", "/path/file2", 'D', 5, "loading.package1"),
342                 new Entry("owning.package2", "/path/file3", 'D', 0, "loading.package2"));
343     }
344 
345     @Test
testRead_missingPackageLine_throws()346     public void testRead_missingPackageLine_throws() {
347         String inputText = ""
348                 + "DCL1\n"
349                 + "D:10:loading.package:/path/file\n";
350         assertThrows(IOException.class, () -> read(inputText));
351     }
352 
353     @Test
testRead_malformedFile_throws()354     public void testRead_malformedFile_throws() {
355         String inputText = ""
356                 + "DCL1\n"
357                 + "P:owning.package\n"
358                 + "Hello world!\n";
359         assertThrows(IOException.class, () -> read(inputText));
360     }
361 
362     @Test
testRead_badFileType_throws()363     public void testRead_badFileType_throws() {
364         String inputText = ""
365                 + "DCL1\n"
366                 + "P:owning.package\n"
367                 + "X:10:loading.package:/path/file\n";
368         assertThrows(IOException.class, () -> read(inputText));
369     }
370 
371     @Test
testRead_badUserId_throws()372     public void testRead_badUserId_throws() {
373         String inputText = ""
374                 + "DCL1\n"
375                 + "P:owning.package\n"
376                 + "D:999999999999999999:loading.package:/path/file\n";
377         assertThrows(IOException.class, () -> read(inputText));
378     }
379 
380     @Test
testRead_missingPackages_throws()381     public void testRead_missingPackages_throws() {
382         String inputText = ""
383                 + "DCL1\n"
384                 + "P:owning.package\n"
385                 + "D:1:,:/path/file\n";
386         assertThrows(IOException.class, () -> read(inputText));
387     }
388 
389     @Test
testWrite_empty()390     public void testWrite_empty() throws Exception {
391         assertEquals("DCL1\n", write(NO_ENTRIES));
392     }
393 
394     @Test
testWrite_oneEntry()395     public void testWrite_oneEntry() throws Exception {
396         String expected = ""
397                 + "DCL1\n"
398                 + "P:owning.package\n"
399                 + "D:10:loading.package:/path/fi\\\\le\n";
400         String actual = write(
401                 new Entry("owning.package", "/path/fi\\le", 'D', 10, "loading.package"));
402         assertEquals(expected, actual);
403     }
404 
405     @Test
testWrite_complex_roundTrips()406     public void testWrite_complex_roundTrips() throws Exception {
407         // There isn't a canonical order for the output in the presence of multiple items.
408         // So we just check that if we read back what we write we end up where we started.
409         Entry[] entries = {
410             new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package1"),
411             new Entry("owning.package1", "/path/file1", 'D', 10, "loading.package2"),
412             new Entry("owning.package1", "/path/file2", 'D', 5, "loading.package1"),
413             new Entry("owning.package2", "/path/fi\\le3", 'D', 0, "loading.package2")
414         };
415         assertHasEntries(read(write(entries)), entries);
416     }
417 
418     @Test
testWrite_failure_throws()419     public void testWrite_failure_throws() {
420         PackageDynamicCodeLoading info = makePackageDcl(
421                 new Entry("owning.package", "/path/fi\\le", 'D', 10, "loading.package"));
422         assertThrows(IOException.class, () -> info.write(new ThrowingOutputStream()));
423     }
424 
425     @Test
testEscape_trivialCase_returnsSameString()426     public void testEscape_trivialCase_returnsSameString() {
427         assertSame(TRIVIAL_STRING, escape(TRIVIAL_STRING));
428     }
429 
430     @Test
testEscape()431     public void testEscape() {
432         String input = "backslash\\newline\nreturn\r";
433         String expected = "backslash\\\\newline\\nreturn\\r";
434         assertEquals(expected, escape(input));
435     }
436 
437     @Test
testUnescape_trivialCase_returnsSameString()438     public void testUnescape_trivialCase_returnsSameString() throws Exception {
439         assertSame(TRIVIAL_STRING, unescape(TRIVIAL_STRING));
440     }
441 
442     @Test
testUnescape()443     public void testUnescape() throws Exception {
444         String input = "backslash\\\\newline\\nreturn\\r";
445         String expected = "backslash\\newline\nreturn\r";
446         assertEquals(expected, unescape(input));
447     }
448 
449     @Test
testUnescape_badEscape_throws()450     public void testUnescape_badEscape_throws() {
451         assertThrows(IOException.class, () -> unescape("this is \\bad"));
452     }
453 
454     @Test
testUnescape_trailingBackslash_throws()455     public void testUnescape_trailingBackslash_throws() {
456         assertThrows(IOException.class, () -> unescape("don't do this\\"));
457     }
458 
459     @Test
testEscapeUnescape_roundTrips()460     public void testEscapeUnescape_roundTrips() throws Exception {
461         assertRoundTripsWithEscape("foo");
462         assertRoundTripsWithEscape("\\\\\n\n\r");
463         assertRoundTripsWithEscape("\\a\\b\\");
464         assertRoundTripsWithEscape("\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\");
465     }
466 
assertRoundTripsWithEscape(String original)467     private void assertRoundTripsWithEscape(String original) throws Exception {
468         assertEquals(original, unescape(escape(original)));
469     }
470 
record(PackageDynamicCodeLoading info, Entry entry)471     private boolean record(PackageDynamicCodeLoading info, Entry entry) {
472         return info.record(entry.mOwningPackage, entry.mPath, entry.mFileType, entry.mUserId,
473                 entry.mLoadingPackage);
474     }
475 
read(String inputText)476     private PackageDynamicCodeLoading read(String inputText) throws Exception {
477         ByteArrayInputStream inputStream =
478                 new ByteArrayInputStream(inputText.getBytes(UTF_8));
479 
480         PackageDynamicCodeLoading info = new PackageDynamicCodeLoading();
481         info.read(inputStream);
482 
483         return info;
484     }
485 
write(Entry... entries)486     private String write(Entry... entries) throws Exception {
487         ByteArrayOutputStream output = new ByteArrayOutputStream();
488         makePackageDcl(entries).write(output);
489         return new String(output.toByteArray(), UTF_8);
490     }
491 
entriesFrom(PackageDynamicCodeLoading info)492     private Set<Entry> entriesFrom(PackageDynamicCodeLoading info) {
493         ImmutableSet.Builder<Entry> entries = ImmutableSet.builder();
494         for (String owningPackage : info.getAllPackagesWithDynamicCodeLoading()) {
495             PackageDynamicCode packageInfo = info.getPackageDynamicCodeInfo(owningPackage);
496             Map<String, DynamicCodeFile> usageMap = packageInfo.mFileUsageMap;
497             for (Map.Entry<String, DynamicCodeFile> fileEntry : usageMap.entrySet()) {
498                 String path = fileEntry.getKey();
499                 DynamicCodeFile fileInfo = fileEntry.getValue();
500                 for (String loadingPackage : fileInfo.mLoadingPackages) {
501                     entries.add(new Entry(owningPackage, path, fileInfo.mFileType, fileInfo.mUserId,
502                             loadingPackage));
503                 }
504             }
505         }
506 
507         return entries.build();
508     }
509 
makePackageDcl(Entry... entries)510     private PackageDynamicCodeLoading makePackageDcl(Entry... entries) {
511         PackageDynamicCodeLoading result = new PackageDynamicCodeLoading();
512         for (Entry entry : entries) {
513             result.record(entry.mOwningPackage, entry.mPath, entry.mFileType, entry.mUserId,
514                     entry.mLoadingPackage);
515         }
516         return result;
517 
518     }
519 
assertHasEntries(PackageDynamicCodeLoading info, Entry... expected)520     private void assertHasEntries(PackageDynamicCodeLoading info, Entry... expected) {
521         assertEquals(ImmutableSet.copyOf(expected), entriesFrom(info));
522     }
523 
assertHasPackages(PackageDynamicCodeLoading info, String... expected)524     private void assertHasPackages(PackageDynamicCodeLoading info, String... expected) {
525         assertEquals(ImmutableSet.copyOf(expected), info.getAllPackagesWithDynamicCodeLoading());
526     }
527 
528     /**
529      * Immutable representation of one entry in the dynamic code loading data (one package
530      * owning one file loaded by one package). Has well-behaved equality, hash and toString
531      * for ease of use in assertions.
532      */
533     private static class Entry {
534         private final String mOwningPackage;
535         private final String mPath;
536         private final char mFileType;
537         private final int mUserId;
538         private final String mLoadingPackage;
539 
Entry(String owningPackage, String path, char fileType, int userId, String loadingPackage)540         private Entry(String owningPackage, String path, char fileType, int userId,
541                 String loadingPackage) {
542             mOwningPackage = owningPackage;
543             mPath = path;
544             mFileType = fileType;
545             mUserId = userId;
546             mLoadingPackage = loadingPackage;
547         }
548 
549         @Override
equals(Object o)550         public boolean equals(Object o) {
551             if (this == o) return true;
552             if (o == null || getClass() != o.getClass()) return false;
553             Entry that = (Entry) o;
554             return mFileType == that.mFileType
555                     && mUserId == that.mUserId
556                     && Objects.equals(mOwningPackage, that.mOwningPackage)
557                     && Objects.equals(mPath, that.mPath)
558                     && Objects.equals(mLoadingPackage, that.mLoadingPackage);
559         }
560 
561         @Override
hashCode()562         public int hashCode() {
563             return Objects.hash(mOwningPackage, mPath, mFileType, mUserId, mLoadingPackage);
564         }
565 
566         @Override
toString()567         public String toString() {
568             return "Entry("
569                     + "\"" + mOwningPackage + '"'
570                     + ", \"" + mPath + '"'
571                     + ", '" + mFileType + '\''
572                     + ", " + mUserId
573                     + ", \"" + mLoadingPackage + '\"'
574                     + ')';
575         }
576     }
577 
578     private static class ThrowingOutputStream extends OutputStream {
579         @Override
write(int b)580         public void write(int b) throws IOException {
581             throw new IOException("Intentional failure");
582         }
583     }
584 }
585