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