java-topology/defects/clementine/test/ClementineLibraryWatcherTest.java
russell@unturf.com 669ed13408 clementine: 2 CWE-407 defects; mpd + rhythmbox CLEAN
clementine-0001: LibraryWatcher ScanSubdirectory FindSongByPath O(F*S) +
files_on_disk.contains O(S*F) — linear scan with TODO comment, fix with
HashMap + HashSet. HIGH severity, 250x at N=1000.

clementine-0002: SongSender indexOf(s) O(N^2) in SendAlbum/SendPlaylist/
SendUrls loops — fix with integer counter + QSet for requested_ids.
MEDIUM severity, 500x at N=1000.

MPD: CLEAN — uses std::set, bitmask arrays, std::map throughout.
Rhythmbox: CLEAN — uses g_hash_table for all membership checks.

4/4 unit tests PASS.
2026-03-30 14:42:15 -04:00

109 lines
3.7 KiB
Java

import java.util.*;
/**
* CWE-407 simulation: Clementine LibraryWatcher::ScanSubdirectory
* Two defects:
* 1. FindSongByPath: linear scan of songs_in_db per file -> O(F*S)
* 2. files_on_disk.contains: linear scan per song -> O(S*F)
*
* Fix: HashMap for song-by-path lookup, HashSet for files_on_disk membership.
*/
public class ClementineLibraryWatcherTest {
// --- Defective: linear scan to find song by path ---
static String findSongByPathLinear(List<String> songsInDb, String path) {
for (String song : songsInDb) {
if (song.equals(path)) return song;
}
return null;
}
static long scanSubdirectoryDefective(List<String> filesOnDisk, List<String> songsInDb) {
long ops = 0;
// Phase 1: For each file on disk, find matching song (linear scan)
for (String file : filesOnDisk) {
for (String song : songsInDb) {
ops++;
if (song.equals(file)) break;
}
}
// Phase 2: For each song in DB, check if file exists on disk (linear scan)
for (String song : songsInDb) {
for (String file : filesOnDisk) {
ops++;
if (file.equals(song)) break;
}
}
return ops;
}
// --- Fixed: HashMap + HashSet ---
static long scanSubdirectoryFixed(List<String> filesOnDisk, List<String> songsInDb) {
long ops = 0;
// Build hash map for O(1) song-by-path lookup
Map<String, String> songsByPath = new HashMap<>();
for (String song : songsInDb) {
songsByPath.put(song, song);
ops++;
}
// Build hash set for O(1) file-on-disk membership
Set<String> filesOnDiskSet = new HashSet<>(filesOnDisk);
ops += filesOnDisk.size();
// Phase 1: For each file, O(1) lookup
for (String file : filesOnDisk) {
songsByPath.get(file);
ops++;
}
// Phase 2: For each song, O(1) contains
for (String song : songsInDb) {
filesOnDiskSet.contains(song);
ops++;
}
return ops;
}
public static void main(String[] args) {
int[] sizes = {100, 250, 500, 1000};
System.out.println("=== Clementine LibraryWatcher ScanSubdirectory CWE-407 Test ===");
System.out.println("Defect: FindSongByPath O(F*S) linear scan + files_on_disk.contains O(S*F)");
System.out.println();
System.out.printf("%-8s %-15s %-15s %-10s %-6s%n",
"N", "Defective ops", "Fixed ops", "Ratio", "PASS");
boolean allPass = true;
for (int n : sizes) {
// Create matching file lists (worst case: all files exist in DB)
List<String> filesOnDisk = new ArrayList<>();
List<String> songsInDb = new ArrayList<>();
for (int i = 0; i < n; i++) {
String path = "/music/artist/album/track" + String.format("%04d", i) + ".mp3";
filesOnDisk.add(path);
songsInDb.add(path);
}
// Shuffle DB order to avoid best-case
Collections.shuffle(songsInDb, new Random(42));
long defectiveOps = scanSubdirectoryDefective(filesOnDisk, songsInDb);
long fixedOps = scanSubdirectoryFixed(filesOnDisk, songsInDb);
double ratio = (double) defectiveOps / fixedOps;
boolean pass = ratio > 5.0;
allPass &= pass;
System.out.printf("%-8d %-15d %-15d %-10.1fx %-6s%n",
n, defectiveOps, fixedOps, ratio, pass ? "PASS" : "FAIL");
}
System.out.println();
System.out.println("Overall: " + (allPass ? "PASS" : "FAIL"));
System.exit(allPass ? 0 : 1);
}
}