blob: 5d22d265e943f44edef051c680bfd6445187bb60 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.commons.compress.archivers.cpio;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveOutputStream;
/**
* CPIOArchiveOutputStream is a stream for writting cpio streams. All formats of cpio
* are supported (old ascii, old binary, new portable format and the new
* portable format with crc).
* <p/>
* <p/>
* 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 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.
* <p/>
* <code><pre>
* CPIOArchiveOutputStream cpioOut = new CPIOArchiveOutputStream(new BufferedOutputStream(
* new FileOutputStream(new File(&quot;test.cpio&quot;))));
* CPIOArchiveEntry cpioEntry = new CPIOArchiveEntry();
* cpioEntry.setName(&quot;testfile&quot;);
* String testContents = &quot;12345&quot;;
* cpioEntry.setFileSize(testContents.length());
* cpioOut.putNextEntry(cpioEntry);
* cpioOut.write(testContents.getBytes());
* cpioOut.finish();
* cpioOut.close();
* </pre></code>
* <p/>
* Note: This implementation should be compatible to cpio 2.5
*
* based on code from the jRPM project (jrpm.sourceforge.net)
*/
public class CpioArchiveOutputStream extends ArchiveOutputStream implements CpioConstants {
private CpioArchiveEntry cpioEntry;
private boolean closed = false;
private boolean finished;
private short entryFormat = FORMAT_NEW;
private HashMap names = new HashMap();
private long crc = 0;
private long written;
private OutputStream out = null;
/**
* Check to make sure that this stream has not been closed
*
* @throws IOException if the stream is already closed
*/
private void ensureOpen() throws IOException {
if (this.closed) {
throw new IOException("Stream closed");
}
}
/**
* Construct the cpio output stream with a specified format
*
* @param out The cpio stream
* @param format The format of the stream
*/
public CpioArchiveOutputStream(final OutputStream out, final short format) {
this.out = new FilterOutputStream(out);
setFormat(format);
}
/**
* Construct the cpio output stream. The format for this CPIO stream is the
* "new" format
*
* @param out The cpio stream
*/
public CpioArchiveOutputStream(final OutputStream out) {
this(out, FORMAT_NEW);
}
/**
* Set a default header format. This will be used if no format is defined in
* the cpioEntry given to putNextEntry().
*
* @param format A CPIO format
*/
public void setFormat(final short format) {
switch (format) {
case FORMAT_NEW:
case FORMAT_NEW_CRC:
case FORMAT_OLD_ASCII:
case FORMAT_OLD_BINARY:
break;
default:
throw new IllegalArgumentException("Unknown header type");
}
this.entryFormat = format;
}
/**
* 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 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.
*
* @param e the CPIO cpioEntry to be written
* @throws IOException if an I/O error has occurred or if a CPIO file error has
* occurred
*/
public void putNextEntry(final CpioArchiveEntry e) throws IOException {
ensureOpen();
if (this.cpioEntry != null) {
closeEntry(); // close previous entry
}
if (e.getTime() == -1) {
e.setTime(System.currentTimeMillis());
}
if (e.getFormat() == -1) {
e.setFormat(this.entryFormat);
}
if (this.names.put(e.getName(), e) != null) {
throw new IOException("duplicate entry: " + e.getName());
}
writeHeader(e);
this.cpioEntry = e;
this.written = 0;
}
private void writeHeader(final CpioArchiveEntry e) throws IOException {
switch (e.getFormat()) {
case FORMAT_NEW:
out.write(MAGIC_NEW.getBytes());
writeNewEntry(e);
break;
case FORMAT_NEW_CRC:
out.write(MAGIC_NEW_CRC.getBytes());
writeNewEntry(e);
break;
case FORMAT_OLD_ASCII:
out.write(MAGIC_OLD_ASCII.getBytes());
writeOldAsciiEntry(e);
break;
case FORMAT_OLD_BINARY:
boolean swapHalfWord = true;
writeBinaryLong(MAGIC_OLD_BINARY, 2, swapHalfWord);
writeOldBinaryEntry(e, swapHalfWord);
break;
}
}
private void writeNewEntry(final CpioArchiveEntry entry) throws IOException {
writeAsciiLong(entry.getInode(), 8, 16);
writeAsciiLong(entry.getMode(), 8, 16);
writeAsciiLong(entry.getUID(), 8, 16);
writeAsciiLong(entry.getGID(), 8, 16);
writeAsciiLong(entry.getNumberOfLinks(), 8, 16);
writeAsciiLong(entry.getTime(), 8, 16);
writeAsciiLong(entry.getSize(), 8, 16);
writeAsciiLong(entry.getDeviceMaj(), 8, 16);
writeAsciiLong(entry.getDeviceMin(), 8, 16);
writeAsciiLong(entry.getRemoteDeviceMaj(), 8, 16);
writeAsciiLong(entry.getRemoteDeviceMin(), 8, 16);
writeAsciiLong(entry.getName().length() + 1, 8, 16);
writeAsciiLong(entry.getChksum(), 8, 16);
writeCString(entry.getName());
pad(entry.getHeaderSize() + entry.getName().length() + 1, 4);
}
private void writeOldAsciiEntry(final CpioArchiveEntry entry) throws IOException {
writeAsciiLong(entry.getDevice(), 6, 8);
writeAsciiLong(entry.getInode(), 6, 8);
writeAsciiLong(entry.getMode(), 6, 8);
writeAsciiLong(entry.getUID(), 6, 8);
writeAsciiLong(entry.getGID(), 6, 8);
writeAsciiLong(entry.getNumberOfLinks(), 6, 8);
writeAsciiLong(entry.getRemoteDevice(), 6, 8);
writeAsciiLong(entry.getTime(), 11, 8);
writeAsciiLong(entry.getName().length() + 1, 6, 8);
writeAsciiLong(entry.getSize(), 11, 8);
writeCString(entry.getName());
}
private void writeOldBinaryEntry(final CpioArchiveEntry entry,
final boolean swapHalfWord) throws IOException {
writeBinaryLong(entry.getDevice(), 2, swapHalfWord);
writeBinaryLong(entry.getInode(), 2, swapHalfWord);
writeBinaryLong(entry.getMode(), 2, swapHalfWord);
writeBinaryLong(entry.getUID(), 2, swapHalfWord);
writeBinaryLong(entry.getGID(), 2, swapHalfWord);
writeBinaryLong(entry.getNumberOfLinks(), 2, swapHalfWord);
writeBinaryLong(entry.getRemoteDevice(), 2, swapHalfWord);
writeBinaryLong(entry.getTime(), 4, swapHalfWord);
writeBinaryLong(entry.getName().length() + 1, 2, swapHalfWord);
writeBinaryLong(entry.getSize(), 4, swapHalfWord);
writeCString(entry.getName());
pad(entry.getHeaderSize() + entry.getName().length() + 1, 2);
}
/**
* Closes the current CPIO entry and positions the stream for writing the
* next entry.
*
* @throws IOException if an I/O error has occurred or if a CPIO file error has
* occurred
*/
public void closeEntry() throws IOException {
ensureOpen();
if (this.cpioEntry.getSize() != this.written) {
throw new IOException("invalid entry size (expected "
+ this.cpioEntry.getSize() + " but got " + this.written
+ " bytes)");
}
if ((this.cpioEntry.getFormat() | FORMAT_NEW_MASK) == FORMAT_NEW_MASK) {
pad(this.cpioEntry.getSize(), 4);
} else if ((this.cpioEntry.getFormat() | FORMAT_OLD_BINARY) == FORMAT_OLD_BINARY) {
pad(this.cpioEntry.getSize(), 2);
}
if ((this.cpioEntry.getFormat() | FORMAT_NEW_CRC) == FORMAT_NEW_CRC) {
if (this.crc != this.cpioEntry.getChksum()) {
throw new IOException("CRC Error");
}
}
if (this.cpioEntry != null) {
this.cpioEntry = null;
}
this.crc = 0;
this.written = 0;
}
/**
* Writes an array of bytes to the current CPIO entry data. This method will
* block until all the bytes are written.
*
* @param b the data to be written
* @param off the start offset in the data
* @param len the number of bytes that are written
* @throws IOException if an I/O error has occurred or if a CPIO file error has
* occurred
*/
public synchronized void write(final byte[] b, final int off, final int len)
throws IOException {
ensureOpen();
if (off < 0 || len < 0 || off > b.length - len) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
if (this.cpioEntry == null) {
throw new IOException("no current CPIO entry");
}
if (this.written + len > this.cpioEntry.getSize()) {
throw new IOException("attempt to write past end of STORED entry");
}
out.write(b, off, len);
this.written += len;
if ((this.cpioEntry.getFormat() | FORMAT_NEW_CRC) == FORMAT_NEW_CRC) {
for (int pos = 0; pos < len; pos++) {
this.crc += b[pos] & 0xFF;
}
}
}
/**
* Finishes writing the contents of the CPIO output stream without closing
* the underlying stream. Use this method when applying multiple filters in
* succession to the same output stream.
*
* @throws IOException if an I/O exception has occurred or if a CPIO file error
* has occurred
*/
public void finish() throws IOException {
ensureOpen();
// TODO: synchronize and finish
if (this.finished) {
return;
}
if (this.cpioEntry != null) {
closeEntry();
}
this.cpioEntry = new CpioArchiveEntry(this.entryFormat);
this.cpioEntry.setMode(0);
this.cpioEntry.setName("TRAILER!!!");
this.cpioEntry.setNumberOfLinks(1);
writeHeader(this.cpioEntry);
closeEntry();
}
/**
* Closes the CPIO output stream as well as the stream being filtered.
*
* @throws IOException if an I/O error has occurred or if a CPIO file error has
* occurred
*/
public void close() throws IOException {
if (!this.closed) {
super.close();
this.closed = true;
}
}
private void pad(final long count, final int border) throws IOException {
long skip = count % border;
if (skip > 0) {
byte tmp[] = new byte[(int) (border - skip)];
out.write(tmp);
}
}
private void writeBinaryLong(final long number, final int length,
final boolean swapHalfWord) throws IOException {
byte tmp[] = long2byteArray(number, length, swapHalfWord);
out.write(tmp);
}
private void writeAsciiLong(final long number, final int length,
final int radix) throws IOException {
StringBuffer tmp = new StringBuffer();
String tmpStr;
if (radix == 16) {
tmp.append(Long.toHexString(number));
} else if (radix == 8) {
tmp.append(Long.toOctalString(number));
} else {
tmp.append(Long.toString(number));
}
if (tmp.length() <= length) {
long insertLength = length - tmp.length();
for (int pos = 0; pos < insertLength; pos++) {
tmp.insert(0, "0");
}
tmpStr = tmp.toString();
} else {
tmpStr = tmp.substring(tmp.length() - length);
}
out.write(tmpStr.getBytes());
}
private void writeCString(final String str) throws IOException {
out.write(str.getBytes());
out.write('\0');
}
/**
* Converts a byte array to a long. Halfwords can be swaped with setting
* swapHalfWord=true.
*
* @param number An array of bytes containing a number
* @param length The length of the returned array
* @param swapHalfWord Swap halfwords ([0][1][2][3]->[1][0][3][2])
* @return The long value
*/
private static byte[] long2byteArray(final long number, final int length,
final boolean swapHalfWord) {
byte[] ret = new byte[length];
int pos = 0;
long tmp_number = 0;
if (length % 2 != 0 || length < 2) {
throw new UnsupportedOperationException();
}
tmp_number = number;
for (pos = length - 1; pos >= 0; pos--) {
ret[pos] = (byte) (tmp_number & 0xFF);
tmp_number >>= 8;
}
if (!swapHalfWord) {
byte tmp = 0;
for (pos = 0; pos < length; pos++) {
tmp = ret[pos];
ret[pos++] = ret[pos];
ret[pos] = tmp;
}
}
return ret;
}
/* (non-Javadoc)
* @see org.apache.commons.compress.archivers.ArchiveOutputStream#closeArchiveEntry()
*/
public void closeArchiveEntry() throws IOException {
this.closeEntry();
}
/* (non-Javadoc)
* @see org.apache.commons.compress.archivers.ArchiveOutputStream#putArchiveEntry(org.apache.commons.compress.archivers.ArchiveEntry)
*/
public void putArchiveEntry(ArchiveEntry entry) throws IOException {
this.putNextEntry((CpioArchiveEntry)entry);
}
/* (non-Javadoc)
* @see java.io.OutputStream#write(int)
*/
public void write(int b) throws IOException {
out.write(b);
}
}