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 */ 019package org.apache.commons.compress.archivers.cpio; 020 021import java.io.File; 022import java.io.IOException; 023import java.io.OutputStream; 024import java.nio.ByteBuffer; 025import java.nio.file.LinkOption; 026import java.nio.file.Path; 027import java.util.Arrays; 028import java.util.HashMap; 029 030import org.apache.commons.compress.archivers.ArchiveOutputStream; 031import org.apache.commons.compress.archivers.zip.ZipEncoding; 032import org.apache.commons.compress.archivers.zip.ZipEncodingHelper; 033import org.apache.commons.compress.utils.ArchiveUtils; 034import org.apache.commons.compress.utils.CharsetNames; 035 036/** 037 * CpioArchiveOutputStream is a stream for writing CPIO streams. All formats of CPIO are supported (old ASCII, old binary, new portable format and the new 038 * portable format with CRC). 039 * 040 * <p> 041 * An entry can be written by creating an instance of CpioArchiveEntry and fill it with the necessary values and put it into the CPIO stream. Afterwards write 042 * the contents of the file into the CPIO stream. Either close the stream by calling finish() or put a next entry into the cpio stream. 043 * </p> 044 * 045 * <pre> 046 * CpioArchiveOutputStream out = new CpioArchiveOutputStream( 047 * new FileOutputStream(new File("test.cpio"))); 048 * CpioArchiveEntry entry = new CpioArchiveEntry(); 049 * entry.setName("testfile"); 050 * String contents = "12345"; 051 * entry.setFileSize(contents.length()); 052 * entry.setMode(CpioConstants.C_ISREG); // regular file 053 * ... set other attributes, e.g. time, number of links 054 * out.putArchiveEntry(entry); 055 * out.write(testContents.getBytes()); 056 * out.close(); 057 * </pre> 058 * 059 * <p> 060 * Note: This implementation should be compatible to cpio 2.5 061 * </p> 062 * 063 * <p> 064 * This class uses mutable fields and is not considered threadsafe. 065 * </p> 066 * 067 * <p> 068 * based on code from the jRPM project (jrpm.sourceforge.net) 069 * </p> 070 */ 071public class CpioArchiveOutputStream extends ArchiveOutputStream<CpioArchiveEntry> implements CpioConstants { 072 073 private CpioArchiveEntry entry; 074 075 private boolean closed; 076 077 /** Indicates if this archive is finished */ 078 private boolean finished; 079 080 /** 081 * See {@link CpioArchiveEntry#CpioArchiveEntry(short)} for possible values. 082 */ 083 private final short entryFormat; 084 085 private final HashMap<String, CpioArchiveEntry> names = new HashMap<>(); 086 087 private long crc; 088 089 private long written; 090 091 private final OutputStream out; 092 093 private final int blockSize; 094 095 private long nextArtificalDeviceAndInode = 1; 096 097 /** 098 * The encoding to use for file names and labels. 099 */ 100 private final ZipEncoding zipEncoding; 101 102 // the provided encoding (for unit tests) 103 final String charsetName; 104 105 /** 106 * Constructs the cpio output stream. The format for this CPIO stream is the "new" format using ASCII encoding for file names 107 * 108 * @param out The cpio stream 109 */ 110 public CpioArchiveOutputStream(final OutputStream out) { 111 this(out, FORMAT_NEW); 112 } 113 114 /** 115 * Constructs the cpio output stream with a specified format, a blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE} and using ASCII as the file name 116 * encoding. 117 * 118 * @param out The cpio stream 119 * @param format The format of the stream 120 */ 121 public CpioArchiveOutputStream(final OutputStream out, final short format) { 122 this(out, format, BLOCK_SIZE, CharsetNames.US_ASCII); 123 } 124 125 /** 126 * Constructs the cpio output stream with a specified format using ASCII as the file name encoding. 127 * 128 * @param out The cpio stream 129 * @param format The format of the stream 130 * @param blockSize The block size of the archive. 131 * 132 * @since 1.1 133 */ 134 public CpioArchiveOutputStream(final OutputStream out, final short format, final int blockSize) { 135 this(out, format, blockSize, CharsetNames.US_ASCII); 136 } 137 138 /** 139 * Constructs the cpio output stream with a specified format using ASCII as the file name encoding. 140 * 141 * @param out The cpio stream 142 * @param format The format of the stream 143 * @param blockSize The block size of the archive. 144 * @param encoding The encoding of file names to write - use null for the platform's default. 145 * 146 * @since 1.6 147 */ 148 public CpioArchiveOutputStream(final OutputStream out, final short format, final int blockSize, final String encoding) { 149 this.out = out; 150 switch (format) { 151 case FORMAT_NEW: 152 case FORMAT_NEW_CRC: 153 case FORMAT_OLD_ASCII: 154 case FORMAT_OLD_BINARY: 155 break; 156 default: 157 throw new IllegalArgumentException("Unknown format: " + format); 158 159 } 160 this.entryFormat = format; 161 this.blockSize = blockSize; 162 this.charsetName = encoding; 163 this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); 164 } 165 166 /** 167 * Constructs the cpio output stream. The format for this CPIO stream is the "new" format. 168 * 169 * @param out The cpio stream 170 * @param encoding The encoding of file names to write - use null for the platform's default. 171 * @since 1.6 172 */ 173 public CpioArchiveOutputStream(final OutputStream out, final String encoding) { 174 this(out, FORMAT_NEW, BLOCK_SIZE, encoding); 175 } 176 177 /** 178 * Closes the CPIO output stream as well as the stream being filtered. 179 * 180 * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred 181 */ 182 @Override 183 public void close() throws IOException { 184 try { 185 if (!finished) { 186 finish(); 187 } 188 } finally { 189 if (!this.closed) { 190 out.close(); 191 this.closed = true; 192 } 193 } 194 } 195 196 /* 197 * (non-Javadoc) 198 * 199 * @see org.apache.commons.compress.archivers.ArchiveOutputStream#closeArchiveEntry () 200 */ 201 @Override 202 public void closeArchiveEntry() throws IOException { 203 if (finished) { 204 throw new IOException("Stream has already been finished"); 205 } 206 207 ensureOpen(); 208 209 if (entry == null) { 210 throw new IOException("Trying to close non-existent entry"); 211 } 212 213 if (this.entry.getSize() != this.written) { 214 throw new IOException("Invalid entry size (expected " + this.entry.getSize() + " but got " + this.written + " bytes)"); 215 } 216 pad(this.entry.getDataPadCount()); 217 if (this.entry.getFormat() == FORMAT_NEW_CRC && this.crc != this.entry.getChksum()) { 218 throw new IOException("CRC Error"); 219 } 220 this.entry = null; 221 this.crc = 0; 222 this.written = 0; 223 } 224 225 /** 226 * Creates a new CpioArchiveEntry. The entryName must be an ASCII encoded string. 227 * 228 * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, String) 229 */ 230 @Override 231 public CpioArchiveEntry createArchiveEntry(final File inputFile, final String entryName) throws IOException { 232 if (finished) { 233 throw new IOException("Stream has already been finished"); 234 } 235 return new CpioArchiveEntry(inputFile, entryName); 236 } 237 238 /** 239 * Creates a new CpioArchiveEntry. The entryName must be an ASCII encoded string. 240 * 241 * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, String) 242 */ 243 @Override 244 public CpioArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException { 245 if (finished) { 246 throw new IOException("Stream has already been finished"); 247 } 248 return new CpioArchiveEntry(inputPath, entryName, options); 249 } 250 251 /** 252 * Encodes the given string using the configured encoding. 253 * 254 * @param str the String to write 255 * @throws IOException if the string couldn't be written 256 * @return result of encoding the string 257 */ 258 private byte[] encode(final String str) throws IOException { 259 final ByteBuffer buf = zipEncoding.encode(str); 260 final int len = buf.limit() - buf.position(); 261 return Arrays.copyOfRange(buf.array(), buf.arrayOffset(), buf.arrayOffset() + len); 262 } 263 264 /** 265 * Check to make sure that this stream has not been closed 266 * 267 * @throws IOException if the stream is already closed 268 */ 269 private void ensureOpen() throws IOException { 270 if (this.closed) { 271 throw new IOException("Stream closed"); 272 } 273 } 274 275 /** 276 * Finishes writing the contents of the CPIO output stream without closing the underlying stream. Use this method when applying multiple filters in 277 * succession to the same output stream. 278 * 279 * @throws IOException if an I/O exception has occurred or if a CPIO file error has occurred 280 */ 281 @Override 282 public void finish() throws IOException { 283 ensureOpen(); 284 if (finished) { 285 throw new IOException("This archive has already been finished"); 286 } 287 288 if (this.entry != null) { 289 throw new IOException("This archive contains unclosed entries."); 290 } 291 this.entry = new CpioArchiveEntry(this.entryFormat); 292 this.entry.setName(CPIO_TRAILER); 293 this.entry.setNumberOfLinks(1); 294 writeHeader(this.entry); 295 closeArchiveEntry(); 296 297 final int lengthOfLastBlock = (int) (getBytesWritten() % blockSize); 298 if (lengthOfLastBlock != 0) { 299 pad(blockSize - lengthOfLastBlock); 300 } 301 302 finished = true; 303 } 304 305 private void pad(final int count) throws IOException { 306 if (count > 0) { 307 final byte[] buff = new byte[count]; 308 out.write(buff); 309 count(count); 310 } 311 } 312 313 /** 314 * Begins writing a new CPIO file entry and positions the stream to the start of the entry data. Closes the current entry if still active. The current time 315 * will be used if the entry has no set modification time and the default header format will be used if no other format is specified in the entry. 316 * 317 * @param entry the CPIO cpioEntry to be written 318 * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred 319 * @throws ClassCastException if entry is not an instance of CpioArchiveEntry 320 */ 321 @Override 322 public void putArchiveEntry(final CpioArchiveEntry entry) throws IOException { 323 if (finished) { 324 throw new IOException("Stream has already been finished"); 325 } 326 327 ensureOpen(); 328 if (this.entry != null) { 329 closeArchiveEntry(); // close previous entry 330 } 331 if (entry.getTime() == -1) { 332 entry.setTime(System.currentTimeMillis() / 1000); 333 } 334 335 final short format = entry.getFormat(); 336 if (format != this.entryFormat) { 337 throw new IOException("Header format: " + format + " does not match existing format: " + this.entryFormat); 338 } 339 340 if (this.names.put(entry.getName(), entry) != null) { 341 throw new IOException("Duplicate entry: " + entry.getName()); 342 } 343 344 writeHeader(entry); 345 this.entry = entry; 346 this.written = 0; 347 } 348 349 /** 350 * Writes an array of bytes to the current CPIO entry data. This method will block until all the bytes are written. 351 * 352 * @param b the data to be written 353 * @param off the start offset in the data 354 * @param len the number of bytes that are written 355 * @throws IOException if an I/O error has occurred or if a CPIO file error has occurred 356 */ 357 @Override 358 public void write(final byte[] b, final int off, final int len) throws IOException { 359 ensureOpen(); 360 if (off < 0 || len < 0 || off > b.length - len) { 361 throw new IndexOutOfBoundsException(); 362 } 363 if (len == 0) { 364 return; 365 } 366 367 if (this.entry == null) { 368 throw new IOException("No current CPIO entry"); 369 } 370 if (this.written + len > this.entry.getSize()) { 371 throw new IOException("Attempt to write past end of STORED entry"); 372 } 373 out.write(b, off, len); 374 this.written += len; 375 if (this.entry.getFormat() == FORMAT_NEW_CRC) { 376 for (int pos = 0; pos < len; pos++) { 377 this.crc += b[pos] & 0xFF; 378 this.crc &= 0xFFFFFFFFL; 379 } 380 } 381 count(len); 382 } 383 384 private void writeAsciiLong(final long number, final int length, final int radix) throws IOException { 385 final StringBuilder tmp = new StringBuilder(); 386 final String tmpStr; 387 if (radix == 16) { 388 tmp.append(Long.toHexString(number)); 389 } else if (radix == 8) { 390 tmp.append(Long.toOctalString(number)); 391 } else { 392 tmp.append(number); 393 } 394 395 if (tmp.length() <= length) { 396 final int insertLength = length - tmp.length(); 397 for (int pos = 0; pos < insertLength; pos++) { 398 tmp.insert(0, "0"); 399 } 400 tmpStr = tmp.toString(); 401 } else { 402 tmpStr = tmp.substring(tmp.length() - length); 403 } 404 final byte[] b = ArchiveUtils.toAsciiBytes(tmpStr); 405 out.write(b); 406 count(b.length); 407 } 408 409 private void writeBinaryLong(final long number, final int length, final boolean swapHalfWord) throws IOException { 410 final byte[] tmp = CpioUtil.long2byteArray(number, length, swapHalfWord); 411 out.write(tmp); 412 count(tmp.length); 413 } 414 415 /** 416 * Writes an encoded string to the stream followed by \0 417 * 418 * @param str the String to write 419 * @throws IOException if the string couldn't be written 420 */ 421 private void writeCString(final byte[] str) throws IOException { 422 out.write(str); 423 out.write('\0'); 424 count(str.length + 1); 425 } 426 427 private void writeHeader(final CpioArchiveEntry e) throws IOException { 428 switch (e.getFormat()) { 429 case FORMAT_NEW: 430 out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW)); 431 count(6); 432 writeNewEntry(e); 433 break; 434 case FORMAT_NEW_CRC: 435 out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW_CRC)); 436 count(6); 437 writeNewEntry(e); 438 break; 439 case FORMAT_OLD_ASCII: 440 out.write(ArchiveUtils.toAsciiBytes(MAGIC_OLD_ASCII)); 441 count(6); 442 writeOldAsciiEntry(e); 443 break; 444 case FORMAT_OLD_BINARY: 445 final boolean swapHalfWord = true; 446 writeBinaryLong(MAGIC_OLD_BINARY, 2, swapHalfWord); 447 writeOldBinaryEntry(e, swapHalfWord); 448 break; 449 default: 450 throw new IOException("Unknown format " + e.getFormat()); 451 } 452 } 453 454 private void writeNewEntry(final CpioArchiveEntry entry) throws IOException { 455 long inode = entry.getInode(); 456 long devMin = entry.getDeviceMin(); 457 if (CPIO_TRAILER.equals(entry.getName())) { 458 inode = devMin = 0; 459 } else if (inode == 0 && devMin == 0) { 460 inode = nextArtificalDeviceAndInode & 0xFFFFFFFF; 461 devMin = nextArtificalDeviceAndInode++ >> 32 & 0xFFFFFFFF; 462 } else { 463 nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 0x100000000L * devMin) + 1; 464 } 465 466 writeAsciiLong(inode, 8, 16); 467 writeAsciiLong(entry.getMode(), 8, 16); 468 writeAsciiLong(entry.getUID(), 8, 16); 469 writeAsciiLong(entry.getGID(), 8, 16); 470 writeAsciiLong(entry.getNumberOfLinks(), 8, 16); 471 writeAsciiLong(entry.getTime(), 8, 16); 472 writeAsciiLong(entry.getSize(), 8, 16); 473 writeAsciiLong(entry.getDeviceMaj(), 8, 16); 474 writeAsciiLong(devMin, 8, 16); 475 writeAsciiLong(entry.getRemoteDeviceMaj(), 8, 16); 476 writeAsciiLong(entry.getRemoteDeviceMin(), 8, 16); 477 final byte[] name = encode(entry.getName()); 478 writeAsciiLong(name.length + 1L, 8, 16); 479 writeAsciiLong(entry.getChksum(), 8, 16); 480 writeCString(name); 481 pad(entry.getHeaderPadCount(name.length)); 482 } 483 484 private void writeOldAsciiEntry(final CpioArchiveEntry entry) throws IOException { 485 long inode = entry.getInode(); 486 long device = entry.getDevice(); 487 if (CPIO_TRAILER.equals(entry.getName())) { 488 inode = device = 0; 489 } else if (inode == 0 && device == 0) { 490 inode = nextArtificalDeviceAndInode & 0777777; 491 device = nextArtificalDeviceAndInode++ >> 18 & 0777777; 492 } else { 493 nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 01000000 * device) + 1; 494 } 495 496 writeAsciiLong(device, 6, 8); 497 writeAsciiLong(inode, 6, 8); 498 writeAsciiLong(entry.getMode(), 6, 8); 499 writeAsciiLong(entry.getUID(), 6, 8); 500 writeAsciiLong(entry.getGID(), 6, 8); 501 writeAsciiLong(entry.getNumberOfLinks(), 6, 8); 502 writeAsciiLong(entry.getRemoteDevice(), 6, 8); 503 writeAsciiLong(entry.getTime(), 11, 8); 504 final byte[] name = encode(entry.getName()); 505 writeAsciiLong(name.length + 1L, 6, 8); 506 writeAsciiLong(entry.getSize(), 11, 8); 507 writeCString(name); 508 } 509 510 private void writeOldBinaryEntry(final CpioArchiveEntry entry, final boolean swapHalfWord) throws IOException { 511 long inode = entry.getInode(); 512 long device = entry.getDevice(); 513 if (CPIO_TRAILER.equals(entry.getName())) { 514 inode = device = 0; 515 } else if (inode == 0 && device == 0) { 516 inode = nextArtificalDeviceAndInode & 0xFFFF; 517 device = nextArtificalDeviceAndInode++ >> 16 & 0xFFFF; 518 } else { 519 nextArtificalDeviceAndInode = Math.max(nextArtificalDeviceAndInode, inode + 0x10000 * device) + 1; 520 } 521 522 writeBinaryLong(device, 2, swapHalfWord); 523 writeBinaryLong(inode, 2, swapHalfWord); 524 writeBinaryLong(entry.getMode(), 2, swapHalfWord); 525 writeBinaryLong(entry.getUID(), 2, swapHalfWord); 526 writeBinaryLong(entry.getGID(), 2, swapHalfWord); 527 writeBinaryLong(entry.getNumberOfLinks(), 2, swapHalfWord); 528 writeBinaryLong(entry.getRemoteDevice(), 2, swapHalfWord); 529 writeBinaryLong(entry.getTime(), 4, swapHalfWord); 530 final byte[] name = encode(entry.getName()); 531 writeBinaryLong(name.length + 1L, 2, swapHalfWord); 532 writeBinaryLong(entry.getSize(), 4, swapHalfWord); 533 writeCString(name); 534 pad(entry.getHeaderPadCount(name.length)); 535 } 536 537}