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  package org.apache.commons.compress.archivers.tar;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertFalse;
21  import static org.junit.jupiter.api.Assertions.assertThrows;
22  import static org.junit.jupiter.api.Assertions.assertTrue;
23  import static org.junit.jupiter.api.Assertions.fail;
24  
25  import java.io.BufferedOutputStream;
26  import java.io.ByteArrayOutputStream;
27  import java.io.File;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.io.OutputStream;
31  import java.nio.ByteBuffer;
32  import java.nio.channels.SeekableByteChannel;
33  import java.nio.charset.StandardCharsets;
34  import java.nio.file.Files;
35  import java.nio.file.Path;
36  import java.util.Calendar;
37  import java.util.Date;
38  import java.util.List;
39  import java.util.TimeZone;
40  import java.util.zip.GZIPInputStream;
41  
42  import org.apache.commons.compress.AbstractTest;
43  import org.apache.commons.io.IOUtils;
44  import org.junit.jupiter.api.Test;
45  
46  public class TarFileTest extends AbstractTest {
47  
48      private void datePriorToEpoch(final String archive) throws Exception {
49          try (TarFile tarFile = new TarFile(getPath(archive))) {
50              final TarArchiveEntry entry = tarFile.getEntries().get(0);
51              assertEquals("foo", entry.getName());
52              assertEquals(TarConstants.LF_NORMAL, entry.getLinkFlag());
53              final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
54              cal.set(1969, 11, 31, 23, 59, 59);
55              cal.set(Calendar.MILLISECOND, 0);
56              assertEquals(cal.getTime(), entry.getLastModifiedDate());
57              assertTrue(entry.isCheckSumOK());
58          }
59      }
60  
61      /**
62       * This test ensures the implementation is reading the padded last block if a tool has added one to an archive
63       */
64      @Test
65      public void testArchiveWithTrailer() throws IOException {
66          try (SeekableByteChannel channel = Files.newByteChannel(getPath("archive_with_trailer.tar"));
67                  TarFile tarfile = new TarFile(channel, TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE, null, false)) {
68              final String tarAppendix = "Hello, world!\n";
69              final ByteBuffer buffer = ByteBuffer.allocate(tarAppendix.length());
70              channel.read(buffer);
71              assertEquals(tarAppendix, new String(buffer.array()));
72          }
73      }
74  
75      @Test
76      public void testCompress197() throws IOException {
77          try (TarFile tarFile = new TarFile(getPath("COMPRESS-197.tar"))) {
78              // noop
79          }
80      }
81  
82      @Test
83      public void testCompress558() throws IOException {
84          final String folderName = "apache-activemq-5.16.0/examples/openwire/advanced-scenarios/jms-example-exclusive-consumer/src/main/";
85          // @formatter:off
86          final String consumerJavaName =
87              "apache-activemq-5.16.0/examples/openwire/advanced-scenarios/jms-example-exclusive-consumer/src/main/java/example/queue/exclusive/Consumer.java";
88          final String producerJavaName =
89              "apache-activemq-5.16.0/examples/openwire/advanced-scenarios/jms-example-exclusive-consumer/src/main/java/example/queue/exclusive/Producer.java";
90          // @formatter:on
91  
92          final ByteArrayOutputStream bos = new ByteArrayOutputStream();
93          try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos)) {
94              tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
95              final TarArchiveEntry rootfolder = new TarArchiveEntry(folderName);
96              tos.putArchiveEntry(rootfolder);
97              final TarArchiveEntry consumerJava = new TarArchiveEntry(consumerJavaName);
98              tos.putArchiveEntry(consumerJava);
99              final TarArchiveEntry producerJava = new TarArchiveEntry(producerJavaName);
100             tos.putArchiveEntry(producerJava);
101             tos.closeArchiveEntry();
102         }
103         final byte[] data = bos.toByteArray();
104         try (TarFile tarFile = new TarFile(data)) {
105             final List<TarArchiveEntry> entries = tarFile.getEntries();
106             assertEquals(folderName, entries.get(0).getName());
107             assertEquals(TarConstants.LF_DIR, entries.get(0).getLinkFlag());
108             assertEquals(consumerJavaName, entries.get(1).getName());
109             assertEquals(TarConstants.LF_NORMAL, entries.get(1).getLinkFlag());
110             assertEquals(producerJavaName, entries.get(2).getName());
111             assertEquals(TarConstants.LF_NORMAL, entries.get(2).getLinkFlag());
112         }
113     }
114 
115     @Test
116     public void testCompress657() throws IOException {
117         try (TarFile tarFile = new TarFile(getPath("COMPRESS-657/orjson-3.7.8.tar"))) {
118             for (final TarArchiveEntry entry : tarFile.getEntries()) {
119                 if (entry.isDirectory()) {
120                     // An entry cannot be a directory and a "normal file" at the same time.
121                     assertFalse(entry.isFile(), "Entry '" + entry.getName() + "' is both a directory and a file");
122                 }
123             }
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 {
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             final StringBuilder subDirBuilder = new StringBuilder();
147             for (int j = 0; j < i; j++) {
148                 subDirBuilder.append("a");
149             }
150             final String subDir = subDirBuilder.toString();
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     public 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     public void testParseTarTruncatedInContent() {
191         assertThrows(IOException.class, () -> new TarFile(getPath("COMPRESS-544_truncated_in_content.tar")));
192     }
193 
194     @Test
195     public void testParseTarTruncatedInPadding() {
196         assertThrows(IOException.class, () -> new TarFile(getPath("COMPRESS-544_truncated_in_padding.tar")));
197     }
198 
199     @Test
200     public void testParseTarWithNonNumberPaxHeaders() {
201         assertThrows(IOException.class, () -> new TarFile(getPath("COMPRESS-529-fail.tar")));
202     }
203 
204     @Test
205     public void testParseTarWithSpecialPaxHeaders() {
206         assertThrows(IOException.class, () -> new TarFile(getPath("COMPRESS-530-fail.tar")));
207     }
208 
209     @Test
210     public 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     public void testRejectsArchivesWithNegativeSizes() throws Exception {
226         assertThrows(IOException.class, () -> new TarFile(getFile("COMPRESS-569-fail.tar")));
227     }
228 
229     @Test
230     public 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     public 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     public void testShouldThrowAnExceptionOnTruncatedEntries() throws Exception {
265         createTempDirectory("COMPRESS-279");
266         assertThrows(IOException.class, () -> new TarFile(getPath("COMPRESS-279.tar")));
267     }
268 
269     @Test
270     public 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     public 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     public 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     public 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     public 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     public void testThrowException() {
343         assertThrows(IOException.class, () -> new TarFile(getPath("COMPRESS-553-fail.tar")));
344     }
345 
346     @Test
347     public void testThrowExceptionWithNullEntry() {
348         assertThrows(IOException.class, () -> new TarFile(getPath("COMPRESS-554-fail.tar")));
349     }
350 
351     @Test
352     public 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 }