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 */ 017package org.apache.commons.compress.archivers.tar; 018 019import java.io.ByteArrayOutputStream; 020import java.io.Closeable; 021import java.io.File; 022import java.io.IOException; 023import java.io.InputStream; 024import java.nio.ByteBuffer; 025import java.nio.channels.SeekableByteChannel; 026import java.nio.file.Files; 027import java.nio.file.Path; 028import java.util.ArrayList; 029import java.util.Arrays; 030import java.util.HashMap; 031import java.util.LinkedList; 032import java.util.List; 033import java.util.Map; 034 035import org.apache.commons.compress.archivers.zip.ZipEncoding; 036import org.apache.commons.compress.archivers.zip.ZipEncodingHelper; 037import org.apache.commons.compress.utils.ArchiveUtils; 038import org.apache.commons.compress.utils.BoundedArchiveInputStream; 039import org.apache.commons.compress.utils.BoundedInputStream; 040import org.apache.commons.compress.utils.BoundedSeekableByteChannelInputStream; 041import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; 042 043/** 044 * Provides random access to UNIX archives. 045 * 046 * @since 1.21 047 */ 048public class TarFile implements Closeable { 049 050 private final class BoundedTarEntryInputStream extends BoundedArchiveInputStream { 051 052 private final SeekableByteChannel channel; 053 054 private final TarArchiveEntry entry; 055 056 private long entryOffset; 057 058 private int currentSparseInputStreamIndex; 059 060 BoundedTarEntryInputStream(final TarArchiveEntry entry, final SeekableByteChannel channel) throws IOException { 061 super(entry.getDataOffset(), entry.getRealSize()); 062 if (channel.size() - entry.getSize() < entry.getDataOffset()) { 063 throw new IOException("entry size exceeds archive size"); 064 } 065 this.entry = entry; 066 this.channel = channel; 067 } 068 069 @Override 070 protected int read(final long pos, final ByteBuffer buf) throws IOException { 071 if (entryOffset >= entry.getRealSize()) { 072 return -1; 073 } 074 075 final int totalRead; 076 if (entry.isSparse()) { 077 totalRead = readSparse(entryOffset, buf, buf.limit()); 078 } else { 079 totalRead = readArchive(pos, buf); 080 } 081 082 if (totalRead == -1) { 083 if (buf.array().length > 0) { 084 throw new IOException("Truncated TAR archive"); 085 } 086 setAtEOF(true); 087 } else { 088 entryOffset += totalRead; 089 buf.flip(); 090 } 091 return totalRead; 092 } 093 094 private int readArchive(final long pos, final ByteBuffer buf) throws IOException { 095 channel.position(pos); 096 return channel.read(buf); 097 } 098 099 private int readSparse(final long pos, final ByteBuffer buf, final int numToRead) throws IOException { 100 // if there are no actual input streams, just read from the original archive 101 final List<InputStream> entrySparseInputStreams = sparseInputStreams.get(entry.getName()); 102 if (entrySparseInputStreams == null || entrySparseInputStreams.isEmpty()) { 103 return readArchive(entry.getDataOffset() + pos, buf); 104 } 105 106 if (currentSparseInputStreamIndex >= entrySparseInputStreams.size()) { 107 return -1; 108 } 109 110 final InputStream currentInputStream = entrySparseInputStreams.get(currentSparseInputStreamIndex); 111 final byte[] bufArray = new byte[numToRead]; 112 final int readLen = currentInputStream.read(bufArray); 113 if (readLen != -1) { 114 buf.put(bufArray, 0, readLen); 115 } 116 117 // if the current input stream is the last input stream, 118 // just return the number of bytes read from current input stream 119 if (currentSparseInputStreamIndex == entrySparseInputStreams.size() - 1) { 120 return readLen; 121 } 122 123 // if EOF of current input stream is meet, open a new input stream and recursively call read 124 if (readLen == -1) { 125 currentSparseInputStreamIndex++; 126 return readSparse(pos, buf, numToRead); 127 } 128 129 // if the rest data of current input stream is not long enough, open a new input stream 130 // and recursively call read 131 if (readLen < numToRead) { 132 currentSparseInputStreamIndex++; 133 final int readLenOfNext = readSparse(pos + readLen, buf, numToRead - readLen); 134 if (readLenOfNext == -1) { 135 return readLen; 136 } 137 138 return readLen + readLenOfNext; 139 } 140 141 // if the rest data of current input stream is enough(which means readLen == len), just return readLen 142 return readLen; 143 } 144 } 145 146 private static final int SMALL_BUFFER_SIZE = 256; 147 148 private final byte[] smallBuf = new byte[SMALL_BUFFER_SIZE]; 149 150 private final SeekableByteChannel archive; 151 152 /** 153 * The encoding of the tar file 154 */ 155 private final ZipEncoding zipEncoding; 156 157 private final LinkedList<TarArchiveEntry> entries = new LinkedList<>(); 158 159 private final int blockSize; 160 161 private final boolean lenient; 162 163 private final int recordSize; 164 165 private final ByteBuffer recordBuffer; 166 167 // the global sparse headers, this is only used in PAX Format 0.X 168 private final List<TarArchiveStructSparse> globalSparseHeaders = new ArrayList<>(); 169 170 private boolean hasHitEOF; 171 172 /** 173 * The meta-data about the current entry 174 */ 175 private TarArchiveEntry currEntry; 176 177 // the global PAX header 178 private Map<String, String> globalPaxHeaders = new HashMap<>(); 179 180 private final Map<String, List<InputStream>> sparseInputStreams = new HashMap<>(); 181 182 /** 183 * Constructor for TarFile. 184 * 185 * @param content the content to use 186 * @throws IOException when reading the tar archive fails 187 */ 188 public TarFile(final byte[] content) throws IOException { 189 this(new SeekableInMemoryByteChannel(content)); 190 } 191 192 /** 193 * Constructor for TarFile. 194 * 195 * @param content the content to use 196 * @param lenient when set to true illegal values for group/userid, mode, device numbers and timestamp will be 197 * ignored and the fields set to {@link TarArchiveEntry#UNKNOWN}. When set to false such illegal fields cause an 198 * exception instead. 199 * @throws IOException when reading the tar archive fails 200 */ 201 public TarFile(final byte[] content, final boolean lenient) throws IOException { 202 this(new SeekableInMemoryByteChannel(content), TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE, null, lenient); 203 } 204 205 /** 206 * Constructor for TarFile. 207 * 208 * @param content the content to use 209 * @param encoding the encoding to use 210 * @throws IOException when reading the tar archive fails 211 */ 212 public TarFile(final byte[] content, final String encoding) throws IOException { 213 this(new SeekableInMemoryByteChannel(content), TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE, encoding, false); 214 } 215 216 /** 217 * Constructor for TarFile. 218 * 219 * @param archive the file of the archive to use 220 * @throws IOException when reading the tar archive fails 221 */ 222 public TarFile(final File archive) throws IOException { 223 this(archive.toPath()); 224 } 225 226 /** 227 * Constructor for TarFile. 228 * 229 * @param archive the file of the archive to use 230 * @param lenient when set to true illegal values for group/userid, mode, device numbers and timestamp will be 231 * ignored and the fields set to {@link TarArchiveEntry#UNKNOWN}. When set to false such illegal fields cause an 232 * exception instead. 233 * @throws IOException when reading the tar archive fails 234 */ 235 public TarFile(final File archive, final boolean lenient) throws IOException { 236 this(archive.toPath(), lenient); 237 } 238 239 /** 240 * Constructor for TarFile. 241 * 242 * @param archive the file of the archive to use 243 * @param encoding the encoding to use 244 * @throws IOException when reading the tar archive fails 245 */ 246 public TarFile(final File archive, final String encoding) throws IOException { 247 this(archive.toPath(), encoding); 248 } 249 250 /** 251 * Constructor for TarFile. 252 * 253 * @param archivePath the path of the archive to use 254 * @throws IOException when reading the tar archive fails 255 */ 256 public TarFile(final Path archivePath) throws IOException { 257 this(Files.newByteChannel(archivePath), TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE, null, false); 258 } 259 260 /** 261 * Constructor for TarFile. 262 * 263 * @param archivePath the path of the archive to use 264 * @param lenient when set to true illegal values for group/userid, mode, device numbers and timestamp will be 265 * ignored and the fields set to {@link TarArchiveEntry#UNKNOWN}. When set to false such illegal fields cause an 266 * exception instead. 267 * @throws IOException when reading the tar archive fails 268 */ 269 public TarFile(final Path archivePath, final boolean lenient) throws IOException { 270 this(Files.newByteChannel(archivePath), TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE, null, lenient); 271 } 272 273 /** 274 * Constructor for TarFile. 275 * 276 * @param archivePath the path of the archive to use 277 * @param encoding the encoding to use 278 * @throws IOException when reading the tar archive fails 279 */ 280 public TarFile(final Path archivePath, final String encoding) throws IOException { 281 this(Files.newByteChannel(archivePath), TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE, encoding, false); 282 } 283 284 /** 285 * Constructor for TarFile. 286 * 287 * @param content the content to use 288 * @throws IOException when reading the tar archive fails 289 */ 290 public TarFile(final SeekableByteChannel content) throws IOException { 291 this(content, TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE, null, false); 292 } 293 294 /** 295 * Constructor for TarFile. 296 * 297 * @param archive the seekable byte channel to use 298 * @param blockSize the blocks size to use 299 * @param recordSize the record size to use 300 * @param encoding the encoding to use 301 * @param lenient when set to true illegal values for group/userid, mode, device numbers and timestamp will be 302 * ignored and the fields set to {@link TarArchiveEntry#UNKNOWN}. When set to false such illegal fields cause an 303 * exception instead. 304 * @throws IOException when reading the tar archive fails 305 */ 306 public TarFile(final SeekableByteChannel archive, final int blockSize, final int recordSize, final String encoding, final boolean lenient) throws IOException { 307 this.archive = archive; 308 this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); 309 this.recordSize = recordSize; 310 this.recordBuffer = ByteBuffer.allocate(this.recordSize); 311 this.blockSize = blockSize; 312 this.lenient = lenient; 313 314 TarArchiveEntry entry; 315 while ((entry = getNextTarEntry()) != null) { 316 entries.add(entry); 317 } 318 } 319 320 /** 321 * Update the current entry with the read pax headers 322 * @param headers Headers read from the pax header 323 * @param sparseHeaders Sparse headers read from pax header 324 */ 325 private void applyPaxHeadersToCurrentEntry(final Map<String, String> headers, final List<TarArchiveStructSparse> sparseHeaders) 326 throws IOException { 327 currEntry.updateEntryFromPaxHeaders(headers); 328 currEntry.setSparseHeaders(sparseHeaders); 329 } 330 331 /** 332 * Build the input streams consisting of all-zero input streams and non-zero input streams. 333 * When reading from the non-zero input streams, the data is actually read from the original input stream. 334 * The size of each input stream is introduced by the sparse headers. 335 * 336 * @implNote Some all-zero input streams and non-zero input streams have the size of 0. We DO NOT store the 337 * 0 size input streams because they are meaningless. 338 */ 339 private void buildSparseInputStreams() throws IOException { 340 final List<InputStream> streams = new ArrayList<>(); 341 342 final List<TarArchiveStructSparse> sparseHeaders = currEntry.getOrderedSparseHeaders(); 343 344 // Stream doesn't need to be closed at all as it doesn't use any resources 345 final InputStream zeroInputStream = new TarArchiveSparseZeroInputStream(); //NOSONAR 346 // logical offset into the extracted entry 347 long offset = 0; 348 long numberOfZeroBytesInSparseEntry = 0; 349 for (final TarArchiveStructSparse sparseHeader : sparseHeaders) { 350 final long zeroBlockSize = sparseHeader.getOffset() - offset; 351 if (zeroBlockSize < 0) { 352 // sparse header says to move backwards inside the extracted entry 353 throw new IOException("Corrupted struct sparse detected"); 354 } 355 356 // only store the zero block if it is not empty 357 if (zeroBlockSize > 0) { 358 streams.add(new BoundedInputStream(zeroInputStream, zeroBlockSize)); 359 numberOfZeroBytesInSparseEntry += zeroBlockSize; 360 } 361 362 // only store the input streams with non-zero size 363 if (sparseHeader.getNumbytes() > 0) { 364 final long start = 365 currEntry.getDataOffset() + sparseHeader.getOffset() - numberOfZeroBytesInSparseEntry; 366 if (start + sparseHeader.getNumbytes() < start) { 367 // possible integer overflow 368 throw new IOException("Unreadable TAR archive, sparse block offset or length too big"); 369 } 370 streams.add(new BoundedSeekableByteChannelInputStream(start, sparseHeader.getNumbytes(), archive)); 371 } 372 373 offset = sparseHeader.getOffset() + sparseHeader.getNumbytes(); 374 } 375 376 sparseInputStreams.put(currEntry.getName(), streams); 377 } 378 379 @Override 380 public void close() throws IOException { 381 archive.close(); 382 } 383 384 /** 385 * This method is invoked once the end of the archive is hit, it 386 * tries to consume the remaining bytes under the assumption that 387 * the tool creating this archive has padded the last block. 388 */ 389 private void consumeRemainderOfLastBlock() throws IOException { 390 final long bytesReadOfLastBlock = archive.position() % blockSize; 391 if (bytesReadOfLastBlock > 0) { 392 repositionForwardBy(blockSize - bytesReadOfLastBlock); 393 } 394 } 395 396 /** 397 * Get all TAR Archive Entries from the TarFile 398 * 399 * @return All entries from the tar file 400 */ 401 public List<TarArchiveEntry> getEntries() { 402 return new ArrayList<>(entries); 403 } 404 405 /** 406 * Gets the input stream for the provided Tar Archive Entry. 407 * @param entry Entry to get the input stream from 408 * @return Input stream of the provided entry 409 * @throws IOException Corrupted TAR archive. Can't read entry. 410 */ 411 public InputStream getInputStream(final TarArchiveEntry entry) throws IOException { 412 try { 413 return new BoundedTarEntryInputStream(entry, archive); 414 } catch (final RuntimeException ex) { 415 throw new IOException("Corrupted TAR archive. Can't read entry", ex); 416 } 417 } 418 419 /** 420 * Get the next entry in this tar archive as longname data. 421 * 422 * @return The next entry in the archive as longname data, or null. 423 * @throws IOException on error 424 */ 425 private byte[] getLongNameData() throws IOException { 426 final ByteArrayOutputStream longName = new ByteArrayOutputStream(); 427 int length; 428 try (final InputStream in = getInputStream(currEntry)) { 429 while ((length = in.read(smallBuf)) >= 0) { 430 longName.write(smallBuf, 0, length); 431 } 432 } 433 getNextTarEntry(); 434 if (currEntry == null) { 435 // Bugzilla: 40334 436 // Malformed tar file - long entry name not followed by entry 437 return null; 438 } 439 byte[] longNameData = longName.toByteArray(); 440 // remove trailing null terminator(s) 441 length = longNameData.length; 442 while (length > 0 && longNameData[length - 1] == 0) { 443 --length; 444 } 445 if (length != longNameData.length) { 446 longNameData = Arrays.copyOf(longNameData, length); 447 } 448 return longNameData; 449 } 450 451 /** 452 * Get the next entry in this tar archive. This will skip 453 * to the end of the current entry, if there is one, and 454 * place the position of the channel at the header of the 455 * next entry, and read the header and instantiate a new 456 * TarEntry from the header bytes and return that entry. 457 * If there are no more entries in the archive, null will 458 * be returned to indicate that the end of the archive has 459 * been reached. 460 * 461 * @return The next TarEntry in the archive, or null if there is no next entry. 462 * @throws IOException when reading the next TarEntry fails 463 */ 464 private TarArchiveEntry getNextTarEntry() throws IOException { 465 if (isAtEOF()) { 466 return null; 467 } 468 469 if (currEntry != null) { 470 // Skip to the end of the entry 471 repositionForwardTo(currEntry.getDataOffset() + currEntry.getSize()); 472 throwExceptionIfPositionIsNotInArchive(); 473 skipRecordPadding(); 474 } 475 476 final ByteBuffer headerBuf = getRecord(); 477 if (null == headerBuf) { 478 /* hit EOF */ 479 currEntry = null; 480 return null; 481 } 482 483 try { 484 final long position = archive.position(); 485 currEntry = new TarArchiveEntry(globalPaxHeaders, headerBuf.array(), zipEncoding, lenient, position); 486 } catch (final IllegalArgumentException e) { 487 throw new IOException("Error detected parsing the header", e); 488 } 489 490 if (currEntry.isGNULongLinkEntry()) { 491 final byte[] longLinkData = getLongNameData(); 492 if (longLinkData == null) { 493 // Bugzilla: 40334 494 // Malformed tar file - long link entry name not followed by 495 // entry 496 return null; 497 } 498 currEntry.setLinkName(zipEncoding.decode(longLinkData)); 499 } 500 501 if (currEntry.isGNULongNameEntry()) { 502 final byte[] longNameData = getLongNameData(); 503 if (longNameData == null) { 504 // Bugzilla: 40334 505 // Malformed tar file - long entry name not followed by 506 // entry 507 return null; 508 } 509 510 // COMPRESS-509 : the name of directories should end with '/' 511 final String name = zipEncoding.decode(longNameData); 512 currEntry.setName(name); 513 if (currEntry.isDirectory() && !name.endsWith("/")) { 514 currEntry.setName(name + "/"); 515 } 516 } 517 518 if (currEntry.isGlobalPaxHeader()) { // Process Global Pax headers 519 readGlobalPaxHeaders(); 520 } 521 522 try { 523 if (currEntry.isPaxHeader()) { // Process Pax headers 524 paxHeaders(); 525 } else if (!globalPaxHeaders.isEmpty()) { 526 applyPaxHeadersToCurrentEntry(globalPaxHeaders, globalSparseHeaders); 527 } 528 } catch (final NumberFormatException e) { 529 throw new IOException("Error detected parsing the pax header", e); 530 } 531 532 if (currEntry.isOldGNUSparse()) { // Process sparse files 533 readOldGNUSparse(); 534 } 535 536 return currEntry; 537 } 538 539 /** 540 * Get the next record in this tar archive. This will skip 541 * over any remaining data in the current entry, if there 542 * is one, and place the input stream at the header of the 543 * next entry. 544 * 545 * <p>If there are no more entries in the archive, null will be 546 * returned to indicate that the end of the archive has been 547 * reached. At the same time the {@code hasHitEOF} marker will be 548 * set to true.</p> 549 * 550 * @return The next TarEntry in the archive, or null if there is no next entry. 551 * @throws IOException when reading the next TarEntry fails 552 */ 553 private ByteBuffer getRecord() throws IOException { 554 ByteBuffer headerBuf = readRecord(); 555 setAtEOF(isEOFRecord(headerBuf)); 556 if (isAtEOF() && headerBuf != null) { 557 // Consume rest 558 tryToConsumeSecondEOFRecord(); 559 consumeRemainderOfLastBlock(); 560 headerBuf = null; 561 } 562 return headerBuf; 563 } 564 565 protected final boolean isAtEOF() { 566 return hasHitEOF; 567 } 568 569 private boolean isDirectory() { 570 return currEntry != null && currEntry.isDirectory(); 571 } 572 573 private boolean isEOFRecord(final ByteBuffer headerBuf) { 574 return headerBuf == null || ArchiveUtils.isArrayZero(headerBuf.array(), recordSize); 575 } 576 577 /** 578 * <p> 579 * For PAX Format 0.0, the sparse headers(GNU.sparse.offset and GNU.sparse.numbytes) 580 * may appear multi times, and they look like: 581 * <pre> 582 * GNU.sparse.size=size 583 * GNU.sparse.numblocks=numblocks 584 * repeat numblocks times 585 * GNU.sparse.offset=offset 586 * GNU.sparse.numbytes=numbytes 587 * end repeat 588 * </pre> 589 * 590 * <p> 591 * For PAX Format 0.1, the sparse headers are stored in a single variable : GNU.sparse.map 592 * <pre> 593 * GNU.sparse.map 594 * Map of non-null data chunks. It is a string consisting of comma-separated values "offset,size[,offset-1,size-1...]" 595 * </pre> 596 * 597 * <p> 598 * For PAX Format 1.X: 599 * <br> 600 * The sparse map itself is stored in the file data block, preceding the actual file data. 601 * It consists of a series of decimal numbers delimited by newlines. The map is padded with nulls to the nearest block boundary. 602 * The first number gives the number of entries in the map. Following are map entries, each one consisting of two numbers 603 * giving the offset and size of the data block it describes. 604 * @throws IOException 605 */ 606 private void paxHeaders() throws IOException { 607 List<TarArchiveStructSparse> sparseHeaders = new ArrayList<>(); 608 final Map<String, String> headers; 609 try (final InputStream input = getInputStream(currEntry)) { 610 headers = TarUtils.parsePaxHeaders(input, sparseHeaders, globalPaxHeaders, currEntry.getSize()); 611 } 612 613 // for 0.1 PAX Headers 614 if (headers.containsKey(TarGnuSparseKeys.MAP)) { 615 sparseHeaders = new ArrayList<>(TarUtils.parseFromPAX01SparseHeaders(headers.get(TarGnuSparseKeys.MAP))); 616 } 617 getNextTarEntry(); // Get the actual file entry 618 if (currEntry == null) { 619 throw new IOException("premature end of tar archive. Didn't find any entry after PAX header."); 620 } 621 applyPaxHeadersToCurrentEntry(headers, sparseHeaders); 622 623 // for 1.0 PAX Format, the sparse map is stored in the file data block 624 if (currEntry.isPaxGNU1XSparse()) { 625 try (final InputStream input = getInputStream(currEntry)) { 626 sparseHeaders = TarUtils.parsePAX1XSparseHeaders(input, recordSize); 627 } 628 currEntry.setSparseHeaders(sparseHeaders); 629 // data of the entry is after the pax gnu entry. So we need to update the data position once again 630 currEntry.setDataOffset(currEntry.getDataOffset() + recordSize); 631 } 632 633 // sparse headers are all done reading, we need to build 634 // sparse input streams using these sparse headers 635 buildSparseInputStreams(); 636 } 637 638 private void readGlobalPaxHeaders() throws IOException { 639 try (InputStream input = getInputStream(currEntry)) { 640 globalPaxHeaders = TarUtils.parsePaxHeaders(input, globalSparseHeaders, globalPaxHeaders, 641 currEntry.getSize()); 642 } 643 getNextTarEntry(); // Get the actual file entry 644 645 if (currEntry == null) { 646 throw new IOException("Error detected parsing the pax header"); 647 } 648 } 649 650 /** 651 * Adds the sparse chunks from the current entry to the sparse chunks, 652 * including any additional sparse entries following the current entry. 653 * 654 * @throws IOException when reading the sparse entry fails 655 */ 656 private void readOldGNUSparse() throws IOException { 657 if (currEntry.isExtended()) { 658 TarArchiveSparseEntry entry; 659 do { 660 final ByteBuffer headerBuf = getRecord(); 661 if (headerBuf == null) { 662 throw new IOException("premature end of tar archive. Didn't find extended_header after header with extended flag."); 663 } 664 entry = new TarArchiveSparseEntry(headerBuf.array()); 665 currEntry.getSparseHeaders().addAll(entry.getSparseHeaders()); 666 currEntry.setDataOffset(currEntry.getDataOffset() + recordSize); 667 } while (entry.isExtended()); 668 } 669 670 // sparse headers are all done reading, we need to build 671 // sparse input streams using these sparse headers 672 buildSparseInputStreams(); 673 } 674 675 /** 676 * Read a record from the input stream and return the data. 677 * 678 * @return The record data or null if EOF has been hit. 679 * @throws IOException if reading from the archive fails 680 */ 681 private ByteBuffer readRecord() throws IOException { 682 recordBuffer.rewind(); 683 final int readNow = archive.read(recordBuffer); 684 if (readNow != recordSize) { 685 return null; 686 } 687 return recordBuffer; 688 } 689 690 private void repositionForwardBy(final long offset) throws IOException { 691 repositionForwardTo(archive.position() + offset); 692 } 693 694 private void repositionForwardTo(final long newPosition) throws IOException { 695 final long currPosition = archive.position(); 696 if (newPosition < currPosition) { 697 throw new IOException("trying to move backwards inside of the archive"); 698 } 699 archive.position(newPosition); 700 } 701 702 protected final void setAtEOF(final boolean b) { 703 hasHitEOF = b; 704 } 705 706 /** 707 * The last record block should be written at the full size, so skip any 708 * additional space used to fill a record after an entry 709 * 710 * @throws IOException when skipping the padding of the record fails 711 */ 712 private void skipRecordPadding() throws IOException { 713 if (!isDirectory() && currEntry.getSize() > 0 && currEntry.getSize() % recordSize != 0) { 714 final long numRecords = (currEntry.getSize() / recordSize) + 1; 715 final long padding = (numRecords * recordSize) - currEntry.getSize(); 716 repositionForwardBy(padding); 717 throwExceptionIfPositionIsNotInArchive(); 718 } 719 } 720 721 /** 722 * Checks if the current position of the SeekableByteChannel is in the archive. 723 * @throws IOException If the position is not in the archive 724 */ 725 private void throwExceptionIfPositionIsNotInArchive() throws IOException { 726 if (archive.size() < archive.position()) { 727 throw new IOException("Truncated TAR archive"); 728 } 729 } 730 731 /** 732 * Tries to read the next record resetting the position in the 733 * archive if it is not an EOF record. 734 * 735 * <p>This is meant to protect against cases where a tar 736 * implementation has written only one EOF record when two are 737 * expected. Actually this won't help since a non-conforming 738 * implementation likely won't fill full blocks consisting of - by 739 * default - ten records either so we probably have already read 740 * beyond the archive anyway.</p> 741 * 742 * @throws IOException if reading the record of resetting the position in the archive fails 743 */ 744 private void tryToConsumeSecondEOFRecord() throws IOException { 745 boolean shouldReset = true; 746 try { 747 shouldReset = !isEOFRecord(readRecord()); 748 } finally { 749 if (shouldReset) { 750 archive.position(archive.position() - recordSize); 751 } 752 } 753 } 754}