Skip to main content

structfs_path_validation/
lib.rs

1//! The canonical StructFS path component grammar.
2//!
3//! This crate exists so that everything that validates path components —
4//! `structfs-core-store` at runtime and `structfs-path-macro` at compile
5//! time — shares one implementation. A forked grammar can drift; a shared
6//! one cannot.
7//!
8//! # Grammar
9//!
10//! A path component is one of:
11//!
12//! - A **pure numeric string** (`0`, `42`) — used for array indexing.
13//! - A **Unicode identifier** per UAX#31: first char is `XID_Start`, or an
14//!   underscore followed by at least one `XID_Continue` char; remaining
15//!   chars are `XID_Continue`.
16//!
17//! Empty components, bare `_`, and components containing `/`, `-`, spaces,
18//! or other punctuation are invalid. Arbitrary strings can be made valid
19//! with Namecode encoding (see `PathComponent::encode` in core-store).
20
21/// Validate a single path component against the StructFS grammar.
22///
23/// Returns a human-readable description of the problem on failure.
24pub fn validate_component(component: &str) -> Result<(), String> {
25    if component.is_empty() {
26        return Err("empty component".to_string());
27    }
28
29    // Allow pure numeric strings (for array indexing)
30    if component.chars().all(|c| c.is_ascii_digit()) {
31        return Ok(());
32    }
33
34    let mut chars = component.chars();
35    let first = chars.next().unwrap();
36
37    // First char: XID_Start or underscore followed by XID_Continue
38    let valid_start = unicode_ident::is_xid_start(first)
39        || (first == '_'
40            && chars
41                .clone()
42                .next()
43                .is_some_and(unicode_ident::is_xid_continue));
44
45    if !valid_start {
46        return Err("must start with a letter or underscore followed by letter/digit".to_string());
47    }
48
49    // Rest: XID_Continue
50    for c in chars {
51        if !unicode_ident::is_xid_continue(c) {
52            return Err(format!("invalid character '{}' in identifier", c));
53        }
54    }
55
56    Ok(())
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn valid_components() {
65        for s in ["foo", "_foo", "café", "名前", "0", "42", "a1", "_1"] {
66            assert!(validate_component(s).is_ok(), "expected valid: {s:?}");
67        }
68    }
69
70    #[test]
71    fn invalid_components() {
72        for s in ["", "_", "-", "a-b", "a b", ".hidden", "1abc", "a/b", "a$b"] {
73            assert!(validate_component(s).is_err(), "expected invalid: {s:?}");
74        }
75    }
76}