package unit; import java.util.*; /** * Unit test for buildkit-0001: cacheKeyStorage.HasLink() CWE-407. * * Defect: HasLink() in the v1 remote cache importer calls * slices.Contains(it.links[l], target) where it.links[l] is a []string. * For K cache entries each with L links, repeated HasLink() calls cost O(K×L). * * File: cache/remotecache/v1/cachestorage.go:244 * Symbol: (*cacheKeyStorage).HasLink — `slices.Contains(it.links[l], target)` * * Fix: Change the links value type from []string to map[string]struct{}. * HasLink() becomes a map lookup: O(1). Building the map costs O(L) once. * Total cost for K entries: O(K×L) build + O(K) queries = O(K×L) → amortized * O(1) per query after build. Total work at K=L=100: from 10000 to ~200. * * Modeled here in Java: * - []string ≡ List (defective) * - map[string]struct{} ≡ Set (fixed) * - nlink key ≡ String (simplified) * - comparisons counted at the membership-test site * * Expected at K=L=100: * defective ≈ K × L = 10000 (worst-case: target not found → full scan) * fixed ≈ K + K×L = 10100 build + K = 10200 total, but query cost = K = 100 * ratio (query-only) > 10× */ public class BuildkitHasLinkTest { // ── Defective: links stored as List, HasLink = linear scan ──────── static class DefectiveCacheKeyStorage { // maps cacheEntryID → linkKey → []string (list of target IDs) final Map>> byID = new LinkedHashMap<>(); long comparisons = 0; void addLink(String entryID, String linkKey, String targetID) { byID.computeIfAbsent(entryID, k -> new LinkedHashMap<>()) .computeIfAbsent(linkKey, k -> new ArrayList<>()) .add(targetID); } /** slices.Contains equivalent: O(L) linear scan. */ boolean hasLink(String entryID, String linkKey, String target) { Map> links = byID.get(entryID); if (links == null) return false; List targets = links.get(linkKey); if (targets == null) return false; for (String t : targets) { comparisons++; if (t.equals(target)) return true; } return false; } } // ── Fixed: links stored as Set, HasLink = O(1) map lookup ───────── static class FixedCacheKeyStorage { // maps cacheEntryID → linkKey → map[string]struct{} (set of target IDs) final Map>> byID = new LinkedHashMap<>(); long buildComparisons = 0; // cost of building the sets long queryComparisons = 0; // cost of HasLink queries void addLink(String entryID, String linkKey, String targetID) { buildComparisons++; // one hash op per insertion byID.computeIfAbsent(entryID, k -> new LinkedHashMap<>()) .computeIfAbsent(linkKey, k -> new HashSet<>()) .add(targetID); } /** map lookup equivalent: O(1). */ boolean hasLink(String entryID, String linkKey, String target) { Map> links = byID.get(entryID); if (links == null) return false; Set targets = links.get(linkKey); if (targets == null) return false; queryComparisons++; // O(1) hash lookup return targets.contains(target); } long comparisons() { return buildComparisons + queryComparisons; } } // ── Simulation helpers ──────────────────────────────────────────────────── /** * Populate K cache entries, each with L links under a single linkKey. * Then query hasLink for each entry's last-inserted target (worst-case * for the defective linear scan: target is at end of list). */ public static long simulateDefective(int K, int L) { DefectiveCacheKeyStorage storage = new DefectiveCacheKeyStorage(); String linkKey = "lk:sha256:abc/0/selector"; String[] lastTarget = new String[K]; for (int k = 0; k < K; k++) { String entryID = "entry_" + k; for (int l = 0; l < L; l++) { String targetID = "target_" + k + "_" + l; storage.addLink(entryID, linkKey, targetID); lastTarget[k] = targetID; } } // Query: check each entry's last target (worst-case: at end of list → full scan) for (int k = 0; k < K; k++) { String entryID = "entry_" + k; boolean found = storage.hasLink(entryID, linkKey, lastTarget[k]); assert found : "hasLink should return true for known target"; } return storage.comparisons; } public static long simulateFixedQuery(int K, int L) { FixedCacheKeyStorage storage = new FixedCacheKeyStorage(); String linkKey = "lk:sha256:abc/0/selector"; String[] lastTarget = new String[K]; for (int k = 0; k < K; k++) { String entryID = "entry_" + k; for (int l = 0; l < L; l++) { String targetID = "target_" + k + "_" + l; storage.addLink(entryID, linkKey, targetID); lastTarget[k] = targetID; } } // same queries for (int k = 0; k < K; k++) { String entryID = "entry_" + k; boolean found = storage.hasLink(entryID, linkKey, lastTarget[k]); assert found : "hasLink should return true for known target"; } return storage.queryComparisons; // only query cost, not build } // ── Tests ───────────────────────────────────────────────────────────────── static void testCorrectnessMatch() { DefectiveCacheKeyStorage def = new DefectiveCacheKeyStorage(); FixedCacheKeyStorage fix = new FixedCacheKeyStorage(); String linkKey = "lk:sha256:test/0/"; for (int i = 0; i < 5; i++) { String eid = "e_" + i; for (int j = 0; j < 5; j++) { String tid = "t_" + i + "_" + j; def.addLink(eid, linkKey, tid); fix.addLink(eid, linkKey, tid); } } // Check positive cases for (int i = 0; i < 5; i++) { String eid = "e_" + i; for (int j = 0; j < 5; j++) { String tid = "t_" + i + "_" + j; boolean d = def.hasLink(eid, linkKey, tid); boolean f = fix.hasLink(eid, linkKey, tid); assert d == f : "hasLink mismatch for " + eid + "/" + tid + ": def=" + d + " fix=" + f; assert d : "expected true for known link"; } } // Check negative cases boolean d = def.hasLink("e_0", linkKey, "nonexistent"); boolean f = fix.hasLink("e_0", linkKey, "nonexistent"); assert !d && !f : "hasLink should return false for unknown target"; System.out.println("PASS testCorrectnessMatch"); } static void testDefectiveGrowsQuadratically() { long prev = -1; for (int S : new int[]{10, 20, 40}) { long c = simulateDefective(S, S); if (prev > 0) { double ratio = (double) c / prev; assert ratio > 3.0 : "defective should grow >3x when K=L doubled; got " + ratio + " at K=L=" + S; } prev = c; } System.out.println("PASS testDefectiveGrowsQuadratically"); } static void testFixedQueryGrowsLinearly() { long prev = -1; for (int S : new int[]{10, 20, 40}) { long c = simulateFixedQuery(S, S); if (prev > 0) { double ratio = (double) c / prev; assert ratio < 2.5 : "fixed query cost should grow ~2x when K doubled; got " + ratio + " at K=L=" + S; } prev = c; } System.out.println("PASS testFixedQueryGrowsLinearly"); } static void testRatioAtScale() { int K = 100, L = 100; long defComp = simulateDefective(K, L); long fixQuery = simulateFixedQuery(K, L); double ratio = (double) defComp / fixQuery; // defective: K queries × L comparisons each = K*L = 10000 assert defComp == (long) K * L : "defective comparisons should be K*L=" + (K * L) + "; got " + defComp; // fixed query: K queries × 1 = K = 100 assert fixQuery == K : "fixed query comparisons should be K=" + K + "; got " + fixQuery; assert ratio > 10.0 : "ratio should be >10x at K=L=100; got " + ratio; System.out.printf( "PASS testRatioAtScale (defective=%d, fixed_query=%d, ratio=%.1fx)%n", defComp, fixQuery, ratio); } public static void main(String[] args) { testCorrectnessMatch(); testDefectiveGrowsQuadratically(); testFixedQueryGrowsLinearly(); testRatioAtScale(); System.out.println("All buildkit-0001 tests passed."); } }