1 /*
2  * Copyright (C) 2017 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 
17 package com.android.cts.tzdata;
18 
19 import static org.junit.Assert.assertArrayEquals;
20 
21 import com.android.i18n.timezone.TzDataSetVersion;
22 import libcore.timezone.testing.ZoneInfoTestHelper;
23 
24 import com.android.timezone.distro.DistroVersion;
25 import com.android.timezone.distro.TimeZoneDistro;
26 import com.android.timezone.distro.builder.TimeZoneDistroBuilder;
27 import com.android.tradefed.device.DeviceNotAvailableException;
28 import com.android.tradefed.testtype.DeviceTestCase;
29 
30 import java.io.File;
31 import java.io.FileOutputStream;
32 import java.io.IOException;
33 import java.nio.charset.StandardCharsets;
34 import java.nio.file.Files;
35 import java.nio.file.Path;
36 import java.util.Comparator;
37 import java.util.StringJoiner;
38 import java.util.function.Consumer;
39 
40 /**
41  * Tests for the tzdatacheck binary.
42  *
43  * <p>The tzdatacheck binary operates over two directories: the "system directory" containing the
44  * time zone rules in the system image, and a "data directory" in the data partition which can
45  * optionally contain time zone rules data files for bionic/libcore and ICU.
46  *
47  * <p>This test executes the tzdatacheck binary to confirm it operates correctly in a number of
48  * simulated situations; simulated system and data directories in various states are created in a
49  * location the shell user has permission to access and the tzdatacheck binary is then executed.
50  * The status code and directory state after execution is then used to determine if the tzdatacheck
51  * binary operated correctly.
52  *
53  * <p>Most of the tests below prepare simulated directory structure for the system and data dirs
54  * on the host before pushing them to the device. Device state is then checked rather than syncing
55  * the files back.
56  */
57 public class TzDataCheckTest extends DeviceTestCase {
58 
59     /**
60      * The name of the directory containing the current time zone rules data beneath
61      * {@link #mDataDir}.  Also known to {@link
62      * com.android.timezone.distro.installer.TimeZoneDistroInstaller} and tzdatacheck.cpp.
63      */
64     private static final String CURRENT_DIR_NAME = "current";
65 
66     /**
67      * The name of the directory containing the staged time zone rules data beneath
68      * {@link #mDataDir}.  Also known to {@link
69      * com.android.timezone.distro.installer.TimeZoneDistroInstaller} and tzdatacheck.cpp.
70      */
71     private static final String STAGED_DIR_NAME = "staged";
72 
73     /**
74      * The name of the file inside the staged directory that indicates the staged operation is an
75      * uninstall. Also known to {@link com.android.timezone.distro.installer.TimeZoneDistroInstaller} and
76      * tzdatacheck.cpp.
77      */
78     private static final String UNINSTALL_TOMBSTONE_FILE_NAME = "STAGED_UNINSTALL_TOMBSTONE";
79 
80     /**
81      * The name of the /system time zone data file. Also known to tzdatacheck.cpp.
82      */
83     private static final String SYSTEM_TZ_VERSION_FILE_NAME = "tz_version";
84 
85     /** A valid time zone rules version guaranteed to be older than {@link #RULES_VERSION_TWO} */
86     private static final String RULES_VERSION_ONE = "2016g";
87     /** A valid time zone rules version guaranteed to be newer than {@link #RULES_VERSION_ONE} */
88     private static final String RULES_VERSION_TWO = "2016h";
89     /**
90      * An arbitrary, valid time zone rules version used when it doesn't matter what the rules
91      * version is.
92      */
93     private static final String VALID_RULES_VERSION = RULES_VERSION_ONE;
94 
95     /** An arbitrary valid revision number. */
96     private static final int VALID_REVISION = 1;
97 
98     private String mDeviceAndroidRootDir;
99     private PathPair mTestRootDir;
100     private PathPair mSystemDir;
101     private PathPair mDataDir;
102 
setUp()103     public void setUp() throws Exception {
104         super.setUp();
105 
106         // It's not clear how we would get this without invoking "/system/bin/sh", but we need the
107         // value first to do so. It has been hardcoded instead.
108         mDeviceAndroidRootDir = "/system";
109 
110         // Create a test root directory on host and device.
111         Path hostTestRootDir = Files.createTempDirectory("tzdatacheck_test");
112         mTestRootDir = new PathPair(
113                 hostTestRootDir,
114                 "/data/local/tmp/tzdatacheck_test");
115         createDeviceDirectory(mTestRootDir);
116 
117         // tzdatacheck requires two directories: a "system" path and a "data" path.
118         mSystemDir = mTestRootDir.createSubPath("system_dir");
119         mDataDir = mTestRootDir.createSubPath("data_dir");
120 
121         // Create the host-side directory structure (for preparing files before pushing them to
122         // device and looking at files retrieved from device).
123         createHostDirectory(mSystemDir);
124         createHostDirectory(mDataDir);
125 
126         // Create the equivalent device-side directory structure for receiving files.
127         createDeviceDirectory(mSystemDir);
128         createDeviceDirectory(mDataDir);
129     }
130 
131     @Override
tearDown()132     public void tearDown() throws Exception {
133         // Remove the test root directories that have been created by this test.
134         deleteHostDirectory(mTestRootDir, true /* failOnError */);
135         deleteDeviceDirectory(mTestRootDir, true /* failOnError */);
136         super.tearDown();
137     }
138 
139     /**
140      * Test the base file used by tzdatacheck exists in the expected location - tzcdatacheck relies
141      * on this file to determine the version of tzdata on device. The path is passed to tzdatacheck
142      * via a command line argument hardcoded in system/core/rootdir/init.rc.
143      */
testExpectedBaseFilesExist()144     public void testExpectedBaseFilesExist() throws Exception {
145         String baseTzFilesDir = "/apex/com.android.tzdata/etc/tz/";
146         assertDeviceFileExists(baseTzFilesDir + "tz_version");
147     }
148 
testTooFewArgs()149     public void testTooFewArgs() throws Exception {
150         // No need to set up or push files to the device for this test.
151         assertEquals(1, runTzDataCheckWithArgs(new String[0]));
152         assertEquals(1, runTzDataCheckWithArgs(new String[] { "oneArg" }));
153     }
154 
155     // {dataDir}/staged exists but it is a file.
testStaging_stagingDirIsFile()156     public void testStaging_stagingDirIsFile() throws Exception {
157         // Set up the /system directory structure on host.
158         createSystemTzVersionFileOnHost(VALID_RULES_VERSION);
159 
160         // Set up the /data directory structure on host.
161         PathPair dataStagedDir = mDataDir.createSubPath(STAGED_DIR_NAME);
162         // Create a file with the same name as the directory that tzdatacheck expects.
163         Files.write(dataStagedDir.hostPath, new byte[] { 'a' });
164 
165         // Push the host test directory and contents to the device.
166         pushHostTestDirToDevice();
167 
168         // Execute tzdatacheck and check the status code. Failures due to staging issues are
169         // generally ignored providing the device is left in a reasonable state.
170         assertEquals(0, runTzDataCheckOnDevice());
171 
172         // Assert the file was just ignored. This is a fairly arbitrary choice to leave it rather
173         // than delete.
174         assertDevicePathExists(dataStagedDir);
175         assertDevicePathIsFile(dataStagedDir);
176     }
177 
178     // {dataDir}/staged exists but /current dir is a file.
testStaging_uninstall_currentDirIsFile()179     public void testStaging_uninstall_currentDirIsFile() throws Exception {
180         // Set up the /system directory structure on host.
181         createSystemTzVersionFileOnHost(VALID_RULES_VERSION);
182 
183         // Set up the /data directory structure on host.
184 
185         // Create a staged uninstall.
186         PathPair dataStagedDir = mDataDir.createSubPath(STAGED_DIR_NAME);
187         createStagedUninstallOnHost(dataStagedDir);
188 
189         // Create a file with the same name as the directory that tzdatacheck expects.
190         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
191         Files.write(dataCurrentDir.hostPath, new byte[] { 'a' });
192 
193         // Push the host test directory and contents to the device.
194         pushHostTestDirToDevice();
195 
196         // Execute tzdatacheck and check the status code.
197         assertEquals(0, runTzDataCheckOnDevice());
198 
199         // Assert the device was left in a valid "uninstalled" state.
200         assertDevicePathDoesNotExist(dataStagedDir);
201         assertDevicePathDoesNotExist(dataCurrentDir);
202     }
203 
204     // {dataDir}/staged contains an uninstall, but there is nothing to uninstall.
testStaging_uninstall_noCurrent()205     public void testStaging_uninstall_noCurrent() throws Exception {
206         // Set up the /system directory structure on host.
207         createSystemTzVersionFileOnHost(VALID_RULES_VERSION);
208 
209         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
210 
211         // Set up the /data directory structure on host.
212 
213         // Create a staged uninstall.
214         PathPair dataStagedDir = mDataDir.createSubPath(STAGED_DIR_NAME);
215         createStagedUninstallOnHost(dataStagedDir);
216 
217         // Push the host test directory and contents to the device.
218         pushHostTestDirToDevice();
219 
220         // Execute tzdatacheck and check the status code. Failures due to staging issues are
221         // generally ignored providing the device is left in a reasonable state.
222         assertEquals(0, runTzDataCheckOnDevice());
223 
224         // Assert the device was left in a valid "uninstalled" state.
225         assertDevicePathDoesNotExist(dataStagedDir);
226         assertDevicePathDoesNotExist(dataCurrentDir);
227     }
228 
229     // {dataDir}/staged contains an uninstall, and there is something to uninstall.
testStaging_uninstall_withCurrent()230     public void testStaging_uninstall_withCurrent() throws Exception {
231         // Set up the /system directory structure on host.
232         createSystemTzVersionFileOnHost(VALID_RULES_VERSION);
233 
234         // Set up the /data directory structure on host.
235 
236         // Create a staged uninstall.
237         PathPair dataStagedDir = mDataDir.createSubPath(STAGED_DIR_NAME);
238         createStagedUninstallOnHost(dataStagedDir);
239 
240         // Create a current installed distro.
241         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
242         byte[] distroBytes = createValidDistroBuilder().buildBytes();
243         unpackOnHost(dataCurrentDir, distroBytes);
244 
245         // Push the host test directory and contents to the device.
246         pushHostTestDirToDevice();
247 
248         // Execute tzdatacheck and check the status code. Failures due to staging issues are
249         // generally ignored providing the device is left in a reasonable state.
250         assertEquals(0, runTzDataCheckOnDevice());
251 
252         // Assert the device was left in a valid "uninstalled" state.
253         assertDevicePathDoesNotExist(dataStagedDir);
254         assertDevicePathDoesNotExist(dataCurrentDir);
255     }
256 
257     // {dataDir}/staged exists but /current dir is a file.
testStaging_install_currentDirIsFile()258     public void testStaging_install_currentDirIsFile() throws Exception {
259         // Set up the /system directory structure on host.
260         createSystemTzVersionFileOnHost(VALID_RULES_VERSION);
261 
262         // Set up the /data directory structure on host.
263 
264         // Create a staged install.
265         PathPair dataStagedDir = mDataDir.createSubPath(STAGED_DIR_NAME);
266         byte[] distroBytes = createValidDistroBuilder().buildBytes();
267         unpackOnHost(dataStagedDir, distroBytes);
268 
269         // Create a file with the same name as the directory that tzdatacheck expects.
270         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
271         Files.write(dataCurrentDir.hostPath, new byte[] { 'a' });
272 
273         // Push the host test directory and contents to the device.
274         pushHostTestDirToDevice();
275 
276         // Execute tzdatacheck and check the status code. Failures due to staging issues are
277         // generally ignored providing the device is left in a reasonable state.
278         assertEquals(0, runTzDataCheckOnDevice());
279 
280         // Assert the device was left in a valid "installed" state.
281         assertDevicePathDoesNotExist(dataStagedDir);
282         assertDeviceDirContainsDistro(dataCurrentDir, distroBytes);
283     }
284 
285     // {dataDir}/staged contains an install, but there is nothing to replace.
testStaging_install_noCurrent()286     public void testStaging_install_noCurrent() throws Exception {
287         // Set up the /system directory structure on host.
288         createSystemTzVersionFileOnHost(VALID_RULES_VERSION);
289 
290         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
291 
292         // Set up the /data directory structure on host.
293 
294         // Create a staged install.
295         PathPair dataStagedDir = mDataDir.createSubPath(STAGED_DIR_NAME);
296         byte[] stagedDistroBytes = createValidDistroBuilder().buildBytes();
297         unpackOnHost(dataStagedDir, stagedDistroBytes);
298 
299         // Push the host test directory and contents to the device.
300         pushHostTestDirToDevice();
301 
302         // Execute tzdatacheck and check the status code. Failures due to staging issues are
303         // generally ignored providing the device is left in a reasonable state.
304         assertEquals(0, runTzDataCheckOnDevice());
305 
306         // Assert the device was left in a valid "installed" state.
307         assertDevicePathDoesNotExist(dataStagedDir);
308         assertDeviceDirContainsDistro(dataCurrentDir, stagedDistroBytes);
309     }
310 
311     // {dataDir}/staged contains an install, and there is something to replace.
testStaging_install_withCurrent()312     public void testStaging_install_withCurrent() throws Exception {
313         // Set up the /system directory structure on host.
314         createSystemTzVersionFileOnHost(VALID_RULES_VERSION);
315 
316         DistroVersion currentDistroVersion = new DistroVersion(
317                 TzDataSetVersion.currentFormatMajorVersion(), 1, VALID_RULES_VERSION, 1);
318         DistroVersion stagedDistroVersion = new DistroVersion(
319                 TzDataSetVersion.currentFormatMajorVersion(), 1, VALID_RULES_VERSION, 2);
320 
321         // Set up the /data directory structure on host.
322 
323         // Create a staged uninstall.
324         PathPair dataStagedDir = mDataDir.createSubPath(STAGED_DIR_NAME);
325         byte[] stagedDistroBytes = createValidDistroBuilder()
326                 .setDistroVersion(stagedDistroVersion)
327                 .buildBytes();
328         unpackOnHost(dataStagedDir, stagedDistroBytes);
329 
330         // Create a current installed distro.
331         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
332         byte[] currentDistroBytes = createValidDistroBuilder()
333                 .setDistroVersion(currentDistroVersion)
334                 .buildBytes();
335         unpackOnHost(dataCurrentDir, currentDistroBytes);
336 
337         // Push the host test directory and contents to the device.
338         pushHostTestDirToDevice();
339 
340         // Execute tzdatacheck and check the status code. Failures due to staging issues are
341         // generally ignored providing the device is left in a reasonable state.
342         assertEquals(0, runTzDataCheckOnDevice());
343 
344         // Assert the device was left in a valid "installed" state.
345         // The stagedDistro should now be the one in the current dir.
346         assertDevicePathDoesNotExist(dataStagedDir);
347         assertDeviceDirContainsDistro(dataCurrentDir, stagedDistroBytes);
348     }
349 
350     // {dataDir}/staged contains an invalid install, and there is something to replace.
351     // Most of the invalid cases are tested without staging; this is just to prove that staging
352     // an invalid distro is handled the same.
testStaging_install_withCurrent_invalidStaged()353     public void testStaging_install_withCurrent_invalidStaged() throws Exception {
354         // Set up the /system directory structure on host.
355         createSystemTzVersionFileOnHost(VALID_RULES_VERSION);
356 
357         // Set up the /data directory structure on host.
358 
359         // Create a staged uninstall which contains invalid files (missing distro version).
360         PathPair dataStagedDir = mDataDir.createSubPath(STAGED_DIR_NAME);
361         byte[] stagedDistroBytes = createValidDistroBuilder()
362                 .clearVersionForTests()
363                 .buildUnvalidatedBytes();
364         unpackOnHost(dataStagedDir, stagedDistroBytes);
365 
366         // Create a current installed distro.
367         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
368         byte[] currentDistroBytes = createValidDistroBuilder().buildBytes();
369         unpackOnHost(dataCurrentDir, currentDistroBytes);
370 
371         // Push the host test directory and contents to the device.
372         pushHostTestDirToDevice();
373 
374         // Execute tzdatacheck and check the status code. The staged directory will have become the
375         // current one, but then it will be discovered to be invalid and will be removed.
376         assertEquals(3, runTzDataCheckOnDevice());
377 
378         // Assert the device was left in a valid "uninstalled" state.
379         assertDevicePathDoesNotExist(dataStagedDir);
380         assertDevicePathDoesNotExist(dataCurrentDir);
381     }
382 
383     // No {dataDir}/current exists.
testNoCurrentDataDir()384     public void testNoCurrentDataDir() throws Exception {
385         // Set up the /system directory structure on host.
386         createSystemTzVersionFileOnHost(VALID_RULES_VERSION);
387 
388         // Deliberately not creating anything on host in the data dir here, leaving the empty
389         // structure.
390 
391         // Push the host test directory and contents to the device.
392         pushHostTestDirToDevice();
393 
394         // Execute tzdatacheck and check the status code.
395         assertEquals(0, runTzDataCheckOnDevice());
396     }
397 
398     // {dataDir}/current exists but it is a file.
testCurrentDataDirIsFile()399     public void testCurrentDataDirIsFile() throws Exception {
400         // Set up the /system directory structure on host.
401         createSystemTzVersionFileOnHost(VALID_RULES_VERSION);
402 
403         // Set up the /data directory structure on host.
404         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
405         // Create a file with the same name as the directory that tzdatacheck expects.
406         Files.write(dataCurrentDir.hostPath, new byte[] { 'a' });
407 
408         // Push the host test directory and contents to the device.
409         pushHostTestDirToDevice();
410 
411         // Execute tzdatacheck and check the status code.
412         assertEquals(2, runTzDataCheckOnDevice());
413 
414         // Assert the file was just ignored. This is a fairly arbitrary choice to leave it rather
415         // than delete.
416         assertDevicePathExists(dataCurrentDir);
417         assertDevicePathIsFile(dataCurrentDir);
418     }
419 
420     // {dataDir}/current exists but is missing the distro version file.
testMissingDataDirDistroVersionFile()421     public void testMissingDataDirDistroVersionFile() throws Exception {
422         // Set up the /system directory structure on host.
423         createSystemTzVersionFileOnHost(VALID_RULES_VERSION);
424 
425         // Set up the /data directory structure on host.
426         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
427         byte[] distroWithoutAVersionFileBytes = createValidDistroBuilder()
428                 .clearVersionForTests()
429                 .buildUnvalidatedBytes();
430         unpackOnHost(dataCurrentDir, distroWithoutAVersionFileBytes);
431 
432         // Push the host test directory and contents to the device.
433         pushHostTestDirToDevice();
434 
435         // Execute tzdatacheck and check the status code.
436         assertEquals(3, runTzDataCheckOnDevice());
437 
438         // Assert the current data directory was deleted.
439         assertDevicePathDoesNotExist(dataCurrentDir);
440     }
441 
442     // {dataDir}/current exists but the distro version file is short.
testShortDataDirDistroVersionFile()443     public void testShortDataDirDistroVersionFile() throws Exception {
444         // Set up the /system directory structure on host.
445         createSystemTzVersionFileOnHost(VALID_RULES_VERSION);
446 
447         // Set up the /data directory structure on host.
448         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
449         unpackOnHost(dataCurrentDir, createValidDistroBuilder().buildBytes());
450         // Replace the distro version file with a short file.
451         Path distroVersionFile =
452                 dataCurrentDir.hostPath.resolve(TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
453         assertHostFileExists(distroVersionFile);
454         Files.write(distroVersionFile, new byte[3]);
455 
456         // Push the host test directory and contents to the device.
457         pushHostTestDirToDevice();
458 
459         // Execute tzdatacheck and check the status code.
460         assertEquals(3, runTzDataCheckOnDevice());
461 
462         // Assert the current data directory was deleted.
463         assertDevicePathDoesNotExist(dataCurrentDir);
464     }
465 
466     // {dataDir}/current exists and the distro version file is long enough, but contains junk.
testCorruptDistroVersionFile()467     public void testCorruptDistroVersionFile() throws Exception {
468         // Set up the /system directory structure on host.
469         createSystemTzVersionFileOnHost(VALID_RULES_VERSION);
470 
471         // Set up the /data directory structure on host.
472         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
473         unpackOnHost(dataCurrentDir, createValidDistroBuilder().buildBytes());
474 
475         // Replace the distro version file with junk.
476         Path distroVersionFile =
477                 dataCurrentDir.hostPath.resolve(TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
478         assertHostFileExists(distroVersionFile);
479 
480         int fileLength = (int) Files.size(distroVersionFile);
481         byte[] junkArray = new byte[fileLength]; // all zeros
482         Files.write(distroVersionFile, junkArray);
483 
484         // Push the host test directory and contents to the device.
485         pushHostTestDirToDevice();
486 
487         // Execute tzdatacheck and check the status code.
488         assertEquals(4, runTzDataCheckOnDevice());
489 
490         // Assert the current data directory was deleted.
491         assertDevicePathDoesNotExist(dataCurrentDir);
492     }
493 
494     // {dataDir}/current exists but the distro version is incorrect.
testInvalidMajorDistroVersion_older()495     public void testInvalidMajorDistroVersion_older() throws Exception {
496         // Set up the /system directory structure on host.
497         createSystemTzVersionFileOnHost(VALID_RULES_VERSION);
498 
499         // Set up the /data directory structure on host.
500         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
501         DistroVersion oldMajorDistroVersion = new DistroVersion(
502                 TzDataSetVersion.currentFormatMajorVersion() - 1, 1, VALID_RULES_VERSION, 1);
503         byte[] distroBytes = createValidDistroBuilder()
504                 .setDistroVersion(oldMajorDistroVersion)
505                 .buildBytes();
506         unpackOnHost(dataCurrentDir, distroBytes);
507 
508         // Push the host test directory and contents to the device.
509         pushHostTestDirToDevice();
510 
511         // Execute tzdatacheck and check the status code.
512         assertEquals(5, runTzDataCheckOnDevice());
513 
514         // Assert the current data directory was deleted.
515         assertDevicePathDoesNotExist(dataCurrentDir);
516     }
517 
518     // {dataDir}/current exists but the distro version is incorrect.
testInvalidMajorDistroVersion_newer()519     public void testInvalidMajorDistroVersion_newer() throws Exception {
520         // Set up the /system directory structure on host.
521         createSystemTzVersionFileOnHost(VALID_RULES_VERSION);
522 
523         // Set up the /data directory structure on host.
524         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
525         DistroVersion newMajorDistroVersion = new DistroVersion(
526                 TzDataSetVersion.currentFormatMajorVersion() + 1,
527                 TzDataSetVersion.currentFormatMinorVersion(),
528                 VALID_RULES_VERSION, VALID_REVISION);
529         byte[] distroBytes = createValidDistroBuilder()
530                 .setDistroVersion(newMajorDistroVersion)
531                 .buildBytes();
532         unpackOnHost(dataCurrentDir, distroBytes);
533 
534         // Push the host test directory and contents to the device.
535         pushHostTestDirToDevice();
536 
537         // Execute tzdatacheck and check the status code.
538         assertEquals(5, runTzDataCheckOnDevice());
539 
540         // Assert the current data directory was deleted.
541         assertDevicePathDoesNotExist(dataCurrentDir);
542     }
543 
544     // {dataDir}/current exists but the distro version is incorrect.
testInvalidMinorDistroVersion_older()545     public void testInvalidMinorDistroVersion_older() throws Exception {
546         // Set up the /system directory structure on host.
547         createSystemTzVersionFileOnHost(VALID_RULES_VERSION);
548 
549         // Set up the /data directory structure on host.
550         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
551         DistroVersion oldMinorDistroVersion = new DistroVersion(
552                 TzDataSetVersion.currentFormatMajorVersion(),
553                 TzDataSetVersion.currentFormatMinorVersion() - 1,
554                 VALID_RULES_VERSION, 1);
555         byte[] distroBytes = createValidDistroBuilder()
556                 .setDistroVersion(oldMinorDistroVersion)
557                 .buildBytes();
558         unpackOnHost(dataCurrentDir, distroBytes);
559 
560         // Push the host test directory and contents to the device.
561         pushHostTestDirToDevice();
562 
563         // Execute tzdatacheck and check the status code.
564         assertEquals(5, runTzDataCheckOnDevice());
565 
566         // Assert the current data directory was deleted.
567         assertDevicePathDoesNotExist(dataCurrentDir);
568     }
569 
570     // {dataDir}/current exists but the distro version is newer (which is accepted because it should
571     // be backwards compatible).
testValidMinorDistroVersion_newer()572     public void testValidMinorDistroVersion_newer() throws Exception {
573         // Set up the /system directory structure on host.
574         createSystemTzVersionFileOnHost(VALID_RULES_VERSION);
575 
576         // Set up the /data directory structure on host.
577         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
578         DistroVersion newMajorDistroVersion = new DistroVersion(
579                 TzDataSetVersion.currentFormatMajorVersion(),
580                 TzDataSetVersion.currentFormatMinorVersion() + 1,
581                 VALID_RULES_VERSION, VALID_REVISION);
582         byte[] distroBytes = createValidDistroBuilder()
583                 .setDistroVersion(newMajorDistroVersion)
584                 .buildBytes();
585         unpackOnHost(dataCurrentDir, distroBytes);
586 
587         // Push the host test directory and contents to the device.
588         pushHostTestDirToDevice();
589 
590         // Execute tzdatacheck and check the status code.
591         assertEquals(0, runTzDataCheckOnDevice());
592 
593         // Assert the current data directory was not touched.
594         assertDeviceDirContainsDistro(dataCurrentDir, distroBytes);
595     }
596 
597     // {dataDir}/current is valid but the tz_version file in /system is missing.
testSystemTzVersionFileMissing()598     public void testSystemTzVersionFileMissing() throws Exception {
599         // Deliberately not writing anything in /system here.
600 
601         // Set up the /data directory structure on host.
602         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
603         byte[] validDistroBytes = createValidDistroBuilder().buildBytes();
604         unpackOnHost(dataCurrentDir, validDistroBytes);
605 
606         // Push the host test directory and contents to the device.
607         pushHostTestDirToDevice();
608 
609         // Execute tzdatacheck and check the status code.
610         assertEquals(6, runTzDataCheckOnDevice());
611 
612         // Assert the current data directory was not touched.
613         assertDeviceDirContainsDistro(dataCurrentDir, validDistroBytes);
614     }
615 
616     // {dataDir}/current is valid but the tz_version file in /system is junk.
testSystemTzVersionFileCorrupt()617     public void testSystemTzVersionFileCorrupt() throws Exception {
618         // Set up the /system directory structure on host.
619         byte[] invalidTzDataBytes = new byte[20];
620         Files.write(mSystemDir.hostPath.resolve(SYSTEM_TZ_VERSION_FILE_NAME), invalidTzDataBytes);
621 
622         // Set up the /data directory structure on host.
623         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
624         byte[] validDistroBytes = createValidDistroBuilder().buildBytes();
625         unpackOnHost(dataCurrentDir, validDistroBytes);
626 
627         // Push the host test directory and contents to the device.
628         pushHostTestDirToDevice();
629 
630         // Execute tzdatacheck and check the status code.
631         assertEquals(7, runTzDataCheckOnDevice());
632 
633         // Assert the current data directory was not touched.
634         assertDeviceDirContainsDistro(dataCurrentDir, validDistroBytes);
635     }
636 
637     // {dataDir}/current is valid and the tz_version file in /system is for older data.
testSystemTzRulesOlder()638     public void testSystemTzRulesOlder() throws Exception {
639         // Set up the /system directory structure on host.
640         createSystemTzVersionFileOnHost(RULES_VERSION_ONE);
641 
642         // Set up the /data directory structure on host.
643         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
644         // Newer than RULES_VERSION_ONE in /system
645         final String distroRulesVersion = RULES_VERSION_TWO;
646         DistroVersion distroVersion = new DistroVersion(
647                 TzDataSetVersion.currentFormatMajorVersion(),
648                 TzDataSetVersion.currentFormatMinorVersion(), distroRulesVersion, VALID_REVISION);
649         byte[] distroBytes = createValidDistroBuilder()
650                 .setDistroVersion(distroVersion)
651                 .setTzDataFile(createValidTzDataBytes(distroRulesVersion))
652                 .buildBytes();
653         unpackOnHost(dataCurrentDir, distroBytes);
654 
655         // Push the host test directory and contents to the device.
656         pushHostTestDirToDevice();
657 
658         // Execute tzdatacheck and check the status code.
659         assertEquals(0, runTzDataCheckOnDevice());
660 
661         // Assert the current data directory was not touched.
662         assertDeviceDirContainsDistro(dataCurrentDir, distroBytes);
663     }
664 
665     // {dataDir}/current is valid and the tz_version file in /system is the same. Data dir should be
666     // kept.
testSystemTzVersionSame()667     public void testSystemTzVersionSame() throws Exception {
668         // Set up the /system directory structure on host.
669         final String systemRulesVersion = VALID_RULES_VERSION;
670         createSystemTzVersionFileOnHost(systemRulesVersion);
671 
672         // Set up the /data directory structure on host.
673         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
674         DistroVersion distroVersion = new DistroVersion(
675                 TzDataSetVersion.currentFormatMajorVersion(),
676                 TzDataSetVersion.currentFormatMinorVersion(),
677                 systemRulesVersion,
678                 VALID_REVISION);
679         byte[] distroBytes = createValidDistroBuilder()
680                 .setDistroVersion(distroVersion)
681                 .setTzDataFile(createValidTzDataBytes(systemRulesVersion))
682                 .buildBytes();
683         unpackOnHost(dataCurrentDir, distroBytes);
684 
685         // Push the host test directory and contents to the device.
686         pushHostTestDirToDevice();
687 
688         // Execute tzdatacheck and check the status code.
689         assertEquals(0, runTzDataCheckOnDevice());
690 
691         // Assert the current data directory was not touched.
692         assertDeviceDirContainsDistro(dataCurrentDir, distroBytes);
693     }
694 
695     // {dataDir}/current is valid and the tzdata file in /system is the newer.
testSystemTzVersionNewer()696     public void testSystemTzVersionNewer() throws Exception {
697         // Set up the /system directory structure on host.
698         String systemRulesVersion = RULES_VERSION_TWO;
699         createSystemTzVersionFileOnHost(systemRulesVersion);
700 
701         // Set up the /data directory structure on host.
702         PathPair dataCurrentDir = mDataDir.createSubPath(CURRENT_DIR_NAME);
703         String distroRulesVersion = RULES_VERSION_ONE; // Older than the system version.
704         DistroVersion distroVersion = new DistroVersion(
705                 TzDataSetVersion.currentFormatMajorVersion(),
706                 TzDataSetVersion.currentFormatMinorVersion(),
707                 distroRulesVersion,
708                 VALID_REVISION);
709         byte[] distroBytes = createValidDistroBuilder()
710                 .setDistroVersion(distroVersion)
711                 .setTzDataFile(createValidTzDataBytes(distroRulesVersion))
712                 .buildBytes();
713         unpackOnHost(dataCurrentDir, distroBytes);
714 
715         // Push the host test directory and contents to the device.
716         pushHostTestDirToDevice();
717 
718         // Execute tzdatacheck and check the status code.
719         assertEquals(0, runTzDataCheckOnDevice());
720 
721         // It is important the dataCurrentDir is deleted in this case - this test case is the main
722         // reason tzdatacheck exists.
723         assertDevicePathDoesNotExist(dataCurrentDir);
724     }
725 
createSystemTzVersionFileOnHost(String systemRulesVersion)726     private void createSystemTzVersionFileOnHost(String systemRulesVersion) throws Exception {
727         byte[] systemTzData = createValidTzVersionBytes(systemRulesVersion);
728         Files.write(mSystemDir.hostPath.resolve(SYSTEM_TZ_VERSION_FILE_NAME), systemTzData);
729     }
730 
createStagedUninstallOnHost(PathPair stagedDir)731     private static void createStagedUninstallOnHost(PathPair stagedDir) throws Exception {
732         createHostDirectory(stagedDir);
733 
734         PathPair uninstallTombstoneFile = stagedDir.createSubPath(UNINSTALL_TOMBSTONE_FILE_NAME);
735         // Create an empty file.
736         new FileOutputStream(uninstallTombstoneFile.hostFile()).close();
737     }
738 
unpackOnHost(PathPair path, byte[] distroBytes)739     private static void unpackOnHost(PathPair path, byte[] distroBytes) throws Exception {
740         createHostDirectory(path);
741         new TimeZoneDistro(distroBytes).extractTo(path.hostFile());
742     }
743 
createValidDistroBuilder()744     private static TimeZoneDistroBuilder createValidDistroBuilder() throws Exception {
745         String distroRulesVersion = VALID_RULES_VERSION;
746         DistroVersion validDistroVersion =
747                 new DistroVersion(
748                         TzDataSetVersion.currentFormatMajorVersion(),
749                         TzDataSetVersion.currentFormatMinorVersion(),
750                         distroRulesVersion, VALID_REVISION);
751         return new TimeZoneDistroBuilder()
752                 .setDistroVersion(validDistroVersion)
753                 .setTzDataFile(createValidTzDataBytes(distroRulesVersion))
754                 .setIcuDataFile(new byte[10]);
755     }
756 
createValidTzDataBytes(String rulesVersion)757     private static byte[] createValidTzDataBytes(String rulesVersion) {
758         return new ZoneInfoTestHelper.TzDataBuilder()
759                 .initializeToValid()
760                 .setHeaderMagic("tzdata" + rulesVersion)
761                 .build();
762     }
763 
createValidTzVersionBytes(String rulesVersion)764     private static byte[] createValidTzVersionBytes(String rulesVersion) throws Exception {
765         return new TzDataSetVersion(
766                 TzDataSetVersion.currentFormatMajorVersion(),
767                 TzDataSetVersion.currentFormatMinorVersion(),
768                 rulesVersion,
769                 VALID_REVISION)
770                 .toBytes();
771     }
772 
runTzDataCheckOnDevice()773     private int runTzDataCheckOnDevice() throws Exception {
774         return runTzDataCheckWithArgs(new String[] { mSystemDir.devicePath, mDataDir.devicePath });
775     }
776 
runTzDataCheckWithArgs(String[] args)777     private int runTzDataCheckWithArgs(String[] args) throws Exception {
778         String command = createTzDataCheckCommand(mDeviceAndroidRootDir, args);
779         return executeCommandOnDeviceWithResultCode(command).statusCode;
780     }
781 
createTzDataCheckCommand(String rootDir, String[] args)782     private static String createTzDataCheckCommand(String rootDir, String[] args) {
783         StringJoiner joiner = new StringJoiner(" ");
784         String tzDataCheckCommand = rootDir + "/bin/tzdatacheck";
785         joiner.add(tzDataCheckCommand);
786         for (String arg : args) {
787             joiner.add(arg);
788         }
789         return joiner.toString();
790     }
791 
assertHostFileExists(Path path)792     private static void assertHostFileExists(Path path) {
793         assertTrue(Files.exists(path));
794     }
795 
executeCommandOnDeviceRaw(String command)796     private String executeCommandOnDeviceRaw(String command) throws DeviceNotAvailableException {
797         return getDevice().executeShellCommand(command);
798     }
799 
createDeviceDirectory(PathPair dir)800     private void createDeviceDirectory(PathPair dir) throws DeviceNotAvailableException {
801         executeCommandOnDeviceRaw("mkdir -p " + dir.devicePath);
802     }
803 
createHostDirectory(PathPair dir)804     private static void createHostDirectory(PathPair dir) throws Exception {
805         Files.createDirectory(dir.hostPath);
806     }
807 
808     private static class ShellResult {
809         final String output;
810         final int statusCode;
811 
ShellResult(String output, int statusCode)812         private ShellResult(String output, int statusCode) {
813             this.output = output;
814             this.statusCode = statusCode;
815         }
816     }
817 
executeCommandOnDeviceWithResultCode(String command)818     private ShellResult executeCommandOnDeviceWithResultCode(String command) throws Exception {
819         // A file to hold the script we're going to create.
820         PathPair scriptFile = mTestRootDir.createSubPath("script.sh");
821         // A file to hold the output of the script.
822         PathPair scriptOut = mTestRootDir.createSubPath("script.out");
823 
824         // The content of the script. Runs the command, capturing stdout and stderr to scriptOut
825         // and printing the result code.
826         String hostScriptContent = command + " > " + scriptOut.devicePath + " 2>&1 ; echo -n $?";
827 
828         // Parse and return the result.
829         try {
830             Files.write(scriptFile.hostPath, hostScriptContent.getBytes(StandardCharsets.US_ASCII));
831 
832             // Push the script to the device.
833             pushFile(scriptFile);
834 
835             // Execute the script using "sh".
836             String execCommandUnderShell =
837                     mDeviceAndroidRootDir + "/bin/sh " + scriptFile.devicePath;
838             String resultCodeString = executeCommandOnDeviceRaw(execCommandUnderShell);
839 
840             // Pull back scriptOut to the host and read the content.
841             pullFile(scriptOut);
842             byte[] outputBytes = Files.readAllBytes(scriptOut.hostPath);
843             String output = new String(outputBytes, StandardCharsets.US_ASCII);
844 
845             int resultCode;
846             try {
847                 resultCode = Integer.parseInt(resultCodeString);
848             } catch (NumberFormatException e) {
849                 fail("Command: " + command
850                         + " returned a non-integer: \"" + resultCodeString + "\""
851                         + ", output=\"" + output + "\"");
852                 return null;
853             }
854             return new ShellResult(output, resultCode);
855         } finally {
856             deleteDeviceFile(scriptFile, false /* failOnError */);
857             deleteDeviceFile(scriptOut, false /* failOnError */);
858             deleteHostFile(scriptFile, false /* failOnError */);
859             deleteHostFile(scriptOut, false /* failOnError */);
860         }
861     }
862 
pushHostTestDirToDevice()863     private void pushHostTestDirToDevice() throws Exception {
864         assertTrue(getDevice().pushDir(mTestRootDir.hostFile(), mTestRootDir.devicePath));
865     }
866 
pullFile(PathPair file)867     private void pullFile(PathPair file) throws DeviceNotAvailableException {
868         assertTrue("Could not pull file " + file.devicePath + " to " + file.hostFile(),
869                 getDevice().pullFile(file.devicePath, file.hostFile()));
870     }
871 
pushFile(PathPair file)872     private void pushFile(PathPair file) throws DeviceNotAvailableException {
873         assertTrue("Could not push file " + file.hostFile() + " to " + file.devicePath,
874                 getDevice().pushFile(file.hostFile(), file.devicePath));
875     }
876 
deleteHostFile(PathPair file, boolean failOnError)877     private void deleteHostFile(PathPair file, boolean failOnError) {
878         try {
879             Files.deleteIfExists(file.hostPath);
880         } catch (IOException e) {
881             if (failOnError) {
882                 fail(e);
883             }
884         }
885     }
886 
deleteDeviceDirectory(PathPair dir, boolean failOnError)887     private void deleteDeviceDirectory(PathPair dir, boolean failOnError)
888             throws DeviceNotAvailableException {
889         String deviceDir = dir.devicePath;
890         try {
891             executeCommandOnDeviceRaw("rm -r " + deviceDir);
892         } catch (Exception e) {
893             if (failOnError) {
894                 throw deviceFail(e);
895             }
896         }
897     }
898 
deleteDeviceFile(PathPair file, boolean failOnError)899     private void deleteDeviceFile(PathPair file, boolean failOnError)
900             throws DeviceNotAvailableException {
901         try {
902             assertDevicePathIsFile(file);
903             executeCommandOnDeviceRaw("rm " + file.devicePath);
904         } catch (Exception e) {
905             if (failOnError) {
906                 throw deviceFail(e);
907             }
908         }
909     }
910 
deleteHostDirectory(PathPair dir, final boolean failOnError)911     private static void deleteHostDirectory(PathPair dir, final boolean failOnError) {
912         Path hostPath = dir.hostPath;
913         if (Files.exists(hostPath)) {
914             Consumer<Path> pathConsumer = file -> {
915                 try {
916                     Files.delete(file);
917                 } catch (Exception e) {
918                     if (failOnError) {
919                         fail(e);
920                     }
921                 }
922             };
923 
924             try {
925                 Files.walk(hostPath).sorted(Comparator.reverseOrder()).forEach(pathConsumer);
926             } catch (IOException e) {
927                 fail(e);
928             }
929         }
930     }
931 
assertDeviceFileExists(String s)932     private void assertDeviceFileExists(String s) throws DeviceNotAvailableException {
933         assertTrue(getDevice().doesFileExist(s));
934     }
935 
assertDevicePathExists(PathPair path)936     private void assertDevicePathExists(PathPair path) throws DeviceNotAvailableException {
937         assertDeviceFileExists(path.devicePath);
938     }
939 
assertDeviceDirContainsDistro(PathPair distroPath, byte[] expectedDistroBytes)940     private void assertDeviceDirContainsDistro(PathPair distroPath, byte[] expectedDistroBytes)
941             throws Exception {
942         // Pull back just the version file and compare it.
943         File localFile = mTestRootDir.createSubPath("temp.file").hostFile();
944         try {
945             String remoteVersionFile = distroPath.devicePath + "/"
946                     + TimeZoneDistro.DISTRO_VERSION_FILE_NAME;
947             assertTrue("Could not pull file " + remoteVersionFile + " to " + localFile,
948                     getDevice().pullFile(remoteVersionFile, localFile));
949 
950             byte[] bytes = Files.readAllBytes(localFile.toPath());
951             assertArrayEquals(bytes,
952                     new TimeZoneDistro(expectedDistroBytes).getDistroVersion().toBytes());
953         } finally {
954             localFile.delete();
955         }
956     }
957 
assertDevicePathDoesNotExist(PathPair path)958     private void assertDevicePathDoesNotExist(PathPair path) throws DeviceNotAvailableException {
959         assertFalse(getDevice().doesFileExist(path.devicePath));
960     }
961 
assertDevicePathIsFile(PathPair path)962     private void assertDevicePathIsFile(PathPair path) throws DeviceNotAvailableException {
963         // This check cannot rely on getDevice().getFile(devicePath).isDirectory() here because that
964         // requires that the user has rights to list all files beneath each and every directory in
965         // the path. That is not the case for the shell user and the /data and /data/local
966         // directories. http://b/35753041.
967         String output = executeCommandOnDeviceRaw("stat -c %F " + path.devicePath);
968         assertTrue(path.devicePath + " not a file. Received: " + output,
969                 output.startsWith("regular") && output.endsWith("file\n"));
970     }
971 
deviceFail(Exception e)972     private static DeviceNotAvailableException deviceFail(Exception e)
973             throws DeviceNotAvailableException {
974         if (e instanceof DeviceNotAvailableException) {
975             throw (DeviceNotAvailableException) e;
976         }
977         fail(e);
978         return null;
979     }
980 
fail(Exception e)981     private static void fail(Exception e) {
982         e.printStackTrace();
983         fail(e.getMessage());
984     }
985 
986     /** A path that has equivalents on both host and device. */
987     private static class PathPair {
988         private final Path hostPath;
989         private final String devicePath;
990 
PathPair(Path hostPath, String devicePath)991         PathPair(Path hostPath, String devicePath) {
992             this.hostPath = hostPath;
993             this.devicePath = devicePath;
994         }
995 
hostFile()996         File hostFile() {
997             return hostPath.toFile();
998         }
999 
createSubPath(String s)1000         PathPair createSubPath(String s) {
1001             return new PathPair(hostPath.resolve(s), devicePath + "/" + s);
1002         }
1003     }
1004 }
1005