bern_protocol/
resource.rs1pub fn check_resource_name(name: &str) -> Result<(), String> {
12 if name.is_empty() {
13 return Err("a resource name cannot be empty".into());
14 }
15 if name.starts_with('/') {
16 return Err(format!(
17 "resource `{name}` is absolute; name it relative to Bern.toml"
18 ));
19 }
20 for segment in name.split('/') {
21 if segment.is_empty() {
22 return Err(format!("resource `{name}` has an empty path segment"));
23 }
24 if segment.starts_with('.') {
25 return Err(format!(
26 "resource `{name}`: `{segment}` starts with `.`; hidden files, `.` and `..` are not resources"
27 ));
28 }
29 if let Some(invalid) = segment
30 .chars()
31 .find(|c| !(c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.')))
32 {
33 return Err(format!(
34 "resource `{name}` contains `{invalid}`; names use ASCII letters, digits, `-`, `_` and `.`"
35 ));
36 }
37 }
38 Ok(())
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44
45 #[test]
46 fn names_are_portable_relative_paths() {
47 for name in ["media/aurora.mp4", "seed.json", "a/b-c/d_e.f.txt", "x"] {
48 assert_eq!(check_resource_name(name), Ok(()), "{name}");
49 }
50 for name in [
51 "",
52 "/media/a.mp4",
53 "media//a.mp4",
54 "media/",
55 "../a.mp4",
56 "media/./a.mp4",
57 ".hidden",
58 "media\\a.mp4",
59 "media/a b.mp4",
60 "影片.mp4",
61 ] {
62 assert!(check_resource_name(name).is_err(), "{name}");
63 }
64 }
65}