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