summaryrefslogtreecommitdiffstats
path: root/tzdata
diff options
context:
space:
mode:
Diffstat (limited to 'tzdata')
-rw-r--r--tzdata/Android.mk51
-rwxr-xr-xtzdata/tools/createIcuUpdateResources.sh89
-rwxr-xr-xtzdata/tools/createTzDataBundle.sh25
-rw-r--r--tzdata/tools/src/main/libcore/tzdata/update/tools/CreateTzDataBundle.java127
-rw-r--r--tzdata/tools/src/main/libcore/tzdata/update/tools/TzDataBundleBuilder.java134
-rw-r--r--tzdata/tools/tzupdate.properties14
-rw-r--r--tzdata/update/src/main/libcore/tzdata/update/ConfigBundle.java121
-rw-r--r--tzdata/update/src/main/libcore/tzdata/update/FileUtils.java203
-rw-r--r--tzdata/update/src/main/libcore/tzdata/update/TzDataBundleInstaller.java141
-rw-r--r--tzdata/update/src/test/libcore/tzdata/update/ConfigBundleTest.java151
-rw-r--r--tzdata/update/src/test/libcore/tzdata/update/FileUtilsTest.java324
-rw-r--r--tzdata/update/src/test/libcore/tzdata/update/TzDataBundleInstallerTest.java210
-rw-r--r--tzdata/update_test_app/Android.mk25
-rw-r--r--tzdata/update_test_app/AndroidManifest.xml35
-rw-r--r--tzdata/update_test_app/res/drawable/ic_launcher.pngbin0 -> 9397 bytes
-rw-r--r--tzdata/update_test_app/res/layout/activity_main.xml106
-rw-r--r--tzdata/update_test_app/res/values/strings.xml13
-rw-r--r--tzdata/update_test_app/res/xml/filepaths.xml4
-rw-r--r--tzdata/update_test_app/src/libcore/tzdata/update_test_app/installupdatetestapp/MainActivity.java271
19 files changed, 2044 insertions, 0 deletions
diff --git a/tzdata/Android.mk b/tzdata/Android.mk
new file mode 100644
index 0000000..9da8832
--- /dev/null
+++ b/tzdata/Android.mk
@@ -0,0 +1,51 @@
+# 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)
+
+# Subprojects with separate makefiles
+subdirs := update_test_app
+subdir_makefiles := $(call all-named-subdir-makefiles,$(subdirs))
+
+# Library of tools classes for tzdata updates. Not required on device, except in tests.
+include $(CLEAR_VARS)
+LOCAL_MODULE := tzdata_tools
+LOCAL_MODULE_TAGS := optional
+LOCAL_SRC_FILES := $(call all-java-files-under, tools/src/main)
+LOCAL_JAVACFLAGS := -encoding UTF-8
+LOCAL_STATIC_JAVA_LIBRARIES := tzdata_update
+LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# Library of support classes for tzdata updates. Shared between update generation and
+# on-device code.
+include $(CLEAR_VARS)
+LOCAL_MODULE := tzdata_update
+LOCAL_MODULE_TAGS := optional
+LOCAL_SRC_FILES := $(call all-java-files-under, update/src/main)
+LOCAL_JAVACFLAGS := -encoding UTF-8
+LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# Tests for tzdata_update code
+include $(CLEAR_VARS)
+LOCAL_MODULE := tzdata_update-tests
+LOCAL_MODULE_TAGS := optional
+LOCAL_SRC_FILES := $(call all-java-files-under, update/src/test)
+LOCAL_JAVACFLAGS := -encoding UTF-8
+LOCAL_STATIC_JAVA_LIBRARIES := tzdata_update tzdata_tools
+LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+include $(subdir_makefiles)
diff --git a/tzdata/tools/createIcuUpdateResources.sh b/tzdata/tools/createIcuUpdateResources.sh
new file mode 100755
index 0000000..2db7132
--- /dev/null
+++ b/tzdata/tools/createIcuUpdateResources.sh
@@ -0,0 +1,89 @@
+#!/bin/bash
+#
+# A script that generates an ICU data file containing just timezone rules data.
+# The file can be used to provide time zone rules updates for compatible
+# devices. Note: Only the rules are contained and new timezones will not have
+# the translations.
+#
+# Usage:
+# ./createIcuUpdateResources.sh <tzdata tar.gz file> <ICU version>
+#
+# e.g.
+# ./createIcuUpdateResources.sh ~/Downloads/tzdata2015b.tar.gz 55
+#
+# After execution the file is generated.
+
+if (( $# != 2 )); then
+ echo "Missing arguments"
+ echo "Usage:"
+ echo "./createIcuUpdateResources.sh <tzdata tar.gz file> <ICU version>"
+ exit 1
+fi
+
+if [[ -z "${ANDROID_BUILD_TOP}" ]]; then
+ echo "Configure your environment with build/envsetup.sh and lunch"
+ exit 1
+fi
+
+TZ_DATA_FILE=$1
+ICU_VERSION=$2
+
+if [[ ! -f ${TZ_DATA_FILE} ]]; then
+ echo "${TZ_DATA_FILE} not found"
+ exit 1
+fi
+
+# Keep track of the original working dir. Must be the "tools" dir.
+START_DIR=`pwd`
+ICU_DIR=${ANDROID_BUILD_TOP}/external/icu/icu4c/source
+BUILD_DIR=${START_DIR}/icu_build
+
+# Fail if anything below fails
+set -e
+
+rm -rf ${BUILD_DIR}
+mkdir -p ${BUILD_DIR}
+cd ${BUILD_DIR}
+
+# Configure the build
+${ICU_DIR}/runConfigureICU Linux
+mkdir -p ${BUILD_DIR}/bin
+cd ${BUILD_DIR}/tools/tzcode
+ln -s ${ICU_DIR}/tools/tzcode/icuregions ./icuregions
+ln -s ${ICU_DIR}/tools/tzcode/icuzones ./icuzones
+cp ${TZ_DATA_FILE} .
+
+# Make the tools
+make
+
+# Then make the whole thing
+cd ${BUILD_DIR}
+make -j32
+
+# Generate the tzdata.lst file used to configure which files are included.
+ICU_LIB_DIR=${BUILD_DIR}/lib
+BIN_DIR=${BUILD_DIR}/bin
+TZ_FILES=tzdata.lst
+
+echo metaZones.res > ${TZ_FILES}
+echo timezoneTypes.res >> ${TZ_FILES}
+echo windowsZones.res >> ${TZ_FILES}
+echo zoneinfo64.res >> ${TZ_FILES}
+
+# Copy all the .res files we need here a from, e.g. ./data/out/build/icudt55l
+RES_DIR=data/out/build/icudt${ICU_VERSION}l
+cp ${RES_DIR}/metaZones.res ${BUILD_DIR}
+cp ${RES_DIR}/timezoneTypes.res ${BUILD_DIR}
+cp ${RES_DIR}/windowsZones.res ${BUILD_DIR}
+cp ${RES_DIR}/zoneinfo64.res ${BUILD_DIR}
+
+# This is the package name required for the .dat file to be accepted by ICU.
+# This also affects the generated file name.
+ICU_PACKAGE=icudt${ICU_VERSION}l
+
+# Create the file
+LD_LIBRARY_PATH=${ICU_LIB_DIR} ${BIN_DIR}/pkgdata -F -m common -v -T . -d . -p ${ICU_PACKAGE} ${TZ_FILES}
+cp ${ICU_PACKAGE}.dat ${START_DIR}/icu_tzdata.dat
+
+# Copy the file to the original working dir.
+echo File can be found here: ${START_DIR}/icu_tzdata.dat
diff --git a/tzdata/tools/createTzDataBundle.sh b/tzdata/tools/createTzDataBundle.sh
new file mode 100755
index 0000000..05646fc
--- /dev/null
+++ b/tzdata/tools/createTzDataBundle.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# A script to generate TZ data updates.
+#
+# Usage: ./createTzDataBundle.sh <tzupdate.properties file> <output file>
+# See libcore.tzdata.update.tools.CreateTzDataBundle for more information.
+
+TOOLS_DIR=src/main/libcore/tzdata/update/tools
+UPDATE_DIR=../update/src/main/libcore/tzdata/update
+GEN_DIR=./gen
+
+# Fail if anything below fails
+set -e
+
+rm -rf ${GEN_DIR}
+mkdir -p ${GEN_DIR}
+
+javac \
+ ${TOOLS_DIR}/CreateTzDataBundle.java \
+ ${TOOLS_DIR}/TzDataBundleBuilder.java \
+ ${UPDATE_DIR}/ConfigBundle.java \
+ ${UPDATE_DIR}/FileUtils.java \
+ -d ${GEN_DIR}
+
+java -cp ${GEN_DIR} libcore.tzdata.update.tools.CreateTzDataBundle $@
diff --git a/tzdata/tools/src/main/libcore/tzdata/update/tools/CreateTzDataBundle.java b/tzdata/tools/src/main/libcore/tzdata/update/tools/CreateTzDataBundle.java
new file mode 100644
index 0000000..cdb004a
--- /dev/null
+++ b/tzdata/tools/src/main/libcore/tzdata/update/tools/CreateTzDataBundle.java
@@ -0,0 +1,127 @@
+/*
+ * 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 libcore.tzdata.update.tools;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.util.Properties;
+import libcore.tzdata.update.ConfigBundle;
+import libcore.tzdata.update.FileUtils;
+
+/**
+ * A command-line tool for creating a TZ data update bundle.
+ *
+ * Args:
+ * tzdata.properties file - the file describing the bundle (see template file in tzdata/tools)
+ * output file - the name of the file to be generated
+ */
+public class CreateTzDataBundle {
+
+ private CreateTzDataBundle() {}
+
+ public static void main(String[] args) throws Exception {
+ if (args.length != 2) {
+ printUsage();
+ System.exit(1);
+ }
+ File f = new File(args[0]);
+ if (!f.exists()) {
+ System.err.println("Properties file " + f + " not found");
+ printUsage();
+ System.exit(2);
+ }
+ Properties p = loadProperties(f);
+ TzDataBundleBuilder builder = new TzDataBundleBuilder()
+ .setTzDataVersion(getMandatoryProperty(p, "tzdata.version"))
+ .addBionicTzData(getMandatoryPropertyFile(p, "bionic.file"))
+ .addIcuTzData(getMandatoryPropertyFile(p, "icu.file"));
+
+ int i = 1;
+ while (true) {
+ String localFileNameProperty = "checksum.file.local." + i;
+ String localFileName = p.getProperty(localFileNameProperty);
+ String onDeviceFileNameProperty = "checksum.file.ondevice." + i;
+ String onDeviceFileName = p.getProperty(onDeviceFileNameProperty);
+ boolean foundLocalFileNameProperty = localFileName != null;
+ boolean foundOnDeviceFileNameProperty = onDeviceFileName != null;
+ if (!foundLocalFileNameProperty && !foundOnDeviceFileNameProperty) {
+ break;
+ } else if (foundLocalFileNameProperty != foundOnDeviceFileNameProperty) {
+ System.out.println("Properties file must specify both, or neither of: "
+ + localFileNameProperty + " and " + onDeviceFileNameProperty);
+ System.exit(5);
+ }
+
+ long checksum = FileUtils.calculateChecksum(new File(localFileName));
+ builder.addChecksum(onDeviceFileName, checksum);
+ i++;
+ }
+ if (i == 1) {
+ // For safety we enforce >= 1 checksum entry. The installer does not require it.
+ System.out.println("There must be at least one checksum file");
+ System.exit(6);
+ }
+ System.out.println("Update contains checksums for " + (i-1) + " files");
+
+ ConfigBundle bundle = builder.build();
+ File outputFile = new File(args[1]);
+ try (OutputStream os = new FileOutputStream(outputFile)) {
+ os.write(bundle.getBundleBytes());
+ }
+ System.out.println("Wrote: " + outputFile);
+ }
+
+ private static File getMandatoryPropertyFile(Properties p, String propertyName) {
+ String fileName = getMandatoryProperty(p, propertyName);
+ File file = new File(fileName);
+ if (!file.exists()) {
+ System.out.println(
+ "Missing file: " + file + " for property " + propertyName + " does not exist.");
+ printUsage();
+ System.exit(4);
+ }
+ return file;
+ }
+
+ private static String getMandatoryProperty(Properties p, String propertyName) {
+ String value = p.getProperty(propertyName);
+ if (value == null) {
+ System.out.println("Missing property: " + propertyName);
+ printUsage();
+ System.exit(3);
+ }
+ return value;
+ }
+
+ private static Properties loadProperties(File f) throws IOException {
+ Properties p = new Properties();
+ try (Reader reader = new InputStreamReader(new FileInputStream(f))) {
+ p.load(reader);
+ }
+ return p;
+ }
+
+ private static void printUsage() {
+ System.out.println("Usage:");
+ System.out.println("\t" + CreateTzDataBundle.class.getName() +
+ " <tzupdate.properties file> <output file>");
+ }
+}
diff --git a/tzdata/tools/src/main/libcore/tzdata/update/tools/TzDataBundleBuilder.java b/tzdata/tools/src/main/libcore/tzdata/update/tools/TzDataBundleBuilder.java
new file mode 100644
index 0000000..3550c6f
--- /dev/null
+++ b/tzdata/tools/src/main/libcore/tzdata/update/tools/TzDataBundleBuilder.java
@@ -0,0 +1,134 @@
+/*
+ * 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 libcore.tzdata.update.tools;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import libcore.tzdata.update.ConfigBundle;
+
+/**
+ * A class for creating a {@link ConfigBundle} containing timezone update data.
+ */
+public final class TzDataBundleBuilder {
+
+ private String tzDataVersion;
+ private StringBuilder checksumsFileContent = new StringBuilder();
+ private File zoneInfoFile;
+ private File icuTzDataFile;
+
+ public TzDataBundleBuilder setTzDataVersion(String tzDataVersion) {
+ this.tzDataVersion = tzDataVersion;
+ return this;
+ }
+
+ public TzDataBundleBuilder addChecksum(String fileName, long checksum) {
+ checksumsFileContent.append(Long.toString(checksum))
+ .append(',')
+ .append(fileName)
+ .append('\n');
+ return this;
+ }
+
+ public TzDataBundleBuilder addBionicTzData(File zoneInfoFile) {
+ this.zoneInfoFile = zoneInfoFile;
+ return this;
+ }
+
+ public TzDataBundleBuilder addIcuTzData(File icuTzDataFile) {
+ this.icuTzDataFile = icuTzDataFile;
+ return this;
+ }
+
+ /**
+ * Builds a {@link libcore.tzdata.update.ConfigBundle}.
+ */
+ public ConfigBundle build() throws IOException {
+ if (tzDataVersion == null) {
+ throw new IllegalStateException("Missing tzDataVersion");
+ }
+ if (zoneInfoFile == null) {
+ throw new IllegalStateException("Missing zoneInfo file");
+ }
+
+ return buildUnvalidated();
+ }
+
+ // For use in tests.
+ public TzDataBundleBuilder clearChecksumEntries() {
+ checksumsFileContent.setLength(0);
+ return this;
+ }
+
+ // For use in tests.
+ public TzDataBundleBuilder clearBionicTzData() {
+ this.zoneInfoFile = null;
+ return this;
+ }
+
+ /**
+ * For use in tests. Use {@link #build()}.
+ */
+ public ConfigBundle buildUnvalidated() throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (ZipOutputStream zos = new ZipOutputStream(baos)) {
+ addZipEntry(zos, ConfigBundle.CHECKSUMS_FILE_NAME,
+ checksumsFileContent.toString().getBytes(StandardCharsets.UTF_8));
+ if (tzDataVersion != null) {
+ addZipEntry(zos, ConfigBundle.TZ_DATA_VERSION_FILE_NAME,
+ tzDataVersion.getBytes(StandardCharsets.UTF_8));
+ }
+ if (zoneInfoFile != null) {
+ addZipEntry(zos, ConfigBundle.ZONEINFO_FILE_NAME,
+ readFileAsByteArray(zoneInfoFile));
+ }
+ if (icuTzDataFile != null) {
+ addZipEntry(zos, ConfigBundle.ICU_DATA_FILE_NAME,
+ readFileAsByteArray(icuTzDataFile));
+ }
+ }
+ return new ConfigBundle(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();
+ }
+
+ /**
+ * Returns the contents of 'path' as a byte array.
+ */
+ public static byte[] readFileAsByteArray(File file) throws IOException {
+ byte[] buffer = new byte[8192];
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (FileInputStream fis = new FileInputStream(file)) {
+ int count;
+ while ((count = fis.read(buffer)) != -1) {
+ baos.write(buffer, 0, count);
+ }
+ }
+ return baos.toByteArray();
+ }
+}
+
diff --git a/tzdata/tools/tzupdate.properties b/tzdata/tools/tzupdate.properties
new file mode 100644
index 0000000..e3fe002
--- /dev/null
+++ b/tzdata/tools/tzupdate.properties
@@ -0,0 +1,14 @@
+# Edit these to reflect the update files.
+
+# This should be the tzdata version. e.g. "2015a". Lexicographical sort order
+# may become important in future so if inventing interim releases only add
+# characters to the end.
+tzdata.version=
+bionic.file=
+icu.file=
+
+# Edit these as required to point to the file expected to exist on the device.
+checksum.file.local.1=../../../bionic/libc/zoneinfo/tzdata
+checksum.file.ondevice.1=/system/usr/share/zoneinfo/tzdata
+checksum.file.local.2=../../../external/icu/icu4c/source/stubdata/icudt55l.dat
+checksum.file.ondevice.2=/system/usr/icu/icudt55l.dat
diff --git a/tzdata/update/src/main/libcore/tzdata/update/ConfigBundle.java b/tzdata/update/src/main/libcore/tzdata/update/ConfigBundle.java
new file mode 100644
index 0000000..6e2ff9d
--- /dev/null
+++ b/tzdata/update/src/main/libcore/tzdata/update/ConfigBundle.java
@@ -0,0 +1,121 @@
+/*
+ * 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 libcore.tzdata.update;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+/**
+ * A configuration bundle. This is a thin wrapper around some in-memory bytes representing a zip
+ * archive and logic for its safe extraction.
+ */
+public final class ConfigBundle {
+
+ /** The name of the file inside the bundle containing the TZ data version. */
+ public static final String TZ_DATA_VERSION_FILE_NAME = "tzdata_version";
+
+ /** The name of the file inside the bundle containing the expected device checksums. */
+ public static final String CHECKSUMS_FILE_NAME = "checksums";
+
+ /** The name of the file inside the bundle containing bionic/libcore TZ data. */
+ public static final String ZONEINFO_FILE_NAME = "tzdata";
+
+ /** The name of the file inside the bundle containing ICU TZ data. */
+ public static final String ICU_DATA_FILE_NAME = "icu/icu_tzdata.dat";
+
+ private static final int BUFFER_SIZE = 8192;
+
+ private final byte[] bytes;
+
+ public ConfigBundle(byte[] bytes) {
+ this.bytes = bytes;
+ }
+
+ public byte[] getBundleBytes() {
+ return bytes;
+ }
+
+ public void extractTo(File targetDir) throws IOException {
+ extractZipSafely(new ByteArrayInputStream(bytes), targetDir, true /* makeWorldReadable */);
+ }
+
+ /** Visible for testing */
+ static void extractZipSafely(InputStream is, File targetDir, boolean makeWorldReadable)
+ throws IOException {
+
+ // Create the extraction dir, if needed.
+ FileUtils.ensureDirectoriesExist(targetDir, makeWorldReadable);
+
+ try (ZipInputStream zipInputStream = new ZipInputStream(is)) {
+ byte[] buffer = new byte[BUFFER_SIZE];
+ ZipEntry entry;
+ while ((entry = zipInputStream.getNextEntry()) != null) {
+ // Validate the entry name: make sure the unpacked file will exist beneath the
+ // targetDir.
+ String name = entry.getName();
+ File entryFile = FileUtils.createSubFile(targetDir, name);
+
+ if (entry.isDirectory()) {
+ FileUtils.ensureDirectoriesExist(entryFile, makeWorldReadable);
+ } else {
+ // Create the path if there was no directory entry.
+ if (!entryFile.getParentFile().exists()) {
+ FileUtils.ensureDirectoriesExist(
+ entryFile.getParentFile(), makeWorldReadable);
+ }
+
+ try (FileOutputStream fos = new FileOutputStream(entryFile)) {
+ int count;
+ while ((count = zipInputStream.read(buffer)) != -1) {
+ fos.write(buffer, 0, count);
+ }
+ // sync to disk
+ fos.getFD().sync();
+ }
+ // mark entryFile -rw-r--r--
+ if (makeWorldReadable) {
+ FileUtils.makeWorldReadable(entryFile);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ ConfigBundle that = (ConfigBundle) o;
+
+ if (!Arrays.equals(bytes, that.bytes)) {
+ return false;
+ }
+
+ return true;
+ }
+
+}
diff --git a/tzdata/update/src/main/libcore/tzdata/update/FileUtils.java b/tzdata/update/src/main/libcore/tzdata/update/FileUtils.java
new file mode 100644
index 0000000..8b7da78
--- /dev/null
+++ b/tzdata/update/src/main/libcore/tzdata/update/FileUtils.java
@@ -0,0 +1,203 @@
+/*
+ * 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 libcore.tzdata.update;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.zip.CRC32;
+
+/**
+ * Utility methods for files operations.
+ */
+public final class FileUtils {
+
+ private FileUtils() {
+ }
+
+ /**
+ * Creates a new {@link java.io.File} from the {@code parentDir} and {@code name}, but only if
+ * the
+ * resulting file would exist beneath {@code parentDir}. Useful if {@code name} could contain
+ * "/../" or symlinks. The returned object has an absolute path.
+ *
+ * @throws java.io.IOException
+ * if the file would not exist beneath {@code parentDir}
+ */
+ public static File createSubFile(File parentDir, String name) throws IOException {
+ // The subFile must exist beneath parentDir. If name contains "/../" this may not be the
+ // case so we check.
+ File subFile = canonicalizeDirPath(new File(parentDir, name));
+ if (!subFile.getPath().startsWith(parentDir.getCanonicalPath())) {
+ throw new IOException(name + " must exist beneath " + parentDir);
+ }
+ return subFile;
+ }
+
+ /**
+ * Makes sure a directory exists. If it doesn't exist, it is created. Parent directories are
+ * also created as needed. If {@code makeWorldReadable} is {@code true} the directory's default
+ * permissions will be set. Even when {@code makeWorldReadable} is {@code true}, only
+ * directories explicitly created will have their permissions set; existing directories are
+ * untouched.
+ *
+ * @throws IOException
+ * if the directory or one of its parents did not already exist and could not be created
+ */
+ public static void ensureDirectoriesExist(File dir, boolean makeWorldReadable)
+ throws IOException {
+ LinkedList<File> dirs = new LinkedList<>();
+ File currentDir = dir;
+ do {
+ dirs.addFirst(currentDir);
+ currentDir = currentDir.getParentFile();
+ } while (currentDir != null);
+
+ for (File dirToCheck : dirs) {
+ if (!dirToCheck.exists()) {
+ if (!dirToCheck.mkdir()) {
+ throw new IOException("Unable to create directory: " + dir);
+ }
+ if (makeWorldReadable) {
+ makeDirectoryWorldAccessible(dirToCheck);
+ }
+ } else if (!dirToCheck.isDirectory()) {
+ throw new IOException(dirToCheck + " exists but is not a directory");
+ }
+ }
+ }
+
+ /**
+ * Returns a file with all symlinks and relative paths such as "/../" resolved <em>except</em>
+ * for the base name (the last element of the path). Useful for detecting symlinks.
+ */
+ public static File canonicalizeDirPath(File file) throws IOException {
+ return new File(file.getParentFile().getCanonicalFile(), file.getName());
+ }
+
+ public static void makeDirectoryWorldAccessible(File directory) throws IOException {
+ if (!directory.isDirectory()) {
+ throw new IOException(directory + " must be a directory");
+ }
+ makeWorldReadable(directory);
+ if (!directory.setExecutable(true, false /* ownerOnly */)) {
+ throw new IOException("Unable to make " + directory + " world-executable");
+ }
+ }
+
+ public static void makeWorldReadable(File file) throws IOException {
+ if (!file.setReadable(true, false /* ownerOnly */)) {
+ throw new IOException("Unable to make " + file + " world-readable");
+ }
+ }
+
+ /**
+ * Calculates the checksum from the contents of a file.
+ */
+ public static long calculateChecksum(File file) throws IOException {
+ final int BUFFER_SIZE = 8196;
+ CRC32 crc32 = new CRC32();
+ try (FileInputStream fis = new FileInputStream(file)) {
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int count;
+ while ((count = fis.read(buffer)) != -1) {
+ crc32.update(buffer, 0, count);
+ }
+ }
+ return crc32.getValue();
+ }
+
+ public static void rename(File from, File to) throws IOException {
+ ensureFileDoesNotExist(to);
+ if (!from.renameTo(to)) {
+ throw new IOException("Unable to rename " + from + " to " + to);
+ }
+ }
+
+ public static void ensureFileDoesNotExist(File file) throws IOException {
+ if (file.exists()) {
+ if (!file.isFile()) {
+ throw new IOException(file + " is not a file");
+ }
+ doDelete(file);
+ }
+ }
+
+ public static void doDelete(File file) throws IOException {
+ if (!file.delete()) {
+ throw new IOException("Unable to delete: " + file);
+ }
+ }
+
+ public static boolean isSymlink(File file) throws IOException {
+ return !file.getCanonicalPath().equals(canonicalizeDirPath(file).getPath());
+ }
+
+ public static void deleteRecursive(File toDelete) throws IOException {
+ if (toDelete.isDirectory()) {
+ for (File file : toDelete.listFiles()) {
+ if (file.isDirectory() && !FileUtils.isSymlink(file)) {
+ // The isSymlink() check is important so that we don't delete files in other
+ // directories: only the symlink itself.
+ deleteRecursive(file);
+ } else {
+ // Delete symlinks to directories or files.
+ FileUtils.doDelete(file);
+ }
+ }
+ String[] remainingFiles = toDelete.list();
+ if (remainingFiles.length != 0) {
+ throw new IOException("Unable to delete files: " + Arrays
+ .toString(remainingFiles));
+ }
+ }
+ FileUtils.doDelete(toDelete);
+ }
+
+ public static boolean filesExist(File rootDir, String... fileNames) throws IOException {
+ for (String fileName : fileNames) {
+ File file = new File(rootDir, fileName);
+ if (!file.exists()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Read all lines from a UTF-8 encoded file, returning them as a list of strings.
+ */
+ public static List<String> readLines(File file) throws IOException {
+ FileInputStream in = new FileInputStream(file);
+ try (BufferedReader fileReader = new BufferedReader(
+ new InputStreamReader(in, StandardCharsets.UTF_8));
+ ) {
+ List<String> lines = new ArrayList<>();
+ String line;
+ while ((line = fileReader.readLine()) != null) {
+ lines.add(line);
+ }
+ return lines;
+ }
+ }
+}
diff --git a/tzdata/update/src/main/libcore/tzdata/update/TzDataBundleInstaller.java b/tzdata/update/src/main/libcore/tzdata/update/TzDataBundleInstaller.java
new file mode 100644
index 0000000..df0b2a7
--- /dev/null
+++ b/tzdata/update/src/main/libcore/tzdata/update/TzDataBundleInstaller.java
@@ -0,0 +1,141 @@
+/*
+ * 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 libcore.tzdata.update;
+
+import android.util.Slog;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * A bundle-validation / extraction class. Separate from the services code that uses it for easier
+ * testing.
+ */
+public final class TzDataBundleInstaller {
+
+ static final String CURRENT_TZ_DATA_DIR_NAME = "current";
+ static final String WORKING_DIR_NAME = "working";
+ static final String OLD_TZ_DATA_DIR_NAME = "old";
+
+ private final String logTag;
+ private final File installDir;
+
+ public TzDataBundleInstaller(String logTag, File installDir) {
+ this.logTag = logTag;
+ this.installDir = installDir;
+ }
+
+ /**
+ * Install the supplied content.
+ *
+ * <p>Errors during unpacking or installation will throw an {@link IOException}.
+ * If the content is invalid this method returns {@code false}.
+ * If the installation completed successfully this method returns {@code true}.
+ */
+ public boolean install(byte[] content) throws IOException {
+ File oldTzDataDir = new File(installDir, OLD_TZ_DATA_DIR_NAME);
+ if (oldTzDataDir.exists()) {
+ FileUtils.deleteRecursive(oldTzDataDir);
+ }
+
+ File currentTzDataDir = new File(installDir, CURRENT_TZ_DATA_DIR_NAME);
+ File workingDir = new File(installDir, WORKING_DIR_NAME);
+
+ Slog.i(logTag, "Applying time zone update");
+ File unpackedContentDir = unpackBundle(content, workingDir);
+ try {
+ if (!checkBundleFilesExist(unpackedContentDir)) {
+ Slog.i(logTag, "Update not applied: Bundle is missing files");
+ return false;
+ }
+
+ if (verifySystemChecksums(unpackedContentDir)) {
+ FileUtils.makeDirectoryWorldAccessible(unpackedContentDir);
+
+ if (currentTzDataDir.exists()) {
+ Slog.i(logTag, "Moving " + currentTzDataDir + " to " + oldTzDataDir);
+ FileUtils.rename(currentTzDataDir, oldTzDataDir);
+ }
+ Slog.i(logTag, "Moving " + unpackedContentDir + " to " + currentTzDataDir);
+ FileUtils.rename(unpackedContentDir, currentTzDataDir);
+ Slog.i(logTag, "Update applied: " + currentTzDataDir + " successfully created");
+ return true;
+ }
+ Slog.i(logTag, "Update not applied: System checksum did not match");
+ return false;
+ } finally {
+ deleteBestEffort(oldTzDataDir);
+ deleteBestEffort(unpackedContentDir);
+ }
+ }
+
+ private void deleteBestEffort(File dir) {
+ if (dir.exists()) {
+ try {
+ FileUtils.deleteRecursive(dir);
+ } catch (IOException e) {
+ // Logged but otherwise ignored.
+ Slog.w(logTag, "Unable to delete " + dir, e);
+ }
+ }
+ }
+
+ private File unpackBundle(byte[] content, File targetDir) throws IOException {
+ Slog.i(logTag, "Unpacking update content to: " + targetDir);
+ ConfigBundle bundle = new ConfigBundle(content);
+ bundle.extractTo(targetDir);
+ return targetDir;
+ }
+
+ private boolean checkBundleFilesExist(File unpackedContentDir) throws IOException {
+ Slog.i(logTag, "Verifying bundle contents");
+ return FileUtils.filesExist(unpackedContentDir,
+ ConfigBundle.TZ_DATA_VERSION_FILE_NAME,
+ ConfigBundle.CHECKSUMS_FILE_NAME,
+ ConfigBundle.ZONEINFO_FILE_NAME,
+ ConfigBundle.ICU_DATA_FILE_NAME);
+ }
+
+ private boolean verifySystemChecksums(File unpackedContentDir) throws IOException {
+ Slog.i(logTag, "Verifying system file checksums");
+ File checksumsFile = new File(unpackedContentDir, ConfigBundle.CHECKSUMS_FILE_NAME);
+ for (String line : FileUtils.readLines(checksumsFile)) {
+ int delimiterPos = line.indexOf(',');
+ if (delimiterPos <= 0 || delimiterPos == line.length() - 1) {
+ throw new IOException("Bad checksum entry: " + line);
+ }
+ long expectedChecksum;
+ try {
+ expectedChecksum = Long.parseLong(line.substring(0, delimiterPos));
+ } catch (NumberFormatException e) {
+ throw new IOException("Invalid checksum value: " + line);
+ }
+ String filePath = line.substring(delimiterPos + 1);
+ File file = new File(filePath);
+ if (!file.exists()) {
+ Slog.i(logTag, "Failed checksum test for file: " + file + ": file not found");
+ return false;
+ }
+ long actualChecksum = FileUtils.calculateChecksum(file);
+ if (actualChecksum != expectedChecksum) {
+ Slog.i(logTag, "Failed checksum test for file: " + file
+ + ": required=" + expectedChecksum + ", actual=" + actualChecksum);
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/tzdata/update/src/test/libcore/tzdata/update/ConfigBundleTest.java b/tzdata/update/src/test/libcore/tzdata/update/ConfigBundleTest.java
new file mode 100644
index 0000000..f1325e7
--- /dev/null
+++ b/tzdata/update/src/test/libcore/tzdata/update/ConfigBundleTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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 libcore.tzdata.update;
+
+import junit.framework.TestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import libcore.io.IoUtils;
+
+/**
+ * Tests for {@link ConfigBundle}.
+ */
+public class ConfigBundleTest extends TestCase {
+
+ private final List<File> testFiles = new ArrayList<>();
+
+ @Override
+ public void tearDown() throws Exception {
+ // Delete files / directories in reverse order.
+ Collections.reverse(testFiles);
+ for (File tempFile : testFiles) {
+ tempFile.delete();
+ }
+ super.tearDown();
+ }
+
+ public void testExtractZipSafely_goodZip() throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (ZipOutputStream zipOutputStream = new ZipOutputStream(baos)) {
+ addZipEntry(zipOutputStream, "/leadingSlash");
+ addZipEntry(zipOutputStream, "absolute");
+ addZipEntry(zipOutputStream, "subDir/../file");
+ addZipEntry(zipOutputStream, "subDir/subDir/subDir/file");
+ addZipEntry(zipOutputStream, "subDir/subDir2/"); // Directory entry
+ addZipEntry(zipOutputStream, "subDir/../subDir3/"); // Directory entry
+ }
+ File dir = createTempDir();
+ File targetDir = new File(dir, "target");
+ TestInputStream inputStream =
+ new TestInputStream(new ByteArrayInputStream(baos.toByteArray()));
+ ConfigBundle.extractZipSafely(inputStream, targetDir, true /* makeWorldReadable */);
+ inputStream.assertClosed();
+ assertFilesExist(
+ new File(targetDir, "leadingSlash"),
+ new File(targetDir, "absolute"),
+ new File(targetDir, "file"),
+ new File(targetDir, "subDir/subDir/subDir/file"));
+ assertDirsExist(
+ new File(targetDir, "subDir/subDir2"),
+ new File(targetDir, "subDir3"));
+ }
+
+ public void testExtractZipSafely_badZip_fileOutsideTarget() throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (ZipOutputStream zipOutputStream = new ZipOutputStream(baos)) {
+ addZipEntry(zipOutputStream, "../one");
+ }
+ doExtractZipFails(baos);
+ }
+
+ public void testExtractZipSafely_badZip_dirOutsideTarget() throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (ZipOutputStream zipOutputStream = new ZipOutputStream(baos)) {
+ addZipEntry(zipOutputStream, "../one/");
+ }
+ doExtractZipFails(baos);
+ }
+
+ private void doExtractZipFails(ByteArrayOutputStream baos) {
+ File dir = createTempDir();
+ File targetDir = new File(dir, "target");
+ TestInputStream inputStream = new TestInputStream(
+ new ByteArrayInputStream(baos.toByteArray()));
+ try {
+ ConfigBundle.extractZipSafely(inputStream, targetDir, true /* makeWorldReadable */);
+ fail();
+ } catch (IOException expected) {
+ }
+ inputStream.assertClosed();
+ }
+
+ private static void addZipEntry(ZipOutputStream zipOutputStream, String name)
+ throws IOException {
+ ZipEntry zipEntry = new ZipEntry(name);
+ zipOutputStream.putNextEntry(zipEntry);
+ if (!zipEntry.isDirectory()) {
+ zipOutputStream.write('a');
+ }
+ }
+
+ private File createTempDir() {
+ final String tempPrefix = getClass().getSimpleName();
+ File tempDir = IoUtils.createTemporaryDirectory(tempPrefix);
+ testFiles.add(tempDir);
+ return tempDir;
+ }
+
+ private static void assertFilesExist(File... files) {
+ for (File f : files) {
+ assertTrue(f + " file expected to exist", f.exists() && f.isFile());
+ }
+ }
+
+ private static void assertDirsExist(File... dirs) {
+ for (File dir : dirs) {
+ assertTrue(dir + " directory expected to exist", dir.exists() && dir.isDirectory());
+ }
+ }
+
+ private static class TestInputStream extends FilterInputStream {
+
+ private boolean closed;
+
+ public TestInputStream(InputStream in) {
+ super(in);
+ }
+
+ @Override
+ public void close() throws IOException {
+ closed = true;
+ super.close();
+ }
+
+ public void assertClosed() {
+ assertTrue(closed);
+ }
+ }
+}
diff --git a/tzdata/update/src/test/libcore/tzdata/update/FileUtilsTest.java b/tzdata/update/src/test/libcore/tzdata/update/FileUtilsTest.java
new file mode 100644
index 0000000..ce02bfe
--- /dev/null
+++ b/tzdata/update/src/test/libcore/tzdata/update/FileUtilsTest.java
@@ -0,0 +1,324 @@
+/*
+ * 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 libcore.tzdata.update;
+
+import junit.framework.TestCase;
+
+import android.system.Os;
+import android.system.OsConstants;
+import android.system.StructStat;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import libcore.io.IoUtils;
+import libcore.io.Libcore;
+
+/**
+ * Tests for {@link FileUtils}.
+ */
+public class FileUtilsTest extends TestCase {
+
+ private List<File> testFiles = new ArrayList<>();
+
+ @Override
+ public void tearDown() throws Exception {
+ // Delete in reverse order
+ Collections.reverse(testFiles);
+ for (File tempFile : testFiles) {
+ tempFile.delete();
+ }
+ super.tearDown();
+ }
+
+ public void testCalculateChecksum() throws Exception {
+ final String content = "Content";
+ File file1 = createTextFile(content);
+ File file2 = createTextFile(content);
+ File file3 = createTextFile(content + "!");
+
+ long file1CheckSum = FileUtils.calculateChecksum(file1);
+ long file2CheckSum = FileUtils.calculateChecksum(file2);
+ long file3Checksum = FileUtils.calculateChecksum(file3);
+
+ assertEquals(file1CheckSum, file2CheckSum);
+ assertTrue(file1CheckSum != file3Checksum);
+ }
+
+ public void testDeleteRecursive() throws Exception {
+ File dir = createTempDir();
+ File file1 = createRegularFile(dir, "file1");
+ File file2 = createRegularFile(dir, "file2");
+ File symLink1 = createSymlink(file1, dir, "symLink1");
+ File subDir = createDir(dir, "subDir");
+ File file3 = createRegularFile(subDir, "subFile1");
+ File file4 = createRegularFile(subDir, "subFile2");
+ File symLink2 = createSymlink(file1, dir, "symLink2");
+
+ File otherDir = createTempDir();
+ File otherFile = createRegularFile(otherDir, "kept");
+
+ File linkToOtherDir = createSymlink(otherDir, subDir, "linkToOtherDir");
+ File linkToOtherFile = createSymlink(otherFile, subDir, "linkToOtherFile");
+
+ File[] filesToDelete = { dir, file1, file2, symLink1, subDir, file3, file4, symLink2,
+ linkToOtherDir, linkToOtherFile };
+ File[] filesToKeep = { otherDir, otherFile };
+ assertFilesExist(filesToDelete);
+ assertFilesExist(filesToKeep);
+
+ FileUtils.deleteRecursive(dir);
+ assertFilesDoNotExist(filesToDelete);
+ assertFilesExist(filesToKeep);
+ }
+
+ public void testIsSymlink() throws Exception {
+ File dir = createTempDir();
+ File subDir = createDir(dir, "subDir");
+ File fileInSubDir = createRegularFile(subDir, "fileInSubDir");
+ File normalFile = createRegularFile(dir, "normalFile");
+ File symlinkToDir = createSymlink(subDir, dir, "symlinkToDir");
+ File symlinkToFile = createSymlink(fileInSubDir, dir, "symlinkToFile");
+ File symlinkToFileInSubDir = createSymlink(fileInSubDir, dir, "symlinkToFileInSubDir");
+ File normalFileViaSymlink = new File(symlinkToDir, "normalFile");
+
+ assertFalse(FileUtils.isSymlink(dir));
+ assertFalse(FileUtils.isSymlink(subDir));
+ assertFalse(FileUtils.isSymlink(fileInSubDir));
+ assertFalse(FileUtils.isSymlink(normalFile));
+ assertTrue(FileUtils.isSymlink(symlinkToDir));
+ assertTrue(FileUtils.isSymlink(symlinkToFile));
+ assertTrue(FileUtils.isSymlink(symlinkToFileInSubDir));
+ assertFalse(FileUtils.isSymlink(normalFileViaSymlink));
+ }
+
+ public void testCreateSubFile() throws Exception {
+ File dir1 = createTempDir();
+ File subFile = FileUtils.createSubFile(dir1, "file");
+ assertFileCanonicalEquals(new File(dir1, "file"), subFile);
+
+ assertCreateSubFileThrows(dir1, "../file");
+ assertCreateSubFileThrows(dir1, "../../file");
+ assertCreateSubFileThrows(dir1, "../otherdir/file");
+
+ File dir2 = createTempDir();
+ File dir2Subdir = createDir(dir2, "dir2Subdir");
+ File expectedSymlinkToDir2 = createSymlink(dir2Subdir, dir1, "symlinkToDir2");
+
+ File actualSymlinkToDir2 = FileUtils.createSubFile(dir1, "symlinkToDir2");
+ assertEquals(expectedSymlinkToDir2, actualSymlinkToDir2);
+
+ assertCreateSubFileThrows(dir1, "symlinkToDir2/fileInSymlinkedDir");
+ }
+
+ public void testEnsureDirectoryExists() throws Exception {
+ File dir = createTempDir();
+
+ File exists = new File(dir, "exists");
+ assertTrue(exists.mkdir());
+ assertTrue(exists.setReadable(true /* readable */, true /* ownerOnly */));
+ assertTrue(exists.setExecutable(true /* readable */, true /* ownerOnly */));
+ FileUtils.ensureDirectoriesExist(exists, true /* makeWorldReadable */);
+ assertDirExistsAndIsAccessible(exists, false /* requireWorldReadable */);
+
+ File subDir = new File(dir, "subDir");
+ assertFalse(subDir.exists());
+ FileUtils.ensureDirectoriesExist(subDir, true /* makeWorldReadable */);
+ assertDirExistsAndIsAccessible(subDir, true /* requireWorldReadable */);
+
+ File one = new File(dir, "one");
+ File two = new File(one, "two");
+ File three = new File(two, "three");
+ FileUtils.ensureDirectoriesExist(three, true /* makeWorldReadable */);
+ assertDirExistsAndIsAccessible(one, true /* requireWorldReadable */);
+ assertDirExistsAndIsAccessible(two, true /* requireWorldReadable */);
+ assertDirExistsAndIsAccessible(three, true /* requireWorldReadable */);
+ }
+
+ public void testEnsureDirectoriesExist_noPermissions() throws Exception {
+ File dir = createTempDir();
+ assertDirExistsAndIsAccessible(dir, false /* requireWorldReadable */);
+
+ File unreadableSubDir = new File(dir, "unreadableSubDir");
+ assertTrue(unreadableSubDir.mkdir());
+ assertTrue(unreadableSubDir.setReadable(false /* readable */, true /* ownerOnly */));
+ assertTrue(unreadableSubDir.setExecutable(false /* readable */, true /* ownerOnly */));
+
+ File toCreate = new File(unreadableSubDir, "toCreate");
+ try {
+ FileUtils.ensureDirectoriesExist(toCreate, true /* makeWorldReadable */);
+ fail();
+ } catch (IOException expected) {
+ }
+ assertDirExistsAndIsAccessible(dir, false /* requireWorldReadable */);
+ assertFalse(unreadableSubDir.canRead() && unreadableSubDir.canExecute());
+ assertFalse(toCreate.exists());
+ }
+
+ public void testEnsureFileDoesNotExist() throws Exception {
+ File dir = createTempDir();
+
+ FileUtils.ensureFileDoesNotExist(new File(dir, "doesNotExist"));
+
+ File exists1 = createRegularFile(dir, "exists1");
+ assertTrue(exists1.exists());
+ FileUtils.ensureFileDoesNotExist(exists1);
+ assertFalse(exists1.exists());
+
+ exists1 = createRegularFile(dir, "exists1");
+ File symlink = createSymlink(exists1, dir, "symlinkToFile");
+ assertTrue(symlink.exists());
+ FileUtils.ensureFileDoesNotExist(symlink);
+ assertFalse(symlink.exists());
+ assertTrue(exists1.exists());
+
+ // Only files and symlinks supported. We do not delete directories.
+ File emptyDir = createTempDir();
+ try {
+ FileUtils.ensureFileDoesNotExist(emptyDir);
+ fail();
+ } catch (IOException expected) {
+ }
+ assertTrue(emptyDir.exists());
+ }
+
+ // This test does not pass when run as root because root can do anything even if the permissions
+ // don't allow it.
+ public void testEnsureFileDoesNotExist_noPermission() throws Exception {
+ File dir = createTempDir();
+
+ File protectedDir = createDir(dir, "protected");
+ File undeletable = createRegularFile(protectedDir, "undeletable");
+ assertTrue(protectedDir.setWritable(false));
+ assertTrue(undeletable.exists());
+ try {
+ FileUtils.ensureFileDoesNotExist(undeletable);
+ fail();
+ } catch (IOException expected) {
+ } finally {
+ assertTrue(protectedDir.setWritable(true)); // Reset for clean-up
+ }
+ assertTrue(undeletable.exists());
+ }
+
+ public void testCheckFilesExist() throws Exception {
+ File dir = createTempDir();
+ createRegularFile(dir, "exists1");
+ File subDir = createDir(dir, "subDir");
+ createRegularFile(subDir, "exists2");
+ assertTrue(FileUtils.filesExist(dir, "exists1", "subDir/exists2"));
+ assertFalse(FileUtils.filesExist(dir, "doesNotExist"));
+ assertFalse(FileUtils.filesExist(dir, "subDir/doesNotExist"));
+ }
+
+ public void testReadLines() throws Exception {
+ File file = createTextFile("One\nTwo\nThree\n");
+
+ List<String> lines = FileUtils.readLines(file);
+ assertEquals(3, lines.size());
+ assertEquals(lines, Arrays.asList("One", "Two", "Three"));
+ }
+
+ private File createTextFile(String contents) throws IOException {
+ File file = File.createTempFile(getClass().getSimpleName(), ".txt");
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ BufferedWriter writer = new BufferedWriter(
+ new OutputStreamWriter(fos, StandardCharsets.UTF_8));
+ writer.write(contents);
+ writer.close();
+ }
+ return file;
+ }
+
+ private File createSymlink(File file, File symlinkDir, String symlinkName) throws Exception {
+ assertTrue(file.exists());
+
+ File symlink = new File(symlinkDir, symlinkName);
+ Os.symlink(file.getAbsolutePath(), symlink.getAbsolutePath());
+ testFiles.add(symlink);
+ return symlink;
+ }
+
+ private static void assertCreateSubFileThrows(File parentDir, String name) {
+ try {
+ FileUtils.createSubFile(parentDir, name);
+ fail();
+ } catch (IOException expected) {
+ assertTrue(expected.getMessage().contains("must exist beneath"));
+ }
+ }
+
+ private static void assertFilesDoNotExist(File... files) {
+ for (File f : files) {
+ assertFalse(f + " unexpectedly exists", f.exists());
+ }
+ }
+
+ private static void assertFilesExist(File... files) {
+ for (File f : files) {
+ assertTrue(f + " expected to exist", f.exists());
+ }
+ }
+
+ private static void assertDirExistsAndIsAccessible(File dir, boolean requireWorldReadable)
+ throws Exception {
+ assertTrue(dir.exists() && dir.isDirectory() && dir.canRead() && dir.canExecute());
+
+ String path = dir.getCanonicalPath();
+ StructStat sb = Libcore.os.stat(path);
+ int mask = OsConstants.S_IXUSR | OsConstants.S_IRUSR;
+ if (requireWorldReadable) {
+ mask = mask | OsConstants.S_IXGRP | OsConstants.S_IRGRP
+ | OsConstants.S_IXOTH | OsConstants.S_IROTH;
+ }
+ assertTrue("Permission mask required: " + Integer.toOctalString(mask),
+ (sb.st_mode & mask) == mask);
+ }
+
+ private static void assertFileCanonicalEquals(File expected, File actual) throws IOException {
+ assertEquals(expected.getCanonicalFile(), actual.getCanonicalFile());
+ }
+
+ private File createTempDir() {
+ final String tempPrefix = getClass().getSimpleName();
+ File tempDir = IoUtils.createTemporaryDirectory(tempPrefix);
+ testFiles.add(tempDir);
+ return tempDir;
+ }
+
+ private File createDir(File parentDir, String name) {
+ File dir = new File(parentDir, name);
+ assertTrue(dir.mkdir());
+ testFiles.add(dir);
+ return dir;
+ }
+
+ private File createRegularFile(File dir, String name) throws Exception {
+ File file = new File(dir, name);
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ fos.write("Hello".getBytes());
+ }
+ testFiles.add(file);
+ return file;
+ }
+}
diff --git a/tzdata/update/src/test/libcore/tzdata/update/TzDataBundleInstallerTest.java b/tzdata/update/src/test/libcore/tzdata/update/TzDataBundleInstallerTest.java
new file mode 100644
index 0000000..1825bb3
--- /dev/null
+++ b/tzdata/update/src/test/libcore/tzdata/update/TzDataBundleInstallerTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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 libcore.tzdata.update;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import libcore.tzdata.update.tools.TzDataBundleBuilder;
+
+/**
+ * Tests for {@link libcore.tzdata.update.TzDataBundleInstaller}.
+ */
+public class TzDataBundleInstallerTest extends TestCase {
+
+ private static final File SYSTEM_ZONE_INFO_FILE = new File("/system/usr/share/zoneinfo/tzdata");
+
+ private TzDataBundleInstaller installer;
+ private File tempDir;
+ private File testInstallDir;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ tempDir = createDirectory("tempDir");
+ testInstallDir = createDirectory("testInstall");
+ installer = new TzDataBundleInstaller("TzDataBundleInstallerTest", testInstallDir);
+ }
+
+ private static File createDirectory(String prefix) throws IOException {
+ File dir = File.createTempFile(prefix, "");
+ assertTrue(dir.delete());
+ assertTrue(dir.mkdir());
+ return dir;
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ if (testInstallDir.exists()) {
+ FileUtils.deleteRecursive(testInstallDir);
+ }
+ if (tempDir.exists()) {
+ FileUtils.deleteRecursive(tempDir);
+ }
+ super.tearDown();
+ }
+
+ /** Tests the first update on a device */
+ public void testSuccessfulFirstUpdate() throws Exception {
+ ConfigBundle tzData = createValidTzDataBundle("2030a");
+
+ assertTrue(install(tzData));
+ assertTzDataInstalled(tzData);
+ }
+
+ /**
+ * Tests an update on a device when there is a prior update already applied.
+ */
+ public void testSuccessfulFollowOnUpdate() throws Exception {
+ ConfigBundle tzData1 = createValidTzDataBundle("2030a");
+ assertTrue(install(tzData1));
+ assertTzDataInstalled(tzData1);
+
+ ConfigBundle tzData2 = createValidTzDataBundle("2030b");
+ assertTrue(install(tzData2));
+ assertTzDataInstalled(tzData2);
+ }
+
+
+ /** Tests that a bundle with a missing file will not update the content. */
+ public void testMissingRequiredBundleFile() throws Exception {
+ ConfigBundle installedConfigBundle = createValidTzDataBundle("2030a");
+ assertTrue(install(installedConfigBundle));
+ assertTzDataInstalled(installedConfigBundle);
+
+ ConfigBundle incompleteUpdate =
+ createValidTzDataBundleBuilder("2030b").clearBionicTzData().buildUnvalidated();
+ assertFalse(install(incompleteUpdate));
+ assertTzDataInstalled(installedConfigBundle);
+ }
+
+ /**
+ * Tests that an update will be unpacked even if there is a partial update from a previous run.
+ */
+ public void testInstallWithWorkingDir() throws Exception {
+ File workingDir = new File(testInstallDir, TzDataBundleInstaller.WORKING_DIR_NAME);
+ assertTrue(workingDir.mkdir());
+ createFile(new File(workingDir, "myFile"));
+
+ ConfigBundle tzData = createValidTzDataBundle("2030a");
+ assertTrue(install(tzData));
+ assertTzDataInstalled(tzData);
+ }
+
+ /**
+ * Tests that a bundle with a checksum entry that references a missing file will not update the
+ * content.
+ */
+ public void testChecksumBundleEntry_fileMissing() throws Exception {
+ ConfigBundle badUpdate =
+ createValidTzDataBundleBuilder("2030b")
+ .addChecksum("/fileDoesNotExist", 1234)
+ .build();
+ assertFalse(install(badUpdate));
+ assertNoContentInstalled();
+ }
+
+ /**
+ * Tests that a bundle with a checksum entry with a bad checksum will not update the
+ * content.
+ */
+ public void testChecksumBundleEntry_incorrectChecksum() throws Exception {
+ File fileToChecksum = SYSTEM_ZONE_INFO_FILE;
+ long badChecksum = FileUtils.calculateChecksum(fileToChecksum) + 1;
+ ConfigBundle badUpdate =
+ createValidTzDataBundleBuilder("2030b")
+ .clearChecksumEntries()
+ .addChecksum(fileToChecksum.getPath(), badChecksum)
+ .build();
+ assertFalse(install(badUpdate));
+ assertNoContentInstalled();
+ }
+
+ private boolean install(ConfigBundle configBundle) throws Exception {
+ return installer.install(configBundle.getBundleBytes());
+ }
+
+ private ConfigBundle createValidTzDataBundle(String tzDataVersion)
+ throws IOException {
+ return createValidTzDataBundleBuilder(tzDataVersion).build();
+ }
+
+ private TzDataBundleBuilder createValidTzDataBundleBuilder(String tzDataVersion)
+ throws IOException {
+
+ // The file to include in the installation-time checksum check.
+ File fileToChecksum = SYSTEM_ZONE_INFO_FILE;
+ long checksum = FileUtils.calculateChecksum(fileToChecksum);
+
+ File bionicTzData = new File(tempDir, "zoneinfo");
+ createFile(bionicTzData);
+
+ File icuData = new File(tempDir, "icudata");
+ createFile(icuData);
+
+ return new TzDataBundleBuilder()
+ .addChecksum(fileToChecksum.getPath(), checksum)
+ .setTzDataVersion(tzDataVersion)
+ .addBionicTzData(bionicTzData)
+ .addIcuTzData(icuData);
+ }
+
+ private void assertTzDataInstalled(ConfigBundle expectedTzData) throws Exception {
+ assertTrue(testInstallDir.exists());
+
+ File currentTzDataDir = new File(testInstallDir, TzDataBundleInstaller.CURRENT_TZ_DATA_DIR_NAME);
+ assertTrue(currentTzDataDir.exists());
+
+ File checksumFile = new File(currentTzDataDir, ConfigBundle.CHECKSUMS_FILE_NAME);
+ assertTrue(checksumFile.exists());
+
+ File versionFile = new File(currentTzDataDir,
+ ConfigBundle.TZ_DATA_VERSION_FILE_NAME);
+ assertTrue(versionFile.exists());
+
+ File bionicFile = new File(currentTzDataDir, ConfigBundle.ZONEINFO_FILE_NAME);
+ assertTrue(bionicFile.exists());
+
+ File icuFile = new File(currentTzDataDir, ConfigBundle.ICU_DATA_FILE_NAME);
+ assertTrue(icuFile.exists());
+
+ // Also check no working directory is left lying around.
+ File workingDir = new File(testInstallDir, TzDataBundleInstaller.WORKING_DIR_NAME);
+ assertFalse(workingDir.exists());
+ }
+
+ private void assertNoContentInstalled() {
+ File currentTzDataDir = new File(testInstallDir, TzDataBundleInstaller.CURRENT_TZ_DATA_DIR_NAME);
+ assertFalse(currentTzDataDir.exists());
+
+ // Also check no working directories are left lying around.
+ File workingDir = new File(testInstallDir, TzDataBundleInstaller.WORKING_DIR_NAME);
+ assertFalse(workingDir.exists());
+
+ File oldDataDir = new File(testInstallDir, TzDataBundleInstaller.OLD_TZ_DATA_DIR_NAME);
+ assertFalse(oldDataDir.exists());
+ }
+
+ private static void createFile(File file) {
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ fos.write('a');
+ } catch (IOException e) {
+ fail(e.getMessage());
+ }
+ }
+}
diff --git a/tzdata/update_test_app/Android.mk b/tzdata/update_test_app/Android.mk
new file mode 100644
index 0000000..ee70819
--- /dev/null
+++ b/tzdata/update_test_app/Android.mk
@@ -0,0 +1,25 @@
+# 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)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+LOCAL_PROGUARD_ENABLED := disabled
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_PACKAGE_NAME := UpdateTestApp
+LOCAL_CERTIFICATE := platform
+include $(BUILD_PACKAGE)
diff --git a/tzdata/update_test_app/AndroidManifest.xml b/tzdata/update_test_app/AndroidManifest.xml
new file mode 100644
index 0000000..67a8450
--- /dev/null
+++ b/tzdata/update_test_app/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="libcore.tzdata.update_test_app.installupdatetestapp" >
+
+ <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
+ <application
+ android:allowBackup="false"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@android:style/Theme.Holo.Light">
+ <activity
+ android:name=".MainActivity"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <provider
+ android:name="android.support.v4.content.FileProvider"
+ android:authorities="libcore.tzdata.update_test_app.fileprovider"
+ android:grantUriPermissions="true"
+ android:exported="false">
+ <meta-data
+ android:name="android.support.FILE_PROVIDER_PATHS"
+ android:resource="@xml/filepaths" />
+ </provider>
+
+ </application>
+
+</manifest>
diff --git a/tzdata/update_test_app/res/drawable/ic_launcher.png b/tzdata/update_test_app/res/drawable/ic_launcher.png
new file mode 100644
index 0000000..96a442e
--- /dev/null
+++ b/tzdata/update_test_app/res/drawable/ic_launcher.png
Binary files differ
diff --git a/tzdata/update_test_app/res/layout/activity_main.xml b/tzdata/update_test_app/res/layout/activity_main.xml
new file mode 100644
index 0000000..b265837
--- /dev/null
+++ b/tzdata/update_test_app/res/layout/activity_main.xml
@@ -0,0 +1,106 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ tools:context=".MainActivity">
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/action"
+ android:id="@+id/action_label" />
+
+ <EditText
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/action"
+ android:layout_weight="1"
+ android:text="@string/default_action" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/version"
+ android:id="@+id/version_label" />
+
+ <EditText
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/version"
+ android:layout_weight="1"
+ android:text="@string/default_version" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/content_path"
+ android:id="@+id/content_path_label" />
+
+ <EditText
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/default_content_path"
+ android:id="@+id/content_path" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/required_hash"
+ android:id="@+id/required_hash_label" />
+
+ <EditText
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/default_required_hash"
+ android:id="@+id/required_hash" />
+
+ </LinearLayout>
+
+ <Button
+ android:id="@+id/trigger_install_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_centerVertical="true"
+ android:text="@string/trigger_install" />
+
+ <ScrollView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:scrollbars="vertical"
+ android:fillViewport="true">
+
+ <TextView
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:id="@+id/log"
+ android:singleLine="false" />
+
+ </ScrollView>
+
+</LinearLayout>
diff --git a/tzdata/update_test_app/res/values/strings.xml b/tzdata/update_test_app/res/values/strings.xml
new file mode 100644
index 0000000..524f9d8
--- /dev/null
+++ b/tzdata/update_test_app/res/values/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app_name">InstallUpdateTestApp</string>
+ <string name="action">Action</string>
+ <string name="content_path">Content Path</string>
+ <string name="default_action">android.intent.action.UPDATE_TZDATA</string>
+ <string name="default_content_path">/data/local/tmp/out.zip</string>
+ <string name="default_required_hash">NONE</string>
+ <string name="default_version">1</string>
+ <string name="required_hash">Required Hash</string>
+ <string name="trigger_install">Trigger Install</string>
+ <string name="version">Version</string>
+</resources>
diff --git a/tzdata/update_test_app/res/xml/filepaths.xml b/tzdata/update_test_app/res/xml/filepaths.xml
new file mode 100644
index 0000000..c95b8f4
--- /dev/null
+++ b/tzdata/update_test_app/res/xml/filepaths.xml
@@ -0,0 +1,4 @@
+<!-- Used by FileProvider. See AndroidManifest.xml -->
+<paths>
+ <files-path path="temp/" name="temp" />
+</paths>
diff --git a/tzdata/update_test_app/src/libcore/tzdata/update_test_app/installupdatetestapp/MainActivity.java b/tzdata/update_test_app/src/libcore/tzdata/update_test_app/installupdatetestapp/MainActivity.java
new file mode 100644
index 0000000..f9d911b
--- /dev/null
+++ b/tzdata/update_test_app/src/libcore/tzdata/update_test_app/installupdatetestapp/MainActivity.java
@@ -0,0 +1,271 @@
+/*
+ * 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 libcore.tzdata.update_test_app.installupdatetestapp;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.support.v4.content.FileProvider;
+import android.util.Base64;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Date;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+
+public class MainActivity extends Activity implements View.OnClickListener {
+
+ private static final String UPDATE_CERTIFICATE_KEY = "config_update_certificate";
+ private static final String EXTRA_REQUIRED_HASH = "REQUIRED_HASH";
+ private static final String EXTRA_SIGNATURE = "SIGNATURE";
+ private static final String EXTRA_VERSION_NUMBER = "VERSION";
+
+ public static final String TEST_CERT = "" +
+ "MIIDsjCCAxugAwIBAgIJAPLf2gS0zYGUMA0GCSqGSIb3DQEBBQUAMIGYMQswCQYDVQQGEwJVUzET" +
+ "MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEPMA0GA1UEChMGR29v" +
+ "Z2xlMRAwDgYDVQQLEwd0ZXN0aW5nMRYwFAYDVQQDEw1HZXJlbXkgQ29uZHJhMSEwHwYJKoZIhvcN" +
+ "AQkBFhJnY29uZHJhQGdvb2dsZS5jb20wHhcNMTIwNzE0MTc1MjIxWhcNMTIwODEzMTc1MjIxWjCB" +
+ "mDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZp" +
+ "ZXcxDzANBgNVBAoTBkdvb2dsZTEQMA4GA1UECxMHdGVzdGluZzEWMBQGA1UEAxMNR2VyZW15IENv" +
+ "bmRyYTEhMB8GCSqGSIb3DQEJARYSZ2NvbmRyYUBnb29nbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUA" +
+ "A4GNADCBiQKBgQCjGGHATBYlmas+0sEECkno8LZ1KPglb/mfe6VpCT3GhSr+7br7NG/ZwGZnEhLq" +
+ "E7YIH4fxltHmQC3Tz+jM1YN+kMaQgRRjo/LBCJdOKaMwUbkVynAH6OYsKevjrOPk8lfM5SFQzJMG" +
+ "sA9+Tfopr5xg0BwZ1vA/+E3mE7Tr3M2UvwIDAQABo4IBADCB/TAdBgNVHQ4EFgQUhzkS9E6G+x8W" +
+ "L4EsmRjDxu28tHUwgc0GA1UdIwSBxTCBwoAUhzkS9E6G+x8WL4EsmRjDxu28tHWhgZ6kgZswgZgx" +
+ "CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3" +
+ "MQ8wDQYDVQQKEwZHb29nbGUxEDAOBgNVBAsTB3Rlc3RpbmcxFjAUBgNVBAMTDUdlcmVteSBDb25k" +
+ "cmExITAfBgkqhkiG9w0BCQEWEmdjb25kcmFAZ29vZ2xlLmNvbYIJAPLf2gS0zYGUMAwGA1UdEwQF" +
+ "MAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAYiugFDmbDOQ2U/+mqNt7o8ftlEo9SJrns6O8uTtK6AvR" +
+ "orDrR1AXTXkuxwLSbmVfedMGOZy7Awh7iZa8hw5x9XmUudfNxvmrKVEwGQY2DZ9PXbrnta/dwbhK" +
+ "mWfoepESVbo7CKIhJp8gRW0h1Z55ETXD57aGJRvQS4pxkP8ANhM=";
+
+
+ public static final String TEST_KEY = "" +
+ "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKMYYcBMFiWZqz7SwQQKSejwtnUo" +
+ "+CVv+Z97pWkJPcaFKv7tuvs0b9nAZmcSEuoTtggfh/GW0eZALdPP6MzVg36QxpCBFGOj8sEIl04p" +
+ "ozBRuRXKcAfo5iwp6+Os4+TyV8zlIVDMkwawD35N+imvnGDQHBnW8D/4TeYTtOvczZS/AgMBAAEC" +
+ "gYBxwFalNSwZK3WJipq+g6KLCiBn1JxGGDQlLKrweFaSuFyFky9fd3IvkIabirqQchD612sMb+GT" +
+ "0t1jptW6z4w2w6++IW0A3apDOCwoD+uvDBXrbFqI0VbyAWUNqHVdaFFIRk2IHGEE6463mGRdmILX" +
+ "IlCd/85RTHReg4rl/GFqWQJBANgLAIR4pWbl5Gm+DtY18wp6Q3pJAAMkmP/lISCBIidu1zcqYIKt" +
+ "PoDW4Knq9xnhxPbXrXKv4YzZWHBK8GkKhQ0CQQDBQnXufQcMew+PwiS0oJvS+eQ6YJwynuqG2ejg" +
+ "WE+T7489jKtscRATpUXpZUYmDLGg9bLt7L62hFvFSj2LO2X7AkBcdrD9AWnBFWlh/G77LVHczSEu" +
+ "KCoyLiqxcs5vy/TjLaQ8vw1ZQG580/qJnr+tOxyCjSJ18GK3VppsTRaBznfNAkB3nuCKNp9HTWCL" +
+ "dfrsRsFMrFpk++mSt6SoxXaMbn0LL2u1CD4PCEiQMGt+lK3/3TmRTKNs+23sYS7Ahjxj0udDAkEA" +
+ "p57Nj65WNaWeYiOfTwKXkLj8l29H5NbaGWxPT0XkWr4PvBOFZVH/wj0/qc3CMVGnv11+DyO+QUCN" +
+ "SqBB5aRe8g==";
+
+ private EditText actionEditText;
+ private EditText versionEditText;
+ private EditText contentPathEditText;
+ private EditText requiredHashEditText;
+ private TextView logView;
+
+ private ExecutorService executor;
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ Button triggerInstallButton = (Button) findViewById(R.id.trigger_install_button);
+ triggerInstallButton.setOnClickListener(this);
+
+ actionEditText = (EditText) findViewById(R.id.action);
+ versionEditText = (EditText) findViewById(R.id.version);
+ contentPathEditText = (EditText) findViewById(R.id.content_path);
+ requiredHashEditText = (EditText) findViewById(R.id.required_hash);
+ logView = (TextView) findViewById(R.id.log);
+ executor = Executors.newFixedThreadPool(1);
+ }
+
+ @Override
+ public void onClick(View v) {
+ final String action = actionEditText.getText().toString();
+ final String contentPath = contentPathEditText.getText().toString();
+ final String version = versionEditText.getText().toString();
+ final String requiredHash = requiredHashEditText.getText().toString();
+
+ new AsyncTask<Void, String, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ final File contentFile = new File(contentPath);
+ File tempDir = new File(getFilesDir(), "temp");
+ if (!tempDir.exists() && !tempDir.mkdir()) {
+ publishProgress("Unable to create: " + tempDir);
+ return null;
+ }
+
+ File copyOfContentFile;
+ try {
+ copyOfContentFile = File.createTempFile("content", ".tmp", tempDir);
+ copyFile(contentFile, copyOfContentFile);
+ } catch (IOException e) {
+ publishProgress("Error", exceptionToString(e));
+ return null;
+ }
+ publishProgress("Created copy of " + contentFile + " at " + copyOfContentFile);
+
+ String originalCert = null;
+ try {
+ originalCert = overrideCert(TEST_CERT);
+ sleep(1000);
+ publishProgress("Overridden update cert");
+
+ String signature = createSignature(copyOfContentFile, version, requiredHash);
+ sendIntent(copyOfContentFile, action, version, requiredHash, signature);
+ publishProgress("Sent update intent");
+ } catch (Exception e) {
+ publishProgress("Error", exceptionToString(e));
+ } finally {
+ if (originalCert != null) {
+ sleep(1000);
+ try {
+ overrideCert(originalCert);
+ publishProgress("Reverted update cert");
+ } catch (Exception e) {
+ publishProgress("Unable to revert update cert", exceptionToString(e));
+ }
+ }
+ }
+ publishProgress("Update intent sent successfully");
+ return null;
+ }
+
+ @Override
+ protected void onProgressUpdate(String... values) {
+ for (String message : values) {
+ addToLog(message, null);
+ }
+ }
+ }.executeOnExecutor(executor);
+ }
+
+ private String overrideCert(String cert) throws Exception {
+ final String key = UPDATE_CERTIFICATE_KEY;
+ String originalCert = Settings.Secure.getString(getContentResolver(), key);
+ if (!Settings.Secure.putString(getContentResolver(), key, cert)) {
+ throw new Exception("Unable to override update certificate");
+ }
+ return originalCert;
+ }
+
+ private void sleep(long millisDelay) {
+ try {
+ Thread.sleep(millisDelay);
+ } catch (InterruptedException e) {
+ // Ignore
+ }
+ }
+
+ private void sendIntent(
+ File contentFile, String action, String version, String required, String sig) {
+ Intent i = new Intent();
+ i.setAction(action);
+ Uri contentUri =
+ FileProvider.getUriForFile(
+ getApplicationContext(), "libcore.tzdata.update_test_app.fileprovider",
+ contentFile);
+ i.setData(contentUri);
+ i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ i.putExtra(EXTRA_VERSION_NUMBER, version);
+ i.putExtra(EXTRA_REQUIRED_HASH, required);
+ i.putExtra(EXTRA_SIGNATURE, sig);
+ sendBroadcast(i);
+ }
+
+ private void addToLog(String message, Exception e) {
+ logString(message);
+ if (e != null) {
+ String text = exceptionToString(e);
+ logString(text);
+ }
+ }
+
+ private void logString(String value) {
+ logView.append(new Date() + " " + value + "\n");
+ int scrollAmount =
+ logView.getLayout().getLineTop(logView.getLineCount()) - logView.getHeight();
+ logView.scrollTo(0, scrollAmount);
+ }
+
+ private static String createSignature(File contentFile, String version, String requiredHash)
+ throws Exception {
+ byte[] contentBytes = readBytes(contentFile);
+ Signature signer = Signature.getInstance("SHA512withRSA");
+ signer.initSign(createKey());
+ signer.update(contentBytes);
+ signer.update(version.trim().getBytes());
+ signer.update(requiredHash.getBytes());
+ return new String(Base64.encode(signer.sign(), Base64.DEFAULT));
+ }
+
+ private static byte[] readBytes(File contentFile) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (FileInputStream fis = new FileInputStream(contentFile)) {
+ int count;
+ byte[] buffer = new byte[8192];
+ while ((count = fis.read(buffer)) != -1) {
+ baos.write(buffer, 0, count);
+ }
+ }
+ return baos.toByteArray();
+ }
+
+ private static PrivateKey createKey() throws Exception {
+ byte[] derKey = Base64.decode(TEST_KEY.getBytes(), Base64.DEFAULT);
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(derKey);
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ return keyFactory.generatePrivate(keySpec);
+ }
+
+ private static String exceptionToString(Exception e) {
+ StringWriter writer = new StringWriter();
+ e.printStackTrace(new PrintWriter(writer));
+ return writer.getBuffer().toString();
+ }
+
+ private static void copyFile(File from, File to) throws IOException {
+ byte[] buffer = new byte[8192];
+ int count;
+ try (
+ FileInputStream in = new FileInputStream(from);
+ FileOutputStream out = new FileOutputStream(to)
+ ) {
+ while ((count = in.read(buffer)) != -1) {
+ out.write(buffer, 0, count);
+ }
+ }
+ }
+}