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