ReviewOS

also looking at this

craft-native/craft

fix(android): an apostrophe in a reply hangs the page forever

#156
Merged glennmichael123 wants to merge fix/android-js-escaping into main
4 files +279 -73
packages/zig/src/bridge_android_calendar.zigmodified+17-18
Changes to packages/zig/src/bridge_android_calendar.zig
@@ -1,28 +1,26 @@
11//! `deleteCalendarEvent` on Android — the first action here that answers
22//! through the reply channel rather than by returning.
33//!
4//! ## What the shim does, and the one thing this does not reproduce
4//! ## What the shim does
55//!
66//! The Kotlin builds a content URI from the event id, deletes through the
77//! `ContentResolver`, and resolves `true`; on any exception it rejects with
88//! the message. That is all ported.
99//!
10//! What is not ported is how it *sends* the rejection:
10//! The rejection is built as JSON through `bridge_error.appendJsonEscaped`
11//! rather than pasted into a quoted string. When this module was written the
12//! shim did the latter —
1113//!
1214//! "window._craftDeleteEventReject && window._craftDeleteEventReject('${e.message}')"
1315//!
14//! The message goes into a single-quoted JavaScript string with no escaping,
15//! and `eventId.toLong()` puts the caller's own text in that message —
16//! `NumberFormatException: For input string: "1'x"`. A `'` in the id therefore
17//! produces a script that does not parse, `evaluateJavascript` runs nothing,
18//! and **neither the resolve nor the reject fires**. The promise never
19//! settles, and the injected JS builds it by hand with no timeout, so the page
20//! waits forever with nothing in the console.
21//!
22//! Here the payload is built as JSON through `bridge_error.appendJsonEscaped`,
23//! so the same id rejects normally. That is a divergence in the page's favour
24//! and it is deliberate; the shim keeps the bug on the twenty other paths
25//! until #154 is fixed.
16//! — and `eventId.toLong()` puts the caller's own text in that message, so
17//! `craft.calendar.delete("1'x")` produced a script that did not parse:
18//! `evaluateJavascript` ran nothing, neither the resolve nor the reject fired,
19//! and the hand-built promise had no timeout to notice. #154 fixed all
20//! fifty-eight such emissions, and `android_reply_escaping_test` now keeps
21//! them fixed, so this is no longer a divergence — but the escaping stays
22//! here, because `events.settle` takes a payload that is already JSON and this
23//! module is what has to produce it.
2624//!
2725//! ## `toLong()` is reproduced exactly, including what it refuses
2826//!
@@ -114,7 +112,8 @@ pub fn deleteEvent(j: Jni, activity: jobject, id: i64) !void {
114112/// Reject with `message`, escaped.
115113///
116114/// The escaping is the whole point of this function existing rather than the
117/// call being inline — see the module comment and #154.
115/// call being inline — `events.settle` sends the payload as written, so a raw
116/// message would emit a script that does not parse. See the module comment.
118117pub fn rejectWith(allocator: std.mem.Allocator, message: []const u8) !void {
119118 const payload = try rejectPayload(allocator, message);
120119 defer allocator.free(payload);
@@ -171,9 +170,9 @@ test "an id Java would refuse is refused here too" {
171170}
172171
173172test "a rejection message with a quote is escaped rather than emitted raw" {
174 // The bug this action does not reproduce. On the shim, an id of `1'x`
175 // produces `…Reject('For input string: "1'x"')`, which does not parse — so
176 // evaluateJavascript runs nothing and the promise never settles.
173 // The bug that used to be one line away. Before #154 the shim turned an id
174 // of `1'x` into `…Reject('For input string: "1'x"')`, which does not parse
175 // — so evaluateJavascript ran nothing and the promise never settled.
177176 const payload = try rejectPayload(testing.allocator, "For input string: \"1'x\"");
178177 defer testing.allocator.free(payload);
179178