also looking at this
fix(ios): answer a malformed argument instead of leaving the page waiting
#246
2 files
+136
-0
| @@ -1800,6 +1800,74 @@ fn caseUsesCallback(region: []const u8, action: []const u8, use: CaseUse) bool { | ||
| 1800 | 1800 | return false; |
| 1801 | 1801 | } |
| 1802 | 1802 | |
| 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. | |
| 1808 | fn 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. | |
| 1846 | fn 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 | ||
| 1803 | 1871 | fn specAnswers(region: []const u8, action: []const u8) bool { |
| 1804 | 1872 | return caseUsesCallback(region, action, .answers); |
| 1805 | 1873 | } |
| @@ -1903,6 +1971,48 @@ test "every call the spec answers carries a callbackId to answer it on" { | ||
| 1903 | 1971 | try testing.expect(literal_posts >= 40); |
| 1904 | 1972 | } |
| 1905 | 1973 | |
| 1974 | test "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 | ||
| 1906 | 2016 | test "every call the page waits on has a case that can resolve it" { |
| 1907 | 2017 | const region = dispatcherRegion(); |
| 1908 | 2018 | var awaited = try collectAwaitedActions(testing.allocator); |