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.builder;
17 
18 import com.android.timezone.distro.DistroException;
19 import com.android.timezone.distro.DistroVersion;
20 import com.android.timezone.distro.TimeZoneDistro;
21 
22 import java.io.ByteArrayOutputStream;
23 import java.io.File;
24 import java.io.FileInputStream;
25 import java.io.IOException;
26 import java.nio.charset.StandardCharsets;
27 import java.util.zip.ZipEntry;
28 import java.util.zip.ZipOutputStream;
29 
30 /**
31  * A class for creating a {@link TimeZoneDistro} containing timezone update data. Used in real
32  * distro creation code and tests.
33  */
34 public final class TimeZoneDistroBuilder {
35 
36     /**
37      * An arbitrary timestamp (actually 1/1/1970 00:00:00 UTC) used as the modification time for all
38      * files within a distro to reduce unnecessary differences when a distro is regenerated from the
39      * same input data. To use UTC time in zip file, a year before 1980 is chosen.
40      */
41     private static final long ENTRY_TIMESTAMP = 0L;
42 
43     private DistroVersion distroVersion;
44     private byte[] tzData;
45     private byte[] icuData;
46     private String tzLookupXml;
47     private String telephonyLookupXml;
48 
setDistroVersion(DistroVersion distroVersion)49     public TimeZoneDistroBuilder setDistroVersion(DistroVersion distroVersion) {
50         this.distroVersion = distroVersion;
51         return this;
52     }
53 
clearVersionForTests()54     public TimeZoneDistroBuilder clearVersionForTests() {
55         // This has the effect of omitting the version file in buildUnvalidated().
56         this.distroVersion = null;
57         return this;
58     }
59 
replaceFormatVersionForTests(int majorVersion, int minorVersion)60     public TimeZoneDistroBuilder replaceFormatVersionForTests(int majorVersion, int minorVersion) {
61         try {
62             distroVersion = new DistroVersion(
63                     majorVersion, minorVersion, distroVersion.rulesVersion, distroVersion.revision);
64         } catch (DistroException e) {
65             throw new IllegalArgumentException();
66         }
67         return this;
68     }
69 
setTzDataFile(File tzDataFile)70     public TimeZoneDistroBuilder setTzDataFile(File tzDataFile) throws IOException {
71         return setTzDataFile(readFileAsByteArray(tzDataFile));
72     }
73 
setTzDataFile(byte[] tzData)74     public TimeZoneDistroBuilder setTzDataFile(byte[] tzData) {
75         this.tzData = tzData;
76         return this;
77     }
78 
79     // For use in tests.
clearTzDataForTests()80     public TimeZoneDistroBuilder clearTzDataForTests() {
81         this.tzData = null;
82         return this;
83     }
84 
setIcuDataFile(File icuDataFile)85     public TimeZoneDistroBuilder setIcuDataFile(File icuDataFile) throws IOException {
86         return setIcuDataFile(readFileAsByteArray(icuDataFile));
87     }
88 
setIcuDataFile(byte[] icuData)89     public TimeZoneDistroBuilder setIcuDataFile(byte[] icuData) {
90         this.icuData = icuData;
91         return this;
92     }
93 
setTzLookupFile(File tzLookupFile)94     public TimeZoneDistroBuilder setTzLookupFile(File tzLookupFile) throws IOException {
95         return setTzLookupXml(readFileAsUtf8(tzLookupFile));
96     }
97 
setTzLookupXml(String tzLookupXml)98     public TimeZoneDistroBuilder setTzLookupXml(String tzLookupXml) {
99         this.tzLookupXml = tzLookupXml;
100         return this;
101     }
102 
setTelephonyLookupFile(File telephonyLookupFile)103     public TimeZoneDistroBuilder setTelephonyLookupFile(File telephonyLookupFile)
104             throws IOException {
105         return setTelephonyLookupXml(readFileAsUtf8(telephonyLookupFile));
106     }
107 
setTelephonyLookupXml(String telephonyLookupXml)108     public TimeZoneDistroBuilder setTelephonyLookupXml(String telephonyLookupXml) {
109         this.telephonyLookupXml = telephonyLookupXml;
110         return this;
111     }
112 
113     // For use in tests.
clearIcuDataForTests()114     public TimeZoneDistroBuilder clearIcuDataForTests() {
115         this.icuData = null;
116         return this;
117     }
118 
119     /**
120      * For use in tests. Use {@link #buildBytes()} for a version with validation.
121      */
buildUnvalidatedBytes()122     public byte[] buildUnvalidatedBytes() throws DistroException {
123         ByteArrayOutputStream baos = new ByteArrayOutputStream();
124         try (ZipOutputStream zos = new ZipOutputStream(baos)) {
125             if (distroVersion != null) {
126                 addZipEntry(zos, TimeZoneDistro.DISTRO_VERSION_FILE_NAME, distroVersion.toBytes());
127             }
128 
129             if (tzData != null) {
130                 addZipEntry(zos, TimeZoneDistro.TZDATA_FILE_NAME, tzData);
131             }
132             if (icuData != null) {
133                 addZipEntry(zos, TimeZoneDistro.ICU_DATA_FILE_NAME, icuData);
134             }
135             if (tzLookupXml != null) {
136                 addZipEntry(zos, TimeZoneDistro.TZLOOKUP_FILE_NAME,
137                         tzLookupXml.getBytes(StandardCharsets.UTF_8));
138             }
139             if (telephonyLookupXml != null) {
140                 addZipEntry(zos, TimeZoneDistro.TELEPHONYLOOKUP_FILE_NAME,
141                         telephonyLookupXml.getBytes(StandardCharsets.UTF_8));
142             }
143         } catch (IOException e) {
144             throw new DistroException("Unable to create zip file", e);
145         }
146         return baos.toByteArray();
147     }
148 
149     /**
150      * Builds a {@code byte[]} for a Distro .zip file.
151      */
buildBytes()152     public byte[] buildBytes() throws DistroException {
153         if (distroVersion == null) {
154             throw new IllegalStateException("Missing distroVersion");
155         }
156         if (icuData == null) {
157             throw new IllegalStateException("Missing icuData");
158         }
159         if (tzData == null) {
160             throw new IllegalStateException("Missing tzData");
161         }
162         return buildUnvalidatedBytes();
163     }
164 
addZipEntry(ZipOutputStream zos, String name, byte[] content)165     private static void addZipEntry(ZipOutputStream zos, String name, byte[] content)
166             throws DistroException {
167         try {
168             ZipEntry zipEntry = new ZipEntry(name);
169             zipEntry.setSize(content.length);
170             // Set the time to a fixed value so the zip entry is deterministic.
171             zipEntry.setTime(ENTRY_TIMESTAMP);
172             zos.putNextEntry(zipEntry);
173             zos.write(content);
174             zos.closeEntry();
175         } catch (IOException e) {
176             throw new DistroException("Unable to add zip entry", e);
177         }
178     }
179 
180     /**
181      * Returns the contents of 'path' as a byte array.
182      */
readFileAsByteArray(File file)183     private static byte[] readFileAsByteArray(File file) throws IOException {
184         byte[] buffer = new byte[8192];
185         ByteArrayOutputStream baos = new ByteArrayOutputStream();
186         try (FileInputStream  fis = new FileInputStream(file)) {
187             int count;
188             while ((count = fis.read(buffer)) != -1) {
189                 baos.write(buffer, 0, count);
190             }
191         }
192         return baos.toByteArray();
193     }
194 
195     /**
196      * Returns the contents of 'path' as a String, having interpreted the file as UTF-8.
197      */
readFileAsUtf8(File file)198     private String readFileAsUtf8(File file) throws IOException {
199         return new String(readFileAsByteArray(file), StandardCharsets.UTF_8);
200     }
201 }
202 
203