Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jars generated through TinyBundles 3.0.0 have a MANIFEST.MF file with a system LastModified date in 2106 #46

Open
ikannak opened this issue Jan 11, 2022 · 4 comments
Assignees
Labels

Comments

@ikannak
Copy link

ikannak commented Jan 11, 2022

We started noticing this last Saturday, but it appears to be happening in our code at least since we updated TinyBundles to 3.0.0 in 2020. We allow customers to automatically generate bnd jar bundles for use in Karaf using tinybundles 3.0.0, using the following code:

            //Initiate the bundle
	TinyBundle bundle = bundle();
	
	// all uploaded resources in the source location (scripts, properties, etc) and the blueprint.xml
	// must be added by using the "Include Resource" bundle header (from bnd) in order to include them in the generated bundle
	File sourceDir = new File(sourceLocation);
	File targetDir = new File(targetLocation);
	Collection<File> sourceDirFiles = FileUtils.listFiles(sourceDir, null,true);

	StringBuilder relativePathBuilder = new StringBuilder();
	String targetJarFilePath = targetLocation + "/" + phaseName + "-" + phaseVersion + ".jar";
	try {
		boolean firstPath = true;
		for (File file : sourceDirFiles) {
			URL fileUrl = file.toURI().toURL();
			String relativePath = file.getPath().substring(sourceLocation.length() + 1);
			// convert back slashes to forward ones: needed by OSGI 
			relativePath = util.fileutils.FileUtils.separatorsToUnix(relativePath);
			LOGGER.debug("Include-Resource header: adding resource with name=" + relativePath
					+ ",URL=" + fileUrl);
			bundle.add(relativePath, fileUrl);
			if (!firstPath) {
				relativePathBuilder.append(",");
			}
			relativePathBuilder.append(relativePath).append("=").append(relativePath);
			firstPath = false;
		}

		// set all the relative resource paths as the value of the Include-Resource instruction
		bundle.set(Constants.INCLUDE_RESOURCE, relativePathBuilder.toString());
		// set all other Bundle Headers
		bundle.set(Constants.BUNDLE_VERSION , phaseVersion);
		bundle.set(Constants.BUNDLE_NAME, phaseDisplayName);
		bundle.set(Constants.BUNDLE_SYMBOLICNAME ,  phaseName);
		bundle.set(Constants.PRIVATE_PACKAGE, "");
		bundle.set(Constants.IMPORT_PACKAGE , IMPORT_PACKAGES_SCRIPTING_PHASE);
		//Constants.BUNDLE_BLUEPRINT bnd constant wrongly prints Bundle-Copyright, so made internal constant
		bundle.set(BUNDLE_BLUEPRINT, "OSGI-INF" +  "/" + BLUEPRINT_FILENAME);

		// create the bundle as an inputstream with tinybundles and bnd
		InputStream outputBundleJarInputStream = bundle.build(withBnd());
		// Create the location if it does not exist
		
		// if the target directory does not exist, create it
		if (!targetDir.exists()) {
			FileUtils.forceMkdir(targetDir);
		}
		// write the bundle jar
		writeJar(outputBundleJarInputStream, targetJarFilePath);

With writeJar being the following method:

     /**
 * write the contents of the InputStream to a file with specified path
 * 
 * @param jarFileInputStream
 * @param targetJarFilePath
 * @throws IOException
 */
private void writeJar(InputStream jarFileInputStream,
		String targetJarFilePath) throws IOException {
	FileOutputStream targetJarOutputStream = new FileOutputStream(
			targetJarFilePath);
	LOGGER.debug("writing bundle jar to " + targetJarFilePath);
	BufferedInputStream in = new BufferedInputStream(jarFileInputStream);
	BufferedOutputStream out = new BufferedOutputStream(
			targetJarOutputStream);
	try {
		byte[] buffer = new byte[10000];
		while (true) {
			int bytesRead = in.read(buffer);
			if (bytesRead == -1) {
				break;
			}
			out.write(buffer, 0, bytesRead);
		}
	} finally {
		in.close();
		out.close();
	}

	LOGGER.debug("Bundle jar created.");
}

This might look like a lot of unnecessary code, but I have tested this with JUST:

            //Initiate the bundle
	TinyBundle bundle = bundle();
	// create the bundle as an inputstream with tinybundles and bnd
            InputStream outputBundleJarInputStream = bundle.build(withBnd());
	
	// write the bundle jar
	writeJar(outputBundleJarInputStream, targetJarFilePath);

So with essentially an empty bundle, and the Manifest.MF file was still generated with a LastModifed system date that was set in 2106. This does not affect the functionality of the jar (it still works in the way that we expect it to) and it can safely be unpacked using tools like 7zip, and it can also be read as a Java Jar class. The main issue this brings is that a custom unzip process using native Java code needs to account for this, because if this LastModified property is used directly for the same property on a Java File object, it fails with error java.lang.IllegalArgumentException: Negative time
at java.base/java.io.File.setLastModified(File.java:1441)

I have attached a zip file containing an example jar as generated by the second code snippet.

TestScriptingPhaseBlueprintAndBundleGeneration-1.0.0.jar.zip

@ikannak ikannak changed the title Jars generated through TinyBundles 3.0.0 suddenly have a MANIFEST.MF file with a system LastModified date in 2106 Jars generated through TinyBundles 3.0.0 have a MANIFEST.MF file with a system LastModified date in 2106 Jan 11, 2022
@ikannak
Copy link
Author

ikannak commented Jan 13, 2022

I have checked and compared Tinybundles 3.0.0 with bndlib 3.5.0 to tinybundles 1.0.0 and bndlib 1.43.0, the previous version we used.

It appears that the change that introduced this change in date representation was in bndlib. Apparently, the Jar.class doManifest() method has been updated in recent versions to use the lastModified date used in the Jar as the new value for the lastModified system property for the MANIFEST.MF file, adjusted by the JVM timezone offset. However, tinybundles sets this lastModified date to 0 (or rather doesn't pass it as a parameter), leading to this date potentially being negative when used in timezones with a positive offset from UTC, because the ZipUtil class bndlib uses to set this LastModified property subtracts the offset from the modified time, leading to a situation where the Lastmodified time for the MANIFEST.MF entry may be negative, or rather, since it's an unsigned integer, underflowing.

@oliverlietz oliverlietz self-assigned this Aug 21, 2023
@oliverlietz oliverlietz added the status: cannot reproduce Cannot Reproduce label Aug 24, 2023
@oliverlietz
Copy link
Member

Cannot reproduce. The timestamp (Bnd-LastModified: 1641909652026) in the attached JAR converts to date Tue Jan 11 2022 14:00:52 GMT+0000.

@ikannak
Copy link
Author

ikannak commented Aug 25, 2023

Cannot reproduce. The timestamp (Bnd-LastModified: 1641909652026) in the attached JAR converts to date Tue Jan 11 2022 14:00:52 GMT+0000.

It's not the Bnd-LastModified: timestamp that I'm talking about. it's the actual Windows "Last Modified" date system property that is the issue:

C:\Users\nak\Downloads\META-INF>dir /T:W MANIFEST.MF
 Volume in drive C has no label.
 Volume Serial Number is A889-EBFA

 Directory of C:\Users\nak\Downloads\META-INF

07/02/2106  07:28               427 MANIFEST.MF
               1 File(s)            427 bytes
               0 Dir(s)  69.311.770.624 bytes free

If you inspect the jar using 7Zip, you'll see that the MANIFEST.MF file has the Last Modified Date set to sometime in 2106.

@oliverlietz
Copy link
Member

@ikannak Can you please create a test case against latest TinyBundles snapshot? I don't have a local Windows for testing, but CI is now running on Windows also.

@oliverlietz oliverlietz reopened this Aug 25, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: In Progress
Development

No branches or pull requests

2 participants