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 && this.crc != this.entry.getChksum()) { 339 throw new IOException("CRC Error"); 340 } 341 this.entry = null; 342 this.crc = 0; 343 this.written = 0; 344 } 345 346 /** 347 * Writes an array of bytes to the current CPIO entry data. This method will 348 * block until all the bytes are written. 349 * 350 * @param b 351 * the data to be written 352 * @param off 353 * the start offset in the data 354 * @param len 355 * the number of bytes that are written 356 * @throws IOException 357 * if an I/O error has occurred or if a CPIO file error has 358 * occurred 359 */ 360 public void write(final byte[] b, final int off, final int len) 361 throws IOException { 362 ensureOpen(); 363 if (off < 0 || len < 0 || off > b.length - len) { 364 throw new IndexOutOfBoundsException(); 365 } else if (len == 0) { 366 return; 367 } 368 369 if (this.entry == null) { 370 throw new IOException("no current CPIO entry"); 371 } 372 if (this.written + len > this.entry.getSize()) { 373 throw new IOException("attempt to write past end of STORED entry"); 374 } 375 out.write(b, off, len); 376 this.written += len; 377 if (this.entry.getFormat() == FORMAT_NEW_CRC) { 378 for (int pos = 0; pos < len; pos++) { 379 this.crc += b[pos] & 0xFF; 380 } 381 } 382 count(len); 383 } 384 385 /** 386 * Finishes writing the contents of the CPIO output stream without closing 387 * the underlying stream. Use this method when applying multiple filters in 388 * succession to the same output stream. 389 * 390 * @throws IOException 391 * if an I/O exception has occurred or if a CPIO file error has 392 * occurred 393 */ 394 public void finish() throws IOException { 395 ensureOpen(); 396 if (finished) { 397 throw new IOException("This archive has already been finished"); 398 } 399 400 if (this.entry != null) { 401 throw new IOException("This archive contains unclosed entries."); 402 } 403 this.entry = new CpioArchiveEntry(this.entryFormat); 404 this.entry.setName(CPIO_TRAILER); 405 this.entry.setNumberOfLinks(1); 406 writeHeader(this.entry); 407 closeArchiveEntry(); 408 409 int lengthOfLastBlock = (int) (getBytesWritten() % blockSize); 410 if (lengthOfLastBlock != 0) { 411 pad(blockSize - lengthOfLastBlock); 412 } 413 414 finished = true; 415 } 416 417 /** 418 * Closes the CPIO output stream as well as the stream being filtered. 419 * 420 * @throws IOException 421 * if an I/O error has occurred or if a CPIO file error has 422 * occurred 423 */ 424 public void close() throws IOException { 425 if(!finished) { 426 finish(); 427 } 428 429 if (!this.closed) { 430 out.close(); 431 this.closed = true; 432 } 433 } 434 435 private void pad(int count) throws IOException{ 436 if (count > 0){ 437 byte buff[] = new byte[count]; 438 out.write(buff); 439 count(count); 440 } 441 } 442 443 private void writeBinaryLong(final long number, final int length, 444 final boolean swapHalfWord) throws IOException { 445 byte tmp[] = CpioUtil.long2byteArray(number, length, swapHalfWord); 446 out.write(tmp); 447 count(tmp.length); 448 } 449 450 private void writeAsciiLong(final long number, final int length, 451 final int radix) throws IOException { 452 StringBuffer tmp = new StringBuffer(); 453 String tmpStr; 454 if (radix == 16) { 455 tmp.append(Long.toHexString(number)); 456 } else if (radix == 8) { 457 tmp.append(Long.toOctalString(number)); 458 } else { 459 tmp.append(Long.toString(number)); 460 } 461 462 if (tmp.length() <= length) { 463 long insertLength = length - tmp.length(); 464 for (int pos = 0; pos < insertLength; pos++) { 465 tmp.insert(0, "0"); 466 } 467 tmpStr = tmp.toString(); 468 } else { 469 tmpStr = tmp.substring(tmp.length() - length); 470 } 471 byte[] b = ArchiveUtils.toAsciiBytes(tmpStr); 472 out.write(b); 473 count(b.length); 474 } 475 476 /** 477 * Writes an ASCII string to the stream followed by \0 478 * @param str the String to write 479 * @throws IOException if the string couldn't be written 480 */ 481 private void writeCString(final String str) throws IOException { 482 byte[] b = ArchiveUtils.toAsciiBytes(str); 483 out.write(b); 484 out.write('\0'); 485 count(b.length + 1); 486 } 487 488 /** 489 * Creates a new ArchiveEntry. The entryName must be an ASCII encoded string. 490 * 491 * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, java.lang.String) 492 */ 493 public ArchiveEntry createArchiveEntry(File inputFile, String entryName) 494 throws IOException { 495 if(finished) { 496 throw new IOException("Stream has already been finished"); 497 } 498 return new CpioArchiveEntry(inputFile, entryName); 499 } 500 501 }