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