Skip to main content

prikk_replay/
path.rs

1//! Repository-relative lexical path validation.
2//!
3//! This module owns the canonical repository-relative path value used by replay/lifecycle semantic
4//! state. Filesystem layout, materialization policy, and worktree ownership remain in `prikk-store`.
5//! The path subset is intentionally ASCII-only until Unicode NFC normalization is designed and
6//! tested.
7//!
8//! The lexical grammar itself (`validate_repo_path`) moved to `prikk-object` (DC-54): object
9//! envelope encoders need to call it without creating a `prikk-object -> prikk-replay` dependency
10//! cycle. Re-exported here so every existing `prikk_replay::validate_repo_path` caller keeps
11//! compiling unchanged.
12
13use std::collections::BTreeSet;
14
15use prikk_error::{PrikkError, Result};
16
17use prikk_object::ascii_fold;
18pub use prikk_object::validate_repo_path;
19
20/// A validated repository-relative path.
21#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct RepoPath(String);
23
24impl RepoPath {
25    /// Validate and construct a repository-relative path.
26    pub fn parse(value: &str) -> Result<Self> {
27        validate_repo_path(value)?;
28        Ok(Self(value.to_string()))
29    }
30
31    /// Return the canonical slash-separated representation.
32    #[must_use]
33    pub fn as_str(&self) -> &str {
34        &self.0
35    }
36}
37
38/// Reject duplicate paths and case-insensitive collisions.
39pub fn validate_no_path_collisions(paths: &[RepoPath]) -> Result<()> {
40    let mut exact = BTreeSet::<&str>::new();
41    let mut folded = BTreeSet::<String>::new();
42    for path in paths {
43        let exact_value = path.as_str();
44        if !exact.insert(exact_value) {
45            return Err(PrikkError::InvalidName(format!(
46                "duplicate repository path: {exact_value}"
47            )));
48        }
49        let folded_value = ascii_fold(exact_value);
50        if !folded.insert(folded_value) {
51            return Err(PrikkError::InvalidName(format!(
52                "case-insensitive path collision involving: {exact_value}"
53            )));
54        }
55    }
56    Ok(())
57}
58
59#[cfg(test)]
60mod tests;