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