from __future__ import annotations
import argparse
import pathlib
import re
import sys
import tomllib
ROOT = pathlib.Path(__file__).resolve().parent
POLICY = ROOT / "index-coverage.toml"
OUTPUT = ROOT / "CLASSFILE_COVERAGE.md"
def policy(root: pathlib.Path) -> dict:
document = tomllib.loads((root / "index-coverage.toml").read_text(encoding="utf-8"))
if document.get("schema") != "sim.classfile-index-coverage/v1":
raise ValueError("index-coverage.toml: unsupported schema")
return document
def opcode_names(root: pathlib.Path, config: dict) -> list[str]:
path = root / config["opcode_manifest"]
lines = [line for line in path.read_text(encoding="utf-8").splitlines() if line and not line.startswith("#")]
if not lines or "mnemonic" not in lines[0].split("\t"):
raise ValueError(f"{path}: missing mnemonic column")
fields = lines[0].split("\t")
mnemonic = fields.index("mnemonic")
names = [line.split("\t")[mnemonic] for line in lines[1:]]
if len(names) != 256 or len(set(names)) != 256:
raise ValueError(f"{path}: expected 256 unique opcode rows, found {len(names)}")
return names
def braced_body(text: str, declaration: str, path: pathlib.Path) -> str:
match = re.search(declaration, text)
if match is None:
raise ValueError(f"{path}: coverage declaration not found")
start = text.find("{", match.start())
depth = 0
for offset in range(start, len(text)):
if text[offset] == "{":
depth += 1
elif text[offset] == "}":
depth -= 1
if depth == 0:
return text[start + 1 : offset]
raise ValueError(f"{path}: unterminated coverage declaration")
def constant_names(root: pathlib.Path, config: dict) -> list[str]:
path = root / config["constant_source"]
body = braced_body(
path.read_text(encoding="utf-8"),
rf"\bpub\s+enum\s+{re.escape(config['constant_enum'])}\s*{{",
path,
)
names = re.findall(r"(?m)^\s{4}([A-Z][A-Za-z0-9]*)\s*(?:\([^\n]*\)|\{|,)", body)
if not names or len(names) != len(set(names)):
raise ValueError(f"{path}: constant coverage is empty or ambiguous")
return names
def attribute_names(root: pathlib.Path, config: dict) -> list[str]:
path = root / config["attribute_source"]
body = braced_body(
path.read_text(encoding="utf-8"),
rf"\bpub\s+fn\s+{re.escape(config['attribute_function'])}\s*\(",
path,
)
names = sorted(set(re.findall(r'"([A-Z][A-Za-z0-9]+)"', body)))
if not names:
raise ValueError(f"{path}: standard attribute coverage is empty")
return names
def projection(opcodes: list[str], constants: list[str], attributes: list[str]) -> str:
rows = []
for kind, names in (("opcode", opcodes), ("constant", constants), ("attribute", attributes)):
rows.extend(f"| {kind} | `{name}` |" for name in names)
return """<!-- @generated by generate_index_coverage.py; DO NOT EDIT. -->
# JVM classfile coverage
This Index coverage projection is generated from the opcode manifest and the
owning constant and standard-attribute source declarations. It is not an
independent classfile inventory.
| Kind | Identity |
|---|---|
""" + "\n".join(rows) + (
f"\n\nTotals: {len(opcodes)} opcodes, {len(constants)} constants, "
f"{len(attributes)} standard attributes; coverage difference: 0.\n"
)
def source_files(workspace: pathlib.Path):
excluded = {".git", "target", ".meta-workspace"}
constellation = any(child.is_dir() and child.name.startswith("sim-") for child in workspace.iterdir())
for path in sorted(workspace.rglob("*")):
if not path.is_file() or path.suffix not in {".rs", ".toml", ".tsv", ".csv", ".json", ".yaml", ".yml"}:
continue
if any(part in excluded for part in path.parts):
continue
relative = path.relative_to(workspace)
if constellation and (not relative.parts or not relative.parts[0].startswith("sim-")):
continue
yield path
def crate_name(path: pathlib.Path, workspace: pathlib.Path) -> str:
for parent in (path.parent, *path.parents):
manifest = parent / "Cargo.toml"
if manifest.is_file() and workspace in (parent, *parent.parents):
match = re.search(r'(?m)^name\s*=\s*"([^"]+)"', manifest.read_text(encoding="utf-8"))
if match:
return match.group(1)
return ""
def guard_findings(root: pathlib.Path, workspace: pathlib.Path, config: dict, coverage: dict, opcodes: list[str], constants: list[str], attributes: list[str]) -> list[str]:
findings: list[str] = []
owner = config["owner_crate"]
generated = {root / item for item in config["generated_files"]}
authorities = {
root / "opcode-manifest.tsv",
root / coverage["constant_source"],
root / coverage["attribute_source"],
root / "src/opcode_generated.rs",
}
opcode_set = {name for name in opcodes if not name.startswith("reserved_")}
for path in source_files(workspace):
if path in generated or path in authorities:
continue
text = path.read_text(encoding="utf-8")
relative = path.relative_to(workspace)
named_inventory = re.search(
r"(?is)(?:const|static)\s+[A-Za-z0-9_]*(?:opcode|instruction|constant|attribute)[A-Za-z0-9_]*.*?=.*?;",
text,
)
if "tests" not in path.parts and named_inventory:
findings.append(f"{relative} contains a duplicate classfile inventory")
member = crate_name(path, workspace)
runtime_or_verifier = "runtime" in member or "verif" in member
parses_bytes = re.search(r"\b(?:ByteReader|read_u[124]|from_be_bytes|read_exact)\b", text)
classfile_context = re.search(r"(?i)classfile|CAFEBABE|constant_pool", text)
if member != owner and runtime_or_verifier and parses_bytes and classfile_context:
findings.append(f"{relative} parses classfile bytes outside {owner}")
production = "tests" not in path.parts and not path.name.endswith("_test.rs")
if production and re.search(r"from_utf8_lossy", text) and re.search(r"(?i)modified[_ -]?utf|classfile", text):
findings.append(f"{relative} performs a lossy modified UTF-8 crossing")
return findings
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--check", action="store_true", help="fail unless the generated projection is current")
parser.add_argument("--scan", action="store_true", help="enforce classfile source ownership guards")
parser.add_argument("--workspace-root", type=pathlib.Path, help="root searched by ownership guards")
args = parser.parse_args()
try:
document = policy(ROOT)
coverage = document["coverage"]
opcodes = opcode_names(ROOT, coverage)
constants = constant_names(ROOT, coverage)
attributes = attribute_names(ROOT, coverage)
expected = projection(opcodes, constants, attributes)
current = OUTPUT.is_file() and OUTPUT.read_text(encoding="utf-8") == expected
if not args.check:
OUTPUT.write_text(expected, encoding="utf-8")
current = True
workspace = (args.workspace_root or ROOT).resolve()
findings = guard_findings(
ROOT,
workspace,
document["guards"],
coverage,
opcodes,
constants,
attributes,
) if args.scan else []
except (KeyError, OSError, ValueError, tomllib.TOMLDecodeError) as error:
print(error, file=sys.stderr)
return 1
if not current:
print("generated classfile coverage is stale; run ./generate_index_coverage.py", file=sys.stderr)
for finding in findings:
print(finding, file=sys.stderr)
return 0 if current and not findings else 1
if __name__ == "__main__":
raise SystemExit(main())