pacenotes_shared/
package_ref.rs1#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
4pub struct PackageRef {
5 pub directory: String,
6 pub name: String,
7}
8
9impl PackageRef {
10 pub fn parse(text: &str) -> Option<PackageRef> {
13 let (directory, name) = text.split_once('/')?;
14 if directory.is_empty() || directory.contains('\\') || !crate::valid_name(name) {
15 return None;
16 }
17 Some(PackageRef {
18 directory: directory.to_string(),
19 name: name.to_string(),
20 })
21 }
22}
23
24impl std::fmt::Display for PackageRef {
25 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 write!(formatter, "{}/{}", self.directory, self.name)
27 }
28}
29
30#[cfg(test)]
31mod tests {
32 #[test]
33 fn a_ref_splits_directory_from_a_path_shaped_name() {
34 let reference = super::PackageRef::parse("public/codriver/polish/male/Kamil").unwrap();
35 assert_eq!(reference.directory, "public");
36 assert_eq!(reference.name, "codriver/polish/male/Kamil");
37 assert_eq!(reference.to_string(), "public/codriver/polish/male/Kamil");
38 }
39
40 #[test]
41 fn a_ref_round_trips_through_its_text_form() {
42 let reference = super::PackageRef::parse("premium-voices/pro").unwrap();
43 assert_eq!(reference.directory, "premium-voices");
44 assert_eq!(reference.name, "pro");
45 assert_eq!(reference.to_string(), "premium-voices/pro");
46 }
47
48 #[test]
49 fn a_ref_can_never_widen_into_a_filesystem_path() {
50 for bad in [
51 "pro",
52 "public",
53 "/pro",
54 "/codriver/Kamil",
55 "public/",
56 "public/../secrets",
57 "public\\pro",
58 "public\\codriver/Kamil",
59 "..\\..\\data/auth.db",
60 ] {
61 assert!(
62 super::PackageRef::parse(bad).is_none(),
63 "{bad:?} should be rejected"
64 );
65 }
66 }
67}