Files

245 lines
12 KiB
Java

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<Integer> 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<int[]> 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<Integer> scan(byte[] b) {
List<Integer> 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<Integer> 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();
}
}