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