    001/*
    002 *  Licensed to the Apache Software Foundation (ASF) under one or more
    003 *  contributor license agreements.  See the NOTICE file distributed with
    004 *  this work for additional information regarding copyright ownership.
    005 *  The ASF licenses this file to You under the Apache License, Version 2.0
    006 *  (the "License"); you may not use this file except in compliance with
    007 *  the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 *  Unless required by applicable law or agreed to in writing, software
    012 *  distributed under the License is distributed on an "AS IS" BASIS,
    013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 *  See the License for the specific language governing permissions and
    015 *  limitations under the License.
    016 */
    017package org.apache.commons.compress.archivers.sevenz;
    018
    019import static java.nio.charset.StandardCharsets.UTF_16LE;
    020
    021import java.io.BufferedInputStream;
    022import java.io.ByteArrayOutputStream;
    023import java.io.Closeable;
    024import java.io.DataOutput;
    025import java.io.DataOutputStream;
    026import java.io.File;
    027import java.io.IOException;
    028import java.io.InputStream;
    029import java.io.OutputStream;
    030import java.nio.ByteBuffer;
    031import java.nio.ByteOrder;
    032import java.nio.channels.SeekableByteChannel;
    033import java.nio.file.Files;
    034import java.nio.file.LinkOption;
    035import java.nio.file.OpenOption;
    036import java.nio.file.Path;
    037import java.nio.file.StandardOpenOption;
    038import java.nio.file.attribute.BasicFileAttributes;
    039import java.util.ArrayList;
    040import java.util.Arrays;
    041import java.util.BitSet;
    042import java.util.Collections;
    043import java.util.Date;
    044import java.util.EnumSet;
    045import java.util.HashMap;
    046import java.util.LinkedList;
    047import java.util.List;
    048import java.util.Map;
    049import java.util.stream.Collectors;
    050import java.util.stream.Stream;
    051import java.util.stream.StreamSupport;
    052import java.util.zip.CRC32;
    053
    054import org.apache.commons.compress.archivers.ArchiveEntry;
    055import org.apache.commons.compress.utils.CountingOutputStream;
    056import org.apache.commons.compress.utils.TimeUtils;
    057
    058/**
    059 * Writes a 7z file.
    060 *
    061 * @since 1.6
    062 */
    063public class SevenZOutputFile implements Closeable {
    064
    065    private final class OutputStreamWrapper extends OutputStream {
    066
    067        private static final int BUF_SIZE = 8192;
    068        private final ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE);
    069
    070        @Override
    071        public void close() throws IOException {
    072            // the file will be closed by the containing class's close method
    073        }
    074
    075        @Override
    076        public void flush() throws IOException {
    077            // no reason to flush the channel
    078        }
    079
    080        @Override
    081        public void write(final byte[] b) throws IOException {
    082            OutputStreamWrapper.this.write(b, 0, b.length);
    083        }
    084
    085        @Override
    086        public void write(final byte[] b, final int off, final int len)
    087            throws IOException {
    088            if (len > BUF_SIZE) {
    089                channel.write(ByteBuffer.wrap(b, off, len));
    090            } else {
    091                buffer.clear();
    092                buffer.put(b, off, len).flip();
    093                channel.write(buffer);
    094            }
    095            compressedCrc32.update(b, off, len);
    096            fileBytesWritten += len;
    097        }
    098
    099        @Override
    100        public void write(final int b) throws IOException {
    101            buffer.clear();
    102            buffer.put((byte) b).flip();
    103            channel.write(buffer);
    104            compressedCrc32.update(b);
    105            fileBytesWritten++;
    106        }
    107    }
    108    private static <T> Iterable<T> reverse(final Iterable<T> i) {
    109        final LinkedList<T> l = new LinkedList<>();
    110        for (final T t : i) {
    111            l.addFirst(t);
    112        }
    113        return l;
    114    }
    115    private final SeekableByteChannel channel;
    116    private final List<SevenZArchiveEntry> files = new ArrayList<>();
    117    private int numNonEmptyStreams;
    118    private final CRC32 crc32 = new CRC32();
    119    private final CRC32 compressedCrc32 = new CRC32();
    120    private long fileBytesWritten;
    121    private boolean finished;
    122    private CountingOutputStream currentOutputStream;
    123    private CountingOutputStream[] additionalCountingStreams;
    124    private Iterable<? extends SevenZMethodConfiguration> contentMethods =
    125            Collections.singletonList(new SevenZMethodConfiguration(SevenZMethod.LZMA2));
    126
    127    private final Map<SevenZArchiveEntry, long[]> additionalSizes = new HashMap<>();
    128
    129    private AES256Options aes256Options;
    130
    131    /**
    132     * Opens file to write a 7z archive to.
    133     *
    134     * @param fileName the file to write to
    135     * @throws IOException if opening the file fails
    136     */
    137    public SevenZOutputFile(final File fileName) throws IOException {
    138        this(fileName, null);
    139    }
    140
    141    /**
    142     * Opens file to write a 7z archive to.
    143     *
    144     * @param fileName the file to write to
    145     * @param password optional password if the archive has to be encrypted
    146     * @throws IOException if opening the file fails
    147     * @since 1.23
    148     */
    149    public SevenZOutputFile(final File fileName, final char[] password) throws IOException {
    150        this(
    151            Files.newByteChannel(
    152                fileName.toPath(),
    153                EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)
    154            ),
    155            password
    156        );
    157    }
    158
    159    /**
    160     * Prepares channel to write a 7z archive to.
    161     *
    162     * <p>{@link
    163     * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
    164     * allows you to write to an in-memory archive.</p>
    165     *
    166     * @param channel the channel to write to
    167     * @throws IOException if the channel cannot be positioned properly
    168     * @since 1.13
    169     */
    170    public SevenZOutputFile(final SeekableByteChannel channel) throws IOException {
    171        this(channel, null);
    172    }
    173
    174    /**
    175     * Prepares channel to write a 7z archive to.
    176     *
    177     * <p>{@link
    178     * org.apache.commons.compress.utils.SeekableInMemoryByteChannel}
    179     * allows you to write to an in-memory archive.</p>
    180     *
    181     * @param channel the channel to write to
    182     * @param password optional password if the archive has to be encrypted
    183     * @throws IOException if the channel cannot be positioned properly
    184     * @since 1.23
    185     */
    186    public SevenZOutputFile(final SeekableByteChannel channel, final char[] password) throws IOException {
    187        this.channel = channel;
    188        channel.position(SevenZFile.SIGNATURE_HEADER_SIZE);
    189        if (password != null) {
    190            this.aes256Options = new AES256Options(password);
    191        }
    192    }
    193
    194    /**
    195     * Closes the archive, calling {@link #finish} if necessary.
    196     *
    197     * @throws IOException on error
    198     */
    199    @Override
    200    public void close() throws IOException {
    201        try {
    202            if (!finished) {
    203                finish();
    204            }
    205        } finally {
    206            channel.close();
    207        }
    208    }
    209
    210    /**
    211     * Closes the archive entry.
    212     * @throws IOException on error
    213     */
    214    public void closeArchiveEntry() throws IOException {
    215        if (currentOutputStream != null) {
    216            currentOutputStream.flush();
    217            currentOutputStream.close();
    218        }
    219
    220        final SevenZArchiveEntry entry = files.get(files.size() - 1);
    221        if (fileBytesWritten > 0) { // this implies currentOutputStream != null
    222            entry.setHasStream(true);
    223            ++numNonEmptyStreams;
    224            entry.setSize(currentOutputStream.getBytesWritten()); //NOSONAR
    225            entry.setCompressedSize(fileBytesWritten);
    226            entry.setCrcValue(crc32.getValue());
    227            entry.setCompressedCrcValue(compressedCrc32.getValue());
    228            entry.setHasCrc(true);
    229            if (additionalCountingStreams != null) {
    230                final long[] sizes = new long[additionalCountingStreams.length];
    231                Arrays.setAll(sizes, i -> additionalCountingStreams[i].getBytesWritten());
    232                additionalSizes.put(entry, sizes);
    233            }
    234        } else {
    235            entry.setHasStream(false);
    236            entry.setSize(0);
    237            entry.setCompressedSize(0);
    238            entry.setHasCrc(false);
    239        }
    240        currentOutputStream = null;
    241        additionalCountingStreams = null;
    242        crc32.reset();
    243        compressedCrc32.reset();
    244        fileBytesWritten = 0;
    245    }
    246
    247    /**
    248     * Create an archive entry using the inputFile and entryName provided.
    249     *
    250     * @param inputFile file to create an entry from
    251     * @param entryName the name to use
    252     * @return the ArchiveEntry set up with details from the file
    253     */
    254    public SevenZArchiveEntry createArchiveEntry(final File inputFile,
    255            final String entryName) {
    256        final SevenZArchiveEntry entry = new SevenZArchiveEntry();
    257        entry.setDirectory(inputFile.isDirectory());
    258        entry.setName(entryName);
    259        try {
    260            fillDates(inputFile.toPath(), entry);
    261        } catch (final IOException e) { // NOSONAR
    262            entry.setLastModifiedDate(new Date(inputFile.lastModified()));
    263        }
    264        return entry;
    265    }
    266
    267    /**
    268     * Create an archive entry using the inputPath and entryName provided.
    269     *
    270     * @param inputPath path to create an entry from
    271     * @param entryName the name to use
    272     * @param options options indicating how symbolic links are handled.
    273     * @return the ArchiveEntry set up with details from the file
    274     *
    275     * @throws IOException on error
    276     * @since 1.21
    277     */
    278    public SevenZArchiveEntry createArchiveEntry(final Path inputPath,
    279        final String entryName, final LinkOption... options) throws IOException {
    280        final SevenZArchiveEntry entry = new SevenZArchiveEntry();
    281        entry.setDirectory(Files.isDirectory(inputPath, options));
    282        entry.setName(entryName);
    283        fillDates(inputPath, entry, options);
    284        return entry;
    285    }
    286
    287    private void fillDates(final Path inputPath, final SevenZArchiveEntry entry,
    288        final LinkOption... options) throws IOException {
    289        final BasicFileAttributes attributes = Files.readAttributes(inputPath, BasicFileAttributes.class, options);
    290        entry.setLastModifiedTime(attributes.lastModifiedTime());
    291        entry.setCreationTime(attributes.creationTime());
    292        entry.setAccessTime(attributes.lastAccessTime());
    293    }
    294
    295    /**
    296     * Finishes the addition of entries to this archive, without closing it.
    297     *
    298     * @throws IOException if archive is already closed.
    299     */
    300    public void finish() throws IOException {
    301        if (finished) {
    302            throw new IOException("This archive has already been finished");
    303        }
    304        finished = true;
    305
    306        final long headerPosition = channel.position();
    307
    308        final ByteArrayOutputStream headerBaos = new ByteArrayOutputStream();
    309        final DataOutputStream header = new DataOutputStream(headerBaos);
    310
    311        writeHeader(header);
    312        header.flush();
    313        final byte[] headerBytes = headerBaos.toByteArray();
    314        channel.write(ByteBuffer.wrap(headerBytes));
    315
    316        final CRC32 crc32 = new CRC32();
    317        crc32.update(headerBytes);
    318
    319        final ByteBuffer bb = ByteBuffer.allocate(SevenZFile.sevenZSignature.length
    320                                            + 2 /* version */
    321                                            + 4 /* start header CRC */
    322                                            + 8 /* next header position */
    323                                            + 8 /* next header length */
    324                                            + 4 /* next header CRC */)
    325            .order(ByteOrder.LITTLE_ENDIAN);
    326        // signature header
    327        channel.position(0);
    328        bb.put(SevenZFile.sevenZSignature);
    329        // version
    330        bb.put((byte) 0).put((byte) 2);
    331
    332        // placeholder for start header CRC
    333        bb.putInt(0);
    334
    335        // start header
    336        bb.putLong(headerPosition - SevenZFile.SIGNATURE_HEADER_SIZE)
    337            .putLong(0xffffFFFFL & headerBytes.length)
    338            .putInt((int) crc32.getValue());
    339        crc32.reset();
    340        crc32.update(bb.array(), SevenZFile.sevenZSignature.length + 6, 20);
    341        bb.putInt(SevenZFile.sevenZSignature.length + 2, (int) crc32.getValue());
    342        bb.flip();
    343        channel.write(bb);
    344    }
    345
    346    private Iterable<? extends SevenZMethodConfiguration> getContentMethods(final SevenZArchiveEntry entry) {
    347        final Iterable<? extends SevenZMethodConfiguration> ms = entry.getContentMethods();
    348        Iterable<? extends SevenZMethodConfiguration> iter = ms == null ? contentMethods : ms;
    349
    350        if (aes256Options != null) {
    351            // prepend encryption
    352            iter =
    353                Stream
    354                    .concat(
    355                        Stream.of(new SevenZMethodConfiguration(SevenZMethod.AES256SHA256, aes256Options)),
    356                        StreamSupport.stream(iter.spliterator(), false)
    357                    )
    358                    .collect(Collectors.toList());
    359        }
    360        return iter;
    361    }
    362
    363    /*
    364     * Creation of output stream is deferred until data is actually
    365     * written as some codecs might write header information even for
    366     * empty streams and directories otherwise.
    367     */
    368    private OutputStream getCurrentOutputStream() throws IOException {
    369        if (currentOutputStream == null) {
    370            currentOutputStream = setupFileOutputStream();
    371        }
    372        return currentOutputStream;
    373    }
    374
    375    /**
    376     * Records an archive entry to add.
    377     *
    378     * The caller must then write the content to the archive and call
    379     * {@link #closeArchiveEntry()} to complete the process.
    380     *
    381     * @param archiveEntry describes the entry
    382     * @deprecated Use {@link #putArchiveEntry(SevenZArchiveEntry)}.
    383     */
    384    @Deprecated
    385    public void putArchiveEntry(final ArchiveEntry archiveEntry) {
    386        putArchiveEntry((SevenZArchiveEntry) archiveEntry);
    387    }
    388
    389    /**
    390     * Records an archive entry to add.
    391     *
    392     * The caller must then write the content to the archive and call
    393     * {@link #closeArchiveEntry()} to complete the process.
    394     *
    395     * @param archiveEntry describes the entry
    396     * @since 1.25.0
    397     */
    398    public void putArchiveEntry(final SevenZArchiveEntry archiveEntry) {
    399        files.add(archiveEntry);
    400    }
    401
    402    /**
    403     * Sets the default compression method to use for entry contents - the
    404     * default is LZMA2.
    405     *
    406     * <p>Currently only {@link SevenZMethod#COPY}, {@link
    407     * SevenZMethod#LZMA2}, {@link SevenZMethod#BZIP2} and {@link
    408     * SevenZMethod#DEFLATE} are supported.</p>
    409     *
    410     * <p>This is a short form for passing a single-element iterable
    411     * to {@link #setContentMethods}.</p>
    412     * @param method the default compression method
    413     */
    414    public void setContentCompression(final SevenZMethod method) {
    415        setContentMethods(Collections.singletonList(new SevenZMethodConfiguration(method)));
    416    }
    417
    418    /**
    419     * Sets the default (compression) methods to use for entry contents - the
    420     * default is LZMA2.
    421     *
    422     * <p>Currently only {@link SevenZMethod#COPY}, {@link
    423     * SevenZMethod#LZMA2}, {@link SevenZMethod#BZIP2} and {@link
    424     * SevenZMethod#DEFLATE} are supported.</p>
    425     *
    426     * <p>The methods will be consulted in iteration order to create
    427     * the final output.</p>
    428     *
    429     * @since 1.8
    430     * @param methods the default (compression) methods
    431     */
    432    public void setContentMethods(final Iterable<? extends SevenZMethodConfiguration> methods) {
    433        this.contentMethods = reverse(methods);
    434    }
    435
    436    private CountingOutputStream setupFileOutputStream() throws IOException {
    437        if (files.isEmpty()) {
    438            throw new IllegalStateException("No current 7z entry");
    439        }
    440
    441        // doesn't need to be closed, just wraps the instance field channel
    442        OutputStream out = new OutputStreamWrapper(); // NOSONAR
    443        final ArrayList<CountingOutputStream> moreStreams = new ArrayList<>();
    444        boolean first = true;
    445        for (final SevenZMethodConfiguration m : getContentMethods(files.get(files.size() - 1))) {
    446            if (!first) {
    447                final CountingOutputStream cos = new CountingOutputStream(out);
    448                moreStreams.add(cos);
    449                out = cos;
    450            }
    451            out = Coders.addEncoder(out, m.getMethod(), m.getOptions());
    452            first = false;
    453        }
    454        if (!moreStreams.isEmpty()) {
    455            additionalCountingStreams = moreStreams.toArray(new CountingOutputStream[0]);
    456        }
    457        return new CountingOutputStream(out) {
    458            @Override
    459            public void write(final byte[] b) throws IOException {
    460                super.write(b);
    461                crc32.update(b);
    462            }
    463
    464            @Override
    465            public void write(final byte[] b, final int off, final int len)
    466                throws IOException {
    467                super.write(b, off, len);
    468                crc32.update(b, off, len);
    469            }
    470
    471            @Override
    472            public void write(final int b) throws IOException {
    473                super.write(b);
    474                crc32.update(b);
    475            }
    476        };
    477    }
    478
    479    /**
    480     * Writes a byte array to the current archive entry.
    481     * @param b The byte array to be written.
    482     * @throws IOException on error
    483     */
    484    public void write(final byte[] b) throws IOException {
    485        write(b, 0, b.length);
    486    }
    487
    488    /**
    489     * Writes part of a byte array to the current archive entry.
    490     * @param b The byte array to be written.
    491     * @param off offset into the array to start writing from
    492     * @param len number of bytes to write
    493     * @throws IOException on error
    494     */
    495    public void write(final byte[] b, final int off, final int len) throws IOException {
    496        if (len > 0) {
    497            getCurrentOutputStream().write(b, off, len);
    498        }
    499    }
    500
    501    /**
    502     * Writes all of the given input stream to the current archive entry.
    503     * @param inputStream the data source.
    504     * @throws IOException if an I/O error occurs.
    505     * @since 1.21
    506     */
    507    public void write(final InputStream inputStream) throws IOException {
    508        final byte[] buffer = new byte[8024];
    509        int n = 0;
    510        while (-1 != (n = inputStream.read(buffer))) {
    511            write(buffer, 0, n);
    512        }
    513    }
    514
    515    /**
    516     * Writes a byte to the current archive entry.
    517     * @param b The byte to be written.
    518     * @throws IOException on error
    519     */
    520    public void write(final int b) throws IOException {
    521        getCurrentOutputStream().write(b);
    522    }
    523
    524    /**
    525     * Writes all of the given input stream to the current archive entry.
    526     * @param path the data source.
    527     * @param options options specifying how the file is opened.
    528     * @throws IOException if an I/O error occurs.
    529     * @since 1.21
    530     */
    531    public void write(final Path path, final OpenOption... options) throws IOException {
    532        try (InputStream in = new BufferedInputStream(Files.newInputStream(path, options))) {
    533            write(in);
    534        }
    535    }
    536
    537    private void writeBits(final DataOutput header, final BitSet bits, final int length) throws IOException {
    538        int cache = 0;
    539        int shift = 7;
    540        for (int i = 0; i < length; i++) {
    541            cache |= (bits.get(i) ? 1 : 0) << shift;
    542            if (--shift < 0) {
    543                header.write(cache);
    544                shift = 7;
    545                cache = 0;
    546            }
    547        }
    548        if (shift != 7) {
    549            header.write(cache);
    550        }
    551    }
    552
    553    private void writeFileAntiItems(final DataOutput header) throws IOException {
    554        boolean hasAntiItems = false;
    555        final BitSet antiItems = new BitSet(0);
    556        int antiItemCounter = 0;
    557        for (final SevenZArchiveEntry file1 : files) {
    558            if (!file1.hasStream()) {
    559                final boolean isAnti = file1.isAntiItem();
    560                antiItems.set(antiItemCounter++, isAnti);
    561                hasAntiItems |= isAnti;
    562            }
    563        }
    564        if (hasAntiItems) {
    565            header.write(NID.kAnti);
    566            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    567            final DataOutputStream out = new DataOutputStream(baos);
    568            writeBits(out, antiItems, antiItemCounter);
    569            out.flush();
    570            final byte[] contents = baos.toByteArray();
    571            writeUint64(header, contents.length);
    572            header.write(contents);
    573        }
    574    }
    575
    576    private void writeFileATimes(final DataOutput header) throws IOException {
    577        int numAccessDates = 0;
    578        for (final SevenZArchiveEntry entry : files) {
    579            if (entry.getHasAccessDate()) {
    580                ++numAccessDates;
    581            }
    582        }
    583        if (numAccessDates > 0) {
    584            header.write(NID.kATime);
    585
    586            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    587            final DataOutputStream out = new DataOutputStream(baos);
    588            if (numAccessDates != files.size()) {
    589                out.write(0);
    590                final BitSet aTimes = new BitSet(files.size());
    591                for (int i = 0; i < files.size(); i++) {
    592                    aTimes.set(i, files.get(i).getHasAccessDate());
    593                }
    594                writeBits(out, aTimes, files.size());
    595            } else {
    596                out.write(1); // "allAreDefined" == true
    597            }
    598            out.write(0);
    599            for (final SevenZArchiveEntry entry : files) {
    600                if (entry.getHasAccessDate()) {
    601                    final long ntfsTime = TimeUtils.toNtfsTime(entry.getAccessTime());
    602                    out.writeLong(Long.reverseBytes(ntfsTime));
    603                }
    604            }
    605            out.flush();
    606            final byte[] contents = baos.toByteArray();
    607            writeUint64(header, contents.length);
    608            header.write(contents);
    609        }
    610    }
    611
    612    private void writeFileCTimes(final DataOutput header) throws IOException {
    613        int numCreationDates = 0;
    614        for (final SevenZArchiveEntry entry : files) {
    615            if (entry.getHasCreationDate()) {
    616                ++numCreationDates;
    617            }
    618        }
    619        if (numCreationDates > 0) {
    620            header.write(NID.kCTime);
    621
    622            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    623            final DataOutputStream out = new DataOutputStream(baos);
    624            if (numCreationDates != files.size()) {
    625                out.write(0);
    626                final BitSet cTimes = new BitSet(files.size());
    627                for (int i = 0; i < files.size(); i++) {
    628                    cTimes.set(i, files.get(i).getHasCreationDate());
    629                }
    630                writeBits(out, cTimes, files.size());
    631            } else {
    632                out.write(1); // "allAreDefined" == true
    633            }
    634            out.write(0);
    635            for (final SevenZArchiveEntry entry : files) {
    636                if (entry.getHasCreationDate()) {
    637                    final long ntfsTime = TimeUtils.toNtfsTime(entry.getCreationTime());
    638                    out.writeLong(Long.reverseBytes(ntfsTime));
    639                }
    640            }
    641            out.flush();
    642            final byte[] contents = baos.toByteArray();
    643            writeUint64(header, contents.length);
    644            header.write(contents);
    645        }
    646    }
    647
    648    private void writeFileEmptyFiles(final DataOutput header) throws IOException {
    649        boolean hasEmptyFiles = false;
    650        int emptyStreamCounter = 0;
    651        final BitSet emptyFiles = new BitSet(0);
    652        for (final SevenZArchiveEntry file1 : files) {
    653            if (!file1.hasStream()) {
    654                final boolean isDir = file1.isDirectory();
    655                emptyFiles.set(emptyStreamCounter++, !isDir);
    656                hasEmptyFiles |= !isDir;
    657            }
    658        }
    659        if (hasEmptyFiles) {
    660            header.write(NID.kEmptyFile);
    661            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    662            final DataOutputStream out = new DataOutputStream(baos);
    663            writeBits(out, emptyFiles, emptyStreamCounter);
    664            out.flush();
    665            final byte[] contents = baos.toByteArray();
    666            writeUint64(header, contents.length);
    667            header.write(contents);
    668        }
    669    }
    670
    671    private void writeFileEmptyStreams(final DataOutput header) throws IOException {
    672        final boolean hasEmptyStreams = files.stream().anyMatch(entry -> !entry.hasStream());
    673        if (hasEmptyStreams) {
    674            header.write(NID.kEmptyStream);
    675            final BitSet emptyStreams = new BitSet(files.size());
    676            for (int i = 0; i < files.size(); i++) {
    677                emptyStreams.set(i, !files.get(i).hasStream());
    678            }
    679            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    680            final DataOutputStream out = new DataOutputStream(baos);
    681            writeBits(out, emptyStreams, files.size());
    682            out.flush();
    683            final byte[] contents = baos.toByteArray();
    684            writeUint64(header, contents.length);
    685            header.write(contents);
    686        }
    687    }
    688
    689    private void writeFileMTimes(final DataOutput header) throws IOException {
    690        int numLastModifiedDates = 0;
    691        for (final SevenZArchiveEntry entry : files) {
    692            if (entry.getHasLastModifiedDate()) {
    693                ++numLastModifiedDates;
    694            }
    695        }
    696        if (numLastModifiedDates > 0) {
    697            header.write(NID.kMTime);
    698
    699            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    700            final DataOutputStream out = new DataOutputStream(baos);
    701            if (numLastModifiedDates != files.size()) {
    702                out.write(0);
    703                final BitSet mTimes = new BitSet(files.size());
    704                for (int i = 0; i < files.size(); i++) {
    705                    mTimes.set(i, files.get(i).getHasLastModifiedDate());
    706                }
    707                writeBits(out, mTimes, files.size());
    708            } else {
    709                out.write(1); // "allAreDefined" == true
    710            }
    711            out.write(0);
    712            for (final SevenZArchiveEntry entry : files) {
    713                if (entry.getHasLastModifiedDate()) {
    714                    final long ntfsTime = TimeUtils.toNtfsTime(entry.getLastModifiedTime());
    715                    out.writeLong(Long.reverseBytes(ntfsTime));
    716                }
    717            }
    718            out.flush();
    719            final byte[] contents = baos.toByteArray();
    720            writeUint64(header, contents.length);
    721            header.write(contents);
    722        }
    723    }
    724
    725    private void writeFileNames(final DataOutput header) throws IOException {
    726        header.write(NID.kName);
    727
    728        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    729        final DataOutputStream out = new DataOutputStream(baos);
    730        out.write(0);
    731        for (final SevenZArchiveEntry entry : files) {
    732            out.write(entry.getName().getBytes(UTF_16LE));
    733            out.writeShort(0);
    734        }
    735        out.flush();
    736        final byte[] contents = baos.toByteArray();
    737        writeUint64(header, contents.length);
    738        header.write(contents);
    739    }
    740
    741    private void writeFilesInfo(final DataOutput header) throws IOException {
    742        header.write(NID.kFilesInfo);
    743
    744        writeUint64(header, files.size());
    745
    746        writeFileEmptyStreams(header);
    747        writeFileEmptyFiles(header);
    748        writeFileAntiItems(header);
    749        writeFileNames(header);
    750        writeFileCTimes(header);
    751        writeFileATimes(header);
    752        writeFileMTimes(header);
    753        writeFileWindowsAttributes(header);
    754        header.write(NID.kEnd);
    755    }
    756
    757    private void writeFileWindowsAttributes(final DataOutput header) throws IOException {
    758        int numWindowsAttributes = 0;
    759        for (final SevenZArchiveEntry entry : files) {
    760            if (entry.getHasWindowsAttributes()) {
    761                ++numWindowsAttributes;
    762            }
    763        }
    764        if (numWindowsAttributes > 0) {
    765            header.write(NID.kWinAttributes);
    766
    767            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    768            final DataOutputStream out = new DataOutputStream(baos);
    769            if (numWindowsAttributes != files.size()) {
    770                out.write(0);
    771                final BitSet attributes = new BitSet(files.size());
    772                for (int i = 0; i < files.size(); i++) {
    773                    attributes.set(i, files.get(i).getHasWindowsAttributes());
    774                }
    775                writeBits(out, attributes, files.size());
    776            } else {
    777                out.write(1); // "allAreDefined" == true
    778            }
    779            out.write(0);
    780            for (final SevenZArchiveEntry entry : files) {
    781                if (entry.getHasWindowsAttributes()) {
    782                    out.writeInt(Integer.reverseBytes(entry.getWindowsAttributes()));
    783                }
    784            }
    785            out.flush();
    786            final byte[] contents = baos.toByteArray();
    787            writeUint64(header, contents.length);
    788            header.write(contents);
    789        }
    790    }
    791
    792    private void writeFolder(final DataOutput header, final SevenZArchiveEntry entry) throws IOException {
    793        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    794        int numCoders = 0;
    795        for (final SevenZMethodConfiguration m : getContentMethods(entry)) {
    796            numCoders++;
    797            writeSingleCodec(m, bos);
    798        }
    799
    800        writeUint64(header, numCoders);
    801        header.write(bos.toByteArray());
    802        for (long i = 0; i < numCoders - 1; i++) {
    803            writeUint64(header, i + 1);
    804            writeUint64(header, i);
    805        }
    806    }
    807
    808    private void writeHeader(final DataOutput header) throws IOException {
    809        header.write(NID.kHeader);
    810
    811        header.write(NID.kMainStreamsInfo);
    812        writeStreamsInfo(header);
    813        writeFilesInfo(header);
    814        header.write(NID.kEnd);
    815    }
    816
    817    private void writePackInfo(final DataOutput header) throws IOException {
    818        header.write(NID.kPackInfo);
    819
    820        writeUint64(header, 0);
    821        writeUint64(header, 0xffffFFFFL & numNonEmptyStreams);
    822
    823        header.write(NID.kSize);
    824        for (final SevenZArchiveEntry entry : files) {
    825            if (entry.hasStream()) {
    826                writeUint64(header, entry.getCompressedSize());
    827            }
    828        }
    829
    830        header.write(NID.kCRC);
    831        header.write(1); // "allAreDefined" == true
    832        for (final SevenZArchiveEntry entry : files) {
    833            if (entry.hasStream()) {
    834                header.writeInt(Integer.reverseBytes((int) entry.getCompressedCrcValue()));
    835            }
    836        }
    837
    838        header.write(NID.kEnd);
    839    }
    840
    841    private void writeSingleCodec(final SevenZMethodConfiguration m, final OutputStream bos) throws IOException {
    842        final byte[] id = m.getMethod().getId();
    843        final byte[] properties = Coders.findByMethod(m.getMethod())
    844            .getOptionsAsProperties(m.getOptions());
    845
    846        int codecFlags = id.length;
    847        if (properties.length > 0) {
    848            codecFlags |= 0x20;
    849        }
    850        bos.write(codecFlags);
    851        bos.write(id);
    852
    853        if (properties.length > 0) {
    854            bos.write(properties.length);
    855            bos.write(properties);
    856        }
    857    }
    858
    859    private void writeStreamsInfo(final DataOutput header) throws IOException {
    860        if (numNonEmptyStreams > 0) {
    861            writePackInfo(header);
    862            writeUnpackInfo(header);
    863        }
    864
    865        writeSubStreamsInfo(header);
    866
    867        header.write(NID.kEnd);
    868    }
    869
    870    private void writeSubStreamsInfo(final DataOutput header) throws IOException {
    871        header.write(NID.kSubStreamsInfo);
    872        //
    873        //        header.write(NID.kCRC);
    874        //        header.write(1);
    875        //        for (final SevenZArchiveEntry entry : files) {
    876        //            if (entry.getHasCrc()) {
    877        //                header.writeInt(Integer.reverseBytes(entry.getCrc()));
    878        //            }
    879        //        }
    880        //
    881        header.write(NID.kEnd);
    882    }
    883
    884    private void writeUint64(final DataOutput header, long value) throws IOException {
    885        int firstByte = 0;
    886        int mask = 0x80;
    887        int i;
    888        for (i = 0; i < 8; i++) {
    889            if (value < 1L << 7  * (i + 1)) {
    890                firstByte |= value >>> 8 * i;
    891                break;
    892            }
    893            firstByte |= mask;
    894            mask >>>= 1;
    895        }
    896        header.write(firstByte);
    897        for (; i > 0; i--) {
    898            header.write((int) (0xff & value));
    899            value >>>= 8;
    900        }
    901    }
    902
    903    private void writeUnpackInfo(final DataOutput header) throws IOException {
    904        header.write(NID.kUnpackInfo);
    905
    906        header.write(NID.kFolder);
    907        writeUint64(header, numNonEmptyStreams);
    908        header.write(0);
    909        for (final SevenZArchiveEntry entry : files) {
    910            if (entry.hasStream()) {
    911                writeFolder(header, entry);
    912            }
    913        }
    914
    915        header.write(NID.kCodersUnpackSize);
    916        for (final SevenZArchiveEntry entry : files) {
    917            if (entry.hasStream()) {
    918                final long[] moreSizes = additionalSizes.get(entry);
    919                if (moreSizes != null) {
    920                    for (final long s : moreSizes) {
    921                        writeUint64(header, s);
    922                    }
    923                }
    924                writeUint64(header, entry.getSize());
    925            }
    926        }
    927
    928        header.write(NID.kCRC);
    929        header.write(1); // "allAreDefined" == true
    930        for (final SevenZArchiveEntry entry : files) {
    931            if (entry.hasStream()) {
    932                header.writeInt(Integer.reverseBytes((int) entry.getCrcValue()));
    933            }
    934        }
    935
    936        header.write(NID.kEnd);
    937    }
    938
    939}