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 }