diff --git a/build.gradle b/build.gradle
index 4579fca..32f2445 100644
--- a/build.gradle
+++ b/build.gradle
@@ -117,11 +117,11 @@ group = 'cpw.mods'
version = gradleutils.version
logger.lifecycle('Version: ' + version)
-ext.asmVersion = 9.3
dependencies {
- api("org.ow2.asm:asm:${asmVersion}")
- api("org.ow2.asm:asm-tree:${asmVersion}")
- api("org.ow2.asm:asm-commons:${asmVersion}")
+ api("org.ow2.asm:asm:${project.asm_version}")
+ api("org.ow2.asm:asm-tree:${project.asm_version}")
+ api("org.ow2.asm:asm-commons:${project.asm_version}")
+ implementation("org.jetbrains:annotations:${project.jb_annotations_version}")
testImplementation('org.junit.jupiter:junit-jupiter-api:5.8.+')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.8.+')
}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..8d73330
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,2 @@
+asm_version=9.3
+jb_annotations_version=22.0.0
diff --git a/sjh-jmh/build.gradle b/sjh-jmh/build.gradle
index 03d7ed3..28a6b83 100644
--- a/sjh-jmh/build.gradle
+++ b/sjh-jmh/build.gradle
@@ -7,9 +7,9 @@ dependencies {
implementation('org.junit.jupiter:junit-jupiter-engine:5.8.+')
implementation('org.apache.logging.log4j:log4j-core:2.17.1')
implementation('org.apache.logging.log4j:log4j-api:2.17.1')
- implementation('org.ow2.asm:asm:9.3')
- implementation('org.ow2.asm:asm-tree:9.3')
- implementation('org.ow2.asm:asm-commons:9.3')
+ implementation("org.ow2.asm:asm:${project.asm_version}")
+ implementation("org.ow2.asm:asm-tree:${project.asm_version}")
+ implementation("org.ow2.asm:asm-commons:${project.asm_version}")
implementation('org.openjdk.jmh:jmh-core:1.35')
jmhOnly('org.openjdk.jmh:jmh-core:1.35')
jmhOnly('org.openjdk.jmh:jmh-generator-annprocess:1.35')
@@ -42,4 +42,4 @@ task jmh(type: JavaExec, dependsOn: sourceSets.main.output) {
args '-f', '1' // forks
args '-rff', project.file("${rootProject.buildDir}/jmh_results.txt") // results file
args 'cpw.mods.niofs.union.benchmarks.UnionFileSystemBenchmark'
-}
\ No newline at end of file
+}
diff --git a/src/main/java/cpw/mods/jarhandling/JarContents.java b/src/main/java/cpw/mods/jarhandling/JarContents.java
new file mode 100644
index 0000000..0a0b53b
--- /dev/null
+++ b/src/main/java/cpw/mods/jarhandling/JarContents.java
@@ -0,0 +1,55 @@
+package cpw.mods.jarhandling;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import java.net.URI;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.jar.Manifest;
+
+/**
+ * Access to the contents of a list of {@link Path}s, interpreted as a jar file.
+ * Typically used to build the {@linkplain JarMetadata metadata} for a {@link SecureJar}.
+ *
+ *
Create with {@link JarContentsBuilder}.
+ * Convert to a full jar with {@link SecureJar#from(JarContents)}.
+ */
+@ApiStatus.NonExtendable
+public interface JarContents {
+ /**
+ * @see SecureJar#getPrimaryPath()
+ */
+ Path getPrimaryPath();
+
+ /**
+ * Looks for a file in the jar.
+ */
+ Optional findFile(String name);
+
+ /**
+ * {@return the manifest of the jar}
+ * Empty if no manifest is present in the jar.
+ */
+ Manifest getManifest();
+
+ /**
+ * {@return all the packages in the jar}
+ * (Every folder containing a {@code .class} file is considered a package.)
+ */
+ Set getPackages();
+
+ /**
+ * {@return all the packages in the jar, with some root packages excluded}
+ *
+ * This can be used to skip scanning of folders that are known to not contain code,
+ * but would be expensive to go through.
+ */
+ Set getPackagesExcluding(String... excludedRootPackages);
+
+ /**
+ * Parses the {@code META-INF/services} files in the jar, and returns the list of service providers.
+ */
+ List getMetaInfServices();
+}
diff --git a/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java b/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java
new file mode 100644
index 0000000..3090066
--- /dev/null
+++ b/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java
@@ -0,0 +1,58 @@
+package cpw.mods.jarhandling;
+
+import cpw.mods.jarhandling.impl.JarContentsImpl;
+import cpw.mods.niofs.union.UnionPathFilter;
+import org.jetbrains.annotations.Nullable;
+
+import java.nio.file.Path;
+import java.util.Objects;
+import java.util.function.Supplier;
+import java.util.jar.Manifest;
+
+/**
+ * Builder for {@link JarContents}.
+ */
+public final class JarContentsBuilder {
+ private Path[] paths = new Path[0];
+ private Supplier defaultManifest = Manifest::new;
+ @Nullable
+ private UnionPathFilter pathFilter = null;
+
+ public JarContentsBuilder() {}
+
+ /**
+ * Sets the root paths for the files of this jar.
+ */
+ public JarContentsBuilder paths(Path... paths) {
+ this.paths = paths;
+ return this;
+ }
+
+ /**
+ * Overrides the default manifest for this jar.
+ * The default manifest is only used when the jar does not provide a manifest already.
+ */
+ public JarContentsBuilder defaultManifest(Supplier manifest) {
+ Objects.requireNonNull(manifest);
+
+ this.defaultManifest = manifest;
+ return this;
+ }
+
+ /**
+ * Overrides the path filter for this jar, to exclude some entries from the underlying file system.
+ *
+ * @see UnionPathFilter
+ */
+ public JarContentsBuilder pathFilter(@Nullable UnionPathFilter pathFilter) {
+ this.pathFilter = pathFilter;
+ return this;
+ }
+
+ /**
+ * Builds the jar.
+ */
+ public JarContents build() {
+ return new JarContentsImpl(paths, defaultManifest, pathFilter == null ? null : pathFilter::test);
+ }
+}
diff --git a/src/main/java/cpw/mods/jarhandling/JarMetadata.java b/src/main/java/cpw/mods/jarhandling/JarMetadata.java
index f46f243..7a8eda5 100644
--- a/src/main/java/cpw/mods/jarhandling/JarMetadata.java
+++ b/src/main/java/cpw/mods/jarhandling/JarMetadata.java
@@ -33,16 +33,22 @@ public interface JarMetadata {
"volatile","const","float","native","super","while");
Pattern KEYWORD_PARTS = Pattern.compile("(?<=^|\\.)(" + String.join("|", ILLEGAL_KEYWORDS) + ")(?=\\.|$)");
- static JarMetadata from(final SecureJar jar, final Path... path) {
- if (path.length==0) throw new IllegalArgumentException("Need at least one path");
+ /**
+ * Builds the jar metadata for a jar following the normal rules for Java jars.
+ *
+ * If the jar has a {@code module-info.class} file, the module info is read from there.
+ * Otherwise, the jar is an automatic module, whose name is optionally derived
+ * from {@code Automatic-Module-Name} in the manifest.
+ */
+ static JarMetadata from(JarContents jar) {
final var pkgs = jar.getPackages();
- var mi = jar.moduleDataProvider().findFile("module-info.class");
+ var mi = jar.findFile("module-info.class");
if (mi.isPresent()) {
return new ModuleJarMetadata(mi.get(), pkgs);
} else {
- var providers = jar.getProviders();
- var fileCandidate = fromFileName(path[0], pkgs, providers);
- var aname = jar.moduleDataProvider().getManifest().getMainAttributes().getValue("Automatic-Module-Name");
+ var providers = jar.getMetaInfServices();
+ var fileCandidate = fromFileName(jar.getPrimaryPath(), pkgs, providers);
+ var aname = jar.getManifest().getMainAttributes().getValue("Automatic-Module-Name");
if (aname != null) {
return new SimpleJarMetadata(aname, fileCandidate.version(), pkgs, providers);
} else {
@@ -50,8 +56,8 @@ static JarMetadata from(final SecureJar jar, final Path... path) {
}
}
}
- static SimpleJarMetadata fromFileName(final Path path, final Set pkgs, final List providers) {
+ static SimpleJarMetadata fromFileName(final Path path, final Set pkgs, final List providers) {
// detect Maven-like paths
Path versionMaybe = path.getParent();
if (versionMaybe != null)
@@ -137,4 +143,26 @@ private static String cleanModuleName(String mn) {
return mn;
}
+
+ /**
+ * @deprecated Use {@link #from(JarContents)} instead.
+ */
+ @Deprecated(forRemoval = true, since = "2.1.16")
+ static JarMetadata from(final SecureJar jar, final Path... path) {
+ if (path.length==0) throw new IllegalArgumentException("Need at least one path");
+ final var pkgs = jar.getPackages();
+ var mi = jar.moduleDataProvider().findFile("module-info.class");
+ if (mi.isPresent()) {
+ return new ModuleJarMetadata(mi.get(), pkgs);
+ } else {
+ var providers = jar.getProviders();
+ var fileCandidate = fromFileName(path[0], pkgs, providers);
+ var aname = jar.moduleDataProvider().getManifest().getMainAttributes().getValue("Automatic-Module-Name");
+ if (aname != null) {
+ return new SimpleJarMetadata(aname, fileCandidate.version(), pkgs, providers);
+ } else {
+ return fileCandidate;
+ }
+ }
+ }
}
diff --git a/src/main/java/cpw/mods/jarhandling/SecureJar.java b/src/main/java/cpw/mods/jarhandling/SecureJar.java
index 9af0197..3b3b3f5 100644
--- a/src/main/java/cpw/mods/jarhandling/SecureJar.java
+++ b/src/main/java/cpw/mods/jarhandling/SecureJar.java
@@ -1,6 +1,8 @@
package cpw.mods.jarhandling;
import cpw.mods.jarhandling.impl.Jar;
+import cpw.mods.jarhandling.impl.JarContentsImpl;
+import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
@@ -19,74 +21,94 @@
import java.util.jar.Attributes;
import java.util.jar.Manifest;
+/**
+ * A secure jar is the full definition for a module,
+ * including all its paths and code signing metadata.
+ */
public interface SecureJar {
- interface ModuleDataProvider {
- String name();
- ModuleDescriptor descriptor();
- URI uri();
- Optional findFile(String name);
- Optional open(final String name);
+ /**
+ * Creates a jar from a list of paths.
+ * See {@link JarContentsBuilder} for more configuration options.
+ */
+ static SecureJar from(final Path... paths) {
+ return from(new JarContentsBuilder().paths(paths).build());
+ }
- Manifest getManifest();
+ /**
+ * Creates a jar from its contents, with default metadata.
+ */
+ static SecureJar from(JarContents contents) {
+ return from(contents, JarMetadata.from(contents));
+ }
- CodeSigner[] verifyAndGetSigners(String cname, byte[] bytes);
+ /**
+ * Creates a jar from its contents and metadata.
+ */
+ static SecureJar from(JarContents contents, JarMetadata metadata) {
+ return new Jar((JarContentsImpl) contents, metadata);
}
ModuleDataProvider moduleDataProvider();
+ /**
+ * A {@link SecureJar} can be built from multiple paths, either to directories or to {@code .jar} archives.
+ * This function returns the first of these paths, either to a directory or to an archive file.
+ *
+ * This is generally used for reporting purposes,
+ * for example to obtain a human-readable single location for this jar.
+ */
Path getPrimaryPath();
+ /**
+ * {@return the signers of the manifest, or {@code null} if the manifest is not signed}
+ */
+ @Nullable
CodeSigner[] getManifestSigners();
Status verifyPath(Path path);
Status getFileStatus(String name);
+ @Nullable
Attributes getTrustedManifestEntries(String name);
boolean hasSecurityData();
- static SecureJar from(final Path... paths) {
- return from(jar -> JarMetadata.from(jar, paths), paths);
- }
+ String name();
- static SecureJar from(BiPredicate filter, final Path... paths) {
- return from(jar->JarMetadata.from(jar, paths), filter, paths);
- }
+ Path getPath(String first, String... rest);
- static SecureJar from(Function metadataSupplier, final Path... paths) {
- return from(Manifest::new, metadataSupplier, paths);
- }
+ /**
+ * {@return the root path in the jar's own filesystem}
+ */
+ Path getRootPath();
- static SecureJar from(Function metadataSupplier, BiPredicate filter, final Path... paths) {
- return from(Manifest::new, metadataSupplier, filter, paths);
- }
+ interface ModuleDataProvider {
+ String name();
+ ModuleDescriptor descriptor();
+ URI uri();
+ Optional findFile(String name);
+ Optional open(final String name);
- static SecureJar from(Supplier defaultManifest, Function metadataSupplier, final Path... paths) {
- return from(defaultManifest, metadataSupplier, null, paths);
- }
+ Manifest getManifest();
- static SecureJar from(Supplier defaultManifest, Function metadataSupplier, BiPredicate filter, final Path... paths) {
- return new Jar(defaultManifest, metadataSupplier, filter, paths);
+ CodeSigner[] verifyAndGetSigners(String cname, byte[] bytes);
}
- Set getPackages();
-
- List getProviders();
-
- String name();
-
- Path getPath(String first, String... rest);
-
- Path getRootPath();
-
+ /**
+ * Same as {@link ModuleDescriptor.Provides}, but with an exposed constructor.
+ * Use only if the {@link #fromPath} method is useful to you.
+ */
record Provider(String serviceName, List providers) {
+ /**
+ * Helper method to parse service provider implementations from a {@link Path}.
+ */
public static Provider fromPath(final Path path, final BiPredicate pkgFilter) {
final var sname = path.getFileName().toString();
try {
var entries = Files.readAllLines(path).stream()
.map(String::trim)
- .filter(l->l.length() > 0 && !l.startsWith("#"))
+ .filter(l->l.length() > 0 && !l.startsWith("#")) // We support comments :)
.filter(p-> pkgFilter == null || pkgFilter.test(p.replace('.','/'), ""))
.toList();
return new Provider(sname, entries);
@@ -99,4 +121,58 @@ public static Provider fromPath(final Path path, final BiPredicate getPackages() {
+ return moduleDataProvider().descriptor().packages();
+ }
+
+ /**
+ * @deprecated Obtain via the {@link ModuleDescriptor} of the jar if you really need this.
+ */
+ @Deprecated(forRemoval = true, since = "2.1.16")
+ List getProviders();
+
+ /**
+ * @deprecated Use {@link JarContentsBuilder} and {@link #from(JarContents)} instead.
+ */
+ @Deprecated(forRemoval = true, since = "2.1.16")
+ static SecureJar from(BiPredicate filter, final Path... paths) {
+ return from(jar->JarMetadata.from(jar, paths), filter, paths);
+ }
+
+ /**
+ * @deprecated Use {@link JarContentsBuilder} and {@link #from(JarContents)} instead.
+ */
+ @Deprecated(forRemoval = true, since = "2.1.16")
+ static SecureJar from(Function metadataSupplier, final Path... paths) {
+ return from(Manifest::new, metadataSupplier, paths);
+ }
+
+ /**
+ * @deprecated Use {@link JarContentsBuilder} and {@link #from(JarContents)} instead.
+ */
+ @Deprecated(forRemoval = true, since = "2.1.16")
+ static SecureJar from(Function metadataSupplier, BiPredicate filter, final Path... paths) {
+ return from(Manifest::new, metadataSupplier, filter, paths);
+ }
+
+ /**
+ * @deprecated Use {@link JarContentsBuilder} and {@link #from(JarContents)} instead.
+ */
+ @Deprecated(forRemoval = true, since = "2.1.16")
+ static SecureJar from(Supplier defaultManifest, Function metadataSupplier, final Path... paths) {
+ return from(defaultManifest, metadataSupplier, null, paths);
+ }
+
+ /**
+ * @deprecated Use {@link JarContentsBuilder} and {@link #from(JarContents)} instead.
+ */
+ @Deprecated(forRemoval = true, since = "2.1.16")
+ static SecureJar from(Supplier defaultManifest, Function metadataSupplier, BiPredicate filter, final Path... paths) {
+ return new Jar(defaultManifest, metadataSupplier, filter, paths);
+ }
}
diff --git a/src/main/java/cpw/mods/jarhandling/impl/Jar.java b/src/main/java/cpw/mods/jarhandling/impl/Jar.java
index 520b637..ee9c3e2 100644
--- a/src/main/java/cpw/mods/jarhandling/impl/Jar.java
+++ b/src/main/java/cpw/mods/jarhandling/impl/Jar.java
@@ -3,45 +3,53 @@
import cpw.mods.jarhandling.JarMetadata;
import cpw.mods.jarhandling.SecureJar;
import cpw.mods.niofs.union.UnionFileSystem;
-import cpw.mods.niofs.union.UnionFileSystemProvider;
import cpw.mods.util.LambdaExceptionUtils;
+import org.jetbrains.annotations.Nullable;
-import java.io.IOException;
import java.io.InputStream;
-import java.io.UncheckedIOException;
import java.lang.module.ModuleDescriptor;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.nio.file.spi.FileSystemProvider;
import java.security.CodeSigner;
import java.util.*;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.jar.Attributes;
-import java.util.jar.JarFile;
-import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
-import static java.util.stream.Collectors.*;
-
public class Jar implements SecureJar {
- private static final CodeSigner[] EMPTY_CODESIGNERS = new CodeSigner[0];
- private static final UnionFileSystemProvider UFSP = (UnionFileSystemProvider) FileSystemProvider.installedProviders().stream().filter(fsp->fsp.getScheme().equals("union")).findFirst().orElseThrow(()->new IllegalStateException("Couldn't find UnionFileSystemProvider"));
+ private final JarContentsImpl contents;
private final Manifest manifest;
- private final Hashtable pendingSigners = new Hashtable<>();
- private final Hashtable verifiedSigners = new Hashtable<>();
- private final ManifestVerifier verifier = new ManifestVerifier();
- private final Map statusData = new HashMap<>();
- private final JarMetadata metadata;
+ private final JarSigningData signingData;
private final UnionFileSystem filesystem;
- private final boolean isMultiRelease;
- private final Map nameOverrides;
+
private final JarModuleDataProvider moduleDataProvider;
- private Set packages;
- private List providers;
+
+ private final JarMetadata metadata;
+
+ @Deprecated(forRemoval = true, since = "2.1.16")
+ public Jar(final Supplier defaultManifest, final Function metadataFunction, final BiPredicate pathfilter, final Path... paths) {
+ this.contents = new JarContentsImpl(paths, defaultManifest, pathfilter);
+ this.manifest = contents.getManifest();
+ this.signingData = contents.signingData;
+ this.filesystem = contents.filesystem;
+
+ this.moduleDataProvider = new JarModuleDataProvider(this);
+ this.metadata = metadataFunction.apply(this);
+ }
+
+ public Jar(JarContentsImpl contents, JarMetadata metadata) {
+ this.contents = contents;
+ this.manifest = contents.getManifest();
+ this.signingData = contents.signingData;
+ this.filesystem = contents.filesystem;
+
+ this.moduleDataProvider = new JarModuleDataProvider(this);
+ this.metadata = metadata;
+ }
public URI getURI() {
return this.filesystem.getRootDirectories().iterator().next().toUri();
@@ -62,148 +70,36 @@ public Path getPrimaryPath() {
}
public Optional findFile(final String name) {
- var rel = filesystem.getPath(name);
- if (this.nameOverrides.containsKey(rel)) {
- rel = this.filesystem.getPath("META-INF", "versions", this.nameOverrides.get(rel).toString()).resolve(rel);
- }
- return Optional.of(this.filesystem.getRoot().resolve(rel)).filter(Files::exists).map(Path::toUri);
- }
-
- private record StatusData(String name, Status status, CodeSigner[] signers) {
- static void add(final String name, final Status status, final CodeSigner[] signers, Jar jar) {
- jar.statusData.put(name, new StatusData(name, status, signers));
- }
- }
-
- @SuppressWarnings("unchecked")
- public Jar(final Supplier defaultManifest, final Function metadataFunction, final BiPredicate pathfilter, final Path... paths) {
- var validPaths = Arrays.stream(paths).filter(Files::exists).toArray(Path[]::new);
- if (validPaths.length == 0)
- throw new UncheckedIOException(new IOException("Invalid paths argument, contained no existing paths: " + Arrays.toString(paths)));
- this.moduleDataProvider = new JarModuleDataProvider(this);
- this.filesystem = UFSP.newFileSystem(pathfilter, validPaths);
- try {
- Manifest mantmp = null;
- for (int x = validPaths.length - 1; x >= 0; x--) { // Walk backwards because this is what cpw wanted?
- var path = validPaths[x];
- if (Files.isDirectory(path)) {
- var manfile = path.resolve(JarFile.MANIFEST_NAME);
- if (Files.exists(manfile)) {
- try (var is = Files.newInputStream(manfile)) {
- mantmp = new Manifest(is);
- break;
- }
- }
- } else {
- try (var jis = new JarInputStream(Files.newInputStream(path))) {
- var jv = SecureJarVerifier.getJarVerifier(jis);
- if (jv != null) {
- while (SecureJarVerifier.isParsingMeta(jv)) {
- if (jis.getNextJarEntry() == null) break;
- }
-
- if (SecureJarVerifier.hasSignatures(jv)) {
- pendingSigners.putAll(SecureJarVerifier.getPendingSigners(jv));
- var manifestSigners = SecureJarVerifier.getVerifiedSigners(jv).get(JarFile.MANIFEST_NAME);
- if (manifestSigners != null) verifiedSigners.put(JarFile.MANIFEST_NAME, manifestSigners);
- StatusData.add(JarFile.MANIFEST_NAME, Status.VERIFIED, verifiedSigners.get(JarFile.MANIFEST_NAME), this);
- }
- }
-
- if (jis.getManifest() != null) {
- mantmp = new Manifest(jis.getManifest());
- break;
- }
- }
- }
- }
- this.manifest = mantmp == null ? defaultManifest.get() : mantmp;
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- this.isMultiRelease = Boolean.parseBoolean(getManifest().getMainAttributes().getValue("Multi-Release"));
- if (this.isMultiRelease) {
- var vers = filesystem.getRoot().resolve("META-INF/versions");
- try (var walk = Files.walk(vers)){
- var allnames = walk.filter(p1 ->!p1.isAbsolute())
- .filter(path1 -> !Files.isDirectory(path1))
- .map(p1 -> p1.subpath(2, p1.getNameCount()))
- .collect(groupingBy(p->p.subpath(1, p.getNameCount()),
- mapping(p->Integer.parseInt(p.getName(0).toString()), toUnmodifiableList())));
- this.nameOverrides = allnames.entrySet().stream()
- .map(e->Map.entry(e.getKey(), e.getValue().stream().reduce(Integer::max).orElse(8)))
- .filter(e-> e.getValue() < Runtime.version().feature())
- .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
- } catch (IOException ioe) {
- throw new UncheckedIOException(ioe);
- }
- } else {
- this.nameOverrides = Map.of();
- }
- this.metadata = metadataFunction.apply(this);
- }
-
- public Manifest getManifest() {
- return manifest;
+ return contents.findFile(name);
}
@Override
+ @Nullable
public CodeSigner[] getManifestSigners() {
- return getData(JarFile.MANIFEST_NAME).map(r->r.signers).orElse(null);
- }
-
- public synchronized CodeSigner[] verifyAndGetSigners(final String name, final byte[] bytes) {
- if (!hasSecurityData()) return null;
- if (statusData.containsKey(name)) return statusData.get(name).signers;
-
- var signers = verifier.verify(this.manifest, pendingSigners, verifiedSigners, name, bytes);
- if (signers == null) {
- StatusData.add(name, Status.INVALID, null, this);
- return null;
- } else {
- var ret = signers.orElse(null);
- StatusData.add(name, Status.VERIFIED, ret, this);
- return ret;
- }
+ return signingData.getManifestSigners();
}
@Override
public Status verifyPath(final Path path) {
if (path.getFileSystem() != filesystem) throw new IllegalArgumentException("Wrong filesystem");
final var pathname = path.toString();
- if (statusData.containsKey(pathname)) return getFileStatus(pathname);
- try {
- var bytes = Files.readAllBytes(path);
- verifyAndGetSigners(pathname, bytes);
- return getFileStatus(pathname);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- private Optional getData(final String name) {
- return Optional.ofNullable(statusData.get(name));
+ return signingData.verifyPath(manifest, path, pathname);
}
@Override
public Status getFileStatus(final String name) {
- return hasSecurityData() ? getData(name).map(r->r.status).orElse(Status.NONE) : Status.UNVERIFIED;
+ return signingData.getFileStatus(name);
}
@Override
+ @Nullable
public Attributes getTrustedManifestEntries(final String name) {
- var manattrs = manifest.getAttributes(name);
- var mansigners = getManifestSigners();
- var objsigners = getData(name).map(sd->sd.signers).orElse(EMPTY_CODESIGNERS);
- if (mansigners == null || (mansigners.length == objsigners.length)) {
- return manattrs;
- } else {
- return null;
- }
+ return signingData.getTrustedManifestEntries(manifest, name);
}
+
@Override
public boolean hasSecurityData() {
- return !pendingSigners.isEmpty() || !this.verifiedSigners.isEmpty();
+ return signingData.hasSecurityData();
}
@Override
@@ -213,41 +109,12 @@ public String name() {
@Override
public Set getPackages() {
- if (this.packages == null) {
- try (var walk = Files.walk(this.filesystem.getRoot())) {
- this.packages = walk
- .filter(path->path.getNameCount()>0)
- .filter(path->!path.getName(0).toString().equals("META-INF"))
- .filter(path->path.getFileName().toString().endsWith(".class"))
- .filter(Files::isRegularFile)
- .map(Path::getParent)
- .map(path->path.toString().replace('/','.'))
- .filter(pkg->pkg.length()!=0)
- .collect(toSet());
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
- return this.packages;
+ return contents.getPackages();
}
@Override
public List getProviders() {
- if (this.providers == null) {
- final var services = this.filesystem.getRoot().resolve("META-INF/services/");
- if (Files.exists(services)) {
- try (var walk = Files.walk(services)) {
- this.providers = walk.filter(path->!Files.isDirectory(path))
- .map((Path path1) -> Provider.fromPath(path1, filesystem.getFilesystemFilter()))
- .toList();
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- } else {
- this.providers = List.of();
- }
- }
- return this.providers;
+ return contents.getMetaInfServices();
}
@Override
@@ -293,12 +160,12 @@ public Optional open(final String name) {
@Override
public Manifest getManifest() {
- return jar.getManifest();
+ return jar.manifest;
}
@Override
public CodeSigner[] verifyAndGetSigners(final String cname, final byte[] bytes) {
- return jar.verifyAndGetSigners(cname, bytes);
+ return jar.signingData.verifyAndGetSigners(jar.manifest, cname, bytes);
}
}
}
diff --git a/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java b/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java
new file mode 100644
index 0000000..a39807b
--- /dev/null
+++ b/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java
@@ -0,0 +1,216 @@
+package cpw.mods.jarhandling.impl;
+
+import cpw.mods.jarhandling.JarContents;
+import cpw.mods.jarhandling.SecureJar;
+import cpw.mods.niofs.union.UnionFileSystem;
+import cpw.mods.niofs.union.UnionFileSystemProvider;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.BiPredicate;
+import java.util.function.Supplier;
+import java.util.jar.JarFile;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+
+public class JarContentsImpl implements JarContents {
+ private static final UnionFileSystemProvider UFSP = (UnionFileSystemProvider) FileSystemProvider.installedProviders()
+ .stream()
+ .filter(fsp->fsp.getScheme().equals("union"))
+ .findFirst()
+ .orElseThrow(()->new IllegalStateException("Couldn't find UnionFileSystemProvider"));
+
+ final UnionFileSystem filesystem;
+ // Code signing data
+ final JarSigningData signingData = new JarSigningData();
+ // Manifest of the jar
+ private final Manifest manifest;
+ // Name overrides, if the jar is a multi-release jar
+ private final Map nameOverrides;
+
+ // Cache for repeated getPackages calls
+ private Set packages;
+ // Cache for repeated getMetaInfServices calls
+ private List providers;
+
+ public JarContentsImpl(Path[] paths, Supplier defaultManifest, @Nullable BiPredicate pathFilter) {
+ var validPaths = Arrays.stream(paths).filter(Files::exists).toArray(Path[]::new);
+ if (validPaths.length == 0)
+ throw new UncheckedIOException(new IOException("Invalid paths argument, contained no existing paths: " + Arrays.toString(paths)));
+ this.filesystem = UFSP.newFileSystem(pathFilter, validPaths);
+ // Find the manifest, and read its signing data
+ this.manifest = readManifestAndSigningData(defaultManifest, validPaths);
+ // Read multi-release jar information
+ this.nameOverrides = readMultiReleaseInfo();
+ }
+
+ private Manifest readManifestAndSigningData(Supplier defaultManifest, Path[] validPaths) {
+ try {
+ for (int x = validPaths.length - 1; x >= 0; x--) { // Walk backwards because this is what cpw wanted?
+ var path = validPaths[x];
+ if (Files.isDirectory(path)) {
+ // Just a directory: read the manifest file, but don't do any signature verification
+ var manfile = path.resolve(JarFile.MANIFEST_NAME);
+ if (Files.exists(manfile)) {
+ try (var is = Files.newInputStream(manfile)) {
+ return new Manifest(is);
+ }
+ }
+ } else {
+ try (var jis = new JarInputStream(Files.newInputStream(path))) {
+ // Jar file: use the signature verification code
+ signingData.readJarSigningData(jis);
+
+ if (jis.getManifest() != null) {
+ return new Manifest(jis.getManifest());
+ }
+ }
+ }
+ }
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ return defaultManifest.get();
+ }
+
+ /**
+ * Read multi-release information from the jar.
+ * Example of a multi-release jar layout:
+ *
+ *
+ * jar root
+ * - A.class
+ * - B.class
+ * - C.class
+ * - D.class
+ * - META-INF
+ * - versions
+ * - 9
+ * - A.class
+ * - B.class
+ * - 10
+ * - A.class
+ *
+ */
+ private Map readMultiReleaseInfo() {
+ // Must have the manifest entry
+ boolean isMultiRelease = Boolean.parseBoolean(getManifest().getMainAttributes().getValue("Multi-Release"));
+ if (!isMultiRelease) {
+ return Map.of();
+ }
+
+ var vers = filesystem.getRoot().resolve("META-INF/versions");
+ try (var walk = Files.walk(vers)) {
+ Map pathToJavaVersion = new HashMap<>();
+ walk
+ // Look for files, not directories
+ .filter(p -> !Files.isDirectory(p))
+ .forEach(p -> {
+ int javaVersion = Integer.parseInt(p.getName(2).toString());
+ Path remainder = p.subpath(3, p.getNameCount());
+ if (javaVersion <= Runtime.version().feature()) {
+ // Associate path with the highest supported java version
+ pathToJavaVersion.merge(remainder, javaVersion, Integer::max);
+ }
+ });
+ return pathToJavaVersion;
+ } catch (IOException ioe) {
+ throw new UncheckedIOException(ioe);
+ }
+ }
+
+ @Override
+ public Path getPrimaryPath() {
+ return filesystem.getPrimaryPath();
+ }
+
+ @Override
+ public Optional findFile(String name) {
+ var rel = filesystem.getPath(name);
+ if (this.nameOverrides.containsKey(rel)) {
+ rel = this.filesystem.getPath("META-INF", "versions", this.nameOverrides.get(rel).toString()).resolve(rel);
+ }
+ return Optional.of(this.filesystem.getRoot().resolve(rel)).filter(Files::exists).map(Path::toUri);
+ }
+
+ @Override
+ public Manifest getManifest() {
+ return manifest;
+ }
+
+ @Override
+ public Set getPackagesExcluding(String... excludedRootPackages) {
+ Set ignoredRootPackages = new HashSet<>(excludedRootPackages.length+1);
+ ignoredRootPackages.add("META-INF"); // Always ignore META-INF
+ ignoredRootPackages.addAll(List.of(excludedRootPackages)); // And additional user-provided packages
+
+ Set packages = new HashSet<>();
+ try {
+ Files.walkFileTree(this.filesystem.getRoot(), new SimpleFileVisitor<>() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
+ if (file.getFileName().toString().endsWith(".class") && attrs.isRegularFile()) {
+ var pkg = file.getParent().toString().replace('/', '.');
+ if (!pkg.isEmpty()) {
+ packages.add(pkg);
+ }
+ }
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) {
+ if (path.getNameCount() > 0 && ignoredRootPackages.contains(path.getName(0).toString())) {
+ return FileVisitResult.SKIP_SUBTREE;
+ }
+ return FileVisitResult.CONTINUE;
+ }
+ });
+ return Set.copyOf(packages);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ @Override
+ public Set getPackages() {
+ if (this.packages == null) {
+ this.packages = getPackagesExcluding();
+ }
+ return this.packages;
+ }
+
+ @Override
+ public List getMetaInfServices() {
+ if (this.providers == null) {
+ final var services = this.filesystem.getRoot().resolve("META-INF/services/");
+ if (Files.exists(services)) {
+ try (var walk = Files.walk(services)) {
+ this.providers = walk.filter(path->!Files.isDirectory(path))
+ .map((Path path1) -> SecureJar.Provider.fromPath(path1, filesystem.getFilesystemFilter()))
+ .toList();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ } else {
+ this.providers = List.of();
+ }
+ }
+ return this.providers;
+ }
+}
diff --git a/src/main/java/cpw/mods/jarhandling/impl/JarSigningData.java b/src/main/java/cpw/mods/jarhandling/impl/JarSigningData.java
new file mode 100644
index 0000000..1ba3d3a
--- /dev/null
+++ b/src/main/java/cpw/mods/jarhandling/impl/JarSigningData.java
@@ -0,0 +1,111 @@
+package cpw.mods.jarhandling.impl;
+
+import cpw.mods.jarhandling.SecureJar;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.CodeSigner;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Optional;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+
+/**
+ * The signing data for a {@link Jar}.
+ */
+public class JarSigningData {
+ private static final CodeSigner[] EMPTY_CODESIGNERS = new CodeSigner[0];
+
+ private final Hashtable pendingSigners = new Hashtable<>();
+ private final Hashtable verifiedSigners = new Hashtable<>();
+ private final ManifestVerifier verifier = new ManifestVerifier();
+ private final Map statusData = new HashMap<>();
+
+ record StatusData(String name, SecureJar.Status status, CodeSigner[] signers) {
+ static void add(final String name, final SecureJar.Status status, final CodeSigner[] signers, JarSigningData data) {
+ data.statusData.put(name, new StatusData(name, status, signers));
+ }
+ }
+
+ /**
+ * Read signing data from a {@link JarInputStream}.
+ * For now this is the only way of reading signing data.
+ */
+ void readJarSigningData(JarInputStream jis) throws IOException {
+ var jv = SecureJarVerifier.getJarVerifier(jis);
+ if (jv != null) {
+ while (SecureJarVerifier.isParsingMeta(jv)) {
+ if (jis.getNextJarEntry() == null) break;
+ }
+
+ if (SecureJarVerifier.hasSignatures(jv)) {
+ pendingSigners.putAll(SecureJarVerifier.getPendingSigners(jv));
+ var manifestSigners = SecureJarVerifier.getVerifiedSigners(jv).get(JarFile.MANIFEST_NAME);
+ if (manifestSigners != null) verifiedSigners.put(JarFile.MANIFEST_NAME, manifestSigners);
+ StatusData.add(JarFile.MANIFEST_NAME, SecureJar.Status.VERIFIED, verifiedSigners.get(JarFile.MANIFEST_NAME), this);
+ }
+ }
+ }
+
+ @Nullable
+ CodeSigner[] getManifestSigners() {
+ return getData(JarFile.MANIFEST_NAME).map(r->r.signers).orElse(null);
+ }
+
+ SecureJar.Status verifyPath(Manifest manifest, Path path, String filename) {
+ if (statusData.containsKey(filename)) return getFileStatus(filename);
+ try {
+ var bytes = Files.readAllBytes(path);
+ verifyAndGetSigners(manifest, filename, bytes);
+ return getFileStatus(filename);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ SecureJar.Status getFileStatus(String name) {
+ return hasSecurityData() ? getData(name).map(r->r.status).orElse(SecureJar.Status.NONE) : SecureJar.Status.UNVERIFIED;
+ }
+
+ @Nullable
+ Attributes getTrustedManifestEntries(Manifest manifest, String name) {
+ var manattrs = manifest.getAttributes(name);
+ var mansigners = getManifestSigners();
+ var objsigners = getData(name).map(sd->sd.signers).orElse(EMPTY_CODESIGNERS);
+ if (mansigners == null || (mansigners.length == objsigners.length)) {
+ return manattrs;
+ } else {
+ return null;
+ }
+ }
+
+ boolean hasSecurityData() {
+ return !pendingSigners.isEmpty() || !this.verifiedSigners.isEmpty();
+ }
+
+ private Optional getData(final String name) {
+ return Optional.ofNullable(statusData.get(name));
+ }
+
+ synchronized CodeSigner[] verifyAndGetSigners(Manifest manifest, String name, byte[] bytes) {
+ if (!hasSecurityData()) return null;
+ if (statusData.containsKey(name)) return statusData.get(name).signers;
+
+ var signers = verifier.verify(manifest, pendingSigners, verifiedSigners, name, bytes);
+ if (signers == null) {
+ StatusData.add(name, SecureJar.Status.INVALID, null, this);
+ return null;
+ } else {
+ var ret = signers.orElse(null);
+ StatusData.add(name, SecureJar.Status.VERIFIED, ret, this);
+ return ret;
+ }
+ }
+}
diff --git a/src/main/java/cpw/mods/jarhandling/impl/ModuleJarMetadata.java b/src/main/java/cpw/mods/jarhandling/impl/ModuleJarMetadata.java
index 08a15f9..9956d13 100644
--- a/src/main/java/cpw/mods/jarhandling/impl/ModuleJarMetadata.java
+++ b/src/main/java/cpw/mods/jarhandling/impl/ModuleJarMetadata.java
@@ -16,7 +16,10 @@
import java.util.HashSet;
import java.util.Set;
-
+/**
+ * {@link JarMetadata} implementation for a modular jar.
+ * Reads the module descriptor from the jar.
+ */
public class ModuleJarMetadata implements JarMetadata {
private final ModuleDescriptor descriptor;
diff --git a/src/main/java/cpw/mods/jarhandling/impl/SecureJarVerifier.java b/src/main/java/cpw/mods/jarhandling/impl/SecureJarVerifier.java
index 42ac5bf..25e5dfa 100644
--- a/src/main/java/cpw/mods/jarhandling/impl/SecureJarVerifier.java
+++ b/src/main/java/cpw/mods/jarhandling/impl/SecureJarVerifier.java
@@ -8,6 +8,9 @@
import java.util.Map;
import java.util.jar.JarInputStream;
+/**
+ * Reflection / Unsafe wrapper class around the unexposed {@link java.util.jar.JarVerifier}.
+ */
public class SecureJarVerifier {
private static final boolean USE_UNSAAFE = Boolean.parseBoolean(System.getProperty("securejarhandler.useUnsafeAccessor", "true"));
private static IAccessor ACCESSOR = USE_UNSAAFE ? new UnsafeAccessor() : new Reflection();
diff --git a/src/main/java/cpw/mods/jarhandling/impl/SimpleJarMetadata.java b/src/main/java/cpw/mods/jarhandling/impl/SimpleJarMetadata.java
index 00e1a45..1ded493 100644
--- a/src/main/java/cpw/mods/jarhandling/impl/SimpleJarMetadata.java
+++ b/src/main/java/cpw/mods/jarhandling/impl/SimpleJarMetadata.java
@@ -7,6 +7,9 @@
import java.util.List;
import java.util.Set;
+/**
+ * {@link JarMetadata} implementation for a non-modular jar, turning it into an automatic module.
+ */
public record SimpleJarMetadata(String name, String version, Set pkgs, List providers) implements JarMetadata {
@Override
public ModuleDescriptor descriptor() {
diff --git a/src/main/java/cpw/mods/niofs/union/UnionFileSystem.java b/src/main/java/cpw/mods/niofs/union/UnionFileSystem.java
index 42f7aec..2f0ca75 100644
--- a/src/main/java/cpw/mods/niofs/union/UnionFileSystem.java
+++ b/src/main/java/cpw/mods/niofs/union/UnionFileSystem.java
@@ -1,5 +1,7 @@
package cpw.mods.niofs.union;
+import org.jetbrains.annotations.Nullable;
+
import java.io.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
@@ -101,6 +103,7 @@ public synchronized Throwable fillInStackTrace() {
private final String key;
private final List basepaths;
private final int lastElementIndex;
+ @Nullable
private final BiPredicate pathFilter;
private final Map embeddedFileSystems;
@@ -108,6 +111,7 @@ public Path getPrimaryPath() {
return basepaths.get(basepaths.size() - 1);
}
+ @Nullable
public BiPredicate getFilesystemFilter() {
return pathFilter;
}
@@ -119,7 +123,7 @@ String getKey() {
private record EmbeddedFileSystemMetadata(Path path, FileSystem fs, SeekableByteChannel fsCh) {
}
- public UnionFileSystem(final UnionFileSystemProvider provider, final BiPredicate pathFilter, final String key, final Path... basepaths) {
+ public UnionFileSystem(final UnionFileSystemProvider provider, @Nullable BiPredicate pathFilter, final String key, final Path... basepaths) {
this.pathFilter = pathFilter;
this.provider = provider;
this.key = key;
diff --git a/src/main/java/cpw/mods/niofs/union/UnionFileSystemProvider.java b/src/main/java/cpw/mods/niofs/union/UnionFileSystemProvider.java
index ea4f7df..ba0a689 100644
--- a/src/main/java/cpw/mods/niofs/union/UnionFileSystemProvider.java
+++ b/src/main/java/cpw/mods/niofs/union/UnionFileSystemProvider.java
@@ -1,5 +1,7 @@
package cpw.mods.niofs.union;
+import org.jetbrains.annotations.Nullable;
+
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
@@ -99,13 +101,13 @@ public FileSystem newFileSystem(final Path path, final Map env) throw
}
}
- public UnionFileSystem newFileSystem(final BiPredicate pathfilter, final Path... paths) {
+ public UnionFileSystem newFileSystem(@Nullable BiPredicate pathfilter, final Path... paths) {
if (paths.length == 0) throw new IllegalArgumentException("Need at least one path");
var key = makeKey(paths[0]);
return newFileSystemInternal(key, pathfilter, paths);
}
- private UnionFileSystem newFileSystemInternal(final String key, final BiPredicate pathfilter, final Path... paths) {
+ private UnionFileSystem newFileSystemInternal(final String key, @Nullable BiPredicate pathfilter, final Path... paths) {
var normpaths = Arrays.stream(paths)
.map(Path::toAbsolutePath)
.map(Path::normalize)
diff --git a/src/main/java/cpw/mods/niofs/union/UnionPathFilter.java b/src/main/java/cpw/mods/niofs/union/UnionPathFilter.java
new file mode 100644
index 0000000..0a5c1cc
--- /dev/null
+++ b/src/main/java/cpw/mods/niofs/union/UnionPathFilter.java
@@ -0,0 +1,16 @@
+package cpw.mods.niofs.union;
+
+/**
+ * Filter for paths in a {@link UnionFileSystem}.
+ */
+@FunctionalInterface
+public interface UnionPathFilter {
+ /**
+ * Test if an entry should be included in the union filesystem.
+ *
+ * @param entry the path of the entry being checked, relative to the base path
+ * @param basePath the base path, i.e. one of the root paths the filesystem is built out of
+ * @return {@code true} to include the entry, {@code} false to exclude it
+ */
+ boolean test(String entry, String basePath);
+}
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 2aac894..6ad8598 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -11,7 +11,8 @@
requires org.objectweb.asm;
requires org.objectweb.asm.tree;
requires java.base;
+ requires static org.jetbrains.annotations;
provides java.nio.file.spi.FileSystemProvider with UnionFileSystemProvider;
uses cpw.mods.cl.ModularURLHandler.IURLProvider;
provides ModularURLHandler.IURLProvider with UnionURLStreamHandler;
-}
\ No newline at end of file
+}
diff --git a/src/test/java/cpw/mods/jarhandling/impl/TestMultiRelease.java b/src/test/java/cpw/mods/jarhandling/impl/TestMultiRelease.java
new file mode 100644
index 0000000..bf5592c
--- /dev/null
+++ b/src/test/java/cpw/mods/jarhandling/impl/TestMultiRelease.java
@@ -0,0 +1,39 @@
+package cpw.mods.jarhandling.impl;
+
+import cpw.mods.jarhandling.SecureJar;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class TestMultiRelease {
+ @Test
+ public void testMultiRelease() {
+ Path rootDir = Paths.get("src", "test", "resources", "multirelease");
+ var jar = SecureJar.from(rootDir);
+
+ var aContents = readString(jar, "a.txt");
+ // Should be overridden by the Java 9 version
+ Assertions.assertEquals("new", aContents.strip());
+ // Java 1000 override should not be loaded
+ Assertions.assertNotEquals("too new", aContents.strip());
+
+ var bContents = readString(jar, "b.txt");
+ // No override
+ Assertions.assertEquals("old", bContents.strip());
+ // In particular, Java 1000 override should not be used
+ Assertions.assertNotEquals("too new", bContents.strip());
+ }
+
+ private static String readString(SecureJar jar, String file) {
+ // Note: we must read the jar through the module data provider for version-specific files to be used
+ try (var is = jar.moduleDataProvider().open(file).get()) {
+ return new String(is.readAllBytes(), StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/test/java/cpw/mods/jarhandling/impl/TestSecureJarLoading.java b/src/test/java/cpw/mods/jarhandling/impl/TestSecureJarLoading.java
index ed8596d..5df9ccd 100644
--- a/src/test/java/cpw/mods/jarhandling/impl/TestSecureJarLoading.java
+++ b/src/test/java/cpw/mods/jarhandling/impl/TestSecureJarLoading.java
@@ -31,7 +31,7 @@ void testSecureJar() throws Exception {
if (SecureJarVerifier.isSigningRelated(ze.getName())) continue;
if (ze.isDirectory()) continue;
final var zeName = ze.getName();
- var cs = ((Jar)jar).verifyAndGetSigners(ze.getName(), zis.readAllBytes());
+ var cs = jar.moduleDataProvider().verifyAndGetSigners(ze.getName(), zis.readAllBytes());
jar.getTrustedManifestEntries(zeName);
assertAll("Behaves as a properly secured JAR",
()->assertNotNull(cs, "Has code signers array"),
@@ -53,7 +53,7 @@ void testInsecureJar() throws Exception {
if (SecureJarVerifier.isSigningRelated(ze.getName())) continue;
if (ze.isDirectory()) continue;
final var zeName = ze.getName();
- var cs = ((Jar)jar).verifyAndGetSigners(ze.getName(), zis.readAllBytes());
+ var cs = jar.moduleDataProvider().verifyAndGetSigners(ze.getName(), zis.readAllBytes());
assertAll("Jar behaves correctly",
()->assertNull(cs, "No code signers")
);
@@ -83,7 +83,7 @@ void testTampered() throws Exception {
SecureJar jar = SecureJar.from(path);
ZipFile zf = new ZipFile(path.toFile());
final var entry = zf.getEntry("test/Signed.class");
- var cs = ((Jar)jar).verifyAndGetSigners(entry.getName(), zf.getInputStream(entry).readAllBytes());
+ var cs = jar.moduleDataProvider().verifyAndGetSigners(entry.getName(), zf.getInputStream(entry).readAllBytes());
assertNull(cs);
}
@@ -93,7 +93,7 @@ void testPartial() throws Exception {
SecureJar jar = SecureJar.from(path);
ZipFile zf = new ZipFile(path.toFile());
final var sentry = zf.getEntry("test/Signed.class");
- final var scs = ((Jar)jar).verifyAndGetSigners(sentry.getName(), zf.getInputStream(sentry).readAllBytes());
+ final var scs = jar.moduleDataProvider().verifyAndGetSigners(sentry.getName(), zf.getInputStream(sentry).readAllBytes());
assertAll("Behaves as a properly secured JAR",
()->assertNotNull(scs, "Has code signers array"),
()->assertTrue(scs.length>0, "With length > 0"),
@@ -101,7 +101,7 @@ void testPartial() throws Exception {
()->assertNotNull(jar.getTrustedManifestEntries(sentry.getName()), "Has trusted manifest entries")
);
final var uentry = zf.getEntry("test/UnSigned.class");
- final var ucs = ((Jar)jar).verifyAndGetSigners(uentry.getName(), zf.getInputStream(uentry).readAllBytes());
+ final var ucs = jar.moduleDataProvider().verifyAndGetSigners(uentry.getName(), zf.getInputStream(uentry).readAllBytes());
assertNull(ucs);
}
@@ -115,7 +115,7 @@ void testEmptyJar() throws Exception {
if (SecureJarVerifier.isSigningRelated(ze.getName())) continue;
if (ze.isDirectory()) continue;
final var zeName = ze.getName();
- var cs = ((Jar)jar).verifyAndGetSigners(ze.getName(), zis.readAllBytes());
+ var cs = jar.moduleDataProvider().verifyAndGetSigners(ze.getName(), zis.readAllBytes());
assertAll("Jar behaves correctly",
()->assertNull(cs, "No code signers")
);
diff --git a/src/test/resources/multirelease/META-INF/MANIFEST.MF b/src/test/resources/multirelease/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..e714012
--- /dev/null
+++ b/src/test/resources/multirelease/META-INF/MANIFEST.MF
@@ -0,0 +1 @@
+Multi-Release: true
diff --git a/src/test/resources/multirelease/META-INF/versions/1000/a.txt b/src/test/resources/multirelease/META-INF/versions/1000/a.txt
new file mode 100644
index 0000000..bac343d
--- /dev/null
+++ b/src/test/resources/multirelease/META-INF/versions/1000/a.txt
@@ -0,0 +1 @@
+too new
diff --git a/src/test/resources/multirelease/META-INF/versions/1000/b.txt b/src/test/resources/multirelease/META-INF/versions/1000/b.txt
new file mode 100644
index 0000000..bac343d
--- /dev/null
+++ b/src/test/resources/multirelease/META-INF/versions/1000/b.txt
@@ -0,0 +1 @@
+too new
diff --git a/src/test/resources/multirelease/META-INF/versions/9/a.txt b/src/test/resources/multirelease/META-INF/versions/9/a.txt
new file mode 100644
index 0000000..3e75765
--- /dev/null
+++ b/src/test/resources/multirelease/META-INF/versions/9/a.txt
@@ -0,0 +1 @@
+new
diff --git a/src/test/resources/multirelease/a.txt b/src/test/resources/multirelease/a.txt
new file mode 100644
index 0000000..3367afd
--- /dev/null
+++ b/src/test/resources/multirelease/a.txt
@@ -0,0 +1 @@
+old
diff --git a/src/test/resources/multirelease/b.txt b/src/test/resources/multirelease/b.txt
new file mode 100644
index 0000000..3367afd
--- /dev/null
+++ b/src/test/resources/multirelease/b.txt
@@ -0,0 +1 @@
+old