    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;
    024import java.nio.charset.StandardCharsets;
    025import java.nio.file.LinkOption;
    026import java.nio.file.Path;
    027
    028import org.apache.commons.compress.archivers.ArchiveEntry;
    029import org.apache.commons.compress.archivers.ArchiveOutputStream;
    030import org.apache.commons.compress.utils.ArchiveUtils;
    031
    032/**
    033 * Implements the "ar" archive format as an output stream.
    034 *
    035 * @NotThreadSafe
    036 */
    037public class ArArchiveOutputStream extends ArchiveOutputStream {
    038    /** Fail if a long file name is required in the archive. */
    039    public static final int LONGFILE_ERROR = 0;
    040
    041    /** BSD ar extensions are used to store long file names in the archive. */
    042    public static final int LONGFILE_BSD = 1;
    043
    044    private final OutputStream out;
    045    private long entryOffset;
    046    private ArArchiveEntry prevEntry;
    047    private boolean haveUnclosedEntry;
    048    private int longFileMode = LONGFILE_ERROR;
    049
    050    /** indicates if this archive is finished */
    051    private boolean finished;
    052
    053    public ArArchiveOutputStream(final OutputStream pOut) {
    054        this.out = pOut;
    055    }
    056
    057    /**
    058     * Set the long file mode.
    059     * This can be LONGFILE_ERROR(0) or LONGFILE_BSD(1).
    060     * This specifies the treatment of long file names (names &gt;= 16).
    061     * Default is LONGFILE_ERROR.
    062     * @param longFileMode the mode to use
    063     * @since 1.3
    064     */
    065    public void setLongFileMode(final int longFileMode) {
    066        this.longFileMode = longFileMode;
    067    }
    068
    069    private void writeArchiveHeader() throws IOException {
    070        final byte [] header = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER);
    071        out.write(header);
    072    }
    073
    074    @Override
    075    public void closeArchiveEntry() throws IOException {
    076        if(finished) {
    077            throw new IOException("Stream has already been finished");
    078        }
    079        if (prevEntry == null || !haveUnclosedEntry){
    080            throw new IOException("No current entry to close");
    081        }
    082        if (entryOffset % 2 != 0) {
    083            out.write('\n'); // Pad byte
    084        }
    085        haveUnclosedEntry = false;
    086    }
    087
    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        final 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(StandardCharsets.US_ASCII);
    129        write(bytes);
    130        return bytes.length;
    131    }
    132
    133    private void writeEntryHeader(final ArArchiveEntry pEntry) throws IOException {
    134
    135        long offset = 0;
    136        boolean mustAppendName = false;
    137
    138        final String n = pEntry.getName();
    139        final int nLength = n.length();
    140        if (LONGFILE_ERROR == longFileMode && nLength > 16) {
    141            throw new IOException("File name too long, > 16 chars: "+n);
    142        }
    143        if (LONGFILE_BSD == longFileMode &&
    144            (nLength > 16 || n.contains(" "))) {
    145            mustAppendName = true;
    146            offset += write(ArArchiveInputStream.BSD_LONGNAME_PREFIX
    147                            + String.valueOf(nLength));
    148        } else {
    149            offset += write(n);
    150        }
    151
    152        offset = fill(offset, 16, ' ');
    153        final String m = "" + pEntry.getLastModified();
    154        if (m.length() > 12) {
    155            throw new IOException("Last modified too long");
    156        }
    157        offset += write(m);
    158
    159        offset = fill(offset, 28, ' ');
    160        final String u = "" + pEntry.getUserId();
    161        if (u.length() > 6) {
    162            throw new IOException("User id too long");
    163        }
    164        offset += write(u);
    165
    166        offset = fill(offset, 34, ' ');
    167        final String g = "" + pEntry.getGroupId();
    168        if (g.length() > 6) {
    169            throw new IOException("Group id too long");
    170        }
    171        offset += write(g);
    172
    173        offset = fill(offset, 40, ' ');
    174        final String fm = "" + Integer.toString(pEntry.getMode(), 8);
    175        if (fm.length() > 8) {
    176            throw new IOException("Filemode too long");
    177        }
    178        offset += write(fm);
    179
    180        offset = fill(offset, 48, ' ');
    181        final String s =
    182            String.valueOf(pEntry.getLength()
    183                           + (mustAppendName ? nLength : 0));
    184        if (s.length() > 10) {
    185            throw new IOException("Size too long");
    186        }
    187        offset += write(s);
    188
    189        offset = fill(offset, 58, ' ');
    190
    191        offset += write(ArArchiveEntry.TRAILER);
    192
    193        if (mustAppendName) {
    194            offset += write(n);
    195        }
    196
    197    }
    198
    199    @Override
    200    public void write(final byte[] b, final int off, final 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        try {
    212            if (!finished) {
    213                finish();
    214            }
    215        } finally {
    216            out.close();
    217            prevEntry = null;
    218        }
    219    }
    220
    221    @Override
    222    public ArchiveEntry createArchiveEntry(final File inputFile, final String entryName)
    223        throws IOException {
    224        if (finished) {
    225            throw new IOException("Stream has already been finished");
    226        }
    227        return new ArArchiveEntry(inputFile, entryName);
    228    }
    229
    230    /**
    231     * {@inheritDoc}
    232     *
    233     * @since 1.21
    234     */
    235    @Override
    236    public ArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
    237        if (finished) {
    238            throw new IOException("Stream has already been finished");
    239        }
    240        return new ArArchiveEntry(inputPath, entryName, options);
    241    }
    242
    243    @Override
    244    public void finish() throws IOException {
    245        if(haveUnclosedEntry) {
    246            throw new IOException("This archive contains unclosed entries.");
    247        }
    248        if(finished) {
    249            throw new IOException("This archive has already been finished");
    250        }
    251        finished = true;
    252    }
    253}