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