java-topology/defects/scribus/unit/test_scribus_0004.py
russell@unturf.com b71284e245 scribus+darktable: 5-MOAD scan; scribus-0004 CWE-407 file-save names.contains O(S^2) 375x; darktable COMPLETE
scribus-0004: scribus{150,170,171}format_save.cpp writes styles via
names.contains() on QList<QString> (from QMap::keys()) inside O(S) loop
— O(S²) per save. Fix: use lists.charStyles().contains() (QMap, O(log N))
directly. 6 defect sites across 3 format variants. 375x at N=1000.

darktable: 5-MOAD scan complete (darktable-0001..0005 + MOAD-0002/0003/0005
CLEAN already committed). No new defects found beyond prior scan.

SCAN-TODO: mark both targets as scanned with summary.
2026-04-03 14:38:26 -04:00

131 lines
4.7 KiB
Python

"""
scribus-0004: file saver names.contains O(N²) style filter on save
Simulates the defect in scribus{150,170,171}format_save.cpp where
QList<QString> names = lists.charStyleNames();
for style in styleList:
if names.contains(style.name()): // O(N) per iteration → O(N²) total
is replaced by:
for style in styleList:
if lists.charStyles().contains(style.name()): // O(log N) via QMap
"""
import time
import random
PYTHONUNBUFFERED = 1 # noqa: ensure unbuffered output reminder
# ---------------------------------------------------------------------------
# Defective: QList<QString>::contains inside for-loop → O(N²)
# Simulates: charStyleNames() returns QList (from QMap::keys()), then
# we call list.contains() for each style in styleList.
# ---------------------------------------------------------------------------
def write_styles_defective(style_names_list, used_names_set):
"""
style_names_list: all style names in sorted order (simulates styleList)
used_names_set: names that are 'used' (simulates ResourceCollection)
Defective: convert to list then do O(N) contains per iteration.
"""
ops = 0
# Simulate QList<QString> names = lists.charStyleNames()
# (QMap::keys() returns QList — O(K) copy)
names_as_list = list(used_names_set) # simulates QList<QString>
result = []
for style_name in style_names_list:
# QList::contains() — O(K) linear scan
for n in names_as_list:
ops += 1
if n == style_name:
result.append(style_name)
break
return ops, result
# ---------------------------------------------------------------------------
# Fixed: QMap<QString,QString>::contains → O(log N)
# Simulates: lists.charStyles().contains(charStyle.name())
# ---------------------------------------------------------------------------
def write_styles_fixed(style_names_list, used_names_set):
"""
Fixed: use the map directly for O(log N) lookup per style.
In Python we model QMap::contains as set membership (O(1)), which
matches the O(log N) QMap::contains behavior asymptotically vs O(N).
"""
ops = 0
result = []
for style_name in style_names_list:
# QMap::contains — O(log N) (simulated as O(1) hash set)
ops += 1
if style_name in used_names_set:
result.append(style_name)
return ops, result
# ---------------------------------------------------------------------------
# Benchmark
# ---------------------------------------------------------------------------
def benchmark(N, label):
"""
N: number of styles in the document
Half of all styles are 'used' (worst-case: used_names is also size N/2).
"""
all_styles = [f"style_{i}" for i in range(N)]
used_names = set(all_styles[:N // 2]) # first half are used
# Defective: O(N²) — list.contains inside loop
t0 = time.perf_counter()
defect_ops, defect_result = write_styles_defective(all_styles, used_names)
t1 = time.perf_counter()
defect_time = t1 - t0
# Fixed: O(N log N) → O(N) — map/set contains per style
t2 = time.perf_counter()
fixed_ops, fixed_result = write_styles_fixed(all_styles, used_names)
t3 = time.perf_counter()
fixed_time = t3 - t2
op_ratio = defect_ops / max(fixed_ops, 1)
time_ratio = defect_time / max(fixed_time, 1e-9)
# Verify correctness — both should produce same output
assert sorted(defect_result) == sorted(fixed_result), (
f"Result mismatch at N={N}: defect={len(defect_result)} fixed={len(fixed_result)}"
)
print(
f" N={N:5d}: defect_ops={defect_ops:8d} fixed_ops={fixed_ops:6d} "
f"op_ratio={op_ratio:7.1f}x "
f"defect_t={defect_time*1000:.1f}ms fixed_t={fixed_time*1000:.1f}ms "
f"time_ratio={time_ratio:.1f}x [{label}]"
)
return op_ratio
def main():
print("scribus-0004: file saver names.contains O(N²) — benchmark")
print("=" * 75)
ratio_100 = benchmark(100, "N=100")
ratio_500 = benchmark(500, "N=500")
ratio_1000 = benchmark(1000, "N=1000 (professional template)")
print()
print(f" op_ratio at N=100: {ratio_100:.1f}x")
print(f" op_ratio at N=500: {ratio_500:.1f}x")
print(f" op_ratio at N=1000: {ratio_1000:.1f}x")
# Assert meaningful speedup — O(N²) vs O(N) should show >10x at N=500
assert ratio_100 > 3, f"Expected >3x at N=100, got {ratio_100:.1f}x"
assert ratio_500 > 10, f"Expected >10x at N=500, got {ratio_500:.1f}x"
assert ratio_1000 > 20, f"Expected >20x at N=1000, got {ratio_1000:.1f}x"
print()
print("PASS — scribus-0004 confirmed: O(N²) file-save style filter speedup ✓")
if __name__ == "__main__":
main()