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