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