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        // doesn't need to be closed, just wraps the instance field channel
313        OutputStream out = new OutputStreamWrapper(); // NOSONAR
314        final ArrayList<CountingOutputStream> moreStreams = new ArrayList<>();
315        boolean first = true;
316        for (final SevenZMethodConfiguration m : getContentMethods(files.get(files.size() - 1))) {
317            if (!first) {
318                final CountingOutputStream cos = new CountingOutputStream(out);
319                moreStreams.add(cos);
320                out = cos;
321            }
322            out = Coders.addEncoder(out, m.getMethod(), m.getOptions());
323            first = false;
324        }
325        if (!moreStreams.isEmpty()) {
326            additionalCountingStreams = moreStreams.toArray(new CountingOutputStream[0]);
327        }
328        return new CountingOutputStream(out) {
329            @Override
330            public void write(final int b) throws IOException {
331                super.write(b);
332                crc32.update(b);
333            }
334
335            @Override
336            public void write(final byte[] b) throws IOException {
337                super.write(b);
338                crc32.update(b);
339            }
340
341            @Override
342            public void write(final byte[] b, final int off, final int len)
343                throws IOException {
344                super.write(b, off, len);
345                crc32.update(b, off, len);
346            }
347        };
348    }
349
350    private Iterable<? extends SevenZMethodConfiguration> getContentMethods(final SevenZArchiveEntry entry) {
351        final Iterable<? extends SevenZMethodConfiguration> ms = entry.getContentMethods();
352        return ms == null ? contentMethods : ms;
353    }
354
355    private void writeHeader(final DataOutput header) throws IOException {
356        header.write(NID.kHeader);
357
358        header.write(NID.kMainStreamsInfo);
359        writeStreamsInfo(header);
360        writeFilesInfo(header);
361        header.write(NID.kEnd);
362    }
363
364    private void writeStreamsInfo(final DataOutput header) throws IOException {
365        if (numNonEmptyStreams > 0) {
366            writePackInfo(header);
367            writeUnpackInfo(header);
368        }
369
370        writeSubStreamsInfo(header);
371
372        header.write(NID.kEnd);
373    }
374
375    private void writePackInfo(final DataOutput header) throws IOException {
376        header.write(NID.kPackInfo);
377
378        writeUint64(header, 0);
379        writeUint64(header, 0xffffFFFFL & numNonEmptyStreams);
380
381        header.write(NID.kSize);
382        for (final SevenZArchiveEntry entry : files) {
383            if (entry.hasStream()) {
384                writeUint64(header, entry.getCompressedSize());
385            }
386        }
387
388        header.write(NID.kCRC);
389        header.write(1); // "allAreDefined" == true
390        for (final SevenZArchiveEntry entry : files) {
391            if (entry.hasStream()) {
392                header.writeInt(Integer.reverseBytes((int) entry.getCompressedCrcValue()));
393            }
394        }
395
396        header.write(NID.kEnd);
397    }
398
399    private void writeUnpackInfo(final DataOutput header) throws IOException {
400        header.write(NID.kUnpackInfo);
401
402        header.write(NID.kFolder);
403        writeUint64(header, numNonEmptyStreams);
404        header.write(0);
405        for (final SevenZArchiveEntry entry : files) {
406            if (entry.hasStream()) {
407                writeFolder(header, entry);
408            }
409        }
410
411        header.write(NID.kCodersUnpackSize);
412        for (final SevenZArchiveEntry entry : files) {
413            if (entry.hasStream()) {
414                final long[] moreSizes = additionalSizes.get(entry);
415                if (moreSizes != null) {
416                    for (final long s : moreSizes) {
417                        writeUint64(header, s);
418                    }
419                }
420                writeUint64(header, entry.getSize());
421            }
422        }
423
424        header.write(NID.kCRC);
425        header.write(1); // "allAreDefined" == true
426        for (final SevenZArchiveEntry entry : files) {
427            if (entry.hasStream()) {
428                header.writeInt(Integer.reverseBytes((int) entry.getCrcValue()));
429            }
430        }
431
432        header.write(NID.kEnd);
433    }
434
435    private void writeFolder(final DataOutput header, final SevenZArchiveEntry entry) throws IOException {
436        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
437        int numCoders = 0;
438        for (final SevenZMethodConfiguration m : getContentMethods(entry)) {
439            numCoders++;
440            writeSingleCodec(m, bos);
441        }
442
443        writeUint64(header, numCoders);
444        header.write(bos.toByteArray());
445        for (long i = 0; i < numCoders - 1; i++) {
446            writeUint64(header, i + 1);
447            writeUint64(header, i);
448        }
449    }
450
451    private void writeSingleCodec(final SevenZMethodConfiguration m, final OutputStream bos) throws IOException {
452        final byte[] id = m.getMethod().getId();
453        final byte[] properties = Coders.findByMethod(m.getMethod())
454            .getOptionsAsProperties(m.getOptions());
455
456        int codecFlags = id.length;
457        if (properties.length > 0) {
458            codecFlags |= 0x20;
459        }
460        bos.write(codecFlags);
461        bos.write(id);
462
463        if (properties.length > 0) {
464            bos.write(properties.length);
465            bos.write(properties);
466        }
467    }
468
469    private void writeSubStreamsInfo(final DataOutput header) throws IOException {
470        header.write(NID.kSubStreamsInfo);
471//
472//        header.write(NID.kCRC);
473//        header.write(1);
474//        for (final SevenZArchiveEntry entry : files) {
475//            if (entry.getHasCrc()) {
476//                header.writeInt(Integer.reverseBytes(entry.getCrc()));
477//            }
478//        }
479//
480        header.write(NID.kEnd);
481    }
482
483    private void writeFilesInfo(final DataOutput header) throws IOException {
484        header.write(NID.kFilesInfo);
485
486        writeUint64(header, files.size());
487
488        writeFileEmptyStreams(header);
489        writeFileEmptyFiles(header);
490        writeFileAntiItems(header);
491        writeFileNames(header);
492        writeFileCTimes(header);
493        writeFileATimes(header);
494        writeFileMTimes(header);
495        writeFileWindowsAttributes(header);
496        header.write(NID.kEnd);
497    }
498
499    private void writeFileEmptyStreams(final DataOutput header) throws IOException {
500        boolean hasEmptyStreams = false;
501        for (final SevenZArchiveEntry entry : files) {
502            if (!entry.hasStream()) {
503                hasEmptyStreams = true;
504                break;
505            }
506        }
507        if (hasEmptyStreams) {
508            header.write(NID.kEmptyStream);
509            final BitSet emptyStreams = new BitSet(files.size());
510            for (int i = 0; i < files.size(); i++) {
511                emptyStreams.set(i, !files.get(i).hasStream());
512            }
513            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
514            final DataOutputStream out = new DataOutputStream(baos);
515            writeBits(out, emptyStreams, files.size());
516            out.flush();
517            final byte[] contents = baos.toByteArray();
518            writeUint64(header, contents.length);
519            header.write(contents);
520        }
521    }
522
523    private void writeFileEmptyFiles(final DataOutput header) throws IOException {
524        boolean hasEmptyFiles = false;
525        int emptyStreamCounter = 0;
526        final BitSet emptyFiles = new BitSet(0);
527        for (final SevenZArchiveEntry file1 : files) {
528            if (!file1.hasStream()) {
529                final boolean isDir = file1.isDirectory();
530                emptyFiles.set(emptyStreamCounter++, !isDir);
531                hasEmptyFiles |= !isDir;
532            }
533        }
534        if (hasEmptyFiles) {
535            header.write(NID.kEmptyFile);
536            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
537            final DataOutputStream out = new DataOutputStream(baos);
538            writeBits(out, emptyFiles, emptyStreamCounter);
539            out.flush();
540            final byte[] contents = baos.toByteArray();
541            writeUint64(header, contents.length);
542            header.write(contents);
543        }
544    }
545
546    private void writeFileAntiItems(final DataOutput header) throws IOException {
547        boolean hasAntiItems = false;
548        final BitSet antiItems = new BitSet(0);
549        int antiItemCounter = 0;
550        for (final SevenZArchiveEntry file1 : files) {
551            if (!file1.hasStream()) {
552                final boolean isAnti = file1.isAntiItem();
553                antiItems.set(antiItemCounter++, isAnti);
554                hasAntiItems |= isAnti;
555            }
556        }
557        if (hasAntiItems) {
558            header.write(NID.kAnti);
559            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
560            final DataOutputStream out = new DataOutputStream(baos);
561            writeBits(out, antiItems, antiItemCounter);
562            out.flush();
563            final byte[] contents = baos.toByteArray();
564            writeUint64(header, contents.length);
565            header.write(contents);
566        }
567    }
568
569    private void writeFileNames(final DataOutput header) throws IOException {
570        header.write(NID.kName);
571
572        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
573        final DataOutputStream out = new DataOutputStream(baos);
574        out.write(0);
575        for (final SevenZArchiveEntry entry : files) {
576            out.write(entry.getName().getBytes("UTF-16LE"));
577            out.writeShort(0);
578        }
579        out.flush();
580        final byte[] contents = baos.toByteArray();
581        writeUint64(header, contents.length);
582        header.write(contents);
583    }
584
585    private void writeFileCTimes(final DataOutput header) throws IOException {
586        int numCreationDates = 0;
587        for (final SevenZArchiveEntry entry : files) {
588            if (entry.getHasCreationDate()) {
589                ++numCreationDates;
590            }
591        }
592        if (numCreationDates > 0) {
593            header.write(NID.kCTime);
594
595            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
596            final DataOutputStream out = new DataOutputStream(baos);
597            if (numCreationDates != files.size()) {
598                out.write(0);
599                final BitSet cTimes = new BitSet(files.size());
600                for (int i = 0; i < files.size(); i++) {
601                    cTimes.set(i, files.get(i).getHasCreationDate());
602                }
603                writeBits(out, cTimes, files.size());
604            } else {
605                out.write(1); // "allAreDefined" == true
606            }
607            out.write(0);
608            for (final SevenZArchiveEntry entry : files) {
609                if (entry.getHasCreationDate()) {
610                    out.writeLong(Long.reverseBytes(
611                            SevenZArchiveEntry.javaTimeToNtfsTime(entry.getCreationDate())));
612                }
613            }
614            out.flush();
615            final byte[] contents = baos.toByteArray();
616            writeUint64(header, contents.length);
617            header.write(contents);
618        }
619    }
620
621    private void writeFileATimes(final DataOutput header) throws IOException {
622        int numAccessDates = 0;
623        for (final SevenZArchiveEntry entry : files) {
624            if (entry.getHasAccessDate()) {
625                ++numAccessDates;
626            }
627        }
628        if (numAccessDates > 0) {
629            header.write(NID.kATime);
630
631            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
632            final DataOutputStream out = new DataOutputStream(baos);
633            if (numAccessDates != files.size()) {
634                out.write(0);
635                final BitSet aTimes = new BitSet(files.size());
636                for (int i = 0; i < files.size(); i++) {
637                    aTimes.set(i, files.get(i).getHasAccessDate());
638                }
639                writeBits(out, aTimes, files.size());
640            } else {
641                out.write(1); // "allAreDefined" == true
642            }
643            out.write(0);
644            for (final SevenZArchiveEntry entry : files) {
645                if (entry.getHasAccessDate()) {
646                    out.writeLong(Long.reverseBytes(
647                            SevenZArchiveEntry.javaTimeToNtfsTime(entry.getAccessDate())));
648                }
649            }
650            out.flush();
651            final byte[] contents = baos.toByteArray();
652            writeUint64(header, contents.length);
653            header.write(contents);
654        }
655    }
656
657    private void writeFileMTimes(final DataOutput header) throws IOException {
658        int numLastModifiedDates = 0;
659        for (final SevenZArchiveEntry entry : files) {
660            if (entry.getHasLastModifiedDate()) {
661                ++numLastModifiedDates;
662            }
663        }
664        if (numLastModifiedDates > 0) {
665            header.write(NID.kMTime);
666
667            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
668            final DataOutputStream out = new DataOutputStream(baos);
669            if (numLastModifiedDates != files.size()) {
670                out.write(0);
671                final BitSet mTimes = new BitSet(files.size());
672                for (int i = 0; i < files.size(); i++) {
673                    mTimes.set(i, files.get(i).getHasLastModifiedDate());
674                }
675                writeBits(out, mTimes, files.size());
676            } else {
677                out.write(1); // "allAreDefined" == true
678            }
679            out.write(0);
680            for (final SevenZArchiveEntry entry : files) {
681                if (entry.getHasLastModifiedDate()) {
682                    out.writeLong(Long.reverseBytes(
683                            SevenZArchiveEntry.javaTimeToNtfsTime(entry.getLastModifiedDate())));
684                }
685            }
686            out.flush();
687            final byte[] contents = baos.toByteArray();
688            writeUint64(header, contents.length);
689            header.write(contents);
690        }
691    }
692
693    private void writeFileWindowsAttributes(final DataOutput header) throws IOException {
694        int numWindowsAttributes = 0;
695        for (final SevenZArchiveEntry entry : files) {
696            if (entry.getHasWindowsAttributes()) {
697                ++numWindowsAttributes;
698            }
699        }
700        if (numWindowsAttributes > 0) {
701            header.write(NID.kWinAttributes);
702
703            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
704            final DataOutputStream out = new DataOutputStream(baos);
705            if (numWindowsAttributes != files.size()) {
706                out.write(0);
707                final BitSet attributes = new BitSet(files.size());
708                for (int i = 0; i < files.size(); i++) {
709                    attributes.set(i, files.get(i).getHasWindowsAttributes());
710                }
711                writeBits(out, attributes, files.size());
712            } else {
713                out.write(1); // "allAreDefined" == true
714            }
715            out.write(0);
716            for (final SevenZArchiveEntry entry : files) {
717                if (entry.getHasWindowsAttributes()) {
718                    out.writeInt(Integer.reverseBytes(entry.getWindowsAttributes()));
719                }
720            }
721            out.flush();
722            final byte[] contents = baos.toByteArray();
723            writeUint64(header, contents.length);
724            header.write(contents);
725        }
726    }
727
728    private void writeUint64(final DataOutput header, long value) throws IOException {
729        int firstByte = 0;
730        int mask = 0x80;
731        int i;
732        for (i = 0; i < 8; i++) {
733            if (value < ((1L << ( 7  * (i + 1))))) {
734                firstByte |= (value >>> (8 * i));
735                break;
736            }
737            firstByte |= mask;
738            mask >>>= 1;
739        }
740        header.write(firstByte);
741        for (; i > 0; i--) {
742            header.write((int) (0xff & value));
743            value >>>= 8;
744        }
745    }
746
747    private void writeBits(final DataOutput header, final BitSet bits, final int length) throws IOException {
748        int cache = 0;
749        int shift = 7;
750        for (int i = 0; i < length; i++) {
751            cache |= ((bits.get(i) ? 1 : 0) << shift);
752            if (--shift < 0) {
753                header.write(cache);
754                shift = 7;
755                cache = 0;
756            }
757        }
758        if (shift != 7) {
759            header.write(cache);
760        }
761    }
762
763    private static <T> Iterable<T> reverse(final Iterable<T> i) {
764        final LinkedList<T> l = new LinkedList<>();
765        for (final T t : i) {
766            l.addFirst(t);
767        }
768        return l;
769    }
770
771    private class OutputStreamWrapper extends OutputStream {
772        private static final int BUF_SIZE = 8192;
773        private final ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE);
774        @Override
775        public void write(final int b) throws IOException {
776            buffer.clear();
777            buffer.put((byte) b).flip();
778            channel.write(buffer);
779            compressedCrc32.update(b);
780            fileBytesWritten++;
781        }
782
783        @Override
784        public void write(final byte[] b) throws IOException {
785            OutputStreamWrapper.this.write(b, 0, b.length);
786        }
787
788        @Override
789        public void write(final byte[] b, final int off, final int len)
790            throws IOException {
791            if (len > BUF_SIZE) {
792                channel.write(ByteBuffer.wrap(b, off, len));
793            } else {
794                buffer.clear();
795                buffer.put(b, off, len).flip();
796                channel.write(buffer);
797            }
798            compressedCrc32.update(b, off, len);
799            fileBytesWritten += len;
800        }
801
802        @Override
803        public void flush() throws IOException {
804            // no reason to flush the channel
805        }
806
807        @Override
808        public void close() throws IOException {
809            // the file will be closed by the containing class's close method
810        }
811    }
812}