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