SevenZOutputFile.java

  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. import static java.nio.charset.StandardCharsets.UTF_16LE;

  19. import java.io.BufferedInputStream;
  20. import java.io.ByteArrayOutputStream;
  21. import java.io.Closeable;
  22. import java.io.DataOutput;
  23. import java.io.DataOutputStream;
  24. import java.io.File;
  25. import java.io.IOException;
  26. import java.io.InputStream;
  27. import java.io.OutputStream;
  28. import java.nio.ByteBuffer;
  29. import java.nio.ByteOrder;
  30. import java.nio.channels.SeekableByteChannel;
  31. import java.nio.file.Files;
  32. import java.nio.file.LinkOption;
  33. import java.nio.file.OpenOption;
  34. import java.nio.file.Path;
  35. import java.nio.file.StandardOpenOption;
  36. import java.nio.file.attribute.BasicFileAttributes;
  37. import java.util.ArrayList;
  38. import java.util.Arrays;
  39. import java.util.BitSet;
  40. import java.util.Collections;
  41. import java.util.Date;
  42. import java.util.EnumSet;
  43. import java.util.HashMap;
  44. import java.util.LinkedList;
  45. import java.util.List;
  46. import java.util.Map;
  47. import java.util.stream.Collectors;
  48. import java.util.stream.Stream;
  49. import java.util.stream.StreamSupport;
  50. import java.util.zip.CRC32;

  51. import org.apache.commons.compress.archivers.ArchiveEntry;
  52. import org.apache.commons.io.file.attribute.FileTimes;
  53. import org.apache.commons.io.output.CountingOutputStream;

  54. /**
  55.  * Writes a 7z file.
  56.  *
  57.  * @since 1.6
  58.  */
  59. public class SevenZOutputFile implements Closeable {

  60.     private final class OutputStreamWrapper extends OutputStream {

  61.         private static final int BUF_SIZE = 8192;
  62.         private final ByteBuffer buffer = ByteBuffer.allocate(BUF_SIZE);

  63.         @Override
  64.         public void close() throws IOException {
  65.             // the file will be closed by the containing class's close method
  66.         }

  67.         @Override
  68.         public void flush() throws IOException {
  69.             // no reason to flush the channel
  70.         }

  71.         @Override
  72.         public void write(final byte[] b) throws IOException {
  73.             write(b, 0, b.length);
  74.         }

  75.         @Override
  76.         public void write(final byte[] b, final int off, final int len) throws IOException {
  77.             if (len > BUF_SIZE) {
  78.                 channel.write(ByteBuffer.wrap(b, off, len));
  79.             } else {
  80.                 buffer.clear();
  81.                 buffer.put(b, off, len).flip();
  82.                 channel.write(buffer);
  83.             }
  84.             compressedCrc32.update(b, off, len);
  85.             fileBytesWritten += len;
  86.         }

  87.         @Override
  88.         public void write(final int b) throws IOException {
  89.             buffer.clear();
  90.             buffer.put((byte) b).flip();
  91.             channel.write(buffer);
  92.             compressedCrc32.update(b);
  93.             fileBytesWritten++;
  94.         }
  95.     }

  96.     private static <T> Iterable<T> reverse(final Iterable<T> i) {
  97.         final LinkedList<T> l = new LinkedList<>();
  98.         for (final T t : i) {
  99.             l.addFirst(t);
  100.         }
  101.         return l;
  102.     }

  103.     private final SeekableByteChannel channel;
  104.     private final List<SevenZArchiveEntry> files = new ArrayList<>();
  105.     private int numNonEmptyStreams;
  106.     private final CRC32 crc32 = new CRC32();
  107.     private final CRC32 compressedCrc32 = new CRC32();
  108.     private long fileBytesWritten;
  109.     private boolean finished;
  110.     private CountingOutputStream currentOutputStream;
  111.     private CountingOutputStream[] additionalCountingStreams;
  112.     private Iterable<? extends SevenZMethodConfiguration> contentMethods = Collections.singletonList(new SevenZMethodConfiguration(SevenZMethod.LZMA2));
  113.     private final Map<SevenZArchiveEntry, long[]> additionalSizes = new HashMap<>();
  114.     private AES256Options aes256Options;

  115.     /**
  116.      * Opens file to write a 7z archive to.
  117.      *
  118.      * @param fileName the file to write to
  119.      * @throws IOException if opening the file fails
  120.      */
  121.     public SevenZOutputFile(final File fileName) throws IOException {
  122.         this(fileName, null);
  123.     }

  124.     /**
  125.      * Opens file to write a 7z archive to.
  126.      *
  127.      * @param fileName the file to write to
  128.      * @param password optional password if the archive has to be encrypted
  129.      * @throws IOException if opening the file fails
  130.      * @since 1.23
  131.      */
  132.     public SevenZOutputFile(final File fileName, final char[] password) throws IOException {
  133.         this(Files.newByteChannel(fileName.toPath(), EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)),
  134.                 password);
  135.     }

  136.     /**
  137.      * Prepares channel to write a 7z archive to.
  138.      *
  139.      * <p>
  140.      * {@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to write to an in-memory archive.
  141.      * </p>
  142.      *
  143.      * @param channel the channel to write to
  144.      * @throws IOException if the channel cannot be positioned properly
  145.      * @since 1.13
  146.      */
  147.     public SevenZOutputFile(final SeekableByteChannel channel) throws IOException {
  148.         this(channel, null);
  149.     }

  150.     /**
  151.      * Prepares channel to write a 7z archive to.
  152.      *
  153.      * <p>
  154.      * {@link org.apache.commons.compress.utils.SeekableInMemoryByteChannel} allows you to write to an in-memory archive.
  155.      * </p>
  156.      *
  157.      * @param channel  the channel to write to
  158.      * @param password optional password if the archive has to be encrypted
  159.      * @throws IOException if the channel cannot be positioned properly
  160.      * @since 1.23
  161.      */
  162.     public SevenZOutputFile(final SeekableByteChannel channel, final char[] password) throws IOException {
  163.         this.channel = channel;
  164.         channel.position(SevenZFile.SIGNATURE_HEADER_SIZE);
  165.         if (password != null) {
  166.             this.aes256Options = new AES256Options(password);
  167.         }
  168.     }

  169.     /**
  170.      * Closes the archive, calling {@link #finish} if necessary.
  171.      *
  172.      * @throws IOException on error
  173.      */
  174.     @Override
  175.     public void close() throws IOException {
  176.         try {
  177.             if (!finished) {
  178.                 finish();
  179.             }
  180.         } finally {
  181.             channel.close();
  182.         }
  183.     }

  184.     /**
  185.      * Closes the archive entry.
  186.      *
  187.      * @throws IOException on error
  188.      */
  189.     public void closeArchiveEntry() throws IOException {
  190.         if (currentOutputStream != null) {
  191.             currentOutputStream.flush();
  192.             currentOutputStream.close();
  193.         }

  194.         final SevenZArchiveEntry entry = files.get(files.size() - 1);
  195.         if (fileBytesWritten > 0) { // this implies currentOutputStream != null
  196.             entry.setHasStream(true);
  197.             ++numNonEmptyStreams;
  198.             entry.setSize(currentOutputStream.getByteCount()); // NOSONAR
  199.             entry.setCompressedSize(fileBytesWritten);
  200.             entry.setCrcValue(crc32.getValue());
  201.             entry.setCompressedCrcValue(compressedCrc32.getValue());
  202.             entry.setHasCrc(true);
  203.             if (additionalCountingStreams != null) {
  204.                 final long[] sizes = new long[additionalCountingStreams.length];
  205.                 Arrays.setAll(sizes, i -> additionalCountingStreams[i].getByteCount());
  206.                 additionalSizes.put(entry, sizes);
  207.             }
  208.         } else {
  209.             entry.setHasStream(false);
  210.             entry.setSize(0);
  211.             entry.setCompressedSize(0);
  212.             entry.setHasCrc(false);
  213.         }
  214.         currentOutputStream = null;
  215.         additionalCountingStreams = null;
  216.         crc32.reset();
  217.         compressedCrc32.reset();
  218.         fileBytesWritten = 0;
  219.     }

  220.     /**
  221.      * Creates an archive entry using the inputFile and entryName provided.
  222.      *
  223.      * @param inputFile file to create an entry from
  224.      * @param entryName the name to use
  225.      * @return the ArchiveEntry set up with details from the file
  226.      */
  227.     public SevenZArchiveEntry createArchiveEntry(final File inputFile, final String entryName) {
  228.         final SevenZArchiveEntry entry = new SevenZArchiveEntry();
  229.         entry.setDirectory(inputFile.isDirectory());
  230.         entry.setName(entryName);
  231.         try {
  232.             fillDates(inputFile.toPath(), entry);
  233.         } catch (final IOException e) { // NOSONAR
  234.             entry.setLastModifiedDate(new Date(inputFile.lastModified()));
  235.         }
  236.         return entry;
  237.     }

  238.     /**
  239.      * Creates an archive entry using the inputPath and entryName provided.
  240.      *
  241.      * @param inputPath path to create an entry from
  242.      * @param entryName the name to use
  243.      * @param options   options indicating how symbolic links are handled.
  244.      * @return the ArchiveEntry set up with details from the file
  245.      *
  246.      * @throws IOException on error
  247.      * @since 1.21
  248.      */
  249.     public SevenZArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
  250.         final SevenZArchiveEntry entry = new SevenZArchiveEntry();
  251.         entry.setDirectory(Files.isDirectory(inputPath, options));
  252.         entry.setName(entryName);
  253.         fillDates(inputPath, entry, options);
  254.         return entry;
  255.     }

  256.     private void fillDates(final Path inputPath, final SevenZArchiveEntry entry, final LinkOption... options) throws IOException {
  257.         final BasicFileAttributes attributes = Files.readAttributes(inputPath, BasicFileAttributes.class, options);
  258.         entry.setLastModifiedTime(attributes.lastModifiedTime());
  259.         entry.setCreationTime(attributes.creationTime());
  260.         entry.setAccessTime(attributes.lastAccessTime());
  261.     }

  262.     /**
  263.      * Finishes the addition of entries to this archive, without closing it.
  264.      *
  265.      * @throws IOException if archive is already closed.
  266.      */
  267.     public void finish() throws IOException {
  268.         if (finished) {
  269.             throw new IOException("This archive has already been finished");
  270.         }
  271.         finished = true;

  272.         final long headerPosition = channel.position();

  273.         final ByteArrayOutputStream headerBaos = new ByteArrayOutputStream();
  274.         final DataOutputStream header = new DataOutputStream(headerBaos);

  275.         writeHeader(header);
  276.         header.flush();
  277.         final byte[] headerBytes = headerBaos.toByteArray();
  278.         channel.write(ByteBuffer.wrap(headerBytes));

  279.         final CRC32 crc32 = new CRC32();
  280.         crc32.update(headerBytes);

  281.         final ByteBuffer bb = ByteBuffer.allocate(SevenZFile.sevenZSignature.length + 2 /* version */
  282.                 + 4 /* start header CRC */
  283.                 + 8 /* next header position */
  284.                 + 8 /* next header length */
  285.                 + 4 /* next header CRC */).order(ByteOrder.LITTLE_ENDIAN);
  286.         // signature header
  287.         channel.position(0);
  288.         bb.put(SevenZFile.sevenZSignature);
  289.         // version
  290.         bb.put((byte) 0).put((byte) 2);

  291.         // placeholder for start header CRC
  292.         bb.putInt(0);

  293.         // start header
  294.         bb.putLong(headerPosition - SevenZFile.SIGNATURE_HEADER_SIZE).putLong(0xffffFFFFL & headerBytes.length).putInt((int) crc32.getValue());
  295.         crc32.reset();
  296.         crc32.update(bb.array(), SevenZFile.sevenZSignature.length + 6, 20);
  297.         bb.putInt(SevenZFile.sevenZSignature.length + 2, (int) crc32.getValue());
  298.         bb.flip();
  299.         channel.write(bb);
  300.     }

  301.     private Iterable<? extends SevenZMethodConfiguration> getContentMethods(final SevenZArchiveEntry entry) {
  302.         final Iterable<? extends SevenZMethodConfiguration> ms = entry.getContentMethods();
  303.         Iterable<? extends SevenZMethodConfiguration> iter = ms == null ? contentMethods : ms;

  304.         if (aes256Options != null) {
  305.             // prepend encryption
  306.             iter = Stream
  307.                     .concat(Stream.of(new SevenZMethodConfiguration(SevenZMethod.AES256SHA256, aes256Options)), StreamSupport.stream(iter.spliterator(), false))
  308.                     .collect(Collectors.toList());
  309.         }
  310.         return iter;
  311.     }

  312.     /*
  313.      * Creation of output stream is deferred until data is actually written as some codecs might write header information even for empty streams and directories
  314.      * otherwise.
  315.      */
  316.     private OutputStream getCurrentOutputStream() throws IOException {
  317.         if (currentOutputStream == null) {
  318.             currentOutputStream = setupFileOutputStream();
  319.         }
  320.         return currentOutputStream;
  321.     }

  322.     /**
  323.      * Records an archive entry to add.
  324.      *
  325.      * The caller must then write the content to the archive and call {@link #closeArchiveEntry()} to complete the process.
  326.      *
  327.      * @param archiveEntry describes the entry
  328.      * @deprecated Use {@link #putArchiveEntry(SevenZArchiveEntry)}.
  329.      */
  330.     @Deprecated
  331.     public void putArchiveEntry(final ArchiveEntry archiveEntry) {
  332.         putArchiveEntry((SevenZArchiveEntry) archiveEntry);
  333.     }

  334.     /**
  335.      * Records an archive entry to add.
  336.      *
  337.      * The caller must then write the content to the archive and call {@link #closeArchiveEntry()} to complete the process.
  338.      *
  339.      * @param archiveEntry describes the entry
  340.      * @since 1.25.0
  341.      */
  342.     public void putArchiveEntry(final SevenZArchiveEntry archiveEntry) {
  343.         files.add(archiveEntry);
  344.     }

  345.     /**
  346.      * Sets the default compression method to use for entry contents - the default is LZMA2.
  347.      *
  348.      * <p>
  349.      * Currently only {@link SevenZMethod#COPY}, {@link SevenZMethod#LZMA2}, {@link SevenZMethod#BZIP2} and {@link SevenZMethod#DEFLATE} are supported.
  350.      * </p>
  351.      *
  352.      * <p>
  353.      * This is a short form for passing a single-element iterable to {@link #setContentMethods}.
  354.      * </p>
  355.      *
  356.      * @param method the default compression method
  357.      */
  358.     public void setContentCompression(final SevenZMethod method) {
  359.         setContentMethods(Collections.singletonList(new SevenZMethodConfiguration(method)));
  360.     }

  361.     /**
  362.      * Sets the default (compression) methods to use for entry contents - the default is LZMA2.
  363.      *
  364.      * <p>
  365.      * Currently only {@link SevenZMethod#COPY}, {@link SevenZMethod#LZMA2}, {@link SevenZMethod#BZIP2} and {@link SevenZMethod#DEFLATE} are supported.
  366.      * </p>
  367.      *
  368.      * <p>
  369.      * The methods will be consulted in iteration order to create the final output.
  370.      * </p>
  371.      *
  372.      * @since 1.8
  373.      * @param methods the default (compression) methods
  374.      */
  375.     public void setContentMethods(final Iterable<? extends SevenZMethodConfiguration> methods) {
  376.         this.contentMethods = reverse(methods);
  377.     }

  378.     private CountingOutputStream setupFileOutputStream() throws IOException {
  379.         if (files.isEmpty()) {
  380.             throw new IllegalStateException("No current 7z entry");
  381.         }

  382.         // doesn't need to be closed, just wraps the instance field channel
  383.         OutputStream out = new OutputStreamWrapper(); // NOSONAR
  384.         final ArrayList<CountingOutputStream> moreStreams = new ArrayList<>();
  385.         boolean first = true;
  386.         for (final SevenZMethodConfiguration m : getContentMethods(files.get(files.size() - 1))) {
  387.             if (!first) {
  388.                 final CountingOutputStream cos = new CountingOutputStream(out);
  389.                 moreStreams.add(cos);
  390.                 out = cos;
  391.             }
  392.             out = Coders.addEncoder(out, m.getMethod(), m.getOptions());
  393.             first = false;
  394.         }
  395.         if (!moreStreams.isEmpty()) {
  396.             additionalCountingStreams = moreStreams.toArray(new CountingOutputStream[0]);
  397.         }
  398.         return new CountingOutputStream(out) {
  399.             @Override
  400.             public void write(final byte[] b) throws IOException {
  401.                 super.write(b);
  402.                 crc32.update(b);
  403.             }

  404.             @Override
  405.             public void write(final byte[] b, final int off, final int len) throws IOException {
  406.                 super.write(b, off, len);
  407.                 crc32.update(b, off, len);
  408.             }

  409.             @Override
  410.             public void write(final int b) throws IOException {
  411.                 super.write(b);
  412.                 crc32.update(b);
  413.             }
  414.         };
  415.     }

  416.     /**
  417.      * Writes a byte array to the current archive entry.
  418.      *
  419.      * @param b The byte array to be written.
  420.      * @throws IOException on error
  421.      */
  422.     public void write(final byte[] b) throws IOException {
  423.         write(b, 0, b.length);
  424.     }

  425.     /**
  426.      * Writes part of a byte array to the current archive entry.
  427.      *
  428.      * @param b   The byte array to be written.
  429.      * @param off offset into the array to start writing from
  430.      * @param len number of bytes to write
  431.      * @throws IOException on error
  432.      */
  433.     public void write(final byte[] b, final int off, final int len) throws IOException {
  434.         if (len > 0) {
  435.             getCurrentOutputStream().write(b, off, len);
  436.         }
  437.     }

  438.     /**
  439.      * Writes all of the given input stream to the current archive entry.
  440.      *
  441.      * @param inputStream the data source.
  442.      * @throws IOException if an I/O error occurs.
  443.      * @since 1.21
  444.      */
  445.     public void write(final InputStream inputStream) throws IOException {
  446.         final byte[] buffer = new byte[8024];
  447.         int n = 0;
  448.         while (-1 != (n = inputStream.read(buffer))) {
  449.             write(buffer, 0, n);
  450.         }
  451.     }

  452.     /**
  453.      * Writes a byte to the current archive entry.
  454.      *
  455.      * @param b The byte to be written.
  456.      * @throws IOException on error
  457.      */
  458.     public void write(final int b) throws IOException {
  459.         getCurrentOutputStream().write(b);
  460.     }

  461.     /**
  462.      * Writes all of the given input stream to the current archive entry.
  463.      *
  464.      * @param path    the data source.
  465.      * @param options options specifying how the file is opened.
  466.      * @throws IOException if an I/O error occurs.
  467.      * @since 1.21
  468.      */
  469.     public void write(final Path path, final OpenOption... options) throws IOException {
  470.         try (InputStream in = new BufferedInputStream(Files.newInputStream(path, options))) {
  471.             write(in);
  472.         }
  473.     }

  474.     private void writeBits(final DataOutput header, final BitSet bits, final int length) throws IOException {
  475.         int cache = 0;
  476.         int shift = 7;
  477.         for (int i = 0; i < length; i++) {
  478.             cache |= (bits.get(i) ? 1 : 0) << shift;
  479.             if (--shift < 0) {
  480.                 header.write(cache);
  481.                 shift = 7;
  482.                 cache = 0;
  483.             }
  484.         }
  485.         if (shift != 7) {
  486.             header.write(cache);
  487.         }
  488.     }

  489.     private void writeFileAntiItems(final DataOutput header) throws IOException {
  490.         boolean hasAntiItems = false;
  491.         final BitSet antiItems = new BitSet(0);
  492.         int antiItemCounter = 0;
  493.         for (final SevenZArchiveEntry file1 : files) {
  494.             if (!file1.hasStream()) {
  495.                 final boolean isAnti = file1.isAntiItem();
  496.                 antiItems.set(antiItemCounter++, isAnti);
  497.                 hasAntiItems |= isAnti;
  498.             }
  499.         }
  500.         if (hasAntiItems) {
  501.             header.write(NID.kAnti);
  502.             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
  503.             final DataOutputStream out = new DataOutputStream(baos);
  504.             writeBits(out, antiItems, antiItemCounter);
  505.             out.flush();
  506.             final byte[] contents = baos.toByteArray();
  507.             writeUint64(header, contents.length);
  508.             header.write(contents);
  509.         }
  510.     }

  511.     private void writeFileATimes(final DataOutput header) throws IOException {
  512.         int numAccessDates = 0;
  513.         for (final SevenZArchiveEntry entry : files) {
  514.             if (entry.getHasAccessDate()) {
  515.                 ++numAccessDates;
  516.             }
  517.         }
  518.         if (numAccessDates > 0) {
  519.             header.write(NID.kATime);

  520.             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
  521.             final DataOutputStream out = new DataOutputStream(baos);
  522.             if (numAccessDates != files.size()) {
  523.                 out.write(0);
  524.                 final BitSet aTimes = new BitSet(files.size());
  525.                 for (int i = 0; i < files.size(); i++) {
  526.                     aTimes.set(i, files.get(i).getHasAccessDate());
  527.                 }
  528.                 writeBits(out, aTimes, files.size());
  529.             } else {
  530.                 out.write(1); // "allAreDefined" == true
  531.             }
  532.             out.write(0);
  533.             for (final SevenZArchiveEntry entry : files) {
  534.                 if (entry.getHasAccessDate()) {
  535.                     final long ntfsTime = FileTimes.toNtfsTime(entry.getAccessTime());
  536.                     out.writeLong(Long.reverseBytes(ntfsTime));
  537.                 }
  538.             }
  539.             out.flush();
  540.             final byte[] contents = baos.toByteArray();
  541.             writeUint64(header, contents.length);
  542.             header.write(contents);
  543.         }
  544.     }

  545.     private void writeFileCTimes(final DataOutput header) throws IOException {
  546.         int numCreationDates = 0;
  547.         for (final SevenZArchiveEntry entry : files) {
  548.             if (entry.getHasCreationDate()) {
  549.                 ++numCreationDates;
  550.             }
  551.         }
  552.         if (numCreationDates > 0) {
  553.             header.write(NID.kCTime);

  554.             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
  555.             final DataOutputStream out = new DataOutputStream(baos);
  556.             if (numCreationDates != files.size()) {
  557.                 out.write(0);
  558.                 final BitSet cTimes = new BitSet(files.size());
  559.                 for (int i = 0; i < files.size(); i++) {
  560.                     cTimes.set(i, files.get(i).getHasCreationDate());
  561.                 }
  562.                 writeBits(out, cTimes, files.size());
  563.             } else {
  564.                 out.write(1); // "allAreDefined" == true
  565.             }
  566.             out.write(0);
  567.             for (final SevenZArchiveEntry entry : files) {
  568.                 if (entry.getHasCreationDate()) {
  569.                     final long ntfsTime = FileTimes.toNtfsTime(entry.getCreationTime());
  570.                     out.writeLong(Long.reverseBytes(ntfsTime));
  571.                 }
  572.             }
  573.             out.flush();
  574.             final byte[] contents = baos.toByteArray();
  575.             writeUint64(header, contents.length);
  576.             header.write(contents);
  577.         }
  578.     }

  579.     private void writeFileEmptyFiles(final DataOutput header) throws IOException {
  580.         boolean hasEmptyFiles = false;
  581.         int emptyStreamCounter = 0;
  582.         final BitSet emptyFiles = new BitSet(0);
  583.         for (final SevenZArchiveEntry file1 : files) {
  584.             if (!file1.hasStream()) {
  585.                 final boolean isDir = file1.isDirectory();
  586.                 emptyFiles.set(emptyStreamCounter++, !isDir);
  587.                 hasEmptyFiles |= !isDir;
  588.             }
  589.         }
  590.         if (hasEmptyFiles) {
  591.             header.write(NID.kEmptyFile);
  592.             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
  593.             final DataOutputStream out = new DataOutputStream(baos);
  594.             writeBits(out, emptyFiles, emptyStreamCounter);
  595.             out.flush();
  596.             final byte[] contents = baos.toByteArray();
  597.             writeUint64(header, contents.length);
  598.             header.write(contents);
  599.         }
  600.     }

  601.     private void writeFileEmptyStreams(final DataOutput header) throws IOException {
  602.         final boolean hasEmptyStreams = files.stream().anyMatch(entry -> !entry.hasStream());
  603.         if (hasEmptyStreams) {
  604.             header.write(NID.kEmptyStream);
  605.             final BitSet emptyStreams = new BitSet(files.size());
  606.             for (int i = 0; i < files.size(); i++) {
  607.                 emptyStreams.set(i, !files.get(i).hasStream());
  608.             }
  609.             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
  610.             final DataOutputStream out = new DataOutputStream(baos);
  611.             writeBits(out, emptyStreams, files.size());
  612.             out.flush();
  613.             final byte[] contents = baos.toByteArray();
  614.             writeUint64(header, contents.length);
  615.             header.write(contents);
  616.         }
  617.     }

  618.     private void writeFileMTimes(final DataOutput header) throws IOException {
  619.         int numLastModifiedDates = 0;
  620.         for (final SevenZArchiveEntry entry : files) {
  621.             if (entry.getHasLastModifiedDate()) {
  622.                 ++numLastModifiedDates;
  623.             }
  624.         }
  625.         if (numLastModifiedDates > 0) {
  626.             header.write(NID.kMTime);

  627.             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
  628.             final DataOutputStream out = new DataOutputStream(baos);
  629.             if (numLastModifiedDates != files.size()) {
  630.                 out.write(0);
  631.                 final BitSet mTimes = new BitSet(files.size());
  632.                 for (int i = 0; i < files.size(); i++) {
  633.                     mTimes.set(i, files.get(i).getHasLastModifiedDate());
  634.                 }
  635.                 writeBits(out, mTimes, files.size());
  636.             } else {
  637.                 out.write(1); // "allAreDefined" == true
  638.             }
  639.             out.write(0);
  640.             for (final SevenZArchiveEntry entry : files) {
  641.                 if (entry.getHasLastModifiedDate()) {
  642.                     final long ntfsTime = FileTimes.toNtfsTime(entry.getLastModifiedTime());
  643.                     out.writeLong(Long.reverseBytes(ntfsTime));
  644.                 }
  645.             }
  646.             out.flush();
  647.             final byte[] contents = baos.toByteArray();
  648.             writeUint64(header, contents.length);
  649.             header.write(contents);
  650.         }
  651.     }

  652.     private void writeFileNames(final DataOutput header) throws IOException {
  653.         header.write(NID.kName);

  654.         final ByteArrayOutputStream baos = new ByteArrayOutputStream();
  655.         final DataOutputStream out = new DataOutputStream(baos);
  656.         out.write(0);
  657.         for (final SevenZArchiveEntry entry : files) {
  658.             out.write(entry.getName().getBytes(UTF_16LE));
  659.             out.writeShort(0);
  660.         }
  661.         out.flush();
  662.         final byte[] contents = baos.toByteArray();
  663.         writeUint64(header, contents.length);
  664.         header.write(contents);
  665.     }

  666.     private void writeFilesInfo(final DataOutput header) throws IOException {
  667.         header.write(NID.kFilesInfo);

  668.         writeUint64(header, files.size());

  669.         writeFileEmptyStreams(header);
  670.         writeFileEmptyFiles(header);
  671.         writeFileAntiItems(header);
  672.         writeFileNames(header);
  673.         writeFileCTimes(header);
  674.         writeFileATimes(header);
  675.         writeFileMTimes(header);
  676.         writeFileWindowsAttributes(header);
  677.         header.write(NID.kEnd);
  678.     }

  679.     private void writeFileWindowsAttributes(final DataOutput header) throws IOException {
  680.         int numWindowsAttributes = 0;
  681.         for (final SevenZArchiveEntry entry : files) {
  682.             if (entry.getHasWindowsAttributes()) {
  683.                 ++numWindowsAttributes;
  684.             }
  685.         }
  686.         if (numWindowsAttributes > 0) {
  687.             header.write(NID.kWinAttributes);

  688.             final ByteArrayOutputStream baos = new ByteArrayOutputStream();
  689.             final DataOutputStream out = new DataOutputStream(baos);
  690.             if (numWindowsAttributes != files.size()) {
  691.                 out.write(0);
  692.                 final BitSet attributes = new BitSet(files.size());
  693.                 for (int i = 0; i < files.size(); i++) {
  694.                     attributes.set(i, files.get(i).getHasWindowsAttributes());
  695.                 }
  696.                 writeBits(out, attributes, files.size());
  697.             } else {
  698.                 out.write(1); // "allAreDefined" == true
  699.             }
  700.             out.write(0);
  701.             for (final SevenZArchiveEntry entry : files) {
  702.                 if (entry.getHasWindowsAttributes()) {
  703.                     out.writeInt(Integer.reverseBytes(entry.getWindowsAttributes()));
  704.                 }
  705.             }
  706.             out.flush();
  707.             final byte[] contents = baos.toByteArray();
  708.             writeUint64(header, contents.length);
  709.             header.write(contents);
  710.         }
  711.     }

  712.     private void writeFolder(final DataOutput header, final SevenZArchiveEntry entry) throws IOException {
  713.         final ByteArrayOutputStream bos = new ByteArrayOutputStream();
  714.         int numCoders = 0;
  715.         for (final SevenZMethodConfiguration m : getContentMethods(entry)) {
  716.             numCoders++;
  717.             writeSingleCodec(m, bos);
  718.         }

  719.         writeUint64(header, numCoders);
  720.         header.write(bos.toByteArray());
  721.         for (long i = 0; i < numCoders - 1; i++) {
  722.             writeUint64(header, i + 1);
  723.             writeUint64(header, i);
  724.         }
  725.     }

  726.     private void writeHeader(final DataOutput header) throws IOException {
  727.         header.write(NID.kHeader);

  728.         header.write(NID.kMainStreamsInfo);
  729.         writeStreamsInfo(header);
  730.         writeFilesInfo(header);
  731.         header.write(NID.kEnd);
  732.     }

  733.     private void writePackInfo(final DataOutput header) throws IOException {
  734.         header.write(NID.kPackInfo);

  735.         writeUint64(header, 0);
  736.         writeUint64(header, 0xffffFFFFL & numNonEmptyStreams);

  737.         header.write(NID.kSize);
  738.         for (final SevenZArchiveEntry entry : files) {
  739.             if (entry.hasStream()) {
  740.                 writeUint64(header, entry.getCompressedSize());
  741.             }
  742.         }

  743.         header.write(NID.kCRC);
  744.         header.write(1); // "allAreDefined" == true
  745.         for (final SevenZArchiveEntry entry : files) {
  746.             if (entry.hasStream()) {
  747.                 header.writeInt(Integer.reverseBytes((int) entry.getCompressedCrcValue()));
  748.             }
  749.         }

  750.         header.write(NID.kEnd);
  751.     }

  752.     private void writeSingleCodec(final SevenZMethodConfiguration m, final OutputStream bos) throws IOException {
  753.         final byte[] id = m.getMethod().getId();
  754.         final byte[] properties = Coders.findByMethod(m.getMethod()).getOptionsAsProperties(m.getOptions());

  755.         int codecFlags = id.length;
  756.         if (properties.length > 0) {
  757.             codecFlags |= 0x20;
  758.         }
  759.         bos.write(codecFlags);
  760.         bos.write(id);

  761.         if (properties.length > 0) {
  762.             bos.write(properties.length);
  763.             bos.write(properties);
  764.         }
  765.     }

  766.     private void writeStreamsInfo(final DataOutput header) throws IOException {
  767.         if (numNonEmptyStreams > 0) {
  768.             writePackInfo(header);
  769.             writeUnpackInfo(header);
  770.         }

  771.         writeSubStreamsInfo(header);

  772.         header.write(NID.kEnd);
  773.     }

  774.     private void writeSubStreamsInfo(final DataOutput header) throws IOException {
  775.         header.write(NID.kSubStreamsInfo);
  776.         //
  777.         // header.write(NID.kCRC);
  778.         // header.write(1);
  779.         // for (final SevenZArchiveEntry entry : files) {
  780.         // if (entry.getHasCrc()) {
  781.         // header.writeInt(Integer.reverseBytes(entry.getCrc()));
  782.         // }
  783.         // }
  784.         //
  785.         header.write(NID.kEnd);
  786.     }

  787.     private void writeUint64(final DataOutput header, long value) throws IOException {
  788.         int firstByte = 0;
  789.         int mask = 0x80;
  790.         int i;
  791.         for (i = 0; i < 8; i++) {
  792.             if (value < 1L << 7 * (i + 1)) {
  793.                 firstByte |= value >>> 8 * i;
  794.                 break;
  795.             }
  796.             firstByte |= mask;
  797.             mask >>>= 1;
  798.         }
  799.         header.write(firstByte);
  800.         for (; i > 0; i--) {
  801.             header.write((int) (0xff & value));
  802.             value >>>= 8;
  803.         }
  804.     }

  805.     private void writeUnpackInfo(final DataOutput header) throws IOException {
  806.         header.write(NID.kUnpackInfo);

  807.         header.write(NID.kFolder);
  808.         writeUint64(header, numNonEmptyStreams);
  809.         header.write(0);
  810.         for (final SevenZArchiveEntry entry : files) {
  811.             if (entry.hasStream()) {
  812.                 writeFolder(header, entry);
  813.             }
  814.         }

  815.         header.write(NID.kCodersUnpackSize);
  816.         for (final SevenZArchiveEntry entry : files) {
  817.             if (entry.hasStream()) {
  818.                 final long[] moreSizes = additionalSizes.get(entry);
  819.                 if (moreSizes != null) {
  820.                     for (final long s : moreSizes) {
  821.                         writeUint64(header, s);
  822.                     }
  823.                 }
  824.                 writeUint64(header, entry.getSize());
  825.             }
  826.         }

  827.         header.write(NID.kCRC);
  828.         header.write(1); // "allAreDefined" == true
  829.         for (final SevenZArchiveEntry entry : files) {
  830.             if (entry.hasStream()) {
  831.                 header.writeInt(Integer.reverseBytes((int) entry.getCrcValue()));
  832.             }
  833.         }

  834.         header.write(NID.kEnd);
  835.     }

  836. }