1pub const NAMESPACE_PREFIXES: &[&str] = &[
12 "home", "tmp", "dev", "sys", "etc", "lib", "boot", "usr", "var",
13];
14
15#[cfg(test)]
18pub(crate) fn canonicalize_path(p: &str) -> String {
19 let stripped = p.trim_start_matches('/');
20 let first = stripped.split('/').next().unwrap_or("");
21 if NAMESPACE_PREFIXES.contains(&first) || first == "proc" {
22 stripped.to_owned()
23 } else {
24 format!("home/{stripped}")
25 }
26}
27
28#[cfg(test)]
33pub(crate) fn valid_world_name(world_name: &str) -> bool {
34 validate_world_name(world_name).is_ok()
35}
36
37pub fn validate_world_name(world_name: &str) -> Result<(), &'static str> {
40 if world_name.is_empty() {
41 return Err("world path is empty");
42 }
43 if is_reserved_world_name(world_name) {
44 return Err("world path is a reserved namespace root");
45 }
46 if world_name.contains('\\') {
47 return Err("world path contains backslash");
48 }
49 if world_name.chars().any(char::is_control) {
50 return Err("world path contains control bytes");
51 }
52 for segment in world_name.split('/') {
53 if segment.is_empty() {
54 return Err("world path has empty segment");
55 }
56 if is_dot_segment(segment) {
57 return Err("world path contains dot or encoded-dot segment");
58 }
59 }
60 Ok(())
61}
62
63fn is_dot_segment(segment: &str) -> bool {
72 let Some(rest) = strip_dot_token(segment) else {
73 return false;
74 };
75 rest.is_empty()
76 || strip_dot_token(rest)
77 .map(|tail| tail.is_empty())
78 .unwrap_or(false)
79}
80
81fn strip_dot_token(segment: &str) -> Option<&str> {
85 if let Some(rest) = segment.strip_prefix('.') {
86 return Some(rest);
87 }
88 if segment
89 .as_bytes()
90 .get(..3)
91 .is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"%2e"))
92 {
93 return Some(&segment[3..]);
94 }
95 None
96}
97
98fn is_reserved_world_name(world_name: &str) -> bool {
103 NAMESPACE_PREFIXES.contains(&world_name)
104 || matches!(world_name, "proc" | "var/log")
105 || world_name.starts_with("proc/")
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn canonicalize_preserves_explicit_namespaces() {
114 assert_eq!(canonicalize_path("/home/tmp/foo"), "home/tmp/foo");
115 assert_eq!(canonicalize_path("/home/etc/foo"), "home/etc/foo");
116 assert_eq!(canonicalize_path("/tmp/foo"), "tmp/foo");
117 assert_eq!(canonicalize_path("/etc/foo"), "etc/foo");
118 assert_eq!(canonicalize_path("/foo"), "home/foo");
119 }
120
121 #[test]
122 fn control_bytes_are_not_valid_world_names() {
123 assert!(valid_world_name("home/ok"));
124 assert!(!valid_world_name("home/bad\nname"));
125 assert!(!valid_world_name(""));
126 }
127
128 #[test]
129 fn dot_segments_empty_segments_and_backslashes_are_not_valid_world_names() {
130 assert!(!valid_world_name("home/../etc/secret"));
131 assert!(!valid_world_name("home/%2E%2E/etc/secret"));
132 assert!(!valid_world_name("home/./x"));
133 assert!(!valid_world_name("home//x"));
134 assert!(!valid_world_name("home/x/"));
135 assert!(!valid_world_name("home\\x"));
136 assert_eq!(
137 validate_world_name("home/%2E%2E/etc/secret"),
138 Err("world path contains dot or encoded-dot segment")
139 );
140 assert_eq!(
141 validate_world_name("home//x"),
142 Err("world path has empty segment")
143 );
144 assert_eq!(
145 validate_world_name("home\\x"),
146 Err("world path contains backslash")
147 );
148 }
149
150 #[test]
151 fn namespace_roots_and_proc_subtree_are_not_world_names() {
152 for name in [
153 "home",
154 "tmp",
155 "dev",
156 "sys",
157 "proc",
158 "proc/anything",
159 "etc",
160 "lib",
161 "boot",
162 "usr",
163 "var",
164 "var/log",
165 ] {
166 assert!(!valid_world_name(name), "{name}");
167 }
168 assert!(valid_world_name("home/x"));
169 assert!(valid_world_name("var/log/deletes"));
170 }
171}