package unit; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Models libgit2's reference_path_available() — given a sorted list of packed * ref names, determine whether a candidate new ref name would collide with any * existing entry (i.e., the new ref name is a strict prefix component of some * existing ref: "refs/foo" conflicts with "refs/foo/bar"). * * SLOW: O(R) linear scan of every packed ref. * FAST: O(log R) binary search to the first entry >= candidate + "/". * * CWE-407: libgit2 src/libgit2/refdb_fs.c:1185 */ public class RefPathAvailableAlgorithm { // ------------------------------------------------------------------------- // Slow (defective) implementation — mirrors the current libgit2 C code. // ------------------------------------------------------------------------- static class SlowChecker { final List packedRefs; // sorted SlowChecker(List packedRefs) { this.packedRefs = packedRefs; } /** Returns true if newRef would collide as a directory component. */ boolean collides(String newRef) { int ops = 0; for (String existingRef : packedRefs) { ops++; int refLen = existingRef.length(); int newLen = newRef.length(); int cmpLen = Math.min(refLen, newLen); String lead = (refLen < newLen) ? newRef : existingRef; if (existingRef.regionMatches(0, newRef, 0, cmpLen) && lead.charAt(cmpLen) == '/') { lastOps = ops; return true; } } lastOps = ops; return false; } int lastOps; int totalOps(int runs) { return lastOps; } // per single call } // ------------------------------------------------------------------------- // Fast (fixed) implementation — O(log R) binary search. // ------------------------------------------------------------------------- static class FastChecker { final List packedRefs; // sorted FastChecker(List packedRefs) { this.packedRefs = packedRefs; } /** Returns true if newRef would collide as a directory component. */ boolean collides(String newRef) { String prefix = newRef + "/"; // Binary search for the first entry >= prefix int pos = Collections.binarySearch(packedRefs, prefix); if (pos < 0) pos = -(pos + 1); // insertion point ops = 1; // O(log R) — count as single logical search step if (pos >= packedRefs.size()) return false; return packedRefs.get(pos).startsWith(prefix); } int ops; } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- static List buildPackedRefs(int count) { List refs = new ArrayList<>(count); for (int i = 0; i < count; i++) { refs.add(String.format("refs/remotes/origin/branch-%07d", i)); } Collections.sort(refs); return refs; } // ------------------------------------------------------------------------- // Tests // ------------------------------------------------------------------------- static int passed = 0; static int total = 0; static void check(String label, boolean condition) { total++; if (condition) { passed++; System.out.println(" PASS " + label); } else { System.out.println(" FAIL " + label); } } public static void main(String[] args) { System.out.println("=== RefPathAvailableAlgorithm ==="); // --- Correctness: no collision --- { List refs = new ArrayList<>(); refs.add("refs/heads/main"); refs.add("refs/heads/next"); refs.add("refs/tags/v1.0"); Collections.sort(refs); SlowChecker slow = new SlowChecker(refs); FastChecker fast = new FastChecker(refs); String candidate = "refs/heads/feature"; boolean slowResult = slow.collides(candidate); boolean fastResult = fast.collides(candidate); check("no-collision slow returns false", !slowResult); check("no-collision fast returns false", !fastResult); check("no-collision results agree", slowResult == fastResult); } // --- Correctness: collision (new ref is prefix of existing) --- { List refs = new ArrayList<>(); refs.add("refs/heads/foo/bar"); refs.add("refs/heads/foo/baz"); refs.add("refs/heads/zzz"); Collections.sort(refs); SlowChecker slow = new SlowChecker(refs); FastChecker fast = new FastChecker(refs); // "refs/heads/foo" would be a directory component of existing refs String candidate = "refs/heads/foo"; boolean slowResult = slow.collides(candidate); boolean fastResult = fast.collides(candidate); check("collision slow returns true", slowResult); check("collision fast returns true", fastResult); check("collision results agree", slowResult == fastResult); } // --- Correctness: near-miss (prefix but no slash) --- { List refs = new ArrayList<>(); refs.add("refs/heads/foobar"); Collections.sort(refs); SlowChecker slow = new SlowChecker(refs); FastChecker fast = new FastChecker(refs); // "refs/heads/foo" is a STRING prefix of "refs/heads/foobar" // but NOT a directory prefix (no slash after "foo") String candidate = "refs/heads/foo"; boolean slowResult = slow.collides(candidate); boolean fastResult = fast.collides(candidate); check("near-miss slow returns false", !slowResult); check("near-miss fast returns false", !fastResult); check("near-miss results agree", slowResult == fastResult); } // --- Correctness: empty cache --- { List refs = new ArrayList<>(); SlowChecker slow = new SlowChecker(refs); FastChecker fast = new FastChecker(refs); boolean slowResult = slow.collides("refs/heads/anything"); boolean fastResult = fast.collides("refs/heads/anything"); check("empty-cache slow returns false", !slowResult); check("empty-cache fast returns false", !fastResult); } // --- Correctness: candidate is before all entries --- { List refs = new ArrayList<>(); refs.add("refs/heads/zzz/child"); Collections.sort(refs); SlowChecker slow = new SlowChecker(refs); FastChecker fast = new FastChecker(refs); boolean slowResult = slow.collides("refs/heads/aaa"); boolean fastResult = fast.collides("refs/heads/aaa"); check("before-all slow returns false", !slowResult); check("before-all fast returns false", !fastResult); check("before-all results agree", slowResult == fastResult); } // --- Performance: O(n²) vs O(n log n) --- { int R = 100_000; List refs = buildPackedRefs(R); // Candidate that collides with last entry to force full scan in slow: // We pick a ref name that IS a prefix of many entries. // Add a colliding ref: refs.add("refs/remotes/origin/branch-0000000"); // new_ref that would collide: "refs/remotes/origin" collides if // "refs/remotes/origin/..." exist — but "refs/remotes/origin" itself // is not in the list. Let's test a true no-collision near the end // to force full scan. Collections.sort(refs); // For slow, worst case: no collision but must scan all R entries. String noCollisionCandidate = "refs/zzz/new"; SlowChecker slow = new SlowChecker(refs); FastChecker fast = new FastChecker(refs); long t0 = System.nanoTime(); int slowRuns = 1000; for (int i = 0; i < slowRuns; i++) slow.collides(noCollisionCandidate); long slowNs = System.nanoTime() - t0; long t1 = System.nanoTime(); int fastRuns = 1000; for (int i = 0; i < fastRuns; i++) fast.collides(noCollisionCandidate); long fastNs = System.nanoTime() - t1; // Slow must scan all R entries per call. Fast does O(log R). // We verify slow.lastOps = R, fast.ops = 1 (symbolic). boolean slowScansAll = (slow.lastOps == refs.size()); boolean fastIsLogN = (fast.ops == 1); double ratio = (double) slowNs / fastNs; System.out.printf(" INFO slow=%d ops/call fast=O(logN) ratio=%.1fx%n", slow.lastOps, ratio); check("slow scans all R entries", slowScansAll); check("fast uses binary search", fastIsLogN); check("fast is meaningfully faster (>= 5x)", ratio >= 5.0); } System.out.println(); System.out.printf("%d/%d PASS%n", passed, total); if (passed != total) System.exit(1); } }