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