java-topology/defects/libgit2/unit/RefPathAvailableAlgorithm.java

251 lines
9.5 KiB
Java

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<String> packedRefs; // sorted
SlowChecker(List<String> 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<String> packedRefs; // sorted
FastChecker(List<String> 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<String> buildPackedRefs(int count) {
List<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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);
}
}