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 songsInDb, String path) { for (String song : songsInDb) { if (song.equals(path)) return song; } return null; } static long scanSubdirectoryDefective(List filesOnDisk, List 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 filesOnDisk, List songsInDb) { long ops = 0; // Build hash map for O(1) song-by-path lookup Map songsByPath = new HashMap<>(); for (String song : songsInDb) { songsByPath.put(song, song); ops++; } // Build hash set for O(1) file-on-disk membership Set 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 filesOnDisk = new ArrayList<>(); List 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); } }