diff options
Diffstat (limited to 'tzdata')
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 Binary files differnew file mode 100644 index 0000000..96a442e --- /dev/null +++ b/tzdata/update_test_app/res/drawable/ic_launcher.png 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); + } + } + } +} |