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 com.android.server.om; 18 19 import static com.android.server.om.OverlayManagerServiceImpl.OperationFailedException; 20 21 import static org.junit.Assert.assertEquals; 22 import static org.mockito.ArgumentMatchers.any; 23 import static org.mockito.Mockito.mock; 24 import static org.mockito.Mockito.when; 25 26 import android.annotation.NonNull; 27 import android.content.Intent; 28 import android.content.om.OverlayIdentifier; 29 import android.content.om.OverlayInfo; 30 import android.content.om.OverlayInfo.State; 31 import android.content.om.OverlayableInfo; 32 import android.content.pm.UserPackage; 33 import android.os.FabricatedOverlayInfo; 34 import android.os.FabricatedOverlayInternal; 35 import android.text.TextUtils; 36 import android.util.ArrayMap; 37 import android.util.ArraySet; 38 39 import androidx.annotation.Nullable; 40 41 import com.android.internal.content.om.OverlayConfig; 42 import com.android.server.pm.pkg.AndroidPackage; 43 import com.android.server.pm.pkg.AndroidPackageSplit; 44 import com.android.server.pm.pkg.PackageState; 45 46 import org.junit.Assert; 47 import org.junit.Before; 48 import org.mockito.Mockito; 49 50 import java.util.ArrayList; 51 import java.util.Arrays; 52 import java.util.Collections; 53 import java.util.List; 54 import java.util.Map; 55 import java.util.Set; 56 57 /** Base class for creating {@link OverlayManagerServiceImplTests} tests. */ 58 class OverlayManagerServiceImplTestsBase { 59 private OverlayManagerServiceImpl mImpl; 60 private FakeDeviceState mState; 61 private FakePackageManagerHelper mPackageManager; 62 private FakeIdmapDaemon mIdmapDaemon; 63 private OverlayConfig mOverlayConfig; 64 private String mConfigSignaturePackageName; 65 66 @Before setUp()67 public void setUp() { 68 mState = new FakeDeviceState(); 69 mPackageManager = new FakePackageManagerHelper(mState); 70 mIdmapDaemon = new FakeIdmapDaemon(mState); 71 mOverlayConfig = mock(OverlayConfig.class); 72 when(mOverlayConfig.getPriority(any())).thenReturn(OverlayConfig.DEFAULT_PRIORITY); 73 when(mOverlayConfig.isEnabled(any())).thenReturn(false); 74 when(mOverlayConfig.isMutable(any())).thenReturn(true); 75 reinitializeImpl(); 76 } 77 reinitializeImpl()78 void reinitializeImpl() { 79 mImpl = new OverlayManagerServiceImpl(mPackageManager, 80 new IdmapManager(mIdmapDaemon, mPackageManager), 81 new OverlayManagerSettings(), 82 mOverlayConfig, 83 new String[0]); 84 } 85 getImpl()86 OverlayManagerServiceImpl getImpl() { 87 return mImpl; 88 } 89 getIdmapd()90 FakeIdmapDaemon getIdmapd() { 91 return mIdmapDaemon; 92 } 93 getState()94 FakeDeviceState getState() { 95 return mState; 96 } 97 setConfigSignaturePackageName(String packageName)98 void setConfigSignaturePackageName(String packageName) { 99 mConfigSignaturePackageName = packageName; 100 } 101 assertState(@tate int expected, final OverlayIdentifier overlay, int userId)102 void assertState(@State int expected, final OverlayIdentifier overlay, int userId) { 103 final OverlayInfo info = mImpl.getOverlayInfo(overlay, userId); 104 if (info == null) { 105 throw new IllegalStateException("overlay '" + overlay + "' not installed"); 106 } 107 final String msg = String.format("expected %s but was %s:", 108 OverlayInfo.stateToString(expected), OverlayInfo.stateToString(info.state)); 109 assertEquals(msg, expected, info.state); 110 } 111 assertOverlayInfoForTarget(final String targetPackageName, int userId, OverlayInfo... overlayInfos)112 void assertOverlayInfoForTarget(final String targetPackageName, int userId, 113 OverlayInfo... overlayInfos) { 114 final List<OverlayInfo> expected = 115 mImpl.getOverlayInfosForTarget(targetPackageName, userId); 116 final List<OverlayInfo> actual = Arrays.asList(overlayInfos); 117 assertEquals(expected, actual); 118 } 119 app(String packageName)120 FakeDeviceState.PackageBuilder app(String packageName) { 121 return new FakeDeviceState.PackageBuilder(packageName, null /* targetPackageName */, 122 null /* targetOverlayableName */, "data"); 123 } 124 target(String packageName)125 FakeDeviceState.PackageBuilder target(String packageName) { 126 return new FakeDeviceState.PackageBuilder(packageName, null /* targetPackageName */, 127 null /* targetOverlayableName */, ""); 128 } 129 overlay(String packageName, String targetPackageName)130 FakeDeviceState.PackageBuilder overlay(String packageName, String targetPackageName) { 131 return overlay(packageName, targetPackageName, null /* targetOverlayableName */); 132 } 133 overlay(String packageName, String targetPackageName, String targetOverlayableName)134 FakeDeviceState.PackageBuilder overlay(String packageName, String targetPackageName, 135 String targetOverlayableName) { 136 return new FakeDeviceState.PackageBuilder(packageName, targetPackageName, 137 targetOverlayableName, ""); 138 } 139 addPackage(FakeDeviceState.PackageBuilder pkg, int userId)140 void addPackage(FakeDeviceState.PackageBuilder pkg, int userId) { 141 mState.add(pkg, userId); 142 } 143 144 enum ConfigState { 145 IMMUTABLE_DISABLED, 146 IMMUTABLE_ENABLED, 147 MUTABLE_DISABLED, 148 MUTABLE_ENABLED 149 } 150 configureSystemOverlay(@onNull String packageName, @NonNull ConfigState state, int priority)151 void configureSystemOverlay(@NonNull String packageName, @NonNull ConfigState state, 152 int priority) { 153 final boolean mutable = state == ConfigState.MUTABLE_DISABLED 154 || state == ConfigState.MUTABLE_ENABLED; 155 final boolean enabled = state == ConfigState.IMMUTABLE_ENABLED 156 || state == ConfigState.MUTABLE_ENABLED; 157 when(mOverlayConfig.getPriority(packageName)).thenReturn(priority); 158 when(mOverlayConfig.isEnabled(packageName)).thenReturn(enabled); 159 when(mOverlayConfig.isMutable(packageName)).thenReturn(mutable); 160 } 161 162 /** 163 * Adds the package to the device. 164 * 165 * This corresponds to when the OMS receives the 166 * {@link Intent#ACTION_PACKAGE_ADDED} broadcast. 167 * 168 * @throws IllegalStateException if the package is currently installed 169 */ installAndAssert(@onNull FakeDeviceState.PackageBuilder pkg, int userId, @NonNull Set<UserPackage> onAddedUpdatedPackages)170 void installAndAssert(@NonNull FakeDeviceState.PackageBuilder pkg, int userId, 171 @NonNull Set<UserPackage> onAddedUpdatedPackages) 172 throws OperationFailedException { 173 if (mState.select(pkg.packageName, userId) != null) { 174 throw new IllegalStateException("package " + pkg.packageName + " already installed"); 175 } 176 mState.add(pkg, userId); 177 assertEquals(onAddedUpdatedPackages, mImpl.onPackageAdded(pkg.packageName, userId)); 178 } 179 180 /** 181 * Begins upgrading the package. 182 * 183 * This corresponds to when the OMS receives the 184 * {@link Intent#ACTION_PACKAGE_REMOVED} broadcast with the 185 * {@link Intent#EXTRA_REPLACING} extra and then receives the 186 * {@link Intent#ACTION_PACKAGE_ADDED} broadcast with the 187 * {@link Intent#EXTRA_REPLACING} extra. 188 * 189 * @throws IllegalStateException if the package is not currently installed 190 */ upgradeAndAssert(FakeDeviceState.PackageBuilder pkg, int userId, @NonNull Set<UserPackage> onReplacingUpdatedPackages, @NonNull Set<UserPackage> onReplacedUpdatedPackages)191 void upgradeAndAssert(FakeDeviceState.PackageBuilder pkg, int userId, 192 @NonNull Set<UserPackage> onReplacingUpdatedPackages, 193 @NonNull Set<UserPackage> onReplacedUpdatedPackages) 194 throws OperationFailedException { 195 final FakeDeviceState.Package replacedPackage = mState.select(pkg.packageName, userId); 196 if (replacedPackage == null) { 197 throw new IllegalStateException("package " + pkg.packageName + " not installed"); 198 } 199 200 assertEquals(onReplacingUpdatedPackages, mImpl.onPackageReplacing(pkg.packageName, 201 /* systemUpdateUninstall */ false, userId)); 202 mState.add(pkg, userId); 203 assertEquals(onReplacedUpdatedPackages, mImpl.onPackageReplaced(pkg.packageName, userId)); 204 } 205 206 /** 207 * Begins downgrading the package. Usually used simulating a system uninstall of its /data 208 * variant. 209 * 210 * This corresponds to when the OMS receives the 211 * {@link Intent#ACTION_PACKAGE_REMOVED} broadcast with the 212 * {@link Intent#EXTRA_REPLACING} and {@link Intent#EXTRA_SYSTEM_UPDATE_UNINSTALL} extras 213 * and then receives the {@link Intent#ACTION_PACKAGE_ADDED} broadcast with the 214 * {@link Intent#EXTRA_REPLACING} extra. 215 * 216 * @throws IllegalStateException if the package is not currently installed 217 */ downgradeAndAssert(FakeDeviceState.PackageBuilder pkg, int userId, @NonNull Set<UserPackage> onReplacingUpdatedPackages, @NonNull Set<UserPackage> onReplacedUpdatedPackages)218 void downgradeAndAssert(FakeDeviceState.PackageBuilder pkg, int userId, 219 @NonNull Set<UserPackage> onReplacingUpdatedPackages, 220 @NonNull Set<UserPackage> onReplacedUpdatedPackages) 221 throws OperationFailedException { 222 final FakeDeviceState.Package replacedPackage = mState.select(pkg.packageName, userId); 223 if (replacedPackage == null) { 224 throw new IllegalStateException("package " + pkg.packageName + " not installed"); 225 } 226 227 assertEquals(onReplacingUpdatedPackages, mImpl.onPackageReplacing(pkg.packageName, 228 /* systemUpdateUninstall */ true, userId)); 229 mState.add(pkg, userId); 230 assertEquals(onReplacedUpdatedPackages, mImpl.onPackageReplaced(pkg.packageName, userId)); 231 } 232 233 /** 234 * Removes the package from the device. 235 * 236 * This corresponds to when the OMS receives the 237 * {@link Intent#ACTION_PACKAGE_REMOVED} broadcast. 238 * 239 * @throws IllegalStateException if the package is not currently installed 240 */ uninstallAndAssert(@onNull String packageName, int userId, @NonNull Set<UserPackage> onRemovedUpdatedPackages)241 void uninstallAndAssert(@NonNull String packageName, int userId, 242 @NonNull Set<UserPackage> onRemovedUpdatedPackages) { 243 final FakeDeviceState.Package pkg = mState.select(packageName, userId); 244 if (pkg == null) { 245 throw new IllegalStateException("package " + packageName + " not installed"); 246 } 247 mState.remove(pkg.packageName); 248 assertEquals(onRemovedUpdatedPackages, mImpl.onPackageRemoved(pkg.packageName, userId)); 249 } 250 251 /** Represents the state of packages installed on a fake device. */ 252 static class FakeDeviceState { 253 private ArrayMap<String, Package> mPackages = new ArrayMap<>(); 254 add(PackageBuilder pkgBuilder, int userId)255 void add(PackageBuilder pkgBuilder, int userId) { 256 final Package pkg = pkgBuilder.build(); 257 final Package previousPkg = select(pkg.packageName, userId); 258 mPackages.put(pkg.packageName, pkg); 259 260 pkg.installedUserIds.add(userId); 261 if (previousPkg != null) { 262 pkg.installedUserIds.addAll(previousPkg.installedUserIds); 263 } 264 } 265 remove(String packageName)266 void remove(String packageName) { 267 mPackages.remove(packageName); 268 } 269 uninstall(String packageName, int userId)270 void uninstall(String packageName, int userId) { 271 final Package pkg = mPackages.get(packageName); 272 if (pkg != null) { 273 pkg.installedUserIds.remove(userId); 274 } 275 } 276 select(String packageName, int userId)277 Package select(String packageName, int userId) { 278 final Package pkg = mPackages.get(packageName); 279 return pkg != null && pkg.installedUserIds.contains(userId) ? pkg : null; 280 } 281 selectFromPath(String path)282 private Package selectFromPath(String path) { 283 return mPackages.values().stream() 284 .filter(p -> p.apkPath.equals(path)).findFirst().orElse(null); 285 } 286 287 static final class PackageBuilder { 288 private String packageName; 289 private String targetPackage; 290 private String certificate = "[default]"; 291 private String partition; 292 private int version = 0; 293 private ArrayList<String> overlayableNames = new ArrayList<>(); 294 private String targetOverlayableName; 295 PackageBuilder(String packageName, String targetPackage, String targetOverlayableName, String partition)296 private PackageBuilder(String packageName, String targetPackage, 297 String targetOverlayableName, String partition) { 298 this.packageName = packageName; 299 this.targetPackage = targetPackage; 300 this.targetOverlayableName = targetOverlayableName; 301 this.partition = partition; 302 } 303 setCertificate(String certificate)304 PackageBuilder setCertificate(String certificate) { 305 this.certificate = certificate; 306 return this; 307 } 308 addOverlayable(String overlayableName)309 PackageBuilder addOverlayable(String overlayableName) { 310 overlayableNames.add(overlayableName); 311 return this; 312 } 313 setVersion(int version)314 PackageBuilder setVersion(int version) { 315 this.version = version; 316 return this; 317 } 318 build()319 Package build() { 320 String path = ""; 321 if (TextUtils.isEmpty(partition)) { 322 if (targetPackage == null) { 323 path = "/system/app"; 324 } else { 325 path = "/vendor/overlay"; 326 } 327 } else { 328 String type = targetPackage == null ? "app" : "overlay"; 329 path = String.format("%s/%s", partition, type); 330 } 331 332 final String apkPath = String.format("%s/%s/base.apk", path, packageName); 333 final Package newPackage = new Package(packageName, targetPackage, 334 targetOverlayableName, version, apkPath, certificate); 335 newPackage.overlayableNames.addAll(overlayableNames); 336 return newPackage; 337 } 338 } 339 340 static final class Package { 341 final String packageName; 342 final String targetPackageName; 343 final String targetOverlayableName; 344 final int versionCode; 345 final String apkPath; 346 final String certificate; 347 final ArrayList<String> overlayableNames = new ArrayList<>(); 348 private final ArraySet<Integer> installedUserIds = new ArraySet<>(); 349 Package(String packageName, String targetPackageName, String targetOverlayableName, int versionCode, String apkPath, String certificate)350 private Package(String packageName, String targetPackageName, 351 String targetOverlayableName, int versionCode, String apkPath, 352 String certificate) { 353 this.packageName = packageName; 354 this.targetPackageName = targetPackageName; 355 this.targetOverlayableName = targetOverlayableName; 356 this.versionCode = versionCode; 357 this.apkPath = apkPath; 358 this.certificate = certificate; 359 } 360 361 @Nullable getPackageForUser(int user)362 private PackageState getPackageForUser(int user) { 363 if (!installedUserIds.contains(user)) { 364 return null; 365 } 366 final AndroidPackage pkg = Mockito.mock(AndroidPackage.class); 367 when(pkg.getPackageName()).thenReturn(packageName); 368 when(pkg.getLongVersionCode()).thenReturn((long) versionCode); 369 when(pkg.getOverlayTarget()).thenReturn(targetPackageName); 370 when(pkg.getOverlayTargetOverlayableName()).thenReturn(targetOverlayableName); 371 when(pkg.getOverlayCategory()).thenReturn("Fake-category-" + targetPackageName); 372 var baseSplit = mock(AndroidPackageSplit.class); 373 when(baseSplit.getPath()).thenReturn(apkPath); 374 when(pkg.getSplits()).thenReturn(List.of(baseSplit)); 375 376 var pkgState = Mockito.mock(PackageState.class); 377 when(pkgState.getPackageName()).thenReturn(packageName); 378 when(pkgState.getAndroidPackage()).thenReturn(pkg); 379 return pkgState; 380 } 381 } 382 } 383 384 final class FakePackageManagerHelper implements PackageManagerHelper { 385 private final FakeDeviceState mState; 386 FakePackageManagerHelper(FakeDeviceState state)387 private FakePackageManagerHelper(FakeDeviceState state) { 388 mState = state; 389 } 390 391 @NonNull 392 @Override initializeForUser(int userId)393 public ArrayMap<String, PackageState> initializeForUser(int userId) { 394 final ArrayMap<String, PackageState> packages = new ArrayMap<>(); 395 mState.mPackages.forEach((key, value) -> { 396 final PackageState pkg = value.getPackageForUser(userId); 397 if (pkg != null) { 398 packages.put(key, pkg); 399 } 400 }); 401 return packages; 402 } 403 404 @Nullable 405 @Override getPackageStateForUser(@onNull String packageName, int userId)406 public PackageState getPackageStateForUser(@NonNull String packageName, int userId) { 407 final FakeDeviceState.Package pkgState = mState.select(packageName, userId); 408 return pkgState == null ? null : pkgState.getPackageForUser(userId); 409 } 410 411 @Override isInstantApp(@onNull String packageName, int userId)412 public boolean isInstantApp(@NonNull String packageName, int userId) { 413 return false; 414 } 415 416 @Override signaturesMatching(@onNull String packageName1, @NonNull String packageName2, int userId)417 public boolean signaturesMatching(@NonNull String packageName1, 418 @NonNull String packageName2, int userId) { 419 final FakeDeviceState.Package pkg1 = mState.select(packageName1, userId); 420 final FakeDeviceState.Package pkg2 = mState.select(packageName2, userId); 421 return pkg1 != null && pkg2 != null && pkg1.certificate.equals(pkg2.certificate); 422 } 423 424 @Override getConfigSignaturePackage()425 public @NonNull String getConfigSignaturePackage() { 426 return mConfigSignaturePackageName; 427 } 428 429 @Nullable 430 @Override getOverlayableForTarget(@onNull String packageName, @NonNull String targetOverlayableName, int userId)431 public OverlayableInfo getOverlayableForTarget(@NonNull String packageName, 432 @NonNull String targetOverlayableName, int userId) { 433 final FakeDeviceState.Package pkg = mState.select(packageName, userId); 434 if (pkg == null || !pkg.overlayableNames.contains(targetOverlayableName)) { 435 return null; 436 } 437 return new OverlayableInfo(targetOverlayableName, null /* actor */); 438 } 439 440 @Nullable 441 @Override getPackagesForUid(int uid)442 public String[] getPackagesForUid(int uid) { 443 throw new UnsupportedOperationException(); 444 } 445 446 @NonNull 447 @Override getNamedActors()448 public Map<String, Map<String, String>> getNamedActors() { 449 return Collections.emptyMap(); 450 } 451 452 @Override doesTargetDefineOverlayable(String targetPackageName, int userId)453 public boolean doesTargetDefineOverlayable(String targetPackageName, int userId) { 454 final FakeDeviceState.Package pkg = mState.select(targetPackageName, userId); 455 return pkg != null && pkg.overlayableNames.contains(targetPackageName); 456 } 457 458 @Override enforcePermission(String permission, String message)459 public void enforcePermission(String permission, String message) throws SecurityException { 460 throw new UnsupportedOperationException(); 461 } 462 } 463 464 static class FakeIdmapDaemon extends IdmapDaemon { 465 private final FakeDeviceState mState; 466 private final ArrayMap<String, IdmapHeader> mIdmapFiles = new ArrayMap<>(); 467 private final ArrayMap<String, FabricatedOverlayInfo> mFabricatedOverlays = 468 new ArrayMap<>(); 469 private int mFabricatedAssetSeq = 0; 470 FakeIdmapDaemon(FakeDeviceState state)471 FakeIdmapDaemon(FakeDeviceState state) { 472 this.mState = state; 473 } 474 getCrc(@onNull final String path)475 private int getCrc(@NonNull final String path) { 476 final FakeDeviceState.Package pkg = mState.selectFromPath(path); 477 Assert.assertNotNull("path = " + path, pkg); 478 return pkg.versionCode; 479 } 480 481 @Override createIdmap(String targetPath, String overlayPath, String overlayName, int policies, boolean enforce, int userId)482 String createIdmap(String targetPath, String overlayPath, String overlayName, 483 int policies, boolean enforce, int userId) { 484 mIdmapFiles.put(overlayPath, new IdmapHeader(getCrc(targetPath), 485 getCrc(overlayPath), targetPath, overlayName, policies, enforce)); 486 return overlayPath; 487 } 488 489 @Override removeIdmap(String overlayPath, int userId)490 boolean removeIdmap(String overlayPath, int userId) { 491 return mIdmapFiles.remove(overlayPath) != null; 492 } 493 494 @Override verifyIdmap(String targetPath, String overlayPath, String overlayName, int policies, boolean enforce, int userId)495 boolean verifyIdmap(String targetPath, String overlayPath, String overlayName, int policies, 496 boolean enforce, int userId) { 497 final IdmapHeader idmap = mIdmapFiles.get(overlayPath); 498 if (idmap == null) { 499 return false; 500 } 501 return idmap.isUpToDate(getCrc(targetPath), getCrc(overlayPath), targetPath, policies, 502 enforce); 503 } 504 505 @Override idmapExists(String overlayPath, int userId)506 boolean idmapExists(String overlayPath, int userId) { 507 return mIdmapFiles.containsKey(overlayPath); 508 } 509 510 @Override createFabricatedOverlay(@onNull FabricatedOverlayInternal overlay)511 FabricatedOverlayInfo createFabricatedOverlay(@NonNull FabricatedOverlayInternal overlay) { 512 final String path = Integer.toString(mFabricatedAssetSeq++); 513 final FabricatedOverlayInfo info = new FabricatedOverlayInfo(); 514 info.path = path; 515 info.overlayName = overlay.overlayName; 516 info.packageName = overlay.packageName; 517 info.targetPackageName = overlay.targetPackageName; 518 info.targetOverlayable = overlay.targetOverlayable; 519 mFabricatedOverlays.put(path, info); 520 return info; 521 } 522 523 @Override deleteFabricatedOverlay(@onNull String path)524 boolean deleteFabricatedOverlay(@NonNull String path) { 525 return mFabricatedOverlays.remove(path) != null; 526 } 527 528 @Override getFabricatedOverlayInfos()529 List<FabricatedOverlayInfo> getFabricatedOverlayInfos() { 530 return new ArrayList<>(mFabricatedOverlays.values()); 531 } 532 getIdmap(String overlayPath)533 IdmapHeader getIdmap(String overlayPath) { 534 return mIdmapFiles.get(overlayPath); 535 } 536 537 static class IdmapHeader { 538 private final int targetCrc; 539 private final int overlayCrc; 540 final String targetPath; 541 final String overlayName; 542 final int policies; 543 final boolean enforceOverlayable; 544 IdmapHeader(int targetCrc, int overlayCrc, String targetPath, String overlayName, int policies, boolean enforceOverlayable)545 private IdmapHeader(int targetCrc, int overlayCrc, String targetPath, 546 String overlayName, int policies, boolean enforceOverlayable) { 547 this.targetCrc = targetCrc; 548 this.overlayCrc = overlayCrc; 549 this.targetPath = targetPath; 550 this.overlayName = overlayName; 551 this.policies = policies; 552 this.enforceOverlayable = enforceOverlayable; 553 } 554 isUpToDate(int expectedTargetCrc, int expectedOverlayCrc, String expectedTargetPath, int expectedPolicies, boolean expectedEnforceOverlayable)555 private boolean isUpToDate(int expectedTargetCrc, int expectedOverlayCrc, 556 String expectedTargetPath, int expectedPolicies, 557 boolean expectedEnforceOverlayable) { 558 return expectedTargetCrc == targetCrc && expectedOverlayCrc == overlayCrc 559 && expectedTargetPath.equals(targetPath) && expectedPolicies == policies 560 && expectedEnforceOverlayable == enforceOverlayable; 561 } 562 } 563 } 564 } 565