CpioArchiveOutputStream.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one
  3.  * or more contributor license agreements.  See the NOTICE file
  4.  * distributed with this work for additional information
  5.  * regarding copyright ownership.  The ASF licenses this file
  6.  * to you under the Apache License, Version 2.0 (the
  7.  * "License"); you may not use this file except in compliance
  8.  * with the License.  You may obtain a copy of the License at
  9.  *
  10.  * http://www.apache.org/licenses/LICENSE-2.0
  11.  *
  12.  * Unless required by applicable law or agreed to in writing,
  13.  * software distributed under the License is distributed on an
  14.  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15.  * KIND, either express or implied.  See the License for the
  16.  * specific language governing permissions and limitations
  17.  * under the License.
  18.  */
  19. package org.apache.commons.compress.archivers.cpio;

  20. import java.io.File;
  21. import java.io.IOException;
  22. import java.io.OutputStream;
  23. import java.nio.ByteBuffer;
  24. import java.nio.file.LinkOption;
  25. import java.nio.file.Path;
  26. import java.util.Arrays;
  27. import java.util.HashMap;

  28. import org.apache.commons.compress.archivers.ArchiveOutputStream;
  29. import org.apache.commons.compress.archivers.zip.ZipEncoding;
  30. import org.apache.commons.compress.archivers.zip.ZipEncodingHelper;
  31. import org.apache.commons.compress.utils.ArchiveUtils;

  32. /**
  33.  * CpioArchiveOutputStream is a stream for writing CPIO streams. All formats of CPIO are supported (old ASCII, old binary, new portable format and the new
  34.  * portable format with CRC).
  35.  *
  36.  * <p>
  37.  * 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
  38.  * 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.
  39.  * </p>
  40.  *
  41.  * <pre>
  42.  * CpioArchiveOutputStream out = new CpioArchiveOutputStream(
  43.  *         new FileOutputStream(new File("test.cpio")));
  44.  * CpioArchiveEntry entry = new CpioArchiveEntry();
  45.  * entry.setName("testfile");
  46.  * String contents = &quot;12345&quot;;
  47.  * entry.setFileSize(contents.length());
  48.  * entry.setMode(CpioConstants.C_ISREG); // regular file
  49.  * ... set other attributes, e.g. time, number of links
  50.  * out.putArchiveEntry(entry);
  51.  * out.write(testContents.getBytes());
  52.  * out.close();
  53.  * </pre>
  54.  *
  55.  * <p>
  56.  * Note: This implementation should be compatible to cpio 2.5
  57.  * </p>
  58.  *
  59.  * <p>
  60.  * This class uses mutable fields and is not considered threadsafe.
  61.  * </p>
  62.  *
  63.  * <p>
  64.  * based on code from the jRPM project (jrpm.sourceforge.net)
  65.  * </p>
  66.  */
  67. public class CpioArchiveOutputStream extends ArchiveOutputStream<CpioArchiveEntry> implements CpioConstants {

  68.     private CpioArchiveEntry entry;

  69.     /**
  70.      * See {@link CpioArchiveEntry#CpioArchiveEntry(short)} for possible values.
  71.      */
  72.     private final short entryFormat;

  73.     private final HashMap<String, CpioArchiveEntry> names = new HashMap<>();

  74.     private long crc;

  75.     private long written;

  76.     private final int blockSize;

  77.     private long nextArtificalDeviceAndInode = 1;

  78.     /**
  79.      * The encoding to use for file names and labels.
  80.      */
  81.     private final ZipEncoding zipEncoding;

  82.     // the provided encoding (for unit tests)
  83.     final String charsetName;

  84.     /**
  85.      * Constructs the cpio output stream. The format for this CPIO stream is the "new" format using ASCII encoding for file names
  86.      *
  87.      * @param out The cpio stream
  88.      */
  89.     public CpioArchiveOutputStream(final OutputStream out) {
  90.         this(out, FORMAT_NEW);
  91.     }

  92.     /**
  93.      * 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
  94.      * encoding.
  95.      *
  96.      * @param out    The cpio stream
  97.      * @param format The format of the stream
  98.      */
  99.     public CpioArchiveOutputStream(final OutputStream out, final short format) {
  100.         this(out, format, BLOCK_SIZE, CpioUtil.DEFAULT_CHARSET_NAME);
  101.     }

  102.     /**
  103.      * Constructs the cpio output stream with a specified format using ASCII as the file name encoding.
  104.      *
  105.      * @param out       The cpio stream
  106.      * @param format    The format of the stream
  107.      * @param blockSize The block size of the archive.
  108.      *
  109.      * @since 1.1
  110.      */
  111.     public CpioArchiveOutputStream(final OutputStream out, final short format, final int blockSize) {
  112.         this(out, format, blockSize, CpioUtil.DEFAULT_CHARSET_NAME);
  113.     }

  114.     /**
  115.      * Constructs the cpio output stream with a specified format using ASCII as the file name encoding.
  116.      *
  117.      * @param out       The cpio stream
  118.      * @param format    The format of the stream
  119.      * @param blockSize The block size of the archive.
  120.      * @param encoding  The encoding of file names to write - use null for the platform's default.
  121.      *
  122.      * @since 1.6
  123.      */
  124.     public CpioArchiveOutputStream(final OutputStream out, final short format, final int blockSize, final String encoding) {
  125.         super(out);
  126.         switch (format) {
  127.         case FORMAT_NEW:
  128.         case FORMAT_NEW_CRC:
  129.         case FORMAT_OLD_ASCII:
  130.         case FORMAT_OLD_BINARY:
  131.             break;
  132.         default:
  133.             throw new IllegalArgumentException("Unknown format: " + format);

  134.         }
  135.         this.entryFormat = format;
  136.         this.blockSize = blockSize;
  137.         this.charsetName = encoding;
  138.         this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
  139.     }

  140.     /**
  141.      * Constructs the cpio output stream. The format for this CPIO stream is the "new" format.
  142.      *
  143.      * @param out      The cpio stream
  144.      * @param encoding The encoding of file names to write - use null for the platform's default.
  145.      * @since 1.6
  146.      */
  147.     public CpioArchiveOutputStream(final OutputStream out, final String encoding) {
  148.         this(out, FORMAT_NEW, BLOCK_SIZE, encoding);
  149.     }

  150.     /**
  151.      * Closes the CPIO output stream as well as the stream being filtered.
  152.      *
  153.      * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
  154.      */
  155.     @Override
  156.     public void close() throws IOException {
  157.         try {
  158.             if (!isFinished()) {
  159.                 finish();
  160.             }
  161.         } finally {
  162.             if (!isClosed()) {
  163.                 super.close();
  164.             }
  165.         }
  166.     }

  167.     /*
  168.      * (non-Javadoc)
  169.      *
  170.      * @see org.apache.commons.compress.archivers.ArchiveOutputStream#closeArchiveEntry ()
  171.      */
  172.     @Override
  173.     public void closeArchiveEntry() throws IOException {
  174.         checkFinished();
  175.         checkOpen();
  176.         if (entry == null) {
  177.             throw new IOException("Trying to close non-existent entry");
  178.         }

  179.         if (this.entry.getSize() != this.written) {
  180.             throw new IOException("Invalid entry size (expected " + this.entry.getSize() + " but got " + this.written + " bytes)");
  181.         }
  182.         pad(this.entry.getDataPadCount());
  183.         if (this.entry.getFormat() == FORMAT_NEW_CRC && this.crc != this.entry.getChksum()) {
  184.             throw new IOException("CRC Error");
  185.         }
  186.         this.entry = null;
  187.         this.crc = 0;
  188.         this.written = 0;
  189.     }

  190.     /**
  191.      * Creates a new CpioArchiveEntry. The entryName must be an ASCII encoded string.
  192.      *
  193.      * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, String)
  194.      */
  195.     @Override
  196.     public CpioArchiveEntry createArchiveEntry(final File inputFile, final String entryName) throws IOException {
  197.         checkFinished();
  198.         return new CpioArchiveEntry(inputFile, entryName);
  199.     }

  200.     /**
  201.      * Creates a new CpioArchiveEntry. The entryName must be an ASCII encoded string.
  202.      *
  203.      * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, String)
  204.      */
  205.     @Override
  206.     public CpioArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
  207.         checkFinished();
  208.         return new CpioArchiveEntry(inputPath, entryName, options);
  209.     }

  210.     /**
  211.      * Encodes the given string using the configured encoding.
  212.      *
  213.      * @param str the String to write
  214.      * @throws IOException if the string couldn't be written
  215.      * @return result of encoding the string
  216.      */
  217.     private byte[] encode(final String str) throws IOException {
  218.         final ByteBuffer buf = zipEncoding.encode(str);
  219.         final int len = buf.limit() - buf.position();
  220.         return Arrays.copyOfRange(buf.array(), buf.arrayOffset(), buf.arrayOffset() + len);
  221.     }

  222.     /**
  223.      * Finishes writing the contents of the CPIO output stream without closing the underlying stream. Use this method when applying multiple filters in
  224.      * succession to the same output stream.
  225.      *
  226.      * @throws IOException if an I/O exception has occurred or if a CPIO file error has occurred
  227.      */
  228.     @Override
  229.     public void finish() throws IOException {
  230.         checkOpen();
  231.         checkFinished();

  232.         if (this.entry != null) {
  233.             throw new IOException("This archive contains unclosed entries.");
  234.         }
  235.         this.entry = new CpioArchiveEntry(this.entryFormat);
  236.         this.entry.setName(CPIO_TRAILER);
  237.         this.entry.setNumberOfLinks(1);
  238.         writeHeader(this.entry);
  239.         closeArchiveEntry();

  240.         final int lengthOfLastBlock = (int) (getBytesWritten() % blockSize);
  241.         if (lengthOfLastBlock != 0) {
  242.             pad(blockSize - lengthOfLastBlock);
  243.         }
  244.         super.finish();
  245.     }

  246.     private void pad(final int count) throws IOException {
  247.         if (count > 0) {
  248.             final byte[] buff = new byte[count];
  249.             out.write(buff);
  250.             count(count);
  251.         }
  252.     }

  253.     /**
  254.      * 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
  255.      * 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.
  256.      *
  257.      * @param entry the CPIO cpioEntry to be written
  258.      * @throws IOException        if an I/O error has occurred or if a CPIO file error has occurred
  259.      * @throws ClassCastException if entry is not an instance of CpioArchiveEntry
  260.      */
  261.     @Override
  262.     public void putArchiveEntry(final CpioArchiveEntry entry) throws IOException {
  263.         checkFinished();
  264.         checkOpen();
  265.         if (this.entry != null) {
  266.             closeArchiveEntry(); // close previous entry
  267.         }
  268.         if (entry.getTime() == -1) {
  269.             entry.setTime(System.currentTimeMillis() / 1000);
  270.         }

  271.         final short format = entry.getFormat();
  272.         if (format != this.entryFormat) {
  273.             throw new IOException("Header format: " + format + " does not match existing format: " + this.entryFormat);
  274.         }

  275.         if (this.names.put(entry.getName(), entry) != null) {
  276.             throw new IOException("Duplicate entry: " + entry.getName());
  277.         }

  278.         writeHeader(entry);
  279.         this.entry = entry;
  280.         this.written = 0;
  281.     }

  282.     /**
  283.      * Writes an array of bytes to the current CPIO entry data. This method will block until all the bytes are written.
  284.      *
  285.      * @param b   the data to be written
  286.      * @param off the start offset in the data
  287.      * @param len the number of bytes that are written
  288.      * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
  289.      */
  290.     @Override
  291.     public void write(final byte[] b, final int off, final int len) throws IOException {
  292.         checkOpen();
  293.         if (off < 0 || len < 0 || off > b.length - len) {
  294.             throw new IndexOutOfBoundsException();
  295.         }
  296.         if (len == 0) {
  297.             return;
  298.         }

  299.         if (this.entry == null) {
  300.             throw new IOException("No current CPIO entry");
  301.         }
  302.         if (this.written + len > this.entry.getSize()) {
  303.             throw new IOException("Attempt to write past end of STORED entry");
  304.         }
  305.         out.write(b, off, len);
  306.         this.written += len;
  307.         if (this.entry.getFormat() == FORMAT_NEW_CRC) {
  308.             for (int pos = 0; pos < len; pos++) {
  309.                 this.crc += b[pos] & 0xFF;
  310.                 this.crc &= 0xFFFFFFFFL;
  311.             }
  312.         }
  313.         count(len);
  314.     }

  315.     private void writeAsciiLong(final long number, final int length, final int radix) throws IOException {
  316.         final StringBuilder tmp = new StringBuilder();
  317.         final String tmpStr;
  318.         if (radix == 16) {
  319.             tmp.append(Long.toHexString(number));
  320.         } else if (radix == 8) {
  321.             tmp.append(Long.toOctalString(number));
  322.         } else {
  323.             tmp.append(number);
  324.         }

  325.         if (tmp.length() <= length) {
  326.             final int insertLength = length - tmp.length();
  327.             for (int pos = 0; pos < insertLength; pos++) {
  328.                 tmp.insert(0, "0");
  329.             }
  330.             tmpStr = tmp.toString();
  331.         } else {
  332.             tmpStr = tmp.substring(tmp.length() - length);
  333.         }
  334.         final byte[] b = ArchiveUtils.toAsciiBytes(tmpStr);
  335.         out.write(b);
  336.         count(b.length);
  337.     }

  338.     private void writeBinaryLong(final long number, final int length, final boolean swapHalfWord) throws IOException {
  339.         final byte[] tmp = CpioUtil.long2byteArray(number, length, swapHalfWord);
  340.         out.write(tmp);
  341.         count(tmp.length);
  342.     }

  343.     /**
  344.      * Writes an encoded string to the stream followed by \0
  345.      *
  346.      * @param str the String to write
  347.      * @throws IOException if the string couldn't be written
  348.      */
  349.     private void writeCString(final byte[] str) throws IOException {
  350.         out.write(str);
  351.         out.write('\0');
  352.         count(str.length + 1);
  353.     }

  354.     private void writeHeader(final CpioArchiveEntry e) throws IOException {
  355.         switch (e.getFormat()) {
  356.         case FORMAT_NEW:
  357.             out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW));
  358.             count(6);
  359.             writeNewEntry(e);
  360.             break;
  361.         case FORMAT_NEW_CRC:
  362.             out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW_CRC));
  363.             count(6);
  364.             writeNewEntry(e);
  365.             break;
  366.         case FORMAT_OLD_ASCII:
  367.             out.write(ArchiveUtils.toAsciiBytes(MAGIC_OLD_ASCII));
  368.             count(6);
  369.             writeOldAsciiEntry(e);
  370.             break;
  371.         case FORMAT_OLD_BINARY:
  372.             final boolean swapHalfWord = true;
  373.             writeBinaryLong(MAGIC_OLD_BINARY, 2, swapHalfWord);
  374.             writeOldBinaryEntry(e, swapHalfWord);
  375.             break;
  376.         default:
  377.             throw new IOException("Unknown format " + e.getFormat());
  378.         }
  379.     }

  380.     private void writeNewEntry(final CpioArchiveEntry entry) throws IOException {
  381.         long inode = entry.getInode();
  382.         long devMin = entry.getDeviceMin();
  383.         if (CPIO_TRAILER.equals(entry.getName())) {
  384.             inode = devMin = 0;
  385.         } else if (inode == 0 && devMin == 0) {
  386.             inode = nextArtificalDeviceAndInode & 0xFFFFFFFF;
  387.             devMin = nextArtificalDeviceAndInode++ >> 32 & 0xFFFFFFFF;
  388.         } else {
  389.             nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 0x100000000L * devMin) + 1;
  390.         }

  391.         writeAsciiLong(inode, 8, 16);
  392.         writeAsciiLong(entry.getMode(), 8, 16);
  393.         writeAsciiLong(entry.getUID(), 8, 16);
  394.         writeAsciiLong(entry.getGID(), 8, 16);
  395.         writeAsciiLong(entry.getNumberOfLinks(), 8, 16);
  396.         writeAsciiLong(entry.getTime(), 8, 16);
  397.         writeAsciiLong(entry.getSize(), 8, 16);
  398.         writeAsciiLong(entry.getDeviceMaj(), 8, 16);
  399.         writeAsciiLong(devMin, 8, 16);
  400.         writeAsciiLong(entry.getRemoteDeviceMaj(), 8, 16);
  401.         writeAsciiLong(entry.getRemoteDeviceMin(), 8, 16);
  402.         final byte[] name = encode(entry.getName());
  403.         writeAsciiLong(name.length + 1L, 8, 16);
  404.         writeAsciiLong(entry.getChksum(), 8, 16);
  405.         writeCString(name);
  406.         pad(entry.getHeaderPadCount(name.length));
  407.     }

  408.     private void writeOldAsciiEntry(final CpioArchiveEntry entry) throws IOException {
  409.         long inode = entry.getInode();
  410.         long device = entry.getDevice();
  411.         if (CPIO_TRAILER.equals(entry.getName())) {
  412.             inode = device = 0;
  413.         } else if (inode == 0 && device == 0) {
  414.             inode = nextArtificalDeviceAndInode & 0777777;
  415.             device = nextArtificalDeviceAndInode++ >> 18 & 0777777;
  416.         } else {
  417.             nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 01000000 * device) + 1;
  418.         }

  419.         writeAsciiLong(device, 6, 8);
  420.         writeAsciiLong(inode, 6, 8);
  421.         writeAsciiLong(entry.getMode(), 6, 8);
  422.         writeAsciiLong(entry.getUID(), 6, 8);
  423.         writeAsciiLong(entry.getGID(), 6, 8);
  424.         writeAsciiLong(entry.getNumberOfLinks(), 6, 8);
  425.         writeAsciiLong(entry.getRemoteDevice(), 6, 8);
  426.         writeAsciiLong(entry.getTime(), 11, 8);
  427.         final byte[] name = encode(entry.getName());
  428.         writeAsciiLong(name.length + 1L, 6, 8);
  429.         writeAsciiLong(entry.getSize(), 11, 8);
  430.         writeCString(name);
  431.     }

  432.     private void writeOldBinaryEntry(final CpioArchiveEntry entry, final boolean swapHalfWord) throws IOException {
  433.         long inode = entry.getInode();
  434.         long device = entry.getDevice();
  435.         if (CPIO_TRAILER.equals(entry.getName())) {
  436.             inode = device = 0;
  437.         } else if (inode == 0 && device == 0) {
  438.             inode = nextArtificalDeviceAndInode & 0xFFFF;
  439.             device = nextArtificalDeviceAndInode++ >> 16 & 0xFFFF;
  440.         } else {
  441.             nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 0x10000 * device) + 1;
  442.         }

  443.         writeBinaryLong(device, 2, swapHalfWord);
  444.         writeBinaryLong(inode, 2, swapHalfWord);
  445.         writeBinaryLong(entry.getMode(), 2, swapHalfWord);
  446.         writeBinaryLong(entry.getUID(), 2, swapHalfWord);
  447.         writeBinaryLong(entry.getGID(), 2, swapHalfWord);
  448.         writeBinaryLong(entry.getNumberOfLinks(), 2, swapHalfWord);
  449.         writeBinaryLong(entry.getRemoteDevice(), 2, swapHalfWord);
  450.         writeBinaryLong(entry.getTime(), 4, swapHalfWord);
  451.         final byte[] name = encode(entry.getName());
  452.         writeBinaryLong(name.length + 1L, 2, swapHalfWord);
  453.         writeBinaryLong(entry.getSize(), 4, swapHalfWord);
  454.         writeCString(name);
  455.         pad(entry.getHeaderPadCount(name.length));
  456.     }

  457. }