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