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