001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.commons.compress.archivers.tar;
020
021import static java.nio.charset.StandardCharsets.UTF_8;
022
023import java.io.File;
024import java.io.IOException;
025import java.io.OutputStream;
026import java.io.StringWriter;
027import java.math.BigDecimal;
028import java.math.RoundingMode;
029import java.nio.ByteBuffer;
030import java.nio.file.LinkOption;
031import java.nio.file.Path;
032import java.nio.file.attribute.FileTime;
033import java.time.Instant;
034import java.util.Arrays;
035import java.util.HashMap;
036import java.util.Map;
037
038import org.apache.commons.compress.archivers.ArchiveOutputStream;
039import org.apache.commons.compress.archivers.zip.ZipEncoding;
040import org.apache.commons.compress.archivers.zip.ZipEncodingHelper;
041import org.apache.commons.compress.utils.CharsetNames;
042import org.apache.commons.compress.utils.CountingOutputStream;
043import org.apache.commons.compress.utils.FixedLengthBlockOutputStream;
044import org.apache.commons.compress.utils.TimeUtils;
045
046/**
047 * The TarOutputStream writes a UNIX tar archive as an OutputStream. Methods are provided to put
048 * entries, and then write their contents by writing to this stream using write().
049 *
050 * <p>tar archives consist of a sequence of records of 512 bytes each
051 * that are grouped into blocks. Prior to Apache Commons Compress 1.14
052 * it has been possible to configure a record size different from 512
053 * bytes and arbitrary block sizes. Starting with Compress 1.15 512 is
054 * the only valid option for the record size and the block size must
055 * be a multiple of 512. Also the default block size changed from
056 * 10240 bytes prior to Compress 1.15 to 512 bytes with Compress
057 * 1.15.</p>
058 *
059 * @NotThreadSafe
060 */
061public class TarArchiveOutputStream extends ArchiveOutputStream<TarArchiveEntry> {
062
063    /**
064     * Fail if a long file name is required in the archive.
065     */
066    public static final int LONGFILE_ERROR = 0;
067
068    /**
069     * Long paths will be truncated in the archive.
070     */
071    public static final int LONGFILE_TRUNCATE = 1;
072
073    /**
074     * GNU tar extensions are used to store long file names in the archive.
075     */
076    public static final int LONGFILE_GNU = 2;
077
078    /**
079     * POSIX/PAX extensions are used to store long file names in the archive.
080     */
081    public static final int LONGFILE_POSIX = 3;
082
083    /**
084     * Fail if a big number (e.g. size &gt; 8GiB) is required in the archive.
085     */
086    public static final int BIGNUMBER_ERROR = 0;
087
088    /**
089     * star/GNU tar/BSD tar extensions are used to store big number in the archive.
090     */
091    public static final int BIGNUMBER_STAR = 1;
092
093    /**
094     * POSIX/PAX extensions are used to store big numbers in the archive.
095     */
096    public static final int BIGNUMBER_POSIX = 2;
097    private static final int RECORD_SIZE = 512;
098
099    private static final ZipEncoding ASCII = ZipEncodingHelper.getZipEncoding(CharsetNames.US_ASCII);
100
101    private static final int BLOCK_SIZE_UNSPECIFIED = -511;
102    private long currSize;
103    private String currName;
104    private long currBytes;
105    private final byte[] recordBuf;
106    private int longFileMode = LONGFILE_ERROR;
107    private int bigNumberMode = BIGNUMBER_ERROR;
108
109    private long recordsWritten;
110
111    private final int recordsPerBlock;
112
113    private boolean closed;
114
115    /**
116     * Indicates if putArchiveEntry has been called without closeArchiveEntry
117     */
118
119    private boolean haveUnclosedEntry;
120
121    /**
122     * indicates if this archive is finished
123     */
124    private boolean finished;
125
126    private final FixedLengthBlockOutputStream out;
127
128    private final CountingOutputStream countingOut;
129
130    private final ZipEncoding zipEncoding;
131
132    /**
133     * The provided encoding (for unit tests).
134     */
135    final String encoding;
136
137    private boolean addPaxHeadersForNonAsciiNames;
138
139    /**
140     * Constructs a new instance.
141     *
142     * <p>Uses a block size of 512 bytes.</p>
143     *
144     * @param os the output stream to use
145     */
146    public TarArchiveOutputStream(final OutputStream os) {
147        this(os, BLOCK_SIZE_UNSPECIFIED);
148    }
149
150    /**
151     * Constructs a new instance.
152     *
153     * @param os the output stream to use
154     * @param blockSize the block size to use. Must be a multiple of 512 bytes.
155     */
156    public TarArchiveOutputStream(final OutputStream os, final int blockSize) {
157        this(os, blockSize, null);
158    }
159
160    /**
161     * Constructs a new instance.
162     *
163     * @param os the output stream to use
164     * @param blockSize the block size to use
165     * @param recordSize the record size to use. Must be 512 bytes.
166     * @deprecated recordSize must always be 512 bytes. An IllegalArgumentException will be thrown
167     * if any other value is used
168     */
169    @Deprecated
170    public TarArchiveOutputStream(final OutputStream os, final int blockSize,
171        final int recordSize) {
172        this(os, blockSize, recordSize, null);
173    }
174
175
176    /**
177     * Constructs a new instance.
178     *
179     * @param os the output stream to use
180     * @param blockSize the block size to use . Must be a multiple of 512 bytes.
181     * @param recordSize the record size to use. Must be 512 bytes.
182     * @param encoding name of the encoding to use for file names
183     * @since 1.4
184     * @deprecated recordSize must always be 512 bytes. An IllegalArgumentException will be thrown
185     * if any other value is used.
186     */
187    @Deprecated
188    public TarArchiveOutputStream(final OutputStream os, final int blockSize,
189        final int recordSize, final String encoding) {
190        this(os, blockSize, encoding);
191        if (recordSize != RECORD_SIZE) {
192            throw new IllegalArgumentException(
193                "Tar record size must always be 512 bytes. Attempt to set size of " + recordSize);
194        }
195
196    }
197
198    /**
199     * Constructs a new instance.
200     *
201     * @param os the output stream to use
202     * @param blockSize the block size to use. Must be a multiple of 512 bytes.
203     * @param encoding name of the encoding to use for file names
204     * @since 1.4
205     */
206    public TarArchiveOutputStream(final OutputStream os, final int blockSize,
207        final String encoding) {
208        final int realBlockSize;
209        if (BLOCK_SIZE_UNSPECIFIED == blockSize) {
210            realBlockSize = RECORD_SIZE;
211        } else {
212            realBlockSize = blockSize;
213        }
214
215        if (realBlockSize <= 0 || realBlockSize % RECORD_SIZE != 0) {
216            throw new IllegalArgumentException("Block size must be a multiple of 512 bytes. Attempt to use set size of " + blockSize);
217        }
218        out = new FixedLengthBlockOutputStream(countingOut = new CountingOutputStream(os),
219                                               RECORD_SIZE);
220        this.encoding = encoding;
221        this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
222
223        this.recordBuf = new byte[RECORD_SIZE];
224        this.recordsPerBlock = realBlockSize / RECORD_SIZE;
225    }
226
227    /**
228     * Constructs a new instance.
229     *
230     * <p>Uses a block size of 512 bytes.</p>
231     *
232     * @param os the output stream to use
233     * @param encoding name of the encoding to use for file names
234     * @since 1.4
235     */
236    public TarArchiveOutputStream(final OutputStream os, final String encoding) {
237        this(os, BLOCK_SIZE_UNSPECIFIED, encoding);
238    }
239
240    private void addFileTimePaxHeader(final Map<String, String> paxHeaders,
241        final String header, final FileTime value) {
242        if (value != null) {
243            final Instant instant = value.toInstant();
244            final long seconds = instant.getEpochSecond();
245            final int nanos = instant.getNano();
246            if (nanos == 0) {
247                paxHeaders.put(header, String.valueOf(seconds));
248            } else {
249                addInstantPaxHeader(paxHeaders, header, seconds, nanos);
250            }
251        }
252    }
253
254    private void addFileTimePaxHeaderForBigNumber(final Map<String, String> paxHeaders,
255        final String header, final FileTime value,
256        final long maxValue) {
257        if (value != null) {
258            final Instant instant = value.toInstant();
259            final long seconds = instant.getEpochSecond();
260            final int nanos = instant.getNano();
261            if (nanos == 0) {
262                addPaxHeaderForBigNumber(paxHeaders, header, seconds, maxValue);
263            } else {
264                addInstantPaxHeader(paxHeaders, header, seconds, nanos);
265            }
266        }
267    }
268
269    private void addInstantPaxHeader(final Map<String, String> paxHeaders,
270        final String header, final long seconds, final int nanos) {
271        final BigDecimal bdSeconds = BigDecimal.valueOf(seconds);
272        final BigDecimal bdNanos = BigDecimal.valueOf(nanos).movePointLeft(9).setScale(7, RoundingMode.DOWN);
273        final BigDecimal timestamp = bdSeconds.add(bdNanos);
274        paxHeaders.put(header, timestamp.toPlainString());
275    }
276
277    private void addPaxHeaderForBigNumber(final Map<String, String> paxHeaders,
278        final String header, final long value,
279        final long maxValue) {
280        if (value < 0 || value > maxValue) {
281            paxHeaders.put(header, String.valueOf(value));
282        }
283    }
284
285    private void addPaxHeadersForBigNumbers(final Map<String, String> paxHeaders,
286        final TarArchiveEntry entry) {
287        addPaxHeaderForBigNumber(paxHeaders, "size", entry.getSize(),
288            TarConstants.MAXSIZE);
289        addPaxHeaderForBigNumber(paxHeaders, "gid", entry.getLongGroupId(),
290            TarConstants.MAXID);
291        addFileTimePaxHeaderForBigNumber(paxHeaders, "mtime",
292                entry.getLastModifiedTime(), TarConstants.MAXSIZE);
293        addFileTimePaxHeader(paxHeaders, "atime", entry.getLastAccessTime());
294        if (entry.getStatusChangeTime() != null) {
295            addFileTimePaxHeader(paxHeaders, "ctime", entry.getStatusChangeTime());
296        } else {
297            // ctime is usually set from creation time on platforms where the real ctime is not available
298            addFileTimePaxHeader(paxHeaders, "ctime", entry.getCreationTime());
299        }
300        addPaxHeaderForBigNumber(paxHeaders, "uid", entry.getLongUserId(),
301            TarConstants.MAXID);
302        // libarchive extensions
303        addFileTimePaxHeader(paxHeaders, "LIBARCHIVE.creationtime", entry.getCreationTime());
304        // star extensions by Jörg Schilling
305        addPaxHeaderForBigNumber(paxHeaders, "SCHILY.devmajor",
306            entry.getDevMajor(), TarConstants.MAXID);
307        addPaxHeaderForBigNumber(paxHeaders, "SCHILY.devminor",
308            entry.getDevMinor(), TarConstants.MAXID);
309        // there is no PAX header for file mode
310        failForBigNumber("mode", entry.getMode(), TarConstants.MAXID);
311    }
312
313    /**
314     * Closes the underlying OutputStream.
315     *
316     * @throws IOException on error
317     */
318    @Override
319    public void close() throws IOException {
320        try {
321            if (!finished) {
322                finish();
323            }
324        } finally {
325            if (!closed) {
326                out.close();
327                closed = true;
328            }
329        }
330    }
331
332    /**
333     * Closes an entry. This method MUST be called for all file entries that contain data. The reason
334     * is that we must buffer data written to the stream in order to satisfy the buffer's record
335     * based writes. Thus, there may be data fragments still being assembled that must be written to
336     * the output stream before this entry is closed and the next entry written.
337     *
338     * @throws IOException on error
339     */
340    @Override
341    public void closeArchiveEntry() throws IOException {
342        if (finished) {
343            throw new IOException("Stream has already been finished");
344        }
345        if (!haveUnclosedEntry) {
346            throw new IOException("No current entry to close");
347        }
348        out.flushBlock();
349        if (currBytes < currSize) {
350            throw new IOException("Entry '" + currName + "' closed at '"
351                + currBytes
352                + "' before the '" + currSize
353                + "' bytes specified in the header were written");
354        }
355        recordsWritten += currSize / RECORD_SIZE;
356
357        if (0 != currSize % RECORD_SIZE) {
358            recordsWritten++;
359        }
360        haveUnclosedEntry = false;
361    }
362
363    @Override
364    public TarArchiveEntry createArchiveEntry(final File inputFile, final String entryName)
365        throws IOException {
366        if (finished) {
367            throw new IOException("Stream has already been finished");
368        }
369        return new TarArchiveEntry(inputFile, entryName);
370    }
371
372    @Override
373    public TarArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
374        if (finished) {
375            throw new IOException("Stream has already been finished");
376        }
377        return new TarArchiveEntry(inputPath, entryName, options);
378    }
379
380    private byte[] encodeExtendedPaxHeadersContents(final Map<String, String> headers) {
381        final StringWriter w = new StringWriter();
382        headers.forEach((k, v) -> {
383            int len = k.length() + v.length()
384                + 3 /* blank, equals and newline */
385                + 2 /* guess 9 < actual length < 100 */;
386            String line = len + " " + k + "=" + v + "\n";
387            int actualLength = line.getBytes(UTF_8).length;
388            while (len != actualLength) {
389                // Adjust for cases where length < 10 or > 100
390                // or where UTF-8 encoding isn't a single octet
391                // per character.
392                // Must be in loop as size may go from 99 to 100 in
393                // first pass, so we'd need a second.
394                len = actualLength;
395                line = len + " " + k + "=" + v + "\n";
396                actualLength = line.getBytes(UTF_8).length;
397            }
398            w.write(line);
399        });
400        return w.toString().getBytes(UTF_8);
401    }
402
403    private void failForBigNumber(final String field, final long value, final long maxValue) {
404        failForBigNumber(field, value, maxValue, "");
405    }
406
407    private void failForBigNumber(final String field, final long value, final long maxValue,
408        final String additionalMsg) {
409        if (value < 0 || value > maxValue) {
410            throw new IllegalArgumentException(field + " '" + value //NOSONAR
411                + "' is too big ( > "
412                + maxValue + " )." + additionalMsg);
413        }
414    }
415
416    private void failForBigNumbers(final TarArchiveEntry entry) {
417        failForBigNumber("entry size", entry.getSize(), TarConstants.MAXSIZE);
418        failForBigNumberWithPosixMessage("group id", entry.getLongGroupId(), TarConstants.MAXID);
419        failForBigNumber("last modification time",
420            TimeUtils.toUnixTime(entry.getLastModifiedTime()),
421            TarConstants.MAXSIZE);
422        failForBigNumber("user id", entry.getLongUserId(), TarConstants.MAXID);
423        failForBigNumber("mode", entry.getMode(), TarConstants.MAXID);
424        failForBigNumber("major device number", entry.getDevMajor(),
425            TarConstants.MAXID);
426        failForBigNumber("minor device number", entry.getDevMinor(),
427            TarConstants.MAXID);
428    }
429
430    private void failForBigNumberWithPosixMessage(final String field, final long value,
431        final long maxValue) {
432        failForBigNumber(field, value, maxValue,
433            " Use STAR or POSIX extensions to overcome this limit");
434    }
435
436    /**
437     * Finishes the TAR archive without closing the underlying OutputStream.
438     *
439     * An archive consists of a series of file entries terminated by an
440     * end-of-archive entry, which consists of two 512 blocks of zero bytes.
441     * POSIX.1 requires two EOF records, like some other implementations.
442     *
443     * @throws IOException on error
444     */
445    @Override
446    public void finish() throws IOException {
447        if (finished) {
448            throw new IOException("This archive has already been finished");
449        }
450
451        if (haveUnclosedEntry) {
452            throw new IOException("This archive contains unclosed entries.");
453        }
454        writeEOFRecord();
455        writeEOFRecord();
456        padAsNeeded();
457        out.flush();
458        finished = true;
459    }
460
461    @Override
462    public void flush() throws IOException {
463        out.flush();
464    }
465
466    @Override
467    public long getBytesWritten() {
468        return countingOut.getBytesWritten();
469    }
470
471    @Deprecated
472    @Override
473    public int getCount() {
474        return (int) getBytesWritten();
475    }
476
477    /**
478     * Gets the record size being used by this stream's TarBuffer.
479     *
480     * @return The TarBuffer record size.
481     * @deprecated
482     */
483    @Deprecated
484    public int getRecordSize() {
485        return RECORD_SIZE;
486    }
487
488    /**
489     * Handles long file or link names according to the longFileMode setting.
490     *
491     * <p>I.e. if the given name is too long to be written to a plain tar header then <ul> <li>it
492     * creates a pax header who's name is given by the paxHeaderName parameter if longFileMode is
493     * POSIX</li> <li>it creates a GNU longlink entry who's type is given by the linkType parameter
494     * if longFileMode is GNU</li> <li>it throws an exception if longFileMode is ERROR</li> <li>it
495     * truncates the name if longFileMode is TRUNCATE</li> </ul></p>
496     *
497     * @param entry entry the name belongs to
498     * @param name the name to write
499     * @param paxHeaders current map of pax headers
500     * @param paxHeaderName name of the pax header to write
501     * @param linkType type of the GNU entry to write
502     * @param fieldName the name of the field
503     * @throws IllegalArgumentException if the {@link TarArchiveOutputStream#longFileMode} equals
504     *                                  {@link TarArchiveOutputStream#LONGFILE_ERROR} and the file
505     *                                  name is too long
506     * @return whether a pax header has been written.
507     */
508    private boolean handleLongName(final TarArchiveEntry entry, final String name,
509        final Map<String, String> paxHeaders,
510        final String paxHeaderName, final byte linkType, final String fieldName)
511        throws IOException {
512        final ByteBuffer encodedName = zipEncoding.encode(name);
513        final int len = encodedName.limit() - encodedName.position();
514        if (len >= TarConstants.NAMELEN) {
515
516            if (longFileMode == LONGFILE_POSIX) {
517                paxHeaders.put(paxHeaderName, name);
518                return true;
519            }
520            if (longFileMode == LONGFILE_GNU) {
521                // create a TarEntry for the LongLink, the contents
522                // of which are the link's name
523                final TarArchiveEntry longLinkEntry = new TarArchiveEntry(TarConstants.GNU_LONGLINK,
524                    linkType);
525
526                longLinkEntry.setSize(len + 1L); // +1 for NUL
527                transferModTime(entry, longLinkEntry);
528                putArchiveEntry(longLinkEntry);
529                write(encodedName.array(), encodedName.arrayOffset(), len);
530                write(0); // NUL terminator
531                closeArchiveEntry();
532            } else if (longFileMode != LONGFILE_TRUNCATE) {
533                throw new IllegalArgumentException(fieldName + " '" + name //NOSONAR
534                    + "' is too long ( > "
535                    + TarConstants.NAMELEN + " bytes)");
536            }
537        }
538        return false;
539    }
540
541    private void padAsNeeded() throws IOException {
542        final int start = Math.toIntExact(recordsWritten % recordsPerBlock);
543        if (start != 0) {
544            for (int i = start; i < recordsPerBlock; i++) {
545                writeEOFRecord();
546            }
547        }
548    }
549
550    /**
551     * Puts an entry on the output stream. This writes the entry's header record and positions the
552     * output stream for writing the contents of the entry. Once this method is called, the stream
553     * is ready for calls to write() to write the entry's contents. Once the contents are written,
554     * closeArchiveEntry() <B>MUST</B> be called to ensure that all buffered data is completely
555     * written to the output stream.
556     *
557     * @param archiveEntry The TarEntry to be written to the archive.
558     * @throws IOException on error
559     * @throws ClassCastException if archiveEntry is not an instance of TarArchiveEntry
560     * @throws IllegalArgumentException if the {@link TarArchiveOutputStream#longFileMode} equals
561     *                                  {@link TarArchiveOutputStream#LONGFILE_ERROR} and the file
562     *                                  name is too long
563     * @throws IllegalArgumentException if the {@link TarArchiveOutputStream#bigNumberMode} equals
564     *         {@link TarArchiveOutputStream#BIGNUMBER_ERROR} and one of the numeric values
565     *         exceeds the limits of a traditional tar header.
566     */
567    @Override
568    public void putArchiveEntry(final TarArchiveEntry archiveEntry) throws IOException {
569        if (finished) {
570            throw new IOException("Stream has already been finished");
571        }
572        if (archiveEntry.isGlobalPaxHeader()) {
573            final byte[] data = encodeExtendedPaxHeadersContents(archiveEntry.getExtraPaxHeaders());
574            archiveEntry.setSize(data.length);
575            archiveEntry.writeEntryHeader(recordBuf, zipEncoding, bigNumberMode == BIGNUMBER_STAR);
576            writeRecord(recordBuf);
577            currSize= archiveEntry.getSize();
578            currBytes = 0;
579            this.haveUnclosedEntry = true;
580            write(data);
581            closeArchiveEntry();
582        } else {
583            final Map<String, String> paxHeaders = new HashMap<>();
584            final String entryName = archiveEntry.getName();
585            final boolean paxHeaderContainsPath = handleLongName(archiveEntry, entryName, paxHeaders, "path",
586                TarConstants.LF_GNUTYPE_LONGNAME, "file name");
587            final String linkName = archiveEntry.getLinkName();
588            final boolean paxHeaderContainsLinkPath = linkName != null && !linkName.isEmpty()
589                && handleLongName(archiveEntry, linkName, paxHeaders, "linkpath",
590                TarConstants.LF_GNUTYPE_LONGLINK, "link name");
591
592            if (bigNumberMode == BIGNUMBER_POSIX) {
593                addPaxHeadersForBigNumbers(paxHeaders, archiveEntry);
594            } else if (bigNumberMode != BIGNUMBER_STAR) {
595                failForBigNumbers(archiveEntry);
596            }
597
598            if (addPaxHeadersForNonAsciiNames && !paxHeaderContainsPath
599                && !ASCII.canEncode(entryName)) {
600                paxHeaders.put("path", entryName);
601            }
602
603            if (addPaxHeadersForNonAsciiNames && !paxHeaderContainsLinkPath
604                && (archiveEntry.isLink() || archiveEntry.isSymbolicLink())
605                && !ASCII.canEncode(linkName)) {
606                paxHeaders.put("linkpath", linkName);
607            }
608            paxHeaders.putAll(archiveEntry.getExtraPaxHeaders());
609
610            if (!paxHeaders.isEmpty()) {
611                writePaxHeaders(archiveEntry, entryName, paxHeaders);
612            }
613
614            archiveEntry.writeEntryHeader(recordBuf, zipEncoding, bigNumberMode == BIGNUMBER_STAR);
615            writeRecord(recordBuf);
616
617            currBytes = 0;
618
619            if (archiveEntry.isDirectory()) {
620                currSize = 0;
621            } else {
622                currSize = archiveEntry.getSize();
623            }
624            currName = entryName;
625            haveUnclosedEntry = true;
626        }
627    }
628
629    /**
630     * Sets whether to add a PAX extension header for non-ASCII file names.
631     *
632     * @param b whether to add a PAX extension header for non-ASCII file names.
633     * @since 1.4
634     */
635    public void setAddPaxHeadersForNonAsciiNames(final boolean b) {
636        addPaxHeadersForNonAsciiNames = b;
637    }
638
639    /**
640     * Sets the big number mode. This can be BIGNUMBER_ERROR(0), BIGNUMBER_STAR(1) or
641     * BIGNUMBER_POSIX(2). This specifies the treatment of big files (sizes &gt;
642     * TarConstants.MAXSIZE) and other numeric values too big to fit into a traditional tar header.
643     * Default is BIGNUMBER_ERROR.
644     *
645     * @param bigNumberMode the mode to use
646     * @since 1.4
647     */
648    public void setBigNumberMode(final int bigNumberMode) {
649        this.bigNumberMode = bigNumberMode;
650    }
651
652    /**
653     * Sets the long file mode. This can be LONGFILE_ERROR(0), LONGFILE_TRUNCATE(1), LONGFILE_GNU(2) or
654     * LONGFILE_POSIX(3). This specifies the treatment of long file names (names &gt;=
655     * TarConstants.NAMELEN). Default is LONGFILE_ERROR.
656     *
657     * @param longFileMode the mode to use
658     */
659    public void setLongFileMode(final int longFileMode) {
660        this.longFileMode = longFileMode;
661    }
662
663    /**
664     * Tests whether the character could lead to problems when used inside a TarArchiveEntry name
665     * for a PAX header.
666     *
667     * @return true if the character could lead to problems when used inside a TarArchiveEntry name
668     * for a PAX header.
669     */
670    private boolean shouldBeReplaced(final char c) {
671        return c == 0 // would be read as Trailing null
672            || c == '/' // when used as last character TAE will consider the PAX header a directory
673            || c == '\\'; // same as '/' as slashes get "normalized" on Windows
674    }
675
676    private String stripTo7Bits(final String name) {
677        final int length = name.length();
678        final StringBuilder result = new StringBuilder(length);
679        for (int i = 0; i < length; i++) {
680            final char stripped = (char) (name.charAt(i) & 0x7F);
681            if (shouldBeReplaced(stripped)) {
682                result.append("_");
683            } else {
684                result.append(stripped);
685            }
686        }
687        return result.toString();
688    }
689
690    private void transferModTime(final TarArchiveEntry from, final TarArchiveEntry to) {
691        long fromModTimeSeconds = TimeUtils.toUnixTime(from.getLastModifiedTime());
692        if (fromModTimeSeconds < 0 || fromModTimeSeconds > TarConstants.MAXSIZE) {
693            fromModTimeSeconds = 0;
694        }
695        to.setLastModifiedTime(TimeUtils.unixTimeToFileTime(fromModTimeSeconds));
696    }
697
698    /**
699     * Writes bytes to the current tar archive entry. This method is aware of the current entry and
700     * will throw an exception if you attempt to write bytes past the length specified for the
701     * current entry.
702     *
703     * @param wBuf The buffer to write to the archive.
704     * @param wOffset The offset in the buffer from which to get bytes.
705     * @param numToWrite The number of bytes to write.
706     * @throws IOException on error
707     */
708    @Override
709    public void write(final byte[] wBuf, final int wOffset, final int numToWrite) throws IOException {
710        if (!haveUnclosedEntry) {
711            throw new IllegalStateException("No current tar entry");
712        }
713        if (currBytes + numToWrite > currSize) {
714            throw new IOException("Request to write '" + numToWrite
715                + "' bytes exceeds size in header of '"
716                + currSize + "' bytes for entry '"
717                + currName + "'");
718        }
719        out.write(wBuf, wOffset, numToWrite);
720        currBytes += numToWrite;
721    }
722
723    /**
724     * Writes an EOF (end of archive) record to the tar archive. An EOF record consists of a record
725     * of all zeros.
726     */
727    private void writeEOFRecord() throws IOException {
728        Arrays.fill(recordBuf, (byte) 0);
729        writeRecord(recordBuf);
730    }
731
732    /**
733     * Writes a PAX extended header with the given map as contents.
734     *
735     * @since 1.4
736     */
737    void writePaxHeaders(final TarArchiveEntry entry,
738        final String entryName,
739        final Map<String, String> headers) throws IOException {
740        String name = "./PaxHeaders.X/" + stripTo7Bits(entryName);
741        if (name.length() >= TarConstants.NAMELEN) {
742            name = name.substring(0, TarConstants.NAMELEN - 1);
743        }
744        final TarArchiveEntry pex = new TarArchiveEntry(name,
745            TarConstants.LF_PAX_EXTENDED_HEADER_LC);
746        transferModTime(entry, pex);
747
748        final byte[] data = encodeExtendedPaxHeadersContents(headers);
749        pex.setSize(data.length);
750        putArchiveEntry(pex);
751        write(data);
752        closeArchiveEntry();
753    }
754
755    /**
756     * Writes an archive record to the archive.
757     *
758     * @param record The record data to write to the archive.
759     * @throws IOException on error
760     */
761    private void writeRecord(final byte[] record) throws IOException {
762        if (record.length != RECORD_SIZE) {
763            throw new IOException("Record to write has length '"
764                + record.length
765                + "' which is not the record size of '"
766                + RECORD_SIZE + "'");
767        }
768
769        out.write(record);
770        recordsWritten++;
771    }
772}