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