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 java.nio.charset.StandardCharsets.UTF_8;
21  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
22  import static org.junit.jupiter.api.Assertions.assertEquals;
23  import static org.junit.jupiter.api.Assertions.assertFalse;
24  import static org.junit.jupiter.api.Assertions.assertNotNull;
25  import static org.junit.jupiter.api.Assertions.assertNull;
26  import static org.junit.jupiter.api.Assertions.assertThrows;
27  import static org.junit.jupiter.api.Assertions.assertTrue;
28  
29  import java.io.ByteArrayInputStream;
30  import java.io.ByteArrayOutputStream;
31  import java.io.File;
32  import java.io.IOException;
33  import java.io.InputStream;
34  import java.io.InputStreamReader;
35  import java.io.OutputStream;
36  import java.io.Reader;
37  import java.nio.file.Files;
38  import java.security.MessageDigest;
39  import java.util.Calendar;
40  import java.util.Date;
41  import java.util.HashMap;
42  import java.util.Map;
43  import java.util.TimeZone;
44  
45  import org.apache.commons.compress.AbstractTest;
46  import org.apache.commons.compress.archivers.ArchiveEntry;
47  import org.apache.commons.compress.archivers.ArchiveOutputStream;
48  import org.apache.commons.compress.archivers.ArchiveStreamFactory;
49  import org.apache.commons.io.IOUtils;
50  import org.apache.commons.io.output.NullOutputStream;
51  import org.junit.jupiter.api.Disabled;
52  import org.junit.jupiter.api.Test;
53  
54  public class TarArchiveOutputStreamTest extends AbstractTest {
55  
56      private static byte[] createTarArchiveContainingOneDirectory(final String fileName, final Date modificationDate) throws IOException {
57          final ByteArrayOutputStream baos = new ByteArrayOutputStream();
58          try (TarArchiveOutputStream tarOut = new TarArchiveOutputStream(baos, 1024)) {
59              tarOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
60              final TarArchiveEntry tarEntry = new TarArchiveEntry("d");
61              tarEntry.setModTime(modificationDate);
62              tarEntry.setMode(TarArchiveEntry.DEFAULT_DIR_MODE);
63              tarEntry.setModTime(modificationDate.getTime());
64              tarEntry.setName(fileName);
65              tarOut.putArchiveEntry(tarEntry);
66              tarOut.closeArchiveEntry();
67          }
68  
69          return baos.toByteArray();
70      }
71  
72      private byte[] getResourceContents(final String name) throws IOException {
73          ByteArrayOutputStream bos;
74          try (InputStream resourceAsStream = getClass().getResourceAsStream(name)) {
75              bos = new ByteArrayOutputStream();
76              IOUtils.copy(resourceAsStream, bos);
77          }
78          return bos.toByteArray();
79      }
80  
81      @Test
82      public void testBigNumberErrorMode() throws Exception {
83          final TarArchiveEntry t = new TarArchiveEntry("foo");
84          t.setSize(0100000000000L);
85          final ByteArrayOutputStream bos = new ByteArrayOutputStream();
86          try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos)) {
87              assertThrows(IllegalArgumentException.class, () -> tos.putArchiveEntry(t));
88          }
89      }
90  
91      @Test
92      public void testBigNumberPosixMode() throws Exception {
93          final TarArchiveEntry t = new TarArchiveEntry("foo");
94          t.setSize(0100000000000L);
95          final ByteArrayOutputStream bos = new ByteArrayOutputStream();
96          final TarArchiveOutputStream tos = new TarArchiveOutputStream(bos);
97          tos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX);
98          tos.putArchiveEntry(t);
99          // make sure header is written to byte array
100         tos.write(new byte[10 * 1024]);
101         final byte[] data = bos.toByteArray();
102         assertEquals("00000000000 ",
103                 new String(data, 1024 + TarConstants.NAMELEN + TarConstants.MODELEN + TarConstants.UIDLEN + TarConstants.GIDLEN, 12, UTF_8));
104         try (TarArchiveInputStream tin = new TarArchiveInputStream(new ByteArrayInputStream(data))) {
105             final TarArchiveEntry e = tin.getNextTarEntry();
106             assertEquals(0100000000000L, e.getSize());
107         }
108         // generates IOE because of unclosed entries.
109         // However, we don't really want to create such large entries.
110         closeQuietly(tos);
111     }
112 
113     @Test
114     public void testBigNumberStarMode() throws Exception {
115         final TarArchiveEntry t = new TarArchiveEntry("foo");
116         t.setSize(0100000000000L);
117         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
118         final TarArchiveOutputStream tos = new TarArchiveOutputStream(bos);
119         tos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR);
120         tos.putArchiveEntry(t);
121         // make sure header is written to byte array
122         tos.write(new byte[10 * 1024]);
123         final byte[] data = bos.toByteArray();
124         assertEquals(0x80, data[TarConstants.NAMELEN + TarConstants.MODELEN + TarConstants.UIDLEN + TarConstants.GIDLEN] & 0x80);
125         try (TarArchiveInputStream tin = new TarArchiveInputStream(new ByteArrayInputStream(data))) {
126             final TarArchiveEntry e = tin.getNextTarEntry();
127             assertEquals(0100000000000L, e.getSize());
128         }
129         // generates IOE because of unclosed entries.
130         // However, we don't really want to create such large entries.
131         closeQuietly(tos);
132     }
133 
134     @Test
135     public void testBlockSizes() throws Exception {
136         final String fileName = "/test1.xml";
137         final byte[] contents = getResourceContents(fileName);
138         testPadding(TarConstants.DEFAULT_BLKSIZE, fileName, contents); // USTAR / pre-pax
139         testPadding(5120, fileName, contents); // PAX default
140         testPadding(1 << 15, fileName, contents); // PAX max
141         testPadding(-2, fileName, contents); // don't specify a block size -> use minimum length
142 
143         // don't specify a block size -> use minimum length
144         assertThrows(IllegalArgumentException.class, () -> testPadding(511, fileName, contents));
145 
146         // don't specify a block size -> use minimum length
147         assertThrows(IllegalArgumentException.class, () -> testPadding(0, fileName, contents));
148 
149         // test with "content" that is an exact multiple of record length
150         final byte[] contents2 = new byte[2048];
151         java.util.Arrays.fill(contents2, (byte) 42);
152         testPadding(TarConstants.DEFAULT_BLKSIZE, fileName, contents2);
153     }
154 
155     @Test
156     public void testCount() throws Exception {
157         final File f = createTempFile("commons-compress-tarcount", ".tar");
158         try (OutputStream fos = Files.newOutputStream(f.toPath());
159                 ArchiveOutputStream<ArchiveEntry> tarOut = ArchiveStreamFactory.DEFAULT.createArchiveOutputStream(ArchiveStreamFactory.TAR, fos)) {
160             final File file1 = getFile("test1.xml");
161             final TarArchiveEntry sEntry = new TarArchiveEntry(file1, file1.getName());
162             tarOut.putArchiveEntry(sEntry);
163             try (InputStream in = Files.newInputStream(file1.toPath())) {
164                 final byte[] buf = new byte[8192];
165                 int read = 0;
166                 while ((read = in.read(buf)) > 0) {
167                     tarOut.write(buf, 0, read);
168                 }
169             }
170             tarOut.closeArchiveEntry();
171             // Close, then measure, and test.
172             tarOut.close();
173             assertEquals(f.length(), tarOut.getBytesWritten());
174         }
175     }
176 
177     /**
178      * When using long file names the longLinkEntry included the current timestamp as the Entry modification date. This was never exposed to the client, but it
179      * caused identical archives to have different MD5 hashes.
180      */
181     @Test
182     public void testLongNameMd5Hash() throws Exception {
183         // @formatter:off
184         final String longFileName =
185             "a/considerably/longer/file/name/which/forces/use/of/the/long/link/header/which/appears/to/always/use/the/current/time/as/modification/date";
186         // @formatter:on
187         final Date modificationDate = new Date();
188 
189         final byte[] archive1 = createTarArchiveContainingOneDirectory(longFileName, modificationDate);
190         final byte[] digest1 = MessageDigest.getInstance("MD5").digest(archive1);
191 
192         // let a second elapse otherwise the modification dates will be equal
193         Thread.sleep(1000L);
194 
195         // now recreate exactly the same tar file
196         final byte[] archive2 = createTarArchiveContainingOneDirectory(longFileName, modificationDate);
197         // and I would expect the MD5 hash to be the same, but for long names it isn't
198         final byte[] digest2 = MessageDigest.getInstance("MD5").digest(archive2);
199 
200         assertArrayEquals(digest1, digest2);
201 
202         // do I still have the correct modification date?
203         // let a second elapse, so we don't get the current time
204         Thread.sleep(1000);
205         try (TarArchiveInputStream tarIn = new TarArchiveInputStream(new ByteArrayInputStream(archive2))) {
206             final ArchiveEntry nextEntry = tarIn.getNextEntry();
207             assertEquals(longFileName, nextEntry.getName());
208             // tar archive stores modification time to second granularity only (floored)
209             assertEquals(modificationDate.getTime() / 1000, nextEntry.getLastModifiedDate().getTime() / 1000);
210         }
211     }
212 
213     @Test
214     public void testMaxFileSizeError() throws Exception {
215         final TarArchiveEntry t = new TarArchiveEntry("foo");
216         t.setSize(077777777777L);
217         final TarArchiveOutputStream tos1 = new TarArchiveOutputStream(new ByteArrayOutputStream());
218         tos1.putArchiveEntry(t);
219         t.setSize(0100000000000L);
220         final TarArchiveOutputStream tos2 = new TarArchiveOutputStream(new ByteArrayOutputStream());
221         assertThrows(RuntimeException.class, () -> tos2.putArchiveEntry(t), "Should have generated RuntimeException");
222     }
223 
224     @Test
225     public void testOldEntryError() throws Exception {
226         final TarArchiveEntry t = new TarArchiveEntry("foo");
227         t.setSize(Integer.MAX_VALUE);
228         t.setModTime(-1000);
229         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(new ByteArrayOutputStream())) {
230             assertThrows(RuntimeException.class, () -> tos.putArchiveEntry(t));
231         }
232     }
233 
234     @Test
235     public void testOldEntryPosixMode() throws Exception {
236         final TarArchiveEntry t = new TarArchiveEntry("foo");
237         t.setSize(Integer.MAX_VALUE);
238         t.setModTime(-1000);
239         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
240         final TarArchiveOutputStream tos = new TarArchiveOutputStream(bos);
241         tos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX);
242         tos.putArchiveEntry(t);
243         // make sure header is written to byte array
244         tos.write(new byte[10 * 1024]);
245         final byte[] data = bos.toByteArray();
246         assertEquals("00000000000 ", new String(data,
247                 1024 + TarConstants.NAMELEN + TarConstants.MODELEN + TarConstants.UIDLEN + TarConstants.GIDLEN + TarConstants.SIZELEN, 12, UTF_8));
248         try (TarArchiveInputStream tin = new TarArchiveInputStream(new ByteArrayInputStream(data))) {
249             final TarArchiveEntry e = tin.getNextTarEntry();
250             final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
251             cal.set(1969, 11, 31, 23, 59, 59);
252             cal.set(Calendar.MILLISECOND, 0);
253             assertEquals(cal.getTime(), e.getLastModifiedDate());
254         }
255         // generates IOE because of unclosed entries.
256         // However, we don't really want to create such large entries.
257         closeQuietly(tos);
258     }
259 
260     @Test
261     public void testOldEntryStarMode() throws Exception {
262         final TarArchiveEntry t = new TarArchiveEntry("foo");
263         t.setSize(Integer.MAX_VALUE);
264         t.setModTime(-1000);
265         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
266         final TarArchiveOutputStream tos = new TarArchiveOutputStream(bos);
267         tos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR);
268         tos.putArchiveEntry(t);
269         // make sure header is written to byte array
270         tos.write(new byte[10 * 1024]);
271         final byte[] data = bos.toByteArray();
272         assertEquals((byte) 0xff, data[TarConstants.NAMELEN + TarConstants.MODELEN + TarConstants.UIDLEN + TarConstants.GIDLEN + TarConstants.SIZELEN]);
273         try (TarArchiveInputStream tin = new TarArchiveInputStream(new ByteArrayInputStream(data))) {
274             final TarArchiveEntry e = tin.getNextTarEntry();
275             final Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
276             cal.set(1969, 11, 31, 23, 59, 59);
277             cal.set(Calendar.MILLISECOND, 0);
278             assertEquals(cal.getTime(), e.getLastModifiedDate());
279         }
280         // generates IOE because of unclosed entries.
281         // However, we don't really want to create such large entries.
282         closeQuietly(tos);
283     }
284 
285     private void testPadding(int blockSize, final String fileName, final byte[] contents) throws IOException {
286         final File f = createTempFile("commons-compress-padding", ".tar");
287         try (OutputStream fos = Files.newOutputStream(f.toPath())) {
288             final TarArchiveOutputStream tos;
289             if (blockSize != -2) {
290                 tos = new TarArchiveOutputStream(fos, blockSize);
291             } else {
292                 blockSize = 512;
293                 tos = new TarArchiveOutputStream(fos);
294             }
295             TarArchiveEntry sEntry;
296             sEntry = new TarArchiveEntry(fileName);
297             sEntry.setSize(contents.length);
298             tos.putArchiveEntry(sEntry);
299             tos.write(contents);
300             tos.closeArchiveEntry();
301             tos.close();
302             final int fileRecordsSize = (int) Math.ceil((double) contents.length / 512) * 512;
303             final int headerSize = 512;
304             final int endOfArchiveSize = 1024;
305             final int unpaddedSize = headerSize + fileRecordsSize + endOfArchiveSize;
306             final int paddedSize = (int) Math.ceil((double) unpaddedSize / blockSize) * blockSize;
307             assertEquals(paddedSize, f.length());
308         }
309     }
310 
311     @Test
312     public void testPaxHeadersWithLength101() throws Exception {
313         final Map<String, String> m = new HashMap<>();
314         m.put("a", "0123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789" + "0123");
315         final byte[] data = writePaxHeader(m);
316         assertEquals("00000000145 ", new String(data, TarConstants.NAMELEN + TarConstants.MODELEN + TarConstants.UIDLEN + TarConstants.GIDLEN, 12, UTF_8));
317         assertEquals("101 a=0123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789" + "0123\n",
318                 new String(data, 512, 101, UTF_8));
319     }
320 
321     @Test
322     public void testPaxHeadersWithLength99() throws Exception {
323         final Map<String, String> m = new HashMap<>();
324         m.put("a", "0123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789" + "012");
325         final byte[] data = writePaxHeader(m);
326         assertEquals("00000000143 ", new String(data, TarConstants.NAMELEN + TarConstants.MODELEN + TarConstants.UIDLEN + TarConstants.GIDLEN, 12, UTF_8));
327         assertEquals("99 a=0123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789" + "012\n",
328                 new String(data, 512, 99, UTF_8));
329     }
330 
331     @Test
332     public void testPutGlobalPaxHeaderEntry() throws IOException {
333         final String x = "If at first you don't succeed, give up";
334         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
335         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos)) {
336             final int pid = 73;
337             final int globCount = 1;
338             final byte lfPaxGlobalExtendedHeader = TarConstants.LF_PAX_GLOBAL_EXTENDED_HEADER;
339             final TarArchiveEntry globalHeader = new TarArchiveEntry("/tmp/GlobalHead." + pid + "." + globCount, lfPaxGlobalExtendedHeader);
340             globalHeader.addPaxHeader("SCHILLY.xattr.user.org.apache.weasels", "global-weasels");
341             tos.putArchiveEntry(globalHeader);
342             TarArchiveEntry entry = new TarArchiveEntry("message");
343             entry.setSize(x.length());
344             tos.putArchiveEntry(entry);
345             tos.write(x.getBytes());
346             tos.closeArchiveEntry();
347             entry = new TarArchiveEntry("counter-message");
348             final String y = "Nothing succeeds like excess";
349             entry.setSize(y.length());
350             entry.addPaxHeader("SCHILLY.xattr.user.org.apache.weasels.species", "unknown");
351             tos.putArchiveEntry(entry);
352             tos.write(y.getBytes());
353             tos.closeArchiveEntry();
354         }
355         final TarArchiveInputStream in = new TarArchiveInputStream(new ByteArrayInputStream(bos.toByteArray()));
356         TarArchiveEntry entryIn = in.getNextTarEntry();
357         assertNotNull(entryIn);
358         assertEquals("message", entryIn.getName());
359         assertEquals(TarConstants.LF_NORMAL, entryIn.getLinkFlag());
360         assertEquals("global-weasels", entryIn.getExtraPaxHeader("SCHILLY.xattr.user.org.apache.weasels"));
361         final Reader reader = new InputStreamReader(in);
362         for (int i = 0; i < x.length(); i++) {
363             assertEquals(x.charAt(i), reader.read());
364         }
365         assertEquals(-1, reader.read());
366         entryIn = in.getNextTarEntry();
367         assertEquals("counter-message", entryIn.getName());
368         assertEquals("global-weasels", entryIn.getExtraPaxHeader("SCHILLY.xattr.user.org.apache.weasels"));
369         assertEquals("unknown", entryIn.getExtraPaxHeader("SCHILLY.xattr.user.org.apache.weasels.species"));
370         assertNull(in.getNextTarEntry());
371     }
372 
373     @SuppressWarnings("deprecation")
374     @Test
375     public void testRecordSize() throws IOException {
376         assertThrows(IllegalArgumentException.class, () -> new TarArchiveOutputStream(new ByteArrayOutputStream(), 512, 511),
377                 "should have rejected recordSize of 511");
378         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(new ByteArrayOutputStream(), 512, 512)) {
379             assertEquals(512, tos.getRecordSize(), "recordSize");
380         }
381         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(new ByteArrayOutputStream(), 512, 512, null)) {
382             assertEquals(512, tos.getRecordSize(), "recordSize");
383         }
384     }
385 
386     private void testRoundtripWith67CharFileName(final int mode) throws Exception {
387         final String n = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
388         assertEquals(67, n.length());
389         final TarArchiveEntry t = new TarArchiveEntry(n);
390         t.setSize(10 * 1024);
391         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
392         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos, "ASCII")) {
393             tos.setLongFileMode(mode);
394             tos.putArchiveEntry(t);
395             tos.write(new byte[10 * 1024]);
396             tos.closeArchiveEntry();
397         }
398         final byte[] data = bos.toByteArray();
399         try (TarArchiveInputStream tin = new TarArchiveInputStream(new ByteArrayInputStream(data))) {
400             assertEquals(n, tin.getNextTarEntry().getName());
401         }
402     }
403 
404     /**
405      * @see "https://issues.apache.org/jira/browse/COMPRESS-200"
406      */
407     @Test
408     public void testRoundtripWith67CharFileNameGnu() throws Exception {
409         testRoundtripWith67CharFileName(TarArchiveOutputStream.LONGFILE_GNU);
410     }
411 
412     /**
413      * @see "https://issues.apache.org/jira/browse/COMPRESS-200"
414      */
415     @Test
416     public void testRoundtripWith67CharFileNamePosix() throws Exception {
417         testRoundtripWith67CharFileName(TarArchiveOutputStream.LONGFILE_POSIX);
418     }
419 
420     private void testWriteLongDirectoryName(final int mode) throws Exception {
421         final String n = "01234567890123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789"
422                 + "01234567890123456789012345678901234567890123456789/";
423         final TarArchiveEntry t = new TarArchiveEntry(n);
424         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
425         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos, "ASCII")) {
426             tos.setLongFileMode(mode);
427             tos.putArchiveEntry(t);
428             tos.closeArchiveEntry();
429         }
430         final byte[] data = bos.toByteArray();
431         try (TarArchiveInputStream tin = new TarArchiveInputStream(new ByteArrayInputStream(data))) {
432             final TarArchiveEntry e = tin.getNextTarEntry();
433             assertEquals(n, e.getName());
434             assertTrue(e.isDirectory());
435         }
436     }
437 
438     @Test
439     public void testWriteLongDirectoryNameErrorMode() throws Exception {
440         final String n = "01234567890123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789"
441                 + "01234567890123456789012345678901234567890123456789/";
442 
443         assertThrows(RuntimeException.class, () -> {
444             final TarArchiveEntry t = new TarArchiveEntry(n);
445             final ByteArrayOutputStream bos = new ByteArrayOutputStream();
446             try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos, "ASCII")) {
447                 tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_ERROR);
448                 tos.putArchiveEntry(t);
449                 tos.closeArchiveEntry();
450             }
451         }, "Truncated name didn't throw an exception");
452     }
453 
454     /**
455      * @see "https://issues.apache.org/jira/browse/COMPRESS-203"
456      */
457     @Test
458     public void testWriteLongDirectoryNameGnuMode() throws Exception {
459         testWriteLongDirectoryName(TarArchiveOutputStream.LONGFILE_GNU);
460     }
461 
462     /**
463      * @see "https://issues.apache.org/jira/browse/COMPRESS-203"
464      */
465     @Test
466     public void testWriteLongDirectoryNamePosixMode() throws Exception {
467         testWriteLongDirectoryName(TarArchiveOutputStream.LONGFILE_POSIX);
468     }
469 
470     @Test
471     public void testWriteLongDirectoryNameTruncateMode() throws Exception {
472         final String n = "01234567890123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789"
473                 + "01234567890123456789012345678901234567890123456789/";
474         final TarArchiveEntry t = new TarArchiveEntry(n);
475         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
476         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos, "ASCII")) {
477             tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_TRUNCATE);
478             tos.putArchiveEntry(t);
479             tos.closeArchiveEntry();
480         }
481         final byte[] data = bos.toByteArray();
482         try (TarArchiveInputStream tin = new TarArchiveInputStream(new ByteArrayInputStream(data))) {
483             final TarArchiveEntry e = tin.getNextTarEntry();
484             assertEquals(n.substring(0, TarConstants.NAMELEN) + "/", e.getName(), "Entry name");
485             assertEquals(TarConstants.LF_DIR, e.getLinkFlag());
486             assertTrue(e.isDirectory(), "The entry is not a directory");
487         }
488     }
489 
490     @Test
491     public void testWriteLongFileNamePosixMode() throws Exception {
492         // @formatter:off
493         final String n = "01234567890123456789012345678901234567890123456789"
494                 + "01234567890123456789012345678901234567890123456789"
495                 + "01234567890123456789012345678901234567890123456789";
496         // @formatter:on
497         final TarArchiveEntry t = new TarArchiveEntry(n);
498         t.setSize(10 * 1024);
499         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
500         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos, "ASCII")) {
501             tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
502             tos.putArchiveEntry(t);
503             tos.write(new byte[10 * 1024]);
504             tos.closeArchiveEntry();
505             final byte[] data = bos.toByteArray();
506             assertEquals("160 path=" + n + "\n", new String(data, 512, 160, UTF_8));
507             try (TarArchiveInputStream tin = new TarArchiveInputStream(new ByteArrayInputStream(data))) {
508                 assertEquals(n, tin.getNextTarEntry().getName());
509                 assertEquals(TarConstants.LF_NORMAL, tin.getCurrentEntry().getLinkFlag());
510             }
511         }
512     }
513 
514     @Test
515     public void testWriteLongFileNameThrowsException() throws Exception {
516         final String n = "01234567890123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789"
517                 + "01234567890123456789012345678901234567890123456789";
518         final TarArchiveEntry t = new TarArchiveEntry(n);
519         final TarArchiveOutputStream tos = new TarArchiveOutputStream(new ByteArrayOutputStream(), "ASCII");
520         assertThrows(IllegalArgumentException.class, () -> tos.putArchiveEntry(t));
521     }
522 
523     /**
524      * @see "https://issues.apache.org/jira/browse/COMPRESS-237"
525      */
526     private void testWriteLongLinkName(final int mode) throws Exception {
527         final String linkName = "01234567890123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789"
528                 + "01234567890123456789012345678901234567890123456789/test";
529         final TarArchiveEntry entry = new TarArchiveEntry("test", TarConstants.LF_SYMLINK);
530         entry.setLinkName(linkName);
531 
532         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
533         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos, "ASCII")) {
534             tos.setLongFileMode(mode);
535             tos.putArchiveEntry(entry);
536             tos.closeArchiveEntry();
537         }
538 
539         final byte[] data = bos.toByteArray();
540         try (TarArchiveInputStream tin = new TarArchiveInputStream(new ByteArrayInputStream(data))) {
541             final TarArchiveEntry e = tin.getNextTarEntry();
542             assertEquals("test", e.getName(), "Entry name");
543             assertEquals(linkName, e.getLinkName(), "Link name");
544             assertTrue(e.isSymbolicLink(), "The entry is not a symbolic link");
545             assertEquals(TarConstants.LF_SYMLINK, e.getLinkFlag(), "Link flag");
546         }
547     }
548 
549     /**
550      * @see "https://issues.apache.org/jira/browse/COMPRESS-237"
551      */
552     @Test
553     public void testWriteLongLinkNameErrorMode() throws Exception {
554         final String linkName = "01234567890123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789"
555                 + "01234567890123456789012345678901234567890123456789/test";
556         final TarArchiveEntry entry = new TarArchiveEntry("test", TarConstants.LF_SYMLINK);
557         entry.setLinkName(linkName);
558 
559         assertThrows(RuntimeException.class, () -> {
560             final ByteArrayOutputStream bos = new ByteArrayOutputStream();
561             try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos, "ASCII")) {
562                 tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_ERROR);
563                 tos.putArchiveEntry(entry);
564                 tos.closeArchiveEntry();
565             }
566         }, "Truncated link name didn't throw an exception");
567     }
568 
569     /**
570      * @see "https://issues.apache.org/jira/browse/COMPRESS-237"
571      */
572     @Test
573     public void testWriteLongLinkNameGnuMode() throws Exception {
574         testWriteLongLinkName(TarArchiveOutputStream.LONGFILE_GNU);
575     }
576 
577     /**
578      * @see "https://issues.apache.org/jira/browse/COMPRESS-237"
579      */
580     @Test
581     public void testWriteLongLinkNamePosixMode() throws Exception {
582         testWriteLongLinkName(TarArchiveOutputStream.LONGFILE_POSIX);
583     }
584 
585     @Test
586     public void testWriteLongLinkNameTruncateMode() throws Exception {
587         final String linkName = "01234567890123456789012345678901234567890123456789" + "01234567890123456789012345678901234567890123456789"
588                 + "01234567890123456789012345678901234567890123456789/";
589         final TarArchiveEntry entry = new TarArchiveEntry("test", TarConstants.LF_SYMLINK);
590         entry.setLinkName(linkName);
591 
592         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
593         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos, "ASCII")) {
594             tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_TRUNCATE);
595             tos.putArchiveEntry(entry);
596             tos.closeArchiveEntry();
597         }
598 
599         final byte[] data = bos.toByteArray();
600         try (TarArchiveInputStream tin = new TarArchiveInputStream(new ByteArrayInputStream(data))) {
601             final TarArchiveEntry e = tin.getNextTarEntry();
602             assertEquals(linkName.substring(0, TarConstants.NAMELEN), e.getLinkName(), "Link name");
603             assertEquals(TarConstants.LF_SYMLINK, e.getLinkFlag(), "Link flag");
604         }
605     }
606 
607     /**
608      * @see "https://issues.apache.org/jira/browse/COMPRESS-203"
609      */
610     @Test
611     public void testWriteNonAsciiDirectoryNamePosixMode() throws Exception {
612         final String n = "f\u00f6\u00f6/";
613         final TarArchiveEntry t = new TarArchiveEntry(n);
614         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
615         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos)) {
616             tos.setAddPaxHeadersForNonAsciiNames(true);
617             tos.putArchiveEntry(t);
618             tos.closeArchiveEntry();
619         }
620         final byte[] data = bos.toByteArray();
621         try (TarArchiveInputStream tin = new TarArchiveInputStream(new ByteArrayInputStream(data))) {
622             final TarArchiveEntry e = tin.getNextTarEntry();
623             assertEquals(n, e.getName());
624             assertEquals(TarConstants.LF_DIR, e.getLinkFlag());
625             assertTrue(e.isDirectory());
626         }
627     }
628 
629     @Test
630     public void testWriteNonAsciiLinkPathNamePaxHeader() throws Exception {
631         final String n = "\u00e4";
632         final TarArchiveEntry t = new TarArchiveEntry("a", TarConstants.LF_LINK);
633         t.setSize(10 * 1024);
634         t.setLinkName(n);
635         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
636         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos)) {
637             tos.setAddPaxHeadersForNonAsciiNames(true);
638             tos.putArchiveEntry(t);
639             tos.write(new byte[10 * 1024]);
640             tos.closeArchiveEntry();
641         }
642         final byte[] data = bos.toByteArray();
643         assertEquals("15 linkpath=" + n + "\n", new String(data, 512, 15, UTF_8));
644         try (TarArchiveInputStream tin = new TarArchiveInputStream(new ByteArrayInputStream(data))) {
645             final TarArchiveEntry e = tin.getNextTarEntry();
646             assertEquals(n, e.getLinkName());
647             assertEquals(TarConstants.LF_LINK, e.getLinkFlag(), "Link flag");
648         }
649     }
650 
651     /**
652      * @see "https://issues.apache.org/jira/browse/COMPRESS-265"
653      */
654     @Test
655     public void testWriteNonAsciiNameWithUnfortunateNamePosixMode() throws Exception {
656         final String n = "f\u00f6\u00f6\u00dc";
657         final TarArchiveEntry t = new TarArchiveEntry(n);
658         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
659         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos)) {
660             tos.setAddPaxHeadersForNonAsciiNames(true);
661             tos.putArchiveEntry(t);
662             tos.closeArchiveEntry();
663         }
664         final byte[] data = bos.toByteArray();
665         try (TarArchiveInputStream tin = new TarArchiveInputStream(new ByteArrayInputStream(data))) {
666             final TarArchiveEntry e = tin.getNextTarEntry();
667             assertEquals(n, e.getName());
668             assertEquals(TarConstants.LF_NORMAL, e.getLinkFlag());
669             assertFalse(e.isDirectory());
670         }
671     }
672 
673     @Test
674     public void testWriteNonAsciiPathNamePaxHeader() throws Exception {
675         final String n = "\u00e4";
676         final TarArchiveEntry t = new TarArchiveEntry(n);
677         t.setSize(10 * 1024);
678         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
679         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos)) {
680             tos.setAddPaxHeadersForNonAsciiNames(true);
681             tos.putArchiveEntry(t);
682             tos.write(new byte[10 * 1024]);
683             tos.closeArchiveEntry();
684         }
685         final byte[] data = bos.toByteArray();
686         assertEquals("11 path=" + n + "\n", new String(data, 512, 11, UTF_8));
687         try (TarArchiveInputStream tin = new TarArchiveInputStream(new ByteArrayInputStream(data))) {
688             final TarArchiveEntry e = tin.getNextTarEntry();
689             assertEquals(n, e.getName());
690             assertEquals(TarConstants.LF_NORMAL, e.getLinkFlag());
691         }
692     }
693 
694     @Test
695     public void testWriteSimplePaxHeaders() throws Exception {
696         final Map<String, String> m = new HashMap<>();
697         m.put("a", "b");
698         final byte[] data = writePaxHeader(m);
699         assertEquals("00000000006 ", new String(data, TarConstants.NAMELEN + TarConstants.MODELEN + TarConstants.UIDLEN + TarConstants.GIDLEN, 12, UTF_8));
700         assertEquals("6 a=b\n", new String(data, 512, 6, UTF_8));
701     }
702 
703     /**
704      * @see "https://issues.apache.org/jira/browse/COMPRESS-642"
705      */
706     @Disabled("The test needs to write 1.1 TB in chunks of 512 bytes which takes a long time. So it's disabled by default")
707     @Test
708     public void testWritingBigFile() throws Exception {
709         final TarArchiveEntry t = new TarArchiveEntry("foo");
710         t.setSize((Integer.MAX_VALUE + 1L) * TarConstants.DEFAULT_RCDSIZE);
711         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(NullOutputStream.NULL_OUTPUT_STREAM)) {
712             tos.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_POSIX);
713             tos.putArchiveEntry(t);
714 
715             final byte[] bytes = new byte[TarConstants.DEFAULT_RCDSIZE];
716             for (int i = 0; i < Integer.MAX_VALUE; i++) {
717                 tos.write(bytes);
718             }
719             tos.write(bytes);
720             tos.closeArchiveEntry();
721         }
722     }
723 
724     private byte[] writePaxHeader(final Map<String, String> m) throws Exception {
725         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
726         try (TarArchiveOutputStream tos = new TarArchiveOutputStream(bos, "ASCII")) {
727             tos.writePaxHeaders(new TarArchiveEntry("x"), "foo", m);
728 
729             // add a dummy entry so data gets written
730             final TarArchiveEntry t = new TarArchiveEntry("foo");
731             t.setSize(10 * 1024);
732             tos.putArchiveEntry(t);
733             tos.write(new byte[10 * 1024]);
734             tos.closeArchiveEntry();
735         }
736 
737         return bos.toByteArray();
738     }
739 
740 }