From 13fffd267cd7026deff279c39dca9d981ed3e458 Mon Sep 17 00:00:00 2001 From: Artem Iglikov Date: Mon, 8 May 2017 17:17:13 +0100 Subject: [PATCH] Add some tests for TarBackupReader ... and PerformAdbRestoreTask. Involves some refactoring, splitting readAppManifest() into two methods. Also a bit of cleanup: make private field actually private and use static imports for some constants. Bug: 38090803 Bug: 37619463 Test: runtest -p com.android.server.backup frameworks-services Change-Id: Ic30a6c5a515da1efb67caaae6eb75f4313797d5c --- .../android/server/backup/FileMetadata.java | 2 + .../backup/restore/FullRestoreEngine.java | 252 +++++----- .../backup/restore/PerformAdbRestoreTask.java | 449 +++++++++--------- .../server/backup/utils/TarBackupReader.java | 428 ++++++++--------- .../res/raw/backup_telephony_no_password | Bin 0 -> 21805 bytes .../res/raw/backup_telephony_with_password | Bin 0 -> 22309 bytes .../restore/PerformAdbRestoreTaskTest.java | 95 ++++ .../backup/utils/TarBackupReaderTest.java | 140 ++++++ 8 files changed, 798 insertions(+), 568 deletions(-) create mode 100644 services/tests/servicestests/res/raw/backup_telephony_no_password create mode 100644 services/tests/servicestests/res/raw/backup_telephony_with_password create mode 100644 services/tests/servicestests/src/com/android/server/backup/restore/PerformAdbRestoreTaskTest.java create mode 100644 services/tests/servicestests/src/com/android/server/backup/utils/TarBackupReaderTest.java diff --git a/services/backup/java/com/android/server/backup/FileMetadata.java b/services/backup/java/com/android/server/backup/FileMetadata.java index afddd3e725e1e..01af195d2aab7 100644 --- a/services/backup/java/com/android/server/backup/FileMetadata.java +++ b/services/backup/java/com/android/server/backup/FileMetadata.java @@ -34,6 +34,8 @@ public class FileMetadata { public long mode; // e.g. 0666 (actually int) public long mtime; // last mod time, UTC time_t (actually int) public long size; // bytes of content + public int version; // App version. + public boolean hasApk; // Whether backup file contains apk. @Override public String toString() { diff --git a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java index 1d5be28d0c87f..888dcd7e7a5ec 100644 --- a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java +++ b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java @@ -16,6 +16,16 @@ package com.android.server.backup.restore; +import static com.android.server.backup.RefactoredBackupManagerService.BACKUP_MANIFEST_FILENAME; +import static com.android.server.backup.RefactoredBackupManagerService.BACKUP_METADATA_FILENAME; +import static com.android.server.backup.RefactoredBackupManagerService.DEBUG; +import static com.android.server.backup.RefactoredBackupManagerService.MORE_DEBUG; +import static com.android.server.backup.RefactoredBackupManagerService.OP_TYPE_RESTORE_WAIT; +import static com.android.server.backup.RefactoredBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE; +import static com.android.server.backup.RefactoredBackupManagerService.TAG; +import static com.android.server.backup.RefactoredBackupManagerService.TIMEOUT_RESTORE_INTERVAL; +import static com.android.server.backup.RefactoredBackupManagerService + .TIMEOUT_SHARED_BACKUP_INTERVAL; import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_OPERATION_TIMEOUT; import android.app.ApplicationThreadConstants; @@ -52,58 +62,61 @@ import java.util.HashSet; */ public class FullRestoreEngine extends RestoreEngine { - private RefactoredBackupManagerService backupManagerService; + private final RefactoredBackupManagerService mBackupManagerService; // Task in charge of monitoring timeouts - BackupRestoreTask mMonitorTask; + private final BackupRestoreTask mMonitorTask; + + private final RestoreInstallObserver mInstallObserver = new RestoreInstallObserver(); + private final RestoreDeleteObserver mDeleteObserver = new RestoreDeleteObserver(); // Dedicated observer, if any - IFullBackupRestoreObserver mObserver; + private IFullBackupRestoreObserver mObserver; - IBackupManagerMonitor mMonitor; + final IBackupManagerMonitor mMonitor; // Where we're delivering the file data as we go - IBackupAgent mAgent; + private IBackupAgent mAgent; // Are we permitted to only deliver a specific package's metadata? - PackageInfo mOnlyPackage; + final PackageInfo mOnlyPackage; - boolean mAllowApks; - boolean mAllowObbs; + final boolean mAllowApks; + private final boolean mAllowObbs; // Which package are we currently handling data for? - String mAgentPackage; + private String mAgentPackage; // Info for working with the target app process - ApplicationInfo mTargetApp; + private ApplicationInfo mTargetApp; // Machinery for restoring OBBs - FullBackupObbConnection mObbConnection = null; + private FullBackupObbConnection mObbConnection = null; // possible handling states for a given package in the restore dataset - final HashMap mPackagePolicies + private final HashMap mPackagePolicies = new HashMap<>(); // installer package names for each encountered app, derived from the manifests - final HashMap mPackageInstallers = new HashMap<>(); + private final HashMap mPackageInstallers = new HashMap<>(); // Signatures for a given package found in its manifest file - final HashMap mManifestSignatures + private final HashMap mManifestSignatures = new HashMap<>(); // Packages we've already wiped data on when restoring their first file - final HashSet mClearedPackages = new HashSet<>(); + private final HashSet mClearedPackages = new HashSet<>(); // How much data have we moved? - long mBytes; + private long mBytes; // Working buffer - byte[] mBuffer; + final byte[] mBuffer; // Pipes for moving data - ParcelFileDescriptor[] mPipes = null; + private ParcelFileDescriptor[] mPipes = null; // Widget blob to be restored out-of-band - byte[] mWidgetData = null; + private byte[] mWidgetData = null; final int mEphemeralOpToken; @@ -111,7 +124,7 @@ public class FullRestoreEngine extends RestoreEngine { BackupRestoreTask monitorTask, IFullBackupRestoreObserver observer, IBackupManagerMonitor monitor, PackageInfo onlyPackage, boolean allowApks, boolean allowObbs, int ephemeralOpToken) { - this.backupManagerService = backupManagerService; + mBackupManagerService = backupManagerService; mEphemeralOpToken = ephemeralOpToken; mMonitorTask = monitorTask; mObserver = observer; @@ -134,7 +147,7 @@ public class FullRestoreEngine extends RestoreEngine { public boolean restoreOneFile(InputStream instream, boolean mustKillAgent, byte[] buffer, PackageInfo onlyPackage, boolean allowApks, int token, IBackupManagerMonitor monitor) { if (!isRunning()) { - Slog.w(RefactoredBackupManagerService.TAG, "Restore engine used after halting"); + Slog.w(TAG, "Restore engine used after halting"); return false; } @@ -150,12 +163,12 @@ public class FullRestoreEngine extends RestoreEngine { FileMetadata info; try { - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.v(RefactoredBackupManagerService.TAG, "Reading tar header for restoring file"); + if (MORE_DEBUG) { + Slog.v(TAG, "Reading tar header for restoring file"); } info = tarBackupReader.readTarHeaders(); if (info != null) { - if (RefactoredBackupManagerService.MORE_DEBUG) { + if (MORE_DEBUG) { info.dump(); } @@ -165,9 +178,7 @@ public class FullRestoreEngine extends RestoreEngine { // one app's data but see a different app's on the wire if (onlyPackage != null) { if (!pkg.equals(onlyPackage.packageName)) { - Slog.w(RefactoredBackupManagerService.TAG, - "Expected data for " + onlyPackage - + " but saw " + pkg); + Slog.w(TAG, "Expected data for " + onlyPackage + " but saw " + pkg); setResult(RestoreEngine.TRANSPORT_FAILURE); setRunning(false); return false; @@ -183,9 +194,8 @@ public class FullRestoreEngine extends RestoreEngine { // Clean up the previous agent relationship if necessary, // and let the observer know we're considering a new app. if (mAgent != null) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "Saw new package; finalizing old one"); + if (DEBUG) { + Slog.d(TAG, "Saw new package; finalizing old one"); } // Now we're really done tearDownPipes(); @@ -195,19 +205,20 @@ public class FullRestoreEngine extends RestoreEngine { } } - if (info.path.equals(RefactoredBackupManagerService.BACKUP_MANIFEST_FILENAME)) { - RestorePolicy appManifest = tarBackupReader.readAppManifest( - backupManagerService.getPackageManager(), allowApks, - mManifestSignatures, info); - mPackagePolicies.put(pkg, appManifest); + if (info.path.equals(BACKUP_MANIFEST_FILENAME)) { + Signature[] signatures = tarBackupReader.readAppManifestAndReturnSignatures( + info); + RestorePolicy restorePolicy = tarBackupReader.chooseRestorePolicy( + mBackupManagerService.getPackageManager(), allowApks, info, signatures); + mManifestSignatures.put(info.packageName, signatures); + mPackagePolicies.put(pkg, restorePolicy); mPackageInstallers.put(pkg, info.installerPackageName); // We've read only the manifest content itself at this point, // so consume the footer before looping around to the next // input file tarBackupReader.skipTarPadding(info.size); mObserver = FullBackupRestoreObserverUtils.sendOnRestorePackage(mObserver, pkg); - } else if (info.path.equals( - RefactoredBackupManagerService.BACKUP_METADATA_FILENAME)) { + } else if (info.path.equals(BACKUP_METADATA_FILENAME)) { // Metadata blobs! tarBackupReader.readMetadata(info); @@ -234,18 +245,17 @@ public class FullRestoreEngine extends RestoreEngine { // If we're in accept-if-apk state, then the first file we // see MUST be the apk. if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "APK file; installing"); + if (DEBUG) { + Slog.d(TAG, "APK file; installing"); } // Try to install the app. String installerName = mPackageInstallers.get(pkg); boolean isSuccessfullyInstalled = RestoreUtils.installApk( - instream, backupManagerService.getPackageManager(), + instream, mBackupManagerService.getPackageManager(), mInstallObserver, mDeleteObserver, mManifestSignatures, mPackagePolicies, info, installerName, - bytesReadListener, backupManagerService.getDataDir() - ); + bytesReadListener, mBackupManagerService.getDataDir() + ); // good to go; promote to ACCEPT mPackagePolicies.put(pkg, isSuccessfullyInstalled ? RestorePolicy.ACCEPT @@ -265,9 +275,8 @@ public class FullRestoreEngine extends RestoreEngine { case ACCEPT: if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "apk present but ACCEPT"); + if (DEBUG) { + Slog.d(TAG, "apk present but ACCEPT"); } // we can take the data without the apk, so we // *want* to do so. skip the apk by declaring this @@ -281,8 +290,7 @@ public class FullRestoreEngine extends RestoreEngine { // Something has gone dreadfully wrong when determining // the restore policy from the manifest. Ignore the // rest of this package's data. - Slog.e(RefactoredBackupManagerService.TAG, - "Invalid policy from manifest"); + Slog.e(TAG, "Invalid policy from manifest"); okay = false; mPackagePolicies.put(pkg, RestorePolicy.IGNORE); break; @@ -295,19 +303,17 @@ public class FullRestoreEngine extends RestoreEngine { // If the policy is satisfied, go ahead and set up to pipe the // data to the agent. - if (RefactoredBackupManagerService.MORE_DEBUG && okay && mAgent != null) { - Slog.i(RefactoredBackupManagerService.TAG, - "Reusing existing agent instance"); + if (MORE_DEBUG && okay && mAgent != null) { + Slog.i(TAG, "Reusing existing agent instance"); } if (okay && mAgent == null) { - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "Need to launch agent for " + pkg); + if (MORE_DEBUG) { + Slog.d(TAG, "Need to launch agent for " + pkg); } try { mTargetApp = - backupManagerService.getPackageManager().getApplicationInfo( + mBackupManagerService.getPackageManager().getApplicationInfo( pkg, 0); // If we haven't sent any data to this app yet, we probably @@ -317,29 +323,28 @@ public class FullRestoreEngine extends RestoreEngine { // responsible for coherently managing a full // restore. if (mTargetApp.backupAgentName == null) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, + if (DEBUG) { + Slog.d(TAG, "Clearing app data preparatory to full restore"); } - backupManagerService.clearApplicationDataSynchronous(pkg); + mBackupManagerService.clearApplicationDataSynchronous(pkg); } else { - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, "backup agent (" + if (MORE_DEBUG) { + Slog.d(TAG, "backup agent (" + mTargetApp.backupAgentName + ") => no clear"); } } mClearedPackages.add(pkg); } else { - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "We've initialized this app already; no clear " - + "required"); + if (MORE_DEBUG) { + Slog.d(TAG, "We've initialized this app already; no clear " + + "required"); } } // All set; now set up the IPC and launch the agent setUpPipes(); - mAgent = backupManagerService.bindToAgentSynchronous(mTargetApp, + mAgent = mBackupManagerService.bindToAgentSynchronous(mTargetApp, ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL); mAgentPackage = pkg; } catch (IOException e) { @@ -349,9 +354,7 @@ public class FullRestoreEngine extends RestoreEngine { } if (mAgent == null) { - Slog.e( - RefactoredBackupManagerService.TAG, - "Unable to create agent for " + pkg); + Slog.e(TAG, "Unable to create agent for " + pkg); okay = false; tearDownPipes(); mPackagePolicies.put(pkg, RestorePolicy.IGNORE); @@ -361,7 +364,7 @@ public class FullRestoreEngine extends RestoreEngine { // Sanity check: make sure we never give data to the wrong app. This // should never happen but a little paranoia here won't go amiss. if (okay && !pkg.equals(mAgentPackage)) { - Slog.e(RefactoredBackupManagerService.TAG, "Restoring data for " + pkg + Slog.e(TAG, "Restoring data for " + pkg + " but agent is for " + mAgentPackage); okay = false; } @@ -373,84 +376,74 @@ public class FullRestoreEngine extends RestoreEngine { if (okay) { boolean agentSuccess = true; long toCopy = info.size; - final boolean isSharedStorage = pkg.equals( - RefactoredBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE); + final boolean isSharedStorage = pkg.equals(SHARED_BACKUP_AGENT_PACKAGE); final long timeout = isSharedStorage ? - RefactoredBackupManagerService.TIMEOUT_SHARED_BACKUP_INTERVAL : - RefactoredBackupManagerService.TIMEOUT_RESTORE_INTERVAL; + TIMEOUT_SHARED_BACKUP_INTERVAL : + TIMEOUT_RESTORE_INTERVAL; try { - backupManagerService.prepareOperationTimeout(token, + mBackupManagerService.prepareOperationTimeout(token, timeout, mMonitorTask, - RefactoredBackupManagerService.OP_TYPE_RESTORE_WAIT); + OP_TYPE_RESTORE_WAIT); if (FullBackup.OBB_TREE_TOKEN.equals(info.domain)) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "Restoring OBB file for " + pkg - + " : " + info.path); + if (DEBUG) { + Slog.d(TAG, "Restoring OBB file for " + pkg + + " : " + info.path); } mObbConnection.restoreObbFile(pkg, mPipes[0], info.size, info.type, info.path, info.mode, info.mtime, token, - backupManagerService.getBackupManagerBinder()); + mBackupManagerService.getBackupManagerBinder()); } else if (FullBackup.KEY_VALUE_DATA_TOKEN.equals(info.domain)) { // This is only possible during adb restore. // TODO: Refactor to clearly separate the flows. - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "Restoring key-value file for " + pkg - + " : " + info.path); + if (DEBUG) { + Slog.d(TAG, "Restoring key-value file for " + pkg + + " : " + info.path); } KeyValueAdbRestoreEngine restoreEngine = new KeyValueAdbRestoreEngine( - backupManagerService, - backupManagerService.getDataDir(), info, mPipes[0], + mBackupManagerService, + mBackupManagerService.getDataDir(), info, mPipes[0], mAgent, token); new Thread(restoreEngine, "restore-key-value-runner").start(); } else { - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "Invoking agent to restore file " - + info.path); + if (MORE_DEBUG) { + Slog.d(TAG, "Invoking agent to restore file " + info.path); } // fire up the app's agent listening on the socket. If // the agent is running in the system process we can't // just invoke it asynchronously, so we provide a thread // for it here. if (mTargetApp.processName.equals("system")) { - Slog.d(RefactoredBackupManagerService.TAG, - "system process agent - spinning a thread"); + Slog.d(TAG, "system process agent - spinning a thread"); RestoreFileRunnable runner = new RestoreFileRunnable( - backupManagerService, mAgent, info, mPipes[0], token); + mBackupManagerService, mAgent, info, mPipes[0], token); new Thread(runner, "restore-sys-runner").start(); } else { mAgent.doRestoreFile(mPipes[0], info.size, info.type, info.domain, info.path, info.mode, info.mtime, - token, backupManagerService.getBackupManagerBinder()); + token, mBackupManagerService.getBackupManagerBinder()); } } } catch (IOException e) { // couldn't dup the socket for a process-local restore - Slog.d(RefactoredBackupManagerService.TAG, - "Couldn't establish restore"); + Slog.d(TAG, "Couldn't establish restore"); agentSuccess = false; okay = false; } catch (RemoteException e) { // whoops, remote entity went away. We'll eat the content // ourselves, then, and not copy it over. - Slog.e(RefactoredBackupManagerService.TAG, - "Agent crashed during full restore"); + Slog.e(TAG, "Agent crashed during full restore"); agentSuccess = false; okay = false; } // Copy over the data if the agent is still good if (okay) { - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.v(RefactoredBackupManagerService.TAG, - " copying to restore agent: " - + toCopy + " bytes"); + if (MORE_DEBUG) { + Slog.v(TAG, " copying to restore agent: " + toCopy + " bytes"); } boolean pipeOkay = true; FileOutputStream pipe = new FileOutputStream( @@ -473,9 +466,8 @@ public class FullRestoreEngine extends RestoreEngine { try { pipe.write(buffer, 0, nRead); } catch (IOException e) { - Slog.e(RefactoredBackupManagerService.TAG, - "Failed to write to restore pipe: " - + e.getMessage()); + Slog.e(TAG, "Failed to write to restore pipe: " + + e.getMessage()); pipeOkay = false; } } @@ -487,15 +479,14 @@ public class FullRestoreEngine extends RestoreEngine { // and now that we've sent it all, wait for the remote // side to acknowledge receipt - agentSuccess = backupManagerService.waitUntilOperationComplete(token); + agentSuccess = mBackupManagerService.waitUntilOperationComplete(token); } // okay, if the remote end failed at any point, deal with // it by ignoring the rest of the restore on it if (!agentSuccess) { - Slog.w(RefactoredBackupManagerService.TAG, - "Agent failure restoring " + pkg + "; ending restore"); - backupManagerService.getBackupHandler().removeMessages( + Slog.w(TAG, "Agent failure restoring " + pkg + "; ending restore"); + mBackupManagerService.getBackupHandler().removeMessages( MSG_RESTORE_OPERATION_TIMEOUT); tearDownPipes(); tearDownAgent(mTargetApp); @@ -516,8 +507,8 @@ public class FullRestoreEngine extends RestoreEngine { // dropped file, or an already-ignored package: skip to the // next stream entry by reading and discarding this file. if (!okay) { - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, "[discarding file content]"); + if (MORE_DEBUG) { + Slog.d(TAG, "[discarding file content]"); } long bytesToConsume = (info.size + 511) & ~511; while (bytesToConsume > 0) { @@ -536,9 +527,8 @@ public class FullRestoreEngine extends RestoreEngine { } } } catch (IOException e) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.w(RefactoredBackupManagerService.TAG, - "io exception on restore socket read: " + e.getMessage()); + if (DEBUG) { + Slog.w(TAG, "io exception on restore socket read: " + e.getMessage()); } setResult(RestoreEngine.TRANSPORT_FAILURE); info = null; @@ -546,9 +536,8 @@ public class FullRestoreEngine extends RestoreEngine { // If we got here we're either running smoothly or we've finished if (info == null) { - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.i(RefactoredBackupManagerService.TAG, - "No [more] data for this package; tearing down"); + if (MORE_DEBUG) { + Slog.i(TAG, "No [more] data for this package; tearing down"); } tearDownPipes(); setRunning(false); @@ -559,11 +548,11 @@ public class FullRestoreEngine extends RestoreEngine { return (info != null); } - void setUpPipes() throws IOException { + private void setUpPipes() throws IOException { mPipes = ParcelFileDescriptor.createPipe(); } - void tearDownPipes() { + private void tearDownPipes() { // Teardown might arise from the inline restore processing or from the asynchronous // timeout mechanism, and these might race. Make sure we don't try to close and // null out the pipes twice. @@ -575,16 +564,16 @@ public class FullRestoreEngine extends RestoreEngine { mPipes[1].close(); mPipes[1] = null; } catch (IOException e) { - Slog.w(RefactoredBackupManagerService.TAG, "Couldn't close agent pipes", e); + Slog.w(TAG, "Couldn't close agent pipes", e); } mPipes = null; } } } - void tearDownAgent(ApplicationInfo app) { + private void tearDownAgent(ApplicationInfo app) { if (mAgent != null) { - backupManagerService.tearDownAgentAndKill(app); + mBackupManagerService.tearDownAgentAndKill(app); mAgent = null; } } @@ -595,13 +584,10 @@ public class FullRestoreEngine extends RestoreEngine { setRunning(false); } - final RestoreInstallObserver mInstallObserver = new RestoreInstallObserver(); - final RestoreDeleteObserver mDeleteObserver = new RestoreDeleteObserver(); - private static boolean isRestorableFile(FileMetadata info) { if (FullBackup.CACHE_TREE_TOKEN.equals(info.domain)) { - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.i(RefactoredBackupManagerService.TAG, "Dropping cache file path " + info.path); + if (MORE_DEBUG) { + Slog.i(TAG, "Dropping cache file path " + info.path); } return false; } @@ -612,9 +598,8 @@ public class FullRestoreEngine extends RestoreEngine { // API. Respect the no-backup intention and don't let the data get to // the app. if (info.path.startsWith("no_backup/")) { - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.i(RefactoredBackupManagerService.TAG, - "Dropping no_backup file path " + info.path); + if (MORE_DEBUG) { + Slog.i(TAG, "Dropping no_backup file path " + info.path); } return false; } @@ -626,8 +611,8 @@ public class FullRestoreEngine extends RestoreEngine { private static boolean isCanonicalFilePath(String path) { if (path.contains("..") || path.contains("//")) { - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.w(RefactoredBackupManagerService.TAG, "Dropping invalid path " + path); + if (MORE_DEBUG) { + Slog.w(TAG, "Dropping invalid path " + path); } return false; } @@ -641,8 +626,7 @@ public class FullRestoreEngine extends RestoreEngine { // TODO: use a more user-friendly name string mObserver.onRestorePackage(name); } catch (RemoteException e) { - Slog.w(RefactoredBackupManagerService.TAG, - "full restore observer went away: restorePackage"); + Slog.w(TAG, "full restore observer went away: restorePackage"); mObserver = null; } } diff --git a/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java index 6b265612408db..f3a68a08e9d43 100644 --- a/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java +++ b/services/backup/java/com/android/server/backup/restore/PerformAdbRestoreTask.java @@ -16,6 +16,20 @@ package com.android.server.backup.restore; +import static com.android.server.backup.RefactoredBackupManagerService.BACKUP_FILE_HEADER_MAGIC; +import static com.android.server.backup.RefactoredBackupManagerService.BACKUP_FILE_VERSION; +import static com.android.server.backup.RefactoredBackupManagerService.BACKUP_MANIFEST_FILENAME; +import static com.android.server.backup.RefactoredBackupManagerService.BACKUP_METADATA_FILENAME; +import static com.android.server.backup.RefactoredBackupManagerService.DEBUG; +import static com.android.server.backup.RefactoredBackupManagerService.MORE_DEBUG; +import static com.android.server.backup.RefactoredBackupManagerService.OP_TYPE_RESTORE_WAIT; +import static com.android.server.backup.RefactoredBackupManagerService.PBKDF_CURRENT; +import static com.android.server.backup.RefactoredBackupManagerService.PBKDF_FALLBACK; +import static com.android.server.backup.RefactoredBackupManagerService.SETTINGS_PACKAGE; +import static com.android.server.backup.RefactoredBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE; +import static com.android.server.backup.RefactoredBackupManagerService.TAG; +import static com.android.server.backup.RefactoredBackupManagerService.TIMEOUT_FULL_BACKUP_INTERVAL; +import static com.android.server.backup.RefactoredBackupManagerService.TIMEOUT_RESTORE_INTERVAL; import static com.android.server.backup.internal.BackupHandler.MSG_RESTORE_OPERATION_TIMEOUT; import android.app.ApplicationThreadConstants; @@ -32,6 +46,7 @@ import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; import com.android.server.backup.FileMetadata; import com.android.server.backup.KeyValueAdbRestoreEngine; import com.android.server.backup.PackageManagerBackupAgent; @@ -43,7 +58,6 @@ import com.android.server.backup.utils.PasswordUtils; import com.android.server.backup.utils.RestoreUtils; import com.android.server.backup.utils.TarBackupReader; -import java.io.DataInputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -68,38 +82,44 @@ import javax.crypto.spec.SecretKeySpec; public class PerformAdbRestoreTask implements Runnable { - private RefactoredBackupManagerService backupManagerService; - ParcelFileDescriptor mInputFile; - String mCurrentPassword; - String mDecryptPassword; - IFullBackupRestoreObserver mObserver; - AtomicBoolean mLatchObject; - IBackupAgent mAgent; - PackageManagerBackupAgent mPackageManagerBackupAgent; - String mAgentPackage; - ApplicationInfo mTargetApp; - FullBackupObbConnection mObbConnection = null; - ParcelFileDescriptor[] mPipes = null; - byte[] mWidgetData = null; + private final RefactoredBackupManagerService mBackupManagerService; + private final ParcelFileDescriptor mInputFile; + private final String mCurrentPassword; + private final String mDecryptPassword; + private final AtomicBoolean mLatchObject; + private final PackageManagerBackupAgent mPackageManagerBackupAgent; + private final RestoreInstallObserver mInstallObserver = new RestoreInstallObserver(); + private final RestoreDeleteObserver mDeleteObserver = new RestoreDeleteObserver(); - long mBytes; + private IFullBackupRestoreObserver mObserver; + private IBackupAgent mAgent; + private String mAgentPackage; + private ApplicationInfo mTargetApp; + private FullBackupObbConnection mObbConnection = null; + private ParcelFileDescriptor[] mPipes = null; + private byte[] mWidgetData = null; + + private long mBytes; // Runner that can be placed on a separate thread to do in-process invocation // of the "restore finished" API asynchronously. Used by adb restore. - class RestoreFinishedRunnable implements Runnable { + private static class RestoreFinishedRunnable implements Runnable { - final IBackupAgent mAgent; - final int mToken; + private final IBackupAgent mAgent; + private final int mToken; + private final RefactoredBackupManagerService mBackupManagerService; - RestoreFinishedRunnable(IBackupAgent agent, int token) { + RestoreFinishedRunnable(IBackupAgent agent, int token, + RefactoredBackupManagerService backupManagerService) { mAgent = agent; mToken = token; + mBackupManagerService = backupManagerService; } @Override public void run() { try { - mAgent.doRestoreFinished(mToken, backupManagerService.getBackupManagerBinder()); + mAgent.doRestoreFinished(mToken, mBackupManagerService.getBackupManagerBinder()); } catch (RemoteException e) { // never happens; this is used only for local binder calls } @@ -107,23 +127,23 @@ public class PerformAdbRestoreTask implements Runnable { } // possible handling states for a given package in the restore dataset - final HashMap mPackagePolicies + private final HashMap mPackagePolicies = new HashMap<>(); // installer package names for each encountered app, derived from the manifests - final HashMap mPackageInstallers = new HashMap<>(); + private final HashMap mPackageInstallers = new HashMap<>(); // Signatures for a given package found in its manifest file - final HashMap mManifestSignatures + private final HashMap mManifestSignatures = new HashMap<>(); // Packages we've already wiped data on when restoring their first file - final HashSet mClearedPackages = new HashSet<>(); + private final HashSet mClearedPackages = new HashSet<>(); public PerformAdbRestoreTask(RefactoredBackupManagerService backupManagerService, ParcelFileDescriptor fd, String curPassword, String decryptPassword, IFullBackupRestoreObserver observer, AtomicBoolean latch) { - this.backupManagerService = backupManagerService; + this.mBackupManagerService = backupManagerService; mInputFile = fd; mCurrentPassword = curPassword; mDecryptPassword = decryptPassword; @@ -139,115 +159,66 @@ public class PerformAdbRestoreTask implements Runnable { // Which packages we've already wiped data on. We prepopulate this // with a whitelist of packages known to be unclearable. mClearedPackages.add("android"); - mClearedPackages.add(RefactoredBackupManagerService.SETTINGS_PACKAGE); + mClearedPackages.add(SETTINGS_PACKAGE); } @Override public void run() { - Slog.i(RefactoredBackupManagerService.TAG, "--- Performing full-dataset restore ---"); + Slog.i(TAG, "--- Performing full-dataset restore ---"); mObbConnection.establish(); mObserver = FullBackupRestoreObserverUtils.sendStartRestore(mObserver); // Are we able to restore shared-storage data? if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - mPackagePolicies.put(RefactoredBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE, + mPackagePolicies.put(SHARED_BACKUP_AGENT_PACKAGE, RestorePolicy.ACCEPT); } FileInputStream rawInStream = null; - DataInputStream rawDataIn = null; try { - if (!backupManagerService.backupPasswordMatches(mCurrentPassword)) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.w(RefactoredBackupManagerService.TAG, - "Backup password mismatch; aborting"); + if (!mBackupManagerService.backupPasswordMatches(mCurrentPassword)) { + if (DEBUG) { + Slog.w(TAG, "Backup password mismatch; aborting"); } return; } mBytes = 0; - byte[] buffer = new byte[32 * 1024]; + rawInStream = new FileInputStream(mInputFile.getFileDescriptor()); - rawDataIn = new DataInputStream(rawInStream); - // First, parse out the unencrypted/uncompressed header - boolean compressed = false; - InputStream preCompressStream = rawInStream; - final InputStream in; - - boolean okay = false; - final int headerLen = RefactoredBackupManagerService.BACKUP_FILE_HEADER_MAGIC.length(); - byte[] streamHeader = new byte[headerLen]; - rawDataIn.readFully(streamHeader); - byte[] magicBytes = RefactoredBackupManagerService.BACKUP_FILE_HEADER_MAGIC.getBytes( - "UTF-8"); - if (Arrays.equals(magicBytes, streamHeader)) { - // okay, header looks good. now parse out the rest of the fields. - String s = readHeaderLine(rawInStream); - final int archiveVersion = Integer.parseInt(s); - if (archiveVersion <= RefactoredBackupManagerService.BACKUP_FILE_VERSION) { - // okay, it's a version we recognize. if it's version 1, we may need - // to try two different PBKDF2 regimes to compare checksums. - final boolean pbkdf2Fallback = (archiveVersion == 1); - - s = readHeaderLine(rawInStream); - compressed = (Integer.parseInt(s) != 0); - s = readHeaderLine(rawInStream); - if (s.equals("none")) { - // no more header to parse; we're good to go - okay = true; - } else if (mDecryptPassword != null && mDecryptPassword.length() > 0) { - preCompressStream = decodeAesHeaderAndInitialize(s, pbkdf2Fallback, - rawInStream); - if (preCompressStream != null) { - okay = true; - } - } else { - Slog.w(RefactoredBackupManagerService.TAG, - "Archive is encrypted but no password given"); - } - } else { - Slog.w(RefactoredBackupManagerService.TAG, "Wrong header version: " + s); - } - } else { - Slog.w(RefactoredBackupManagerService.TAG, "Didn't read the right header magic"); - } - - if (!okay) { - Slog.w(RefactoredBackupManagerService.TAG, "Invalid restore data; aborting."); + InputStream tarInputStream = parseBackupFileHeaderAndReturnTarStream(rawInStream, + mDecryptPassword); + if (tarInputStream == null) { + // There was an error reading the backup file, which is already handled and logged. + // Just abort. return; } - // okay, use the right stream layer based on compression - in = (compressed) ? new InflaterInputStream(preCompressStream) : preCompressStream; - + byte[] buffer = new byte[32 * 1024]; boolean didRestore; do { - didRestore = restoreOneFile(in, false /* mustKillAgent */, buffer, + didRestore = restoreOneFile(tarInputStream, false /* mustKillAgent */, buffer, null /* onlyPackage */, true /* allowApks */, - backupManagerService.generateRandomIntegerToken(), null /* monitor */); + mBackupManagerService.generateRandomIntegerToken(), null /* monitor */); } while (didRestore); - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.v(RefactoredBackupManagerService.TAG, - "Done consuming input tarfile, total bytes=" + mBytes); + if (MORE_DEBUG) { + Slog.v(TAG, "Done consuming input tarfile, total bytes=" + mBytes); } } catch (IOException e) { - Slog.e(RefactoredBackupManagerService.TAG, "Unable to read restore input"); + Slog.e(TAG, "Unable to read restore input"); } finally { tearDownPipes(); tearDownAgent(mTargetApp, true); try { - if (rawDataIn != null) { - rawDataIn.close(); - } if (rawInStream != null) { rawInStream.close(); } mInputFile.close(); } catch (IOException e) { - Slog.w(RefactoredBackupManagerService.TAG, "Close of restore data pipe threw", e); + Slog.w(TAG, "Close of restore data pipe threw", e); /* nothing we can do about this */ } synchronized (mLatchObject) { @@ -256,12 +227,79 @@ public class PerformAdbRestoreTask implements Runnable { } mObbConnection.tearDown(); mObserver = FullBackupRestoreObserverUtils.sendEndRestore(mObserver); - Slog.d(RefactoredBackupManagerService.TAG, "Full restore pass complete."); - backupManagerService.getWakelock().release(); + Slog.d(TAG, "Full restore pass complete."); + mBackupManagerService.getWakelock().release(); } } - String readHeaderLine(InputStream in) throws IOException { + private static void readFullyOrThrow(InputStream in, byte[] buffer) throws IOException { + int offset = 0; + while (offset < buffer.length) { + int bytesRead = in.read(buffer, offset, buffer.length - offset); + if (bytesRead <= 0) { + throw new IOException("Couldn't fully read data"); + } + offset += bytesRead; + } + } + + @VisibleForTesting + public static InputStream parseBackupFileHeaderAndReturnTarStream( + InputStream rawInputStream, + String decryptPassword) + throws IOException { + // First, parse out the unencrypted/uncompressed header + boolean compressed = false; + InputStream preCompressStream = rawInputStream; + + boolean okay = false; + final int headerLen = BACKUP_FILE_HEADER_MAGIC.length(); + byte[] streamHeader = new byte[headerLen]; + readFullyOrThrow(rawInputStream, streamHeader); + byte[] magicBytes = BACKUP_FILE_HEADER_MAGIC.getBytes( + "UTF-8"); + if (Arrays.equals(magicBytes, streamHeader)) { + // okay, header looks good. now parse out the rest of the fields. + String s = readHeaderLine(rawInputStream); + final int archiveVersion = Integer.parseInt(s); + if (archiveVersion <= BACKUP_FILE_VERSION) { + // okay, it's a version we recognize. if it's version 1, we may need + // to try two different PBKDF2 regimes to compare checksums. + final boolean pbkdf2Fallback = (archiveVersion == 1); + + s = readHeaderLine(rawInputStream); + compressed = (Integer.parseInt(s) != 0); + s = readHeaderLine(rawInputStream); + if (s.equals("none")) { + // no more header to parse; we're good to go + okay = true; + } else if (decryptPassword != null && decryptPassword.length() > 0) { + preCompressStream = decodeAesHeaderAndInitialize( + decryptPassword, s, pbkdf2Fallback, + rawInputStream); + if (preCompressStream != null) { + okay = true; + } + } else { + Slog.w(TAG, "Archive is encrypted but no password given"); + } + } else { + Slog.w(TAG, "Wrong header version: " + s); + } + } else { + Slog.w(TAG, "Didn't read the right header magic"); + } + + if (!okay) { + Slog.w(TAG, "Invalid restore data; aborting."); + return null; + } + + // okay, use the right stream layer based on compression + return compressed ? new InflaterInputStream(preCompressStream) : preCompressStream; + } + + private static String readHeaderLine(InputStream in) throws IOException { int c; StringBuilder buffer = new StringBuilder(80); while ((c = in.read()) >= 0) { @@ -273,7 +311,8 @@ public class PerformAdbRestoreTask implements Runnable { return buffer.toString(); } - InputStream attemptMasterKeyDecryption(String algorithm, byte[] userSalt, byte[] ckSalt, + private static InputStream attemptMasterKeyDecryption(String decryptPassword, String algorithm, + byte[] userSalt, byte[] ckSalt, int rounds, String userIvHex, String masterKeyBlobHex, InputStream rawInStream, boolean doLog) { InputStream result = null; @@ -281,7 +320,7 @@ public class PerformAdbRestoreTask implements Runnable { try { Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKey userKey = PasswordUtils - .buildPasswordKey(algorithm, mDecryptPassword, userSalt, + .buildPasswordKey(algorithm, decryptPassword, userSalt, rounds); byte[] IV = PasswordUtils.hexToByteArray(userIvHex); IvParameterSpec ivSpec = new IvParameterSpec(IV); @@ -317,11 +356,11 @@ public class PerformAdbRestoreTask implements Runnable { // Only if all of the above worked properly will 'result' be assigned result = new CipherInputStream(rawInStream, c); } else if (doLog) { - Slog.w(RefactoredBackupManagerService.TAG, "Incorrect password"); + Slog.w(TAG, "Incorrect password"); } } catch (InvalidAlgorithmParameterException e) { if (doLog) { - Slog.e(RefactoredBackupManagerService.TAG, "Needed parameter spec unavailable!", e); + Slog.e(TAG, "Needed parameter spec unavailable!", e); } } catch (BadPaddingException e) { // This case frequently occurs when the wrong password is used to decrypt @@ -329,31 +368,32 @@ public class PerformAdbRestoreTask implements Runnable { // used in the checksum failure log in order to avoid providing additional // information to an attacker. if (doLog) { - Slog.w(RefactoredBackupManagerService.TAG, "Incorrect password"); + Slog.w(TAG, "Incorrect password"); } } catch (IllegalBlockSizeException e) { if (doLog) { - Slog.w(RefactoredBackupManagerService.TAG, "Invalid block size in master key"); + Slog.w(TAG, "Invalid block size in master key"); } } catch (NoSuchAlgorithmException e) { if (doLog) { - Slog.e(RefactoredBackupManagerService.TAG, - "Needed decryption algorithm unavailable!"); + Slog.e(TAG, "Needed decryption algorithm unavailable!"); } } catch (NoSuchPaddingException e) { if (doLog) { - Slog.e(RefactoredBackupManagerService.TAG, "Needed padding mechanism unavailable!"); + Slog.e(TAG, "Needed padding mechanism unavailable!"); } } catch (InvalidKeyException e) { if (doLog) { - Slog.w(RefactoredBackupManagerService.TAG, "Illegal password; aborting"); + Slog.w(TAG, "Illegal password; aborting"); } } return result; } - InputStream decodeAesHeaderAndInitialize(String encryptionName, boolean pbkdf2Fallback, + private static InputStream decodeAesHeaderAndInitialize(String decryptPassword, + String encryptionName, + boolean pbkdf2Fallback, InputStream rawInStream) { InputStream result = null; try { @@ -371,22 +411,21 @@ public class PerformAdbRestoreTask implements Runnable { String masterKeyBlobHex = readHeaderLine(rawInStream); // 9 // decrypt the master key blob - result = attemptMasterKeyDecryption(RefactoredBackupManagerService.PBKDF_CURRENT, + result = attemptMasterKeyDecryption(decryptPassword, PBKDF_CURRENT, userSalt, ckSalt, rounds, userIvHex, masterKeyBlobHex, rawInStream, false); if (result == null && pbkdf2Fallback) { result = attemptMasterKeyDecryption( - RefactoredBackupManagerService.PBKDF_FALLBACK, userSalt, ckSalt, + decryptPassword, PBKDF_FALLBACK, userSalt, ckSalt, rounds, userIvHex, masterKeyBlobHex, rawInStream, true); } } else { - Slog.w(RefactoredBackupManagerService.TAG, - "Unsupported encryption method: " + encryptionName); + Slog.w(TAG, "Unsupported encryption method: " + encryptionName); } } catch (NumberFormatException e) { - Slog.w(RefactoredBackupManagerService.TAG, "Can't parse restore data header"); + Slog.w(TAG, "Can't parse restore data header"); } catch (IOException e) { - Slog.w(RefactoredBackupManagerService.TAG, "Can't read input header"); + Slog.w(TAG, "Can't read input header"); } return result; @@ -406,7 +445,7 @@ public class PerformAdbRestoreTask implements Runnable { try { info = tarBackupReader.readTarHeaders(); if (info != null) { - if (RefactoredBackupManagerService.MORE_DEBUG) { + if (MORE_DEBUG) { info.dump(); } @@ -421,9 +460,8 @@ public class PerformAdbRestoreTask implements Runnable { // Clean up the previous agent relationship if necessary, // and let the observer know we're considering a new app. if (mAgent != null) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "Saw new package; finalizing old one"); + if (DEBUG) { + Slog.d(TAG, "Saw new package; finalizing old one"); } // Now we're really done tearDownPipes(); @@ -433,19 +471,21 @@ public class PerformAdbRestoreTask implements Runnable { } } - if (info.path.equals(RefactoredBackupManagerService.BACKUP_MANIFEST_FILENAME)) { - RestorePolicy appManifest = tarBackupReader.readAppManifest( - backupManagerService.getPackageManager(), allowApks, - mManifestSignatures, info); - mPackagePolicies.put(pkg, appManifest); + if (info.path.equals(BACKUP_MANIFEST_FILENAME)) { + Signature[] signatures = tarBackupReader.readAppManifestAndReturnSignatures( + info); + RestorePolicy restorePolicy = tarBackupReader.chooseRestorePolicy( + mBackupManagerService.getPackageManager(), allowApks, + info, signatures); + mManifestSignatures.put(info.packageName, signatures); + mPackagePolicies.put(pkg, restorePolicy); mPackageInstallers.put(pkg, info.installerPackageName); // We've read only the manifest content itself at this point, // so consume the footer before looping around to the next // input file tarBackupReader.skipTarPadding(info.size); mObserver = FullBackupRestoreObserverUtils.sendOnRestorePackage(mObserver, pkg); - } else if (info.path.equals( - RefactoredBackupManagerService.BACKUP_METADATA_FILENAME)) { + } else if (info.path.equals(BACKUP_METADATA_FILENAME)) { // Metadata blobs! tarBackupReader.readMetadata(info); @@ -472,18 +512,17 @@ public class PerformAdbRestoreTask implements Runnable { // If we're in accept-if-apk state, then the first file we // see MUST be the apk. if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "APK file; installing"); + if (DEBUG) { + Slog.d(TAG, "APK file; installing"); } // Try to install the app. String installerName = mPackageInstallers.get(pkg); boolean isSuccessfullyInstalled = RestoreUtils.installApk( - instream, backupManagerService.getPackageManager(), + instream, mBackupManagerService.getPackageManager(), mInstallObserver, mDeleteObserver, mManifestSignatures, mPackagePolicies, info, installerName, - bytesReadListener, backupManagerService.getDataDir() - ); + bytesReadListener, mBackupManagerService.getDataDir() + ); // good to go; promote to ACCEPT mPackagePolicies.put(pkg, isSuccessfullyInstalled ? RestorePolicy.ACCEPT @@ -503,9 +542,8 @@ public class PerformAdbRestoreTask implements Runnable { case ACCEPT: if (info.domain.equals(FullBackup.APK_TREE_TOKEN)) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "apk present but ACCEPT"); + if (DEBUG) { + Slog.d(TAG, "apk present but ACCEPT"); } // we can take the data without the apk, so we // *want* to do so. skip the apk by declaring this @@ -519,8 +557,7 @@ public class PerformAdbRestoreTask implements Runnable { // Something has gone dreadfully wrong when determining // the restore policy from the manifest. Ignore the // rest of this package's data. - Slog.e(RefactoredBackupManagerService.TAG, - "Invalid policy from manifest"); + Slog.e(TAG, "Invalid policy from manifest"); okay = false; mPackagePolicies.put(pkg, RestorePolicy.IGNORE); break; @@ -533,19 +570,17 @@ public class PerformAdbRestoreTask implements Runnable { // If the policy is satisfied, go ahead and set up to pipe the // data to the agent. - if (RefactoredBackupManagerService.DEBUG && okay && mAgent != null) { - Slog.i(RefactoredBackupManagerService.TAG, - "Reusing existing agent instance"); + if (DEBUG && okay && mAgent != null) { + Slog.i(TAG, "Reusing existing agent instance"); } if (okay && mAgent == null) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "Need to launch agent for " + pkg); + if (DEBUG) { + Slog.d(TAG, "Need to launch agent for " + pkg); } try { mTargetApp = - backupManagerService.getPackageManager().getApplicationInfo( + mBackupManagerService.getPackageManager().getApplicationInfo( pkg, 0); // If we haven't sent any data to this app yet, we probably @@ -555,29 +590,28 @@ public class PerformAdbRestoreTask implements Runnable { // responsible for coherently managing a full // restore. if (mTargetApp.backupAgentName == null) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, + if (DEBUG) { + Slog.d(TAG, "Clearing app data preparatory to full restore"); } - backupManagerService.clearApplicationDataSynchronous(pkg); + mBackupManagerService.clearApplicationDataSynchronous(pkg); } else { - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, "backup agent (" + if (DEBUG) { + Slog.d(TAG, "backup agent (" + mTargetApp.backupAgentName + ") => no clear"); } } mClearedPackages.add(pkg); } else { - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "We've initialized this app already; no clear " - + "required"); + if (DEBUG) { + Slog.d(TAG, "We've initialized this app already; no clear " + + "required"); } } // All set; now set up the IPC and launch the agent setUpPipes(); - mAgent = backupManagerService.bindToAgentSynchronous(mTargetApp, + mAgent = mBackupManagerService.bindToAgentSynchronous(mTargetApp, ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL); mAgentPackage = pkg; } catch (IOException e) { @@ -587,9 +621,7 @@ public class PerformAdbRestoreTask implements Runnable { } if (mAgent == null) { - Slog.e( - RefactoredBackupManagerService.TAG, - "Unable to create agent for " + pkg); + Slog.e(TAG, "Unable to create agent for " + pkg); okay = false; tearDownPipes(); mPackagePolicies.put(pkg, RestorePolicy.IGNORE); @@ -599,7 +631,7 @@ public class PerformAdbRestoreTask implements Runnable { // Sanity check: make sure we never give data to the wrong app. This // should never happen but a little paranoia here won't go amiss. if (okay && !pkg.equals(mAgentPackage)) { - Slog.e(RefactoredBackupManagerService.TAG, "Restoring data for " + pkg + Slog.e(TAG, "Restoring data for " + pkg + " but agent is for " + mAgentPackage); okay = false; } @@ -612,67 +644,60 @@ public class PerformAdbRestoreTask implements Runnable { boolean agentSuccess = true; long toCopy = info.size; try { - backupManagerService + mBackupManagerService .prepareOperationTimeout(token, - RefactoredBackupManagerService.TIMEOUT_RESTORE_INTERVAL, + TIMEOUT_RESTORE_INTERVAL, null, - RefactoredBackupManagerService.OP_TYPE_RESTORE_WAIT); + OP_TYPE_RESTORE_WAIT); if (FullBackup.OBB_TREE_TOKEN.equals(info.domain)) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "Restoring OBB file for " + pkg - + " : " + info.path); + if (DEBUG) { + Slog.d(TAG, "Restoring OBB file for " + pkg + + " : " + info.path); } mObbConnection.restoreObbFile(pkg, mPipes[0], info.size, info.type, info.path, info.mode, info.mtime, token, - backupManagerService.getBackupManagerBinder()); + mBackupManagerService.getBackupManagerBinder()); } else if (FullBackup.KEY_VALUE_DATA_TOKEN.equals(info.domain)) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "Restoring key-value file for " + pkg - + " : " + info.path); + if (DEBUG) { + Slog.d(TAG, "Restoring key-value file for " + pkg + + " : " + info.path); } KeyValueAdbRestoreEngine restoreEngine = new KeyValueAdbRestoreEngine( - backupManagerService, - backupManagerService.getDataDir(), info, mPipes[0], + mBackupManagerService, + mBackupManagerService.getDataDir(), info, mPipes[0], mAgent, token); new Thread(restoreEngine, "restore-key-value-runner").start(); } else { - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "Invoking agent to restore file " - + info.path); + if (DEBUG) { + Slog.d(TAG, "Invoking agent to restore file " + info.path); } // fire up the app's agent listening on the socket. If // the agent is running in the system process we can't // just invoke it asynchronously, so we provide a thread // for it here. if (mTargetApp.processName.equals("system")) { - Slog.d(RefactoredBackupManagerService.TAG, - "system process agent - spinning a thread"); + Slog.d(TAG, "system process agent - spinning a thread"); RestoreFileRunnable runner = new RestoreFileRunnable( - backupManagerService, mAgent, info, mPipes[0], token); + mBackupManagerService, mAgent, info, mPipes[0], token); new Thread(runner, "restore-sys-runner").start(); } else { mAgent.doRestoreFile(mPipes[0], info.size, info.type, info.domain, info.path, info.mode, info.mtime, - token, backupManagerService.getBackupManagerBinder()); + token, mBackupManagerService.getBackupManagerBinder()); } } } catch (IOException e) { // couldn't dup the socket for a process-local restore - Slog.d(RefactoredBackupManagerService.TAG, - "Couldn't establish restore"); + Slog.d(TAG, "Couldn't establish restore"); agentSuccess = false; okay = false; } catch (RemoteException e) { // whoops, remote entity went away. We'll eat the content // ourselves, then, and not copy it over. - Slog.e(RefactoredBackupManagerService.TAG, - "Agent crashed during full restore"); + Slog.e(TAG, "Agent crashed during full restore"); agentSuccess = false; okay = false; } @@ -700,8 +725,7 @@ public class PerformAdbRestoreTask implements Runnable { try { pipe.write(buffer, 0, nRead); } catch (IOException e) { - Slog.e(RefactoredBackupManagerService.TAG, - "Failed to write to restore pipe", e); + Slog.e(TAG, "Failed to write to restore pipe", e); pipeOkay = false; } } @@ -713,17 +737,16 @@ public class PerformAdbRestoreTask implements Runnable { // and now that we've sent it all, wait for the remote // side to acknowledge receipt - agentSuccess = backupManagerService.waitUntilOperationComplete(token); + agentSuccess = mBackupManagerService.waitUntilOperationComplete(token); } // okay, if the remote end failed at any point, deal with // it by ignoring the rest of the restore on it if (!agentSuccess) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "Agent failure restoring " + pkg + "; now ignoring"); + if (DEBUG) { + Slog.d(TAG, "Agent failure restoring " + pkg + "; now ignoring"); } - backupManagerService.getBackupHandler().removeMessages( + mBackupManagerService.getBackupHandler().removeMessages( MSG_RESTORE_OPERATION_TIMEOUT); tearDownPipes(); tearDownAgent(mTargetApp, false); @@ -735,8 +758,8 @@ public class PerformAdbRestoreTask implements Runnable { // ignored package: skip to the next tar stream entry by // reading and discarding this file. if (!okay) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, "[discarding file content]"); + if (DEBUG) { + Slog.d(TAG, "[discarding file content]"); } long bytesToConsume = (info.size + 511) & ~511; while (bytesToConsume > 0) { @@ -755,9 +778,8 @@ public class PerformAdbRestoreTask implements Runnable { } } } catch (IOException e) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.w(RefactoredBackupManagerService.TAG, "io exception on restore socket read", - e); + if (DEBUG) { + Slog.w(TAG, "io exception on restore socket read", e); } // treat as EOF info = null; @@ -768,8 +790,8 @@ public class PerformAdbRestoreTask implements Runnable { private static boolean isCanonicalFilePath(String path) { if (path.contains("..") || path.contains("//")) { - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.w(RefactoredBackupManagerService.TAG, "Dropping invalid path " + path); + if (MORE_DEBUG) { + Slog.w(TAG, "Dropping invalid path " + path); } return false; } @@ -777,11 +799,11 @@ public class PerformAdbRestoreTask implements Runnable { return true; } - void setUpPipes() throws IOException { + private void setUpPipes() throws IOException { mPipes = ParcelFileDescriptor.createPipe(); } - void tearDownPipes() { + private void tearDownPipes() { if (mPipes != null) { try { mPipes[0].close(); @@ -789,49 +811,46 @@ public class PerformAdbRestoreTask implements Runnable { mPipes[1].close(); mPipes[1] = null; } catch (IOException e) { - Slog.w(RefactoredBackupManagerService.TAG, "Couldn't close agent pipes", e); + Slog.w(TAG, "Couldn't close agent pipes", e); } mPipes = null; } } - void tearDownAgent(ApplicationInfo app, boolean doRestoreFinished) { + private void tearDownAgent(ApplicationInfo app, boolean doRestoreFinished) { if (mAgent != null) { try { // In the adb restore case, we do restore-finished here if (doRestoreFinished) { - final int token = backupManagerService.generateRandomIntegerToken(); + final int token = mBackupManagerService.generateRandomIntegerToken(); final AdbRestoreFinishedLatch latch = new AdbRestoreFinishedLatch( - backupManagerService, token); - backupManagerService + mBackupManagerService, token); + mBackupManagerService .prepareOperationTimeout(token, - RefactoredBackupManagerService.TIMEOUT_FULL_BACKUP_INTERVAL, + TIMEOUT_FULL_BACKUP_INTERVAL, latch, - RefactoredBackupManagerService.OP_TYPE_RESTORE_WAIT); + OP_TYPE_RESTORE_WAIT); if (mTargetApp.processName.equals("system")) { - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.d(RefactoredBackupManagerService.TAG, - "system agent - restoreFinished on thread"); + if (MORE_DEBUG) { + Slog.d(TAG, "system agent - restoreFinished on thread"); } - Runnable runner = new RestoreFinishedRunnable(mAgent, token); + Runnable runner = new RestoreFinishedRunnable(mAgent, token, + mBackupManagerService); new Thread(runner, "restore-sys-finished-runner").start(); } else { mAgent.doRestoreFinished(token, - backupManagerService.getBackupManagerBinder()); + mBackupManagerService.getBackupManagerBinder()); } latch.await(); } - backupManagerService.tearDownAgentAndKill(app); + mBackupManagerService.tearDownAgentAndKill(app); } catch (RemoteException e) { - Slog.d(RefactoredBackupManagerService.TAG, "Lost app trying to shut down"); + Slog.d(TAG, "Lost app trying to shut down"); } mAgent = null; } } - final RestoreInstallObserver mInstallObserver = new RestoreInstallObserver(); - final RestoreDeleteObserver mDeleteObserver = new RestoreDeleteObserver(); - } diff --git a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java index 12c1af2bbd4dd..b97b56cecc047 100644 --- a/services/backup/java/com/android/server/backup/utils/TarBackupReader.java +++ b/services/backup/java/com/android/server/backup/utils/TarBackupReader.java @@ -34,6 +34,10 @@ import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_SYSTEM_APP_NO import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_VERSIONS_MATCH; import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER; +import static com.android.server.backup.RefactoredBackupManagerService.DEBUG; +import static com.android.server.backup.RefactoredBackupManagerService.MORE_DEBUG; +import static com.android.server.backup.RefactoredBackupManagerService.TAG; + import android.app.backup.BackupAgent; import android.app.backup.BackupManagerMonitor; import android.app.backup.FullBackup; @@ -54,7 +58,6 @@ import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; -import java.util.HashMap; /** * Utility methods to read backup tar file. @@ -150,23 +153,20 @@ public class TarBackupReader { case '5': { info.type = BackupAgent.TYPE_DIRECTORY; if (info.size != 0) { - Slog.w(RefactoredBackupManagerService.TAG, - "Directory entry with nonzero size in header"); + Slog.w(TAG, "Directory entry with nonzero size in header"); info.size = 0; } break; } case 0: { // presume EOF - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.w(RefactoredBackupManagerService.TAG, - "Saw type=0 in tar header block, info=" + info); + if (MORE_DEBUG) { + Slog.w(TAG, "Saw type=0 in tar header block, info=" + info); } return null; } default: { - Slog.e(RefactoredBackupManagerService.TAG, - "Unknown tar entity type: " + typeChar); + Slog.e(TAG, "Unknown tar entity type: " + typeChar); throw new IOException("Unknown entity type " + typeChar); } } @@ -180,9 +180,8 @@ public class TarBackupReader { info.path = info.path.substring(FullBackup.SHARED_PREFIX.length()); info.packageName = RefactoredBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE; info.domain = FullBackup.SHARED_STORAGE_TOKEN; - if (RefactoredBackupManagerService.DEBUG) { - Slog.i(RefactoredBackupManagerService.TAG, - "File in shared storage: " + info.path); + if (DEBUG) { + Slog.i(TAG, "File in shared storage: " + info.path); } } else if (FullBackup.APPS_PREFIX.regionMatches(0, info.path, 0, FullBackup.APPS_PREFIX.length())) { @@ -214,10 +213,9 @@ public class TarBackupReader { } } } catch (IOException e) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.e(RefactoredBackupManagerService.TAG, - "Parse error in header: " + e.getMessage()); - if (RefactoredBackupManagerService.MORE_DEBUG) { + if (DEBUG) { + Slog.e(TAG, "Parse error in header: " + e.getMessage()); + if (MORE_DEBUG) { hexLog(block); } } @@ -242,41 +240,35 @@ public class TarBackupReader { if (size <= 0) { throw new IllegalArgumentException("size must be > 0"); } - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.i(RefactoredBackupManagerService.TAG, " ... readExactly(" + size + ") called"); + if (MORE_DEBUG) { + Slog.i(TAG, " ... readExactly(" + size + ") called"); } int soFar = 0; while (soFar < size) { int nRead = in.read(buffer, offset + soFar, size - soFar); if (nRead <= 0) { - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.w(RefactoredBackupManagerService.TAG, - "- wanted exactly " + size + " but got only " + soFar); + if (MORE_DEBUG) { + Slog.w(TAG, "- wanted exactly " + size + " but got only " + soFar); } break; } soFar += nRead; - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.v(RefactoredBackupManagerService.TAG, - " + got " + nRead + "; now wanting " + (size - soFar)); + if (MORE_DEBUG) { + Slog.v(TAG, " + got " + nRead + "; now wanting " + (size - soFar)); } } return soFar; } /** - * Reads app manifest and returns a policy constant. + * Reads app manifest, filling version and hasApk fields in the metadata, and returns array of + * signatures. * - * @param packageManager - PackageManager instance. - * @param allowApks - allow restore set to include apks. - * @param manifestSignatures - parsed signatures will be put here. * @param info - file metadata. - * @return a policy constant. + * @return array of signatures or null, in case of an error. * @throws IOException in case of an error. */ - public RestorePolicy readAppManifest(PackageManager packageManager, - boolean allowApks, HashMap manifestSignatures, - FileMetadata info) + public Signature[] readAppManifestAndReturnSignatures(FileMetadata info) throws IOException { // Fail on suspiciously large manifest files if (info.size > 64 * 1024) { @@ -284,9 +276,9 @@ public class TarBackupReader { } byte[] buffer = new byte[(int) info.size]; - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.i(RefactoredBackupManagerService.TAG, - " readAppManifest() looking for " + info.size + " bytes"); + if (MORE_DEBUG) { + Slog.i(TAG, + " readAppManifestAndReturnSignatures() looking for " + info.size + " bytes"); } if (readExactly(mInputStream, buffer, 0, (int) info.size) == info.size) { mBytesReadListener.onBytesRead(info.size); @@ -294,7 +286,6 @@ public class TarBackupReader { throw new IOException("Unexpected EOF in manifest"); } - RestorePolicy policy = RestorePolicy.IGNORE; String[] str = new String[1]; int offset = 0; @@ -307,7 +298,7 @@ public class TarBackupReader { // TODO: handle if (manifestPackage.equals(info.packageName)) { offset = extractLine(buffer, offset, str); - version = Integer.parseInt(str[0]); // app version + info.version = Integer.parseInt(str[0]); // app version offset = extractLine(buffer, offset, str); // This is the platform version, which we don't use, but we parse it // as a safety against corruption in the manifest. @@ -315,7 +306,7 @@ public class TarBackupReader { offset = extractLine(buffer, offset, str); info.installerPackageName = (str[0].length() > 0) ? str[0] : null; offset = extractLine(buffer, offset, str); - boolean hasApk = str[0].equals("1"); + info.hasApk = str[0].equals("1"); offset = extractLine(buffer, offset, str); int numSigs = Integer.parseInt(str[0]); if (numSigs > 0) { @@ -324,162 +315,9 @@ public class TarBackupReader { offset = extractLine(buffer, offset, str); sigs[i] = new Signature(str[0]); } - manifestSignatures.put(info.packageName, sigs); - - // Okay, got the manifest info we need... - try { - PackageInfo pkgInfo = packageManager.getPackageInfo( - info.packageName, PackageManager.GET_SIGNATURES); - // Fall through to IGNORE if the app explicitly disallows backup - final int flags = pkgInfo.applicationInfo.flags; - if ((flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0) { - // Restore system-uid-space packages only if they have - // defined a custom backup agent - if ((pkgInfo.applicationInfo.uid - >= Process.FIRST_APPLICATION_UID) - || (pkgInfo.applicationInfo.backupAgentName != null)) { - // Verify signatures against any installed version; if they - // don't match, then we fall though and ignore the data. The - // signatureMatch() method explicitly ignores the signature - // check for packages installed on the system partition, because - // such packages are signed with the platform cert instead of - // the app developer's cert, so they're different on every - // device. - if (AppBackupUtils.signaturesMatch(sigs, - pkgInfo)) { - if ((pkgInfo.applicationInfo.flags - & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) != 0) { - Slog.i(RefactoredBackupManagerService.TAG, - "Package has restoreAnyVersion; taking data"); - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, - LOG_EVENT_ID_RESTORE_ANY_VERSION, - pkgInfo, - LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - null); - policy = RestorePolicy.ACCEPT; - } else if (pkgInfo.versionCode >= version) { - Slog.i(RefactoredBackupManagerService.TAG, - "Sig + version match; taking data"); - policy = RestorePolicy.ACCEPT; - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, - LOG_EVENT_ID_VERSIONS_MATCH, - pkgInfo, - LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - null); - } else { - // The data is from a newer version of the app than - // is presently installed. That means we can only - // use it if the matching apk is also supplied. - if (allowApks) { - Slog.i(RefactoredBackupManagerService.TAG, - "Data version " + version - + " is newer than installed " - + "version " - + pkgInfo.versionCode - + " - requiring apk"); - policy = RestorePolicy.ACCEPT_IF_APK; - } else { - Slog.i(RefactoredBackupManagerService.TAG, - "Data requires newer version " - + version + "; ignoring"); - mMonitor = BackupManagerMonitorUtils - .monitorEvent(mMonitor, - LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER, - pkgInfo, - LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - BackupManagerMonitorUtils - .putMonitoringExtra( - null, - EXTRA_LOG_OLD_VERSION, - version)); - - policy = RestorePolicy.IGNORE; - } - } - } else { - Slog.w(RefactoredBackupManagerService.TAG, - "Restore manifest signatures do not match " - + "installed application for " - + info.packageName); - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, - LOG_EVENT_ID_FULL_RESTORE_SIGNATURE_MISMATCH, - pkgInfo, - LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - null); - } - } else { - Slog.w(RefactoredBackupManagerService.TAG, - "Package " + info.packageName - + " is system level with no agent"); - mMonitor = BackupManagerMonitorUtils.monitorEvent( - - - mMonitor, - LOG_EVENT_ID_SYSTEM_APP_NO_AGENT, - pkgInfo, - LOG_EVENT_CATEGORY_AGENT, - null); - } - } else { - if (RefactoredBackupManagerService.DEBUG) { - Slog.i(RefactoredBackupManagerService.TAG, - "Restore manifest from " - + info.packageName + " but allowBackup=false"); - } - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, - LOG_EVENT_ID_FULL_RESTORE_ALLOW_BACKUP_FALSE, - pkgInfo, - LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - null); - } - } catch (PackageManager.NameNotFoundException e) { - // Okay, the target app isn't installed. We can process - // the restore properly only if the dataset provides the - // apk file and we can successfully install it. - if (allowApks) { - if (RefactoredBackupManagerService.DEBUG) { - Slog.i(RefactoredBackupManagerService.TAG, - "Package " + info.packageName - + " not installed; requiring apk in dataset"); - } - policy = RestorePolicy.ACCEPT_IF_APK; - } else { - policy = RestorePolicy.IGNORE; - } - Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra( - null, - EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName); - monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra( - monitoringExtras, - EXTRA_LOG_POLICY_ALLOW_APKS, allowApks); - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, - LOG_EVENT_ID_APK_NOT_INSTALLED, - null, - LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - monitoringExtras); - } - - if (policy == RestorePolicy.ACCEPT_IF_APK && !hasApk) { - Slog.i(RefactoredBackupManagerService.TAG, - "Cannot restore package " + info.packageName - + " without the matching .apk"); - mMonitor = BackupManagerMonitorUtils.monitorEvent( - mMonitor, - LOG_EVENT_ID_CANNOT_RESTORE_WITHOUT_APK, - null, - LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, - BackupManagerMonitorUtils.putMonitoringExtra(null, - EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName)); - } + return sigs; } else { - Slog.i(RefactoredBackupManagerService.TAG, - "Missing signature on backed-up package " - + info.packageName); + Slog.i(TAG, "Missing signature on backed-up package " + info.packageName); mMonitor = BackupManagerMonitorUtils.monitorEvent( mMonitor, LOG_EVENT_ID_MISSING_SIGNATURE, @@ -489,9 +327,8 @@ public class TarBackupReader { EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName)); } } else { - Slog.i(RefactoredBackupManagerService.TAG, - "Expected package " + info.packageName - + " but restore manifest claims " + manifestPackage); + Slog.i(TAG, "Expected package " + info.packageName + + " but restore manifest claims " + manifestPackage); Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName); monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra( @@ -505,9 +342,8 @@ public class TarBackupReader { monitoringExtras); } } else { - Slog.i(RefactoredBackupManagerService.TAG, - "Unknown restore manifest version " + version - + " for package " + info.packageName); + Slog.i(TAG, "Unknown restore manifest version " + version + + " for package " + info.packageName); Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName); monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(monitoringExtras, @@ -521,8 +357,7 @@ public class TarBackupReader { } } catch (NumberFormatException e) { - Slog.w(RefactoredBackupManagerService.TAG, - "Corrupt restore manifest for package " + info.packageName); + Slog.w(TAG, "Corrupt restore manifest for package " + info.packageName); mMonitor = BackupManagerMonitorUtils.monitorEvent( mMonitor, BackupManagerMonitor.LOG_EVENT_ID_CORRUPT_MANIFEST, @@ -531,7 +366,166 @@ public class TarBackupReader { BackupManagerMonitorUtils.putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName)); } catch (IllegalArgumentException e) { - Slog.w(RefactoredBackupManagerService.TAG, e.getMessage()); + Slog.w(TAG, e.getMessage()); + } + + return null; + } + + /** + * Chooses restore policy. + * + * @param packageManager - PackageManager instance. + * @param allowApks - allow restore set to include apks. + * @param info - file metadata. + * @param signatures - array of signatures parsed from backup file. + * @return a restore policy constant. + */ + public RestorePolicy chooseRestorePolicy(PackageManager packageManager, + boolean allowApks, FileMetadata info, Signature[] signatures) { + if (signatures == null) { + return RestorePolicy.IGNORE; + } + + RestorePolicy policy = RestorePolicy.IGNORE; + + // Okay, got the manifest info we need... + try { + PackageInfo pkgInfo = packageManager.getPackageInfo( + info.packageName, PackageManager.GET_SIGNATURES); + // Fall through to IGNORE if the app explicitly disallows backup + final int flags = pkgInfo.applicationInfo.flags; + if ((flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0) { + // Restore system-uid-space packages only if they have + // defined a custom backup agent + if ((pkgInfo.applicationInfo.uid + >= Process.FIRST_APPLICATION_UID) + || (pkgInfo.applicationInfo.backupAgentName != null)) { + // Verify signatures against any installed version; if they + // don't match, then we fall though and ignore the data. The + // signatureMatch() method explicitly ignores the signature + // check for packages installed on the system partition, because + // such packages are signed with the platform cert instead of + // the app developer's cert, so they're different on every + // device. + if (AppBackupUtils.signaturesMatch(signatures, pkgInfo)) { + if ((pkgInfo.applicationInfo.flags + & ApplicationInfo.FLAG_RESTORE_ANY_VERSION) != 0) { + Slog.i(TAG, "Package has restoreAnyVersion; taking data"); + mMonitor = BackupManagerMonitorUtils.monitorEvent( + mMonitor, + LOG_EVENT_ID_RESTORE_ANY_VERSION, + pkgInfo, + LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, + null); + policy = RestorePolicy.ACCEPT; + } else if (pkgInfo.versionCode >= info.version) { + Slog.i(TAG, "Sig + version match; taking data"); + policy = RestorePolicy.ACCEPT; + mMonitor = BackupManagerMonitorUtils.monitorEvent( + mMonitor, + LOG_EVENT_ID_VERSIONS_MATCH, + pkgInfo, + LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, + null); + } else { + // The data is from a newer version of the app than + // is presently installed. That means we can only + // use it if the matching apk is also supplied. + if (allowApks) { + Slog.i(TAG, "Data version " + info.version + + " is newer than installed " + + "version " + + pkgInfo.versionCode + + " - requiring apk"); + policy = RestorePolicy.ACCEPT_IF_APK; + } else { + Slog.i(TAG, "Data requires newer version " + + info.version + "; ignoring"); + mMonitor = BackupManagerMonitorUtils + .monitorEvent(mMonitor, + LOG_EVENT_ID_VERSION_OF_BACKUP_OLDER, + pkgInfo, + LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, + BackupManagerMonitorUtils + .putMonitoringExtra( + null, + EXTRA_LOG_OLD_VERSION, + info.version)); + + policy = RestorePolicy.IGNORE; + } + } + } else { + Slog.w(TAG, "Restore manifest signatures do not match " + + "installed application for " + + info.packageName); + mMonitor = BackupManagerMonitorUtils.monitorEvent( + mMonitor, + LOG_EVENT_ID_FULL_RESTORE_SIGNATURE_MISMATCH, + pkgInfo, + LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, + null); + } + } else { + Slog.w(TAG, "Package " + info.packageName + + " is system level with no agent"); + mMonitor = BackupManagerMonitorUtils.monitorEvent( + mMonitor, + LOG_EVENT_ID_SYSTEM_APP_NO_AGENT, + pkgInfo, + LOG_EVENT_CATEGORY_AGENT, + null); + } + } else { + if (DEBUG) { + Slog.i(TAG, + "Restore manifest from " + info.packageName + " but allowBackup=false"); + } + mMonitor = BackupManagerMonitorUtils.monitorEvent( + mMonitor, + LOG_EVENT_ID_FULL_RESTORE_ALLOW_BACKUP_FALSE, + pkgInfo, + LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, + null); + } + } catch (PackageManager.NameNotFoundException e) { + // Okay, the target app isn't installed. We can process + // the restore properly only if the dataset provides the + // apk file and we can successfully install it. + if (allowApks) { + if (DEBUG) { + Slog.i(TAG, "Package " + info.packageName + + " not installed; requiring apk in dataset"); + } + policy = RestorePolicy.ACCEPT_IF_APK; + } else { + policy = RestorePolicy.IGNORE; + } + Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra( + null, + EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName); + monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra( + monitoringExtras, + EXTRA_LOG_POLICY_ALLOW_APKS, allowApks); + mMonitor = BackupManagerMonitorUtils.monitorEvent( + mMonitor, + LOG_EVENT_ID_APK_NOT_INSTALLED, + null, + LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, + monitoringExtras); + } + + if (policy == RestorePolicy.ACCEPT_IF_APK && !info.hasApk) { + Slog.i(TAG, "Cannot restore package " + info.packageName + + " without the matching .apk"); + mMonitor = BackupManagerMonitorUtils.monitorEvent( + mMonitor, + LOG_EVENT_ID_CANNOT_RESTORE_WITHOUT_APK, + null, + LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, + BackupManagerMonitorUtils.putMonitoringExtra(null, + EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName)); } return policy; @@ -543,9 +537,8 @@ public class TarBackupReader { long partial = (size + 512) % 512; if (partial > 0) { final int needed = 512 - (int) partial; - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.i(RefactoredBackupManagerService.TAG, - "Skipping tar padding: " + needed + " bytes"); + if (MORE_DEBUG) { + Slog.i(TAG, "Skipping tar padding: " + needed + " bytes"); } byte[] buffer = new byte[needed]; if (readExactly(mInputStream, buffer, 0, needed) == needed) { @@ -588,24 +581,21 @@ public class TarBackupReader { int token = in.readInt(); int size = in.readInt(); if (size > 64 * 1024) { - throw new IOException("Datum " - + Integer.toHexString(token) + throw new IOException("Datum " + Integer.toHexString(token) + " too big; corrupt? size=" + info.size); } switch (token) { case RefactoredBackupManagerService.BACKUP_WIDGET_METADATA_TOKEN: { - if (RefactoredBackupManagerService.MORE_DEBUG) { - Slog.i(RefactoredBackupManagerService.TAG, - "Got widget metadata for " + info.packageName); + if (MORE_DEBUG) { + Slog.i(TAG, "Got widget metadata for " + info.packageName); } mWidgetData = new byte[size]; in.read(mWidgetData); break; } default: { - if (RefactoredBackupManagerService.DEBUG) { - Slog.i(RefactoredBackupManagerService.TAG, "Ignoring metadata blob " - + Integer.toHexString(token) + if (DEBUG) { + Slog.i(TAG, "Ignoring metadata blob " + Integer.toHexString(token) + " for " + info.packageName); } in.skipBytes(size); @@ -614,9 +604,9 @@ public class TarBackupReader { } } } else { - Slog.w(RefactoredBackupManagerService.TAG, - "Metadata mismatch: package " + info.packageName - + " but widget data for " + pkg); + Slog.w(TAG, + "Metadata mismatch: package " + info.packageName + " but widget data for " + + pkg); Bundle monitoringExtras = BackupManagerMonitorUtils.putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME, info.packageName); @@ -630,7 +620,7 @@ public class TarBackupReader { monitoringExtras); } } else { - Slog.w(RefactoredBackupManagerService.TAG, "Unsupported metadata version " + version); + Slog.w(TAG, "Unsupported metadata version " + version); Bundle monitoringExtras = BackupManagerMonitorUtils .putMonitoringExtra(null, EXTRA_LOG_EVENT_PACKAGE_NAME, @@ -692,7 +682,7 @@ public class TarBackupReader { throws IOException { // We should never see a pax extended header larger than this if (info.size > 32 * 1024) { - Slog.w(RefactoredBackupManagerService.TAG, + Slog.w(TAG, "Suspiciously large pax header size " + info.size + " - aborting"); throw new IOException("Sanity failure: pax header size " + info.size); @@ -740,8 +730,8 @@ public class TarBackupReader { } else if ("size".equals(keyStr)) { info.size = Long.parseLong(valStr); } else { - if (RefactoredBackupManagerService.DEBUG) { - Slog.i(RefactoredBackupManagerService.TAG, "Unhandled pax key: " + key); + if (DEBUG) { + Slog.i(TAG, "Unhandled pax key: " + key); } } diff --git a/services/tests/servicestests/res/raw/backup_telephony_no_password b/services/tests/servicestests/res/raw/backup_telephony_no_password new file mode 100644 index 0000000000000000000000000000000000000000..12b6cbfd168fd17e3e7ba82b879dc66d1de25638 GIT binary patch literal 21805 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 literal 0 HcmV?d00001 diff --git a/services/tests/servicestests/res/raw/backup_telephony_with_password b/services/tests/servicestests/res/raw/backup_telephony_with_password new file mode 100644 index 0000000000000000000000000000000000000000..ce847a33210dab065928d691c1ddc65da5d4550d GIT binary patch literal 22309 zcmV)QK(xO>PDD~qNkkw*K|@PbPzp5)F$zINQ!O$zHVQB|HaRgcI50FZHaIatFhxQ{ zGciLmI59>wLPkYHK{PZtIW##&H8MjmGc+zH#0If zGebp1FhMdwH%2r#LN+l(HbFBmH$*T+LNP%_I5RgeHAO)%GC4RoHZ?{yL`6Y4H8nv& zI5$B-F*!LgLqa!1GB!CvHZwLuG&Vs-K{G}6yI5sswI6^c-Lo+!sGeS2w zFgG_tGebm0H#IalG%`3sGC4&=IW|H%H#9>qGBP$WH!w9pH$^!` zHAOKwHaRvqI59>!Gej^jK`=BzLo`MfH=r9Q+pR?{it463rrA z`7ats#H@zQ&F7TQ6;ZWYg(S2q$4PEyx9oD-1ooPm%h!o2Z7Pdtr6#nSkQLB8k934$ zKoKt?@~=6Ge8mjkQ`R^N@>@N)G;OB%?qGy?>JO0V;Q{PiQp|nc6q+FB7iO;fXc4?W zG@|IfeM%(EsWKqFPV-qVU7K|Dt+t*S0qdaDGke4dp)YL>HtAUm>rRNCCPg%!G>86E z_GQI&w72}s31{)nKw~0^NiBAN+Au-_U#Ej+<3-1(T8{iDi0(LElGU#;8L!6ls@*&Swisk26UZL|eG3B3Yv%EN8CNA{JfJ9> z#rQK$lIjQZ^61JXRq`;{@HvT$kZ*~$6AC|w2o?AlID)+85v8l3;}=%F2C_OudC>!w zcrLJD_+DX3sismP@Mt74#gi%+F_%0YdPxx% zseHZglczq<-Nmt+OD*pSMNiL0@CKaJ)-g8#aPTluY(o728qUN0laoOeF*Yg?ckaf< z);dwa+P)kao$KfSa0m}OZuPn%2BnrWX`7*auO+PJ7Qj}2LP@|8I|)vo0Mz7Ww<>84 znw%iUQjsg$VV)rSzTtL^1{JMFc1bMb%Q3a{cptM~EzsyweHw2teo|SKF?GQC14$GC z2PY^hMfdEN<2!A1-J8PP5A~4Am?z+V}|(WITACMMNa&1j*pyBzBA8 z9T&wFH}6Z{{xlAz+qJBvDRtQaqwmJe{KZ@Hv~}J^z=}>_dgcOj0P5I`zfhbG)Fla+ zswHyi;%tp{<@Vc=yA{Fkvg;VSLaF79;i_fu)&m5KPeDoCV=WGOZpKZ zwr)3gpv+)X#d};pL ztU@Y^e{5jk<@I>G@b%jwLJ$(Eoo-DT2J5yiCibxrG>UmeMS?R#xrS=A+!j{z6@&&I z_Ta-qWr1i!!31{uQumE+TpV9mA9gW_9X4X*X9=j~xXlcp1_>kB=2etRz0D`dY0q5ezIHx*JIl7VUZz0hV$29}zK2FzT&FHA+&Vmn9D^F}k!w)tq3K0m=+%k<7r{H`Igs8J_T z0?bTDiufWlP(ka9m-)3`@fot*HMurqu+4a{O4JGf5fF$F&Jm#;6R<;aE^kh16M$OX zw#V(pW^^r2{Jp0g`iWkl^VqxD5zvWTCHFHbQ_m`(H6Y)BkGpQXTc|DKj6PziD(C(` zeK@*QahrZ8n1$VhVEVHvqXX5l)bkmZAwp>XuaAKFCU41c)M?J_{Iz`BO=35#9tvAI z*#5iVeg!+lzAXIXYB;+_R}uCg_7sF>abZX^WxoGN4iZi602U5PK9a{KC^Y+|*CY*E zm~CuIK@E{CWsCWZ4n4P&;@dNSe-ZfTQ%#!d6F|v`$;pgVAW6#XC+f+)0h#ZUZ}kDo)B9KmxTR^LGg+{%RX^Hm<7X9?%Xk~7F#qOj!`6L8 z;-i}^!_y2#uoT+!hf*obj4?lxp=~~AZ)VI``Eic&V2xryscps z_T^+#>L{szYfmMAnQsodhSiipHd1z2YmU$w%4WWP|6}KeJLH6_>vr}X>uTzEXUbmQ+ zd%U{SM&=w$ptodTtX5IXmuN$?_P1?!R3Jw}!lnN>aQtvAl)5R_1>%aYO!`bAFEdzn zu7A*&3}!v|Vt+gWfq!j3lcr}M;8?dn6aLYJkJ=iyR{~-1-k}1AC1vfmT5p#_$Vr8m zB2MaEp!~9XC8D|B1&IlaKO*pFzB}hr!TQ>t(WeP5>sc3h)#gdO_bhmxUPx2U z1r9_bhdd=K^ZZly64XYJM2f(pVxq<7+J~P)u|I~*Upm2Ka1UudFPceBe?^ytj98T4 zLHZz`LNDLUjT51T9uy7tv7vF{iy9vyv(GM54P@hvSHz`aBx2(^QFL1@7Rg2!e9Nh4 z7MkQ}S>F})DNBwF^_WU}j(KVp&Q$mea5S*PW|>hUwh2B1T4mzb&4l3JS#bwjwWL98Jy-pya{^SB#+J^TrEox{C*chi3OytvB&xG| zmsm>uC3?vUW!h;hhEYLshZ+EM(xru^_B3?rdrsfX3XE7<*ZX|Q{J%*=jWH+ruD0BZ zVg7IP)Olh|yCj@eR^wg{rLXhD8L^buO=bE`A?FL9P^vhAVB{q3ea!h0z;S>Gqm1H6 zGa}A31AEnzILKX_O*$$+v(57ee89?rb+KO36wMIS@TUpl@o^kyHH44hM8%l_#Sj6G zLgW~|F{2WioL)E_O}{a$RI-&T+IuI6G>c-3zerl(tmQ$dlVkly)0c+>zG0qr&1tg- zJY8es(y%Ypm&v+CXYO%@u5)e#5jNu%bsYwR3@%B(^MIi*oIG?*^i+E$4r_gr*MZ`%}IP_7b{wBvDboF{b%5+w8fC_2}e# zh`>0p)u+M$z2nR}UnMkV99#Xf=S|L@csmwiO}GCrzarLr;Z2(9FkDq~;hGS9^bo)R zhSbh<<&0Fa0L>a-TL=7n7E34jw8)Sb!?@Y5OTL))nP3_vtMdI6&5LY@=Hn zVG_ar6g@Q8X0}qgnd#-JuzF#uvQ#65h&vxLl^z4*L_KlMj{o4t4pAZ%qH zBE2IuXf)XHLm6#iE!=VW!fUNHKr3<|=f+p0FQA4-C_}P& zrEt{?`OHjheq?}!zyjAMiX~{TrecLLJJw8{wFLd3y=T7kK_&LOu_5dNS3hE4KM{0p zS!boR=FabRII&mFvfgVr`PO>IUej;5<-tatdI4Sxl~M8AMNJ(OM@cw#IkS>oJbnR& zMwQg&72W;8D$B5gGlzG;J%sXxiLq z>pFXukf;>>EaIkiek(fi0|TW(;Q3Z&NcWuHK~WS_AA1#Mv*5n>AS`>ZBP05cntFt` zSILbmWEnh28L+ri@A)qmrpf0=1Adx9hu#QajKl}&5Wz?H(%Y8aZG{Pl3ty&sjIO|> zer36oLny0~Qt}>4#?cEbvgrYja~f*f=`F355Cv4-Kl@o0^m~6Wk(uVC=-fkb-59!r z6Z@L^0boFDXNThxFD#DQ-T-ZN$t2pC~~E&tOYNk{FT(iCh=Vx zE*_BUeK6{QcbcQJ-w_deA`X>x408?vfLk%`@7ACJikyfqxTh}H@Y>YaQ@GeWin$)t z9B3{f|5Mii{Kyv>?)^Vt32W#0HD~XxYP4!A0ZCAB$=6g~dW>la))#BDWv(f9y`&*- ziFb0R33Ou>{MsHv{~TRr+{sr!781N>@Y?NIv@g-~0^v@d6a%7DYHK*IsScSOsYX4m z3)B?=PT1i6lJ0t1BgJonn>fRx-})!id4jU+zCNoz)XJI{Hd?V!={~uY0A&0BXRB2}@CRkrK8gUf2J`jkMHJLJ|oIquz zw*hiy68q{~V?<2AnnOKt`I=BtrAKLd5kaDcj}tOZh|5sJ6L{bxvlqh|%o|JCaaMq! zY#V3u+4y#Ki~q|-Y!jNC5*+g5o+N%w>nd&jzLRlyxJ_U2E3g3rMC$YLtYKTqTjN&YP{V8!y5>!AQ6oP1{WJC^2dqe2 z3RGxg#USgucRjcvBT~iiHJgS?ry3Y|lB8XuVhAo`Xw49#L_U&^(~ey1DF_-UPcydQ zm%|dYGNEUEaF=1NHufvN)0u8>S(~hOR}RkRV_?5oW%Bk(U$zl>)uCBwf8^W_v<8CaXM4QNtT^I(PTR6Leed3IZ} z-eNK~PAoO{hJHu&(!*0hP^)KnH{tTNrP_{5gMSwMZ01I)Mm<$9y{CyqDPR;B(gWqB z&<9J#x+j^A+KexavYV7{r#{|-@c`TQXWe5|qfPv#!UhuKE;BRxIAzYPw^O624H=ES zB>I+i)-EfoxsltZw}8K(W>uSTA=M2bN}JYeFWOb51l|%aC_3++dR?A7NVq)^nLHQ) zU>m+ggX&*p2MwIo2m7Q?BT}3vqR{ku8w$-(P;}SIn88~)U3?UK!2H5t#%V45ECF6;!AM;nyObBEivjpZ&=nadLIkO*L-73&Pzax;2f+KNWfjA zPAjGtY$b*E`)p)&!y0A>33!*;J^3G5_zJ3&8^!s)|3~dU+frcPX2eYMD*2yko#er2 zA$r6E&~(nEoC*%Nqoy>HbdIxo2j{iwLa(V?v_}lEEqB50f)EVNBRwvhy;%mMo9GCO zZ)wO9w=XvA_;`P0BY<;2U?2*|k}O>KC(+jN_w`iO`#^9>M;f=*)JqN>{t(QSbeLBu z_h_sv^Q7;-PAk~vfccoSb_+Hsp=EtM;W8-dV}E3CJA2lGoyS#Gm`dxqU48W@hI0CE zZ0TsapM6S*7KnsTaYRpE@qyE;YIY^?8d6PBRhx^quX>XjM7Xtz9M(fgsev?V-NZ?U zhoiD+vIejWZY+Zd8VzoQBZ{sRN@Ff&C?EPeh~!8Jo^GFaFTWc`51Tv*e^{*ie%xLx zGH}bPDCLQRC5hQN1DMxQ_|-S&;1$x)6z)TzVY(|?3q4nf z?kD=Br=H4Ok92=&uRW!Wf)A-+)C7D}>w>8FVGbbUjY=WjfAOshu%xxC*Q7^Tk>c;T zw6pWw@7Y6ipiWOq)EK0U->axy6BH7jrx^-)qep2VB&GISL_(pyD!L&j$Qf|#Ge9pv zi(>=m;RR+dXKDNVf1wrK{3U2Xm$Q&8F2i7%01S|hm_$Qe31D2@j!;`cj|5)3L(~;`#Zy2c#X#1H{4JBO`+uH42r@&)~S>B3b z0b>A0M8D!LIn6`XFzsc_AvClT$LS7o2DowXBrxS&e&Jo#S2SJdsj9DJ-5FV22%uXm zL(j6C`pEa;ryDemV=w){_*lK=amtXTjN-B13Efjfl|KY#QfDG0dqIGIKWq#<8)_Sp zu~Pef?ZxMn3z5ZCcJsO1G=vwn1CM2L2C=(2q!;6Xtr!V_^KZlwE~ehbb58B_V`<7f zwx^G=(6t=8CmVskuW(oiDMx%DN5#!}5|EX|@0~|=)lp=dEfPVVAAc?kXYXQ2q5_LP zJ~({2q94SAjob@iNg>@#hQeANAXwQraEE^j`SSd~!s4hJriD-HqtN0(K|gN=<8b5v z`971~nq0f`vt+K_oSFZtB3;^VL}UY#xJ@>Y(4CidlrOL{071ZSLz#U`v(eSEvf35g*CM}Y2nJBm6?^~%AaO4buix{fo| z&;uC)$2K;$w%@IGKZK0qhqj)x`sc_3@v~!<89!hKy$-7~9)k_jK%D1dr8_Hnu8Ws{ z85h)mzi5MBMC42|t_B)69bbU%&P>su5G#swj8MK5)6QPv) z3gKnXoaf09=l3+}g48Fa<-fz3A(UHnsAnL*$&ikNiwZxY2q90s>IsT%LM7=8ya_7x z0VvfQW5>4=qDZ)A`n#@e+u3qAN;|*($|h#>CZ4UKU9a*_|0UL$G0BdU-F<_nEIBWl zN>2fyW7Mby9A)l5#AMalK|M7hP24btxWHc|tj@rUpUvDCZr%A|+1LI`b+eMwE!dx7 z1%ZLoASzl@WXOg5ZW+6(79GrFx2owCS9FS;fMS zxN`)8z!X5x49P|1+u)q~Mr$tf1nXt*%g}WALNMTmg^gN=BYNp)MA*}uHKohMX!K)y5)vSZ_ zGy)f0L%I5+H6w58}g+{szWr)SG}-g|1(wxewMq zn<;6wpO%pVP{!5M&tn&3T-4g;o*=8X&E+4V@eafEY^A-!_<|h{O4zXbViLKlDup7f z5Okm($VvpGD;}pE@~!Ho=!apErN+`YPXKS6F%N9>&&H3Ke!JFOCO{6=QCGK9P(Yn2 z0%E%Nu>g#y39(j@k2cm6VC6}oYQDK;89}`Q>OmAlBmHT`flv|DP>!_ti_b`-*s}CW zXsdZPP5%@uhQJBF>Zs1-^6%MgUy$~Bn|GAyaD6xH=HxOz7r(5Y5*s-@`eI^vS_uoS zAhX+qD_b~~%gt0@3x8u-W0rBm2|F`5)(3g-YAC)VNI6kM9@%N1YZ@ZCos-T9bgolx zsBu>WoJWLj7Ir_Aw0R<{ZIejKFnXnLD}^5uw3|>wuW9fU;Bc1Fkj-FlJQ~lM4xOS;}f^D6KH8*U7gFdR(>2KH4$&zS+*Is&E+#Gp!--rCCMZFBhRRc zDKyU7-%!RJp*p~1i{m1n@q(}z;lN0rvfX%FVr7hb$qV5n+|)pxB=M)D`AxP!zvD#s zAOtJ7>*XRkd%r49usKzSJhRUWwW0~|oh}gKDivc{7w^__NR3s_$aV}uj@00f;)LvXah4?TP~UIw*3=`K9_7--^Ga9gm{asc@n~cUZ*uM_ADko+Owv`8eBu zeVcq6-V^ZX7vJUAu0UZGnqN)Cih{fnu!3;VDqKt`lX`V>G?k4Q^_P(hV=PNkQT`}9^mSMPt2JcKkUgZcEd*ldT=A_8M4ewE_l4u?{u;24q9o zM3VRZu$u@+f*e!~u?wzKpX2FL)~auXk_o~9ig?jZ2|1&S0`n>WKaiA~z6OoYjNt!iNna{AtLa@f>n_uVD zW#bbaZf{Nc@*3Y`(nT$nb$q>I8K5_>clwA#RME7Wa-IJ@$kR8;(>E~QduJ4n?ZKdU z;>PIGoFH!y;@WgJ!`HVKKHbruJ>~iWl{E=rps;Y|6!%&}Y$}}Fn7KuQEgO~B!n>7ev3OdcwXEQ7$d#Nd41RgJ2$ARh>c%#S z2wF`fR+~GrR*x1hNx6u5>x(U)<^_}kRC+>k+{yK=LwGI1_32_-+-tiiT3-j{A&VIZ znFSO&^A-D~IC;$#Gk(FvpM#tKws)K=!48aNgbl6CWI1m(%b{6!hpZ6@IVNwo{?mhW z)HpvQc5U*s)Ew-?lh5=^aSy(fQ%$@-X8CPUt?H3nKQLXi3WkkqVFl9GADT z^2qf5HPY4N`tE-ZFg?et;afD9OJcnqN_x5DWoOey^x0Ob0|nihXpsdwRTc8?w8^yr zLedg?Hq4x+&o3*ioY;_EU_Fr_zutzjY&Fj;*aW70c#q7(afmV|m_VT-+(SllDn9pl z>a!MBBI=-rs&8;hUA%)woLb5T_1bc!g;@Ivuuu!E`MFW?t?|YA$G8S(ntD-VK z*=yHOZ#WJO{S_yMK&5f09eksgudTE;6#eD_BbQ`QQJrP$TD>Mc%#m7C9ilbXtO~s& zjFoxNeXI5wn3Irb2@uzLGE0Nu@{x)o--jU22f5s~s%eqC*=eU8mRT?T8`4TGVREGO zWXs0`{tHU>y#`)TcNlhrCs6~kG&F;WNJ!X8%Qn6gTlGQvA6sN4nm4HP!EN6cy&NMZ z_~muM!IvYFtIIH`Us3OLj?$+rXr328o)u4ngnTq<>pFPWfc0HOMaUqN>R_GJQ%Z@i z09f8j zlKkIfna9IT_T`)aek_S>t9BEw%`D%VGZrT=Ej=MQ<_w{ehlrl`@?r1vjC7LiCDQ5g z7nQBLNL`~_l=gmz9s)f9x&X^UfZ zNQYZ}`4bTRcW>IQ%-uGVl{?0!IenZC!p{02TA=jTs97K!B%52gwEQ99$T?2R7cErr z>8)jQd^>CvxT}$x0+V)&`M6#hUVxx8WA)9)zItF%LgkD*sp53F%?0ISWegL8%?+N`-6RU$4_?M!@1_Gn{4ifPn;MTguF}uR)Y) z$}}cD@)Yfz%wXqNK9jgYOVDEr$gIb9*Igz>^8YLzh z=1Q^i?woBZN?R2+no+_(9@ejdC~ADLH`sifL({N!opem8do};~|OIir_{eQ)@;{3%uEMvG?V7gvQk{6<-*HmlV&t|2CI;;|%m&`O^op zC&K=i{hsP`#qge4?eL?>h9i3jHle5bLW<~~(!6wW!5LJmin}CA3{*?j-<;(qGB4#T z+mAQM-pU_$Ags~-et=^^GK;6i(i8JtMp}s$_B&r!@4LYK*VQqY z;0iiC8VU*I3G2p@@uoy~>tUw)+S*Gr^|PCOAN3F?Y&nxIx-GXiIIWxmKYv4d2`-&f zHo|F-Slq04UU>yAKQVI3No5lf5n0sWX}o9vi|OkpB*^cE43|AU98lsaw`M{St4RD4 zXt3(&0p004{s_Fg3Rqj~??T}nSlt5p=LNHKbMI|req&6691mt$=}oU2n%NYi=Z-7D z0{bZ+K%rSD`fR;y=1l|-mX7nswD~9yJYQ}&$%~;*E1i2We?veM4-A~rOV(e*GcgnQ zyL`4!(aB-38i9Bv<=ib+FDxOANSjPYy51C;cQJl8!Yno8u@|2xRbi;<`5~kDeEvr-}?aO?V+S^{{AzTmJukN28qG_CSH~HD#SKbn^@1E zGYCHv35M~YT;SSh5Q}~HgkJSL!Dm>(Rrs;YjMDR{#5CV=kRcibKwT+!0K0wY-i>WCfzAdu|xzzIw zn(0B<>PJ9&Y(p0-y+x);QfN@Np%K6NJ4N>Oj;uITZ8oJOt#W4CQwJFJ@k`XG5($$9 zkLlrYRVcQZKNnR@SkotEUm(k>z_Zc=m`>#~LO&SkbL`0HN$yi=5Lh{Ew!2Y{0qHVZFu05xBhzvaFn|KM5U zK91aY?pLCzvJ!~_E~jr4e<67+9gQS^Hhk`digejMvM^iejfU#m%RpgyZezj_|G(hA zQXRGV=%>yxQbwz_IC&X5WRAzJsJqNx9=&${Z0E#FM$-x9q?RqeI*od>V`LSZtk6`_ zyeDzQ$kD2TG7#w|bSAbhx3&+ti~ky%W~ia&t3FS9rCFjq`kgzIAx${yu;vaj2OBZW z-T7*B0M&U`QT=%j;_4&YF+y};kg!G9OrxZT3h4F^&J(J~TH}sBov=jVeO!n5Bg~5O zt0{=rPR=ccUf8teLqqj-PTOE}J1Po?&BTw9;=SDl$pE&9Zsm(yOuY*5GoPD@#>PWycU|B@K`6|)I?7zU z^8ma5{Fv+lh@Bz>Zc&@0J37VBL$n%xg_a`1dn_kWZqWyqbo*O5is`7^eMNpT|Cj`( zc2b@-f}$3CePkb0a$$^+PF?>W1hxn+M~?iR=AN)Db3?St34B~n1!-|1^NG1a9fH8(WZMx*5&)KVCzD|7`<+8KT>6@WC zcWr;&RY-Ec6NFrNsg5t+(QlWIyoWRv;aAoaRl9~c%D0aafooWuyFT4q1UF^Vxip3Fy4iv>naE7d^9E#TM^Ue*c+2UBQB${evSi*PyH>Lg{UbuUy2 z(eTvrejE#}nHqEY6bkS~My0CHQXzha`-9X%|$RvT-< zHX-`t1@v(V=z^^v{?AKN+5M*~QlB`aG|K%hQ30se!N`rX;_*I~kyasK^OB~u(t$uVy$JMgv)@P3*xyM&) zR3U_`l(^;R*3%AwPBjYV>BD2s8w(tK09lDahU>^3EnQV{ZXbTPPGw1X5`0u2TaT)b z>J_Z!tGLf;r({}xeN-uBidmFyubN~la-rNGXxe=FDZ{-*_w_{hk7Em>Byl*oAjSu!}YM$Oqs@-AQkV`V-wiu@~GI>3L`U1McS`o2;UVM z)n|=B#z&zqd3Q0w3M+3pdZ_4lnAHE;7523bLLx}U-UzsadW%YymbeRF5BQ_K*c-rm z8nji6G7FyTm3e9rudk=fc{Hw2WdH0c{E2d=}%0R5L38+appmP*;l#RdoqM5`w zUnZ}C4Re+1VK}*W^Gp*`d}3C6p+PIz{gDz7VP}Lzz8%*R2WvR=_vz|QR>`vk2$`G1 z*d>giN;B)+ktwr?LmI>;2Y50L!AwZ;IDUAAP1ryhx5g?pFqzqtqj#_eXL8PSaOD(5 z+w3ApJsn&b$6)e__IKEc%#~2#o}zgrl#jQg)UL+ZlZj5&El=`Lon=Xr>l@EH(_&kO zU&l^76aJ21Xz!@xIR0|LvQgvDGNTL+mg;uvpm@Yx)ug0 zCUYQ3hT}^~D1p0fvR@13$yh?rTKK+5$ca3UkT7z&YJ@-9f9%mo%H~#B;8Levpm__7 zk(Ad%_b$zEb3MFnk2&1k%Amm?9NWpHQRkZ&mjtpx%tdeT@WtwD@TS4BKp@7&JU*eSudIqPcMW%x1gn`mopO@BF0 z(C_n_vyUgxPjN%VB`_s{_g`!qn&LQCBLZk{!<&fwPyp*x9Ewc1Y}iTbJ9zoeJN4-5 zHbNze+a0fw60>J+@Fl|C@q#}|87J|-y%u{iD$}X{78*Xpu2j{wh39|vY;3tV);q$nv^rmpRUad} zt%uG|m32G#7euh3anJGt{43Xk_|(k_0`9Db_dWlaw?QXAeUp=w)of9RgaOmT*p7l! zYxY36sSTle5@$ps++n5&+0zR5h_;iv<`~Pp05n3$7c>SQ4O`8Q==`yXyY?z*Q{OxL z3P;T7$A%bdD=eSnydSX$^q@S6R1?AneweUBM%oG_O4m8J_lu$QD6RAX|I!W%#2NHp zUr)6E@~Zl)`7}4kHXh;85O1SAAxlexfF)Hn4+{}9vk7-6)8(6JkfQOteHj)luw_e~ zYzbyUgre#F7w;Gbj?+?bYr$&CJJ$se`D0eAr-`V1ssL4%&s1tvkrV}_v`M?V7ub=V z0ER4ZqENI)FGrjC9eabg#KytNw8zL|HiD%HLIzX7(+RJ$2!GOu=8$ueaE@($smPsOnVH&USCU!QYiw3u%9YJCp8c`k|mh%V&4q z$;B9s?Md6i9*b)5hT#FFXF2mMarjJnv{{gi;0S7#*Fa<=W&4cCo2`ErDPgoW8HC%r zxw7SZEu?q|wTgiUMJcXqrrxn_(ky^)9Ip(+f^jBO#kRx(MaM>|>@=P|hEyGUxi^N3 z4i9WKZ;nrA+>4{sT&;d_N*|^|Hf<6!?4A14JHo4=-60+|=n)4(H?ik!rWX$LwcU6U75cn(e%xsTf7FGo!e31 zrj#gDm;*wdIIHmys3>holey%EFdGZ52|3#%l7z=8pILR zilkzwA}(PojlNQ~GZlXp4$pK~3+mPbiE8ho`|cGgUJ7k%4@ka{VF+1{`uBS}Mz)Yh zCTF6p-q+GPCAK23g&DOzc+U8|xTncwR>=TE=qfBK!}3{7ENPrV`u@e}{NdX}3ucCe zhI{q>IWa{)vedRigcK-+h*lS!IaarxL~O}Kpdw5$ zrK$`cF=>!O=DwA}b5tiaX&9OS(K!X3^Ex|ztSYFx~ zHV?)5VxMv7U~wHjJ_{hZf3sHIko`W#U(?~`89JJ4Sug#m{KpoXNU00e*9wWvT+r-- z3a|xtWwDW{BZJKu_8|F%PRm3>$xu_odqwz)3WejeBjG(8pOZr`2W-D)bPw0fp_7me zTW`SEP6{dJN34i1X2800z@EC(uK_Nhj5hwm5?DlXo6aQ60 zMrBU#Nh7nud#KHmL&``>e2j6B{=+C-48o?HRZC0umFuyv89YBJX z7rI}NzG^B~2t337;4og)^Uu0s$tqjJx>8V#{y&*})F5#!cFV_AEI}eZ2B!y{K?$6S z;?JWC7qaAMz|%`Zlt9HIqehZK=r(Wo=-!6LGY+OrE0u5fjq#JDkBvUDgYcqZlDmjY z39PdOA9+@jffL6-1aBHpx-~Uc7VAhb*(-JmYU`7{v9AS5?x%tIT|>EfEoPkvhsW}~ z(YyW(aYQ|z!*7d`t>kYXbJ7BQ<6lYuTmQ#r`zxvM{=^fhNgO{?Bn|qVW1CO7JwT&!vNlk)%QyQS0NeuOjd(3) zqKmB?TW*6!AH&^FQZX3r`b{zkjzdfh5j@ruMFRvfBN9s+H3gvkGVx*!0Son`sF)m> zb)@(4;L9lh`iaEI=y_3gxN;+$sTwzWIoyg9Iubi4XLs@JjRU^x`FrKh)?~p0lWGxY z&$c^>UAv3sYN}(UErSiSnHQjXjv>?ukMWoUGS`h-oYSb{o|NReiJnEk=wu83zCoi{ zVLA`jh$}4xRwah2MpA4e>^tLeHL33*OadV`In=FRNbF)#E1q%dCq7yaG!a;zLkgNO zY_uDf02P~Fmg`TOZNNqxA@Z7C$9-`D&+4}4)Rwo*9WE1)SU5%{YiN>=;C_4VSC3PP zwu9@qiBs7W$aWd4VpDM~HoAWadKBW7Ws{L)4m3Kj52kHLv)dOT+_ic9BwBD@gA^k8 z|75`X1p3-nRgF)%#Hs`l{mFlR(Y!Mul)N`no(X`T&8LyO)794)N`60y`(>8rv_=zI z?l`!LQ~2RWJ2?W<0*>OkNVZd`2BN#Td>ZcH4e=L=SUqlmo{q;>(OJH5iAjKL7@9gHOdq-fe4CcsY^GH0KlTmar*{Y zu$bw)MTg0!_&I^WdIfV_@{8=CDI>tKEXd4N(IrCh)-bj;S}RBPB{8Um_j$L=>pH2d zE(nbH={{#3C0j?Y_iMYZ)$z^7R4ulB;dVV-?qEOlS&>uC=>vX_O04<*(>Z@*9KvMa zKh(oKD`^bzR7jSjh4q#YiWFkNV^}B(P{Kxs4V9X;a^<5_FGeJ~pgd=OrP*HrLz`oa z)MZAun;Gn-d~Lv~P%Kc_uV&=~3x_S$G$UyUc4iE>pO`a-4{Z#!V-9d+2#P67dc?pGF zCH&Sf`}0$-JB;!YR*9fNtTFy$NkDlAnE}EGG;P&J`GS<}r8ibau*f!-Ef89$WW>F% z!b6?CI}nLG=*DP^43DuVr-QVc^h1ysg?rG?5;jDp>(WcbMF)!;P2Ew+~Yfc=OC9pXH%m$%=#^b}d)Qm~~l zt``pUK0iWGSDa20@;?4uw8x;R??`#)2r5O6Z^wPE=1c#*WO$5uk>QU3Mp`e2%-V!D zybsZ(=cyH8`-q{ad&~3uuf*9|BSr$gfdJWt=dPsNd@(zC8!b7rZR4r&6iW*UHw0kU znjgRpQ+19~SU4Vf{r%o*Y7k5GtrzdtBijPpkbHb3ZNUemsTi zK^6G`OFOeVlCc;p-eY}e$+ul8+G9kBzkS)7GH1Li#jEAbblI*m)TKsfF)`lzPfl7I zbJ)^$ipqNEZY)CV%(MPuOi%a6$bAsuTdoEov8!V3rlI&!trsf6;_gwEz89<#d@@TT zQxweb9Q{z*{y5KZl4YP&Z#{zR-jNMNr9yIeb3I_B0Z!Hly#w)!ORGl8N?4l4it@r;LUif=f0@g z{|?eG;x_~Trq&yjeXegaKCh9;;VEP@&>ekKL%c)JisdeDct_@;^20xnCkbhe3zX1c z9H|sPbGz5d;RA5%jLfoMJz<$V-b5Ew268h>UCyey?uJPCHD<%pA~b&@HaGTTESykH zpz*(Pq0|C<%~Qp}z3MDkBDXI78y?KbLcJMvsMT^rfgYAC3E49rAYZ`XvejHXtSs11 z{zk8sVJ~nu_QDNIJp_>5?&9V1q5F~906Mia`-c9rdglS!7A5HcFPu;10%K?2b{cPy z+=z33XDT>eXM+6;-;hJq4*7@$X-EpWyaMITW3Odkfb_Gzw8e1Vy8)jbR#w$?UqLvV zP6byjd#39GskO1o3d;Q$t%(X$(nhB6GN`KNA=|jHFuFZZ+)+eBAAJ%xaU4Q1cbj9t;4AgSf94CaP`8Bny&xD=5syq())lxFhvZb*2hYvm#jt zUU!SypFm@7rtUXuVreiADVVBl&~WmwV28vCGL+9_eJ&C9HLhg(fC@EkG?1bX4w~nX z5}XG>lt?QRB!o$V%Tq3oHC`#!AEFEUJT9Z)sOS?$ON{l{OCeIs1Iqw>ou_niaiS5=Zj0&Rq1Au&4^ z&iNKN-VLQO3!D7|+%phFzH9>Ar?|B`V&K5i-`!Ij!#qCkS=kf5MpPR~XY$0%uGxTo zXxZA(=|@!Xz`+Pxo}Bt^k-ooX8-;8i9!4n?b-o7uY=?9xAq5e2xFOY0KVXg!xH-)U z0#Yd+H^9?9kU~4j#kSLlgb3pkJs3^%G@wM<8^m_5d2o#uHZe`$FEVlFtKRr zb%lD<=r%ajI2AbVpuU%;b)>mgb4_9;_8a&YkMcUh09EoaRzQdv4t)m`3lH}YShf8C z@-Dd1sG2=TRp%S3y}f2lPt3=0+aw=5gk>fR05AvTV;+I89)& z*a1C=(?AC;pt!$ZdZ``^fl+hC(b|>_6AEoYnh4-%=&J-@yE+DoEp1xq*b3!Te;u_# zlA^(??5JB(ysH%8hxBkVCNk8*T^eG^I3|9#l%=R7U}i%2*5FpRQDG$sSB?docPpVE zf$`u4s&&yO^Am#MJ#_89m^$Hl=6WTkb&TqI z(82Jgtj)jf;4Gwn1WNLHo#)0=7dTQ+#e}mxcB1n&U89q-NhRicwI7KIf*F@JQXdYg zht|?ALFLK6u3a?I&dCI9A9*UxP@v^M?FCTa(FqSXwLV;DD@wXOO<}5$*eh>ppTFT3($Fu4?*Uxt!EL_`R3dgnvnjpK% z$$9v^VtJaA*`sCPqz2T*8U)-H>22GSg#TZWIIS>=@90+nyk4g#ME%4FmrN?7%H=<4 zy2ZT?fGkJk5_BaR8=YZpX-0&RwK9esTC5M#{dAyG=2|uMb?GrHF!U*(eH)sLrrZU) zX{phoxuKgJ+D8NzuOG6VN_Et)bA;rc^7}T{xZ)Ns&o^eRW{$meN-y}8F@n4gbu1mj zgqqMUZd3EG#)S?UQ6NR?>ZWl(w3E<2pu}I4%R$lhGQxVjiG~!t5bHm zYKEpF5*sr9LDzeeI}?q9GmYqbXM?^XuBn%B!9OonG>vGqP{$r`T5D-7PZ8 zmqw;aTI{dP9o#YIZzp;l^8R2)LoBcMI7SZbncN&H`sf1kd$-e$t5u&nl^H+{@Pq|f3HA8$=t{B{TtpK6MRH+ zi-*u?%i_U*FIqP&^7QcEFKtEgIz0g%a zVh*ejR;3gCvkWIia%v=wYuM}WVGImNc8Q!3L&*bI$kXhG@|4;g*1KZ4QnQNBxdW54c8vq`Ff5YpXQC7tMmT*2#X>xj1$tusmCgMb(hdnHNACGmW8TS6l3ENzZcf_^fT|VWZ4Cs#+#13 z_F#^o4I07)VjrSJF5ZCL+{UPzBU_pP>pg*Q#DkhxXONs~WV;1?TFnd^F3YKqGu|2v zerz%%e0z~6^w8SjeM@t;$%?kU>OJ7xbH$}AVjsoU>jm&zgOvQ!3&wB?y+0;;c+MIm>2&rf zljvqOQ=A&M+B9VoIuH@hzTBuL!Uzk})qrTV)<}-eRERhJgTZ z=wfLSOs`GOHS!O-K=<5av7uHa+I9JCxfjI_HM`~oRZAlT_i?U^WK_F+5W(F^Q|C1o zNpyk?vu83IJxG(GMw(0XWp8YpQeY`b*_do#)6>wWJv%JB#{KnxpxsFd41kL31y4W% z_V6cWIQ~6_4B@SW%PPy;`C^(;vl?411j%tojZ+O#GK}CijN8B!GqVa(g=^b5I`x?* zNw8x?G>|GCdQq%DY?kSS3A0`R_MNH*${YZ!hmRCjnTRzOnJS6BK!AA}Y{}C-S}Snv zrAUVkBl&t;s59?hbO;Wgjwa-IYJhrcI3JNP!@+D0bL6ZhgpMEWw^MseK_k?}pO52R zH_8PyWyWuz?j)pIG#A8Dg7R)zZ*@SrCO!nbO(zuUzsH=$23wLAT3`?)i(6qwwTmR;Upqs!5Cvm01}7}}xS?j+1xdD|aSeSjqd zcmS;Bevg0?Ym+%kOuBnmcel|*+}`1spo=~QQam4oQo3^|Y)X-b_?kxvgcdFlDk%V` zRRo+Enjjplw9LYG%4D3Mwv;7-j^3%4GY@}SF(*F%D zF{*uwXZKY>=2EFU8&{omq$(Rw|83<+!q+E>=SR&}_tG-XT!~byf%f+o`J9MHd0eEm z1eElO`m0A)sm4cyQ z!7DM4@xXuRj|vSIa^0WrGSK$S4~ER@M6ek4MmR>_((A%=TC2KN2ZX-q<8D|rzW->Z zCU^4%mHt}$IHGKHN2l|{q;dch02Jge+)^ioxTnWWCZ|--4q1*g%xCyh;cCE_gRCOi zOa0-g*!zj-*;wJnRi8%#7z)~$U<+uA_zXxKe0_3R zQU`hd6j>fS>$w-u7OP5_STFe-fSju+G-!P0#MkynHqpqU7Hf)txh{T>5(i>k3-GLa ze4MP0%;B@U8z2IXr%yn&F{3mG4r|scCTBSmyR>j*VP7eD1KSmOUm!y8sXw28^T=ao z2%FiT2qymqxZ7(n%kbI85NqN>-As0t5!*qSU&L(?kN$ zhLCyS^3}2#TPL0{7ZU+!V#0WP{E@Rm$=Qp6ETWFb9Uu7N8t__1?*3Ix!}HA?Lg!xH zBt>f$$bj`I!^~^Ii%E^6{ryh6EdRz`=3GQUh9Dkv1=1p_s`0JxEQ>d ztpmOmd{DomYT1HAKF1t!8$Nk809xkGDujxP9fw9WK42hd)4ugdcohK$K1-h*93~ni z3)m|TyM$Lqfc5qr#Hh}ePIHdxnALh##46^W=;|s8tAEW#ffj9kA7&gEM0Iksn0|YP zcdav<9DZMKu%oazvka63P1T3XaMre=bN5Ot5(b##^~Z{F#R(f;Fpdb?vJi*DV0#b% zU~rL2ffY#o1=#fMsyRi?WkS5Th!heMU8bAHUSBx8w!;>d^lff&lV}K&rPycXafgb$ zpdFgdOUG0o|1>F*DB6p(IKMc@IxAt$o zUZ}1ls3F+}VZzpgv9Lqb?)@8lS@cCb?zcpx6+gY4=B9kyP0E0skdaIVESp}?v_O^Dh*&xLGOwF zwS0D0`aPjgU)c&LL>mtnp#xfiliXxjWr>VtSXF@i`@Htgtz8A~Pz;dud-?LJZ>lWBX>kMrd$#IT++i=TTrym!;841alup#c>*qmRrmAo3^70+;1Yn4PE{=RWn3t8Kn27{r&e;$6R=AfAZ4G8XkX}7 zUCUeNqtWtdcOW`tD!6Zg@wU#N4pi7n0Q5H+r?F=>1=rzL^Vu+Pjw?}N)-h!Rt!8`J z{HOVm6cXi5_1A{lKWcW!=!7E74DjZIDoCxMv|6rKZhQUnrc?8uSBqeBjY#VwNFgxL zq5%zR?B$( zZ5}fh5he+JPkW{6?x4!J#qMAT6z1r+y%#H7ApRwV&yC2M$B_H9Chh;$xyDjl#RV}~ zEvzuq1n6tO_`0V292OYF8d-%ZZyb7@i}~}yiU6!k)R4Il!vDN3Ct(%sTa)d_h2ay| zYgM3R*wu*@IhiIg6j^_yB>dX99aG#aY3RNS{W4(MJ|^WStfoacLU@|%kGS#1CT@cm z8T{j=yO)xbqz>*1!&9IC!`hC=f6~covb98Y*0xEnu=vdmNmFVvYmyGPa`^fDZEA6B zhK2u$_SG&2jXpH7Afm++`_f1`d{@;r- z=3}Es6y}(L1d;(3AU=k7qj9wVUTVFP{1&%2Gj%4;f|V!2qx7}b9>@2bDPb5z6R(BV z8a34CY0ajA25Wm}ROr%;!A>E;aO~6b9`h$GWtfA%LqdN?ijQ8Ga@JN;riP}fkK9li z-8I{@&FH-<{UcwYVfC>zmzmGxhLbq?>E#Nx7f0kyP4qXl=onTV#~%M z_Z~(Ar+0y{&e=Q5)83^tm&JH0;6wANT(*Gd%h@^a9R28pF?0}jG)oWT$E>iKBSg8` znvxSuVe%(_^?72<2590J;t)Mz$CNTR-xJNov>E2XSJaZ>x8=C;6ZRl%{OGEP6bX_1 zcd|Kc*%MU36oSvN7z|HIQQJ8toe04Mqs3l2MDb#=Kh-=`&ymaM5Lo7>HO{-wR5-4H zZprGWAJkyz6T5<)mI69>w5YT^b&|2~6vsT|VH75-S}^Pv==!0C;6~%9Q~>mfIm0R~ z;a|o|p&614~ADM7v$;o{PBO z-+P9DC|v>EUsk=qxHV=q$82eDQXEpp2omS{WN`DTvu=mI5tLPR&;Y0PAN^S!LE4qi zHE_1Yrojo%`x}bJoLtdFgTYzhEc>y3DGW_cn&U1TpKxe4XQI5VzpSJpe!wf6CH8K- zf~~LZu9n;@17xx=mJG^XE!IH}8G~_R$zRRsA>>mvD9o^1QQ-46p^->-Y1Kj6ukjMLXqd9-yl zwfk;n5`Mmkku;xyMLOMBJNBnsxDiudIC*Os(N6E6C`CR@oja*<^z*9g>-nNPz`%R6 zm84DQ3(Mb}$K`%vLBOqEG(@7b<1+0{7DDphk0B2fwf_-zQ(JmV13z4@<=r8-TXTgH zl5KCR_@QX3;0QA676stUB8$W)w>vC0 z<)T3q@}NBg`6gTQL>l9&({>Oz(I#XC{kg^K?kcl%&4CSyf!dS@Kb60qdMC!%oMngJ z&5{is2P|D{MFuYeXuiY)SjX9P6=+1m`6tuLTGNp>zl-DPm9Tjn*1T%I+fq(UpFcE- z?aqlOaGtoa2{?e66#qgl2bay{RFbr%D~hG`lH6t41KV2gwjm&|wE@>b+IostDs9WG znQbzZbfJfuW=9mA|AYWbHOVYa^Cv|gEw+EdpEoQDDo7U4mGl` z+r?su;Wr~6@teL+aCYS=4HTQg(QO-h?Jsxyh%Zpn4=l5438PmdT{hr2#|XmA4gp8g z1sA0Swa&CQo=Xn=J^u`}{Vm42B)~g5_1E3NbxX68H3`THW>`8@Tq~w?Cdg$?Aib)E zeO^Kb09jB0cVo6REh@aw2FOnCv`}v>ByDJoN+*Bw8PyI@DAGUos&mag{8h4L(d@Gc zS(kw9k^=TO5~D)Jjp~EyQ?h#jZez2S#|p&Y5!5{+6z`%`>j%=y@v1v!`%FTD