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