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