325 lines
14 KiB
Java
325 lines
14 KiB
Java
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<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;
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|