diff --git a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/AbstractJarEntry.java b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/AbstractJarEntry.java index 2dd771f..8b6a285 100644 --- a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/AbstractJarEntry.java +++ b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/AbstractJarEntry.java @@ -32,6 +32,7 @@ import java.io.IOException; import java.util.jar.JarEntry; +import java.util.jar.JarFile; import java.util.jar.JarOutputStream; /** @@ -42,11 +43,44 @@ */ public abstract class AbstractJarEntry { + /** + * A {@link #getVersion()} value for jar entries that are at the base + * version in the jar. + */ + public static final int UNVERSIONED = -1; + + private static final String META_INF = "META-INF/"; + private static final String VERSIONS_PREFIX = META_INF + "versions/"; + protected final String name; protected final long time; + private String unversionedName; + private int version = UNVERSIONED; private String packageName; private String simpleName; + /** + * Create a new jar entry for a specific multi-release variant. + * + *

If {@code unversionedName} starts with {@code META-INF}, it will be + * treated as being in the base version no matter what value is provided for + * {@code version}, to match the behavior of the JDK's {@link JarFile}.

+ * + * @param version the Java version number to associate this entry with + * @param unversionedName the name without any versioned prefix + * @param time the time the entry was created at + */ + protected AbstractJarEntry(final int version, final String unversionedName, final long time) { + if (version == UNVERSIONED || unversionedName.startsWith(META_INF)) { + this.name = unversionedName; + } else { + this.version = version; + this.unversionedName = unversionedName; + this.name = VERSIONS_PREFIX + version + '/' + unversionedName; + } + this.time = time; + } + protected AbstractJarEntry(final String name, final long time) { this.name = name; this.time = time; @@ -55,6 +89,8 @@ protected AbstractJarEntry(final String name, final long time) { /** * Gets the fully-qualified name of the jar entry. * + *

This method does not have any special handling for multi-release jars.

+ * * @return The name */ public final String getName() { @@ -70,6 +106,48 @@ public final long getTime() { return this.time; } + /** + * Get the name of this entry, as it will be seen by a multi-release-aware + * jar handler. + * + *

When a file path is in the {@code META-INF/versions/} folder but does + * not provide a valid multi-release version, it will be treated as if it + * were an ordinary, un-versioned resource.

+ * + *

This will always handle multi-release paths, even when the + * {@code Multi-Release} manifest attribute is set to false.

+ * + * @return the full entry name, without any version prefix + */ + public final String getUnversionedName() { + if (this.unversionedName != null) return this.unversionedName; + + if (!this.name.startsWith(VERSIONS_PREFIX)) { + return this.unversionedName = this.name; + } + // / + final String trimmed = this.name.substring(VERSIONS_PREFIX.length()); + final int divider = trimmed.indexOf('/'); + if (divider == -1) { // malformed, ignore + return this.unversionedName = this.name; + } + + final String version = trimmed.substring(0, divider); + final String unversioned = trimmed.substring(divider + 1); + try { + if (!unversioned.startsWith(META_INF)) { // Files already within META-INF cannot be versioned + final int parsedVersion = Integer.parseInt(version); + if (parsedVersion >= 0) { + this.version = parsedVersion; + return this.unversionedName = unversioned; + } + } + } catch (final NumberFormatException ignored) { // invalid integer, treat as unversioned + // fall through + } + return this.unversionedName = this.name; + } + /** * Gets the package that contains the jar entry, an empty * string if in the root package. @@ -78,9 +156,10 @@ public final long getTime() { */ public final String getPackage() { if (this.packageName != null) return this.packageName; - final int index = this.name.lastIndexOf('/'); + final String name = this.getUnversionedName(); + final int index = name.lastIndexOf('/'); if (index == -1) return this.packageName = ""; - return this.packageName = this.name.substring(0, index); + return this.packageName = name.substring(0, index); } /** @@ -92,12 +171,28 @@ public final String getSimpleName() { if (this.simpleName != null) return this.simpleName; final int packageLength = this.getPackage().isEmpty() ? -1 : this.getPackage().length(); final int extensionLength = this.getExtension().isEmpty() ? -1 : this.getExtension().length(); - return this.simpleName = this.name.substring( + final String name = this.getUnversionedName(); + return this.simpleName = name.substring( packageLength + 1, - this.name.length() - (extensionLength + 1) + name.length() - (extensionLength + 1) ); } + /** + * If this is a multi-release variant of a class file in a multi-release + * jar, the version associated with this variant. + * + * @return the version, or {@link #UNVERSIONED} if this is the base version, + * or a file that would not be interpreted as a multi-release variant + * within the version folder. + * @see #getUnversionedName() for a description of the conditions on multi-release jars + */ + public int getVersion() { + if (this.unversionedName != null) return this.version; + this.getUnversionedName(); // initialize versions + return this.version; + } + /** * Gets the extension of the jar entry. * @@ -132,9 +227,9 @@ public final void write(final JarOutputStream jos) throws IOException { /** * Processes the jar entry with the given transformer. * - * @param vistor The transformer + * @param visitor The transformer * @return The jar entry */ - public abstract AbstractJarEntry accept(final JarEntryTransformer vistor); + public abstract AbstractJarEntry accept(final JarEntryTransformer visitor); } diff --git a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarClassEntry.java b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarClassEntry.java index 51fdbac..3ed0049 100644 --- a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarClassEntry.java +++ b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarClassEntry.java @@ -42,6 +42,11 @@ public class JarClassEntry extends AbstractJarEntry { private final byte[] contents; + public JarClassEntry(final int version, final String unversionedName, final long time, final byte[] contents) { + super(version, unversionedName, time); + this.contents = contents; + } + public JarClassEntry(final String name, final long time, final byte[] contents) { super(name, time); this.contents = contents; @@ -58,8 +63,8 @@ public final byte[] getContents() { } @Override - public final JarClassEntry accept(final JarEntryTransformer vistor) { - return vistor.transform(this); + public final JarClassEntry accept(final JarEntryTransformer visitor) { + return visitor.transform(this); } } diff --git a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarManifestEntry.java b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarManifestEntry.java index f55f012..4f66817 100644 --- a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarManifestEntry.java +++ b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarManifestEntry.java @@ -78,8 +78,8 @@ public final byte[] getContents() { } @Override - public JarManifestEntry accept(final JarEntryTransformer vistor) { - return vistor.transform(this); + public JarManifestEntry accept(final JarEntryTransformer visitor) { + return visitor.transform(this); } } diff --git a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarResourceEntry.java b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarResourceEntry.java index 65f94ae..490bf38 100644 --- a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarResourceEntry.java +++ b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarResourceEntry.java @@ -42,6 +42,11 @@ public class JarResourceEntry extends AbstractJarEntry { private final byte[] contents; private String extension; + public JarResourceEntry(final int version, final String unversionedName, final long time, final byte[] contents) { + super(version, unversionedName, time); + this.contents = contents; + } + public JarResourceEntry(final String name, final long time, final byte[] contents) { super(name, time); this.contents = contents; @@ -61,8 +66,8 @@ public final byte[] getContents() { } @Override - public final JarResourceEntry accept(final JarEntryTransformer vistor) { - return vistor.transform(this); + public final JarResourceEntry accept(final JarEntryTransformer visitor) { + return visitor.transform(this); } } diff --git a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarServiceProviderConfigurationEntry.java b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarServiceProviderConfigurationEntry.java index d3ebb96..51fb92c 100644 --- a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarServiceProviderConfigurationEntry.java +++ b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/JarServiceProviderConfigurationEntry.java @@ -79,8 +79,8 @@ public final byte[] getContents() { } @Override - public final JarServiceProviderConfigurationEntry accept(final JarEntryTransformer vistor) { - return vistor.transform(this); + public final JarServiceProviderConfigurationEntry accept(final JarEntryTransformer visitor) { + return visitor.transform(this); } } diff --git a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/asm/JarEntryRemappingTransformer.java b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/asm/JarEntryRemappingTransformer.java index 9b9e96a..777ea45 100644 --- a/bombe-jar/src/main/java/org/cadixdev/bombe/jar/asm/JarEntryRemappingTransformer.java +++ b/bombe-jar/src/main/java/org/cadixdev/bombe/jar/asm/JarEntryRemappingTransformer.java @@ -85,9 +85,9 @@ public JarClassEntry transform(final JarClassEntry entry) { ), 0); // Create the jar entry - final String originalName = entry.getName().substring(0, entry.getName().length() - ".class".length()); + final String originalName = entry.getUnversionedName().substring(0, entry.getUnversionedName().length() - ".class".length()); final String name = this.remapper.map(originalName) + ".class"; - return new JarClassEntry(name, entry.getTime(), writer.toByteArray()); + return new JarClassEntry(entry.getVersion(), name, entry.getTime(), writer.toByteArray()); } @Override diff --git a/bombe-jar/src/test/groovy/org/cadixdev/bombe/jar/test/JarEntrySpec.groovy b/bombe-jar/src/test/groovy/org/cadixdev/bombe/jar/test/JarEntrySpec.groovy index e682c63..e1c7904 100644 --- a/bombe-jar/src/test/groovy/org/cadixdev/bombe/jar/test/JarEntrySpec.groovy +++ b/bombe-jar/src/test/groovy/org/cadixdev/bombe/jar/test/JarEntrySpec.groovy @@ -31,6 +31,7 @@ package org.cadixdev.bombe.jar.test import org.cadixdev.bombe.jar.AbstractJarEntry +import org.cadixdev.bombe.jar.JarClassEntry import org.cadixdev.bombe.jar.JarResourceEntry import spock.lang.Specification @@ -41,6 +42,10 @@ class JarEntrySpec extends Specification { private static final AbstractJarEntry PACKAGED_ENTRY = new JarResourceEntry("pack/beep.boop", 0, null) private static final AbstractJarEntry ROOT_ENTRY = new JarResourceEntry("beep.boop", 0, null) + private static final AbstractJarEntry VERSION_BY_PATH = new JarClassEntry("META-INF/versions/9/module-info.class", 0, null) + private static final AbstractJarEntry VERSION_EXPLICIT = new JarClassEntry(11, "pack/a/b.class", 0, null) + private static final AbstractJarEntry VERSION_MALFORMED = new JarClassEntry("META-INF/versions/ab/module-info.class", 0, null) + private static final AbstractJarEntry VERSION_UNVERSIONABLE = new JarClassEntry("META-INF/versions/14/META-INF/services/a.b\$Provider", 0, null) def "reads name correctly"(final AbstractJarEntry entry, final String packageName, @@ -57,4 +62,22 @@ class JarEntrySpec extends Specification { ROOT_ENTRY | '' | 'beep' | 'boop' } + def "handles multirelease paths correctly"(final AbstractJarEntry entry, + final String fullName, + final int version, + final String name) { + expect: + entry.name == fullName + entry.version == version + entry.unversionedName == name + + where: + entry | fullName | version | name + PACKAGED_ENTRY | "pack/beep.boop" | AbstractJarEntry.UNVERSIONED | "pack/beep.boop" + VERSION_BY_PATH | "META-INF/versions/9/module-info.class" | 9 | "module-info.class" + VERSION_EXPLICIT | "META-INF/versions/11/pack/a/b.class" | 11 | "pack/a/b.class" + VERSION_MALFORMED | "META-INF/versions/ab/module-info.class" | AbstractJarEntry.UNVERSIONED | "META-INF/versions/ab/module-info.class" + VERSION_UNVERSIONABLE | "META-INF/versions/14/META-INF/services/a.b\$Provider" | AbstractJarEntry.UNVERSIONED | "META-INF/versions/14/META-INF/services/a.b\$Provider" + } + } diff --git a/bombe-jar/src/test/groovy/org/cadixdev/bombe/jar/test/asm/JarEntryRemappingTransformerSpec.groovy b/bombe-jar/src/test/groovy/org/cadixdev/bombe/jar/test/asm/JarEntryRemappingTransformerSpec.groovy index c7e4298..fa1ebdc 100644 --- a/bombe-jar/src/test/groovy/org/cadixdev/bombe/jar/test/asm/JarEntryRemappingTransformerSpec.groovy +++ b/bombe-jar/src/test/groovy/org/cadixdev/bombe/jar/test/asm/JarEntryRemappingTransformerSpec.groovy @@ -80,6 +80,28 @@ class JarEntryRemappingTransformerSpec extends Specification { node.name == 'pkg/Demo' } + def "remaps multi-release class"() { + given: + // Create a test class + def obf = new ClassWriter(0) + obf.visit(Opcodes.V9, Opcodes.ACC_PUBLIC, 'a', null, 'java/lang/Object', null) + + // Run it through the transformer + def entry = TRANSFORMER.transform(new JarClassEntry('META-INF/versions/9/a.class', 0, obf.toByteArray())) + + // Use a ClassNode for convenience + def node = new ClassNode() + def reader = new ClassReader(entry.contents) + reader.accept(node, 0) + + expect: + entry.name == 'META-INF/versions/9/pkg/Demo.class' + entry.version == 9 + entry.unversionedName == 'pkg/Demo.class' + node.name == 'pkg/Demo' + + } + def "remaps manifest"() { given: // Create a test Manifest