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}