JDK-8206389 : JarEntry.setCreation/LastAccessTime without setLastModifiedTime causes Invalid CEN header
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.util.jar
  • Affected Version: 9,10,11
  • Priority: P3
  • Status: Closed
  • Resolution: Fixed
  • OS: generic
  • CPU: generic
  • Submitted: 2018-06-30
  • Updated: 2018-11-30
  • Resolved: 2018-07-09
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 11
11.0.2Fixed
Description
ADDITIONAL SYSTEM INFORMATION :
Macbook Pro 15-inch 2016
Mac OS X High Sierra 10.13.5
OpenJDK jdk-10.0.1.jdk & jdk-11.jdk

A DESCRIPTION OF THE PROBLEM :
I have observed that when building a Jar using JarOutputStream, when adding a JarEntry if I call JarEntry.setLastModifiedTime prior to calling setTime, the Jar is built incorrectly and reading it using java.util.jar.JarFile will throw an exception.

java.util.zip.ZipException: invalid CEN header (bad header size)

Expected Results: Regardless of order, or calls to these methods should produce a valid Jar file or exception while writing the Jar file to indicate the error. 




REGRESSION : Last worked in version 8u171

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Open a JarOutputStream
2. Create a single JarEntry
3. setCreationTime
4. setLastAccessTime
5. setLastModifiedTime
6. setTime
7. JarOutputStream.putNextEntry

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Expect the Jar to be built correctly or for the JarOutputStream to fail if it is unable to be built correctly. 

This previously worked on Oracle JRE jdk1.8.0_171 and earlier.
ACTUAL -
The JarOutputStream does not fail, but the jar is not built correctly, and then reading the jar using java.util.jar.JarFile will throw java.util.zip.ZipException: invalid CEN header (bad header size).

I can recreate these results on Open JDK 10.0.1 and 11.

---------- BEGIN SOURCE ----------
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipException;

/**
 * @author Daniel DeGroff
 */
public class JarOutputStreamTest {

  public static void main(String[] args) throws IOException {

    // Create a test file to include in the jar.
    Files.deleteIfExists(Paths.get("./foo"));
    Path file = Files.createFile(Paths.get("./foo"));

    FileTime creationTime = (FileTime) Files.getAttribute(file, "creationTime");
    FileTime lastAccessTime = (FileTime) Files.getAttribute(file, "lastAccessTime");
    FileTime lastModifiedTime = Files.getLastModifiedTime(file);

    File jarFile = new File("testcase");
    jarFile.deleteOnExit();

    // 1. Passes, order is Ok.
    runTestCase(jarFile, file, entry -> {
      entry.setCreationTime(creationTime);
      entry.setLastAccessTime(lastAccessTime);

      // Calling setTime prior to setLastModifiedTime is Ok.
      entry.setTime(lastModifiedTime.toMillis());
      entry.setLastModifiedTime(lastModifiedTime);
    });

    // 2. Passes, omit the call to setTime
    runTestCase(jarFile, file, entry -> {
      entry.setCreationTime(creationTime);
      entry.setLastAccessTime(lastAccessTime);

      // Omitting the call to setTime is Ok.
      entry.setLastModifiedTime(lastModifiedTime);
    });

    // 3. Passes, omit setCreationTime and setLastAccessTime then order does not matter
    runTestCase(jarFile, file, entry -> {
      // Calling these two in either order is ok when we don't call setCreationTime and setLastAccessTime
      entry.setTime(lastModifiedTime.toMillis());
      entry.setLastModifiedTime(lastModifiedTime);
    });

    // 4. Passes, omit setCreationTime and setLastAccessTime then order does not matter
    runTestCase(jarFile, file, entry -> {
      // Calling these two in either order is ok when we don't call setCreationTime and setLastAccessTime
      entry.setLastModifiedTime(lastModifiedTime);
      entry.setTime(lastModifiedTime.toMillis());
    });

    // 5. Fails
    runTestCase(jarFile, file, entry -> {
      entry.setCreationTime(creationTime);
      entry.setLastAccessTime(lastAccessTime);

      // Calling setLastModifiedTime prior to setTime when also calling setCreationTime and setLastAccessTime fails.
      entry.setLastModifiedTime(lastModifiedTime);
      entry.setTime(lastModifiedTime.toMillis());
    });
  }


