Viewing is open to everyone. Downloading and starring need a GitHub sign-in. Sign in with GitHub

HostHarness.java

v0.1.0-build.85 Latest Commit ede38f5

Size
19 KB
Lines
282
Language
plaintext
SHA-256
4bb53d090278…
View on GitHub

Sign in with GitHub to download. Each download is recorded with your GitHub username.

1// Host-JVM harness for the CKey JNI bridge. Plain Java, no Android, no Kotlin
2// runtime needed: it calls org.openckey.ckey.CKeyNative's static native
3// methods directly, so it runs against either the Kotlin-compiled class or
4// the Java mirror generated by gen_mirror.py. See run.sh.
5
6import java.nio.ByteBuffer;
7import java.nio.charset.StandardCharsets;
8import java.util.Arrays;
9
10import org.openckey.ckey.CKeyException;
11import org.openckey.ckey.CKeyNative;
12
13public final class HostHarness {
14 static int checks = 0;
15
16 static void check(boolean ok, String what) {
17 checks++;
18 if (!ok) throw new AssertionError("FAILED: " + what);
19 System.out.println("ok " + what);
20 }
21
22 interface Body { void run() throws Exception; }
23
24 static void throwsType(Class<? extends Throwable> type, Body body, String what) throws Exception {
25 try {
26 body.run();
27 } catch (Throwable t) {
28 check(type.isInstance(t), what + " -> " + t);
29 return;
30 }
31 check(false, what + " (no exception)");
32 }
33
34 static int[] info5() { return new int[5]; }
35
36 /** Straight RGBA8 -> Rec. 709 luma, composited over white, placed in a larger frame. */
37 static byte[] frameFromRgba(byte[] rgba, int side, int frameW, int frameH, int stride, int x0, int y0) {
38 byte[] frame = new byte[stride * frameH];
39 Arrays.fill(frame, (byte) 0xff);
40 for (int y = 0; y < side; y++)
41 for (int x = 0; x < side; x++) {
42 int p = (y * side + x) * 4;
43 int r = rgba[p] & 0xff, g = rgba[p + 1] & 0xff, b = rgba[p + 2] & 0xff, a = rgba[p + 3] & 0xff;
44 int luma = (2126 * r + 7152 * g + 722 * b + 5000) / 10000;
45 frame[(y0 + y) * stride + x0 + x] = (byte) ((luma * a + 255 * (255 - a) + 127) / 255);
46 }
47 return frame;
48 }
49
50 public static void main(String[] args) throws Exception {
51 int features = CKeyNative.nativeFeatures();
52 System.out.println("features=" + features + " version=" + CKeyNative.nativeSoftwareVersion() + " abi=" + CKeyNative.nativeAbiVersion());
53 check((features & 7) == 7, "scan, render and telemetry compiled in");
54 check(CKeyNative.nativeSoftwareVersion().matches("^0\\.[0-9]+\\.[0-9]+(-(build|dev)\\.[0-9]{1,6})?$"), "software version matches the telemetry schema");
55 check(CKeyNative.nativeCapacity(1, 1, 0) > 0 && CKeyNative.nativeCapacity(99, 1, 0) == 0, "capacity");
56 check(CKeyNative.nativeModulesAcross(1) == 29 && CKeyNative.nativeModulesAcross(31) == 232, "modules across v1/v31");
57
58 // ---- encode -> render RGBA -> gray -> decode (Profile 0 standard)
59 String text = "https://openckey.org/hello?from=android-jni";
60 byte[] payload = text.getBytes(StandardCharsets.UTF_8);
61 byte[] symbol = CKeyNative.nativeEncode(payload, 0, 1, 0);
62 int[] info = CKeyNative.nativeSymbolInfo(symbol);
63 check(info[0] >= 1 && info[1] == 1 && info[2] == 0 && info[3] == payload.length, "symbol info " + Arrays.toString(info));
64
65 int side = 480;
66 byte[] rgba = CKeyNative.nativeRenderRaster(symbol, null, null, null, side, 0, false, false, false, 4, 1.0, true, null, 0, 0);
67 check(rgba.length == side * side * 4, "RGBA buffer size");
68 int frameW = 760, frameH = 620, stride = 800, x0 = 140, y0 = 70;
69 byte[] frame = frameFromRgba(rgba, side, frameW, frameH, stride, x0, y0);
70 int[] dinfo = info5();
71 double[] quad = new double[8];
72 byte[] decoded = CKeyNative.nativeDecodeArray(frame, 0, frameW, frameH, stride, 6, dinfo, quad);
73 check(decoded != null && Arrays.equals(decoded, payload), "round trip via byte[] (P0 v" + dinfo[1] + ", candidates " + dinfo[3] + ")");
74 check(dinfo[0] == 0 && dinfo[1] == info[0] && dinfo[2] == 0, "decode reports profile P0 and the encoded version");
75 check(quad[0] > x0 - 20 && quad[0] < x0 + side + 20, "quad is in frame coordinates");
76
77 // Same frame through a direct ByteBuffer with a non-zero offset.
78 int pad = 37;
79 ByteBuffer direct = ByteBuffer.allocateDirect(pad + frame.length);
80 direct.position(pad);
81 direct.put(frame);
82 direct.position(0);
83 byte[] viaBuffer = CKeyNative.nativeDecodeBuffer(direct, pad, frameW, frameH, stride, 6, info5(), new double[8]);
84 check(viaBuffer != null && Arrays.equals(viaBuffer, payload), "round trip via direct ByteBuffer + offset");
85
86 // UTF-8 beyond the BMP, PRINT layout, level H, GRAY8 output, inverted polarity.
87 byte[] utf8 = "Grüße aus Köln 👋 — address:1 Main St".getBytes(StandardCharsets.UTF_8);
88 byte[] printSymbol = CKeyNative.nativeEncode(utf8, 0, 3, 1);
89 byte[] gray = CKeyNative.nativeRenderRaster(printSymbol, null, null, null, 400, 1, true, false, false, 4, 1.0, true, null, 0, 0);
90 check(gray.length == 400 * 400, "GRAY8 buffer size");
91 byte[] inverted = new byte[gray.length];
92 for (int i = 0; i < gray.length; i++) inverted[i] = (byte) (255 - (gray[i] & 0xff));
93 int[] pinfo = info5();
94 byte[] printDecoded = CKeyNative.nativeDecodeArray(gray, 0, 400, 400, 400, 6, pinfo, new double[8]);
95 check(printDecoded != null && Arrays.equals(printDecoded, utf8) && pinfo[0] == 1 && pinfo[2] == 1, "PRINT layout (P0.1) round trip, level H");
96 byte[] invDecoded = CKeyNative.nativeDecodeArray(inverted, 0, 400, 400, 400, 6, info5(), new double[8]);
97 check(invDecoded != null && Arrays.equals(invDecoded, utf8), "inverted polarity round trip");
98
99 // Blank frame: no result, no exception.
100 byte[] blank = new byte[320 * 240];
101 Arrays.fill(blank, (byte) 0x80);
102 check(CKeyNative.nativeDecodeArray(blank, 0, 320, 240, 320, 6, info5(), new double[8]) == null, "blank frame decodes to null");
103
104 // ---- transparent output and premultiplication
105 byte[] straight = CKeyNative.nativeRenderRaster(symbol, null, null, null, 128, 0, false, true, false, 4, 1.0, true, null, 0, 0);
106 byte[] premul = CKeyNative.nativeRenderRaster(symbol, null, null, null, 128, 0, false, true, true, 4, 1.0, true, null, 0, 0);
107 int zeroAlpha = 0, partial = 0, mismatches = 0;
108 for (int p = 0; p < straight.length; p += 4) {
109 int a = straight[p + 3] & 0xff;
110 if (a == 0) zeroAlpha++;
111 if (a > 0 && a < 255) partial++;
112 if ((premul[p + 3] & 0xff) != a) mismatches++;
113 for (int c = 0; c < 3; c++) {
114 int expect = ((straight[p + c] & 0xff) * a + 127) / 255;
115 if ((premul[p + c] & 0xff) != expect) mismatches++;
116 if ((premul[p + c] & 0xff) > (premul[p + 3] & 0xff)) mismatches++; // premultiplied invariant: c <= a
117 }
118 }
119 check(zeroAlpha > 0, "transparent output has alpha-0 pixels (" + zeroAlpha + ")");
120 check(partial > 0, "coverage antialiasing yields partial alpha (" + partial + ")");
121 check(mismatches == 0, "premultiplied RGBA = round(c * a / 255) with c <= a");
122
123 // ---- exports
124 byte[] png = CKeyNative.nativeRenderPng(symbol, null, null, null, 256, 0, true, 4, 1.0, true, null, 0, 0, 300.0);
125 check(png.length > 8 && (png[0] & 0xff) == 0x89 && png[1] == 'P' && png[2] == 'N' && png[3] == 'G', "PNG signature (" + png.length + " bytes)");
126 String svg = CKeyNative.nativeRenderSvg(symbol, null, null, null, 8);
127 check(svg.contains("<svg") && svg.trim().endsWith("</svg>"), "SVG export (" + svg.length() + " chars)");
128 byte[] pdf = CKeyNative.nativeRenderPdf(symbol, null, null, null, 144.0, false);
129 check(new String(pdf, 0, 5, StandardCharsets.US_ASCII).equals("%PDF-"), "PDF export (" + pdf.length + " bytes)");
130
131 // ---- styles
132 int[] ints = new int[15];
133 double[] doubles = new double[4];
134 CKeyNative.nativeStylePreset(1, ints, doubles);
135 check(ints[2] == 0xf6eee0 && ints[8] == 2 && doubles[0] == 0.6 && ints[14] == 0, "CONCEPT preset marshalled (ink, accents, bevel, policy strict)");
136 String report = CKeyNative.nativeCheckStyle(symbol, ints, doubles, null);
137 check(report.startsWith("style OK"), "check style: " + report);
138 byte[] concept = CKeyNative.nativeRenderRaster(symbol, ints, doubles, null, side, 0, false, false, false, 4, 1.0, true, null, 0, 0);
139 byte[] conceptFrame = frameFromRgba(concept, side, frameW, frameH, stride, x0, y0);
140 byte[] conceptDecoded = CKeyNative.nativeDecodeArray(conceptFrame, 0, frameW, frameH, stride, 6, info5(), new double[8]);
141 check(conceptDecoded != null && Arrays.equals(conceptDecoded, payload), "CONCEPT style (light on dark) round trip");
142
143 int[] lowContrast = new int[15];
144 double[] d0 = new double[4];
145 CKeyNative.nativeStylePreset(0, lowContrast, d0);
146 lowContrast[2] = 0xf0f0f0; lowContrast[3] = 0xf0f0f0; // near-white ink on white
147 throwsType(CKeyException.class, () -> CKeyNative.nativeCheckStyle(symbol, lowContrast, d0, null), "low-contrast style refused with the core's diagnostic");
148 try {
149 CKeyNative.nativeRenderRaster(symbol, lowContrast, d0, null, 128, 0, false, false, false, 4, 1.0, true, null, 0, 0);
150 } catch (CKeyException e) {
151 check(e.getStatus() == 1 && !e.getMessage().isEmpty(), "strict render refusal explains: " + e.getMessage());
152 }
153 lowContrast[14] = 1; // ADAPTIVE: the renderer repairs the style
154 byte[] adaptive = CKeyNative.nativeRenderRaster(symbol, lowContrast, d0, null, side, 0, false, false, false, 4, 1.0, true, null, 0, 0);
155 byte[] adaptiveDecoded = CKeyNative.nativeDecodeArray(frameFromRgba(adaptive, side, frameW, frameH, stride, x0, y0), 0, frameW, frameH, stride, 6, info5(), new double[8]);
156 check(adaptiveDecoded != null && Arrays.equals(adaptiveDecoded, payload), "ADAPTIVE policy repairs the style and the result decodes");
157
158 // Logo pixels path (straight RGBA).
159 byte[] logo = new byte[8 * 8 * 4];
160 for (int i = 0; i < logo.length; i += 4) { logo[i] = (byte) 200; logo[i + 3] = (byte) 255; }
161 int[] logoInts = new int[15];
162 double[] logoDoubles = new double[4];
163 CKeyNative.nativeStylePreset(0, logoInts, logoDoubles);
164 logoInts[13] = 1;
165 byte[] withLogo = CKeyNative.nativeRenderRaster(symbol, logoInts, logoDoubles, null, 256, 0, false, false, false, 4, 1.0, true, logo, 8, 8);
166 check(withLogo.length == 256 * 256 * 4, "raster with a logo bitmap");
167
168 // ---- certification (optional core module)
169 if ((features & 8) != 0) {
170 int[] outI = new int[15];
171 double[] outD = new double[4];
172 double[] cert = new double[9];
173 String msg = CKeyNative.nativeCertify(symbol, null, null, null, false, null, null, 0, 0, null, outI, outD, cert);
174 check(cert[0] == 0 && cert[1] == 1, "default style certified: " + msg);
175 String tmsg = CKeyNative.nativeCertify(symbol, null, null, null, true, null, null, 0, 0, null, outI, outD, cert);
176 check(cert[1] == 0, "transparent without a backdrop is never certified: " + tmsg);
177 String bmsg = CKeyNative.nativeCertify(symbol, null, null, null, true, new int[] {0xffffff, 0xeeeeee}, null, 0, 0,
178 new double[] {8, 0.9, 0.03, 0.2, 2}, outI, outD, cert);
179 check(cert[1] == 1, "transparent over light backdrops certified: " + bmsg);
180 } else {
181 System.out.println("skip certification (ckey_certify.cpp not in this core)");
182 }
183
184 // ---- argument validation (Java exceptions, never crashes)
185 throwsType(IllegalArgumentException.class, () -> CKeyNative.nativeSymbolInfo(new byte[10]), "wrong-size symbol blob");
186 byte[] corrupt = symbol.clone();
187 corrupt[0] ^= 0x55; // abi_version
188 throwsType(IllegalArgumentException.class, () -> CKeyNative.nativeRenderSvg(corrupt, null, null, null, 4), "corrupt symbol header");
189 throwsType(IllegalArgumentException.class, () -> CKeyNative.nativeDecodeArray(frame, 0, frameW, frameH, frameW - 1, 6, info5(), new double[8]), "stride < width");
190 throwsType(IllegalArgumentException.class, () -> CKeyNative.nativeDecodeArray(frame, 100, frameW, frameH, stride, 6, info5(), new double[8]), "offset past the end");
191 throwsType(IllegalArgumentException.class, () -> CKeyNative.nativeDecodeBuffer(ByteBuffer.allocate(100), 0, 10, 10, 10, 6, info5(), new double[8]), "heap buffer refused by native");
192 throwsType(IllegalArgumentException.class, () -> CKeyNative.nativeRenderRaster(symbol, new int[3], new double[4], null, 64, 0, false, false, false, 4, 1, true, null, 0, 0), "style array shape");
193 throwsType(NullPointerException.class, () -> CKeyNative.nativeEncode(null, 0, 1, 0), "null payload");
194 throwsType(CKeyException.class, () -> CKeyNative.nativeEncode(new byte[20000], 0, 1, 0), "oversized payload");
195 throwsType(CKeyException.class, () -> CKeyNative.nativeEncode(new byte[100], 1, 3, 0), "payload over a fixed version's capacity");
196
197 // ---- telemetry
198 CKeyNative.nativeTelemetryResetForTesting();
199 check(CKeyNative.nativeTelemetryGetMode() == 1, "telemetry default mode 1");
200 check(CKeyNative.nativeTelemetrySetMode(3) == 1 && CKeyNative.nativeTelemetryGetMode() == 1, "mode 3 refused, mode unchanged");
201 check(CKeyNative.nativeTelemetrySetClient("android", CKeyNative.nativeSoftwareVersion(), "android") == 0, "set_client android");
202 check(CKeyNative.nativeTelemetrySetClient("kotlin", "1", "android") == 1, "invalid client refused");
203 String endpoint = CKeyNative.nativeTelemetryEndpoint();
204 check(endpoint.equals("https://openckey.org/api/telemetry/v1/batch"), "endpoint " + endpoint);
205 long t = 1000;
206 CKeyNative.nativeTelemetrySessionBegin(t);
207 CKeyNative.nativeTelemetryFrame(t + 10, 7);
208 CKeyNative.nativeTelemetryFrame(t + 50, 30);
209 check(CKeyNative.nativeTelemetrySessionSuccess(t + 60, 0) == 0, "session success P0");
210 check(CKeyNative.nativeTelemetrySessionSuccess(t + 70, 9) == 1, "invalid profile code refused");
211 CKeyNative.nativeTelemetrySessionEnd(t + 80);
212 long[] snap = new long[33];
213 check(CKeyNative.nativeTelemetrySnapshot(snap) == 0 && snap[0] == 1 && snap[1] == 1 && snap[3] == 2 && snap[4] == 1, "snapshot " + Arrays.toString(Arrays.copyOf(snap, 9)));
214 check(snap[9] == 1 && snap[10] == 60 && snap[11 + 2] == 1, "time-to-decode histogram (60 ms in bucket 2)");
215 check(!CKeyNative.nativeTelemetryShouldSend(t + 100), "no batch before the delta is 60 s old");
216 check(CKeyNative.nativeTelemetryShouldSend(t + 61_000), "batch due after 60 s");
217 long[] id = new long[1];
218 String batch = CKeyNative.nativeTelemetryTakeBatch("2026-11-05", id);
219 check(batch != null && id[0] != 0 && batch.contains("\"sdk\":\"android\"") && batch.contains("\"platform\":\"android\"") && batch.contains("\"day\":\"2026-11-05\""), "batch JSON: " + batch);
220 check(CKeyNative.nativeTelemetryTakeBatch("2026-11-05", new long[1]) == null, "no second batch while one is in flight");
221 throwsType(CKeyException.class, () -> CKeyNative.nativeTelemetryTakeBatch("2026-13-45", new long[1]), "invalid day refused");
222 check(CKeyNative.nativeTelemetryBatchResult(id[0], 204, t + 62_000) == 0, "batch result 204");
223 check(CKeyNative.nativeTelemetryBatchResult(id[0], 204, t + 62_000) == 1, "stale batch id refused");
224 String state = CKeyNative.nativeTelemetryExportState();
225 check(state.startsWith("{\"state\":1"), "export state");
226 check(CKeyNative.nativeTelemetrySetMode(0) == 0 && CKeyNative.nativeTelemetrySnapshot(snap) == 0 && snap[0] == 0, "mode 0 purges counters");
227 check(CKeyNative.nativeTelemetryImportState(state) == 1, "import refused in mode 0");
228 CKeyNative.nativeTelemetrySetMode(2);
229 check(CKeyNative.nativeTelemetryImportState(state) == 0, "import accepted in mode 2");
230 CKeyNative.nativeTelemetrySessionBegin(t);
231 check(!CKeyNative.nativeTelemetryShouldSend(t + 10_000_000), "mode 2 never sends");
232 CKeyNative.nativeTelemetryResetForTesting();
233
234 safeActions();
235 System.out.println("HostHarness: " + checks + " checks passed");
236 }
237
238 static String field(String block, String name) {
239 java.util.regex.Matcher m = java.util.regex.Pattern.compile("\"" + name + "\":\\s*(\"[^\"]*\"|[0-9]+)").matcher(block);
240 if (!m.find()) throw new AssertionError("vector without " + name);
241 String v = m.group(1);
242 return v.startsWith("\"") ? v.substring(1, v.length() - 1) : v;
243 }
244
245 /** tests/vectors/safe-actions-v1.json through nativeSafeAction (the bridge, not the policy). */
246 static void safeActions() throws Exception {
247 String path = System.getProperty("org.openckey.ckey.vectors");
248 if (path == null) { System.out.println("skip safe action vectors (no org.openckey.ckey.vectors)"); return; }
249 String json = new String(java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(path)), StandardCharsets.UTF_8);
250 String[] blocks = json.split("\\{\\s*\"name\":");
251 int n = 0;
252 for (int i = 1; i < blocks.length; i++) {
253 String b = blocks[i];
254 String name = b.substring(b.indexOf('"') + 1, b.indexOf('"', b.indexOf('"') + 1));
255 byte[] payload = field(b, "payload").getBytes(StandardCharsets.UTF_8);
256 String hex = field(b, "public_key");
257 byte[] key = new byte[32];
258 for (int k = 0; k < 32; k++) key[k] = (byte) Integer.parseInt(hex.substring(2 * k, 2 * k + 2), 16);
259 int[] status = new int[1];
260 String out = CKeyNative.nativeSafeAction(payload, key, Long.parseLong(field(b, "now")), Integer.parseInt(field(b, "max_lifetime")), status);
261 String expect = field(b, "expect");
262 boolean ok = switch (expect) {
263 case "ok" -> status[0] == 0 && out.split("\n", -1).length == 9 && out.contains("\n" + field(b, "request_id") + "\n");
264 case "integrity" -> status[0] == 6;
265 case "expired" -> status[0] == 7;
266 case "invalid" -> status[0] == 1;
267 case "invalid_or_integrity" -> status[0] == 1 || status[0] == 6;
268 default -> false;
269 };
270 if (!ok) throw new AssertionError("FAILED: safe action vector \"" + name + "\": expected " + expect + ", got " + status[0] + " " + out);
271 n++;
272 }
273 check(n >= 20, "safe action vectors: " + n + " verdicts match");
274 int[] status = new int[1];
275 String first = field(blocks[1], "payload");
276 String parsed = CKeyNative.nativeSafeAction(first.getBytes(StandardCharsets.UTF_8), null, 0, 3600, status);
277 check(status[0] == 0 && parsed.startsWith("0\ncitee".replace("citee", "citeory.com")), "parse only (no key)");
278 CKeyNative.nativeSafeAction("ckey-action:1:AAAA".getBytes(StandardCharsets.UTF_8), null, 0, 3600, status);
279 check(status[0] == 1, "malformed envelope -> invalid, no exception");
280 throwsType(IllegalArgumentException.class, () -> CKeyNative.nativeSafeAction(new byte[1], new byte[31], 0, 600, new int[1]), "short public key");
281 }
282}