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