Skip to content

Commit 5157bba

Browse files
hyperpolymathclaude
andcommitted
proof(xml-7-44): make Xml744.Escape.exfiltrate provably total
The `replaceAll` worker inside `exfiltrate` used three `assert_total` escapes (`strHead`, `head`, `tail`) on Idris2 0.7 stdlib, and the intermediate refactor that removed them broke the termination check (recursion through `pack $ drop ...` is not structurally smaller). Rewrite the worker as `replaceAllChars : Nat -> List Char -> ...` with a fuel parameter sized to `length (unpack str)`. Each recursive step consumes ≥ 1 char of the third argument so fuel is never the binding constraint — it simply makes termination structurally obvious to Idris2's checker, matching the Nat-fueled pattern used in the ephapax totality campaign. `idris2 --check Xml744/Escape.idr` reports all four exported names (`exfiltrate`, `infiltrate`, `infiltrateAttr`, `makeSafe`) as `total`, with no `assert_total` / `assert_smaller` / `believe_me` left in the file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3ed5c3c commit 5157bba

1 file changed

Lines changed: 16 additions & 10 deletions

File tree

idris2-ecosystem/xml-toolkit/src/Xml744/Escape.idr

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ infiltrateAttr : String -> String
4848
infiltrateAttr = concatMap escapeAttrChar . unpack
4949

5050
||| Exfiltrate: safely extract content from XML (unescape entities)
51+
|||
52+
||| The worker `replaceAllChars` recurses on a `Nat` fuel sized to the input.
53+
||| Each step consumes at least one character of the third argument, so the
54+
||| fuel bound is never reached in practice — it exists to make termination
55+
||| structurally obvious to Idris2 without `assert_total` / `assert_smaller`.
5156
export
5257
exfiltrate : String -> String
5358
exfiltrate s =
@@ -58,18 +63,19 @@ exfiltrate s =
5863
s5 = replaceAll "&apos;" "'" s4
5964
in s5
6065
where
66+
replaceAllChars : Nat -> List Char -> List Char -> List Char -> List Char
67+
replaceAllChars _ _ _ [] = []
68+
replaceAllChars _ [] _ xs = xs
69+
replaceAllChars Z _ _ xs = xs
70+
replaceAllChars (S k) (f :: fs) to (x :: xs) =
71+
if isPrefixOf (f :: fs) (x :: xs)
72+
then to ++ replaceAllChars k (f :: fs) to (drop (length fs) xs)
73+
else x :: replaceAllChars k (f :: fs) to xs
74+
6175
replaceAll : String -> String -> String -> String
6276
replaceAll from to str =
63-
case unpack from of
64-
[] => str -- empty search string, return unchanged
65-
(fc :: _) =>
66-
case break (== fc) (unpack str) of
67-
(before, []) => str
68-
(before, (r :: rs)) =>
69-
if isPrefixOf (unpack from) (r :: rs)
70-
then pack before ++ to ++ replaceAll from to (pack $ drop (length from) (r :: rs))
71-
else pack before ++ singleton r ++
72-
replaceAll from to (pack rs)
77+
let chars = unpack str
78+
in pack (replaceAllChars (length chars) (unpack from) (unpack to) chars)
7379

7480
||| Check if a string contains any unescaped dangerous characters
7581
export

0 commit comments

Comments
 (0)