• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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