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