Add UTF8.valid?, a byte-level UTF-8 validator#4
Merged
Conversation
Validates a byte array as well-formed UTF-8 per RFC 3629, rejecting overlong encodings, UTF-16 surrogate code points (U+D800..U+DFFF), and code points beyond U+10FFFF. The scan is allocation-free and pure Carp, mapping the lead-byte classification directly to Unicode Table 3-7. Adds 18 asserts covering ASCII, 2/3/4-byte sequences, the boundaries around the surrogate gap (U+D7FF/U+E000) and the 4-byte range (U+10000 min, U+10FFFF max), and malformed input (overlongs, lone and truncated continuations, both surrogate ends, >U+10FFFF, F5/FF leads). Regenerates docs.
There was a problem hiding this comment.
Build & Tests
Checked out claude/add-valid-predicate locally and ran the suite:
carp -x test/utf8.carp → 35/35 pass (17 existing + 18 new). CI is green on both ubuntu-latest and macos-latest; the single test job also runs angler lint, carp-fmt --check, and gendocs.carp, so those passed too.
Findings
I read the whole function, checked it against Unicode Table 3-7 / RFC 3629, then went well beyond the suite — 20 extra adversarial cases, all of which behave correctly:
- Lead-byte classification (
utf8.carp:164-174) is exhaustive and correct.0xC0/0xC1, the lone-continuation range0x80-0xBF, and0xF5-0xFFall fall through to(set! ok false). The tightened first-continuation ranges forE0(≥A0),ED(≤9F),F0(≥90),F4(≤8F) correctly kill overlongs, surrogates, and >U+10FFFF in a single pass. I confirmed each boundary empirically (E0 9F→reject /E0 A0→accept;F4 90→reject /F4 8F→accept). - Continuation handling (
utf8.carp:175-183) is sound. The truncation guard(> (+ i need) (- len 1))is correct, and everyunsafe-nthis provably in bounds (all indices ≤len-1after that guard). The first continuation is checked against the tightened[lo,hi]; the rest against[0x80,0xBF]via theforloop — I verified a bad 2nd/3rd continuation byte in 3- and 4-byte sequences is rejected (E2 82 00,F0 9F 98 00), which the suite doesn't cover. - Multi-codepoint advancement works.
aé€😀and back-to-back 3-byte sequences validate; interior faults after a valid prefix (61 FF 62,abc 80) are caught — it's not just checking the first character. - The unsigned-byte assumption holds.
Byte.to-intreturns 0-255 (confirmed255→255), which the≥194comparisons depend on. - Style fits. This
let-do/for/set!imperative idiom already appears into-runes(utf8.carp:80-89), so it's not a new pattern for the file. - Docs/changelog:
docs/UTF8.htmlwas regenerated; the repo has no CHANGELOG and the README is a narrative tutorial with no API list, so nothing else to update. - API shape — a predicate over
(Ref (Array Byte))rather than aUTF8/String— is the right call: validity is a byte-level property, and callers like web#40 can check before allocating.
Found no bugs, no missing edge cases, no style drift.
Verdict: merge
Correct, well-tested, and green across the full CI pipeline; I built it and ran 20 additional adversarial cases and it held up on every one. Merge is of course the maintainer's decision.
hellerve
approved these changes
Jul 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
UTF8.valid?(Fn [(Ref (Array Byte))] Bool)— a byte-level check for whether an array is well-formed UTF-8 per RFC 3629. It rejects:C0 80,E0 9F BF)U+D800..U+DFFF(e.g.ED A0 80)U+10FFFF(e.g.F4 90 80 80)F5..FF)The scan is allocation-free and pure Carp. The lead-byte classification maps directly to the Unicode standard's Table 3-7: each lead byte fixes how many continuation bytes follow and the tightened valid range of the first one (
E0→A0..BF,ED→80..9F,F0→90..BF,F4→80..8F), which is what kills overlongs, surrogates, and over-max in a single pass.Why
Follow-up to carpentry-org/web#40 (WebSocket text-frame UTF-8 validation). The review there was: "We should use the utf8 library for that." — but this library had no validator to reuse. This PR adds one so
web(and anyone else) can depend on it instead of hand-rolling the check.from-stringandString.from-bytesboth assume well-formed input, so a standalone guard for untrusted bytes is a natural fit here.The API is a predicate on raw bytes rather than on a
UTF8/Stringvalue on purpose: validity is a property of bytes, and callers (likeweb, whose WebSocket payloads are(Array Byte)) can check before constructing anything — no allocation, no throwaway string on the reject path.Tests
Adds 18 asserts to
test/utf8.carp: ASCII and empty input; valid 2/3/4-byte sequences (U+00A9,U+20AC,U+1F600); the boundaries either side of the surrogate gap (U+D7FF,U+E000) and of the 4-byte range (U+10000min,U+10FFFFmax); and malformed input — overlong 2- and 3-byte, lone/truncated continuations, both surrogate ends (U+D800,U+DFFF),>U+10FFFF, andF5/FFleads.Verification
carp -x test/utf8.carp→ 35/35 pass (17 existing + 18 new), run locally.carp-fmt --checkandanglerclean onutf8.carpandtest/utf8.carp.docs/UTF8.htmlregenerated viagendocs.carp.Once this is released, web#40 can drop its own
WebSocket.valid-utf8?and callUTF8.valid?.Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.