  private static void runTestCase(File jarFile, Path file, Consumer<JarEntry> consumer) throws IOException {
    try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(jarFile.toPath()), new Manifest())) {
      JarEntry entry = new JarEntry(file.toString());
      consumer.accept(entry);
      entry.setSize((Long) Files.getAttribute(file, "size"));
      jos.putNextEntry(entry);
      jos.flush();
      jos.closeEntry();
    }

    try {
      new JarFile(jarFile);
      System.out.println("Success!");
    } catch (ZipException e) {
      // Throws java.util.zip.ZipException: invalid CEN header (bad header size)
      System.out.println("Fail. " + e.getClass().getCanonicalName() + ": " + e.getMessage());
    }
  }
}

---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Workaround 1:

Call JarEntry.setTime before JarEntry.setLastModifiedTime

Workaround 2:

Leave setLastModifiedTime before setTime but omit setCreationTime and setLastAccessTime

Workaround 3:

Do not call setTime, instead only call setCreationTime, setLastAccessTime, and setLastModifiedTime

 

FREQUENCY : always



Comments
Fix Request: Usability regression with JarEntry library. Trivial fix. Already integrated into JDK 12 : Review thread : http://mail.openjdk.java.net/pipermail/core-libs-dev/2018-July/054302.html Tests updated also.
14-09-2018

the info-zip spec listed on java.util.zip package page does have the spec for it. http://www.info-zip.org/doc/appnote-19970311-iz.zip
07-07-2018

The closest thing to a spec is https://opensource.apple.com/source/zip/zip-6/unzip/unzip/proginfo/extra.fld Extended Timestamp Extra Field
07-07-2018

The problem is in ZipOutputStream::writeCEN(). It seems the EXTID_EXTT header field needs to always have the same format (9 bytes). So, when the test of (e.mtime != null) fails and we write the shortened version of the field that is when the problem occurs. It's hard to find a definitive specification for this extension, but one possible fix below just uses the xdostime value which should always be present even in the case where e.mtime == null. This seems to work. I'm not certain that the flag component is being set right however. diff -r 995f511e5530 src/java.base/share/classes/java/util/zip/ZipOutputStream.java --- a/src/java.base/share/classes/java/util/zip/ZipOutputStream.java Tue Jul 03 14:14:17 2018 +0100 +++ b/src/java.base/share/classes/java/util/zip/ZipOutputStream.java Thu Jul 05 17:47:31 2018 +0100 @@ -628,13 +628,12 @@ : fileTimeToWinTime(e.ctime)); } else { writeShort(EXTID_EXTT); + writeShort(5); // flag + mtime + writeByte(flagEXTT); if (e.mtime != null) { - writeShort(5); // flag + mtime - writeByte(flagEXTT); writeInt(umtime); } else { - writeShort(1); // flag only - writeByte(flagEXTT); + writeInt(e.xdostime); } } }
05-07-2018

Can reproduce from testcase. There is a comment in the writeCEN method of ZipOutputStream which looks suspicious. // cen info-zip extended timestamp only outputs mtime // but set the flag for a/ctime, if present in loc The code is implemented as above, ie the CEN header for the entry only includes the last modified time, but not the creation time or last access time, yet it sets the flags for all three attributes. Also, considering that if you edit the testcase to exclude the calls which set the a/ctime, then everything works correctly. That makes it seem possible that some inconsistency might appear in the CEN header. As to why the ordering of setTime() and setLastModifiedTime() affects it, that may be due to the interaction between the xdostime field, which is the original non-extended modification time and mtime, which is the extended one. ZipEntry.setTime() sets the mtime field to null, effectively disabling the extended header completely. All the above needs to be confirmed by reading the relevant specification for this stuff, which I have not found yet.
05-07-2018

To reproduce the issue, run the attached test case. Following are the results: JDK 8u171 - Pass JDK 9-ea+128 - Pass JDK 9-ea+129 - Fail JDK 9-ea+181 - Fail JDK 10.0.1- Fail JDK 11-ea+18 - Fail The regression was introduced in b129 of JDK 9. On scanning the bug fixes that went to this build, JDK-8161942 seems to be the only fix dealing with times in jar/zip files, so may be related to this issue.
05-07-2018