ReviewOS

also looking at this

craft-native/craft

fix(ios): answer a malformed argument instead of leaving the page waiting

#246
Merged glennmichael123 wants to merge fix/ios-invalid-argument into main
2 files +136 -0
packages/zig/test/ios_conformance_test.zigmodified+110-0
Changes to packages/zig/test/ios_conformance_test.zig
@@ -1800,6 +1800,74 @@ fn caseUsesCallback(region: []const u8, action: []const u8, use: CaseUse) bool {
18001800 return false;
18011801}
18021802
1803/// The lines of the dispatcher's `case` for `action`, label excluded.
1804///
1805/// Same boundary rule as `caseUsesCallback`: from the label to the next line
1806/// at or above its indentation that starts another case, a `default:` or the
1807/// closing brace.
1808fn caseBody(region: []const u8, action: []const u8) ?[]const u8 {
1809 var case_indent: ?usize = null;
1810 var body_start: usize = 0;
1811 var offset: usize = 0;
1812 var lines = std.mem.splitScalar(u8, region, '\n');
1813 while (lines.next()) |line| {
1814 const next_offset = offset + line.len + 1;
1815 defer offset = next_offset;
1816 const body = std.mem.trimStart(u8, line, " \t");
1817 if (body.len == 0) continue;
1818 const indent = line.len - body.len;
1819
1820 if (case_indent) |at| {
1821 const closes = indent <= at and (std.mem.startsWith(u8, body, "case ") or
1822 std.mem.startsWith(u8, body, "default") or body[0] == '}');
1823 if (closes) return region[body_start..offset];
1824 continue;
1825 }
1826 if (std.mem.startsWith(u8, body, "case ") and labels(body, action)) {
1827 case_indent = indent;
1828 body_start = next_offset;
1829 }
1830 }
1831 if (case_indent != null) return region[body_start..];
1832 return null;
1833}
1834
1835/// Whether every `if let` written directly in this case has an `else`.
1836///
1837/// A case that casts its argument and answers only inside the `if` leaves the
1838/// page waiting for ever on a malformed one: `craft.setBadge('3')` posted a
1839/// callbackId and nothing settled it, on any path (#221). The `else` does not
1840/// have to reject — `caseUsesCallback` is what checks the case answers at all
1841/// — but a branch that simply is not there cannot.
1842///
1843/// Only the `if let`s at the case's own indentation are read. One nested
1844/// inside a closure or a helper belongs to that closure's contract, not to
1845/// this dispatch.
1846fn caseAnswersEveryPath(region: []const u8, action: []const u8) bool {
1847 const body = caseBody(region, action) orelse return true;
1848
1849 var if_indent: ?usize = null;
1850 var lines = std.mem.splitScalar(u8, body, '\n');
1851 while (lines.next()) |line| {
1852 const trimmed = std.mem.trimStart(u8, line, " \t");
1853 if (trimmed.len == 0 or std.mem.startsWith(u8, trimmed, "//")) continue;
1854 const indent = line.len - trimmed.len;
1855
1856 if (if_indent) |at| {
1857 if (indent != at) continue;
1858 // The line that closes it. `} else` in any form is the branch
1859 // this is looking for; a bare `}` is the gap.
1860 if (!std.mem.startsWith(u8, trimmed, "}")) continue;
1861 if (std.mem.indexOf(u8, trimmed, "else") == null) return false;
1862 if_indent = null;
1863 continue;
1864 }
1865 if (std.mem.startsWith(u8, trimmed, "if let ")) if_indent = indent;
1866 }
1867 // An `if let` still open at the end of the case never closed with an else.
1868 return if_indent == null;
1869}
1870
18031871fn specAnswers(region: []const u8, action: []const u8) bool {
18041872 return caseUsesCallback(region, action, .answers);
18051873}
@@ -1903,6 +1971,48 @@ test "every call the spec answers carries a callbackId to answer it on" {
19031971 try testing.expect(literal_posts >= 40);
19041972}
19051973
1974test "no case the page waits on casts its argument without answering the other way" {
1975 // #221: three cases — openURL, setBadge, setFlashlight — cast and then
1976 // answered only inside the `if`, so `craft.setBadge('3')` posted a
1977 // callbackId that nothing ever settled. The Siri pair had the same shape.
1978 // Zig refuses all five, so this is the path an app with no Zig runtime
1979 // takes, and the one a Zig build falls back to when the payload will not
1980 // serialize.
1981 const region = dispatcherRegion();
1982 var awaited = try collectAwaitedActions(testing.allocator);
1983 defer awaited.deinit();
1984 try testing.expect(awaited.count() >= 80);
1985
1986 // Cases still missing an `else`. It may only shrink: an entry here is a
1987 // page promise that never settles for a malformed argument.
1988 const known_gaps = [_][]const u8{};
1989
1990 // Every gap, not the first: one case at a time would hide the rest of a
1991 // class the whole point of this check is to see all of.
1992 var gaps: usize = 0;
1993 var it = awaited.keyIterator();
1994 while (it.next()) |action| {
1995 if (caseAnswersEveryPath(region, action.*)) continue;
1996 for (known_gaps) |allowed| {
1997 if (std.mem.eql(u8, allowed, action.*)) break;
1998 } else {
1999 gaps += 1;
2000 std.debug.print(
2001 "the page waits on `{s}`, and its case has an `if let` with no `else`.\n",
2002 .{action.*},
2003 );
2004 }
2005 }
2006 if (gaps > 0) {
2007 std.debug.print(
2008 " An argument that does not cast settles nothing at all, and the page waits\n" ++
2009 " out its own timeout instead of hearing INVALID_ARGUMENT.\n",
2010 .{},
2011 );
2012 return error.CaseLeavesAMalformedArgumentUnanswered;
2013 }
2014}
2015
19062016test "every call the page waits on has a case that can resolve it" {
19072017 const region = dispatcherRegion();
19082018 var awaited = try collectAwaitedActions(testing.allocator);