import java.util.*; /** * DigiKamXmpKeywordBagTest — digikam-0003 * * Reproduces the CWE-407 defect in MetaEngine::addToXmpTagStringBag and * DMetadata::addToXmpTagStringBag: QStringList::contains() called inside a * for-loop over old entries, making the merge/subtract O(O*N) instead of O(O+N). * * Models both the defective (ArrayList) and fixed (HashSet membership) variants, * measuring op-counts at K=200 keywords. */ public class DigiKamXmpKeywordBagTest { // Defective: mimics QStringList.contains() inside loop — O(O*N) static List addToXmpTagStringBagDefective(List oldEntries, List entriesToAdd) { List newEntries = new ArrayList<>(entriesToAdd); for (String old : oldEntries) { if (!newEntries.contains(old)) { // O(N) linear scan newEntries.add(old); } } return newEntries; } // Fixed: convert membership-checked list to HashSet first — O(O+N) static List addToXmpTagStringBagFixed(List oldEntries, List entriesToAdd) { List newEntries = new ArrayList<>(entriesToAdd); Set newEntriesSet = new HashSet<>(entriesToAdd); // O(N) build for (String old : oldEntries) { if (!newEntriesSet.contains(old)) { // O(1) lookup newEntries.add(old); newEntriesSet.add(old); } } return newEntries; } // Defective: mimics removeFromXmpTagStringBag with contains() in loop — O(C*R) static List removeFromXmpTagStringBagDefective(List currentEntries, List entriesToRemove) { List newEntries = new ArrayList<>(); for (String current : currentEntries) { if (!entriesToRemove.contains(current)) { // O(R) linear scan newEntries.add(current); } } return newEntries; } // Fixed: convert removal set to HashSet first — O(C+R) static List removeFromXmpTagStringBagFixed(List currentEntries, List entriesToRemove) { Set removeSet = new HashSet<>(entriesToRemove); // O(R) build List newEntries = new ArrayList<>(); for (String current : currentEntries) { if (!removeSet.contains(current)) { // O(1) lookup newEntries.add(current); } } return newEntries; } static long[] benchmarkAdd(int keywords) { // Build old entries (existing keywords in XMP bag) List oldEntries = new ArrayList<>(); for (int i = 0; i < keywords; i++) { oldEntries.add("keyword_" + i); } // entriesToAdd: half overlap, half new List entriesToAdd = new ArrayList<>(); for (int i = 0; i < keywords; i++) { entriesToAdd.add(i % 2 == 0 ? "keyword_" + i : "new_keyword_" + i); } int RUNS = 500; long start = System.nanoTime(); for (int r = 0; r < RUNS; r++) { addToXmpTagStringBagDefective(new ArrayList<>(oldEntries), new ArrayList<>(entriesToAdd)); } long defectiveNs = System.nanoTime() - start; start = System.nanoTime(); for (int r = 0; r < RUNS; r++) { addToXmpTagStringBagFixed(new ArrayList<>(oldEntries), new ArrayList<>(entriesToAdd)); } long fixedNs = System.nanoTime() - start; return new long[]{defectiveNs, fixedNs}; } static long[] benchmarkRemove(int keywords) { List currentEntries = new ArrayList<>(); for (int i = 0; i < keywords; i++) { currentEntries.add("keyword_" + i); } // entriesToRemove: half of them List entriesToRemove = new ArrayList<>(); for (int i = 0; i < keywords; i += 2) { entriesToRemove.add("keyword_" + i); } int RUNS = 500; long start = System.nanoTime(); for (int r = 0; r < RUNS; r++) { removeFromXmpTagStringBagDefective(new ArrayList<>(currentEntries), new ArrayList<>(entriesToRemove)); } long defectiveNs = System.nanoTime() - start; start = System.nanoTime(); for (int r = 0; r < RUNS; r++) { removeFromXmpTagStringBagFixed(new ArrayList<>(currentEntries), new ArrayList<>(entriesToRemove)); } long fixedNs = System.nanoTime() - start; return new long[]{defectiveNs, fixedNs}; } public static void main(String[] args) { System.out.println("DigiKamXmpKeywordBagTest — digikam-0003 CWE-407"); System.out.println("MetaEngine/DMetadata::addToXmpTagStringBag and removeFromXmpTagStringBag"); System.out.println("QStringList::contains() inside loop — O(O*N) vs O(O+N) with QSet"); System.out.println(); // Correctness checks List old = Arrays.asList("alpha", "beta", "gamma"); List toAdd = Arrays.asList("beta", "delta"); List defResult = addToXmpTagStringBagDefective(old, toAdd); List fixResult = addToXmpTagStringBagFixed(old, toAdd); // Both should contain: beta, delta, alpha, gamma (delta was new, alpha+gamma merged from old) assert defResult.containsAll(Arrays.asList("alpha", "beta", "gamma", "delta")) : "Defective: missing entries, got " + defResult; assert fixResult.containsAll(Arrays.asList("alpha", "beta", "gamma", "delta")) : "Fixed: missing entries, got " + fixResult; assert defResult.size() == fixResult.size() : "Size mismatch: defective=" + defResult.size() + " fixed=" + fixResult.size(); List current = Arrays.asList("alpha", "beta", "gamma", "delta"); List toRemove = Arrays.asList("beta", "delta"); List defRemResult = removeFromXmpTagStringBagDefective(current, toRemove); List fixRemResult = removeFromXmpTagStringBagFixed(current, toRemove); assert defRemResult.equals(Arrays.asList("alpha", "gamma")) : "Defective remove: expected [alpha, gamma], got " + defRemResult; assert fixRemResult.equals(Arrays.asList("alpha", "gamma")) : "Fixed remove: expected [alpha, gamma], got " + fixRemResult; System.out.println("PASS correctness (add and remove)"); // Benchmark int[] sizes = {50, 100, 200}; System.out.printf("%-10s %-18s %-18s %-10s%n", "Keywords", "Defective (ms)", "Fixed (ms)", "Ratio"); System.out.println("-".repeat(60)); for (int k : sizes) { long[] addResult = benchmarkAdd(k); double addRatio = (double) addResult[0] / addResult[1]; System.out.printf("add K=%-4d defective=%8.2f ms fixed=%8.2f ms ratio=%.1fx%n", k, addResult[0] / 1e6, addResult[1] / 1e6, addRatio); } for (int k : sizes) { long[] remResult = benchmarkRemove(k); double remRatio = (double) remResult[0] / remResult[1]; System.out.printf("rem K=%-4d defective=%8.2f ms fixed=%8.2f ms ratio=%.1fx%n", k, remResult[0] / 1e6, remResult[1] / 1e6, remRatio); } // Assert meaningful speedup at K=200 long[] result200 = benchmarkAdd(200); double ratio200 = (double) result200[0] / result200[1]; System.out.printf("%nK=200 add speedup ratio: %.1fx%n", ratio200); assert ratio200 >= 5.0 : "Expected >=5x speedup at K=200, got " + String.format("%.1f", ratio200) + "x"; System.out.println("PASS speedup assertion (>=5x at K=200)"); System.out.println("ALL PASS"); } }