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