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