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