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