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