diff --git a/defects/darktable/patch/darktable-0001-map-locations-image-diff-list-find.patch b/defects/darktable/patch/darktable-0001-map-locations-image-diff-list-find.patch new file mode 100644 index 000000000..f24632c44 --- /dev/null +++ b/defects/darktable/patch/darktable-0001-map-locations-image-diff-list-find.patch @@ -0,0 +1,90 @@ +# UNDF: UNDF-2026-000000795 +# UNDF: (leave blank) +# darktable-0001: dt_map_location_update_images g_list_find O(N*M) image diff +# +# In src/common/map_locations.c, dt_map_location_update_images() computes the +# symmetric difference between old images (imgs) and new images (new_imgs) by +# calling g_list_find() inside two sequential for-loops. Each g_list_find() is +# O(M) on a GList, making each loop O(N*M). For a location covering N=1000 +# geotagged images with M=1000 new images, this is O(N*M) = 1,000,000 +# pointer comparisons instead of O(N+M) with a hash set. +# +# Similarly, dt_map_location_update_locations() does the same pattern with +# old_tags vs tags lists: two loops with g_list_find() inside each. +# +# Fix: build a GHashTable from one list, then iterate the other with O(1) +# hash lookups. +# +# Severity: MEDIUM — triggered on every map location update; scales with +# collection size. +--- a/src/common/map_locations.c ++++ b/src/common/map_locations.c +@@ -535,14 +535,22 @@ + // clean up locations which are not valid anymore ++ GHashTable *tag_set = g_hash_table_new(g_direct_hash, g_direct_equal); ++ for(GList *tag = (GList *)tags; tag; tag = g_list_next(tag)) ++ g_hash_table_add(tag_set, tag->data); ++ + for(GList *tag = old_tags; tag; tag = g_list_next(tag)) + { +- if(!g_list_find((GList *)tags, tag->data)) ++ if(!g_hash_table_contains(tag_set, tag->data)) + { + dt_tag_detach(GPOINTER_TO_INT(tag->data), imgid, + FALSE, FALSE); + } + } ++ g_hash_table_destroy(tag_set); + + // add new locations ++ GHashTable *old_set = g_hash_table_new(g_direct_hash, g_direct_equal); ++ for(GList *tag = old_tags; tag; tag = g_list_next(tag)) ++ g_hash_table_add(old_set, tag->data); ++ + for(GList *tag = (GList *)tags; tag; tag = g_list_next(tag)) + { +- if(!g_list_find(old_tags, tag->data)) ++ if(!g_hash_table_contains(old_set, tag->data)) + { + dt_tag_attach(GPOINTER_TO_INT(tag->data), imgid, + FALSE, FALSE); + } + } ++ g_hash_table_destroy(old_set); + g_list_free(old_tags); + } + +@@ -558,14 +566,22 @@ + gboolean res = FALSE; + // detach images which are not in location anymore ++ GHashTable *new_set = g_hash_table_new(g_direct_hash, g_direct_equal); ++ for(GList *img = new_imgs; img; img = g_list_next(img)) ++ g_hash_table_add(new_set, img->data); ++ + for(GList *img = imgs; img; img = g_list_next(img)) + { +- if(!g_list_find(new_imgs, img->data)) ++ if(!g_hash_table_contains(new_set, img->data)) + { + dt_tag_detach(ld->id, GPOINTER_TO_INT(img->data), FALSE, FALSE); + res = TRUE; + } + } ++ g_hash_table_destroy(new_set); + + // add new images to location ++ GHashTable *old_img_set = g_hash_table_new(g_direct_hash, g_direct_equal); ++ for(GList *img = imgs; img; img = g_list_next(img)) ++ g_hash_table_add(old_img_set, img->data); ++ + for(GList *img = new_imgs; img; img = g_list_next(img)) + { +- if(!g_list_find(imgs, img->data)) ++ if(!g_hash_table_contains(old_img_set, img->data)) + { + dt_tag_attach(ld->id, GPOINTER_TO_INT(img->data), FALSE, FALSE); + res = TRUE; + } + } ++ g_hash_table_destroy(old_img_set); + g_list_free(new_imgs); diff --git a/defects/darktable/patch/darktable-0002-tags-add-to-list-dedup.patch b/defects/darktable/patch/darktable-0002-tags-add-to-list-dedup.patch new file mode 100644 index 000000000..67d699e8a --- /dev/null +++ b/defects/darktable/patch/darktable-0002-tags-add-to-list-dedup.patch @@ -0,0 +1,85 @@ +# UNDF: UNDF-2026-000000796 +# UNDF: (leave blank) +# darktable-0002: _tag_add_tags_to_list g_list_find O(T*L) dedup +# +# In src/common/tags.c, _tag_add_tags_to_list() iterates over a list of tags +# to add, calling g_list_find(*list, t->data) on the accumulator list for each. +# As the accumulator grows, each find is O(L), making the overall complexity +# O(T*L) ≈ O(N²) when adding N tags. +# +# The same file also has _get_tb_removed_tag_string_values() and +# _get_tb_added_tag_string_values() which call g_list_find(a, b->data) and +# g_list_find(b, a->data) inside their respective loops — O(B*A) for the +# before/after tag diff used in undo operations. +# +# Fix: for _tag_add_tags_to_list, maintain a companion GHashTable for O(1) +# membership. For the tb_ functions, build a hash set from one side, iterate +# the other. +# +# Severity: MEDIUM — triggered during batch tag operations (e.g. applying tags +# to multiple selected images). With T=500 tags, ~125,000 comparisons instead +# of ~500. +--- a/src/common/tags.c ++++ b/src/common/tags.c +@@ -38,10 +38,13 @@ + static gchar *_get_tb_removed_tag_string_values(GList *before, + GList *after) + { +- GList *a = after; + gchar *tag_list = NULL; ++ GHashTable *after_set = g_hash_table_new(g_direct_hash, g_direct_equal); ++ for(GList *a = after; a; a = g_list_next(a)) ++ g_hash_table_add(after_set, a->data); ++ + for(GList *b = before; b; b = g_list_next(b)) + { +- if(!g_list_find(a, b->data)) ++ if(!g_hash_table_contains(after_set, b->data)) + { + dt_util_str_cat(&tag_list, "%d,", GPOINTER_TO_INT(b->data)); + } + } ++ g_hash_table_destroy(after_set); + if(tag_list) tag_list[strlen(tag_list) - 1] = '\0'; +@@ -54,10 +57,13 @@ + static gchar *_get_tb_added_tag_string_values(const dt_imgid_t img, + GList *before, + GList *after) + { +- GList *b = before; + gchar *tag_list = NULL; ++ GHashTable *before_set = g_hash_table_new(g_direct_hash, g_direct_equal); ++ for(GList *b = before; b; b = g_list_next(b)) ++ g_hash_table_add(before_set, b->data); ++ + for(GList *a = after; a; a = g_list_next(a)) + { +- if(!g_list_find(b, a->data)) ++ if(!g_hash_table_contains(before_set, a->data)) + { + // ...tag_list building... + } + } ++ g_hash_table_destroy(before_set); +@@ -378,6 +384,9 @@ + static gboolean _tag_add_tags_to_list(GList **list, + const GList *tags) + { + gboolean res = FALSE; ++ GHashTable *seen = g_hash_table_new(g_direct_hash, g_direct_equal); ++ for(GList *existing = *list; existing; existing = g_list_next(existing)) ++ g_hash_table_add(seen, existing->data); ++ + for(const GList *t = tags; t; t = g_list_next(t)) + { +- if(!g_list_find(*list, t->data)) ++ if(!g_hash_table_contains(seen, t->data)) + { + *list = g_list_prepend(*list, t->data); ++ g_hash_table_add(seen, t->data); + res = TRUE; + } + } ++ g_hash_table_destroy(seen); + return res; + } diff --git a/defects/darktable/patch/darktable-0003-map-view-selection-check.patch b/defects/darktable/patch/darktable-0003-map-view-selection-check.patch new file mode 100644 index 000000000..8e4663e9a --- /dev/null +++ b/defects/darktable/patch/darktable-0003-map-view-selection-check.patch @@ -0,0 +1,58 @@ +# UNDF: UNDF-2026-000000797 +# UNDF: (leave blank) +# darktable-0003: map view clustering g_list_find(sel_imgs) inside O(I*J) loop +# +# In src/views/map.c, the map clustering code iterates over all images (i-loop) +# and for each cluster, iterates again over all images (j-loop). Inside the +# j-loop, g_list_find(sel_imgs, GINT_TO_POINTER(p[j].imgid)) performs a linear +# scan of the selected-images list (length S). Total complexity: O(I*J*S) +# which for 5000 map images with 500 selected is O(5000*5000*500) = 12.5 +# billion pointer comparisons. +# +# The outer i-loop also calls g_list_find(sel_imgs, ...) for NOISE points +# and cluster heads — each O(S), contributing O(I*S) additional. +# +# Fix: build a GHashTable from sel_imgs before the loop, replace g_list_find +# with g_hash_table_contains for O(1) lookup. +# +# Severity: HIGH — map view is redrawn on every pan/zoom with the full image +# collection; O(I²×S) collapses to O(I²+S). +--- a/src/views/map.c ++++ b/src/views/map.c +@@ -1506,6 +1506,12 @@ + gboolean *processed = calloc(num_clusters+1,sizeof(gboolean)); + GList *sel_imgs = dt_act_on_get_images(FALSE, FALSE, FALSE); ++ ++ // Build hash set for O(1) selection membership test ++ GHashTable *sel_set = g_hash_table_new(g_direct_hash, g_direct_equal); ++ for(GList *si = sel_imgs; si; si = g_list_next(si)) ++ g_hash_table_add(sel_set, si->data); ++ + int group = -1; + for(i = 0; i< img_count; i++) + { +@@ -1523,8 +1529,7 @@ + if(sel_imgs) +- entry->selected_in_group = g_list_find((GList *)sel_imgs, +- GINT_TO_POINTER(entry->imgid)) +- ? TRUE : FALSE; ++ entry->selected_in_group = ++ g_hash_table_contains(sel_set, GINT_TO_POINTER(entry->imgid)); + lib->images = g_slist_prepend(lib->images, entry); +@@ -1540,8 +1545,7 @@ +- entry->selected_in_group = (sel_imgs && g_list_find((GList *)sel_imgs, +- GINT_TO_POINTER(p[i].imgid))) +- ? TRUE : FALSE; ++ entry->selected_in_group = sel_imgs ++ && g_hash_table_contains(sel_set, GINT_TO_POINTER(p[i].imgid)); +@@ -1557,7 +1561,7 @@ + if(sel_imgs && !entry->selected_in_group) + { +- if(g_list_find((GList *)sel_imgs, GINT_TO_POINTER(p[j].imgid))) ++ if(g_hash_table_contains(sel_set, GINT_TO_POINTER(p[j].imgid))) + entry->selected_in_group = TRUE; + } + } + free(processed); ++ g_hash_table_destroy(sel_set); + g_list_free(sel_imgs); diff --git a/defects/darktable/unit/DarktableTest.class b/defects/darktable/unit/DarktableTest.class new file mode 100644 index 000000000..db76a04ae Binary files /dev/null and b/defects/darktable/unit/DarktableTest.class differ diff --git a/defects/darktable/unit/DarktableTest.java b/defects/darktable/unit/DarktableTest.java new file mode 100644 index 000000000..b8e69e514 --- /dev/null +++ b/defects/darktable/unit/DarktableTest.java @@ -0,0 +1,162 @@ +import java.util.*; + +/** + * CWE-407 simulation tests for darktable defects. + * + * darktable-0001: map_locations image diff via g_list_find O(N*M) + * darktable-0002: _tag_add_tags_to_list dedup via g_list_find O(T*L) + * darktable-0003: map view clustering sel_imgs g_list_find inside O(I*J) loop + */ +public class DarktableTest { + + // --------------------------------------------------------------- + // darktable-0001: map_locations symmetric diff O(N*M) vs O(N+M) + // --------------------------------------------------------------- + + /** Defective: g_list_find inside loop — O(N*M) */ + static int mapLocationDiffDefective(List oldImgs, List newImgs) { + int ops = 0; + // detach old not in new + for (int id : oldImgs) { + for (int nid : newImgs) { ops++; if (nid == id) break; } + } + // attach new not in old + for (int id : newImgs) { + for (int oid : oldImgs) { ops++; if (oid == id) break; } + } + return ops; + } + + /** Fixed: GHashTable for O(1) lookup — O(N+M) */ + static int mapLocationDiffFixed(List oldImgs, List newImgs) { + int ops = 0; + Set newSet = new HashSet<>(newImgs); ops += newImgs.size(); + for (int id : oldImgs) { newSet.contains(id); ops++; } + Set oldSet = new HashSet<>(oldImgs); ops += oldImgs.size(); + for (int id : newImgs) { oldSet.contains(id); ops++; } + return ops; + } + + static void testMapLocationDiff() { + int N = 1000; + List oldImgs = new ArrayList<>(); + List newImgs = new ArrayList<>(); + for (int i = 0; i < N; i++) { oldImgs.add(i); newImgs.add(i + N / 2); } + + int defectOps = mapLocationDiffDefective(oldImgs, newImgs); + int fixedOps = mapLocationDiffFixed(oldImgs, newImgs); + double ratio = (double) defectOps / fixedOps; + + System.out.printf("darktable-0001 map_locations diff: defect=%d fixed=%d ratio=%.1fx%n", + defectOps, fixedOps, ratio); + assert ratio > 10 : "Expected >10x ratio, got " + ratio; + System.out.println(" PASS"); + } + + // --------------------------------------------------------------- + // darktable-0002: tag dedup via g_list_find O(T*L) ≈ O(N²) + // --------------------------------------------------------------- + + /** Defective: linear scan dedup */ + static int tagAddToListDefective(List existingTags, List newTags) { + int ops = 0; + List list = new ArrayList<>(existingTags); + for (int tag : newTags) { + boolean found = false; + for (int t : list) { ops++; if (t == tag) { found = true; break; } } + if (!found) list.add(tag); + } + return ops; + } + + /** Fixed: HashSet companion */ + static int tagAddToListFixed(List existingTags, List newTags) { + int ops = 0; + Set seen = new HashSet<>(existingTags); ops += existingTags.size(); + for (int tag : newTags) { + ops++; + if (!seen.contains(tag)) { seen.add(tag); } + } + return ops; + } + + static void testTagAddToList() { + int N = 500; + List existing = new ArrayList<>(); + List toAdd = new ArrayList<>(); + // all unique — worst case for dedup + for (int i = 0; i < N; i++) existing.add(i); + for (int i = N; i < 2 * N; i++) toAdd.add(i); + + int defectOps = tagAddToListDefective(existing, toAdd); + int fixedOps = tagAddToListFixed(existing, toAdd); + double ratio = (double) defectOps / fixedOps; + + System.out.printf("darktable-0002 tag dedup: defect=%d fixed=%d ratio=%.1fx%n", + defectOps, fixedOps, ratio); + assert ratio > 50 : "Expected >50x ratio, got " + ratio; + System.out.println(" PASS"); + } + + // --------------------------------------------------------------- + // darktable-0003: map view sel_imgs inside O(I*J) clustering loop + // --------------------------------------------------------------- + + /** Defective: g_list_find(sel_imgs, imgid) inside nested I×J loop — O(I²×S) */ + static long mapViewClusterDefective(int imgCount, List selImgs, int[] clusterIds) { + long ops = 0; + for (int i = 0; i < imgCount; i++) { + int group = clusterIds[i]; + for (int j = 0; j < imgCount; j++) { + if (clusterIds[j] == group) { + // linear scan of sel_imgs + for (int s : selImgs) { ops++; if (s == j) break; } + } + } + } + return ops; + } + + /** Fixed: HashSet for O(1) selection check — O(I²+S) */ + static long mapViewClusterFixed(int imgCount, List selImgs, int[] clusterIds) { + long ops = 0; + Set selSet = new HashSet<>(selImgs); ops += selImgs.size(); + for (int i = 0; i < imgCount; i++) { + int group = clusterIds[i]; + for (int j = 0; j < imgCount; j++) { + if (clusterIds[j] == group) { + selSet.contains(j); ops++; + } + } + } + return ops; + } + + static void testMapViewCluster() { + int I = 500; + int S = 200; + List selImgs = new ArrayList<>(); + for (int i = 0; i < S; i++) selImgs.add(i); + // All in one cluster — worst case + int[] clusterIds = new int[I]; + Arrays.fill(clusterIds, 1); + + long defectOps = mapViewClusterDefective(I, selImgs, clusterIds); + long fixedOps = mapViewClusterFixed(I, selImgs, clusterIds); + double ratio = (double) defectOps / fixedOps; + + System.out.printf("darktable-0003 map view cluster sel: defect=%d fixed=%d ratio=%.1fx%n", + defectOps, fixedOps, ratio); + assert ratio > 10 : "Expected >10x ratio, got " + ratio; + System.out.println(" PASS"); + } + + // --------------------------------------------------------------- + + public static void main(String[] args) { + testMapLocationDiff(); + testTagAddToList(); + testMapViewCluster(); + System.out.println("\nAll 3 darktable CWE-407 tests PASSED."); + } +} diff --git a/defects/scribus/patch/scribus-0001-sorted-style-list-contains.patch b/defects/scribus/patch/scribus-0001-sorted-style-list-contains.patch new file mode 100644 index 000000000..cef229503 --- /dev/null +++ b/defects/scribus/patch/scribus-0001-sorted-style-list-contains.patch @@ -0,0 +1,75 @@ +# UNDF: UNDF-2026-000000802 +# UNDF: (leave blank) +# scribus-0001: getSortedStyleList retList.contains O(N²) dedup +# +# In scribus/scribusdoc.cpp, four identical functions — getSortedStyleList(), +# getSortedCharStyleList(), getSortedTableStyleList(), getSortedCellStyleList() +# — walk the style parent chain and accumulate indices into retList using +# QList::contains() to dedup. QList::contains is O(N) per call, and the +# outer loop is O(N) over all styles, making overall complexity O(N²). +# +# Additionally, the inner while-loop walking up the parent chain accumulates +# into retList2 with retList2.contains(pp) — a minor O(depth²) per style. +# +# For a document with N=500 paragraph styles (plausible in long-form publishing +# with inherited house styles), this is ~125,000 linear scans instead of ~500 +# hash lookups. +# +# Fix: maintain a companion QSet for O(1) membership test alongside the +# ordered QList retList. +# +# Severity: MEDIUM — 4 identical defect sites; triggered on style +# reorder/display; scales with style count. +--- a/scribus/scribusdoc.cpp ++++ b/scribus/scribusdoc.cpp +@@ -1198,6 +1198,7 @@ + QList ScribusDoc::getSortedStyleList() const + { + QList retList; ++ QSet retSet; + for (int i = 0; i < m_docParagraphStyles.count(); ++i) + { + if (m_docParagraphStyles[i].parent().isEmpty()) + { +- if (!retList.contains(i)) ++ if (!retSet.contains(i)) ++ { + retList.append(i); ++ retSet.insert(i); ++ } + continue; + } + + QList retList2; ++ QSet retSet2; + ... + retList2.prepend(i); ++ retSet2.insert(i); + while ((!par.isEmpty()) && (par != name)) + { + int pp = m_docParagraphStyles.find(par); +- if ((pp >= 0) && (!retList2.contains(pp))) ++ if ((pp >= 0) && (!retSet2.contains(pp))) ++ { + retList2.prepend(pp); ++ retSet2.insert(pp); ++ } + par = (pp >= 0) ? m_docParagraphStyles[pp].parent() : QString(); + } + for (int r = 0; r < retList2.count(); ++r) + { +- if (!retList.contains(retList2[r])) ++ if (!retSet.contains(retList2[r])) ++ { + retList.append(retList2[r]); ++ retSet.insert(retList2[r]); ++ } + } + } + return retList; + } + + // Same fix applies identically to: + // getSortedCharStyleList() (line 1230) + // getSortedTableStyleList() (line 1262) + // getSortedCellStyleList() (line 1294) diff --git a/defects/scribus/patch/scribus-0002-get-used-patterns-contains.patch b/defects/scribus/patch/scribus-0002-get-used-patterns-contains.patch new file mode 100644 index 000000000..fe40fe08a --- /dev/null +++ b/defects/scribus/patch/scribus-0002-get-used-patterns-contains.patch @@ -0,0 +1,67 @@ +# UNDF: UNDF-2026-000000803 +# UNDF: (leave blank) +# scribus-0002: getUsedPatterns results.contains O(I×R) pattern collection +# +# In scribus/scribusdoc.cpp, getUsedPatterns() iterates over all page items +# and for each checks results.contains(pattern) — a QStringList linear scan +# O(R) where R is the number of results accumulated so far. With I page items +# each potentially contributing 3 pattern names (fill, stroke, mask), the +# overall complexity is O(3×I×R). For a document with 2000 items and 200 +# patterns, that is ~1.2M string comparisons. +# +# The same pattern repeats in getUsedPatternsSelection() and +# getUsedPatternsHelper() and getPatternDependencyList(). +# +# Fix: maintain a companion QSet for O(1) membership alongside the +# QStringList for ordered results. +# +# Severity: MEDIUM — triggered during document save, export, and pattern +# cleanup; scales with item count × pattern count. +--- a/scribus/scribusdoc.cpp ++++ b/scribus/scribusdoc.cpp +@@ -3933,6 +3933,7 @@ + QStringList ScribusDoc::getUsedPatterns() const + { + QStringList results; ++ QSet resultSet; + + for (PageItemIterator it(this, ...); *it; ++it) + { + const PageItem* currItem = *it; +- if ((!results.contains(currItem->pattern())) && ...) ++ if ((!resultSet.contains(currItem->pattern())) && ...) ++ { + results.append(currItem->pattern()); ++ resultSet.insert(currItem->pattern()); ++ } + if (!currItem->strokePattern().isEmpty()) + { +- if (!results.contains(currItem->strokePattern())) ++ if (!resultSet.contains(currItem->strokePattern())) ++ { + results.append(currItem->strokePattern()); ++ resultSet.insert(currItem->strokePattern()); ++ } + } + if (!currItem->patternMask().isEmpty()) + { +- if (!results.contains(currItem->patternMask())) ++ if (!resultSet.contains(currItem->patternMask())) ++ { + results.append(currItem->patternMask()); ++ resultSet.insert(currItem->patternMask()); ++ } + } + } + + // Same fix for the inner docPatterns loop (lines 3955-3983) +- // Replace results.contains(...) with resultSet.contains(...) ++ // and add resultSet.insert(...) after each results.append(...) + + return results; + } + + // Same fix applies to: + // getUsedPatternsSelection() (line 3988) + // getUsedPatternsHelper() (line 4036) + // getPatternDependencyList() (line 4087) diff --git a/defects/scribus/patch/scribus-0003-selection-addItems-contains.patch b/defects/scribus/patch/scribus-0003-selection-addItems-contains.patch new file mode 100644 index 000000000..6150c150b --- /dev/null +++ b/defects/scribus/patch/scribus-0003-selection-addItems-contains.patch @@ -0,0 +1,44 @@ +# UNDF: UNDF-2026-000000804 +# UNDF: (leave blank) +# scribus-0003: Selection::addItems m_SelList.contains O(N×M) +# +# In scribus/selection.cpp, addItems() iterates over the items to add (N) and +# for each calls m_SelList.contains(item), which is O(M) on the existing +# QList> selection list. Total: O(N×M). For a "Select All" +# on a 5000-item document adding to an existing selection of 2000, this is +# ~10M pointer comparisons. +# +# The single-item addItem() also calls m_SelList.contains(item) — O(M) per +# call — but is less severe since it adds one item at a time. +# +# Fix: build a QSet from m_SelList before the loop for O(1) lookup, +# or maintain a companion QSet as a class member. +# +# Severity: MEDIUM — triggered on every multi-select operation (rubber-band, +# Select All, group selection); scales with document item count. +--- a/scribus/selection.cpp ++++ b/scribus/selection.cpp +@@ -194,6 +194,12 @@ + bool Selection::addItems(const QList& items) + { + if (items.isEmpty()) + return false; + ++ // Build hash set for O(1) membership check instead of O(M) QList::contains ++ QSet existing; ++ existing.reserve(m_SelList.count()); ++ for (int i = 0; i < m_SelList.count(); ++i) ++ existing.insert(m_SelList.at(i).data()); ++ + QList< QPointer > toAdd; + toAdd.reserve(items.count()); + for (int i = 0; i < items.count(); ++i) + { + PageItem* item = items.at(i); +- if (m_SelList.contains(item)) ++ if (existing.contains(item)) + continue; + toAdd.append(item); ++ existing.insert(item); + item->setSelected(true); + } diff --git a/defects/scribus/unit/ScribusTest.class b/defects/scribus/unit/ScribusTest.class new file mode 100644 index 000000000..fb366b131 Binary files /dev/null and b/defects/scribus/unit/ScribusTest.class differ diff --git a/defects/scribus/unit/ScribusTest.java b/defects/scribus/unit/ScribusTest.java new file mode 100644 index 000000000..1aa432b97 --- /dev/null +++ b/defects/scribus/unit/ScribusTest.java @@ -0,0 +1,195 @@ +import java.util.*; + +/** + * CWE-407 simulation tests for Scribus defects. + * + * scribus-0001: getSortedStyleList retList.contains O(N²) dedup + * scribus-0002: getUsedPatterns results.contains O(I×R) pattern accumulation + * scribus-0003: Selection::addItems m_SelList.contains O(N×M) selection dedup + */ +public class ScribusTest { + + // --------------------------------------------------------------- + // scribus-0001: getSortedStyleList — QList::contains inside loop + // Simulates walking parent chains and deduplicating into retList + // --------------------------------------------------------------- + + /** Defective: List.contains O(N) inside O(N) loop */ + static int sortedStyleListDefective(int styleCount, int[] parentIdx) { + int ops = 0; + List retList = new ArrayList<>(); + for (int i = 0; i < styleCount; i++) { + // Check if already in retList + for (int x : retList) { ops++; if (x == i) break; } + if (!retList.contains(i)) { + // Walk parent chain + List chain = new ArrayList<>(); + chain.add(i); + int p = parentIdx[i]; + while (p >= 0) { + boolean found = false; + for (int x : chain) { ops++; if (x == p) { found = true; break; } } + if (!found) chain.add(0, p); + p = parentIdx[p]; + } + // Merge chain into retList with contains check + for (int idx : chain) { + boolean found = false; + for (int x : retList) { ops++; if (x == idx) { found = true; break; } } + if (!found) retList.add(idx); + } + } + } + return ops; + } + + /** Fixed: QSet companion */ + static int sortedStyleListFixed(int styleCount, int[] parentIdx) { + int ops = 0; + List retList = new ArrayList<>(); + Set retSet = new HashSet<>(); + for (int i = 0; i < styleCount; i++) { + ops++; + if (!retSet.contains(i)) { + List chain = new ArrayList<>(); + Set chainSet = new HashSet<>(); + chain.add(i); chainSet.add(i); ops++; + int p = parentIdx[i]; + while (p >= 0) { + ops++; + if (!chainSet.contains(p)) { chain.add(0, p); chainSet.add(p); } + p = parentIdx[p]; + } + for (int idx : chain) { + ops++; + if (!retSet.contains(idx)) { retList.add(idx); retSet.add(idx); } + } + } + } + return ops; + } + + static void testSortedStyleList() { + int N = 500; + int[] parentIdx = new int[N]; + // Linear parent chain: 0←1←2←...←N-1 (worst case) + parentIdx[0] = -1; + for (int i = 1; i < N; i++) parentIdx[i] = i - 1; + + int defectOps = sortedStyleListDefective(N, parentIdx); + int fixedOps = sortedStyleListFixed(N, parentIdx); + double ratio = (double) defectOps / fixedOps; + + System.out.printf("scribus-0001 sorted style list: defect=%d fixed=%d ratio=%.1fx%n", + defectOps, fixedOps, ratio); + assert ratio > 10 : "Expected >10x ratio, got " + ratio; + System.out.println(" PASS"); + } + + // --------------------------------------------------------------- + // scribus-0002: getUsedPatterns — QStringList::contains inside item loop + // --------------------------------------------------------------- + + /** Defective: linear scan for each pattern check */ + static int getUsedPatternsDefective(String[][] itemPatterns) { + int ops = 0; + List results = new ArrayList<>(); + for (String[] pats : itemPatterns) { + for (String pat : pats) { + if (pat == null || pat.isEmpty()) continue; + boolean found = false; + for (String r : results) { ops++; if (r.equals(pat)) { found = true; break; } } + if (!found) results.add(pat); + } + } + return ops; + } + + /** Fixed: HashSet companion */ + static int getUsedPatternsFixed(String[][] itemPatterns) { + int ops = 0; + Set resultSet = new HashSet<>(); + for (String[] pats : itemPatterns) { + for (String pat : pats) { + if (pat == null || pat.isEmpty()) continue; + ops++; + if (!resultSet.contains(pat)) resultSet.add(pat); + } + } + return ops; + } + + static void testGetUsedPatterns() { + int items = 2000; + int patsPerItem = 3; // fill, stroke, mask + int uniquePatterns = 200; + Random rng = new Random(42); + String[][] itemPatterns = new String[items][patsPerItem]; + for (int i = 0; i < items; i++) + for (int j = 0; j < patsPerItem; j++) + itemPatterns[i][j] = "pattern_" + rng.nextInt(uniquePatterns); + + int defectOps = getUsedPatternsDefective(itemPatterns); + int fixedOps = getUsedPatternsFixed(itemPatterns); + double ratio = (double) defectOps / fixedOps; + + System.out.printf("scribus-0002 used patterns: defect=%d fixed=%d ratio=%.1fx%n", + defectOps, fixedOps, ratio); + assert ratio > 10 : "Expected >10x ratio, got " + ratio; + System.out.println(" PASS"); + } + + // --------------------------------------------------------------- + // scribus-0003: Selection::addItems — m_SelList.contains inside loop + // --------------------------------------------------------------- + + /** Defective: QList::contains O(M) for each of N items */ + static int selectionAddItemsDefective(int existing, int toAdd) { + int ops = 0; + List selList = new ArrayList<>(); + for (int i = 0; i < existing; i++) selList.add(i); + + for (int i = existing; i < existing + toAdd; i++) { + // linear scan + for (int s : selList) { ops++; if (s == i) break; } + selList.add(i); + } + return ops; + } + + /** Fixed: QSet companion for O(1) check */ + static int selectionAddItemsFixed(int existing, int toAdd) { + int ops = 0; + Set selSet = new HashSet<>(); + for (int i = 0; i < existing; i++) { selSet.add(i); ops++; } + + for (int i = existing; i < existing + toAdd; i++) { + ops++; + if (!selSet.contains(i)) selSet.add(i); + } + return ops; + } + + static void testSelectionAddItems() { + int existing = 2000; + int toAdd = 3000; + + int defectOps = selectionAddItemsDefective(existing, toAdd); + int fixedOps = selectionAddItemsFixed(existing, toAdd); + double ratio = (double) defectOps / fixedOps; + + System.out.printf("scribus-0003 selection addItems: defect=%d fixed=%d ratio=%.1fx%n", + defectOps, fixedOps, ratio); + assert ratio > 10 : "Expected >10x ratio, got " + ratio; + System.out.println(" PASS"); + } + + // --------------------------------------------------------------- + + public static void main(String[] args) { + testSortedStyleList(); + testGetUsedPatterns(); + testSelectionAddItems(); + System.out.println("\nAll 3 Scribus CWE-407 tests PASSED."); + } +}