70 lines
2.5 KiB
Diff
70 lines
2.5 KiB
Diff
# UNDF: UNDF-2026-000000841
|
|
# UNDF: (leave blank)
|
|
# CWE-407: LibraryWatcher::ScanSubdirectory O(F*S) FindSongByPath + files_on_disk.contains
|
|
# Severity: HIGH
|
|
# Speedup: ~250x at F=S=500 (typical music directory with 500 files)
|
|
#
|
|
# In ScanSubdirectory, two O(N^2) patterns exist:
|
|
#
|
|
# 1. FindSongByPath (line 358) does a linear scan of songs_in_db for each
|
|
# file on disk. With F files and S songs, this is O(F*S). The code even
|
|
# has a "// TODO: Make this faster" comment acknowledging the problem.
|
|
#
|
|
# 2. files_on_disk.contains (line 437) does a linear scan of the QStringList
|
|
# for each song in the database, another O(S*F).
|
|
#
|
|
# Fix: Build a QHash<QString,Song> from songs_in_db for O(1) lookup by path,
|
|
# and convert files_on_disk to a QSet<QString> for O(1) contains.
|
|
#
|
|
--- a/src/library/librarywatcher.cpp
|
|
+++ b/src/library/librarywatcher.cpp
|
|
@@ -293,6 +293,7 @@
|
|
QMap<QString, QStringList> album_art;
|
|
QStringList files_on_disk;
|
|
+ QSet<QString> files_on_disk_set; // O(1) membership test for deleted-song detection
|
|
SubdirectoryList my_new_subdirs;
|
|
|
|
// If a directory is moved then only its parent gets a changed notification,
|
|
@@ -336,6 +337,7 @@
|
|
album_art[dir_part] << child;
|
|
else if (!child_info.isHidden())
|
|
files_on_disk << child;
|
|
+ files_on_disk_set.insert(child);
|
|
}
|
|
}
|
|
}
|
|
@@ -345,8 +347,14 @@
|
|
// Ask the database for a list of files in this directory
|
|
SongList songs_in_db = t->FindSongsInSubdirectory(path);
|
|
|
|
+ // Build hash map for O(1) song-by-path lookup (was O(S) linear scan per file)
|
|
+ QHash<QString, Song> songs_by_path;
|
|
+ for (const Song& song : songs_in_db) {
|
|
+ songs_by_path.insert(song.url().toLocalFile(), song);
|
|
+ }
|
|
+
|
|
QSet<QString> cues_processed;
|
|
|
|
// Now compare the list from the database with the list of files on disk
|
|
@@ -355,7 +363,9 @@
|
|
|
|
// associated cue
|
|
QString matching_cue = NoExtensionPart(file) + ".cue";
|
|
|
|
- Song matching_song;
|
|
- if (FindSongByPath(songs_in_db, file, &matching_song)) {
|
|
+ Song matching_song;
|
|
+ auto it = songs_by_path.find(file);
|
|
+ if (it != songs_by_path.end()) {
|
|
+ matching_song = it.value();
|
|
uint matching_cue_mtime = GetMtimeForCue(matching_cue);
|
|
|
|
@@ -434,7 +444,7 @@
|
|
// Look for deleted songs
|
|
for (const Song& song : songs_in_db) {
|
|
if (!song.is_unavailable() &&
|
|
- !files_on_disk.contains(song.url().toLocalFile())) {
|
|
+ !files_on_disk_set.contains(song.url().toLocalFile())) {
|
|
qLog(Debug) << "Song deleted from disk:" << song.url().toLocalFile();
|
|
t->deleted_songs << song;
|
|
}
|