diff --git a/defects/openscad/patch/openscad-0001-amf-vertex-dedup.patch b/defects/openscad/patch/openscad-0001-amf-vertex-dedup.patch new file mode 100644 index 000000000..7f3a4caa0 --- /dev/null +++ b/defects/openscad/patch/openscad-0001-amf-vertex-dedup.patch @@ -0,0 +1,65 @@ +--- a/src/io/export_amf.cc ++++ b/src/io/export_amf.cc +@@ -36,6 +36,7 @@ + + #include + #include ++#include + #include + #include + #include +@@ -48,18 +49,33 @@ + struct vertex_str { + std::string x, y, z; + bool operator==(const vertex_str& rhs) { return x == rhs.x && y == rhs.y && z == rhs.z; } ++ bool operator==(const vertex_str& rhs) const { return x == rhs.x && y == rhs.y && z == rhs.z; } + }; + using vertex_vec = std::vector; + ++struct vertex_str_hash { ++ size_t operator()(const vertex_str& v) const { ++ size_t h = std::hash{}(v.x); ++ h ^= std::hash{}(v.y) + 0x9e3779b9 + (h << 6) + (h >> 2); ++ h ^= std::hash{}(v.z) + 0x9e3779b9 + (h << 6) + (h >> 2); ++ return h; ++ } ++}; ++ + #ifdef ENABLE_CGAL + using Vertex = CGAL_Polyhedron::Vertex; + using Point = Vertex::Point; + using VCI = CGAL_Polyhedron::Vertex_const_iterator; + using FCI = CGAL_Polyhedron::Facet_const_iterator; + using HFCC = CGAL_Polyhedron::Halfedge_around_facet_const_circulator; + #endif + +-static size_t add_vertex(std::vector& vertices, const Point& p) ++struct triangle { ++ size_t vi1, vi2, vi3; ++}; ++ ++static int objectid; ++ ++static size_t add_vertex(std::vector& vertices, ++ std::unordered_map& vertex_index, ++ const Point& p) + { + double x = CGAL::to_double(p.x()); + double y = CGAL::to_double(p.y()); + double z = CGAL::to_double(p.z()); + vertex_str vs{STR(x), STR(y), STR(z)}; +- auto vi = std::find(vertices.begin(), vertices.end(), vs); +- if (vi == vertices.end()) { +- vertices.push_back(vs); +- return vertices.size() - 1; ++ auto it = vertex_index.find(vs); ++ if (it == vertex_index.end()) { ++ size_t idx = vertices.size(); ++ vertices.push_back(vs); ++ vertex_index[vs] = idx; ++ return idx; + } else { +- return std::distance(vertices.begin(), vi); ++ return it->second; + } + } diff --git a/defects/openscad/test/OpenScadAmfVertexDedupTest.class b/defects/openscad/test/OpenScadAmfVertexDedupTest.class new file mode 100644 index 000000000..1c9d517fc Binary files /dev/null and b/defects/openscad/test/OpenScadAmfVertexDedupTest.class differ diff --git a/defects/openscad/test/OpenScadAmfVertexDedupTest.java b/defects/openscad/test/OpenScadAmfVertexDedupTest.java new file mode 100644 index 000000000..a47200aa9 --- /dev/null +++ b/defects/openscad/test/OpenScadAmfVertexDedupTest.java @@ -0,0 +1,106 @@ +import java.util.*; + +/** + * Unit test for OpenSCAD MOAD-0001: export_amf.cc add_vertex() O(V²) vertex dedup. + * + * Defect: add_vertex() uses std::find on a vector to deduplicate vertices + * during AMF export. For a mesh with V unique vertices, each insertion scans + * the entire vector → O(V²) total. + * + * Fix: maintain a parallel HashMap for O(1) lookup, keeping the vector for + * index-ordered output. + * + * CWE-407: Inefficient Algorithmic Complexity + */ +public class OpenScadAmfVertexDedupTest { + + // --- Defective: linear scan on list for every vertex --- + static int addVertexDefective(List vertices, String v) { + int idx = vertices.indexOf(v); + if (idx == -1) { + vertices.add(v); + return vertices.size() - 1; + } + return idx; + } + + // --- Fixed: HashMap for O(1) lookup --- + static int addVertexFixed(List vertices, Map index, String v) { + Integer idx = index.get(v); + if (idx == null) { + int newIdx = vertices.size(); + vertices.add(v); + index.put(v, newIdx); + return newIdx; + } + return idx; + } + + public static void main(String[] args) { + // Correctness test + testCorrectness(); + + // Performance test + testPerformance(500); + testPerformance(2000); + testPerformance(5000); + + System.out.println("ALL TESTS PASSED"); + } + + static void testCorrectness() { + List vertsDef = new ArrayList<>(); + List vertsFix = new ArrayList<>(); + Map index = new HashMap<>(); + + // Simulate vertices with some duplicates + String[] inputs = { + "1.0,2.0,3.0", "4.0,5.0,6.0", "1.0,2.0,3.0", + "7.0,8.0,9.0", "4.0,5.0,6.0", "10.0,11.0,12.0" + }; + + for (String v : inputs) { + int idxDef = addVertexDefective(vertsDef, v); + int idxFix = addVertexFixed(vertsFix, index, v); + assert idxDef == idxFix : "Index mismatch for " + v + ": " + idxDef + " vs " + idxFix; + } + + assert vertsDef.size() == vertsFix.size() : "Size mismatch"; + assert vertsDef.size() == 4 : "Expected 4 unique vertices, got " + vertsDef.size(); + + for (int i = 0; i < vertsDef.size(); i++) { + assert vertsDef.get(i).equals(vertsFix.get(i)) : "Content mismatch at " + i; + } + + System.out.println("PASS correctness"); + } + + static void testPerformance(int V) { + // Generate V unique vertices, then re-insert them all (simulates mesh with shared vertices) + String[] verts = new String[V]; + for (int i = 0; i < V; i++) { + verts[i] = i + ".0," + (i * 2) + ".0," + (i * 3) + ".0"; + } + + // Defective path: insert all unique, then look up all again + List vertsDef = new ArrayList<>(); + long t0 = System.nanoTime(); + for (String v : verts) addVertexDefective(vertsDef, v); + for (String v : verts) addVertexDefective(vertsDef, v); // all hits = full scan each + long defectNs = System.nanoTime() - t0; + + // Fixed path + List vertsFix = new ArrayList<>(); + Map index = new HashMap<>(); + long t1 = System.nanoTime(); + for (String v : verts) addVertexFixed(vertsFix, index, v); + for (String v : verts) addVertexFixed(vertsFix, index, v); + long fixedNs = System.nanoTime() - t1; + + double ratio = (double) defectNs / fixedNs; + System.out.printf("PASS V=%d defect=%dms fixed=%dms ratio=%.1fx%n", + V, defectNs / 1_000_000, fixedNs / 1_000_000, ratio); + + assert ratio > 2.0 : "Expected ratio > 2x at V=" + V + " but got " + ratio; + } +}