Skip to main content

pkg/
package_state.rs

1use crate::{
2    package::{RemoteName, RemotePackage},
3    PackageName,
4};
5use pkgar_keys::PublicKeyFile;
6use serde_derive::{Deserialize, Serialize};
7use std::{
8    cmp::Ordering,
9    collections::{BTreeMap, BTreeSet},
10};
11
12/// Contains current user packages state
13#[derive(Serialize, Deserialize, Debug, Clone)]
14#[serde(default)]
15pub struct PackageState {
16    /// list of can't be accidentally uninstalled packages
17    pub protected: BTreeSet<PackageName>,
18    /// installed public keys per remote name.
19    /// using pkgar_keys as a wrapper of dryoc public key.
20    pub pubkeys: BTreeMap<RemoteName, PublicKeyFile>,
21    /// install state per packages
22    pub installed: BTreeMap<PackageName, InstallState>,
23}
24
25#[derive(Serialize, Deserialize, Default, Debug, Clone)]
26#[serde(default)]
27pub struct InstallState {
28    pub remote: RemoteName,
29    pub blake3: String,
30    pub manual: bool,
31    // only useful during install
32    #[serde(skip_serializing)]
33    pub network_size: u64,
34    pub storage_size: u64,
35    pub dependencies: BTreeSet<PackageName>,
36    pub dependents: BTreeSet<PackageName>,
37}
38
39#[derive(Default, Debug, Clone)]
40pub struct PackageList {
41    pub install: Vec<PackageName>,
42    pub uninstall: Vec<PackageName>,
43    pub update: Vec<PackageName>,
44    pub install_size: u64,
45    pub network_size: u64,
46    pub uninstall_size: u64,
47}
48
49impl PackageState {
50    pub fn from_toml(text: &str) -> Result<Self, toml::de::Error> {
51        toml::from_str(text)
52    }
53
54    pub fn to_toml(&self) -> String {
55        // to_string *should* be safe to unwrap for this struct
56        toml::to_string(self).unwrap()
57    }
58
59    // mutably add valid packages to the graph.
60    /// Returns list of packages that need to be resolved,
61    /// which are not yet added to the package config.
62    /// If zero vector returned, it means all package deps are satisfied
63    pub fn install(&mut self, packages: &[RemotePackage]) -> Vec<PackageName> {
64        let mut missing_set = BTreeSet::new();
65        let mut missing_deps = Vec::new();
66        let package_names: BTreeSet<&PackageName> =
67            packages.iter().map(|p| &p.package.name).collect();
68
69        let mut recursion = 100;
70        loop {
71            let mut has_new_missing_deps = false;
72
73            for pkg in packages {
74                if missing_set.contains(&pkg.package.name) {
75                    continue;
76                }
77
78                let mut has_missing_deps = false;
79                for dep_name in &pkg.package.depends {
80                    if self.installed.contains_key(dep_name) {
81                    } else if !package_names.contains(dep_name) {
82                        if missing_set.insert(dep_name.clone()) {
83                            missing_deps.push(dep_name.clone());
84                        }
85                        has_missing_deps = true;
86                    } else if missing_set.contains(dep_name) {
87                        has_missing_deps = true;
88                    } else {
89                    }
90                }
91
92                if has_missing_deps {
93                    if missing_set.insert(pkg.package.name.clone()) {
94                        missing_deps.push(pkg.package.name.clone());
95                    }
96                    // dependents should be marked as missing well
97                    has_new_missing_deps = true;
98                }
99            }
100
101            if !has_new_missing_deps {
102                break;
103            }
104
105            if recursion == 0 {
106                panic!("Dependencies recursion exhausted");
107            }
108            recursion -= 1;
109        }
110
111        // all packages with their dependents should be satisfied
112        let mut unsatisfied_deps: BTreeMap<PackageName, BTreeSet<PackageName>> = BTreeMap::new();
113        for rpkg in packages {
114            let pkg = &rpkg.package;
115            if missing_set.contains(&pkg.name) {
116                continue;
117            }
118
119            let (manual, dependents, remote) = if let Some(existing) = self.installed.get(&pkg.name)
120            {
121                (
122                    existing.manual,
123                    existing.dependents.clone(),
124                    existing.remote.clone(),
125                )
126            } else {
127                (
128                    false,
129                    unsatisfied_deps.remove(&pkg.name).unwrap_or_default(),
130                    rpkg.remote.to_string(),
131                )
132            };
133
134            let new_state = InstallState {
135                remote,
136                blake3: pkg.blake3.clone(),
137                manual,
138                network_size: pkg.network_size,
139                storage_size: pkg.storage_size,
140                dependencies: pkg.depends.iter().cloned().collect(),
141                dependents,
142            };
143
144            self.installed.insert(pkg.name.clone(), new_state);
145
146            for dep_name in &pkg.depends {
147                if let Some(dep_state) = self.installed.get_mut(dep_name) {
148                    dep_state.dependents.insert(pkg.name.clone());
149                } else {
150                    if let Some(dep_state) = unsatisfied_deps.get_mut(dep_name) {
151                        dep_state.insert(pkg.name.clone());
152                    } else {
153                        let mut dep_state = BTreeSet::new();
154                        dep_state.insert(pkg.name.clone());
155                        unsatisfied_deps.insert(dep_name.clone(), dep_state);
156                    }
157                }
158            }
159        }
160
161        if !unsatisfied_deps.is_empty() {
162            panic!("Some unsatisfied deps are remained: {:?}", unsatisfied_deps);
163        }
164
165        missing_deps
166    }
167
168    // mutably remove packages from the graph.
169    /// Returns list of packages that also need to be resolved,
170    /// which are not all of their deps is listed in list of packages.
171    /// If zero vector returned, it means uninstallation can be executed.
172    pub fn uninstall(&mut self, packages: &[PackageName]) -> Vec<PackageName> {
173        let mut pending_resolution = Vec::new();
174        let mut packages_to_remove = packages.to_vec();
175
176        // Filter out protected packages. Caller can wipe out the list beforehand to skip this behaviour.
177        packages_to_remove.retain(|name| !self.protected.contains(name));
178
179        let remove_set: BTreeSet<&PackageName> = packages_to_remove.iter().collect();
180        let mut safe_to_remove = Vec::new();
181
182        for name in &packages_to_remove {
183            let Some(state) = self.installed.get(name) else {
184                continue;
185            };
186            let missing_dependents: Vec<_> = state
187                .dependents
188                .iter()
189                .cloned()
190                .filter(|dep| !remove_set.contains(dep))
191                .collect();
192            let missing_dependencies: Vec<_> = state
193                .dependencies
194                .iter()
195                .cloned()
196                .filter(|dep| {
197                    !remove_set.contains(dep) && self.installed.get(dep).is_some_and(|p| !p.manual)
198                })
199                .collect();
200
201            if missing_dependents.is_empty() && missing_dependencies.is_empty() {
202                safe_to_remove.push(name.clone());
203            } else {
204                pending_resolution.extend(missing_dependents);
205                pending_resolution.push(name.clone());
206                pending_resolution.extend(missing_dependencies);
207            }
208        }
209
210        for name in safe_to_remove {
211            if let Some(state) = self.installed.remove(&name) {
212                for dep_name in &state.dependencies {
213                    if let Some(dep_state) = self.installed.get_mut(dep_name) {
214                        dep_state.dependents.remove(&name);
215                    }
216                }
217            }
218        }
219
220        pending_resolution
221    }
222
223    // Diff between old and new state, returns list of installed and uninstalled packages
224    pub fn diff(&self, newer: &Self) -> PackageList {
225        let mut diff = PackageList::default();
226
227        let mut old = self.installed.iter();
228        let mut new = newer.installed.iter();
229        let mut old_item = old.next();
230        let mut new_item = new.next();
231
232        loop {
233            match (old_item, new_item) {
234                (Some((k1, v1)), Some((k2, v2))) => match k1.cmp(k2) {
235                    Ordering::Less => {
236                        diff.uninstall.push(k1.clone());
237                        diff.uninstall_size += v1.storage_size;
238                        old_item = old.next();
239                    }
240                    Ordering::Greater => {
241                        diff.install.push(k2.clone());
242                        diff.install_size += v2.storage_size;
243                        diff.network_size += v2.network_size;
244                        new_item = new.next();
245                    }
246                    Ordering::Equal => {
247                        if v1.blake3 != v2.blake3 {
248                            diff.update.push(k1.clone());
249                            diff.install_size += v2.storage_size;
250                            diff.uninstall_size += v1.storage_size;
251                            diff.network_size += v2.network_size;
252                        }
253                        old_item = old.next();
254                        new_item = new.next();
255                    }
256                },
257                (Some((k1, v1)), None) => {
258                    diff.uninstall.push(k1.clone());
259                    diff.uninstall_size += v1.storage_size;
260                    old_item = old.next();
261                }
262                (None, Some((k2, v2))) => {
263                    diff.install.push(k2.clone());
264                    diff.install_size += v2.storage_size;
265                    diff.network_size += v2.network_size;
266                    new_item = new.next();
267                }
268                (None, None) => break,
269            }
270        }
271
272        diff
273    }
274
275    pub fn get_installed_list(&self) -> Vec<PackageName> {
276        self.installed.keys().cloned().collect()
277    }
278
279    /// Mark packages manually installed or not. Returns list of changed packages.
280    /// PackageState are not marked automatically in any install mechanism.
281    pub fn mark_as_manual(&mut self, manual: bool, packages: &[PackageName]) -> Vec<PackageName> {
282        let mut marked = Vec::new();
283
284        for package in packages {
285            if let Some(pkg) = self.installed.get_mut(package) {
286                if pkg.manual == manual {
287                    continue;
288                }
289                pkg.manual = manual;
290                marked.push(package.clone());
291            }
292        }
293        marked
294    }
295}
296
297impl Default for PackageState {
298    fn default() -> Self {
299        Self {
300            // TODO: Hardcoded
301            protected: vec![
302                PackageName::new("kernel").unwrap(),
303                PackageName::new("base-initfs").unwrap(),
304                PackageName::new("base").unwrap(),
305                PackageName::new("ion").unwrap(),
306                PackageName::new("pkg").unwrap(),
307                PackageName::new("relibc").unwrap(),
308                PackageName::new("libgcc").unwrap(),
309                PackageName::new("libstdcxx").unwrap(),
310            ]
311            .into_iter()
312            .collect(),
313            pubkeys: Default::default(),
314            installed: Default::default(),
315        }
316    }
317}
318
319impl PackageList {
320    pub fn is_empty(&self) -> bool {
321        self.install.is_empty() && self.uninstall.is_empty() && self.update.is_empty()
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use crate::Package;
328
329    use super::*;
330
331    // --- Helper Functions for Test Data ---
332
333    fn cpkg(name: &str) -> PackageName {
334        PackageName::new(name).unwrap()
335    }
336
337    fn mock_package(name: &str, depends: Vec<&str>) -> RemotePackage {
338        RemotePackage {
339            package: Package {
340                name: cpkg(name),
341                version: "1.0.0".to_string(),
342                target: "x86_64-unknown-redox".to_string(),
343                blake3: "hash".to_string(),
344                source_identifier: "src".to_string(),
345                commit_identifier: "commit".to_string(),
346                time_identifier: "time".to_string(),
347                storage_size: 1000,
348                network_size: 500,
349                depends: depends.into_iter().map(|s| cpkg(s)).collect(),
350            },
351            remote: "origin".into(),
352        }
353    }
354
355    fn mock_empty_db() -> PackageState {
356        PackageState {
357            protected: BTreeSet::new(),
358            pubkeys: BTreeMap::new(),
359            installed: BTreeMap::new(),
360        }
361    }
362
363    #[test]
364    fn test_install_simple_success() {
365        let mut db = mock_empty_db();
366        let nano = mock_package("nano", vec![]);
367        let packages = vec![nano];
368        let names = vec![cpkg("nano")];
369
370        let missing = db.install(&packages);
371
372        assert_eq!(missing, vec![]);
373        assert_eq!(db.get_installed_list(), names);
374        assert_eq!(db.installed[&cpkg("nano")].manual, false);
375        assert_eq!(db.installed[&cpkg("nano")].remote, "origin");
376
377        assert_eq!(db.mark_as_manual(true, &names), vec![cpkg("nano")]);
378        assert_eq!(db.installed[&cpkg("nano")].manual, true);
379    }
380
381    #[test]
382    fn test_install_missing_dependency() {
383        let mut db = mock_empty_db();
384        let bash = mock_package("bash", vec!["readline", "terminfo"]);
385        let readline = mock_package("readline", vec!["ncurses"]);
386        let ncurses = mock_package("ncurses", vec![]);
387        let terminfo = mock_package("terminfo", vec![]);
388        let packages = vec![bash, readline, terminfo, ncurses];
389        // 1-st
390        let missing = db.install(&packages[..1]);
391        assert_eq!(
392            missing,
393            vec![cpkg("readline"), cpkg("terminfo"), cpkg("bash")]
394        );
395        assert_eq!(db.get_installed_list(), vec![]);
396        // 2-nd
397        let missing = db.install(&packages[..3]);
398        assert_eq!(
399            missing,
400            vec![cpkg("ncurses"), cpkg("readline"), cpkg("bash")]
401        );
402        assert_eq!(db.get_installed_list(), vec![cpkg("terminfo")]);
403        // 3-rd
404        let missing = db.install(&packages[..]);
405        assert_eq!(missing, vec![]);
406        assert_eq!(
407            db.get_installed_list(),
408            vec![
409                cpkg("bash"),
410                cpkg("ncurses"),
411                cpkg("readline"),
412                cpkg("terminfo"),
413            ]
414        );
415
416        assert_eq!(
417            db.installed[&cpkg("bash")].dependents,
418            vec![].iter().cloned().collect()
419        );
420        assert_eq!(
421            db.installed[&cpkg("readline")].dependents,
422            vec![cpkg("bash")].iter().cloned().collect()
423        );
424        assert_eq!(
425            db.installed[&cpkg("ncurses")].dependents,
426            vec![cpkg("readline")].iter().cloned().collect()
427        );
428    }
429
430    #[test]
431    fn test_uninstall_dependent() {
432        let mut db = mock_empty_db();
433        let base = mock_package("base", vec![]);
434        let init = mock_package("base-initfs", vec!["redoxfs"]);
435        let redoxfs = mock_package("redoxfs", vec![]);
436        db.install(&[base, init, redoxfs]);
437        let result = db.uninstall(&[cpkg("redoxfs")]);
438        assert_eq!(
439            db.get_installed_list(),
440            vec![cpkg("base"), cpkg("base-initfs"), cpkg("redoxfs")]
441        );
442        assert_eq!(result, vec![cpkg("base-initfs"), cpkg("redoxfs")]);
443        let result = db.uninstall(&result);
444        assert_eq!(result, vec![]);
445        assert_eq!(db.get_installed_list(), vec![cpkg("base")]);
446    }
447
448    #[test]
449    fn test_uninstall_with_dependencies_unmarked() {
450        let mut db = mock_empty_db();
451
452        let gettext = mock_package("gettext", vec!["libiconv"]);
453        let libiconv = mock_package("libiconv", vec![]);
454        db.install(&[gettext, libiconv]);
455        let result = db.uninstall(&[cpkg("gettext")]);
456        assert_eq!(result, vec![cpkg("gettext"), cpkg("libiconv")]);
457        assert_eq!(
458            db.get_installed_list(),
459            vec![cpkg("gettext"), cpkg("libiconv")]
460        );
461        let result = db.uninstall(&result);
462        assert_eq!(result, vec![]);
463        assert_eq!(db.get_installed_list(), vec![]);
464    }
465
466    #[test]
467    fn test_uninstall_with_dependencies_marked() {
468        let mut db = mock_empty_db();
469
470        let gettext = mock_package("gettext", vec!["libiconv"]);
471        let libiconv = mock_package("libiconv", vec![]);
472        db.install(&[gettext, libiconv]);
473        let result = db.mark_as_manual(true, &vec![cpkg("gettext"), cpkg("libiconv")]);
474        assert_eq!(result.len(), 2usize);
475        let result = db.uninstall(&[cpkg("gettext")]);
476        assert_eq!(result, vec![]);
477        assert_eq!(db.get_installed_list(), vec![cpkg("libiconv")]);
478    }
479
480    #[test]
481    fn test_toml_integration() -> Result<(), toml::de::Error> {
482        const TOML_DATA: &str = r#"
483            [installed.bash]
484            remote = "origin"
485            blake3 = "abc"
486            manual = true
487            storage_size = 3000
488            network_size = 2000
489            dependencies = ["ncurses"]
490            dependents = []
491
492            [installed.ncurses]
493            remote = "origin"
494            blake3 = "def"
495            manual = false
496            storage_size = 2000
497            network_size = 1000
498            dependencies = []
499            dependents = ["bash"]
500        "#;
501
502        let db: PackageState = PackageState::from_toml(TOML_DATA)?;
503
504        assert_eq!(db.get_installed_list(), vec![cpkg("bash"), cpkg("ncurses")]);
505
506        Ok(())
507    }
508}