176e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller/*
276e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller * Copyright (C) 2015 The Android Open Source Project
376e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller *
476e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller * Licensed under the Apache License, Version 2.0 (the "License");
576e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller * you may not use this file except in compliance with the License.
676e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller * You may obtain a copy of the License at
776e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller *
876e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller *      http://www.apache.org/licenses/LICENSE-2.0
976e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller *
1076e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller * Unless required by applicable law or agreed to in writing, software
1176e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller * distributed under the License is distributed on an "AS IS" BASIS,
1276e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1376e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller * See the License for the specific language governing permissions and
1476e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller * limitations under the License.
1576e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller */
1676e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fullerpackage com.android.timezone.distro;
1776e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
1876e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fullerimport java.io.ByteArrayInputStream;
1976e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fullerimport java.io.ByteArrayOutputStream;
2076e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fullerimport java.io.File;
2176e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fullerimport java.io.FileOutputStream;
2276e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fullerimport java.io.IOException;
2376e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fullerimport java.io.InputStream;
2476e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fullerimport java.util.Arrays;
2547d149abc26ae3ddd6322ad852610aba7f061bafNeil Fullerimport java.util.function.Supplier;
2676e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fullerimport java.util.zip.ZipEntry;
2776e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fullerimport java.util.zip.ZipInputStream;
2876e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
2976e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller/**
306e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller * A time zone distro. This is a thin wrapper around an {@link InputStream} containing a zip archive
316e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller * with knowledge of its expected structure and logic for its safe extraction. One of
326e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller * {@link #extractTo(File)} or {@link #getDistroVersion()} must be called for the
336e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller * {@link InputStream} to be closed.
3476e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller */
3576e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fullerpublic final class TimeZoneDistro {
3676e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
37ad7e81c00260b6dcb9d9ecf2c3c8aa161f555059Neil Fuller    /** The standard name of Android time zone distro files. */
38ad7e81c00260b6dcb9d9ecf2c3c8aa161f555059Neil Fuller    public static final String FILE_NAME = "distro.zip";
39ad7e81c00260b6dcb9d9ecf2c3c8aa161f555059Neil Fuller
4076e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    /** The name of the file inside the distro containing bionic/libcore TZ data. */
4176e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    public static final String TZDATA_FILE_NAME = "tzdata";
4276e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
4376e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    /** The name of the file inside the distro containing ICU TZ data. */
4476e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    public static final String ICU_DATA_FILE_NAME = "icu/icu_tzdata.dat";
4576e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
4676e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    /** The name of the file inside the distro containing time zone lookup data. */
4776e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    public static final String TZLOOKUP_FILE_NAME = "tzlookup.xml";
4876e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
4976e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    /**
5076e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller     * The name of the file inside the distro containing the distro version information.
5176e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller     * The content is ASCII bytes representing a set of version numbers. See {@link DistroVersion}.
5276e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller     * This constant must match the one in system/core/tzdatacheck/tzdatacheck.cpp.
5376e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller     */
5476e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    public static final String DISTRO_VERSION_FILE_NAME = "distro_version";
5576e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
5676e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    private static final int BUFFER_SIZE = 8192;
5776e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
5876e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    /**
5976e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller     * Maximum size of entry getEntryContents() will pull into a byte array. To avoid exhausting
6076e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller     * heap memory when encountering unexpectedly large entries. 128k should be enough for anyone.
6176e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller     */
6276e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    private static final long MAX_GET_ENTRY_CONTENTS_SIZE = 128 * 1024;
6376e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
646e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller    private final InputStream inputStream;
6576e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
6647d149abc26ae3ddd6322ad852610aba7f061bafNeil Fuller    /**
676e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller     * Creates a TimeZoneDistro using a byte array. A convenience for
686e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller     * {@code new TimeZoneDistro(new ByteArrayInputStream(bytes))}.
6947d149abc26ae3ddd6322ad852610aba7f061bafNeil Fuller     */
7076e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    public TimeZoneDistro(byte[] bytes) {
716e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller        this(new ByteArrayInputStream(bytes));
7247d149abc26ae3ddd6322ad852610aba7f061bafNeil Fuller    }
7347d149abc26ae3ddd6322ad852610aba7f061bafNeil Fuller
7447d149abc26ae3ddd6322ad852610aba7f061bafNeil Fuller    /**
756e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller     * Creates a TimeZoneDistro wrapping an {@link InputStream}.
7647d149abc26ae3ddd6322ad852610aba7f061bafNeil Fuller     */
776e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller    public TimeZoneDistro(InputStream inputStream) {
786e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller        this.inputStream = inputStream;
7976e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    }
8076e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
816e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller    /**
826e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller     * Consumes the wrapped {@link InputStream} returning only the {@link DistroVersion}.
836e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller     * The wrapped {@link InputStream} is closed after this call.
846e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller     */
8576e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    public DistroVersion getDistroVersion() throws DistroException, IOException {
866e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller        byte[] contents = getEntryContents(inputStream, DISTRO_VERSION_FILE_NAME);
8776e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller        if (contents == null) {
8876e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller            throw new DistroException("Distro version file entry not found");
8976e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller        }
9076e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller        return DistroVersion.fromBytes(contents);
9176e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    }
9276e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
9376e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    private static byte[] getEntryContents(InputStream is, String entryName) throws IOException {
9476e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller        try (ZipInputStream zipInputStream = new ZipInputStream(is)) {
9576e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller            ZipEntry entry;
9676e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller            while ((entry = zipInputStream.getNextEntry()) != null) {
9776e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                String name = entry.getName();
9876e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
9976e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                if (!entryName.equals(name)) {
10076e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                    continue;
10176e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                }
10276e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                // Guard against massive entries consuming too much heap memory.
10376e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                if (entry.getSize() > MAX_GET_ENTRY_CONTENTS_SIZE) {
10476e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                    throw new IOException("Entry " + entryName + " too large: " + entry.getSize());
10576e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                }
10676e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                byte[] buffer = new byte[BUFFER_SIZE];
10776e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
10876e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                    int count;
10976e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                    while ((count = zipInputStream.read(buffer)) != -1) {
11076e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                        baos.write(buffer, 0, count);
11176e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                    }
11276e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                    return baos.toByteArray();
11376e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                }
11476e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller            }
11576e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller            // Entry not found.
11676e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller            return null;
11776e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller        }
11876e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    }
11976e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
1206e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller    /**
1216e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller     * Consumes the wrapped {@link InputStream}, extracting the content to {@code targetDir}.
1226e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller     * The wrapped {@link InputStream} is closed after this call.
1236e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller     */
12476e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    public void extractTo(File targetDir) throws IOException {
1256e484a7cdddda4035ca36826437cf3718dd7fa47Neil Fuller        extractZipSafely(inputStream, targetDir, true /* makeWorldReadable */);
12676e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    }
12776e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
12876e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    /** Visible for testing */
12976e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    static void extractZipSafely(InputStream is, File targetDir, boolean makeWorldReadable)
13076e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller            throws IOException {
13176e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
13276e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller        // Create the extraction dir, if needed.
13376e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller        FileUtils.ensureDirectoriesExist(targetDir, makeWorldReadable);
13476e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
13576e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller        try (ZipInputStream zipInputStream = new ZipInputStream(is)) {
13676e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller            byte[] buffer = new byte[BUFFER_SIZE];
13776e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller            ZipEntry entry;
13876e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller            while ((entry = zipInputStream.getNextEntry()) != null) {
13976e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                // Validate the entry name: make sure the unpacked file will exist beneath the
14076e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                // targetDir.
14176e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                String name = entry.getName();
14276e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                // Note, we assume that nothing will quickly insert a symlink after createSubFile()
14376e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                // that might invalidate the guarantees about name existing beneath targetDir.
14476e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                File entryFile = FileUtils.createSubFile(targetDir, name);
14576e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
14676e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                if (entry.isDirectory()) {
14776e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                    FileUtils.ensureDirectoriesExist(entryFile, makeWorldReadable);
14876e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                } else {
14976e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                    // Create the path if there was no directory entry.
15076e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                    if (!entryFile.getParentFile().exists()) {
15176e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                        FileUtils.ensureDirectoriesExist(
15276e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                                entryFile.getParentFile(), makeWorldReadable);
15376e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                    }
15476e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller
15576e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                    try (FileOutputStream fos = new FileOutputStream(entryFile)) {
15676e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                        int count;
15776e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                        while ((count = zipInputStream.read(buffer)) != -1) {
15876e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                            fos.write(buffer, 0, count);
15976e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                        }
16076e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                        // sync to disk
16176e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                        fos.getFD().sync();
16276e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                    }
16376e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                    // mark entryFile -rw-r--r--
16476e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                    if (makeWorldReadable) {
16576e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                        FileUtils.makeWorldReadable(entryFile);
16676e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                    }
16776e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller                }
16876e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller            }
16976e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller        }
17076e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller    }
17176e2f39dc8dd9f2b0c2fb081a86ef6ad3e4e5632Neil Fuller}
172