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