java-topology/defects/digikam/test/DigiKamXmpKeywordBagTest.java
russell@unturf.com 16a8ba0900 digikam: 2 new defects (0003 MOAD-0001, 0004 MOAD-0004); lmms: all 5 MOADs CLEAN
digikam-0003 CWE-407: MetaEngine/DMetadata addToXmpTagStringBag and
removeFromXmpTagStringBag call QStringList::contains() inside a loop over
existing entries — O(O*N) per image during batch metadata write.
Fix: build QSet<QString> before the loop for O(1) lookup.
17x speedup at K=200 keywords (unit test PASS).

digikam-0004 CWE-312: O2::onVerificationReceived logs the full OAuth2
token exchange POST body (including client_secret_) via qDebug() at
GrantFlowAuthorizationCode completion — exposes cloud service credentials
in debug logs/stderr for Google Photos, Flickr, OneDrive integrations.
Fix: replace full-body dump with redacted log line.

lmms: all 5 MOADs CLEAN — std::find uses are non-hot, contains() calls
are on QHash/QMap/QSet, no credential logging, no leaked thread context,
no unsynchronized cache access.
2026-03-31 21:15:08 -04:00

175 lines
7.6 KiB
Java

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<String> addToXmpTagStringBagDefective(List<String> oldEntries, List<String> entriesToAdd) {
List<String> 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<String> addToXmpTagStringBagFixed(List<String> oldEntries, List<String> entriesToAdd) {
List<String> newEntries = new ArrayList<>(entriesToAdd);
Set<String> 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<String> removeFromXmpTagStringBagDefective(List<String> currentEntries, List<String> entriesToRemove) {
List<String> 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<String> removeFromXmpTagStringBagFixed(List<String> currentEntries, List<String> entriesToRemove) {
Set<String> removeSet = new HashSet<>(entriesToRemove); // O(R) build
List<String> 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<String> oldEntries = new ArrayList<>();
for (int i = 0; i < keywords; i++) {
oldEntries.add("keyword_" + i);
}
// entriesToAdd: half overlap, half new
List<String> 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<String> currentEntries = new ArrayList<>();
for (int i = 0; i < keywords; i++) {
currentEntries.add("keyword_" + i);
}
// entriesToRemove: half of them
List<String> 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<String> old = Arrays.asList("alpha", "beta", "gamma");
List<String> toAdd = Arrays.asList("beta", "delta");
List<String> defResult = addToXmpTagStringBagDefective(old, toAdd);
List<String> 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<String> current = Arrays.asList("alpha", "beta", "gamma", "delta");
List<String> toRemove = Arrays.asList("beta", "delta");
List<String> defRemResult = removeFromXmpTagStringBagDefective(current, toRemove);
List<String> 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");
}
}