    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.File;
    022    import java.io.IOException;
    023    import java.io.OutputStream;
    024
    025    import org.apache.commons.compress.archivers.ArchiveEntry;
    026    import org.apache.commons.compress.archivers.ArchiveOutputStream;
    027    import org.apache.commons.compress.utils.ArchiveUtils;
    028
    029    /**
    030     * Implements the "ar" archive format as an output stream.
    031     *
    032     * @NotThreadSafe
    033     */
    034    public class ArArchiveOutputStream extends ArchiveOutputStream {
    035        /** Fail if a long file name is required in the archive. */
    036        public static final int LONGFILE_ERROR = 0;
    037
    038        /** BSD ar extensions are used to store long file names in the archive. */
    039        public static final int LONGFILE_BSD = 1;
    040
    041        private final OutputStream out;
    042        private long entryOffset = 0;
    043        private ArArchiveEntry prevEntry;
    044        private boolean haveUnclosedEntry = false;
    045        private int longFileMode = LONGFILE_ERROR;
    046
    047        /** indicates if this archive is finished */
    048        private boolean finished = false;
    049
    050        public ArArchiveOutputStream( final OutputStream pOut ) {
    051            this.out = pOut;
    052        }
    053
    054        /**
    055         * Set the long file mode.
    056         * This can be LONGFILE_ERROR(0) or LONGFILE_BSD(1).
    057         * This specifies the treatment of long file names (names >= 16).
    058         * Default is LONGFILE_ERROR.
    059         * @param longFileMode the mode to use
    060         * @since Apache Commons Compress 1.3
    061         */
    062        public void setLongFileMode(int longFileMode) {
    063            this.longFileMode = longFileMode;
    064        }
    065
    066        private long writeArchiveHeader() throws IOException {
    067            byte [] header = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER);
    068            out.write(header);
    069            return header.length;
    070        }
    071
    072        /** {@inheritDoc} */
    073        @Override
    074        public void closeArchiveEntry() throws IOException {
    075            if(finished) {
    076                throw new IOException("Stream has already been finished");
    077            }
    078            if (prevEntry == null || !haveUnclosedEntry){
    079                throw new IOException("No current entry to close");
    080            }
    081            if ((entryOffset % 2) != 0) {
    082                out.write('\n'); // Pad byte
    083            }
    084            haveUnclosedEntry = false;
    085        }
    086
    087        /** {@inheritDoc} */
    088        @Override
    089        public void putArchiveEntry( final ArchiveEntry pEntry ) throws IOException {
    090            if(finished) {
    091                throw new IOException("Stream has already been finished");
    092            }
    093
    094            ArArchiveEntry pArEntry = (ArArchiveEntry)pEntry;
    095            if (prevEntry == null) {
    096                writeArchiveHeader();
    097            } else {
    098                if (prevEntry.getLength() != entryOffset) {
    099                    throw new IOException("length does not match entry (" + prevEntry.getLength() + " != " + entryOffset);
    100                }
    101
    102                if (haveUnclosedEntry) {
    103                    closeArchiveEntry();
    104                }
    105            }
    106
    107            prevEntry = pArEntry;
    108
    109            writeEntryHeader(pArEntry);
    110
    111            entryOffset = 0;
    112            haveUnclosedEntry = true;
    113        }
    114
    115        private long fill( final long pOffset, final long pNewOffset, final char pFill ) throws IOException {
    116            final long diff = pNewOffset - pOffset;
    117
    118            if (diff > 0) {
    119                for (int i = 0; i < diff; i++) {
    120                    write(pFill);
    121                }
    122            }
    123
    124            return pNewOffset;
    125        }
    126
    127        private long write( final String data ) throws IOException {
    128            final byte[] bytes = data.getBytes("ascii");
    129            write(bytes);
    130            return bytes.length;
    131        }
    132
    133        private long writeEntryHeader( final ArArchiveEntry pEntry ) throws IOException {
    134
    135            long offset = 0;
    136            boolean mustAppendName = false;
    137
    138            final String n = pEntry.getName();
    139            if (LONGFILE_ERROR == longFileMode && n.length() > 16) {
    140                throw new IOException("filename too long, > 16 chars: "+n);
    141            }
    142            if (LONGFILE_BSD == longFileMode &&
    143                (n.length() > 16 || n.indexOf(" ") > -1)) {
    144                mustAppendName = true;
    145                offset += write(ArArchiveInputStream.BSD_LONGNAME_PREFIX
    146                                + String.valueOf(n.length()));
    147            } else {
    148                offset += write(n);
    149            }
    150
    151            offset = fill(offset, 16, ' ');
    152            final String m = "" + (pEntry.getLastModified());
    153            if (m.length() > 12) {
    154                throw new IOException("modified too long");
    155            }
    156            offset += write(m);
    157
    158            offset = fill(offset, 28, ' ');
    159            final String u = "" + pEntry.getUserId();
    160            if (u.length() > 6) {
    161                throw new IOException("userid too long");
    162            }
    163            offset += write(u);
    164
    165            offset = fill(offset, 34, ' ');
    166            final String g = "" + pEntry.getGroupId();
    167            if (g.length() > 6) {
    168                throw new IOException("groupid too long");
    169            }
    170            offset += write(g);
    171
    172            offset = fill(offset, 40, ' ');
    173            final String fm = "" + Integer.toString(pEntry.getMode(), 8);
    174            if (fm.length() > 8) {
    175                throw new IOException("filemode too long");
    176            }
    177            offset += write(fm);
    178
    179            offset = fill(offset, 48, ' ');
    180            final String s =
    181                String.valueOf(pEntry.getLength()
    182                               + (mustAppendName ? n.length() : 0));
    183            if (s.length() > 10) {
    184                throw new IOException("size too long");
    185            }
    186            offset += write(s);
    187
    188            offset = fill(offset, 58, ' ');
    189
    190            offset += write(ArArchiveEntry.TRAILER);
    191
    192            if (mustAppendName) {
    193                offset += write(n);
    194            }
    195
    196            return offset;
    197        }
    198
    199        @Override
    200        public void write(byte[] b, int off, int len) throws IOException {
    201            out.write(b, off, len);
    202            count(len);
    203            entryOffset += len;
    204        }
    205
    206        /**
    207         * Calls finish if necessary, and then closes the OutputStream
    208         */
    209        @Override
    210        public void close() throws IOException {
    211            if(!finished) {
    212                finish();
    213            }
    214            out.close();
    215            prevEntry = null;
    216        }
    217
    218        /** {@inheritDoc} */
    219        @Override
    220        public ArchiveEntry createArchiveEntry(File inputFile, String entryName)
    221                throws IOException {
    222            if(finished) {
    223                throw new IOException("Stream has already been finished");
    224            }
    225            return new ArArchiveEntry(inputFile, entryName);
    226        }
    227
    228        /** {@inheritDoc} */
    229        @Override
    230        public void finish() throws IOException {
    231            if(haveUnclosedEntry) {
    232                throw new IOException("This archive contains unclosed entries.");
    233            } else if(finished) {
    234                throw new IOException("This archive has already been finished");
    235            }
    236            finished = true;
    237        }
    238    }