/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.timezone.distro.tools; import com.android.timezone.distro.DistroException; import com.android.timezone.distro.DistroVersion; import com.android.timezone.distro.TimeZoneDistro; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; /** * A class for creating a {@link TimeZoneDistro} containing timezone update data. Used in real * distro creation code and tests. */ public final class TimeZoneDistroBuilder { /** * An arbitrary timestamp (actually 1/1/2017 00:00:00 UTC) used as the modification time for all * files within a distro to reduce unnecessary differences when a distro is regenerated from the * same input data. */ private static long ENTRY_TIMESTAMP = 1483228800000L; private DistroVersion distroVersion; private byte[] tzData; private byte[] icuData; private String tzLookupXml; public TimeZoneDistroBuilder setDistroVersion(DistroVersion distroVersion) { this.distroVersion = distroVersion; return this; } public TimeZoneDistroBuilder clearVersionForTests() { // This has the effect of omitting the version file in buildUnvalidated(). this.distroVersion = null; return this; } public TimeZoneDistroBuilder replaceFormatVersionForTests(int majorVersion, int minorVersion) { try { distroVersion = new DistroVersion( majorVersion, minorVersion, distroVersion.rulesVersion, distroVersion.revision); } catch (DistroException e) { throw new IllegalArgumentException(); } return this; } public TimeZoneDistroBuilder setTzDataFile(File tzDataFile) throws IOException { return setTzDataFile(readFileAsByteArray(tzDataFile)); } public TimeZoneDistroBuilder setTzDataFile(byte[] tzData) { this.tzData = tzData; return this; } // For use in tests. public TimeZoneDistroBuilder clearTzDataForTests() { this.tzData = null; return this; } public TimeZoneDistroBuilder setIcuDataFile(File icuDataFile) throws IOException { return setIcuDataFile(readFileAsByteArray(icuDataFile)); } public TimeZoneDistroBuilder setIcuDataFile(byte[] icuData) { this.icuData = icuData; return this; } public TimeZoneDistroBuilder setTzLookupFile(File tzLookupFile) throws IOException { return setTzLookupXml(readFileAsUtf8(tzLookupFile)); } public TimeZoneDistroBuilder setTzLookupXml(String tzlookupXml) { this.tzLookupXml = tzlookupXml; return this; } // For use in tests. public TimeZoneDistroBuilder clearIcuDataForTests() { this.icuData = null; return this; } /** * For use in tests. Use {@link #buildBytes()} for a version with validation. */ public byte[] buildUnvalidatedBytes() throws DistroException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (ZipOutputStream zos = new ZipOutputStream(baos)) { if (distroVersion != null) { addZipEntry(zos, TimeZoneDistro.DISTRO_VERSION_FILE_NAME, distroVersion.toBytes()); } if (tzData != null) { addZipEntry(zos, TimeZoneDistro.TZDATA_FILE_NAME, tzData); } if (icuData != null) { addZipEntry(zos, TimeZoneDistro.ICU_DATA_FILE_NAME, icuData); } if (tzLookupXml != null) { addZipEntry(zos, TimeZoneDistro.TZLOOKUP_FILE_NAME, tzLookupXml.getBytes(StandardCharsets.UTF_8)); } } catch (IOException e) { throw new DistroException("Unable to create zip file", e); } return baos.toByteArray(); } /** * Builds a {@code byte[]} for a Distro .zip file. */ public byte[] buildBytes() throws DistroException { if (distroVersion == null) { throw new IllegalStateException("Missing distroVersion"); } if (icuData == null) { throw new IllegalStateException("Missing icuData"); } if (tzData == null) { throw new IllegalStateException("Missing tzData"); } return buildUnvalidatedBytes(); } private static void addZipEntry(ZipOutputStream zos, String name, byte[] content) throws DistroException { try { ZipEntry zipEntry = new ZipEntry(name); zipEntry.setSize(content.length); // Set the time to a fixed value so the zip entry is deterministic. zipEntry.setTime(ENTRY_TIMESTAMP); zos.putNextEntry(zipEntry); zos.write(content); zos.closeEntry(); } catch (IOException e) { throw new DistroException("Unable to add zip entry", e); } } /** * Returns the contents of 'path' as a byte array. */ private static byte[] readFileAsByteArray(File file) throws IOException { byte[] buffer = new byte[8192]; ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (FileInputStream fis = new FileInputStream(file)) { int count; while ((count = fis.read(buffer)) != -1) { baos.write(buffer, 0, count); } } return baos.toByteArray(); } /** * Returns the contents of 'path' as a String, having interpreted the file as UTF-8. */ private String readFileAsUtf8(File file) throws IOException { return new String(readFileAsByteArray(file), StandardCharsets.UTF_8); } }