package workbench; import java.lang.reflect.Field; /** * Checks at runtime whether the javac topology patches are active. * * Verifies each patch by reflection — no flags, no trust-me properties. * * Patch 0001: GraphUtils$TarjanNode.active boolean field * (replaces O(V²) stack.contains(n) with O(1) field read) * Note: merged into OpenJDK 21+ upstream — will show PRESENT on JDK 21+ * even without --patch-module. On older JDKs --patch-module is required. * * Patch 0004: Dependencies — List.contains+add → LinkedHashSet.add * We cannot reliably reflect on the internal node structure without * --add-opens; detected via the presence of a specific inner class shape. * Falls back to "cannot verify" if reflection is blocked. * * Requires --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED * * Run standalone (unpatched runtime): * java --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \ * -cp . workbench.PatchVerifier * * Run with --patch-module applied: * java --patch-module jdk.compiler=/tmp/jt-all-classes \ * --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \ * -cp . workbench.PatchVerifier */ public class PatchVerifier { private static final String TARJAN_NODE = "com.sun.tools.javac.util.GraphUtils$TarjanNode"; public record Check(String id, boolean present, String detail) { @Override public String toString() { return (present ? "[OK] " : "[--] ") + id + ": " + detail; } } public record Status(boolean allPresent, Check[] checks) { public boolean patched() { return allPresent; } @Override public String toString() { StringBuilder sb = new StringBuilder(); for (Check c : checks) sb.append(c).append(" | "); if (sb.length() > 3) sb.setLength(sb.length() - 3); return sb.toString(); } public String summary() { int ok = 0; for (Check c : checks) if (c.present()) ok++; return ok + "/" + checks.length + " patches verified"; } } public static Status check() { Check[] checks = { check0001(), check0004(), }; boolean all = true; for (Check c : checks) if (!c.present()) all = false; return new Status(all, checks); } /** 0001: GraphUtils$TarjanNode.active boolean field. */ private static Check check0001() { try { Class cls = Class.forName(TARJAN_NODE); try { Field f = cls.getDeclaredField("active"); return new Check("0001/GraphUtils.active", true, f.getType().getSimpleName() + " field present — O(V+E) Tarjan (may be upstream JDK)"); } catch (NoSuchFieldException e) { return new Check("0001/GraphUtils.active", false, "field missing — stock O(V²) stack.contains(); apply --patch-module"); } } catch (ClassNotFoundException e) { return new Check("0001/GraphUtils.active", false, "GraphUtils not accessible — need --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"); } catch (Exception e) { return new Check("0001/GraphUtils.active", false, "reflection error: " + e.getMessage()); } } /** 0004: Dependencies — LinkedHashSet instead of ArrayList for depsByKind. */ private static Check check0004() { // Generic type erasure means we can't see LinkedHashSet vs ArrayList via reflection. // Scan the raw class bytes of the inner Node class for the type descriptor. // Stock JDK: "java/util/ArrayList" in constant pool // Patched: "java/util/LinkedHashSet" in constant pool (ArrayList removed) // Node is nested: Dependencies$GraphDependencies$Node try { Class cls = Class.forName( "com.sun.tools.javac.util.Dependencies$GraphDependencies$Node"); String resourcePath = cls.getName().replace('.', '/') + ".class"; byte[] bytes; // Named module: use Module.getResourceAsStream (not ClassLoader) try (java.io.InputStream in = cls.getModule().getResourceAsStream(resourcePath)) { bytes = in == null ? null : in.readAllBytes(); } if (bytes == null) { return new Check("0004/Dependencies.LinkedHashSet", false, "class bytes not readable from class loader"); } String pool = new String(bytes, java.nio.charset.StandardCharsets.ISO_8859_1); boolean hasLinkedHashSet = pool.contains("java/util/LinkedHashSet"); boolean hasArrayList = pool.contains("java/util/ArrayList"); if (hasLinkedHashSet && !hasArrayList) { return new Check("0004/Dependencies.LinkedHashSet", true, "constant pool has LinkedHashSet, no ArrayList — O(1) add"); } else if (hasArrayList) { return new Check("0004/Dependencies.LinkedHashSet", false, "constant pool has ArrayList — stock O(N) List.contains+add"); } else { return new Check("0004/Dependencies.LinkedHashSet", false, "neither ArrayList nor LinkedHashSet in constant pool (inconclusive)"); } } catch (ClassNotFoundException e) { return new Check("0004/Dependencies.LinkedHashSet", false, "Dependencies not accessible — need --add-exports"); } catch (Exception e) { return new Check("0004/Dependencies.LinkedHashSet", false, "byte scan failed: " + e.getMessage()); } } /** Standalone: prints status, exits 0 if all checks pass. */ public static void main(String[] args) { System.out.println("java.home: " + System.getProperty("java.home")); System.out.println("java.version: " + System.getProperty("java.version")); System.out.println(); Status s = check(); for (Check c : s.checks()) System.out.println(" " + c); System.out.println(); System.out.println(" => " + s.summary()); System.exit(s.allPresent() ? 0 : 1); } }