Skip to main content

pacenotes_installer\engine/
overlap.rs

1/// A file more than one package in a plan writes. Not an error: the later
2/// package wins, the log says so, and the server is told (`installer/CLAUDE.md`).
3#[derive(Clone, Debug, PartialEq, Eq)]
4pub struct Overlap {
5    /// As the packages spell it.
6    pub path: String,
7    /// In install order; the last one wins.
8    pub packages: Vec<String>,
9}
10
11impl Overlap {
12    /// The writers as `directory/name` refs, the shape a conflict report carries
13    /// under its one game; a key that does not split is dropped, since the
14    /// server discards a malformed entry.
15    pub fn refs(&self) -> Vec<String> {
16        self.packages
17            .iter()
18            .filter_map(|key| {
19                let (_, directory, name) = pacenotes_shared::split_download_path(key)?;
20                Some(format!("{directory}/{name}"))
21            })
22            .collect()
23    }
24}
25
26/// Compared folded, as the filesystem compares names; the file lists come from
27/// the caller, who reads each zip once.
28pub fn overlaps(
29    install_order: &[String],
30    files: &std::collections::BTreeMap<String, Vec<String>>,
31) -> Vec<Overlap> {
32    let mut writers: std::collections::BTreeMap<String, (String, Vec<String>)> =
33        std::collections::BTreeMap::new();
34    for package in install_order {
35        for path in files.get(package).into_iter().flatten() {
36            let (_, packages) = writers
37                .entry(crate::ledger::fold(path))
38                .or_insert_with(|| (path.clone(), Vec::new()));
39            packages.push(package.clone());
40        }
41    }
42    writers
43        .into_values()
44        .filter(|(_, packages)| packages.len() > 1)
45        .map(|(path, packages)| Overlap { path, packages })
46        .collect()
47}
48
49#[cfg(test)]
50mod tests {
51    const KAMIL: &str = "rbr/public/codriver/polish/male/Kamil";
52    const POLISH: &str = "rbr/public/language/polish";
53
54    fn files_of(entries: &[(&str, &[&str])]) -> std::collections::BTreeMap<String, Vec<String>> {
55        entries
56            .iter()
57            .map(|(package, paths)| {
58                (
59                    package.to_string(),
60                    paths.iter().map(|path| path.to_string()).collect(),
61                )
62            })
63            .collect()
64    }
65
66    fn install_order() -> [String; 2] {
67        [POLISH.to_string(), KAMIL.to_string()]
68    }
69
70    #[test]
71    fn overlapping_writers_are_reported_in_install_order() {
72        let files = files_of(&[
73            (
74                POLISH,
75                &[
76                    "Plugins/Pacenote/PaceNote.ini",
77                    "Plugins/Pacenote/language/polish/strings.ini",
78                ],
79            ),
80            (
81                KAMIL,
82                &[
83                    "Plugins/Pacenote/PaceNote.ini",
84                    "Plugins/Pacenote/sounds/polish/male/Kamil/1-1.ogg",
85                ],
86            ),
87        ]);
88        let found = super::overlaps(&install_order(), &files);
89        assert_eq!(found.len(), 1);
90        assert_eq!(found[0].path, "Plugins/Pacenote/PaceNote.ini");
91        assert_eq!(found[0].packages, install_order().to_vec());
92    }
93
94    #[test]
95    fn an_overlap_is_found_across_spellings() {
96        let files = files_of(&[
97            (POLISH, &["Plugins/Pacenote/X.ini"]),
98            (KAMIL, &["plugins/pacenote/x.INI"]),
99        ]);
100        assert_eq!(super::overlaps(&install_order(), &files).len(), 1);
101    }
102
103    #[test]
104    fn packages_that_share_nothing_report_nothing() {
105        let files = files_of(&[(POLISH, &["Plugins/a.ini"]), (KAMIL, &["Plugins/b.ini"])]);
106        assert!(super::overlaps(&install_order(), &files).is_empty());
107    }
108
109    fn overlap_of(packages: &[&str]) -> super::Overlap {
110        super::Overlap {
111            path: "Plugins/Pacenote/PaceNote.ini".into(),
112            packages: packages.iter().map(|key| key.to_string()).collect(),
113        }
114    }
115
116    #[test]
117    fn an_overlap_names_its_writers_the_way_a_report_does() {
118        let refs = overlap_of(&[POLISH, KAMIL]).refs();
119        assert_eq!(
120            refs,
121            vec![
122                "public/language/polish",
123                "public/codriver/polish/male/Kamil"
124            ]
125        );
126        for reference in refs {
127            assert!(
128                pacenotes_shared::PackageRef::parse(&reference).is_some(),
129                "{reference}"
130            );
131        }
132    }
133
134    #[test]
135    fn an_unsplittable_key_is_left_out_of_the_refs() {
136        assert_eq!(
137            overlap_of(&["nonsense", KAMIL]).refs(),
138            vec!["public/codriver/polish/male/Kamil"]
139        );
140    }
141}