1use prikk_error::{PrikkError, Result};
11
12#[must_use]
22pub fn ascii_fold(name: &str) -> String {
23 name.to_ascii_lowercase()
24}
25
26pub fn validate_repo_path(value: &str) -> Result<()> {
28 if value.is_empty() {
29 return Err(PrikkError::InvalidName(
30 "repository path must not be empty".to_string(),
31 ));
32 }
33 if value.starts_with('/') {
34 return Err(PrikkError::InvalidName(
35 "absolute paths are not allowed".to_string(),
36 ));
37 }
38 if value.contains('\\') {
39 return Err(PrikkError::InvalidName(
40 "backslashes are not allowed in repository paths".to_string(),
41 ));
42 }
43 if value.contains(':') {
44 return Err(PrikkError::InvalidName(
45 "colon characters are not allowed in repository paths".to_string(),
46 ));
47 }
48 if !value.is_ascii() {
49 return Err(PrikkError::InvalidName(
50 "non-ASCII paths are deferred until Unicode NFC normalization is implemented"
51 .to_string(),
52 ));
53 }
54 if value.bytes().any(|byte| byte < 0x20 || byte == 0x7f) {
55 return Err(PrikkError::InvalidName(
56 "control characters are not allowed in repository paths".to_string(),
57 ));
58 }
59 for (index, component) in value.split('/').enumerate() {
60 if index == 0 && component.eq_ignore_ascii_case(".prikk") {
61 return Err(PrikkError::InvalidName(
62 "repository paths must not target the .prikk metadata directory".to_string(),
63 ));
64 }
65 validate_component(component)?;
66 }
67 Ok(())
68}
69
70fn validate_component(component: &str) -> Result<()> {
71 if component.is_empty() {
72 return Err(PrikkError::InvalidName(
73 "empty path components are not allowed".to_string(),
74 ));
75 }
76 if component == "." || component == ".." {
77 return Err(PrikkError::InvalidName(
78 "dot path components are not allowed".to_string(),
79 ));
80 }
81 if component.ends_with(' ') || component.ends_with('.') {
82 return Err(PrikkError::InvalidName(
83 "path components must not end with space or dot".to_string(),
84 ));
85 }
86 if is_windows_reserved_name(component) {
87 return Err(PrikkError::InvalidName(format!(
88 "Windows reserved path component is not allowed: {component}"
89 )));
90 }
91 Ok(())
92}
93
94#[must_use]
99pub fn is_windows_reserved_name(component: &str) -> bool {
100 let base = component
101 .split('.')
102 .next()
103 .unwrap_or(component)
104 .to_ascii_uppercase();
105 matches!(base.as_str(), "CON" | "PRN" | "AUX" | "NUL")
106 || matches!(
107 base.as_str(),
108 "COM1" | "COM2" | "COM3" | "COM4" | "COM5" | "COM6" | "COM7" | "COM8" | "COM9"
109 )
110 || matches!(
111 base.as_str(),
112 "LPT1" | "LPT2" | "LPT3" | "LPT4" | "LPT5" | "LPT6" | "LPT7" | "LPT8" | "LPT9"
113 )
114}