import java.nio.file.*; import java.util.*; /** * Independent check of the Phase C negative claim: "libcxsdk.so contains no * static palette tables." * * The canonical scanner is analysis/tools/extract_palettes.py, but this machine * has no working python3, so this JDK equivalent lets a verifier re-run the scan. * * The tool reports three things and is explicit about how much each one proves. * * [A] DETECTOR SANITY -- the known iron-bow table is planted into a synthetic * buffer and the broad scan must find it. This only proves the scan is not * blind; it proves nothing about the library. * * [B] POSITIVE CONTROL (the decisive part) -- the same table is searched for * verbatim in the library under the DOCUMENTED byte layout. This matters: * csdk/src/mag160c_official_palette256.h records the vendor table as * "256 x 4 bytes (B, G, R, 0)" — the 4th byte is ZERO, while the ARGB ints * in OfficialTables.kt have alpha forced to 0xFF by the generator, so * searching the Kotlin ints directly can never match the binary. The tool * therefore rebuilds the needle from the csdk header (ground truth) and * additionally tries an RGB-only match that ignores the 4th byte. * Iron-bow is the one vendor palette the project holds (captured at runtime * from CoreSDKLib dev+0xb18, verified pixel-exact against the official * renderer); if this library stored static tables of that family, it would * be among them. Absence in every encoding is therefore strong evidence * for the negative claim. * * [C] BROAD SCAN (reported, but NOT usable as evidence) -- a generic "256-entry * colour ramp" search. Measured on this library it flags ~11% of all * positions, all of them ARM32 code where the 4th byte of each word happens * to be constant and adjacent words differ little. A generic ramp test * cannot separate palettes from integer/address arrays, so this count is * deliberately reported with its false-positive density instead of being * presented as a verdict. * * The negative claim therefore rests on [B] plus the independent structural * evidence: the Ghidra listing shows CFunctions::SetColorPalette COMPUTING all * twelve tables with arithmetic (analysis/sdk_re/android_app/libcxsdk_decomp.txt), * which is why no static data can be found. * * Usage: * java PalScan.java analysis/sdk_re/android_app/bin/libcxsdk.so \ * csdk/src/mag160c_official_palette256.h */ public class PalScan { static final int N = 256; static final int STEP_LIMIT = 0x200000; // max per-step change static final int CHANNEL_SPAN = 64; // a colour scale spans a channel static final int MIN_SPANNING_CHANNELS = 2; public static void main(String[] args) throws Exception { String libPath = args[0]; String headerPath = args.length > 1 ? args[1] : null; byte[] lib = Files.readAllBytes(Paths.get(libPath)); System.out.println("file: " + libPath + " (" + lib.length + " bytes)"); if (headerPath == null) { System.out.println("(pass csdk/src/mag160c_official_palette256.h as the 2nd argument)"); return; } // Ground truth for the in-binary layout: the csdk header holds the table // exactly as it appears in memory, (B, G, R, 0) per entry. int[][] rgb = readHeader(headerPath); if (rgb.length != N) { System.out.println("header parsed " + rgb.length + " entries, expected " + N + " - aborting"); return; } System.out.println("ground-truth table: " + N + " entries (B,G,R,0), first 3 = " + rgb[0][0] + "," + rgb[0][1] + "," + rgb[0][2] + " " + rgb[1][0] + "," + rgb[1][1] + "," + rgb[1][2] + " " + rgb[2][0] + "," + rgb[2][1] + "," + rgb[2][2]); // ---------- [A] detector sanity ---------- byte[] planted = encodeHeader(rgb, false); int plantedHits = scan(planted).size(); System.out.println(); System.out.println("[A] detector sanity: planted the known table into a 1024-byte buffer"); System.out.println(" detected: " + plantedHits + " candidate(s)" + (plantedHits == 0 ? " <-- SCAN IS BLIND, [C] is meaningless" : " (scan is not blind)")); // ---------- [B] positive control ---------- System.out.println(); System.out.println("[B] POSITIVE CONTROL - is the known vendor table stored verbatim?"); int exact = indexOf(lib, encodeHeader(rgb, false), 0); System.out.println(" B,G,R,0 (documented layout) : " + (exact < 0 ? "not found" : "*** FOUND at 0x" + Integer.toHexString(exact) + " ***")); int exactFF = indexOf(lib, encodeHeader(rgb, true), 0); System.out.println(" B,G,R,0xFF (alpha forced) : " + (exactFF < 0 ? "not found" : "*** FOUND at 0x" + Integer.toHexString(exactFF) + " ***")); int rgbOnly = matchRgbIgnoringFourth(lib, rgb); System.out.println(" B,G,R ignoring 4th byte : " + (rgbOnly < 0 ? "not found" : "*** FOUND at 0x" + Integer.toHexString(rgbOnly) + " ***")); // ---------- [C] broad scan ---------- System.out.println(); System.out.println("[C] broad scan for any 256-entry colour ramp (supporting only):"); List hits = scan(lib); int positions = (lib.length - N * 4) / 4; System.out.printf(" candidates: %d of %d positions (%.1f%%)%n", hits.size(), positions, 100.0 * hits.size() / Math.max(1, positions)); if (!hits.isEmpty()) { System.out.println(" These are ARM32 code regions: the ramp test cannot tell a colour"); System.out.println(" scale from a run of small integers, so this count is NOT evidence."); } // ---------- verdict ---------- System.out.println(); boolean found = exact >= 0 || exactFF >= 0 || rgbOnly >= 0; if (found) { System.out.println("VERDICT: a static copy of the vendor table IS present -> the negative claim is WRONG."); } else { System.out.println("VERDICT: the known vendor table is absent in every encoding tested."); System.out.println(" Together with the Ghidra listing (CFunctions::SetColorPalette computes"); System.out.println(" the tables arithmetically) this upholds the negative claim:"); System.out.println(" libcxsdk.so stores no static palette tables."); } } /** Parse csdk/src/mag160c_official_palette256.h into {B,G,R} triples. */ static int[][] readHeader(String path) throws Exception { List rows = new ArrayList<>(); for (String line : Files.readAllLines(Paths.get(path), java.nio.charset.StandardCharsets.UTF_8)) { if (!line.contains("{") || !line.contains("}")) continue; String body = line.substring(line.indexOf('{') + 1, line.indexOf('}')); String[] parts = body.split(","); if (parts.length < 3) continue; try { rows.add(new int[]{ Integer.parseInt(parts[0].trim()), Integer.parseInt(parts[1].trim()), Integer.parseInt(parts[2].trim()), }); } catch (NumberFormatException e) { // header/trailer lines } } return rows.toArray(new int[0][]); } static byte[] encodeHeader(int[][] rgb, boolean alphaFF) { byte[] out = new byte[rgb.length * 4]; for (int i = 0; i < rgb.length; i++) { out[i*4] = (byte) rgb[i][0]; out[i*4+1] = (byte) rgb[i][1]; out[i*4+2] = (byte) rgb[i][2]; out[i*4+3] = (byte) (alphaFF ? 0xFF : 0x00); } return out; } /** Match all 256 (B,G,R) triples at 4-byte stride, ignoring the 4th byte. */ static int matchRgbIgnoringFourth(byte[] data, int[][] rgb) { for (int o = 0; o + rgb.length * 4 <= data.length; o += 4) { boolean ok = true; for (int i = 0; i < rgb.length && ok; i++) { int p = o + i * 4; if ((data[p] & 0xFF) != rgb[i][0] || (data[p+1] & 0xFF) != rgb[i][1] || (data[p+2] & 0xFF) != rgb[i][2]) ok = false; } if (ok) return o; } return -1; } /** All offsets whose 1024-byte window passes the ramp test. */ static List scan(byte[] b) { List out = new ArrayList<>(); for (int o = 0; o + N * 4 <= b.length; o += 4) if (isPaletteRun(b, o)) out.add(o); return out; } static boolean isPaletteRun(byte[] b, int o) { int alpha = b[o + 3] & 0xFF; int prev = -1, distinct = 0; int[] mn = {255, 255, 255}, mx = {0, 0, 0}; for (int i = 0; i < N; i++) { int p = o + i * 4; if ((b[p + 3] & 0xFF) != alpha) return false; // unused byte constant int bl = b[p] & 0xFF, g = b[p + 1] & 0xFF, r = b[p + 2] & 0xFF; int v = bl | (g << 8) | (r << 16); if (prev >= 0 && Math.abs(v - prev) > STEP_LIMIT) return false; if (v != prev) distinct++; if (bl < mn[0]) mn[0] = bl; if (bl > mx[0]) mx[0] = bl; if (g < mn[1]) mn[1] = g; if (g > mx[1]) mx[1] = g; if (r < mn[2]) mn[2] = r; if (r > mx[2]) mx[2] = r; prev = v; } if (distinct < 32) return false; int spanning = 0; for (int c = 0; c < 3; c++) if (mx[c] - mn[c] >= CHANNEL_SPAN) spanning++; return spanning >= MIN_SPANNING_CHANNELS; } static byte[] encode(int[] anchor, int order) { byte[] out = new byte[anchor.length * 4]; for (int i = 0; i < anchor.length; i++) { int v = anchor[i]; int r = (v >> 16) & 0xFF, g = (v >> 8) & 0xFF, bl = v & 0xFF, al = (v >>> 24) & 0xFF; switch (order) { case 0: putLE(out, i * 4, v); break; case 1: putBE(out, i * 4, v); break; case 2: out[i*4]=(byte) bl; out[i*4+1]=(byte) g; out[i*4+2]=(byte) r; out[i*4+3]=(byte) al; break; case 3: out[i*4]=(byte) r; out[i*4+1]=(byte) g; out[i*4+2]=(byte) bl; out[i*4+3]=(byte) al; break; case 4: out[i*4]=0; out[i*4+1]=(byte) bl; out[i*4+2]=(byte) g; out[i*4+3]=(byte) r; break; } } return out; } static void putLE(byte[] o, int p, int v) { o[p]=(byte)(v&0xFF); o[p+1]=(byte)((v>>8)&0xFF); o[p+2]=(byte)((v>>16)&0xFF); o[p+3]=(byte)((v>>>24)&0xFF); } static void putBE(byte[] o, int p, int v) { o[p]=(byte)((v>>>24)&0xFF); o[p+1]=(byte)((v>>16)&0xFF); o[p+2]=(byte)((v>>8)&0xFF); o[p+3]=(byte)(v&0xFF); } static int indexOf(byte[] hay, byte[] needle, int from) { outer: for (int i = from; i + needle.length <= hay.length; i++) { for (int j = 0; j < needle.length; j++) if (hay[i + j] != needle[j]) continue outer; return i; } return -1; } static int[] readAnchor(String ktPath) throws Exception { String src = Files.readString(Paths.get(ktPath), java.nio.charset.StandardCharsets.UTF_8); int k = src.indexOf("val PALETTE256_ARGB = intArrayOf("); int start = k + "val PALETTE256_ARGB = intArrayOf(".length(); int end = src.indexOf(")", start); List vals = new ArrayList<>(); StringBuilder num = new StringBuilder(); for (char c : src.substring(start, end).toCharArray()) { if (c == '-' || (c >= '0' && c <= '9')) num.append(c); else if (num.length() > 0) { vals.add(Integer.parseInt(num.toString())); num.setLength(0); } } return vals.stream().mapToInt(Integer::intValue).toArray(); } }