Skip to main content

loadsmith_install/rule/
mod.rs

1use std::path::Path;
2
3use camino::{Utf8Path, Utf8PathBuf};
4use globset::GlobSet;
5use loadsmith_core::PackageRef;
6use serde::{Deserialize, Serialize};
7
8use crate::ConflictStrategy;
9
10pub use glob::GlobRule;
11pub use route::RouteRule;
12
13mod glob;
14mod route;
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub enum InstallRule {
18    Glob(GlobRule),
19    Route(RouteRule),
20}
21
22#[derive(Debug, Clone, Copy)]
23pub struct InstallRuleset<'a> {
24    exclude: Option<&'a GlobSet>,
25    rules: &'a [InstallRule],
26    default_rule: Option<&'a InstallRule>,
27}
28
29#[derive(Debug, Clone, Default)]
30pub struct OwnedInstallRuleset {
31    exclude: Option<GlobSet>,
32    rules: Vec<InstallRule>,
33    default_rule: Option<usize>,
34}
35
36impl InstallRule {
37    pub fn matches(&self, path: impl AsRef<Utf8Path>) -> bool {
38        let path = path.as_ref();
39        self.matches_path(path) || self.matches_extension(path)
40    }
41
42    pub fn matches_path(&self, path: impl AsRef<Utf8Path>) -> bool {
43        let path = path.as_ref();
44        match self {
45            InstallRule::Glob(glob) => glob.matches(path),
46            InstallRule::Route(route) => route.matches_path(path),
47        }
48    }
49
50    pub fn matches_extension(&self, path: impl AsRef<Utf8Path>) -> bool {
51        let path = path.as_ref();
52        match self {
53            InstallRule::Glob(glob) => glob.matches(path),
54            InstallRule::Route(route) => route.matches_extension(path),
55        }
56    }
57
58    pub fn map_file(
59        &self,
60        path: impl AsRef<Utf8Path>,
61        package: &PackageRef,
62    ) -> Option<Utf8PathBuf> {
63        match self {
64            InstallRule::Glob(glob) => glob.map_file(path, package),
65            InstallRule::Route(route) => route.map_file(path, package),
66        }
67    }
68
69    pub fn matches_mapped(&self, path: impl AsRef<Utf8Path>) -> bool {
70        let path = path.as_ref();
71        match self {
72            InstallRule::Glob(glob) => path.starts_with(&*glob.target),
73            InstallRule::Route(route) => path.starts_with(&*route.target),
74        }
75    }
76
77    pub fn use_links(&self) -> bool {
78        match self {
79            InstallRule::Glob(glob) => glob.use_links,
80            InstallRule::Route(route) => route.use_links(),
81        }
82    }
83
84    pub fn conflict_strategy(&self) -> ConflictStrategy {
85        match self {
86            InstallRule::Glob(_) => ConflictStrategy::Overwrite,
87            InstallRule::Route(route) => route.conflict_strategy(),
88        }
89    }
90}
91
92impl From<GlobRule> for InstallRule {
93    fn from(glob: GlobRule) -> Self {
94        InstallRule::Glob(glob)
95    }
96}
97
98impl From<RouteRule> for InstallRule {
99    fn from(route: RouteRule) -> Self {
100        InstallRule::Route(route)
101    }
102}
103
104impl<'a> InstallRuleset<'a> {
105    pub const fn new(rules: &'a [InstallRule]) -> Self {
106        Self {
107            rules,
108            default_rule: None,
109            exclude: None,
110        }
111    }
112
113    pub fn with_default_rule(mut self, default_rule: &'a InstallRule) -> Self {
114        self.default_rule = Some(default_rule);
115        self
116    }
117
118    pub fn with_exclude(mut self, exclude: &'a GlobSet) -> Self {
119        self.exclude = Some(exclude);
120        self
121    }
122
123    pub fn rules(&self) -> &[InstallRule] {
124        self.rules
125    }
126
127    pub fn default_rule(&self) -> Option<&InstallRule> {
128        self.default_rule
129    }
130
131    pub fn exclude(&self) -> Option<&GlobSet> {
132        self.exclude
133    }
134
135    pub fn find_rule_for_path(&self, path: impl AsRef<Utf8Path>) -> Option<&'a InstallRule> {
136        let path = path.as_ref();
137        self.rules
138            .iter()
139            .find(|rule| rule.matches_path(path))
140            .or_else(|| self.rules.iter().find(|rule| rule.matches_extension(path)))
141            .or(self.default_rule)
142    }
143
144    pub fn find_rule_for_mapped_path(&self, path: impl AsRef<Utf8Path>) -> Option<&'a InstallRule> {
145        let path = path.as_ref();
146        self.rules.iter().find(|rule| rule.matches_mapped(path))
147    }
148
149    pub fn map_file(
150        &self,
151        path: impl AsRef<Utf8Path>,
152        package: &PackageRef,
153    ) -> Option<Utf8PathBuf> {
154        self.map_file_and_return_rule(path, package)
155            .map(|(mapped, _rule)| mapped)
156    }
157
158    pub fn map_file_and_return_rule(
159        &self,
160        path: impl AsRef<Utf8Path>,
161        package: &PackageRef,
162    ) -> Option<(Utf8PathBuf, &InstallRule)> {
163        let path = path.as_ref();
164        if self.is_excluded(path) {
165            return None;
166        }
167
168        self.find_rule_for_path(path)
169            .and_then(|rule| rule.map_file(path, package).map(|path| (path, rule)))
170    }
171
172    pub fn is_excluded(&self, path: impl AsRef<Path>) -> bool {
173        self.exclude
174            .as_ref()
175            .map_or(false, |exclude| exclude.is_match(path))
176    }
177}
178
179impl OwnedInstallRuleset {
180    pub fn from_rule_iter<I, R>(rules: I, default_rule_index: Option<usize>) -> Option<Self>
181    where
182        I: IntoIterator<Item = R>,
183        R: Into<InstallRule>,
184    {
185        let rules = rules.into_iter().map(Into::into).collect();
186        Self::with_rules(rules, default_rule_index)
187    }
188
189    pub fn with_rules(rules: Vec<InstallRule>, default_rule_index: Option<usize>) -> Option<Self> {
190        if default_rule_index.is_some_and(|index| !(0..rules.len()).contains(&index)) {
191            return None;
192        }
193
194        Some(Self {
195            rules,
196            default_rule: default_rule_index,
197            exclude: None,
198        })
199    }
200
201    pub fn with_exclude(mut self, exclude: GlobSet) -> Self {
202        self.exclude = Some(exclude);
203        self
204    }
205
206    pub fn add_rule(&mut self, rule: InstallRule) {
207        self.rules.push(rule);
208    }
209
210    pub fn insert_rule(&mut self, index: usize, rule: InstallRule) {
211        if let Some(default_index) = self.default_rule {
212            if index <= default_index {
213                self.default_rule = Some(default_index + 1);
214            }
215        }
216
217        self.rules.insert(index, rule);
218    }
219
220    pub fn set_exclude(&mut self, exclude: GlobSet) {
221        self.exclude = Some(exclude);
222    }
223
224    pub fn rules(&self) -> &[InstallRule] {
225        &self.rules
226    }
227
228    pub fn default_rule(&self) -> Option<&InstallRule> {
229        self.default_rule.map(|index| &self.rules[index])
230    }
231
232    pub fn exclude(&self) -> Option<&GlobSet> {
233        self.exclude.as_ref()
234    }
235
236    pub fn into_parts(self) -> (Vec<InstallRule>, Option<usize>, Option<GlobSet>) {
237        (self.rules, self.default_rule, self.exclude)
238    }
239
240    pub fn as_ref(&self) -> InstallRuleset<'_> {
241        InstallRuleset {
242            rules: &self.rules,
243            default_rule: self.default_rule.map(|index| &self.rules[index]),
244            exclude: self.exclude.as_ref(),
245        }
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use loadsmith_core::Version;
252
253    use super::*;
254
255    #[test]
256    fn ruleset_matches_path_over_extension() {
257        let rules = vec![
258            InstallRule::Route(RouteRule::new_static("Mods").with_file_extension("dll")),
259            InstallRule::Route(RouteRule::new_static("Plugins")),
260        ];
261
262        let ruleset = InstallRuleset::new(&rules);
263
264        let pkg = PackageRef::new("test".to_string(), Version::new(1, 0, 0));
265
266        // The path takes precedence over file extension, so the "Plugins" rule
267        // should match even though the file has a .dll extension.
268        assert_eq!(
269            ruleset.map_file("Plugins/Plugin.dll", &pkg),
270            Some(Utf8PathBuf::from("Plugins/test/Plugin.dll"))
271        );
272    }
273}