    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
    039    private static final char PAD = '\n';
    040
    041    private static final char SPACE = ' ';
    042
    043    /** Fail if a long file name is required in the archive. */
    044    public static final int LONGFILE_ERROR = 0;
    045
    046    /** BSD ar extensions are used to store long file names in the archive. */
    047    public static final int LONGFILE_BSD = 1;
    048
    049    private final OutputStream out;
    050    private long entryOffset;
    051    private int headerPlus;
    052    private ArArchiveEntry prevEntry;
    053    private boolean prevEntryOpen;
    054    private int longFileMode = LONGFILE_ERROR;
    055
    056    /** Indicates if this archive is finished */
    057    private boolean finished;
    058
    059    public ArArchiveOutputStream(final OutputStream out) {
    060        this.out = out;
    061    }
    062
    063    /**
    064     * @throws IOException
    065     */
    066    private void checkFinished() throws IOException {
    067        if (finished) {
    068            throw new IOException("Stream has already been finished");
    069        }
    070    }
    071
    072    private String checkLength(final String value, final int max, final String name) throws IOException {
    073        if (value.length() > max) {
    074            throw new IOException(name + " too long");
    075        }
    076        return value;
    077    }
    078
    079    /**
    080     * Calls finish if necessary, and then closes the OutputStream
    081     */
    082    @Override
    083    public void close() throws IOException {
    084        try {
    085            if (!finished) {
    086                finish();
    087            }
    088        } finally {
    089            out.close();
    090            prevEntry = null;
    091        }
    092    }
    093
    094    @Override
    095    public void closeArchiveEntry() throws IOException {
    096        checkFinished();
    097        if (prevEntry == null || !prevEntryOpen) {
    098            throw new IOException("No current entry to close");
    099        }
    100        if ((headerPlus + entryOffset) % 2 != 0) {
    101            out.write(PAD); // Pad byte
    102        }
    103        prevEntryOpen = false;
    104    }
    105
    106    @Override
    107    public ArArchiveEntry createArchiveEntry(final File inputFile, final String entryName) throws IOException {
    108        checkFinished();
    109        return new ArArchiveEntry(inputFile, entryName);
    110    }
    111
    112    /**
    113     * {@inheritDoc}
    114     *
    115     * @since 1.21
    116     */
    117    @Override
    118    public ArArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
    119        checkFinished();
    120        return new ArArchiveEntry(inputPath, entryName, options);
    121    }
    122
    123    @Override
    124    public void finish() throws IOException {
    125        if (prevEntryOpen) {
    126            throw new IOException("This archive contains unclosed entries.");
    127        }
    128        checkFinished();
    129        finished = true;
    130    }
    131
    132    private int pad(final int offset, final int newOffset, final char fill) throws IOException {
    133        final int diff = newOffset - offset;
    134        if (diff > 0) {
    135            for (int i = 0; i < diff; i++) {
    136                write(fill);
    137            }
    138        }
    139        return newOffset;
    140    }
    141
    142    @Override
    143    public void putArchiveEntry(final ArArchiveEntry entry) throws IOException {
    144        checkFinished();
    145        if (prevEntry == null) {
    146            writeArchiveHeader();
    147        } else {
    148            if (prevEntry.getLength() != entryOffset) {
    149                throw new IOException("Length does not match entry (" + prevEntry.getLength() + " != " + entryOffset);
    150            }
    151            if (prevEntryOpen) {
    152                closeArchiveEntry();
    153            }
    154        }
    155        prevEntry = entry;
    156        headerPlus = writeEntryHeader(entry);
    157        entryOffset = 0;
    158        prevEntryOpen = true;
    159    }
    160
    161    /**
    162     * Sets the long file mode. This can be LONGFILE_ERROR(0) or LONGFILE_BSD(1). This specifies the treatment of long file names (names &gt;= 16). Default is
    163     * LONGFILE_ERROR.
    164     *
    165     * @param longFileMode the mode to use
    166     * @since 1.3
    167     */
    168    public void setLongFileMode(final int longFileMode) {
    169        this.longFileMode = longFileMode;
    170    }
    171
    172    @Override
    173    public void write(final byte[] b, final int off, final int len) throws IOException {
    174        out.write(b, off, len);
    175        count(len);
    176        entryOffset += len;
    177    }
    178
    179    private int write(final String data) throws IOException {
    180        final byte[] bytes = data.getBytes(US_ASCII);
    181        write(bytes);
    182        return bytes.length;
    183    }
    184
    185    private void writeArchiveHeader() throws IOException {
    186        out.write(ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER));
    187    }
    188
    189    private int writeEntryHeader(final ArArchiveEntry entry) throws IOException {
    190        int offset = 0;
    191        boolean appendName = false;
    192        final String eName = entry.getName();
    193        final int nLength = eName.length();
    194        if (LONGFILE_ERROR == longFileMode && nLength > 16) {
    195            throw new IOException("File name too long, > 16 chars: " + eName);
    196        }
    197        if (LONGFILE_BSD == longFileMode && (nLength > 16 || eName.indexOf(SPACE) > -1)) {
    198            appendName = true;
    199            final String fileNameLen = ArArchiveInputStream.BSD_LONGNAME_PREFIX + nLength;
    200            if (fileNameLen.length() > 16) {
    201                throw new IOException("File length too long, > 16 chars: " + eName);
    202            }
    203            offset += write(fileNameLen);
    204        } else {
    205            offset += write(eName);
    206        }
    207        offset = pad(offset, 16, SPACE);
    208        // Last modified
    209        offset += write(checkLength(String.valueOf(entry.getLastModified()), 12, "Last modified"));
    210        offset = pad(offset, 28, SPACE);
    211        // User ID
    212        offset += write(checkLength(String.valueOf(entry.getUserId()), 6, "User ID"));
    213        offset = pad(offset, 34, SPACE);
    214        // Group ID
    215        offset += write(checkLength(String.valueOf(entry.getGroupId()), 6, "Group ID"));
    216        offset = pad(offset, 40, SPACE);
    217        // Mode
    218        offset += write(checkLength(String.valueOf(Integer.toString(entry.getMode(), 8)), 8, "File mode"));
    219        offset = pad(offset, 48, SPACE);
    220        // Size
    221        // On overflow, the file size is incremented by the length of the name.
    222        offset += write(checkLength(String.valueOf(entry.getLength() + (appendName ? nLength : 0)), 10, "Size"));
    223        offset = pad(offset, 58, SPACE);
    224        offset += write(ArArchiveEntry.TRAILER);
    225        // Name
    226        if (appendName) {
    227            offset += write(eName);
    228        }
    229        return offset;
    230    }
    231
    232}