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}