1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.timezone.distro; 17 18 import java.io.ByteArrayInputStream; 19 import java.io.ByteArrayOutputStream; 20 import java.io.File; 21 import java.io.FileOutputStream; 22 import java.io.IOException; 23 import java.io.InputStream; 24 import java.util.zip.ZipEntry; 25 import java.util.zip.ZipInputStream; 26 27 /** 28 * A time zone distro. This is a thin wrapper around an {@link InputStream} containing a zip archive 29 * with knowledge of its expected structure and logic for its safe extraction. One of 30 * {@link #extractTo(File)} or {@link #getDistroVersion()} must be called for the 31 * {@link InputStream} to be closed. 32 */ 33 public final class TimeZoneDistro { 34 35 /** The standard name of Android time zone distro files. */ 36 public static final String FILE_NAME = "distro.zip"; 37 38 /** The name of the file inside the distro containing bionic/libcore TZ data. */ 39 public static final String TZDATA_FILE_NAME = "tzdata"; 40 41 /** The name of the file inside the distro containing ICU TZ data. */ 42 public static final String ICU_DATA_FILE_NAME = "icu/icu_tzdata.dat"; 43 44 /** The name of the file inside the distro containing time zone lookup data. */ 45 public static final String TZLOOKUP_FILE_NAME = "tzlookup.xml"; 46 47 /** The name of the file inside the distro containing telephony lookup data. */ 48 public static final String TELEPHONYLOOKUP_FILE_NAME = "telephonylookup.xml"; 49 50 /** 51 * The name of the file inside the distro containing the distro version information. 52 * The content is ASCII bytes representing a set of version numbers. See {@link DistroVersion}. 53 * This constant must match the one in system/core/tzdatacheck/tzdatacheck.cpp. 54 */ 55 public static final String DISTRO_VERSION_FILE_NAME = "distro_version"; 56 57 private static final int BUFFER_SIZE = 8192; 58 59 /** 60 * Maximum size of entry getEntryContents() will pull into a byte array. To avoid exhausting 61 * heap memory when encountering unexpectedly large entries. 128k should be enough for anyone. 62 */ 63 private static final long MAX_GET_ENTRY_CONTENTS_SIZE = 128 * 1024; 64 65 private final InputStream inputStream; 66 67 /** 68 * Creates a TimeZoneDistro using a byte array. A convenience for 69 * {@code new TimeZoneDistro(new ByteArrayInputStream(bytes))}. 70 */ TimeZoneDistro(byte[] bytes)71 public TimeZoneDistro(byte[] bytes) { 72 this(new ByteArrayInputStream(bytes)); 73 } 74 75 /** 76 * Creates a TimeZoneDistro wrapping an {@link InputStream}. 77 */ TimeZoneDistro(InputStream inputStream)78 public TimeZoneDistro(InputStream inputStream) { 79 this.inputStream = inputStream; 80 } 81 82 /** 83 * Consumes the wrapped {@link InputStream} returning only the {@link DistroVersion}. 84 * The wrapped {@link InputStream} is closed after this call. 85 */ getDistroVersion()86 public DistroVersion getDistroVersion() throws DistroException, IOException { 87 byte[] contents = getEntryContents(inputStream, DISTRO_VERSION_FILE_NAME); 88 if (contents == null) { 89 throw new DistroException("Distro version file entry not found"); 90 } 91 return DistroVersion.fromBytes(contents); 92 } 93 getEntryContents(InputStream is, String entryName)94 private static byte[] getEntryContents(InputStream is, String entryName) throws IOException { 95 try (ZipInputStream zipInputStream = new ZipInputStream(is)) { 96 ZipEntry entry; 97 while ((entry = zipInputStream.getNextEntry()) != null) { 98 String name = entry.getName(); 99 100 if (!entryName.equals(name)) { 101 continue; 102 } 103 // Guard against massive entries consuming too much heap memory. 104 if (entry.getSize() > MAX_GET_ENTRY_CONTENTS_SIZE) { 105 throw new IOException("Entry " + entryName + " too large: " + entry.getSize()); 106 } 107 byte[] buffer = new byte[BUFFER_SIZE]; 108 try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { 109 int count; 110 while ((count = zipInputStream.read(buffer)) != -1) { 111 baos.write(buffer, 0, count); 112 } 113 return baos.toByteArray(); 114 } 115 } 116 // Entry not found. 117 return null; 118 } 119 } 120 121 /** 122 * Consumes the wrapped {@link InputStream}, extracting the content to {@code targetDir}. 123 * The wrapped {@link InputStream} is closed after this call. 124 */ extractTo(File targetDir)125 public void extractTo(File targetDir) throws IOException { 126 extractZipSafely(inputStream, targetDir, true /* makeWorldReadable */); 127 } 128 129 /** Visible for testing */ extractZipSafely(InputStream is, File targetDir, boolean makeWorldReadable)130 static void extractZipSafely(InputStream is, File targetDir, boolean makeWorldReadable) 131 throws IOException { 132 133 // Create the extraction dir, if needed. 134 FileUtils.ensureDirectoriesExist(targetDir, makeWorldReadable); 135 136 try (ZipInputStream zipInputStream = new ZipInputStream(is)) { 137 byte[] buffer = new byte[BUFFER_SIZE]; 138 ZipEntry entry; 139 while ((entry = zipInputStream.getNextEntry()) != null) { 140 // Validate the entry name: make sure the unpacked file will exist beneath the 141 // targetDir. 142 String name = entry.getName(); 143 // Note, we assume that nothing will quickly insert a symlink after createSubFile() 144 // that might invalidate the guarantees about name existing beneath targetDir. 145 File entryFile = FileUtils.createSubFile(targetDir, name); 146 147 if (entry.isDirectory()) { 148 FileUtils.ensureDirectoriesExist(entryFile, makeWorldReadable); 149 } else { 150 // Create the path if there was no directory entry. 151 if (!entryFile.getParentFile().exists()) { 152 FileUtils.ensureDirectoriesExist( 153 entryFile.getParentFile(), makeWorldReadable); 154 } 155 156 try (FileOutputStream fos = new FileOutputStream(entryFile)) { 157 int count; 158 while ((count = zipInputStream.read(buffer)) != -1) { 159 fos.write(buffer, 0, count); 160 } 161 // sync to disk 162 fos.getFD().sync(); 163 } 164 // mark entryFile -rw-r--r-- 165 if (makeWorldReadable) { 166 FileUtils.makeWorldReadable(entryFile); 167 } 168 } 169 } 170 } 171 } 172 } 173