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