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)]
28pub enum InstallRule {
29 Glob(GlobRule),
31 Route(RouteRule),
33}
34
35#[derive(Debug, Clone, Copy)]
60pub struct InstallRuleset<'a> {
61 exclude: Option<&'a GlobSet>,
62 rules: &'a [InstallRule],
63 default_rule: Option<&'a InstallRule>,
64}
65
66#[derive(Debug, Clone, Default)]
85pub struct OwnedInstallRuleset {
86 exclude: Option<GlobSet>,
87 rules: Vec<InstallRule>,
88 default_rule: Option<usize>,
89}
90
91impl InstallRule {
92 pub fn matches(&self, path: impl AsRef<Utf8Path>) -> bool {
94 let path = path.as_ref();
95 self.matches_path(path) || self.matches_extension(path)
96 }
97
98 pub fn matches_path(&self, path: impl AsRef<Utf8Path>) -> bool {
103 let path = path.as_ref();
104 match self {
105 InstallRule::Glob(glob) => glob.matches(path),
106 InstallRule::Route(route) => route.matches_path(path),
107 }
108 }
109
110 pub fn matches_extension(&self, path: impl AsRef<Utf8Path>) -> bool {
115 let path = path.as_ref();
116 match self {
117 InstallRule::Glob(glob) => glob.matches(path),
118 InstallRule::Route(route) => route.matches_extension(path),
119 }
120 }
121
122 pub fn map_file(
124 &self,
125 path: impl AsRef<Utf8Path>,
126 package: &PackageRef,
127 ) -> Option<Utf8PathBuf> {
128 match self {
129 InstallRule::Glob(glob) => glob.map_file(path, package),
130 InstallRule::Route(route) => route.map_file(path, package),
131 }
132 }
133
134 pub fn matches_mapped(&self, path: impl AsRef<Utf8Path>) -> bool {
136 let path = path.as_ref();
137 match self {
138 InstallRule::Glob(glob) => path.starts_with(&*glob.target),
139 InstallRule::Route(route) => path.starts_with(&*route.target),
140 }
141 }
142
143 pub fn use_links(&self) -> bool {
145 match self {
146 InstallRule::Glob(glob) => glob.use_links,
147 InstallRule::Route(route) => route.use_links(),
148 }
149 }
150
151 pub fn conflict_strategy(&self) -> ConflictStrategy {
156 match self {
157 InstallRule::Glob(_) => ConflictStrategy::Overwrite,
158 InstallRule::Route(route) => route.conflict_strategy(),
159 }
160 }
161}
162
163impl From<GlobRule> for InstallRule {
164 fn from(glob: GlobRule) -> Self {
165 InstallRule::Glob(glob)
166 }
167}
168
169impl From<RouteRule> for InstallRule {
170 fn from(route: RouteRule) -> Self {
171 InstallRule::Route(route)
172 }
173}
174
175impl<'a> InstallRuleset<'a> {
176 pub const fn new(rules: &'a [InstallRule]) -> Self {
178 Self {
179 rules,
180 default_rule: None,
181 exclude: None,
182 }
183 }
184
185 pub fn with_default_rule(mut self, default_rule: &'a InstallRule) -> Self {
187 self.default_rule = Some(default_rule);
188 self
189 }
190
191 pub fn with_exclude(mut self, exclude: &'a GlobSet) -> Self {
193 self.exclude = Some(exclude);
194 self
195 }
196
197 pub fn rules(&self) -> &[InstallRule] {
199 self.rules
200 }
201
202 pub fn default_rule(&self) -> Option<&InstallRule> {
204 self.default_rule
205 }
206
207 pub fn exclude(&self) -> Option<&GlobSet> {
209 self.exclude
210 }
211
212 pub fn find_rule_for_path(&self, path: impl AsRef<Utf8Path>) -> Option<&'a InstallRule> {
217 let path = path.as_ref();
218 self.rules
219 .iter()
220 .find(|rule| rule.matches_path(path))
221 .or_else(|| self.rules.iter().find(|rule| rule.matches_extension(path)))
222 .or(self.default_rule)
223 }
224
225 pub fn find_rule_for_mapped_path(&self, path: impl AsRef<Utf8Path>) -> Option<&'a InstallRule> {
227 let path = path.as_ref();
228 self.rules.iter().find(|rule| rule.matches_mapped(path))
229 }
230
231 pub fn map_file(
235 &self,
236 path: impl AsRef<Utf8Path>,
237 package: &PackageRef,
238 ) -> Option<Utf8PathBuf> {
239 self.map_file_and_return_rule(path, package)
240 .map(|(mapped, _rule)| mapped)
241 }
242
243 pub fn map_file_and_return_rule(
248 &self,
249 path: impl AsRef<Utf8Path>,
250 package: &PackageRef,
251 ) -> Option<(Utf8PathBuf, &InstallRule)> {
252 let path = path.as_ref();
253 if self.is_excluded(path) {
254 return None;
255 }
256
257 self.find_rule_for_path(path)
258 .and_then(|rule| rule.map_file(path, package).map(|path| (path, rule)))
259 }
260
261 pub fn is_excluded(&self, path: impl AsRef<Path>) -> bool {
263 self.exclude
264 .as_ref()
265 .map_or(false, |exclude| exclude.is_match(path))
266 }
267}
268
269impl OwnedInstallRuleset {
270 pub fn from_rule_iter<I, R>(rules: I, default_rule_index: Option<usize>) -> Option<Self>
275 where
276 I: IntoIterator<Item = R>,
277 R: Into<InstallRule>,
278 {
279 let rules = rules.into_iter().map(Into::into).collect();
280 Self::with_rules(rules, default_rule_index)
281 }
282
283 pub fn with_rules(rules: Vec<InstallRule>, default_rule_index: Option<usize>) -> Option<Self> {
287 if default_rule_index.is_some_and(|index| !(0..rules.len()).contains(&index)) {
288 return None;
289 }
290
291 Some(Self {
292 rules,
293 default_rule: default_rule_index,
294 exclude: None,
295 })
296 }
297
298 pub fn with_exclude(mut self, exclude: GlobSet) -> Self {
300 self.exclude = Some(exclude);
301 self
302 }
303
304 pub fn add_rule(&mut self, rule: InstallRule) {
306 self.rules.push(rule);
307 }
308
309 pub fn insert_rule(&mut self, index: usize, rule: InstallRule) {
311 if let Some(default_index) = self.default_rule {
312 if index <= default_index {
313 self.default_rule = Some(default_index + 1);
314 }
315 }
316
317 self.rules.insert(index, rule);
318 }
319
320 pub fn set_exclude(&mut self, exclude: GlobSet) {
322 self.exclude = Some(exclude);
323 }
324
325 pub fn rules(&self) -> &[InstallRule] {
327 &self.rules
328 }
329
330 pub fn default_rule(&self) -> Option<&InstallRule> {
332 self.default_rule.map(|index| &self.rules[index])
333 }
334
335 pub fn exclude(&self) -> Option<&GlobSet> {
337 self.exclude.as_ref()
338 }
339
340 pub fn into_parts(self) -> (Vec<InstallRule>, Option<usize>, Option<GlobSet>) {
343 (self.rules, self.default_rule, self.exclude)
344 }
345
346 pub fn as_ref(&self) -> InstallRuleset<'_> {
348 InstallRuleset {
349 rules: &self.rules,
350 default_rule: self.default_rule.map(|index| &self.rules[index]),
351 exclude: self.exclude.as_ref(),
352 }
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use loadsmith_core::Version;
359
360 use super::*;
361
362 #[test]
363 fn ruleset_matches_path_over_extension() {
364 let rules = vec![
365 InstallRule::Route(RouteRule::new_static("Mods").with_file_extension("dll")),
366 InstallRule::Route(RouteRule::new_static("Plugins")),
367 ];
368
369 let ruleset = InstallRuleset::new(&rules);
370
371 let pkg = PackageRef::new("test".to_string(), Version::new(1, 0, 0));
372
373 assert_eq!(
376 ruleset.map_file("Plugins/Plugin.dll", &pkg),
377 Some(Utf8PathBuf::from("Plugins/test/Plugin.dll"))
378 );
379 }
380}