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