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