// `Bytes` topical primitive — byte-level pre-validation helpers.
//
// These are the operations a caller might run before deciding whether to
// wrap bytes in a `Text` (which validates at construction). They are also
// the validation entry points used internally by the topical primitives
// (`Graphemes.count(s)` etc.) before they delegate to their underscore
// implementations.
//
// Each public function takes the inline union `(String box | Array[U8] box)`
// rather than a `ByteSeq` alias — the name `ByteSeq` is already taken by
// Pony's builtin package for a more restrictive (val-only) union, and
// redeclaring it here would clash with the auto-imported builtin.
// `AllValid` sentinel returned by `Bytes.first_bad_utf8_offset` when the
// input is fully well-formed UTF-8.
primitive AllValid
fun string(): String val => "AllValid"
primitive Bytes
fun is_valid_utf8(b: (String box | Array[U8] box)): Bool =>
"""
True iff `b` is well-formed UTF-8 per RFC 3629. Rejects:
* overlong encodings
* surrogates (U+D800..U+DFFF)
* codepoints above U+10FFFF
* truncated sequences
"""
match first_bad_utf8_offset(b)
| AllValid => true
else false
end
fun first_bad_utf8_offset(b: (String box | Array[U8] box))
: (USize | AllValid)
=>
"""
Return the byte offset of the first byte that breaks UTF-8
well-formedness, or `AllValid` if every byte is fine. The offset
points at the byte where validation failed — for a truncated
sequence, that's the lead byte; for a bad continuation, the bad
byte itself.
"""
let size = match b
| let s: String box => s.size()
| let a: Array[U8] box => a.size()
end
var i: USize = 0
while i < size do
let b0 = try _at(b, i)? else return i end
if b0 < 0x80 then
// ASCII
i = i + 1
elseif b0 < 0xC2 then
// 0x80-0xBF: stray continuation byte
// 0xC0-0xC1: would be overlong 2-byte
return i
elseif b0 < 0xE0 then
// 0xC2-0xDF: 2-byte sequence
let b1 = try _at(b, i + 1)? else return i end
if not _is_cont(b1) then return i + 1 end
i = i + 2
elseif b0 < 0xF0 then
// 0xE0-0xEF: 3-byte sequence
let b1 = try _at(b, i + 1)? else return i end
let b2 = try _at(b, i + 2)? else return i end
// First continuation byte has range constraints to forbid
// overlong encodings and surrogates:
// 0xE0: b1 in 0xA0..0xBF (forbid overlong)
// 0xED: b1 in 0x80..0x9F (forbid surrogates U+D800..U+DFFF)
// else: b1 in 0x80..0xBF
let b1_ok =
if b0 == 0xE0 then (b1 >= 0xA0) and (b1 <= 0xBF)
elseif b0 == 0xED then (b1 >= 0x80) and (b1 <= 0x9F)
else _is_cont(b1)
end
if not b1_ok then return i + 1 end
if not _is_cont(b2) then return i + 2 end
i = i + 3
elseif b0 < 0xF5 then
// 0xF0-0xF4: 4-byte sequence (covers U+10000..U+10FFFF)
let b1 = try _at(b, i + 1)? else return i end
let b2 = try _at(b, i + 2)? else return i end
let b3 = try _at(b, i + 3)? else return i end
// First continuation byte has range constraints:
// 0xF0: b1 in 0x90..0xBF (forbid overlong)
// 0xF4: b1 in 0x80..0x8F (cap at U+10FFFF)
// else: b1 in 0x80..0xBF
let b1_ok =
if b0 == 0xF0 then (b1 >= 0x90) and (b1 <= 0xBF)
elseif b0 == 0xF4 then (b1 >= 0x80) and (b1 <= 0x8F)
else _is_cont(b1)
end
if not b1_ok then return i + 1 end
if not _is_cont(b2) then return i + 2 end
if not _is_cont(b3) then return i + 3 end
i = i + 4
else
// 0xF5-0xFF: invalid lead byte (would encode codepoints above
// U+10FFFF)
return i
end
end
AllValid
fun _is_cont(b: U8): Bool =>
(b >= 0x80) and (b <= 0xBF)
fun _at(b: (String box | Array[U8] box), i: USize): U8 ? =>
match b
| let s: String box => s(i)?
| let a: Array[U8] box => a(i)?
end