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 assert_eq!(
269 ruleset.map_file("Plugins/Plugin.dll", &pkg),
270 Some(Utf8PathBuf::from("Plugins/test/Plugin.dll"))
271 );
272 }
273}