9acae0f91625a1f6e13601276f2e11b319d346d872e85d758377fd649da5d5d8.tri (8264B)
1 !import base as B 2 !import native_math as N 3 !import io_v2 as IO 4 !export readSource 5 !export readText 6 !export encodeText 7 !export writeText 8 !export lineCursor 9 !export readLine 10 11 problem operation detail progress = ["invalid-data" operation detail progress] 12 -- base.foldr supplies accumulator first, unlike the conventional Haskell API. 13 flatten chunks = B.foldr (suffix chunk : B.append chunk suffix) [] (B.reverse chunks) 14 15 checkedLimit limit next = case B.checkContract B.nat? limit of 16 ((t t), (count, t)) = case N.lte count 9223372036854775807 of 17 (t t) = next count 18 _ = IO.pure (B.err ["invalid-bound" "text/limit" "invalid text byte limit" "none"] t) 19 _ = IO.pure (B.err ["invalid-bound" "text/limit" "invalid text byte limit" "none"] t) 20 21 -- A bounded split, not a scalar validator: native encode validates the complete 22 -- prefix before it can produce bytes. [] signals an improper character spine. 23 split 0 rest = [[] rest] 24 split _ [] = [[] []] 25 split count [c | rest] = case split (N.predecessor count) rest of 26 [prefix after] = [(B.pair c prefix) after] 27 _ = [] 28 split _ _ = [] 29 30 encodeLoop remaining text chunks = case split 1024 text of 31 [[] []] = IO.pure (B.ok (flatten chunks) t) 32 [part rest] = do IO.onOk 33 bytes <- IO.encodeUTF8 part 34 case N.lte (B.length bytes) remaining of 35 (t t) = encodeLoop (N.subtract remaining (B.length bytes)) rest (B.pair bytes chunks) 36 _ = IO.pure (B.err ["invalid-bound" "text/encode" "encoded text exceeds byte limit" "none"] t) 37 _ = IO.pure (B.err (problem "text/encode" "improper scalar list" "none") t) 38 encodeText limit text = checkedLimit limit (remaining : encodeLoop remaining text []) 39 40 -- Force the small acknowledged-byte counter before the next request, without 41 -- retaining arithmetic that depends on previously emitted chunks. 42 forceCount t next = next t 43 forceCount (t bit rest) next = case bit of 44 t = forceCount rest next 45 (t t) = forceCount rest next 46 _ = IO.fail "Invalid text counter" 47 forceCount _ _ = IO.fail "Invalid text counter" 48 writeFailure confirmed cause = IO.pure (B.err ["write-text" confirmed cause] t) 49 writeLoop writer text confirmed = forceCount confirmed (_ : case split 1024 text of 50 [[] []] = IO.pure (B.ok t t) 51 [part rest] = IO.bind (IO.encodeUTF8 part) (encoded : case encoded of 52 ((t t), (bytes, t)) = IO.bind (IO.writeAll writer bytes) (written : case written of 53 ((t t), (t, t)) = writeLoop writer rest (N.add confirmed (B.length bytes)) 54 (t, (cause, t)) = writeFailure confirmed cause 55 _ = IO.fail "Invalid text write reply") 56 (t, (cause, t)) = writeFailure confirmed cause 57 _ = IO.fail "Invalid UTF-8 encode reply") 58 _ = writeFailure confirmed (problem "text/write" "improper scalar list" "none")) 59 writeText writer text = writeLoop writer text 0 60 61 -- Whole-source convenience, not a constant-memory text stream. Retain the exact 62 -- received octets and decoded scalars without normalization or BOM stripping. 63 -- A one-byte EOF probe detects excess input at the budget boundary. Callers must 64 -- not share this cursor concurrently; the individual reads are leased, not the 65 -- entire source convenience operation. Composite failure retains confirmed input 66 -- bytes (before any uncertain failing read) plus the unchanged native problem. 67 readFailure consumed cause = IO.pure (B.err ["read-source" consumed cause] t) 68 readCount 0 = 1 69 readCount remaining = case N.lte remaining 4093 of 70 (t t) = remaining 71 _ = 4093 72 readLoop reader limit remaining carry first rawChunks textChunks = 73 IO.bind (IO.readSome reader (readCount remaining)) (received : case received of 74 ((t t), (("eof", t), t)) = IO.bind (IO.decodeUTF8 1 carry) (decoded : case decoded of 75 ((t t), ([[] []], t)) = IO.pure (B.ok (B.pair (flatten rawChunks) (flatten textChunks)) t) 76 (t, (cause, t)) = readFailure (N.subtract limit remaining) cause 77 _ = IO.fail "Invalid UTF-8 final reply") 78 ((t t), (("data", bytes), t)) = let count = B.length bytes in 79 case N.lte count remaining of 80 (t t) = IO.bind (IO.decodeUTF8 0 (B.append carry bytes)) (decoded : case decoded of 81 ((t t), ([text pending], t)) = case first of 82 1 = case text of 83 [] = readLoop reader limit (N.subtract remaining count) pending 1 (B.pair bytes rawChunks) textChunks 84 [65279 | _] = readFailure (N.add (N.subtract limit remaining) count) 85 (problem "text/read-source" "initial source BOM is not permitted" "none") 86 _ = readLoop reader limit (N.subtract remaining count) pending 0 (B.pair bytes rawChunks) (B.pair text textChunks) 87 _ = readLoop reader limit (N.subtract remaining count) pending 0 (B.pair bytes rawChunks) (B.pair text textChunks) 88 (t, (cause, t)) = readFailure (N.add (N.subtract limit remaining) count) cause 89 _ = IO.fail "Invalid UTF-8 decode reply") 90 _ = readFailure (N.add (N.subtract limit remaining) count) 91 ["invalid-bound" "text/read-source" "source exceeds byte limit" "none"] 92 (t, (cause, t)) = readFailure (N.subtract limit remaining) cause 93 _ = IO.fail "Invalid source read reply") 94 readSource reader limit = checkedLimit limit (count : readLoop reader count count [] 1 [] []) 95 96 -- Single-consumer source cursor. Thread ONLY the returned cursor forward; do not 97 -- interleave raw reads, duplicate/reuse a cursor, or resume it after read failure. 98 -- Read-ahead is bounded to 4093 bytes. This is not a native resource or a lock. 99 lineCursor reader = [reader [] 0] 100 101 -- LF is framing, not part of the line budget. CR, BOM and NUL remain content. 102 -- Scan before decoding: malformed bytes after LF belong to the NEXT line. 103 lineSplit [] = [[] [] 0] 104 lineSplit [10 | rest] = [[] rest 1] 105 lineSplit [byte | rest] = case lineSplit rest of 106 [prefix after ended] = [(B.pair byte prefix) after ended] 107 _ = [] 108 lineSplit _ = [] 109 lineFailure consumed cause = IO.pure (B.err ["read-line" consumed cause] t) 110 lineReady reader after ending chunks = IO.pure 111 (B.ok [["line" (flatten chunks) ending] [reader after 0]] t) 112 lineEOF reader consumed carry chunks = IO.bind (IO.decodeUTF8 1 carry) (decoded : case decoded of 113 ((t t), ([[] []], t)) = case consumed of 114 0 = IO.pure (B.ok [["eof"] [reader [] 1]] t) 115 _ = IO.pure (B.ok [["line" (flatten chunks) "eof"] [reader [] 1]] t) 116 (t, (cause, t)) = lineFailure consumed cause 117 _ = IO.fail "Invalid line final UTF-8 reply") 118 lineLoop reader limit remaining carry chunks buffered = forceCount remaining (_ : case buffered of 119 [] = IO.bind (IO.readSome reader 4093) (received : case received of 120 ((t t), (("eof", t), t)) = lineEOF reader (N.subtract limit remaining) carry chunks 121 ((t t), (("data", bytes), t)) = lineLoop reader limit remaining carry chunks bytes 122 (t, (cause, t)) = lineFailure (N.subtract limit remaining) cause 123 _ = IO.fail "Invalid line read reply") 124 _ = case lineSplit buffered of 125 [part after ended] = let count = B.length part in 126 case N.lte count remaining of 127 (t t) = IO.bind (IO.decodeUTF8 ended (B.append carry part)) (decoded : case decoded of 128 ((t t), ([text pending], t)) = case ended of 129 1 = lineReady reader after "lf" (B.pair text chunks) 130 _ = lineLoop reader limit (N.subtract remaining count) pending (B.pair text chunks) [] 131 (t, (cause, t)) = lineFailure (N.add (N.subtract limit remaining) count) cause 132 _ = IO.fail "Invalid line UTF-8 reply") 133 _ = lineFailure (N.add (N.subtract limit remaining) count) 134 ["invalid-bound" "text/read-line" "line exceeds byte limit" "none"] 135 _ = IO.fail "Invalid line cursor bytes") 136 readLine cursor limit = checkedLimit limit (count : case cursor of 137 [reader buffered 0] = lineLoop reader count count [] [] buffered 138 [reader [] 1] = IO.pure (B.ok [["eof"] cursor] t) 139 _ = IO.pure (B.err ["invalid-argument" "text/read-line" "invalid line cursor" "none"] t)) 140 141 -- Generic bounded whole-text convenience. Unlike readSource, initial BOM is 142 -- ordinary content. It shares the incremental decoder, not the source policy. 143 readText reader limit = checkedLimit limit (count : IO.onOk 144 (readLoop reader count count [] 0 [] []) 145 (received : case received of 146 (_, text) = IO.pure (B.ok text t) 147 _ = IO.fail "Invalid text read result"))