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