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(
336                        DistroVersion.CURRENT_FORMAT_MAJOR_VERSION + 1,
337                        DistroVersion.CURRENT_FORMAT_MINOR_VERSION)
338                .buildUnvalidatedBytes();
339        assertEquals(
340                TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_FORMAT_VERSION,
341                installer.stageInstallWithErrorCode(new TimeZoneDistro(distroBytes)));
342        assertNoDistroOperationStaged();
343        assertNoInstalledDistro();
344    }
345
346    /**
347     * Tests that a distro with a badly formed distro version will be rejected.
348     */
349    public void testStageInstallWithErrorCode_withBadlyFormedDistroVersion() throws Exception {
350        // Create a distro that has an invalid major distro version. It should be 3 numeric
351        // characters, "." and 3 more numeric characters.
352        byte[] invalidFormatVersionBytes =
353                createValidTimeZoneDistroBuilder(NEW_RULES_VERSION, 1).buildUnvalidatedBytes();
354        invalidFormatVersionBytes[0] = 'A';
355
356        TimeZoneDistro distro = createTimeZoneDistroWithVersionBytes(invalidFormatVersionBytes);
357        assertEquals(
358                TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
359                installer.stageInstallWithErrorCode(distro));
360        assertNoDistroOperationStaged();
361        assertNoInstalledDistro();
362    }
363
364    /**
365     * Tests that a distro with a badly formed revision will be rejected.
366     */
367    public void testStageInstallWithErrorCode_withBadlyFormedRevision() throws Exception {
368        // Create a distro that has an invalid revision. It should be 3 numeric characters.
369        byte[] invalidRevisionBytes =
370                createValidTimeZoneDistroBuilder(NEW_RULES_VERSION, 1).buildUnvalidatedBytes();
371        invalidRevisionBytes[invalidRevisionBytes.length - 3] = 'A';
372
373        TimeZoneDistro distro = createTimeZoneDistroWithVersionBytes(invalidRevisionBytes);
374        assertEquals(
375                TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
376                installer.stageInstallWithErrorCode(distro));
377        assertNoDistroOperationStaged();
378        assertNoInstalledDistro();
379    }
380
381    /**
382     * Tests that a distro with a badly formed rules version will be rejected.
383     */
384    public void testStageInstallWithErrorCode_withBadlyFormedRulesVersion() throws Exception {
385        // Create a distro that has an invalid rules version. It should be in the form "2016c".
386        byte[] invalidRulesVersionBytes =
387                createValidTimeZoneDistroBuilder(NEW_RULES_VERSION, 1).buildUnvalidatedBytes();
388        invalidRulesVersionBytes[invalidRulesVersionBytes.length - 6] = 'B';
389
390        TimeZoneDistro distro = createTimeZoneDistroWithVersionBytes(invalidRulesVersionBytes);
391        assertEquals(
392                TimeZoneDistroInstaller.INSTALL_FAIL_BAD_DISTRO_STRUCTURE,
393                installer.stageInstallWithErrorCode(distro));
394        assertNoDistroOperationStaged();
395        assertNoInstalledDistro();
396    }
397
398    /** Tests what happens if a stageUninstall() is attempted when there's nothing installed. */
399    public void testStageUninstall_noExistingDistro() throws Exception {
400        // To stage an uninstall, there would need to be installed rules.
401        assertEquals(
402                TimeZoneDistroInstaller.UNINSTALL_NOTHING_INSTALLED,
403                installer.stageUninstall());
404
405        assertNoDistroOperationStaged();
406        assertNoInstalledDistro();
407    }
408
409    /** Tests what happens if a stageUninstall() is attempted when there's something installed. */
410    public void testStageUninstall_existingInstalledDataDistro() throws Exception {
411        // To stage an uninstall, we need to have some installed rules.
412        byte[] installedDistroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
413        simulateInstalledDistro(installedDistroBytes);
414
415        File stagedDataDir = installer.getStagedTzDataDir();
416        assertTrue(stagedDataDir.mkdir());
417
418        assertEquals(
419                TimeZoneDistroInstaller.UNINSTALL_SUCCESS,
420                installer.stageUninstall());
421        assertDistroUninstallStaged();
422        assertInstalledDistro(installedDistroBytes);
423    }
424
425    /**
426     * Tests what happens if a stageUninstall() is attempted when there's something installed
427     * and there's a staged install.
428     */
429    public void testStageUninstall_existingStagedInstall() throws Exception {
430        File stagedDataDir = installer.getStagedTzDataDir();
431        assertTrue(stagedDataDir.mkdir());
432
433        // Stage an install.
434        byte[] distroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
435        assertEquals(TimeZoneDistroInstaller.INSTALL_SUCCESS,
436                installer.stageInstallWithErrorCode(new TimeZoneDistro(distroBytes)));
437
438        // Now uninstall. It should just remove the staged install.
439        assertEquals(
440                TimeZoneDistroInstaller.UNINSTALL_NOTHING_INSTALLED,
441                installer.stageUninstall());
442        assertNoDistroOperationStaged();
443    }
444
445    /**
446     * Tests what happens if a stageUninstall() is attempted when there's something installed
447     * and there's a staged uninstall.
448     */
449    public void testStageUninstall_existingStagedUninstall() throws Exception {
450        // To stage an uninstall, we need to have some installed rules.
451        byte[] installedDistroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
452        simulateInstalledDistro(installedDistroBytes);
453
454        File stagedDataDir = installer.getStagedTzDataDir();
455        assertTrue(stagedDataDir.mkdir());
456
457        assertEquals(
458                TimeZoneDistroInstaller.UNINSTALL_SUCCESS,
459                installer.stageUninstall());
460        assertDistroUninstallStaged();
461        assertInstalledDistro(installedDistroBytes);
462
463        // Now stage a second uninstall.
464        assertEquals(
465                TimeZoneDistroInstaller.UNINSTALL_SUCCESS,
466                installer.stageUninstall());
467        assertDistroUninstallStaged();
468        assertInstalledDistro(installedDistroBytes);
469    }
470
471    /**
472     * Tests what happens if a stageUninstall() is attempted when there are unexpected working
473     * directories present.
474     */
475    public void testStageUninstall_oldDirsAlreadyExists() throws Exception {
476        // To stage an uninstall, we need to have some installed rules.
477        byte[] installedDistroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
478        simulateInstalledDistro(installedDistroBytes);
479
480        File oldStagedDataDir = installer.getOldStagedDataDir();
481        assertTrue(oldStagedDataDir.mkdir());
482
483        File workingDir = installer.getWorkingDir();
484        assertTrue(workingDir.mkdir());
485
486        assertEquals(
487                TimeZoneDistroInstaller.UNINSTALL_SUCCESS,
488                installer.stageUninstall());
489
490        assertDistroUninstallStaged();
491        assertFalse(workingDir.exists());
492        assertFalse(oldStagedDataDir.exists());
493        assertInstalledDistro(installedDistroBytes);
494    }
495
496    public void testGetSystemRulesVersion() throws Exception {
497        assertEquals(SYSTEM_RULES_VERSION, installer.getSystemRulesVersion());
498    }
499
500    public void testGetInstalledDistroVersion() throws Exception {
501        // Check result when nothing installed.
502        assertNull(installer.getInstalledDistroVersion());
503        assertNoDistroOperationStaged();
504        assertNoInstalledDistro();
505
506        // Now simulate there being an existing install active.
507        byte[] distroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
508        simulateInstalledDistro(distroBytes);
509        assertInstalledDistro(distroBytes);
510
511        // Check result when something installed.
512        assertEquals(new TimeZoneDistro(distroBytes).getDistroVersion(),
513                installer.getInstalledDistroVersion());
514        assertNoDistroOperationStaged();
515        assertInstalledDistro(distroBytes);
516    }
517
518    public void testGetStagedDistroOperation() throws Exception {
519        byte[] distro1Bytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
520        byte[] distro2Bytes = createValidTimeZoneDistroBytes(NEWER_RULES_VERSION, 1);
521
522        // Check result when nothing staged.
523        assertNull(installer.getStagedDistroOperation());
524        assertNoDistroOperationStaged();
525        assertNoInstalledDistro();
526
527        // Check result after unsuccessfully staging an uninstall.
528        // Can't stage an uninstall without an installed distro.
529        assertEquals(
530                TimeZoneDistroInstaller.UNINSTALL_NOTHING_INSTALLED,
531                installer.stageUninstall());
532        assertNull(installer.getStagedDistroOperation());
533        assertNoDistroOperationStaged();
534        assertNoInstalledDistro();
535
536        // Check result after staging an install.
537        assertEquals(
538                TimeZoneDistroInstaller.INSTALL_SUCCESS,
539                installer.stageInstallWithErrorCode(new TimeZoneDistro(distro1Bytes)));
540        StagedDistroOperation expectedStagedInstall =
541                StagedDistroOperation.install(new TimeZoneDistro(distro1Bytes).getDistroVersion());
542        assertEquals(expectedStagedInstall, installer.getStagedDistroOperation());
543        assertInstallDistroStaged(distro1Bytes);
544        assertNoInstalledDistro();
545
546        // Check result after unsuccessfully staging an uninstall (but after removing a staged
547        // install). Can't stage an uninstall without an installed distro.
548        assertEquals(
549                TimeZoneDistroInstaller.UNINSTALL_NOTHING_INSTALLED,
550                installer.stageUninstall());
551        assertNull(installer.getStagedDistroOperation());
552        assertNoDistroOperationStaged();
553        assertNoInstalledDistro();
554
555        // Now simulate there being an existing install active.
556        simulateInstalledDistro(distro1Bytes);
557        assertInstalledDistro(distro1Bytes);
558
559        // Check state after successfully staging an uninstall.
560        assertEquals(
561                TimeZoneDistroInstaller.UNINSTALL_SUCCESS,
562                installer.stageUninstall());
563        StagedDistroOperation expectedStagedUninstall = StagedDistroOperation.uninstall();
564        assertEquals(expectedStagedUninstall, installer.getStagedDistroOperation());
565        assertDistroUninstallStaged();
566        assertInstalledDistro(distro1Bytes);
567
568        // Check state after successfully staging an install.
569        assertEquals(TimeZoneDistroInstaller.INSTALL_SUCCESS,
570                installer.stageInstallWithErrorCode(new TimeZoneDistro(distro2Bytes)));
571        StagedDistroOperation expectedStagedInstall2 =
572                StagedDistroOperation.install(new TimeZoneDistro(distro2Bytes).getDistroVersion());
573        assertEquals(expectedStagedInstall2, installer.getStagedDistroOperation());
574        assertInstallDistroStaged(distro2Bytes);
575        assertInstalledDistro(distro1Bytes);
576    }
577
578    private static byte[] createValidTimeZoneDistroBytes(
579            String rulesVersion, int revision) throws Exception {
580        return createValidTimeZoneDistroBuilder(rulesVersion, revision).buildBytes();
581    }
582
583    private static TimeZoneDistroBuilder createValidTimeZoneDistroBuilder(
584            String rulesVersion, int revision) throws Exception {
585
586        byte[] tzData = createTzData(rulesVersion);
587        byte[] icuData = new byte[] { 'a' };
588        String tzlookupXml = "<timezones ianaversion=\"" + rulesVersion + "\">\n"
589                + "  <countryzones>\n"
590                + "    <country code=\"us\" default=\"America/New_York\" everutc=\"n\">\n"
591                + "      <id>America/New_York</id>\n"
592                + "      <id>America/Los_Angeles</id>\n"
593                + "    </country>\n"
594                + "    <country code=\"gb\" default=\"Europe/London\" everutc=\"y\">\n"
595                + "      <id>Europe/London</id>\n"
596                + "    </country>\n"
597                + "  </countryzones>\n"
598                + "</timezones>\n";
599        DistroVersion distroVersion = new DistroVersion(
600                DistroVersion.CURRENT_FORMAT_MAJOR_VERSION,
601                DistroVersion.CURRENT_FORMAT_MINOR_VERSION,
602                rulesVersion,
603                revision);
604        return new TimeZoneDistroBuilder()
605                .setDistroVersion(distroVersion)
606                .setTzDataFile(tzData)
607                .setIcuDataFile(icuData)
608                .setTzLookupXml(tzlookupXml);
609    }
610
611    private void assertInstallDistroStaged(byte[] expectedDistroBytes) throws Exception {
612        assertTrue(testInstallDir.exists());
613
614        File stagedTzDataDir = installer.getStagedTzDataDir();
615        assertTrue(stagedTzDataDir.exists());
616
617        File distroVersionFile =
618                new File(stagedTzDataDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
619        assertTrue(distroVersionFile.exists());
620
621        File tzdataFile = new File(stagedTzDataDir, TimeZoneDistro.TZDATA_FILE_NAME);
622        assertTrue(tzdataFile.exists());
623
624        File icuFile = new File(stagedTzDataDir, TimeZoneDistro.ICU_DATA_FILE_NAME);
625        assertTrue(icuFile.exists());
626
627        File tzLookupFile = new File(stagedTzDataDir, TimeZoneDistro.TZLOOKUP_FILE_NAME);
628        assertTrue(tzLookupFile.exists());
629
630        // Assert getStagedDistroState() is reporting correctly.
631        StagedDistroOperation stagedDistroOperation = installer.getStagedDistroOperation();
632        assertNotNull(stagedDistroOperation);
633        assertFalse(stagedDistroOperation.isUninstall);
634        assertEquals(new TimeZoneDistro(expectedDistroBytes).getDistroVersion(),
635                stagedDistroOperation.distroVersion);
636
637        File expectedZipContentDir = createUniqueDirectory(tempDir, "expectedZipContent");
638        new TimeZoneDistro(expectedDistroBytes).extractTo(expectedZipContentDir);
639
640        assertContentsMatches(
641                new File(expectedZipContentDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME),
642                distroVersionFile);
643        assertContentsMatches(
644                new File(expectedZipContentDir, TimeZoneDistro.ICU_DATA_FILE_NAME),
645                icuFile);
646        assertContentsMatches(
647                new File(expectedZipContentDir, TimeZoneDistro.TZDATA_FILE_NAME),
648                tzdataFile);
649        assertContentsMatches(
650                new File(expectedZipContentDir, TimeZoneDistro.TZLOOKUP_FILE_NAME),
651                tzLookupFile);
652        assertFileCount(4, expectedZipContentDir);
653
654        // Also check no working directory is left lying around.
655        File workingDir = installer.getWorkingDir();
656        assertFalse(workingDir.exists());
657    }
658
659    private static void assertFileCount(int expectedFiles, File rootDir) throws Exception {
660        final List<Path> paths = new ArrayList<>();
661        FileVisitor<Path> visitor = new SimpleFileVisitor<Path>() {
662            @Override
663            public FileVisitResult visitFile(Path filePath, BasicFileAttributes attrs)
664                        throws IOException {
665                paths.add(filePath);
666                return FileVisitResult.CONTINUE;
667            }
668        };
669        Files.walkFileTree(rootDir.toPath(), visitor);
670        assertEquals("Found: " + paths, expectedFiles, paths.size());
671    }
672
673    private void assertContentsMatches(File expected, File actual) throws IOException {
674        byte[] actualBytes = IoUtils.readFileAsByteArray(actual.getPath());
675        byte[] expectedBytes = IoUtils.readFileAsByteArray(expected.getPath());
676        assertArrayEquals(expectedBytes, actualBytes);
677    }
678
679    private void assertNoDistroOperationStaged() throws Exception {
680        assertNull(installer.getStagedDistroOperation());
681
682        File stagedTzDataDir = installer.getStagedTzDataDir();
683        assertFalse(stagedTzDataDir.exists());
684
685        // Also check no working directories are left lying around.
686        File workingDir = installer.getWorkingDir();
687        assertFalse(workingDir.exists());
688
689        File oldDataDir = installer.getOldStagedDataDir();
690        assertFalse(oldDataDir.exists());
691    }
692
693    private void assertDistroUninstallStaged() throws Exception {
694        assertEquals(StagedDistroOperation.uninstall(), installer.getStagedDistroOperation());
695
696        File stagedTzDataDir = installer.getStagedTzDataDir();
697        assertTrue(stagedTzDataDir.exists());
698        assertTrue(stagedTzDataDir.isDirectory());
699
700        File uninstallTombstone =
701                new File(stagedTzDataDir, TimeZoneDistroInstaller.UNINSTALL_TOMBSTONE_FILE_NAME);
702        assertTrue(uninstallTombstone.exists());
703        assertTrue(uninstallTombstone.isFile());
704
705        // Also check no working directories are left lying around.
706        File workingDir = installer.getWorkingDir();
707        assertFalse(workingDir.exists());
708
709        File oldDataDir = installer.getOldStagedDataDir();
710        assertFalse(oldDataDir.exists());
711    }
712
713    private void simulateInstalledDistro(byte[] distroBytes) throws Exception {
714        File currentTzDataDir = installer.getCurrentTzDataDir();
715        assertFalse(currentTzDataDir.exists());
716        assertTrue(currentTzDataDir.mkdir());
717        new TimeZoneDistro(distroBytes).extractTo(currentTzDataDir);
718    }
719
720    private void assertNoInstalledDistro() {
721        assertFalse(installer.getCurrentTzDataDir().exists());
722    }
723
724    private void assertInstalledDistro(byte[] distroBytes) throws Exception {
725        File currentTzDataDir = installer.getCurrentTzDataDir();
726        assertTrue(currentTzDataDir.exists());
727        File versionFile = new File(currentTzDataDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
728        assertTrue(versionFile.exists());
729        byte[] expectedVersionBytes = new TimeZoneDistro(distroBytes).getDistroVersion().toBytes();
730        byte[] actualVersionBytes = FileUtils.readBytes(versionFile, expectedVersionBytes.length);
731        assertArrayEquals(expectedVersionBytes, actualVersionBytes);
732    }
733
734    private static byte[] createTzData(String rulesVersion) {
735        return new ZoneInfoTestHelper.TzDataBuilder()
736                .initializeToValid()
737                .setHeaderMagic("tzdata" + rulesVersion)
738                .build();
739    }
740
741    private static void createFile(File file, byte[] bytes) {
742        try (FileOutputStream fos = new FileOutputStream(file)) {
743            fos.write(bytes);
744        } catch (IOException e) {
745            fail(e.getMessage());
746        }
747    }
748
749    /**
750     * Creates a TimeZoneDistro containing arbitrary bytes in the version file. Used for testing
751     * distros with badly formed version info.
752     */
753    private TimeZoneDistro createTimeZoneDistroWithVersionBytes(byte[] versionBytes)
754            throws Exception {
755
756        // Extract a valid distro to a working dir.
757        byte[] distroBytes = createValidTimeZoneDistroBytes(NEW_RULES_VERSION, 1);
758        File workingDir = createUniqueDirectory(tempDir, "versionBytes");
759        new TimeZoneDistro(distroBytes).extractTo(workingDir);
760
761        // Modify the version file.
762        File versionFile = new File(workingDir, TimeZoneDistro.DISTRO_VERSION_FILE_NAME);
763        assertTrue(versionFile.exists());
764        try (FileOutputStream fos = new FileOutputStream(versionFile, false /* append */)) {
765            fos.write(versionBytes);
766        }
767
768        // Zip the distro back up again.
769        ByteArrayOutputStream baos = new ByteArrayOutputStream();
770        try (ZipOutputStream zos = new ZipOutputStream(baos)) {
771            Path workingDirPath = workingDir.toPath();
772            Files.walkFileTree(workingDirPath, new SimpleFileVisitor<Path>() {
773                @Override
774                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
775                        throws IOException {
776                    byte[] bytes = IoUtils.readFileAsByteArray(file.toString());
777                    String relativeFileName = workingDirPath.relativize(file).toString();
778                    addZipEntry(zos, relativeFileName, bytes);
779                    return FileVisitResult.CONTINUE;
780                }
781            });
782        }
783
784        return new TimeZoneDistro(baos.toByteArray());
785    }
786
787    private static void addZipEntry(ZipOutputStream zos, String name, byte[] content)
788            throws IOException {
789        ZipEntry zipEntry = new ZipEntry(name);
790        zipEntry.setSize(content.length);
791        zos.putNextEntry(zipEntry);
792        zos.write(content);
793        zos.closeEntry();
794    }
795}
796