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