View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one or more
3    *  contributor license agreements.  See the NOTICE file distributed with
4    *  this work for additional information regarding copyright ownership.
5    *  The ASF licenses this file to You under the Apache License, Version 2.0
6    *  (the "License"); you may not use this file except in compliance with
7    *  the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  
18  package org.apache.commons.compress.archivers.tar;
19  
20  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertNotNull;
23  import static org.junit.jupiter.api.Assertions.assertNotSame;
24  import static org.junit.jupiter.api.Assertions.assertNull;
25  import static org.junit.jupiter.api.Assertions.assertSame;
26  import static org.junit.jupiter.api.Assertions.assertThrows;
27  import static org.junit.jupiter.api.Assertions.assertTrue;
28  import static org.junit.jupiter.api.Assertions.fail;
29  
30  import java.io.BufferedOutputStream;
31  import java.io.ByteArrayInputStream;
32  import java.io.ByteArrayOutputStream;
33  import java.io.File;
34  import java.io.IOException;
35  import java.io.InputStream;
36  import java.io.OutputStream;
37  import java.nio.charset.StandardCharsets;
38  import java.nio.file.Files;
39  import java.nio.file.Path;
40  import java.util.Arrays;
41  import java.util.Calendar;
42  import java.util.Date;
43  import java.util.TimeZone;
44  import java.util.zip.GZIPInputStream;
45  
46  import org.apache.commons.compress.AbstractTest;
47  import org.apache.commons.compress.archivers.ArchiveException;
48  import org.apache.commons.compress.archivers.ArchiveStreamFactory;
49  import org.apache.commons.io.IOUtils;
50  import org.apache.commons.io.function.IOConsumer;
51  import org.junit.jupiter.api.Test;
52  
53  public class TarArchiveInputStreamTest extends AbstractTest {
54  
55      private void datePriorToEpoch(final String archive) throws Exception {
56          try (TarArchiveInputStream in = new TarArchiveInputStream(Files.newInputStream(getFile(archive).toPath()))) {
57              final TarArchiveEntry tae = in.getNextTarEntry();
58              assertEquals("foo", tae.getName());
59              assertEquals(TarConstants.LF_NORMAL, tae.getLinkFlag());
60              final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
61              cal.set(1969, 11, 31, 23, 59, 59);
62              cal.set(Calendar.MILLISECOND, 0);
63              assertEquals(cal.getTime(), tae.getLastModifiedDate());
64              assertTrue(tae.isCheckSumOK());
65          }
66      }
67  
68      private void getNextEntryUntilIOException(final TarArchiveInputStream archive) {
69          assertThrows(IOException.class, () -> archive.forEach(IOConsumer.noop()));
70      }
71  
72      @SuppressWarnings("resource") // Caller closes
73      private TarArchiveInputStream getTestStream(final String name) {
74          return new TarArchiveInputStream(TarArchiveInputStreamTest.class.getResourceAsStream(name));
75      }
76  
77      @Test
78      public void testCompress197() throws IOException {
79          try (TarArchiveInputStream tar = getTestStream("/COMPRESS-197.tar")) {
80              TarArchiveEntry entry = tar.getNextTarEntry();
81              while (entry != null) {
82                  entry = tar.getNextTarEntry();
83              }
84          }
85      }
86  
87      @Test
88      public void testCompress197ForEach() throws IOException {
89          try (TarArchiveInputStream tar = getTestStream("/COMPRESS-197.tar")) {
90              tar.forEach(IOConsumer.noop());
91          }
92      }
93  
94      @Test
95      public void testCompress558() throws IOException {
96          final String folderName = "apache-activemq-5.16.0/examples/openwire/advanced-scenarios/jms-example-exclusive-consumer/src/main/";
97          // @formatter:off
98          final String consumerJavaName =
99              "apache-activemq-5.16.0/examples/openwire/advanced-scenarios/jms-example-exclusive-consumer/src/main/java/example/queue/exclusive/Consumer.java";
100         final String producerJavaName =
101             "apache-activemq-5.16.0/examples/openwire/advanced-scenarios/jms-example-exclusive-consumer/src/main/java/example/queue/exclusive/Producer.java";
102         // @formatter:on
103 
104         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
105         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos)) {
106             tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
107             final TarArchiveEntry rootfolder = new TarArchiveEntry(folderName);
108             tos.putArchiveEntry(rootfolder);
109             final TarArchiveEntry consumerJava = new TarArchiveEntry(consumerJavaName);
110             tos.putArchiveEntry(consumerJava);
111             final TarArchiveEntry producerJava = new TarArchiveEntry(producerJavaName);
112             tos.putArchiveEntry(producerJava);
113             tos.closeArchiveEntry();
114         }
115         final byte[] data = bos.toByteArray();
116         try (ByteArrayInputStream bis = new ByteArrayInputStream(data);
117                 TarArchiveInputStream tis = new TarArchiveInputStream(bis)) {
118             assertEquals(folderName, tis.getNextTarEntry().getName());
119             assertEquals(TarConstants.LF_DIR, tis.getCurrentEntry().getLinkFlag());
120             assertEquals(consumerJavaName, tis.getNextTarEntry().getName());
121             assertEquals(TarConstants.LF_NORMAL, tis.getCurrentEntry().getLinkFlag());
122             assertEquals(producerJavaName, tis.getNextTarEntry().getName());
123             assertEquals(TarConstants.LF_NORMAL, tis.getCurrentEntry().getLinkFlag());
124         }
125     }
126 
127     @Test
128     public void testDatePriorToEpochInGNUFormat() throws Exception {
129         datePriorToEpoch("preepoch-star.tar");
130     }
131 
132     @Test
133     public void testDatePriorToEpochInPAXFormat() throws Exception {
134         datePriorToEpoch("preepoch-posix.tar");
135     }
136 
137     @Test
138     public void testDirectoryWithLongNameEndsWithSlash() throws IOException, ArchiveException {
139         final String rootPath = getTempDirFile().getAbsolutePath();
140         final String dirDirectory = "COMPRESS-509";
141         final int count = 100;
142         final File root = new File(rootPath + "/" + dirDirectory);
143         root.mkdirs();
144         for (int i = 1; i < count; i++) {
145             // create empty dirs with incremental length
146             String subDir = "";
147             for (int j = 0; j < i; j++) {
148                 subDir += "a";
149             }
150             final File dir = new File(rootPath + "/" + dirDirectory, "/" + subDir);
151             dir.mkdir();
152 
153             // tar these dirs
154             final String fileName = "/" + dirDirectory + "/" + subDir;
155             final File tarF = new File(rootPath + "/tar" + i + ".tar");
156             try (OutputStream dest = Files.newOutputStream(tarF.toPath())) {
157                 final TarArchiveOutputStream out = new TarArchiveOutputStream(new BufferedOutputStream(dest));
158                 out.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR);
159                 out.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
160 
161                 final File file = new File(rootPath, fileName);
162                 final TarArchiveEntry entry = new TarArchiveEntry(file);
163                 entry.setName(fileName);
164                 out.putArchiveEntry(entry);
165                 out.closeArchiveEntry();
166                 out.flush();
167             }
168 
169             // untar these tars
170             try (InputStream is = Files.newInputStream(tarF.toPath());
171                     TarArchiveInputStream debInputStream = ArchiveStreamFactory.DEFAULT.createArchiveInputStream("tar", is)) {
172                 TarArchiveEntry outEntry;
173                 while ((outEntry = debInputStream.getNextEntry()) != null) {
174                     assertTrue(outEntry.getName().endsWith("/"), outEntry.getName());
175                 }
176             }
177         }
178     }
179 
180     @Test
181     public void testGetAndSetOfPaxEntry() throws Exception {
182         try (TarArchiveInputStream is = getTestStream("/COMPRESS-356.tar")) {
183             final TarArchiveEntry entry = is.getNextTarEntry();
184             assertEquals("package/package.json", entry.getName());
185             assertEquals(TarConstants.LF_NORMAL, entry.getLinkFlag());
186             assertEquals(is.getCurrentEntry(), entry);
187             final TarArchiveEntry weaselEntry = new TarArchiveEntry(entry.getName());
188             weaselEntry.setSize(entry.getSize());
189             is.setCurrentEntry(weaselEntry);
190             assertEquals(entry, is.getCurrentEntry());
191             assertNotSame(entry, is.getCurrentEntry());
192             assertSame(weaselEntry, is.getCurrentEntry());
193             assertThrows(IllegalStateException.class, () -> {
194                 is.setCurrentEntry(null);
195                 is.read();
196             }, "should abort because current entry is nulled");
197             is.setCurrentEntry(entry);
198             is.read();
199         }
200     }
201 
202     @Test
203     public void testMultiByteReadConsistentlyReturnsMinusOneAtEof() throws Exception {
204         final byte[] buf = new byte[2];
205         try (InputStream in = newInputStream("bla.tar");
206                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
207             assertNotNull(archive.getNextEntry());
208             IOUtils.toByteArray(archive);
209             assertEquals(-1, archive.read(buf));
210             assertEquals(-1, archive.read(buf));
211         }
212     }
213 
214     @Test
215     public void testParseTarTruncatedInContent() throws IOException {
216         try (InputStream in = newInputStream("COMPRESS-544_truncated_in_content-fail.tar");
217                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
218             getNextEntryUntilIOException(archive);
219         }
220     }
221 
222     @Test
223     public void testParseTarTruncatedInPadding() throws IOException {
224         try (InputStream in = newInputStream("COMPRESS-544_truncated_in_padding-fail.tar");
225                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
226             getNextEntryUntilIOException(archive);
227         }
228     }
229 
230     @Test
231     public void testParseTarWithNonNumberPaxHeaders() throws IOException {
232         try (InputStream in = newInputStream("COMPRESS-529-fail.tar");
233                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
234             assertThrows(IOException.class, () -> archive.getNextEntry());
235         }
236     }
237 
238     @Test
239     public void testParseTarWithSpecialPaxHeaders() throws IOException {
240         try (InputStream in = newInputStream("COMPRESS-530-fail.tar");
241                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
242             assertThrows(IOException.class, () -> archive.getNextEntry());
243             assertThrows(IOException.class, () -> IOUtils.toByteArray(archive));
244         }
245     }
246 
247     @Test
248     public void testReadsArchiveCompletely_COMPRESS245() {
249         try (InputStream is = TarArchiveInputStreamTest.class.getResourceAsStream("/COMPRESS-245.tar.gz")) {
250             final InputStream gin = new GZIPInputStream(is);
251             try (TarArchiveInputStream tar = new TarArchiveInputStream(gin)) {
252                 int count = 0;
253                 TarArchiveEntry entry = tar.getNextTarEntry();
254                 while (entry != null) {
255                     count++;
256                     entry = tar.getNextTarEntry();
257                 }
258                 assertEquals(31, count);
259             }
260         } catch (final IOException e) {
261             fail("COMPRESS-245: " + e.getMessage());
262         }
263     }
264 
265     @Test
266     public void testRejectsArchivesWithNegativeSizes() throws Exception {
267         try (InputStream in = newInputStream("COMPRESS-569-fail.tar");
268                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
269             getNextEntryUntilIOException(archive);
270         }
271     }
272 
273     /**
274      * This test ensures the implementation is reading the padded last block if a tool has added one to an archive
275      */
276     @Test
277     public void testShouldConsumeArchiveCompletely() throws Exception {
278         try (InputStream is = TarArchiveInputStreamTest.class.getResourceAsStream("/archive_with_trailer.tar");
279                 TarArchiveInputStream tar = new TarArchiveInputStream(is)) {
280             while (tar.getNextTarEntry() != null) {
281                 // just consume the archive
282             }
283             final byte[] expected = { 'H', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', '\n' };
284             final byte[] actual = new byte[expected.length];
285             is.read(actual);
286             assertArrayEquals(expected, actual, () -> Arrays.toString(actual));
287         }
288     }
289 
290     @Test
291     public void testShouldReadBigGid() throws Exception {
292         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
293         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos)) {
294             tos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX);
295             final TarArchiveEntry t = new TarArchiveEntry("name");
296             t.setGroupId(4294967294L);
297             t.setSize(1);
298             tos.putArchiveEntry(t);
299             tos.write(30);
300             tos.closeArchiveEntry();
301         }
302         final byte[] data = bos.toByteArray();
303         final ByteArrayInputStream bis = new ByteArrayInputStream(data);
304         try (TarArchiveInputStream tis = new TarArchiveInputStream(bis)) {
305             final TarArchiveEntry t = tis.getNextTarEntry();
306             assertEquals(4294967294L, t.getLongGroupId());
307         }
308     }
309 
310     /**
311      * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-324">COMPRESS-324</a>
312      */
313     @Test
314     public void testShouldReadGNULongNameEntryWithWrongName() throws Exception {
315         try (TarArchiveInputStream is = getTestStream("/COMPRESS-324.tar")) {
316             final TarArchiveEntry entry = is.getNextTarEntry();
317             assertEquals(
318                     "1234567890123456789012345678901234567890123456789012345678901234567890"
319                             + "1234567890123456789012345678901234567890123456789012345678901234567890"
320                             + "1234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890.txt",
321                     entry.getName());
322         }
323     }
324 
325     @Test
326     public void testShouldThrowAnExceptionOnTruncatedEntries() throws Exception {
327         final Path dir = createTempDirectory("COMPRESS-279");
328         try (TarArchiveInputStream is = getTestStream("/COMPRESS-279-fail.tar")) {
329             assertThrows(IOException.class, () -> {
330                 TarArchiveEntry entry = is.getNextTarEntry();
331                 int count = 0;
332                 while (entry != null) {
333                     Files.copy(is, dir.resolve(String.valueOf(count)));
334                     count++;
335                     entry = is.getNextTarEntry();
336                 }
337             });
338         }
339     }
340 
341     @Test
342     public void testShouldUseSpecifiedEncodingWhenReadingGNULongNames() throws Exception {
343         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
344         final String encoding = StandardCharsets.UTF_16.name();
345         final String name = "1234567890123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789" + "01234567890\u00e4";
346         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos, encoding)) {
347             tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
348             final TarArchiveEntry t = new TarArchiveEntry(name);
349             t.setSize(1);
350             tos.putArchiveEntry(t);
351             tos.write(30);
352             tos.closeArchiveEntry();
353         }
354         final byte[] data = bos.toByteArray();
355         final ByteArrayInputStream bis = new ByteArrayInputStream(data);
356         try (TarArchiveInputStream tis = new TarArchiveInputStream(bis, encoding)) {
357             final TarArchiveEntry t = tis.getNextTarEntry();
358             assertEquals(name, t.getName());
359         }
360     }
361 
362     @Test
363     public void testSingleByteReadConsistentlyReturnsMinusOneAtEof() throws Exception {
364         try (InputStream in = newInputStream("bla.tar");
365                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
366             assertNotNull(archive.getNextEntry());
367             IOUtils.toByteArray(archive);
368             assertEquals(-1, archive.read());
369             assertEquals(-1, archive.read());
370         }
371     }
372 
373     /**
374      * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-417">COMPRESS-417</a>
375      */
376     @Test
377     public void testSkipsDevNumbersWhenEntryIsNoDevice() throws Exception {
378         try (TarArchiveInputStream is = getTestStream("/COMPRESS-417.tar")) {
379             assertEquals("test1.xml", is.getNextTarEntry().getName());
380             assertEquals(TarConstants.LF_NORMAL, is.getCurrentEntry().getLinkFlag());
381             assertEquals("test2.xml", is.getNextTarEntry().getName());
382             assertEquals(TarConstants.LF_NORMAL, is.getCurrentEntry().getLinkFlag());
383             assertNull(is.getNextTarEntry());
384         }
385     }
386 
387     /**
388      * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-355">COMPRESS-355</a>
389      */
390     @Test
391     public void testSurvivesBlankLinesInPaxHeader() throws Exception {
392         try (TarArchiveInputStream is = getTestStream("/COMPRESS-355.tar")) {
393             final TarArchiveEntry entry = is.getNextTarEntry();
394             assertEquals("package/package.json", entry.getName());
395             assertEquals(TarConstants.LF_NORMAL, entry.getLinkFlag());
396             assertNull(is.getNextTarEntry());
397         }
398     }
399 
400     /**
401      * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-356">COMPRESS-356</a>
402      */
403     @Test
404     public void testSurvivesPaxHeaderWithNameEndingInSlash() throws Exception {
405         try (TarArchiveInputStream is = getTestStream("/COMPRESS-356.tar")) {
406             final TarArchiveEntry entry = is.getNextTarEntry();
407             assertEquals("package/package.json", entry.getName());
408             assertEquals(TarConstants.LF_NORMAL, entry.getLinkFlag());
409             assertNull(is.getNextTarEntry());
410         }
411     }
412 
413     @Test
414     public void testThrowException() throws IOException {
415         try (InputStream in = newInputStream("COMPRESS-553-fail.tar");
416                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
417             getNextEntryUntilIOException(archive);
418         }
419     }
420 
421     @Test
422     public void testThrowExceptionWithNullEntry() throws IOException {
423         try (InputStream in = newInputStream("COMPRESS-554-fail.tar");
424                 TarArchiveInputStream archive = new TarArchiveInputStream(in)) {
425             getNextEntryUntilIOException(archive);
426         }
427     }
428 
429     @Test
430     public void testWorkaroundForBrokenTimeHeader() throws Exception {
431         try (TarArchiveInputStream in = new TarArchiveInputStream(newInputStream("simple-aix-native-tar.tar"))) {
432             TarArchiveEntry tae = in.getNextTarEntry();
433             tae = in.getNextTarEntry();
434             assertEquals("sample/link-to-txt-file.lnk", tae.getName());
435             assertEquals(TarConstants.LF_SYMLINK, tae.getLinkFlag());
436             assertEquals(new Date(0), tae.getLastModifiedDate());
437             assertTrue(tae.isSymbolicLink());
438             assertTrue(tae.isCheckSumOK());
439         }
440     }
441 
442 }