    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.cpio;
    020
    021import java.io.File;
    022import java.io.IOException;
    023import java.io.OutputStream;
    024import java.nio.ByteBuffer;
    025import java.nio.file.LinkOption;
    026import java.nio.file.Path;
    027import java.util.Arrays;
    028import java.util.HashMap;
    029
    030import org.apache.commons.compress.archivers.ArchiveOutputStream;
    031import org.apache.commons.compress.archivers.zip.ZipEncoding;
    032import org.apache.commons.compress.archivers.zip.ZipEncodingHelper;
    033import org.apache.commons.compress.utils.ArchiveUtils;
    034import org.apache.commons.compress.utils.CharsetNames;
    035
    036/**
    037 * CpioArchiveOutputStream is a stream for writing CPIO streams. All formats of CPIO are supported (old ASCII, old binary, new portable format and the new
    038 * portable format with CRC).
    039 *
    040 * <p>
    041 * An entry can be written by creating an instance of CpioArchiveEntry and fill it with the necessary values and put it into the CPIO stream. Afterwards write
    042 * the contents of the file into the CPIO stream. Either close the stream by calling finish() or put a next entry into the cpio stream.
    043 * </p>
    044 *
    045 * <pre>
    046 * CpioArchiveOutputStream out = new CpioArchiveOutputStream(
    047 *         new FileOutputStream(new File("test.cpio")));
    048 * CpioArchiveEntry entry = new CpioArchiveEntry();
    049 * entry.setName("testfile");
    050 * String contents = &quot;12345&quot;;
    051 * entry.setFileSize(contents.length());
    052 * entry.setMode(CpioConstants.C_ISREG); // regular file
    053 * ... set other attributes, e.g. time, number of links
    054 * out.putArchiveEntry(entry);
    055 * out.write(testContents.getBytes());
    056 * out.close();
    057 * </pre>
    058 *
    059 * <p>
    060 * Note: This implementation should be compatible to cpio 2.5
    061 * </p>
    062 *
    063 * <p>
    064 * This class uses mutable fields and is not considered threadsafe.
    065 * </p>
    066 *
    067 * <p>
    068 * based on code from the jRPM project (jrpm.sourceforge.net)
    069 * </p>
    070 */
    071public class CpioArchiveOutputStream extends ArchiveOutputStream<CpioArchiveEntry> implements CpioConstants {
    072
    073    private CpioArchiveEntry entry;
    074
    075    private boolean closed;
    076
    077    /** Indicates if this archive is finished */
    078    private boolean finished;
    079
    080    /**
    081     * See {@link CpioArchiveEntry#CpioArchiveEntry(short)} for possible values.
    082     */
    083    private final short entryFormat;
    084
    085    private final HashMap<String, CpioArchiveEntry> names = new HashMap<>();
    086
    087    private long crc;
    088
    089    private long written;
    090
    091    private final OutputStream out;
    092
    093    private final int blockSize;
    094
    095    private long nextArtificalDeviceAndInode = 1;
    096
    097    /**
    098     * The encoding to use for file names and labels.
    099     */
    100    private final ZipEncoding zipEncoding;
    101
    102    // the provided encoding (for unit tests)
    103    final String charsetName;
    104
    105    /**
    106     * Constructs the cpio output stream. The format for this CPIO stream is the "new" format using ASCII encoding for file names
    107     *
    108     * @param out The cpio stream
    109     */
    110    public CpioArchiveOutputStream(final OutputStream out) {
    111        this(out, FORMAT_NEW);
    112    }
    113
    114    /**
    115     * Constructs the cpio output stream with a specified format, a blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE} and using ASCII as the file name
    116     * encoding.
    117     *
    118     * @param out    The cpio stream
    119     * @param format The format of the stream
    120     */
    121    public CpioArchiveOutputStream(final OutputStream out, final short format) {
    122        this(out, format, BLOCK_SIZE, CharsetNames.US_ASCII);
    123    }
    124
    125    /**
    126     * Constructs the cpio output stream with a specified format using ASCII as the file name encoding.
    127     *
    128     * @param out       The cpio stream
    129     * @param format    The format of the stream
    130     * @param blockSize The block size of the archive.
    131     *
    132     * @since 1.1
    133     */
    134    public CpioArchiveOutputStream(final OutputStream out, final short format, final int blockSize) {
    135        this(out, format, blockSize, CharsetNames.US_ASCII);
    136    }
    137
    138    /**
    139     * Constructs the cpio output stream with a specified format using ASCII as the file name encoding.
    140     *
    141     * @param out       The cpio stream
    142     * @param format    The format of the stream
    143     * @param blockSize The block size of the archive.
    144     * @param encoding  The encoding of file names to write - use null for the platform's default.
    145     *
    146     * @since 1.6
    147     */
    148    public CpioArchiveOutputStream(final OutputStream out, final short format, final int blockSize, final String encoding) {
    149        this.out = out;
    150        switch (format) {
    151        case FORMAT_NEW:
    152        case FORMAT_NEW_CRC:
    153        case FORMAT_OLD_ASCII:
    154        case FORMAT_OLD_BINARY:
    155            break;
    156        default:
    157            throw new IllegalArgumentException("Unknown format: " + format);
    158
    159        }
    160        this.entryFormat = format;
    161        this.blockSize = blockSize;
    162        this.charsetName = encoding;
    163        this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
    164    }
    165
    166    /**
    167     * Constructs the cpio output stream. The format for this CPIO stream is the "new" format.
    168     *
    169     * @param out      The cpio stream
    170     * @param encoding The encoding of file names to write - use null for the platform's default.
    171     * @since 1.6
    172     */
    173    public CpioArchiveOutputStream(final OutputStream out, final String encoding) {
    174        this(out, FORMAT_NEW, BLOCK_SIZE, encoding);
    175    }
    176
    177    /**
    178     * Closes the CPIO output stream as well as the stream being filtered.
    179     *
    180     * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
    181     */
    182    @Override
    183    public void close() throws IOException {
    184        try {
    185            if (!finished) {
    186                finish();
    187            }
    188        } finally {
    189            if (!this.closed) {
    190                out.close();
    191                this.closed = true;
    192            }
    193        }
    194    }
    195
    196    /*
    197     * (non-Javadoc)
    198     *
    199     * @see org.apache.commons.compress.archivers.ArchiveOutputStream#closeArchiveEntry ()
    200     */
    201    @Override
    202    public void closeArchiveEntry() throws IOException {
    203        if (finished) {
    204            throw new IOException("Stream has already been finished");
    205        }
    206
    207        ensureOpen();
    208
    209        if (entry == null) {
    210            throw new IOException("Trying to close non-existent entry");
    211        }
    212
    213        if (this.entry.getSize() != this.written) {
    214            throw new IOException("Invalid entry size (expected " + this.entry.getSize() + " but got " + this.written + " bytes)");
    215        }
    216        pad(this.entry.getDataPadCount());
    217        if (this.entry.getFormat() == FORMAT_NEW_CRC && this.crc != this.entry.getChksum()) {
    218            throw new IOException("CRC Error");
    219        }
    220        this.entry = null;
    221        this.crc = 0;
    222        this.written = 0;
    223    }
    224
    225    /**
    226     * Creates a new CpioArchiveEntry. The entryName must be an ASCII encoded string.
    227     *
    228     * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, String)
    229     */
    230    @Override
    231    public CpioArchiveEntry createArchiveEntry(final File inputFile, final String entryName) throws IOException {
    232        if (finished) {
    233            throw new IOException("Stream has already been finished");
    234        }
    235        return new CpioArchiveEntry(inputFile, entryName);
    236    }
    237
    238    /**
    239     * Creates a new CpioArchiveEntry. The entryName must be an ASCII encoded string.
    240     *
    241     * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, String)
    242     */
    243    @Override
    244    public CpioArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
    245        if (finished) {
    246            throw new IOException("Stream has already been finished");
    247        }
    248        return new CpioArchiveEntry(inputPath, entryName, options);
    249    }
    250
    251    /**
    252     * Encodes the given string using the configured encoding.
    253     *
    254     * @param str the String to write
    255     * @throws IOException if the string couldn't be written
    256     * @return result of encoding the string
    257     */
    258    private byte[] encode(final String str) throws IOException {
    259        final ByteBuffer buf = zipEncoding.encode(str);
    260        final int len = buf.limit() - buf.position();
    261        return Arrays.copyOfRange(buf.array(), buf.arrayOffset(), buf.arrayOffset() + len);
    262    }
    263
    264    /**
    265     * Check to make sure that this stream has not been closed
    266     *
    267     * @throws IOException if the stream is already closed
    268     */
    269    private void ensureOpen() throws IOException {
    270        if (this.closed) {
    271            throw new IOException("Stream closed");
    272        }
    273    }
    274
    275    /**
    276     * Finishes writing the contents of the CPIO output stream without closing the underlying stream. Use this method when applying multiple filters in
    277     * succession to the same output stream.
    278     *
    279     * @throws IOException if an I/O exception has occurred or if a CPIO file error has occurred
    280     */
    281    @Override
    282    public void finish() throws IOException {
    283        ensureOpen();
    284        if (finished) {
    285            throw new IOException("This archive has already been finished");
    286        }
    287
    288        if (this.entry != null) {
    289            throw new IOException("This archive contains unclosed entries.");
    290        }
    291        this.entry = new CpioArchiveEntry(this.entryFormat);
    292        this.entry.setName(CPIO_TRAILER);
    293        this.entry.setNumberOfLinks(1);
    294        writeHeader(this.entry);
    295        closeArchiveEntry();
    296
    297        final int lengthOfLastBlock = (int) (getBytesWritten() % blockSize);
    298        if (lengthOfLastBlock != 0) {
    299            pad(blockSize - lengthOfLastBlock);
    300        }
    301
    302        finished = true;
    303    }
    304
    305    private void pad(final int count) throws IOException {
    306        if (count > 0) {
    307            final byte[] buff = new byte[count];
    308            out.write(buff);
    309            count(count);
    310        }
    311    }
    312
    313    /**
    314     * Begins writing a new CPIO file entry and positions the stream to the start of the entry data. Closes the current entry if still active. The current time
    315     * will be used if the entry has no set modification time and the default header format will be used if no other format is specified in the entry.
    316     *
    317     * @param entry the CPIO cpioEntry to be written
    318     * @throws IOException        if an I/O error has occurred or if a CPIO file error has occurred
    319     * @throws ClassCastException if entry is not an instance of CpioArchiveEntry
    320     */
    321    @Override
    322    public void putArchiveEntry(final CpioArchiveEntry entry) throws IOException {
    323        if (finished) {
    324            throw new IOException("Stream has already been finished");
    325        }
    326
    327        ensureOpen();
    328        if (this.entry != null) {
    329            closeArchiveEntry(); // close previous entry
    330        }
    331        if (entry.getTime() == -1) {
    332            entry.setTime(System.currentTimeMillis() / 1000);
    333        }
    334
    335        final short format = entry.getFormat();
    336        if (format != this.entryFormat) {
    337            throw new IOException("Header format: " + format + " does not match existing format: " + this.entryFormat);
    338        }
    339
    340        if (this.names.put(entry.getName(), entry) != null) {
    341            throw new IOException("Duplicate entry: " + entry.getName());
    342        }
    343
    344        writeHeader(entry);
    345        this.entry = entry;
    346        this.written = 0;
    347    }
    348
    349    /**
    350     * Writes an array of bytes to the current CPIO entry data. This method will block until all the bytes are written.
    351     *
    352     * @param b   the data to be written
    353     * @param off the start offset in the data
    354     * @param len the number of bytes that are written
    355     * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
    356     */
    357    @Override
    358    public void write(final byte[] b, final int off, final int len) throws IOException {
    359        ensureOpen();
    360        if (off < 0 || len < 0 || off > b.length - len) {
    361            throw new IndexOutOfBoundsException();
    362        }
    363        if (len == 0) {
    364            return;
    365        }
    366
    367        if (this.entry == null) {
    368            throw new IOException("No current CPIO entry");
    369        }
    370        if (this.written + len > this.entry.getSize()) {
    371            throw new IOException("Attempt to write past end of STORED entry");
    372        }
    373        out.write(b, off, len);
    374        this.written += len;
    375        if (this.entry.getFormat() == FORMAT_NEW_CRC) {
    376            for (int pos = 0; pos < len; pos++) {
    377                this.crc += b[pos] & 0xFF;
    378                this.crc &= 0xFFFFFFFFL;
    379            }
    380        }
    381        count(len);
    382    }
    383
    384    private void writeAsciiLong(final long number, final int length, final int radix) throws IOException {
    385        final StringBuilder tmp = new StringBuilder();
    386        final String tmpStr;
    387        if (radix == 16) {
    388            tmp.append(Long.toHexString(number));
    389        } else if (radix == 8) {
    390            tmp.append(Long.toOctalString(number));
    391        } else {
    392            tmp.append(number);
    393        }
    394
    395        if (tmp.length() <= length) {
    396            final int insertLength = length - tmp.length();
    397            for (int pos = 0; pos < insertLength; pos++) {
    398                tmp.insert(0, "0");
    399            }
    400            tmpStr = tmp.toString();
    401        } else {
    402            tmpStr = tmp.substring(tmp.length() - length);
    403        }
    404        final byte[] b = ArchiveUtils.toAsciiBytes(tmpStr);
    405        out.write(b);
    406        count(b.length);
    407    }
    408
    409    private void writeBinaryLong(final long number, final int length, final boolean swapHalfWord) throws IOException {
    410        final byte[] tmp = CpioUtil.long2byteArray(number, length, swapHalfWord);
    411        out.write(tmp);
    412        count(tmp.length);
    413    }
    414
    415    /**
    416     * Writes an encoded string to the stream followed by \0
    417     *
    418     * @param str the String to write
    419     * @throws IOException if the string couldn't be written
    420     */
    421    private void writeCString(final byte[] str) throws IOException {
    422        out.write(str);
    423        out.write('\0');
    424        count(str.length + 1);
    425    }
    426
    427    private void writeHeader(final CpioArchiveEntry e) throws IOException {
    428        switch (e.getFormat()) {
    429        case FORMAT_NEW:
    430            out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW));
    431            count(6);
    432            writeNewEntry(e);
    433            break;
    434        case FORMAT_NEW_CRC:
    435            out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW_CRC));
    436            count(6);
    437            writeNewEntry(e);
    438            break;
    439        case FORMAT_OLD_ASCII:
    440            out.write(ArchiveUtils.toAsciiBytes(MAGIC_OLD_ASCII));
    441            count(6);
    442            writeOldAsciiEntry(e);
    443            break;
    444        case FORMAT_OLD_BINARY:
    445            final boolean swapHalfWord = true;
    446            writeBinaryLong(MAGIC_OLD_BINARY, 2, swapHalfWord);
    447            writeOldBinaryEntry(e, swapHalfWord);
    448            break;
    449        default:
    450            throw new IOException("Unknown format " + e.getFormat());
    451        }
    452    }
    453
    454    private void writeNewEntry(final CpioArchiveEntry entry) throws IOException {
    455        long inode = entry.getInode();
    456        long devMin = entry.getDeviceMin();
    457        if (CPIO_TRAILER.equals(entry.getName())) {
    458            inode = devMin = 0;
    459        } else if (inode == 0 && devMin == 0) {
    460            inode = nextArtificalDeviceAndInode & 0xFFFFFFFF;
    461            devMin = nextArtificalDeviceAndInode++ >> 32 & 0xFFFFFFFF;
    462        } else {
    463            nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 0x100000000L * devMin) + 1;
    464        }
    465
    466        writeAsciiLong(inode, 8, 16);
    467        writeAsciiLong(entry.getMode(), 8, 16);
    468        writeAsciiLong(entry.getUID(), 8, 16);
    469        writeAsciiLong(entry.getGID(), 8, 16);
    470        writeAsciiLong(entry.getNumberOfLinks(), 8, 16);
    471        writeAsciiLong(entry.getTime(), 8, 16);
    472        writeAsciiLong(entry.getSize(), 8, 16);
    473        writeAsciiLong(entry.getDeviceMaj(), 8, 16);
    474        writeAsciiLong(devMin, 8, 16);
    475        writeAsciiLong(entry.getRemoteDeviceMaj(), 8, 16);
    476        writeAsciiLong(entry.getRemoteDeviceMin(), 8, 16);
    477        final byte[] name = encode(entry.getName());
    478        writeAsciiLong(name.length + 1L, 8, 16);
    479        writeAsciiLong(entry.getChksum(), 8, 16);
    480        writeCString(name);
    481        pad(entry.getHeaderPadCount(name.length));
    482    }
    483
    484    private void writeOldAsciiEntry(final CpioArchiveEntry entry) throws IOException {
    485        long inode = entry.getInode();
    486        long device = entry.getDevice();
    487        if (CPIO_TRAILER.equals(entry.getName())) {
    488            inode = device = 0;
    489        } else if (inode == 0 && device == 0) {
    490            inode = nextArtificalDeviceAndInode & 0777777;
    491            device = nextArtificalDeviceAndInode++ >> 18 & 0777777;
    492        } else {
    493            nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 01000000 * device) + 1;
    494        }
    495
    496        writeAsciiLong(device, 6, 8);
    497        writeAsciiLong(inode, 6, 8);
    498        writeAsciiLong(entry.getMode(), 6, 8);
    499        writeAsciiLong(entry.getUID(), 6, 8);
    500        writeAsciiLong(entry.getGID(), 6, 8);
    501        writeAsciiLong(entry.getNumberOfLinks(), 6, 8);
    502        writeAsciiLong(entry.getRemoteDevice(), 6, 8);
    503        writeAsciiLong(entry.getTime(), 11, 8);
    504        final byte[] name = encode(entry.getName());
    505        writeAsciiLong(name.length + 1L, 6, 8);
    506        writeAsciiLong(entry.getSize(), 11, 8);
    507        writeCString(name);
    508    }
    509
    510    private void writeOldBinaryEntry(final CpioArchiveEntry entry, final boolean swapHalfWord) throws IOException {
    511        long inode = entry.getInode();
    512        long device = entry.getDevice();
    513        if (CPIO_TRAILER.equals(entry.getName())) {
    514            inode = device = 0;
    515        } else if (inode == 0 && device == 0) {
    516            inode = nextArtificalDeviceAndInode & 0xFFFF;
    517            device = nextArtificalDeviceAndInode++ >> 16 & 0xFFFF;
    518        } else {
    519            nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 0x10000 * device) + 1;
    520        }
    521
    522        writeBinaryLong(device, 2, swapHalfWord);
    523        writeBinaryLong(inode, 2, swapHalfWord);
    524        writeBinaryLong(entry.getMode(), 2, swapHalfWord);
    525        writeBinaryLong(entry.getUID(), 2, swapHalfWord);
    526        writeBinaryLong(entry.getGID(), 2, swapHalfWord);
    527        writeBinaryLong(entry.getNumberOfLinks(), 2, swapHalfWord);
    528        writeBinaryLong(entry.getRemoteDevice(), 2, swapHalfWord);
    529        writeBinaryLong(entry.getTime(), 4, swapHalfWord);
    530        final byte[] name = encode(entry.getName());
    531        writeBinaryLong(name.length + 1L, 2, swapHalfWord);
    532        writeBinaryLong(entry.getSize(), 4, swapHalfWord);
    533        writeCString(name);
    534        pad(entry.getHeaderPadCount(name.length));
    535    }
    536
    537}