Java tutorial
/* * 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 bobs.is.compress.sevenzip; 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.io.RandomAccessFile; import java.util.ArrayList; import java.util.BitSet; import java.util.Collections; import java.util.Date; 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; /** * Writes a 7z file. * @since 1.6 */ public class SevenZOutputFile implements Closeable { private final RandomAccessFile file; private final List<SevenZArchiveEntry> files = new ArrayList<SevenZArchiveEntry>(); 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<SevenZArchiveEntry, long[]>(); /** * Opens file to write a 7z archive to. * * @param filename name of the file to write to * @throws IOException if opening the file fails */ public SevenZOutputFile(final File filename) throws IOException { file = new RandomAccessFile(filename, "rw"); file.seek(SevenZFile.SIGNATURE_HEADER_SIZE); } /** * Sets the default compression method to use for entry contents - the * default is LZMA2. * * <p>Currently only {@link SevenZMethod#COPY}, {@link * SevenZMethod#LZMA2}, {@link SevenZMethod#BZIP2} and {@link * SevenZMethod#DEFLATE} are supported.</p> * * <p>This is a short form for passing a single-element iterable * to {@link #setContentMethods}.</p> * @param method the default compression method */ public void setContentCompression(final SevenZMethod method) { setContentMethods(Collections.singletonList(new SevenZMethodConfiguration(method))); } /** * Sets the default (compression) methods to use for entry contents - the * default is LZMA2. * * <p>Currently only {@link SevenZMethod#COPY}, {@link * SevenZMethod#LZMA2}, {@link SevenZMethod#BZIP2} and {@link * SevenZMethod#DEFLATE} are supported.</p> * * <p>The methods will be consulted in iteration order to create * the final output.</p> * * @since 1.8 * @param methods the default (compression) methods */ public void setContentMethods(final Iterable<? extends SevenZMethodConfiguration> methods) { this.contentMethods = reverse(methods); } /** * Closes the archive, calling {@link #finish} if necessary. * * @throws IOException on error */ @Override public void close() throws IOException { if (!finished) { finish(); } file.close(); } /** * Create an archive entry using the inputFile and entryName provided. * * @param inputFile file to create an entry from * @param entryName the name to use * @return the ArchiveEntry set up with details from the file * * @throws IOException on error */ 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; } /** * Records an archive entry to add. * * The caller must then write the content to the archive and call * {@link #closeArchiveEntry()} to complete the process. * * @param archiveEntry describes the entry * @throws IOException on error */ public void putArchiveEntry(final ArchiveEntry archiveEntry) throws IOException { final SevenZArchiveEntry entry = (SevenZArchiveEntry) archiveEntry; files.add(entry); } /** * Closes the archive entry. * @throws IOException on error */ 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; } /** * Writes a byte to the current archive entry. * @param b The byte to be written. * @throws IOException on error */ public void write(final int b) throws IOException { getCurrentOutputStream().write(b); } /** * Writes a byte array to the current archive entry. * @param b The byte array to be written. * @throws IOException on error */ public void write(final byte[] b) throws IOException { write(b, 0, b.length); } /** * Writes part of a byte array to the current archive entry. * @param b The byte array to be written. * @param off offset into the array to start writing from * @param len number of bytes to write * @throws IOException on error */ public void write(final byte[] b, final int off, final int len) throws IOException { if (len > 0) { getCurrentOutputStream().write(b, off, len); } } /** * Finishes the addition of entries to this archive, without closing it. * * @throws IOException if archive is already closed. */ public void finish() throws IOException { if (finished) { throw new IOException("This archive has already been finished"); } finished = true; final long headerPosition = file.getFilePointer(); final ByteArrayOutputStream headerBaos = new ByteArrayOutputStream(); final DataOutputStream header = new DataOutputStream(headerBaos); writeHeader(header); header.flush(); final byte[] headerBytes = headerBaos.toByteArray(); file.write(headerBytes); final CRC32 crc32 = new CRC32(); // signature header file.seek(0); file.write(SevenZFile.sevenZSignature); // version file.write(0); file.write(2); // start header final ByteArrayOutputStream startHeaderBaos = new ByteArrayOutputStream(); final DataOutputStream startHeaderStream = new DataOutputStream(startHeaderBaos); startHeaderStream.writeLong(Long.reverseBytes(headerPosition - SevenZFile.SIGNATURE_HEADER_SIZE)); startHeaderStream.writeLong(Long.reverseBytes(0xffffFFFFL & headerBytes.length)); crc32.reset(); crc32.update(headerBytes); startHeaderStream.writeInt(Integer.reverseBytes((int) crc32.getValue())); startHeaderStream.flush(); final byte[] startHeaderBytes = startHeaderBaos.toByteArray(); crc32.reset(); crc32.update(startHeaderBytes); file.writeInt(Integer.reverseBytes((int) crc32.getValue())); file.write(startHeaderBytes); } /* * Creation of output stream is deferred until data is actually * written as some codecs might write header information even for * empty streams and directories otherwise. */ 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<CountingOutputStream>(); 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[moreStreams.size()]); } 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 writeHeader(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); // "allAreDefined" == true 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); // "allAreDefined" == true 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 (int 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.kCRC); // header.write(1); // for (final SevenZArchiveEntry entry : files) { // if (entry.getHasCrc()) { // header.writeInt(Integer.reverseBytes(entry.getCrc())); // } // } // 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); // "allAreDefined" == true } 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); // "allAreDefined" == true } 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); // "allAreDefined" == true } 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); // "allAreDefined" == true } 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<T>(); for (final T t : i) { l.addFirst(t); } return l; } private class OutputStreamWrapper extends OutputStream { @Override public void write(final int b) throws IOException { file.write(b); 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 { file.write(b, off, len); compressedCrc32.update(b, off, len); fileBytesWritten += len; } @Override public void flush() throws IOException { // no reason to flush a RandomAccessFile } @Override public void close() throws IOException { // the file will be closed by the containing class's close method } } }