    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 java.io.ByteArrayOutputStream;
    021import java.io.Closeable;
    022import java.io.DataOutput;
    023import java.io.DataOutputStream;
    024import java.io.File;
    025import java.io.IOException;
    026import java.io.OutputStream;
    027import java.io.RandomAccessFile;
    028import java.util.ArrayList;
    029import java.util.BitSet;
    030import java.util.Collections;
    031import java.util.Date;
    032import java.util.HashMap;
    033import java.util.List;
    034import java.util.LinkedList;
    035import java.util.Map;
    036import java.util.zip.CRC32;
    037
    038import org.apache.commons.compress.archivers.ArchiveEntry;
    039import org.apache.commons.compress.utils.CountingOutputStream;
    040
    041/**
    042 * Writes a 7z file.
    043 * @since 1.6
    044 */
    045public class SevenZOutputFile implements Closeable {
    046    private final RandomAccessFile file;
    047    private final List<SevenZArchiveEntry> files = new ArrayList<SevenZArchiveEntry>();
    048    private int numNonEmptyStreams = 0;
    049    private final CRC32 crc32 = new CRC32();
    050    private final CRC32 compressedCrc32 = new CRC32();
    051    private long fileBytesWritten = 0;
    052    private boolean finished = false;
    053    private CountingOutputStream currentOutputStream;
    054    private CountingOutputStream[] additionalCountingStreams;
    055    private Iterable<? extends SevenZMethodConfiguration> contentMethods =
    056            Collections.singletonList(new SevenZMethodConfiguration(SevenZMethod.LZMA2));
    057    private final Map<SevenZArchiveEntry, long[]> additionalSizes = new HashMap<SevenZArchiveEntry, long[]>();
    058
    059    /**
    060     * Opens file to write a 7z archive to.
    061     *
    062     * @param filename name of the file to write to
    063     * @throws IOException if opening the file fails
    064     */
    065    public SevenZOutputFile(final File filename) throws IOException {
    066        file = new RandomAccessFile(filename, "rw");
    067        file.seek(SevenZFile.SIGNATURE_HEADER_SIZE);
    068    }
    069
    070    /**
    071     * Sets the default compression method to use for entry contents - the
    072     * default is LZMA2.
    073     *
    074     * <p>Currently only {@link SevenZMethod#COPY}, {@link
    075     * SevenZMethod#LZMA2}, {@link SevenZMethod#BZIP2} and {@link
    076     * SevenZMethod#DEFLATE} are supported.</p>
    077     *
    078     * <p>This is a short form for passing a single-element iterable
    079     * to {@link #setContentMethods}.</p>
    080     * @param method the default compression method
    081     */
    082    public void setContentCompression(final SevenZMethod method) {
    083        setContentMethods(Collections.singletonList(new SevenZMethodConfiguration(method)));
    084    }
    085
    086    /**
    087     * Sets the default (compression) methods to use for entry contents - the
    088     * default is LZMA2.
    089     *
    090     * <p>Currently only {@link SevenZMethod#COPY}, {@link
    091     * SevenZMethod#LZMA2}, {@link SevenZMethod#BZIP2} and {@link
    092     * SevenZMethod#DEFLATE} are supported.</p>
    093     *
    094     * <p>The methods will be consulted in iteration order to create
    095     * the final output.</p>
    096     *
    097     * @since 1.8
    098     * @param methods the default (compression) methods
    099     */
    100    public void setContentMethods(final Iterable<? extends SevenZMethodConfiguration> methods) {
    101        this.contentMethods = reverse(methods);
    102    }
    103
    104    /**
    105     * Closes the archive, calling {@link #finish} if necessary.
    106     *
    107     * @throws IOException on error
    108     */
    109    @Override
    110    public void close() throws IOException {
    111        if (!finished) {
    112            finish();
    113        }
    114        file.close();
    115    }
    116
    117    /**
    118     * Create an archive entry using the inputFile and entryName provided.
    119     *
    120     * @param inputFile file to create an entry from
    121     * @param entryName the name to use
    122     * @return the ArchiveEntry set up with details from the file
    123     *
    124     * @throws IOException on error
    125     */
    126    public SevenZArchiveEntry createArchiveEntry(final File inputFile,
    127            final String entryName) throws IOException {
    128        final SevenZArchiveEntry entry = new SevenZArchiveEntry();
    129        entry.setDirectory(inputFile.isDirectory());
    130        entry.setName(entryName);
    131        entry.setLastModifiedDate(new Date(inputFile.lastModified()));
    132        return entry;
    133    }
    134
    135    /**
    136     * Records an archive entry to add.
    137     *
    138     * The caller must then write the content to the archive and call
    139     * {@link #closeArchiveEntry()} to complete the process.
    140     *
    141     * @param archiveEntry describes the entry
    142     * @throws IOException on error
    143     */
    144    public void putArchiveEntry(final ArchiveEntry archiveEntry) throws IOException {
    145        final SevenZArchiveEntry entry = (SevenZArchiveEntry) archiveEntry;
    146        files.add(entry);
    147    }
    148
    149    /**
    150     * Closes the archive entry.
    151     * @throws IOException on error
    152     */
    153    public void closeArchiveEntry() throws IOException {
    154        if (currentOutputStream != null) {
    155            currentOutputStream.flush();
    156            currentOutputStream.close();
    157        }
    158
    159        final SevenZArchiveEntry entry = files.get(files.size() - 1);
    160        if (fileBytesWritten > 0) {
    161            entry.setHasStream(true);
    162            ++numNonEmptyStreams;
    163            entry.setSize(currentOutputStream.getBytesWritten());
    164            entry.setCompressedSize(fileBytesWritten);
    165            entry.setCrcValue(crc32.getValue());
    166            entry.setCompressedCrcValue(compressedCrc32.getValue());
    167            entry.setHasCrc(true);
    168            if (additionalCountingStreams != null) {
    169                final long[] sizes = new long[additionalCountingStreams.length];
    170                for (int i = 0; i < additionalCountingStreams.length; i++) {
    171                    sizes[i] = additionalCountingStreams[i].getBytesWritten();
    172                }
    173                additionalSizes.put(entry, sizes);
    174            }
    175        } else {
    176            entry.setHasStream(false);
    177            entry.setSize(0);
    178            entry.setCompressedSize(0);
    179            entry.setHasCrc(false);
    180        }
    181        currentOutputStream = null;
    182        additionalCountingStreams = null;
    183        crc32.reset();
    184        compressedCrc32.reset();
    185        fileBytesWritten = 0;
    186    }
    187
    188    /**
    189     * Writes a byte to the current archive entry.
    190     * @param b The byte to be written.
    191     * @throws IOException on error
    192     */
    193    public void write(final int b) throws IOException {
    194        getCurrentOutputStream().write(b);
    195    }
    196
    197    /**
    198     * Writes a byte array to the current archive entry.
    199     * @param b The byte array to be written.
    200     * @throws IOException on error
    201     */
    202    public void write(final byte[] b) throws IOException {
    203        write(b, 0, b.length);
    204    }
    205
    206    /**
    207     * Writes part of a byte array to the current archive entry.
    208     * @param b The byte array to be written.
    209     * @param off offset into the array to start writing from
    210     * @param len number of bytes to write
    211     * @throws IOException on error
    212     */
    213    public void write(final byte[] b, final int off, final int len) throws IOException {
    214        if (len > 0) {
    215            getCurrentOutputStream().write(b, off, len);
    216        }
    217    }
    218
    219    /**
    220     * Finishes the addition of entries to this archive, without closing it.
    221     *
    222     * @throws IOException if archive is already closed.
    223     */
    224    public void finish() throws IOException {
    225        if (finished) {
    226            throw new IOException("This archive has already been finished");
    227        }
    228        finished = true;
    229
    230        final long headerPosition = file.getFilePointer();
    231
    232        final ByteArrayOutputStream headerBaos = new ByteArrayOutputStream();
    233        final DataOutputStream header = new DataOutputStream(headerBaos);
    234
    235        writeHeader(header);
    236        header.flush();
    237        final byte[] headerBytes = headerBaos.toByteArray();
    238        file.write(headerBytes);
    239
    240        final CRC32 crc32 = new CRC32();
    241
    242        // signature header
    243        file.seek(0);
    244        file.write(SevenZFile.sevenZSignature);
    245        // version
    246        file.write(0);
    247        file.write(2);
    248
    249        // start header
    250        final ByteArrayOutputStream startHeaderBaos = new ByteArrayOutputStream();
    251        final DataOutputStream startHeaderStream = new DataOutputStream(startHeaderBaos);
    252        startHeaderStream.writeLong(Long.reverseBytes(headerPosition - SevenZFile.SIGNATURE_HEADER_SIZE));
    253        startHeaderStream.writeLong(Long.reverseBytes(0xffffFFFFL & headerBytes.length));
    254        crc32.reset();
    255        crc32.update(headerBytes);
    256        startHeaderStream.writeInt(Integer.reverseBytes((int)crc32.getValue()));
    257        startHeaderStream.flush();
    258        final byte[] startHeaderBytes = startHeaderBaos.toByteArray();
    259        crc32.reset();
    260        crc32.update(startHeaderBytes);
    261        file.writeInt(Integer.reverseBytes((int) crc32.getValue()));
    262        file.write(startHeaderBytes);
    263    }
    264
    265    /*
    266     * Creation of output stream is deferred until data is actually
    267     * written as some codecs might write header information even for
    268     * empty streams and directories otherwise.
    269     */
    270    private OutputStream getCurrentOutputStream() throws IOException {
    271        if (currentOutputStream == null) {
    272            currentOutputStream = setupFileOutputStream();
    273        }
    274        return currentOutputStream;
    275    }
    276
    277    private CountingOutputStream setupFileOutputStream() throws IOException {
    278        if (files.isEmpty()) {
    279            throw new IllegalStateException("No current 7z entry");
    280        }
    281
    282        OutputStream out = new OutputStreamWrapper();
    283        final ArrayList<CountingOutputStream> moreStreams = new ArrayList<CountingOutputStream>();
    284        boolean first = true;
    285        for (final SevenZMethodConfiguration m : getContentMethods(files.get(files.size() - 1))) {
    286            if (!first) {
    287                final CountingOutputStream cos = new CountingOutputStream(out);
    288                moreStreams.add(cos);
    289                out = cos;
    290            }
    291            out = Coders.addEncoder(out, m.getMethod(), m.getOptions());
    292            first = false;
    293        }
    294        if (!moreStreams.isEmpty()) {
    295            additionalCountingStreams = moreStreams.toArray(new CountingOutputStream[moreStreams.size()]);
    296        }
    297        return new CountingOutputStream(out) {
    298            @Override
    299            public void write(final int b) throws IOException {
    300                super.write(b);
    301                crc32.update(b);
    302            }
    303
    304            @Override
    305            public void write(final byte[] b) throws IOException {
    306                super.write(b);
    307                crc32.update(b);
    308            }
    309
    310            @Override
    311            public void write(final byte[] b, final int off, final int len)
    312                throws IOException {
    313                super.write(b, off, len);
    314                crc32.update(b, off, len);
    315            }
    316        };
    317    }
    318
    319    private Iterable<? extends SevenZMethodConfiguration> getContentMethods(final SevenZArchiveEntry entry) {
    320        final Iterable<? extends SevenZMethodConfiguration> ms = entry.getContentMethods();
    321        return ms == null ? contentMethods : ms;
    322    }
    323
    324    private void writeHeader(final DataOutput header) throws IOException {
    325        header.write(NID.kHeader);
    326
    327        header.write(NID.kMainStreamsInfo);
    328        writeStreamsInfo(header);
    329        writeFilesInfo(header);
    330        header.write(NID.kEnd);
    331    }
    332
    333    private void writeStreamsInfo(final DataOutput header) throws IOException {
    334        if (numNonEmptyStreams > 0) {
    335            writePackInfo(header);
    336            writeUnpackInfo(header);
    337        }
    338
    339        writeSubStreamsInfo(header);
    340
    341        header.write(NID.kEnd);
    342    }
    343
    344    private void writePackInfo(final DataOutput header) throws IOException {
    345        header.write(NID.kPackInfo);
    346
    347        writeUint64(header, 0);
    348        writeUint64(header, 0xffffFFFFL & numNonEmptyStreams);
    349
    350        header.write(NID.kSize);
    351        for (final SevenZArchiveEntry entry : files) {
    352            if (entry.hasStream()) {
    353                writeUint64(header, entry.getCompressedSize());
    354            }
    355        }
    356
    357        header.write(NID.kCRC);
    358        header.write(1); // "allAreDefined" == true
    359        for (final SevenZArchiveEntry entry : files) {
    360            if (entry.hasStream()) {
    361                header.writeInt(Integer.reverseBytes((int) entry.getCompressedCrcValue()));
    362            }
    363        }
    364
    365        header.write(NID.kEnd);
    366    }
    367
    368    private void writeUnpackInfo(final DataOutput header) throws IOException {
    369        header.write(NID.kUnpackInfo);
    370
    371        header.write(NID.kFolder);
    372        writeUint64(header, numNonEmptyStreams);
    373        header.write(0);
    374        for (final SevenZArchiveEntry entry : files) {
    375            if (entry.hasStream()) {
    376                writeFolder(header, entry);
    377            }
    378        }
    379
    380        header.write(NID.kCodersUnpackSize);
    381        for (final SevenZArchiveEntry entry : files) {
    382            if (entry.hasStream()) {
    383                final long[] moreSizes = additionalSizes.get(entry);
    384                if (moreSizes != null) {
    385                    for (final long s : moreSizes) {
    386                        writeUint64(header, s);
    387                    }
    388                }
    389                writeUint64(header, entry.getSize());
    390            }
    391        }
    392
    393        header.write(NID.kCRC);
    394        header.write(1); // "allAreDefined" == true
    395        for (final SevenZArchiveEntry entry : files) {
    396            if (entry.hasStream()) {
    397                header.writeInt(Integer.reverseBytes((int) entry.getCrcValue()));
    398            }
    399        }
    400
    401        header.write(NID.kEnd);
    402    }
    403
    404    private void writeFolder(final DataOutput header, final SevenZArchiveEntry entry) throws IOException {
    405        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
    406        int numCoders = 0;
    407        for (final SevenZMethodConfiguration m : getContentMethods(entry)) {
    408            numCoders++;
    409            writeSingleCodec(m, bos);
    410        }
    411
    412        writeUint64(header, numCoders);
    413        header.write(bos.toByteArray());
    414        for (int i = 0; i < numCoders - 1; i++) {
    415            writeUint64(header, i + 1);
    416            writeUint64(header, i);
    417        }
    418    }
    419
    420    private void writeSingleCodec(final SevenZMethodConfiguration m, final OutputStream bos) throws IOException {
    421        final byte[] id = m.getMethod().getId();
    422        final byte[] properties = Coders.findByMethod(m.getMethod())
    423            .getOptionsAsProperties(m.getOptions());
    424
    425        int codecFlags = id.length;
    426        if (properties.length > 0) {
    427            codecFlags |= 0x20;
    428        }
    429        bos.write(codecFlags);
    430        bos.write(id);
    431
    432        if (properties.length > 0) {
    433            bos.write(properties.length);
    434            bos.write(properties);
    435        }
    436    }
    437
    438    private void writeSubStreamsInfo(final DataOutput header) throws IOException {
    439        header.write(NID.kSubStreamsInfo);
    440//
    441//        header.write(NID.kCRC);
    442//        header.write(1);
    443//        for (final SevenZArchiveEntry entry : files) {
    444//            if (entry.getHasCrc()) {
    445//                header.writeInt(Integer.reverseBytes(entry.getCrc()));
    446//            }
    447//        }
    448//
    449        header.write(NID.kEnd);
    450    }
    451
    452    private void writeFilesInfo(final DataOutput header) throws IOException {
    453        header.write(NID.kFilesInfo);
    454
    455        writeUint64(header, files.size());
    456
    457        writeFileEmptyStreams(header);
    458        writeFileEmptyFiles(header);
    459        writeFileAntiItems(header);
    460        writeFileNames(header);
    461        writeFileCTimes(header);
    462        writeFileATimes(header);
    463        writeFileMTimes(header);
    464        writeFileWindowsAttributes(header);
    465        header.write(NID.kEnd);
    466    }
    467
    468    private void writeFileEmptyStreams(final DataOutput header) throws IOException {
    469        boolean hasEmptyStreams = false;
    470        for (final SevenZArchiveEntry entry : files) {
    471            if (!entry.hasStream()) {
    472                hasEmptyStreams = true;
    473                break;
    474            }
    475        }
    476        if (hasEmptyStreams) {
    477            header.write(NID.kEmptyStream);
    478            final BitSet emptyStreams = new BitSet(files.size());
    479            for (int i = 0; i < files.size(); i++) {
    480                emptyStreams.set(i, !files.get(i).hasStream());
    481            }
    482            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    483            final DataOutputStream out = new DataOutputStream(baos);
    484            writeBits(out, emptyStreams, files.size());
    485            out.flush();
    486            final byte[] contents = baos.toByteArray();
    487            writeUint64(header, contents.length);
    488            header.write(contents);
    489        }
    490    }
    491
    492    private void writeFileEmptyFiles(final DataOutput header) throws IOException {
    493        boolean hasEmptyFiles = false;
    494        int emptyStreamCounter = 0;
    495        final BitSet emptyFiles = new BitSet(0);
    496        for (final SevenZArchiveEntry file1 : files) {
    497            if (!file1.hasStream()) {
    498                final boolean isDir = file1.isDirectory();
    499                emptyFiles.set(emptyStreamCounter++, !isDir);
    500                hasEmptyFiles |= !isDir;
    501            }
    502        }
    503        if (hasEmptyFiles) {
    504            header.write(NID.kEmptyFile);
    505            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    506            final DataOutputStream out = new DataOutputStream(baos);
    507            writeBits(out, emptyFiles, emptyStreamCounter);
    508            out.flush();
    509            final byte[] contents = baos.toByteArray();
    510            writeUint64(header, contents.length);
    511            header.write(contents);
    512        }
    513    }
    514
    515    private void writeFileAntiItems(final DataOutput header) throws IOException {
    516        boolean hasAntiItems = false;
    517        final BitSet antiItems = new BitSet(0);
    518        int antiItemCounter = 0;
    519        for (final SevenZArchiveEntry file1 : files) {
    520            if (!file1.hasStream()) {
    521                final boolean isAnti = file1.isAntiItem();
    522                antiItems.set(antiItemCounter++, isAnti);
    523                hasAntiItems |= isAnti;
    524            }
    525        }
    526        if (hasAntiItems) {
    527            header.write(NID.kAnti);
    528            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    529            final DataOutputStream out = new DataOutputStream(baos);
    530            writeBits(out, antiItems, antiItemCounter);
    531            out.flush();
    532            final byte[] contents = baos.toByteArray();
    533            writeUint64(header, contents.length);
    534            header.write(contents);
    535        }
    536    }
    537
    538    private void writeFileNames(final DataOutput header) throws IOException {
    539        header.write(NID.kName);
    540
    541        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    542        final DataOutputStream out = new DataOutputStream(baos);
    543        out.write(0);
    544        for (final SevenZArchiveEntry entry : files) {
    545            out.write(entry.getName().getBytes("UTF-16LE"));
    546            out.writeShort(0);
    547        }
    548        out.flush();
    549        final byte[] contents = baos.toByteArray();
    550        writeUint64(header, contents.length);
    551        header.write(contents);
    552    }
    553
    554    private void writeFileCTimes(final DataOutput header) throws IOException {
    555        int numCreationDates = 0;
    556        for (final SevenZArchiveEntry entry : files) {
    557            if (entry.getHasCreationDate()) {
    558                ++numCreationDates;
    559            }
    560        }
    561        if (numCreationDates > 0) {
    562            header.write(NID.kCTime);
    563
    564            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    565            final DataOutputStream out = new DataOutputStream(baos);
    566            if (numCreationDates != files.size()) {
    567                out.write(0);
    568                final BitSet cTimes = new BitSet(files.size());
    569                for (int i = 0; i < files.size(); i++) {
    570                    cTimes.set(i, files.get(i).getHasCreationDate());
    571                }
    572                writeBits(out, cTimes, files.size());
    573            } else {
    574                out.write(1); // "allAreDefined" == true
    575            }
    576            out.write(0);
    577            for (final SevenZArchiveEntry entry : files) {
    578                if (entry.getHasCreationDate()) {
    579                    out.writeLong(Long.reverseBytes(
    580                            SevenZArchiveEntry.javaTimeToNtfsTime(entry.getCreationDate())));
    581                }
    582            }
    583            out.flush();
    584            final byte[] contents = baos.toByteArray();
    585            writeUint64(header, contents.length);
    586            header.write(contents);
    587        }
    588    }
    589
    590    private void writeFileATimes(final DataOutput header) throws IOException {
    591        int numAccessDates = 0;
    592        for (final SevenZArchiveEntry entry : files) {
    593            if (entry.getHasAccessDate()) {
    594                ++numAccessDates;
    595            }
    596        }
    597        if (numAccessDates > 0) {
    598            header.write(NID.kATime);
    599
    600            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    601            final DataOutputStream out = new DataOutputStream(baos);
    602            if (numAccessDates != files.size()) {
    603                out.write(0);
    604                final BitSet aTimes = new BitSet(files.size());
    605                for (int i = 0; i < files.size(); i++) {
    606                    aTimes.set(i, files.get(i).getHasAccessDate());
    607                }
    608                writeBits(out, aTimes, files.size());
    609            } else {
    610                out.write(1); // "allAreDefined" == true
    611            }
    612            out.write(0);
    613            for (final SevenZArchiveEntry entry : files) {
    614                if (entry.getHasAccessDate()) {
    615                    out.writeLong(Long.reverseBytes(
    616                            SevenZArchiveEntry.javaTimeToNtfsTime(entry.getAccessDate())));
    617                }
    618            }
    619            out.flush();
    620            final byte[] contents = baos.toByteArray();
    621            writeUint64(header, contents.length);
    622            header.write(contents);
    623        }
    624    }
    625
    626    private void writeFileMTimes(final DataOutput header) throws IOException {
    627        int numLastModifiedDates = 0;
    628        for (final SevenZArchiveEntry entry : files) {
    629            if (entry.getHasLastModifiedDate()) {
    630                ++numLastModifiedDates;
    631            }
    632        }
    633        if (numLastModifiedDates > 0) {
    634            header.write(NID.kMTime);
    635
    636            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    637            final DataOutputStream out = new DataOutputStream(baos);
    638            if (numLastModifiedDates != files.size()) {
    639                out.write(0);
    640                final BitSet mTimes = new BitSet(files.size());
    641                for (int i = 0; i < files.size(); i++) {
    642                    mTimes.set(i, files.get(i).getHasLastModifiedDate());
    643                }
    644                writeBits(out, mTimes, files.size());
    645            } else {
    646                out.write(1); // "allAreDefined" == true
    647            }
    648            out.write(0);
    649            for (final SevenZArchiveEntry entry : files) {
    650                if (entry.getHasLastModifiedDate()) {
    651                    out.writeLong(Long.reverseBytes(
    652                            SevenZArchiveEntry.javaTimeToNtfsTime(entry.getLastModifiedDate())));
    653                }
    654            }
    655            out.flush();
    656            final byte[] contents = baos.toByteArray();
    657            writeUint64(header, contents.length);
    658            header.write(contents);
    659        }
    660    }
    661
    662    private void writeFileWindowsAttributes(final DataOutput header) throws IOException {
    663        int numWindowsAttributes = 0;
    664        for (final SevenZArchiveEntry entry : files) {
    665            if (entry.getHasWindowsAttributes()) {
    666                ++numWindowsAttributes;
    667            }
    668        }
    669        if (numWindowsAttributes > 0) {
    670            header.write(NID.kWinAttributes);
    671
    672            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    673            final DataOutputStream out = new DataOutputStream(baos);
    674            if (numWindowsAttributes != files.size()) {
    675                out.write(0);
    676                final BitSet attributes = new BitSet(files.size());
    677                for (int i = 0; i < files.size(); i++) {
    678                    attributes.set(i, files.get(i).getHasWindowsAttributes());
    679                }
    680                writeBits(out, attributes, files.size());
    681            } else {
    682                out.write(1); // "allAreDefined" == true
    683            }
    684            out.write(0);
    685            for (final SevenZArchiveEntry entry : files) {
    686                if (entry.getHasWindowsAttributes()) {
    687                    out.writeInt(Integer.reverseBytes(entry.getWindowsAttributes()));
    688                }
    689            }
    690            out.flush();
    691            final byte[] contents = baos.toByteArray();
    692            writeUint64(header, contents.length);
    693            header.write(contents);
    694        }
    695    }
    696
    697    private void writeUint64(final DataOutput header, long value) throws IOException {
    698        int firstByte = 0;
    699        int mask = 0x80;
    700        int i;
    701        for (i = 0; i < 8; i++) {
    702            if (value < ((1L << ( 7  * (i + 1))))) {
    703                firstByte |= (value >>> (8 * i));
    704                break;
    705            }
    706            firstByte |= mask;
    707            mask >>>= 1;
    708        }
    709        header.write(firstByte);
    710        for (; i > 0; i--) {
    711            header.write((int) (0xff & value));
    712            value >>>= 8;
    713        }
    714    }
    715
    716    private void writeBits(final DataOutput header, final BitSet bits, final int length) throws IOException {
    717        int cache = 0;
    718        int shift = 7;
    719        for (int i = 0; i < length; i++) {
    720            cache |= ((bits.get(i) ? 1 : 0) << shift);
    721            if (--shift < 0) {
    722                header.write(cache);
    723                shift = 7;
    724                cache = 0;
    725            }
    726        }
    727        if (shift != 7) {
    728            header.write(cache);
    729        }
    730    }
    731
    732    private static <T> Iterable<T> reverse(final Iterable<T> i) {
    733        final LinkedList<T> l = new LinkedList<T>();
    734        for (final T t : i) {
    735            l.addFirst(t);
    736        }
    737        return l;
    738    }
    739
    740    private class OutputStreamWrapper extends OutputStream {
    741        @Override
    742        public void write(final int b) throws IOException {
    743            file.write(b);
    744            compressedCrc32.update(b);
    745            fileBytesWritten++;
    746        }
    747
    748        @Override
    749        public void write(final byte[] b) throws IOException {
    750            OutputStreamWrapper.this.write(b, 0, b.length);
    751        }
    752
    753        @Override
    754        public void write(final byte[] b, final int off, final int len)
    755            throws IOException {
    756            file.write(b, off, len);
    757            compressedCrc32.update(b, off, len);
    758            fileBytesWritten += len;
    759        }
    760
    761        @Override
    762        public void flush() throws IOException {
    763            // no reason to flush a RandomAccessFile
    764        }
    765
    766        @Override
    767        public void close() throws IOException {
    768            // the file will be closed by the containing class's close method
    769        }
    770    }
    771}