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 /** 48 * The name of the file inside the distro containing the distro version information. 49 * The content is ASCII bytes representing a set of version numbers. See {@link DistroVersion}. 50 * This constant must match the one in system/core/tzdatacheck/tzdatacheck.cpp. 51 */ 52 public static final String DISTRO_VERSION_FILE_NAME = "distro_version"; 53 54 private static final int BUFFER_SIZE = 8192; 55 56 /** 57 * Maximum size of entry getEntryContents() will pull into a byte array. To avoid exhausting 58 * heap memory when encountering unexpectedly large entries. 128k should be enough for anyone. 59 */ 60 private static final long MAX_GET_ENTRY_CONTENTS_SIZE = 128 * 1024; 61 62 private final InputStream inputStream; 63 64 /** 65 * Creates a TimeZoneDistro using a byte array. A convenience for 66 * {@code new TimeZoneDistro(new ByteArrayInputStream(bytes))}. 67 */ TimeZoneDistro(byte[] bytes)68 public TimeZoneDistro(byte[] bytes) { 69 this(new ByteArrayInputStream(bytes)); 70 } 71 72 /** 73 * Creates a TimeZoneDistro wrapping an {@link InputStream}. 74 */ TimeZoneDistro(InputStream inputStream)75 public TimeZoneDistro(InputStream inputStream) { 76 this.inputStream = inputStream; 77 } 78 79 /** 80 * Consumes the wrapped {@link InputStream} returning only the {@link DistroVersion}. 81 * The wrapped {@link InputStream} is closed after this call. 82 */ getDistroVersion()83 public DistroVersion getDistroVersion() throws DistroException, IOException { 84 byte[] contents = getEntryContents(inputStream, DISTRO_VERSION_FILE_NAME); 85 if (contents == null) { 86 throw new DistroException("Distro version file entry not found"); 87 } 88 return DistroVersion.fromBytes(contents); 89 } 90 getEntryContents(InputStream is, String entryName)91 private static byte[] getEntryContents(InputStream is, String entryName) throws IOException { 92 try (ZipInputStream zipInputStream = new ZipInputStream(is)) { 93 ZipEntry entry; 94 while ((entry = zipInputStream.getNextEntry()) != null) { 95 String name = entry.getName(); 96 97 if (!entryName.equals(name)) { 98 continue; 99 } 100 // Guard against massive entries consuming too much heap memory. 101 if (entry.getSize() > MAX_GET_ENTRY_CONTENTS_SIZE) { 102 throw new IOException("Entry " + entryName + " too large: " + entry.getSize()); 103 } 104 byte[] buffer = new byte[BUFFER_SIZE]; 105 try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { 106 int count; 107 while ((count = zipInputStream.read(buffer)) != -1) { 108 baos.write(buffer, 0, count); 109 } 110 return baos.toByteArray(); 111 } 112 } 113 // Entry not found. 114 return null; 115 } 116 } 117 118 /** 119 * Consumes the wrapped {@link InputStream}, extracting the content to {@code targetDir}. 120 * The wrapped {@link InputStream} is closed after this call. 121 */ extractTo(File targetDir)122 public void extractTo(File targetDir) throws IOException { 123 extractZipSafely(inputStream, targetDir, true /* makeWorldReadable */); 124 } 125 126 /** Visible for testing */ extractZipSafely(InputStream is, File targetDir, boolean makeWorldReadable)127 static void extractZipSafely(InputStream is, File targetDir, boolean makeWorldReadable) 128 throws IOException { 129 130 // Create the extraction dir, if needed. 131 FileUtils.ensureDirectoriesExist(targetDir, makeWorldReadable); 132 133 try (ZipInputStream zipInputStream = new ZipInputStream(is)) { 134 byte[] buffer = new byte[BUFFER_SIZE]; 135 ZipEntry entry; 136 while ((entry = zipInputStream.getNextEntry()) != null) { 137 // Validate the entry name: make sure the unpacked file will exist beneath the 138 // targetDir. 139 String name = entry.getName(); 140 // Note, we assume that nothing will quickly insert a symlink after createSubFile() 141 // that might invalidate the guarantees about name existing beneath targetDir. 142 File entryFile = FileUtils.createSubFile(targetDir, name); 143 144 if (entry.isDirectory()) { 145 FileUtils.ensureDirectoriesExist(entryFile, makeWorldReadable); 146 } else { 147 // Create the path if there was no directory entry. 148 if (!entryFile.getParentFile().exists()) { 149 FileUtils.ensureDirectoriesExist( 150 entryFile.getParentFile(), makeWorldReadable); 151 } 152 153 try (FileOutputStream fos = new FileOutputStream(entryFile)) { 154 int count; 155 while ((count = zipInputStream.read(buffer)) != -1) { 156 fos.write(buffer, 0, count); 157 } 158 // sync to disk 159 fos.getFD().sync(); 160 } 161 // mark entryFile -rw-r--r-- 162 if (makeWorldReadable) { 163 FileUtils.makeWorldReadable(entryFile); 164 } 165 } 166 } 167 } 168 } 169 } 170