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