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