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