docs+analysis: independent verification guide for phases A-F (calibrated scanners, self-tested commands)
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user