From d4829ef904a0d434b9ce74808f492998ba78bd46 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Fri, 10 Nov 2023 23:50:26 +0100 Subject: [PATCH 01/14] Add SecureJarBuilder --- build.gradle | 2 + .../java/cpw/mods/jarhandling/SecureJar.java | 86 +++++++++++-------- .../mods/jarhandling/SecureJarBuilder.java | 71 +++++++++++++++ src/main/java/module-info.java | 3 +- 4 files changed, 127 insertions(+), 35 deletions(-) create mode 100644 src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java diff --git a/build.gradle b/build.gradle index 4579fca..c511e45 100644 --- a/build.gradle +++ b/build.gradle @@ -118,10 +118,12 @@ version = gradleutils.version logger.lifecycle('Version: ' + version) ext.asmVersion = 9.3 +ext.jbAnnotationsVersion = '22.0.0' dependencies { api("org.ow2.asm:asm:${asmVersion}") api("org.ow2.asm:asm-tree:${asmVersion}") api("org.ow2.asm:asm-commons:${asmVersion}") + implementation("org.jetbrains:annotations:${jbAnnotationsVersion}") testImplementation('org.junit.jupiter:junit-jupiter-api:5.8.+') testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.8.+') } diff --git a/src/main/java/cpw/mods/jarhandling/SecureJar.java b/src/main/java/cpw/mods/jarhandling/SecureJar.java index 9af0197..2e786d1 100644 --- a/src/main/java/cpw/mods/jarhandling/SecureJar.java +++ b/src/main/java/cpw/mods/jarhandling/SecureJar.java @@ -19,17 +19,19 @@ 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. + * + *

An instance can be built with {@link SecureJarBuilder}. + */ public interface SecureJar { - interface ModuleDataProvider { - String name(); - ModuleDescriptor descriptor(); - URI uri(); - Optional findFile(String name); - Optional open(final String name); - - Manifest getManifest(); - - CodeSigner[] verifyAndGetSigners(String cname, byte[] bytes); + /** + * Creates a jar from a list of paths. + * See {@link SecureJarBuilder} for more configuration options. + */ + static SecureJar from(final Path... paths) { + return new SecureJarBuilder().paths(paths).build(); } ModuleDataProvider moduleDataProvider(); @@ -46,30 +48,6 @@ interface ModuleDataProvider { boolean hasSecurityData(); - static SecureJar from(final Path... paths) { - return from(jar -> JarMetadata.from(jar, paths), paths); - } - - static SecureJar from(BiPredicate filter, final Path... paths) { - return from(jar->JarMetadata.from(jar, paths), filter, paths); - } - - static SecureJar from(Function metadataSupplier, final Path... paths) { - return from(Manifest::new, metadataSupplier, paths); - } - - static SecureJar from(Function metadataSupplier, BiPredicate filter, final Path... paths) { - return from(Manifest::new, metadataSupplier, filter, paths); - } - - static SecureJar from(Supplier defaultManifest, Function metadataSupplier, final Path... paths) { - return from(defaultManifest, metadataSupplier, null, paths); - } - - static SecureJar from(Supplier defaultManifest, Function metadataSupplier, BiPredicate filter, final Path... paths) { - return new Jar(defaultManifest, metadataSupplier, filter, paths); - } - Set getPackages(); List getProviders(); @@ -80,6 +58,18 @@ static SecureJar from(Supplier defaultManifest, Function findFile(String name); + Optional open(final String name); + + Manifest getManifest(); + + CodeSigner[] verifyAndGetSigners(String cname, byte[] bytes); + } + record Provider(String serviceName, List providers) { public static Provider fromPath(final Path path, final BiPredicate pkgFilter) { final var sname = path.getFileName().toString(); @@ -99,4 +89,32 @@ public static Provider fromPath(final Path path, final BiPredicate filter, final Path... paths) { + return from(jar->JarMetadata.from(jar, paths), filter, paths); + } + + @Deprecated(forRemoval = true) + static SecureJar from(Function metadataSupplier, final Path... paths) { + return from(Manifest::new, metadataSupplier, paths); + } + + @Deprecated(forRemoval = true) + static SecureJar from(Function metadataSupplier, BiPredicate filter, final Path... paths) { + return from(Manifest::new, metadataSupplier, filter, paths); + } + + @Deprecated(forRemoval = true) + static SecureJar from(Supplier defaultManifest, Function metadataSupplier, final Path... paths) { + return from(defaultManifest, metadataSupplier, null, paths); + } + + @Deprecated(forRemoval = true) + 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/SecureJarBuilder.java b/src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java new file mode 100644 index 0000000..b0bd252 --- /dev/null +++ b/src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java @@ -0,0 +1,71 @@ +package cpw.mods.jarhandling; + +import cpw.mods.jarhandling.impl.Jar; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.jar.Manifest; + +public final class SecureJarBuilder { + private Supplier defaultManifest = Manifest::new; + @Nullable + private BiPredicate pathFilter = null; + private Function metadataSupplier = null; + private Path[] paths = new Path[0]; + + public SecureJarBuilder() {} + + /** + * Overrides the default manifest for this jar. + * The default manifest is only used when the jar does not provide a manifest already. + */ + public SecureJarBuilder defaultManifest(Supplier manifest) { + Objects.requireNonNull(manifest); + + this.defaultManifest = manifest; + return this; + } + + /** + * Overrides the path filter for this jar. + * TODO: wtf are the arguments passed to the path filter + */ + public SecureJarBuilder pathFilter(@Nullable BiPredicate pathFilter) { + this.pathFilter = pathFilter; + return this; + } + + /** + * Overrides the {@link JarMetadata} for this jar. + * TODO: this function is + */ + public SecureJarBuilder metadata(Function metadataSupplier) { + Objects.requireNonNull(metadataSupplier); + + this.metadataSupplier = metadataSupplier; + return this; + } + + /** + * Sets the paths for the files of this jar. + */ + public SecureJarBuilder paths(Path... paths) { + this.paths = paths; + return this; + } + + /** + * Builds the jar. + */ + public SecureJar build() { + if (metadataSupplier == null) { + metadataSupplier = jar -> JarMetadata.from(jar, paths); + } + + return new Jar(defaultManifest, metadataSupplier, pathFilter, paths); + } +} 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 +} From a40e0ab5cfd789f1a9f2d05bdbb06c9161087d9d Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Sat, 11 Nov 2023 11:28:40 +0100 Subject: [PATCH 02/14] Split SecureJar implementation across new JarContents and JarSigningData --- .../cpw/mods/jarhandling/JarContents.java | 45 ++++ .../cpw/mods/jarhandling/JarMetadata.java | 43 +++- .../java/cpw/mods/jarhandling/SecureJar.java | 25 +- .../mods/jarhandling/SecureJarBuilder.java | 14 +- .../java/cpw/mods/jarhandling/impl/Jar.java | 213 ++++-------------- .../jarhandling/impl/JarContentsImpl.java | 167 ++++++++++++++ .../mods/jarhandling/impl/JarSigningData.java | 111 +++++++++ .../jarhandling/impl/ModuleJarMetadata.java | 5 +- .../jarhandling/impl/SecureJarVerifier.java | 3 + .../jarhandling/impl/SimpleJarMetadata.java | 3 + .../cpw/mods/niofs/union/UnionFileSystem.java | 6 +- .../niofs/union/UnionFileSystemProvider.java | 6 +- .../impl/TestSecureJarLoading.java | 12 +- 13 files changed, 451 insertions(+), 202 deletions(-) create mode 100644 src/main/java/cpw/mods/jarhandling/JarContents.java create mode 100644 src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java create mode 100644 src/main/java/cpw/mods/jarhandling/impl/JarSigningData.java 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..ce2e33a --- /dev/null +++ b/src/main/java/cpw/mods/jarhandling/JarContents.java @@ -0,0 +1,45 @@ +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}. + */ +@ApiStatus.NonExtendable +public interface JarContents { + /** + * @see SecureJar#getPrimaryPath() + */ + // TODO: document what this is actually used for + Path getPrimaryPath(); + + /** + * Looks for a file in the jar. + */ + Optional findFile(String name); + + /** + * Gets the manifest of the jar. + * Empty if no manifest is present in the jar. + */ + Manifest getManifest(); + + /** + * Gets all the packages in the jar. + * (Every folder containing a {@code .class} file is considered a package.) + */ + Set getPackages(); + + /** + * 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/JarMetadata.java b/src/main/java/cpw/mods/jarhandling/JarMetadata.java index f46f243..dc02498 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,27 @@ private static String cleanModuleName(String mn) { return mn; } + + /** + * @deprecated Use {@link #from(JarContents)} instead. + * TODO: add since + */ + @Deprecated(forRemoval = true) + 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 2e786d1..095f3e5 100644 --- a/src/main/java/cpw/mods/jarhandling/SecureJar.java +++ b/src/main/java/cpw/mods/jarhandling/SecureJar.java @@ -1,6 +1,7 @@ package cpw.mods.jarhandling; import cpw.mods.jarhandling.impl.Jar; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.InputStream; @@ -36,21 +37,30 @@ static SecureJar from(final Path... paths) { 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. + */ 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(); - Set getPackages(); - - List getProviders(); + default Set getPackages() { + return moduleDataProvider().descriptor().packages(); + } String name(); @@ -90,9 +100,16 @@ enum Status { NONE, INVALID, UNVERIFIED, VERIFIED } - // The methods below are deprecated for removal - use SecureJarBuilder instead! // TODO: add since + /** + * @deprecated Obtain via the {@link ModuleDescriptor} of the jar if you really need this. + */ + @Deprecated(forRemoval = true) + List getProviders(); + + // The members below are deprecated for removal - use SecureJarBuilder instead! + @Deprecated(forRemoval = true) static SecureJar from(BiPredicate filter, final Path... paths) { return from(jar->JarMetadata.from(jar, paths), filter, paths); diff --git a/src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java b/src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java index b0bd252..348f4f3 100644 --- a/src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java +++ b/src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java @@ -1,6 +1,7 @@ package cpw.mods.jarhandling; import cpw.mods.jarhandling.impl.Jar; +import cpw.mods.jarhandling.impl.JarContentsImpl; import org.jetbrains.annotations.Nullable; import java.nio.file.Path; @@ -14,7 +15,7 @@ public final class SecureJarBuilder { private Supplier defaultManifest = Manifest::new; @Nullable private BiPredicate pathFilter = null; - private Function metadataSupplier = null; + private Function metadataSupplier = JarMetadata::from; private Path[] paths = new Path[0]; public SecureJarBuilder() {} @@ -41,9 +42,8 @@ public SecureJarBuilder pathFilter(@Nullable BiPredicate pathFil /** * Overrides the {@link JarMetadata} for this jar. - * TODO: this function is */ - public SecureJarBuilder metadata(Function metadataSupplier) { + public SecureJarBuilder metadata(Function metadataSupplier) { Objects.requireNonNull(metadataSupplier); this.metadataSupplier = metadataSupplier; @@ -62,10 +62,8 @@ public SecureJarBuilder paths(Path... paths) { * Builds the jar. */ public SecureJar build() { - if (metadataSupplier == null) { - metadataSupplier = jar -> JarMetadata.from(jar, paths); - } - - return new Jar(defaultManifest, metadataSupplier, pathFilter, paths); + JarContentsImpl contents = new JarContentsImpl(defaultManifest, pathFilter, paths); + JarMetadata metadata = metadataSupplier.apply(contents); + return new Jar(contents, metadata); } } diff --git a/src/main/java/cpw/mods/jarhandling/impl/Jar.java b/src/main/java/cpw/mods/jarhandling/impl/Jar.java index 520b637..0766475 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) + public Jar(final Supplier defaultManifest, final Function metadataFunction, final BiPredicate pathfilter, final Path... paths) { + this.contents = new JarContentsImpl(defaultManifest, pathfilter, paths); + 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..936a598 --- /dev/null +++ b/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java @@ -0,0 +1,167 @@ +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.Files; +import java.nio.file.Path; +import java.nio.file.spi.FileSystemProvider; +import java.util.Arrays; +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; + +import static java.util.stream.Collectors.*; + +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(Supplier defaultManifest, @Nullable BiPredicate pathFilter, 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.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(); + } + + private Map readMultiReleaseInfo() { + boolean isMultiRelease = Boolean.parseBoolean(getManifest().getMainAttributes().getValue("Multi-Release")); + if (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()))); + return 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 { + return Map.of(); + } + } + + @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 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; + } + + @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/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") ); From ae6275bcf2825cc050fb0e21d96021eb9e86c29f Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Sat, 11 Nov 2023 13:07:42 +0100 Subject: [PATCH 03/14] Add option to exclude some folder from the package scan --- .../java/cpw/mods/jarhandling/SecureJar.java | 15 ++++++- .../mods/jarhandling/SecureJarBuilder.java | 37 +++++++++++----- .../java/cpw/mods/jarhandling/impl/Jar.java | 2 +- .../jarhandling/impl/JarContentsImpl.java | 43 ++++++++++++++----- 4 files changed, 73 insertions(+), 24 deletions(-) diff --git a/src/main/java/cpw/mods/jarhandling/SecureJar.java b/src/main/java/cpw/mods/jarhandling/SecureJar.java index 095f3e5..ab32a91 100644 --- a/src/main/java/cpw/mods/jarhandling/SecureJar.java +++ b/src/main/java/cpw/mods/jarhandling/SecureJar.java @@ -40,6 +40,9 @@ static SecureJar from(final Path... paths) { /** * 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(); @@ -66,6 +69,9 @@ default Set getPackages() { Path getPath(String first, String... rest); + /** + * {@return the root path in the jar's own filesystem} + */ Path getRootPath(); interface ModuleDataProvider { @@ -80,13 +86,20 @@ interface ModuleDataProvider { CodeSigner[] verifyAndGetSigners(String cname, byte[] bytes); } + /** + * 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); diff --git a/src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java b/src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java index 348f4f3..70c23b4 100644 --- a/src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java +++ b/src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java @@ -12,14 +12,23 @@ import java.util.jar.Manifest; public final class SecureJarBuilder { + private Path[] paths = new Path[0]; private Supplier defaultManifest = Manifest::new; + private String[] ignoredRootPackages = new String[0]; @Nullable private BiPredicate pathFilter = null; private Function metadataSupplier = JarMetadata::from; - private Path[] paths = new Path[0]; public SecureJarBuilder() {} + /** + * Sets the paths for the files of this jar. + */ + public SecureJarBuilder 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. @@ -32,8 +41,10 @@ public SecureJarBuilder defaultManifest(Supplier manifest) { } /** - * Overrides the path filter for this jar. - * TODO: wtf are the arguments passed to the path filter + * Overrides the path filter for this jar, to exclude some entries from the underlying file system. + * + *

The second parameter to the filter is the base path, i.e. one of the paths passed to {@link #paths(Path...)}. + * The first parameter to the filter is the path of the entry being checked, relative to the base path. */ public SecureJarBuilder pathFilter(@Nullable BiPredicate pathFilter) { this.pathFilter = pathFilter; @@ -41,20 +52,24 @@ public SecureJarBuilder pathFilter(@Nullable BiPredicate pathFil } /** - * Overrides the {@link JarMetadata} for this jar. + * Exclude some root folders from being scanned for code. + * This can be used to skip scanning of folders that are known to not contain code, + * but would be expensive to go through. */ - public SecureJarBuilder metadata(Function metadataSupplier) { - Objects.requireNonNull(metadataSupplier); + public SecureJarBuilder ignoreRootPackages(String... ignoredRootPackages) { + Objects.requireNonNull(ignoredRootPackages); - this.metadataSupplier = metadataSupplier; + this.ignoredRootPackages = ignoredRootPackages; return this; } /** - * Sets the paths for the files of this jar. + * Overrides the {@link JarMetadata} for this jar. */ - public SecureJarBuilder paths(Path... paths) { - this.paths = paths; + public SecureJarBuilder metadata(Function metadataSupplier) { + Objects.requireNonNull(metadataSupplier); + + this.metadataSupplier = metadataSupplier; return this; } @@ -62,7 +77,7 @@ public SecureJarBuilder paths(Path... paths) { * Builds the jar. */ public SecureJar build() { - JarContentsImpl contents = new JarContentsImpl(defaultManifest, pathFilter, paths); + JarContentsImpl contents = new JarContentsImpl(paths, defaultManifest, ignoredRootPackages, pathFilter); JarMetadata metadata = metadataSupplier.apply(contents); return new Jar(contents, metadata); } diff --git a/src/main/java/cpw/mods/jarhandling/impl/Jar.java b/src/main/java/cpw/mods/jarhandling/impl/Jar.java index 0766475..650c8e6 100644 --- a/src/main/java/cpw/mods/jarhandling/impl/Jar.java +++ b/src/main/java/cpw/mods/jarhandling/impl/Jar.java @@ -32,7 +32,7 @@ public class Jar implements SecureJar { @Deprecated(forRemoval = true) public Jar(final Supplier defaultManifest, final Function metadataFunction, final BiPredicate pathfilter, final Path... paths) { - this.contents = new JarContentsImpl(defaultManifest, pathfilter, paths); + this.contents = new JarContentsImpl(paths, defaultManifest, new String[0], pathfilter); this.manifest = contents.getManifest(); this.signingData = contents.signingData; this.filesystem = contents.filesystem; diff --git a/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java b/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java index 936a598..d993630 100644 --- a/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java +++ b/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java @@ -9,10 +9,14 @@ 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.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -40,12 +44,14 @@ public class JarContentsImpl implements JarContents { // Name overrides, if the jar is a multi-release jar private final Map nameOverrides; + // Folders known to not contain packages + private final Set ignoredRootPackages; // Cache for repeated getPackages calls private Set packages; // Cache for repeated getMetaInfServices calls private List providers; - public JarContentsImpl(Supplier defaultManifest, @Nullable BiPredicate pathFilter, Path[] paths) { + public JarContentsImpl(Path[] paths, Supplier defaultManifest, String[] ignoredRootPackages, @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))); @@ -54,6 +60,8 @@ public JarContentsImpl(Supplier defaultManifest, @Nullable BiPredicate this.manifest = readManifestAndSigningData(defaultManifest, validPaths); // Read multi-release jar information this.nameOverrides = readMultiReleaseInfo(); + + this.ignoredRootPackages = Set.of(ignoredRootPackages); } private Manifest readManifestAndSigningData(Supplier defaultManifest, Path[] validPaths) { @@ -129,16 +137,29 @@ public Manifest getManifest() { @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()); + 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; + } + }); + this.packages = Set.copyOf(packages); } catch (IOException e) { throw new UncheckedIOException(e); } From a51291d484a03aed49d0d2043f70e384fdc465b9 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Sun, 12 Nov 2023 14:58:49 +0100 Subject: [PATCH 04/14] Restore not scanning META-INF for packages --- src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java b/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java index d993630..c271765 100644 --- a/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java +++ b/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java @@ -45,7 +45,7 @@ public class JarContentsImpl implements JarContents { private final Map nameOverrides; // Folders known to not contain packages - private final Set ignoredRootPackages; + private final Set ignoredRootPackages = new HashSet<>(); // Cache for repeated getPackages calls private Set packages; // Cache for repeated getMetaInfServices calls @@ -61,7 +61,8 @@ public JarContentsImpl(Path[] paths, Supplier defaultManifest, String[ // Read multi-release jar information this.nameOverrides = readMultiReleaseInfo(); - this.ignoredRootPackages = Set.of(ignoredRootPackages); + this.ignoredRootPackages.add("META-INF"); // Always ignore META-INF + this.ignoredRootPackages.addAll(List.of(ignoredRootPackages)); // And additional user-provided packages } private Manifest readManifestAndSigningData(Supplier defaultManifest, Path[] validPaths) { From 846e34435195a7eae5b83fe2d98114458ad46e0f Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Sun, 12 Nov 2023 15:31:38 +0100 Subject: [PATCH 05/14] Allow instantiating JarContents separately from the SecureJar --- .../cpw/mods/jarhandling/JarContents.java | 4 +- .../mods/jarhandling/JarContentsBuilder.java | 72 +++++++++++++++++++ .../java/cpw/mods/jarhandling/SecureJar.java | 1 + .../mods/jarhandling/SecureJarBuilder.java | 48 +++++++------ 4 files changed, 101 insertions(+), 24 deletions(-) create mode 100644 src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java diff --git a/src/main/java/cpw/mods/jarhandling/JarContents.java b/src/main/java/cpw/mods/jarhandling/JarContents.java index ce2e33a..8d21b5d 100644 --- a/src/main/java/cpw/mods/jarhandling/JarContents.java +++ b/src/main/java/cpw/mods/jarhandling/JarContents.java @@ -12,13 +12,15 @@ /** * 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 SecureJarBuilder#contents(JarContents)}. */ @ApiStatus.NonExtendable public interface JarContents { /** * @see SecureJar#getPrimaryPath() */ - // TODO: document what this is actually used for Path getPrimaryPath(); /** 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..67cd4f4 --- /dev/null +++ b/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java @@ -0,0 +1,72 @@ +package cpw.mods.jarhandling; + +import cpw.mods.jarhandling.impl.JarContentsImpl; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.util.Objects; +import java.util.function.BiPredicate; +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; + private String[] ignoredRootPackages = new String[0]; + @Nullable + private BiPredicate pathFilter = null; + + public JarContentsBuilder() {} + + /** + * Sets the 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. + * + *

The second parameter to the filter is the base path, i.e. one of the paths passed to {@link #paths(Path...)}. + * The first parameter to the filter is the path of the entry being checked, relative to the base path. + */ + public JarContentsBuilder pathFilter(@Nullable BiPredicate pathFilter) { + this.pathFilter = pathFilter; + return this; + } + + /** + * Exclude some root folders from being scanned for code. + * This can be used to skip scanning of folders that are known to not contain code, + * but would be expensive to go through. + */ + public JarContentsBuilder ignoreRootPackages(String... ignoredRootPackages) { + Objects.requireNonNull(ignoredRootPackages); + + this.ignoredRootPackages = ignoredRootPackages; + return this; + } + + /** + * Builds the jar. + */ + public JarContents build() { + return new JarContentsImpl(paths, defaultManifest, ignoredRootPackages, pathFilter); + } +} diff --git a/src/main/java/cpw/mods/jarhandling/SecureJar.java b/src/main/java/cpw/mods/jarhandling/SecureJar.java index ab32a91..a1cf37d 100644 --- a/src/main/java/cpw/mods/jarhandling/SecureJar.java +++ b/src/main/java/cpw/mods/jarhandling/SecureJar.java @@ -1,6 +1,7 @@ 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; diff --git a/src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java b/src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java index 70c23b4..fbed3b7 100644 --- a/src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java +++ b/src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java @@ -11,55 +11,57 @@ import java.util.function.Supplier; import java.util.jar.Manifest; +/** + * Builder for a {@link SecureJar}. + * + * @see JarContentsBuilder + */ public final class SecureJarBuilder { - private Path[] paths = new Path[0]; - private Supplier defaultManifest = Manifest::new; - private String[] ignoredRootPackages = new String[0]; + private final JarContentsBuilder contentsBuilder = new JarContentsBuilder(); @Nullable - private BiPredicate pathFilter = null; + private JarContentsImpl contents; private Function metadataSupplier = JarMetadata::from; public SecureJarBuilder() {} /** - * Sets the paths for the files of this jar. + * @see JarContentsBuilder#paths(Path...) */ public SecureJarBuilder paths(Path... paths) { - this.paths = paths; + contentsBuilder.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. + * @see JarContentsBuilder#defaultManifest(Supplier) */ public SecureJarBuilder defaultManifest(Supplier manifest) { - Objects.requireNonNull(manifest); - - this.defaultManifest = manifest; + contentsBuilder.defaultManifest(manifest); return this; } /** - * Overrides the path filter for this jar, to exclude some entries from the underlying file system. - * - *

The second parameter to the filter is the base path, i.e. one of the paths passed to {@link #paths(Path...)}. - * The first parameter to the filter is the path of the entry being checked, relative to the base path. + * @see JarContentsBuilder#pathFilter(BiPredicate) */ public SecureJarBuilder pathFilter(@Nullable BiPredicate pathFilter) { - this.pathFilter = pathFilter; + contentsBuilder.pathFilter(pathFilter); return this; } /** - * Exclude some root folders from being scanned for code. - * This can be used to skip scanning of folders that are known to not contain code, - * but would be expensive to go through. + * @see JarContentsBuilder#ignoreRootPackages(String...) */ public SecureJarBuilder ignoreRootPackages(String... ignoredRootPackages) { - Objects.requireNonNull(ignoredRootPackages); + contentsBuilder.ignoreRootPackages(ignoredRootPackages); + return this; + } - this.ignoredRootPackages = ignoredRootPackages; + /** + * Directly overrides the {@link JarContents} for this jar. + * If set, all the previous builder methods are ignored. + */ + public SecureJarBuilder contents(JarContents contents) { + this.contents = (JarContentsImpl) contents; return this; } @@ -77,8 +79,8 @@ public SecureJarBuilder metadata(Function metadataSupp * Builds the jar. */ public SecureJar build() { - JarContentsImpl contents = new JarContentsImpl(paths, defaultManifest, ignoredRootPackages, pathFilter); + JarContents contents = this.contents == null ? contentsBuilder.build() : this.contents; JarMetadata metadata = metadataSupplier.apply(contents); - return new Jar(contents, metadata); + return new Jar((JarContentsImpl) contents, metadata); } } From eeb54446f3e5506e1b39faf6d0a628d3923139b7 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Sun, 12 Nov 2023 15:44:15 +0100 Subject: [PATCH 06/14] Remove SecureJarBuilder, JarContentsBuilder is enough --- .../cpw/mods/jarhandling/JarContents.java | 2 +- .../java/cpw/mods/jarhandling/SecureJar.java | 20 ++++- .../mods/jarhandling/SecureJarBuilder.java | 86 ------------------- 3 files changed, 17 insertions(+), 91 deletions(-) delete mode 100644 src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java diff --git a/src/main/java/cpw/mods/jarhandling/JarContents.java b/src/main/java/cpw/mods/jarhandling/JarContents.java index 8d21b5d..ba89748 100644 --- a/src/main/java/cpw/mods/jarhandling/JarContents.java +++ b/src/main/java/cpw/mods/jarhandling/JarContents.java @@ -14,7 +14,7 @@ * Typically used to build the {@linkplain JarMetadata metadata} for a {@link SecureJar}. * *

Create with {@link JarContentsBuilder}. - * Convert to a full jar with {@link SecureJarBuilder#contents(JarContents)}. + * Convert to a full jar with {@link SecureJar#from(JarContents)}. */ @ApiStatus.NonExtendable public interface JarContents { diff --git a/src/main/java/cpw/mods/jarhandling/SecureJar.java b/src/main/java/cpw/mods/jarhandling/SecureJar.java index a1cf37d..951b0b8 100644 --- a/src/main/java/cpw/mods/jarhandling/SecureJar.java +++ b/src/main/java/cpw/mods/jarhandling/SecureJar.java @@ -24,16 +24,28 @@ /** * A secure jar is the full definition for a module, * including all its paths and code signing metadata. - * - *

An instance can be built with {@link SecureJarBuilder}. */ public interface SecureJar { /** * Creates a jar from a list of paths. - * See {@link SecureJarBuilder} for more configuration options. + * See {@link JarContentsBuilder} for more configuration options. */ static SecureJar from(final Path... paths) { - return new SecureJarBuilder().paths(paths).build(); + return from(new JarContentsBuilder().paths(paths).build()); + } + + /** + * Creates a jar from its contents, with default metadata. + */ + static SecureJar from(JarContents contents) { + return from(contents, JarMetadata.from(contents)); + } + + /** + * Creates a jar from its contents and metadata. + */ + static SecureJar from(JarContents contents, JarMetadata metadata) { + return new Jar((JarContentsImpl) contents, metadata); } ModuleDataProvider moduleDataProvider(); diff --git a/src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java b/src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java deleted file mode 100644 index fbed3b7..0000000 --- a/src/main/java/cpw/mods/jarhandling/SecureJarBuilder.java +++ /dev/null @@ -1,86 +0,0 @@ -package cpw.mods.jarhandling; - -import cpw.mods.jarhandling.impl.Jar; -import cpw.mods.jarhandling.impl.JarContentsImpl; -import org.jetbrains.annotations.Nullable; - -import java.nio.file.Path; -import java.util.Objects; -import java.util.function.BiPredicate; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.jar.Manifest; - -/** - * Builder for a {@link SecureJar}. - * - * @see JarContentsBuilder - */ -public final class SecureJarBuilder { - private final JarContentsBuilder contentsBuilder = new JarContentsBuilder(); - @Nullable - private JarContentsImpl contents; - private Function metadataSupplier = JarMetadata::from; - - public SecureJarBuilder() {} - - /** - * @see JarContentsBuilder#paths(Path...) - */ - public SecureJarBuilder paths(Path... paths) { - contentsBuilder.paths(paths); - return this; - } - - /** - * @see JarContentsBuilder#defaultManifest(Supplier) - */ - public SecureJarBuilder defaultManifest(Supplier manifest) { - contentsBuilder.defaultManifest(manifest); - return this; - } - - /** - * @see JarContentsBuilder#pathFilter(BiPredicate) - */ - public SecureJarBuilder pathFilter(@Nullable BiPredicate pathFilter) { - contentsBuilder.pathFilter(pathFilter); - return this; - } - - /** - * @see JarContentsBuilder#ignoreRootPackages(String...) - */ - public SecureJarBuilder ignoreRootPackages(String... ignoredRootPackages) { - contentsBuilder.ignoreRootPackages(ignoredRootPackages); - return this; - } - - /** - * Directly overrides the {@link JarContents} for this jar. - * If set, all the previous builder methods are ignored. - */ - public SecureJarBuilder contents(JarContents contents) { - this.contents = (JarContentsImpl) contents; - return this; - } - - /** - * Overrides the {@link JarMetadata} for this jar. - */ - public SecureJarBuilder metadata(Function metadataSupplier) { - Objects.requireNonNull(metadataSupplier); - - this.metadataSupplier = metadataSupplier; - return this; - } - - /** - * Builds the jar. - */ - public SecureJar build() { - JarContents contents = this.contents == null ? contentsBuilder.build() : this.contents; - JarMetadata metadata = metadataSupplier.apply(contents); - return new Jar((JarContentsImpl) contents, metadata); - } -} From 3547906b725eeddb7459e6cd9f3b1785f51b1147 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Sun, 12 Nov 2023 17:33:39 +0100 Subject: [PATCH 07/14] Use gradle.properties for dependency versions --- build.gradle | 10 ++++------ gradle.properties | 2 ++ sjh-jmh/build.gradle | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 gradle.properties diff --git a/build.gradle b/build.gradle index c511e45..32f2445 100644 --- a/build.gradle +++ b/build.gradle @@ -117,13 +117,11 @@ group = 'cpw.mods' version = gradleutils.version logger.lifecycle('Version: ' + version) -ext.asmVersion = 9.3 -ext.jbAnnotationsVersion = '22.0.0' dependencies { - api("org.ow2.asm:asm:${asmVersion}") - api("org.ow2.asm:asm-tree:${asmVersion}") - api("org.ow2.asm:asm-commons:${asmVersion}") - implementation("org.jetbrains:annotations:${jbAnnotationsVersion}") + 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 +} From c7de85865c0b83fb3e28a9ff065a4fa258deb782 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Sun, 12 Nov 2023 19:56:57 +0100 Subject: [PATCH 08/14] Simplify readMultiReleaseInfo and add a test for it --- .../jarhandling/impl/JarContentsImpl.java | 57 +++++++++++++------ .../jarhandling/impl/TestMultiRelease.java | 39 +++++++++++++ .../multirelease/META-INF/MANIFEST.MF | 1 + .../multirelease/META-INF/versions/1000/a.txt | 1 + .../multirelease/META-INF/versions/1000/b.txt | 1 + .../multirelease/META-INF/versions/9/a.txt | 1 + src/test/resources/multirelease/a.txt | 1 + src/test/resources/multirelease/b.txt | 1 + 8 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 src/test/java/cpw/mods/jarhandling/impl/TestMultiRelease.java create mode 100644 src/test/resources/multirelease/META-INF/MANIFEST.MF create mode 100644 src/test/resources/multirelease/META-INF/versions/1000/a.txt create mode 100644 src/test/resources/multirelease/META-INF/versions/1000/b.txt create mode 100644 src/test/resources/multirelease/META-INF/versions/9/a.txt create mode 100644 src/test/resources/multirelease/a.txt create mode 100644 src/test/resources/multirelease/b.txt diff --git a/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java b/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java index c271765..c37a495 100644 --- a/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java +++ b/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java @@ -16,6 +16,7 @@ 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; @@ -94,26 +95,50 @@ private Manifest readManifestAndSigningData(Supplier defaultManifest, 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) { - 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()))); - return 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 { + 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 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/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 From 36e9375ceab5b81b0568000fe2a7448f229cb3d9 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Sun, 12 Nov 2023 19:58:47 +0100 Subject: [PATCH 09/14] Apply suggestions from code review Co-authored-by: Matyrobbrt <65940752+Matyrobbrt@users.noreply.github.com> --- src/main/java/cpw/mods/jarhandling/JarContents.java | 4 ++-- src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/cpw/mods/jarhandling/JarContents.java b/src/main/java/cpw/mods/jarhandling/JarContents.java index ba89748..357b261 100644 --- a/src/main/java/cpw/mods/jarhandling/JarContents.java +++ b/src/main/java/cpw/mods/jarhandling/JarContents.java @@ -29,13 +29,13 @@ public interface JarContents { Optional findFile(String name); /** - * Gets the manifest of the jar. + * {@return the manifest of the jar} * Empty if no manifest is present in the jar. */ Manifest getManifest(); /** - * Gets all the packages in the jar. + * {@return all the packages in the jar} * (Every folder containing a {@code .class} file is considered a package.) */ Set getPackages(); diff --git a/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java b/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java index 67cd4f4..f768e9b 100644 --- a/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java +++ b/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java @@ -22,7 +22,7 @@ public final class JarContentsBuilder { public JarContentsBuilder() {} /** - * Sets the paths for the files of this jar. + * Sets the root paths for the files of this jar. */ public JarContentsBuilder paths(Path... paths) { this.paths = paths; From c02e249c988095df7b9ef1675a921d2c6150622a Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Sun, 12 Nov 2023 20:01:42 +0100 Subject: [PATCH 10/14] Make ignoredRootPackages a Set in more cases --- src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java | 5 +++-- src/main/java/cpw/mods/jarhandling/impl/Jar.java | 2 +- src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java b/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java index f768e9b..880c25c 100644 --- a/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java +++ b/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java @@ -5,6 +5,7 @@ import java.nio.file.Path; import java.util.Objects; +import java.util.Set; import java.util.function.BiPredicate; import java.util.function.Supplier; import java.util.jar.Manifest; @@ -15,7 +16,7 @@ public final class JarContentsBuilder { private Path[] paths = new Path[0]; private Supplier defaultManifest = Manifest::new; - private String[] ignoredRootPackages = new String[0]; + private Set ignoredRootPackages = Set.of(); @Nullable private BiPredicate pathFilter = null; @@ -59,7 +60,7 @@ public JarContentsBuilder pathFilter(@Nullable BiPredicate pathF public JarContentsBuilder ignoreRootPackages(String... ignoredRootPackages) { Objects.requireNonNull(ignoredRootPackages); - this.ignoredRootPackages = ignoredRootPackages; + this.ignoredRootPackages = Set.of(ignoredRootPackages); return this; } diff --git a/src/main/java/cpw/mods/jarhandling/impl/Jar.java b/src/main/java/cpw/mods/jarhandling/impl/Jar.java index 650c8e6..53a3354 100644 --- a/src/main/java/cpw/mods/jarhandling/impl/Jar.java +++ b/src/main/java/cpw/mods/jarhandling/impl/Jar.java @@ -32,7 +32,7 @@ public class Jar implements SecureJar { @Deprecated(forRemoval = true) public Jar(final Supplier defaultManifest, final Function metadataFunction, final BiPredicate pathfilter, final Path... paths) { - this.contents = new JarContentsImpl(paths, defaultManifest, new String[0], pathfilter); + this.contents = new JarContentsImpl(paths, defaultManifest, Set.of(), pathfilter); this.manifest = contents.getManifest(); this.signingData = contents.signingData; this.filesystem = contents.filesystem; diff --git a/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java b/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java index c37a495..16b4091 100644 --- a/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java +++ b/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java @@ -52,7 +52,7 @@ public class JarContentsImpl implements JarContents { // Cache for repeated getMetaInfServices calls private List providers; - public JarContentsImpl(Path[] paths, Supplier defaultManifest, String[] ignoredRootPackages, @Nullable BiPredicate pathFilter) { + public JarContentsImpl(Path[] paths, Supplier defaultManifest, Set ignoredRootPackages, @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))); @@ -63,7 +63,7 @@ public JarContentsImpl(Path[] paths, Supplier defaultManifest, String[ this.nameOverrides = readMultiReleaseInfo(); this.ignoredRootPackages.add("META-INF"); // Always ignore META-INF - this.ignoredRootPackages.addAll(List.of(ignoredRootPackages)); // And additional user-provided packages + this.ignoredRootPackages.addAll(ignoredRootPackages); // And additional user-provided packages } private Manifest readManifestAndSigningData(Supplier defaultManifest, Path[] validPaths) { From 96f9deb772f84b7ac33756f3b8b226c592a88122 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Mon, 13 Nov 2023 10:56:01 +0100 Subject: [PATCH 11/14] Replace BiPredicate by UnionPathFilter in JarContentsBuilder --- .../cpw/mods/jarhandling/JarContentsBuilder.java | 11 +++++------ .../cpw/mods/niofs/union/UnionPathFilter.java | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 src/main/java/cpw/mods/niofs/union/UnionPathFilter.java diff --git a/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java b/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java index 880c25c..6349254 100644 --- a/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java +++ b/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java @@ -1,12 +1,12 @@ 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.Set; -import java.util.function.BiPredicate; import java.util.function.Supplier; import java.util.jar.Manifest; @@ -18,7 +18,7 @@ public final class JarContentsBuilder { private Supplier defaultManifest = Manifest::new; private Set ignoredRootPackages = Set.of(); @Nullable - private BiPredicate pathFilter = null; + private UnionPathFilter pathFilter = null; public JarContentsBuilder() {} @@ -44,10 +44,9 @@ public JarContentsBuilder defaultManifest(Supplier manifest) { /** * Overrides the path filter for this jar, to exclude some entries from the underlying file system. * - *

The second parameter to the filter is the base path, i.e. one of the paths passed to {@link #paths(Path...)}. - * The first parameter to the filter is the path of the entry being checked, relative to the base path. + * @see UnionPathFilter */ - public JarContentsBuilder pathFilter(@Nullable BiPredicate pathFilter) { + public JarContentsBuilder pathFilter(@Nullable UnionPathFilter pathFilter) { this.pathFilter = pathFilter; return this; } @@ -68,6 +67,6 @@ public JarContentsBuilder ignoreRootPackages(String... ignoredRootPackages) { * Builds the jar. */ public JarContents build() { - return new JarContentsImpl(paths, defaultManifest, ignoredRootPackages, pathFilter); + return new JarContentsImpl(paths, defaultManifest, ignoredRootPackages, pathFilter == null ? null : pathFilter::test); } } 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); +} From 1cac33177a17755b223141592a355b84f918b1cf Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Mon, 13 Nov 2023 11:31:26 +0100 Subject: [PATCH 12/14] Move root package exclusion from JarContentsBuilder to getPackagesExcluding --- .../cpw/mods/jarhandling/JarContents.java | 8 +++ .../mods/jarhandling/JarContentsBuilder.java | 16 +---- .../java/cpw/mods/jarhandling/SecureJar.java | 12 ++-- .../java/cpw/mods/jarhandling/impl/Jar.java | 2 +- .../jarhandling/impl/JarContentsImpl.java | 66 ++++++++++--------- 5 files changed, 52 insertions(+), 52 deletions(-) diff --git a/src/main/java/cpw/mods/jarhandling/JarContents.java b/src/main/java/cpw/mods/jarhandling/JarContents.java index 357b261..0a0b53b 100644 --- a/src/main/java/cpw/mods/jarhandling/JarContents.java +++ b/src/main/java/cpw/mods/jarhandling/JarContents.java @@ -40,6 +40,14 @@ public interface JarContents { */ 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. */ diff --git a/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java b/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java index 6349254..3090066 100644 --- a/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java +++ b/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java @@ -6,7 +6,6 @@ import java.nio.file.Path; import java.util.Objects; -import java.util.Set; import java.util.function.Supplier; import java.util.jar.Manifest; @@ -16,7 +15,6 @@ public final class JarContentsBuilder { private Path[] paths = new Path[0]; private Supplier defaultManifest = Manifest::new; - private Set ignoredRootPackages = Set.of(); @Nullable private UnionPathFilter pathFilter = null; @@ -51,22 +49,10 @@ public JarContentsBuilder pathFilter(@Nullable UnionPathFilter pathFilter) { return this; } - /** - * Exclude some root folders from being scanned for code. - * This can be used to skip scanning of folders that are known to not contain code, - * but would be expensive to go through. - */ - public JarContentsBuilder ignoreRootPackages(String... ignoredRootPackages) { - Objects.requireNonNull(ignoredRootPackages); - - this.ignoredRootPackages = Set.of(ignoredRootPackages); - return this; - } - /** * Builds the jar. */ public JarContents build() { - return new JarContentsImpl(paths, defaultManifest, ignoredRootPackages, pathFilter == null ? null : pathFilter::test); + return new JarContentsImpl(paths, defaultManifest, pathFilter == null ? null : pathFilter::test); } } diff --git a/src/main/java/cpw/mods/jarhandling/SecureJar.java b/src/main/java/cpw/mods/jarhandling/SecureJar.java index 951b0b8..4825d40 100644 --- a/src/main/java/cpw/mods/jarhandling/SecureJar.java +++ b/src/main/java/cpw/mods/jarhandling/SecureJar.java @@ -74,10 +74,6 @@ static SecureJar from(JarContents contents, JarMetadata metadata) { boolean hasSecurityData(); - default Set getPackages() { - return moduleDataProvider().descriptor().packages(); - } - String name(); Path getPath(String first, String... rest); @@ -128,6 +124,14 @@ enum Status { // TODO: add since + /** + * @deprecated Obtain via the {@link ModuleDescriptor} of the jar if you really need this. + */ + @Deprecated(forRemoval = true) + default Set getPackages() { + return moduleDataProvider().descriptor().packages(); + } + /** * @deprecated Obtain via the {@link ModuleDescriptor} of the jar if you really need this. */ diff --git a/src/main/java/cpw/mods/jarhandling/impl/Jar.java b/src/main/java/cpw/mods/jarhandling/impl/Jar.java index 53a3354..e9f41ba 100644 --- a/src/main/java/cpw/mods/jarhandling/impl/Jar.java +++ b/src/main/java/cpw/mods/jarhandling/impl/Jar.java @@ -32,7 +32,7 @@ public class Jar implements SecureJar { @Deprecated(forRemoval = true) public Jar(final Supplier defaultManifest, final Function metadataFunction, final BiPredicate pathfilter, final Path... paths) { - this.contents = new JarContentsImpl(paths, defaultManifest, Set.of(), pathfilter); + this.contents = new JarContentsImpl(paths, defaultManifest, pathfilter); this.manifest = contents.getManifest(); this.signingData = contents.signingData; this.filesystem = contents.filesystem; diff --git a/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java b/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java index 16b4091..a39807b 100644 --- a/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java +++ b/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java @@ -28,8 +28,6 @@ import java.util.jar.JarInputStream; import java.util.jar.Manifest; -import static java.util.stream.Collectors.*; - public class JarContentsImpl implements JarContents { private static final UnionFileSystemProvider UFSP = (UnionFileSystemProvider) FileSystemProvider.installedProviders() .stream() @@ -45,14 +43,12 @@ public class JarContentsImpl implements JarContents { // Name overrides, if the jar is a multi-release jar private final Map nameOverrides; - // Folders known to not contain packages - private final Set ignoredRootPackages = new HashSet<>(); // Cache for repeated getPackages calls private Set packages; // Cache for repeated getMetaInfServices calls private List providers; - public JarContentsImpl(Path[] paths, Supplier defaultManifest, Set ignoredRootPackages, @Nullable BiPredicate pathFilter) { + 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))); @@ -61,9 +57,6 @@ public JarContentsImpl(Path[] paths, Supplier defaultManifest, Set defaultManifest, Path[] validPaths) { @@ -161,34 +154,43 @@ public Manifest getManifest() { } @Override - public Set getPackages() { - if (this.packages == null) { - 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); - } + 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; } + 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; + @Override + public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) { + if (path.getNameCount() > 0 && ignoredRootPackages.contains(path.getName(0).toString())) { + return FileVisitResult.SKIP_SUBTREE; } - }); - this.packages = Set.copyOf(packages); - } catch (IOException e) { - throw new UncheckedIOException(e); - } + 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; } From 95f1496f4bfdb64fba202becf31ba311ce705461 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Mon, 13 Nov 2023 13:17:58 +0100 Subject: [PATCH 13/14] Add some @deprecated annotations --- .../java/cpw/mods/jarhandling/SecureJar.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/java/cpw/mods/jarhandling/SecureJar.java b/src/main/java/cpw/mods/jarhandling/SecureJar.java index 4825d40..aadf561 100644 --- a/src/main/java/cpw/mods/jarhandling/SecureJar.java +++ b/src/main/java/cpw/mods/jarhandling/SecureJar.java @@ -138,28 +138,41 @@ default Set getPackages() { @Deprecated(forRemoval = true) List getProviders(); - // The members below are deprecated for removal - use SecureJarBuilder instead! - + /** + * @deprecated Use {@link JarContentsBuilder} and {@link #from(JarContents)} instead. + */ @Deprecated(forRemoval = true) 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) 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) 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) 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) static SecureJar from(Supplier defaultManifest, Function metadataSupplier, BiPredicate filter, final Path... paths) { return new Jar(defaultManifest, metadataSupplier, filter, paths); From d1eb6d73fcbfd322856e0958b8c5caa2d32c51b3 Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Mon, 13 Nov 2023 13:25:27 +0100 Subject: [PATCH 14/14] Add since=2.1.16 to terminal deprecations --- .../java/cpw/mods/jarhandling/JarMetadata.java | 3 +-- .../java/cpw/mods/jarhandling/SecureJar.java | 16 +++++++--------- src/main/java/cpw/mods/jarhandling/impl/Jar.java | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main/java/cpw/mods/jarhandling/JarMetadata.java b/src/main/java/cpw/mods/jarhandling/JarMetadata.java index dc02498..7a8eda5 100644 --- a/src/main/java/cpw/mods/jarhandling/JarMetadata.java +++ b/src/main/java/cpw/mods/jarhandling/JarMetadata.java @@ -146,9 +146,8 @@ private static String cleanModuleName(String mn) { /** * @deprecated Use {@link #from(JarContents)} instead. - * TODO: add since */ - @Deprecated(forRemoval = true) + @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(); diff --git a/src/main/java/cpw/mods/jarhandling/SecureJar.java b/src/main/java/cpw/mods/jarhandling/SecureJar.java index aadf561..3b3b3f5 100644 --- a/src/main/java/cpw/mods/jarhandling/SecureJar.java +++ b/src/main/java/cpw/mods/jarhandling/SecureJar.java @@ -122,12 +122,10 @@ enum Status { NONE, INVALID, UNVERIFIED, VERIFIED } - // TODO: add since - /** * @deprecated Obtain via the {@link ModuleDescriptor} of the jar if you really need this. */ - @Deprecated(forRemoval = true) + @Deprecated(forRemoval = true, since = "2.1.16") default Set getPackages() { return moduleDataProvider().descriptor().packages(); } @@ -135,13 +133,13 @@ default Set getPackages() { /** * @deprecated Obtain via the {@link ModuleDescriptor} of the jar if you really need this. */ - @Deprecated(forRemoval = true) + @Deprecated(forRemoval = true, since = "2.1.16") List getProviders(); /** * @deprecated Use {@link JarContentsBuilder} and {@link #from(JarContents)} instead. */ - @Deprecated(forRemoval = true) + @Deprecated(forRemoval = true, since = "2.1.16") static SecureJar from(BiPredicate filter, final Path... paths) { return from(jar->JarMetadata.from(jar, paths), filter, paths); } @@ -149,7 +147,7 @@ static SecureJar from(BiPredicate filter, final Path... paths) { /** * @deprecated Use {@link JarContentsBuilder} and {@link #from(JarContents)} instead. */ - @Deprecated(forRemoval = true) + @Deprecated(forRemoval = true, since = "2.1.16") static SecureJar from(Function metadataSupplier, final Path... paths) { return from(Manifest::new, metadataSupplier, paths); } @@ -157,7 +155,7 @@ static SecureJar from(Function metadataSupplier, final P /** * @deprecated Use {@link JarContentsBuilder} and {@link #from(JarContents)} instead. */ - @Deprecated(forRemoval = true) + @Deprecated(forRemoval = true, since = "2.1.16") static SecureJar from(Function metadataSupplier, BiPredicate filter, final Path... paths) { return from(Manifest::new, metadataSupplier, filter, paths); } @@ -165,7 +163,7 @@ static SecureJar from(Function metadataSupplier, BiPredi /** * @deprecated Use {@link JarContentsBuilder} and {@link #from(JarContents)} instead. */ - @Deprecated(forRemoval = true) + @Deprecated(forRemoval = true, since = "2.1.16") static SecureJar from(Supplier defaultManifest, Function metadataSupplier, final Path... paths) { return from(defaultManifest, metadataSupplier, null, paths); } @@ -173,7 +171,7 @@ static SecureJar from(Supplier defaultManifest, Function 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 e9f41ba..ee9c3e2 100644 --- a/src/main/java/cpw/mods/jarhandling/impl/Jar.java +++ b/src/main/java/cpw/mods/jarhandling/impl/Jar.java @@ -30,7 +30,7 @@ public class Jar implements SecureJar { private final JarMetadata metadata; - @Deprecated(forRemoval = true) + @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();