Skip to main content

is_spdx_expression_shape

Function is_spdx_expression_shape 

Source
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 / OR keyword match);
  • every byte in the SPDX expression alphabet: ASCII alphanumeric plus ., -, +, (, ), : (the DocumentRef-…:LicenseRef-… separator), and a single ASCII space (token separator). Tabs, control characters, non-ASCII bytes, _ (not in idstring), , (SPDX uses AND / OR keywords, not comma), / (the dual-license/A colloquial 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.