View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.commons.compress.archivers.tar;
20  
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertFalse;
23  import static org.junit.jupiter.api.Assertions.assertThrows;
24  import static org.junit.jupiter.api.Assertions.assertTrue;
25  import static org.junit.jupiter.api.Assertions.fail;
26  
27  import java.io.BufferedOutputStream;
28  import java.io.ByteArrayOutputStream;
29  import java.io.File;
30  import java.io.IOException;
31  import java.io.InputStream;
32  import java.io.OutputStream;
33  import java.nio.ByteBuffer;
34  import java.nio.channels.SeekableByteChannel;
35  import java.nio.charset.StandardCharsets;
36  import java.nio.file.Files;
37  import java.nio.file.Path;
38  import java.util.Calendar;
39  import java.util.Date;
40  import java.util.List;
41  import java.util.TimeZone;
42  import java.util.zip.GZIPInputStream;
43  
44  import org.apache.commons.compress.AbstractTest;
45  import org.apache.commons.io.IOUtils;
46  import org.junit.jupiter.api.Test;
47  
48  import shaded.org.apache.commons.lang3.StringUtils;
49  
50  class TarFileTest extends AbstractTest {
51  
52      private void datePriorToEpoch(final String archive) throws Exception {
53          try (TarFile tarFile = new TarFile(getPath(archive))) {
54              final TarArchiveEntry entry = tarFile.getEntries().get(0);
55              assertEquals("foo", entry.getName());
56              assertEquals(TarConstants.LF_NORMAL, entry.getLinkFlag());
57              final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
58              cal.set(1969, 11, 31, 23, 59, 59);
59              cal.set(Calendar.MILLISECOND, 0);
60              assertEquals(cal.getTime(), entry.getLastModifiedDate());
61              assertTrue(entry.isCheckSumOK());
62          }
63      }
64  
65      /**
66       * This test ensures the implementation is reading the padded last block if a tool has added one to an archive
67       */
68      @Test
69      void testArchiveWithTrailer() throws IOException {
70          try (SeekableByteChannel channel = Files.newByteChannel(getPath("archive_with_trailer.tar"));
71                  TarFile tarfile = new TarFile(channel, TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE, null, false)) {
72              final String tarAppendix = "Hello, world!\n";
73              final ByteBuffer buffer = ByteBuffer.allocate(tarAppendix.length());
74              channel.read(buffer);
75              assertEquals(tarAppendix, new String(buffer.array()));
76          }
77      }
78  
79      @Test
80      void testCompress197() throws IOException {
81          try (TarFile tarFile = new TarFile(getPath("COMPRESS-197.tar"))) {
82              // noop
83          }
84      }
85  
86      @Test
87      void testCompress558() throws IOException {
88          final String folderName = "apache-activemq-5.16.0/examples/openwire/advanced-scenarios/jms-example-exclusive-consumer/src/main/";
89          // @formatter:off
90          final String consumerJavaName =
91              "apache-activemq-5.16.0/examples/openwire/advanced-scenarios/jms-example-exclusive-consumer/src/main/java/example/queue/exclusive/Consumer.java";
92          final String producerJavaName =
93              "apache-activemq-5.16.0/examples/openwire/advanced-scenarios/jms-example-exclusive-consumer/src/main/java/example/queue/exclusive/Producer.java";
94          // @formatter:on
95  
96          final ByteArrayOutputStream bos = new ByteArrayOutputStream();
97          try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos)) {
98              tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
99              final TarArchiveEntry rootfolder = new TarArchiveEntry(folderName);
100             tos.putArchiveEntry(rootfolder);
101             final TarArchiveEntry consumerJava = new TarArchiveEntry(consumerJavaName);
102             tos.putArchiveEntry(consumerJava);
103             final TarArchiveEntry producerJava = new TarArchiveEntry(producerJavaName);
104             tos.putArchiveEntry(producerJava);
105             tos.closeArchiveEntry();
106         }
107         final byte[] data = bos.toByteArray();
108         try (TarFile tarFile = new TarFile(data)) {
109             final List<TarArchiveEntry> entries = tarFile.getEntries();
110             assertEquals(folderName, entries.get(0).getName());
111             assertEquals(TarConstants.LF_DIR, entries.get(0).getLinkFlag());
112             assertEquals(consumerJavaName, entries.get(1).getName());
113             assertEquals(TarConstants.LF_NORMAL, entries.get(1).getLinkFlag());
114             assertEquals(producerJavaName, entries.get(2).getName());
115             assertEquals(TarConstants.LF_NORMAL, entries.get(2).getLinkFlag());
116         }
117     }
118 
119     @Test
120     void testCompress657() throws IOException {
121         try (TarFile tarFile = new TarFile(getPath("COMPRESS-657/orjson-3.7.8.tar"))) {
122             for (final TarArchiveEntry entry : tarFile.getEntries()) {
123                 if (entry.isDirectory()) {
124                     // An entry cannot be a directory and a "normal file" at the same time.
125                     assertFalse(entry.isFile(), "Entry '" + entry.getName() + "' is both a directory and a file");
126                 }
127             }
128         }
129     }
130 
131     @Test
132     void testDatePriorToEpochInGNUFormat() throws Exception {
133         datePriorToEpoch("preepoch-star.tar");
134     }
135 
136     @Test
137     void testDatePriorToEpochInPAXFormat() throws Exception {
138         datePriorToEpoch("preepoch-posix.tar");
139     }
140 
141     @Test
142     void testDirectoryWithLongNameEndsWithSlash() throws IOException {
143         final String rootPath = getTempDirFile().getAbsolutePath();
144         final String dirDirectory = "COMPRESS-509";
145         final int count = 100;
146         final File root = new File(rootPath + "/" + dirDirectory);
147         root.mkdirs();
148         for (int i = 1; i < count; i++) {
149             // create empty dirs with incremental length
150             final String subDir = StringUtils.repeat('a', i);
151             final File dir = new File(rootPath + "/" + dirDirectory, "/" + subDir);
152             dir.mkdir();
153 
154             // tar these dirs
155             final String fileName = "/" + dirDirectory + "/" + subDir;
156             final File tarF = new File(rootPath + "/tar" + i + ".tar");
157             try (OutputStream dest = Files.newOutputStream(tarF.toPath());
158                     TarArchiveOutputStream out = new TarArchiveOutputStream(new BufferedOutputStream(dest))) {
159                 out.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX);
160                 out.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
161 
162                 final File file = new File(rootPath, fileName);
163                 final TarArchiveEntry entry = new TarArchiveEntry(file);
164                 entry.setName(fileName);
165                 out.putArchiveEntry(entry);
166                 out.closeArchiveEntry();
167                 out.flush();
168             }
169             // untar these tars
170             try (TarFile tarFile = new TarFile(tarF)) {
171                 for (final TarArchiveEntry entry : tarFile.getEntries()) {
172                     assertTrue(entry.getName().endsWith("/"), "Entry name: " + entry.getName());
173                 }
174             }
175         }
176     }
177 
178     @Test
179     void testMultiByteReadConsistentlyReturnsMinusOneAtEof() throws Exception {
180         final byte[] buf = new byte[2];
181         try (TarFile tarFile = new TarFile(getPath("bla.tar"));
182                 InputStream input = tarFile.getInputStream(tarFile.getEntries().get(0))) {
183             IOUtils.toByteArray(input);
184             assertEquals(-1, input.read(buf));
185             assertEquals(-1, input.read(buf));
186         }
187     }
188 
189     @Test
190     void testParseTarTruncatedInContent() {
191         assertThrows(IOException.class, () -> new TarFile(getPath("COMPRESS-544_truncated_in_content.tar")));
192     }
193 
194     @Test
195     void testParseTarTruncatedInPadding() {
196         assertThrows(IOException.class, () -> new TarFile(getPath("COMPRESS-544_truncated_in_padding.tar")));
197     }
198 
199     @Test
200     void testParseTarWithNonNumberPaxHeaders() {
201         assertThrows(IOException.class, () -> new TarFile(getPath("COMPRESS-529-fail.tar")));
202     }
203 
204     @Test
205     void testParseTarWithSpecialPaxHeaders() {
206         assertThrows(IOException.class, () -> new TarFile(getPath("COMPRESS-530-fail.tar")));
207     }
208 
209     @Test
210     void testReadsArchiveCompletely_COMPRESS245() {
211         try {
212             final Path tempTar = tempResultDir.toPath().resolve("COMPRESS-245.tar");
213             try (GZIPInputStream gin = new GZIPInputStream(Files.newInputStream(getPath("COMPRESS-245.tar.gz")))) {
214                 Files.copy(gin, tempTar);
215             }
216             try (TarFile tarFile = new TarFile(tempTar)) {
217                 assertEquals(31, tarFile.getEntries().size());
218             }
219         } catch (final IOException e) {
220             fail("COMPRESS-245: " + e.getMessage());
221         }
222     }
223 
224     @Test
225     void testRejectsArchivesWithNegativeSizes() throws Exception {
226         assertThrows(IOException.class, () -> new TarFile(getFile("COMPRESS-569-fail.tar")));
227     }
228 
229     @Test
230     void testShouldReadBigGid() throws Exception {
231         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
232         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos)) {
233             tos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX);
234             final TarArchiveEntry t = new TarArchiveEntry("name");
235             t.setGroupId(4294967294L);
236             t.setSize(1);
237             tos.putArchiveEntry(t);
238             tos.write(30);
239             tos.closeArchiveEntry();
240         }
241         final byte[] data = bos.toByteArray();
242         try (TarFile tarFile = new TarFile(data)) {
243             final List<TarArchiveEntry> entries = tarFile.getEntries();
244             assertEquals(4294967294L, entries.get(0).getLongGroupId());
245         }
246     }
247 
248     /**
249      * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-324">COMPRESS-324</a>
250      */
251     @Test
252     void testShouldReadGNULongNameEntryWithWrongName() throws Exception {
253         try (TarFile tarFile = new TarFile(getPath("COMPRESS-324.tar"))) {
254             final List<TarArchiveEntry> entries = tarFile.getEntries();
255             assertEquals(
256                     "1234567890123456789012345678901234567890123456789012345678901234567890"
257                             + "1234567890123456789012345678901234567890123456789012345678901234567890"
258                             + "1234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890.txt",
259                     entries.get(0).getName());
260         }
261     }
262 
263     @Test
264     void testShouldThrowAnExceptionOnTruncatedEntries() throws Exception {
265         createTempDirectory("COMPRESS-279");
266         assertThrows(IOException.class, () -> new TarFile(getPath("COMPRESS-279.tar")));
267     }
268 
269     @Test
270     void testShouldUseSpecifiedEncodingWhenReadingGNULongNames() throws Exception {
271         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
272         final String encoding = StandardCharsets.UTF_16.name();
273         final String name = "1234567890123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789" + "01234567890\u00e4";
274         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos, encoding)) {
275             tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
276             final TarArchiveEntry t = new TarArchiveEntry(name);
277             t.setSize(1);
278             tos.putArchiveEntry(t);
279             tos.write(30);
280             tos.closeArchiveEntry();
281         }
282         final byte[] data = bos.toByteArray();
283         try (TarFile tarFile = new TarFile(data, encoding)) {
284             final List<TarArchiveEntry> entries = tarFile.getEntries();
285             assertEquals(1, entries.size());
286             assertEquals(name, entries.get(0).getName());
287         }
288     }
289 
290     @Test
291     void testSingleByteReadConsistentlyReturnsMinusOneAtEof() throws Exception {
292         try (TarFile tarFile = new TarFile(getPath("bla.tar"));
293                 InputStream input = tarFile.getInputStream(tarFile.getEntries().get(0))) {
294             IOUtils.toByteArray(input);
295             assertEquals(-1, input.read());
296             assertEquals(-1, input.read());
297         }
298     }
299 
300     /**
301      * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-417">COMPRESS-417</a>
302      */
303     @Test
304     void testSkipsDevNumbersWhenEntryIsNoDevice() throws Exception {
305         try (TarFile tarFile = new TarFile(getPath("COMPRESS-417.tar"))) {
306             final List<TarArchiveEntry> entries = tarFile.getEntries();
307             assertEquals(2, entries.size());
308             assertEquals("test1.xml", entries.get(0).getName());
309             assertEquals(TarConstants.LF_NORMAL, entries.get(0).getLinkFlag());
310             assertEquals("test2.xml", entries.get(1).getName());
311             assertEquals(TarConstants.LF_NORMAL, entries.get(1).getLinkFlag());
312         }
313     }
314 
315     /**
316      * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-355">COMPRESS-355</a>
317      */
318     @Test
319     void testSurvivesBlankLinesInPaxHeader() throws Exception {
320         try (TarFile tarFile = new TarFile(getPath("COMPRESS-355.tar"))) {
321             final List<TarArchiveEntry> entries = tarFile.getEntries();
322             assertEquals(1, entries.size());
323             assertEquals("package/package.json", entries.get(0).getName());
324             assertEquals(TarConstants.LF_NORMAL, entries.get(0).getLinkFlag());
325         }
326     }
327 
328     /**
329      * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-356">COMPRESS-356</a>
330      */
331     @Test
332     void testSurvivesPaxHeaderWithNameEndingInSlash() throws Exception {
333         try (TarFile tarFile = new TarFile(getPath("COMPRESS-356.tar"))) {
334             final List<TarArchiveEntry> entries = tarFile.getEntries();
335             assertEquals(1, entries.size());
336             assertEquals("package/package.json", entries.get(0).getName());
337             assertEquals(TarConstants.LF_NORMAL, entries.get(0).getLinkFlag());
338         }
339     }
340 
341     @Test
342     void testThrowException() {
343         assertThrows(IOException.class, () -> new TarFile(getPath("COMPRESS-553-fail.tar")));
344     }
345 
346     @Test
347     void testThrowExceptionWithNullEntry() {
348         assertThrows(IOException.class, () -> new TarFile(getPath("COMPRESS-554-fail.tar")));
349     }
350 
351     @Test
352     void testWorkaroundForBrokenTimeHeader() throws IOException {
353         try (TarFile tarFile = new TarFile(getPath("simple-aix-native-tar.tar"))) {
354             final List<TarArchiveEntry> entries = tarFile.getEntries();
355             assertEquals(3, entries.size());
356             final TarArchiveEntry entry = entries.get(1);
357             assertEquals("sample/link-to-txt-file.lnk", entry.getName());
358             assertEquals(TarConstants.LF_SYMLINK, entry.getLinkFlag());
359             assertEquals(new Date(0), entry.getLastModifiedDate());
360             assertTrue(entry.isSymbolicLink());
361             assertTrue(entry.isCheckSumOK());
362         }
363     }
364 }