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