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