Home | History | Annotate | Download | only in installer
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.timezone.distro.installer;
     17 
     18 import com.android.timezone.distro.DistroException;
     19 import com.android.timezone.distro.DistroVersion;
     20 import com.android.timezone.distro.FileUtils;
     21 import com.android.timezone.distro.StagedDistroOperation;
     22 import com.android.timezone.distro.TimeZoneDistro;
     23 
     24 import android.annotation.IntDef;
     25 import android.util.Slog;
     26 
     27 import java.io.File;
     28 import java.io.FileNotFoundException;
     29 import java.io.IOException;
     30 import java.lang.annotation.Retention;
     31 import java.lang.annotation.RetentionPolicy;
     32 import libcore.util.TimeZoneFinder;
     33 import libcore.util.ZoneInfoDB;
     34 
     35 /**
     36  * A distro-validation / extraction class. Separate from the services code that uses it for easier
     37  * testing. This class is not thread-safe: callers are expected to handle mutual exclusion.
     38  */
     39 public class TimeZoneDistroInstaller {
     40 
     41     @Retention(RetentionPolicy.SOURCE)
     42     @IntDef(prefix = { "INSTALL_" }, value = {
     43             INSTALL_SUCCESS,
     44             INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
     45             INSTALL_FAIL_BAD_DISTRO_FORMAT_VERSION,
     46             INSTALL_FAIL_RULES_TOO_OLD,
     47             INSTALL_FAIL_VALIDATION_ERROR,
     48     })
     49     private @interface InstallResultType {}
     50 
     51     /** {@link #stageInstallWithErrorCode(TimeZoneDistro)} result code: Success. */
     52     public final static int INSTALL_SUCCESS = 0;
     53 
     54     /** {@link #stageInstallWithErrorCode(TimeZoneDistro)} result code: Distro corrupt. */
     55     public final static int INSTALL_FAIL_BAD_DISTRO_STRUCTURE = 1;
     56 
     57     /**
     58      * {@link #stageInstallWithErrorCode(TimeZoneDistro)} result code: Distro version incompatible.
     59      */
     60     public final static int INSTALL_FAIL_BAD_DISTRO_FORMAT_VERSION = 2;
     61 
     62     /**
     63      * {@link #stageInstallWithErrorCode(TimeZoneDistro)} result code: Distro rules too old for
     64      * device.
     65      */
     66     public final static int INSTALL_FAIL_RULES_TOO_OLD = 3;
     67 
     68     /**
     69      * {@link #stageInstallWithErrorCode(TimeZoneDistro)} result code: Distro content failed
     70      * validation.
     71      */
     72     public final static int INSTALL_FAIL_VALIDATION_ERROR = 4;
     73 
     74     @Retention(RetentionPolicy.SOURCE)
     75     @IntDef(prefix = { "UNINSTALL_" }, value = {
     76             UNINSTALL_SUCCESS,
     77             UNINSTALL_NOTHING_INSTALLED,
     78             UNINSTALL_FAIL,
     79     })
     80     private @interface UninstallResultType {}
     81 
     82     /**
     83      * {@link #stageUninstall()} result code: An uninstall has been successfully staged.
     84      */
     85     public final static int UNINSTALL_SUCCESS = 0;
     86 
     87     /**
     88      * {@link #stageUninstall()} result code: Nothing was installed that required an uninstall to be
     89      * staged.
     90      */
     91     public final static int UNINSTALL_NOTHING_INSTALLED = 1;
     92 
     93     /**
     94      * {@link #stageUninstall()} result code: The uninstall could not be staged.
     95      */
     96     public final static int UNINSTALL_FAIL = 2;
     97 
     98     // This constant must match one in system/timezone/tzdatacheck/tzdatacheck.cpp.
     99     private static final String STAGED_TZ_DATA_DIR_NAME = "staged";
    100     // This constant must match one in system/timezone/tzdatacheck/tzdatacheck.cpp.
    101     private static final String CURRENT_TZ_DATA_DIR_NAME = "current";
    102     private static final String WORKING_DIR_NAME = "working";
    103     private static final String OLD_TZ_DATA_DIR_NAME = "old";
    104 
    105     /**
    106      * The name of the file in the staged directory used to indicate a staged uninstallation.
    107      */
    108     // This constant must match one in system/timezone/tzdatacheck/tzdatacheck.cpp.
    109     // VisibleForTesting.
    110     public static final String UNINSTALL_TOMBSTONE_FILE_NAME = "STAGED_UNINSTALL_TOMBSTONE";
    111 
    112     private final String logTag;
    113     private final File systemTzDataFile;
    114     private final File oldStagedDataDir;
    115     private final File stagedTzDataDir;
    116     private final File currentTzDataDir;
    117     private final File workingDir;
    118 
    119     public TimeZoneDistroInstaller(String logTag, File systemTzDataFile, File installDir) {
    120         this.logTag = logTag;
    121         this.systemTzDataFile = systemTzDataFile;
    122         oldStagedDataDir = new File(installDir, OLD_TZ_DATA_DIR_NAME);
    123         stagedTzDataDir = new File(installDir, STAGED_TZ_DATA_DIR_NAME);
    124         currentTzDataDir = new File(installDir, CURRENT_TZ_DATA_DIR_NAME);
    125         workingDir = new File(installDir, WORKING_DIR_NAME);
    126     }
    127 
    128     // VisibleForTesting
    129     File getOldStagedDataDir() {
    130         return oldStagedDataDir;
    131     }
    132 
    133     // VisibleForTesting
    134     File getStagedTzDataDir() {
    135         return stagedTzDataDir;
    136     }
    137 
    138     // VisibleForTesting
    139     File getCurrentTzDataDir() {
    140         return currentTzDataDir;
    141     }
    142 
    143     // VisibleForTesting
    144     File getWorkingDir() {
    145         return workingDir;
    146     }
    147 
    148     /**
    149      * Stage an install of the supplied content, to be installed the next time the device boots.
    150      *
    151      * <p>Errors during unpacking or staging will throw an {@link IOException}.
    152      * Returns {@link #INSTALL_SUCCESS} on success, or one of the failure codes.
    153      */
    154     public @InstallResultType int stageInstallWithErrorCode(TimeZoneDistro distro)
    155             throws IOException {
    156         if (oldStagedDataDir.exists()) {
    157             FileUtils.deleteRecursive(oldStagedDataDir);
    158         }
    159         if (workingDir.exists()) {
    160             FileUtils.deleteRecursive(workingDir);
    161         }
    162 
    163         Slog.i(logTag, "Unpacking / verifying time zone update");
    164         try {
    165             unpackDistro(distro, workingDir);
    166 
    167             DistroVersion distroVersion;
    168             try {
    169                 distroVersion = readDistroVersion(workingDir);
    170             } catch (DistroException e) {
    171                 Slog.i(logTag, "Invalid distro version: " + e.getMessage());
    172                 return INSTALL_FAIL_BAD_DISTRO_STRUCTURE;
    173             }
    174             if (distroVersion == null) {
    175                 Slog.i(logTag, "Update not applied: Distro version could not be loaded");
    176                 return INSTALL_FAIL_BAD_DISTRO_STRUCTURE;
    177             }
    178             if (!DistroVersion.isCompatibleWithThisDevice(distroVersion)) {
    179                 Slog.i(logTag, "Update not applied: Distro format version check failed: "
    180                         + distroVersion);
    181                 return INSTALL_FAIL_BAD_DISTRO_FORMAT_VERSION;
    182             }
    183 
    184             if (!checkDistroDataFilesExist(workingDir)) {
    185                 Slog.i(logTag, "Update not applied: Distro is missing required data file(s)");
    186                 return INSTALL_FAIL_BAD_DISTRO_STRUCTURE;
    187             }
    188 
    189             if (!checkDistroRulesNewerThanSystem(systemTzDataFile, distroVersion)) {
    190                 Slog.i(logTag, "Update not applied: Distro rules version check failed");
    191                 return INSTALL_FAIL_RULES_TOO_OLD;
    192             }
    193 
    194             // Validate the tzdata file.
    195             File zoneInfoFile = new File(workingDir, TimeZoneDistro.TZDATA_FILE_NAME);
    196             ZoneInfoDB.TzData tzData = ZoneInfoDB.TzData.loadTzData(zoneInfoFile.getPath());
    197             if (tzData == null) {
    198                 Slog.i(logTag, "Update not applied: " + zoneInfoFile + " could not be loaded");
    199                 return INSTALL_FAIL_VALIDATION_ERROR;
    200             }
    201             try {
    202                 tzData.validate();
    203             } catch (IOException e) {
    204                 Slog.i(logTag, "Update not applied: " + zoneInfoFile + " failed validation", e);
    205                 return INSTALL_FAIL_VALIDATION_ERROR;
    206             } finally {
    207                 tzData.close();
    208             }
    209 
    210             // Validate the tzlookup.xml file.
    211             File tzLookupFile = new File(workingDir, TimeZoneDistro.TZLOOKUP_FILE_NAME);
    212             if (!tzLookupFile.exists()) {
    213                 Slog.i(logTag, "Update not applied: " + tzLookupFile + " does not exist");
    214                 return INSTALL_FAIL_BAD_DISTRO_STRUCTURE;
    215             }
    216             try {
    217                 TimeZoneFinder timeZoneFinder =
    218                         TimeZoneFinder.createInstance(tzLookupFile.getPath());
    219                 timeZoneFinder.validate();
    220             } catch (IOException e) {
    221                 Slog.i(logTag, "Update not applied: " + tzLookupFile + " failed validation", e);
    222                 return INSTALL_FAIL_VALIDATION_ERROR;
    223             }
    224 
    225             // TODO(nfuller): Add validity checks for ICU data / canarying before applying.
    226             // http://b/64016752
    227 
    228             Slog.i(logTag, "Applying time zone update");
    229             FileUtils.makeDirectoryWorldAccessible(workingDir);
    230 
    231             // Check if there is already a staged install or uninstall and remove it if there is.
    232             if (!stagedTzDataDir.exists()) {
    233                 Slog.i(logTag, "Nothing to unstage at " + stagedTzDataDir);
    234             } else {
    235                 Slog.i(logTag, "Moving " + stagedTzDataDir + " to " + oldStagedDataDir);
    236                 // Move stagedTzDataDir out of the way in one operation so we can't partially delete
    237                 // the contents.
    238                 FileUtils.rename(stagedTzDataDir, oldStagedDataDir);
    239             }
    240 
    241             // Move the workingDir to be the new staged directory.
    242             Slog.i(logTag, "Moving " + workingDir + " to " + stagedTzDataDir);
    243             FileUtils.rename(workingDir, stagedTzDataDir);
    244             Slog.i(logTag, "Install staged: " + stagedTzDataDir + " successfully created");
    245             return INSTALL_SUCCESS;
    246         } finally {
    247             deleteBestEffort(oldStagedDataDir);
    248             deleteBestEffort(workingDir);
    249         }
    250     }
    251 
    252     /**
    253      * Stage an uninstall of the current timezone update in /data which, on reboot, will return the
    254      * device to using data from /system. If there was something else already staged it will be
    255      * removed by this call.
    256      *
    257      * Returns {@link #UNINSTALL_SUCCESS} if staging the uninstallation was
    258      * successful and reboot will be required. Returns {@link #UNINSTALL_NOTHING_INSTALLED} if
    259      * there was nothing installed in /data that required an uninstall to be staged, anything that
    260      * was staged will have been removed and therefore no reboot is required.
    261      *
    262      * <p>Errors encountered during uninstallation will throw an {@link IOException}.
    263      */
    264     public @UninstallResultType int stageUninstall() throws IOException {
    265         Slog.i(logTag, "Uninstalling time zone update");
    266 
    267         if (oldStagedDataDir.exists()) {
    268             // If we can't remove this, an exception is thrown and we don't continue.
    269             FileUtils.deleteRecursive(oldStagedDataDir);
    270         }
    271         if (workingDir.exists()) {
    272             FileUtils.deleteRecursive(workingDir);
    273         }
    274 
    275         try {
    276             // Check if there is already an install or uninstall staged and remove it.
    277             if (!stagedTzDataDir.exists()) {
    278                 Slog.i(logTag, "Nothing to unstage at " + stagedTzDataDir);
    279             } else {
    280                 Slog.i(logTag, "Moving " + stagedTzDataDir + " to " + oldStagedDataDir);
    281                 // Move stagedTzDataDir out of the way in one operation so we can't partially delete
    282                 // the contents.
    283                 FileUtils.rename(stagedTzDataDir, oldStagedDataDir);
    284             }
    285 
    286             // If there's nothing actually installed, there's nothing to uninstall so no need to
    287             // stage anything.
    288             if (!currentTzDataDir.exists()) {
    289                 Slog.i(logTag, "Nothing to uninstall at " + currentTzDataDir);
    290                 return UNINSTALL_NOTHING_INSTALLED;
    291             }
    292 
    293             // Stage an uninstall in workingDir.
    294             FileUtils.ensureDirectoriesExist(workingDir, true /* makeWorldReadable */);
    295             FileUtils.createEmptyFile(new File(workingDir, UNINSTALL_TOMBSTONE_FILE_NAME));
    296 
    297             // Move the workingDir to be the new staged directory.
    298             Slog.i(logTag, "Moving " + workingDir + " to " + stagedTzDataDir);
    299             FileUtils.rename(workingDir, stagedTzDataDir);
    300             Slog.i(logTag, "Uninstall staged: " + stagedTzDataDir + " successfully created");
    301 
    302             return UNINSTALL_SUCCESS;
    303         } finally {
    304             deleteBestEffort(oldStagedDataDir);
    305             deleteBestEffort(workingDir);
    306         }
    307     }
    308 
    309     /**
    310      * Reads the currently installed distro version. Returns {@code null} if there is no distro
    311      * installed.
    312      *
    313      * @throws IOException if there was a problem reading data from /data
    314      * @throws DistroException if there was a problem with the installed distro format/structure
    315      */
    316     public DistroVersion getInstalledDistroVersion() throws DistroException, IOException {
    317         if (!currentTzDataDir.exists()) {
    318             return null;
    319         }
    320         return readDistroVersion(currentTzDataDir);
    321     }
    322 
    323     /**
    324      * Reads information about any currently staged distro operation. Returns {@code null} if there
    325      * is no distro operation staged.
    326      *
    327      * @throws IOException if there was a problem reading data from /data
    328      * @throws DistroException if there was a problem with the staged distro format/structure
    329      */
    330     public StagedDistroOperation getStagedDistroOperation() throws DistroException, IOException {
    331         if (!stagedTzDataDir.exists()) {
    332             return null;
    333         }
    334         if (new File(stagedTzDataDir, UNINSTALL_TOMBSTONE_FILE_NAME).exists()) {
    335             return StagedDistroOperation.uninstall();
    336         } else {
    337             return StagedDistroOperation.install(readDistroVersion(stagedTzDataDir));
    338         }
    339     }
    340 
    341     /**
    342      * Reads the timezone rules version present in /system. i.e. the version that would be present
    343      * after a factory reset.
    344      *
    345      * @throws IOException if there was a problem reading data
    346      */
    347     public String getSystemRulesVersion() throws IOException {
    348         return readSystemRulesVersion(systemTzDataFile);
    349     }
    350 
    351     private void deleteBestEffort(File dir) {
    352         if (dir.exists()) {
    353             Slog.i(logTag, "Deleting " + dir);
    354             try {
    355                 FileUtils.deleteRecursive(dir);
    356             } catch (IOException e) {
    357                 // Logged but otherwise ignored.
    358                 Slog.w(logTag, "Unable to delete " + dir, e);
    359             }
    360         }
    361     }
    362 
    363     private void unpackDistro(TimeZoneDistro distro, File targetDir) throws IOException {
    364         Slog.i(logTag, "Unpacking update content to: " + targetDir);
    365         distro.extractTo(targetDir);
    366     }
    367 
    368     private boolean checkDistroDataFilesExist(File unpackedContentDir) throws IOException {
    369         Slog.i(logTag, "Verifying distro contents");
    370         return FileUtils.filesExist(unpackedContentDir,
    371                 TimeZoneDistro.TZDATA_FILE_NAME,
    372                 TimeZoneDistro.ICU_DATA_FILE_NAME);
    373     }
    374 
    375     private DistroVersion readDistroVersion(File distroDir) throws DistroException, IOException {
    376         Slog.d(logTag, "Reading distro format version: " + distroDir);
    377         File distroVersionFile = new File(distroDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
    378         if (!distroVersionFile.exists()) {
    379             throw new DistroException("No distro version file found: " + distroVersionFile);
    380         }
    381         byte[] versionBytes =
    382                 FileUtils.readBytes(distroVersionFile, DistroVersion.DISTRO_VERSION_FILE_LENGTH);
    383         return DistroVersion.fromBytes(versionBytes);
    384     }
    385 
    386     /**
    387      * Returns true if the the distro IANA rules version is >= system IANA rules version.
    388      */
    389     private boolean checkDistroRulesNewerThanSystem(
    390             File systemTzDataFile, DistroVersion distroVersion) throws IOException {
    391 
    392         // We only check the /system tzdata file and assume that other data like ICU is in sync.
    393         // There is a CTS test that checks ICU and bionic/libcore are in sync.
    394         Slog.i(logTag, "Reading /system rules version");
    395         String systemRulesVersion = readSystemRulesVersion(systemTzDataFile);
    396 
    397         String distroRulesVersion = distroVersion.rulesVersion;
    398         // canApply = distroRulesVersion >= systemRulesVersion
    399         boolean canApply = distroRulesVersion.compareTo(systemRulesVersion) >= 0;
    400         if (!canApply) {
    401             Slog.i(logTag, "Failed rules version check: distroRulesVersion="
    402                     + distroRulesVersion + ", systemRulesVersion=" + systemRulesVersion);
    403         } else {
    404             Slog.i(logTag, "Passed rules version check: distroRulesVersion="
    405                     + distroRulesVersion + ", systemRulesVersion=" + systemRulesVersion);
    406         }
    407         return canApply;
    408     }
    409 
    410     private String readSystemRulesVersion(File systemTzDataFile) throws IOException {
    411         if (!systemTzDataFile.exists()) {
    412             Slog.i(logTag, "tzdata file cannot be found in /system");
    413             throw new FileNotFoundException("system tzdata does not exist: " + systemTzDataFile);
    414         }
    415         return ZoneInfoDB.TzData.getRulesVersion(systemTzDataFile);
    416     }
    417 }
    418