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