darktable+scribus: CWE-407 scan — 6 defects (3 darktable, 3 scribus)

darktable-0001: map_locations image diff g_list_find O(N*M) MEDIUM 375x
darktable-0002: tags _tag_add_tags_to_list g_list_find O(T*L) MEDIUM 375x
darktable-0003: map view clustering g_list_find(sel_imgs) O(I²×S) HIGH 160x
scribus-0001: getSortedStyleList retList.contains O(N²) MEDIUM 167x (×4 copies)
scribus-0002: getUsedPatterns results.contains O(I×R) MEDIUM 98x
scribus-0003: Selection::addItems m_SelList.contains O(N×M) MEDIUM 2100x
This commit is contained in:
russell@unturf.com 2026-03-30 11:28:25 -04:00
parent f4593ef84a
commit 13db08af50
10 changed files with 776 additions and 0 deletions

View file

@ -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);

View file

@ -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;
}

View file

@ -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);

Binary file not shown.

View file

@ -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<Integer> oldImgs, List<Integer> 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<Integer> oldImgs, List<Integer> newImgs) {
int ops = 0;
Set<Integer> newSet = new HashSet<>(newImgs); ops += newImgs.size();
for (int id : oldImgs) { newSet.contains(id); ops++; }
Set<Integer> oldSet = new HashSet<>(oldImgs); ops += oldImgs.size();
for (int id : newImgs) { oldSet.contains(id); ops++; }
return ops;
}
static void testMapLocationDiff() {
int N = 1000;
List<Integer> oldImgs = new ArrayList<>();
List<Integer> 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<Integer> existingTags, List<Integer> newTags) {
int ops = 0;
List<Integer> 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<Integer> existingTags, List<Integer> newTags) {
int ops = 0;
Set<Integer> 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<Integer> existing = new ArrayList<>();
List<Integer> 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<Integer> 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<Integer> selImgs, int[] clusterIds) {
long ops = 0;
Set<Integer> 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<Integer> 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.");
}
}

View file

@ -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<int>::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<int> for O(1) membership test alongside the
# ordered QList<int> 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<int> ScribusDoc::getSortedStyleList() const
{
QList<int> retList;
+ QSet<int> 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<int> retList2;
+ QSet<int> 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)

View file

@ -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<QString> 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<QString> 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)

View file

@ -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<QPointer<PageItem>> 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<PageItem*> 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<PageItem *>& items)
{
if (items.isEmpty())
return false;
+ // Build hash set for O(1) membership check instead of O(M) QList::contains
+ QSet<PageItem*> existing;
+ existing.reserve(m_SelList.count());
+ for (int i = 0; i < m_SelList.count(); ++i)
+ existing.insert(m_SelList.at(i).data());
+
QList< QPointer<PageItem> > 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);
}

Binary file not shown.

View file

@ -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<int>::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<Integer> 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<Integer> 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<int> companion */
static int sortedStyleListFixed(int styleCount, int[] parentIdx) {
int ops = 0;
List<Integer> retList = new ArrayList<>();
Set<Integer> retSet = new HashSet<>();
for (int i = 0; i < styleCount; i++) {
ops++;
if (!retSet.contains(i)) {
List<Integer> chain = new ArrayList<>();
Set<Integer> 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: 012...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<String> 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<String> 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<Integer> 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<Integer> 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.");
}
}