Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 101 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,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.
*
* <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 +89,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 +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.
*
* <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 +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);
}

/**
Expand All @@ -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.
*
Expand Down Expand Up @@ -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);

}
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