diff --git a/loader/build.gradle b/loader/build.gradle index 1fccd416c..4b2f3185b 100644 --- a/loader/build.gradle +++ b/loader/build.gradle @@ -14,7 +14,7 @@ sourceSets { jmh testJars - // Additional test JARs, to be loaded via SecureJar.from(...) + // Additional test JARs, to be loaded via Jar.of(...) testjar1 testjar2 // Test classpath code, to make sure that ModuleClassLoader is properly isolated from the classpath diff --git a/loader/src/jmh/java/cpw/mods/cl/benchmarks/JarModuleFinderBenchmark.java b/loader/src/jmh/java/cpw/mods/cl/benchmarks/JarModuleFinderBenchmark.java index ab21ecb82..974fbac02 100644 --- a/loader/src/jmh/java/cpw/mods/cl/benchmarks/JarModuleFinderBenchmark.java +++ b/loader/src/jmh/java/cpw/mods/cl/benchmarks/JarModuleFinderBenchmark.java @@ -1,9 +1,12 @@ package cpw.mods.cl.benchmarks; import cpw.mods.cl.JarModuleFinder; -import cpw.mods.jarhandling.SecureJar; +import cpw.mods.jarhandling.JarContents; +import cpw.mods.jarhandling.impl.Jar; +import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.List; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; @@ -24,9 +27,9 @@ public void setup() throws Exception { } @Benchmark - public void benchJarModuleFinderOf(Blackhole blackhole) { - var secureJar1 = SecureJar.from(path1, path2); - var secureJar2 = SecureJar.from(path3); + public void benchJarModuleFinderOf(Blackhole blackhole) throws IOException { + var secureJar1 = Jar.of(JarContents.ofPaths(List.of(path1, path2))); + var secureJar2 = Jar.of(path3); var jarModuleFinder = JarModuleFinder.of(secureJar1, secureJar2); blackhole.consume(jarModuleFinder); diff --git a/loader/src/main/java/cpw/mods/cl/ModuleClassLoader.java b/loader/src/main/java/cpw/mods/cl/ModuleClassLoader.java index 9f3725415..07765324a 100644 --- a/loader/src/main/java/cpw/mods/cl/ModuleClassLoader.java +++ b/loader/src/main/java/cpw/mods/cl/ModuleClassLoader.java @@ -202,10 +202,9 @@ protected byte[] getClassBytes(final ModuleReader reader, final ModuleReference private Class readerToClass(final ModuleReader reader, final ModuleReference ref, final String name) { var bytes = maybeTransformClassBytes(getClassBytes(reader, ref, name), name, null); if (bytes.length == 0) return null; - var cname = name.replace('.', '/') + ".class"; var modroot = this.resolvedRoots.get(ref.descriptor().name()); ProtectionDomainHelper.tryDefinePackage(this, name, modroot.jar().getManifest(), t -> modroot.jar().getManifest().getAttributes(t), this::definePackage); // Packages are dirctories, and can't be signed, so use raw attributes instead of signed. - var cs = ProtectionDomainHelper.createCodeSource(toURL(ref.location()), modroot.jar().verifyAndGetSigners(cname, bytes)); + var cs = ProtectionDomainHelper.createCodeSource(toURL(ref.location()), null); var cls = defineClass(name, bytes, 0, bytes.length, ProtectionDomainHelper.createProtectionDomain(cs, this)); ProtectionDomainHelper.trySetPackageModule(cls.getPackage(), cls.getModule()); return cls; diff --git a/loader/src/main/java/cpw/mods/cl/ProtectionDomainHelper.java b/loader/src/main/java/cpw/mods/cl/ProtectionDomainHelper.java index cb4bf20db..df28e8834 100644 --- a/loader/src/main/java/cpw/mods/cl/ProtectionDomainHelper.java +++ b/loader/src/main/java/cpw/mods/cl/ProtectionDomainHelper.java @@ -13,11 +13,12 @@ import java.util.function.Function; import java.util.jar.Attributes; import java.util.jar.Manifest; +import org.jetbrains.annotations.Nullable; public class ProtectionDomainHelper { private static final Map csCache = new HashMap<>(); - public static CodeSource createCodeSource(final URL url, final CodeSigner[] signers) { + public static CodeSource createCodeSource(URL url, @Nullable CodeSigner[] signers) { synchronized (csCache) { return csCache.computeIfAbsent(url, u -> new CodeSource(url, signers)); } diff --git a/loader/src/main/java/cpw/mods/jarhandling/CompositeModContainer.java b/loader/src/main/java/cpw/mods/jarhandling/CompositeModContainer.java new file mode 100644 index 000000000..f0a15b0bf --- /dev/null +++ b/loader/src/main/java/cpw/mods/jarhandling/CompositeModContainer.java @@ -0,0 +1,145 @@ +package cpw.mods.jarhandling; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.jar.Manifest; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.jetbrains.annotations.Nullable; + +public final class CompositeModContainer implements JarContents { + private final JarContents[] delegates; + @Nullable + private final PathFilter[] filters; + + public CompositeModContainer(List delegates) { + this(delegates, null); + } + + public CompositeModContainer(List delegates, @Nullable List filters) { + this.delegates = delegates.toArray(JarContents[]::new); + this.filters = filters != null ? filters.toArray(PathFilter[]::new) : null; + if (delegates.isEmpty()) { + throw new IllegalArgumentException("Cannot construct an empty mod container"); + } + if (filters != null && delegates.size() != filters.size()) { + throw new IllegalArgumentException("The number of delegates and filters must match."); + } + } + + @Override + public Path getPrimaryPath() { + return delegates[0].getPrimaryPath(); + } + + @Override + public String toString() { + return "[" + Arrays.stream(delegates).map(Object::toString).collect(Collectors.joining(", ")) + "]"; + } + + @Override + public Optional findFile(String relativePath) { + for (var delegate : delegates) { + var result = delegate.findFile(relativePath); + if (result.isPresent()) { + return result; + } + } + return Optional.empty(); + } + + @Nullable + @Override + public Manifest getJarManifest() { + for (var delegate : delegates) { + var manifest = delegate.getJarManifest(); + if (manifest != null) { + return manifest; + } + } + return null; + } + + @Override + public InputStream openFile(String relativePath) throws IOException { + for (var delegate : delegates) { + var stream = delegate.openFile(relativePath); + if (stream != null) { + return stream; + } + } + return null; + } + + @Override + public byte[] readFile(String relativePath) throws IOException { + for (var delegate : delegates) { + var content = delegate.readFile(relativePath); + if (content != null) { + return content; + } + } + return null; + } + + @Override + public boolean hasContentRoot(Path path) { + for (var delegate : delegates) { + if (delegate.hasContentRoot(path)) { + return true; + } + } + return false; + } + + @Override + public Stream getClasspathRoots() { + return Arrays.stream(delegates).flatMap(JarContents::getClasspathRoots); + } + + @Override + public void visitContent(ModContentVisitor visitor) { + // This is based on the logic that openResource will return the file from the *first* delegate + // Every relative path we return will not be returned again + var distinctVisitor = new ModContentVisitor() { + final Set pathsVisited = new HashSet<>(); + + @Override + public void visit(String relativePath, IOSupplier contentSupplier, IOSupplier attributesSupplier) { + if (pathsVisited.add(relativePath)) { + visitor.visit(relativePath, contentSupplier, attributesSupplier); + } + } + }; + + for (var delegate : delegates) { + delegate.visitContent(distinctVisitor); + } + } + + @Override + public void close() throws IOException { + IOException error = null; + for (var delegate : delegates) { + try { + delegate.close(); + } catch (IOException e) { + if (error == null) { + error = new IOException("Failed to close one ore more delegates of " + this); + } + error.addSuppressed(e); + } + } + if (error != null) { + throw error; + } + } +} diff --git a/loader/src/main/java/cpw/mods/jarhandling/EmptyModContainer.java b/loader/src/main/java/cpw/mods/jarhandling/EmptyModContainer.java new file mode 100644 index 000000000..2c1e9cfc0 --- /dev/null +++ b/loader/src/main/java/cpw/mods/jarhandling/EmptyModContainer.java @@ -0,0 +1,54 @@ +package cpw.mods.jarhandling; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.nio.file.Path; +import java.util.Optional; +import java.util.stream.Stream; +import org.jetbrains.annotations.Nullable; + +public final class EmptyModContainer implements JarContents { + private final Path path; + + public EmptyModContainer(Path path) { + this.path = path; + } + + @Override + public Optional findFile(String relativePath) { + return Optional.empty(); + } + + @Override + public Path getPrimaryPath() { + return path; + } + + @Override + public @Nullable InputStream openFile(String relativePath) throws IOException { + return null; + } + + @Override + public byte @Nullable [] readFile(String relativePath) throws IOException { + return null; + } + + @Override + public boolean hasContentRoot(Path path) { + return false; + } + + @Override + public Stream getClasspathRoots() { + return Stream.empty(); + } + + @Override + public void visitContent(ModContentVisitor visitor) {} + + @Override + public void close() throws IOException {} +} diff --git a/loader/src/main/java/cpw/mods/jarhandling/FolderModContainer.java b/loader/src/main/java/cpw/mods/jarhandling/FolderModContainer.java new file mode 100644 index 000000000..b2986c82a --- /dev/null +++ b/loader/src/main/java/cpw/mods/jarhandling/FolderModContainer.java @@ -0,0 +1,94 @@ +package cpw.mods.jarhandling; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributeView; +import java.util.Optional; +import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public record FolderModContainer(Path path) implements JarContents { + private static final Logger LOG = LoggerFactory.getLogger(FolderModContainer.class); + + @Override + public Path getPrimaryPath() { + return path; + } + + @Override + public InputStream openFile(String relativePath) throws IOException { + try { + return Files.newInputStream(path.resolve(relativePath)); + } catch (NoSuchFileException e) { + return null; + } + } + + @Override + public byte[] readFile(String relativePath) throws IOException { + try { + return Files.readAllBytes(path.resolve(relativePath)); + } catch (NoSuchFileException e) { + return null; + } + } + + @Override + public boolean hasContentRoot(Path path) { + return this.path.equals(path); + } + + @Override + public Stream getClasspathRoots() { + try { + return Stream.of(path.toUri().toURL()); + } catch (MalformedURLException e) { + LOG.error("Failed to convert path to URL: {}", path, e); + return Stream.of(); + } + } + + @Override + public void visitContent(ModContentVisitor visitor) { + var startingPoint = path; + try (var stream = Files.walk(startingPoint)) { + stream.forEach(path -> { + var file = path.toFile(); + if (file.isFile()) { + var relativePath = PathNormalization.normalize(startingPoint.relativize(path).toString()); + visitor.visit(relativePath, () -> Files.newInputStream(path), () -> { + var attributes = Files.getFileAttributeView(path, BasicFileAttributeView.class).readAttributes(); + return new ModContentAttributes(attributes.lastModifiedTime(), attributes.size()); + }); + } + }); + } catch (IOException e) { + throw new UncheckedIOException("Failed to walk contents of " + path, e); + } + } + + @Override + public String toString() { + return path.toString(); + } + + @Override + public Optional findFile(String relativePath) { + if (relativePath.startsWith("/")) { + throw new IllegalArgumentException("Must be a relative path: " + relativePath); + } + var pathToFile = path.resolve(relativePath); + return pathToFile.toFile().isFile() ? Optional.of(pathToFile.toUri()) : Optional.empty(); + } + + @Override + public void close() {} +} diff --git a/loader/src/main/java/cpw/mods/jarhandling/IOSupplier.java b/loader/src/main/java/cpw/mods/jarhandling/IOSupplier.java new file mode 100644 index 000000000..599b86198 --- /dev/null +++ b/loader/src/main/java/cpw/mods/jarhandling/IOSupplier.java @@ -0,0 +1,8 @@ +package cpw.mods.jarhandling; + +import java.io.IOException; + +@FunctionalInterface +public interface IOSupplier { + T get() throws IOException; +} diff --git a/loader/src/main/java/cpw/mods/jarhandling/JarContents.java b/loader/src/main/java/cpw/mods/jarhandling/JarContents.java index 9d7f52ae2..26272bdb7 100644 --- a/loader/src/main/java/cpw/mods/jarhandling/JarContents.java +++ b/loader/src/main/java/cpw/mods/jarhandling/JarContents.java @@ -1,72 +1,144 @@ package cpw.mods.jarhandling; -import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; import java.net.URI; +import java.net.URL; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.jar.Manifest; -import org.jetbrains.annotations.ApiStatus; - -/** - * Access to the contents of a list of {@link Path}s, interpreted as a jar file. - * Typically used to build the {@linkplain JarMetadata metadata} for a {@link SecureJar}. - * - *

Create with {@link JarContentsBuilder}. - * Convert to a full jar with {@link SecureJar#from(JarContents)}. - */ -@ApiStatus.NonExtendable -public interface JarContents extends Closeable { - /** - * @see SecureJar#getPrimaryPath() - */ +import java.util.stream.Stream; +import net.neoforged.neoforgespi.locating.ModFileLoadingException; +import org.jetbrains.annotations.Nullable; + +public sealed interface JarContents extends AutoCloseable permits CompositeModContainer, FolderModContainer, JarModContainer, EmptyModContainer { + record FilteredPath(Path path, @Nullable CompositeModContainer.PathFilter filter) {} + + static JarContents ofFilteredPaths(Collection paths) throws IOException { + if (paths.isEmpty()) { + throw new IllegalArgumentException("Cannot construct an empty mod container."); + } else { + try { + var containers = paths.stream().map(path -> { + try { + return JarContents.ofPath(path.path); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }).toList(); + var filters = paths.stream().map(FilteredPath::filter).toList(); + return new CompositeModContainer(containers, filters); + } catch (UncheckedIOException e) { + throw e.getCause(); + } + } + } + + static JarContents ofPaths(Collection paths) throws IOException { + if (paths.size() == 1) { + return ofPath(paths.iterator().next()); + } else if (paths.size() > 1) { + try { + return new CompositeModContainer(paths.stream().map(path -> { + if (!Files.exists(path)) { + return empty(path); + } + try { + return JarContents.ofPath(path); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }).toList()); + } catch (UncheckedIOException e) { + throw e.getCause(); + } + } else { + throw new IllegalArgumentException("Cannot construct an empty mod container."); + } + } + + static JarContents ofPath(Path path) throws IOException { + if (Files.isRegularFile(path)) { + return new JarModContainer(path); + } else if (Files.isDirectory(path)) { + return new FolderModContainer(path); + } else { + throw new ModFileLoadingException("Cannot construct mod container from missing " + path); + } + } + + static JarContents empty(Path path) { + return new EmptyModContainer(path); + } + + Optional findFile(String relativePath); + Path getPrimaryPath(); - /** - * Looks for a file in the jar. - */ - Optional findFile(String name); + @Nullable + default Manifest getJarManifest() { + return null; + } - /** - * {@return the manifest of the jar} - * Empty if no manifest is present in the jar. - */ - Manifest getManifest(); + @Nullable + InputStream openFile(String relativePath) throws IOException; - /** - * {@return all the packages in the jar} - * (Every folder containing a {@code .class} file is considered a package.) - */ - Set getPackages(); + byte @Nullable [] readFile(String relativePath) throws IOException; /** - * {@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. + * Does this mod container have the given file system path as one of its content roots? */ - Set getPackagesExcluding(String... excludedRootPackages); + boolean hasContentRoot(Path path); /** - * Parses the {@code META-INF/services} files in the jar, and returns the list of service providers. + * @return The roots of this mod-container for use in an {@link java.net.URLClassLoader}. */ - List getMetaInfServices(); + Stream getClasspathRoots(); /** - * Create plain jar contents from a single jar file or folder. - * For more advanced use-cases see {@link JarContentsBuilder}. + * Visits all content found in this container. */ - static JarContents of(Path fileOrFolder) { - return new JarContentsBuilder().paths(fileOrFolder).build(); + void visitContent(ModContentVisitor visitor); + + @Override + void close() throws IOException; + + @FunctionalInterface + interface PathFilter { + boolean test(String relativePath); } - /** - * Create a virtual jar that consists of the contents of the given jar-files and folders. - * For more advanced use-cases see {@link JarContentsBuilder}. - */ - static JarContents of(Collection filesOrFolders) { - return new JarContentsBuilder().paths(filesOrFolders.toArray(new Path[0])).build(); + static Builder builder() { + return new Builder(); + } + + final class Builder { + private final List paths = new ArrayList<>(); + + private Builder() {} + + public Builder path(Path path) { + return this; + } + + public Builder filteredPath(Path path, PathFilter filter) { + return this; + } + + public JarContents build() throws IOException { + if (paths.isEmpty()) { + // return EMPTY; // TODO + throw new IllegalArgumentException(); + } + if (paths.size() == 1 && paths.getFirst().filter == null) { + return ofPath(paths.getFirst().path); + } + return ofFilteredPaths(paths); + } } } diff --git a/loader/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java b/loader/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java deleted file mode 100644 index d54998c2f..000000000 --- a/loader/src/main/java/cpw/mods/jarhandling/JarContentsBuilder.java +++ /dev/null @@ -1,57 +0,0 @@ -package cpw.mods.jarhandling; - -import cpw.mods.jarhandling.impl.JarContentsImpl; -import cpw.mods.niofs.union.UnionPathFilter; -import java.nio.file.Path; -import java.util.Objects; -import java.util.function.Supplier; -import java.util.jar.Manifest; -import org.jetbrains.annotations.Nullable; - -/** - * Builder for {@link JarContents}. - */ -public final class JarContentsBuilder { - private Path[] paths = new Path[0]; - private Supplier defaultManifest = Manifest::new; - @Nullable - private UnionPathFilter pathFilter = null; - - public JarContentsBuilder() {} - - /** - * Sets the root paths for the files of this jar. - */ - public JarContentsBuilder paths(Path... paths) { - this.paths = paths; - return this; - } - - /** - * Overrides the default manifest for this jar. - * The default manifest is only used when the jar does not provide a manifest already. - */ - public JarContentsBuilder defaultManifest(Supplier manifest) { - Objects.requireNonNull(manifest); - - this.defaultManifest = manifest; - return this; - } - - /** - * Overrides the path filter for this jar, to exclude some entries from the underlying file system. - * - * @see UnionPathFilter - */ - public JarContentsBuilder pathFilter(@Nullable UnionPathFilter pathFilter) { - this.pathFilter = pathFilter; - return this; - } - - /** - * Builds the jar. - */ - public JarContents build() { - return new JarContentsImpl(paths, defaultManifest, pathFilter == null ? null : pathFilter::test); - } -} diff --git a/loader/src/main/java/cpw/mods/jarhandling/JarMetadata.java b/loader/src/main/java/cpw/mods/jarhandling/JarMetadata.java index 91ea5fad5..015485ee9 100644 --- a/loader/src/main/java/cpw/mods/jarhandling/JarMetadata.java +++ b/loader/src/main/java/cpw/mods/jarhandling/JarMetadata.java @@ -2,9 +2,17 @@ import cpw.mods.jarhandling.impl.ModuleJarMetadata; import cpw.mods.jarhandling.impl.SimpleJarMetadata; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; import java.lang.module.ModuleDescriptor; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.jar.Manifest; import java.util.regex.Pattern; import org.jetbrains.annotations.Nullable; @@ -53,24 +61,87 @@ default List providers() { * 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) { - var mi = jar.findFile("module-info.class"); - if (mi.isPresent()) { - return new ModuleJarMetadata(mi.get(), jar::getPackages); - } else { + static JarMetadata from(JarContents jar) throws IOException { + var packages = new HashSet(); + var serviceProviders = new ArrayList(); + indexJarContent(jar, packages, serviceProviders); + + try (var moduleInfoIn = jar.openFile("module-info.class")) { + if (moduleInfoIn != null) { + return new ModuleJarMetadata(moduleInfoIn, () -> packages); + } + var nav = computeNameAndVersion(jar.getPrimaryPath()); String name = nav.name(); String version = nav.version(); - String automaticModuleName = jar.getManifest().getMainAttributes().getValue("Automatic-Module-Name"); - if (automaticModuleName != null) { - name = automaticModuleName; + Manifest jarManifest = jar.getJarManifest(); + if (jarManifest != null) { + String automaticModuleName = jarManifest.getMainAttributes().getValue("Automatic-Module-Name"); + if (automaticModuleName != null) { + name = automaticModuleName; + } } - return new SimpleJarMetadata(name, version, jar::getPackages, jar.getMetaInfServices()); + return new SimpleJarMetadata(name, version, () -> packages, serviceProviders); } } + static void indexJarContent(JarContents jar, Set packages, List serviceProviders) { + jar.visitContent((relativePath, contentSupplier, attributesSupplier) -> { + if (relativePath.startsWith("META-INF/services/")) { + var serviceClass = relativePath.substring("META-INF/services/".length()); + if (serviceClass.contains("/")) { + return; // In some subdirectory under META-INF/services/, this is not a real service file + } + + var implementationClasses = new ArrayList(); + try (var reader = new BufferedReader(new InputStreamReader(contentSupplier.get()))) { + for (var line = reader.readLine(); line != null; line = reader.readLine()) { + var soc = line.indexOf('#'); + if (soc != -1) { + line = line.substring(0, soc); + } + line = line.trim(); + if (line.isEmpty()) { + continue; + } + // NOTE: This differs from previous iterations of SecureJar in that we do not filter the + // impl-class against the JarContents filters. + // Whoever builds the Jar is responsible for only making service manifests + // visible that are actually valid with the filter in place. + implementationClasses.add(line); + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to read service file " + relativePath + " from " + jar, e); + } + + serviceProviders.add(new SecureJar.Provider(serviceClass, implementationClasses)); + + } else if (relativePath.contains("/") && relativePath.endsWith(".class")) { + var segments = relativePath.split("/"); + + // the JDK checks whether each segment of a package name is a valid java identifier + // we perform this check on each directory name + // See jdk.internal.module.Checks.isJavaIdentifier + for (var segment : segments) { + if (!JlsConstants.isJavaIdentifier(segment)) { + return; // If any segment is not a valid java identifier, we skip the package name + } + } + + var packageName = new StringBuilder(); + for (int i = 0; i < segments.length - 1; i++) { + if (i != 0) { + packageName.append('.'); + } + packageName.append(segments[i]); + } + packages.add(packageName.toString()); + } + }); + } + private static NameAndVersion computeNameAndVersion(Path path) { // detect Maven-like paths Path versionMaybe = path.getParent(); diff --git a/loader/src/main/java/cpw/mods/jarhandling/JarModContainer.java b/loader/src/main/java/cpw/mods/jarhandling/JarModContainer.java new file mode 100644 index 000000000..d300ace60 --- /dev/null +++ b/loader/src/main/java/cpw/mods/jarhandling/JarModContainer.java @@ -0,0 +1,136 @@ +package cpw.mods.jarhandling; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.nio.file.Path; +import java.util.Optional; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class JarModContainer implements JarContents { + private static final Logger LOG = LoggerFactory.getLogger(JarModContainer.class); + + private final Path path; + private final JarFile jarFile; + private final Manifest jarManifest; + + JarModContainer(Path path) throws IOException { + this.path = path; + this.jarFile = new JarFile(path.toFile()); + this.jarManifest = this.jarFile.getManifest(); + } + + @Override + public Optional findFile(String relativePath) { + var entry = jarFile.getEntry(relativePath); + if (entry != null && !entry.isDirectory()) { + return Optional.of(URI.create("jar:" + path.toUri() + "!/" + relativePath)); + } + return Optional.empty(); + } + + @Override + public Path getPrimaryPath() { + return path; + } + + @Override + public String toString() { + return path.toString(); + } + + @Nullable + @Override + public Manifest getJarManifest() { + return jarManifest; + } + + @Override + public InputStream openFile(String relativePath) throws IOException { + var entry = jarFile.getEntry(relativePath); + if (entry != null && !entry.isDirectory()) { + return jarFile.getInputStream(entry); + } + return null; + } + + @Override + public byte[] readFile(String relativePath) throws IOException { + var entry = jarFile.getEntry(relativePath); + if (entry != null && !entry.isDirectory()) { + try (var input = jarFile.getInputStream(entry)) { + // TODO in theory we can at least use entry uncompressed size as a hint here + return input.readAllBytes(); + } + } + return null; + } + + @Override + public boolean hasContentRoot(Path path) { + return this.path.equals(path); + } + + @Override + public Stream getClasspathRoots() { + try { + return Stream.of(this.path.toUri().toURL()); + } catch (MalformedURLException e) { + LOG.error("Failed to convert path to URL: {}", path, e); + return Stream.of(); + } + } + + @Override + public void visitContent(ModContentVisitor visitor) { + var it = jarFile.entries().asIterator(); + while (it.hasNext()) { + var entry = it.next(); + if (entry.isDirectory()) { + continue; + } + var relativePath = PathNormalization.normalize(entry.getName()); + visitor.visit(relativePath, () -> jarFile.getInputStream(entry), () -> new ModContentAttributes( + entry.getLastModifiedTime(), entry.getSize())); + } + } + + @Nullable + public String getMainJarManifestAttribute(Attributes.Name name) { + var manifest = getJarManifest(); + if (manifest == null) { + return null; + } + return manifest.getMainAttributes().getValue(name); + } + + public Path path() { + return path; + } + + @Override + public void close() { + try { + jarFile.close(); + } catch (IOException e) { + throw new UncheckedIOException("Failed to close ZIP-File " + path, e); + } + } + + public Optional getCodeSigningFingerprint() { + return Optional.empty(); // TODO + } + + public Optional getTrustData() { + return Optional.empty(); // TODO + } +} diff --git a/loader/src/main/java/cpw/mods/jarhandling/JlsConstants.java b/loader/src/main/java/cpw/mods/jarhandling/JlsConstants.java new file mode 100644 index 000000000..b543a92c0 --- /dev/null +++ b/loader/src/main/java/cpw/mods/jarhandling/JlsConstants.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package cpw.mods.jarhandling; + +import java.util.Set; + +final class JlsConstants { + // https://docs.oracle.com/javase/specs/jls/se22/html/jls-3.html#jls-3.9 + static final Set RESERVED_KEYWORDS = Set.of( + "abstract", + "assert", + "boolean", + "break", + "byte", + "case", + "catch", + "char", + "class", + "const", + "continue", + "default", + "do", + "double", + "else", + "enum", + "extends", + "final", + "finally", + "float", + "for", + "goto", + "if", + "implements", + "import", + "instanceof", + "int", + "interface", + "long", + "native", + "new", + "package", + "private", + "protected", + "public", + "return", + "short", + "static", + "strictfp", + "super", + "switch", + "synchronized", + "this", + "throw", + "throws", + "transient", + "try", + "void", + "volatile", + "while", + // Not really keywords, but "boolean literals" + "true", + "false", + // Not really a keyword, but the "null literal" + "null", + "_"); + + // Same as jdk.internal.module.Checks.isClassName + // A string is a type name, if each of the segments delimited by '.' are valid identifiers + public static boolean isTypeName(String str) { + var lastIdx = 0; + for (var idx = str.indexOf('.'); idx != -1; idx = str.indexOf('.', lastIdx)) { + if (!isJavaIdentifier(str.substring(lastIdx, idx))) { + return false; + } + lastIdx = idx + 1; + } + return isJavaIdentifier(str.substring(lastIdx)); + } + + // Same as jdk.internal.module.Checks.isJavaIdentifier + public static boolean isJavaIdentifier(String str) { + if (str.isEmpty() || RESERVED_KEYWORDS.contains(str)) { + return false; + } + + // This iterates over the Unicode code points instead of UTF-16 characters + for (var i = 0; i < str.length();) { + int codePoint = Character.codePointAt(str, i); + + if (i == 0 && !Character.isJavaIdentifierStart(codePoint) + || !Character.isJavaIdentifierPart(codePoint)) { + return false; + } + + i += Character.charCount(codePoint); + } + + return true; + } + + private JlsConstants() {} + + public static String packageName(String line) { + var idx = line.lastIndexOf('.'); + if (idx == -1) { + return ""; + } else { + return line.substring(0, idx); + } + } +} diff --git a/loader/src/main/java/cpw/mods/jarhandling/ModContentAttributes.java b/loader/src/main/java/cpw/mods/jarhandling/ModContentAttributes.java new file mode 100644 index 000000000..1523f8d4b --- /dev/null +++ b/loader/src/main/java/cpw/mods/jarhandling/ModContentAttributes.java @@ -0,0 +1,5 @@ +package cpw.mods.jarhandling; + +import java.nio.file.attribute.FileTime; + +public record ModContentAttributes(FileTime lastModified, long size) {} diff --git a/loader/src/main/java/cpw/mods/jarhandling/ModContentVisitor.java b/loader/src/main/java/cpw/mods/jarhandling/ModContentVisitor.java new file mode 100644 index 000000000..6bf3bd066 --- /dev/null +++ b/loader/src/main/java/cpw/mods/jarhandling/ModContentVisitor.java @@ -0,0 +1,10 @@ +package cpw.mods.jarhandling; + +import java.io.InputStream; + +@FunctionalInterface +public interface ModContentVisitor { + void visit(String relativePath, + IOSupplier contentSupplier, + IOSupplier attributesSupplier); +} diff --git a/loader/src/main/java/cpw/mods/jarhandling/PathNormalization.java b/loader/src/main/java/cpw/mods/jarhandling/PathNormalization.java new file mode 100644 index 000000000..7cd8ea045 --- /dev/null +++ b/loader/src/main/java/cpw/mods/jarhandling/PathNormalization.java @@ -0,0 +1,78 @@ +package cpw.mods.jarhandling; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +final class PathNormalization { + private static final char SEPARATOR = '/'; + + private PathNormalization() {} + + public static boolean isNormalized(CharSequence path) { + if (path.isEmpty()) { + return true; // This will fail for other reasons + } + + // Normalized paths use forward slashes + for (int i = 0; i < path.length(); i++) { + if (path.charAt(i) == '\\') { + return false; + } + } + + char prevCh = '\0'; + for (int i = 0; i < path.length(); i++) { + char ch = path.charAt(i); + if ((i == 0 || i == path.length() - 1) && ch == SEPARATOR) { + return false; // No leading or trailing separators + } + if (ch == SEPARATOR && prevCh == SEPARATOR) { + return false; // No repeated separators + } + // TODO We do not support ./ or ../ + prevCh = ch; + } + + return true; + } + + public static String normalize(CharSequence path) { + var result = new StringBuilder(path.length()); + + int startOfSegment = 0; + for (int i = 0; i < path.length(); i++) { + char ch = path.charAt(i); + if (ch == '\\') { + ch = SEPARATOR; + } + if (ch == SEPARATOR) { + if (i > startOfSegment) { + if (!result.isEmpty()) { + result.append(SEPARATOR); + } + + var segment = path.subSequence(startOfSegment, i); + validateSegment(segment); + result.append(segment); + } + startOfSegment = i + 1; + } + } + if (startOfSegment < path.length()) { + if (!result.isEmpty()) { + result.append(SEPARATOR); + } + var segment = path.subSequence(startOfSegment, path.length()); + validateSegment(segment); + result.append(segment); + } + + return result.toString(); + } + + private static void validateSegment(CharSequence segment) { + if (segment.equals(".") || segment.equals("..")) { + throw new IllegalArgumentException("./ or ../ segments in paths are not supported"); + } + } +} diff --git a/loader/src/main/java/cpw/mods/jarhandling/SecureJar.java b/loader/src/main/java/cpw/mods/jarhandling/SecureJar.java index cf12efabe..15c707fc2 100644 --- a/loader/src/main/java/cpw/mods/jarhandling/SecureJar.java +++ b/loader/src/main/java/cpw/mods/jarhandling/SecureJar.java @@ -1,7 +1,5 @@ package cpw.mods.jarhandling; -import cpw.mods.jarhandling.impl.Jar; -import cpw.mods.jarhandling.impl.JarContentsImpl; import cpw.mods.niofs.union.UnionPathFilter; import java.io.IOException; import java.io.InputStream; @@ -12,10 +10,8 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; -import java.security.CodeSigner; import java.util.List; import java.util.Optional; -import java.util.jar.Attributes; import java.util.jar.Manifest; import org.jetbrains.annotations.Nullable; @@ -24,28 +20,6 @@ * including all its paths and code signing metadata. */ public interface SecureJar { - /** - * Creates a jar from a list of paths. - * See {@link JarContentsBuilder} for more configuration options. - */ - static SecureJar from(final Path... paths) { - return from(new JarContentsBuilder().paths(paths).build()); - } - - /** - * 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(); /** @@ -57,20 +31,7 @@ static SecureJar from(JarContents contents, JarMetadata metadata) { */ 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(); + JarContents container(); String name(); @@ -121,12 +82,6 @@ interface ModuleDataProvider { * {@return the manifest of the jar} */ Manifest getManifest(); - - /** - * {@return the signers if the class name can be verified, or {@code null} otherwise} - */ - @Nullable - CodeSigner[] verifyAndGetSigners(String cname, byte[] bytes); } /** diff --git a/loader/src/main/java/cpw/mods/jarhandling/VirtualJar.java b/loader/src/main/java/cpw/mods/jarhandling/VirtualJar.java index 963564b05..899951a2b 100644 --- a/loader/src/main/java/cpw/mods/jarhandling/VirtualJar.java +++ b/loader/src/main/java/cpw/mods/jarhandling/VirtualJar.java @@ -1,18 +1,13 @@ package cpw.mods.jarhandling; -import cpw.mods.niofs.union.UnionFileSystem; -import cpw.mods.niofs.union.UnionFileSystemProvider; import java.io.IOException; import java.io.InputStream; import java.lang.module.ModuleDescriptor; import java.net.URI; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.spi.FileSystemProvider; -import java.security.CodeSigner; +import java.nio.file.Paths; import java.util.Optional; import java.util.Set; -import java.util.jar.Attributes; import java.util.jar.Manifest; import org.jetbrains.annotations.Nullable; @@ -27,33 +22,17 @@ public final class VirtualJar implements SecureJar { /** * Creates a new virtual jar. * - * @param name the name of the virtual jar; will be used as the module name - * @param referencePath a path to an existing directory or jar file, for debugging and display purposes - * (for example a path to the real jar of the caller) - * @param packages the list of packages in this virtual jar + * @param name the name of the virtual jar; will be used as the module name + * @param packages the list of packages in this virtual jar */ - public VirtualJar(String name, Path referencePath, String... packages) { - if (!Files.exists(referencePath)) { - throw new IllegalArgumentException("VirtualJar reference path " + referencePath + " must exist"); - } - + public VirtualJar(String name, String... packages) { this.moduleDescriptor = ModuleDescriptor.newAutomaticModule(name) .packages(Set.of(packages)) .build(); - // Create a dummy file system from the reference path, with a filter that always returns false - this.dummyFileSystem = UFSP.newFileSystem((path, basePath) -> false, referencePath); } - // Implementation details below - 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 ModuleDescriptor moduleDescriptor; private final ModuleDataProvider moduleData = new VirtualJarModuleDataProvider(); - private final UnionFileSystem dummyFileSystem; private final Manifest manifest = new Manifest(); @Override @@ -63,32 +42,12 @@ public ModuleDataProvider moduleDataProvider() { @Override public Path getPrimaryPath() { - return dummyFileSystem.getPrimaryPath(); - } - - @Override - public @Nullable CodeSigner[] getManifestSigners() { - return null; + return Paths.get(name()); // TODO } @Override - public Status verifyPath(Path path) { - return Status.NONE; - } - - @Override - public Status getFileStatus(String name) { - return Status.NONE; - } - - @Override - public @Nullable Attributes getTrustedManifestEntries(String name) { - return null; - } - - @Override - public boolean hasSecurityData() { - return false; + public JarContents container() { + return JarContents.empty(Paths.get("")); } @Override @@ -98,18 +57,16 @@ public String name() { @Override public Path getPath(String first, String... rest) { - return dummyFileSystem.getPath(first, rest); + return Paths.get(first, rest); } @Override public Path getRootPath() { - return dummyFileSystem.getRoot(); + throw new RuntimeException(); // TODO } @Override - public void close() throws IOException { - dummyFileSystem.close(); - } + public void close() throws IOException {} private class VirtualJarModuleDataProvider implements ModuleDataProvider { @Override @@ -142,11 +99,5 @@ public Optional open(String name) { public Manifest getManifest() { return manifest; } - - @Override - @Nullable - public CodeSigner[] verifyAndGetSigners(String cname, byte[] bytes) { - return null; - } } } diff --git a/loader/src/main/java/cpw/mods/jarhandling/impl/Jar.java b/loader/src/main/java/cpw/mods/jarhandling/impl/Jar.java index aed4e4f35..eb6c66c58 100644 --- a/loader/src/main/java/cpw/mods/jarhandling/impl/Jar.java +++ b/loader/src/main/java/cpw/mods/jarhandling/impl/Jar.java @@ -1,5 +1,6 @@ package cpw.mods.jarhandling.impl; +import cpw.mods.jarhandling.JarContents; import cpw.mods.jarhandling.JarMetadata; import cpw.mods.jarhandling.SecureJar; import cpw.mods.niofs.union.UnionFileSystem; @@ -11,81 +12,69 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.security.CodeSigner; import java.util.Optional; -import java.util.jar.Attributes; import java.util.jar.Manifest; import org.jetbrains.annotations.Nullable; public class Jar implements SecureJar { - private final JarContentsImpl contents; + private final JarContents container; private final Manifest manifest; - private final JarSigningData signingData; - private final UnionFileSystem filesystem; - private final JarModuleDataProvider moduleDataProvider; - private final JarMetadata metadata; - public Jar(JarContentsImpl contents, JarMetadata metadata) { - this.contents = contents; - this.manifest = contents.getManifest(); - this.signingData = contents.signingData; - this.filesystem = contents.filesystem; + @Nullable + private UnionFileSystem filesystem; - this.moduleDataProvider = new JarModuleDataProvider(this); - this.metadata = metadata; + public static Jar of(Path path) throws IOException { + return of(JarContents.ofPath(path)); } - public URI getURI() { - return this.filesystem.getRootDirectories().iterator().next().toUri(); + public static Jar of(JarContents container) throws IOException { + return of(container, JarMetadata.from(container)); } - public ModuleDescriptor computeDescriptor() { - return metadata.descriptor(); + public static Jar of(JarContents container, JarMetadata metadata) { + return new Jar(container, metadata); } - @Override - public ModuleDataProvider moduleDataProvider() { - return moduleDataProvider; - } + private Jar(JarContents container, JarMetadata metadata) { + this.container = container; + this.manifest = container.getJarManifest(); - @Override - public Path getPrimaryPath() { - return filesystem.getPrimaryPath(); + this.moduleDataProvider = new JarModuleDataProvider(this); + this.metadata = metadata; } - public Optional findFile(final String name) { - return contents.findFile(name); + @Override + public JarContents container() { + return container; } - @Override @Nullable - public CodeSigner[] getManifestSigners() { - return signingData.getManifestSigners(); + public URI getURI() { + var primaryPath = container.getPrimaryPath(); + if (primaryPath != null) { + return primaryPath.toUri(); + } + return null; } - @Override - public Status verifyPath(final Path path) { - if (path.getFileSystem() != filesystem) throw new IllegalArgumentException("Wrong filesystem"); - final var pathname = path.toString(); - return signingData.verifyPath(manifest, path, pathname); + public ModuleDescriptor computeDescriptor() { + return metadata.descriptor(); } @Override - public Status getFileStatus(final String name) { - return signingData.getFileStatus(name); + public ModuleDataProvider moduleDataProvider() { + return moduleDataProvider; } @Override - @Nullable - public Attributes getTrustedManifestEntries(final String name) { - return signingData.getTrustedManifestEntries(manifest, name); + public Path getPrimaryPath() { + return container.getPrimaryPath(); } - @Override - public boolean hasSecurityData() { - return signingData.hasSecurityData(); + public Optional findFile(final String name) { + return container.findFile(name); } @Override @@ -105,7 +94,7 @@ public Path getRootPath() { @Override public void close() throws IOException { - contents.close(); + container.close(); } @Override @@ -143,11 +132,5 @@ public Optional open(final String name) { public Manifest getManifest() { return jar.manifest; } - - @Override - @Nullable - public CodeSigner[] verifyAndGetSigners(final String cname, final byte[] bytes) { - return jar.signingData.verifyAndGetSigners(jar.manifest, cname, bytes); - } } } diff --git a/loader/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java b/loader/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java index 64ac0d899..1e1928dda 100644 --- a/loader/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java +++ b/loader/src/main/java/cpw/mods/jarhandling/impl/JarContentsImpl.java @@ -1,6 +1,5 @@ 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; @@ -27,7 +26,7 @@ import java.util.jar.Manifest; import org.jetbrains.annotations.Nullable; -public class JarContentsImpl implements JarContents { +public class JarContentsImpl { private static final UnionFileSystemProvider UFSP = (UnionFileSystemProvider) FileSystemProvider.installedProviders() .stream() .filter(fsp -> fsp.getScheme().equals("union")) @@ -36,8 +35,6 @@ public class JarContentsImpl implements JarContents { private static final Set NAUGHTY_SERVICE_FILES = Set.of("org.codehaus.groovy.runtime.ExtensionModule"); 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 @@ -54,12 +51,12 @@ public JarContentsImpl(Path[] paths, Supplier defaultManifest, @Nullab 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); + this.manifest = readManifest(defaultManifest, validPaths); // Read multi-release jar information this.nameOverrides = readMultiReleaseInfo(); } - private Manifest readManifestAndSigningData(Supplier defaultManifest, Path[] validPaths) { + private Manifest readManifest(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]; @@ -73,9 +70,6 @@ private Manifest readManifestAndSigningData(Supplier defaultManifest, } } 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()); } @@ -136,12 +130,10 @@ private Map readMultiReleaseInfo() { } } - @Override public Path getPrimaryPath() { return filesystem.getPrimaryPath(); } - @Override public Optional findFile(String name) { var rel = filesystem.getPath(name); if (this.nameOverrides.containsKey(rel)) { @@ -150,12 +142,10 @@ public Optional findFile(String name) { return Optional.of(this.filesystem.getRoot().resolve(rel)).filter(Files::exists).map(Path::toUri); } - @Override public Manifest getManifest() { return manifest; } - @Override public Set getPackagesExcluding(String... excludedRootPackages) { Set ignoredRootPackages = new HashSet<>(excludedRootPackages.length + 1); ignoredRootPackages.add("META-INF"); // Always ignore META-INF @@ -189,7 +179,6 @@ public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) { } } - @Override public Set getPackages() { if (this.packages == null) { this.packages = getPackagesExcluding(); @@ -197,7 +186,6 @@ public Set getPackages() { return this.packages; } - @Override public List getMetaInfServices() { if (this.providers == null) { final var services = this.filesystem.getRoot().resolve("META-INF/services/"); @@ -217,7 +205,6 @@ public List getMetaInfServices() { return this.providers; } - @Override public void close() throws IOException { filesystem.close(); } diff --git a/loader/src/main/java/cpw/mods/jarhandling/impl/JarSigningData.java b/loader/src/main/java/cpw/mods/jarhandling/impl/JarSigningData.java deleted file mode 100644 index 4130d4d7e..000000000 --- a/loader/src/main/java/cpw/mods/jarhandling/impl/JarSigningData.java +++ /dev/null @@ -1,111 +0,0 @@ -package cpw.mods.jarhandling.impl; - -import cpw.mods.jarhandling.SecureJar; -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; -import org.jetbrains.annotations.Nullable; - -/** - * 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)); - } - - @Nullable - 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/loader/src/main/java/cpw/mods/jarhandling/impl/ManifestVerifier.java b/loader/src/main/java/cpw/mods/jarhandling/impl/ManifestVerifier.java deleted file mode 100644 index 61ea6e14d..000000000 --- a/loader/src/main/java/cpw/mods/jarhandling/impl/ManifestVerifier.java +++ /dev/null @@ -1,92 +0,0 @@ -package cpw.mods.jarhandling.impl; - -import java.security.CodeSigner; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Base64; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.jar.Attributes; -import java.util.jar.Manifest; - -class ManifestVerifier { - private static final boolean DEBUG = Boolean.parseBoolean(System.getProperty("securejarhandler.debugVerifier", "false")); - - private static final Base64.Decoder BASE64D = Base64.getDecoder(); - private final Map HASHERS = new HashMap<>(); - - private MessageDigest getHasher(String name) { - return HASHERS.computeIfAbsent(name.toLowerCase(Locale.ENGLISH), k -> { - try { - return MessageDigest.getInstance(k); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - }); - } - - private void log(String line) { - System.out.println(line); - } - - /** - * This is Dumb API, but it's a package private class so la-de-da! - * return: - * null - Something went wrong, digests were not verified. - * Optional.empty() - No signatures to verify, missing *-Digest entry in manifest, or nobody signed that particular entry - * Optional.isPresent() - code signers! - */ - Optional verify(final Manifest manifest, final Map pending, - final Map verified, final String name, final byte[] data) { - if (DEBUG) - log("[SJH] Verifying: " + name); - Attributes attr = manifest.getAttributes(name); - if (attr == null) { - if (DEBUG) - log("[SJH] No Manifest Entry"); - return Optional.empty(); - } - - record Expected(MessageDigest hash, byte[] value) {} - ; - var expected = new ArrayList(); - attr.forEach((k, v) -> { - var key = k.toString(); - if (key.toLowerCase(Locale.ENGLISH).endsWith("-digest")) { - var algo = key.substring(0, key.length() - 7); - var hash = BASE64D.decode((String) v); - expected.add(new Expected(getHasher(algo), hash)); - } - }); - if (expected.isEmpty()) { - if (DEBUG) - log("[SJH] No Manifest Hashes"); - return Optional.empty(); - } - - for (var exp : expected) { - synchronized (exp.hash()) { - exp.hash().reset(); - byte[] actual = exp.hash().digest(data); - if (DEBUG) { - log("[SJH] " + exp.hash().getAlgorithm() + " Expected: " + SecureJarVerifier.toHexString(exp.value())); - log("[SJH] " + exp.hash().getAlgorithm() + " Actual: " + SecureJarVerifier.toHexString(actual)); - } - if (!Arrays.equals(exp.value(), actual)) { - if (DEBUG) - log("[SJH] Failed: Invalid hashes"); - return null; - } - } - } - - var signers = pending.remove(name); - if (signers != null) - verified.put(name, signers); - return Optional.ofNullable(signers); - } -} diff --git a/loader/src/main/java/cpw/mods/jarhandling/impl/ModuleJarMetadata.java b/loader/src/main/java/cpw/mods/jarhandling/impl/ModuleJarMetadata.java index 70605d052..9ee70b4db 100644 --- a/loader/src/main/java/cpw/mods/jarhandling/impl/ModuleJarMetadata.java +++ b/loader/src/main/java/cpw/mods/jarhandling/impl/ModuleJarMetadata.java @@ -3,11 +3,8 @@ import cpw.mods.jarhandling.JarMetadata; import cpw.mods.jarhandling.LazyJarMetadata; import java.io.IOException; -import java.io.UncheckedIOException; +import java.io.InputStream; import java.lang.module.ModuleDescriptor; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -26,15 +23,11 @@ public class ModuleJarMetadata extends LazyJarMetadata { private final ModuleClassVisitor mcv; private final Supplier> packagesSupplier; - public ModuleJarMetadata(URI uri, Supplier> packagesSupplier) { - try (var is = Files.newInputStream(Path.of(uri))) { - ClassReader cr = new ClassReader(is); - var mcv = new ModuleClassVisitor(); - cr.accept(mcv, ClassReader.SKIP_CODE); - this.mcv = mcv; - } catch (IOException e) { - throw new UncheckedIOException(e); - } + public ModuleJarMetadata(InputStream in, Supplier> packagesSupplier) throws IOException { + ClassReader cr = new ClassReader(in); + var mcv = new ModuleClassVisitor(); + cr.accept(mcv, ClassReader.SKIP_CODE); + this.mcv = mcv; // Defer package scanning until computeDescriptor() this.packagesSupplier = packagesSupplier; diff --git a/loader/src/main/java/cpw/mods/jarhandling/impl/SecureJarVerifier.java b/loader/src/main/java/cpw/mods/jarhandling/impl/SecureJarVerifier.java deleted file mode 100644 index b9be1d1cb..000000000 --- a/loader/src/main/java/cpw/mods/jarhandling/impl/SecureJarVerifier.java +++ /dev/null @@ -1,223 +0,0 @@ -package cpw.mods.jarhandling.impl; - -import java.lang.reflect.Field; -import java.security.CodeSigner; -import java.util.Locale; -import java.util.Map; -import java.util.jar.JarInputStream; -import sun.misc.Unsafe; - -/** - * 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(); - - private static final char[] LOOKUP = "0123456789abcdef".toCharArray(); - - public static String toHexString(final byte[] bytes) { - final var buffer = new StringBuffer(2 * bytes.length); - for (int i = 0, bytesLength = bytes.length; i < bytesLength; i++) { - final int aByte = bytes[i] & 0xff; - buffer.append(LOOKUP[(aByte & 0xf0) >> 4]); - buffer.append(LOOKUP[aByte & 0xf]); - } - return buffer.toString(); - } - - //https://docs.oracle.com/en/java/javase/16/docs/specs/jar/jar.html#signed-jar-file - public static boolean isSigningRelated(String path) { - String filename = path.toLowerCase(Locale.ENGLISH); - if (!filename.startsWith("meta-inf/")) // Must be in META-INF directory - return false; - filename = filename.substring(9); - if (filename.indexOf('/') != -1) // Can't be a sub-directory - return false; - if ("manifest.mf".equals(filename) || // Main manifest, which has the file hashes - filename.endsWith(".sf") || // Signature file, which has hashes of the entries in the manifest file - filename.endsWith(".dsa") || // PKCS7 signature, DSA - filename.endsWith(".rsa")) // PKCS7 signature, SHA-256 + RSA - return true; - - if (!filename.startsWith("sig-")) // Unspecifed signature format - return false; - - int ext = filename.lastIndexOf('.'); - if (ext == -1) // No extension, aparently is ok - return true; - if (ext < filename.length() - 4) // Only 1-3 character {-4 because we're at the . char} - return false; - for (int x = ext + 1; x < filename.length(); x++) { - char c = filename.charAt(x); - if ((c < 'a' || c > 'z') && (c < '0' || c > '9')) // Must be alphanumeric - return false; - } - return true; - } - - public static Object getJarVerifier(Object inst) { - return ACCESSOR.getJarVerifier(inst); - } - - public static boolean isParsingMeta(Object inst) { - return ACCESSOR.isParsingMeta(inst); - } - - public static boolean hasSignatures(Object inst) { - return ACCESSOR.hasSignatures(inst); - } - - public static Map getVerifiedSigners(Object inst) { - return ACCESSOR.getVerifiedSigners(inst); - } - - public static Map getPendingSigners(Object inst) { - return ACCESSOR.getPendingSigners(inst); - } - - private interface IAccessor { - Object getJarVerifier(Object inst); - - boolean isParsingMeta(Object inst); - - boolean hasSignatures(Object inst); - - Map getVerifiedSigners(Object inst); - - Map getPendingSigners(Object inst); - } - - private static class Reflection implements IAccessor { - private static final Field jarVerifier; - private static final Field parsingMeta; - private static final Field verifiedSigners; - private static final Field sigFileSigners; - private static final Field anyToVerify; - - static { - final var moduleLayer = ModuleLayer.boot(); - final var myModule = moduleLayer.findModule("fml_loader"); - if (myModule.isPresent()) { - final var gj9h = myModule.get(); - moduleLayer - .findModule("java.base") - .filter(m -> m.isOpen("java.util.jar", gj9h) && m.isExported("sun.security.util", gj9h)) - .orElseThrow(() -> new IllegalStateException(""" - Missing JVM arguments. Please correct your runtime profile and run again. - --add-opens java.base/java.util.jar=fml_loader - --add-exports java.base/sun.security.util=fml_loader""")); - } else if (Boolean.parseBoolean(System.getProperty("securejarhandler.throwOnMissingModule", "true"))) { - // Hack for JMH benchmark: in JMH, SecureJarHandler does not load as a module, but we add-open to all unnamed in the jvm args - throw new RuntimeException("Failed to find securejarhandler module!"); - } - try { - jarVerifier = JarInputStream.class.getDeclaredField("jv"); - sigFileSigners = jarVerifier.getType().getDeclaredField("sigFileSigners"); - verifiedSigners = jarVerifier.getType().getDeclaredField("verifiedSigners"); - parsingMeta = jarVerifier.getType().getDeclaredField("parsingMeta"); - anyToVerify = jarVerifier.getType().getDeclaredField("anyToVerify"); - jarVerifier.setAccessible(true); - sigFileSigners.setAccessible(true); - verifiedSigners.setAccessible(true); - parsingMeta.setAccessible(true); - anyToVerify.setAccessible(true); - } catch (NoSuchFieldException e) { - throw new IllegalStateException("Missing essential fields", e); - } - } - - @Override - public Object getJarVerifier(Object inst) { - return getField(jarVerifier, inst); - } - - @Override - public boolean isParsingMeta(Object inst) { - return (Boolean) getField(parsingMeta, inst); - } - - @Override - public boolean hasSignatures(Object inst) { - return (Boolean) getField(anyToVerify, inst); - } - - @SuppressWarnings("unchecked") - @Override - public Map getVerifiedSigners(Object inst) { - return (Map) getField(verifiedSigners, inst); - } - - @SuppressWarnings("unchecked") - @Override - public Map getPendingSigners(Object inst) { - return (Map) getField(verifiedSigners, inst); - } - - private static Object getField(Field f, Object inst) { - try { - return f.get(inst); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - } - - private static class UnsafeAccessor implements IAccessor { - private static final Unsafe UNSAFE; - private static final Class JV_TYPE; - static { - try { - var f = Unsafe.class.getDeclaredField("theUnsafe"); - f.setAccessible(true); - UNSAFE = (Unsafe) f.get(null); - JV_TYPE = JarInputStream.class.getDeclaredField("jv").getType(); - } catch (Exception e) { - throw new RuntimeException("Unable to get Unsafe reference, this should never be possible," + - " be sure to report this will exact details on what JVM you're running.", e); - } - } - - private static final long jarVerifier = getOffset(JarInputStream.class, "jv"); - private static final long sigFileSigners = getOffset(JV_TYPE, "sigFileSigners"); - private static final long verifiedSigners = getOffset(JV_TYPE, "verifiedSigners"); - private static final long parsingMeta = getOffset(JV_TYPE, "parsingMeta"); - private static final long anyToVerify = getOffset(JV_TYPE, "anyToVerify"); - - private static long getOffset(Class clz, String name) { - try { - return UNSAFE.objectFieldOffset(clz.getDeclaredField(name)); - } catch (Exception e) { - throw new RuntimeException("Unable to get index for " + clz.getName() + "." + name + ", " + - " be sure to report this will exact details on what JVM you're running.", e); - } - } - - @Override - public Object getJarVerifier(Object inst) { - return UNSAFE.getObject(inst, jarVerifier); - } - - @Override - public boolean isParsingMeta(Object inst) { - return UNSAFE.getBoolean(inst, parsingMeta); - } - - @Override - public boolean hasSignatures(Object inst) { - return UNSAFE.getBoolean(inst, anyToVerify); - } - - @SuppressWarnings("unchecked") - @Override - public Map getVerifiedSigners(Object inst) { - return (Map) UNSAFE.getObject(inst, verifiedSigners); - } - - @SuppressWarnings("unchecked") - @Override - public Map getPendingSigners(Object inst) { - return (Map) UNSAFE.getObject(inst, sigFileSigners); - } - } -} diff --git a/loader/src/main/java/net/neoforged/fml/ModList.java b/loader/src/main/java/net/neoforged/fml/ModList.java index 92c834996..d3ddf4c57 100644 --- a/loader/src/main/java/net/neoforged/fml/ModList.java +++ b/loader/src/main/java/net/neoforged/fml/ModList.java @@ -66,11 +66,10 @@ private ModList(final List modFiles, final List sortedList) { private String fileToLine(IModFile mf) { var mainMod = mf.getModInfos().getFirst(); - return String.format(Locale.ENGLISH, "%-50.50s|%-30.30s|%-30.30s|%-20.20s|Manifest: %s", mf.getFileName(), + return String.format(Locale.ENGLISH, "%-50.50s|%-30.30s|%-30.30s|%-20.20s", mf.getFileName(), mainMod.getDisplayName(), mainMod.getModId(), - mainMod.getVersion(), - ((ModFileInfo) mf.getModFileInfo()).getCodeSigningFingerprint().orElse("NOSIGNATURE")); + mainMod.getVersion()); } private String crashReport() { diff --git a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java index 38a230827..05647f15c 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java +++ b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java @@ -8,6 +8,7 @@ import com.mojang.logging.LogUtils; import cpw.mods.cl.JarModuleFinder; import cpw.mods.jarhandling.SecureJar; +import cpw.mods.jarhandling.VirtualJar; import cpw.mods.modlauncher.ClassTransformer; import cpw.mods.modlauncher.LaunchPluginHandler; import cpw.mods.modlauncher.Launcher; @@ -121,7 +122,8 @@ public final class FMLLoader implements AutoCloseable { private AccessTransformerEngine accessTransformer; private LanguageProviderLoader languageProviderLoader; private LoadingModList loadingModList; - public Runnable progressWindowTick; + // NOTE: NeoForge patches reference this field directly, sadly. + public static final Runnable progressWindowTick = ImmediateWindowHandler::renderTick; public BackgroundScanHandler backgroundScanHandler; @VisibleForTesting DiscoveryResult discoveryResult; @@ -154,6 +156,10 @@ private FMLLoader(ClassLoader currentClassLoader, String[] programArgs, Dist dis makeCurrent(); } + public Path getCacheDir() { + return cacheDir; + } + private static Dist detectDist(ClassLoader classLoader) { var clientAvailable = classLoader.getResource("net/minecraft/client/main/Main.class") != null; return clientAvailable ? Dist.CLIENT : Dist.DEDICATED_SERVER; @@ -246,7 +252,6 @@ public static FMLLoader create(@Nullable Instrumentation instrumentation, Startu loader.loadEarlyServices(); ImmediateWindowHandler.load(startupArgs.headless(), loader.programArgs); - loader.progressWindowTick = ImmediateWindowHandler::renderTick; var mixinFacade = new MixinFacade(); @@ -441,6 +446,10 @@ private String formatModFileLocation(IModFile file) { } private static List getBasePaths(SecureJar jar, boolean ignoreFilter) { + if (jar instanceof VirtualJar) { + return List.of(); // virtual jars have no real paths + } + var unionFs = (UnionFileSystem) jar.getRootPath().getFileSystem(); if (!ignoreFilter && unionFs.getFilesystemFilter() != null) { throw new IllegalStateException("Filtering for plugin jars is not supported: " + jar); diff --git a/loader/src/main/java/net/neoforged/fml/loading/ImmediateWindowHandler.java b/loader/src/main/java/net/neoforged/fml/loading/ImmediateWindowHandler.java index b9434db11..16dfb9610 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/ImmediateWindowHandler.java +++ b/loader/src/main/java/net/neoforged/fml/loading/ImmediateWindowHandler.java @@ -93,7 +93,9 @@ public static void acceptGameLayer(final ModuleLayer layer) { } public static void renderTick() { - provider.periodicTick(); + if (provider != null) { + provider.periodicTick(); + } } public static String getGLVersion() { diff --git a/loader/src/main/java/net/neoforged/fml/loading/mixin/MixinFacade.java b/loader/src/main/java/net/neoforged/fml/loading/mixin/MixinFacade.java index 6a18b90c6..94b1a61e2 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/mixin/MixinFacade.java +++ b/loader/src/main/java/net/neoforged/fml/loading/mixin/MixinFacade.java @@ -8,8 +8,6 @@ import cpw.mods.jarhandling.SecureJar; import cpw.mods.jarhandling.VirtualJar; import cpw.mods.modlauncher.TransformingClassLoader; -import java.net.URISyntaxException; -import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -116,13 +114,6 @@ private void decorateMixinConfigsWithModIds(List modFiles) { } public SecureJar createGeneratedCodeContainer() { - Path codeSource; - try { - codeSource = Path.of(Mixins.class.getProtectionDomain().getCodeSource().getLocation().toURI()); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - - return new VirtualJar("mixin_synthetic", codeSource, ArgsClassGenerator.SYNTHETIC_PACKAGE); + return new VirtualJar("mixin_synthetic", ArgsClassGenerator.SYNTHETIC_PACKAGE); } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModDiscoverer.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModDiscoverer.java index a0715997f..104376a4b 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModDiscoverer.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModDiscoverer.java @@ -7,7 +7,8 @@ import com.mojang.logging.LogUtils; import cpw.mods.jarhandling.JarContents; -import cpw.mods.jarhandling.SecureJar; +import cpw.mods.jarhandling.impl.Jar; +import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; @@ -187,12 +188,19 @@ public void addLibrary(Path path) { return; } - var modFile = new ModFile( - SecureJar.from(path), - JarModsDotTomlModFileReader::manifestParser, - IModFile.Type.LIBRARY, - defaultAttributes); - addModFile(modFile); + // TODO: Libraries no longer need to be ModFile wrapped + + try { + var modFile = new ModFile( + Jar.of(path), + JarModsDotTomlModFileReader::manifestParser, + IModFile.Type.LIBRARY, + defaultAttributes); + addModFile(modFile); + } catch (IOException e) { + // TODO KEY + addIssue(ModLoadingIssue.error("fml.modloadingissue.brokenfile.unknown").withAffectedPath(path)); + } } @Override @@ -207,7 +215,7 @@ public Optional addPath(List groupedPaths, ModFileDiscoveryAttri JarContents jarContents; try { - jarContents = JarContents.of(groupedPaths); + jarContents = JarContents.ofPaths(groupedPaths); } catch (Exception e) { if (causeChainContains(e, ZipException.class)) { addIssue(ModLoadingIssue.error("fml.modloadingissue.brokenfile.invalidzip").withAffectedPath(primaryPath).withCause(e)); diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFile.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFile.java index 90cabdc4e..cdaa79634 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFile.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFile.java @@ -7,6 +7,7 @@ import com.google.common.collect.ImmutableMap; import com.mojang.logging.LogUtils; +import cpw.mods.jarhandling.JarContents; import cpw.mods.jarhandling.SecureJar; import java.io.IOException; import java.io.UncheckedIOException; @@ -19,7 +20,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.Consumer; -import java.util.function.Function; import java.util.function.Supplier; import java.util.jar.Attributes; import java.util.jar.Manifest; @@ -99,6 +99,11 @@ public SecureJar getSecureJar() { return this.jar; } + @Override + public JarContents getContent() { + return jar.container(); + } + @Override public List getModInfos() { return modFileInfo.getMods(); @@ -151,10 +156,9 @@ public ModFileScanData compileContent() { } public void scanFile(Consumer pathConsumer) { - final Function status = p -> getSecureJar().verifyPath(p); var rootPath = getSecureJar().getRootPath(); try (Stream files = Files.find(rootPath, Integer.MAX_VALUE, (p, a) -> p.getNameCount() > 0 && p.getFileName().toString().endsWith(".class"))) { - setSecurityStatus(files.peek(pathConsumer).map(status).reduce((s1, s2) -> SecureJar.Status.values()[Math.min(s1.ordinal(), s2.ordinal())]).orElse(SecureJar.Status.INVALID)); + files.forEach(pathConsumer); } catch (IOException e) { throw new UncheckedIOException("Failed to scan " + rootPath, e); } @@ -233,10 +237,6 @@ public IModFileInfo getModFileInfo() { return modFileInfo; } - @Deprecated(forRemoval = true) - @Override - public void setSecurityStatus(final SecureJar.Status status) {} - public ArtifactVersion getJarVersion() { return new DefaultArtifactVersion(this.jarVersion); } diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFileInfo.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFileInfo.java index 68e1cc160..a034803c3 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFileInfo.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModFileInfo.java @@ -6,19 +6,8 @@ package net.neoforged.fml.loading.moddiscovery; import com.mojang.logging.LogUtils; -import cpw.mods.modlauncher.api.LambdaExceptionUtils; import java.net.URL; -import java.security.CodeSigner; -import java.security.InvalidKeyException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.SignatureException; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -26,8 +15,6 @@ import java.util.Optional; import java.util.function.Consumer; import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.security.auth.x500.X500Principal; import net.neoforged.fml.loading.LogMarkers; import net.neoforged.fml.loading.StringUtils; import net.neoforged.neoforgespi.language.IConfigurable; @@ -157,39 +144,7 @@ public URL getIssueURL() { } public Optional getCodeSigningFingerprint() { - var signers = this.modFile.getSecureJar().getManifestSigners(); - return (signers == null ? Stream.of() : Arrays.stream(signers)) - .flatMap(csa -> csa.getSignerCertPath().getCertificates().stream()) - .findFirst() - .map(LambdaExceptionUtils.rethrowFunction(Certificate::getEncoded)) - .map(bytes -> LambdaExceptionUtils.uncheck(() -> MessageDigest.getInstance("SHA-256")).digest(bytes)) - .map(StringUtils::binToHex) - .map(str -> String.join(":", str.split("(?<=\\G.{2})"))); - } - - public Optional getTrustData() { - return Arrays.stream(this.modFile.getSecureJar().getManifestSigners()) - .flatMap(csa -> csa.getSignerCertPath().getCertificates().stream()) - .findFirst() - .map(X509Certificate.class::cast) - .map(c -> { - StringBuffer sb = new StringBuffer(); - sb.append(c.getSubjectX500Principal().getName(X500Principal.RFC2253).split(",")[0]); - boolean selfSigned = false; - try { - c.verify(c.getPublicKey()); - selfSigned = true; - } catch (CertificateException | NoSuchAlgorithmException | InvalidKeyException | SignatureException | NoSuchProviderException e) { - // not self signed - } - if (selfSigned) { - sb.append(" self-signed"); - } else { - sb.append(" signed by ").append(c.getIssuerX500Principal().getName(X500Principal.RFC2253).split(",")[0]); - } - ; - return sb.toString(); - }); + return Optional.empty(); } @Override diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModJarMetadata.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModJarMetadata.java index b42b8318d..1ded04127 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModJarMetadata.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/ModJarMetadata.java @@ -8,16 +8,21 @@ import cpw.mods.jarhandling.JarContents; import cpw.mods.jarhandling.JarMetadata; import cpw.mods.jarhandling.LazyJarMetadata; +import cpw.mods.jarhandling.SecureJar; import java.lang.module.ModuleDescriptor; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; import java.util.Objects; +import java.util.Set; import net.neoforged.neoforgespi.locating.IModFile; public final class ModJarMetadata extends LazyJarMetadata implements JarMetadata { - private final JarContents jarContents; + private final JarContents container; private IModFile modFile; - public ModJarMetadata(JarContents jarContents) { - this.jarContents = jarContents; + public ModJarMetadata(JarContents container) { + this.container = container; } public void setModFile(IModFile file) { @@ -36,20 +41,20 @@ public String version() { @Override public ModuleDescriptor computeDescriptor() { + Set packages = new HashSet<>(); + List serviceProviders = new ArrayList<>(); + JarMetadata.indexJarContent(container, packages, serviceProviders); + var bld = ModuleDescriptor.newAutomaticModule(name()) .version(version()) - .packages(jarContents.getPackagesExcluding("assets", "data")); - jarContents.getMetaInfServices().stream() + .packages(packages); + serviceProviders.stream() .filter(p -> !p.providers().isEmpty()) .forEach(p -> bld.provides(p.serviceName(), p.providers())); modFile.getModFileInfo().usesServices().forEach(bld::uses); return bld.build(); } - public IModFile modFile() { - return modFile; - } - @Override public boolean equals(Object obj) { if (obj == this) return true; diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/GameLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/GameLocator.java index 031c3fcf2..0c6f62471 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/GameLocator.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/GameLocator.java @@ -5,10 +5,8 @@ package net.neoforged.fml.loading.moddiscovery.locators; -import com.google.common.collect.Streams; import cpw.mods.jarhandling.JarContents; -import cpw.mods.jarhandling.JarContentsBuilder; -import cpw.mods.jarhandling.SecureJar; +import cpw.mods.jarhandling.impl.Jar; import java.io.BufferedInputStream; import java.io.IOException; import java.nio.file.Files; @@ -18,7 +16,6 @@ import java.util.HashSet; import java.util.List; import java.util.jar.Manifest; -import java.util.stream.Stream; import net.neoforged.fml.ModLoadingIssue; import net.neoforged.fml.loading.FMLLoader; import net.neoforged.fml.loading.MavenCoordinate; @@ -39,6 +36,7 @@ public class GameLocator implements IModFileCandidateLocator { public static final String CLIENT_CLASS = "net/minecraft/client/Minecraft.class"; private static final Logger LOG = LoggerFactory.getLogger(GameLocator.class); public static final String LIBRARIES_DIRECTORY_PROPERTY = "libraryDirectory"; + public static final String[] NEOFORGE_SPECIFIC_PATH_PREFIXES = { "net/neoforged/neoforge/", "META-INF/services/", "META-INF/coremods.json", JarModsDotTomlModFileReader.MODS_TOML }; @Override public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) { @@ -67,7 +65,6 @@ public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) if (Files.isDirectory(mcClassesRoot) && Files.isRegularFile(mcResourceRoot)) { // We look for all MANIFEST.MF directly on the classpath and try to find the one for NeoForge var manifestRoots = ClasspathResourceUtils.findFileSystemRootsOfFileOnClasspath(ourCl, JarModsDotTomlModFileReader.MANIFEST); - Path resourcesRoot; for (var manifestRoot : manifestRoots) { if (!Files.isDirectory(manifestRoot)) { continue; // We're only interested in directories @@ -149,10 +146,10 @@ private static void locateProductionMinecraft(ILaunchContext context, IDiscovery } try { - var mcJarContents = JarContents.of(minecraftJarContent); + var mcJarContents = JarContents.ofPaths(minecraftJarContent); var mcJarMetadata = new ModJarMetadata(mcJarContents); - var mcSecureJar = SecureJar.from(mcJarContents, mcJarMetadata); + var mcSecureJar = Jar.of(mcJarContents, mcJarMetadata); var mcjar = IModFile.create(mcSecureJar, MinecraftModInfo::buildMinecraftModInfo); mcJarMetadata.setModFile(mcjar); pipeline.addModFile(mcjar); @@ -233,44 +230,58 @@ private static boolean resolveLibraries(Path libraryDirectory, List paths, } private void addDevelopmentModFiles(List paths, Path minecraftResourcesRoot, IDiscoveryPipeline pipeline) { - var packages = getNeoForgeSpecificPathPrefixes(); - - var mcJarContents = new JarContentsBuilder() - .paths(Streams.concat(paths.stream(), Stream.of(minecraftResourcesRoot)).toArray(Path[]::new)) - .pathFilter((entry, basePath) -> { - // We serve everything, except for things in the forge packages. - if (basePath.equals(minecraftResourcesRoot) || entry.endsWith("/")) { - return true; - } - // Any non-class file will be served from the client extra jar file mentioned above - if (!entry.endsWith(".class")) { + var mcJarContentsBuilder = JarContents.builder(); + for (var path : paths) { + mcJarContentsBuilder.filteredPath(path, relativePath -> { + // Any non-class file will be served from the client extra jar file mentioned above + if (!path.endsWith(".class")) { + return false; + } + for (var pkg : NEOFORGE_SPECIFIC_PATH_PREFIXES) { + if (path.startsWith(pkg)) { return false; } - for (var pkg : packages) { - if (entry.startsWith(pkg)) { - return false; - } - } - return true; - }) - .build(); + } + return true; + }); + } + mcJarContentsBuilder.path(minecraftResourcesRoot); + JarContents mcJarContents; + try { + mcJarContents = mcJarContentsBuilder.build(); + } catch (IOException e) { + pipeline.addIssue(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation").withCause(e)); + return; + } var mcJarMetadata = new ModJarMetadata(mcJarContents); - var mcSecureJar = SecureJar.from(mcJarContents, mcJarMetadata); + var mcSecureJar = Jar.of(mcJarContents, mcJarMetadata); var minecraftModFile = IModFile.create(mcSecureJar, MinecraftModInfo::buildMinecraftModInfo); mcJarMetadata.setModFile(minecraftModFile); pipeline.addModFile(minecraftModFile); // We need to separate out our resources/code so that we can show up as a different data pack. - var neoforgeJarContents = new JarContentsBuilder() - .paths(paths.toArray(Path[]::new)) - .pathFilter((entry, basePath) -> { - if (!entry.endsWith(".class")) return true; - for (var pkg : packages) - if (entry.startsWith(pkg)) return true; - return false; - }) - .build(); + var neoforgeJarBuilder = JarContents.builder(); + for (var path : paths) { + neoforgeJarBuilder.filteredPath(path, relativePath -> { + if (!relativePath.endsWith(".class")) { + return true; + } + for (var includedPrefix : NEOFORGE_SPECIFIC_PATH_PREFIXES) { + if (relativePath.startsWith(includedPrefix)) { + return true; + } + } + return false; + }); + } + JarContents neoforgeJarContents; + try { + neoforgeJarContents = neoforgeJarBuilder.build(); + } catch (IOException e) { + pipeline.addIssue(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation").withCause(e)); + return; + } var modFile = JarModsDotTomlModFileReader.createModFile(neoforgeJarContents, ModFileDiscoveryAttributes.DEFAULT); if (modFile == null) { throw new IllegalStateException("Failed to construct a mod from the NeoForge classes and resources directories."); @@ -278,10 +289,6 @@ private void addDevelopmentModFiles(List paths, Path minecraftResourcesRoo pipeline.addModFile(modFile); } - private static String[] getNeoForgeSpecificPathPrefixes() { - return new String[] { "net/neoforged/neoforge/", "META-INF/services/", "META-INF/coremods.json", JarModsDotTomlModFileReader.MODS_TOML }; - } - @Override public int getPriority() { return HIGHEST_SYSTEM_PRIORITY; diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/InDevFolderLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/InDevFolderLocator.java index 09cacaca0..59eddf72c 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/InDevFolderLocator.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/InDevFolderLocator.java @@ -79,7 +79,11 @@ public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) context.addLocated(path); paths.add(path); } - pipeline.addJarContent(JarContents.of(paths), ModFileDiscoveryAttributes.DEFAULT, IncompatibleFileReporting.ERROR); + try { + pipeline.addJarContent(JarContents.ofPaths(paths), ModFileDiscoveryAttributes.DEFAULT, IncompatibleFileReporting.ERROR); + } catch (IOException e) { + pipeline.addIssue(ModLoadingIssue.error("")); // TODO + } } // Add groups that remain but are not on the classpath at all to support legacy configurations @@ -87,7 +91,11 @@ public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) for (var entry : new HashSet<>(virtualJarMemberIndex.values())) { var paths = entry.files.stream().map(File::toPath).toList(); if (paths.stream().noneMatch(context::isLocated)) { - pipeline.addJarContent(JarContents.of(paths), ModFileDiscoveryAttributes.DEFAULT, IncompatibleFileReporting.ERROR); + try { + pipeline.addJarContent(JarContents.ofPaths(paths), ModFileDiscoveryAttributes.DEFAULT, IncompatibleFileReporting.ERROR); + } catch (IOException e) { + pipeline.addIssue(ModLoadingIssue.error("")); // TODO + } } } } diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/InDevJarLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/InDevJarLocator.java index 17df1000b..153104f1e 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/InDevJarLocator.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/InDevJarLocator.java @@ -16,7 +16,6 @@ /** * This locator finds mods and game libraries that are passed as jar files on the classpath. - * Libraries are handled by {@link ClasspathLibrariesLocator}. */ public class InDevJarLocator implements IModFileCandidateLocator { @Override diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/JarInJarDependencyLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/JarInJarDependencyLocator.java index c8ed394e8..1885eac80 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/JarInJarDependencyLocator.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/JarInJarDependencyLocator.java @@ -5,27 +5,26 @@ package net.neoforged.fml.loading.moddiscovery.locators; -import com.google.common.collect.ImmutableMap; import com.mojang.logging.LogUtils; -import cpw.mods.jarhandling.JarContents; +import java.io.IOException; import java.io.InputStream; -import java.net.URI; -import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.NoSuchFileException; -import java.nio.file.Path; +import java.security.MessageDigest; import java.util.Collection; +import java.util.HexFormat; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; import net.neoforged.fml.ModLoadingException; import net.neoforged.fml.ModLoadingIssue; -import net.neoforged.jarjar.selection.JarSelector; +import net.neoforged.fml.loading.FMLLoader; import net.neoforged.neoforgespi.language.IModInfo; import net.neoforged.neoforgespi.locating.IDependencyLocator; import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; import net.neoforged.neoforgespi.locating.IModFile; +import net.neoforged.neoforgespi.locating.IncompatibleFileReporting; import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; import net.neoforged.neoforgespi.locating.ModFileLoadingException; import org.apache.maven.artifact.versioning.ArtifactVersion; @@ -55,15 +54,30 @@ public void scanMods(List loadedMods, IDiscoveryPipeline pipeline) { } @SuppressWarnings("resource") - protected Optional loadModFileFrom(IModFile file, final Path path, IDiscoveryPipeline pipeline) { + protected Optional loadModFileFrom(IModFile file, String path, IDiscoveryPipeline pipeline) { try { - var pathInModFile = file.findResource(path.toString()); - var filePathUri = new URI("jij:" + (pathInModFile.toAbsolutePath().toUri().getRawSchemeSpecificPart())).normalize(); - var outerFsArgs = ImmutableMap.of("packagePath", pathInModFile); - var zipFS = FileSystems.newFileSystem(filePathUri, outerFsArgs); - var jar = JarContents.of(zipFS.getPath("/")); - var providerResult = pipeline.readModFile(jar, ModFileDiscoveryAttributes.DEFAULT.withParent(file)); - return Optional.ofNullable(providerResult); + var cacheDir = FMLLoader.current().getCacheDir(); + var jarInMemory = file.getContent().readFile(path); + var md = MessageDigest.getInstance("MD5"); + var hash = HexFormat.of().formatHex(md.digest(jarInMemory)); + + var lastSep = Math.max(path.lastIndexOf('\\'), path.lastIndexOf('/')); + var filename = path.substring(lastSep + 1); + + var jarCacheDir = cacheDir.resolve("embedded_jars").resolve(hash); + Files.createDirectories(jarCacheDir); + var cachedFile = jarCacheDir.resolve(filename); + long expectedSize = jarInMemory.length; + long existingSize = -1; + try { + existingSize = Files.size(cachedFile); + } catch (IOException ignored) {} + if (existingSize != expectedSize) { + // TODO atomic move crap + Files.write(cachedFile, jarInMemory); + } + + return pipeline.addPath(cachedFile, ModFileDiscoveryAttributes.DEFAULT.withParent(file), IncompatibleFileReporting.ERROR); } catch (Exception e) { LOGGER.error("Failed to load mod file {} from {}", path, file.getFileName()); final RuntimeException exception = new ModFileLoadingException("Failed to load mod file " + file.getFileName()); @@ -123,9 +137,9 @@ protected String identifyMod(final IModFile modFile) { private record ModWithVersionRange(IModInfo modInfo, VersionRange versionRange, ArtifactVersion artifactVersion) {} - protected Optional loadResourceFromModFile(final IModFile modFile, final Path path) { + protected Optional loadResourceFromModFile(final IModFile modFile, final String path) { try { - return Optional.of(Files.newInputStream(modFile.findResource(path.toString()))); + return Optional.of(Files.newInputStream(modFile.findResource(path))); } catch (final NoSuchFileException e) { LOGGER.trace("Failed to load resource {} from {}, it does not contain dependency information.", path, modFile.getFileName()); return Optional.empty(); diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/JarSelector.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/JarSelector.java new file mode 100644 index 000000000..f1bcf6f14 --- /dev/null +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/JarSelector.java @@ -0,0 +1,508 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.loading.moddiscovery.locators; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import com.google.common.collect.Sets; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; +import net.neoforged.jarjar.metadata.ContainedJarIdentifier; +import net.neoforged.jarjar.metadata.ContainedJarMetadata; +import net.neoforged.jarjar.metadata.ContainedVersion; +import net.neoforged.jarjar.metadata.Metadata; +import net.neoforged.jarjar.metadata.MetadataIOHandler; +import net.neoforged.jarjar.selection.util.Constants; +import org.apache.maven.artifact.versioning.ArtifactVersion; +import org.apache.maven.artifact.versioning.VersionRange; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class JarSelector { + private static final Logger LOGGER = LoggerFactory.getLogger(JarSelector.class); + + private JarSelector() { + throw new IllegalStateException("Can not instantiate an instance of: JarSelector. This is a utility class"); + } + + public static List detectAndSelect( + final List source, + final BiFunction> resourceReader, + final BiFunction> sourceProducer, + final Function identificationProducer, + final Function>, E> failureExceptionProducer) throws E { + final Set> detectedMetadata = detect(source, resourceReader, sourceProducer, identificationProducer); + final Multimap detectedJarsBySource = detectedMetadata.stream().collect(Multimaps.toMultimap(DetectionResult::metadata, DetectionResult::source, HashMultimap::create)); + final Multimap detectedJarsByRootSource = detectedMetadata.stream().collect(Multimaps.toMultimap(DetectionResult::metadata, DetectionResult::rootSource, HashMultimap::create)); + final Multimap metadataByIdentifier = Multimaps.index(detectedJarsByRootSource.keySet(), ContainedJarMetadata::identifier); + + final Set select = select(detectedJarsBySource.keySet()); + + if (select.stream().anyMatch(result -> !result.selected().isPresent())) { + //We have entered into failure territory. Let's collect all of those that failed + final Set failed = select.stream().filter(result -> !result.selected().isPresent()).collect(Collectors.toSet()); + + final List> resolutionFailures = new ArrayList<>(); + for (final SelectionResult failedResult : failed) { + final ContainedJarIdentifier failedIdentifier = failedResult.identifier(); + final Collection metadata = metadataByIdentifier.get(failedIdentifier); + final Set> sources = metadata.stream().map(containedJarMetadata -> { + final Collection rootSources = detectedJarsBySource.get(containedJarMetadata); + return new SourceWithRequestedVersionRange(rootSources, containedJarMetadata.version().range(), containedJarMetadata.version().artifactVersion()); + }) + .collect(Collectors.toSet()); + + final ResolutionFailureInformation resolutionFailure = new ResolutionFailureInformation<>(getFailureReason(failedResult), failedIdentifier, sources); + + resolutionFailures.add(resolutionFailure); + } + + final E exception = failureExceptionProducer.apply(resolutionFailures); + LOGGER.error("Failed to select jars for {}", resolutionFailures); + throw exception; + } + + final List selectedJars = select.stream() + .map(SelectionResult::selected) + .filter(Optional::isPresent) + .map(Optional::get) + .filter(detectedJarsBySource::containsKey) + .map(selectedJarMetadata -> { + final Collection sourceOfJar = detectedJarsBySource.get(selectedJarMetadata); + return sourceProducer.apply(sourceOfJar.iterator().next(), selectedJarMetadata.path()); + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + + final Map selectedJarsByIdentification = selectedJars.stream() + .collect(Collectors.toMap(identificationProducer, Function.identity(), (t, t2) -> { + LOGGER.warn("Attempted to select two dependency jars from JarJar which have the same identification: {} and {}. Using {}", t, t2, t); + return t; + })); + + final Map sourceJarsByIdentification = source.stream() + .collect(Collectors.toMap(identificationProducer, Function.identity(), (t, t2) -> { + LOGGER.warn("Attempted to select two source jars for JarJar which have the same identification: {} and {}. Using {}", t, t2, t); + return t; + })); + + //Strip out jars which are already included by source. We can't do any resolution on this anyway so we force the use of those by not returning them. + final Set operatingKeySet = new HashSet<>(selectedJarsByIdentification.keySet()); //PREVENT CME's. + operatingKeySet.stream().filter(sourceJarsByIdentification::containsKey) + .peek(identification -> LOGGER.warn("Attempted to select a dependency jar for JarJar which was passed in as source: {}. Using {}", identification, sourceJarsByIdentification.get(identification))) + .forEach(selectedJarsByIdentification::remove); + return new ArrayList<>(selectedJarsByIdentification.values()); + } + + private static Set> detect( + final List source, + final BiFunction> resourceReader, + final BiFunction> sourceProducer, + final Function identificationProducer) { + final Map> metadataInputStreamsBySource = source.stream().collect( + Collectors.toMap( + Function.identity(), + t -> resourceReader.apply(t, Constants.CONTAINED_JARS_METADATA_PATH))); + + final Map rootMetadataBySource = metadataInputStreamsBySource.entrySet().stream() + .filter(kvp -> kvp.getValue().isPresent()) + .map(kvp -> new SourceWithOptionalMetadata<>(kvp.getKey(), MetadataIOHandler.fromStream(kvp.getValue().get()))) + .filter(sourceWithOptionalMetadata -> sourceWithOptionalMetadata.metadata().isPresent()) + .collect( + Collectors.toMap( + SourceWithOptionalMetadata::source, + sourceWithOptionalMetadata -> sourceWithOptionalMetadata.metadata().get())); + + return recursivelyDetectContainedJars( + rootMetadataBySource, + resourceReader, + sourceProducer, + identificationProducer); + } + + private static Set> recursivelyDetectContainedJars( + final Map rootMetadataBySource, + final BiFunction> resourceReader, + final BiFunction> sourceProducer, + final Function identificationProducer) { + final Set> results = Sets.newHashSet(); + final Map rootSourcesBySource = Maps.newHashMap(); + + final Queue sourcesToProcess = new LinkedList<>(); + for (final Map.Entry entry : rootMetadataBySource.entrySet()) { + entry.getValue().jars().stream().map(containedJarMetadata -> new DetectionResult<>(containedJarMetadata, entry.getKey(), entry.getKey())) + .forEach(results::add); + + for (final ContainedJarMetadata jar : entry.getValue().jars()) { + final Optional source = sourceProducer.apply(entry.getKey(), jar.path()); + if (source.isPresent()) { + sourcesToProcess.add(source.get()); + rootSourcesBySource.put(source.get(), entry.getKey()); + } else { + LOGGER.warn("The source jar: " + identificationProducer.apply(entry.getKey()) + " is supposed to contain a jar: " + jar.path() + " but it does not exist."); + } + } + } + + while (!sourcesToProcess.isEmpty()) { + final T source = sourcesToProcess.remove(); + final T rootSource = rootSourcesBySource.get(source); + final Optional metadataInputStream = resourceReader.apply(source, Constants.CONTAINED_JARS_METADATA_PATH); + if (metadataInputStream.isPresent()) { + final Optional metadata = MetadataIOHandler.fromStream(metadataInputStream.get()); + if (metadata.isPresent()) { + metadata.get().jars().stream().map(containedJarMetadata -> new DetectionResult<>(containedJarMetadata, source, rootSource)) + .forEach(results::add); + + for (final ContainedJarMetadata jar : metadata.get().jars()) { + final Optional sourceJar = sourceProducer.apply(source, jar.path()); + if (sourceJar.isPresent()) { + sourcesToProcess.add(sourceJar.get()); + rootSourcesBySource.put(sourceJar.get(), rootSource); + } else { + LOGGER.warn("The source jar: " + identificationProducer.apply(source) + " is supposed to contain a jar: " + jar.path() + " but it does not exist."); + } + } + } + } + } + + return results; + } + + private static Set select(final Set containedJarMetadata) { + final Multimap jarsByIdentifier = containedJarMetadata.stream() + .collect( + Multimaps.toMultimap( + ContainedJarMetadata::identifier, + Function.identity(), + HashMultimap::create)); + + return jarsByIdentifier.keySet().stream() + .map(identifier -> { + final Collection jars = jarsByIdentifier.get(identifier); + + if (jars.size() <= 1) { + //Quick return: + return new SelectionResult(identifier, jars, Optional.of(jars.iterator().next()), false); + } + + //Find the most agreeable version: + final VersionRange range = jars.stream() + .map(ContainedJarMetadata::version) + .map(ContainedVersion::range) + .reduce(null, JarSelector::restrictRanges); + + if (range == null || !isValid(range)) { + return new SelectionResult(identifier, jars, Optional.empty(), true); + } + + if (range.getRecommendedVersion() != null) { + final Optional selected = jars.stream().filter(jar -> jar.version().artifactVersion().equals(range.getRecommendedVersion())).findFirst(); + return new SelectionResult(identifier, jars, selected, false); + } + + final Optional selected = jars.stream().filter(jar -> range.containsVersion(jar.version().artifactVersion())).findFirst(); + return new SelectionResult(identifier, jars, selected, false); + }) + .collect(Collectors.toSet()); + } + + private static VersionRange restrictRanges(final VersionRange versionRange, final VersionRange versionRange2) { + if (versionRange == null) { + return versionRange2; + } + + if (versionRange2 == null) { + return versionRange; + } + + return versionRange.restrict(versionRange2); + } + + private static boolean isValid(final VersionRange range) { + return range.getRecommendedVersion() == null && range.hasRestrictions(); + } + + private static FailureReason getFailureReason(SelectionResult selectionResult) { + if (selectionResult.selected().isPresent()) + throw new IllegalArgumentException("Resolution succeeded, not failure possible"); + + if (selectionResult.noValidRangeFound()) + return FailureReason.VERSION_RESOLUTION_FAILED; + + return FailureReason.NO_MATCHING_JAR; + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static final class SourceWithOptionalMetadata { + private final Z source; + private final Optional metadata; + + SourceWithOptionalMetadata(Z source, Optional metadata) { + this.source = source; + this.metadata = metadata; + } + + public Z source() { + return source; + } + + public Optional metadata() { + return metadata; + } + + @SuppressWarnings("rawtypes") + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + final SourceWithOptionalMetadata that = (SourceWithOptionalMetadata) obj; + return Objects.equals(this.source, that.source) && + Objects.equals(this.metadata, that.metadata); + } + + @Override + public int hashCode() { + return Objects.hash(source, metadata); + } + + @Override + public String toString() { + return "SourceWithOptionalMetadata[" + + "source=" + source + ", " + + "metadata=" + metadata + ']'; + } + } + + private static final class DetectionResult { + private final ContainedJarMetadata metadata; + private final Z source; + private final Z rootSource; + + private DetectionResult(ContainedJarMetadata metadata, Z source, Z rootSource) { + this.metadata = metadata; + this.source = source; + this.rootSource = rootSource; + } + + public ContainedJarMetadata metadata() { + return metadata; + } + + public Z source() { + return source; + } + + public Z rootSource() { + return rootSource; + } + + @SuppressWarnings("rawtypes") + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + final DetectionResult that = (DetectionResult) obj; + return Objects.equals(this.metadata, that.metadata) && + Objects.equals(this.source, that.source) && + Objects.equals(this.rootSource, that.rootSource); + } + + @Override + public int hashCode() { + return Objects.hash(metadata, source, rootSource); + } + + @Override + public String toString() { + return "DetectionResult[" + + "metadata=" + metadata + ", " + + "source=" + source + ", " + + "rootSource=" + rootSource + ']'; + } + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static final class SelectionResult { + private final ContainedJarIdentifier identifier; + private final Collection candidates; + private final Optional selected; + private final boolean noValidRangeFound; + + private SelectionResult(ContainedJarIdentifier identifier, Collection candidates, Optional selected, final boolean noValidRangeFound) { + this.identifier = identifier; + this.candidates = candidates; + this.selected = selected; + this.noValidRangeFound = noValidRangeFound; + } + + public ContainedJarIdentifier identifier() { + return identifier; + } + + public Collection candidates() { + return candidates; + } + + public Optional selected() { + return selected; + } + + public boolean noValidRangeFound() { + return noValidRangeFound; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + final SelectionResult that = (SelectionResult) obj; + return Objects.equals(this.identifier, that.identifier) && + Objects.equals(this.candidates, that.candidates) && + Objects.equals(this.selected, that.selected); + } + + @Override + public int hashCode() { + return Objects.hash(identifier, candidates, selected); + } + + @Override + public String toString() { + return "SelectionResult[" + + "identifier=" + identifier + ", " + + "candidates=" + candidates + ", " + + "selected=" + selected + ']'; + } + } + + public enum FailureReason { + VERSION_RESOLUTION_FAILED, + NO_MATCHING_JAR, + } + + public static final class SourceWithRequestedVersionRange { + private final Collection sources; + private final VersionRange requestedVersionRange; + private final ArtifactVersion includedVersion; + + public SourceWithRequestedVersionRange(Collection sources, VersionRange requestedVersionRange, ArtifactVersion includedVersion) { + this.sources = sources; + this.requestedVersionRange = requestedVersionRange; + this.includedVersion = includedVersion; + } + + public Collection sources() { + return sources; + } + + public VersionRange requestedVersionRange() { + return requestedVersionRange; + } + + public ArtifactVersion includedVersion() { + return includedVersion; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof SourceWithRequestedVersionRange)) return false; + + final SourceWithRequestedVersionRange that = (SourceWithRequestedVersionRange) o; + + if (!sources.equals(that.sources)) return false; + if (!requestedVersionRange.equals(that.requestedVersionRange)) return false; + return includedVersion.equals(that.includedVersion); + } + + @Override + public int hashCode() { + int result = sources.hashCode(); + result = 31 * result + requestedVersionRange.hashCode(); + result = 31 * result + includedVersion.hashCode(); + return result; + } + + @Override + public String toString() { + return "SourceWithRequestedVersionRange{" + + "source=" + sources + + ", requestedVersionRange=" + requestedVersionRange + + ", includedVersion=" + includedVersion + + '}'; + } + } + + public static final class ResolutionFailureInformation { + private final FailureReason failureReason; + private final ContainedJarIdentifier identifier; + private final Collection> sources; + + public ResolutionFailureInformation(final FailureReason failureReason, final ContainedJarIdentifier identifier, final Collection> sources) { + this.failureReason = failureReason; + this.identifier = identifier; + this.sources = sources; + } + + public FailureReason failureReason() { + return failureReason; + } + + public ContainedJarIdentifier identifier() { + return identifier; + } + + public Collection> sources() { + return sources; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof ResolutionFailureInformation)) return false; + + final ResolutionFailureInformation that = (ResolutionFailureInformation) o; + + if (failureReason != that.failureReason) return false; + if (!identifier.equals(that.identifier)) return false; + return sources.equals(that.sources); + } + + @Override + public int hashCode() { + int result = failureReason.hashCode(); + result = 31 * result + identifier.hashCode(); + result = 31 * result + sources.hashCode(); + return result; + } + + @Override + public String toString() { + return "ResolutionFailureInformation{" + + "failureReason=" + failureReason + + ", identifier=" + identifier + + ", sources=" + sources + + '}'; + } + } +} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/NeoForgeDevProvider.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/NeoForgeDevProvider.java deleted file mode 100644 index 251cf449c..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/NeoForgeDevProvider.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.moddiscovery.locators; - -import com.google.common.collect.Streams; -import cpw.mods.jarhandling.JarContentsBuilder; -import cpw.mods.jarhandling.SecureJar; -import java.io.File; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Stream; -import net.neoforged.fml.loading.moddiscovery.ModJarMetadata; -import net.neoforged.fml.loading.moddiscovery.readers.JarModsDotTomlModFileReader; -import net.neoforged.fml.util.ClasspathResourceUtils; -import net.neoforged.neoforgespi.ILaunchContext; -import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; -import net.neoforged.neoforgespi.locating.IModFile; -import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; -import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; - -/** - * Provides the Minecraft and NeoForge mods in a NeoForge dev environment. - */ -public class NeoForgeDevProvider implements IModFileCandidateLocator { - private final List paths; - - public NeoForgeDevProvider(List paths) { - this.paths = paths; - } - - @Override - public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) { - Path minecraftResourcesRoot = null; - - // try finding client-extra jar explicitly first - var legacyClassPath = System.getProperty("legacyClassPath"); - if (legacyClassPath != null) { - minecraftResourcesRoot = Arrays.stream(legacyClassPath.split(File.pathSeparator)) - .map(Path::of) - .filter(path -> path.getFileName().toString().contains("client-extra")) - .findFirst() - .orElse(null); - } - // then fall back to finding it on the current classpath - if (minecraftResourcesRoot == null) { - minecraftResourcesRoot = ClasspathResourceUtils.findFileSystemRootOfFileOnClasspath("assets/.mcassetsroot"); - } - - var packages = getNeoForgeSpecificPathPrefixes(); - var minecraftResourcesPrefix = minecraftResourcesRoot; - - var mcJarContents = new JarContentsBuilder() - .paths(Streams.concat(paths.stream(), Stream.of(minecraftResourcesRoot)).toArray(Path[]::new)) - .pathFilter((entry, basePath) -> { - // We serve everything, except for things in the forge packages. - if (basePath.equals(minecraftResourcesPrefix) || entry.endsWith("/")) { - return true; - } - // Any non-class file will be served from the client extra jar file mentioned above - if (!entry.endsWith(".class")) { - return false; - } - for (var pkg : packages) { - if (entry.startsWith(pkg)) { - return false; - } - } - return true; - }) - .build(); - - var mcJarMetadata = new ModJarMetadata(mcJarContents); - var mcSecureJar = SecureJar.from(mcJarContents, mcJarMetadata); - var minecraftModFile = IModFile.create(mcSecureJar, MinecraftModInfo::buildMinecraftModInfo); - mcJarMetadata.setModFile(minecraftModFile); - pipeline.addModFile(minecraftModFile); - - // We need to separate out our resources/code so that we can show up as a different data pack. - var neoforgeJarContents = new JarContentsBuilder() - .paths(paths.toArray(Path[]::new)) - .pathFilter((entry, basePath) -> { - if (!entry.endsWith(".class")) return true; - for (var pkg : packages) - if (entry.startsWith(pkg)) return true; - return false; - }) - .build(); - pipeline.addModFile(JarModsDotTomlModFileReader.createModFile(neoforgeJarContents, ModFileDiscoveryAttributes.DEFAULT)); - } - - private static String[] getNeoForgeSpecificPathPrefixes() { - return new String[] { "net/neoforged/neoforge/", "META-INF/services/", "META-INF/coremods.json", JarModsDotTomlModFileReader.MODS_TOML }; - } - - @Override - public String toString() { - return "neoforge devenv provider (" + paths + ")"; - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/PathBasedLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/PathBasedLocator.java deleted file mode 100644 index 06a832e26..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/PathBasedLocator.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.moddiscovery.locators; - -import java.nio.file.Path; -import java.util.List; -import net.neoforged.neoforgespi.ILaunchContext; -import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; -import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; -import net.neoforged.neoforgespi.locating.IncompatibleFileReporting; -import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; - -/** - * "Locates" mods from a fixed set of paths. - */ -public record PathBasedLocator(String name, List paths) implements IModFileCandidateLocator { - public PathBasedLocator(String name, Path... paths) { - this(name, List.of(paths)); - } - - @Override - public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) { - for (var path : paths) { - pipeline.addPath(path, ModFileDiscoveryAttributes.DEFAULT, IncompatibleFileReporting.ERROR); - } - } - - @Override - public int getPriority() { - // Since this locator uses explicitly specified paths, they should not be handled by other locators first - return HIGHEST_SYSTEM_PRIORITY; - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/ProductionClientProvider.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/ProductionClientProvider.java deleted file mode 100644 index 29bc0fe6b..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/ProductionClientProvider.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.moddiscovery.locators; - -import cpw.mods.jarhandling.JarContents; -import cpw.mods.jarhandling.SecureJar; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import net.neoforged.fml.ModLoadingException; -import net.neoforged.fml.ModLoadingIssue; -import net.neoforged.fml.loading.FMLLoader; -import net.neoforged.fml.loading.LibraryFinder; -import net.neoforged.fml.loading.MavenCoordinate; -import net.neoforged.fml.loading.moddiscovery.ModJarMetadata; -import net.neoforged.neoforgespi.ILaunchContext; -import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; -import net.neoforged.neoforgespi.locating.IModFile; -import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; - -/** - * Locates the Minecraft client files in a production environment. - *

- * It assumes that the installer produced two artifacts, one containing the Minecraft classes ("srg") which have - * been renamed to Mojangs official names using their mappings, and another containing only the Minecraft resource - * files ("extra"), and searches for these artifacts in the library directory. - */ -public class ProductionClientProvider implements IModFileCandidateLocator { - private final List additionalContent; - - public ProductionClientProvider(List additionalContent) { - this.additionalContent = additionalContent; - } - - @Override - public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) { - var vers = FMLLoader.versionInfo(); - - var content = new ArrayList(); - addRequiredLibrary(new MavenCoordinate("net.minecraft", "client", "", "srg", vers.mcAndNeoFormVersion()), content); - addRequiredLibrary(new MavenCoordinate("net.minecraft", "client", "", "extra", vers.mcAndNeoFormVersion()), content); - for (var artifact : additionalContent) { - addRequiredLibrary(artifact, content); - } - - try { - var mcJarContents = JarContents.of(content); - - var mcJarMetadata = new ModJarMetadata(mcJarContents); - var mcSecureJar = SecureJar.from(mcJarContents, mcJarMetadata); - var mcjar = IModFile.create(mcSecureJar, MinecraftModInfo::buildMinecraftModInfo); - mcJarMetadata.setModFile(mcjar); - - pipeline.addModFile(mcjar); - } catch (Exception e) { - pipeline.addIssue(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation").withCause(e)); - } - } - - private static void addRequiredLibrary(MavenCoordinate coordinate, List content) { - var path = LibraryFinder.findPathForMaven(coordinate); - if (!Files.exists(path)) { - throw new ModLoadingException(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation").withAffectedPath(path)); - } else { - content.add(path); - } - } - - @Override - public String toString() { - var result = new StringBuilder("production client provider"); - for (var mavenCoordinate : additionalContent) { - result.append(" +").append(mavenCoordinate); - } - return result.toString(); - } - - @Override - public int getPriority() { - return HIGHEST_SYSTEM_PRIORITY; - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/ProductionServerProvider.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/ProductionServerProvider.java deleted file mode 100644 index 41471f51b..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/ProductionServerProvider.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.moddiscovery.locators; - -import cpw.mods.jarhandling.JarContents; -import cpw.mods.jarhandling.JarContentsBuilder; -import cpw.mods.jarhandling.SecureJar; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import net.neoforged.fml.ModLoadingIssue; -import net.neoforged.fml.loading.FMLLoader; -import net.neoforged.fml.loading.LibraryFinder; -import net.neoforged.fml.loading.MavenCoordinate; -import net.neoforged.fml.loading.moddiscovery.ModJarMetadata; -import net.neoforged.neoforgespi.ILaunchContext; -import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; -import net.neoforged.neoforgespi.locating.IModFile; -import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; - -/** - * Locates the Minecraft server files in a production environment. - */ -public class ProductionServerProvider implements IModFileCandidateLocator { - private final List additionalContent; - - public ProductionServerProvider(List additionalContent) { - this.additionalContent = additionalContent; - } - - @Override - public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) { - var vers = FMLLoader.versionInfo(); - - try { - var mc = LibraryFinder.findPathForMaven("net.minecraft", "server", "", "srg", vers.mcAndNeoFormVersion()); - if (!Files.exists(mc)) { - pipeline.addIssue(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation").withAffectedPath(mc)); - return; - } - var mcextra = LibraryFinder.findPathForMaven("net.minecraft", "server", "", "extra", vers.mcAndNeoFormVersion()); - if (!Files.exists(mcextra)) { - pipeline.addIssue(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation").withAffectedPath(mc)); - return; - } - - var mcextra_filtered = SecureJar.from(new JarContentsBuilder() - // We only want it for its resources. So filter everything else out. - .pathFilter((path, base) -> { - return path.equals("META-INF/versions/") || // This is required because it bypasses our filter for the manifest, and it's a multi-release jar. - (!path.endsWith(".class") && - !path.startsWith("META-INF/")); - }) - .paths(mcextra) - .build()); - - var content = new ArrayList(); - content.add(mc); - content.add(mcextra_filtered.getRootPath()); - for (var artifact : additionalContent) { - var extraPath = LibraryFinder.findPathForMaven(artifact); - if (!Files.exists(extraPath)) { - pipeline.addIssue(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation").withAffectedPath(extraPath)); - return; - } - content.add(extraPath); - } - - var mcJarContents = JarContents.of(content); - - var mcJarMetadata = new ModJarMetadata(mcJarContents); - var mcSecureJar = SecureJar.from(mcJarContents, mcJarMetadata); - var mcjar = IModFile.create(mcSecureJar, MinecraftModInfo::buildMinecraftModInfo); - mcJarMetadata.setModFile(mcjar); - - pipeline.addModFile(mcjar); - } catch (Exception e) { - pipeline.addIssue(ModLoadingIssue.error("fml.modloadingissue.corrupted_installation").withCause(e)); - } - } - - @Override - public String toString() { - var result = new StringBuilder("production server provider"); - for (var mavenCoordinate : additionalContent) { - result.append(" +").append(mavenCoordinate); - } - return result.toString(); - } - - @Override - public int getPriority() { - return HIGHEST_SYSTEM_PRIORITY; - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/UserdevLocator.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/UserdevLocator.java deleted file mode 100644 index 7e4d1cec8..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/locators/UserdevLocator.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.moddiscovery.locators; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import net.neoforged.fml.loading.moddiscovery.readers.JarModsDotTomlModFileReader; -import net.neoforged.fml.util.ClasspathResourceUtils; -import net.neoforged.neoforgespi.ILaunchContext; -import net.neoforged.neoforgespi.locating.IDiscoveryPipeline; -import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; -import net.neoforged.neoforgespi.locating.IncompatibleFileReporting; -import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; - -public class UserdevLocator implements IModFileCandidateLocator { - private final Map> modFolders; - - public UserdevLocator(Map> modFolders) { - this.modFolders = modFolders; - } - - @Override - public void findCandidates(ILaunchContext context, IDiscoveryPipeline pipeline) { - var claimed = modFolders.values().stream().flatMap(List::stream).collect(Collectors.toCollection(HashSet::new)); - - for (var modFolderGroup : modFolders.values()) { - pipeline.addPath(modFolderGroup, ModFileDiscoveryAttributes.DEFAULT, IncompatibleFileReporting.ERROR); - } - - var fromClasspath = new ArrayList(); - fromClasspath.addAll(ClasspathResourceUtils.findFileSystemRootsOfFileOnClasspath(JarModsDotTomlModFileReader.MODS_TOML)); - fromClasspath.addAll(ClasspathResourceUtils.findFileSystemRootsOfFileOnClasspath(JarModsDotTomlModFileReader.MANIFEST)); - for (var path : fromClasspath) { - if (claimed.add(path)) { - pipeline.addPath(List.of(path), ModFileDiscoveryAttributes.DEFAULT, IncompatibleFileReporting.WARN_ON_KNOWN_INCOMPATIBILITY); - } - } - } - - @Override - public String toString() { - return "userdev mods and services"; - } - - @Override - public int getPriority() { - return LOWEST_SYSTEM_PRIORITY; - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/readers/JarModsDotTomlModFileReader.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/readers/JarModsDotTomlModFileReader.java index 519b082e9..f86da35f8 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/readers/JarModsDotTomlModFileReader.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/readers/JarModsDotTomlModFileReader.java @@ -7,12 +7,15 @@ import com.mojang.logging.LogUtils; import cpw.mods.jarhandling.JarContents; -import cpw.mods.jarhandling.SecureJar; +import cpw.mods.jarhandling.impl.Jar; +import java.io.IOException; +import java.io.UncheckedIOException; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; +import java.util.jar.Manifest; import net.neoforged.fml.ModLoadingException; import net.neoforged.fml.ModLoadingIssue; import net.neoforged.fml.loading.LogMarkers; @@ -36,17 +39,21 @@ public class JarModsDotTomlModFileReader implements IModFileReader { public static final String MODS_TOML = "META-INF/neoforge.mods.toml"; public static final String MANIFEST = "META-INF/MANIFEST.MF"; - public static IModFile createModFile(JarContents contents, ModFileDiscoveryAttributes discoveryAttributes) { - var type = getModType(contents); + public static IModFile createModFile(JarContents container, ModFileDiscoveryAttributes discoveryAttributes) { + var type = getModType(container); IModFile mod; - if (contents.findFile(MODS_TOML).isPresent()) { - LOGGER.debug(LogMarkers.SCAN, "Found {} mod of type {}: {}", MODS_TOML, type, contents.getPrimaryPath()); - var mjm = new ModJarMetadata(contents); - mod = new ModFile(SecureJar.from(contents, mjm), ModFileParser::modsTomlParser, discoveryAttributes); + if (container.findFile(MODS_TOML).isPresent()) { + LOGGER.debug(LogMarkers.SCAN, "Found {} mod of type {}: {}", MODS_TOML, type, container); + var mjm = new ModJarMetadata(container); + mod = new ModFile(Jar.of(container, mjm), ModFileParser::modsTomlParser, discoveryAttributes); mjm.setModFile(mod); } else if (type != null) { - LOGGER.debug(LogMarkers.SCAN, "Found {} mod of type {}: {}", MANIFEST, type, contents.getPrimaryPath()); - mod = new ModFile(SecureJar.from(contents), JarModsDotTomlModFileReader::manifestParser, type, discoveryAttributes); + LOGGER.debug(LogMarkers.SCAN, "Found {} mod of type {}: {}", MANIFEST, type, container); + try { + mod = new ModFile(Jar.of(container), JarModsDotTomlModFileReader::manifestParser, type, discoveryAttributes); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } else { return null; } @@ -56,7 +63,11 @@ public static IModFile createModFile(JarContents contents, ModFileDiscoveryAttri @Nullable private static IModFile.Type getModType(JarContents jar) { - var typeString = jar.getManifest().getMainAttributes().getValue(ModFile.TYPE); + Manifest jarManifest = jar.getJarManifest(); + if (jarManifest == null) { + return null; + } + var typeString = jarManifest.getMainAttributes().getValue(ModFile.TYPE); try { return typeString != null ? IModFile.Type.valueOf(typeString) : null; } catch (IllegalArgumentException e) { diff --git a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/readers/NestedLibraryModReader.java b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/readers/NestedLibraryModReader.java index 5957edde7..554655629 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/readers/NestedLibraryModReader.java +++ b/loader/src/main/java/net/neoforged/fml/loading/moddiscovery/readers/NestedLibraryModReader.java @@ -6,7 +6,9 @@ package net.neoforged.fml.loading.moddiscovery.readers; import cpw.mods.jarhandling.JarContents; -import cpw.mods.jarhandling.SecureJar; +import cpw.mods.jarhandling.impl.Jar; +import java.io.IOException; +import java.io.UncheckedIOException; import net.neoforged.neoforgespi.locating.IModFile; import net.neoforged.neoforgespi.locating.IModFileReader; import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; @@ -27,7 +29,11 @@ public class NestedLibraryModReader implements IModFileReader { // since we assume those have been included deliberately. Loose jar files in the mods directory // are not considered, since those are likely to have been dropped in by accident. if (discoveryAttributes.parent() != null) { - return IModFile.create(SecureJar.from(jar), JarModsDotTomlModFileReader::manifestParser, IModFile.Type.LIBRARY, discoveryAttributes); + try { + return IModFile.create(Jar.of(jar), JarModsDotTomlModFileReader::manifestParser, IModFile.Type.LIBRARY, discoveryAttributes); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } return null; diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/CommonLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/CommonLaunchHandler.java deleted file mode 100644 index 068230f25..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/CommonLaunchHandler.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.targets; - -import com.mojang.logging.LogUtils; -import java.io.File; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.lang.reflect.InvocationTargetException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Properties; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Collectors; -import net.neoforged.api.distmarker.Dist; -import net.neoforged.fml.loading.LogMarkers; -import net.neoforged.fml.loading.VersionInfo; -import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; -import org.slf4j.Logger; - -public abstract class CommonLaunchHandler { - public abstract String name(); - - protected static final Logger LOGGER = LogUtils.getLogger(); - - public abstract Dist getDist(); - - public abstract boolean isProduction(); - - /** - * Return additional locators to be used for locating mods when this launch handler is used. - */ - public void collectAdditionalModFileLocators(VersionInfo versionInfo, Consumer output) {} - - protected String[] preLaunch(String[] arguments, ModuleLayer layer) { - return arguments; - } - - public static Map> getGroupedModFolders() { - Map> result; - - var modFolders = Optional.ofNullable(System.getenv("MOD_CLASSES")) - .orElse(System.getProperty("fml.modFolders", "")); - var modFoldersFile = System.getProperty("fml.modFoldersFile", ""); - if (!modFoldersFile.isEmpty()) { - LOGGER.debug(LogMarkers.CORE, "Reading additional mod folders from file {}", modFoldersFile); - var p = new Properties(); - try (var in = Files.newBufferedReader(Paths.get(modFoldersFile))) { - p.load(in); - } catch (IOException e) { - throw new UncheckedIOException("Failed to read mod classes list from " + modFoldersFile, e); - } - - result = p.stringPropertyNames() - .stream() - .collect(Collectors.toMap( - Function.identity(), - modId -> Arrays.stream(p.getProperty(modId).split(File.pathSeparator)).map(Paths::get).toList())); - } else if (!modFolders.isEmpty()) { - LOGGER.debug(LogMarkers.CORE, "Got mod coordinates {} from env", modFolders); - record ExplodedModPath(String modId, Path path) {} - // "a/b/;c/d/;" -> "modid%%c:\fish\pepper;modid%%c:\fish2\pepper2\;modid2%%c:\fishy\bums;modid2%%c:\hmm" - result = Arrays.stream(modFolders.split(File.pathSeparator)) - .map(inp -> inp.split("%%", 2)) - .map(splitString -> new ExplodedModPath(splitString.length == 1 ? "defaultmodid" : splitString[0], Paths.get(splitString[splitString.length - 1]))) - .collect(Collectors.groupingBy(ExplodedModPath::modId, Collectors.mapping(ExplodedModPath::path, Collectors.toList()))); - } else { - result = Map.of(); - } - - LOGGER.debug(LogMarkers.CORE, "Found supplied mod coordinates [{}]", result); - return result; - } - - protected abstract void runService(final String[] arguments, final ModuleLayer gameLayer) throws Throwable; - - protected void clientService(final String[] arguments, final ModuleLayer layer) throws Throwable { - runTarget("net.minecraft.client.main.Main", arguments, layer); - } - - protected void serverService(final String[] arguments, final ModuleLayer layer) throws Throwable { - runTarget("net.minecraft.server.Main", arguments, layer); - } - - protected void runTarget(final String target, final String[] arguments, final ModuleLayer layer) throws Throwable { - try { - Class.forName(layer.findModule("minecraft").orElseThrow(), target).getMethod("main", String[].class).invoke(null, (Object) arguments); - } catch (InvocationTargetException e) { - throw e.getCause(); - } - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeClientLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeClientLaunchHandler.java deleted file mode 100644 index 36ba2c796..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeClientLaunchHandler.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.targets; - -import java.util.List; -import java.util.function.Consumer; -import net.neoforged.api.distmarker.Dist; -import net.neoforged.fml.loading.LibraryFinder; -import net.neoforged.fml.loading.MavenCoordinate; -import net.neoforged.fml.loading.VersionInfo; -import net.neoforged.fml.loading.moddiscovery.locators.PathBasedLocator; -import net.neoforged.fml.loading.moddiscovery.locators.ProductionClientProvider; -import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; - -/** - * For production client environments (i.e. vanilla launcher). - */ -public class NeoForgeClientLaunchHandler extends CommonLaunchHandler { - @Override - public String name() { - return "neoforgeclient"; - } - - @Override - public Dist getDist() { - return Dist.CLIENT; - } - - @Override - public boolean isProduction() { - return true; - } - - @Override - protected void runService(String[] arguments, ModuleLayer gameLayer) throws Throwable { - clientService(arguments, gameLayer); - } - - @Override - public void collectAdditionalModFileLocators(VersionInfo versionInfo, Consumer output) { - super.collectAdditionalModFileLocators(versionInfo, output); - - // Overlays the unpatched but renamed Minecraft classes with our patched versions of those classes. - var additionalContent = List.of(new MavenCoordinate("net.neoforged", "neoforge", "", "client", versionInfo.neoForgeVersion())); - output.accept(new ProductionClientProvider(additionalContent)); - - var nfJar = LibraryFinder.findPathForMaven("net.neoforged", "neoforge", "", "universal", versionInfo.neoForgeVersion()); - output.accept(new PathBasedLocator("neoforge", nfJar)); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeDevLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeDevLaunchHandler.java deleted file mode 100644 index 4f67f526e..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeDevLaunchHandler.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.targets; - -import java.nio.file.Files; -import java.util.List; -import java.util.function.Consumer; -import net.neoforged.fml.loading.VersionInfo; -import net.neoforged.fml.loading.moddiscovery.locators.NeoForgeDevProvider; -import net.neoforged.fml.loading.moddiscovery.locators.UserdevLocator; -import net.neoforged.fml.util.ClasspathResourceUtils; -import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * For the NeoForge development environment. - */ -public abstract class NeoForgeDevLaunchHandler extends CommonLaunchHandler { - private static final Logger LOG = LoggerFactory.getLogger(NeoForgeDevLaunchHandler.class); - - /** - * A file we expect to find in the classpath entry that contains the Minecraft code. - */ - private static final String MINECRAFT_CLASS_PATH = "net/minecraft/server/MinecraftServer.class"; - - @Override - public boolean isProduction() { - return false; - } - - @Override - public void collectAdditionalModFileLocators(VersionInfo versionInfo, Consumer output) { - super.collectAdditionalModFileLocators(versionInfo, output); - - NeoForgeDevProvider neoForgeProvider = null; - var groupedModFolders = getGroupedModFolders(); - var minecraftFolders = groupedModFolders.get("minecraft"); - if (minecraftFolders != null) { - // A user can theoretically also pass a minecraft folder group when we're in userdev, - // we have to make sure the folder group actually contains a Minecraft class. - for (var candidateFolder : minecraftFolders) { - if (Files.isRegularFile(candidateFolder.resolve(MINECRAFT_CLASS_PATH))) { - LOG.debug("Launching with NeoForge from {}", minecraftFolders); - neoForgeProvider = new NeoForgeDevProvider(minecraftFolders); - break; - } - } - } - - if (neoForgeProvider == null) { - // Userdev is similar to neoforge dev with the only real difference being that the combined - // output of the neoforge and patched mincraft sources are combined into a jar file - var classesRoot = ClasspathResourceUtils.findFileSystemRootOfFileOnClasspath(MINECRAFT_CLASS_PATH); - LOG.debug("Launching with NeoForge from {}", classesRoot); - neoForgeProvider = new NeoForgeDevProvider(List.of(classesRoot)); - } - - output.accept(neoForgeProvider); - output.accept(new UserdevLocator(groupedModFolders)); - } -} diff --git a/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeServerLaunchHandler.java b/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeServerLaunchHandler.java deleted file mode 100644 index b6a0aa0df..000000000 --- a/loader/src/main/java/net/neoforged/fml/loading/targets/NeoForgeServerLaunchHandler.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.targets; - -import java.util.List; -import java.util.function.Consumer; -import net.neoforged.api.distmarker.Dist; -import net.neoforged.fml.loading.LibraryFinder; -import net.neoforged.fml.loading.MavenCoordinate; -import net.neoforged.fml.loading.VersionInfo; -import net.neoforged.fml.loading.moddiscovery.locators.PathBasedLocator; -import net.neoforged.fml.loading.moddiscovery.locators.ProductionServerProvider; -import net.neoforged.neoforgespi.locating.IModFileCandidateLocator; - -/** - * For production dedicated server environments. - */ -public class NeoForgeServerLaunchHandler extends CommonLaunchHandler { - @Override - public String name() { - return "neoforgeserver"; - } - - @Override - public Dist getDist() { - return Dist.DEDICATED_SERVER; - } - - @Override - public boolean isProduction() { - return true; - } - - @Override - protected void runService(String[] arguments, ModuleLayer gameLayer) throws Throwable { - serverService(arguments, gameLayer); - } - - @Override - public void collectAdditionalModFileLocators(VersionInfo versionInfo, Consumer output) { - super.collectAdditionalModFileLocators(versionInfo, output); - // Overlays the unpatched but renamed Minecraft classes with our patched versions of those classes. - var additionalContent = List.of(new MavenCoordinate("net.neoforged", "neoforge", "", "server", versionInfo.neoForgeVersion())); - output.accept(new ProductionServerProvider(additionalContent)); - - var nfJar = LibraryFinder.findPathForMaven("net.neoforged", "neoforge", "", "universal", versionInfo.neoForgeVersion()); - output.accept(new PathBasedLocator("neoforge", nfJar)); - } -} diff --git a/loader/src/main/java/net/neoforged/neoforgespi/locating/IModFile.java b/loader/src/main/java/net/neoforged/neoforgespi/locating/IModFile.java index 9b1fdb8a3..08b911340 100644 --- a/loader/src/main/java/net/neoforged/neoforgespi/locating/IModFile.java +++ b/loader/src/main/java/net/neoforged/neoforgespi/locating/IModFile.java @@ -5,6 +5,7 @@ package net.neoforged.neoforgespi.locating; +import cpw.mods.jarhandling.JarContents; import cpw.mods.jarhandling.SecureJar; import java.nio.file.Path; import java.util.List; @@ -71,6 +72,11 @@ static IModFile create(SecureJar jar, ModFileInfoParser parser, IModFile.Type ty */ Path findResource(String... pathName); + /** + * Gets the underlying content. + */ + JarContents getContent(); + /** * The mod files specific string data substitution map. * The map returned here is used to interpolate values in the metadata of the included mods. @@ -103,15 +109,6 @@ static IModFile create(SecureJar jar, ModFileInfoParser parser, IModFile.Type ty */ SecureJar getSecureJar(); - /** - * Sets the security status after verification of the mod file has been concluded. - * The security status is only determined if the jar is to be loaded into the runtime. - * - * @param status The new status. - */ - @Deprecated(forRemoval = true) - void setSecurityStatus(SecureJar.Status status); - /** * Returns a list of all mods located inside this jar. *

diff --git a/loader/src/test/java/cpw/mods/cl/test/TestjarUtil.java b/loader/src/test/java/cpw/mods/cl/test/TestjarUtil.java index 09f381200..59f4796c0 100644 --- a/loader/src/test/java/cpw/mods/cl/test/TestjarUtil.java +++ b/loader/src/test/java/cpw/mods/cl/test/TestjarUtil.java @@ -2,11 +2,12 @@ import cpw.mods.cl.JarModuleFinder; import cpw.mods.cl.ModuleClassLoader; -import cpw.mods.jarhandling.SecureJar; +import cpw.mods.jarhandling.JarContents; +import cpw.mods.jarhandling.impl.Jar; import java.io.File; +import java.io.IOException; import java.lang.module.Configuration; import java.lang.module.ModuleFinder; -import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.ServiceLoader; @@ -18,11 +19,11 @@ private record BuiltLayer(ModuleClassLoader cl, ModuleLayer layer) {} /** * Build a layer for a {@code testjarX} source set. */ - private static BuiltLayer buildTestjarLayer(int testjar, List parentLayers) { + private static BuiltLayer buildTestjarLayer(int testjar, List parentLayers) throws IOException { var paths = Stream.of(System.getenv("sjh.testjar" + testjar).split(File.pathSeparator)) .map(Paths::get) - .toArray(Path[]::new); - var jar = SecureJar.from(paths); + .toList(); + var jar = Jar.of(JarContents.ofPaths(paths)); var roots = List.of(jar.name()); var jf = JarModuleFinder.of(jar); diff --git a/loader/src/test/java/cpw/mods/jarhandling/PathNormalizationTest.java b/loader/src/test/java/cpw/mods/jarhandling/PathNormalizationTest.java new file mode 100644 index 000000000..a039fc072 --- /dev/null +++ b/loader/src/test/java/cpw/mods/jarhandling/PathNormalizationTest.java @@ -0,0 +1,24 @@ +package cpw.mods.jarhandling; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +class PathNormalizationTest { + @ParameterizedTest + @CsvSource(textBlock = """ + path path + path/ path + /path/ path + //path/ path + path// path + path/segment path/segment + path//segment path/segment + path//segment// path/segment + path\\/segment// path/segment + """, delimiter = ' ') + public void testIsNormalized(String input, String expected) { + assertEquals(expected, PathNormalization.normalize(input)); + } +} diff --git a/loader/src/test/java/cpw/mods/jarhandling/impl/TestDummyJarProvider.java b/loader/src/test/java/cpw/mods/jarhandling/impl/TestDummyJarProvider.java index d8fab9e14..844920db0 100644 --- a/loader/src/test/java/cpw/mods/jarhandling/impl/TestDummyJarProvider.java +++ b/loader/src/test/java/cpw/mods/jarhandling/impl/TestDummyJarProvider.java @@ -4,6 +4,7 @@ import cpw.mods.cl.JarModuleFinder; import cpw.mods.cl.ModuleClassLoader; +import cpw.mods.jarhandling.JarContents; import cpw.mods.jarhandling.SecureJar; import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -12,11 +13,10 @@ import java.lang.module.ModuleFinder; import java.net.URI; import java.nio.file.Path; -import java.security.CodeSigner; +import java.nio.file.Paths; import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.jar.Attributes; import java.util.jar.Manifest; import org.junit.jupiter.api.Test; import org.objectweb.asm.ClassWriter; @@ -75,11 +75,6 @@ public Optional open(final String name) { public Manifest getManifest() { return null; } - - @Override - public CodeSigner[] verifyAndGetSigners(final String cname, final byte[] bytes) { - return new CodeSigner[0]; - } } record DummyJar() implements SecureJar { @@ -94,28 +89,8 @@ public Path getPrimaryPath() { } @Override - public CodeSigner[] getManifestSigners() { - return new CodeSigner[0]; - } - - @Override - public Status verifyPath(final Path path) { - return null; - } - - @Override - public Status getFileStatus(final String name) { - return null; - } - - @Override - public Attributes getTrustedManifestEntries(final String name) { - return null; - } - - @Override - public boolean hasSecurityData() { - return false; + public JarContents container() { + return JarContents.empty(Paths.get("")); } @Override diff --git a/loader/src/test/java/cpw/mods/jarhandling/impl/TestMetadata.java b/loader/src/test/java/cpw/mods/jarhandling/impl/TestMetadata.java index 702b5907e..54e1ce29b 100644 --- a/loader/src/test/java/cpw/mods/jarhandling/impl/TestMetadata.java +++ b/loader/src/test/java/cpw/mods/jarhandling/impl/TestMetadata.java @@ -2,74 +2,33 @@ import cpw.mods.jarhandling.JarContents; import cpw.mods.jarhandling.JarMetadata; -import cpw.mods.jarhandling.SecureJar; -import java.net.URI; -import java.nio.file.Path; +import java.io.IOException; import java.nio.file.Paths; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.jar.Manifest; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class TestMetadata { @Test - void testMavenJar() { + void testMavenJar() throws IOException { var path = Paths.get("startofthepathchain/new-protected-class-1.16.5/1.1_mapped_official_1.17.1/new-protected-class-1.16.5-1.1_mapped_official_1.17.1-api.jar"); - var meta = JarMetadata.from(new FakeJarContent(path)); + var meta = JarMetadata.from(JarContents.empty(path)); Assertions.assertEquals("_new._protected._class._1._16._5", meta.name()); Assertions.assertEquals("1.1_mapped_official_1.17.1", meta.version()); } @Test - void testRootStart() { + void testRootStart() throws IOException { var path = Paths.get("/instance/mods/1life-1.5.jar"); - var meta = JarMetadata.from(new FakeJarContent(path)); + var meta = JarMetadata.from(JarContents.empty(path)); Assertions.assertEquals("_1life", meta.name()); Assertions.assertEquals("1.5", meta.version()); } @Test - void testNumberStart() { + void testNumberStart() throws IOException { var path = Paths.get("mods/1life-1.5.jar"); - var meta = JarMetadata.from(new FakeJarContent(path)); + var meta = JarMetadata.from(JarContents.empty(path)); Assertions.assertEquals("_1life", meta.name()); Assertions.assertEquals("1.5", meta.version()); } - - record FakeJarContent(Path primaryPath) implements JarContents { - @Override - public Path getPrimaryPath() { - return primaryPath; - } - - @Override - public Optional findFile(String name) { - return Optional.empty(); - } - - @Override - public Manifest getManifest() { - return new Manifest(); - } - - @Override - public Set getPackages() { - return Set.of(); - } - - @Override - public Set getPackagesExcluding(String... excludedRootPackages) { - return Set.of(); - } - - @Override - public List getMetaInfServices() { - return List.of(); - } - - @Override - public void close() {} - } } diff --git a/loader/src/test/java/cpw/mods/jarhandling/impl/TestMultiRelease.java b/loader/src/test/java/cpw/mods/jarhandling/impl/TestMultiRelease.java index 346e7531d..a480c181b 100644 --- a/loader/src/test/java/cpw/mods/jarhandling/impl/TestMultiRelease.java +++ b/loader/src/test/java/cpw/mods/jarhandling/impl/TestMultiRelease.java @@ -10,9 +10,9 @@ public class TestMultiRelease { @Test - public void testMultiRelease() { + public void testMultiRelease() throws IOException { Path rootDir = Paths.get("src", "test", "resources", "multirelease"); - var jar = SecureJar.from(rootDir); + var jar = Jar.of(rootDir); var aContents = readString(jar, "a.txt"); // Should be overridden by the Java 9 version @@ -31,7 +31,7 @@ public void testMultiRelease() { public void testMultiReleaseNoVersions() { Path rootDir = Paths.get("src", "test", "resources", "multirelease-noversions"); // Jars marked with Multi-Release but don't actually have a versions folder should not throw - Assertions.assertDoesNotThrow(() -> SecureJar.from(rootDir)); + Assertions.assertDoesNotThrow(() -> Jar.of(rootDir)); } private static String readString(SecureJar jar, String file) { diff --git a/loader/src/test/java/cpw/mods/jarhandling/impl/TestSecureJarLoading.java b/loader/src/test/java/cpw/mods/jarhandling/impl/TestSecureJarLoading.java index 2f621b888..6e7664a9c 100644 --- a/loader/src/test/java/cpw/mods/jarhandling/impl/TestSecureJarLoading.java +++ b/loader/src/test/java/cpw/mods/jarhandling/impl/TestSecureJarLoading.java @@ -1,20 +1,12 @@ package cpw.mods.jarhandling.impl; import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import cpw.mods.jarhandling.SecureJar; import java.io.UncheckedIOException; -import java.nio.file.Files; import java.nio.file.Paths; -import java.security.MessageDigest; -import java.util.zip.ZipFile; -import java.util.zip.ZipInputStream; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -25,100 +17,17 @@ static void setup() { System.setProperty("securejarhandler.useUnsafeAccessor", "true"); } - @Test // All files are signed - void testSecureJar() throws Exception { - final var path = Paths.get("src", "test", "resources", "signed.zip"); - SecureJar jar = SecureJar.from(path); - try (var is = Files.newInputStream(path)) { - ZipInputStream zis = new ZipInputStream(is); - for (var ze = zis.getNextEntry(); ze != null; ze = zis.getNextEntry()) { - if (SecureJarVerifier.isSigningRelated(ze.getName())) continue; - if (ze.isDirectory()) continue; - final var zeName = ze.getName(); - 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"), - () -> assertTrue(cs.length > 0, "With length > 0"), - () -> assertEquals("8c6124aab9db357d6616492b40d0ea4cd6e4f3f8", SecureJarVerifier.toHexString(MessageDigest.getInstance("SHA-1").digest(cs[0].getSignerCertPath().getCertificates().get(0).getEncoded())), "and the digest is correct for the code signer"), - () -> assertNotNull(jar.getTrustedManifestEntries(zeName), "Has trusted manifest entries")); - } - } - } - - @Test // Nothing is signed - void testInsecureJar() throws Exception { - final var path = Paths.get("src", "test", "resources", "unsigned.zip"); - SecureJar jar = SecureJar.from(path); - try (var is = Files.newInputStream(path)) { - ZipInputStream zis = new ZipInputStream(is); - for (var ze = zis.getNextEntry(); ze != null; ze = zis.getNextEntry()) { - if (SecureJarVerifier.isSigningRelated(ze.getName())) continue; - if (ze.isDirectory()) continue; - final var zeName = ze.getName(); - var cs = jar.moduleDataProvider().verifyAndGetSigners(ze.getName(), zis.readAllBytes()); - assertAll("Jar behaves correctly", - () -> assertNull(cs, "No code signers")); - } - } - } - @Test void testNotJar() throws Exception { final var path = Paths.get("build"); - SecureJar jar = SecureJar.from(path); + SecureJar jar = Jar.of(path); assertAll( - () -> assertFalse(jar.hasSecurityData(), "Jar is not marked secure"), () -> assertTrue(jar.moduleDataProvider().getManifest().getMainAttributes().isEmpty(), "Empty manifest returned")); } @Test void testNonExistent() throws Exception { final var path = Paths.get("thisdoesnotexist"); - assertThrows(UncheckedIOException.class, () -> SecureJar.from(path), "File does not exist"); - } - - @Test // Has a file that is signed, but modified - void testTampered() throws Exception { - final var path = Paths.get("src", "test", "resources", "tampered.zip"); - SecureJar jar = SecureJar.from(path); - ZipFile zf = new ZipFile(path.toFile()); - final var entry = zf.getEntry("test/Signed.class"); - var cs = jar.moduleDataProvider().verifyAndGetSigners(entry.getName(), zf.getInputStream(entry).readAllBytes()); - assertNull(cs); - } - - @Test // Contained a signed file, as well as a unsigned file. - void testPartial() throws Exception { - final var path = Paths.get("src", "test", "resources", "partial.zip"); - SecureJar jar = SecureJar.from(path); - ZipFile zf = new ZipFile(path.toFile()); - final var sentry = zf.getEntry("test/Signed.class"); - 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"), - () -> assertEquals("8c6124aab9db357d6616492b40d0ea4cd6e4f3f8", SecureJarVerifier.toHexString(MessageDigest.getInstance("SHA-1").digest(scs[0].getSignerCertPath().getCertificates().get(0).getEncoded())), "and the digest is correct for the code signer"), - () -> assertNotNull(jar.getTrustedManifestEntries(sentry.getName()), "Has trusted manifest entries")); - final var uentry = zf.getEntry("test/UnSigned.class"); - final var ucs = jar.moduleDataProvider().verifyAndGetSigners(uentry.getName(), zf.getInputStream(uentry).readAllBytes()); - assertNull(ucs); - } - - @Test // Has a jar with only a manifest - void testEmptyJar() throws Exception { - final var path = Paths.get("src", "test", "resources", "empty.zip"); - SecureJar jar = SecureJar.from(path); - try (var is = Files.newInputStream(path)) { - ZipInputStream zis = new ZipInputStream(is); - for (var ze = zis.getNextEntry(); ze != null; ze = zis.getNextEntry()) { - if (SecureJarVerifier.isSigningRelated(ze.getName())) continue; - if (ze.isDirectory()) continue; - final var zeName = ze.getName(); - var cs = jar.moduleDataProvider().verifyAndGetSigners(ze.getName(), zis.readAllBytes()); - assertAll("Jar behaves correctly", - () -> assertNull(cs, "No code signers")); - } - } + assertThrows(UncheckedIOException.class, () -> Jar.of(path), "File does not exist"); } } diff --git a/loader/src/test/java/cpw/mods/modlauncher/TransformingClassLoaderTests.java b/loader/src/test/java/cpw/mods/modlauncher/TransformingClassLoaderTests.java index 44ad1259c..61b1d6960 100644 --- a/loader/src/test/java/cpw/mods/modlauncher/TransformingClassLoaderTests.java +++ b/loader/src/test/java/cpw/mods/modlauncher/TransformingClassLoaderTests.java @@ -18,6 +18,8 @@ import cpw.mods.cl.JarModuleFinder; import cpw.mods.jarhandling.SecureJar; +import cpw.mods.jarhandling.impl.Jar; +import java.io.IOException; import java.lang.module.Configuration; import java.lang.module.ModuleFinder; import java.nio.file.Path; @@ -51,8 +53,8 @@ void testClassLoader() throws Exception { assertEquals(aClass, newClass, "Class instance is the same from Class.forName and tcl.loadClass"); } - private Configuration createTestJarsConfiguration() { - SecureJar testJars = SecureJar.from(Path.of(System.getProperty("testJars.location"))); + private Configuration createTestJarsConfiguration() throws IOException { + SecureJar testJars = Jar.of(Path.of(System.getProperty("testJars.location"))); JarModuleFinder finder = JarModuleFinder.of(testJars); return ModuleLayer.boot().configuration().resolveAndBind(finder, ModuleFinder.ofSystem(), Set.of("cpw.mods.modlauncher.testjars")); } diff --git a/loader/src/test/java/net/neoforged/fml/loading/moddiscovery/providers/ProductionClientProviderTest.java b/loader/src/test/java/net/neoforged/fml/loading/moddiscovery/providers/ProductionClientProviderTest.java deleted file mode 100644 index 6e87d2ecc..000000000 --- a/loader/src/test/java/net/neoforged/fml/loading/moddiscovery/providers/ProductionClientProviderTest.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.loading.moddiscovery.providers; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.List; -import net.neoforged.fml.loading.MavenCoordinate; -import net.neoforged.fml.loading.moddiscovery.locators.ProductionClientProvider; -import org.junit.jupiter.api.Test; - -class ProductionClientProviderTest { - @Test - void testToString() { - var provider = new ProductionClientProvider(List.of()); - assertEquals("production client provider", provider.toString()); - - var providerPlus1 = new ProductionClientProvider(List.of(MavenCoordinate.parse("g:a:v"))); - assertEquals("production client provider +g:a:v", providerPlus1.toString()); - - var providerPlus2 = new ProductionClientProvider(List.of(MavenCoordinate.parse("g:a:v"), MavenCoordinate.parse("g:a:c:v"))); - assertEquals("production client provider +g:a:v +g:a:c:v", providerPlus2.toString()); - } -} diff --git a/loader/src/test/java/net/neoforged/fml/test/TestModFile.java b/loader/src/test/java/net/neoforged/fml/test/TestModFile.java index 9c3e17ac6..2c7aeada2 100644 --- a/loader/src/test/java/net/neoforged/fml/test/TestModFile.java +++ b/loader/src/test/java/net/neoforged/fml/test/TestModFile.java @@ -10,8 +10,9 @@ import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; import com.google.errorprone.annotations.CheckReturnValue; -import cpw.mods.jarhandling.JarContentsBuilder; +import cpw.mods.jarhandling.JarContents; import cpw.mods.jarhandling.SecureJar; +import cpw.mods.jarhandling.impl.Jar; import java.io.IOException; import java.nio.file.FileSystem; import net.neoforged.fml.loading.moddiscovery.ModFile; @@ -35,12 +36,10 @@ private TestModFile(SecureJar jar, FileSystem fileSystem, ModFileInfoParser pars this.compiler = new RuntimeCompiler(fileSystem); } - private static TestModFile buildFile(FileSystem fileSystem, ModFileInfoParser parser) { - var jc = new JarContentsBuilder() - .paths(fileSystem.getPath("/")) - .build(); + private static TestModFile buildFile(FileSystem fileSystem, ModFileInfoParser parser) throws IOException { + var jc = JarContents.ofPath(fileSystem.getPath("/")); var metadata = new ModJarMetadata(jc); - var sj = SecureJar.from(jc, metadata); + var sj = Jar.of(jc, metadata); var mod = new TestModFile(sj, fileSystem, parser); metadata.setModFile(mod); return mod; @@ -56,7 +55,7 @@ public void scan() { } @CheckReturnValue - public static TestModFile newInstance(@Language("toml") String modsDotToml) { + public static TestModFile newInstance(@Language("toml") String modsDotToml) throws IOException { final var fs = Jimfs.newFileSystem(Configuration.unix() .toBuilder() .setWorkingDirectory("/") diff --git a/loader/src/test/java/net/neoforged/neoforgespi/language/ScanDataTest.java b/loader/src/test/java/net/neoforged/neoforgespi/language/ScanDataTest.java index cd78ec7b4..783bbe362 100644 --- a/loader/src/test/java/net/neoforged/neoforgespi/language/ScanDataTest.java +++ b/loader/src/test/java/net/neoforged/neoforgespi/language/ScanDataTest.java @@ -162,7 +162,7 @@ class FieldTest { } } - private static TestModFile modFile() { + private static TestModFile modFile() throws IOException { return TestModFile.newInstance(""" modLoader="javafml" loaderVersion="[3,]"