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