Move distro installer code to system/timezone am: 71a6a6fdb8 am: 41cb382fd0 am: 377fdfa96a
am: ef4ed886be
Change-Id: Ifee0b3f63a695d8be79db0606a4eb38b48b96c47
diff --git a/distro/installer/Android.mk b/distro/installer/Android.mk
new file mode 100644
index 0000000..2b22725
--- /dev/null
+++ b/distro/installer/Android.mk
@@ -0,0 +1,35 @@
+# Copyright (C) 2015 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH:= $(call my-dir)
+
+# The classes needed to handle installation of time zone distros.
+include $(CLEAR_VARS)
+LOCAL_MODULE := time_zone_distro_installer
+LOCAL_MODULE_TAGS := optional
+LOCAL_SRC_FILES := $(call all-java-files-under, src/main)
+LOCAL_JAVA_LIBRARIES := time_zone_distro
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# Tests for time_zone_distro_installer code
+include $(CLEAR_VARS)
+LOCAL_MODULE := time_zone_distro_installer-tests
+LOCAL_MODULE_TAGS := optional
+LOCAL_SRC_FILES := $(call all-java-files-under, src/test)
+LOCAL_STATIC_JAVA_LIBRARIES := time_zone_distro \
+ time_zone_distro_tools \
+ time_zone_distro_installer \
+ tzdata-testing \
+ junit
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/distro/installer/src/main/com/android/timezone/distro/installer/TimeZoneDistroInstaller.java b/distro/installer/src/main/com/android/timezone/distro/installer/TimeZoneDistroInstaller.java
new file mode 100644
index 0000000..870fd31
--- /dev/null
+++ b/distro/installer/src/main/com/android/timezone/distro/installer/TimeZoneDistroInstaller.java
@@ -0,0 +1,374 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.timezone.distro.installer;
+
+import com.android.timezone.distro.DistroException;
+import com.android.timezone.distro.DistroVersion;
+import com.android.timezone.distro.FileUtils;
+import com.android.timezone.distro.StagedDistroOperation;
+import com.android.timezone.distro.TimeZoneDistro;
+
+import android.util.Slog;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import libcore.util.TimeZoneFinder;
+import libcore.util.ZoneInfoDB;
+
+/**
+ * A distro-validation / extraction class. Separate from the services code that uses it for easier
+ * testing. This class is not thread-safe: callers are expected to handle mutual exclusion.
+ */
+public class TimeZoneDistroInstaller {
+ /** {@link #stageInstallWithErrorCode(TimeZoneDistro)} result code: Success. */
+ public final static int INSTALL_SUCCESS = 0;
+ /** {@link #stageInstallWithErrorCode(TimeZoneDistro)} result code: Distro corrupt. */
+ public final static int INSTALL_FAIL_BAD_DISTRO_STRUCTURE = 1;
+ /** {@link #stageInstallWithErrorCode(TimeZoneDistro)} result code: Distro version incompatible. */
+ public final static int INSTALL_FAIL_BAD_DISTRO_FORMAT_VERSION = 2;
+ /** {@link #stageInstallWithErrorCode(TimeZoneDistro)} result code: Distro rules too old for device. */
+ public final static int INSTALL_FAIL_RULES_TOO_OLD = 3;
+ /** {@link #stageInstallWithErrorCode(TimeZoneDistro)} result code: Distro content failed validation. */
+ public final static int INSTALL_FAIL_VALIDATION_ERROR = 4;
+
+ // This constant must match one in system/timezone/tzdatacheck/tzdatacheck.cpp.
+ private static final String STAGED_TZ_DATA_DIR_NAME = "staged";
+ // This constant must match one in system/timezone/tzdatacheck/tzdatacheck.cpp.
+ private static final String CURRENT_TZ_DATA_DIR_NAME = "current";
+ private static final String WORKING_DIR_NAME = "working";
+ private static final String OLD_TZ_DATA_DIR_NAME = "old";
+
+ /**
+ * The name of the file in the staged directory used to indicate a staged uninstallation.
+ */
+ // This constant must match one in system/timezone/tzdatacheck/tzdatacheck.cpp.
+ // VisibleForTesting.
+ public static final String UNINSTALL_TOMBSTONE_FILE_NAME = "STAGED_UNINSTALL_TOMBSTONE";
+
+ private final String logTag;
+ private final File systemTzDataFile;
+ private final File oldStagedDataDir;
+ private final File stagedTzDataDir;
+ private final File currentTzDataDir;
+ private final File workingDir;
+
+ public TimeZoneDistroInstaller(String logTag, File systemTzDataFile, File installDir) {
+ this.logTag = logTag;
+ this.systemTzDataFile = systemTzDataFile;
+ oldStagedDataDir = new File(installDir, OLD_TZ_DATA_DIR_NAME);
+ stagedTzDataDir = new File(installDir, STAGED_TZ_DATA_DIR_NAME);
+ currentTzDataDir = new File(installDir, CURRENT_TZ_DATA_DIR_NAME);
+ workingDir = new File(installDir, WORKING_DIR_NAME);
+ }
+
+ // VisibleForTesting
+ File getOldStagedDataDir() {
+ return oldStagedDataDir;
+ }
+
+ // VisibleForTesting
+ File getStagedTzDataDir() {
+ return stagedTzDataDir;
+ }
+
+ // VisibleForTesting
+ File getCurrentTzDataDir() {
+ return currentTzDataDir;
+ }
+
+ // VisibleForTesting
+ File getWorkingDir() {
+ return workingDir;
+ }
+
+ /**
+ * Stage an install of the supplied content, to be installed the next time the device boots.
+ *
+ * <p>Errors during unpacking or staging will throw an {@link IOException}.
+ * If the distro content is invalid this method returns {@code false}.
+ * If the installation completed successfully this method returns {@code true}.
+ */
+ public boolean install(TimeZoneDistro distro) throws IOException {
+ int result = stageInstallWithErrorCode(distro);
+ return result == INSTALL_SUCCESS;
+ }
+
+ /**
+ * Stage an install of the supplied content, to be installed the next time the device boots.
+ *
+ * <p>Errors during unpacking or staging will throw an {@link IOException}.
+ * Returns {@link #INSTALL_SUCCESS} or an error code.
+ */
+ public int stageInstallWithErrorCode(TimeZoneDistro distro) throws IOException {
+ if (oldStagedDataDir.exists()) {
+ FileUtils.deleteRecursive(oldStagedDataDir);
+ }
+ if (workingDir.exists()) {
+ FileUtils.deleteRecursive(workingDir);
+ }
+
+ Slog.i(logTag, "Unpacking / verifying time zone update");
+ try {
+ unpackDistro(distro, workingDir);
+
+ DistroVersion distroVersion;
+ try {
+ distroVersion = readDistroVersion(workingDir);
+ } catch (DistroException e) {
+ Slog.i(logTag, "Invalid distro version: " + e.getMessage());
+ return INSTALL_FAIL_BAD_DISTRO_STRUCTURE;
+ }
+ if (distroVersion == null) {
+ Slog.i(logTag, "Update not applied: Distro version could not be loaded");
+ return INSTALL_FAIL_BAD_DISTRO_STRUCTURE;
+ }
+ if (!DistroVersion.isCompatibleWithThisDevice(distroVersion)) {
+ Slog.i(logTag, "Update not applied: Distro format version check failed: "
+ + distroVersion);
+ return INSTALL_FAIL_BAD_DISTRO_FORMAT_VERSION;
+ }
+
+ if (!checkDistroDataFilesExist(workingDir)) {
+ Slog.i(logTag, "Update not applied: Distro is missing required data file(s)");
+ return INSTALL_FAIL_BAD_DISTRO_STRUCTURE;
+ }
+
+ if (!checkDistroRulesNewerThanSystem(systemTzDataFile, distroVersion)) {
+ Slog.i(logTag, "Update not applied: Distro rules version check failed");
+ return INSTALL_FAIL_RULES_TOO_OLD;
+ }
+
+ // Validate the tzdata file.
+ File zoneInfoFile = new File(workingDir, TimeZoneDistro.TZDATA_FILE_NAME);
+ ZoneInfoDB.TzData tzData = ZoneInfoDB.TzData.loadTzData(zoneInfoFile.getPath());
+ if (tzData == null) {
+ Slog.i(logTag, "Update not applied: " + zoneInfoFile + " could not be loaded");
+ return INSTALL_FAIL_VALIDATION_ERROR;
+ }
+ try {
+ tzData.validate();
+ } catch (IOException e) {
+ Slog.i(logTag, "Update not applied: " + zoneInfoFile + " failed validation", e);
+ return INSTALL_FAIL_VALIDATION_ERROR;
+ } finally {
+ tzData.close();
+ }
+
+ // Validate the tzlookup.xml file.
+ File tzLookupFile = new File(workingDir, TimeZoneDistro.TZLOOKUP_FILE_NAME);
+ if (!tzLookupFile.exists()) {
+ Slog.i(logTag, "Update not applied: " + tzLookupFile + " does not exist");
+ return INSTALL_FAIL_BAD_DISTRO_STRUCTURE;
+ }
+ try {
+ TimeZoneFinder timeZoneFinder =
+ TimeZoneFinder.createInstance(tzLookupFile.getPath());
+ timeZoneFinder.validate();
+ } catch (IOException e) {
+ Slog.i(logTag, "Update not applied: " + tzLookupFile + " failed validation", e);
+ return INSTALL_FAIL_VALIDATION_ERROR;
+ }
+
+ // TODO(nfuller): Add validity checks for ICU data / canarying before applying.
+ // http://b/31008728
+
+ Slog.i(logTag, "Applying time zone update");
+ FileUtils.makeDirectoryWorldAccessible(workingDir);
+
+ // Check if there is already a staged install or uninstall and remove it if there is.
+ if (!stagedTzDataDir.exists()) {
+ Slog.i(logTag, "Nothing to unstage at " + stagedTzDataDir);
+ } else {
+ Slog.i(logTag, "Moving " + stagedTzDataDir + " to " + oldStagedDataDir);
+ // Move stagedTzDataDir out of the way in one operation so we can't partially delete
+ // the contents.
+ FileUtils.rename(stagedTzDataDir, oldStagedDataDir);
+ }
+
+ // Move the workingDir to be the new staged directory.
+ Slog.i(logTag, "Moving " + workingDir + " to " + stagedTzDataDir);
+ FileUtils.rename(workingDir, stagedTzDataDir);
+ Slog.i(logTag, "Install staged: " + stagedTzDataDir + " successfully created");
+ return INSTALL_SUCCESS;
+ } finally {
+ deleteBestEffort(oldStagedDataDir);
+ deleteBestEffort(workingDir);
+ }
+ }
+
+ /**
+ * Stage an uninstall of the current timezone update in /data which, on reboot, will return the
+ * device to using data from /system. Returns {@code true} if staging the uninstallation was
+ * successful, {@code false} if there was nothing installed in /data to uninstall. If there was
+ * something else staged it will be replaced by this call.
+ *
+ * <p>Errors encountered during uninstallation will throw an {@link IOException}.
+ */
+ public boolean stageUninstall() throws IOException {
+ Slog.i(logTag, "Uninstalling time zone update");
+
+ if (oldStagedDataDir.exists()) {
+ // If we can't remove this, an exception is thrown and we don't continue.
+ FileUtils.deleteRecursive(oldStagedDataDir);
+ }
+ if (workingDir.exists()) {
+ FileUtils.deleteRecursive(workingDir);
+ }
+
+ try {
+ // Check if there is already an install or uninstall staged and remove it.
+ if (!stagedTzDataDir.exists()) {
+ Slog.i(logTag, "Nothing to unstage at " + stagedTzDataDir);
+ } else {
+ Slog.i(logTag, "Moving " + stagedTzDataDir + " to " + oldStagedDataDir);
+ // Move stagedTzDataDir out of the way in one operation so we can't partially delete
+ // the contents.
+ FileUtils.rename(stagedTzDataDir, oldStagedDataDir);
+ }
+
+ // If there's nothing actually installed, there's nothing to uninstall so no need to
+ // stage anything.
+ if (!currentTzDataDir.exists()) {
+ Slog.i(logTag, "Nothing to uninstall at " + currentTzDataDir);
+ return false;
+ }
+
+ // Stage an uninstall in workingDir.
+ FileUtils.ensureDirectoriesExist(workingDir, true /* makeWorldReadable */);
+ FileUtils.createEmptyFile(new File(workingDir, UNINSTALL_TOMBSTONE_FILE_NAME));
+
+ // Move the workingDir to be the new staged directory.
+ Slog.i(logTag, "Moving " + workingDir + " to " + stagedTzDataDir);
+ FileUtils.rename(workingDir, stagedTzDataDir);
+ Slog.i(logTag, "Uninstall staged: " + stagedTzDataDir + " successfully created");
+
+ return true;
+ } finally {
+ deleteBestEffort(oldStagedDataDir);
+ deleteBestEffort(workingDir);
+ }
+ }
+
+ /**
+ * Reads the currently installed distro version. Returns {@code null} if there is no distro
+ * installed.
+ *
+ * @throws IOException if there was a problem reading data from /data
+ * @throws DistroException if there was a problem with the installed distro format/structure
+ */
+ public DistroVersion getInstalledDistroVersion() throws DistroException, IOException {
+ if (!currentTzDataDir.exists()) {
+ return null;
+ }
+ return readDistroVersion(currentTzDataDir);
+ }
+
+ /**
+ * Reads information about any currently staged distro operation. Returns {@code null} if there
+ * is no distro operation staged.
+ *
+ * @throws IOException if there was a problem reading data from /data
+ * @throws DistroException if there was a problem with the staged distro format/structure
+ */
+ public StagedDistroOperation getStagedDistroOperation() throws DistroException, IOException {
+ if (!stagedTzDataDir.exists()) {
+ return null;
+ }
+ if (new File(stagedTzDataDir, UNINSTALL_TOMBSTONE_FILE_NAME).exists()) {
+ return StagedDistroOperation.uninstall();
+ } else {
+ return StagedDistroOperation.install(readDistroVersion(stagedTzDataDir));
+ }
+ }
+
+ /**
+ * Reads the timezone rules version present in /system. i.e. the version that would be present
+ * after a factory reset.
+ *
+ * @throws IOException if there was a problem reading data
+ */
+ public String getSystemRulesVersion() throws IOException {
+ return readSystemRulesVersion(systemTzDataFile);
+ }
+
+ private void deleteBestEffort(File dir) {
+ if (dir.exists()) {
+ Slog.i(logTag, "Deleting " + dir);
+ try {
+ FileUtils.deleteRecursive(dir);
+ } catch (IOException e) {
+ // Logged but otherwise ignored.
+ Slog.w(logTag, "Unable to delete " + dir, e);
+ }
+ }
+ }
+
+ private void unpackDistro(TimeZoneDistro distro, File targetDir) throws IOException {
+ Slog.i(logTag, "Unpacking update content to: " + targetDir);
+ distro.extractTo(targetDir);
+ }
+
+ private boolean checkDistroDataFilesExist(File unpackedContentDir) throws IOException {
+ Slog.i(logTag, "Verifying distro contents");
+ return FileUtils.filesExist(unpackedContentDir,
+ TimeZoneDistro.TZDATA_FILE_NAME,
+ TimeZoneDistro.ICU_DATA_FILE_NAME);
+ }
+
+ private DistroVersion readDistroVersion(File distroDir) throws DistroException, IOException {
+ Slog.i(logTag, "Reading distro format version: " + distroDir);
+ File distroVersionFile = new File(distroDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
+ if (!distroVersionFile.exists()) {
+ throw new DistroException("No distro version file found: " + distroVersionFile);
+ }
+ byte[] versionBytes =
+ FileUtils.readBytes(distroVersionFile, DistroVersion.DISTRO_VERSION_FILE_LENGTH);
+ return DistroVersion.fromBytes(versionBytes);
+ }
+
+ /**
+ * Returns true if the the distro IANA rules version is >= system IANA rules version.
+ */
+ private boolean checkDistroRulesNewerThanSystem(
+ File systemTzDataFile, DistroVersion distroVersion) throws IOException {
+
+ // We only check the /system tzdata file and assume that other data like ICU is in sync.
+ // There is a CTS test that checks ICU and bionic/libcore are in sync.
+ Slog.i(logTag, "Reading /system rules version");
+ String systemRulesVersion = readSystemRulesVersion(systemTzDataFile);
+
+ String distroRulesVersion = distroVersion.rulesVersion;
+ // canApply = distroRulesVersion >= systemRulesVersion
+ boolean canApply = distroRulesVersion.compareTo(systemRulesVersion) >= 0;
+ if (!canApply) {
+ Slog.i(logTag, "Failed rules version check: distroRulesVersion="
+ + distroRulesVersion + ", systemRulesVersion=" + systemRulesVersion);
+ } else {
+ Slog.i(logTag, "Passed rules version check: distroRulesVersion="
+ + distroRulesVersion + ", systemRulesVersion=" + systemRulesVersion);
+ }
+ return canApply;
+ }
+
+ private String readSystemRulesVersion(File systemTzDataFile) throws IOException {
+ if (!systemTzDataFile.exists()) {
+ Slog.i(logTag, "tzdata file cannot be found in /system");
+ throw new FileNotFoundException("system tzdata does not exist: " + systemTzDataFile);
+ }
+ return ZoneInfoDB.TzData.getRulesVersion(systemTzDataFile);
+ }
+}
diff --git a/distro/installer/src/test/com/android/timezone/distro/installer/TimeZoneDistroInstallerTest.java b/distro/installer/src/test/com/android/timezone/distro/installer/TimeZoneDistroInstallerTest.java
new file mode 100644
index 0000000..596b6ff
--- /dev/null
+++ b/distro/installer/src/test/com/android/timezone/distro/installer/TimeZoneDistroInstallerTest.java
@@ -0,0 +1,707 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.timezone.distro.installer;
+
+import com.android.timezone.distro.DistroVersion;
+import com.android.timezone.distro.FileUtils;
+import com.android.timezone.distro.StagedDistroOperation;
+import com.android.timezone.distro.TimeZoneDistro;
+import com.android.timezone.distro.tools.TimeZoneDistroBuilder;
+
+import junit.framework.TestCase;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.FileVisitor;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import libcore.io.IoUtils;
+import libcore.tzdata.testing.ZoneInfoTestHelper;
+
+import static org.junit.Assert.assertArrayEquals;
+
+/**
+ * Tests for {@link TimeZoneDistroInstaller}.
+ */
+public class TimeZoneDistroInstallerTest extends TestCase {
+
+ // OLDER_RULES_VERSION < SYSTEM_RULES_VERSION < NEW_RULES_VERSION < NEWER_RULES_VERSION
+ private static final String OLDER_RULES_VERSION = "2030a";
+ private static final String SYSTEM_RULES_VERSION = "2030b";
+ private static final String NEW_RULES_VERSION = "2030c";
+ private static final String NEWER_RULES_VERSION = "2030d";
+
+ private TimeZoneDistroInstaller installer;
+ private File tempDir;
+ private File testInstallDir;
+ private File testSystemTzDataDir;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ tempDir = createUniqueDirectory(null, "tempDir");
+ testInstallDir = createSubDirectory(tempDir, "testInstall");
+ testSystemTzDataDir = createSubDirectory(tempDir, "testSystemTzData");
+
+ // Create a file to represent the tzdata file in the /system partition of the device.
+ File testSystemTzDataFile = new File(testSystemTzDataDir, "tzdata");
+ byte[] systemTzDataBytes = createTzData(SYSTEM_RULES_VERSION);
+ createFile(testSystemTzDataFile, systemTzDataBytes);
+
+ installer = new TimeZoneDistroInstaller(
+ "TimeZoneDistroInstallerTest", testSystemTzDataFile, testInstallDir);
+ }
+
+ /**
+ * Creates a unique temporary directory. rootDir can be null, in which case the directory will
+ * be created beneath the directory pointed to by the java.io.tmpdir system property.
+ */
+ private static File createUniqueDirectory(File rootDir, String prefix) throws Exception {
+ File dir = File.createTempFile(prefix, "", rootDir);
+ assertTrue(dir.delete());
+ assertTrue(dir.mkdir());
+ return dir;
+ }
+
+ private static File createSubDirectory(File parent, String subDirName) {
+ File dir = new File(parent, subDirName);
+ assertTrue(dir.mkdir());
+ return dir;
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ if (tempDir.exists()) {
+ FileUtils.deleteRecursive(tempDir);
+ }
+ super.tearDown();
+ }
+
+ /** Tests the an update on a device will fail if the /system tzdata file cannot be found. */
+ public void testStageInstallWithErrorCode_badSystemFile() throws Exception {
+ File doesNotExist = new File(testSystemTzDataDir, "doesNotExist");
+ TimeZoneDistroInstaller brokenSystemInstaller = new TimeZoneDistroInstaller(
+ "TimeZoneDistroInstallerTest", doesNotExist, testInstallDir);
+ byte[] distroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
+
+ try {
+ brokenSystemInstaller.stageInstallWithErrorCode(new TimeZoneDistro(distroBytes));
+ fail();
+ } catch (IOException expected) {}
+
+ assertNoDistroOperationStaged();
+ assertNoInstalledDistro();
+ }
+
+ /** Tests the first successful update on a device */
+ public void testStageInstallWithErrorCode_successfulFirstUpdate() throws Exception {
+ byte[] distroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
+
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_SUCCESS,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(distroBytes)));
+ assertInstallDistroStaged(distroBytes);
+ assertNoInstalledDistro();
+ }
+
+ /**
+ * Tests we can install an update the same version as is in /system.
+ */
+ public void testStageInstallWithErrorCode_successfulFirstUpdate_sameVersionAsSystem()
+ throws Exception {
+ byte[] distroBytes = createValidTimeZoneDistroBytes(SYSTEM_RULES_VERSION, 1);
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_SUCCESS,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(distroBytes)));
+ assertInstallDistroStaged(distroBytes);
+ assertNoInstalledDistro();
+ }
+
+ /**
+ * Tests we cannot install an update older than the version in /system.
+ */
+ public void testStageInstallWithErrorCode_unsuccessfulFirstUpdate_olderVersionThanSystem()
+ throws Exception {
+ byte[] distroBytes = createValidTimeZoneDistroBytes(OLDER_RULES_VERSION, 1);
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_FAIL_RULES_TOO_OLD,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(distroBytes)));
+ assertNoDistroOperationStaged();
+ assertNoInstalledDistro();
+ }
+
+ /**
+ * Tests an update on a device when there is a prior update already staged.
+ */
+ public void testStageInstallWithErrorCode_successfulFollowOnUpdate_newerVersion()
+ throws Exception {
+ byte[] distro1Bytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_SUCCESS,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(distro1Bytes)));
+ assertInstallDistroStaged(distro1Bytes);
+
+ byte[] distro2Bytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 2);
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_SUCCESS,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(distro2Bytes)));
+ assertInstallDistroStaged(distro2Bytes);
+
+ byte[] distro3Bytes = createValidTimeZoneDistroBytes(NEWER_RULES_VERSION, 1);
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_SUCCESS,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(distro3Bytes)));
+ assertInstallDistroStaged(distro3Bytes);
+ assertNoInstalledDistro();
+ }
+
+ /**
+ * Tests an update on a device when there is a prior update already applied, but the follow
+ * on update is older than in /system.
+ */
+ public void testStageInstallWithErrorCode_unsuccessfulFollowOnUpdate_olderVersion()
+ throws Exception {
+ byte[] distro1Bytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 2);
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_SUCCESS,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(distro1Bytes)));
+ assertInstallDistroStaged(distro1Bytes);
+
+ byte[] distro2Bytes = createValidTimeZoneDistroBytes(OLDER_RULES_VERSION, 1);
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_FAIL_RULES_TOO_OLD,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(distro2Bytes)));
+ assertInstallDistroStaged(distro1Bytes);
+ assertNoInstalledDistro();
+ }
+
+ /** Tests that a distro with a missing tzdata file will not update the content. */
+ public void testStageInstallWithErrorCode_missingTzDataFile() throws Exception {
+ byte[] stagedDistroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_SUCCESS,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(stagedDistroBytes)));
+ assertInstallDistroStaged(stagedDistroBytes);
+
+ byte[] incompleteDistroBytes =
+ createValidTimeZoneDistroBuilder(NEWER_RULES_VERSION, 1)
+ .clearTzDataForTests()
+ .buildUnvalidatedBytes();
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(incompleteDistroBytes)));
+ assertInstallDistroStaged(stagedDistroBytes);
+ assertNoInstalledDistro();
+ }
+
+ /** Tests that a distro with a missing ICU file will not update the content. */
+ public void testStageInstallWithErrorCode_missingIcuFile() throws Exception {
+ byte[] stagedDistroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_SUCCESS,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(stagedDistroBytes)));
+ assertInstallDistroStaged(stagedDistroBytes);
+
+ byte[] incompleteDistroBytes =
+ createValidTimeZoneDistroBuilder(NEWER_RULES_VERSION, 1)
+ .clearIcuDataForTests()
+ .buildUnvalidatedBytes();
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(incompleteDistroBytes)));
+ assertInstallDistroStaged(stagedDistroBytes);
+ assertNoInstalledDistro();
+ }
+
+ /** Tests that a distro with a missing tzlookup file will not update the content. */
+ public void testStageInstallWithErrorCode_missingTzLookupFile() throws Exception {
+ byte[] stagedDistroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_SUCCESS,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(stagedDistroBytes)));
+ assertInstallDistroStaged(stagedDistroBytes);
+
+ byte[] incompleteDistroBytes =
+ createValidTimeZoneDistroBuilder(NEWER_RULES_VERSION, 1)
+ .setTzLookupXml(null)
+ .buildUnvalidatedBytes();
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(incompleteDistroBytes)));
+ assertInstallDistroStaged(stagedDistroBytes);
+ assertNoInstalledDistro();
+ }
+
+ /** Tests that a distro with a bad tzlookup file will not update the content. */
+ public void testStageInstallWithErrorCode_badTzLookupFile() throws Exception {
+ byte[] stagedDistroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_SUCCESS,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(stagedDistroBytes)));
+ assertInstallDistroStaged(stagedDistroBytes);
+
+ byte[] incompleteDistroBytes =
+ createValidTimeZoneDistroBuilder(NEWER_RULES_VERSION, 1)
+ .setTzLookupXml("<foo />")
+ .buildUnvalidatedBytes();
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_FAIL_VALIDATION_ERROR,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(incompleteDistroBytes)));
+ assertInstallDistroStaged(stagedDistroBytes);
+ assertNoInstalledDistro();
+ }
+
+ /**
+ * Tests that an update will be unpacked even if there is a partial update from a previous run.
+ */
+ public void testStageInstallWithErrorCode_withWorkingDir() throws Exception {
+ File workingDir = installer.getWorkingDir();
+ assertTrue(workingDir.mkdir());
+ createFile(new File(workingDir, "myFile"), new byte[] { 'a' });
+
+ byte[] distroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_SUCCESS,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(distroBytes)));
+ assertInstallDistroStaged(distroBytes);
+ assertNoInstalledDistro();
+ }
+
+ /**
+ * Tests that a distro without a distro version file will be rejected.
+ */
+ public void testStageInstallWithErrorCode_withMissingDistroVersionFile() throws Exception {
+ // Create a distro without a version file.
+ byte[] distroBytes = createValidTimeZoneDistroBuilder(NEW_RULES_VERSION, 1)
+ .clearVersionForTests()
+ .buildUnvalidatedBytes();
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(distroBytes)));
+ assertNoDistroOperationStaged();
+ assertNoInstalledDistro();
+ }
+
+ /**
+ * Tests that a distro with an newer distro version will be rejected.
+ */
+ public void testStageInstallWithErrorCode_withNewerDistroVersion() throws Exception {
+ // Create a distro that will appear to be newer than the one currently supported.
+ byte[] distroBytes = createValidTimeZoneDistroBuilder(NEW_RULES_VERSION, 1)
+ .replaceFormatVersionForTests(2, 1)
+ .buildUnvalidatedBytes();
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_FORMAT_VERSION,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(distroBytes)));
+ assertNoDistroOperationStaged();
+ assertNoInstalledDistro();
+ }
+
+ /**
+ * Tests that a distro with a badly formed distro version will be rejected.
+ */
+ public void testStageInstallWithErrorCode_withBadlyFormedDistroVersion() throws Exception {
+ // Create a distro that has an invalid major distro version. It should be 3 numeric
+ // characters, "." and 3 more numeric characters.
+ DistroVersion validDistroVersion = new DistroVersion(1, 1, NEW_RULES_VERSION, 1);
+ byte[] invalidFormatVersionBytes = validDistroVersion.toBytes();
+ invalidFormatVersionBytes[0] = 'A';
+
+ TimeZoneDistro distro = createTimeZoneDistroWithVersionBytes(invalidFormatVersionBytes);
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
+ installer.stageInstallWithErrorCode(distro));
+ assertNoDistroOperationStaged();
+ assertNoInstalledDistro();
+ }
+
+ /**
+ * Tests that a distro with a badly formed revision will be rejected.
+ */
+ public void testStageInstallWithErrorCode_withBadlyFormedRevision() throws Exception {
+ // Create a distro that has an invalid revision. It should be 3 numeric characters.
+ DistroVersion validDistroVersion = new DistroVersion(1, 1, NEW_RULES_VERSION, 1);
+ byte[] invalidRevisionBytes = validDistroVersion.toBytes();
+ invalidRevisionBytes[invalidRevisionBytes.length - 3] = 'A';
+
+ TimeZoneDistro distro = createTimeZoneDistroWithVersionBytes(invalidRevisionBytes);
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
+ installer.stageInstallWithErrorCode(distro));
+ assertNoDistroOperationStaged();
+ assertNoInstalledDistro();
+ }
+
+ /**
+ * Tests that a distro with a badly formed rules version will be rejected.
+ */
+ public void testStageInstallWithErrorCode_withBadlyFormedRulesVersion() throws Exception {
+ // Create a distro that has an invalid rules version. It should be in the form "2016c".
+ DistroVersion validDistroVersion = new DistroVersion(1, 1, NEW_RULES_VERSION, 1);
+ byte[] invalidRulesVersionBytes = validDistroVersion.toBytes();
+ invalidRulesVersionBytes[invalidRulesVersionBytes.length - 6] = 'B';
+
+ TimeZoneDistro distro = createTimeZoneDistroWithVersionBytes(invalidRulesVersionBytes);
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
+ installer.stageInstallWithErrorCode(distro));
+ assertNoDistroOperationStaged();
+ assertNoInstalledDistro();
+ }
+
+ public void testStageUninstall_noExistingDistro() throws Exception {
+ // To stage an uninstall, there would need to be installed rules.
+ assertFalse(installer.stageUninstall());
+
+ assertNoDistroOperationStaged();
+ assertNoInstalledDistro();
+ }
+
+ public void testStageUninstall_existingStagedDataDistro() throws Exception {
+ // To stage an uninstall, we need to have some installed rules.
+ byte[] installedDistroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
+ simulateInstalledDistro(installedDistroBytes);
+
+ File stagedDataDir = installer.getStagedTzDataDir();
+ assertTrue(stagedDataDir.mkdir());
+
+ assertTrue(installer.stageUninstall());
+ assertDistroUninstallStaged();
+ assertInstalledDistro(installedDistroBytes);
+ }
+
+ public void testStageUninstall_oldDirsAlreadyExists() throws Exception {
+ // To stage an uninstall, we need to have some installed rules.
+ byte[] installedDistroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
+ simulateInstalledDistro(installedDistroBytes);
+
+ File oldStagedDataDir = installer.getOldStagedDataDir();
+ assertTrue(oldStagedDataDir.mkdir());
+
+ File workingDir = installer.getWorkingDir();
+ assertTrue(workingDir.mkdir());
+
+ assertTrue(installer.stageUninstall());
+
+ assertDistroUninstallStaged();
+ assertFalse(workingDir.exists());
+ assertFalse(oldStagedDataDir.exists());
+ assertInstalledDistro(installedDistroBytes);
+ }
+
+ public void testGetSystemRulesVersion() throws Exception {
+ assertEquals(SYSTEM_RULES_VERSION, installer.getSystemRulesVersion());
+ }
+
+ public void testGetInstalledDistroVersion() throws Exception {
+ // Check result when nothing installed.
+ assertNull(installer.getInstalledDistroVersion());
+ assertNoDistroOperationStaged();
+ assertNoInstalledDistro();
+
+ // Now simulate there being an existing install active.
+ byte[] distroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
+ simulateInstalledDistro(distroBytes);
+ assertInstalledDistro(distroBytes);
+
+ // Check result when something installed.
+ assertEquals(new TimeZoneDistro(distroBytes).getDistroVersion(),
+ installer.getInstalledDistroVersion());
+ assertNoDistroOperationStaged();
+ assertInstalledDistro(distroBytes);
+ }
+
+ public void testGetStagedDistroOperation() throws Exception {
+ byte[] distro1Bytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
+ byte[] distro2Bytes = createValidTimeZoneDistroBytes(NEWER_RULES_VERSION, 1);
+
+ // Check result when nothing staged.
+ assertNull(installer.getStagedDistroOperation());
+ assertNoDistroOperationStaged();
+ assertNoInstalledDistro();
+
+ // Check result after unsuccessfully staging an uninstall.
+ // Can't stage an uninstall without an installed distro.
+ assertFalse(installer.stageUninstall());
+ assertNull(installer.getStagedDistroOperation());
+ assertNoDistroOperationStaged();
+ assertNoInstalledDistro();
+
+ // Check result after staging an install.
+ assertEquals(
+ TimeZoneDistroInstaller.INSTALL_SUCCESS,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(distro1Bytes)));
+ StagedDistroOperation expectedStagedInstall =
+ StagedDistroOperation.install(new TimeZoneDistro(distro1Bytes).getDistroVersion());
+ assertEquals(expectedStagedInstall, installer.getStagedDistroOperation());
+ assertInstallDistroStaged(distro1Bytes);
+ assertNoInstalledDistro();
+
+ // Check result after unsuccessfully staging an uninstall (but after removing a staged
+ // install). Can't stage an uninstall without an installed distro.
+ assertFalse(installer.stageUninstall());
+ assertNull(installer.getStagedDistroOperation());
+ assertNoDistroOperationStaged();
+ assertNoInstalledDistro();
+
+ // Now simulate there being an existing install active.
+ simulateInstalledDistro(distro1Bytes);
+ assertInstalledDistro(distro1Bytes);
+
+ // Check state after successfully staging an uninstall.
+ assertTrue(installer.stageUninstall());
+ StagedDistroOperation expectedStagedUninstall = StagedDistroOperation.uninstall();
+ assertEquals(expectedStagedUninstall, installer.getStagedDistroOperation());
+ assertDistroUninstallStaged();
+ assertInstalledDistro(distro1Bytes);
+
+ // Check state after successfully staging an install.
+ assertEquals(TimeZoneDistroInstaller.INSTALL_SUCCESS,
+ installer.stageInstallWithErrorCode(new TimeZoneDistro(distro2Bytes)));
+ StagedDistroOperation expectedStagedInstall2 =
+ StagedDistroOperation.install(new TimeZoneDistro(distro2Bytes).getDistroVersion());
+ assertEquals(expectedStagedInstall2, installer.getStagedDistroOperation());
+ assertInstallDistroStaged(distro2Bytes);
+ assertInstalledDistro(distro1Bytes);
+ }
+
+ private static byte[] createValidTimeZoneDistroBytes(
+ String rulesVersion, int revision) throws Exception {
+ return createValidTimeZoneDistroBuilder(rulesVersion, revision).buildBytes();
+ }
+
+ private static TimeZoneDistroBuilder createValidTimeZoneDistroBuilder(
+ String rulesVersion, int revision) throws Exception {
+
+ byte[] tzData = createTzData(rulesVersion);
+ byte[] icuData = new byte[] { 'a' };
+ String tzlookupXml = "<timezones>\n"
+ + " <countryzones>\n"
+ + " <country code=\"us\">\n"
+ + " <id>America/New_York\"</id>\n"
+ + " <id>America/Los_Angeles</id>\n"
+ + " </country>\n"
+ + " <country code=\"gb\">\n"
+ + " <id>Europe/London</id>\n"
+ + " </country>\n"
+ + " </countryzones>\n"
+ + "</timezones>\n";
+ DistroVersion distroVersion = new DistroVersion(
+ DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
+ DistroVersion.CURRENT_FORMAT_MINOR_VERSION,
+ rulesVersion,
+ revision);
+ return new TimeZoneDistroBuilder()
+ .setDistroVersion(distroVersion)
+ .setTzDataFile(tzData)
+ .setIcuDataFile(icuData)
+ .setTzLookupXml(tzlookupXml);
+ }
+
+ private void assertInstallDistroStaged(byte[] expectedDistroBytes) throws Exception {
+ assertTrue(testInstallDir.exists());
+
+ File stagedTzDataDir = installer.getStagedTzDataDir();
+ assertTrue(stagedTzDataDir.exists());
+
+ File distroVersionFile =
+ new File(stagedTzDataDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
+ assertTrue(distroVersionFile.exists());
+
+ File tzdataFile = new File(stagedTzDataDir, TimeZoneDistro.TZDATA_FILE_NAME);
+ assertTrue(tzdataFile.exists());
+
+ File icuFile = new File(stagedTzDataDir, TimeZoneDistro.ICU_DATA_FILE_NAME);
+ assertTrue(icuFile.exists());
+
+ File tzLookupFile = new File(stagedTzDataDir, TimeZoneDistro.TZLOOKUP_FILE_NAME);
+ assertTrue(tzLookupFile.exists());
+
+ // Assert getStagedDistroState() is reporting correctly.
+ StagedDistroOperation stagedDistroOperation = installer.getStagedDistroOperation();
+ assertNotNull(stagedDistroOperation);
+ assertFalse(stagedDistroOperation.isUninstall);
+ assertEquals(new TimeZoneDistro(expectedDistroBytes).getDistroVersion(),
+ stagedDistroOperation.distroVersion);
+
+ File expectedZipContentDir = createUniqueDirectory(tempDir, "expectedZipContent");
+ new TimeZoneDistro(expectedDistroBytes).extractTo(expectedZipContentDir);
+
+ assertContentsMatches(
+ new File(expectedZipContentDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME),
+ distroVersionFile);
+ assertContentsMatches(
+ new File(expectedZipContentDir, TimeZoneDistro.ICU_DATA_FILE_NAME),
+ icuFile);
+ assertContentsMatches(
+ new File(expectedZipContentDir, TimeZoneDistro.TZDATA_FILE_NAME),
+ tzdataFile);
+ assertContentsMatches(
+ new File(expectedZipContentDir, TimeZoneDistro.TZLOOKUP_FILE_NAME),
+ tzLookupFile);
+ assertFileCount(4, expectedZipContentDir);
+
+ // Also check no working directory is left lying around.
+ File workingDir = installer.getWorkingDir();
+ assertFalse(workingDir.exists());
+ }
+
+ private static void assertFileCount(int expectedFiles, File rootDir) throws Exception {
+ final List<Path> paths = new ArrayList<>();
+ FileVisitor<Path> visitor = new SimpleFileVisitor<Path>() {
+ @Override
+ public FileVisitResult visitFile(Path filePath, BasicFileAttributes attrs)
+ throws IOException {
+ paths.add(filePath);
+ return FileVisitResult.CONTINUE;
+ }
+ };
+ Files.walkFileTree(rootDir.toPath(), visitor);
+ assertEquals("Found: " + paths, expectedFiles, paths.size());
+ }
+
+ private void assertContentsMatches(File expected, File actual) throws IOException {
+ byte[] actualBytes = IoUtils.readFileAsByteArray(actual.getPath());
+ byte[] expectedBytes = IoUtils.readFileAsByteArray(expected.getPath());
+ assertArrayEquals(expectedBytes, actualBytes);
+ }
+
+ private void assertNoDistroOperationStaged() throws Exception {
+ assertNull(installer.getStagedDistroOperation());
+
+ File stagedTzDataDir = installer.getStagedTzDataDir();
+ assertFalse(stagedTzDataDir.exists());
+
+ // Also check no working directories are left lying around.
+ File workingDir = installer.getWorkingDir();
+ assertFalse(workingDir.exists());
+
+ File oldDataDir = installer.getOldStagedDataDir();
+ assertFalse(oldDataDir.exists());
+ }
+
+ private void assertDistroUninstallStaged() throws Exception {
+ assertEquals(StagedDistroOperation.uninstall(), installer.getStagedDistroOperation());
+
+ File stagedTzDataDir = installer.getStagedTzDataDir();
+ assertTrue(stagedTzDataDir.exists());
+ assertTrue(stagedTzDataDir.isDirectory());
+
+ File uninstallTombstone =
+ new File(stagedTzDataDir, TimeZoneDistroInstaller.UNINSTALL_TOMBSTONE_FILE_NAME);
+ assertTrue(uninstallTombstone.exists());
+ assertTrue(uninstallTombstone.isFile());
+
+ // Also check no working directories are left lying around.
+ File workingDir = installer.getWorkingDir();
+ assertFalse(workingDir.exists());
+
+ File oldDataDir = installer.getOldStagedDataDir();
+ assertFalse(oldDataDir.exists());
+ }
+
+ private void simulateInstalledDistro(byte[] distroBytes) throws Exception {
+ File currentTzDataDir = installer.getCurrentTzDataDir();
+ assertFalse(currentTzDataDir.exists());
+ assertTrue(currentTzDataDir.mkdir());
+ new TimeZoneDistro(distroBytes).extractTo(currentTzDataDir);
+ }
+
+ private void assertNoInstalledDistro() {
+ assertFalse(installer.getCurrentTzDataDir().exists());
+ }
+
+ private void assertInstalledDistro(byte[] distroBytes) throws Exception {
+ File currentTzDataDir = installer.getCurrentTzDataDir();
+ assertTrue(currentTzDataDir.exists());
+ File versionFile = new File(currentTzDataDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
+ assertTrue(versionFile.exists());
+ byte[] expectedVersionBytes = new TimeZoneDistro(distroBytes).getDistroVersion().toBytes();
+ byte[] actualVersionBytes = FileUtils.readBytes(versionFile, expectedVersionBytes.length);
+ assertArrayEquals(expectedVersionBytes, actualVersionBytes);
+ }
+
+ private static byte[] createTzData(String rulesVersion) {
+ return new ZoneInfoTestHelper.TzDataBuilder()
+ .initializeToValid()
+ .setHeaderMagic("tzdata" + rulesVersion)
+ .build();
+ }
+
+ private static void createFile(File file, byte[] bytes) {
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ fos.write(bytes);
+ } catch (IOException e) {
+ fail(e.getMessage());
+ }
+ }
+
+ /**
+ * Creates a TimeZoneDistro containing arbitrary bytes in the version file. Used for testing
+ * distros with badly formed version info.
+ */
+ private TimeZoneDistro createTimeZoneDistroWithVersionBytes(byte[] versionBytes)
+ throws Exception {
+
+ // Extract a valid distro to a working dir.
+ byte[] distroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
+ File workingDir = createUniqueDirectory(tempDir, "versionBytes");
+ new TimeZoneDistro(distroBytes).extractTo(workingDir);
+
+ // Modify the version file.
+ File versionFile = new File(workingDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
+ assertTrue(versionFile.exists());
+ try (FileOutputStream fos = new FileOutputStream(versionFile, false /* append */)) {
+ fos.write(versionBytes);
+ }
+
+ // Zip the distro back up again.
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (ZipOutputStream zos = new ZipOutputStream(baos)) {
+ Path workingDirPath = workingDir.toPath();
+ Files.walkFileTree(workingDirPath, new SimpleFileVisitor<Path>() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+ throws IOException {
+ byte[] bytes = IoUtils.readFileAsByteArray(file.toString());
+ String relativeFileName = workingDirPath.relativize(file).toString();
+ addZipEntry(zos, relativeFileName, bytes);
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ }
+
+ return new TimeZoneDistro(baos.toByteArray());
+ }
+
+ private static void addZipEntry(ZipOutputStream zos, String name, byte[] content)
+ throws IOException {
+ ZipEntry zipEntry = new ZipEntry(name);
+ zipEntry.setSize(content.length);
+ zos.putNextEntry(zipEntry);
+ zos.write(content);
+ zos.closeEntry();
+ }
+}