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 = "12345";
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 }