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