    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one
    003     * or more contributor license agreements.  See the NOTICE file
    004     * distributed with this work for additional information
    005     * regarding copyright ownership.  The ASF licenses this file
    006     * to you under the Apache License, Version 2.0 (the
    007     * "License"); you may not use this file except in compliance
    008     * with the License.  You may obtain a copy of the License at
    009     *
    010     * http://www.apache.org/licenses/LICENSE-2.0
    011     *
    012     * Unless required by applicable law or agreed to in writing,
    013     * software distributed under the License is distributed on an
    014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    015     * KIND, either express or implied.  See the License for the
    016     * specific language governing permissions and limitations
    017     * under the License.
    018     */
    019    package org.apache.commons.compress.archivers.ar;
    020
    021    import java.io.IOException;
    022    import java.io.InputStream;
    023
    024    import org.apache.commons.compress.archivers.ArchiveEntry;
    025    import org.apache.commons.compress.archivers.ArchiveInputStream;
    026    import org.apache.commons.compress.utils.ArchiveUtils;
    027
    028    /**
    029     * Implements the "ar" archive format as an input stream.
    030     *
    031     * @NotThreadSafe
    032     *
    033     */
    034    public class ArArchiveInputStream extends ArchiveInputStream {
    035
    036        private final InputStream input;
    037        private long offset = 0;
    038        private boolean closed;
    039
    040        /*
    041         * If getNextEnxtry has been called, the entry metadata is stored in
    042         * currentEntry.
    043         */
    044        private ArArchiveEntry currentEntry = null;
    045
    046        /*
    047         * The offset where the current entry started. -1 if no entry has been
    048         * called
    049         */
    050        private long entryOffset = -1;
    051
    052        /**
    053         * Constructs an Ar input stream with the referenced stream
    054         *
    055         * @param pInput
    056         *            the ar input stream
    057         */
    058        public ArArchiveInputStream(final InputStream pInput) {
    059            input = pInput;
    060            closed = false;
    061        }
    062
    063        /**
    064         * Returns the next AR entry in this stream.
    065         *
    066         * @return the next AR entry.
    067         * @throws IOException
    068         *             if the entry could not be read
    069         */
    070        public ArArchiveEntry getNextArEntry() throws IOException {
    071            if (currentEntry != null) {
    072                final long entryEnd = entryOffset + currentEntry.getLength();
    073                while (offset < entryEnd) {
    074                    int x = read();
    075                    if (x == -1) {
    076                        // hit EOF before previous entry was complete
    077                        // TODO: throw an exception instead?
    078                        return null;
    079                    }
    080                }
    081                currentEntry = null;
    082            }
    083
    084            if (offset == 0) {
    085                final byte[] expected = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER);
    086                final byte[] realized = new byte[expected.length];
    087                final int read = read(realized);
    088                if (read != expected.length) {
    089                    throw new IOException("failed to read header. Occured at byte: " + getCount());
    090                }
    091                for (int i = 0; i < expected.length; i++) {
    092                    if (expected[i] != realized[i]) {
    093                        throw new IOException("invalid header " + ArchiveUtils.toAsciiString(realized));
    094                    }
    095                }
    096            }
    097
    098            if (offset % 2 != 0) {
    099                if (read() < 0) {
    100                    // hit eof
    101                    return null;
    102                }
    103            }
    104
    105            if (input.available() == 0) {
    106                return null;
    107            }
    108
    109            final byte[] name = new byte[16];
    110            final byte[] lastmodified = new byte[12];
    111            final byte[] userid = new byte[6];
    112            final byte[] groupid = new byte[6];
    113            final byte[] filemode = new byte[8];
    114            final byte[] length = new byte[10];
    115
    116            read(name);
    117            read(lastmodified);
    118            read(userid);
    119            read(groupid);
    120            read(filemode);
    121            read(length);
    122
    123            {
    124                final byte[] expected = ArchiveUtils.toAsciiBytes(ArArchiveEntry.TRAILER);
    125                final byte[] realized = new byte[expected.length];
    126                final int read = read(realized);
    127                if (read != expected.length) {
    128                    throw new IOException("failed to read entry header. Occured at byte: " + getCount());
    129                }
    130                for (int i = 0; i < expected.length; i++) {
    131                    if (expected[i] != realized[i]) {
    132                        throw new IOException("invalid entry header. not read the content? Occured at byte: " + getCount());
    133                    }
    134                }
    135            }
    136
    137            entryOffset = offset;
    138
    139            // SVR4/GNU adds a trailing "/" to names
    140            // entry name is stored as ASCII string
    141            String temp = ArchiveUtils.toAsciiString(name).trim();
    142            if (temp.endsWith("/")) {
    143                temp = temp.substring(0, temp.length() - 1);
    144            }
    145            currentEntry = new ArArchiveEntry(temp, Long.parseLong(new String(length).trim()));
    146            return currentEntry;
    147        }
    148
    149        /*
    150         * (non-Javadoc)
    151         *
    152         * @see
    153         * org.apache.commons.compress.archivers.ArchiveInputStream#getNextEntry()
    154         */
    155        public ArchiveEntry getNextEntry() throws IOException {
    156            return getNextArEntry();
    157        }
    158
    159        /*
    160         * (non-Javadoc)
    161         *
    162         * @see java.io.InputStream#close()
    163         */
    164        public void close() throws IOException {
    165            if (!closed) {
    166                closed = true;
    167                input.close();
    168            }
    169            currentEntry = null;
    170        }
    171
    172        /*
    173         * (non-Javadoc)
    174         *
    175         * @see java.io.InputStream#read(byte[], int, int)
    176         */
    177        public int read(byte[] b, final int off, final int len) throws IOException {
    178            int toRead = len;
    179            if (currentEntry != null) {
    180                final long entryEnd = entryOffset + currentEntry.getLength();
    181                if (len > 0 && entryEnd > offset) {
    182                    toRead = (int) Math.min(len, entryEnd - offset);
    183                } else {
    184                    return -1;
    185                }
    186            }
    187            final int ret = this.input.read(b, off, toRead);
    188            count(ret);
    189            offset += (ret > 0 ? ret : 0);
    190            return ret;
    191        }
    192
    193        /**
    194         * Checks if the signature matches ASCII "!<arch>" followed by a single LF
    195         * control character
    196         *
    197         * @param signature
    198         *            the bytes to check
    199         * @param length
    200         *            the number of bytes to check
    201         * @return true, if this stream is an Ar archive stream, false otherwise
    202         */
    203        public static boolean matches(byte[] signature, int length) {
    204            // 3c21 7261 6863 0a3e
    205
    206            if (length < 8) {
    207                return false;
    208            }
    209            if (signature[0] != 0x21) {
    210                return false;
    211            }
    212            if (signature[1] != 0x3c) {
    213                return false;
    214            }
    215            if (signature[2] != 0x61) {
    216                return false;
    217            }
    218            if (signature[3] != 0x72) {
    219                return false;
    220            }
    221            if (signature[4] != 0x63) {
    222                return false;
    223            }
    224            if (signature[5] != 0x68) {
    225                return false;
    226            }
    227            if (signature[6] != 0x3e) {
    228                return false;
    229            }
    230            if (signature[7] != 0x0a) {
    231                return false;
    232            }
    233
    234            return true;
    235        }
    236
    237    }