Skip to main content

loadsmith_install/rule/
glob.rs

1use std::borrow::Cow;
2
3use camino::{Utf8Path, Utf8PathBuf};
4use globset::{Glob, GlobBuilder};
5use loadsmith_core::PackageRef;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub struct GlobRule {
10    pattern: Glob,
11    pub(crate) target: Cow<'static, Utf8Path>,
12    strip_levels: usize,
13    subdir: bool,
14    pub(super) use_links: bool,
15}
16
17impl GlobRule {
18    pub fn new(pattern: Glob, target: impl Into<Cow<'static, Utf8Path>>) -> Self {
19        Self {
20            pattern,
21            target: target.into(),
22            strip_levels: 0,
23            subdir: false,
24            use_links: false,
25        }
26    }
27
28    pub fn try_from_pattern(
29        pattern: impl AsRef<str>,
30        target: impl Into<Cow<'static, Utf8Path>>,
31    ) -> Result<Self, globset::Error> {
32        let pattern = GlobBuilder::new(pattern.as_ref()).build()?;
33        Ok(Self::new(pattern, target))
34    }
35
36    pub fn strip_top_level(mut self, strip_top_level: bool) -> Self {
37        self.strip_levels = if strip_top_level { 1 } else { 0 };
38        self
39    }
40
41    pub fn strip_levels(mut self, strip_levels: usize) -> Self {
42        self.strip_levels = strip_levels;
43        self
44    }
45
46    pub fn use_links(mut self, use_links: bool) -> Self {
47        self.use_links = use_links;
48        self
49    }
50
51    pub fn with_subdir(mut self, subdir: bool) -> Self {
52        self.subdir = subdir;
53        self
54    }
55
56    pub fn matches(&self, path: impl AsRef<Utf8Path>) -> bool {
57        self.pattern.compile_matcher().is_match(path.as_ref())
58    }
59
60    pub fn map_file(
61        &self,
62        path: impl AsRef<Utf8Path>,
63        package: &PackageRef,
64    ) -> Option<Utf8PathBuf> {
65        let path = path.as_ref();
66
67        let suffix = {
68            let mut components = path.components();
69            for _ in 0..self.strip_levels {
70                components.next();
71            }
72
73            if components.clone().next().is_none() {
74                return None;
75            } else {
76                components.as_path()
77            }
78        };
79
80        let mut target = self.target.to_path_buf();
81        if self.subdir {
82            target.push(package.id().as_str());
83        }
84        target.push(suffix);
85
86        Some(target)
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use loadsmith_core::{PackageId, Version};
93
94    use super::*;
95
96    #[test]
97    fn matches() {
98        let rule = GlobRule::new(
99            Glob::new("*.rs").unwrap(),
100            Cow::Borrowed(Utf8Path::new("target")),
101        );
102
103        assert!(rule.matches("main.rs"));
104        assert!(rule.matches("src/main.rs"));
105        assert!(!rule.matches("main.txt"));
106        assert!(!rule.matches("src/main.txt"));
107    }
108
109    #[test]
110    fn map_file() {
111        let rule = GlobRule::try_from_pattern("*.rs", Utf8Path::new("target")).unwrap();
112
113        let package = PackageRef::new(PackageId::new("Author-Name"), Version::new(1, 0, 0));
114
115        assert_eq!(
116            rule.map_file("main.rs", &package),
117            Some(Utf8PathBuf::from("target/main.rs"))
118        );
119
120        assert_eq!(
121            rule.map_file("src/main.rs", &package),
122            Some(Utf8PathBuf::from("target/src/main.rs"))
123        );
124
125        assert_eq!(
126            rule.map_file("main.txt", &package),
127            Some(Utf8PathBuf::from("target/main.txt"))
128        );
129    }
130
131    #[test]
132    fn map_file_strip_top() {
133        let rule = GlobRule::try_from_pattern("in/**", Utf8Path::new("out"))
134            .unwrap()
135            .strip_top_level(true);
136        let package = PackageRef::new(PackageId::new("Author-Name"), Version::new(1, 0, 0));
137
138        assert_eq!(
139            rule.map_file("in/file.txt", &package),
140            Some(Utf8PathBuf::from("out/file.txt"))
141        );
142
143        assert_eq!(
144            rule.map_file("in/nested/file.txt", &package),
145            Some(Utf8PathBuf::from("out/nested/file.txt"))
146        );
147
148        assert_eq!(
149            rule.map_file("in/nested/deep/file.txt", &package),
150            Some(Utf8PathBuf::from("out/nested/deep/file.txt"))
151        );
152
153        assert_eq!(rule.map_file("other.txt", &package), None);
154    }
155
156    #[test]
157    fn map_file_strip_multiple() {
158        let rule = GlobRule::try_from_pattern("UE4SS/Mods/*", Utf8Path::new("shimloader/mod"))
159            .unwrap()
160            .strip_levels(2);
161        let package = PackageRef::new(PackageId::new("Author-Name"), Version::new(1, 0, 0));
162
163        assert_eq!(rule.map_file("file.dll", &package), None);
164
165        assert_eq!(rule.map_file("UE4SS/file.dll", &package), None);
166
167        assert_eq!(
168            rule.map_file("UE4SS/Mods/file.dll", &package),
169            Some(Utf8PathBuf::from("shimloader/mod/file.dll"))
170        );
171
172        assert_eq!(
173            rule.map_file("UE4SS/Mods/nested/file.dll", &package),
174            Some(Utf8PathBuf::from("shimloader/mod/nested/file.dll"))
175        );
176
177        assert_eq!(
178            rule.map_file("UE4SS/Mods/nested/deep/file.dll", &package),
179            Some(Utf8PathBuf::from("shimloader/mod/nested/deep/file.dll"))
180        );
181    }
182}