pub fn is_spdx_expression_shape(s: &str) -> Result<(), String>Expand description
Predicate: assert that s is a valid SPDX-expression shape. The
contract — modeled on the SPDX 2.1 expression grammar
(compound-expression = simple-expression | "(" compound-expression ")" | compound-expression "WITH" exception-id | compound-expression "AND" compound-expression | compound-expression "OR" compound-expression; simple-expression = license-id | license-id "+" | "LicenseRef-" idstring | "DocumentRef-" idstring ":" "LicenseRef-" idstring; idstring = 1*(ALPHA / DIGIT / "-" / ".")), narrowed to the structural alphabet floor every realistic
SPDX expression in the wild uses:
- 1..=
SPDX_EXPRESSION_MAX_LEN(256) bytes; - no leading whitespace (paste-from-aligned-doc footgun);
- no trailing whitespace (paste-from-doc footgun — every
downstream SPDX parser splits on exact token boundaries and
a trailing space breaks the
WITH/AND/ORkeyword match); - every byte in the SPDX expression alphabet: ASCII alphanumeric
plus
.,-,+,(,),:(theDocumentRef-…:LicenseRef-…separator), and a single ASCII space (token separator). Tabs, control characters, non-ASCII bytes,_(not inidstring),,(SPDX usesAND/ORkeywords, not comma),/(thedual-license/Acolloquial idiom is non-SPDX), and every other punctuation byte are each surfaced with a self-locating reason naming the canonical authoring footgun.
The predicate is a structural floor — it enforces the alphabet +
length the SPDX grammar’s character class admits, not the full
expression-parse (compound-expression nesting, AND/OR/WITH
keyword placement, parenthesis balance, idstring well-formedness
per simple-expression production). A future tightening on the
:licenca axis can extend past this shape predicate into a full
SPDX parser + license-id allowlist (peer with how
is_git_repo_url is the structural floor on :repositorio and
a future flake-resolver might tighten the per-URL-scheme arm into
scheme-specific shape predicates). This gate closes the
_/,///tab/CR/LF/non-ASCII/multi-line-blob footguns
structurally at the manifest layer; the parser-shape arms remain
for a follow-up routine once a real SPDX-parser dep is justified.
Returns the parser-shaped reason on rejection (without wrapping in
any error variant) so each per-axis caller —
crate::Caixa::validate_licenca for the universal :licenca
axis at validate time, every future per-license axis (a future
:fonte :license per-dep license-pin axis, a future
per-UpgradeInstruction per-component license-compatibility axis,
a future Lacre per-resolved-dep license-closure axis) — wraps the
same reason in its own typed *Invalid { <axis>, reason } variant.
The reason wording is axis-agnostic (“SPDX expressions reject
leading whitespace”) so every call site reading the same diagnostic
points at the same rule; drift between any two axes’ rule
enforcement is a build error visible at this predicate, not a
per-renderer “this passed validate but helm lint rejected the
Chart.yaml license: value” surprise.
Empty input is rejected here (defensively) and at each call site
via the narrower crate::ManifestError::LicencaEmpty variant —
the same empty-first cascade is_dns_1123_label,
is_gateway_api_http_path, is_wit_world_ref,
is_nats_subject, is_wasi_keyvalue_slot, is_git_ref_name,
is_git_oid, is_git_repo_url, and is_cargo_feature_name
all carry.
Lifted as a typed substrate-side primitive on the same trajectory
the peer value-shape predicates already follow — the typed slot’s
valid set matches the downstream consumer’s accepted set (here,
the caixa-helm chart README.md ## License section + a
future SPDX-aware Chart.yaml license: emitter + the future
per-resolved-dep license-closure axis a forthcoming Lacre
extension would carry), structurally.
§Errors
Returns the parser-shaped reason naming the specific violation
(length / leading-whitespace / trailing-whitespace /
alphabet-class / tab / control-char / non-ASCII / _ /
,-list-separator-confusion / /-dual-license-idiom), without
wrapping in any error variant — every caller maps the same
String into its own typed *Invalid { <axis>, reason } enum
variant.