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