Skip to content

Commit

Permalink
Add support for stripping Jar signatures when recompiling/remapping F…
Browse files Browse the repository at this point in the history
…orge (#47)

In "legacy" mode, we don't copy over the modding platform Jar
unmodified, instead we remap and recompile the source files while
copying over resources.

This breaks the Jar signatures contained in the MANIFEST.MF, so we need
to do the following:

- Don't copy signature related resources (META-INF/*.{SF,RSA,EC,DSA})
- Filter out any digests from the MANIFEST.MF we copy (since the SHA-256
values will obviously differ after remapping/recompiling)
- While we're at it, don't try to copy the bogus MANIFEST.MF from the
source artifact
  • Loading branch information
shartte authored Dec 7, 2024
1 parent 6bfe910 commit d0ac31e
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 5 deletions.
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 {
// 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

0 comments on commit d0ac31e

Please sign in to comment.