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