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/bridge_android_intents.zigmodified+18-11
Changes to packages/zig/src/bridge_android_intents.zig
@@ -66,12 +66,12 @@ fn startActivity(j: Jni, activity: jobject, intent: jobject) !void {
6666/// produces a Uri with a null scheme for nonsense. The failure that matters
6767/// comes later, from `startActivity` finding nothing to handle the result, and
6868/// that arrives here as `JavaException` from the `check` after the call.
69pub fn openUrl(j: Jni, activity: jobject, url: [*:0]const u8) !void {
69pub fn openUrl(j: Jni, allocator: std.mem.Allocator, activity: jobject, url: []const u8) !void {
7070 try j.pushLocalFrame(16);
7171 defer _ = j.popLocalFrame(null);
7272
7373 const uri_cls = try j.findClass("android/net/Uri");
74 const url_string = try j.newStringUtf(url);
74 const url_string = try j.newStringUtf8(allocator, url);
7575 const uri = try j.callStaticObjectMethodA(
7676 uri_cls,
7777 try j.staticMethodId(uri_cls, "parse", "(Ljava/lang/String;)Landroid/net/Uri;"),
@@ -93,7 +93,7 @@ pub fn openUrl(j: Jni, activity: jobject, url: [*:0]const u8) !void {
9393///
9494/// An empty `title` skips `EXTRA_SUBJECT` entirely rather than setting it to
9595/// the empty string — see the module comment.
96pub fn share(j: Jni, activity: jobject, text: [*:0]const u8, title: [*:0]const u8) !void {
96pub fn share(j: Jni, allocator: std.mem.Allocator, activity: jobject, text: []const u8, title: []const u8) !void {
9797 try j.pushLocalFrame(24);
9898 defer _ = j.popLocalFrame(null);
9999
@@ -122,12 +122,12 @@ pub fn share(j: Jni, activity: jobject, text: [*:0]const u8, title: [*:0]const u
122122 );
123123
124124 const extra_text = try intentConstant(j, intent_cls, "EXTRA_TEXT");
125 const body = try j.newStringUtf(text);
125 const body = try j.newStringUtf8(allocator, text);
126126 _ = try j.callObjectMethodA(intent, put_extra, &.{ .{ .l = extra_text }, .{ .l = body } });
127127
128 if (title[0] != 0) {
128 if (title.len != 0) {
129129 const extra_subject = try intentConstant(j, intent_cls, "EXTRA_SUBJECT");
130 const subject = try j.newStringUtf(title);
130 const subject = try j.newStringUtf8(allocator, title);
131131 _ = try j.callObjectMethodA(intent, put_extra, &.{ .{ .l = extra_subject }, .{ .l = subject } });
132132 }
133133
@@ -188,7 +188,12 @@ fn fStaticObjectField(_: jni.JNIEnv, _: jni.jclass, _: jni.jfieldID) callconv(.c
188188 return obj(2);
189189}
190190fn fNewStringUTF(_: jni.JNIEnv, text: [*:0]const u8) callconv(.c) jni.jstring {
191 fake_strings.append(testing.allocator, std.mem.span(text)) catch {};
191 // Copied, not borrowed. The caller re-encodes into a scratch buffer and
192 // frees it as soon as the JVM has the string — which is what a real
193 // `NewStringUTF` licenses, and what made this fake read freed memory when
194 // it held the pointer.
195 const copy = testing.allocator.dupe(u8, std.mem.span(text)) catch return obj(3);
196 fake_strings.append(testing.allocator, copy) catch testing.allocator.free(copy);
192197 return obj(3);
193198}
194199fn fCallStaticObjectMethodA(_: jni.JNIEnv, _: jni.jclass, id: jni.jmethodID, _: [*]const jni.jvalue) callconv(.c) jobject {
@@ -246,6 +251,7 @@ fn fakeEnv(table: *jni.JNINativeInterface) void {
246251fn resetFakes() void {
247252 fake_calls.clearRetainingCapacity();
248253 fake_field_names.clearRetainingCapacity();
254 for (fake_strings.items) |text| testing.allocator.free(text);
249255 fake_strings.clearRetainingCapacity();
250256 fake_throw_on_start = false;
251257 fake_pending = null;
@@ -254,6 +260,7 @@ fn resetFakes() void {
254260fn freeFakes() void {
255261 fake_calls.deinit(testing.allocator);
256262 fake_field_names.deinit(testing.allocator);
263 for (fake_strings.items) |text| testing.allocator.free(text);
257264 fake_strings.deinit(testing.allocator);
258265 fake_calls = .empty;
259266 fake_field_names = .empty;
@@ -280,7 +287,7 @@ test "openURL parses the url and hands the intent to the activity" {
280287 fakeEnv(&table);
281288 const ptr: *const jni.JNINativeInterface = &table;
282289
283 try openUrl(Jni.init(&ptr), obj(8), "https://example.com/x?y=1");
290 try openUrl(Jni.init(&ptr), testing.allocator, obj(8), "https://example.com/x?y=1");
284291
285292 try testing.expect(sawCall("parse"));
286293 try testing.expect(sawCall("<init>"));
@@ -303,7 +310,7 @@ test "a URL nothing can open reports the exception rather than success" {
303310 fake_throw_on_start = true;
304311 try testing.expectError(
305312 jni.JniError.JavaException,
306 openUrl(Jni.init(&ptr), obj(8), "definitely-not-a-scheme://x"),
313 openUrl(Jni.init(&ptr), testing.allocator, obj(8), "definitely-not-a-scheme://x"),
307314 );
308315}
309316
@@ -314,7 +321,7 @@ test "share sets the text extra and the chooser" {
314321 fakeEnv(&table);
315322 const ptr: *const jni.JNINativeInterface = &table;
316323
317 try share(Jni.init(&ptr), obj(8), "hello", "Subject");
324 try share(Jni.init(&ptr), testing.allocator, obj(8), "hello", "Subject");
318325
319326 try testing.expect(sawField("ACTION_SEND"));
320327 try testing.expect(sawField("EXTRA_TEXT"));
@@ -337,7 +344,7 @@ test "an empty title omits the subject rather than setting it empty" {
337344 fakeEnv(&table);
338345 const ptr: *const jni.JNINativeInterface = &table;
339346
340 try share(Jni.init(&ptr), obj(8), "hello", "");
347 try share(Jni.init(&ptr), testing.allocator, obj(8), "hello", "");
341348
342349 try testing.expect(sawField("EXTRA_TEXT"));
343350 try testing.expect(!sawField("EXTRA_SUBJECT"));