1/*
2 * Copyright 2017 Google Inc. All Rights Reserved.
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
17package com.google.turbine.zip;
18
19import static com.google.common.truth.Truth.assertThat;
20import static java.nio.charset.StandardCharsets.UTF_8;
21import static org.junit.Assert.fail;
22
23import com.google.common.collect.ImmutableMap;
24import com.google.common.hash.Hashing;
25import com.google.common.io.ByteStreams;
26import java.io.IOException;
27import java.net.URI;
28import java.nio.file.FileSystem;
29import java.nio.file.FileSystems;
30import java.nio.file.Files;
31import java.nio.file.Path;
32import java.nio.file.StandardOpenOption;
33import java.nio.file.attribute.FileTime;
34import java.util.Enumeration;
35import java.util.LinkedHashMap;
36import java.util.Map;
37import java.util.jar.JarEntry;
38import java.util.jar.JarFile;
39import java.util.jar.JarOutputStream;
40import java.util.zip.ZipException;
41import java.util.zip.ZipOutputStream;
42import org.junit.Rule;
43import org.junit.Test;
44import org.junit.rules.TemporaryFolder;
45import org.junit.runner.RunWith;
46import org.junit.runners.JUnit4;
47
48/** {@link Zip}Test */
49@RunWith(JUnit4.class)
50public class ZipTest {
51
52  @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
53
54  @Test
55  public void testEntries() throws IOException {
56    testEntries(1000);
57  }
58
59  @Test
60  public void zip64_testEntries() throws IOException {
61    testEntries(70000);
62  }
63
64  @Test
65  public void compression() throws IOException {
66    Path path = temporaryFolder.newFile("test.jar").toPath();
67    try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(path))) {
68      for (int i = 0; i < 2; i++) {
69        String name = "entry" + i;
70        byte[] bytes = name.getBytes(UTF_8);
71        jos.putNextEntry(new JarEntry(name));
72        jos.write(bytes);
73      }
74    }
75    assertThat(actual(path)).isEqualTo(expected(path));
76  }
77
78  private void testEntries(int entries) throws IOException {
79    Path path = temporaryFolder.newFile("test.jar").toPath();
80    try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(path))) {
81      for (int i = 0; i < entries; i++) {
82        String name = "entry" + i;
83        byte[] bytes = name.getBytes(UTF_8);
84        createEntry(jos, name, bytes);
85      }
86    }
87    assertThat(actual(path)).isEqualTo(expected(path));
88  }
89
90  private static void createEntry(ZipOutputStream jos, String name, byte[] bytes)
91      throws IOException {
92    JarEntry je = new JarEntry(name);
93    je.setMethod(JarEntry.STORED);
94    je.setSize(bytes.length);
95    je.setCrc(Hashing.crc32().hashBytes(bytes).padToLong());
96    jos.putNextEntry(je);
97    jos.write(bytes);
98  }
99
100  private static Map<String, Long> actual(Path path) throws IOException {
101    Map<String, Long> result = new LinkedHashMap<>();
102    for (Zip.Entry e : new Zip.ZipIterable(path)) {
103      result.put(e.name(), Hashing.goodFastHash(128).hashBytes(e.data()).padToLong());
104    }
105    return result;
106  }
107
108  private static Map<String, Long> expected(Path path) throws IOException {
109    Map<String, Long> result = new LinkedHashMap<>();
110    try (JarFile jf = new JarFile(path.toFile())) {
111      Enumeration<JarEntry> entries = jf.entries();
112      while (entries.hasMoreElements()) {
113        JarEntry je = entries.nextElement();
114        result.put(
115            je.getName(),
116            Hashing.goodFastHash(128)
117                .hashBytes(ByteStreams.toByteArray(jf.getInputStream(je)))
118                .padToLong());
119      }
120    }
121    return result;
122  }
123
124  @Test
125  public void attributes() throws Exception {
126    Path path = temporaryFolder.newFile("test.jar").toPath();
127    Files.delete(path);
128    try (FileSystem fs =
129        FileSystems.newFileSystem(
130            URI.create("jar:file:" + path.toAbsolutePath()), ImmutableMap.of("create", "true"))) {
131      for (int i = 0; i < 3; i++) {
132        String name = "entry" + i;
133        byte[] bytes = name.getBytes(UTF_8);
134        Path entry = fs.getPath(name);
135        Files.write(entry, bytes);
136        Files.setLastModifiedTime(entry, FileTime.fromMillis(0));
137      }
138    }
139    assertThat(actual(path)).isEqualTo(expected(path));
140  }
141
142  @Test
143  public void zipFileCommentsAreSupported() throws Exception {
144    Path path = temporaryFolder.newFile("test.jar").toPath();
145    Files.delete(path);
146    try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(path))) {
147      createEntry(zos, "hello", "world".getBytes(UTF_8));
148      zos.setComment("this is a comment");
149    }
150    assertThat(actual(path)).isEqualTo(expected(path));
151  }
152
153  @Test
154  public void malformedComment() throws Exception {
155    Path path = temporaryFolder.newFile("test.jar").toPath();
156    Files.delete(path);
157
158    try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(path))) {
159      createEntry(zos, "hello", "world".getBytes(UTF_8));
160      zos.setComment("this is a comment");
161    }
162    Files.write(path, "trailing garbage".getBytes(UTF_8), StandardOpenOption.APPEND);
163
164    try {
165      actual(path);
166      fail();
167    } catch (ZipException e) {
168      assertThat(e).hasMessage("zip file comment length was 33, expected 17");
169    }
170  }
171}
172