    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}