package org.apache.commons.compress.archivers.sevenz;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.LinkedList;
import java.util.Map;
import java.util.zip.CRC32;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.utils.CountingOutputStream;
public class SevenZOutputFile implements Closeable {
private final SeekableByteChannel channel;
private final List<SevenZArchiveEntry> files = new ArrayList<>();
private int numNonEmptyStreams = 0;
private final CRC32 crc32 = new CRC32();
private final CRC32 compressedCrc32 = new CRC32();
private long fileBytesWritten = 0;
private boolean finished = false;
private CountingOutputStream currentOutputStream;
private CountingOutputStream[] additionalCountingStreams;
private Iterable<? extends SevenZMethodConfiguration> contentMethods =
Collections.singletonList(new SevenZMethodConfiguration(SevenZMethod.LZMA2));
private final Map<SevenZArchiveEntry, long[]> additionalSizes = new HashMap<>();
public SevenZOutputFile(final File fileName) throws IOException {
this(Files.newByteChannel(fileName.toPath(),
EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)));
}
public SevenZOutputFile(final SeekableByteChannel channel) throws IOException {
this.channel = channel;
channel.position(SevenZFile.SIGNATURE_HEADER_SIZE);
}
public void setContentCompression(final SevenZMethod method) {
setContentMethods(Collections.singletonList(new SevenZMethodConfiguration(method)));
}
public void setContentMethods(final Iterable<? extends SevenZMethodConfiguration> methods) {
this.contentMethods = reverse(methods);
}
@Override
public void close() throws IOException {
try {
if (!finished) {
finish();
}
} finally {
channel.close();
}
}
public SevenZArchiveEntry createArchiveEntry(final File inputFile,
final String entryName) throws IOException {
final SevenZArchiveEntry entry = new SevenZArchiveEntry();
entry.setDirectory(inputFile.isDirectory());
entry.setName(entryName);
entry.setLastModifiedDate(new Date(inputFile.lastModified()));
return entry;
}
public void putArchiveEntry(final ArchiveEntry archiveEntry) throws IOException {
final SevenZArchiveEntry entry = (SevenZArchiveEntry) archiveEntry;
files.add(entry);
}
public void closeArchiveEntry() throws IOException {
if (currentOutputStream != null) {
currentOutputStream.flush();
currentOutputStream.close();
}
final SevenZArchiveEntry entry = files.get(files.size() - 1);
if (fileBytesWritten > 0) {
entry.setHasStream(true);
++numNonEmptyStreams;
entry.setSize(currentOutputStream.getBytesWritten());
entry.setCompressedSize(fileBytesWritten);
entry.setCrcValue(crc32.getValue());
entry.setCompressedCrcValue(compressedCrc32.getValue());
entry.setHasCrc(true);
if (additionalCountingStreams != null) {
final long[] sizes = new long[additionalCountingStreams.length];
for (int i = 0; i < additionalCountingStreams.length; i++) {
sizes[i] = additionalCountingStreams[i].getBytesWritten();
}
additionalSizes.put(entry, sizes);
}
} else {
entry.setHasStream(false);
entry.setSize(0);
entry.setCompressedSize(0);
entry.setHasCrc(false);
}
currentOutputStream = null;
additionalCountingStreams = null;
crc32.reset();
compressedCrc32.reset();
fileBytesWritten = 0;
}
public void write(final int b) throws IOException {
getCurrentOutputStream().write(b);
}
public void write(final byte[] b) throws IOException {
write(b, 0, b.length);
}
public void write(final byte[] b, final int off, final int len) throws IOException {
if (len > 0) {
getCurrentOutputStream().write(b, off, len);
}
}
public void finish() throws IOException {
if (finished) {
throw new IOException("This archive has already been finished");
}
finished = true;
final long headerPosition = channel.position();
final ByteArrayOutputStream headerBaos = new ByteArrayOutputStream();
final DataOutputStream header = new DataOutputStream(headerBaos);
writeHeader(header);
header.flush();
final byte[] headerBytes = headerBaos.toByteArray();
channel.write(ByteBuffer.wrap(headerBytes));
final CRC32 crc32 = new CRC32();
crc32.update(headerBytes);
ByteBuffer bb = ByteBuffer.allocate(SevenZFile.sevenZSignature.length
+ 2
+ 4
+ 8
+ 8
+ 4 )
.order(ByteOrder.LITTLE_ENDIAN);
channel.position(0);
bb.put(SevenZFile.sevenZSignature);
bb.put((byte) 0).put((byte) 2);
bb.putInt(0);
bb.putLong(headerPosition - SevenZFile.SIGNATURE_HEADER_SIZE)
.putLong(0xffffFFFFL & headerBytes.length)
.putInt((int) crc32.getValue());
crc32.reset();
crc32.update(bb.array(), SevenZFile.sevenZSignature.length + 6, 20);
bb.putInt(SevenZFile.sevenZSignature.length + 2, (int) crc32.getValue());
bb.flip();
channel.write(bb);
}
private OutputStream getCurrentOutputStream() throws IOException {
if (currentOutputStream == null) {
currentOutputStream = setupFileOutputStream();
}
return currentOutputStream;
}
private CountingOutputStream setupFileOutputStream() throws IOException {
if (files.isEmpty()) {
throw new IllegalStateException("No current 7z entry");
}
OutputStream out = new OutputStreamWrapper();
final ArrayList<CountingOutputStream> moreStreams = new ArrayList<>();
boolean first = true;
for (final SevenZMethodConfiguration m : getContentMethods(files.get(files.size() - 1))) {
if (!first) {
final CountingOutputStream cos = new CountingOutputStream(out);
moreStreams.add(cos);
out = cos;
}
out = Coders.addEncoder(out, m.getMethod(), m.getOptions());
first = false;
}
if (!moreStreams.isEmpty()) {
additionalCountingStreams = moreStreams.toArray(new CountingOutputStream[0]);
}
return new CountingOutputStream(out) {
@Override
public void write(final int b) throws IOException {
super.write(b);
crc32.update(b);
}
@Override
public void write(final byte[] b) throws IOException {
super.write(b);
crc32.update(b);
}
@Override
public void write(final byte[] b, final int off, final int len)
throws IOException {
super.write(b, off, len);
crc32.update(b, off, len);
}
};
}
private Iterable<? extends SevenZMethodConfiguration> getContentMethods(final SevenZArchiveEntry entry) {
final Iterable<? extends SevenZMethodConfiguration> ms = entry.getContentMethods();
return ms == null ? contentMethods : ms;
}
private void (final DataOutput header) throws IOException {
header.write(NID.kHeader);
header.write(NID.kMainStreamsInfo);
writeStreamsInfo(header);
writeFilesInfo(header);
header.write(NID.kEnd);
}
private void writeStreamsInfo(final DataOutput header) throws IOException {
if (numNonEmptyStreams > 0) {
writePackInfo(header);
writeUnpackInfo(header);
}
writeSubStreamsInfo(header);
header.write(NID.kEnd);
}
private void writePackInfo(final DataOutput header) throws IOException {
header.write(NID.kPackInfo);
writeUint64(header, 0);
writeUint64(header, 0xffffFFFFL & numNonEmptyStreams);
header.write(NID.kSize);
for (final SevenZArchiveEntry entry : files) {
if (entry.hasStream()) {
writeUint64(header, entry.getCompressedSize());
}
}
header.write(NID.kCRC);
header.write(1);
for (final SevenZArchiveEntry entry : files) {
if (entry.hasStream()) {
header.writeInt(Integer.reverseBytes((int) entry.getCompressedCrcValue()));
}
}
header.write(NID.kEnd);
}
private void writeUnpackInfo(final DataOutput header) throws IOException {
header.write(NID.kUnpackInfo);
header.write(NID.kFolder);
writeUint64(header, numNonEmptyStreams);
header.write(0);
for (final SevenZArchiveEntry entry : files) {
if (entry.hasStream()) {
writeFolder(header, entry);
}
}
header.write(NID.kCodersUnpackSize);
for (final SevenZArchiveEntry entry : files) {
if (entry.hasStream()) {
final long[] moreSizes = additionalSizes.get(entry);
if (moreSizes != null) {
for (final long s : moreSizes) {
writeUint64(header, s);
}
}
writeUint64(header, entry.getSize());
}
}
header.write(NID.kCRC);
header.write(1);
for (final SevenZArchiveEntry entry : files) {
if (entry.hasStream()) {
header.writeInt(Integer.reverseBytes((int) entry.getCrcValue()));
}
}
header.write(NID.kEnd);
}
private void writeFolder(final DataOutput header, final SevenZArchiveEntry entry) throws IOException {
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
int numCoders = 0;
for (final SevenZMethodConfiguration m : getContentMethods(entry)) {
numCoders++;
writeSingleCodec(m, bos);
}
writeUint64(header, numCoders);
header.write(bos.toByteArray());
for (long i = 0; i < numCoders - 1; i++) {
writeUint64(header, i + 1);
writeUint64(header, i);
}
}
private void writeSingleCodec(final SevenZMethodConfiguration m, final OutputStream bos) throws IOException {
final byte[] id = m.getMethod().getId();
final byte[] properties = Coders.findByMethod(m.getMethod())
.getOptionsAsProperties(m.getOptions());
int codecFlags = id.length;
if (properties.length > 0) {
codecFlags |= 0x20;
}
bos.write(codecFlags);
bos.write(id);
if (properties.length > 0) {
bos.write(properties.length);
bos.write(properties);
}
}
private void writeSubStreamsInfo(final DataOutput header) throws IOException {
header.write(NID.kSubStreamsInfo);
header.write(NID.kEnd);
}
private void writeFilesInfo(final DataOutput header) throws IOException {
header.write(NID.kFilesInfo);
writeUint64(header, files.size());
writeFileEmptyStreams(header);
writeFileEmptyFiles(header);
writeFileAntiItems(header);
writeFileNames(header);
writeFileCTimes(header);
writeFileATimes(header);
writeFileMTimes(header);
writeFileWindowsAttributes(header);
header.write(NID.kEnd);
}
private void writeFileEmptyStreams(final DataOutput header) throws IOException {
boolean hasEmptyStreams = false;
for (final SevenZArchiveEntry entry : files) {
if (!entry.hasStream()) {
hasEmptyStreams = true;
break;
}
}
if (hasEmptyStreams) {
header.write(NID.kEmptyStream);
final BitSet emptyStreams = new BitSet(files.size());
for (int i = 0; i < files.size(); i++) {
emptyStreams.set(i, !files.get(i).hasStream());
}
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final DataOutputStream out = new DataOutputStream(baos);
writeBits(out, emptyStreams, files.size());
out.flush();
final byte[] contents = baos.toByteArray();
writeUint64(header, contents.length);
header.write(contents);
}
}
private void writeFileEmptyFiles(final DataOutput header) throws IOException {
boolean hasEmptyFiles = false;
int emptyStreamCounter = 0;
final BitSet emptyFiles = new BitSet(0);
for (final SevenZArchiveEntry file1 : files) {
if (!file1.hasStream()) {
final boolean isDir = file1.isDirectory();
emptyFiles.set(emptyStreamCounter++, !isDir);
hasEmptyFiles |= !isDir;
}
}
if (hasEmptyFiles) {
header.write(NID.kEmptyFile);
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final DataOutputStream out = new DataOutputStream(baos);
writeBits(out, emptyFiles, emptyStreamCounter);
out.flush();
final byte[] contents = baos.toByteArray();
writeUint64(header, contents.length);
header.write(contents);
}
}
private void writeFileAntiItems(final DataOutput header) throws IOException {
boolean hasAntiItems = false;
final BitSet antiItems = new BitSet(0);
int antiItemCounter = 0;
for (final SevenZArchiveEntry file1 : files) {
if (!file1.hasStream()) {
final boolean isAnti = file1.isAntiItem();
antiItems.set(antiItemCounter++, isAnti);
hasAntiItems |= isAnti;
}
}
if (hasAntiItems) {
header.write(NID.kAnti);
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final DataOutputStream out = new DataOutputStream(baos);
writeBits(out, antiItems, antiItemCounter);
out.flush();
final byte[] contents = baos.toByteArray();
writeUint64(header, contents.length);
header.write(contents);
}
}
private void writeFileNames(final DataOutput header) throws IOException {
header.write(NID.kName);
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final DataOutputStream out = new DataOutputStream(baos);
out.write(0);
for (final SevenZArchiveEntry entry : files) {
out.write(entry.getName().getBytes("UTF-16LE"));
out.writeShort(0);
}
out.flush();
final byte[] contents = baos.toByteArray();
writeUint64(header, contents.length);
header.write(contents);
}
private void writeFileCTimes(final DataOutput header) throws IOException {
int numCreationDates = 0;
for (final SevenZArchiveEntry entry : files) {
if (entry.getHasCreationDate()) {
++numCreationDates;
}
}
if (numCreationDates > 0) {
header.write(NID.kCTime);
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final DataOutputStream out = new DataOutputStream(baos);
if (numCreationDates != files.size()) {
out.write(0);
final BitSet cTimes = new BitSet(files.size());
for (int i = 0; i < files.size(); i++) {
cTimes.set(i, files.get(i).getHasCreationDate());
}
writeBits(out, cTimes, files.size());
} else {
out.write(1);
}
out.write(0);
for (final SevenZArchiveEntry entry : files) {
if (entry.getHasCreationDate()) {
out.writeLong(Long.reverseBytes(
SevenZArchiveEntry.javaTimeToNtfsTime(entry.getCreationDate())));
}
}
out.flush();
final byte[] contents = baos.toByteArray();
writeUint64(header, contents.length);
header.write(contents);
}
}
private void writeFileATimes(final DataOutput header) throws IOException {
int numAccessDates = 0;
for (final SevenZArchiveEntry entry : files) {
if (entry.getHasAccessDate()) {
++numAccessDates;
}
}
if (numAccessDates > 0) {
header.write(NID.kATime);
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final DataOutputStream out = new DataOutputStream(baos);
if (numAccessDates != files.size()) {
out.write(0);
final BitSet aTimes = new BitSet(files.size());
for (int i = 0; i < files.size(); i++) {
aTimes.set(i, files.get(i).getHasAccessDate());
}
writeBits(out, aTimes, files.size());
} else {
out.write(1);
}
out.write(0);
for (final SevenZArchiveEntry entry : files) {
if (entry.getHasAccessDate()) {
out.writeLong(Long.reverseBytes(
SevenZArchiveEntry.javaTimeToNtfsTime(entry.getAccessDate())));
}
}
out.flush();
final byte[] contents = baos.toByteArray();
writeUint64(header, contents.length);
header.write(contents);
}
}
private void writeFileMTimes(final DataOutput header) throws IOException {
int numLastModifiedDates = 0;
for (final SevenZArchiveEntry entry : files) {
if (entry.getHasLastModifiedDate()) {
++numLastModifiedDates;
}
}
if (numLastModifiedDates > 0) {
header.write(NID.kMTime);
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final DataOutputStream out = new DataOutputStream(baos);
if (numLastModifiedDates != files.size()) {
out.write(0);
final BitSet mTimes = new BitSet(files.size());
for (int i = 0; i < files.size(); i++) {
mTimes.set(i, files.get(i).getHasLastModifiedDate());
}
writeBits(out, mTimes, files.size());
} else {
out.write(1);
}
out.write(0);
for (final SevenZArchiveEntry entry : files) {
if (entry.getHasLastModifiedDate()) {
out.writeLong(Long.reverseBytes(
SevenZArchiveEntry.javaTimeToNtfsTime(entry.getLastModifiedDate())));
}
}
out.flush();
final byte[] contents = baos.toByteArray();
writeUint64(header, contents.length);
header.write(contents);
}
}
private void writeFileWindowsAttributes(final DataOutput header) throws IOException {
int numWindowsAttributes = 0;
for (final SevenZArchiveEntry entry : files) {
if (entry.getHasWindowsAttributes()) {
++numWindowsAttributes;
}
}
if (numWindowsAttributes > 0) {
header.write(NID.kWinAttributes);
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final DataOutputStream out = new DataOutputStream(baos);
if (numWindowsAttributes != files.size()) {
out.write(0);
final BitSet attributes = new BitSet(files.size());
for (int i = 0; i < files.size(); i++) {
attributes.set(i, files.get(i).getHasWindowsAttributes());
}
writeBits(out, attributes, files.size());
} else {
out.write(1);
}
out.write(0);
for (final SevenZArchiveEntry entry : files) {
if (entry.getHasWindowsAttributes()) {
out.writeInt(Integer.reverseBytes(entry.getWindowsAttributes()));
}
}
out.flush();
final byte[] contents = baos.toByteArray();
writeUint64(header, contents.length);
header.write(contents);
}
}
private void writeUint64(final DataOutput header, long value) throws IOException {
int firstByte = 0;
int mask = 0x80;
int i;
for (i = 0; i < 8; i++) {
if (value < ((1L << ( 7 * (i + 1))))) {
firstByte |= (value >>> (8 * i));
break;
}
firstByte |= mask;
mask >>>= 1;
}
header.write(firstByte);
for (; i > 0; i--) {
header.write((int) (0xff & value));
value >>>= 8;
}
}
private void writeBits(final DataOutput header, final BitSet bits, final int length) throws IOException {
int cache = 0;
int shift = 7;
for (int i = 0; i < length; i++) {
cache |= ((bits.get(i) ? 1 : 0) << shift);
if (--shift < 0) {
header.write(cache);
shift = 7;
cache = 0;
}
}
if (shift != 7) {
header.write(cache);
}
}
private static <T> Iterable<T> reverse(final Iterable<T> i) {
final LinkedList<T> l = new LinkedList<>();
for (final T t : i) {
l.addFirst(t);
}
return l;
}
private class OutputStreamWrapper extends OutputStream {
private static final int BUF_SIZE = 8192;
private final ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE);
@Override
public void write(final int b) throws IOException {
buffer.clear();
buffer.put((byte) b).flip();
channel.write(buffer);
compressedCrc32.update(b);
fileBytesWritten++;
}
@Override
public void write(final byte[] b) throws IOException {
OutputStreamWrapper.this.write(b, 0, b.length);
}
@Override
public void write(final byte[] b, final int off, final int len)
throws IOException {
if (len > BUF_SIZE) {
channel.write(ByteBuffer.wrap(b, off, len));
} else {
buffer.clear();
buffer.put(b, off, len).flip();
channel.write(buffer);
}
compressedCrc32.update(b, off, len);
fileBytesWritten += len;
}
@Override
public void flush() throws IOException {
}
@Override
public void close() throws IOException {
}
}
}