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