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