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