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