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