    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 *   https://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 java.io.ByteArrayOutputStream;
    022import java.io.Closeable;
    023import java.io.File;
    024import java.io.IOException;
    025import java.io.InputStream;
    026import java.nio.ByteBuffer;
    027import java.nio.channels.SeekableByteChannel;
    028import java.nio.file.Files;
    029import java.nio.file.Path;
    030import java.util.ArrayList;
    031import java.util.Arrays;
    032import java.util.HashMap;
    033import java.util.LinkedList;
    034import java.util.List;
    035import java.util.Map;
    036
    037import org.apache.commons.compress.archivers.zip.ZipEncoding;
    038import org.apache.commons.compress.archivers.zip.ZipEncodingHelper;
    039import org.apache.commons.compress.utils.ArchiveUtils;
    040import org.apache.commons.compress.utils.BoundedArchiveInputStream;
    041import org.apache.commons.compress.utils.BoundedSeekableByteChannelInputStream;
    042import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
    043import org.apache.commons.io.input.BoundedInputStream;
    044
    045/**
    046 * Provides random access to Unix archives.
    047 *
    048 * @since 1.21
    049 */
    050public class TarFile implements Closeable {
    051
    052    private final class BoundedTarEntryInputStream extends BoundedArchiveInputStream {
    053
    054        private final SeekableByteChannel channel;
    055
    056        private final TarArchiveEntry entry;
    057
    058        private long entryOffset;
    059
    060        private int currentSparseInputStreamIndex;
    061
    062        BoundedTarEntryInputStream(final TarArchiveEntry entry, final SeekableByteChannel channel) throws IOException {
    063            super(entry.getDataOffset(), entry.getRealSize());
    064            if (channel.size() - entry.getSize() < entry.getDataOffset()) {
    065                throw new IOException("entry size exceeds archive size");
    066            }
    067            this.entry = entry;
    068            this.channel = channel;
    069        }
    070
    071        @Override
    072        protected int read(final long pos, final ByteBuffer buf) throws IOException {
    073            if (entryOffset >= entry.getRealSize()) {
    074                return -1;
    075            }
    076
    077            final int totalRead;
    078            if (entry.isSparse()) {
    079                totalRead = readSparse(entryOffset, buf, buf.limit());
    080            } else {
    081                totalRead = readArchive(pos, buf);
    082            }
    083
    084            if (totalRead == -1) {
    085                if (buf.array().length > 0) {
    086                    throw new IOException("Truncated TAR archive");
    087                }
    088                setAtEOF(true);
    089            } else {
    090                entryOffset += totalRead;
    091                buf.flip();
    092            }
    093            return totalRead;
    094        }
    095
    096        private int readArchive(final long pos, final ByteBuffer buf) throws IOException {
    097            channel.position(pos);
    098            return channel.read(buf);
    099        }
    100
    101        private int readSparse(final long pos, final ByteBuffer buf, final int numToRead) throws IOException {
    102            // if there are no actual input streams, just read from the original archive
    103            final List<InputStream> entrySparseInputStreams = sparseInputStreams.get(entry.getName());
    104            if (entrySparseInputStreams == null || entrySparseInputStreams.isEmpty()) {
    105                return readArchive(entry.getDataOffset() + pos, buf);
    106            }
    107
    108            if (currentSparseInputStreamIndex >= entrySparseInputStreams.size()) {
    109                return -1;
    110            }
    111
    112            final InputStream currentInputStream = entrySparseInputStreams.get(currentSparseInputStreamIndex);
    113            final byte[] bufArray = new byte[numToRead];
    114            final int readLen = currentInputStream.read(bufArray);
    115            if (readLen != -1) {
    116                buf.put(bufArray, 0, readLen);
    117            }
    118
    119            // if the current input stream is the last input stream,
    120            // just return the number of bytes read from current input stream
    121            if (currentSparseInputStreamIndex == entrySparseInputStreams.size() - 1) {
    122                return readLen;
    123            }
    124
    125            // if EOF of current input stream is meet, open a new input stream and recursively call read
    126            if (readLen == -1) {
    127                currentSparseInputStreamIndex++;
    128                return readSparse(pos, buf, numToRead);
    129            }
    130
    131            // if the rest data of current input stream is not long enough, open a new input stream
    132            // and recursively call read
    133            if (readLen < numToRead) {
    134                currentSparseInputStreamIndex++;
    135                final int readLenOfNext = readSparse(pos + readLen, buf, numToRead - readLen);
    136                if (readLenOfNext == -1) {
    137                    return readLen;
    138                }
    139
    140                return readLen + readLenOfNext;
    141            }
    142
    143            // if the rest data of current input stream is enough(which means readLen == len), just return readLen
    144            return readLen;
    145        }
    146    }
    147
    148    private static final int SMALL_BUFFER_SIZE = 256;
    149
    150    private final byte[] smallBuf = new byte[SMALL_BUFFER_SIZE];
    151
    152    private final SeekableByteChannel archive;
    153
    154    /**
    155     * The encoding of the tar file
    156     */
    157    private final ZipEncoding zipEncoding;
    158
    159    private final LinkedList<TarArchiveEntry> entries = new LinkedList<>();
    160
    161    private final int blockSize;
    162
    163    private final boolean lenient;
    164
    165    private final int recordSize;
    166
    167    private final ByteBuffer recordBuffer;
    168
    169    // the global sparse headers, this is only used in PAX Format 0.X
    170    private final List<TarArchiveStructSparse> globalSparseHeaders = new ArrayList<>();
    171
    172    private boolean eof;
    173
    174    /**
    175     * The meta-data about the current entry
    176     */
    177    private TarArchiveEntry currEntry;
    178
    179    // the global PAX header
    180    private Map<String, String> globalPaxHeaders = new HashMap<>();
    181
    182    private final Map<String, List<InputStream>> sparseInputStreams = new HashMap<>();
    183
    184    /**
    185     * Constructor for TarFile.
    186     *
    187     * @param content the content to use
    188     * @throws IOException when reading the tar archive fails
    189     */
    190    public TarFile(final byte[] content) throws IOException {
    191        this(new SeekableInMemoryByteChannel(content));
    192    }
    193
    194    /**
    195     * Constructor for TarFile.
    196     *
    197     * @param content the content to use
    198     * @param lenient when set to true illegal values for group/userid, mode, device numbers and timestamp will be ignored and the fields set to
    199     *                {@link TarArchiveEntry#UNKNOWN}. When set to false such illegal fields cause an exception instead.
    200     * @throws IOException when reading the tar archive fails
    201     */
    202    public TarFile(final byte[] content, final boolean lenient) throws IOException {
    203        this(new SeekableInMemoryByteChannel(content), TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE, null, lenient);
    204    }
    205
    206    /**
    207     * Constructor for TarFile.
    208     *
    209     * @param content  the content to use
    210     * @param encoding the encoding to use
    211     * @throws IOException when reading the tar archive fails
    212     */
    213    public TarFile(final byte[] content, final String encoding) throws IOException {
    214        this(new SeekableInMemoryByteChannel(content), TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE, encoding, false);
    215    }
    216
    217    /**
    218     * Constructor for TarFile.
    219     *
    220     * @param archive the file of the archive to use
    221     * @throws IOException when reading the tar archive fails
    222     */
    223    public TarFile(final File archive) throws IOException {
    224        this(archive.toPath());
    225    }
    226
    227    /**
    228     * Constructor for TarFile.
    229     *
    230     * @param archive the file of the archive to use
    231     * @param lenient when set to true illegal values for group/userid, mode, device numbers and timestamp will be ignored and the fields set to
    232     *                {@link TarArchiveEntry#UNKNOWN}. When set to false such illegal fields cause an exception instead.
    233     * @throws IOException when reading the tar archive fails
    234     */
    235    public TarFile(final File archive, final boolean lenient) throws IOException {
    236        this(archive.toPath(), lenient);
    237    }
    238
    239    /**
    240     * Constructor for TarFile.
    241     *
    242     * @param archive  the file of the archive to use
    243     * @param encoding the encoding to use
    244     * @throws IOException when reading the tar archive fails
    245     */
    246    public TarFile(final File archive, final String encoding) throws IOException {
    247        this(archive.toPath(), encoding);
    248    }
    249
    250    /**
    251     * Constructor for TarFile.
    252     *
    253     * @param archivePath the path of the archive to use
    254     * @throws IOException when reading the tar archive fails
    255     */
    256    public TarFile(final Path archivePath) throws IOException {
    257        this(Files.newByteChannel(archivePath), TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE, null, false);
    258    }
    259
    260    /**
    261     * Constructor for TarFile.
    262     *
    263     * @param archivePath the path of the archive to use
    264     * @param lenient     when set to true illegal values for group/userid, mode, device numbers and timestamp will be ignored and the fields set to
    265     *                    {@link TarArchiveEntry#UNKNOWN}. When set to false such illegal fields cause an exception instead.
    266     * @throws IOException when reading the tar archive fails
    267     */
    268    public TarFile(final Path archivePath, final boolean lenient) throws IOException {
    269        this(Files.newByteChannel(archivePath), TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE, null, lenient);
    270    }
    271
    272    /**
    273     * Constructor for TarFile.
    274     *
    275     * @param archivePath the path of the archive to use
    276     * @param encoding    the encoding to use
    277     * @throws IOException when reading the tar archive fails
    278     */
    279    public TarFile(final Path archivePath, final String encoding) throws IOException {
    280        this(Files.newByteChannel(archivePath), TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE, encoding, false);
    281    }
    282
    283    /**
    284     * Constructor for TarFile.
    285     *
    286     * @param content the content to use
    287     * @throws IOException when reading the tar archive fails
    288     */
    289    public TarFile(final SeekableByteChannel content) throws IOException {
    290        this(content, TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE, null, false);
    291    }
    292
    293    /**
    294     * Constructor for TarFile.
    295     *
    296     * @param archive    the seekable byte channel to use
    297     * @param blockSize  the blocks size to use
    298     * @param recordSize the record size to use
    299     * @param encoding   the encoding to use
    300     * @param lenient    when set to true illegal values for group/userid, mode, device numbers and timestamp will be ignored and the fields set to
    301     *                   {@link TarArchiveEntry#UNKNOWN}. When set to false such illegal fields cause an exception instead.
    302     * @throws IOException when reading the tar archive fails
    303     */
    304    public TarFile(final SeekableByteChannel archive, final int blockSize, final int recordSize, final String encoding, final boolean lenient)
    305            throws IOException {
    306        this.archive = archive;
    307        this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
    308        this.recordSize = recordSize;
    309        this.recordBuffer = ByteBuffer.allocate(this.recordSize);
    310        this.blockSize = blockSize;
    311        this.lenient = lenient;
    312
    313        TarArchiveEntry entry;
    314        while ((entry = getNextTarEntry()) != null) {
    315            entries.add(entry);
    316        }
    317    }
    318
    319    /**
    320     * Update the current entry with the read pax headers
    321     *
    322     * @param headers       Headers read from the pax header
    323     * @param sparseHeaders Sparse headers read from pax header
    324     */
    325    private void applyPaxHeadersToCurrentEntry(final Map<String, String> headers, final List<TarArchiveStructSparse> sparseHeaders) throws IOException {
    326        currEntry.updateEntryFromPaxHeaders(headers);
    327        currEntry.setSparseHeaders(sparseHeaders);
    328    }
    329
    330    /**
    331     * Build the input streams consisting of all-zero input streams and non-zero input streams. When reading from the non-zero input streams, the data is
    332     * actually read from the original input stream. The size of each input stream is introduced by the sparse headers.
    333     *
    334     * @implNote Some all-zero input streams and non-zero input streams have the size of 0. We DO NOT store the 0 size input streams because they are
    335     *           meaningless.
    336     */
    337    private void buildSparseInputStreams() throws IOException {
    338        final List<InputStream> streams = new ArrayList<>();
    339        final List<TarArchiveStructSparse> sparseHeaders = currEntry.getOrderedSparseHeaders();
    340        // Stream doesn't need to be closed at all as it doesn't use any resources
    341        final InputStream zeroInputStream = new TarArchiveSparseZeroInputStream(); // NOSONAR
    342        // logical offset into the extracted entry
    343        long offset = 0;
    344        long numberOfZeroBytesInSparseEntry = 0;
    345        for (final TarArchiveStructSparse sparseHeader : sparseHeaders) {
    346            final long zeroBlockSize = sparseHeader.getOffset() - offset;
    347            if (zeroBlockSize < 0) {
    348                // sparse header says to move backwards inside the extracted entry
    349                throw new IOException("Corrupted struct sparse detected");
    350            }
    351            // only store the zero block if it is not empty
    352            if (zeroBlockSize > 0) {
    353                streams.add(BoundedInputStream.builder().setInputStream(zeroInputStream).setMaxCount(zeroBlockSize).get());
    354                numberOfZeroBytesInSparseEntry += zeroBlockSize;
    355            }
    356            // only store the input streams with non-zero size
    357            if (sparseHeader.getNumbytes() > 0) {
    358                final long start = currEntry.getDataOffset() + sparseHeader.getOffset() - numberOfZeroBytesInSparseEntry;
    359                if (start + sparseHeader.getNumbytes() < start) {
    360                    // possible integer overflow
    361                    throw new IOException("Unreadable TAR archive, sparse block offset or length too big");
    362                }
    363                streams.add(new BoundedSeekableByteChannelInputStream(start, sparseHeader.getNumbytes(), archive));
    364            }
    365            offset = sparseHeader.getOffset() + sparseHeader.getNumbytes();
    366        }
    367        sparseInputStreams.put(currEntry.getName(), streams);
    368    }
    369
    370    @Override
    371    public void close() throws IOException {
    372        archive.close();
    373    }
    374
    375    /**
    376     * This method is invoked once the end of the archive is hit, it tries to consume the remaining bytes under the assumption that the tool creating this
    377     * archive has padded the last block.
    378     */
    379    private void consumeRemainderOfLastBlock() throws IOException {
    380        final long bytesReadOfLastBlock = archive.position() % blockSize;
    381        if (bytesReadOfLastBlock > 0) {
    382            repositionForwardBy(blockSize - bytesReadOfLastBlock);
    383        }
    384    }
    385
    386    /**
    387     * Gets all TAR Archive Entries from the TarFile
    388     *
    389     * @return All entries from the tar file
    390     */
    391    public List<TarArchiveEntry> getEntries() {
    392        return new ArrayList<>(entries);
    393    }
    394
    395    /**
    396     * Gets the input stream for the provided Tar Archive Entry.
    397     *
    398     * @param entry Entry to get the input stream from
    399     * @return Input stream of the provided entry
    400     * @throws IOException Corrupted TAR archive. Can't read entry.
    401     */
    402    public InputStream getInputStream(final TarArchiveEntry entry) throws IOException {
    403        try {
    404            return new BoundedTarEntryInputStream(entry, archive);
    405        } catch (final RuntimeException ex) {
    406            throw new IOException("Corrupted TAR archive. Can't read entry", ex);
    407        }
    408    }
    409
    410    /**
    411     * Gets the next entry in this tar archive as long name data.
    412     *
    413     * @return The next entry in the archive as long name data, or null.
    414     * @throws IOException on error
    415     */
    416    private byte[] getLongNameData() throws IOException {
    417        final ByteArrayOutputStream longName = new ByteArrayOutputStream();
    418        int length;
    419        try (InputStream in = getInputStream(currEntry)) {
    420            while ((length = in.read(smallBuf)) >= 0) {
    421                longName.write(smallBuf, 0, length);
    422            }
    423        }
    424        getNextTarEntry();
    425        if (currEntry == null) {
    426            // Bugzilla: 40334
    427            // Malformed tar file - long entry name not followed by entry
    428            return null;
    429        }
    430        byte[] longNameData = longName.toByteArray();
    431        // remove trailing null terminator(s)
    432        length = longNameData.length;
    433        while (length > 0 && longNameData[length - 1] == 0) {
    434            --length;
    435        }
    436        if (length != longNameData.length) {
    437            longNameData = Arrays.copyOf(longNameData, length);
    438        }
    439        return longNameData;
    440    }
    441
    442    /**
    443     * Gets the next entry in this tar archive. This will skip to the end of the current entry, if there is one, and place the position of the channel at the
    444     * header of the next entry, and read the header and instantiate a new TarEntry from the header bytes and return that entry. If there are no more entries in
    445     * the archive, null will be returned to indicate that the end of the archive has been reached.
    446     *
    447     * @return The next TarEntry in the archive, or null if there is no next entry.
    448     * @throws IOException when reading the next TarEntry fails
    449     */
    450    private TarArchiveEntry getNextTarEntry() throws IOException {
    451        if (isAtEOF()) {
    452            return null;
    453        }
    454
    455        if (currEntry != null) {
    456            // Skip to the end of the entry
    457            repositionForwardTo(currEntry.getDataOffset() + currEntry.getSize());
    458            throwExceptionIfPositionIsNotInArchive();
    459            skipRecordPadding();
    460        }
    461
    462        final ByteBuffer headerBuf = getRecord();
    463        if (null == headerBuf) {
    464            // Hit EOF
    465            currEntry = null;
    466            return null;
    467        }
    468
    469        try {
    470            final long position = archive.position();
    471            currEntry = new TarArchiveEntry(globalPaxHeaders, headerBuf.array(), zipEncoding, lenient, position);
    472        } catch (final IllegalArgumentException e) {
    473            throw new IOException("Error detected parsing the header", e);
    474        }
    475
    476        if (currEntry.isGNULongLinkEntry()) {
    477            final byte[] longLinkData = getLongNameData();
    478            if (longLinkData == null) {
    479                // Bugzilla: 40334
    480                // Malformed tar file - long link entry name not followed by
    481                // entry
    482                return null;
    483            }
    484            currEntry.setLinkName(zipEncoding.decode(longLinkData));
    485        }
    486
    487        if (currEntry.isGNULongNameEntry()) {
    488            final byte[] longNameData = getLongNameData();
    489            if (longNameData == null) {
    490                // Bugzilla: 40334
    491                // Malformed tar file - long entry name not followed by
    492                // entry
    493                return null;
    494            }
    495
    496            // COMPRESS-509 : the name of directories should end with '/'
    497            final String name = zipEncoding.decode(longNameData);
    498            currEntry.setName(name);
    499            if (currEntry.isDirectory() && !name.endsWith("/")) {
    500                currEntry.setName(name + "/");
    501            }
    502        }
    503
    504        if (currEntry.isGlobalPaxHeader()) { // Process Global Pax headers
    505            readGlobalPaxHeaders();
    506        }
    507
    508        try {
    509            if (currEntry.isPaxHeader()) { // Process Pax headers
    510                paxHeaders();
    511            } else if (!globalPaxHeaders.isEmpty()) {
    512                applyPaxHeadersToCurrentEntry(globalPaxHeaders, globalSparseHeaders);
    513            }
    514        } catch (final NumberFormatException e) {
    515            throw new IOException("Error detected parsing the pax header", e);
    516        }
    517
    518        if (currEntry.isOldGNUSparse()) { // Process sparse files
    519            readOldGNUSparse();
    520        }
    521
    522        return currEntry;
    523    }
    524
    525    /**
    526     * Gets the next record in this tar archive. This will skip over any remaining data in the current entry, if there is one, and place the input stream at the
    527     * header of the next entry.
    528     *
    529     * <p>
    530     * If there are no more entries in the archive, null will be returned to indicate that the end of the archive has been reached. At the same time the
    531     * {@code hasHitEOF} marker will be set to true.
    532     * </p>
    533     *
    534     * @return The next TarEntry in the archive, or null if there is no next entry.
    535     * @throws IOException when reading the next TarEntry fails
    536     */
    537    private ByteBuffer getRecord() throws IOException {
    538        ByteBuffer headerBuf = readRecord();
    539        setAtEOF(isEOFRecord(headerBuf));
    540        if (isAtEOF() && headerBuf != null) {
    541            // Consume rest
    542            tryToConsumeSecondEOFRecord();
    543            consumeRemainderOfLastBlock();
    544            headerBuf = null;
    545        }
    546        return headerBuf;
    547    }
    548
    549    /**
    550     * Tests whether or not we are at the end-of-file.
    551     *
    552     * @return whether or not we are at the end-of-file.
    553     */
    554    protected final boolean isAtEOF() {
    555        return eof;
    556    }
    557
    558    private boolean isDirectory() {
    559        return currEntry != null && currEntry.isDirectory();
    560    }
    561
    562    private boolean isEOFRecord(final ByteBuffer headerBuf) {
    563        return headerBuf == null || ArchiveUtils.isArrayZero(headerBuf.array(), recordSize);
    564    }
    565
    566    /**
    567     * <p>
    568     * For PAX Format 0.0, the sparse headers(GNU.sparse.offset and GNU.sparse.numbytes) may appear multi times, and they look like:
    569     *
    570     * <pre>
    571     * GNU.sparse.size=size
    572     * GNU.sparse.numblocks=numblocks
    573     * repeat numblocks times
    574     *   GNU.sparse.offset=offset
    575     *   GNU.sparse.numbytes=numbytes
    576     * end repeat
    577     * </pre>
    578     *
    579     * <p>
    580     * For PAX Format 0.1, the sparse headers are stored in a single variable : GNU.sparse.map
    581     *
    582     * <pre>
    583     * GNU.sparse.map
    584     *    Map of non-null data chunks. It is a string consisting of comma-separated values "offset,size[,offset-1,size-1...]"
    585     * </pre>
    586     *
    587     * <p>
    588     * For PAX Format 1.X: <br>
    589     * The sparse map itself is stored in the file data block, preceding the actual file data. It consists of a series of decimal numbers delimited by newlines.
    590     * The map is padded with nulls to the nearest block boundary. The first number gives the number of entries in the map. Following are map entries, each one
    591     * consisting of two numbers giving the offset and size of the data block it describes.
    592     *
    593     * @throws IOException if an I/O error occurs.
    594     */
    595    private void paxHeaders() throws IOException {
    596        List<TarArchiveStructSparse> sparseHeaders = new ArrayList<>();
    597        final Map<String, String> headers;
    598        try (InputStream input = getInputStream(currEntry)) {
    599            headers = TarUtils.parsePaxHeaders(input, sparseHeaders, globalPaxHeaders, currEntry.getSize());
    600        }
    601
    602        // for 0.1 PAX Headers
    603        if (headers.containsKey(TarGnuSparseKeys.MAP)) {
    604            sparseHeaders = new ArrayList<>(TarUtils.parseFromPAX01SparseHeaders(headers.get(TarGnuSparseKeys.MAP)));
    605        }
    606        getNextTarEntry(); // Get the actual file entry
    607        if (currEntry == null) {
    608            throw new IOException("premature end of tar archive. Didn't find any entry after PAX header.");
    609        }
    610        applyPaxHeadersToCurrentEntry(headers, sparseHeaders);
    611
    612        // for 1.0 PAX Format, the sparse map is stored in the file data block
    613        if (currEntry.isPaxGNU1XSparse()) {
    614            try (InputStream input = getInputStream(currEntry)) {
    615                sparseHeaders = TarUtils.parsePAX1XSparseHeaders(input, recordSize);
    616            }
    617            currEntry.setSparseHeaders(sparseHeaders);
    618            // data of the entry is after the pax gnu entry. So we need to update the data position once again
    619            currEntry.setDataOffset(currEntry.getDataOffset() + recordSize);
    620        }
    621
    622        // sparse headers are all done reading, we need to build
    623        // sparse input streams using these sparse headers
    624        buildSparseInputStreams();
    625    }
    626
    627    private void readGlobalPaxHeaders() throws IOException {
    628        try (InputStream input = getInputStream(currEntry)) {
    629            globalPaxHeaders = TarUtils.parsePaxHeaders(input, globalSparseHeaders, globalPaxHeaders, currEntry.getSize());
    630        }
    631        getNextTarEntry(); // Get the actual file entry
    632
    633        if (currEntry == null) {
    634            throw new IOException("Error detected parsing the pax header");
    635        }
    636    }
    637
    638    /**
    639     * Adds the sparse chunks from the current entry to the sparse chunks, including any additional sparse entries following the current entry.
    640     *
    641     * @throws IOException when reading the sparse entry fails
    642     */
    643    private void readOldGNUSparse() throws IOException {
    644        if (currEntry.isExtended()) {
    645            TarArchiveSparseEntry entry;
    646            do {
    647                final ByteBuffer headerBuf = getRecord();
    648                if (headerBuf == null) {
    649                    throw new IOException("premature end of tar archive. Didn't find extended_header after header with extended flag.");
    650                }
    651                entry = new TarArchiveSparseEntry(headerBuf.array());
    652                currEntry.getSparseHeaders().addAll(entry.getSparseHeaders());
    653                currEntry.setDataOffset(currEntry.getDataOffset() + recordSize);
    654            } while (entry.isExtended());
    655        }
    656
    657        // sparse headers are all done reading, we need to build
    658        // sparse input streams using these sparse headers
    659        buildSparseInputStreams();
    660    }
    661
    662    /**
    663     * Reads a record from the input stream and return the data.
    664     *
    665     * @return The record data or null if EOF has been hit.
    666     * @throws IOException if reading from the archive fails
    667     */
    668    private ByteBuffer readRecord() throws IOException {
    669        recordBuffer.rewind();
    670        final int readNow = archive.read(recordBuffer);
    671        if (readNow != recordSize) {
    672            return null;
    673        }
    674        return recordBuffer;
    675    }
    676
    677    private void repositionForwardBy(final long offset) throws IOException {
    678        repositionForwardTo(archive.position() + offset);
    679    }
    680
    681    private void repositionForwardTo(final long newPosition) throws IOException {
    682        final long currPosition = archive.position();
    683        if (newPosition < currPosition) {
    684            throw new IOException("trying to move backwards inside of the archive");
    685        }
    686        archive.position(newPosition);
    687    }
    688
    689    /**
    690     * Sets whether we are at end-of-file.
    691     *
    692     * @param eof whether we are at end-of-file.
    693     */
    694    protected final void setAtEOF(final boolean eof) {
    695        this.eof = eof;
    696    }
    697
    698    /**
    699     * The last record block should be written at the full size, so skip any additional space used to fill a record after an entry
    700     *
    701     * @throws IOException when skipping the padding of the record fails
    702     */
    703    private void skipRecordPadding() throws IOException {
    704        if (!isDirectory() && currEntry.getSize() > 0 && currEntry.getSize() % recordSize != 0) {
    705            final long numRecords = currEntry.getSize() / recordSize + 1;
    706            final long padding = numRecords * recordSize - currEntry.getSize();
    707            repositionForwardBy(padding);
    708            throwExceptionIfPositionIsNotInArchive();
    709        }
    710    }
    711
    712    /**
    713     * Checks if the current position of the SeekableByteChannel is in the archive.
    714     *
    715     * @throws IOException If the position is not in the archive
    716     */
    717    private void throwExceptionIfPositionIsNotInArchive() throws IOException {
    718        if (archive.size() < archive.position()) {
    719            throw new IOException("Truncated TAR archive");
    720        }
    721    }
    722
    723    /**
    724     * Tries to read the next record resetting the position in the archive if it is not an EOF record.
    725     *
    726     * <p>
    727     * This is meant to protect against cases where a tar implementation has written only one EOF record when two are expected. Actually this won't help since a
    728     * non-conforming implementation likely won't fill full blocks consisting of - by default - ten records either so we probably have already read beyond the
    729     * archive anyway.
    730     * </p>
    731     *
    732     * @throws IOException if reading the record of resetting the position in the archive fails
    733     */
    734    private void tryToConsumeSecondEOFRecord() throws IOException {
    735        boolean shouldReset = true;
    736        try {
    737            shouldReset = !isEOFRecord(readRecord());
    738        } finally {
    739            if (shouldReset) {
    740                archive.position(archive.position() - recordSize);
    741            }
    742        }
    743    }
    744}