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.FilterOutputStream; 023 import java.io.IOException; 024 import java.io.OutputStream; 025 import java.util.HashMap; 026 027 import org.apache.commons.compress.archivers.ArchiveEntry; 028 import org.apache.commons.compress.archivers.ArchiveOutputStream; 029 import org.apache.commons.compress.utils.ArchiveUtils; 030 031 /** 032 * CPIOArchiveOutputStream is a stream for writing CPIO streams. All formats of 033 * CPIO are supported (old ASCII, old binary, new portable format and the new 034 * portable format with CRC). 035 * <p/> 036 * <p/> 037 * An entry can be written by creating an instance of CpioArchiveEntry and fill 038 * it with the necessary values and put it into the CPIO stream. Afterwards 039 * write the contents of the file into the CPIO stream. Either close the stream 040 * by calling finish() or put a next entry into the cpio stream. 041 * <p/> 042 * <code><pre> 043 * CpioArchiveOutputStream out = new CpioArchiveOutputStream( 044 * new FileOutputStream(new File("test.cpio"))); 045 * CpioArchiveEntry entry = new CpioArchiveEntry(); 046 * entry.setName("testfile"); 047 * String contents = "12345"; 048 * entry.setFileSize(contents.length()); 049 * entry.setMode(CpioConstants.C_ISREG); // regular file 050 * ... set other attributes, e.g. time, number of links 051 * out.putNextEntry(entry); 052 * out.write(testContents.getBytes()); 053 * out.close(); 054 * </pre></code> 055 * <p/> 056 * Note: This implementation should be compatible to cpio 2.5 057 * 058 * This class uses mutable fields and is not considered threadsafe. 059 * 060 * based on code from the jRPM project (jrpm.sourceforge.net) 061 */ 062 public class CpioArchiveOutputStream extends ArchiveOutputStream implements 063 CpioConstants { 064 065 private CpioArchiveEntry entry; 066 067 private boolean closed = false; 068 069 /** indicates if this archive is finished */ 070 private boolean finished; 071 072 /** 073 * See {@link CpioArchiveEntry#setFormat(short)} for possible values. 074 */ 075 private final short entryFormat; 076 077 private final HashMap names = new HashMap(); 078 079 private long crc = 0; 080 081 private long written; 082 083 private final OutputStream out; 084 085 /** 086 * Construct the cpio output stream with a specified format 087 * 088 * @param out 089 * The cpio stream 090 * @param format 091 * The format of the stream 092 */ 093 public CpioArchiveOutputStream(final OutputStream out, final short format) { 094 this.out = new FilterOutputStream(out); 095 switch (format) { 096 case FORMAT_NEW: 097 case FORMAT_NEW_CRC: 098 case FORMAT_OLD_ASCII: 099 case FORMAT_OLD_BINARY: 100 break; 101 default: 102 throw new IllegalArgumentException("Unknown format: "+format); 103 104 } 105 this.entryFormat = format; 106 } 107 108 /** 109 * Construct the cpio output stream. The format for this CPIO stream is the 110 * "new" format 111 * 112 * @param out 113 * The cpio stream 114 */ 115 public CpioArchiveOutputStream(final OutputStream out) { 116 this(out, FORMAT_NEW); 117 } 118 119 /** 120 * Check to make sure that this stream has not been closed 121 * 122 * @throws IOException 123 * if the stream is already closed 124 */ 125 private void ensureOpen() throws IOException { 126 if (this.closed) { 127 throw new IOException("Stream closed"); 128 } 129 } 130 131 /** 132 * Begins writing a new CPIO file entry and positions the stream to the 133 * start of the entry data. Closes the current entry if still active. The 134 * current time will be used if the entry has no set modification time and 135 * the default header format will be used if no other format is specified in 136 * the entry. 137 * 138 * @param entry 139 * the CPIO cpioEntry to be written 140 * @throws IOException 141 * if an I/O error has occurred or if a CPIO file error has 142 * occurred 143 * @throws ClassCastException if entry is not an instance of CpioArchiveEntry 144 */ 145 public void putArchiveEntry(ArchiveEntry entry) throws IOException { 146 if(finished) { 147 throw new IOException("Stream has already been finished"); 148 } 149 150 CpioArchiveEntry e = (CpioArchiveEntry) entry; 151 ensureOpen(); 152 if (this.entry != null) { 153 closeArchiveEntry(); // close previous entry 154 } 155 if (e.getTime() == -1) { 156 e.setTime(System.currentTimeMillis()); 157 } 158 159 final short format = e.getFormat(); 160 if (format != this.entryFormat){ 161 throw new IOException("Header format: "+format+" does not match existing format: "+this.entryFormat); 162 } 163 164 if (this.names.put(e.getName(), e) != null) { 165 throw new IOException("duplicate entry: " + e.getName()); 166 } 167 168 writeHeader(e); 169 this.entry = e; 170 this.written = 0; 171 } 172 173 private void writeHeader(final CpioArchiveEntry e) throws IOException { 174 switch (e.getFormat()) { 175 case FORMAT_NEW: 176 out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW)); 177 writeNewEntry(e); 178 break; 179 case FORMAT_NEW_CRC: 180 out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW_CRC)); 181 writeNewEntry(e); 182 break; 183 case FORMAT_OLD_ASCII: 184 out.write(ArchiveUtils.toAsciiBytes(MAGIC_OLD_ASCII)); 185 writeOldAsciiEntry(e); 186 break; 187 case FORMAT_OLD_BINARY: 188 boolean swapHalfWord = true; 189 writeBinaryLong(MAGIC_OLD_BINARY, 2, swapHalfWord); 190 writeOldBinaryEntry(e, swapHalfWord); 191 break; 192 } 193 } 194 195 private void writeNewEntry(final CpioArchiveEntry entry) throws IOException { 196 writeAsciiLong(entry.getInode(), 8, 16); 197 writeAsciiLong(entry.getMode(), 8, 16); 198 writeAsciiLong(entry.getUID(), 8, 16); 199 writeAsciiLong(entry.getGID(), 8, 16); 200 writeAsciiLong(entry.getNumberOfLinks(), 8, 16); 201 writeAsciiLong(entry.getTime(), 8, 16); 202 writeAsciiLong(entry.getSize(), 8, 16); 203 writeAsciiLong(entry.getDeviceMaj(), 8, 16); 204 writeAsciiLong(entry.getDeviceMin(), 8, 16); 205 writeAsciiLong(entry.getRemoteDeviceMaj(), 8, 16); 206 writeAsciiLong(entry.getRemoteDeviceMin(), 8, 16); 207 writeAsciiLong(entry.getName().length() + 1, 8, 16); 208 writeAsciiLong(entry.getChksum(), 8, 16); 209 writeCString(entry.getName()); 210 pad(entry.getHeaderPadCount()); 211 } 212 213 private void writeOldAsciiEntry(final CpioArchiveEntry entry) 214 throws IOException { 215 writeAsciiLong(entry.getDevice(), 6, 8); 216 writeAsciiLong(entry.getInode(), 6, 8); 217 writeAsciiLong(entry.getMode(), 6, 8); 218 writeAsciiLong(entry.getUID(), 6, 8); 219 writeAsciiLong(entry.getGID(), 6, 8); 220 writeAsciiLong(entry.getNumberOfLinks(), 6, 8); 221 writeAsciiLong(entry.getRemoteDevice(), 6, 8); 222 writeAsciiLong(entry.getTime(), 11, 8); 223 writeAsciiLong(entry.getName().length() + 1, 6, 8); 224 writeAsciiLong(entry.getSize(), 11, 8); 225 writeCString(entry.getName()); 226 } 227 228 private void writeOldBinaryEntry(final CpioArchiveEntry entry, 229 final boolean swapHalfWord) throws IOException { 230 writeBinaryLong(entry.getDevice(), 2, swapHalfWord); 231 writeBinaryLong(entry.getInode(), 2, swapHalfWord); 232 writeBinaryLong(entry.getMode(), 2, swapHalfWord); 233 writeBinaryLong(entry.getUID(), 2, swapHalfWord); 234 writeBinaryLong(entry.getGID(), 2, swapHalfWord); 235 writeBinaryLong(entry.getNumberOfLinks(), 2, swapHalfWord); 236 writeBinaryLong(entry.getRemoteDevice(), 2, swapHalfWord); 237 writeBinaryLong(entry.getTime(), 4, swapHalfWord); 238 writeBinaryLong(entry.getName().length() + 1, 2, swapHalfWord); 239 writeBinaryLong(entry.getSize(), 4, swapHalfWord); 240 writeCString(entry.getName()); 241 pad(entry.getHeaderPadCount()); 242 } 243 244 /*(non-Javadoc) 245 * 246 * @see 247 * org.apache.commons.compress.archivers.ArchiveOutputStream#closeArchiveEntry 248 * () 249 */ 250 public void closeArchiveEntry() throws IOException { 251 if(finished) { 252 throw new IOException("Stream has already been finished"); 253 } 254 255 ensureOpen(); 256 257 if (entry == null) { 258 throw new IOException("Trying to close non-existent entry"); 259 } 260 261 if (this.entry.getSize() != this.written) { 262 throw new IOException("invalid entry size (expected " 263 + this.entry.getSize() + " but got " + this.written 264 + " bytes)"); 265 } 266 pad(this.entry.getDataPadCount()); 267 if (this.entry.getFormat() == FORMAT_NEW_CRC) { 268 if (this.crc != this.entry.getChksum()) { 269 throw new IOException("CRC Error"); 270 } 271 } 272 this.entry = null; 273 this.crc = 0; 274 this.written = 0; 275 } 276 277 /** 278 * Writes an array of bytes to the current CPIO entry data. This method will 279 * block until all the bytes are written. 280 * 281 * @param b 282 * the data to be written 283 * @param off 284 * the start offset in the data 285 * @param len 286 * the number of bytes that are written 287 * @throws IOException 288 * if an I/O error has occurred or if a CPIO file error has 289 * occurred 290 */ 291 public void write(final byte[] b, final int off, final int len) 292 throws IOException { 293 ensureOpen(); 294 if (off < 0 || len < 0 || off > b.length - len) { 295 throw new IndexOutOfBoundsException(); 296 } else if (len == 0) { 297 return; 298 } 299 300 if (this.entry == null) { 301 throw new IOException("no current CPIO entry"); 302 } 303 if (this.written + len > this.entry.getSize()) { 304 throw new IOException("attempt to write past end of STORED entry"); 305 } 306 out.write(b, off, len); 307 this.written += len; 308 if (this.entry.getFormat() == FORMAT_NEW_CRC) { 309 for (int pos = 0; pos < len; pos++) { 310 this.crc += b[pos] & 0xFF; 311 } 312 } 313 count(len); 314 } 315 316 /** 317 * Finishes writing the contents of the CPIO output stream without closing 318 * the underlying stream. Use this method when applying multiple filters in 319 * succession to the same output stream. 320 * 321 * @throws IOException 322 * if an I/O exception has occurred or if a CPIO file error has 323 * occurred 324 */ 325 public void finish() throws IOException { 326 ensureOpen(); 327 if (finished) { 328 throw new IOException("This archive has already been finished"); 329 } 330 331 if (this.entry != null) { 332 throw new IOException("This archive contains unclosed entries."); 333 } 334 this.entry = new CpioArchiveEntry(this.entryFormat); 335 this.entry.setName(CPIO_TRAILER); 336 this.entry.setNumberOfLinks(1); 337 writeHeader(this.entry); 338 closeArchiveEntry(); 339 340 finished = true; 341 } 342 343 /** 344 * Closes the CPIO output stream as well as the stream being filtered. 345 * 346 * @throws IOException 347 * if an I/O error has occurred or if a CPIO file error has 348 * occurred 349 */ 350 public void close() throws IOException { 351 if(!finished) { 352 finish(); 353 } 354 355 if (!this.closed) { 356 out.close(); 357 this.closed = true; 358 } 359 } 360 361 private void pad(int count) throws IOException{ 362 if (count > 0){ 363 byte buff[] = new byte[count]; 364 out.write(buff); 365 } 366 } 367 368 private void writeBinaryLong(final long number, final int length, 369 final boolean swapHalfWord) throws IOException { 370 byte tmp[] = CpioUtil.long2byteArray(number, length, swapHalfWord); 371 out.write(tmp); 372 } 373 374 private void writeAsciiLong(final long number, final int length, 375 final int radix) throws IOException { 376 StringBuffer tmp = new StringBuffer(); 377 String tmpStr; 378 if (radix == 16) { 379 tmp.append(Long.toHexString(number)); 380 } else if (radix == 8) { 381 tmp.append(Long.toOctalString(number)); 382 } else { 383 tmp.append(Long.toString(number)); 384 } 385 386 if (tmp.length() <= length) { 387 long insertLength = length - tmp.length(); 388 for (int pos = 0; pos < insertLength; pos++) { 389 tmp.insert(0, "0"); 390 } 391 tmpStr = tmp.toString(); 392 } else { 393 tmpStr = tmp.substring(tmp.length() - length); 394 } 395 out.write(ArchiveUtils.toAsciiBytes(tmpStr)); 396 } 397 398 /** 399 * Writes an ASCII string to the stream followed by \0 400 * @param str the String to write 401 * @throws IOException if the string couldn't be written 402 */ 403 private void writeCString(final String str) throws IOException { 404 out.write(ArchiveUtils.toAsciiBytes(str)); 405 out.write('\0'); 406 } 407 408 /** 409 * Creates a new ArchiveEntry. The entryName must be an ASCII encoded string. 410 * 411 * @see org.apache.commons.compress.archivers.ArchiveOutputStream#createArchiveEntry(java.io.File, java.lang.String) 412 */ 413 public ArchiveEntry createArchiveEntry(File inputFile, String entryName) 414 throws IOException { 415 if(finished) { 416 throw new IOException("Stream has already been finished"); 417 } 418 return new CpioArchiveEntry(inputFile, entryName); 419 } 420 421 }