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}