java-topology/defects/libgdx/unit/LibGDXTest.java
russell@unturf.com 783e406633 pygame/libgdx: CWE-407 findings
pygame: CLEAN — runtime uses dict/set throughout (O(1) membership)
libgdx-0002: ModelBuilder.rebuildReferences Array.contains O(P*M) — patch
libgdx-0005: Stage.touchDragged touchFocuses.contains O(F^2) — patch
libgdx-0006: AfterAction.delegate currentActions.indexOf O(W*A) per frame — patch
unit: extend LibGDXTest to 7 tests, all PASS (50x-1971x speedup measured)
2026-03-30 09:13:34 -04:00

428 lines
19 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package unit;
import java.util.*;
/**
* LibGDXTest — libgdx-0001..0006
*
* 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)
* libgdx-0005: Stage.touchDragged() — touchFocuses.contains(focus, true) inside touchFocuses loop O(F^2)
* libgdx-0006: AfterAction.delegate() — currentActions.indexOf(action, true) inside waitForActions loop O(W*A)
*
* 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<String, index> 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<String, Integer> meshById = new HashMap<>(meshCount * 2);
for (int i = 0; i < meshCount; i++) meshById.put(meshIds[i], i);
Map<String, Integer> 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<Object> materials = new ArrayList<>();
List<Object> meshParts = new ArrayList<>();
List<Object> 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<Object, Boolean> matSeen = new IdentityHashMap<>();
IdentityHashMap<Object, Boolean> partSeen = new IdentityHashMap<>();
IdentityHashMap<Object, Boolean> meshSeen = new IdentityHashMap<>();
List<Object> materials = new ArrayList<>();
List<Object> meshParts = new ArrayList<>();
List<Object> 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<Object> 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<Object, Boolean> seen = new IdentityHashMap<>();
List<Object> 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<Integer, Integer> 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;
}
// ── libgdx-0005: Stage.touchDragged SnapshotArray.contains O(F^2) ─────────
/**
* SLOW: simulates Stage.touchDragged() — for each focus in touchFocuses,
* calls touchFocuses.contains(focus, true) which is an O(F) linear scan.
* Total: O(F^2) per touchDragged event.
*/
static long stageTouchDraggedSlow(int focusCount) {
List<Object> focuses = new ArrayList<>();
for (int i = 0; i < focusCount; i++) focuses.add(new Object());
long ops = 0;
// Outer loop: iterate all focuses
for (int i = 0; i < focuses.size(); i++) {
Object focus = focuses.get(i);
// Inner: Array.contains(focus, identity=true) — linear scan
for (int j = focuses.size() - 1; j >= 0; j--) {
ops++;
if (focuses.get(j) == focus) break;
}
}
return ops;
}
/**
* FAST: build ObjectSet once (O(F)), then O(1) per focus check → O(F) total.
*/
static long stageTouchDraggedFast(int focusCount) {
List<Object> focuses = new ArrayList<>();
for (int i = 0; i < focusCount; i++) focuses.add(new Object());
Set<Object> focusSet = new HashSet<>(focuses); // O(F)
long ops = (long) focusCount; // set construction cost
for (int i = 0; i < focuses.size(); i++) {
Object focus = focuses.get(i);
ops++; // O(1) hash lookup
focusSet.contains(focus);
}
return ops;
}
// ── libgdx-0006: AfterAction.delegate indexOf O(W*A) per frame ───────────
/**
* SLOW: simulates AfterAction.delegate() — for each action in waitForActions,
* calls currentActions.indexOf(action, true) which is an O(A) linear scan.
* Total: O(W*A) per frame.
*/
static long afterActionSlow(int waitCount, int actionCount) {
List<Object> currentActions = new ArrayList<>();
List<Object> waitForActions = new ArrayList<>();
for (int i = 0; i < actionCount; i++) currentActions.add(new Object());
for (int i = 0; i < waitCount && i < actionCount; i++) waitForActions.add(currentActions.get(i));
long ops = 0;
for (int i = waitForActions.size() - 1; i >= 0; i--) {
Object action = waitForActions.get(i);
// indexOf scans all A current actions
for (int j = 0; j < currentActions.size(); j++) {
ops++;
if (currentActions.get(j) == action) break;
}
}
return ops;
}
/**
* FAST: build ObjectSet from currentActions once (O(A)), then O(1) per waitForActions check.
*/
static long afterActionFast(int waitCount, int actionCount) {
List<Object> currentActions = new ArrayList<>();
List<Object> waitForActions = new ArrayList<>();
for (int i = 0; i < actionCount; i++) currentActions.add(new Object());
for (int i = 0; i < waitCount && i < actionCount; i++) waitForActions.add(currentActions.get(i));
Set<Object> currentSet = new HashSet<>(currentActions); // O(A)
long ops = (long) actionCount; // set construction cost
for (int i = waitForActions.size() - 1; i >= 0; i--) {
Object action = waitForActions.get(i);
ops++; // O(1)
currentSet.contains(action);
}
return ops;
}
public static void main(String[] args) {
System.out.println("=== UNIT libgdx-0001..0006: 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);
// libgdx-0005: Stage.touchDragged
System.out.println();
System.out.println(" libgdx-0005: Stage.touchDragged SnapshotArray.contains O(F^2)");
final int FC = 200;
long s5 = stageTouchDraggedSlow(FC);
long f5 = stageTouchDraggedFast(FC);
bench(String.format("libgdx-0005 focuses=%d", FC),
() -> stageTouchDraggedSlow(FC), () -> stageTouchDraggedFast(FC), s5, f5);
// libgdx-0006: AfterAction.delegate
System.out.println();
System.out.println(" libgdx-0006: AfterAction.delegate indexOf O(W*A)");
final int WC = 100, AC = 200;
long s6 = afterActionSlow(WC, AC);
long f6 = afterActionFast(WC, AC);
bench(String.format("libgdx-0006 wait=%d actions=%d", WC, AC),
() -> afterActionSlow(WC, AC), () -> afterActionFast(WC, AC), s6, f6);
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++;
assert s5 > f5 * 5 : "libgdx-0005 expected >5x speedup"; pass++;
assert s6 > f6 * 5 : "libgdx-0006 expected >5x speedup"; pass++;
System.out.printf("%d/7 PASS%n", pass);
}
}