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