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