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