001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.commons.compress.archivers.tar; 020 021import java.io.File; 022import java.io.IOException; 023import java.io.OutputStream; 024import java.io.StringWriter; 025import java.nio.ByteBuffer; 026import java.util.Arrays; 027import java.util.Date; 028import java.util.HashMap; 029import java.util.Map; 030import org.apache.commons.compress.archivers.ArchiveEntry; 031import org.apache.commons.compress.archivers.ArchiveOutputStream; 032import org.apache.commons.compress.archivers.zip.ZipEncoding; 033import org.apache.commons.compress.archivers.zip.ZipEncodingHelper; 034import org.apache.commons.compress.utils.CharsetNames; 035import org.apache.commons.compress.utils.CountingOutputStream; 036 037/** 038 * The TarOutputStream writes a UNIX tar archive as an OutputStream. 039 * Methods are provided to put entries, and then write their contents 040 * by writing to this stream using write(). 041 * @NotThreadSafe 042 */ 043public class TarArchiveOutputStream extends ArchiveOutputStream { 044 /** Fail if a long file name is required in the archive. */ 045 public static final int LONGFILE_ERROR = 0; 046 047 /** Long paths will be truncated in the archive. */ 048 public static final int LONGFILE_TRUNCATE = 1; 049 050 /** GNU tar extensions are used to store long file names in the archive. */ 051 public static final int LONGFILE_GNU = 2; 052 053 /** POSIX/PAX extensions are used to store long file names in the archive. */ 054 public static final int LONGFILE_POSIX = 3; 055 056 /** Fail if a big number (e.g. size > 8GiB) is required in the archive. */ 057 public static final int BIGNUMBER_ERROR = 0; 058 059 /** star/GNU tar/BSD tar extensions are used to store big number in the archive. */ 060 public static final int BIGNUMBER_STAR = 1; 061 062 /** POSIX/PAX extensions are used to store big numbers in the archive. */ 063 public static final int BIGNUMBER_POSIX = 2; 064 065 private long currSize; 066 private String currName; 067 private long currBytes; 068 private final byte[] recordBuf; 069 private int assemLen; 070 private final byte[] assemBuf; 071 private int longFileMode = LONGFILE_ERROR; 072 private int bigNumberMode = BIGNUMBER_ERROR; 073 private int recordsWritten; 074 private final int recordsPerBlock; 075 private final int recordSize; 076 077 private boolean closed = false; 078 079 /** Indicates if putArchiveEntry has been called without closeArchiveEntry */ 080 private boolean haveUnclosedEntry = false; 081 082 /** indicates if this archive is finished */ 083 private boolean finished = false; 084 085 private final OutputStream out; 086 087 private final ZipEncoding zipEncoding; 088 089 // the provided encoding (for unit tests) 090 final String encoding; 091 092 private boolean addPaxHeadersForNonAsciiNames = false; 093 private static final ZipEncoding ASCII = 094 ZipEncodingHelper.getZipEncoding("ASCII"); 095 096 /** 097 * Constructor for TarInputStream. 098 * @param os the output stream to use 099 */ 100 public TarArchiveOutputStream(OutputStream os) { 101 this(os, TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE); 102 } 103 104 /** 105 * Constructor for TarInputStream. 106 * @param os the output stream to use 107 * @param encoding name of the encoding to use for file names 108 * @since 1.4 109 */ 110 public TarArchiveOutputStream(OutputStream os, String encoding) { 111 this(os, TarConstants.DEFAULT_BLKSIZE, TarConstants.DEFAULT_RCDSIZE, encoding); 112 } 113 114 /** 115 * Constructor for TarInputStream. 116 * @param os the output stream to use 117 * @param blockSize the block size to use 118 */ 119 public TarArchiveOutputStream(OutputStream os, int blockSize) { 120 this(os, blockSize, TarConstants.DEFAULT_RCDSIZE); 121 } 122 123 /** 124 * Constructor for TarInputStream. 125 * @param os the output stream to use 126 * @param blockSize the block size to use 127 * @param encoding name of the encoding to use for file names 128 * @since 1.4 129 */ 130 public TarArchiveOutputStream(OutputStream os, int blockSize, 131 String encoding) { 132 this(os, blockSize, TarConstants.DEFAULT_RCDSIZE, encoding); 133 } 134 135 /** 136 * Constructor for TarInputStream. 137 * @param os the output stream to use 138 * @param blockSize the block size to use 139 * @param recordSize the record size to use 140 */ 141 public TarArchiveOutputStream(OutputStream os, int blockSize, int recordSize) { 142 this(os, blockSize, recordSize, null); 143 } 144 145 /** 146 * Constructor for TarInputStream. 147 * @param os the output stream to use 148 * @param blockSize the block size to use 149 * @param recordSize the record size to use 150 * @param encoding name of the encoding to use for file names 151 * @since 1.4 152 */ 153 public TarArchiveOutputStream(OutputStream os, int blockSize, 154 int recordSize, String encoding) { 155 out = new CountingOutputStream(os); 156 this.encoding = encoding; 157 this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); 158 159 this.assemLen = 0; 160 this.assemBuf = new byte[recordSize]; 161 this.recordBuf = new byte[recordSize]; 162 this.recordSize = recordSize; 163 this.recordsPerBlock = blockSize / recordSize; 164 } 165 166 /** 167 * Set the long file mode. 168 * This can be LONGFILE_ERROR(0), LONGFILE_TRUNCATE(1) or LONGFILE_GNU(2). 169 * This specifies the treatment of long file names (names >= TarConstants.NAMELEN). 170 * Default is LONGFILE_ERROR. 171 * @param longFileMode the mode to use 172 */ 173 public void setLongFileMode(int longFileMode) { 174 this.longFileMode = longFileMode; 175 } 176 177 /** 178 * Set the big number mode. 179 * This can be BIGNUMBER_ERROR(0), BIGNUMBER_POSIX(1) or BIGNUMBER_STAR(2). 180 * This specifies the treatment of big files (sizes > TarConstants.MAXSIZE) and other numeric values to big to fit into a traditional tar header. 181 * Default is BIGNUMBER_ERROR. 182 * @param bigNumberMode the mode to use 183 * @since 1.4 184 */ 185 public void setBigNumberMode(int bigNumberMode) { 186 this.bigNumberMode = bigNumberMode; 187 } 188 189 /** 190 * Whether to add a PAX extension header for non-ASCII file names. 191 * @since 1.4 192 */ 193 public void setAddPaxHeadersForNonAsciiNames(boolean b) { 194 addPaxHeadersForNonAsciiNames = b; 195 } 196 197 @Deprecated 198 @Override 199 public int getCount() { 200 return (int) getBytesWritten(); 201 } 202 203 @Override 204 public long getBytesWritten() { 205 return ((CountingOutputStream) out).getBytesWritten(); 206 } 207 208 /** 209 * Ends the TAR archive without closing the underlying OutputStream. 210 * 211 * An archive consists of a series of file entries terminated by an 212 * end-of-archive entry, which consists of two 512 blocks of zero bytes. 213 * POSIX.1 requires two EOF records, like some other implementations. 214 * 215 * @throws IOException on error 216 */ 217 @Override 218 public void finish() throws IOException { 219 if (finished) { 220 throw new IOException("This archive has already been finished"); 221 } 222 223 if (haveUnclosedEntry) { 224 throw new IOException("This archives contains unclosed entries."); 225 } 226 writeEOFRecord(); 227 writeEOFRecord(); 228 padAsNeeded(); 229 out.flush(); 230 finished = true; 231 } 232 233 /** 234 * Closes the underlying OutputStream. 235 * @throws IOException on error 236 */ 237 @Override 238 public void close() throws IOException { 239 if (!finished) { 240 finish(); 241 } 242 243 if (!closed) { 244 out.close(); 245 closed = true; 246 } 247 } 248 249 /** 250 * Get the record size being used by this stream's TarBuffer. 251 * 252 * @return The TarBuffer record size. 253 */ 254 public int getRecordSize() { 255 return this.recordSize; 256 } 257 258 /** 259 * Put an entry on the output stream. This writes the entry's 260 * header record and positions the output stream for writing 261 * the contents of the entry. Once this method is called, the 262 * stream is ready for calls to write() to write the entry's 263 * contents. Once the contents are written, closeArchiveEntry() 264 * <B>MUST</B> be called to ensure that all buffered data 265 * is completely written to the output stream. 266 * 267 * @param archiveEntry The TarEntry to be written to the archive. 268 * @throws IOException on error 269 * @throws ClassCastException if archiveEntry is not an instance of TarArchiveEntry 270 */ 271 @Override 272 public void putArchiveEntry(ArchiveEntry archiveEntry) throws IOException { 273 if (finished) { 274 throw new IOException("Stream has already been finished"); 275 } 276 TarArchiveEntry entry = (TarArchiveEntry) archiveEntry; 277 Map<String, String> paxHeaders = new HashMap<String, String>(); 278 final String entryName = entry.getName(); 279 boolean paxHeaderContainsPath = handleLongName(entry, entryName, paxHeaders, "path", 280 TarConstants.LF_GNUTYPE_LONGNAME, "file name"); 281 282 final String linkName = entry.getLinkName(); 283 boolean paxHeaderContainsLinkPath = linkName != null && linkName.length() > 0 284 && handleLongName(entry, linkName, paxHeaders, "linkpath", 285 TarConstants.LF_GNUTYPE_LONGLINK, "link name"); 286 287 if (bigNumberMode == BIGNUMBER_POSIX) { 288 addPaxHeadersForBigNumbers(paxHeaders, entry); 289 } else if (bigNumberMode != BIGNUMBER_STAR) { 290 failForBigNumbers(entry); 291 } 292 293 if (addPaxHeadersForNonAsciiNames && !paxHeaderContainsPath 294 && !ASCII.canEncode(entryName)) { 295 paxHeaders.put("path", entryName); 296 } 297 298 if (addPaxHeadersForNonAsciiNames && !paxHeaderContainsLinkPath 299 && (entry.isLink() || entry.isSymbolicLink()) 300 && !ASCII.canEncode(linkName)) { 301 paxHeaders.put("linkpath", linkName); 302 } 303 304 if (paxHeaders.size() > 0) { 305 writePaxHeaders(entry, entryName, paxHeaders); 306 } 307 308 entry.writeEntryHeader(recordBuf, zipEncoding, 309 bigNumberMode == BIGNUMBER_STAR); 310 writeRecord(recordBuf); 311 312 currBytes = 0; 313 314 if (entry.isDirectory()) { 315 currSize = 0; 316 } else { 317 currSize = entry.getSize(); 318 } 319 currName = entryName; 320 haveUnclosedEntry = true; 321 } 322 323 /** 324 * Close an entry. This method MUST be called for all file 325 * entries that contain data. The reason is that we must 326 * buffer data written to the stream in order to satisfy 327 * the buffer's record based writes. Thus, there may be 328 * data fragments still being assembled that must be written 329 * to the output stream before this entry is closed and the 330 * next entry written. 331 * @throws IOException on error 332 */ 333 @Override 334 public void closeArchiveEntry() throws IOException { 335 if (finished) { 336 throw new IOException("Stream has already been finished"); 337 } 338 if (!haveUnclosedEntry){ 339 throw new IOException("No current entry to close"); 340 } 341 if (assemLen > 0) { 342 for (int i = assemLen; i < assemBuf.length; ++i) { 343 assemBuf[i] = 0; 344 } 345 346 writeRecord(assemBuf); 347 348 currBytes += assemLen; 349 assemLen = 0; 350 } 351 352 if (currBytes < currSize) { 353 throw new IOException("entry '" + currName + "' closed at '" 354 + currBytes 355 + "' before the '" + currSize 356 + "' bytes specified in the header were written"); 357 } 358 haveUnclosedEntry = false; 359 } 360 361 /** 362 * Writes bytes to the current tar archive entry. This method 363 * is aware of the current entry and will throw an exception if 364 * you attempt to write bytes past the length specified for the 365 * current entry. The method is also (painfully) aware of the 366 * record buffering required by TarBuffer, and manages buffers 367 * that are not a multiple of recordsize in length, including 368 * assembling records from small buffers. 369 * 370 * @param wBuf The buffer to write to the archive. 371 * @param wOffset The offset in the buffer from which to get bytes. 372 * @param numToWrite The number of bytes to write. 373 * @throws IOException on error 374 */ 375 @Override 376 public void write(byte[] wBuf, int wOffset, int numToWrite) throws IOException { 377 if (!haveUnclosedEntry) { 378 throw new IllegalStateException("No current tar entry"); 379 } 380 if (currBytes + numToWrite > currSize) { 381 throw new IOException("request to write '" + numToWrite 382 + "' bytes exceeds size in header of '" 383 + currSize + "' bytes for entry '" 384 + currName + "'"); 385 386 // 387 // We have to deal with assembly!!! 388 // The programmer can be writing little 32 byte chunks for all 389 // we know, and we must assemble complete records for writing. 390 // REVIEW Maybe this should be in TarBuffer? Could that help to 391 // eliminate some of the buffer copying. 392 // 393 } 394 395 if (assemLen > 0) { 396 if (assemLen + numToWrite >= recordBuf.length) { 397 int aLen = recordBuf.length - assemLen; 398 399 System.arraycopy(assemBuf, 0, recordBuf, 0, 400 assemLen); 401 System.arraycopy(wBuf, wOffset, recordBuf, 402 assemLen, aLen); 403 writeRecord(recordBuf); 404 405 currBytes += recordBuf.length; 406 wOffset += aLen; 407 numToWrite -= aLen; 408 assemLen = 0; 409 } else { 410 System.arraycopy(wBuf, wOffset, assemBuf, assemLen, 411 numToWrite); 412 413 wOffset += numToWrite; 414 assemLen += numToWrite; 415 numToWrite = 0; 416 } 417 } 418 419 // 420 // When we get here we have EITHER: 421 // o An empty "assemble" buffer. 422 // o No bytes to write (numToWrite == 0) 423 // 424 while (numToWrite > 0) { 425 if (numToWrite < recordBuf.length) { 426 System.arraycopy(wBuf, wOffset, assemBuf, assemLen, 427 numToWrite); 428 429 assemLen += numToWrite; 430 431 break; 432 } 433 434 writeRecord(wBuf, wOffset); 435 436 int num = recordBuf.length; 437 438 currBytes += num; 439 numToWrite -= num; 440 wOffset += num; 441 } 442 } 443 444 /** 445 * Writes a PAX extended header with the given map as contents. 446 * @since 1.4 447 */ 448 void writePaxHeaders(TarArchiveEntry entry, 449 String entryName, 450 Map<String, String> headers) throws IOException { 451 String name = "./PaxHeaders.X/" + stripTo7Bits(entryName); 452 if (name.length() >= TarConstants.NAMELEN) { 453 name = name.substring(0, TarConstants.NAMELEN - 1); 454 } 455 TarArchiveEntry pex = new TarArchiveEntry(name, 456 TarConstants.LF_PAX_EXTENDED_HEADER_LC); 457 transferModTime(entry, pex); 458 459 StringWriter w = new StringWriter(); 460 for (Map.Entry<String, String> h : headers.entrySet()) { 461 String key = h.getKey(); 462 String value = h.getValue(); 463 int len = key.length() + value.length() 464 + 3 /* blank, equals and newline */ 465 + 2 /* guess 9 < actual length < 100 */; 466 String line = len + " " + key + "=" + value + "\n"; 467 int actualLength = line.getBytes(CharsetNames.UTF_8).length; 468 while (len != actualLength) { 469 // Adjust for cases where length < 10 or > 100 470 // or where UTF-8 encoding isn't a single octet 471 // per character. 472 // Must be in loop as size may go from 99 to 100 in 473 // first pass so we'd need a second. 474 len = actualLength; 475 line = len + " " + key + "=" + value + "\n"; 476 actualLength = line.getBytes(CharsetNames.UTF_8).length; 477 } 478 w.write(line); 479 } 480 byte[] data = w.toString().getBytes(CharsetNames.UTF_8); 481 pex.setSize(data.length); 482 putArchiveEntry(pex); 483 write(data); 484 closeArchiveEntry(); 485 } 486 487 private String stripTo7Bits(String name) { 488 final int length = name.length(); 489 StringBuilder result = new StringBuilder(length); 490 for (int i = 0; i < length; i++) { 491 char stripped = (char) (name.charAt(i) & 0x7F); 492 if (shouldBeReplaced(stripped)) { 493 result.append("_"); 494 } else { 495 result.append(stripped); 496 } 497 } 498 return result.toString(); 499 } 500 501 /** 502 * @return true if the character could lead to problems when used 503 * inside a TarArchiveEntry name for a PAX header. 504 */ 505 private boolean shouldBeReplaced(char c) { 506 return c == 0 // would be read as Trailing null 507 || c == '/' // when used as last character TAE will consider the PAX header a directory 508 || c == '\\'; // same as '/' as slashes get "normalized" on Windows 509 } 510 511 /** 512 * Write an EOF (end of archive) record to the tar archive. 513 * An EOF record consists of a record of all zeros. 514 */ 515 private void writeEOFRecord() throws IOException { 516 Arrays.fill(recordBuf, (byte) 0); 517 writeRecord(recordBuf); 518 } 519 520 @Override 521 public void flush() throws IOException { 522 out.flush(); 523 } 524 525 @Override 526 public ArchiveEntry createArchiveEntry(File inputFile, String entryName) 527 throws IOException { 528 if(finished) { 529 throw new IOException("Stream has already been finished"); 530 } 531 return new TarArchiveEntry(inputFile, entryName); 532 } 533 534 /** 535 * Write an archive record to the archive. 536 * 537 * @param record The record data to write to the archive. 538 * @throws IOException on error 539 */ 540 private void writeRecord(byte[] record) throws IOException { 541 if (record.length != recordSize) { 542 throw new IOException("record to write has length '" 543 + record.length 544 + "' which is not the record size of '" 545 + recordSize + "'"); 546 } 547 548 out.write(record); 549 recordsWritten++; 550 } 551 552 /** 553 * Write an archive record to the archive, where the record may be 554 * inside of a larger array buffer. The buffer must be "offset plus 555 * record size" long. 556 * 557 * @param buf The buffer containing the record data to write. 558 * @param offset The offset of the record data within buf. 559 * @throws IOException on error 560 */ 561 private void writeRecord(byte[] buf, int offset) throws IOException { 562 563 if (offset + recordSize > buf.length) { 564 throw new IOException("record has length '" + buf.length 565 + "' with offset '" + offset 566 + "' which is less than the record size of '" 567 + recordSize + "'"); 568 } 569 570 out.write(buf, offset, recordSize); 571 recordsWritten++; 572 } 573 574 private void padAsNeeded() throws IOException { 575 int start = recordsWritten % recordsPerBlock; 576 if (start != 0) { 577 for (int i = start; i < recordsPerBlock; i++) { 578 writeEOFRecord(); 579 } 580 } 581 } 582 583 private void addPaxHeadersForBigNumbers(Map<String, String> paxHeaders, 584 TarArchiveEntry entry) { 585 addPaxHeaderForBigNumber(paxHeaders, "size", entry.getSize(), 586 TarConstants.MAXSIZE); 587 addPaxHeaderForBigNumber(paxHeaders, "gid", entry.getLongGroupId(), 588 TarConstants.MAXID); 589 addPaxHeaderForBigNumber(paxHeaders, "mtime", 590 entry.getModTime().getTime() / 1000, 591 TarConstants.MAXSIZE); 592 addPaxHeaderForBigNumber(paxHeaders, "uid", entry.getLongUserId(), 593 TarConstants.MAXID); 594 // star extensions by J\u00f6rg Schilling 595 addPaxHeaderForBigNumber(paxHeaders, "SCHILY.devmajor", 596 entry.getDevMajor(), TarConstants.MAXID); 597 addPaxHeaderForBigNumber(paxHeaders, "SCHILY.devminor", 598 entry.getDevMinor(), TarConstants.MAXID); 599 // there is no PAX header for file mode 600 failForBigNumber("mode", entry.getMode(), TarConstants.MAXID); 601 } 602 603 private void addPaxHeaderForBigNumber(Map<String, String> paxHeaders, 604 String header, long value, 605 long maxValue) { 606 if (value < 0 || value > maxValue) { 607 paxHeaders.put(header, String.valueOf(value)); 608 } 609 } 610 611 private void failForBigNumbers(TarArchiveEntry entry) { 612 failForBigNumber("entry size", entry.getSize(), TarConstants.MAXSIZE); 613 failForBigNumberWithPosixMessage("group id", entry.getLongGroupId(), TarConstants.MAXID); 614 failForBigNumber("last modification time", 615 entry.getModTime().getTime() / 1000, 616 TarConstants.MAXSIZE); 617 failForBigNumber("user id", entry.getLongUserId(), TarConstants.MAXID); 618 failForBigNumber("mode", entry.getMode(), TarConstants.MAXID); 619 failForBigNumber("major device number", entry.getDevMajor(), 620 TarConstants.MAXID); 621 failForBigNumber("minor device number", entry.getDevMinor(), 622 TarConstants.MAXID); 623 } 624 625 private void failForBigNumber(String field, long value, long maxValue) { 626 failForBigNumber(field, value, maxValue, ""); 627 } 628 629 private void failForBigNumberWithPosixMessage(String field, long value, long maxValue) { 630 failForBigNumber(field, value, maxValue, " Use STAR or POSIX extensions to overcome this limit"); 631 } 632 633 private void failForBigNumber(String field, long value, long maxValue, String additionalMsg) { 634 if (value < 0 || value > maxValue) { 635 throw new RuntimeException(field + " '" + value 636 + "' is too big ( > " 637 + maxValue + " )." + additionalMsg); 638 } 639 } 640 641 /** 642 * Handles long file or link names according to the longFileMode setting. 643 * 644 * <p>I.e. if the given name is too long to be written to a plain 645 * tar header then 646 * <ul> 647 * <li>it creates a pax header who's name is given by the 648 * paxHeaderName parameter if longFileMode is POSIX</li> 649 * <li>it creates a GNU longlink entry who's type is given by 650 * the linkType parameter if longFileMode is GNU</li> 651 * <li>it throws an exception if longFileMode is ERROR</li> 652 * <li>it truncates the name if longFileMode is TRUNCATE</li> 653 * </ul></p> 654 * 655 * @param entry entry the name belongs to 656 * @param name the name to write 657 * @param paxHeaders current map of pax headers 658 * @param paxHeaderName name of the pax header to write 659 * @param linkType type of the GNU entry to write 660 * @param fieldName the name of the field 661 * @return whether a pax header has been written. 662 */ 663 private boolean handleLongName(TarArchiveEntry entry , String name, 664 Map<String, String> paxHeaders, 665 String paxHeaderName, byte linkType, String fieldName) 666 throws IOException { 667 final ByteBuffer encodedName = zipEncoding.encode(name); 668 final int len = encodedName.limit() - encodedName.position(); 669 if (len >= TarConstants.NAMELEN) { 670 671 if (longFileMode == LONGFILE_POSIX) { 672 paxHeaders.put(paxHeaderName, name); 673 return true; 674 } else if (longFileMode == LONGFILE_GNU) { 675 // create a TarEntry for the LongLink, the contents 676 // of which are the link's name 677 TarArchiveEntry longLinkEntry = new TarArchiveEntry(TarConstants.GNU_LONGLINK, linkType); 678 679 longLinkEntry.setSize(len + 1); // +1 for NUL 680 transferModTime(entry, longLinkEntry); 681 putArchiveEntry(longLinkEntry); 682 write(encodedName.array(), encodedName.arrayOffset(), len); 683 write(0); // NUL terminator 684 closeArchiveEntry(); 685 } else if (longFileMode != LONGFILE_TRUNCATE) { 686 throw new RuntimeException(fieldName + " '" + name 687 + "' is too long ( > " 688 + TarConstants.NAMELEN + " bytes)"); 689 } 690 } 691 return false; 692 } 693 694 private void transferModTime(TarArchiveEntry from, TarArchiveEntry to) { 695 Date fromModTime = from.getModTime(); 696 long fromModTimeSeconds = fromModTime.getTime() / 1000; 697 if (fromModTimeSeconds < 0 || fromModTimeSeconds > TarConstants.MAXSIZE) { 698 fromModTime = new Date(0); 699 } 700 to.setModTime(fromModTime); 701 } 702}