ReviewOS

also looking at this

craft-native/craft

fix(android): text going to the JVM is modified UTF-8, and was not being

#168
Merged chris wants to merge fix/android-modified-utf8 into main
12 files +326 -136
packages/zig/src/android_dispatch.zigmodified+26-41
Changes to packages/zig/src/android_dispatch.zig
@@ -359,14 +359,10 @@ fn nativeGetDeviceInfo(env: jni.JNIEnv, _: jni.jobject, activity: jni.jobject) c
359359
360360 const json = device.render(allocator, info) catch return null;
361361
362 // `NewStringUTF` reads to a NUL, and `render` produces a plain slice.
363 // Copied rather than terminated in place because the reply is JSON built
364 // for the page, and giving `render` a sentinel to satisfy would put a
365 // JNI detail into the half of this action that has nothing to do with Java.
366 const owned = allocator.allocSentinel(u8, json.len, 0) catch return null;
367 @memcpy(owned, json);
368
369 return j.newStringUtf(owned.ptr) catch null;
362 // Re-encoded rather than handed over: this JSON carries device text — a
363 // model name, a carrier — and `NewStringUTF` reads modified UTF-8, where a
364 // NUL is two bytes and an astral character is a surrogate pair.
365 return j.newStringUtf8(allocator, json) catch null;
370366}
371367
372368/// `nativeGetMemoryUsage()` — the JVM heap, as JSON.
@@ -383,10 +379,7 @@ fn nativeGetMemoryUsage(env: jni.JNIEnv, _: jni.jobject) callconv(.c) jni.jstrin
383379 };
384380
385381 const json = system.renderMemory(allocator, usage) catch return null;
386 const owned = allocator.allocSentinel(u8, json.len, 0) catch return null;
387 @memcpy(owned, json);
388
389 return j.newStringUtf(owned.ptr) catch null;
382 return j.newStringUtf8(allocator, json) catch null;
390383}
391384
392385/// `nativeLog(message)` — returns whether Zig wrote the line.
@@ -402,10 +395,8 @@ fn nativeLog(env: jni.JNIEnv, _: jni.jobject, message: jni.jstring) callconv(.c)
402395 const allocator = arena.allocator();
403396
404397 const text = j.stringToUtf8(allocator, message) catch return jni.JNI_FALSE;
405 const owned = allocator.allocSentinel(u8, text.len, 0) catch return jni.JNI_FALSE;
406 @memcpy(owned, text);
407398
408 system.writeLog(j, owned.ptr) catch |err| {
399 system.writeLog(j, allocator, text) catch |err| {
409400 std.log.warn("craft: log fell through to the shim ({s})", .{@errorName(err)});
410401 return jni.JNI_FALSE;
411402 };
@@ -429,9 +420,7 @@ fn nativeClipboardRead(env: jni.JNIEnv, _: jni.jobject, activity: jni.jobject) c
429420 return null;
430421 };
431422
432 const owned = allocator.allocSentinel(u8, text.len, 0) catch return null;
433 @memcpy(owned, text);
434 return j.newStringUtf(owned.ptr) catch null;
423 return j.newStringUtf8(allocator, text) catch null;
435424}
436425
437426/// `nativeClipboardWrite(activity, text)`.
@@ -448,25 +437,21 @@ fn nativeClipboardWrite(
448437 const allocator = arena.allocator();
449438
450439 const value = j.stringToUtf8(allocator, text) catch return jni.JNI_FALSE;
451 const owned = allocator.allocSentinel(u8, value.len, 0) catch return jni.JNI_FALSE;
452 @memcpy(owned, value);
453440
454 clipboard.write(j, activity, owned.ptr) catch |err| {
441 clipboard.write(j, allocator, activity, value) catch |err| {
455442 std.log.warn("craft: clipboardWrite fell through to the shim ({s})", .{@errorName(err)});
456443 return jni.JNI_FALSE;
457444 };
458445 return jni.JNI_TRUE;
459446}
460447
461/// Copy a `jstring` into NUL-terminated arena memory, or null.
448/// A `jstring` as arena-owned UTF-8, or null.
462449///
463450/// Every native below needs this and none of them can share the result, since
464/// each arena dies with its call.
465fn ownedUtf8(j: Jni, allocator: std.mem.Allocator, str: jni.jstring) ?[:0]u8 {
466 const text = j.stringToUtf8(allocator, str) catch return null;
467 const owned = allocator.allocSentinel(u8, text.len, 0) catch return null;
468 @memcpy(owned, text);
469 return owned;
451/// each arena dies with its call. A plain slice: the JNI side re-encodes on
452/// the way back out, so nothing here has to carry a terminator.
453fn ownedUtf8(j: Jni, allocator: std.mem.Allocator, str: jni.jstring) ?[]u8 {
454 return j.stringToUtf8(allocator, str) catch null;
470455}
471456
472457fn nativeOpenUrl(
@@ -479,8 +464,10 @@ fn nativeOpenUrl(
479464 var arena = std.heap.ArenaAllocator.init(backing);
480465 defer arena.deinit();
481466
482 const value = ownedUtf8(j, arena.allocator(), url) orelse return jni.JNI_FALSE;
483 intents.openUrl(j, activity, value.ptr) catch |err| {
467 const allocator = arena.allocator();
468
469 const value = ownedUtf8(j, allocator, url) orelse return jni.JNI_FALSE;
470 intents.openUrl(j, allocator, activity, value) catch |err| {
484471 // Not a warning. `openURL` answering false is the documented outcome
485472 // when nothing on the device handles the scheme, and the Kotlin
486473 // catches exactly this — logging it at warn would put a line in
@@ -506,7 +493,7 @@ fn nativeShare(
506493 const body = ownedUtf8(j, allocator, text) orelse return jni.JNI_FALSE;
507494 const subject = ownedUtf8(j, allocator, title) orelse return jni.JNI_FALSE;
508495
509 intents.share(j, activity, body.ptr, subject.ptr) catch |err| {
496 intents.share(j, allocator, activity, body, subject) catch |err| {
510497 std.log.warn("craft: share fell through to the shim ({s})", .{@errorName(err)});
511498 return jni.JNI_FALSE;
512499 };
@@ -525,9 +512,7 @@ fn nativeGetNetworkStatus(env: jni.JNIEnv, _: jni.jobject, activity: jni.jobject
525512 };
526513
527514 const json = network.render(allocator, status) catch return null;
528 const owned = allocator.allocSentinel(u8, json.len, 0) catch return null;
529 @memcpy(owned, json);
530 return j.newStringUtf(owned.ptr) catch null;
515 return j.newStringUtf8(allocator, json) catch null;
531516}
532517
533518fn nativeSecureSet(
@@ -545,7 +530,7 @@ fn nativeSecureSet(
545530 const k = ownedUtf8(j, allocator, key) orelse return jni.JNI_FALSE;
546531 const v = ownedUtf8(j, allocator, value) orelse return jni.JNI_FALSE;
547532
548 securestore.set(j, prefs, k.ptr, v.ptr) catch |err| {
533 securestore.set(j, allocator, prefs, k, v) catch |err| {
549534 std.log.warn("craft: secureSet fell through to the shim ({s})", .{@errorName(err)});
550535 return jni.JNI_FALSE;
551536 };
@@ -569,15 +554,13 @@ fn nativeSecureGet(
569554 const allocator = arena.allocator();
570555
571556 const k = ownedUtf8(j, allocator, key) orelse return null;
572 const value = securestore.get(allocator, j, prefs, k.ptr) catch |err| {
557 const value = securestore.get(allocator, j, prefs, k) catch |err| {
573558 std.log.warn("craft: secureGet fell through to the shim ({s})", .{@errorName(err)});
574559 return null;
575560 };
576561
577562 const json = securestore.renderRead(allocator, value) catch return null;
578 const owned = allocator.allocSentinel(u8, json.len, 0) catch return null;
579 @memcpy(owned, json);
580 return j.newStringUtf(owned.ptr) catch null;
563 return j.newStringUtf8(allocator, json) catch null;
581564}
582565
583566fn nativeSecureRemove(
@@ -590,8 +573,10 @@ fn nativeSecureRemove(
590573 var arena = std.heap.ArenaAllocator.init(backing);
591574 defer arena.deinit();
592575
593 const k = ownedUtf8(j, arena.allocator(), key) orelse return jni.JNI_FALSE;
594 securestore.remove(j, prefs, k.ptr) catch |err| {
576 const allocator = arena.allocator();
577
578 const k = ownedUtf8(j, allocator, key) orelse return jni.JNI_FALSE;
579 securestore.remove(j, allocator, prefs, k) catch |err| {
595580 std.log.warn("craft: secureRemove fell through to the shim ({s})", .{@errorName(err)});
596581 return jni.JNI_FALSE;
597582 };