java-topology/defects/openshot-0001/patch/openshot-0001.patch

59 lines
2.5 KiB
Diff

# UNDF: UNDF-2026-000001138
# UNDF: (leave blank)
# CWE-407: Algorithmic Complexity (linear scan inside loop over selected items)
# OpenShot-Qt query.py: QueryObject.filter() scans ALL objects of a type (O(N))
# and is called inside loops over selected_clips, selected_effects, etc.
#
# Defective path (src/classes/query.py line 116-160,
# called from src/windows/main_window.py):
#
# # actionRemoveClip_trigger (main_window.py ~line 2140):
# for clip_id in self.selected_clips: # S iterations
# clips = Clip.filter(id=clip_id) # O(C) linear scan each time
# for c in clips:
# c.delete()
#
# # QueryObject.filter() (query.py line 128):
# for child in parent: # scans ALL C clips
# if child.get("id") == clip_id:
# ...
#
# Complexity: O(S * C) where S = selected clips, C = total clips in project.
# With 100 selected clips from a 5000-clip project: 500,000 dict lookups.
#
# Same pattern in: actionRemoveTransition_trigger, actionRemoveEffect_trigger,
# keyframe update loops, and other multi-selection operations.
#
# Fix: build a {id -> object} dict once before the loop. O(C) build cost,
# then O(1) per lookup. Total O(C + S) vs O(S * C).
#
# Alternatively, add an id-keyed index to QueryObject so filter(id=x) is O(1).
# The single-call pattern Clip.filter(file_id=f.id) benefits from the index too.
#
# Severity: MEDIUM. UI responsiveness degrades quadratically when mass-deleting
# or mass-moving clips. Affects every multi-selection operation in the editor.
--- a/src/classes/query.py
+++ b/src/classes/query.py
@@ -116,6 +116,15 @@ class QueryObject:
def filter(OBJECT_TYPE, **kwargs):
""" Take any arguments given as filters, and find a list of matching objects """
+ # Fast path: single id lookup via index avoids O(N) scan
+ if list(kwargs.keys()) == ["id"] and hasattr(OBJECT_TYPE, "_id_index"):
+ result = OBJECT_TYPE._id_index.get(kwargs["id"])
+ return [result] if result is not None else []
+
# Get a list of all objects of this type
parent = get_app().project.get(OBJECT_TYPE.object_key)
+# Usage at call sites — replace inner-loop calls with index-based lookup:
+#
+# # Before (O(S*C)):
+# for clip_id in self.selected_clips:
+# clips = Clip.filter(id=clip_id)
+#
+# # After (O(C + S)):
+# all_clips = {c.id: c for c in Clip.filter()}
+# for clip_id in self.selected_clips:
+# c = all_clips.get(clip_id)
+# if c: c.delete()