package unit; import java.util.*; /** * LibGDXTest — libgdx-0001..0004 * * Proves CWE-407 in libGDX: * libgdx-0001: Model.loadNode() — nested for-loop string-ID scan O(parts × meshes + parts × materials) * libgdx-0002: ModelBuilder.rebuildReferences() — Array.contains() in node-part loop O(parts × materials) * libgdx-0003: ModelInstance.invalidate() — Array.contains() in node-part loop O(parts × materials) * libgdx-0004: Kerning.readSubtable2() — IntArray.contains() in GPOS coverage loop O(coverage × classes × K) * * Run: javac -d . LibGDXTest.java && java -ea unit.LibGDXTest */ public class LibGDXTest { static void bench(String label, Runnable slow, Runnable fast, long sOps, long fOps) { slow.run(); fast.run(); // warmup long t0 = System.nanoTime(); slow.run(); long sMs = (System.nanoTime() - t0) / 1_000_000; long t1 = System.nanoTime(); fast.run(); long fMs = (System.nanoTime() - t1) / 1_000_000; double r = fOps > 0 ? (double) sOps / fOps : 0; System.out.printf(" %-52s slow:%4dms (%,d ops) fast:%4dms (%,d ops) speedup:%.0fx%n", label, sMs, sOps, fMs, fOps, r); } // ── libgdx-0001: Model.loadNode nested for-loop string scan ────────────── /** * SLOW: simulates Model.loadNode() — for each node-part, scan all meshParts * (by string ID) and all materials (by string ID). * O(parts × (meshCount + materialCount)) */ static long loadNodeSlow(int nodePartCount, int meshCount, int materialCount) { // Build arrays of String IDs String[] meshIds = new String[meshCount]; String[] materialIds = new String[materialCount]; for (int i = 0; i < meshCount; i++) meshIds[i] = "mesh_" + i; for (int i = 0; i < materialCount; i++) materialIds[i] = "mat_" + i; long ops = 0; // Simulate loading nodePartCount node-parts, each referencing the last mesh/material String targetMesh = meshIds[meshCount - 1]; // worst case: always last String targetMat = materialIds[materialCount - 1]; for (int p = 0; p < nodePartCount; p++) { // for (MeshPart part : meshParts) { if id.equals(...) ... } — O(M) for (int m = 0; m < meshCount; m++) { ops++; if (meshIds[m].equals(targetMesh)) break; } // for (Material mat : materials) { if id.equals(...) ... } — O(T) for (int t = 0; t < materialCount; t++) { ops++; if (materialIds[t].equals(targetMat)) break; } } return ops; } /** * FAST: simulates fixed Model.loadNode() — build HashMap once * before the node loop, use O(1) get() per node-part. */ static long loadNodeFast(int nodePartCount, int meshCount, int materialCount) { String[] meshIds = new String[meshCount]; String[] materialIds = new String[materialCount]; for (int i = 0; i < meshCount; i++) meshIds[i] = "mesh_" + i; for (int i = 0; i < materialCount; i++) materialIds[i] = "mat_" + i; // Build lookup maps once — O(M + T) Map meshById = new HashMap<>(meshCount * 2); for (int i = 0; i < meshCount; i++) meshById.put(meshIds[i], i); Map matById = new HashMap<>(materialCount * 2); for (int i = 0; i < materialCount; i++) matById.put(materialIds[i], i); String targetMesh = meshIds[meshCount - 1]; String targetMat = materialIds[materialCount - 1]; long ops = 0; for (int p = 0; p < nodePartCount; p++) { ops++; meshById.get(targetMesh); // O(1) ops++; matById.get(targetMat); // O(1) } return ops; } // ── libgdx-0002: ModelBuilder.rebuildReferences Array.contains ─────────── /** * SLOW: simulates rebuildReferences() — for each node-part, call Array.contains() * (linear scan by identity) to check if material/meshPart/mesh is already added. * O(parts × (materials + meshParts + meshes)) */ static long rebuildRefsSlow(int nodePartCount, int materialCount) { List materials = new ArrayList<>(); List meshParts = new ArrayList<>(); List meshes = new ArrayList<>(); // Pre-create unique objects Object[] matObjs = new Object[materialCount]; Object[] partObjs = new Object[materialCount]; Object[] meshObjs = new Object[materialCount]; for (int i = 0; i < materialCount; i++) { matObjs[i] = new Object(); partObjs[i] = new Object(); meshObjs[i] = new Object(); } long ops = 0; // Each node-part references materials/parts in round-robin (causes dedup checks) for (int p = 0; p < nodePartCount; p++) { Object mat = matObjs[p % materialCount]; Object part = partObjs[p % materialCount]; Object mesh = meshObjs[p % materialCount]; // Array.contains — linear scan by identity — O(M) boolean hasMat = false; for (Object m : materials) { ops++; if (m == mat) { hasMat = true; break; } } if (!hasMat) materials.add(mat); boolean hasPart = false; for (Object pp : meshParts) { ops++; if (pp == part) { hasPart = true; break; } } if (!hasPart) { meshParts.add(part); boolean hasMesh = false; for (Object msh : meshes) { ops++; if (msh == mesh) { hasMesh = true; break; } } if (!hasMesh) meshes.add(mesh); } } return ops; } /** * FAST: simulates fixed rebuildReferences() — IdentityHashMap for O(1) dedup. */ static long rebuildRefsFast(int nodePartCount, int materialCount) { IdentityHashMap matSeen = new IdentityHashMap<>(); IdentityHashMap partSeen = new IdentityHashMap<>(); IdentityHashMap meshSeen = new IdentityHashMap<>(); List materials = new ArrayList<>(); List meshParts = new ArrayList<>(); List meshes = new ArrayList<>(); Object[] matObjs = new Object[materialCount]; Object[] partObjs = new Object[materialCount]; Object[] meshObjs = new Object[materialCount]; for (int i = 0; i < materialCount; i++) { matObjs[i] = new Object(); partObjs[i] = new Object(); meshObjs[i] = new Object(); } long ops = 0; for (int p = 0; p < nodePartCount; p++) { Object mat = matObjs[p % materialCount]; Object part = partObjs[p % materialCount]; Object mesh = meshObjs[p % materialCount]; ops++; if (matSeen.put(mat, Boolean.TRUE) == null) materials.add(mat); // O(1) ops++; if (partSeen.put(part, Boolean.TRUE) == null) { meshParts.add(part); ops++; if (meshSeen.put(mesh, Boolean.TRUE) == null) meshes.add(mesh); // O(1) } } return ops; } // ── libgdx-0003: ModelInstance.invalidate Array.contains ───────────────── /** * SLOW: simulates ModelInstance.invalidate() — for each node-part, call * materials.contains(part.material, identity=true) — O(T) linear scan. */ static long invalidateSlow(int nodePartCount, int materialCount) { List materials = new ArrayList<>(); Object[] matObjs = new Object[materialCount]; for (int i = 0; i < materialCount; i++) matObjs[i] = new Object(); long ops = 0; for (int p = 0; p < nodePartCount; p++) { Object mat = matObjs[p % materialCount]; boolean found = false; for (Object m : materials) { ops++; if (m == mat) { found = true; break; } } if (!found) materials.add(mat); } return ops; } /** * FAST: simulates fixed invalidate() — IdentityHashMap dedup, O(1) per part. */ static long invalidateFast(int nodePartCount, int materialCount) { IdentityHashMap seen = new IdentityHashMap<>(); List materials = new ArrayList<>(); Object[] matObjs = new Object[materialCount]; for (int i = 0; i < materialCount; i++) matObjs[i] = new Object(); long ops = 0; for (int p = 0; p < nodePartCount; p++) { Object mat = matObjs[p % materialCount]; ops++; if (seen.put(mat, Boolean.TRUE) == null) materials.add(mat); // O(1) } return ops; } // ── libgdx-0004: Kerning GPOS IntArray.contains ────────────────────────── /** * SLOW: simulates GPOS coverage loop — for each covered glyph, scan all class * definitions with IntArray.contains() (linear scan) to find class membership. * O(coverageLen × class1Count × avg_glyphs_per_class) */ static long kerningGposSlow(int coverageLen, int class1Count, int glyphsPerClass) { // Build class arrays: class c contains glyphs [c*glyphsPerClass .. (c+1)*glyphsPerClass) int[][] glyphsByClass = new int[class1Count][]; for (int c = 1; c < class1Count; c++) { glyphsByClass[c] = new int[glyphsPerClass]; for (int k = 0; k < glyphsPerClass; k++) glyphsByClass[c][k] = c * glyphsPerClass + k; } glyphsByClass[0] = new int[0]; // class 0 = unclassified long ops = 0; // Coverage glyphs: worst case, each one is at the end of the last class (or unclassified) for (int i = 0; i < coverageLen; i++) { int glyph = (class1Count - 1) * glyphsPerClass + (i % glyphsPerClass); // last class boolean found = false; for (int j = 1; j < class1Count && !found; j++) { // IntArray.contains — linear scan for (int k = 0; k < glyphsByClass[j].length; k++) { ops++; if (glyphsByClass[j][k] == glyph) { found = true; break; } } } } return ops; } /** * FAST: simulates fixed GPOS handler — build IntIntMap (glyph→class) once, * then O(1) lookup per covered glyph. */ static long kerningGposFast(int coverageLen, int class1Count, int glyphsPerClass) { int[][] glyphsByClass = new int[class1Count][]; for (int c = 1; c < class1Count; c++) { glyphsByClass[c] = new int[glyphsPerClass]; for (int k = 0; k < glyphsPerClass; k++) glyphsByClass[c][k] = c * glyphsPerClass + k; } // Build reverse map once — O(class1Count × glyphsPerClass) Map glyphToClass = new HashMap<>((class1Count * glyphsPerClass) * 2); for (int c = 1; c < class1Count; c++) for (int k = 0; k < glyphsByClass[c].length; k++) glyphToClass.put(glyphsByClass[c][k], c); long ops = 0; for (int i = 0; i < coverageLen; i++) { int glyph = (class1Count - 1) * glyphsPerClass + (i % glyphsPerClass); ops++; glyphToClass.containsKey(glyph); // O(1) } return ops; } public static void main(String[] args) { System.out.println("=== UNIT libgdx-0001..0004: LibGDX CWE-407 ==="); // libgdx-0001: Model.loadNode System.out.println(); System.out.println(" libgdx-0001: Model.loadNode nested string-ID scan"); final int NP1 = 500, M1 = 100, T1 = 50; long s0 = loadNodeSlow(NP1, M1, T1); long f0 = loadNodeFast(NP1, M1, T1); bench(String.format("libgdx-0001 parts=%d meshes=%d mats=%d", NP1, M1, T1), () -> loadNodeSlow(NP1, M1, T1), () -> loadNodeFast(NP1, M1, T1), s0, f0); final int NP2 = 2000, M2 = 200, T2 = 100; long s1 = loadNodeSlow(NP2, M2, T2); long f1 = loadNodeFast(NP2, M2, T2); bench(String.format("libgdx-0001 parts=%d meshes=%d mats=%d", NP2, M2, T2), () -> loadNodeSlow(NP2, M2, T2), () -> loadNodeFast(NP2, M2, T2), s1, f1); // libgdx-0002: ModelBuilder.rebuildReferences System.out.println(); System.out.println(" libgdx-0002: ModelBuilder.rebuildReferences Array.contains"); final int RNP = 1000, RM = 50; long s2 = rebuildRefsSlow(RNP, RM); long f2 = rebuildRefsFast(RNP, RM); bench(String.format("libgdx-0002 parts=%d materials=%d", RNP, RM), () -> rebuildRefsSlow(RNP, RM), () -> rebuildRefsFast(RNP, RM), s2, f2); // libgdx-0003: ModelInstance.invalidate System.out.println(); System.out.println(" libgdx-0003: ModelInstance.invalidate Array.contains"); final int INP = 1000, IM = 50; long s3 = invalidateSlow(INP, IM); long f3 = invalidateFast(INP, IM); bench(String.format("libgdx-0003 parts=%d materials=%d", INP, IM), () -> invalidateSlow(INP, IM), () -> invalidateFast(INP, IM), s3, f3); // libgdx-0004: Kerning GPOS System.out.println(); System.out.println(" libgdx-0004: Kerning GPOS IntArray.contains"); final int KC = 1000, KN = 100, KG = 20; long s4 = kerningGposSlow(KC, KN, KG); long f4 = kerningGposFast(KC, KN, KG); bench(String.format("libgdx-0004 coverage=%d classes=%d glyphs/class=%d", KC, KN, KG), () -> kerningGposSlow(KC, KN, KG), () -> kerningGposFast(KC, KN, KG), s4, f4); System.out.println(); int pass = 0; assert s0 > f0 * 5 : "libgdx-0001 (small) expected >5x speedup"; pass++; assert s1 > f1 * 5 : "libgdx-0001 (large) expected >5x speedup"; pass++; assert s2 > f2 * 5 : "libgdx-0002 expected >5x speedup"; pass++; assert s3 > f3 * 5 : "libgdx-0003 expected >5x speedup"; pass++; assert s4 > f4 * 5 : "libgdx-0004 expected >5x speedup"; pass++; System.out.printf("%d/5 PASS%n", pass); } }