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  
20  package org.apache.commons.compress.archivers.tar;
21  
22  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
23  import static org.junit.jupiter.api.Assertions.assertEquals;
24  import static org.junit.jupiter.api.Assertions.assertNotNull;
25  import static org.junit.jupiter.api.Assertions.assertNotSame;
26  import static org.junit.jupiter.api.Assertions.assertNull;
27  import static org.junit.jupiter.api.Assertions.assertSame;
28  import static org.junit.jupiter.api.Assertions.assertThrows;
29  import static org.junit.jupiter.api.Assertions.assertTrue;
30  import static org.junit.jupiter.api.Assertions.fail;
31  
32  import java.io.BufferedInputStream;
33  import java.io.BufferedOutputStream;
34  import java.io.ByteArrayInputStream;
35  import java.io.ByteArrayOutputStream;
36  import java.io.File;
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.io.OutputStream;
40  import java.nio.charset.StandardCharsets;
41  import java.nio.file.Files;
42  import java.nio.file.Path;
43  import java.util.ArrayList;
44  import java.util.Arrays;
45  import java.util.Calendar;
46  import java.util.Date;
47  import java.util.List;
48  import java.util.Objects;
49  import java.util.TimeZone;
50  import java.util.concurrent.ExecutorService;
51  import java.util.concurrent.Executors;
52  import java.util.concurrent.Future;
53  import java.util.concurrent.atomic.AtomicInteger;
54  import java.util.stream.Collectors;
55  import java.util.stream.IntStream;
56  import java.util.zip.GZIPInputStream;
57  
58  import org.apache.commons.compress.AbstractTest;
59  import org.apache.commons.compress.archivers.ArchiveException;
60  import org.apache.commons.compress.archivers.ArchiveStreamFactory;
61  import org.apache.commons.io.IOUtils;
62  import org.apache.commons.io.function.IOConsumer;
63  import org.junit.jupiter.api.Test;
64  import org.junit.jupiter.params.ParameterizedTest;
65  import org.junit.jupiter.params.provider.ValueSource;
66  
67  class TarArchiveInputStreamTest extends AbstractTest {
68  
69      private void datePriorToEpoch(final String archive) throws Exception {
70          try (TarArchiveInputStream in = new TarArchiveInputStream(Files.newInputStream(getFile(archive).toPath()))) {
71              final TarArchiveEntry tae = in.getNextTarEntry();
72              assertEquals("foo", tae.getName());
73              assertEquals(TarConstants.LF_NORMAL, tae.getLinkFlag());
74              final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
75              cal.set(1969, 11, 31, 23, 59, 59);
76              cal.set(Calendar.MILLISECOND, 0);
77              assertEquals(cal.getTime(), tae.getLastModifiedDate());
78              assertTrue(tae.isCheckSumOK());
79          }
80      }
81  
82      private void getNextEntryUntilIOException(final TarArchiveInputStream archive) {
83          assertThrows(IOException.class, () -> archive.forEach(IOConsumer.noop()));
84      }
85  
86      @SuppressWarnings("resource") // Caller closes
87      private TarArchiveInputStream getTestStream(final String name) {
88          return new TarArchiveInputStream(TarArchiveInputStreamTest.class.getResourceAsStream(name));
89      }
90  
91      @Test
92      void testCompress197() throws IOException {
93          try (TarArchiveInputStream tar = getTestStream("/COMPRESS-197.tar")) {
94              TarArchiveEntry entry = tar.getNextTarEntry();
95              assertNotNull(entry);
96              while (entry != null) {
97                  assertTrue(entry.isTypeFlagUstar());
98                  entry = tar.getNextTarEntry();
99              }
100         }
101     }
102 
103     @Test
104     void testCompress197ForEach() throws IOException {
105         try (TarArchiveInputStream tar = getTestStream("/COMPRESS-197.tar")) {
106             tar.forEach(IOConsumer.noop());
107         }
108     }
109 
110     @Test
111     void testCompress558() throws IOException {
112         final String folderName = "apache-activemq-5.16.0/examples/openwire/advanced-scenarios/jms-example-exclusive-consumer/src/main/";
113         // @formatter:off
114         final String consumerJavaName =
115             "apache-activemq-5.16.0/examples/openwire/advanced-scenarios/jms-example-exclusive-consumer/src/main/java/example/queue/exclusive/Consumer.java";
116         final String producerJavaName =
117             "apache-activemq-5.16.0/examples/openwire/advanced-scenarios/jms-example-exclusive-consumer/src/main/java/example/queue/exclusive/Producer.java";
118         // @formatter:on
119 
120         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
121         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos)) {
122             tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
123             final TarArchiveEntry rootfolder = new TarArchiveEntry(folderName);
124             tos.putArchiveEntry(rootfolder);
125             final TarArchiveEntry consumerJava = new TarArchiveEntry(consumerJavaName);
126             tos.putArchiveEntry(consumerJava);
127             final TarArchiveEntry producerJava = new TarArchiveEntry(producerJavaName);
128             tos.putArchiveEntry(producerJava);
129             tos.closeArchiveEntry();
130         }
131         final byte[] data = bos.toByteArray();
132         try (ByteArrayInputStream bis = new ByteArrayInputStream(data);
133                 TarArchiveInputStream tis = new TarArchiveInputStream(bis)) {
134             assertEquals(folderName, tis.getNextTarEntry().getName());
135             assertEquals(TarConstants.LF_DIR, tis.getCurrentEntry().getLinkFlag());
136             assertEquals(consumerJavaName, tis.getNextTarEntry().getName());
137             assertEquals(TarConstants.LF_NORMAL, tis.getCurrentEntry().getLinkFlag());
138             assertEquals(producerJavaName, tis.getNextTarEntry().getName());
139             assertEquals(TarConstants.LF_NORMAL, tis.getCurrentEntry().getLinkFlag());
140         }
141     }
142 
143     private void testCompress666(final int factor, final boolean bufferInputStream, final String localPath) {
144         final ExecutorService executorService = Executors.newFixedThreadPool(10);
145         try {
146             final List<Future<?>> tasks = IntStream.range(0, 200).mapToObj(index -> executorService.submit(() -> {
147                 TarArchiveEntry tarEntry = null;
148                 try (InputStream inputStream = getClass().getResourceAsStream(localPath);
149                      TarArchiveInputStream tarInputStream = new TarArchiveInputStream(
150                              bufferInputStream ? new BufferedInputStream(new GZIPInputStream(inputStream)) : new GZIPInputStream(inputStream),
151                              TarConstants.DEFAULT_RCDSIZE * factor, TarConstants.DEFAULT_RCDSIZE)) {
152                     while ((tarEntry = tarInputStream.getNextEntry()) != null) {
153                         assertNotNull(tarEntry);
154                     }
155                 } catch (final IOException e) {
156                     fail(Objects.toString(tarEntry), e);
157                 }
158             })).collect(Collectors.toList());
159             final List<Exception> list = new ArrayList<>();
160             for (final Future<?> future : tasks) {
161                 try {
162                     future.get();
163                 } catch (final Exception e) {
164                     list.add(e);
165                 }
166             }
167             // check:
168             if (!list.isEmpty()) {
169                 fail(list.get(0));
170             }
171             // or:
172             // assertTrue(list.isEmpty(), () -> list.size() + " exceptions: " + list.toString());
173         } finally {
174             executorService.shutdownNow();
175         }
176     }
177 
178     /**
179      * Tests https://issues.apache.org/jira/browse/COMPRESS-666
180      *
181      * A factor of 20 is the default.
182      */
183     @ParameterizedTest
184     @ValueSource(ints = { 1, 2, 4, 8, 16, 20, 32, 64, 128 })
185     void testCompress666Buffered(final int factor) {
186         testCompress666(factor, true, "/COMPRESS-666/compress-666.tar.gz");
187     }
188 
189     /**
190      * Tests https://issues.apache.org/jira/browse/COMPRESS-666
191      *
192      * A factor of 20 is the default.
193      */
194     @ParameterizedTest
195     @ValueSource(ints = { 1, 2, 4, 8, 16, 20, 32, 64, 128 })
196     void testCompress666Unbuffered(final int factor) {
197         testCompress666(factor, false, "/COMPRESS-666/compress-666.tar.gz");
198     }
199 
200     @Test
201     void testDatePriorToEpochInGNUFormat() throws Exception {
202         datePriorToEpoch("preepoch-star.tar");
203     }
204 
205     @Test
206     void testDatePriorToEpochInPAXFormat() throws Exception {
207         datePriorToEpoch("preepoch-posix.tar");
208     }
209 
210     @Test
211     void testDirectoryWithLongNameEndsWithSlash() throws IOException, ArchiveException {
212         final String rootPath = getTempDirFile().getAbsolutePath();
213         final String dirDirectory = "COMPRESS-509";
214         final int count = 100;
215         final File root = new File(rootPath + "/" + dirDirectory);
216         root.mkdirs();
217         for (int i = 1; i < count; i++) {
218             // create empty dirs with incremental length
219             String subDir = "";
220             for (int j = 0; j < i; j++) {
221                 subDir += "a";
222             }
223             final File dir = new File(rootPath + "/" + dirDirectory, "/" + subDir);
224             dir.mkdir();
225 
226             // tar these dirs
227             final String fileName = "/" + dirDirectory + "/" + subDir;
228             final File tarF = new File(rootPath + "/tar" + i + ".tar");
229             try (OutputStream dest = Files.newOutputStream(tarF.toPath())) {
230                 final TarArchiveOutputStream out = new TarArchiveOutputStream(new BufferedOutputStream(dest));
231                 out.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR);
232                 out.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
233 
234                 final File file = new File(rootPath, fileName);
235                 final TarArchiveEntry entry = new TarArchiveEntry(file);
236                 entry.setName(fileName);
237                 out.putArchiveEntry(entry);
238                 out.closeArchiveEntry();
239                 out.flush();
240             }
241 
242             // untar these tars
243             try (InputStream is = Files.newInputStream(tarF.toPath());
244                     TarArchiveInputStream debInputStream = ArchiveStreamFactory.DEFAULT.createArchiveInputStream("tar", is)) {
245                 TarArchiveEntry outEntry;
246                 while ((outEntry = debInputStream.getNextEntry()) != null) {
247                     assertTrue(outEntry.getName().endsWith("/"), outEntry.getName());
248                 }
249             }
250         }
251     }
252 
253     @Test
254     void testGetAndSetOfPaxEntry() throws Exception {
255         try (TarArchiveInputStream is = getTestStream("/COMPRESS-356.tar")) {
256             final TarArchiveEntry entry = is.getNextTarEntry();
257             assertEquals("package/package.json", entry.getName());
258             assertEquals(TarConstants.LF_NORMAL, entry.getLinkFlag());
259             assertEquals(is.getCurrentEntry(), entry);
260             final TarArchiveEntry weaselEntry = new TarArchiveEntry(entry.getName());
261             weaselEntry.setSize(entry.getSize());
262             is.setCurrentEntry(weaselEntry);
263             assertEquals(entry, is.getCurrentEntry());
264             assertNotSame(entry, is.getCurrentEntry());
265             assertSame(weaselEntry, is.getCurrentEntry());
266             assertThrows(IllegalStateException.class, () -> {
267                 is.setCurrentEntry(null);
268                 is.read();
269             }, "should abort because current entry is nulled");
270             is.setCurrentEntry(entry);
271             is.read();
272         }
273     }
274 
275     @Test
276     void testMultiByteReadConsistentlyReturnsMinusOneAtEof() throws Exception {
277         final byte[] buf = new byte[2];
278         try (InputStream in = newInputStream("bla.tar");
279                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
280             assertNotNull(archive.getNextEntry());
281             IOUtils.toByteArray(archive);
282             assertEquals(-1, archive.read(buf));
283             assertEquals(-1, archive.read(buf));
284         }
285     }
286 
287     @Test
288     void testParseTarTruncatedInContent() throws IOException {
289         try (InputStream in = newInputStream("COMPRESS-544_truncated_in_content-fail.tar");
290                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
291             getNextEntryUntilIOException(archive);
292         }
293     }
294 
295     @Test
296     void testParseTarTruncatedInPadding() throws IOException {
297         try (InputStream in = newInputStream("COMPRESS-544_truncated_in_padding-fail.tar");
298                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
299             getNextEntryUntilIOException(archive);
300         }
301     }
302 
303     @Test
304     void testParseTarWithNonNumberPaxHeaders() throws IOException {
305         try (InputStream in = newInputStream("COMPRESS-529-fail.tar");
306                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
307             assertThrows(IOException.class, () -> archive.getNextEntry());
308         }
309     }
310 
311     @Test
312     void testParseTarWithSpecialPaxHeaders() throws IOException {
313         try (InputStream in = newInputStream("COMPRESS-530-fail.tar");
314                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
315             assertThrows(IOException.class, () -> archive.getNextEntry());
316             assertThrows(IOException.class, () -> IOUtils.toByteArray(archive));
317         }
318     }
319 
320     @Test
321     void testReadsArchiveCompletely_COMPRESS245() {
322         try (InputStream is = TarArchiveInputStreamTest.class.getResourceAsStream("/COMPRESS-245.tar.gz")) {
323             final InputStream gin = new GZIPInputStream(is);
324             try (TarArchiveInputStream tar = new TarArchiveInputStream(gin)) {
325                 int count = 0;
326                 TarArchiveEntry entry = tar.getNextTarEntry();
327                 while (entry != null) {
328                     count++;
329                     entry = tar.getNextTarEntry();
330                 }
331                 assertEquals(31, count);
332             }
333         } catch (final IOException e) {
334             fail("COMPRESS-245: " + e.getMessage());
335         }
336     }
337 
338     @Test
339     void testRejectsArchivesWithNegativeSizes() throws Exception {
340         try (InputStream in = newInputStream("COMPRESS-569-fail.tar");
341                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
342             getNextEntryUntilIOException(archive);
343         }
344     }
345 
346     /**
347      * This test ensures the implementation is reading the padded last block if a tool has added one to an archive
348      */
349     @Test
350     void testShouldConsumeArchiveCompletely() throws Exception {
351         try (InputStream is = TarArchiveInputStreamTest.class.getResourceAsStream("/archive_with_trailer.tar");
352                 TarArchiveInputStream tar = new TarArchiveInputStream(is)) {
353             while (tar.getNextTarEntry() != null) {
354                 // just consume the archive
355             }
356             final byte[] expected = { 'H', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', '\n' };
357             final byte[] actual = new byte[expected.length];
358             is.read(actual);
359             assertArrayEquals(expected, actual, () -> Arrays.toString(actual));
360         }
361     }
362 
363     @Test
364     void testShouldReadBigGid() throws Exception {
365         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
366         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos)) {
367             tos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX);
368             final TarArchiveEntry t = new TarArchiveEntry("name");
369             t.setGroupId(4294967294L);
370             t.setSize(1);
371             tos.putArchiveEntry(t);
372             tos.write(30);
373             tos.closeArchiveEntry();
374         }
375         final byte[] data = bos.toByteArray();
376         final ByteArrayInputStream bis = new ByteArrayInputStream(data);
377         try (TarArchiveInputStream tis = new TarArchiveInputStream(bis)) {
378             final TarArchiveEntry t = tis.getNextTarEntry();
379             assertEquals(4294967294L, t.getLongGroupId());
380         }
381     }
382 
383     /**
384      * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-324">COMPRESS-324</a>
385      */
386     @Test
387     void testShouldReadGNULongNameEntryWithWrongName() throws Exception {
388         try (TarArchiveInputStream is = getTestStream("/COMPRESS-324.tar")) {
389             final TarArchiveEntry entry = is.getNextTarEntry();
390             assertEquals(
391                     "1234567890123456789012345678901234567890123456789012345678901234567890"
392                             + "1234567890123456789012345678901234567890123456789012345678901234567890"
393                             + "1234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890.txt",
394                     entry.getName());
395         }
396     }
397 
398     @Test
399     void testShouldThrowAnExceptionOnTruncatedEntries() throws Exception {
400         final Path dir = createTempDirectory("COMPRESS-279");
401         try (TarArchiveInputStream is = getTestStream("/COMPRESS-279-fail.tar")) {
402             assertThrows(IOException.class, () -> {
403                 TarArchiveEntry entry = is.getNextTarEntry();
404                 int count = 0;
405                 while (entry != null) {
406                     Files.copy(is, dir.resolve(String.valueOf(count)));
407                     count++;
408                     entry = is.getNextTarEntry();
409                 }
410             });
411         }
412     }
413 
414     @Test
415     void testShouldThrowAnExceptionOnTruncatedStream() throws Exception {
416         final Path dir = createTempDirectory("COMPRESS-279");
417         try (TarArchiveInputStream is = getTestStream("/COMPRESS-279-fail.tar")) {
418             final AtomicInteger count = new AtomicInteger();
419             assertThrows(IOException.class, () -> is.forEach(entry -> Files.copy(is, dir.resolve(String.valueOf(count.getAndIncrement())))));
420         }
421     }
422 
423     @Test
424     void testShouldUseSpecifiedEncodingWhenReadingGNULongNames() throws Exception {
425         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
426         final String encoding = StandardCharsets.UTF_16.name();
427         final String name = "1234567890123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789" + "01234567890\u00e4";
428         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos, encoding)) {
429             tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
430             final TarArchiveEntry t = new TarArchiveEntry(name);
431             t.setSize(1);
432             tos.putArchiveEntry(t);
433             tos.write(30);
434             tos.closeArchiveEntry();
435         }
436         final byte[] data = bos.toByteArray();
437         final ByteArrayInputStream bis = new ByteArrayInputStream(data);
438         try (TarArchiveInputStream tis = new TarArchiveInputStream(bis, encoding)) {
439             final TarArchiveEntry t = tis.getNextTarEntry();
440             assertEquals(name, t.getName());
441         }
442     }
443 
444     @Test
445     void testSingleByteReadConsistentlyReturnsMinusOneAtEof() throws Exception {
446         try (InputStream in = newInputStream("bla.tar");
447                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
448             assertNotNull(archive.getNextEntry());
449             IOUtils.toByteArray(archive);
450             assertEquals(-1, archive.read());
451             assertEquals(-1, archive.read());
452         }
453     }
454 
455     /**
456      * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-417">COMPRESS-417</a>
457      */
458     @Test
459     void testSkipsDevNumbersWhenEntryIsNoDevice() throws Exception {
460         try (TarArchiveInputStream is = getTestStream("/COMPRESS-417.tar")) {
461             assertEquals("test1.xml", is.getNextTarEntry().getName());
462             assertEquals(TarConstants.LF_NORMAL, is.getCurrentEntry().getLinkFlag());
463             assertEquals("test2.xml", is.getNextTarEntry().getName());
464             assertEquals(TarConstants.LF_NORMAL, is.getCurrentEntry().getLinkFlag());
465             assertNull(is.getNextTarEntry());
466         }
467     }
468 
469     /**
470      * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-355">COMPRESS-355</a>
471      */
472     @Test
473     void testSurvivesBlankLinesInPaxHeader() throws Exception {
474         try (TarArchiveInputStream is = getTestStream("/COMPRESS-355.tar")) {
475             final TarArchiveEntry entry = is.getNextTarEntry();
476             assertEquals("package/package.json", entry.getName());
477             assertEquals(TarConstants.LF_NORMAL, entry.getLinkFlag());
478             assertNull(is.getNextTarEntry());
479         }
480     }
481 
482     /**
483      * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-356">COMPRESS-356</a>
484      */
485     @Test
486     void testSurvivesPaxHeaderWithNameEndingInSlash() throws Exception {
487         try (TarArchiveInputStream is = getTestStream("/COMPRESS-356.tar")) {
488             final TarArchiveEntry entry = is.getNextTarEntry();
489             assertEquals("package/package.json", entry.getName());
490             assertEquals(TarConstants.LF_NORMAL, entry.getLinkFlag());
491             assertNull(is.getNextTarEntry());
492         }
493     }
494 
495     @Test
496     void testThrowException() throws IOException {
497         try (InputStream in = newInputStream("COMPRESS-553-fail.tar");
498                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
499             getNextEntryUntilIOException(archive);
500         }
501     }
502 
503     @Test
504     void testThrowExceptionWithNullEntry() throws IOException {
505         try (InputStream in = newInputStream("COMPRESS-554-fail.tar");
506                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
507             getNextEntryUntilIOException(archive);
508         }
509     }
510 
511     @Test
512     void testWorkaroundForBrokenTimeHeader() throws Exception {
513         try (TarArchiveInputStream in = new TarArchiveInputStream(newInputStream("simple-aix-native-tar.tar"))) {
514             TarArchiveEntry tae = in.getNextTarEntry();
515             tae = in.getNextTarEntry();
516             assertEquals("sample/link-to-txt-file.lnk", tae.getName());
517             assertEquals(TarConstants.LF_SYMLINK, tae.getLinkFlag());
518             assertEquals(new Date(0), tae.getLastModifiedDate());
519             assertTrue(tae.isSymbolicLink());
520             assertTrue(tae.isCheckSumOK());
521         }
522     }
523 }