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