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#[derive(Serialize, Deserialize, Debug, Clone)]
14#[serde(default)]
15pub struct PackageState {
16 pub protected: BTreeSet<PackageName>,
18 pub pubkeys: BTreeMap<RemoteName, PublicKeyFile>,
21 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 #[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 toml::to_string(self).unwrap()
57 }
58
59 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 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 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 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 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 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 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 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 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 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 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 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}