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