COMPRESS-289 use a stable last modified time for long name entry,
based on patch by  Bob Robertson


git-svn-id: https://svn.apache.org/repos/asf/commons/proper/compress/trunk@1626280 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveOutputStream.java b/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveOutputStream.java
index 2a5d591..077720a 100644
--- a/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveOutputStream.java
+++ b/src/main/java/org/apache/commons/compress/archivers/tar/TarArchiveOutputStream.java
@@ -271,12 +271,12 @@
         TarArchiveEntry entry = (TarArchiveEntry) archiveEntry;
         Map<String, String> paxHeaders = new HashMap<String, String>();
         final String entryName = entry.getName();
-        boolean paxHeaderContainsPath = handleLongName(entryName, paxHeaders, "path",
+        boolean paxHeaderContainsPath = handleLongName(entry, entryName, paxHeaders, "path",
                                                        TarConstants.LF_GNUTYPE_LONGNAME, "file name");
 
         final String linkName = entry.getLinkName();
         boolean paxHeaderContainsLinkPath = linkName != null && linkName.length() > 0
-            && handleLongName(linkName, paxHeaders, "linkpath",
+            && handleLongName(entry, linkName, paxHeaders, "linkpath",
                               TarConstants.LF_GNUTYPE_LONGLINK, "link name");
 
         if (bigNumberMode == BIGNUMBER_POSIX) {
@@ -637,6 +637,7 @@
      *   <li>it truncates the name if longFileMode is TRUNCATE</li>
      * </ul></p>
      *
+     * @param entry entry the name belongs to
      * @param name the name to write
      * @param paxHeaders current map of pax headers
      * @param paxHeaderName name of the pax header to write
@@ -644,7 +645,7 @@
      * @param fieldName the name of the field
      * @return whether a pax header has been written.
      */
-    private boolean handleLongName(String name,
+    private boolean handleLongName(TarArchiveEntry entry , String name,
                                    Map<String, String> paxHeaders,
                                    String paxHeaderName, byte linkType, String fieldName)
         throws IOException {
@@ -661,6 +662,7 @@
                 TarArchiveEntry longLinkEntry = new TarArchiveEntry(TarConstants.GNU_LONGLINK, linkType);
 
                 longLinkEntry.setSize(len + 1); // +1 for NUL
+                longLinkEntry.setModTime(entry.getModTime());
                 putArchiveEntry(longLinkEntry);
                 write(encodedName.array(), encodedName.arrayOffset(), len);
                 write(0); // NUL terminator
diff --git a/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveOutputStreamTest.java b/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveOutputStreamTest.java
index 60017a6..f18f6a5 100644
--- a/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveOutputStreamTest.java
+++ b/src/test/java/org/apache/commons/compress/archivers/tar/TarArchiveOutputStreamTest.java
@@ -23,17 +23,23 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.MessageDigest;
 import java.util.Calendar;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.TimeZone;
 
 import org.apache.commons.compress.AbstractTestCase;
+import org.apache.commons.compress.archivers.ArchiveEntry;
 import org.apache.commons.compress.archivers.ArchiveOutputStream;
 import org.apache.commons.compress.archivers.ArchiveStreamFactory;
 import org.apache.commons.compress.utils.CharsetNames;
 import org.apache.commons.compress.utils.IOUtils;
 
+import org.junit.Assert;
+
 public class TarArchiveOutputStreamTest extends AbstractTestCase {
 
     public void testCount() throws Exception {
@@ -386,7 +392,7 @@
             tos.putArchiveEntry(t);
             tos.closeArchiveEntry();
             tos.close();
-            
+
             fail("Truncated name didn't throw an exception");
         } catch (RuntimeException e) {
             // expected
@@ -498,7 +504,7 @@
                 + "01234567890123456789012345678901234567890123456789/test";
         TarArchiveEntry entry = new TarArchiveEntry("test", TarConstants.LF_SYMLINK);
         entry.setLinkName(linkname);
-        
+
         try {
             ByteArrayOutputStream bos = new ByteArrayOutputStream();
             TarArchiveOutputStream tos = new TarArchiveOutputStream(bos, "ASCII");
@@ -506,7 +512,7 @@
             tos.putArchiveEntry(entry);
             tos.closeArchiveEntry();
             tos.close();
-            
+
             fail("Truncated link name didn't throw an exception");
         } catch (RuntimeException e) {
             // expected
@@ -519,14 +525,14 @@
             + "01234567890123456789012345678901234567890123456789/";
         TarArchiveEntry entry = new TarArchiveEntry("test" , TarConstants.LF_SYMLINK);
         entry.setLinkName(linkname);
-        
+
         ByteArrayOutputStream bos = new ByteArrayOutputStream();
         TarArchiveOutputStream tos = new TarArchiveOutputStream(bos, "ASCII");
         tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_TRUNCATE);
         tos.putArchiveEntry(entry);
         tos.closeArchiveEntry();
         tos.close();
-        
+
         byte[] data = bos.toByteArray();
         TarArchiveInputStream tin = new TarArchiveInputStream(new ByteArrayInputStream(data));
         TarArchiveEntry e = tin.getNextTarEntry();
@@ -557,14 +563,14 @@
             + "01234567890123456789012345678901234567890123456789/test";
         TarArchiveEntry entry = new TarArchiveEntry("test", TarConstants.LF_SYMLINK);
         entry.setLinkName(linkname);
-        
+
         ByteArrayOutputStream bos = new ByteArrayOutputStream();
         TarArchiveOutputStream tos = new TarArchiveOutputStream(bos, "ASCII");
         tos.setLongFileMode(mode);
         tos.putArchiveEntry(entry);
         tos.closeArchiveEntry();
         tos.close();
-        
+
         byte[] data = bos.toByteArray();
         TarArchiveInputStream tin = new TarArchiveInputStream(new ByteArrayInputStream(data));
         TarArchiveEntry e = tin.getNextTarEntry();
@@ -591,4 +597,57 @@
         assertEquals(TarConstants.DEFAULT_BLKSIZE, f.length());
     }
 
+    /**
+     * When using long file names the longLinkEntry included the
+     * current timestamp as the Entry modification date. This was
+     * never exposed to the client but it caused identical archives to
+     * have different MD5 hashes.
+     *
+     * @throws Exception
+     */
+    public void testLongNameMd5Hash() throws Exception {
+        final String longFileName = "a/considerably/longer/file/name/which/forces/use/of/the/long/link/header/which/appears/to/always/use/the/current/time/as/modification/date";
+        String fname = longFileName;
+        final Date modificationDate = new Date();
+
+        byte[] archive1 = createTarArchiveContainingOneDirectory(fname, modificationDate);
+        byte[] digest1 = MessageDigest.getInstance("MD5").digest(archive1);
+
+        // let a second elapse otherwise the modification dates will be equal
+        Thread.sleep(1000L);
+
+        // now recreate exactly the same tar file
+        byte[] archive2 = createTarArchiveContainingOneDirectory(fname, modificationDate);
+        // and I would expect the MD5 hash to be the same, but for long names it isn't
+        byte[] digest2 = MessageDigest.getInstance("MD5").digest(archive2);
+
+        Assert.assertArrayEquals(digest1, digest2);
+
+        // do I still have the correct modification date?
+        // let a second elapse so we don't get the current time
+        Thread.sleep(1000);
+        TarArchiveInputStream tarIn = new TarArchiveInputStream(new ByteArrayInputStream(archive2));
+        ArchiveEntry nextEntry = tarIn.getNextEntry();
+        assertEquals(longFileName, nextEntry.getName());
+        // tar archive stores modification time to second granularity only (floored)
+        assertEquals(modificationDate.getTime() / 1000, nextEntry.getLastModifiedDate().getTime() / 1000);
+        tarIn.close();
+    }
+
+    private static byte[] createTarArchiveContainingOneDirectory(String fname, Date modificationDate) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        TarArchiveOutputStream tarOut = new TarArchiveOutputStream(baos, 1024);
+        tarOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
+        TarArchiveEntry tarEntry = new TarArchiveEntry("d");
+        tarEntry.setModTime(modificationDate);
+        tarEntry.setMode(TarArchiveEntry.DEFAULT_DIR_MODE);
+        tarEntry.setModTime(modificationDate.getTime());
+        tarEntry.setName(fname);
+        tarOut.putArchiveEntry(tarEntry);
+        tarOut.closeArchiveEntry();
+        tarOut.close();
+
+        return baos.toByteArray();
+    }
+
 }