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