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