View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one or more
3    *  contributor license agreements.  See the NOTICE file distributed with
4    *  this work for additional information regarding copyright ownership.
5    *  The ASF licenses this file to You under the Apache License, Version 2.0
6    *  (the "License"); you may not use this file except in compliance with
7    *  the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  package org.apache.commons.compress.archivers.sevenz;
18  
19  import static java.nio.charset.StandardCharsets.UTF_16LE;
20  
21  import java.io.BufferedInputStream;
22  import java.io.ByteArrayOutputStream;
23  import java.io.Closeable;
24  import java.io.DataOutput;
25  import java.io.DataOutputStream;
26  import java.io.File;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.OutputStream;
30  import java.nio.ByteBuffer;
31  import java.nio.ByteOrder;
32  import java.nio.channels.SeekableByteChannel;
33  import java.nio.file.Files;
34  import java.nio.file.LinkOption;
35  import java.nio.file.OpenOption;
36  import java.nio.file.Path;
37  import java.nio.file.StandardOpenOption;
38  import java.nio.file.attribute.BasicFileAttributes;
39  import java.util.ArrayList;
40  import java.util.Arrays;
41  import java.util.BitSet;
42  import java.util.Collections;
43  import java.util.Date;
44  import java.util.EnumSet;
45  import java.util.HashMap;
46  import java.util.LinkedList;
47  import java.util.List;
48  import java.util.Map;
49  import java.util.stream.Collectors;
50  import java.util.stream.Stream;
51  import java.util.stream.StreamSupport;
52  import java.util.zip.CRC32;
53  
54  import org.apache.commons.compress.archivers.ArchiveEntry;
55  import org.apache.commons.io.file.attribute.FileTimes;
56  import org.apache.commons.io.output.CountingOutputStream;
57  
58  /**
59   * Writes a 7z file.
60   *
61   * @since 1.6
62   */
63  public class SevenZOutputFile implements Closeable {
64  
65      private final class OutputStreamWrapper extends OutputStream {
66  
67          private static final int BUF_SIZE = 8192;
68          private final ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE);
69  
70          @Override
71          public void close() throws IOException {
72              // the file will be closed by the containing class's close method
73          }
74  
75          @Override
76          public void flush() throws IOException {
77              // no reason to flush the channel
78          }
79  
80          @Override
81          public void write(final byte[] b) throws IOException {
82              OutputStreamWrapper.this.write(b, 0, b.length);
83          }
84  
85          @Override
86          public void write(final byte[] b, final int off, final int len) throws IOException {
87              if (len > BUF_SIZE) {
88                  channel.write(ByteBuffer.wrap(b, off, len));
89              } else {
90                  buffer.clear();
91                  buffer.put(b, off, len).flip();
92                  channel.write(buffer);
93              }
94              compressedCrc32.update(b, off, len);
95              fileBytesWritten += len;
96          }
97  
98          @Override
99          public void write(final int b) throws IOException {
100             buffer.clear();
101             buffer.put((byte) b).flip();
102             channel.write(buffer);
103             compressedCrc32.update(b);
104             fileBytesWritten++;
105         }
106     }
107 
108     private static <T> Iterable<T> reverse(final Iterable<T> i) {
109         final LinkedList<T> l = new LinkedList<>();
110         for (final T t : i) {
111             l.addFirst(t);
112         }
113         return l;
114     }
115 
116     private final SeekableByteChannel channel;
117     private final List<SevenZArchiveEntry> files = new ArrayList<>();
118     private int numNonEmptyStreams;
119     private final CRC32 crc32 = new CRC32();
120     private final CRC32 compressedCrc32 = new CRC32();
121     private long fileBytesWritten;
122     private boolean finished;
123     private CountingOutputStream currentOutputStream;
124     private CountingOutputStream[] additionalCountingStreams;
125     private Iterable<? extends SevenZMethodConfiguration> contentMethods = Collections.singletonList(new SevenZMethodConfiguration(SevenZMethod.LZMA2));
126     private final Map<SevenZArchiveEntry, long[]> additionalSizes = new HashMap<>();
127     private AES256Options aes256Options;
128 
129     /**
130      * Opens file to write a 7z archive to.
131      *
132      * @param fileName the file to write to
133      * @throws IOException if opening the file fails
134      */
135     public SevenZOutputFile(final File fileName) throws IOException {
136         this(fileName, null);
137     }
138 
139     /**
140      * Opens file to write a 7z archive to.
141      *
142      * @param fileName the file to write to
143      * @param password optional password if the archive has to be encrypted
144      * @throws IOException if opening the file fails
145      * @since 1.23
146      */
147     public SevenZOutputFile(final File fileName, final char[] password) throws IOException {
148         this(Files.newByteChannel(fileName.toPath(), EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)),
149                 password);
150     }
151 
152     /**
153      * Prepares channel to write a 7z archive to.
154      *
155      * <p>
156      * {@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to write to an in-memory archive.
157      * </p>
158      *
159      * @param channel the channel to write to
160      * @throws IOException if the channel cannot be positioned properly
161      * @since 1.13
162      */
163     public SevenZOutputFile(final SeekableByteChannel channel) throws IOException {
164         this(channel, null);
165     }
166 
167     /**
168      * Prepares channel to write a 7z archive to.
169      *
170      * <p>
171      * {@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to write to an in-memory archive.
172      * </p>
173      *
174      * @param channel  the channel to write to
175      * @param password optional password if the archive has to be encrypted
176      * @throws IOException if the channel cannot be positioned properly
177      * @since 1.23
178      */
179     public SevenZOutputFile(final SeekableByteChannel channel, final char[] password) throws IOException {
180         this.channel = channel;
181         channel.position(SevenZFile.SIGNATURE_HEADER_SIZE);
182         if (password != null) {
183             this.aes256Options = new AES256Options(password);
184         }
185     }
186 
187     /**
188      * Closes the archive, calling {@link #finish} if necessary.
189      *
190      * @throws IOException on error
191      */
192     @Override
193     public void close() throws IOException {
194         try {
195             if (!finished) {
196                 finish();
197             }
198         } finally {
199             channel.close();
200         }
201     }
202 
203     /**
204      * Closes the archive entry.
205      *
206      * @throws IOException on error
207      */
208     public void closeArchiveEntry() throws IOException {
209         if (currentOutputStream != null) {
210             currentOutputStream.flush();
211             currentOutputStream.close();
212         }
213 
214         final SevenZArchiveEntry entry = files.get(files.size() - 1);
215         if (fileBytesWritten > 0) { // this implies currentOutputStream != null
216             entry.setHasStream(true);
217             ++numNonEmptyStreams;
218             entry.setSize(currentOutputStream.getByteCount()); // NOSONAR
219             entry.setCompressedSize(fileBytesWritten);
220             entry.setCrcValue(crc32.getValue());
221             entry.setCompressedCrcValue(compressedCrc32.getValue());
222             entry.setHasCrc(true);
223             if (additionalCountingStreams != null) {
224                 final long[] sizes = new long[additionalCountingStreams.length];
225                 Arrays.setAll(sizes, i -> additionalCountingStreams[i].getByteCount());
226                 additionalSizes.put(entry, sizes);
227             }
228         } else {
229             entry.setHasStream(false);
230             entry.setSize(0);
231             entry.setCompressedSize(0);
232             entry.setHasCrc(false);
233         }
234         currentOutputStream = null;
235         additionalCountingStreams = null;
236         crc32.reset();
237         compressedCrc32.reset();
238         fileBytesWritten = 0;
239     }
240 
241     /**
242      * Creates an archive entry using the inputFile and entryName provided.
243      *
244      * @param inputFile file to create an entry from
245      * @param entryName the name to use
246      * @return the ArchiveEntry set up with details from the file
247      */
248     public SevenZArchiveEntry createArchiveEntry(final File inputFile, final String entryName) {
249         final SevenZArchiveEntry entry = new SevenZArchiveEntry();
250         entry.setDirectory(inputFile.isDirectory());
251         entry.setName(entryName);
252         try {
253             fillDates(inputFile.toPath(), entry);
254         } catch (final IOException e) { // NOSONAR
255             entry.setLastModifiedDate(new Date(inputFile.lastModified()));
256         }
257         return entry;
258     }
259 
260     /**
261      * Creates an archive entry using the inputPath and entryName provided.
262      *
263      * @param inputPath path to create an entry from
264      * @param entryName the name to use
265      * @param options   options indicating how symbolic links are handled.
266      * @return the ArchiveEntry set up with details from the file
267      *
268      * @throws IOException on error
269      * @since 1.21
270      */
271     public SevenZArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
272         final SevenZArchiveEntry entry = new SevenZArchiveEntry();
273         entry.setDirectory(Files.isDirectory(inputPath, options));
274         entry.setName(entryName);
275         fillDates(inputPath, entry, options);
276         return entry;
277     }
278 
279     private void fillDates(final Path inputPath, final SevenZArchiveEntry entry, final LinkOption... options) throws IOException {
280         final BasicFileAttributes attributes = Files.readAttributes(inputPath, BasicFileAttributes.class, options);
281         entry.setLastModifiedTime(attributes.lastModifiedTime());
282         entry.setCreationTime(attributes.creationTime());
283         entry.setAccessTime(attributes.lastAccessTime());
284     }
285 
286     /**
287      * Finishes the addition of entries to this archive, without closing it.
288      *
289      * @throws IOException if archive is already closed.
290      */
291     public void finish() throws IOException {
292         if (finished) {
293             throw new IOException("This archive has already been finished");
294         }
295         finished = true;
296 
297         final long headerPosition = channel.position();
298 
299         final ByteArrayOutputStream headerBaos = new ByteArrayOutputStream();
300         final DataOutputStream header = new DataOutputStream(headerBaos);
301 
302         writeHeader(header);
303         header.flush();
304         final byte[] headerBytes = headerBaos.toByteArray();
305         channel.write(ByteBuffer.wrap(headerBytes));
306 
307         final CRC32 crc32 = new CRC32();
308         crc32.update(headerBytes);
309 
310         final ByteBuffer bb = ByteBuffer.allocate(SevenZFile.sevenZSignature.length + 2 /* version */
311                 + 4 /* start header CRC */
312                 + 8 /* next header position */
313                 + 8 /* next header length */
314                 + 4 /* next header CRC */).order(ByteOrder.LITTLE_ENDIAN);
315         // signature header
316         channel.position(0);
317         bb.put(SevenZFile.sevenZSignature);
318         // version
319         bb.put((byte) 0).put((byte) 2);
320 
321         // placeholder for start header CRC
322         bb.putInt(0);
323 
324         // start header
325         bb.putLong(headerPosition - SevenZFile.SIGNATURE_HEADER_SIZE).putLong(0xffffFFFFL & headerBytes.length).putInt((int) crc32.getValue());
326         crc32.reset();
327         crc32.update(bb.array(), SevenZFile.sevenZSignature.length + 6, 20);
328         bb.putInt(SevenZFile.sevenZSignature.length + 2, (int) crc32.getValue());
329         bb.flip();
330         channel.write(bb);
331     }
332 
333     private Iterable<? extends SevenZMethodConfiguration> getContentMethods(final SevenZArchiveEntry entry) {
334         final Iterable<? extends SevenZMethodConfiguration> ms = entry.getContentMethods();
335         Iterable<? extends SevenZMethodConfiguration> iter = ms == null ? contentMethods : ms;
336 
337         if (aes256Options != null) {
338             // prepend encryption
339             iter = Stream
340                     .concat(Stream.of(new SevenZMethodConfiguration(SevenZMethod.AES256SHA256, aes256Options)), StreamSupport.stream(iter.spliterator(), false))
341                     .collect(Collectors.toList());
342         }
343         return iter;
344     }
345 
346     /*
347      * Creation of output stream is deferred until data is actually written as some codecs might write header information even for empty streams and directories
348      * otherwise.
349      */
350     private OutputStream getCurrentOutputStream() throws IOException {
351         if (currentOutputStream == null) {
352             currentOutputStream = setupFileOutputStream();
353         }
354         return currentOutputStream;
355     }
356 
357     /**
358      * Records an archive entry to add.
359      *
360      * The caller must then write the content to the archive and call {@link #closeArchiveEntry()} to complete the process.
361      *
362      * @param archiveEntry describes the entry
363      * @deprecated Use {@link #putArchiveEntry(SevenZArchiveEntry)}.
364      */
365     @Deprecated
366     public void putArchiveEntry(final ArchiveEntry archiveEntry) {
367         putArchiveEntry((SevenZArchiveEntry) archiveEntry);
368     }
369 
370     /**
371      * Records an archive entry to add.
372      *
373      * The caller must then write the content to the archive and call {@link #closeArchiveEntry()} to complete the process.
374      *
375      * @param archiveEntry describes the entry
376      * @since 1.25.0
377      */
378     public void putArchiveEntry(final SevenZArchiveEntry archiveEntry) {
379         files.add(archiveEntry);
380     }
381 
382     /**
383      * Sets the default compression method to use for entry contents - the default is LZMA2.
384      *
385      * <p>
386      * Currently only {@link SevenZMethod#COPY}, {@link SevenZMethod#LZMA2}, {@link SevenZMethod#BZIP2} and {@link SevenZMethod#DEFLATE} are supported.
387      * </p>
388      *
389      * <p>
390      * This is a short form for passing a single-element iterable to {@link #setContentMethods}.
391      * </p>
392      *
393      * @param method the default compression method
394      */
395     public void setContentCompression(final SevenZMethod method) {
396         setContentMethods(Collections.singletonList(new SevenZMethodConfiguration(method)));
397     }
398 
399     /**
400      * Sets the default (compression) methods to use for entry contents - the default is LZMA2.
401      *
402      * <p>
403      * Currently only {@link SevenZMethod#COPY}, {@link SevenZMethod#LZMA2}, {@link SevenZMethod#BZIP2} and {@link SevenZMethod#DEFLATE} are supported.
404      * </p>
405      *
406      * <p>
407      * The methods will be consulted in iteration order to create the final output.
408      * </p>
409      *
410      * @since 1.8
411      * @param methods the default (compression) methods
412      */
413     public void setContentMethods(final Iterable<? extends SevenZMethodConfiguration> methods) {
414         this.contentMethods = reverse(methods);
415     }
416 
417     private CountingOutputStream setupFileOutputStream() throws IOException {
418         if (files.isEmpty()) {
419             throw new IllegalStateException("No current 7z entry");
420         }
421 
422         // doesn't need to be closed, just wraps the instance field channel
423         OutputStream out = new OutputStreamWrapper(); // NOSONAR
424         final ArrayList<CountingOutputStream> moreStreams = new ArrayList<>();
425         boolean first = true;
426         for (final SevenZMethodConfiguration m : getContentMethods(files.get(files.size() - 1))) {
427             if (!first) {
428                 final CountingOutputStream cos = new CountingOutputStream(out);
429                 moreStreams.add(cos);
430                 out = cos;
431             }
432             out = Coders.addEncoder(out, m.getMethod(), m.getOptions());
433             first = false;
434         }
435         if (!moreStreams.isEmpty()) {
436             additionalCountingStreams = moreStreams.toArray(new CountingOutputStream[0]);
437         }
438         return new CountingOutputStream(out) {
439             @Override
440             public void write(final byte[] b) throws IOException {
441                 super.write(b);
442                 crc32.update(b);
443             }
444 
445             @Override
446             public void write(final byte[] b, final int off, final int len) throws IOException {
447                 super.write(b, off, len);
448                 crc32.update(b, off, len);
449             }
450 
451             @Override
452             public void write(final int b) throws IOException {
453                 super.write(b);
454                 crc32.update(b);
455             }
456         };
457     }
458 
459     /**
460      * Writes a byte array to the current archive entry.
461      *
462      * @param b The byte array to be written.
463      * @throws IOException on error
464      */
465     public void write(final byte[] b) throws IOException {
466         write(b, 0, b.length);
467     }
468 
469     /**
470      * Writes part of a byte array to the current archive entry.
471      *
472      * @param b   The byte array to be written.
473      * @param off offset into the array to start writing from
474      * @param len number of bytes to write
475      * @throws IOException on error
476      */
477     public void write(final byte[] b, final int off, final int len) throws IOException {
478         if (len > 0) {
479             getCurrentOutputStream().write(b, off, len);
480         }
481     }
482 
483     /**
484      * Writes all of the given input stream to the current archive entry.
485      *
486      * @param inputStream the data source.
487      * @throws IOException if an I/O error occurs.
488      * @since 1.21
489      */
490     public void write(final InputStream inputStream) throws IOException {
491         final byte[] buffer = new byte[8024];
492         int n = 0;
493         while (-1 != (n = inputStream.read(buffer))) {
494             write(buffer, 0, n);
495         }
496     }
497 
498     /**
499      * Writes a byte to the current archive entry.
500      *
501      * @param b The byte to be written.
502      * @throws IOException on error
503      */
504     public void write(final int b) throws IOException {
505         getCurrentOutputStream().write(b);
506     }
507 
508     /**
509      * Writes all of the given input stream to the current archive entry.
510      *
511      * @param path    the data source.
512      * @param options options specifying how the file is opened.
513      * @throws IOException if an I/O error occurs.
514      * @since 1.21
515      */
516     public void write(final Path path, final OpenOption... options) throws IOException {
517         try (InputStream in = new BufferedInputStream(Files.newInputStream(path, options))) {
518             write(in);
519         }
520     }
521 
522     private void writeBits(final DataOutput header, final BitSet bits, final int length) throws IOException {
523         int cache = 0;
524         int shift = 7;
525         for (int i = 0; i < length; i++) {
526             cache |= (bits.get(i) ? 1 : 0) << shift;
527             if (--shift < 0) {
528                 header.write(cache);
529                 shift = 7;
530                 cache = 0;
531             }
532         }
533         if (shift != 7) {
534             header.write(cache);
535         }
536     }
537 
538     private void writeFileAntiItems(final DataOutput header) throws IOException {
539         boolean hasAntiItems = false;
540         final BitSet antiItems = new BitSet(0);
541         int antiItemCounter = 0;
542         for (final SevenZArchiveEntry file1 : files) {
543             if (!file1.hasStream()) {
544                 final boolean isAnti = file1.isAntiItem();
545                 antiItems.set(antiItemCounter++, isAnti);
546                 hasAntiItems |= isAnti;
547             }
548         }
549         if (hasAntiItems) {
550             header.write(NID.kAnti);
551             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
552             final DataOutputStream out = new DataOutputStream(baos);
553             writeBits(out, antiItems, antiItemCounter);
554             out.flush();
555             final byte[] contents = baos.toByteArray();
556             writeUint64(header, contents.length);
557             header.write(contents);
558         }
559     }
560 
561     private void writeFileATimes(final DataOutput header) throws IOException {
562         int numAccessDates = 0;
563         for (final SevenZArchiveEntry entry : files) {
564             if (entry.getHasAccessDate()) {
565                 ++numAccessDates;
566             }
567         }
568         if (numAccessDates > 0) {
569             header.write(NID.kATime);
570 
571             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
572             final DataOutputStream out = new DataOutputStream(baos);
573             if (numAccessDates != files.size()) {
574                 out.write(0);
575                 final BitSet aTimes = new BitSet(files.size());
576                 for (int i = 0; i < files.size(); i++) {
577                     aTimes.set(i, files.get(i).getHasAccessDate());
578                 }
579                 writeBits(out, aTimes, files.size());
580             } else {
581                 out.write(1); // "allAreDefined" == true
582             }
583             out.write(0);
584             for (final SevenZArchiveEntry entry : files) {
585                 if (entry.getHasAccessDate()) {
586                     final long ntfsTime = FileTimes.toNtfsTime(entry.getAccessTime());
587                     out.writeLong(Long.reverseBytes(ntfsTime));
588                 }
589             }
590             out.flush();
591             final byte[] contents = baos.toByteArray();
592             writeUint64(header, contents.length);
593             header.write(contents);
594         }
595     }
596 
597     private void writeFileCTimes(final DataOutput header) throws IOException {
598         int numCreationDates = 0;
599         for (final SevenZArchiveEntry entry : files) {
600             if (entry.getHasCreationDate()) {
601                 ++numCreationDates;
602             }
603         }
604         if (numCreationDates > 0) {
605             header.write(NID.kCTime);
606 
607             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
608             final DataOutputStream out = new DataOutputStream(baos);
609             if (numCreationDates != files.size()) {
610                 out.write(0);
611                 final BitSet cTimes = new BitSet(files.size());
612                 for (int i = 0; i < files.size(); i++) {
613                     cTimes.set(i, files.get(i).getHasCreationDate());
614                 }
615                 writeBits(out, cTimes, files.size());
616             } else {
617                 out.write(1); // "allAreDefined" == true
618             }
619             out.write(0);
620             for (final SevenZArchiveEntry entry : files) {
621                 if (entry.getHasCreationDate()) {
622                     final long ntfsTime = FileTimes.toNtfsTime(entry.getCreationTime());
623                     out.writeLong(Long.reverseBytes(ntfsTime));
624                 }
625             }
626             out.flush();
627             final byte[] contents = baos.toByteArray();
628             writeUint64(header, contents.length);
629             header.write(contents);
630         }
631     }
632 
633     private void writeFileEmptyFiles(final DataOutput header) throws IOException {
634         boolean hasEmptyFiles = false;
635         int emptyStreamCounter = 0;
636         final BitSet emptyFiles = new BitSet(0);
637         for (final SevenZArchiveEntry file1 : files) {
638             if (!file1.hasStream()) {
639                 final boolean isDir = file1.isDirectory();
640                 emptyFiles.set(emptyStreamCounter++, !isDir);
641                 hasEmptyFiles |= !isDir;
642             }
643         }
644         if (hasEmptyFiles) {
645             header.write(NID.kEmptyFile);
646             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
647             final DataOutputStream out = new DataOutputStream(baos);
648             writeBits(out, emptyFiles, emptyStreamCounter);
649             out.flush();
650             final byte[] contents = baos.toByteArray();
651             writeUint64(header, contents.length);
652             header.write(contents);
653         }
654     }
655 
656     private void writeFileEmptyStreams(final DataOutput header) throws IOException {
657         final boolean hasEmptyStreams = files.stream().anyMatch(entry -> !entry.hasStream());
658         if (hasEmptyStreams) {
659             header.write(NID.kEmptyStream);
660             final BitSet emptyStreams = new BitSet(files.size());
661             for (int i = 0; i < files.size(); i++) {
662                 emptyStreams.set(i, !files.get(i).hasStream());
663             }
664             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
665             final DataOutputStream out = new DataOutputStream(baos);
666             writeBits(out, emptyStreams, files.size());
667             out.flush();
668             final byte[] contents = baos.toByteArray();
669             writeUint64(header, contents.length);
670             header.write(contents);
671         }
672     }
673 
674     private void writeFileMTimes(final DataOutput header) throws IOException {
675         int numLastModifiedDates = 0;
676         for (final SevenZArchiveEntry entry : files) {
677             if (entry.getHasLastModifiedDate()) {
678                 ++numLastModifiedDates;
679             }
680         }
681         if (numLastModifiedDates > 0) {
682             header.write(NID.kMTime);
683 
684             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
685             final DataOutputStream out = new DataOutputStream(baos);
686             if (numLastModifiedDates != files.size()) {
687                 out.write(0);
688                 final BitSet mTimes = new BitSet(files.size());
689                 for (int i = 0; i < files.size(); i++) {
690                     mTimes.set(i, files.get(i).getHasLastModifiedDate());
691                 }
692                 writeBits(out, mTimes, files.size());
693             } else {
694                 out.write(1); // "allAreDefined" == true
695             }
696             out.write(0);
697             for (final SevenZArchiveEntry entry : files) {
698                 if (entry.getHasLastModifiedDate()) {
699                     final long ntfsTime = FileTimes.toNtfsTime(entry.getLastModifiedTime());
700                     out.writeLong(Long.reverseBytes(ntfsTime));
701                 }
702             }
703             out.flush();
704             final byte[] contents = baos.toByteArray();
705             writeUint64(header, contents.length);
706             header.write(contents);
707         }
708     }
709 
710     private void writeFileNames(final DataOutput header) throws IOException {
711         header.write(NID.kName);
712 
713         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
714         final DataOutputStream out = new DataOutputStream(baos);
715         out.write(0);
716         for (final SevenZArchiveEntry entry : files) {
717             out.write(entry.getName().getBytes(UTF_16LE));
718             out.writeShort(0);
719         }
720         out.flush();
721         final byte[] contents = baos.toByteArray();
722         writeUint64(header, contents.length);
723         header.write(contents);
724     }
725 
726     private void writeFilesInfo(final DataOutput header) throws IOException {
727         header.write(NID.kFilesInfo);
728 
729         writeUint64(header, files.size());
730 
731         writeFileEmptyStreams(header);
732         writeFileEmptyFiles(header);
733         writeFileAntiItems(header);
734         writeFileNames(header);
735         writeFileCTimes(header);
736         writeFileATimes(header);
737         writeFileMTimes(header);
738         writeFileWindowsAttributes(header);
739         header.write(NID.kEnd);
740     }
741 
742     private void writeFileWindowsAttributes(final DataOutput header) throws IOException {
743         int numWindowsAttributes = 0;
744         for (final SevenZArchiveEntry entry : files) {
745             if (entry.getHasWindowsAttributes()) {
746                 ++numWindowsAttributes;
747             }
748         }
749         if (numWindowsAttributes > 0) {
750             header.write(NID.kWinAttributes);
751 
752             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
753             final DataOutputStream out = new DataOutputStream(baos);
754             if (numWindowsAttributes != files.size()) {
755                 out.write(0);
756                 final BitSet attributes = new BitSet(files.size());
757                 for (int i = 0; i < files.size(); i++) {
758                     attributes.set(i, files.get(i).getHasWindowsAttributes());
759                 }
760                 writeBits(out, attributes, files.size());
761             } else {
762                 out.write(1); // "allAreDefined" == true
763             }
764             out.write(0);
765             for (final SevenZArchiveEntry entry : files) {
766                 if (entry.getHasWindowsAttributes()) {
767                     out.writeInt(Integer.reverseBytes(entry.getWindowsAttributes()));
768                 }
769             }
770             out.flush();
771             final byte[] contents = baos.toByteArray();
772             writeUint64(header, contents.length);
773             header.write(contents);
774         }
775     }
776 
777     private void writeFolder(final DataOutput header, final SevenZArchiveEntry entry) throws IOException {
778         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
779         int numCoders = 0;
780         for (final SevenZMethodConfiguration m : getContentMethods(entry)) {
781             numCoders++;
782             writeSingleCodec(m, bos);
783         }
784 
785         writeUint64(header, numCoders);
786         header.write(bos.toByteArray());
787         for (long i = 0; i < numCoders - 1; i++) {
788             writeUint64(header, i + 1);
789             writeUint64(header, i);
790         }
791     }
792 
793     private void writeHeader(final DataOutput header) throws IOException {
794         header.write(NID.kHeader);
795 
796         header.write(NID.kMainStreamsInfo);
797         writeStreamsInfo(header);
798         writeFilesInfo(header);
799         header.write(NID.kEnd);
800     }
801 
802     private void writePackInfo(final DataOutput header) throws IOException {
803         header.write(NID.kPackInfo);
804 
805         writeUint64(header, 0);
806         writeUint64(header, 0xffffFFFFL & numNonEmptyStreams);
807 
808         header.write(NID.kSize);
809         for (final SevenZArchiveEntry entry : files) {
810             if (entry.hasStream()) {
811                 writeUint64(header, entry.getCompressedSize());
812             }
813         }
814 
815         header.write(NID.kCRC);
816         header.write(1); // "allAreDefined" == true
817         for (final SevenZArchiveEntry entry : files) {
818             if (entry.hasStream()) {
819                 header.writeInt(Integer.reverseBytes((int) entry.getCompressedCrcValue()));
820             }
821         }
822 
823         header.write(NID.kEnd);
824     }
825 
826     private void writeSingleCodec(final SevenZMethodConfiguration m, final OutputStream bos) throws IOException {
827         final byte[] id = m.getMethod().getId();
828         final byte[] properties = Coders.findByMethod(m.getMethod()).getOptionsAsProperties(m.getOptions());
829 
830         int codecFlags = id.length;
831         if (properties.length > 0) {
832             codecFlags |= 0x20;
833         }
834         bos.write(codecFlags);
835         bos.write(id);
836 
837         if (properties.length > 0) {
838             bos.write(properties.length);
839             bos.write(properties);
840         }
841     }
842 
843     private void writeStreamsInfo(final DataOutput header) throws IOException {
844         if (numNonEmptyStreams > 0) {
845             writePackInfo(header);
846             writeUnpackInfo(header);
847         }
848 
849         writeSubStreamsInfo(header);
850 
851         header.write(NID.kEnd);
852     }
853 
854     private void writeSubStreamsInfo(final DataOutput header) throws IOException {
855         header.write(NID.kSubStreamsInfo);
856         //
857         // header.write(NID.kCRC);
858         // header.write(1);
859         // for (final SevenZArchiveEntry entry : files) {
860         // if (entry.getHasCrc()) {
861         // header.writeInt(Integer.reverseBytes(entry.getCrc()));
862         // }
863         // }
864         //
865         header.write(NID.kEnd);
866     }
867 
868     private void writeUint64(final DataOutput header, long value) throws IOException {
869         int firstByte = 0;
870         int mask = 0x80;
871         int i;
872         for (i = 0; i < 8; i++) {
873             if (value < 1L << 7 * (i + 1)) {
874                 firstByte |= value >>> 8 * i;
875                 break;
876             }
877             firstByte |= mask;
878             mask >>>= 1;
879         }
880         header.write(firstByte);
881         for (; i > 0; i--) {
882             header.write((int) (0xff & value));
883             value >>>= 8;
884         }
885     }
886 
887     private void writeUnpackInfo(final DataOutput header) throws IOException {
888         header.write(NID.kUnpackInfo);
889 
890         header.write(NID.kFolder);
891         writeUint64(header, numNonEmptyStreams);
892         header.write(0);
893         for (final SevenZArchiveEntry entry : files) {
894             if (entry.hasStream()) {
895                 writeFolder(header, entry);
896             }
897         }
898 
899         header.write(NID.kCodersUnpackSize);
900         for (final SevenZArchiveEntry entry : files) {
901             if (entry.hasStream()) {
902                 final long[] moreSizes = additionalSizes.get(entry);
903                 if (moreSizes != null) {
904                     for (final long s : moreSizes) {
905                         writeUint64(header, s);
906                     }
907                 }
908                 writeUint64(header, entry.getSize());
909             }
910         }
911 
912         header.write(NID.kCRC);
913         header.write(1); // "allAreDefined" == true
914         for (final SevenZArchiveEntry entry : files) {
915             if (entry.hasStream()) {
916                 header.writeInt(Integer.reverseBytes((int) entry.getCrcValue()));
917             }
918         }
919 
920         header.write(NID.kEnd);
921     }
922 
923 }