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