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;