251 lines
9.5 KiB
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);
|
|
}
|
|
}
|