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 *   https://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;
033
034/**
035 * CpioArchiveOutputStream is a stream for writing CPIO streams. All formats of CPIO are supported (old ASCII, old binary, new portable format and the new
036 * portable format with CRC).
037 *
038 * <p>
039 * 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
040 * 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.
041 * </p>
042 *
043 * <pre>
044 * CpioArchiveOutputStream out = new CpioArchiveOutputStream(
045 *         new FileOutputStream(new File("test.cpio")));
046 * CpioArchiveEntry entry = new CpioArchiveEntry();
047 * entry.setName("testfile");
048 * String contents = &quot;12345&quot;;
049 * entry.setFileSize(contents.length());
050 * entry.setMode(CpioConstants.C_ISREG); // regular file
051 * ... set other attributes, for example time, number of links
052 * out.putArchiveEntry(entry);
053 * out.write(testContents.getBytes());
054 * out.close();
055 * </pre>
056 *
057 * <p>
058 * Note: This implementation should be compatible to cpio 2.5
059 * </p>
060 *
061 * <p>
062 * This class uses mutable fields and is not considered threadsafe.
063 * </p>
064 *
065 * <p>
066 * based on code from the jRPM project (jrpm.sourceforge.net)
067 * </p>
068 */
069public class CpioArchiveOutputStream extends ArchiveOutputStream<CpioArchiveEntry> implements CpioConstants {
070
071    /**
072     * The NUL character.
073     */
074    private static final char NUL = '\0';
075
076    private CpioArchiveEntry entry;
077
078    /**
079     * See {@link CpioArchiveEntry#CpioArchiveEntry(short)} for possible values.
080     */
081    private final short entryFormat;
082
083    private final HashMap<String, CpioArchiveEntry> names = new HashMap<>();
084
085    private long crc;
086
087    private long written;
088
089    private final int blockSize;
090
091    private long nextArtificalDeviceAndInode = 1;
092
093    /**
094     * The encoding to use for file names and labels.
095     */
096    private final ZipEncoding zipEncoding;
097
098    // the provided encoding (for unit tests)
099    final String charsetName;
100
101    /**
102     * Constructs the cpio output stream. The format for this CPIO stream is the "new" format using ASCII encoding for file names
103     *
104     * @param out The cpio stream
105     */
106    public CpioArchiveOutputStream(final OutputStream out) {
107        this(out, FORMAT_NEW);
108    }
109
110    /**
111     * 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
112     * encoding.
113     *
114     * @param out    The cpio stream
115     * @param format The format of the stream
116     */
117    public CpioArchiveOutputStream(final OutputStream out, final short format) {
118        this(out, format, BLOCK_SIZE, CpioUtil.DEFAULT_CHARSET_NAME);
119    }
120
121    /**
122     * Constructs the cpio output stream with a specified format using ASCII as the file name encoding.
123     *
124     * @param out       The cpio stream
125     * @param format    The format of the stream
126     * @param blockSize The block size of the archive.
127     * @since 1.1
128     */
129    public CpioArchiveOutputStream(final OutputStream out, final short format, final int blockSize) {
130        this(out, format, blockSize, CpioUtil.DEFAULT_CHARSET_NAME);
131    }
132
133    /**
134     * Constructs the cpio output stream with a specified format using ASCII as the file name encoding.
135     *
136     * @param out       The cpio stream
137     * @param format    The format of the stream
138     * @param blockSize The block size of the archive.
139     * @param encoding  The encoding of file names to write - use null for the platform's default.
140     * @since 1.6
141     */
142    public CpioArchiveOutputStream(final OutputStream out, final short format, final int blockSize, final String encoding) {
143        super(out);
144        switch (format) {
145        case FORMAT_NEW:
146        case FORMAT_NEW_CRC:
147        case FORMAT_OLD_ASCII:
148        case FORMAT_OLD_BINARY:
149            break;
150        default:
151            throw new IllegalArgumentException("Unknown format: " + format);
152
153        }
154        this.entryFormat = format;
155        this.blockSize = blockSize;
156        this.charsetName = encoding;
157        this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding);
158    }
159
160    /**
161     * Constructs the cpio output stream. The format for this CPIO stream is the "new" format.
162     *
163     * @param out      The cpio stream
164     * @param encoding The encoding of file names to write - use null for the platform's default.
165     * @since 1.6
166     */
167    public CpioArchiveOutputStream(final OutputStream out, final String encoding) {
168        this(out, FORMAT_NEW, BLOCK_SIZE, encoding);
169    }
170
171    /**
172     * Closes the CPIO output stream as well as the stream being filtered.
173     *
174     * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
175     */
176    @Override
177    public void close() throws IOException {
178        try {
179            if (!isFinished()) {
180                finish();
181            }
182        } finally {
183            super.close();
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            out.write(new byte[count]);
277            count(count);
278        }
279    }
280
281    /**
282     * 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
283     * 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.
284     *
285     * @param entry the CPIO cpioEntry to be written
286     * @throws IOException        if an I/O error has occurred or if a CPIO file error has occurred
287     * @throws ClassCastException if entry is not an instance of CpioArchiveEntry
288     */
289    @Override
290    public void putArchiveEntry(final CpioArchiveEntry entry) throws IOException {
291        checkFinished();
292        checkOpen();
293        if (this.entry != null) {
294            closeArchiveEntry(); // close previous entry
295        }
296        if (entry.getTime() == -1) {
297            entry.setTime(System.currentTimeMillis() / 1000);
298        }
299
300        final short format = entry.getFormat();
301        if (format != this.entryFormat) {
302            throw new IOException("Header format: " + format + " does not match existing format: " + this.entryFormat);
303        }
304
305        if (this.names.put(entry.getName(), entry) != null) {
306            throw new IOException("Duplicate entry: " + entry.getName());
307        }
308
309        writeHeader(entry);
310        this.entry = entry;
311        this.written = 0;
312    }
313
314    /**
315     * Writes an array of bytes to the current CPIO entry data. This method will block until all the bytes are written.
316     *
317     * @param b   the data to be written
318     * @param off the start offset in the data
319     * @param len the number of bytes that are written
320     * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred
321     */
322    @Override
323    public void write(final byte[] b, final int off, final int len) throws IOException {
324        checkOpen();
325        if (off < 0 || len < 0 || off > b.length - len) {
326            throw new IndexOutOfBoundsException();
327        }
328        if (len == 0) {
329            return;
330        }
331
332        if (this.entry == null) {
333            throw new IOException("No current CPIO entry");
334        }
335        if (this.written + len > this.entry.getSize()) {
336            throw new IOException("Attempt to write past end of STORED entry");
337        }
338        out.write(b, off, len);
339        this.written += len;
340        if (this.entry.getFormat() == FORMAT_NEW_CRC) {
341            for (int pos = 0; pos < len; pos++) {
342                this.crc += b[pos] & 0xFF;
343                this.crc &= 0xFFFFFFFFL;
344            }
345        }
346        count(len);
347    }
348
349    private void writeAsciiLong(final long number, final int length, final int radix) throws IOException {
350        final StringBuilder tmp = new StringBuilder();
351        final String tmpStr;
352        if (radix == 16) {
353            tmp.append(Long.toHexString(number));
354        } else if (radix == 8) {
355            tmp.append(Long.toOctalString(number));
356        } else {
357            tmp.append(number);
358        }
359
360        if (tmp.length() <= length) {
361            final int insertLength = length - tmp.length();
362            for (int pos = 0; pos < insertLength; pos++) {
363                tmp.insert(0, "0");
364            }
365            tmpStr = tmp.toString();
366        } else {
367            tmpStr = tmp.substring(tmp.length() - length);
368        }
369        final byte[] b = writeUsAsciiRaw(tmpStr);
370        count(b.length);
371    }
372
373    private void writeBinaryLong(final long number, final int length, final boolean swapHalfWord) throws IOException {
374        final byte[] tmp = CpioUtil.long2byteArray(number, length, swapHalfWord);
375        out.write(tmp);
376        count(tmp.length);
377    }
378
379    /**
380     * Writes an encoded string to the stream followed by \0
381     *
382     * @param str the String to write
383     * @throws IOException if the string couldn't be written
384     */
385    private void writeCString(final byte[] str) throws IOException {
386        out.write(str);
387        out.write(NUL);
388        count(str.length + 1);
389    }
390
391    private void writeHeader(final CpioArchiveEntry e) throws IOException {
392        switch (e.getFormat()) {
393        case FORMAT_NEW:
394            writeUsAsciiRaw(MAGIC_NEW);
395            count(6);
396            writeNewEntry(e);
397            break;
398        case FORMAT_NEW_CRC:
399            writeUsAsciiRaw(MAGIC_NEW_CRC);
400            count(6);
401            writeNewEntry(e);
402            break;
403        case FORMAT_OLD_ASCII:
404            writeUsAsciiRaw(MAGIC_OLD_ASCII);
405            count(6);
406            writeOldAsciiEntry(e);
407            break;
408        case FORMAT_OLD_BINARY:
409            final boolean swapHalfWord = true;
410            writeBinaryLong(MAGIC_OLD_BINARY, 2, swapHalfWord);
411            writeOldBinaryEntry(e, swapHalfWord);
412            break;
413        default:
414            throw new IOException("Unknown format " + e.getFormat());
415        }
416    }
417
418    private void writeNewEntry(final CpioArchiveEntry entry) throws IOException {
419        long inode = entry.getInode();
420        long devMin = entry.getDeviceMin();
421        if (CPIO_TRAILER.equals(entry.getName())) {
422            inode = devMin = 0;
423        } else if (inode == 0 && devMin == 0) {
424            inode = nextArtificalDeviceAndInode & 0xFFFFFFFF;
425            devMin = nextArtificalDeviceAndInode++ >> 32 & 0xFFFFFFFF;
426        } else {
427            nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 0x100000000L * devMin) + 1;
428        }
429
430        writeAsciiLong(inode, 8, 16);
431        writeAsciiLong(entry.getMode(), 8, 16);
432        writeAsciiLong(entry.getUID(), 8, 16);
433        writeAsciiLong(entry.getGID(), 8, 16);
434        writeAsciiLong(entry.getNumberOfLinks(), 8, 16);
435        writeAsciiLong(entry.getTime(), 8, 16);
436        writeAsciiLong(entry.getSize(), 8, 16);
437        writeAsciiLong(entry.getDeviceMaj(), 8, 16);
438        writeAsciiLong(devMin, 8, 16);
439        writeAsciiLong(entry.getRemoteDeviceMaj(), 8, 16);
440        writeAsciiLong(entry.getRemoteDeviceMin(), 8, 16);
441        final byte[] name = encode(entry.getName());
442        writeAsciiLong(name.length + 1L, 8, 16);
443        writeAsciiLong(entry.getChksum(), 8, 16);
444        writeCString(name);
445        pad(entry.getHeaderPadCount(name.length));
446    }
447
448    private void writeOldAsciiEntry(final CpioArchiveEntry entry) throws IOException {
449        long inode = entry.getInode();
450        long device = entry.getDevice();
451        if (CPIO_TRAILER.equals(entry.getName())) {
452            inode = device = 0;
453        } else if (inode == 0 && device == 0) {
454            inode = nextArtificalDeviceAndInode & 0777777;
455            device = nextArtificalDeviceAndInode++ >> 18 & 0777777;
456        } else {
457            nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 01000000 * device) + 1;
458        }
459
460        writeAsciiLong(device, 6, 8);
461        writeAsciiLong(inode, 6, 8);
462        writeAsciiLong(entry.getMode(), 6, 8);
463        writeAsciiLong(entry.getUID(), 6, 8);
464        writeAsciiLong(entry.getGID(), 6, 8);
465        writeAsciiLong(entry.getNumberOfLinks(), 6, 8);
466        writeAsciiLong(entry.getRemoteDevice(), 6, 8);
467        writeAsciiLong(entry.getTime(), 11, 8);
468        final byte[] name = encode(entry.getName());
469        writeAsciiLong(name.length + 1L, 6, 8);
470        writeAsciiLong(entry.getSize(), 11, 8);
471        writeCString(name);
472    }
473
474    private void writeOldBinaryEntry(final CpioArchiveEntry entry, final boolean swapHalfWord) throws IOException {
475        long inode = entry.getInode();
476        long device = entry.getDevice();
477        if (CPIO_TRAILER.equals(entry.getName())) {
478            inode = device = 0;
479        } else if (inode == 0 && device == 0) {
480            inode = nextArtificalDeviceAndInode & 0xFFFF;
481            device = nextArtificalDeviceAndInode++ >> 16 & 0xFFFF;
482        } else {
483            nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 0x10000 * device) + 1;
484        }
485
486        writeBinaryLong(device, 2, swapHalfWord);
487        writeBinaryLong(inode, 2, swapHalfWord);
488        writeBinaryLong(entry.getMode(), 2, swapHalfWord);
489        writeBinaryLong(entry.getUID(), 2, swapHalfWord);
490        writeBinaryLong(entry.getGID(), 2, swapHalfWord);
491        writeBinaryLong(entry.getNumberOfLinks(), 2, swapHalfWord);
492        writeBinaryLong(entry.getRemoteDevice(), 2, swapHalfWord);
493        writeBinaryLong(entry.getTime(), 4, swapHalfWord);
494        final byte[] name = encode(entry.getName());
495        writeBinaryLong(name.length + 1L, 2, swapHalfWord);
496        writeBinaryLong(entry.getSize(), 4, swapHalfWord);
497        writeCString(name);
498        pad(entry.getHeaderPadCount(name.length));
499    }
500
501}