#!/usr/bin/env python3 """ diffindex.py - case-insensitive package/entry diff between two mw4index manifests. python3 diffindex.py [labelA] [labelB] Per package it prints: * name-set diff - which source assets exist on each side (the reliable signal) * blob multiset - stored-byte md5 counts, names ignored (a rough upper bound on how much payload differs; see the caveat in mw4index.py) Packages present on only one side are reported as A-ONLY / B-ONLY. """ import sys, collections def load(path): names = collections.defaultdict(set) blobs = collections.defaultdict(collections.Counter) sizes = collections.defaultdict(dict) with open(path, encoding="latin-1") as fh: for line in fh: line = line.rstrip("\n") if not line: continue pkg, _rid, name, dlen, _rlen, h = line.split("\t") pkg = pkg.lower() key = name.lower().replace("\\", "/") names[pkg].add(key) blobs[pkg][h] += 1 sizes[pkg][key] = int(dlen) return names, blobs, sizes def main(): if len(sys.argv) < 3: sys.exit(__doc__) la = sys.argv[3] if len(sys.argv) > 3 else "A" lb = sys.argv[4] if len(sys.argv) > 4 else "B" an, ab, asz = load(sys.argv[1]) bn, bb, bsz = load(sys.argv[2]) for pkg in sorted(set(an) | set(bn)): if pkg not in an: print(f"== {pkg}: {lb}-ONLY PACKAGE ({len(bn[pkg])} entries)") continue if pkg not in bn: print(f"== {pkg}: {la}-ONLY PACKAGE ({len(an[pkg])} entries)") continue aonly = sorted(an[pkg] - bn[pkg]) bonly = sorted(bn[pkg] - an[pkg]) shared = sum((ab[pkg] & bb[pkg]).values()) print(f"== {pkg}: {la}={len(an[pkg])} {lb}={len(bn[pkg])} | " f"names: common={len(an[pkg] & bn[pkg])} {la}_only={len(aonly)} {lb}_only={len(bonly)} | " f"blobs: identical={shared} {la}_uniq={sum(ab[pkg].values()) - shared} " f"{lb}_uniq={sum(bb[pkg].values()) - shared}") for n in aonly: print(f" +{la} {n} ({asz[pkg][n]}B)") for n in bonly: print(f" -{lb} {n} ({bsz[pkg][n]}B)") if __name__ == "__main__": main()