summaryrefslogtreecommitdiffstats
path: root/tzdata
diff options
context:
space:
mode:
authorNeil Fuller <nfuller@google.com>2015-03-13 14:31:20 +0000
committerNeil Fuller <nfuller@google.com>2015-03-31 09:28:06 +0100
commit91c98d778c80e53a7f458264233375f982dcae14 (patch)
tree6d048b60f46c805e8a701e3af9798a4b1850a0c5 /tzdata
parent1e342670cc46445bd51d53f7a28f95d1eb879c9f (diff)
downloadlibcore-91c98d778c80e53a7f458264233375f982dcae14.zip
libcore-91c98d778c80e53a7f458264233375f982dcae14.tar.gz
libcore-91c98d778c80e53a7f458264233375f982dcae14.tar.bz2
Timezone data installer code
The code here is used by the system server to install timezone data updates. It is separate so it can be tested. Scripts are included that build an "update bundle" (a zip file with a well-defined contents). ConfigBundle contains logic to extract the bundle safely. TzDataBundleBuilder is used in the test and tools to construct a bundle. The scripts in tools is a placeholder and will evolve. bionic/libc/tools/zoneinfo tools will likely move there so they can be integrated more closely. An app is included for testing updates. Bug: 19941636 Change-Id: Id0985f8c5be2f12858ee8bf52acf52bdb2df8741
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);
+ }
+ }
+ }
+}