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