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