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