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