also looking at this
fix(android): text going to the JVM is modified UTF-8, and was not being
#168
12 files
+326
-136
| @@ -99,7 +99,7 @@ fn detach(vm: jni.JavaVM) void { | ||
| 99 | 99 | /// Silent on every failure, and deliberately: the caller is a device callback |
| 100 | 100 | /// with nowhere to report to, and the alternatives are a log per location fix |
| 101 | 101 | /// or an error nobody reads. `droppedCount` is what a diagnosis reads instead. |
| 102 | pub fn evaluate(script: [*:0]const u8) void { | |
| 102 | pub fn evaluate(allocator: std.mem.Allocator, script: []const u8) void { | |
| 103 | 103 | const vm = java_vm orelse { |
| 104 | 104 | dropped += 1; |
| 105 | 105 | return; |
| @@ -111,12 +111,12 @@ pub fn evaluate(script: [*:0]const u8) void { | ||
| 111 | 111 | }; |
| 112 | 112 | defer if (attachment.owned) detach(vm); |
| 113 | 113 | |
| 114 | deliverThrough(attachment.env, script) catch { | |
| 114 | deliverThrough(allocator, attachment.env, script) catch { | |
| 115 | 115 | dropped += 1; |
| 116 | 116 | }; |
| 117 | 117 | } |
| 118 | 118 | |
| 119 | fn deliverThrough(env: jni.JNIEnv, script: [*:0]const u8) !void { | |
| 119 | fn deliverThrough(allocator: std.mem.Allocator, env: jni.JNIEnv, script: []const u8) !void { | |
| 120 | 120 | const j = jni.Jni.init(env); |
| 121 | 121 | |
| 122 | 122 | // A frame around the class, the string and any pending throwable. Without |
| @@ -127,7 +127,11 @@ fn deliverThrough(env: jni.JNIEnv, script: [*:0]const u8) !void { | ||
| 127 | 127 | defer _ = j.popLocalFrame(null); |
| 128 | 128 | |
| 129 | 129 | const cls = try j.findClass(holder_class); |
| 130 | const text = try j.newStringUtf(script); | |
| 130 | // Re-encoded rather than handed over: a reply carries page and device | |
| 131 | // text — a contact's name, an event's title, an error message — and | |
| 132 | // `NewStringUTF` reads modified UTF-8, in which an emoji is two three-byte | |
| 133 | // halves rather than one four-byte sequence. | |
| 134 | const text = try j.newStringUtf8(allocator, script); | |
| 131 | 135 | |
| 132 | 136 | const deliver = try j.staticMethodId(cls, "deliver", "(Ljava/lang/String;)V"); |
| 133 | 137 | try j.callStaticVoidMethodA(cls, deliver, &.{.{ .l = text }}); |
| @@ -152,9 +156,8 @@ pub fn emitEvent( | ||
| 152 | 156 | try out.appendSlice(allocator, "', {detail: "); |
| 153 | 157 | try out.appendSlice(allocator, detail_json); |
| 154 | 158 | try out.appendSlice(allocator, "}));"); |
| 155 | try out.append(allocator, 0); | |
| 156 | 159 | |
| 157 | evaluate(@ptrCast(out.items.ptr)); | |
| 160 | evaluate(allocator, out.items); | |
| 158 | 161 | } |
| 159 | 162 | |
| 160 | 163 | /// `window._craftXResolve && window._craftXResolve(payload)`. |
| @@ -180,9 +183,8 @@ pub fn settle( | ||
| 180 | 183 | try out.append(allocator, '('); |
| 181 | 184 | try out.appendSlice(allocator, payload_json); |
| 182 | 185 | try out.appendSlice(allocator, ");"); |
| 183 | try out.append(allocator, 0); | |
| 184 | 186 | |
| 185 | evaluate(@ptrCast(out.items.ptr)); | |
| 187 | evaluate(allocator, out.items); | |
| 186 | 188 | } |
| 187 | 189 | |
| 188 | 190 | // ============================================================================= |
| @@ -287,7 +289,7 @@ test "a script reaches deliver on an already-attached thread, and nothing is det | ||
| 287 | 289 | setVm(&ptr); |
| 288 | 290 | defer clearVm(); |
| 289 | 291 | |
| 290 | evaluate("window.x = 1;"); | |
| 292 | evaluate(testing.allocator, "window.x = 1;"); | |
| 291 | 293 | try testing.expectEqualStrings("window.x = 1;", delivered()); |
| 292 | 294 | |
| 293 | 295 | // The UI thread and every binder thread are already attached, and |
| @@ -308,7 +310,7 @@ test "a detached thread is attached and then detached again" { | ||
| 308 | 310 | defer clearVm(); |
| 309 | 311 | |
| 310 | 312 | fake_already_attached = false; |
| 311 | evaluate("window.y = 2;"); | |
| 313 | evaluate(testing.allocator, "window.y = 2;"); | |
| 312 | 314 | |
| 313 | 315 | try testing.expectEqualStrings("window.y = 2;", delivered()); |
| 314 | 316 | try testing.expectEqual(@as(usize, 1), fake_attach_calls); |
| @@ -323,7 +325,7 @@ test "every way delivery can fail is counted, not logged and not crashed" { | ||
| 323 | 325 | // No VM at all — before JNI_OnLoad, or in a build with no runtime linked. |
| 324 | 326 | clearVm(); |
| 325 | 327 | resetDroppedForTest(); |
| 326 | evaluate("window.z = 3;"); | |
| 328 | evaluate(testing.allocator, "window.z = 3;"); | |
| 327 | 329 | try testing.expectEqual(@as(usize, 1), droppedCount()); |
| 328 | 330 | |
| 329 | 331 | // A thread that cannot be attached. |
| @@ -332,7 +334,7 @@ test "every way delivery can fail is counted, not logged and not crashed" { | ||
| 332 | 334 | fake_already_attached = false; |
| 333 | 335 | fake_attach_succeeds = false; |
| 334 | 336 | resetDroppedForTest(); |
| 335 | evaluate("window.z = 3;"); | |
| 337 | evaluate(testing.allocator, "window.z = 3;"); | |
| 336 | 338 | try testing.expectEqual(@as(usize, 1), droppedCount()); |
| 337 | 339 | // Nothing was attached, so nothing may be detached. |
| 338 | 340 | try testing.expectEqual(@as(usize, 0), fake_detach_calls); |
| @@ -342,7 +344,7 @@ test "every way delivery can fail is counted, not logged and not crashed" { | ||
| 342 | 344 | fake_attach_succeeds = true; |
| 343 | 345 | fake_find_class_succeeds = false; |
| 344 | 346 | resetDroppedForTest(); |
| 345 | evaluate("window.z = 3;"); | |
| 347 | evaluate(testing.allocator, "window.z = 3;"); | |
| 346 | 348 | try testing.expectEqual(@as(usize, 1), droppedCount()); |
| 347 | 349 | } |
| 348 | 350 | |
| @@ -358,7 +360,7 @@ test "an attached thread is detached even when delivery fails" { | ||
| 358 | 360 | |
| 359 | 361 | fake_already_attached = false; |
| 360 | 362 | fake_find_class_succeeds = false; |
| 361 | evaluate("window.z = 3;"); | |
| 363 | evaluate(testing.allocator, "window.z = 3;"); | |
| 362 | 364 | |
| 363 | 365 | try testing.expectEqual(@as(usize, 1), fake_attach_calls); |
| 364 | 366 | try testing.expectEqual(@as(usize, 1), fake_detach_calls); |
| @@ -396,3 +398,47 @@ test "settling guards the global that the promise may not have assigned yet" { | ||
| 396 | 398 | delivered(), |
| 397 | 399 | ); |
| 398 | 400 | } |
| 401 | ||
| 402 | test "a reply carrying an emoji reaches the JVM as a surrogate pair" { | |
| 403 | // The end of the wire this whole change is about. A contact called a | |
| 404 | // smiley arrives here as four UTF-8 bytes, and `NewStringUTF` reads | |
| 405 | // *modified* UTF-8, where the same character is two three-byte halves. | |
| 406 | // Handing over the standard form does not produce the string that was | |
| 407 | // meant. | |
| 408 | var invoke: jni.JNIInvokeInterface = undefined; | |
| 409 | fakeVm(&invoke); | |
| 410 | const ptr: *const jni.JNIInvokeInterface = &invoke; | |
| 411 | setVm(&ptr); | |
| 412 | defer clearVm(); | |
| 413 | ||
| 414 | try settle(testing.allocator, "_craftContactsResolve", "[{\"displayName\":\"\u{1F642}\"}]"); | |
| 415 | ||
| 416 | try testing.expectEqualStrings( | |
| 417 | "window._craftContactsResolve && window._craftContactsResolve([{\"displayName\":\"" ++ | |
| 418 | "\xED\xA0\xBD\xED\xB9\x82" ++ "\"}]);", | |
| 419 | delivered(), | |
| 420 | ); | |
| 421 | ||
| 422 | // And the four-byte form is gone rather than merely accompanied. | |
| 423 | try testing.expect(std.mem.indexOf(u8, delivered(), "\xF0\x9F\x99\x82") == null); | |
| 424 | } | |
| 425 | ||
| 426 | test "a NUL in a reply does not truncate it" { | |
| 427 | // `NewStringUTF` reads to a terminator, so a payload carrying U+0000 would | |
| 428 | // end the script early and `evaluateJavascript` would run the fragment | |
| 429 | // before it. A database column holding a NUL byte is the everyday way to | |
| 430 | // get one. | |
| 431 | var invoke: jni.JNIInvokeInterface = undefined; | |
| 432 | fakeVm(&invoke); | |
| 433 | const ptr: *const jni.JNIInvokeInterface = &invoke; | |
| 434 | setVm(&ptr); | |
| 435 | defer clearVm(); | |
| 436 | ||
| 437 | try settle(testing.allocator, "_craftDbQueryResolve", "[\"a\x00b\"]"); | |
| 438 | ||
| 439 | try testing.expectEqualStrings( | |
| 440 | "window._craftDbQueryResolve && window._craftDbQueryResolve([\"a" ++ | |
| 441 | "\xC0\x80" ++ "b\"]);", | |
| 442 | delivered(), | |
| 443 | ); | |
| 444 | } | |