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.installer;
17
18import com.android.timezone.distro.DistroException;
19import com.android.timezone.distro.DistroVersion;
20import com.android.timezone.distro.FileUtils;
21import com.android.timezone.distro.StagedDistroOperation;
22import com.android.timezone.distro.TimeZoneDistro;
23
24import android.util.Slog;
25
26import java.io.File;
27import java.io.FileNotFoundException;
28import java.io.IOException;
29import libcore.util.TimeZoneFinder;
30import libcore.util.ZoneInfoDB;
31
32/**
33 * A distro-validation / extraction class. Separate from the services code that uses it for easier
34 * testing. This class is not thread-safe: callers are expected to handle mutual exclusion.
35 */
36public class TimeZoneDistroInstaller {
37    /** {@link #stageInstallWithErrorCode(TimeZoneDistro)} result code: Success. */
38    public final static int INSTALL_SUCCESS = 0;
39
40    /** {@link #stageInstallWithErrorCode(TimeZoneDistro)} result code: Distro corrupt. */
41    public final static int INSTALL_FAIL_BAD_DISTRO_STRUCTURE = 1;
42
43    /**
44     * {@link #stageInstallWithErrorCode(TimeZoneDistro)} result code: Distro version incompatible.
45     */
46    public final static int INSTALL_FAIL_BAD_DISTRO_FORMAT_VERSION = 2;
47
48    /**
49     * {@link #stageInstallWithErrorCode(TimeZoneDistro)} result code: Distro rules too old for
50     * device.
51     */
52    public final static int INSTALL_FAIL_RULES_TOO_OLD = 3;
53
54    /**
55     * {@link #stageInstallWithErrorCode(TimeZoneDistro)} result code: Distro content failed
56     * validation.
57     */
58    public final static int INSTALL_FAIL_VALIDATION_ERROR = 4;
59
60    /**
61     * {@link #stageUninstall()} result code: An uninstall has been successfully staged.
62     */
63    public final static int UNINSTALL_SUCCESS = 0;
64
65    /**
66     * {@link #stageUninstall()} result code: Nothing was installed that required an uninstall to be
67     * staged.
68     */
69    public final static int UNINSTALL_NOTHING_INSTALLED = 1;
70
71    /**
72     * {@link #stageUninstall()} result code: The uninstall could not be staged.
73     */
74    public final static int UNINSTALL_FAIL = 2;
75
76    // This constant must match one in system/timezone/tzdatacheck/tzdatacheck.cpp.
77    private static final String STAGED_TZ_DATA_DIR_NAME = "staged";
78    // This constant must match one in system/timezone/tzdatacheck/tzdatacheck.cpp.
79    private static final String CURRENT_TZ_DATA_DIR_NAME = "current";
80    private static final String WORKING_DIR_NAME = "working";
81    private static final String OLD_TZ_DATA_DIR_NAME = "old";
82
83    /**
84     * The name of the file in the staged directory used to indicate a staged uninstallation.
85     */
86    // This constant must match one in system/timezone/tzdatacheck/tzdatacheck.cpp.
87    // VisibleForTesting.
88    public static final String UNINSTALL_TOMBSTONE_FILE_NAME = "STAGED_UNINSTALL_TOMBSTONE";
89
90    private final String logTag;
91    private final File systemTzDataFile;
92    private final File oldStagedDataDir;
93    private final File stagedTzDataDir;
94    private final File currentTzDataDir;
95    private final File workingDir;
96
97    public TimeZoneDistroInstaller(String logTag, File systemTzDataFile, File installDir) {
98        this.logTag = logTag;
99        this.systemTzDataFile = systemTzDataFile;
100        oldStagedDataDir = new File(installDir, OLD_TZ_DATA_DIR_NAME);
101        stagedTzDataDir = new File(installDir, STAGED_TZ_DATA_DIR_NAME);
102        currentTzDataDir = new File(installDir, CURRENT_TZ_DATA_DIR_NAME);
103        workingDir = new File(installDir, WORKING_DIR_NAME);
104    }
105
106    // VisibleForTesting
107    File getOldStagedDataDir() {
108        return oldStagedDataDir;
109    }
110
111    // VisibleForTesting
112    File getStagedTzDataDir() {
113        return stagedTzDataDir;
114    }
115
116    // VisibleForTesting
117    File getCurrentTzDataDir() {
118        return currentTzDataDir;
119    }
120
121    // VisibleForTesting
122    File getWorkingDir() {
123        return workingDir;
124    }
125
126    /**
127     * Stage an install of the supplied content, to be installed the next time the device boots.
128     *
129     * <p>Errors during unpacking or staging will throw an {@link IOException}.
130     * If the distro content is invalid this method returns {@code false}.
131     * If the installation completed successfully this method returns {@code true}.
132     */
133    public boolean install(TimeZoneDistro distro) throws IOException {
134        int result = stageInstallWithErrorCode(distro);
135        return result == INSTALL_SUCCESS;
136    }
137
138    /**
139     * Stage an install of the supplied content, to be installed the next time the device boots.
140     *
141     * <p>Errors during unpacking or staging will throw an {@link IOException}.
142     * Returns {@link #INSTALL_SUCCESS} on success, or one of the failure codes.
143     */
144    public int stageInstallWithErrorCode(TimeZoneDistro distro) throws IOException {
145        if (oldStagedDataDir.exists()) {
146            FileUtils.deleteRecursive(oldStagedDataDir);
147        }
148        if (workingDir.exists()) {
149            FileUtils.deleteRecursive(workingDir);
150        }
151
152        Slog.i(logTag, "Unpacking / verifying time zone update");
153        try {
154            unpackDistro(distro, workingDir);
155
156            DistroVersion distroVersion;
157            try {
158                distroVersion = readDistroVersion(workingDir);
159            } catch (DistroException e) {
160                Slog.i(logTag, "Invalid distro version: " + e.getMessage());
161                return INSTALL_FAIL_BAD_DISTRO_STRUCTURE;
162            }
163            if (distroVersion == null) {
164                Slog.i(logTag, "Update not applied: Distro version could not be loaded");
165                return INSTALL_FAIL_BAD_DISTRO_STRUCTURE;
166            }
167            if (!DistroVersion.isCompatibleWithThisDevice(distroVersion)) {
168                Slog.i(logTag, "Update not applied: Distro format version check failed: "
169                        + distroVersion);
170                return INSTALL_FAIL_BAD_DISTRO_FORMAT_VERSION;
171            }
172
173            if (!checkDistroDataFilesExist(workingDir)) {
174                Slog.i(logTag, "Update not applied: Distro is missing required data file(s)");
175                return INSTALL_FAIL_BAD_DISTRO_STRUCTURE;
176            }
177
178            if (!checkDistroRulesNewerThanSystem(systemTzDataFile, distroVersion)) {
179                Slog.i(logTag, "Update not applied: Distro rules version check failed");
180                return INSTALL_FAIL_RULES_TOO_OLD;
181            }
182
183            // Validate the tzdata file.
184            File zoneInfoFile = new File(workingDir, TimeZoneDistro.TZDATA_FILE_NAME);
185            ZoneInfoDB.TzData tzData = ZoneInfoDB.TzData.loadTzData(zoneInfoFile.getPath());
186            if (tzData == null) {
187                Slog.i(logTag, "Update not applied: " + zoneInfoFile + " could not be loaded");
188                return INSTALL_FAIL_VALIDATION_ERROR;
189            }
190            try {
191                tzData.validate();
192            } catch (IOException e) {
193                Slog.i(logTag, "Update not applied: " + zoneInfoFile + " failed validation", e);
194                return INSTALL_FAIL_VALIDATION_ERROR;
195            } finally {
196                tzData.close();
197            }
198
199            // Validate the tzlookup.xml file.
200            File tzLookupFile = new File(workingDir, TimeZoneDistro.TZLOOKUP_FILE_NAME);
201            if (!tzLookupFile.exists()) {
202                Slog.i(logTag, "Update not applied: " + tzLookupFile + " does not exist");
203                return INSTALL_FAIL_BAD_DISTRO_STRUCTURE;
204            }
205            try {
206                TimeZoneFinder timeZoneFinder =
207                        TimeZoneFinder.createInstance(tzLookupFile.getPath());
208                timeZoneFinder.validate();
209            } catch (IOException e) {
210                Slog.i(logTag, "Update not applied: " + tzLookupFile + " failed validation", e);
211                return INSTALL_FAIL_VALIDATION_ERROR;
212            }
213
214            // TODO(nfuller): Add validity checks for ICU data / canarying before applying.
215            // http://b/64016752
216
217            Slog.i(logTag, "Applying time zone update");
218            FileUtils.makeDirectoryWorldAccessible(workingDir);
219
220            // Check if there is already a staged install or uninstall and remove it if there is.
221            if (!stagedTzDataDir.exists()) {
222                Slog.i(logTag, "Nothing to unstage at " + stagedTzDataDir);
223            } else {
224                Slog.i(logTag, "Moving " + stagedTzDataDir + " to " + oldStagedDataDir);
225                // Move stagedTzDataDir out of the way in one operation so we can't partially delete
226                // the contents.
227                FileUtils.rename(stagedTzDataDir, oldStagedDataDir);
228            }
229
230            // Move the workingDir to be the new staged directory.
231            Slog.i(logTag, "Moving " + workingDir + " to " + stagedTzDataDir);
232            FileUtils.rename(workingDir, stagedTzDataDir);
233            Slog.i(logTag, "Install staged: " + stagedTzDataDir + " successfully created");
234            return INSTALL_SUCCESS;
235        } finally {
236            deleteBestEffort(oldStagedDataDir);
237            deleteBestEffort(workingDir);
238        }
239    }
240
241    /**
242     * Stage an uninstall of the current timezone update in /data which, on reboot, will return the
243     * device to using data from /system. If there was something else already staged it will be
244     * removed by this call.
245     *
246     * Returns {@link #UNINSTALL_SUCCESS} if staging the uninstallation was
247     * successful and reboot will be required. Returns {@link #UNINSTALL_NOTHING_INSTALLED} if
248     * there was nothing installed in /data that required an uninstall to be staged, anything that
249     * was staged will have been removed and therefore no reboot is required.
250     *
251     * <p>Errors encountered during uninstallation will throw an {@link IOException}.
252     */
253    public int stageUninstall() throws IOException {
254        Slog.i(logTag, "Uninstalling time zone update");
255
256        if (oldStagedDataDir.exists()) {
257            // If we can't remove this, an exception is thrown and we don't continue.
258            FileUtils.deleteRecursive(oldStagedDataDir);
259        }
260        if (workingDir.exists()) {
261            FileUtils.deleteRecursive(workingDir);
262        }
263
264        try {
265            // Check if there is already an install or uninstall staged and remove it.
266            if (!stagedTzDataDir.exists()) {
267                Slog.i(logTag, "Nothing to unstage at " + stagedTzDataDir);
268            } else {
269                Slog.i(logTag, "Moving " + stagedTzDataDir + " to " + oldStagedDataDir);
270                // Move stagedTzDataDir out of the way in one operation so we can't partially delete
271                // the contents.
272                FileUtils.rename(stagedTzDataDir, oldStagedDataDir);
273            }
274
275            // If there's nothing actually installed, there's nothing to uninstall so no need to
276            // stage anything.
277            if (!currentTzDataDir.exists()) {
278                Slog.i(logTag, "Nothing to uninstall at " + currentTzDataDir);
279                return UNINSTALL_NOTHING_INSTALLED;
280            }
281
282            // Stage an uninstall in workingDir.
283            FileUtils.ensureDirectoriesExist(workingDir, true /* makeWorldReadable */);
284            FileUtils.createEmptyFile(new File(workingDir, UNINSTALL_TOMBSTONE_FILE_NAME));
285
286            // Move the workingDir to be the new staged directory.
287            Slog.i(logTag, "Moving " + workingDir + " to " + stagedTzDataDir);
288            FileUtils.rename(workingDir, stagedTzDataDir);
289            Slog.i(logTag, "Uninstall staged: " + stagedTzDataDir + " successfully created");
290
291            return UNINSTALL_SUCCESS;
292        } finally {
293            deleteBestEffort(oldStagedDataDir);
294            deleteBestEffort(workingDir);
295        }
296    }
297
298    /**
299     * Reads the currently installed distro version. Returns {@code null} if there is no distro
300     * installed.
301     *
302     * @throws IOException if there was a problem reading data from /data
303     * @throws DistroException if there was a problem with the installed distro format/structure
304     */
305    public DistroVersion getInstalledDistroVersion() throws DistroException, IOException {
306        if (!currentTzDataDir.exists()) {
307            return null;
308        }
309        return readDistroVersion(currentTzDataDir);
310    }
311
312    /**
313     * Reads information about any currently staged distro operation. Returns {@code null} if there
314     * is no distro operation staged.
315     *
316     * @throws IOException if there was a problem reading data from /data
317     * @throws DistroException if there was a problem with the staged distro format/structure
318     */
319    public StagedDistroOperation getStagedDistroOperation() throws DistroException, IOException {
320        if (!stagedTzDataDir.exists()) {
321            return null;
322        }
323        if (new File(stagedTzDataDir, UNINSTALL_TOMBSTONE_FILE_NAME).exists()) {
324            return StagedDistroOperation.uninstall();
325        } else {
326            return StagedDistroOperation.install(readDistroVersion(stagedTzDataDir));
327        }
328    }
329
330    /**
331     * Reads the timezone rules version present in /system. i.e. the version that would be present
332     * after a factory reset.
333     *
334     * @throws IOException if there was a problem reading data
335     */
336    public String getSystemRulesVersion() throws IOException {
337        return readSystemRulesVersion(systemTzDataFile);
338    }
339
340    private void deleteBestEffort(File dir) {
341        if (dir.exists()) {
342            Slog.i(logTag, "Deleting " + dir);
343            try {
344                FileUtils.deleteRecursive(dir);
345            } catch (IOException e) {
346                // Logged but otherwise ignored.
347                Slog.w(logTag, "Unable to delete " + dir, e);
348            }
349        }
350    }
351
352    private void unpackDistro(TimeZoneDistro distro, File targetDir) throws IOException {
353        Slog.i(logTag, "Unpacking update content to: " + targetDir);
354        distro.extractTo(targetDir);
355    }
356
357    private boolean checkDistroDataFilesExist(File unpackedContentDir) throws IOException {
358        Slog.i(logTag, "Verifying distro contents");
359        return FileUtils.filesExist(unpackedContentDir,
360                TimeZoneDistro.TZDATA_FILE_NAME,
361                TimeZoneDistro.ICU_DATA_FILE_NAME);
362    }
363
364    private DistroVersion readDistroVersion(File distroDir) throws DistroException, IOException {
365        Slog.d(logTag, "Reading distro format version: " + distroDir);
366        File distroVersionFile = new File(distroDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
367        if (!distroVersionFile.exists()) {
368            throw new DistroException("No distro version file found: " + distroVersionFile);
369        }
370        byte[] versionBytes =
371                FileUtils.readBytes(distroVersionFile, DistroVersion.DISTRO_VERSION_FILE_LENGTH);
372        return DistroVersion.fromBytes(versionBytes);
373    }
374
375    /**
376     * Returns true if the the distro IANA rules version is >= system IANA rules version.
377     */
378    private boolean checkDistroRulesNewerThanSystem(
379            File systemTzDataFile, DistroVersion distroVersion) throws IOException {
380
381        // We only check the /system tzdata file and assume that other data like ICU is in sync.
382        // There is a CTS test that checks ICU and bionic/libcore are in sync.
383        Slog.i(logTag, "Reading /system rules version");
384        String systemRulesVersion = readSystemRulesVersion(systemTzDataFile);
385
386        String distroRulesVersion = distroVersion.rulesVersion;
387        // canApply = distroRulesVersion >= systemRulesVersion
388        boolean canApply = distroRulesVersion.compareTo(systemRulesVersion) >= 0;
389        if (!canApply) {
390            Slog.i(logTag, "Failed rules version check: distroRulesVersion="
391                    + distroRulesVersion + ", systemRulesVersion=" + systemRulesVersion);
392        } else {
393            Slog.i(logTag, "Passed rules version check: distroRulesVersion="
394                    + distroRulesVersion + ", systemRulesVersion=" + systemRulesVersion);
395        }
396        return canApply;
397    }
398
399    private String readSystemRulesVersion(File systemTzDataFile) throws IOException {
400        if (!systemTzDataFile.exists()) {
401            Slog.i(logTag, "tzdata file cannot be found in /system");
402            throw new FileNotFoundException("system tzdata does not exist: " + systemTzDataFile);
403        }
404        return ZoneInfoDB.TzData.getRulesVersion(systemTzDataFile);
405    }
406}
407