import java.util.*; /** * Unit tests for Proton CWE-407 defects. * * proton-0001: find_iface_constructor linear strcmp scan O(C) per lookup * proton-0002: merge_user_dir extant_dirs list explosion O(D×P) */ public class ProtonTest { // ======================================================================== // proton-0001: find_iface_constructor linear scan vs binary search // ======================================================================== /** * Simulates the constructors[] table in steamclient_generated.c. * 213 interface version strings, sorted alphabetically. */ static String[] buildConstructorsTable() { String[] prefixes = { "STEAMAPPLIST_INTERFACE_VERSION", "STEAMAPPS_INTERFACE_VERSION", "STEAMAPPTICKET_INTERFACE_VERSION", "STEAMCONTROLLER_INTERFACE_VERSION", "STEAMHTMLSURFACE_INTERFACE_VERSION_", "STEAMHTTP_INTERFACE_VERSION", "STEAMINVENTORY_INTERFACE_V", "STEAMMUSICREMOTE_INTERFACE_VERSION", "STEAMMUSIC_INTERFACE_VERSION", "SteamClient", "SteamFriends", "SteamGameServer", "SteamGameServerStats", "SteamMatchMakingServers", "SteamMatchMaking", "SteamNetworking", "SteamNetworkingMessages", "SteamNetworkingSockets", "SteamNetworkingUtils", "SteamScreenshots", "SteamTimeline", "SteamUGC", "SteamUser", "SteamUserStats", "SteamUtils", "SteamVideo", }; List table = new ArrayList<>(); for (String prefix : prefixes) { // Generate multiple versions per prefix to simulate 213 entries for (int v = 1; v <= 8; v++) { table.add(prefix + String.format("%03d", v)); } } // Trim to 213 while (table.size() > 213) table.remove(table.size() - 1); Collections.sort(table); return table.toArray(new String[0]); } /** DEFECTIVE: linear scan O(C) per lookup */ static int findIfaceConstructorLinear(String[] table, String ifaceVersion) { for (int i = 0; i < table.length; i++) { if (table[i].equals(ifaceVersion)) return i; } return -1; } /** FIXED: binary search O(log C) per lookup */ static int findIfaceConstructorBinary(String[] table, String ifaceVersion) { int lo = 0, hi = table.length - 1; while (lo <= hi) { int mid = (lo + hi) / 2; int cmp = ifaceVersion.compareTo(table[mid]); if (cmp == 0) return mid; if (cmp < 0) hi = mid - 1; else lo = mid + 1; } return -1; } static void testProton0001() { System.out.println("=== proton-0001: find_iface_constructor O(C) linear scan ==="); String[] table = buildConstructorsTable(); // Build a set of lookup keys (every 3rd entry + some misses) List lookups = new ArrayList<>(); for (int i = 0; i < table.length; i += 3) lookups.add(table[i]); for (int i = 0; i < 30; i++) lookups.add("NONEXISTENT_INTERFACE_" + i); int iterations = 10000; // DEFECTIVE: linear scan long startLinear = System.nanoTime(); int linearOps = 0; for (int iter = 0; iter < iterations; iter++) { for (String key : lookups) { findIfaceConstructorLinear(table, key); linearOps++; } } long linearTime = System.nanoTime() - startLinear; // FIXED: binary search long startBinary = System.nanoTime(); int binaryOps = 0; for (int iter = 0; iter < iterations; iter++) { for (String key : lookups) { findIfaceConstructorBinary(table, key); binaryOps++; } } long binaryTime = System.nanoTime() - startBinary; // Verify correctness for (String key : lookups) { int lin = findIfaceConstructorLinear(table, key); int bin = findIfaceConstructorBinary(table, key); assert lin == bin : "Mismatch for " + key + ": linear=" + lin + " binary=" + bin; } double ratio = (double) linearTime / binaryTime; System.out.printf(" Table size: %d interface versions%n", table.length); System.out.printf(" Lookups: %d%n", linearOps); System.out.printf(" Linear: %d ms%n", linearTime / 1_000_000); System.out.printf(" Binary: %d ms%n", binaryTime / 1_000_000); System.out.printf(" Ratio: %.1fx%n", ratio); assert ratio > 2.0 : "Expected binary search to be >2x faster, got " + ratio + "x"; System.out.println(" PASS"); } // ======================================================================== // proton-0002: merge_user_dir extant_dirs list explosion // ======================================================================== /** * Simulates the defective merge_user_dir behavior. * In Python: extant_dirs += dst_dir adds each character as a list element. * Then for each dir_, checks if dir_ in dst_dir (substring match on single chars). */ static long mergeUserDirDefective(List directories) { List extantDirs = new ArrayList<>(); long ops = 0; for (String dstDir : directories) { // Check if child of extant dir boolean childOfExtant = false; for (String dir_ : extantDirs) { ops++; // In defective code, dir_ is a single character, and // "if dir_ in dst_dir" checks substring containment if (dstDir.contains(dir_)) { childOfExtant = true; break; } } if (!childOfExtant) { // Simulate: some directories already exist (every 3rd) if (directories.indexOf(dstDir) % 3 == 0) { // DEFECT: extant_dirs += dst_dir adds each CHARACTER for (char c : dstDir.toCharArray()) { extantDirs.add(String.valueOf(c)); } } } } return ops; } /** Fixed version: append whole path, use startsWith for prefix check */ static long mergeUserDirFixed(List directories) { Set extantDirs = new HashSet<>(); long ops = 0; for (String dstDir : directories) { boolean childOfExtant = false; for (String extant : extantDirs) { ops++; if (dstDir.startsWith(extant)) { childOfExtant = true; break; } } if (!childOfExtant) { if (directories.indexOf(dstDir) % 3 == 0) { // FIXED: add whole path as one element extantDirs.add(dstDir); } } } return ops; } static void testProton0002() { System.out.println("=== proton-0002: merge_user_dir extant_dirs explosion ==="); // Simulate a prefix migration with 200 directories // Typical game save directory tree during WinXP -> Vista migration List directories = new ArrayList<>(); String base = "/home/user/.steam/steam/steamapps/compatdata/12345/pfx/drive_c/users/steamuser/"; for (int i = 0; i < 200; i++) { directories.add(base + "AppData/Local/GameSave/SubDir" + i + "/data"); } // Count how many directories get processed (not skipped) by each version int defectProcessed = countProcessedDefective(directories); int fixedProcessed = countProcessedFixed(directories); // Also measure list size blowup in defective version int defectListSize = measureListSizeDefective(directories); int fixedSetSize = measureSetSizeFixed(directories); System.out.printf(" Directories: %d%n", directories.size()); System.out.printf(" Avg path len: %d chars%n", directories.get(0).length()); System.out.printf(" Defect processed: %d / %d (skipped %d due to single-char match)%n", defectProcessed, directories.size(), directories.size() - defectProcessed); System.out.printf(" Fixed processed: %d / %d%n", fixedProcessed, directories.size()); System.out.printf(" Defect list size: %d entries (chars, not paths!)%n", defectListSize); System.out.printf(" Fixed set size: %d entries (whole paths)%n", fixedSetSize); double blowup = (double) defectListSize / Math.max(fixedSetSize, 1); System.out.printf(" List size blowup: %.1fx%n", blowup); // The defective version processes far fewer directories due to // single-char matching causing premature skips — nearly ALL directories // are incorrectly skipped because single characters from path[0] match // as substrings of every subsequent path assert defectProcessed < directories.size() / 2 : "Expected defective to skip most dirs, but processed " + defectProcessed; assert fixedProcessed > defectProcessed : "Expected fixed to process more dirs than defective"; System.out.println(" PASS: correctness defect confirmed — " + (directories.size() - defectProcessed) + " dirs incorrectly skipped"); } static int countProcessedDefective(List directories) { List extantDirs = new ArrayList<>(); int processed = 0; for (int idx = 0; idx < directories.size(); idx++) { String dstDir = directories.get(idx); boolean childOfExtant = false; for (String dir_ : extantDirs) { if (dstDir.contains(dir_)) { childOfExtant = true; break; } } if (!childOfExtant) { processed++; if (idx % 3 == 0) { for (char c : dstDir.toCharArray()) extantDirs.add(String.valueOf(c)); } } } return processed; } static int countProcessedFixed(List directories) { Set extantDirs = new HashSet<>(); int processed = 0; for (int idx = 0; idx < directories.size(); idx++) { String dstDir = directories.get(idx); boolean childOfExtant = false; for (String extant : extantDirs) { if (dstDir.startsWith(extant)) { childOfExtant = true; break; } } if (!childOfExtant) { processed++; if (idx % 3 == 0) extantDirs.add(dstDir); } } return processed; } static int measureListSizeDefective(List directories) { List extantDirs = new ArrayList<>(); for (int idx = 0; idx < directories.size(); idx++) { String dstDir = directories.get(idx); boolean childOfExtant = false; for (String dir_ : extantDirs) { if (dstDir.contains(dir_)) { childOfExtant = true; break; } } if (!childOfExtant && idx % 3 == 0) { for (char c : dstDir.toCharArray()) extantDirs.add(String.valueOf(c)); } } return extantDirs.size(); } static int measureSetSizeFixed(List directories) { Set extantDirs = new HashSet<>(); for (int idx = 0; idx < directories.size(); idx++) { String dstDir = directories.get(idx); boolean childOfExtant = false; for (String extant : extantDirs) { if (dstDir.startsWith(extant)) { childOfExtant = true; break; } } if (!childOfExtant && idx % 3 == 0) extantDirs.add(dstDir); } return extantDirs.size(); } public static void main(String[] args) { testProton0001(); System.out.println(); testProton0002(); System.out.println(); System.out.println("All proton tests PASS"); } }