/*
* Copyright (C) 2014 Jörg Prante
*
* Licensed 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.xbib.io.archive.cpio;
import org.xbib.io.CountingOutputStream;
import org.xbib.io.archive.ArchiveOutputStream;
import org.xbib.io.archive.ArchiveUtils;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
/**
* CPIOArchiveOutputStream is a stream for writing 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>
* CpioArchiveOutputStream out = new CpioArchiveOutputStream(
* new FileOutputStream(new File("test.cpio")));
* CpioArchiveEntry entry = new CpioArchiveEntry();
* entry.setName("testfile");
* String contents = "12345";
* entry.setFileSize(contents.length());
* entry.setMode(CpioConstants.C_ISREG); // regular file
* ... set other attributes, e.g. time, number of links
* out.putArchiveEntry(entry);
* out.write(testContents.getBytes());
* out.close();
* </code>
* <p>
* Note: This implementation should be compatible to cpio 2.5
*/
public class CpioArchiveOutputStream extends ArchiveOutputStream<CpioArchiveEntry> implements CpioConstants {
private CpioArchiveEntry entry;
private boolean closed = false;
private boolean finished;
private final short entryFormat;
private final HashMap<String, CpioArchiveEntry> names = new HashMap<String, CpioArchiveEntry>();
private long crc = 0;
private long written;
private final int blockSize;
private long nextArtificalDeviceAndInode = 1;
private final CountingOutputStream out;
/**
* Construct the cpio output stream. The format for this CPIO stream is the
* "new" format
*
* @param out The cpio stream
*/
public CpioArchiveOutputStream(OutputStream out) {
this(out, FORMAT_NEW);
}
/**
* Construct the cpio output stream with a specified format and a
* blocksize of {@link CpioConstants#BLOCK_SIZE BLOCK_SIZE}.
*
* @param out The cpio stream
* @param format The format of the stream
*/
public CpioArchiveOutputStream(OutputStream out, short format) {
this(out, format, BLOCK_SIZE);
}
/**
* Construct the cpio output stream with a specified format
*
* @param out The cpio stream
* @param format The format of the stream
* @param blockSize The block size of the archive.
*/
public CpioArchiveOutputStream(OutputStream out,
short format, int blockSize) {
this.out = new CountingOutputStream(out);
switch (format) {
case FORMAT_NEW:
case FORMAT_NEW_CRC:
case FORMAT_OLD_ASCII:
case FORMAT_OLD_BINARY:
break;
default:
throw new IllegalArgumentException("Unknown format: " + format);
}
this.entryFormat = format;
this.blockSize = blockSize;
}
/**
* Check to make sure that this stream has not been closed
*
* @throws java.io.IOException if the stream is already closed
*/
private void ensureOpen() throws IOException {
if (this.closed) {
throw new IOException("Stream closed");
}
}
@Override
public CpioArchiveEntry newArchiveEntry() {
return new CpioArchiveEntry(FORMAT_NEW);
}
/**
* 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 entry the CPIO cpioEntry to be written
* @throws java.io.IOException if an I/O error has occurred or if a CPIO file error has
* occurred
* @throws ClassCastException if entry is not an instance of CpioArchiveEntry
*/
@Override
public void putArchiveEntry(CpioArchiveEntry entry) throws IOException {
if (finished) {
throw new IOException("stream has already been finished");
}
ensureOpen();
if (this.entry != null) {
closeArchiveEntry(); // close previous entry
}
if (entry.getTime() == -1) {
entry.setTime(System.currentTimeMillis() / 1000);
}
if (entry.getFormat() != this.entryFormat) {
throw new IOException("header format " + entry.getFormat() + " does not match existing format: " + this.entryFormat);
}
if (this.names.put(entry.getName(), entry) != null) {
throw new IOException("duplicate entry: " + entry.getName());
}
writeHeader(entry);
this.entry = entry;
this.written = 0;
}
private void writeHeader(final CpioArchiveEntry e) throws IOException {
switch (e.getFormat()) {
case FORMAT_NEW:
out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW));
writeNewEntry(e);
break;
case FORMAT_NEW_CRC:
out.write(ArchiveUtils.toAsciiBytes(MAGIC_NEW_CRC));
writeNewEntry(e);
break;
case FORMAT_OLD_ASCII:
out.write(ArchiveUtils.toAsciiBytes(MAGIC_OLD_ASCII));
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 {
long inode = entry.getInode();
long devMin = entry.getDeviceMin();
if (CPIO_TRAILER.equals(entry.getName())) {
inode = devMin = 0;
} else {
if (inode == 0 && devMin == 0) {
inode = nextArtificalDeviceAndInode & 0xFFFFFFFF;
devMin = (nextArtificalDeviceAndInode++ >> 32) & 0xFFFFFFFF;
} else {
nextArtificalDeviceAndInode =
Math.max(nextArtificalDeviceAndInode,
inode + 0x100000000L * devMin) + 1;
}
}
writeAsciiLong(inode, 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.getEntrySize(), 8, 16);
writeAsciiLong(entry.getDeviceMaj(), 8, 16);
writeAsciiLong(devMin, 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.getHeaderPadCount());
}
private void writeOldAsciiEntry(final CpioArchiveEntry entry)
throws IOException {
long inode = entry.getInode();
long device = entry.getDevice();
if (CPIO_TRAILER.equals(entry.getName())) {
inode = device = 0;
} else {
if (inode == 0 && device == 0) {
inode = nextArtificalDeviceAndInode & 0777777;
device = (nextArtificalDeviceAndInode++ >> 18) & 0777777;
} else {
nextArtificalDeviceAndInode =
Math.max(nextArtificalDeviceAndInode,
inode + 01000000 * device) + 1;
}
}
writeAsciiLong(device, 6, 8);
writeAsciiLong(inode, 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.getEntrySize(), 11, 8);
writeCString(entry.getName());
}
private void writeOldBinaryEntry(final CpioArchiveEntry entry,
final boolean swapHalfWord) throws IOException {
long inode = entry.getInode();
long device = entry.getDevice();
if (CPIO_TRAILER.equals(entry.getName())) {
inode = device = 0;
} else {
if (inode == 0 && device == 0) {
inode = nextArtificalDeviceAndInode & 0xFFFF;
device = (nextArtificalDeviceAndInode++ >> 16) & 0xFFFF;
} else {
nextArtificalDeviceAndInode =
Math.max(nextArtificalDeviceAndInode,
inode + 0x10000 * device) + 1;
}
}
writeBinaryLong(device, 2, swapHalfWord);
writeBinaryLong(inode, 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.getEntrySize(), 4, swapHalfWord);
writeCString(entry.getName());
pad(entry.getHeaderPadCount());
}
@Override
public void closeArchiveEntry() throws IOException {
if (finished) {
throw new IOException("stream has already been finished");
}
ensureOpen();
if (entry == null) {
throw new IOException("trying to close non-existent entry");
}
if (this.entry.getEntrySize() != this.written) {
throw new IOException("invalid entry size (expected "
+ this.entry.getEntrySize() + " but got " + this.written + " bytes)");
}
pad(this.entry.getDataPadCount());
if (this.entry.getFormat() == FORMAT_NEW_CRC && this.crc != this.entry.getChksum()) {
throw new IOException("CRC Error");
}
this.entry = 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 java.io.IOException if an I/O error has occurred or if a CPIO file error has occurred
*/
@Override
public void write(final byte[] b, final int off, final int len) throws IOException {
ensureOpen();
getWatcher().updateBytesTransferred(len);
if (off < 0 || len < 0 || off > b.length - len) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
if (this.entry == null) {
throw new IOException("no current CPIO entry");
}
if (this.written + len > this.entry.getEntrySize()) {
throw new IOException("attempt to write past end of STORED entry");
}
out.write(b, off, len);
this.written += len;
if (this.entry.getFormat() == 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 java.io.IOException if an I/O exception has occurred or if a CPIO file error has
* occurred
*/
@Override
public void finish() throws IOException {
ensureOpen();
if (finished) {
throw new IOException("This archive has already been finished");
}
if (this.entry != null) {
throw new IOException("This archive contains unclosed entries.");
}
this.entry = new CpioArchiveEntry(this.entryFormat);
this.entry.setName(CPIO_TRAILER);
this.entry.setNumberOfLinks(1);
writeHeader(this.entry);
closeArchiveEntry();
int lengthOfLastBlock = (int) (out.getBytesWritten() % blockSize);
if (lengthOfLastBlock != 0) {
pad(blockSize - lengthOfLastBlock);
}
finished = true;
}
/**
* Closes the CPIO output stream as well as the stream being filtered.
*
* @throws java.io.IOException if an I/O error has occurred or if a CPIO file error has
* occurred
*/
@Override
public void close() throws IOException {
if (!finished) {
finish();
}
if (!this.closed) {
out.close();
this.closed = true;
}
}
private void pad(int count) throws IOException {
if (count > 0) {
byte buff[] = new byte[count];
out.write(buff);
}
}
private void writeBinaryLong(final long number, final int length,
final boolean swapHalfWord) throws IOException {
byte tmp[] = CpioUtil.long2byteArray(number, length, swapHalfWord);
out.write(tmp);
}
private void writeAsciiLong(final long number, final int length,
final int radix) throws IOException {
StringBuilder tmp = new StringBuilder();
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);
}
byte[] b = ArchiveUtils.toAsciiBytes(tmpStr);
out.write(b);
}
/**
* Writes an ASCII string to the stream followed by \0
*
* @param str the String to write
* @throws java.io.IOException if the string couldn't be written
*/
private void writeCString(final String str) throws IOException {
byte[] b = ArchiveUtils.toAsciiBytes(str);
out.write(b);
out.write('\0');
}
}