Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for stripping Jar signatures when recompiling/remapping Forge #47

Merged
merged 3 commits into from
Dec 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
Expand All @@ -34,15 +36,24 @@ public class InjectFromZipFileSource implements InjectSource {
*/
@Nullable
private final Pattern includeFilterPattern;
/**
* This function can modify the content that is being copied.
*/
private final ContentFilter contentFilter;

public InjectFromZipFileSource(ZipFile zf, String sourcePath) {
this(zf, sourcePath, null);
}

public InjectFromZipFileSource(ZipFile zf, String sourcePath, @Nullable Pattern includeFilterPattern) {
this(zf, sourcePath, includeFilterPattern, null);
}

public InjectFromZipFileSource(ZipFile zf, String sourcePath, @Nullable Pattern includeFilterPattern, @Nullable ContentFilter contentFilter) {
this.zf = zf;
this.sourcePath = sanitizeSourcePath(sourcePath);
this.includeFilterPattern = includeFilterPattern;
this.contentFilter = Objects.requireNonNullElse(contentFilter, ContentFilter.NONE);
}

private static String sanitizeSourcePath(String sourcePath) {
Expand Down Expand Up @@ -71,7 +82,7 @@ public CacheKey.AnnotatedValue getCacheKey(FileHashService fileHashService) thro
if ((sourcePath.isEmpty() || entry.getName().startsWith(sourcePath)) && matchesIncludeFilter(entry)) {
digestStream.write(entry.getName().getBytes());
try (var in = zf.getInputStream(entry)) {
in.transferTo(digestStream);
contentFilter.copy(entry, in, digestStream);
}
}
}
Expand Down Expand Up @@ -108,7 +119,7 @@ public void copyTo(ZipOutputStream out) throws IOException {
copiedEntry.setMethod(entry.getMethod());

out.putNextEntry(copiedEntry);
in.transferTo(out);
contentFilter.copy(entry, in, out);
out.closeEntry();
} catch (ZipException e) {
if (!e.getMessage().startsWith("duplicate entry:")) {
Expand All @@ -125,4 +136,11 @@ public void copyTo(ZipOutputStream out) throws IOException {
private boolean matchesIncludeFilter(ZipEntry entry) {
return includeFilterPattern == null || includeFilterPattern.matcher(entry.getName()).matches();
}

@FunctionalInterface
public interface ContentFilter {
ContentFilter NONE = (entry, in, out) -> in.transferTo(out);

void copy(ZipEntry entry, InputStream in, OutputStream out) throws IOException;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipInputStream;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package net.neoforged.neoform.runtime.actions;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;

/**
* This content filter will strip signature related attributes from MANIFEST.MF entries.
*/
public class StripManifestDigestContentFilter implements InjectFromZipFileSource.ContentFilter {
shartte marked this conversation as resolved.
Show resolved Hide resolved
// Theoretically, we'd need to check all digests that the VM supports, but we just go for the most common.
// https://docs.oracle.com/en/java/javase/21/docs/specs/jar/jar.html
private static final Set<Attributes.Name> SIGNATURE_ATTRIBUTES = Set.of(
new Attributes.Name("Magic"),
new Attributes.Name("SHA-256-Digest"),
new Attributes.Name("SHA1-Digest")
);

public static final StripManifestDigestContentFilter INSTANCE = new StripManifestDigestContentFilter();

private StripManifestDigestContentFilter() {
}

@Override
public void copy(ZipEntry entry, InputStream in, OutputStream out) throws IOException {
if (!entry.getName().equals("META-INF/MANIFEST.MF")) {
in.transferTo(out);
} else {
var manifest = new Manifest(in);

var it = manifest.getEntries().values().iterator();
while (it.hasNext()) {
// Remove all signing related attributes
var entryAttrs = it.next();
entryAttrs.keySet().removeIf(SIGNATURE_ATTRIBUTES::contains);
// Remove entries that no longer have attributes
if (entryAttrs.isEmpty()) {
it.remove();
}
}

manifest.write(out);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import net.neoforged.neoform.runtime.actions.MergeWithSourcesAction;
import net.neoforged.neoform.runtime.actions.PatchActionFactory;
import net.neoforged.neoform.runtime.actions.RecompileSourcesAction;
import net.neoforged.neoform.runtime.actions.StripManifestDigestContentFilter;
import net.neoforged.neoform.runtime.artifacts.ClasspathItem;
import net.neoforged.neoform.runtime.config.neoforge.NeoForgeConfig;
import net.neoforged.neoform.runtime.engine.NeoFormEngine;
Expand Down Expand Up @@ -112,6 +113,7 @@ protected void runWithNeoFormEngine(NeoFormEngine engine, List<AutoCloseable> cl
// When source remapping is in effect, we would normally have to remap the NeoForge sources as well
// To circumvent this, we inject the sources before recompile and disable the optimization of
// injecting the already compiled NeoForge classes later.
// Since remapping and recompiling will invariably change the digests, we also need to strip any signatures.
if (engine.getProcessGeneration().sourcesUseIntermediaryNames()) {
engine.applyTransforms(List.of(
new ModifyAction<>(
Expand All @@ -120,8 +122,18 @@ protected void runWithNeoFormEngine(NeoFormEngine engine, List<AutoCloseable> cl
action -> {
// Annoyingly, Forge only had the Java sources in the sources artifact.
// We have to pull resources from the universal jar.
action.getInjectedSources().add(new InjectFromZipFileSource(neoforgeClassesZip, "/", Pattern.compile("^(?!.*\\.class$).*")));
action.getInjectedSources().add(new InjectFromZipFileSource(neoforgeSourcesZip, "/"));
action.getInjectedSources().add(new InjectFromZipFileSource(
neoforgeClassesZip,
"/",
Pattern.compile("^(?!META-INF/[^/]+\\.(SF|RSA|DSA|EC)$|.*\\.class$).*"),
StripManifestDigestContentFilter.INSTANCE
));
action.getInjectedSources().add(new InjectFromZipFileSource(
neoforgeSourcesZip,
"/",
// The MCF sources have a bogus MANIFEST that should be ignored
Pattern.compile("^(?!META-INF/MANIFEST.MF$).*")
));
}
)
));
Expand Down
Loading