    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 */
    018
    019/*
    020 * This package is based on the work done by Timothy Gerard Endres
    021 * (time@ice.com) to whom the Ant project is very grateful for his great code.
    022 */
    023
    024package org.apache.commons.compress.archivers.tar;
    025
    026import java.io.ByteArrayOutputStream;
    027import java.io.IOException;
    028import java.io.InputStream;
    029import java.util.HashMap;
    030import java.util.Map;
    031import java.util.Map.Entry;
    032
    033import org.apache.commons.compress.archivers.ArchiveEntry;
    034import org.apache.commons.compress.archivers.ArchiveInputStream;
    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.CharsetNames;
    039import org.apache.commons.compress.utils.IOUtils;
    040
    041/**
    042 * The TarInputStream reads a UNIX tar archive as an InputStream.
    043 * methods are provided to position at each successive entry in
    044 * the archive, and the read each entry as a normal input stream
    045 * using read().
    046 * @NotThreadSafe
    047 */
    048public class TarArchiveInputStream extends ArchiveInputStream {
    049
    050    private static final int SMALL_BUFFER_SIZE = 256;
    051
    052    private final byte[] SMALL_BUF = new byte[SMALL_BUFFER_SIZE];
    053
    054    /** The size the TAR header */
    055    private final int recordSize;
    056
    057    /** The size of a block */
    058    private final int blockSize;
    059
    060    /** True if file has hit EOF */
    061    private boolean hasHitEOF;
    062
    063    /** Size of the current entry */
    064    private long entrySize;
    065
    066    /** How far into the entry the stream is at */
    067    private long entryOffset;
    068
    069    /** An input stream to read from */
    070    private final InputStream is;
    071
    072    /** The meta-data about the current entry */
    073    private TarArchiveEntry currEntry;
    074
    075    /** The encoding of the file */
    076    private final ZipEncoding zipEncoding;
    077
    078    // the provided encoding (for unit tests)
    079    final String encoding;
    080
    081    // the global PAX header
    082    private Map<String, String> globalPaxHeaders = new HashMap<String, String>();
    083
    084    /**
    085     * Constructor for TarInputStream.
    086     * @param is the input stream to use
    087     */
    088    public TarArchiveInputStream(final InputStream is) {
    089        this(is, TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE);
    090    }
    091
    092    /**
    093     * Constructor for TarInputStream.
    094     * @param is the input stream to use
    095     * @param encoding name of the encoding to use for file names
    096     * @since 1.4
    097     */
    098    public TarArchiveInputStream(final InputStream is, final String encoding) {
    099        this(is, TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE,
    100             encoding);
    101    }
    102
    103    /**
    104     * Constructor for TarInputStream.
    105     * @param is the input stream to use
    106     * @param blockSize the block size to use
    107     */
    108    public TarArchiveInputStream(final InputStream is, final int blockSize) {
    109        this(is, blockSize, TarConstants.DEFAULT_RCDSIZE);
    110    }
    111
    112    /**
    113     * Constructor for TarInputStream.
    114     * @param is the input stream to use
    115     * @param blockSize the block size to use
    116     * @param encoding name of the encoding to use for file names
    117     * @since 1.4
    118     */
    119    public TarArchiveInputStream(final InputStream is, final int blockSize,
    120                                 final String encoding) {
    121        this(is, blockSize, TarConstants.DEFAULT_RCDSIZE, encoding);
    122    }
    123
    124    /**
    125     * Constructor for TarInputStream.
    126     * @param is the input stream to use
    127     * @param blockSize the block size to use
    128     * @param recordSize the record size to use
    129     */
    130    public TarArchiveInputStream(final InputStream is, final int blockSize, final int recordSize) {
    131        this(is, blockSize, recordSize, null);
    132    }
    133
    134    /**
    135     * Constructor for TarInputStream.
    136     * @param is the input stream to use
    137     * @param blockSize the block size to use
    138     * @param recordSize the record size to use
    139     * @param encoding name of the encoding to use for file names
    140     * @since 1.4
    141     */
    142    public TarArchiveInputStream(final InputStream is, final int blockSize, final int recordSize,
    143                                 final String encoding) {
    144        this.is = is;
    145        this.hasHitEOF = false;
    146        this.encoding = encoding;
    147        this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
    148        this.recordSize = recordSize;
    149        this.blockSize = blockSize;
    150    }
    151
    152    /**
    153     * Closes this stream. Calls the TarBuffer's close() method.
    154     * @throws IOException on error
    155     */
    156    @Override
    157    public void close() throws IOException {
    158        is.close();
    159    }
    160
    161    /**
    162     * Get the record size being used by this stream's buffer.
    163     *
    164     * @return The TarBuffer record size.
    165     */
    166    public int getRecordSize() {
    167        return recordSize;
    168    }
    169
    170    /**
    171     * Get the available data that can be read from the current
    172     * entry in the archive. This does not indicate how much data
    173     * is left in the entire archive, only in the current entry.
    174     * This value is determined from the entry's size header field
    175     * and the amount of data already read from the current entry.
    176     * Integer.MAX_VALUE is returned in case more than Integer.MAX_VALUE
    177     * bytes are left in the current entry in the archive.
    178     *
    179     * @return The number of available bytes for the current entry.
    180     * @throws IOException for signature
    181     */
    182    @Override
    183    public int available() throws IOException {
    184        if (isDirectory()) {
    185            return 0;
    186        }
    187        if (entrySize - entryOffset > Integer.MAX_VALUE) {
    188            return Integer.MAX_VALUE;
    189        }
    190        return (int) (entrySize - entryOffset);
    191    }
    192
    193
    194    /**
    195     * Skips over and discards <code>n</code> bytes of data from this input
    196     * stream. The <code>skip</code> method may, for a variety of reasons, end
    197     * up skipping over some smaller number of bytes, possibly <code>0</code>.
    198     * This may result from any of a number of conditions; reaching end of file
    199     * or end of entry before <code>n</code> bytes have been skipped; are only
    200     * two possibilities. The actual number of bytes skipped is returned. If
    201     * <code>n</code> is negative, no bytes are skipped.
    202     *
    203     *
    204     * @param n
    205     *            the number of bytes to be skipped.
    206     * @return the actual number of bytes skipped.
    207     * @throws IOException
    208     *                if some other I/O error occurs.
    209     */
    210    @Override
    211    public long skip(final long n) throws IOException {
    212        if (n <= 0 || isDirectory()) {
    213            return 0;
    214        }
    215
    216        final long available = entrySize - entryOffset;
    217        final long skipped = is.skip(Math.min(n, available));
    218        count(skipped);
    219        entryOffset += skipped;
    220        return skipped;
    221    }
    222
    223    /**
    224     * Since we do not support marking just yet, we return false.
    225     *
    226     * @return False.
    227     */
    228    @Override
    229    public boolean markSupported() {
    230        return false;
    231    }
    232
    233    /**
    234     * Since we do not support marking just yet, we do nothing.
    235     *
    236     * @param markLimit The limit to mark.
    237     */
    238    @Override
    239    public void mark(final int markLimit) {
    240    }
    241
    242    /**
    243     * Since we do not support marking just yet, we do nothing.
    244     */
    245    @Override
    246    public synchronized void reset() {
    247    }
    248
    249    /**
    250     * Get the next entry in this tar archive. This will skip
    251     * over any remaining data in the current entry, if there
    252     * is one, and place the input stream at the header of the
    253     * next entry, and read the header and instantiate a new
    254     * TarEntry from the header bytes and return that entry.
    255     * If there are no more entries in the archive, null will
    256     * be returned to indicate that the end of the archive has
    257     * been reached.
    258     *
    259     * @return The next TarEntry in the archive, or null.
    260     * @throws IOException on error
    261     */
    262    public TarArchiveEntry getNextTarEntry() throws IOException {
    263        if (hasHitEOF) {
    264            return null;
    265        }
    266
    267        if (currEntry != null) {
    268            /* Skip will only go to the end of the current entry */
    269            IOUtils.skip(this, Long.MAX_VALUE);
    270
    271            /* skip to the end of the last record */
    272            skipRecordPadding();
    273        }
    274
    275        final byte[] headerBuf = getRecord();
    276
    277        if (headerBuf == null) {
    278            /* hit EOF */
    279            currEntry = null;
    280            return null;
    281        }
    282
    283        try {
    284            currEntry = new TarArchiveEntry(headerBuf, zipEncoding);
    285        } catch (final IllegalArgumentException e) {
    286            throw new IOException("Error detected parsing the header", e);
    287        }
    288
    289        entryOffset = 0;
    290        entrySize = currEntry.getSize();
    291
    292        if (currEntry.isGNULongLinkEntry()) {
    293            final byte[] longLinkData = getLongNameData();
    294            if (longLinkData == null) {
    295                // Bugzilla: 40334
    296                // Malformed tar file - long link entry name not followed by
    297                // entry
    298                return null;
    299            }
    300            currEntry.setLinkName(zipEncoding.decode(longLinkData));
    301        }
    302
    303        if (currEntry.isGNULongNameEntry()) {
    304            final byte[] longNameData = getLongNameData();
    305            if (longNameData == null) {
    306                // Bugzilla: 40334
    307                // Malformed tar file - long entry name not followed by
    308                // entry
    309                return null;
    310            }
    311            currEntry.setName(zipEncoding.decode(longNameData));
    312        }
    313
    314        if (currEntry.isGlobalPaxHeader()){ // Process Global Pax headers
    315            readGlobalPaxHeaders();
    316        }
    317
    318        if (currEntry.isPaxHeader()){ // Process Pax headers
    319            paxHeaders();
    320        } else if (!globalPaxHeaders.isEmpty()) {
    321            applyPaxHeadersToCurrentEntry(globalPaxHeaders);
    322        }
    323
    324        if (currEntry.isOldGNUSparse()){ // Process sparse files
    325            readOldGNUSparse();
    326        }
    327
    328        // If the size of the next element in the archive has changed
    329        // due to a new size being reported in the posix header
    330        // information, we update entrySize here so that it contains
    331        // the correct value.
    332        entrySize = currEntry.getSize();
    333
    334        return currEntry;
    335    }
    336
    337    /**
    338     * The last record block should be written at the full size, so skip any
    339     * additional space used to fill a record after an entry
    340     */
    341    private void skipRecordPadding() throws IOException {
    342        if (!isDirectory() && this.entrySize > 0 && this.entrySize % this.recordSize != 0) {
    343            final long numRecords = (this.entrySize / this.recordSize) + 1;
    344            final long padding = (numRecords * this.recordSize) - this.entrySize;
    345            final long skipped = IOUtils.skip(is, padding);
    346            count(skipped);
    347        }
    348    }
    349
    350    /**
    351     * Get the next entry in this tar archive as longname data.
    352     *
    353     * @return The next entry in the archive as longname data, or null.
    354     * @throws IOException on error
    355     */
    356    protected byte[] getLongNameData() throws IOException {
    357        // read in the name
    358        final ByteArrayOutputStream longName = new ByteArrayOutputStream();
    359        int length = 0;
    360        while ((length = read(SMALL_BUF)) >= 0) {
    361            longName.write(SMALL_BUF, 0, length);
    362        }
    363        getNextEntry();
    364        if (currEntry == null) {
    365            // Bugzilla: 40334
    366            // Malformed tar file - long entry name not followed by entry
    367            return null;
    368        }
    369        byte[] longNameData = longName.toByteArray();
    370        // remove trailing null terminator(s)
    371        length = longNameData.length;
    372        while (length > 0 && longNameData[length - 1] == 0) {
    373            --length;
    374        }
    375        if (length != longNameData.length) {
    376            final byte[] l = new byte[length];
    377            System.arraycopy(longNameData, 0, l, 0, length);
    378            longNameData = l;
    379        }
    380        return longNameData;
    381    }
    382
    383    /**
    384     * Get the next record in this tar archive. This will skip
    385     * over any remaining data in the current entry, if there
    386     * is one, and place the input stream at the header of the
    387     * next entry.
    388     *
    389     * <p>If there are no more entries in the archive, null will be
    390     * returned to indicate that the end of the archive has been
    391     * reached.  At the same time the {@code hasHitEOF} marker will be
    392     * set to true.</p>
    393     *
    394     * @return The next header in the archive, or null.
    395     * @throws IOException on error
    396     */
    397    private byte[] getRecord() throws IOException {
    398        byte[] headerBuf = readRecord();
    399        hasHitEOF = isEOFRecord(headerBuf);
    400        if (hasHitEOF && headerBuf != null) {
    401            tryToConsumeSecondEOFRecord();
    402            consumeRemainderOfLastBlock();
    403            headerBuf = null;
    404        }
    405        return headerBuf;
    406    }
    407
    408    /**
    409     * Determine if an archive record indicate End of Archive. End of
    410     * archive is indicated by a record that consists entirely of null bytes.
    411     *
    412     * @param record The record data to check.
    413     * @return true if the record data is an End of Archive
    414     */
    415    protected boolean isEOFRecord(final byte[] record) {
    416        return record == null || ArchiveUtils.isArrayZero(record, recordSize);
    417    }
    418
    419    /**
    420     * Read a record from the input stream and return the data.
    421     *
    422     * @return The record data or null if EOF has been hit.
    423     * @throws IOException on error
    424     */
    425    protected byte[] readRecord() throws IOException {
    426
    427        final byte[] record = new byte[recordSize];
    428
    429        final int readNow = IOUtils.readFully(is, record);
    430        count(readNow);
    431        if (readNow != recordSize) {
    432            return null;
    433        }
    434
    435        return record;
    436    }
    437
    438    private void readGlobalPaxHeaders() throws IOException {
    439        globalPaxHeaders = parsePaxHeaders(this);
    440        getNextEntry(); // Get the actual file entry
    441    }
    442
    443    private void paxHeaders() throws IOException{
    444        final Map<String, String> headers = parsePaxHeaders(this);
    445        getNextEntry(); // Get the actual file entry
    446        applyPaxHeadersToCurrentEntry(headers);
    447    }
    448
    449    // NOTE, using a Map here makes it impossible to ever support GNU
    450    // sparse files using the PAX Format 0.0, see
    451    // https://www.gnu.org/software/tar/manual/html_section/tar_92.html#SEC188
    452    Map<String, String> parsePaxHeaders(final InputStream i)
    453        throws IOException {
    454        final Map<String, String> headers = new HashMap<String, String>(globalPaxHeaders);
    455        // Format is "length keyword=value\n";
    456        while(true){ // get length
    457            int ch;
    458            int len = 0;
    459            int read = 0;
    460            while((ch = i.read()) != -1) {
    461                read++;
    462                if (ch == '\n') { // blank line in header
    463                    break;
    464                } else if (ch == ' '){ // End of length string
    465                    // Get keyword
    466                    final ByteArrayOutputStream coll = new ByteArrayOutputStream();
    467                    while((ch = i.read()) != -1) {
    468                        read++;
    469                        if (ch == '='){ // end of keyword
    470                            final String keyword = coll.toString(CharsetNames.UTF_8);
    471                            // Get rest of entry
    472                            final int restLen = len - read;
    473                            if (restLen == 1) { // only NL
    474                                headers.remove(keyword);
    475                            } else {
    476                                final byte[] rest = new byte[restLen];
    477                                final int got = IOUtils.readFully(i, rest);
    478                                if (got != restLen) {
    479                                    throw new IOException("Failed to read "
    480                                                          + "Paxheader. Expected "
    481                                                          + restLen
    482                                                          + " bytes, read "
    483                                                          + got);
    484                                }
    485                                // Drop trailing NL
    486                                final String value = new String(rest, 0,
    487                                                          restLen - 1, CharsetNames.UTF_8);
    488                                headers.put(keyword, value);
    489                            }
    490                            break;
    491                        }
    492                        coll.write((byte) ch);
    493                    }
    494                    break; // Processed single header
    495                }
    496                len *= 10;
    497                len += ch - '0';
    498            }
    499            if (ch == -1){ // EOF
    500                break;
    501            }
    502        }
    503        return headers;
    504    }
    505
    506    private void applyPaxHeadersToCurrentEntry(final Map<String, String> headers) {
    507        /*
    508         * The following headers are defined for Pax.
    509         * atime, ctime, charset: cannot use these without changing TarArchiveEntry fields
    510         * mtime
    511         * comment
    512         * gid, gname
    513         * linkpath
    514         * size
    515         * uid,uname
    516         * SCHILY.devminor, SCHILY.devmajor: don't have setters/getters for those
    517         *
    518         * GNU sparse files use additional members, we use
    519         * GNU.sparse.size to detect the 0.0 and 0.1 versions and
    520         * GNU.sparse.realsize for 1.0.
    521         *
    522         * star files use additional members of which we use
    523         * SCHILY.filetype in order to detect star sparse files.
    524         */
    525        for (final Entry<String, String> ent : headers.entrySet()){
    526            final String key = ent.getKey();
    527            final String val = ent.getValue();
    528            if ("path".equals(key)){
    529                currEntry.setName(val);
    530            } else if ("linkpath".equals(key)){
    531                currEntry.setLinkName(val);
    532            } else if ("gid".equals(key)){
    533                currEntry.setGroupId(Long.parseLong(val));
    534            } else if ("gname".equals(key)){
    535                currEntry.setGroupName(val);
    536            } else if ("uid".equals(key)){
    537                currEntry.setUserId(Long.parseLong(val));
    538            } else if ("uname".equals(key)){
    539                currEntry.setUserName(val);
    540            } else if ("size".equals(key)){
    541                currEntry.setSize(Long.parseLong(val));
    542            } else if ("mtime".equals(key)){
    543                currEntry.setModTime((long) (Double.parseDouble(val) * 1000));
    544            } else if ("SCHILY.devminor".equals(key)){
    545                currEntry.setDevMinor(Integer.parseInt(val));
    546            } else if ("SCHILY.devmajor".equals(key)){
    547                currEntry.setDevMajor(Integer.parseInt(val));
    548            } else if ("GNU.sparse.size".equals(key)) {
    549                currEntry.fillGNUSparse0xData(headers);
    550            } else if ("GNU.sparse.realsize".equals(key)) {
    551                currEntry.fillGNUSparse1xData(headers);
    552            } else if ("SCHILY.filetype".equals(key) && "sparse".equals(val)) {
    553                currEntry.fillStarSparseData(headers);
    554            }
    555        }
    556    }
    557
    558    /**
    559     * Adds the sparse chunks from the current entry to the sparse chunks,
    560     * including any additional sparse entries following the current entry.
    561     *
    562     * @throws IOException on error
    563     *
    564     * @todo Sparse files get not yet really processed.
    565     */
    566    private void readOldGNUSparse() throws IOException {
    567        /* we do not really process sparse files yet
    568        sparses = new ArrayList();
    569        sparses.addAll(currEntry.getSparses());
    570        */
    571        if (currEntry.isExtended()) {
    572            TarArchiveSparseEntry entry;
    573            do {
    574                final byte[] headerBuf = getRecord();
    575                if (headerBuf == null) {
    576                    currEntry = null;
    577                    break;
    578                }
    579                entry = new TarArchiveSparseEntry(headerBuf);
    580                /* we do not really process sparse files yet
    581                sparses.addAll(entry.getSparses());
    582                */
    583            } while (entry.isExtended());
    584        }
    585    }
    586
    587    private boolean isDirectory() {
    588        return currEntry != null && currEntry.isDirectory();
    589    }
    590
    591    /**
    592     * Returns the next Archive Entry in this Stream.
    593     *
    594     * @return the next entry,
    595     *         or {@code null} if there are no more entries
    596     * @throws IOException if the next entry could not be read
    597     */
    598    @Override
    599    public ArchiveEntry getNextEntry() throws IOException {
    600        return getNextTarEntry();
    601    }
    602
    603    /**
    604     * Tries to read the next record rewinding the stream if it is not a EOF record.
    605     *
    606     * <p>This is meant to protect against cases where a tar
    607     * implementation has written only one EOF record when two are
    608     * expected.  Actually this won't help since a non-conforming
    609     * implementation likely won't fill full blocks consisting of - by
    610     * default - ten records either so we probably have already read
    611     * beyond the archive anyway.</p>
    612     */
    613    private void tryToConsumeSecondEOFRecord() throws IOException {
    614        boolean shouldReset = true;
    615        final boolean marked = is.markSupported();
    616        if (marked) {
    617            is.mark(recordSize);
    618        }
    619        try {
    620            shouldReset = !isEOFRecord(readRecord());
    621        } finally {
    622            if (shouldReset && marked) {
    623                pushedBackBytes(recordSize);
    624                is.reset();
    625            }
    626        }
    627    }
    628
    629    /**
    630     * Reads bytes from the current tar archive entry.
    631     *
    632     * This method is aware of the boundaries of the current
    633     * entry in the archive and will deal with them as if they
    634     * were this stream's start and EOF.
    635     *
    636     * @param buf The buffer into which to place bytes read.
    637     * @param offset The offset at which to place bytes read.
    638     * @param numToRead The number of bytes to read.
    639     * @return The number of bytes read, or -1 at EOF.
    640     * @throws IOException on error
    641     */
    642    @Override
    643    public int read(final byte[] buf, final int offset, int numToRead) throws IOException {
    644        int totalRead = 0;
    645
    646        if (hasHitEOF || isDirectory() || entryOffset >= entrySize) {
    647            return -1;
    648        }
    649
    650        if (currEntry == null) {
    651            throw new IllegalStateException("No current tar entry");
    652        }
    653
    654        numToRead = Math.min(numToRead, available());
    655
    656        totalRead = is.read(buf, offset, numToRead);
    657
    658        if (totalRead == -1) {
    659            if (numToRead > 0) {
    660                throw new IOException("Truncated TAR archive");
    661            }
    662            hasHitEOF = true;
    663        } else {
    664            count(totalRead);
    665            entryOffset += totalRead;
    666        }
    667
    668        return totalRead;
    669    }
    670
    671    /**
    672     * Whether this class is able to read the given entry.
    673     *
    674     * <p>May return false if the current entry is a sparse file.</p>
    675     */
    676    @Override
    677    public boolean canReadEntryData(final ArchiveEntry ae) {
    678        if (ae instanceof TarArchiveEntry) {
    679            final TarArchiveEntry te = (TarArchiveEntry) ae;
    680            return !te.isSparse();
    681        }
    682        return false;
    683    }
    684
    685    /**
    686     * Get the current TAR Archive Entry that this input stream is processing
    687     *
    688     * @return The current Archive Entry
    689     */
    690    public TarArchiveEntry getCurrentEntry() {
    691        return currEntry;
    692    }
    693
    694    protected final void setCurrentEntry(final TarArchiveEntry e) {
    695        currEntry = e;
    696    }
    697
    698    protected final boolean isAtEOF() {
    699        return hasHitEOF;
    700    }
    701
    702    protected final void setAtEOF(final boolean b) {
    703        hasHitEOF = b;
    704    }
    705
    706    /**
    707     * This method is invoked once the end of the archive is hit, it
    708     * tries to consume the remaining bytes under the assumption that
    709     * the tool creating this archive has padded the last block.
    710     */
    711    private void consumeRemainderOfLastBlock() throws IOException {
    712        final long bytesReadOfLastBlock = getBytesRead() % blockSize;
    713        if (bytesReadOfLastBlock > 0) {
    714            final long skipped = IOUtils.skip(is, blockSize - bytesReadOfLastBlock);
    715            count(skipped);
    716        }
    717    }
    718
    719    /**
    720     * Checks if the signature matches what is expected for a tar file.
    721     *
    722     * @param signature
    723     *            the bytes to check
    724     * @param length
    725     *            the number of bytes to check
    726     * @return true, if this stream is a tar archive stream, false otherwise
    727     */
    728    public static boolean matches(final byte[] signature, final int length) {
    729        if (length < TarConstants.VERSION_OFFSET+TarConstants.VERSIONLEN) {
    730            return false;
    731        }
    732
    733        if (ArchiveUtils.matchAsciiBuffer(TarConstants.MAGIC_POSIX,
    734                signature, TarConstants.MAGIC_OFFSET, TarConstants.MAGICLEN)
    735            &&
    736            ArchiveUtils.matchAsciiBuffer(TarConstants.VERSION_POSIX,
    737                signature, TarConstants.VERSION_OFFSET, TarConstants.VERSIONLEN)
    738                ){
    739            return true;
    740        }
    741        if (ArchiveUtils.matchAsciiBuffer(TarConstants.MAGIC_GNU,
    742                signature, TarConstants.MAGIC_OFFSET, TarConstants.MAGICLEN)
    743            &&
    744            (
    745             ArchiveUtils.matchAsciiBuffer(TarConstants.VERSION_GNU_SPACE,
    746                signature, TarConstants.VERSION_OFFSET, TarConstants.VERSIONLEN)
    747            ||
    748            ArchiveUtils.matchAsciiBuffer(TarConstants.VERSION_GNU_ZERO,
    749                signature, TarConstants.VERSION_OFFSET, TarConstants.VERSIONLEN)
    750            )
    751                ){
    752            return true;
    753        }
    754        // COMPRESS-107 - recognise Ant tar files
    755        if (ArchiveUtils.matchAsciiBuffer(TarConstants.MAGIC_ANT,
    756                signature, TarConstants.MAGIC_OFFSET, TarConstants.MAGICLEN)
    757            &&
    758            ArchiveUtils.matchAsciiBuffer(TarConstants.VERSION_ANT,
    759                signature, TarConstants.VERSION_OFFSET, TarConstants.VERSIONLEN)
    760                ){
    761            return true;
    762        }
    763        return false;
    764    }
    765
    766}