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<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 }