Files
starRiverProperty/scripts/compare_jar_to_sources.py
T
反编译工作区 0a34c76a82 chore(v0.11): 全路径纳入版本库与走查整改
- .gitignore:显式放行全部 maven-*、scripts、dev-support、frontend、反1、artifacts、历史导出目录
- 新增跟踪:device-manager/device-sdk/legacy-public、davinci-manager、cwos-*、cwos-resource 等源码与附属资源
- davinci FileStorageManagerImpl:Feign Response 关闭、绝对 URL 拉流 SSRF 校验(协议/主机/解析地址)
- davinci OuterCallFeignClient:补充契约说明
- cwos-common-aks AksConstant:final 类 + 私有构造防误实例化
- device-manager DeviceConstant:沿用 DEFAULT_APPLICATIONID 拼写修正

Made-with: Cursor
2026-04-24 23:54:05 +08:00

101 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Compare a JAR's .class entries to decompiled .java tree (Maven src/main/java)."""
from __future__ import annotations
import argparse
import sys
import zipfile
from pathlib import Path
def jar_class_entries(jar_path: Path) -> set[str]:
"""FQN-like names for each .class (uses /), inner as Outer$Inner."""
out: set[str] = set()
with zipfile.ZipFile(jar_path, "r") as zf:
for name in zf.namelist():
if not name.endswith(".class") or "META-INF" in name:
continue
rel = name[:-6] # drop .class
out.add(rel.replace("/", "."))
return out
def java_fqns(src_root: Path) -> set[str]:
"""Best-effort FQN from path .../src/main/java/a/b/C.java -> a.b.C."""
src_root = src_root.resolve()
java_root = src_root / "src/main/java"
if not java_root.is_dir():
if src_root.name == "java" and (src_root.parent / "main").exists():
java_root = src_root
else:
return set()
fqns: set[str] = set()
for f in java_root.rglob("*.java"):
rel = f.relative_to(java_root).with_suffix("")
fqns.add(".".join(rel.parts))
return fqns
def outer_java_for_class(fqn: str, java_fqns: set[str]) -> str | None:
"""Map class FQN to expected .java outer name (strip $ inner)."""
if "$" in fqn:
outer = fqn.split("$", 1)[0]
else:
outer = fqn
if outer in java_fqns:
return outer
return None
def analyze(jar: Path, src_module_roots: list[Path]) -> dict:
classes = jar_class_entries(jar)
all_java: set[str] = set()
for root in src_module_roots:
if root.is_dir():
all_java |= java_fqns(root)
missing_outer: list[str] = []
for c in sorted(classes):
if c.startswith("module-info"):
continue
if outer_java_for_class(c, all_java) is None:
missing_outer.append(c)
# "extra" java: outer types not present as any class in jar (rough)
jar_outers = {x.split("$", 1)[0] for x in classes if not x.startswith("module-info")}
extra_java = sorted(x for x in all_java if x not in jar_outers and "$" not in x)
return {
"jar": str(jar),
"class_count": len(classes),
"java_outer_count": len(all_java),
"missing_classes": missing_outer,
"possibly_extra_sources": extra_java[:200], # cap
"possibly_extra_sources_total": len(extra_java),
}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("jar", type=Path)
ap.add_argument("src_roots", nargs="+", type=Path, help="Module dirs containing src/main/java")
args = ap.parse_args()
r = analyze(args.jar, args.src_roots)
print(f"JAR: {r['jar']}")
print(f"Classes in JAR: {r['class_count']}; outer .java types under src/main/java: {r['java_outer_count']}")
miss = r["missing_classes"]
print(f"Missing (no matching outer .java): {len(miss)}")
for line in miss[:80]:
print(f" - {line}")
if len(miss) > 80:
print(f" ... and {len(miss) - 80} more")
ex = r["possibly_extra_sources"]
tot = r["possibly_extra_sources_total"]
print(f"Possibly extra .java (not as outer in JAR): {tot} (showing up to {len(ex)})")
for line in ex[:40]:
print(f" + {line}")
if tot > 40:
print(f" ...")
return 1 if miss else 0
if __name__ == "__main__":
sys.exit(main())