Skip to content

Commit

Permalink
Merge pull request GH-18 from github:zml2008/Bombe
Browse files Browse the repository at this point in the history
Basic multi-release jar handling
  • Loading branch information
jamierocks committed Aug 29, 2021
2 parents 5f6fa89 + c822ed1 commit aef4659
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 16 deletions.
109 changes: 103 additions & 6 deletions bombe-jar/src/main/java/org/cadixdev/bombe/jar/AbstractJarEntry.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

import java.io.IOException;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;

/**
Expand All @@ -42,11 +43,45 @@
*/
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.
*
* <p>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}.</p>
*
* @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;
Expand All @@ -55,6 +90,8 @@ protected AbstractJarEntry(final String name, final long time) {
/**
* Gets the fully-qualified name of the jar entry.
*
* <p>This method does not have any special handling for multi-release jars.</p>
*
* @return The name
*/
public final String getName() {
Expand All @@ -70,6 +107,49 @@ 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.
*
* <p>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.</p>
*
* <p>This will always handle multi-release paths, even when the
* {@code Multi-Release} manifest attribute is set to false.</p>
*
* @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;
}
// <version number>/<path>
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.
Expand All @@ -78,9 +158,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);
}

/**
Expand All @@ -92,12 +173,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.
*
Expand Down Expand Up @@ -132,9 +229,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);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand All @@ -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"
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions changelogs/0.5.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ Bombe 0.5.0

Bombe 0.5.0 is largely a maintenance release, cleaning up the Bombe codebase and
preparing it for newer Java modules.

## Improvements

- Handling for multi-release JAR files

0 comments on commit aef4659

Please sign in to comment.