fix(validator): FHIR string/markdown/code regex rejects non-breaking spaces (Unicode vs XSD \S) - #472
Open
mauripunzueta wants to merge 1 commit into
Open
fix(validator): FHIR string/markdown/code regex rejects non-breaking spaces (Unicode vs XSD \S)#472mauripunzueta wants to merge 1 commit into
mauripunzueta wants to merge 1 commit into
Conversation
…ntics FHIR primitive `pattern` facets are XSD regexes, and XSD defines `\s` as ASCII-only (`[#x20#x9#xD#xA]`). Rust's `regex` crate makes `\s`/`\S` Unicode-aware, so a value containing U+00A0 (non-breaking space) — Unicode whitespace but not XSD whitespace — was wrongly rejected: it matches neither the literal `[ \r\n\t]` branch nor `\S` in the R4/R4B `string`/`markdown` pattern `[ \r\n\t\S]+`, so the anchored match fails on a valid FHIR string. The same Unicode `\s` also affected `code` (`[^\s]+...`) on every version. Fix at the compile site in `engine/primitives.rs::compiled()`: compile with `unicode(false)` so the shorthand classes carry XSD/ASCII semantics. Because `unicode(false)` on the `&str` engine refuses any program that could match invalid UTF-8 (which `\S`/`[^\s]` can), compile on the `regex::bytes` engine and match the value's UTF-8 bytes. The value is always valid UTF-8, so the only observable effect is ASCII shorthand classes. Across every primitive pattern in all four versions, `\s`/`\S` are the only shorthands present (no `.`, `\d`, `\w`), so byte matching is exactly XSD-equivalent here. Packs stay spec-literal (no regeneration) and no storage/DB is touched. Fixes #425
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
mauripunzueta
marked this pull request as ready for review
July 31, 2026 15:24
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.
Fixes #425.
Problem
FHIR primitive
patternfacets are XSD regexes, and XSD defines the shorthand whitespace class as ASCII only —\s==[#x20#x9#xD#xA]. Rust'sregexcrate makes\s/\SUnicode-aware by default, so U+00A0 (non-breaking space) — Unicode whitespace but not XSD whitespace — matches neither the literal[ \r\n\t]branch nor\Sin the R4/R4Bstring/markdownpattern[ \r\n\t\S]+. The anchored match fails and a valid FHIR string is reported invalid.string/markdown[ \r\n\t\S]+(bug)[ \r\n\t\S]+(bug)[\s\S]+(ok)[\s\S]+(ok)code([^\s]+…)Root cause & fix
The single compile site is
crates/fhir-validator/src/engine/primitives.rs::compiled(), which compiled^(?:{pattern})$on the Unicode-awareregex::Regex. The fix compiles withunicode(false)so the shorthand classes carry XSD/ASCII semantics.The obvious spelling —
(?-u:…)(which the issue suggested) orRegexBuilder…unicode(false)on the&strengine — does not compile: the&strengine refuses any program that could match invalid UTF-8, and(?-u:\S)/(?-u:[^\s])can. The fix therefore compiles on theregex::bytesengine and matches the value's UTF-8 bytes. The validator's input is always a valid UTF-8&str(Value::as_str()), so the only observable effect is that\s/\S/\d/\wbecome ASCII.Why byte matching is safe/equivalent here: across every primitive pattern in all four versions,
\sand\Sare the only shorthand classes that appear (instring/markdown,code,uri/url/canonical\S*, and R4/R4Bbase64Binary). No primitive pattern uses.,\d, or\w, and every bounded quantifier (id {1,64},base64 {4}, dates, uuid) operates on explicit ASCII classes where 1 byte = 1 char. So byte matching is exactly XSD-equivalent — verified empirically and by a 5-persona design review.Scope
Fixing at the compile site inherently corrects both the issue's headline case (
string/markdown, R4/R4B) and thecodeU+00A0 residual (all versions) — same one-line root cause, no extra diff.uri/base64/R5-R6 are already correct under the new engine and are covered by regression tests.Tests
Added unit tests in
primitives.rs(exercised throughcompiled(), so they cover real anchoring + engine + flags):string/markdownR4/R4B accepts U+00A0 (the exact issue case) and lone U+00A0; still accepts spaces/tabs; still rejects empty.[\s\S]+proven unchanged.codetreats U+00A0 as content (single token) but still rejects leading/trailing real spaces.uri\S*rejects a real space, accepts U+00A0, accepts empty.dateandidstill reject a stray U+00A0 (the fix does not blanket-accept non-ASCII);base64Binary{4}grouping unaffected.Full validator suite is green on
R4,R4B,R5,fhirpath;cargo fmt --checkclean.Notes
known-failures-*.jsonbaselines live only on the test(validator): run the pack tests in CI, sweep the official FHIR example corpus #423 branch, not onmain. When test(validator): run the pack tests in CI, sweep the official FHIR example corpus #423 lands, its R4/R4B baselines must be regenerated — the U+00A0 entries currently recorded as failures become correct passes; that drop is the intended effect of this fix, not a regression. The unit tests here are the guardrail onmain.