# 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()