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.DistroVersion;
19import com.android.timezone.distro.FileUtils;
20import com.android.timezone.distro.StagedDistroOperation;
21import com.android.timezone.distro.TimeZoneDistro;
22import com.android.timezone.distro.tools.TimeZoneDistroBuilder;
23
24import junit.framework.TestCase;
25
26import java.io.ByteArrayOutputStream;
27import java.io.File;
28import java.io.FileOutputStream;
29import java.io.IOException;
30import java.nio.file.FileVisitResult;
31import java.nio.file.FileVisitor;
32import java.nio.file.Files;
33import java.nio.file.Path;
34import java.nio.file.SimpleFileVisitor;
35import java.nio.file.attribute.BasicFileAttributes;
36import java.util.ArrayList;
37import java.util.List;
38import java.util.zip.ZipEntry;
39import java.util.zip.ZipOutputStream;
40import libcore.io.IoUtils;
41import libcore.tzdata.testing.ZoneInfoTestHelper;
42
43import static org.junit.Assert.assertArrayEquals;
44
45/**
46 * Tests for {@link TimeZoneDistroInstaller}.
47 */
48public class TimeZoneDistroInstallerTest extends TestCase {
49
50    // OLDER_RULES_VERSION < SYSTEM_RULES_VERSION < NEW_RULES_VERSION < NEWER_RULES_VERSION
51    private static final String OLDER_RULES_VERSION = "2030a";
52    private static final String SYSTEM_RULES_VERSION = "2030b";
53    private static final String NEW_RULES_VERSION = "2030c";
54    private static final String NEWER_RULES_VERSION = "2030d";
55
56    private TimeZoneDistroInstaller installer;
57    private File tempDir;
58    private File testInstallDir;
59    private File testSystemTzDataDir;
60
61    @Override
62    public void setUp() throws Exception {
63        super.setUp();
64        tempDir = createUniqueDirectory(null, "tempDir");
65        testInstallDir = createSubDirectory(tempDir, "testInstall");
66        testSystemTzDataDir =  createSubDirectory(tempDir, "testSystemTzData");
67
68        // Create a file to represent the tzdata file in the /system partition of the device.
69        File testSystemTzDataFile = new File(testSystemTzDataDir, "tzdata");
70        byte[] systemTzDataBytes = createTzData(SYSTEM_RULES_VERSION);
71        createFile(testSystemTzDataFile, systemTzDataBytes);
72
73        installer = new TimeZoneDistroInstaller(
74                "TimeZoneDistroInstallerTest", testSystemTzDataFile, testInstallDir);
75    }
76
77    /**
78     * Creates a unique temporary directory. rootDir can be null, in which case the directory will
79     * be created beneath the directory pointed to by the java.io.tmpdir system property.
80     */
81    private static File createUniqueDirectory(File rootDir, String prefix) throws Exception {
82        File dir = File.createTempFile(prefix, "", rootDir);
83        assertTrue(dir.delete());
84        assertTrue(dir.mkdir());
85        return dir;
86    }
87
88    private static File createSubDirectory(File parent, String subDirName) {
89        File dir = new File(parent, subDirName);
90        assertTrue(dir.mkdir());
91        return dir;
92    }
93
94    @Override
95    public void tearDown() throws Exception {
96        if (tempDir.exists()) {
97            FileUtils.deleteRecursive(tempDir);
98        }
99        super.tearDown();
100    }
101
102    /** Tests the an update on a device will fail if the /system tzdata file cannot be found. */
103    public void testStageInstallWithErrorCode_badSystemFile() throws Exception {
104        File doesNotExist = new File(testSystemTzDataDir, "doesNotExist");
105        TimeZoneDistroInstaller brokenSystemInstaller = new TimeZoneDistroInstaller(
106                "TimeZoneDistroInstallerTest", doesNotExist, testInstallDir);
107        byte[] distroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
108
109        try {
110            brokenSystemInstaller.stageInstallWithErrorCode(new TimeZoneDistro(distroBytes));
111            fail();
112        } catch (IOException expected) {}
113
114        assertNoDistroOperationStaged();
115        assertNoInstalledDistro();
116    }
117
118    /** Tests the first successful update on a device */
119    public void testStageInstallWithErrorCode_successfulFirstUpdate() throws Exception {
120        byte[] distroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
121
122        assertEquals(
123                TimeZoneDistroInstaller.INSTALL_SUCCESS,
124                installer.stageInstallWithErrorCode(new TimeZoneDistro(distroBytes)));
125        assertInstallDistroStaged(distroBytes);
126        assertNoInstalledDistro();
127    }
128
129    /**
130     * Tests we can install an update the same version as is in /system.
131     */
132    public void testStageInstallWithErrorCode_successfulFirstUpdate_sameVersionAsSystem()
133            throws Exception {
134        byte[] distroBytes = createValidTimeZoneDistroBytes(SYSTEM_RULES_VERSION, 1);
135        assertEquals(
136                TimeZoneDistroInstaller.INSTALL_SUCCESS,
137                installer.stageInstallWithErrorCode(new TimeZoneDistro(distroBytes)));
138        assertInstallDistroStaged(distroBytes);
139        assertNoInstalledDistro();
140    }
141
142    /**
143     * Tests we cannot install an update older than the version in /system.
144     */
145    public void testStageInstallWithErrorCode_unsuccessfulFirstUpdate_olderVersionThanSystem()
146            throws Exception {
147        byte[] distroBytes = createValidTimeZoneDistroBytes(OLDER_RULES_VERSION, 1);
148        assertEquals(
149                TimeZoneDistroInstaller.INSTALL_FAIL_RULES_TOO_OLD,
150                installer.stageInstallWithErrorCode(new TimeZoneDistro(distroBytes)));
151        assertNoDistroOperationStaged();
152        assertNoInstalledDistro();
153    }
154
155    /**
156     * Tests an update on a device when there is a prior update already staged.
157     */
158    public void testStageInstallWithErrorCode_successfulFollowOnUpdate_newerVersion()
159            throws Exception {
160        byte[] distro1Bytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
161        assertEquals(
162                TimeZoneDistroInstaller.INSTALL_SUCCESS,
163                installer.stageInstallWithErrorCode(new TimeZoneDistro(distro1Bytes)));
164        assertInstallDistroStaged(distro1Bytes);
165
166        byte[] distro2Bytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 2);
167        assertEquals(
168                TimeZoneDistroInstaller.INSTALL_SUCCESS,
169                installer.stageInstallWithErrorCode(new TimeZoneDistro(distro2Bytes)));
170        assertInstallDistroStaged(distro2Bytes);
171
172        byte[] distro3Bytes = createValidTimeZoneDistroBytes(NEWER_RULES_VERSION, 1);
173        assertEquals(
174                TimeZoneDistroInstaller.INSTALL_SUCCESS,
175                installer.stageInstallWithErrorCode(new TimeZoneDistro(distro3Bytes)));
176        assertInstallDistroStaged(distro3Bytes);
177        assertNoInstalledDistro();
178    }
179
180    /**
181     * Tests an update on a device when there is a prior update already applied, but the follow
182     * on update is older than in /system.
183     */
184    public void testStageInstallWithErrorCode_unsuccessfulFollowOnUpdate_olderVersion()
185            throws Exception {
186        byte[] distro1Bytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 2);
187        assertEquals(
188                TimeZoneDistroInstaller.INSTALL_SUCCESS,
189                installer.stageInstallWithErrorCode(new TimeZoneDistro(distro1Bytes)));
190        assertInstallDistroStaged(distro1Bytes);
191
192        byte[] distro2Bytes = createValidTimeZoneDistroBytes(OLDER_RULES_VERSION, 1);
193        assertEquals(
194                TimeZoneDistroInstaller.INSTALL_FAIL_RULES_TOO_OLD,
195                installer.stageInstallWithErrorCode(new TimeZoneDistro(distro2Bytes)));
196        assertInstallDistroStaged(distro1Bytes);
197        assertNoInstalledDistro();
198    }
199
200    /**
201     * Tests staging an update when there's already an uninstall staged still results in a staged
202     * install.
203     */
204    public void testStageInstallWithErrorCode_existingStagedUninstall()
205            throws Exception {
206        byte[] distro1Bytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
207        simulateInstalledDistro(distro1Bytes);
208        assertInstalledDistro(distro1Bytes);
209
210        assertEquals(TimeZoneDistroInstaller.UNINSTALL_SUCCESS, installer.stageUninstall());
211        assertDistroUninstallStaged();
212        assertInstalledDistro(distro1Bytes);
213
214        byte[] distro2Bytes = createValidTimeZoneDistroBytes(NEWER_RULES_VERSION, 1);
215        assertEquals(
216                TimeZoneDistroInstaller.INSTALL_SUCCESS,
217                installer.stageInstallWithErrorCode(new TimeZoneDistro(distro2Bytes)));
218        assertInstalledDistro(distro1Bytes);
219        assertInstallDistroStaged(distro2Bytes);
220    }
221
222    /** Tests that a distro with a missing tzdata file will not update the content. */
223    public void testStageInstallWithErrorCode_missingTzDataFile() throws Exception {
224        byte[] stagedDistroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
225        assertEquals(
226                TimeZoneDistroInstaller.INSTALL_SUCCESS,
227                installer.stageInstallWithErrorCode(new TimeZoneDistro(stagedDistroBytes)));
228        assertInstallDistroStaged(stagedDistroBytes);
229
230        byte[] incompleteDistroBytes =
231                createValidTimeZoneDistroBuilder(NEWER_RULES_VERSION, 1)
232                        .clearTzDataForTests()
233                        .buildUnvalidatedBytes();
234        assertEquals(
235                TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
236                installer.stageInstallWithErrorCode(new TimeZoneDistro(incompleteDistroBytes)));
237        assertInstallDistroStaged(stagedDistroBytes);
238        assertNoInstalledDistro();
239    }
240
241    /** Tests that a distro with a missing ICU file will not update the content. */
242    public void testStageInstallWithErrorCode_missingIcuFile() throws Exception {
243        byte[] stagedDistroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
244        assertEquals(
245                TimeZoneDistroInstaller.INSTALL_SUCCESS,
246                installer.stageInstallWithErrorCode(new TimeZoneDistro(stagedDistroBytes)));
247        assertInstallDistroStaged(stagedDistroBytes);
248
249        byte[] incompleteDistroBytes =
250                createValidTimeZoneDistroBuilder(NEWER_RULES_VERSION, 1)
251                        .clearIcuDataForTests()
252                        .buildUnvalidatedBytes();
253        assertEquals(
254                TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
255                installer.stageInstallWithErrorCode(new TimeZoneDistro(incompleteDistroBytes)));
256        assertInstallDistroStaged(stagedDistroBytes);
257        assertNoInstalledDistro();
258    }
259
260    /** Tests that a distro with a missing tzlookup file will not update the content. */
261    public void testStageInstallWithErrorCode_missingTzLookupFile() throws Exception {
262        byte[] stagedDistroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
263        assertEquals(
264                TimeZoneDistroInstaller.INSTALL_SUCCESS,
265                installer.stageInstallWithErrorCode(new TimeZoneDistro(stagedDistroBytes)));
266        assertInstallDistroStaged(stagedDistroBytes);
267
268        byte[] incompleteDistroBytes =
269                createValidTimeZoneDistroBuilder(NEWER_RULES_VERSION, 1)
270                        .setTzLookupXml(null)
271                        .buildUnvalidatedBytes();
272        assertEquals(
273                TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
274                installer.stageInstallWithErrorCode(new TimeZoneDistro(incompleteDistroBytes)));
275        assertInstallDistroStaged(stagedDistroBytes);
276        assertNoInstalledDistro();
277    }
278
279    /** Tests that a distro with a bad tzlookup file will not update the content. */
280    public void testStageInstallWithErrorCode_badTzLookupFile() throws Exception {
281        byte[] stagedDistroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
282        assertEquals(
283                TimeZoneDistroInstaller.INSTALL_SUCCESS,
284                installer.stageInstallWithErrorCode(new TimeZoneDistro(stagedDistroBytes)));
285        assertInstallDistroStaged(stagedDistroBytes);
286
287        byte[] incompleteDistroBytes =
288                createValidTimeZoneDistroBuilder(NEWER_RULES_VERSION, 1)
289                        .setTzLookupXml("<foo />")
290                        .buildUnvalidatedBytes();
291        assertEquals(
292                TimeZoneDistroInstaller.INSTALL_FAIL_VALIDATION_ERROR,
293                installer.stageInstallWithErrorCode(new TimeZoneDistro(incompleteDistroBytes)));
294        assertInstallDistroStaged(stagedDistroBytes);
295        assertNoInstalledDistro();
296    }
297
298    /**
299     * Tests that an update will be unpacked even if there is a partial update from a previous run.
300     */
301    public void testStageInstallWithErrorCode_withWorkingDir() throws Exception {
302        File workingDir = installer.getWorkingDir();
303        assertTrue(workingDir.mkdir());
304        createFile(new File(workingDir, "myFile"), new byte[] { 'a' });
305
306        byte[] distroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
307        assertEquals(
308                TimeZoneDistroInstaller.INSTALL_SUCCESS,
309                installer.stageInstallWithErrorCode(new TimeZoneDistro(distroBytes)));
310        assertInstallDistroStaged(distroBytes);
311        assertNoInstalledDistro();
312    }
313
314    /**
315     * Tests that a distro without a distro version file will be rejected.
316     */
317    public void testStageInstallWithErrorCode_withMissingDistroVersionFile() throws Exception {
318        // Create a distro without a version file.
319        byte[] distroBytes = createValidTimeZoneDistroBuilder(NEW_RULES_VERSION, 1)
320                .clearVersionForTests()
321                .buildUnvalidatedBytes();
322        assertEquals(
323                TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
324                installer.stageInstallWithErrorCode(new TimeZoneDistro(distroBytes)));
325        assertNoDistroOperationStaged();
326        assertNoInstalledDistro();
327    }
328
329    /**
330     * Tests that a distro with an newer distro version will be rejected.
331     */
332    public void testStageInstallWithErrorCode_withNewerDistroVersion() throws Exception {
333        // Create a distro that will appear to be newer than the one currently supported.
334        byte[] distroBytes = createValidTimeZoneDistroBuilder(NEW_RULES_VERSION, 1)
335                .replaceFormatVersionForTests(2, 1)
336                .buildUnvalidatedBytes();
337        assertEquals(
338                TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_FORMAT_VERSION,
339                installer.stageInstallWithErrorCode(new TimeZoneDistro(distroBytes)));
340        assertNoDistroOperationStaged();
341        assertNoInstalledDistro();
342    }
343
344    /**
345     * Tests that a distro with a badly formed distro version will be rejected.
346     */
347    public void testStageInstallWithErrorCode_withBadlyFormedDistroVersion() throws Exception {
348        // Create a distro that has an invalid major distro version. It should be 3 numeric
349        // characters, "." and 3 more numeric characters.
350        DistroVersion validDistroVersion = new DistroVersion(1, 1, NEW_RULES_VERSION, 1);
351        byte[] invalidFormatVersionBytes = validDistroVersion.toBytes();
352        invalidFormatVersionBytes[0] = 'A';
353
354        TimeZoneDistro distro = createTimeZoneDistroWithVersionBytes(invalidFormatVersionBytes);
355        assertEquals(
356                TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
357                installer.stageInstallWithErrorCode(distro));
358        assertNoDistroOperationStaged();
359        assertNoInstalledDistro();
360    }
361
362    /**
363     * Tests that a distro with a badly formed revision will be rejected.
364     */
365    public void testStageInstallWithErrorCode_withBadlyFormedRevision() throws Exception {
366        // Create a distro that has an invalid revision. It should be 3 numeric characters.
367        DistroVersion validDistroVersion = new DistroVersion(1, 1, NEW_RULES_VERSION, 1);
368        byte[] invalidRevisionBytes = validDistroVersion.toBytes();
369        invalidRevisionBytes[invalidRevisionBytes.length - 3] = 'A';
370
371        TimeZoneDistro distro = createTimeZoneDistroWithVersionBytes(invalidRevisionBytes);
372        assertEquals(
373                TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
374                installer.stageInstallWithErrorCode(distro));
375        assertNoDistroOperationStaged();
376        assertNoInstalledDistro();
377    }
378
379    /**
380     * Tests that a distro with a badly formed rules version will be rejected.
381     */
382    public void testStageInstallWithErrorCode_withBadlyFormedRulesVersion() throws Exception {
383        // Create a distro that has an invalid rules version. It should be in the form "2016c".
384        DistroVersion validDistroVersion = new DistroVersion(1, 1, NEW_RULES_VERSION, 1);
385        byte[] invalidRulesVersionBytes = validDistroVersion.toBytes();
386        invalidRulesVersionBytes[invalidRulesVersionBytes.length - 6] = 'B';
387
388        TimeZoneDistro distro = createTimeZoneDistroWithVersionBytes(invalidRulesVersionBytes);
389        assertEquals(
390                TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
391                installer.stageInstallWithErrorCode(distro));
392        assertNoDistroOperationStaged();
393        assertNoInstalledDistro();
394    }
395
396    /** Tests what happens if a stageUninstall() is attempted when there's nothing installed. */
397    public void testStageUninstall_noExistingDistro() throws Exception {
398        // To stage an uninstall, there would need to be installed rules.
399        assertEquals(
400                TimeZoneDistroInstaller.UNINSTALL_NOTHING_INSTALLED,
401                installer.stageUninstall());
402
403        assertNoDistroOperationStaged();
404        assertNoInstalledDistro();
405    }
406
407    /** Tests what happens if a stageUninstall() is attempted when there's something installed. */
408    public void testStageUninstall_existingInstalledDataDistro() throws Exception {
409        // To stage an uninstall, we need to have some installed rules.
410        byte[] installedDistroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
411        simulateInstalledDistro(installedDistroBytes);
412
413        File stagedDataDir = installer.getStagedTzDataDir();
414        assertTrue(stagedDataDir.mkdir());
415
416        assertEquals(
417                TimeZoneDistroInstaller.UNINSTALL_SUCCESS,
418                installer.stageUninstall());
419        assertDistroUninstallStaged();
420        assertInstalledDistro(installedDistroBytes);
421    }
422
423    /**
424     * Tests what happens if a stageUninstall() is attempted when there's something installed
425     * and there's a staged install.
426     */
427    public void testStageUninstall_existingStagedInstall() throws Exception {
428        File stagedDataDir = installer.getStagedTzDataDir();
429        assertTrue(stagedDataDir.mkdir());
430
431        // Stage an install.
432        byte[] distroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
433        assertEquals(TimeZoneDistroInstaller.INSTALL_SUCCESS,
434                installer.stageInstallWithErrorCode(new TimeZoneDistro(distroBytes)));
435
436        // Now uninstall. It should just remove the staged install.
437        assertEquals(
438                TimeZoneDistroInstaller.UNINSTALL_NOTHING_INSTALLED,
439                installer.stageUninstall());
440        assertNoDistroOperationStaged();
441    }
442
443    /**
444     * Tests what happens if a stageUninstall() is attempted when there's something installed
445     * and there's a staged uninstall.
446     */
447    public void testStageUninstall_existingStagedUninstall() throws Exception {
448        // To stage an uninstall, we need to have some installed rules.
449        byte[] installedDistroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
450        simulateInstalledDistro(installedDistroBytes);
451
452        File stagedDataDir = installer.getStagedTzDataDir();
453        assertTrue(stagedDataDir.mkdir());
454
455        assertEquals(
456                TimeZoneDistroInstaller.UNINSTALL_SUCCESS,
457                installer.stageUninstall());
458        assertDistroUninstallStaged();
459        assertInstalledDistro(installedDistroBytes);
460
461        // Now stage a second uninstall.
462        assertEquals(
463                TimeZoneDistroInstaller.UNINSTALL_SUCCESS,
464                installer.stageUninstall());
465        assertDistroUninstallStaged();
466        assertInstalledDistro(installedDistroBytes);
467    }
468
469    /**
470     * Tests what happens if a stageUninstall() is attempted when there are unexpected working
471     * directories present.
472     */
473    public void testStageUninstall_oldDirsAlreadyExists() throws Exception {
474        // To stage an uninstall, we need to have some installed rules.
475        byte[] installedDistroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
476        simulateInstalledDistro(installedDistroBytes);
477
478        File oldStagedDataDir = installer.getOldStagedDataDir();
479        assertTrue(oldStagedDataDir.mkdir());
480
481        File workingDir = installer.getWorkingDir();
482        assertTrue(workingDir.mkdir());
483
484        assertEquals(
485                TimeZoneDistroInstaller.UNINSTALL_SUCCESS,
486                installer.stageUninstall());
487
488        assertDistroUninstallStaged();
489        assertFalse(workingDir.exists());
490        assertFalse(oldStagedDataDir.exists());
491        assertInstalledDistro(installedDistroBytes);
492    }
493
494    public void testGetSystemRulesVersion() throws Exception {
495        assertEquals(SYSTEM_RULES_VERSION, installer.getSystemRulesVersion());
496    }
497
498    public void testGetInstalledDistroVersion() throws Exception {
499        // Check result when nothing installed.
500        assertNull(installer.getInstalledDistroVersion());
501        assertNoDistroOperationStaged();
502        assertNoInstalledDistro();
503
504        // Now simulate there being an existing install active.
505        byte[] distroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
506        simulateInstalledDistro(distroBytes);
507        assertInstalledDistro(distroBytes);
508
509        // Check result when something installed.
510        assertEquals(new TimeZoneDistro(distroBytes).getDistroVersion(),
511                installer.getInstalledDistroVersion());
512        assertNoDistroOperationStaged();
513        assertInstalledDistro(distroBytes);
514    }
515
516    public void testGetStagedDistroOperation() throws Exception {
517        byte[] distro1Bytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
518        byte[] distro2Bytes = createValidTimeZoneDistroBytes(NEWER_RULES_VERSION, 1);
519
520        // Check result when nothing staged.
521        assertNull(installer.getStagedDistroOperation());
522        assertNoDistroOperationStaged();
523        assertNoInstalledDistro();
524
525        // Check result after unsuccessfully staging an uninstall.
526        // Can't stage an uninstall without an installed distro.
527        assertEquals(
528                TimeZoneDistroInstaller.UNINSTALL_NOTHING_INSTALLED,
529                installer.stageUninstall());
530        assertNull(installer.getStagedDistroOperation());
531        assertNoDistroOperationStaged();
532        assertNoInstalledDistro();
533
534        // Check result after staging an install.
535        assertEquals(
536                TimeZoneDistroInstaller.INSTALL_SUCCESS,
537                installer.stageInstallWithErrorCode(new TimeZoneDistro(distro1Bytes)));
538        StagedDistroOperation expectedStagedInstall =
539                StagedDistroOperation.install(new TimeZoneDistro(distro1Bytes).getDistroVersion());
540        assertEquals(expectedStagedInstall, installer.getStagedDistroOperation());
541        assertInstallDistroStaged(distro1Bytes);
542        assertNoInstalledDistro();
543
544        // Check result after unsuccessfully staging an uninstall (but after removing a staged
545        // install). Can't stage an uninstall without an installed distro.
546        assertEquals(
547                TimeZoneDistroInstaller.UNINSTALL_NOTHING_INSTALLED,
548                installer.stageUninstall());
549        assertNull(installer.getStagedDistroOperation());
550        assertNoDistroOperationStaged();
551        assertNoInstalledDistro();
552
553        // Now simulate there being an existing install active.
554        simulateInstalledDistro(distro1Bytes);
555        assertInstalledDistro(distro1Bytes);
556
557        // Check state after successfully staging an uninstall.
558        assertEquals(
559                TimeZoneDistroInstaller.UNINSTALL_SUCCESS,
560                installer.stageUninstall());
561        StagedDistroOperation expectedStagedUninstall = StagedDistroOperation.uninstall();
562        assertEquals(expectedStagedUninstall, installer.getStagedDistroOperation());
563        assertDistroUninstallStaged();
564        assertInstalledDistro(distro1Bytes);
565
566        // Check state after successfully staging an install.
567        assertEquals(TimeZoneDistroInstaller.INSTALL_SUCCESS,
568                installer.stageInstallWithErrorCode(new TimeZoneDistro(distro2Bytes)));
569        StagedDistroOperation expectedStagedInstall2 =
570                StagedDistroOperation.install(new TimeZoneDistro(distro2Bytes).getDistroVersion());
571        assertEquals(expectedStagedInstall2, installer.getStagedDistroOperation());
572        assertInstallDistroStaged(distro2Bytes);
573        assertInstalledDistro(distro1Bytes);
574    }
575
576    private static byte[] createValidTimeZoneDistroBytes(
577            String rulesVersion, int revision) throws Exception {
578        return createValidTimeZoneDistroBuilder(rulesVersion, revision).buildBytes();
579    }
580
581    private static TimeZoneDistroBuilder createValidTimeZoneDistroBuilder(
582            String rulesVersion, int revision) throws Exception {
583
584        byte[] tzData = createTzData(rulesVersion);
585        byte[] icuData = new byte[] { 'a' };
586        String tzlookupXml = "<timezones>\n"
587                + "  <countryzones>\n"
588                + "    <country code=\"us\">\n"
589                + "      <id>America/New_York\"</id>\n"
590                + "      <id>America/Los_Angeles</id>\n"
591                + "    </country>\n"
592                + "    <country code=\"gb\">\n"
593                + "      <id>Europe/London</id>\n"
594                + "    </country>\n"
595                + "  </countryzones>\n"
596                + "</timezones>\n";
597        DistroVersion distroVersion = new DistroVersion(
598                DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
599                DistroVersion.CURRENT_FORMAT_MINOR_VERSION,
600                rulesVersion,
601                revision);
602        return new TimeZoneDistroBuilder()
603                .setDistroVersion(distroVersion)
604                .setTzDataFile(tzData)
605                .setIcuDataFile(icuData)
606                .setTzLookupXml(tzlookupXml);
607    }
608
609    private void assertInstallDistroStaged(byte[] expectedDistroBytes) throws Exception {
610        assertTrue(testInstallDir.exists());
611
612        File stagedTzDataDir = installer.getStagedTzDataDir();
613        assertTrue(stagedTzDataDir.exists());
614
615        File distroVersionFile =
616                new File(stagedTzDataDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
617        assertTrue(distroVersionFile.exists());
618
619        File tzdataFile = new File(stagedTzDataDir, TimeZoneDistro.TZDATA_FILE_NAME);
620        assertTrue(tzdataFile.exists());
621
622        File icuFile = new File(stagedTzDataDir, TimeZoneDistro.ICU_DATA_FILE_NAME);
623        assertTrue(icuFile.exists());
624
625        File tzLookupFile = new File(stagedTzDataDir, TimeZoneDistro.TZLOOKUP_FILE_NAME);
626        assertTrue(tzLookupFile.exists());
627
628        // Assert getStagedDistroState() is reporting correctly.
629        StagedDistroOperation stagedDistroOperation = installer.getStagedDistroOperation();
630        assertNotNull(stagedDistroOperation);
631        assertFalse(stagedDistroOperation.isUninstall);
632        assertEquals(new TimeZoneDistro(expectedDistroBytes).getDistroVersion(),
633                stagedDistroOperation.distroVersion);
634
635        File expectedZipContentDir = createUniqueDirectory(tempDir, "expectedZipContent");
636        new TimeZoneDistro(expectedDistroBytes).extractTo(expectedZipContentDir);
637
638        assertContentsMatches(
639                new File(expectedZipContentDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME),
640                distroVersionFile);
641        assertContentsMatches(
642                new File(expectedZipContentDir, TimeZoneDistro.ICU_DATA_FILE_NAME),
643                icuFile);
644        assertContentsMatches(
645                new File(expectedZipContentDir, TimeZoneDistro.TZDATA_FILE_NAME),
646                tzdataFile);
647        assertContentsMatches(
648                new File(expectedZipContentDir, TimeZoneDistro.TZLOOKUP_FILE_NAME),
649                tzLookupFile);
650        assertFileCount(4, expectedZipContentDir);
651
652        // Also check no working directory is left lying around.
653        File workingDir = installer.getWorkingDir();
654        assertFalse(workingDir.exists());
655    }
656
657    private static void assertFileCount(int expectedFiles, File rootDir) throws Exception {
658        final List<Path> paths = new ArrayList<>();
659        FileVisitor<Path> visitor = new SimpleFileVisitor<Path>() {
660            @Override
661            public FileVisitResult visitFile(Path filePath, BasicFileAttributes attrs)
662                        throws IOException {
663                paths.add(filePath);
664                return FileVisitResult.CONTINUE;
665            }
666        };
667        Files.walkFileTree(rootDir.toPath(), visitor);
668        assertEquals("Found: " + paths, expectedFiles, paths.size());
669    }
670
671    private void assertContentsMatches(File expected, File actual) throws IOException {
672        byte[] actualBytes = IoUtils.readFileAsByteArray(actual.getPath());
673        byte[] expectedBytes = IoUtils.readFileAsByteArray(expected.getPath());
674        assertArrayEquals(expectedBytes, actualBytes);
675    }
676
677    private void assertNoDistroOperationStaged() throws Exception {
678        assertNull(installer.getStagedDistroOperation());
679
680        File stagedTzDataDir = installer.getStagedTzDataDir();
681        assertFalse(stagedTzDataDir.exists());
682
683        // Also check no working directories are left lying around.
684        File workingDir = installer.getWorkingDir();
685        assertFalse(workingDir.exists());
686
687        File oldDataDir = installer.getOldStagedDataDir();
688        assertFalse(oldDataDir.exists());
689    }
690
691    private void assertDistroUninstallStaged() throws Exception {
692        assertEquals(StagedDistroOperation.uninstall(), installer.getStagedDistroOperation());
693
694        File stagedTzDataDir = installer.getStagedTzDataDir();
695        assertTrue(stagedTzDataDir.exists());
696        assertTrue(stagedTzDataDir.isDirectory());
697
698        File uninstallTombstone =
699                new File(stagedTzDataDir, TimeZoneDistroInstaller.UNINSTALL_TOMBSTONE_FILE_NAME);
700        assertTrue(uninstallTombstone.exists());
701        assertTrue(uninstallTombstone.isFile());
702
703        // Also check no working directories are left lying around.
704        File workingDir = installer.getWorkingDir();
705        assertFalse(workingDir.exists());
706
707        File oldDataDir = installer.getOldStagedDataDir();
708        assertFalse(oldDataDir.exists());
709    }
710
711    private void simulateInstalledDistro(byte[] distroBytes) throws Exception {
712        File currentTzDataDir = installer.getCurrentTzDataDir();
713        assertFalse(currentTzDataDir.exists());
714        assertTrue(currentTzDataDir.mkdir());
715        new TimeZoneDistro(distroBytes).extractTo(currentTzDataDir);
716    }
717
718    private void assertNoInstalledDistro() {
719        assertFalse(installer.getCurrentTzDataDir().exists());
720    }
721
722    private void assertInstalledDistro(byte[] distroBytes) throws Exception {
723        File currentTzDataDir = installer.getCurrentTzDataDir();
724        assertTrue(currentTzDataDir.exists());
725        File versionFile = new File(currentTzDataDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
726        assertTrue(versionFile.exists());
727        byte[] expectedVersionBytes = new TimeZoneDistro(distroBytes).getDistroVersion().toBytes();
728        byte[] actualVersionBytes = FileUtils.readBytes(versionFile, expectedVersionBytes.length);
729        assertArrayEquals(expectedVersionBytes, actualVersionBytes);
730    }
731
732    private static byte[] createTzData(String rulesVersion) {
733        return new ZoneInfoTestHelper.TzDataBuilder()
734                .initializeToValid()
735                .setHeaderMagic("tzdata" + rulesVersion)
736                .build();
737    }
738
739    private static void createFile(File file, byte[] bytes) {
740        try (FileOutputStream fos = new FileOutputStream(file)) {
741            fos.write(bytes);
742        } catch (IOException e) {
743            fail(e.getMessage());
744        }
745    }
746
747    /**
748     * Creates a TimeZoneDistro containing arbitrary bytes in the version file. Used for testing
749     * distros with badly formed version info.
750     */
751    private TimeZoneDistro createTimeZoneDistroWithVersionBytes(byte[] versionBytes)
752            throws Exception {
753
754        // Extract a valid distro to a working dir.
755        byte[] distroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
756        File workingDir = createUniqueDirectory(tempDir, "versionBytes");
757        new TimeZoneDistro(distroBytes).extractTo(workingDir);
758
759        // Modify the version file.
760        File versionFile = new File(workingDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
761        assertTrue(versionFile.exists());
762        try (FileOutputStream fos = new FileOutputStream(versionFile, false /* append */)) {
763            fos.write(versionBytes);
764        }
765
766        // Zip the distro back up again.
767        ByteArrayOutputStream baos = new ByteArrayOutputStream();
768        try (ZipOutputStream zos = new ZipOutputStream(baos)) {
769            Path workingDirPath = workingDir.toPath();
770            Files.walkFileTree(workingDirPath, new SimpleFileVisitor<Path>() {
771                @Override
772                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
773                        throws IOException {
774                    byte[] bytes = IoUtils.readFileAsByteArray(file.toString());
775                    String relativeFileName = workingDirPath.relativize(file).toString();
776                    addZipEntry(zos, relativeFileName, bytes);
777                    return FileVisitResult.CONTINUE;
778                }
779            });
780        }
781
782        return new TimeZoneDistro(baos.toByteArray());
783    }
784
785    private static void addZipEntry(ZipOutputStream zos, String name, byte[] content)
786            throws IOException {
787        ZipEntry zipEntry = new ZipEntry(name);
788        zipEntry.setSize(content.length);
789        zos.putNextEntry(zipEntry);
790        zos.write(content);
791        zos.closeEntry();
792    }
793}
794