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