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