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}