From 10df11d13fc078368048cb67cdbcc29bf15ac1ea Mon Sep 17 00:00:00 2001 From: JustPav Date: Fri, 7 Aug 2026 18:01:46 +0400 Subject: [PATCH 01/68] fix: Added new tests Co-Authored-By: Claude Sonnet 5 --- lib/lib/tests/plugin_decoder.rs | 159 +++++++++++++++++ lib/lib/tests/scripts_hook.rs | 306 ++++++++++++++++++++++++++++++++ 2 files changed, 465 insertions(+) create mode 100644 lib/lib/tests/plugin_decoder.rs create mode 100644 lib/lib/tests/scripts_hook.rs diff --git a/lib/lib/tests/plugin_decoder.rs b/lib/lib/tests/plugin_decoder.rs new file mode 100644 index 00000000..ba65299e --- /dev/null +++ b/lib/lib/tests/plugin_decoder.rs @@ -0,0 +1,159 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 JustPav +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +use std::collections::HashMap; +use std::fs::{create_dir_all, remove_dir_all, write}; +use std::path::PathBuf; + +use upac::plugin::decoder::error::DecoderError; +use upac::plugin::decoder::manifest::load_decoder_manifests; +use upac::plugin::decoder::triggers::build_trigger_table; +use upac::scripts::error::HookError; +use upac::scripts::file::HookFile; + +fn scratch_dir(name: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!("upac-test-plugin-decoder-{}-{name}", std::process::id())); + let _ = remove_dir_all(&dir); + create_dir_all(&dir).unwrap(); + + dir +} + +fn hook_file(priority: i32, triggers: &[(&str, &[&str])]) -> HookFile { + let mut triggers_map = HashMap::new(); + for (format, names) in triggers { + triggers_map.insert(format.to_string(), names.iter().map(|name| name.to_string()).collect()); + } + + HookFile { + priority, + critical: false, + operation: None, + timing: None, + triggers: triggers_map, + steps: Vec::new(), + } +} + +#[test] +fn build_trigger_table_matches_single_hook() { + let hooks = vec![hook_file(0, &[("deb", &["postinst"])])]; + + let table = build_trigger_table(&hooks, "deb").unwrap(); + + assert_eq!(table.len(), 1); + assert_eq!(table[0].name, "postinst"); + assert_eq!(table[0].hook_id, 0); +} + +#[test] +fn build_trigger_table_ignores_other_formats() { + let hooks = vec![hook_file(0, &[("rpm", &["posttrans"])])]; + + let table = build_trigger_table(&hooks, "deb").unwrap(); + + assert!(table.is_empty()); +} + +#[test] +fn build_trigger_table_picks_higher_priority_hook() { + let hooks = vec![ + hook_file(1, &[("deb", &["postinst"])]), + hook_file(5, &[("deb", &["postinst"])]), + ]; + + let table = build_trigger_table(&hooks, "deb").unwrap(); + + assert_eq!(table.len(), 1); + assert_eq!(table[0].hook_id, 1); +} + +#[test] +fn build_trigger_table_fails_on_priority_tie() { + let hooks = vec![ + hook_file(3, &[("deb", &["postinst"])]), + hook_file(3, &[("deb", &["postinst"])]), + ]; + + let result = build_trigger_table(&hooks, "deb"); + + assert!(matches!(result, Err(HookError::TriggerConflict(name)) if name == "postinst")); +} + +#[test] +fn build_trigger_table_keeps_distinct_names_independent() { + let hooks = vec![hook_file(0, &[("deb", &["postinst", "postrm"])])]; + + let table = build_trigger_table(&hooks, "deb").unwrap(); + let mut names: Vec<&str> = table.iter().map(|entry| entry.name.as_str()).collect(); + names.sort(); + + assert_eq!(names, vec!["postinst", "postrm"]); +} + +#[test] +fn load_decoder_manifests_collects_distinct_formats() { + let dir = scratch_dir("distinct-formats"); + write( + dir.join("deb.decoder"), + "format = \"deb\"\nextensions = [\"deb\"]\nlibrary = \"libupac-deb.so\"\n", + ) + .unwrap(); + write( + dir.join("rpm.decoder"), + "format = \"rpm\"\nextensions = [\"rpm\"]\nlibrary = \"libupac-rpm.so\"\n", + ) + .unwrap(); + + let manifests = load_decoder_manifests(dir.to_str().unwrap(), "decoder").unwrap(); + + assert_eq!(manifests.len(), 2); + assert_eq!(manifests["deb"].library, "libupac-deb.so"); + assert_eq!(manifests["rpm"].extensions, vec!["rpm".to_string()]); +} + +#[test] +fn load_decoder_manifests_ignores_non_matching_extension() { + let dir = scratch_dir("ignore-extension"); + write( + dir.join("deb.decoder"), + "format = \"deb\"\nextensions = [\"deb\"]\nlibrary = \"libupac-deb.so\"\n", + ) + .unwrap(); + write(dir.join("README.md"), b"not a manifest").unwrap(); + + let manifests = load_decoder_manifests(dir.to_str().unwrap(), "decoder").unwrap(); + + assert_eq!(manifests.len(), 1); +} + +#[test] +fn load_decoder_manifests_fails_on_duplicate_format() { + let dir = scratch_dir("duplicate-format"); + write( + dir.join("a.decoder"), + "format = \"deb\"\nextensions = [\"deb\"]\nlibrary = \"libupac-deb-a.so\"\n", + ) + .unwrap(); + write( + dir.join("b.decoder"), + "format = \"deb\"\nextensions = [\"deb\"]\nlibrary = \"libupac-deb-b.so\"\n", + ) + .unwrap(); + + let result = load_decoder_manifests(dir.to_str().unwrap(), "decoder"); + + assert_eq!(result.unwrap_err(), DecoderError::DuplicateFormat("deb".to_string())); +} + +#[test] +fn load_decoder_manifests_fails_on_malformed_toml() { + let dir = scratch_dir("malformed-toml"); + write(dir.join("broken.decoder"), "not valid toml [[[").unwrap(); + + let result = load_decoder_manifests(dir.to_str().unwrap(), "decoder"); + + assert_eq!(result.unwrap_err(), DecoderError::Manifest); +} diff --git a/lib/lib/tests/scripts_hook.rs b/lib/lib/tests/scripts_hook.rs new file mode 100644 index 00000000..27eebf4a --- /dev/null +++ b/lib/lib/tests/scripts_hook.rs @@ -0,0 +1,306 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 JustPav +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +use std::fs::{create_dir_all, remove_dir_all, write}; +use std::path::{Path, PathBuf}; + +use upac::scripts::error::HookError; +use upac::scripts::file::HookFile; +use upac::scripts::load::load_hooks; +use upac::scripts::native::{NativeTrigger, Operation, Timing}; +use upac::scripts::primitive::Step; +use upac_pki::generate::{Identity, SigningIdentity, generate_root, generate_signing_cert}; +use upac_pki::signature::HookSignature; + +fn scratch_dir(name: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!("upac-test-scripts-hook-{}-{name}", std::process::id())); + let _ = remove_dir_all(&dir); + create_dir_all(&dir).unwrap(); + + dir +} + +fn root_cert_file(dir: &Path, common_name: &str) -> (PathBuf, SigningIdentity) { + let root = generate_root(common_name).unwrap(); + let signing = generate_signing_cert(&format!("{common_name} signer"), &root).unwrap(); + + let cert_path = dir.join("root.der"); + write(&cert_path, root.to_bytes().unwrap().certificate_der).unwrap(); + + (cert_path, signing) +} + +fn write_signed_hook(dir: &Path, name: &str, hook_toml: &str, signing: &SigningIdentity) { + write(dir.join(format!("{name}.hook")), hook_toml).unwrap(); + + let signature = HookSignature::sign(hook_toml.as_bytes(), signing).unwrap(); + write(dir.join(format!("{name}.hook.sig")), signature.to_bytes().unwrap()).unwrap(); +} + +#[test] +fn hook_file_parse_succeeds_for_native_trigger() { + let hook_file = HookFile::parse("operation = \"install\"\ntiming = \"pre\"\n").unwrap(); + + assert_eq!(hook_file.native_trigger(), Some(NativeTrigger::pre(Operation::Install))); +} + +#[test] +fn hook_file_parse_succeeds_for_trigger_map() { + let hook_file = HookFile::parse("[triggers]\ndeb = [\"postinst\"]\n").unwrap(); + + assert_eq!(hook_file.native_trigger(), None); + assert_eq!(hook_file.triggers.get("deb").unwrap(), &vec!["postinst".to_string()]); +} + +#[test] +fn hook_file_parse_fails_when_only_operation_is_set() { + let result = HookFile::parse("operation = \"install\"\n"); + + assert_eq!(result.unwrap_err(), HookError::InvalidTrigger); +} + +#[test] +fn hook_file_parse_fails_when_only_timing_is_set() { + let result = HookFile::parse("timing = \"post\"\n"); + + assert_eq!(result.unwrap_err(), HookError::InvalidTrigger); +} + +#[test] +fn hook_file_parse_fails_when_no_trigger_at_all() { + let result = HookFile::parse("priority = 1\n"); + + assert_eq!(result.unwrap_err(), HookError::NoTrigger); +} + +#[test] +fn hook_file_parse_fails_on_malformed_toml() { + let result = HookFile::parse("not valid toml [[["); + + assert_eq!(result.unwrap_err(), HookError::Parse); +} + +#[test] +fn native_trigger_pre_and_post_set_correct_timing() { + assert_eq!( + NativeTrigger::pre(Operation::Update), + NativeTrigger { + operation: Operation::Update, + timing: Timing::Pre, + } + ); + assert_eq!( + NativeTrigger::post(Operation::Update), + NativeTrigger { + operation: Operation::Update, + timing: Timing::Post, + } + ); +} + +#[test] +fn touch_file_step_creates_missing_file_and_rollback_removes_it() { + let dir = scratch_dir("touch-missing"); + let path = dir.join("marker"); + + let mut hook_file = HookFile::parse(&format!( + "operation = \"install\"\ntiming = \"pre\"\n\n[[steps]]\ntype = \"touch_file\"\npath = {:?}\n", + path + )) + .unwrap(); + let mut step = hook_file.steps.remove(0); + + step.execute().unwrap(); + assert!(path.exists()); + + step.rollback().unwrap(); + assert!(!path.exists()); +} + +#[test] +fn touch_file_step_leaves_preexisting_file_after_rollback() { + let dir = scratch_dir("touch-existing"); + let path = dir.join("marker"); + write(&path, b"already here").unwrap(); + + let mut hook_file = HookFile::parse(&format!( + "operation = \"install\"\ntiming = \"pre\"\n\n[[steps]]\ntype = \"touch_file\"\npath = {:?}\n", + path + )) + .unwrap(); + let mut step = hook_file.steps.remove(0); + + step.execute().unwrap(); + step.rollback().unwrap(); + + assert!(path.exists()); +} + +#[test] +fn move_file_step_execute_and_rollback_round_trip() { + let dir = scratch_dir("move-round-trip"); + let from = dir.join("a"); + let to = dir.join("b"); + write(&from, b"content").unwrap(); + + let mut hook_file = HookFile::parse(&format!( + "operation = \"install\"\ntiming = \"pre\"\n\n[[steps]]\ntype = \"move_file\"\nfrom = {:?}\nto = {:?}\n", + from, to + )) + .unwrap(); + let mut step = hook_file.steps.remove(0); + + step.execute().unwrap(); + assert!(!from.exists()); + assert!(to.exists()); + + step.rollback().unwrap(); + assert!(from.exists()); + assert!(!to.exists()); +} + +#[test] +fn create_symlink_step_execute_and_rollback() { + let dir = scratch_dir("symlink"); + let target = dir.join("target"); + let link = dir.join("link"); + write(&target, b"content").unwrap(); + + let mut hook_file = HookFile::parse(&format!( + "operation = \"install\"\ntiming = \"pre\"\n\n[[steps]]\ntype = \"create_symlink\"\ntarget = {:?}\nlink = {:?}\n", + target, link + )) + .unwrap(); + let mut step = hook_file.steps.remove(0); + + step.execute().unwrap(); + assert_eq!(std::fs::read_link(&link).unwrap(), target); + + step.rollback().unwrap(); + assert!(!link.exists()); +} + +#[test] +fn primitive_vec_rollback_guard_unwinds_in_reverse_order() { + use upac::orchestrator::stage::RollbackGuard; + + let dir = scratch_dir("rollback-order"); + let a = dir.join("a"); + let b = dir.join("b"); + let c = dir.join("c"); + write(&a, b"content").unwrap(); + + let mut hook_file = HookFile::parse(&format!( + concat!( + "operation = \"install\"\ntiming = \"pre\"\n\n", + "[[steps]]\ntype = \"move_file\"\nfrom = {:?}\nto = {:?}\n\n", + "[[steps]]\ntype = \"move_file\"\nfrom = {:?}\nto = {:?}\n", + ), + a, b, b, c + )) + .unwrap(); + + let mut executed = Vec::new(); + for mut step in hook_file.steps.drain(..) { + step.execute().unwrap(); + executed.push(step); + } + assert!(c.exists()); + + executed.rollback().unwrap(); + + assert!(a.exists()); + assert!(!b.exists()); + assert!(!c.exists()); +} + +#[test] +fn load_hooks_returns_matching_hook_for_signed_valid_file() { + let hooks_dir = scratch_dir("load-valid"); + let (cert_path, signing) = root_cert_file(&hooks_dir, "load-valid root"); + write_signed_hook( + &hooks_dir, + "install", + "operation = \"install\"\ntiming = \"pre\"\n", + &signing, + ); + + let hooks = load_hooks(hooks_dir.to_str().unwrap(), cert_path.to_str().unwrap(), "hook", "sig").unwrap(); + + assert_eq!(hooks.len(), 1); + assert_eq!(hooks[0].native_trigger(), Some(NativeTrigger::pre(Operation::Install))); +} + +#[test] +fn load_hooks_skips_files_with_non_matching_extension() { + let hooks_dir = scratch_dir("load-skip-extension"); + let (cert_path, signing) = root_cert_file(&hooks_dir, "load-skip root"); + write_signed_hook( + &hooks_dir, + "install", + "operation = \"install\"\ntiming = \"pre\"\n", + &signing, + ); + write(hooks_dir.join("notes.txt"), b"not a hook").unwrap(); + + let hooks = load_hooks(hooks_dir.to_str().unwrap(), cert_path.to_str().unwrap(), "hook", "sig").unwrap(); + + assert_eq!(hooks.len(), 1); +} + +#[test] +fn load_hooks_fails_when_signature_is_tampered() { + let hooks_dir = scratch_dir("load-tampered"); + let (cert_path, signing) = root_cert_file(&hooks_dir, "load-tampered root"); + write_signed_hook( + &hooks_dir, + "install", + "operation = \"install\"\ntiming = \"pre\"\n", + &signing, + ); + + write( + hooks_dir.join("install.hook"), + "operation = \"install\"\ntiming = \"post\"\n", + ) + .unwrap(); + + let result = load_hooks(hooks_dir.to_str().unwrap(), cert_path.to_str().unwrap(), "hook", "sig"); + + assert_eq!(result.unwrap_err(), HookError::InvalidSignature); +} + +#[test] +fn load_hooks_fails_when_root_cert_is_unrelated() { + let hooks_dir = scratch_dir("load-unrelated-root"); + let (_, signing) = root_cert_file(&hooks_dir, "load-unrelated signing root"); + let (unrelated_cert_path, _) = root_cert_file(&hooks_dir, "load-unrelated other root"); + write_signed_hook( + &hooks_dir, + "install", + "operation = \"install\"\ntiming = \"pre\"\n", + &signing, + ); + + let result = load_hooks( + hooks_dir.to_str().unwrap(), + unrelated_cert_path.to_str().unwrap(), + "hook", + "sig", + ); + + assert_eq!(result.unwrap_err(), HookError::InvalidSignature); +} + +#[test] +fn load_hooks_fails_when_hooks_dir_is_missing() { + let hooks_dir = scratch_dir("load-missing-dir").join("does-not-exist"); + let cert_dir = scratch_dir("load-missing-dir-cert"); + let (cert_path, _) = root_cert_file(&cert_dir, "load-missing-dir root"); + + let result = load_hooks(hooks_dir.to_str().unwrap(), cert_path.to_str().unwrap(), "hook", "sig"); + + assert!(matches!(result.unwrap_err(), HookError::Io(_))); +} From 91e4b3331dfc1c49783585a2be0ce6bc2ec5bc9f Mon Sep 17 00:00:00 2001 From: JustPav Date: Sat, 8 Aug 2026 23:11:03 +0400 Subject: [PATCH 02/68] fix: Removed obsolete 'branch' parameter; fix: Fixed visibility of the 'types' module. Co-Authored-By: Claude Sonnet 5 --- lib/abi/src/request.rs | 2 -- lib/lib/src/lib.rs | 9 +++++---- lib/lib/src/mutated/commit/mod.rs | 7 +------ lib/lib/src/mutated/files/mod.rs | 7 +------ lib/lib/src/mutated/installer/mod.rs | 7 +------ lib/lib/src/mutated/rollback/mod.rs | 7 +------ lib/lib/src/mutated/uninstaller/mod.rs | 7 +------ lib/lib/src/mutated/update/mod.rs | 7 +------ lib/lib/src/orchestrator/mod.rs | 1 + lib/lib/src/types/mod.rs | 4 ---- lib/lib/src/unmutated/diff/mod.rs | 7 +------ lib/lib/src/unmutated/diff_files/mod.rs | 7 +------ lib/lib/src/unmutated/diff_packages/mod.rs | 7 +------ lib/lib/src/unmutated/list_commit/mod.rs | 7 +------ lib/lib/src/unmutated/list_history/mod.rs | 7 +------ lib/lib/src/unmutated/list_packages/mod.rs | 7 +------ lib/lib/src/unmutated/list_prefix/mod.rs | 7 +------ lib/lib/src/unmutated/search_files/mod.rs | 7 +------ lib/lib/src/unmutated/search_meta/mod.rs | 7 +------ 19 files changed, 21 insertions(+), 100 deletions(-) diff --git a/lib/abi/src/request.rs b/lib/abi/src/request.rs index a86f4375..ee691cf0 100644 --- a/lib/abi/src/request.rs +++ b/lib/abi/src/request.rs @@ -18,8 +18,6 @@ use crate::types::{CSlice, CVec, check_size}; pub struct CRequestBase { pub struct_size: usize, - pub branch: CSlice, - pub on_hook: Option, pub hook_ctx: *mut c_void, diff --git a/lib/lib/src/lib.rs b/lib/lib/src/lib.rs index 952d6fdc..71e6332c 100644 --- a/lib/lib/src/lib.rs +++ b/lib/lib/src/lib.rs @@ -3,15 +3,16 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later +mod export; +mod mutated; +mod unmutated; + pub mod composefs; pub mod database; pub mod deploy; pub mod errors; -mod export; pub mod lock; -mod mutated; pub mod orchestrator; pub mod plugin; pub mod scripts; -mod types; -mod unmutated; +pub mod types; diff --git a/lib/lib/src/mutated/commit/mod.rs b/lib/lib/src/mutated/commit/mod.rs index d0d2d571..5ce6271b 100644 --- a/lib/lib/src/mutated/commit/mod.rs +++ b/lib/lib/src/mutated/commit/mod.rs @@ -16,15 +16,13 @@ use self::transaction::TransactionStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; use crate::scripts::HookStage; use crate::scripts::native::{NativeTrigger, Operation}; +use crate::types::TmpPath; use crate::types::states::CommitStateId; -use crate::types::{Branch, TmpPath}; mod error; mod transaction; pub struct CommitData<'a> { - pub branch: &'a str, - pub tmp_path: &'a str, pub subject: &'a str, @@ -45,8 +43,6 @@ impl<'a> TryFrom<&'a CCommitRequest> for CommitData<'a> { let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; Ok(CommitData { - branch: (&request.base.branch).try_into()?, - tmp_path: (&request.tmp_path).try_into()?, subject: (&request.subject).try_into()?, @@ -75,7 +71,6 @@ fn assemble() -> SequentialOrchestrator { pub fn run(data: CommitData) -> Result<(), (CommitStateId, CommitError)> { let mut context = Context::new(); context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Branch(data.branch.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/mutated/files/mod.rs b/lib/lib/src/mutated/files/mod.rs index a0995668..5df534b1 100644 --- a/lib/lib/src/mutated/files/mod.rs +++ b/lib/lib/src/mutated/files/mod.rs @@ -20,8 +20,8 @@ use self::transaction::TransactionStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; use crate::scripts::HookStage; use crate::scripts::native::{NativeTrigger, Operation}; +use crate::types::TmpPath; use crate::types::states::FilesStateId; -use crate::types::{Branch, TmpPath}; mod checkout; mod error; @@ -33,8 +33,6 @@ pub struct FilesData<'a> { pub file_kind: DiffKind, pub file_package: &'a CPackageInfo, - pub branch: &'a str, - pub tmp_path: &'a str, pub subject: &'a str, @@ -60,8 +58,6 @@ impl<'a> TryFrom<&'a CFilesRequest> for FilesData<'a> { file_kind: request.file_kind, file_package, - branch: (&request.base.branch).try_into()?, - tmp_path: (&request.tmp_path).try_into()?, subject: (&request.subject).try_into()?, @@ -92,7 +88,6 @@ fn assemble() -> SequentialOrchestrator { pub fn run(data: FilesData) -> Result<(), (FilesStateId, FilesError)> { let mut context = Context::new(); context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Branch(data.branch.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/mutated/installer/mod.rs b/lib/lib/src/mutated/installer/mod.rs index 5670b762..c07f4c5f 100644 --- a/lib/lib/src/mutated/installer/mod.rs +++ b/lib/lib/src/mutated/installer/mod.rs @@ -21,7 +21,7 @@ use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mut use crate::scripts::HookStage; use crate::scripts::native::{NativeTrigger, Operation}; use crate::types::states::InstallStateId; -use crate::types::{Branch, PackageTemp, TmpPath}; +use crate::types::{PackageTemp, TmpPath}; mod checkout; mod error; @@ -33,8 +33,6 @@ mod transaction; pub struct InstallData<'a> { pub packages: Vec, - pub branch: &'a str, - pub tmp_path: &'a str, pub subject: &'a str, @@ -57,8 +55,6 @@ impl<'a> TryFrom<&'a CInstallRequest> for InstallData<'a> { Ok(InstallData { packages: Vec::try_from(&request.packages)?, - branch: (&request.base.branch).try_into()?, - tmp_path: (&request.tmp_path).try_into()?, subject: (&request.subject).try_into()?, @@ -92,7 +88,6 @@ pub fn run(data: InstallData) -> Result<(), (InstallStateId, InstallError)> { let mut context = Context::new(); context.put(data.packages); context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Branch(data.branch.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/mutated/rollback/mod.rs b/lib/lib/src/mutated/rollback/mod.rs index 9aaa9a57..34244d63 100644 --- a/lib/lib/src/mutated/rollback/mod.rs +++ b/lib/lib/src/mutated/rollback/mod.rs @@ -18,8 +18,8 @@ use self::swap::SwapStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; use crate::scripts::HookStage; use crate::scripts::native::{NativeTrigger, Operation}; +use crate::types::TmpPath; use crate::types::states::RollbackStateId; -use crate::types::{Branch, TmpPath}; mod checkout; mod error; @@ -29,8 +29,6 @@ mod swap; pub struct RollbackData<'a> { pub commit_hash: &'a str, - pub branch: &'a str, - pub tmp_path: &'a str, pub hook_message: Option, @@ -50,8 +48,6 @@ impl<'a> TryFrom<&'a CRollbackRequest> for RollbackData<'a> { Ok(RollbackData { commit_hash: (&request.commit_hash).try_into()?, - branch: (&request.base.branch).try_into()?, - tmp_path: (&request.tmp_path).try_into()?, hook_message: request.base.on_hook, @@ -79,7 +75,6 @@ fn assemble() -> SequentialOrchestrator { pub fn run(data: RollbackData) -> Result<(), (RollbackStateId, RollbackError)> { let mut context = Context::new(); context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Branch(data.branch.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/mutated/uninstaller/mod.rs b/lib/lib/src/mutated/uninstaller/mod.rs index dc131389..9cd81445 100644 --- a/lib/lib/src/mutated/uninstaller/mod.rs +++ b/lib/lib/src/mutated/uninstaller/mod.rs @@ -15,7 +15,7 @@ use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mut use crate::scripts::HookStage; use crate::scripts::native::{NativeTrigger, Operation}; use crate::types::states::UninstallStateId; -use crate::types::{Branch, PackageEntry, Targets, TmpPath}; +use crate::types::{PackageEntry, Targets, TmpPath}; pub use self::error::UninstallError; @@ -57,8 +57,6 @@ impl<'a> TryFrom<&'a CPackageInfo> for UninstallPackage<'a> { pub struct UninstallData<'a> { pub packages: Vec>, - pub branch: &'a str, - pub tmp_path: &'a str, pub subject: &'a str, @@ -81,8 +79,6 @@ impl<'a> TryFrom<&'a CUninstallRequest> for UninstallData<'a> { Ok(UninstallData { packages: Vec::try_from(&request.packages)?, - branch: (&request.base.branch).try_into()?, - tmp_path: (&request.tmp_path).try_into()?, subject: (&request.subject).try_into()?, @@ -132,7 +128,6 @@ pub fn run(data: UninstallData) -> Result<(), (UninstallStateId, UninstallError) context.put(targets); context.put(deploy); context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Branch(data.branch.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/mutated/update/mod.rs b/lib/lib/src/mutated/update/mod.rs index b2627dcd..7ad956e2 100644 --- a/lib/lib/src/mutated/update/mod.rs +++ b/lib/lib/src/mutated/update/mod.rs @@ -21,7 +21,7 @@ use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mut use crate::scripts::HookStage; use crate::scripts::native::{NativeTrigger, Operation}; use crate::types::states::UpdateStateId; -use crate::types::{Branch, PackageTemp, TmpPath}; +use crate::types::{PackageTemp, TmpPath}; mod checkout; mod error; @@ -33,8 +33,6 @@ mod transaction; pub struct UpdateData<'a> { pub packages: Vec, - pub branch: &'a str, - pub tmp_path: &'a str, pub subject: &'a str, @@ -57,8 +55,6 @@ impl<'a> TryFrom<&'a CUpdateRequest> for UpdateData<'a> { Ok(UpdateData { packages: Vec::try_from(&request.packages)?, - branch: (&request.base.branch).try_into()?, - tmp_path: (&request.tmp_path).try_into()?, subject: (&request.subject).try_into()?, @@ -92,7 +88,6 @@ pub fn run(data: UpdateData) -> Result<(), (UpdateStateId, UpdateError)> { let mut context = Context::new(); context.put(data.packages); context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Branch(data.branch.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/orchestrator/mod.rs b/lib/lib/src/orchestrator/mod.rs index 7db5c78b..6f482f91 100644 --- a/lib/lib/src/orchestrator/mod.rs +++ b/lib/lib/src/orchestrator/mod.rs @@ -19,6 +19,7 @@ use crate::orchestrator::error::OrchestratorError; use crate::orchestrator::stage::{ConcurrentStage, RollbackGuard, Stage, StageResult}; mod cursor; + pub mod error; pub mod stage; diff --git a/lib/lib/src/types/mod.rs b/lib/lib/src/types/mod.rs index 83c7a78f..9c070b70 100644 --- a/lib/lib/src/types/mod.rs +++ b/lib/lib/src/types/mod.rs @@ -180,10 +180,6 @@ pub struct TmpPath(pub String); as_str_method!(TmpPath); -pub struct Branch(pub String); - -as_str_method!(Branch); - #[cfg(test)] mod tests { use super::*; diff --git a/lib/lib/src/unmutated/diff/mod.rs b/lib/lib/src/unmutated/diff/mod.rs index cccd9fd4..1a398569 100644 --- a/lib/lib/src/unmutated/diff/mod.rs +++ b/lib/lib/src/unmutated/diff/mod.rs @@ -16,7 +16,7 @@ use self::preparing::PreparingStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; use crate::types::states::DiffStateId; -use crate::types::{Branch, DiffFileEntry, DiffPackageEntry}; +use crate::types::{DiffFileEntry, DiffPackageEntry}; mod comparing; mod error; @@ -26,8 +26,6 @@ pub struct DiffData<'a> { pub from_commit_hash: Option<&'a str>, pub to_commit_hash: Option<&'a str>, - pub branch: &'a str, - pub hook_message: Option, pub hook_message_context: *mut c_void, @@ -46,8 +44,6 @@ impl<'a> TryFrom<&'a CDiffRequest> for DiffData<'a> { from_commit_hash: (&request.from_commit_hash).try_into()?, to_commit_hash: (&request.to_commit_hash).try_into()?, - branch: (&request.base.branch).try_into()?, - hook_message: request.base.on_hook, hook_message_context: request.base.hook_ctx, @@ -62,7 +58,6 @@ fn assemble() -> SequentialOrchestrator { pub fn run(data: DiffData) -> Result<(Vec, Vec), (DiffStateId, DiffError)> { let mut context = Context::new(); - context.put(Branch(data.branch.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/unmutated/diff_files/mod.rs b/lib/lib/src/unmutated/diff_files/mod.rs index 58a11210..4ab53fe9 100644 --- a/lib/lib/src/unmutated/diff_files/mod.rs +++ b/lib/lib/src/unmutated/diff_files/mod.rs @@ -15,8 +15,8 @@ use self::comparing::ComparingStage; use self::preparing::PreparingStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; +use crate::types::DiffFileEntry; use crate::types::states::DiffFilesStateId; -use crate::types::{Branch, DiffFileEntry}; mod comparing; mod error; @@ -26,8 +26,6 @@ pub struct DiffFilesData<'a> { pub from_commit_hash: Option<&'a str>, pub to_commit_hash: Option<&'a str>, - pub branch: &'a str, - pub hook_message: Option, pub hook_message_context: *mut c_void, @@ -46,8 +44,6 @@ impl<'a> TryFrom<&'a CDiffFilesRequest> for DiffFilesData<'a> { from_commit_hash: (&request.from_commit_hash).try_into()?, to_commit_hash: (&request.to_commit_hash).try_into()?, - branch: (&request.base.branch).try_into()?, - hook_message: request.base.on_hook, hook_message_context: request.base.hook_ctx, @@ -62,7 +58,6 @@ fn assemble() -> SequentialOrchestrator { pub fn run(data: DiffFilesData) -> Result<(Vec,), (DiffFilesStateId, DiffFilesError)> { let mut context = Context::new(); - context.put(Branch(data.branch.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/unmutated/diff_packages/mod.rs b/lib/lib/src/unmutated/diff_packages/mod.rs index 2b5ea3eb..d9820b05 100644 --- a/lib/lib/src/unmutated/diff_packages/mod.rs +++ b/lib/lib/src/unmutated/diff_packages/mod.rs @@ -15,8 +15,8 @@ use self::comparing::ComparingStage; use self::preparing::PreparingStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; +use crate::types::DiffPackageEntry; use crate::types::states::DiffPackagesStateId; -use crate::types::{Branch, DiffPackageEntry}; mod comparing; mod error; @@ -26,8 +26,6 @@ pub struct DiffPackagesData<'a> { pub from_commit_hash: Option<&'a str>, pub to_commit_hash: Option<&'a str>, - pub branch: &'a str, - pub hook_message: Option, pub hook_message_context: *mut c_void, @@ -46,8 +44,6 @@ impl<'a> TryFrom<&'a CDiffPackagesRequest> for DiffPackagesData<'a> { from_commit_hash: (&request.from_commit_hash).try_into()?, to_commit_hash: (&request.to_commit_hash).try_into()?, - branch: (&request.base.branch).try_into()?, - hook_message: request.base.on_hook, hook_message_context: request.base.hook_ctx, @@ -62,7 +58,6 @@ fn assemble() -> SequentialOrchestrator { pub fn run(data: DiffPackagesData) -> Result<(Vec,), (DiffPackagesStateId, DiffPackagesError)> { let mut context = Context::new(); - context.put(Branch(data.branch.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/unmutated/list_commit/mod.rs b/lib/lib/src/unmutated/list_commit/mod.rs index eea340c6..c0ac128d 100644 --- a/lib/lib/src/unmutated/list_commit/mod.rs +++ b/lib/lib/src/unmutated/list_commit/mod.rs @@ -14,8 +14,8 @@ pub use self::error::ListCommitError; use self::fetching::FetchingStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; +use crate::types::CommitEntry; use crate::types::states::ListCommitStateId; -use crate::types::{Branch, CommitEntry}; mod error; mod fetching; @@ -23,8 +23,6 @@ mod fetching; pub struct ListCommitData<'a> { pub prefix_digest: Option<&'a str>, - pub branch: &'a str, - pub hook_message: Option, pub hook_message_context: *mut c_void, @@ -42,8 +40,6 @@ impl<'a> TryFrom<&'a CListCommitRequest> for ListCommitData<'a> { Ok(ListCommitData { prefix_digest: (&request.prefix_digest).try_into()?, - branch: (&request.base.branch).try_into()?, - hook_message: request.base.on_hook, hook_message_context: request.base.hook_ctx, @@ -58,7 +54,6 @@ fn assemble() -> SequentialOrchestrator { pub fn run(data: ListCommitData) -> Result<(Vec,), (ListCommitStateId, ListCommitError)> { let mut context = Context::new(); - context.put(Branch(data.branch.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/unmutated/list_history/mod.rs b/lib/lib/src/unmutated/list_history/mod.rs index c26f222a..a390d542 100644 --- a/lib/lib/src/unmutated/list_history/mod.rs +++ b/lib/lib/src/unmutated/list_history/mod.rs @@ -14,15 +14,13 @@ pub use self::error::ListHistoryError; use self::fetching::FetchingStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; +use crate::types::HistoryEntry; use crate::types::states::ListHistoryStateId; -use crate::types::{Branch, HistoryEntry}; mod error; mod fetching; pub struct ListHistoryData<'a> { - pub branch: &'a str, - pub hook_message: Option, pub hook_message_context: *mut c_void, @@ -38,8 +36,6 @@ impl<'a> TryFrom<&'a CListHistoryRequest> for ListHistoryData<'a> { let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; Ok(ListHistoryData { - branch: (&request.base.branch).try_into()?, - hook_message: request.base.on_hook, hook_message_context: request.base.hook_ctx, @@ -54,7 +50,6 @@ fn assemble() -> SequentialOrchestrator { pub fn run(data: ListHistoryData) -> Result<(Vec,), (ListHistoryStateId, ListHistoryError)> { let mut context = Context::new(); - context.put(Branch(data.branch.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/unmutated/list_packages/mod.rs b/lib/lib/src/unmutated/list_packages/mod.rs index 7f4bb44e..9d043bed 100644 --- a/lib/lib/src/unmutated/list_packages/mod.rs +++ b/lib/lib/src/unmutated/list_packages/mod.rs @@ -14,15 +14,13 @@ pub use self::error::ListPackagesError; use self::fetching::FetchingStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; +use crate::types::PackageMeta; use crate::types::states::ListPackagesStateId; -use crate::types::{Branch, PackageMeta}; mod error; mod fetching; pub struct ListPackagesData<'a> { - pub branch: &'a str, - pub hook_message: Option, pub hook_message_context: *mut c_void, @@ -38,8 +36,6 @@ impl<'a> TryFrom<&'a CListPackagesRequest> for ListPackagesData<'a> { let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; Ok(ListPackagesData { - branch: (&request.base.branch).try_into()?, - hook_message: request.base.on_hook, hook_message_context: request.base.hook_ctx, @@ -54,7 +50,6 @@ fn assemble() -> SequentialOrchestrator { pub fn run(data: ListPackagesData) -> Result<(Vec,), (ListPackagesStateId, ListPackagesError)> { let mut context = Context::new(); - context.put(Branch(data.branch.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/unmutated/list_prefix/mod.rs b/lib/lib/src/unmutated/list_prefix/mod.rs index 40352147..72d3d095 100644 --- a/lib/lib/src/unmutated/list_prefix/mod.rs +++ b/lib/lib/src/unmutated/list_prefix/mod.rs @@ -14,15 +14,13 @@ pub use self::error::ListPrefixError; use self::fetching::FetchingStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; +use crate::types::PrefixEntry; use crate::types::states::ListPrefixStateId; -use crate::types::{Branch, PrefixEntry}; mod error; mod fetching; pub struct ListPrefixData<'a> { - pub branch: &'a str, - pub hook_message: Option, pub hook_message_context: *mut c_void, @@ -38,8 +36,6 @@ impl<'a> TryFrom<&'a CListPrefixRequest> for ListPrefixData<'a> { let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; Ok(ListPrefixData { - branch: (&request.base.branch).try_into()?, - hook_message: request.base.on_hook, hook_message_context: request.base.hook_ctx, @@ -54,7 +50,6 @@ fn assemble() -> SequentialOrchestrator { pub fn run(data: ListPrefixData) -> Result<(Vec,), (ListPrefixStateId, ListPrefixError)> { let mut context = Context::new(); - context.put(Branch(data.branch.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/unmutated/search_files/mod.rs b/lib/lib/src/unmutated/search_files/mod.rs index 9d055fa2..9e6d1c60 100644 --- a/lib/lib/src/unmutated/search_files/mod.rs +++ b/lib/lib/src/unmutated/search_files/mod.rs @@ -14,8 +14,8 @@ pub use self::error::SearchFilesError; use self::searching::SearchingStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; +use crate::types::SearchFileEntry; use crate::types::states::SearchFilesStateId; -use crate::types::{Branch, SearchFileEntry}; mod error; mod searching; @@ -23,8 +23,6 @@ mod searching; pub struct SearchFilesData<'a> { pub search: &'a str, - pub branch: &'a str, - pub hook_message: Option, pub hook_message_context: *mut c_void, @@ -42,8 +40,6 @@ impl<'a> TryFrom<&'a CSearchFilesRequest> for SearchFilesData<'a> { Ok(SearchFilesData { search: (&request.search).try_into()?, - branch: (&request.base.branch).try_into()?, - hook_message: request.base.on_hook, hook_message_context: request.base.hook_ctx, @@ -58,7 +54,6 @@ fn assemble() -> SequentialOrchestrator { pub fn run(data: SearchFilesData) -> Result<(Vec,), (SearchFilesStateId, SearchFilesError)> { let mut context = Context::new(); - context.put(Branch(data.branch.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/unmutated/search_meta/mod.rs b/lib/lib/src/unmutated/search_meta/mod.rs index f8513437..6a086885 100644 --- a/lib/lib/src/unmutated/search_meta/mod.rs +++ b/lib/lib/src/unmutated/search_meta/mod.rs @@ -14,8 +14,8 @@ pub use self::error::SearchMetaError; use self::searching::SearchingStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; +use crate::types::PackageMeta; use crate::types::states::SearchMetaStateId; -use crate::types::{Branch, PackageMeta}; mod error; mod searching; @@ -23,8 +23,6 @@ mod searching; pub struct SearchMetaData<'a> { pub search: &'a str, - pub branch: &'a str, - pub hook_message: Option, pub hook_message_context: *mut c_void, @@ -42,8 +40,6 @@ impl<'a> TryFrom<&'a CSearchMetaRequest> for SearchMetaData<'a> { Ok(SearchMetaData { search: (&request.search).try_into()?, - branch: (&request.base.branch).try_into()?, - hook_message: request.base.on_hook, hook_message_context: request.base.hook_ctx, @@ -58,7 +54,6 @@ fn assemble() -> SequentialOrchestrator { pub fn run(data: SearchMetaData) -> Result<(Vec,), (SearchMetaStateId, SearchMetaError)> { let mut context = Context::new(); - context.put(Branch(data.branch.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); From 8dde4a8586ecc093f82911a7106741a97568494d Mon Sep 17 00:00:00 2001 From: JustPav Date: Sat, 8 Aug 2026 23:11:55 +0400 Subject: [PATCH 03/68] fix: Added tests for types fix: Removed inline tests in types Co-Authored-By: Claude Sonnet 5 --- lib/lib/src/types/mod.rs | 43 ----------------------------- lib/lib/tests/types.rs | 59 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 43 deletions(-) create mode 100644 lib/lib/tests/types.rs diff --git a/lib/lib/src/types/mod.rs b/lib/lib/src/types/mod.rs index 9c070b70..4ca761a0 100644 --- a/lib/lib/src/types/mod.rs +++ b/lib/lib/src/types/mod.rs @@ -193,17 +193,6 @@ mod tests { } } - #[test] - fn version_c_round_trip_preserves_value() { - let original = sample_version(); - - let c_version = CVersion::from(original.clone()); - let restored = Version::try_from(&c_version).unwrap(); - - assert_eq!(restored, original); - unsafe { c_version.free() }; - } - #[test] fn version_redb_round_trip_preserves_value() { let original = sample_version(); @@ -218,38 +207,6 @@ mod tests { assert_eq!(offset, buf.len()); } - #[test] - fn package_meta_c_round_trip_preserves_value() { - let original = PackageMeta { - name: "upac".to_owned(), - version: sample_version(), - arch: "x86_64".to_owned(), - arch_sub: None, - maintainer: "JustPav".to_owned(), - description: "package manager".to_owned(), - license: Some("GPL-3.0-only".to_owned()), - url: None, - sha256: [7; 32], - installed_size: 4096, - }; - - let c_meta = CPackageMeta::from(original.clone()); - let restored = PackageMeta::try_from(&c_meta).unwrap(); - - assert_eq!(restored.name, original.name); - assert_eq!(restored.version, original.version); - assert_eq!(restored.arch, original.arch); - assert_eq!(restored.arch_sub, original.arch_sub); - assert_eq!(restored.maintainer, original.maintainer); - assert_eq!(restored.description, original.description); - assert_eq!(restored.license, original.license); - assert_eq!(restored.url, original.url); - assert_eq!(restored.sha256, original.sha256); - assert_eq!(restored.installed_size, original.installed_size); - - unsafe { c_meta.free() }; - } - #[test] fn file_entry_redb_round_trip_preserves_value() { let original = FileEntry { diff --git a/lib/lib/tests/types.rs b/lib/lib/tests/types.rs new file mode 100644 index 00000000..007bc328 --- /dev/null +++ b/lib/lib/tests/types.rs @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 JustPav +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +use upac::types::{PackageMeta, Version}; +use upac_abi::package::{CPackageMeta, CVersion}; + +fn sample_version() -> Version { + Version { + epoch: 1, + parts: vec![2, 5, 0], + pre: Some("rc1".to_owned()), + release: 3, + } +} + +#[test] +fn version_c_round_trip_preserves_value() { + let original = sample_version(); + + let c_version = CVersion::from(original.clone()); + let restored = Version::try_from(&c_version).unwrap(); + + assert_eq!(restored, original); + unsafe { c_version.free() }; +} + +#[test] +fn package_meta_c_round_trip_preserves_value() { + let original = PackageMeta { + name: "upac".to_owned(), + version: sample_version(), + arch: "x86_64".to_owned(), + arch_sub: None, + maintainer: "JustPav".to_owned(), + description: "package manager".to_owned(), + license: Some("GPL-3.0-only".to_owned()), + url: None, + sha256: [7; 32], + installed_size: 4096, + }; + + let c_meta = CPackageMeta::from(original.clone()); + let restored = PackageMeta::try_from(&c_meta).unwrap(); + + assert_eq!(restored.name, original.name); + assert_eq!(restored.version, original.version); + assert_eq!(restored.arch, original.arch); + assert_eq!(restored.arch_sub, original.arch_sub); + assert_eq!(restored.maintainer, original.maintainer); + assert_eq!(restored.description, original.description); + assert_eq!(restored.license, original.license); + assert_eq!(restored.url, original.url); + assert_eq!(restored.sha256, original.sha256); + assert_eq!(restored.installed_size, original.installed_size); + + unsafe { c_meta.free() }; +} From 9144fa8f849979edbc2c431ad269e45579082194 Mon Sep 17 00:00:00 2001 From: JustPav Date: Sat, 8 Aug 2026 23:28:26 +0400 Subject: [PATCH 04/68] fix: Update documentation Co-Authored-By: Claude Sonnet 5 --- doc/UPAC project note.en.md | 6 ++++-- doc/UPAC project note.md | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/UPAC project note.en.md b/doc/UPAC project note.en.md index 8ac4c3c2..2238b15f 100644 --- a/doc/UPAC project note.en.md +++ b/doc/UPAC project note.en.md @@ -46,8 +46,8 @@ Terms the rest of the document operates with. - **Content-addressed store (CAS)** — storage where a file is addressed by the hash of its content, not by name; identical files are stored once (deduplication). - **Image** — a whole content-addressed snapshot of a file tree (`/usr` or `/etc`) at a specific version. -- **Digest** — an image's hash, its identity; names the deploy in the cmdline. -- **Deployment** — a specific deployed version of the system; in this model = a pair `(usr-digest, etc-digest)`. +- **Digest** — an image's hash, its identity. +- **Deployment** — a specific deployed version of the system; in this model = a pair `(usr-digest, etc-digest)`, but an asymmetric one: `usr` is primary and externally addressable — it's what the boot cmdline names (`composefs.digest=`, §5.2) and what keys `state/deploy//` (§3, §5.7); `etc` is secondary and resolved from *inside* that usr's own record (its `working_etc` field, §5.7), never carried as its own cmdline parameter. Cmdline tells you "which usr"; "which etc" follows from that record, not from cmdline directly. - **Ref** — a human-readable named pointer to an image. - **overlay (lower / upper)** — stacking a writable layer (upper) over a read-only one (lower); this is how the live `/etc` sits over the image. - **fs-verity** — a kernel feature that cryptographically attests a file's content and catches any change to it. @@ -242,6 +242,8 @@ Rollback is built on a one-shot boot choice plus a late confirmation that the sy 3. The system reached a healthy state — a late hook / init unit makes D' the persistent default and marks the **pair as working**: it updates the current `/usr`'s `working_etc` (§5.7). This is the confirmation. 4. The confirmation did not fire (the system went down earlier, for any reason) — the one-shot variable is already cleared, so the next boot goes to the persistent default, i.e. the previous deploy. This is the auto-rollback. +**D' is the usr-digest, not a combined pair.** `composefs.digest=D'` carries exactly the `usr-digest` — the same value that keys `state/deploy//` and that `open_tree()` (see composefs module, §7) takes directly, with no translation step. This is also how "the currently running deploy" gets resolved at runtime with no separate pointer file on disk (§5.7 calls this out too: "the active deploy is a separate pointer — the booted `composefs.digest` / boot default"): read `/proc/cmdline`, pull `composefs.digest`, that's the usr-digest. The `etc` side of the pair is deliberately NOT in cmdline — once the usr-digest is known, its `state/deploy//meta.json` is read for `working_etc` (§5.7), which names the currently confirmed `etc-digest`. + **Rollback tiers** (which level catches what): 1. Kernel or initramfs did not come up — the firmware itself goes to the persistent default (one-shot cleared) = the previous deploy. diff --git a/doc/UPAC project note.md b/doc/UPAC project note.md index 389096f6..36a97b88 100644 --- a/doc/UPAC project note.md +++ b/doc/UPAC project note.md @@ -46,8 +46,8 @@ - **Контент-адресное хранилище (CAS)** — хранилище, где файл адресуется по хешу своего содержимого, а не по имени; одинаковые файлы хранятся один раз (дедупликация). - **Образ (image)** — цельный контент-адресный слепок дерева файлов (`/usr` или `/etc`) в конкретной версии. -- **Дайджест (digest)** — хеш образа, его идентичность; именует деплой в cmdline. -- **Деплой (deployment)** — конкретная развёрнутая версия системы; в этой модели = пара `(usr-digest, etc-digest)`. +- **Дайджест (digest)** — хеш образа, его идентичность. +- **Деплой (deployment)** — конкретная развёрнутая версия системы; в этой модели = пара `(usr-digest, etc-digest)`, но пара асимметричная: `usr` первичен и адресуем снаружи — именно его называет cmdline при загрузке (`composefs.digest=`, §5.2) и именно им именуется `state/deploy//` (§3, §5.7); `etc` вторичен и достаётся уже ИЗНУТРИ записи этого usr (поле `working_etc`, §5.7), отдельным параметром в cmdline никогда не передаётся. Cmdline говорит "какой usr", а "какой etc" следует уже из его записи, а не напрямую из cmdline. - **Ref** — человекочитаемый именованный указатель на образ. - **overlay (lower / upper)** — наложение писабельного слоя (upper) поверх слоя только для чтения (lower); так живой `/etc` лежит поверх образа. - **fs-verity** — механизм ядра, криптографически заверяющий содержимое файла и ловящий любое его изменение. @@ -242,6 +242,8 @@ upac/ 3. Система дошла до здорового состояния — поздний хук / init-юнит делает D' постоянным дефолтом и помечает **пару рабочей**: обновляет `working_etc` текущего `/usr` (§5.7). Это и есть подтверждение. 4. Подтверждение не сработало (система легла раньше по любой причине) — разовая переменная уже погашена, следующая загрузка идёт в постоянный дефолт, то есть на прошлый деплой. Это автооткат. +**D' — это usr-digest, а не составная пара.** `composefs.digest=D'` несёт именно `usr-digest` — то же самое значение, которым именуется `state/deploy//`, и которое `open_tree()` (см. модуль composefs, §7) принимает напрямую, без всякой трансляции. Этим же способом в рантайме узнаётся "какой деплой сейчас активен" без отдельного файла-указателя на диске (§5.7 это тоже отмечает: "активный деплой — отдельный указатель, загруженный `composefs.digest` / дефолт загрузчика"): читаем `/proc/cmdline`, достаём `composefs.digest`, это и есть usr-digest. `etc` из пары намеренно НЕ в cmdline — как только usr-digest известен, из его `state/deploy//meta.json` читается `working_etc` (§5.7), который называет текущий подтверждённый `etc-digest`. + **Эшелоны отката** (какой уровень что ловит): 1. Ядро или initramfs не встали — прошивка сама уходит в постоянный дефолт (разовая переменная погашена) = прошлый деплой. From c69f2e47026af9b756dfca7bb3815fae89d13292 Mon Sep 17 00:00:00 2001 From: JustPav Date: Sat, 8 Aug 2026 23:35:48 +0400 Subject: [PATCH 05/68] fix: Added database path to the image Co-Authored-By: Claude Sonnet 5 --- lib/lib/lib.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/lib/lib.toml b/lib/lib/lib.toml index 272095e6..aeceb9b2 100644 --- a/lib/lib/lib.toml +++ b/lib/lib/lib.toml @@ -7,6 +7,11 @@ # Changing a name means already-built images stop finding their tables when # reading their DB (tables are looked up by name) — data isn't lost, but # becomes unreachable through the API. +# +# database_path is where the redb file itself lives inside the mounted /usr +# tree (relative to that tree's root, e.g. read via FileHandle::read_file +# after Deploy::open_tree(usr_digest), see doc §7). Changing it means +# already-built images stop finding their own embedded DB on read. [database] packages_table_name = "packages" packages_by_name_table_name = "packages_by_name" @@ -14,6 +19,7 @@ files_table_name = "files" files_by_path_table_name = "files_by_path" packages_meta_type_name = "upac::PackageMeta" files_entry_type_name = "upac::FileEntry" +database_path = "share/upac/packages.redb" # Names of the well-known directories on the deployment partition (deploy # records + composefs repo, see doc §3). Changing a name means existing From efbaa9eaf39f1db50212744eb7ae0124bed30e05 Mon Sep 17 00:00:00 2001 From: JustPav Date: Sat, 8 Aug 2026 23:51:33 +0400 Subject: [PATCH 06/68] Fix: Fixed the release profile in the Cargo.toml workspace. Fix: Fixed the Cargo.toml user cli. Fix: Removed an unused macro. Co-Authored-By: Claude Sonnet 5 --- Cargo.toml | 5 ++++- lib/lib/Cargo.toml | 1 + lib/lib/src/errors.rs | 11 ----------- user/upac-cli/Cargo.toml | 7 ------- 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3e471c11..912f9e24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,5 +19,8 @@ quote = "1" proc-macro2 = "1" [profile.release] -lto = true +opt-level = 3 +lto = "fat" codegen-units = 1 +strip = true +panic = "abort" diff --git a/lib/lib/Cargo.toml b/lib/lib/Cargo.toml index df2df2ce..3fc6ed3a 100644 --- a/lib/lib/Cargo.toml +++ b/lib/lib/Cargo.toml @@ -41,6 +41,7 @@ tokio = { version = "1", features = ["rt"] } serde = { version = "1", features = ["derive"] } serde_json = "1.0.151" toml = "0.8" +linux-kernel-cmdline = "0.1.1" [build-dependencies] toml = "0.8" diff --git a/lib/lib/src/errors.rs b/lib/lib/src/errors.rs index 9e5f569c..c035d602 100644 --- a/lib/lib/src/errors.rs +++ b/lib/lib/src/errors.rs @@ -58,17 +58,6 @@ macro_rules! lock_error_from { } pub(crate) use lock_error_from; -macro_rules! deploy_record_error_from { - ($name:ident) => { - impl From for $name { - fn from(error: DeployRecordError) -> Self { - $name::Common(CommonError::DeployRecord(error)) - } - } - }; -} -pub(crate) use deploy_record_error_from; - #[derive(Debug, Clone, PartialEq, Eq)] pub enum CommonError { OutOfMemory, diff --git a/user/upac-cli/Cargo.toml b/user/upac-cli/Cargo.toml index 556f39a2..599f778e 100644 --- a/user/upac-cli/Cargo.toml +++ b/user/upac-cli/Cargo.toml @@ -29,10 +29,3 @@ sha2 = { version = "0.11" } hex = { version = "0.4" } ctrlc = { version = "3.5.2", features = ["termination"] } gettext-rs = { version = "0.7", features = ["gettext-system"] } - -[profile.release] -opt-level = 3 -lto = "fat" -codegen-units = 1 -strip = true -panic = "abort" From 77ffaff5d0f30d68f2e06cf6d2ebe12247fec72d Mon Sep 17 00:00:00 2001 From: JustPav Date: Sun, 9 Aug 2026 00:01:18 +0400 Subject: [PATCH 07/68] fix: Documentation updated Co-Authored-By: Claude Sonnet 5 --- doc/UPAC project note.en.md | 2 ++ doc/UPAC project note.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/doc/UPAC project note.en.md b/doc/UPAC project note.en.md index 2238b15f..911d7156 100644 --- a/doc/UPAC project note.en.md +++ b/doc/UPAC project note.en.md @@ -244,6 +244,8 @@ Rollback is built on a one-shot boot choice plus a late confirmation that the sy **D' is the usr-digest, not a combined pair.** `composefs.digest=D'` carries exactly the `usr-digest` — the same value that keys `state/deploy//` and that `open_tree()` (see composefs module, §7) takes directly, with no translation step. This is also how "the currently running deploy" gets resolved at runtime with no separate pointer file on disk (§5.7 calls this out too: "the active deploy is a separate pointer — the booted `composefs.digest` / boot default"): read `/proc/cmdline`, pull `composefs.digest`, that's the usr-digest. The `etc` side of the pair is deliberately NOT in cmdline — once the usr-digest is known, its `state/deploy//meta.json` is read for `working_etc` (§5.7), which names the currently confirmed `etc-digest`. +**Why an unrecognized parameter like `composefs.digest=` survives in `/proc/cmdline` at all.** `/proc/cmdline` is not a filtered view of parameters the kernel understands — it's the raw, untouched string the bootloader handed the kernel. When the kernel's argument parser meets a parameter it doesn't recognize, it does not drop it; it logs "Unknown kernel command line parameters ..., will be passed to user space" and leaves the string intact for `/proc/cmdline` and PID 1's own cmdline. This is standard, relied-upon kernel behavior — it's exactly how `systemd.*`, dracut's `rd.*`, `luks.uuid=`, and OSTree's own `ostree=` already work: none of them are kernel parameters either, all of them are userspace-only, all of them survive the same way. + **Rollback tiers** (which level catches what): 1. Kernel or initramfs did not come up — the firmware itself goes to the persistent default (one-shot cleared) = the previous deploy. diff --git a/doc/UPAC project note.md b/doc/UPAC project note.md index 36a97b88..d358f83d 100644 --- a/doc/UPAC project note.md +++ b/doc/UPAC project note.md @@ -244,6 +244,8 @@ upac/ **D' — это usr-digest, а не составная пара.** `composefs.digest=D'` несёт именно `usr-digest` — то же самое значение, которым именуется `state/deploy//`, и которое `open_tree()` (см. модуль composefs, §7) принимает напрямую, без всякой трансляции. Этим же способом в рантайме узнаётся "какой деплой сейчас активен" без отдельного файла-указателя на диске (§5.7 это тоже отмечает: "активный деплой — отдельный указатель, загруженный `composefs.digest` / дефолт загрузчика"): читаем `/proc/cmdline`, достаём `composefs.digest`, это и есть usr-digest. `etc` из пары намеренно НЕ в cmdline — как только usr-digest известен, из его `state/deploy//meta.json` читается `working_etc` (§5.7), который называет текущий подтверждённый `etc-digest`. +**Почему нераспознанный параметр вроде `composefs.digest=` вообще доживает до `/proc/cmdline`.** `/proc/cmdline` — не отфильтрованный список параметров, которые понимает ядро, а сырая, нетронутая строка, которую загрузчик передал ядру. Когда парсер аргументов ядра встречает незнакомый параметр, он его не выкидывает — печатает что-то вроде "Unknown kernel command line parameters ..., will be passed to user space" и оставляет строку как есть, для `/proc/cmdline` и cmdline самого PID1. Это штатное, задокументированное поведение ядра, на которое и так все полагаются — ровно так же работают `systemd.*`, dracut'овские `rd.*`, `luks.uuid=` и собственный `ostree=` у OSTree: ни один из них тоже не параметр ядра, все они чисто userspace, и все доживают до `/proc/cmdline` тем же способом. + **Эшелоны отката** (какой уровень что ловит): 1. Ядро или initramfs не встали — прошивка сама уходит в постоянный дефолт (разовая переменная погашена) = прошлый деплой. From 6d7c2070478ce917fc02cdb6e0705445c24c67af Mon Sep 17 00:00:00 2001 From: JustPav Date: Sun, 9 Aug 2026 00:01:52 +0400 Subject: [PATCH 08/68] New: Added a function to retrieve current user digests Co-Authored-By: Claude Sonnet 5 --- lib/lib/lib.toml | 5 +++++ lib/lib/src/deploy/digest.rs | 16 ++++++++++++++++ lib/lib/src/deploy/error.rs | 8 ++++++++ lib/lib/src/deploy/mod.rs | 1 + 4 files changed, 30 insertions(+) create mode 100644 lib/lib/src/deploy/digest.rs diff --git a/lib/lib/lib.toml b/lib/lib/lib.toml index aeceb9b2..22c771d5 100644 --- a/lib/lib/lib.toml +++ b/lib/lib/lib.toml @@ -24,12 +24,17 @@ database_path = "share/upac/packages.redb" # Names of the well-known directories on the deployment partition (deploy # records + composefs repo, see doc §3). Changing a name means existing # installs stop finding their deploys/repo on upgrade. +# +# usr_digest_cmdline_param names the kernel cmdline parameter written at boot +# entry creation (§5.2, "composefs.digest=D'") and read back at runtime to +# resolve the currently booted usr-digest — one name shared by both sides. [deployment] root_dir = "/" deploys_dir = "state/deploy" repo_dir = "composefs" sysroot_dir = "sysroot" record_filename = "meta.json" +usr_digest_cmdline_param = "composefs.digest" # Name of the abstract Unix socket address upac uses to hold its exclusive # process lock (bind() on this address — a second concurrent upac gets diff --git a/lib/lib/src/deploy/digest.rs b/lib/lib/src/deploy/digest.rs new file mode 100644 index 00000000..b354ba8f --- /dev/null +++ b/lib/lib/src/deploy/digest.rs @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 JustPav +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +use linux_kernel_cmdline::utf8::CmdlineOwned; + +use crate::deploy::error::SysrootError; +use crate::types::deployment::USR_DIGEST_CMDLINE_PARAM; + +pub fn current_usr_digest() -> Result { + let cmdline = CmdlineOwned::from_proc()?; + let digest = cmdline.require_value_of(USR_DIGEST_CMDLINE_PARAM)?; + + Ok(digest.to_owned()) +} diff --git a/lib/lib/src/deploy/error.rs b/lib/lib/src/deploy/error.rs index 024441d4..02e65343 100644 --- a/lib/lib/src/deploy/error.rs +++ b/lib/lib/src/deploy/error.rs @@ -20,6 +20,7 @@ pub enum SysrootError { RepoDirNotFound, ProbeUnavailable, FilesystemTypeNotFound, + CurrentUsrDigestNotFound, System(Errno), } @@ -53,6 +54,12 @@ impl From for SysrootError { } } +impl From for SysrootError { + fn from(_: anyhow::Error) -> Self { + SysrootError::CurrentUsrDigestNotFound + } +} + impl From for ErrorKind { fn from(error: SysrootError) -> Self { match error { @@ -64,6 +71,7 @@ impl From for ErrorKind { SysrootError::RepoDirNotFound => ErrorKind::NotFound, SysrootError::ProbeUnavailable => ErrorKind::Unexpected, SysrootError::FilesystemTypeNotFound => ErrorKind::NotFound, + SysrootError::CurrentUsrDigestNotFound => ErrorKind::NotFound, SysrootError::System(_) => ErrorKind::Unexpected, } } diff --git a/lib/lib/src/deploy/mod.rs b/lib/lib/src/deploy/mod.rs index e5b0606e..c6df154f 100644 --- a/lib/lib/src/deploy/mod.rs +++ b/lib/lib/src/deploy/mod.rs @@ -21,6 +21,7 @@ use crate::composefs::error::RepoError; use crate::composefs::repository::{self, ObjectID}; use crate::types::deployment::{DEPLOYS_DIR, REPO_DIR, ROOT_DIR, SYSROOT_DIR}; +pub mod digest; pub mod error; #[derive(Debug, Clone, Copy, PartialEq, Eq)] From dc3d54450d93c904c4b83aad7b16da77577e3629 Mon Sep 17 00:00:00 2001 From: JustPav Date: Mon, 10 Aug 2026 00:27:59 +0400 Subject: [PATCH 09/68] fix: Separated the first chapter of the documentation; fix: Updated definitions; fix: Added missing definitions. --- doc/rus/Upac - chapter 0.md | 69 +++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 doc/rus/Upac - chapter 0.md diff --git a/doc/rus/Upac - chapter 0.md b/doc/rus/Upac - chapter 0.md new file mode 100644 index 00000000..bf92d03a --- /dev/null +++ b/doc/rus/Upac - chapter 0.md @@ -0,0 +1,69 @@ +# **UPAC — единый проектный документ.** + +Проектный документ. +Ветка проекта: **`lib-rs`**, крейт `lib-rust/`. + +## **§0.** Вступление и определения. + +Этот параграф определяет термины, используемые дальше по тексту без пояснений. + +### **Пункт 1.** Базовые понятия. + +- **Файл** — это именованная область памяти, в которой хранится определённый объём информации; +- **Расширение файла (file extension)** — это суффикс в конце имени файла, отделенный точкой, который указывает операционной системе и программам на формат данных, содержащихся в файле, и определяет, с помощью какого приложения его следует открывать или обрабатывать; +- **Каталог (папка)** — контейнер для файлов и других каталогов; +- **Путь** — это адрес файла или каталога, который указывает его точное расположение в файловой системе. +- **Файловая система** — способ, которым данные разложены на диске так, чтобы ОС видела их как файлы и папки; +- **Диск / блочный девайс** — устройство хранения (физическое или виртуальное); +- **Раздел (partition)** — выделенная часть диска, на которой живёт одна ФС; +- **MBR (Master Boot Record)** — это устаревший формат структуры загрузочного сектора, применяемый с 1983 года, который хранит первичный код загрузчика и таблицу разделов, что огранивает накопитель максимум 4 основными разделами и объёмом до 2 ТБ; +- **GPT (GUID Partition Table)** — это стандарт таблицы разделов накопителя, который позволяет создавать много разделов и работать с дисками объёмом больше 2 терабайт; +- **Монтирование (mount)** — *«подключение»* ФС раздела в точку дерева каталогов, после чего содержимое файловой системы доступно по этому пути; +- **Ядро (kernel)** — главная программа операционной системы, которая выступает посредником между приложениями и железом компьютера, распределяя ресурсы (память, процессор, устройства). +- **Initramfs** — это временная файловая система в оперативной памяти (RAM), которая содержит нужные драйверы и программы, чтобы ядро могло найти, расшифровать или подготовить основной диск и запустить с него главную систему; +- **Прошивка (firmware)** — это базовое программное обеспечение, вшитое в энергонезависимую память микросхемы на материнской плате или устройстве: видеокарте, SSD, контроллере, служащая главным мостом между физическим железом и операционной системой, инициализируя компоненты при включении и предоставляя низкоуровневые инструкции для управления ими; +- **BIOS (Basic Input Output System)** — это устаревший стандарт прошивки 1975 года, который не умеет читать файловые системы и считывает загрузочный код из первого 512-байтного сектора диска (MBR), из-за чего опрашивает железо последовательно, работает медленно и ограничен накопителями до 2 ТБ; +- **UEFI (Unified Extensible Firmware Interface)** — это современный стандарт прошивки 2005 года, который умеет полноценно читать файловую систему FAT32 на специальном разделе (ESP) и напрямую запускать из неё .efi-файлы, что позволяет работать с GPT-дисками любого объёма, опрашивать железо параллельно и проверять подписи кода через Secure Boot; +- **ESP (EFI System Partition)** — системный раздел, обычно с файловой системой вида FAT32, который считывается напрямую прошивкой UEFI при включении компьютера. На нём хранятся файлы bootloader формата .efi, ключи Secure Boot и запускные компоненты, необходимые для первой фазы старта железа. +- **Secure Boot** — это функция прошивки UEFI, которая блокирует запуск любого стороннего кода при включении ПК с помощью проверки подписи этого кода; +- **Загрузчик (bootloader)** — это программа, которую запускает прошивка платы (UEFI/BIOS), чтобы дать пользователю выбор ОС, подгрузить нужные параметры и запустить ядро с диска.; +- **Cmdline (командная строка ядра)** — это текстовая строка с инструкциями и настройками, которые загрузчик передаёт ядру при старте, чтобы определить режим работы системы (например, указать корневой диск, включить отладку или отключить драйвер); +- **Хеш-функция** — это алгоритм, который преобразует любой объём данных в последовательность фиксированной длины. Одинаковые данные всегда дают одинаковый код, а любые разные данные — практически гарантированно уникальный; +- **Хеш (дайджест)** — это уникальная короткая последовательность, который получается после обработки данных хеш-функцией. Если данные не менялись, хеш всегда идентичен; если изменился хотя бы один член последовательности — итоговый код полностью меняется; +- **Пакет** — это архив с файлами программы и её метаданными: списком файлов, необходимых в системе для её работы, и инструкциями по установке, который менеджер пакетов устанавливает, обновляет или удаляет как единое целое; +- **Репозиторий** — это хранилище пакетов, в котором файлы программы лежат вместе с подписями и единым индексом-каталогом. Это позволяет пакетному менеджеру автоматически находить нужные версии, проверять их подлинность и скачивать правильные зависимости; +- **Цифровая подпись** — это зашифрованная метка-штамп от разработчика, прикреплённая к пакету: гарантирует авторство, то есть факт принадлежность текущего кода конретному лицу, и целостность, то есть отсуствие изменений программы после её создания разработчиком; +- **Пакетный менеджер** — это программа, которая автоматически скачивает, устанавливает, обновляет и удаляет программы из репозитория, а также сама находит и ставить все необходимые для их работы программы (зависимости); +- **Атомарность** — свойство неделимости: операция либо применяется целиком, либо не применяется вовсе, без промежуточных полуразобранных состояний; +- **Immutable (неизменяемая) система** — это архитектура операционной системы, в которой системные файлы защищены от изменений во время работы, а любые обновления применяются атомарно и ставясь рядом, позволяя в любой момент мгновенно откатиться к прежнему рабочему состоянию. +- **POSIX (Portable Operating System Interface)** — это семейство стандартов IEEE и ISO, определяющее единый программный интерфейс (API), системные утилиты и поведение командной строки для Unix-подобных операционных систем; +- **FHS (Filesystem Hierarchy Standard)** — это стандарт, который определяет структуру, название и назначение основных каталогов в Unix-подобных операционных системах; + +### **Пункт 2.** Определение используемых системой папок. + +- **`/`** — самая верхняя директория в иерархии файловой системы (FHS), от которой отходят все остальные папки и подмонтированные диски.; +- **`/usr`** — системная папка, содержащая исполняемые файлы (программы): бинарники, библиотеки, другие системные ресурсы. В неизменяемых (immutable) системах этот каталог смонтирован в режиме только для чтения (read-only, ro), а его обновление происходит целиком в виде полной замены одного на другой, что и позволяет мгновенно откатывать всю операционную систему при сбоях; +- **`/etc`** — каталог конфигурационных файлов. В неизменяемых (immutable) системах он остается доступен для записи, чтобы пользователь мог менять настройки, а при обновлении или откате слоя /usr его содержимое автоматически объединяется для консистентности настроек; +- **`/var`** — каталог для изменяемых данных, которые программы создают и обновляют во время работы: логи, базы данных, кэш, очереди печати. Эта папка всегда открыта для записи и полностью сохраняется при любых обновлениях или откатах системы; +- **`/home`** — каталог для личных файлов пользователей: документы, загрузки, проекты, и их индивидуальных настроек программ. Эта папка полностью изолирована от файлов ОС, доступна для записи и не затрагивается при обновлениях или откатах системы; +- **`/boot` (или `/efi`)** — директория или отдельный раздел, где содержатся файлы, необходимые для дальнейшей загрузки операционной системы: сжатый файл ядра (vmlinuz), образ инициализации RAM-диска (initramfs) или единые исполняемые образы загрузки (UKI); +- **`/sysroot`** — временная директория монтирования физического диска на раннем этапе загрузки во время работы initramfs или в атомарных системах. Монтируется раздел диска для последующего выбора нужногых каталогов для загрузки нужной версии операционной системы, превращая его в корень системы `/`; + +### **Пункт 3.** Определение профессионализмов проекта. + +- **Контент-адресное хранилище (cas)** — это метод хранения данных, где адрес файла определяется хешем его содержимого, что обеспечивает автоматическую дедупликацию одинаковых файлов, гарантирует защищенность от подмены и позволяет моментально проверять целостность данных; +- **Образ (image)** — это неизменяемый (ro) снимок файловой системы в конкретной версии, который разворачивается как единое целое и гарантирует одинаковое состояние ОС на любых устройствах; +- **Деплой (deployment)** — это конкретная развёрнутая версия системы на диске, состоящая из первичного неизменяемого слоя операционной системы в виде исполняемых файлов (`/usr`) и связанного с ним слоя конфигураций (`/etc`), где точка загрузки выбирается по хешу системного образа, а нужная версия настроек подтягивается автоматически из его метаданных; +- **Ref (ссылка)** — это человекочитаемое имя, которое указывает на конкретный хеш образа и обновляется при выходе новых версий образа системы; +- **OverlayFS (lower / upper)** — это виртуальная файловая система, которая объединяет слой только для чтения (lower, базовый образ) и записываемый слой (upper, изменения), создавая для пользователя единую папку, где системные файлы остаются неприкосновенными, а любые правки сохраняются отдельно; +- **fs-verity** — это встроенный в ядро Linux механизм защиты целостности файлов, который делает файл неизменяемым (ro) и при каждом чтении проверяет его блоки через дерево Меркла, мгновенно блокируя доступ при малейшем повреждении или подмене данных; +- **Разовый вход (one-shot / BootNext)** — это однократная загрузочная запись UEFI, которая активируется строго на один запуск системы, позволяя протестировать новое обновление и автоматически откатиться на прежнюю рабочую версию при сбое; +- **Трёхстороннее слияние (3-way merge)** — это алгоритм автоматического объединения текстовых файлов, который сравнивает две изменившиеся версии с их общим предком, чтобы сохранить пользовательские правки и накатить системные обновления без конфликтов; +- **`base`** — это оригинальный конфигурационный файл из предыдущей (текущей) версии системы, который служит незатронутым эталоном для вычисления изменений, внесенных как разработчиками, так и пользователем; +- **`new`** — это свежая версия оригинального конфигурационного файла из пришедшего обновления системы, содержащая актуальные настройки от разработчиков; +- **`live`** — это текущий конфигурационный файл в работающей системе, содержащий ручные правки и индивидуальные настройки пользователя; +- **`.upac-new`** — это расширение файла, которое присваивается новым системным настройкам при неразрешимом конфликте во время трёхстороннего слияния, чтобы сохранить их рядом с оригинальным файлом и не затереть ручные правки пользователя; +- **`seq` (sequence / порядковый номер)** — это строго увеличивающийся счетчик, присваиваемый каждому новому деплою, который определяет точную хронологию версий системы и служит гарантированным ориентиром при автоматическом переключении или откате на предыдущие состояния независимо от системных часов; +- **Пин (pin / закрепление)** — это флаг защиты в метаданных деплоя, который блокирует удаление конкретной версии системы при автоматической очистке (сборке мусора), гарантируя сохранение текущей рабочей версии, базовой точки отката или явно отмеченных пользователем состояний; +- **Откат (rollback)** — это операция мгновенного возврата к заведомо рабочему состоянию системы, которая может выполняться как целиком: переключением загрузчика на предыдущий атомарный деплой с его версиями /usr и /etc, так и точечно: сбросом изменений в слое конфигураций /etc к оригинальному состоянию; +- **Сборка мусора (garbage Collection / gc)** — это автоматический или ручной процесс очистки хранилища, который находит и удаляет старые слои файловой системы, файлы и объекты CAS, больше не используемые ни одним активным, текущим или закрепленным (pinned) деплоем, освобождая дисковое пространство без риска повредить рабочую систему. From b15ea2f8f4a2ed4543478cc15e19f5d8ac3743df Mon Sep 17 00:00:00 2001 From: JustPav Date: Mon, 10 Aug 2026 01:11:57 +0400 Subject: [PATCH 10/68] fix: Moved Chapter 1 to a new, separate .md file --- doc/rus/Upac - chapter 1.md | 53 +++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 doc/rus/Upac - chapter 1.md diff --git a/doc/rus/Upac - chapter 1.md b/doc/rus/Upac - chapter 1.md new file mode 100644 index 00000000..8801e164 --- /dev/null +++ b/doc/rus/Upac - chapter 1.md @@ -0,0 +1,53 @@ +# **UPAC — единый проектный документ.** + +Проектный документ. +Ветка проекта: **`lib-rs`**, крейт `lib-rust/`. + +--- + +## **§1.** Постановка задач. + +--- + +**Проблема состояния.** + +На обычной Linux-системе установка, обновление и удаление софта меняют работающую систему *на месте* — правят её файлы прямо там, где она сейчас живёт. Из-за этого у системы в любой момент нет единого проверяемого состояния: она представляет собой груду по отдельности изменённых файлов. Следствия три: +1. Прерванное или неудачное изменение оставляет систему в сломанном полусостоянии; +2. Нельзя воспроизвести или доказать конкретное *«заведомо рабочее»* состояние; +3. Нет чистого способа вернуться назад. + +**Решение проблемы состояния.** + +UPAC рассматривает каждое состояние системы как **цельный, контент-адресный, проверяемый образ**. Любая операция: установка, обновление, удаление, не трогает работающую систему, а **строит из текущего образа новый**. Переключение на новый образ — один атомарный шаг, в следствии чего прежний образ остаётся нетронутым. Отсюда прямо следуют три свойства, отвечающие на проблему: +1. **Наличие у системы свойства атомарности**: операция либо прошла целиком, либо система осталась прежней; +2. **Наличие у системы свойства воспроизводимости и проверяемости**: любое состояние опознаётся и заверяется по хешу, откат — это просто загрузка предыдущего образа. При этом раскладка диска, загрузчик и ядро остаются полностью под контролем пользователя. + +--- + +**Проблема доступа.** + +Базовое дерево системы (`/usr`) неизменяемо и находится под управлением менеджера пакетов. Если пользователь хочет просто добавить туда свои файлы — например, положить обои или ассеты, которые пакет ожидает в `/usr`, — он не может их туда просто скопировать. Приходится заворачивать пару файлов в полноценный пакет: метаданные, сборка, установка, ради самого факта их размещения. Барьер на добавление своего в управляемое дерево неоправданно высок. + +**Решение проблемы доступа.** + +UPAC позволяет добавлять произвольные пользовательские файлы в управляемое дерево (`/usr`) напрямую, одной командой, без написания пакета. Файл попадает в собираемый образ как полноценное содержимое, но пользователю не нужен весь конвейер упаковки. + +--- + +**Проблема совместимости.** + +Под одно и то же ядро Linux существует множество несовместимых форматов пакетов, к примеру: deb, rpm, pkg.tar и т.д. Программа, собранная под один формат пакета, не установится с использованием другого пакетного менеджера, в результате чего пользователь заперт в экосистеме своего пакетного формата, хотя ядро и ABI у всех общие. + +**Решение проблемы совместимости.** + +UPAC не привязан к одному формату пакетов. Разбор конкретного формата вынесен в отдельные бэкенды: по одному на формат, которые приводят пакет к общему внутреннему представлению — дереву файлов и метаданным. За счёт этого один менеджер ставит пакеты разных форматов на одну систему, и формат перестаёт быть границей совместимости. + +--- + +**Проблема управления.** + +Даже когда файл уже в системе, его нельзя привязать к пакету как пользовательский — так, чтобы менеджер его отслеживал и подчищал вместе с пакетом. Особенно это больно в `/usr`: добавленные вручную файлы тем остаются *«сиротами»* вне учёта — их не видно при удалении и не почистить автоматически. + +**Решение проблемы управления.** + +UPAC позволяет прикреплять файл к пакету как пользовательский, с полноценным учётом в базе. Такой файл наследует жизненный цикл пакета: отслеживается, показывается в его составе и удаляется вместе с ним. From 8bbc9a635090592476967e16331aae65df504245 Mon Sep 17 00:00:00 2001 From: JustPav Date: Mon, 10 Aug 2026 01:19:29 +0400 Subject: [PATCH 11/68] fix: Moved Chapter 2 to a new, separate .md file --- doc/rus/Upac - chapter 2.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 doc/rus/Upac - chapter 2.md diff --git a/doc/rus/Upac - chapter 2.md b/doc/rus/Upac - chapter 2.md new file mode 100644 index 00000000..b9416d8c --- /dev/null +++ b/doc/rus/Upac - chapter 2.md @@ -0,0 +1,15 @@ +# **UPAC — единый проектный документ.** + +Проектный документ. +Ветка проекта: **`lib-rs`**, крейт `lib-rust/`. + +## **§2.** Определение того, чем проект НЕ является (Non-goals). + +--- + +- **Не дистрибутив:** UPAC — менеджер пакетов и механизм деплоя, а не операционная система. Он не везёт кураторскую репу, дефолтный набор софта или релиз-цикл, он лишь управляет тем контентом, на который его натравили; +- **Не менеджер конфигурации:** UPAC сводит `/etc` и сохраняет правки пользователя через обновления, но не генерирует и не навязывает конфиг-политику — это не Ansible и не NixOS-модули. Он сохраняет и примиряет, а не генерирует; +- **Не рантайм контейнеров:** UPAC использует те же кирпичи, что и контейнеры: composefs, OCI, но разворачивает хост-систему, а не контейнеры. Он не заменяет docker/podman; +- **Никаких изменений на месте — by design:** Любое изменение системы порождает новый образ; горячей подмены файлов на живой системе нет даже опцией. Это прямое следствие принципов проекта; +- **Не сервер репозиториев:** UPAC — только клиент к готовым внешним репам: зеркала дистрибутивов, OCI-реестры и т.п., своего репозитория или сервера он не поднимает. Единственная локальная альтернатива репе — доставка образа файлом: `--file`; +- **Не чинит файловую систему и диск:** UPAC отвечает за корректность своих операций: проверка пакетов, атомарность образов, целостность репы, и через fs-verity **обнаруживает** порчу контента, отказываясь грузить повреждённый деплой. Но восстановление самой ФС, битых блоков, деградировавшего носителя или ошибок железа — вне его зоны: это задача `fsck`, SMART и замены диска. Развал системы из-за битого диска — не отказ UPAC. From 18619f437607de6e298f632722e3015d0296d120 Mon Sep 17 00:00:00 2001 From: JustPav Date: Mon, 10 Aug 2026 01:28:23 +0400 Subject: [PATCH 12/68] fix: Added headers for reuse --- doc/rus/Upac - chapter 0.md | 6 ++++++ doc/rus/Upac - chapter 1.md | 6 ++++++ doc/rus/Upac - chapter 2.md | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/doc/rus/Upac - chapter 0.md b/doc/rus/Upac - chapter 0.md index bf92d03a..0f141d6c 100644 --- a/doc/rus/Upac - chapter 0.md +++ b/doc/rus/Upac - chapter 0.md @@ -1,3 +1,9 @@ + + # **UPAC — единый проектный документ.** Проектный документ. diff --git a/doc/rus/Upac - chapter 1.md b/doc/rus/Upac - chapter 1.md index 8801e164..38ad2ed4 100644 --- a/doc/rus/Upac - chapter 1.md +++ b/doc/rus/Upac - chapter 1.md @@ -1,3 +1,9 @@ + + # **UPAC — единый проектный документ.** Проектный документ. diff --git a/doc/rus/Upac - chapter 2.md b/doc/rus/Upac - chapter 2.md index b9416d8c..764aea01 100644 --- a/doc/rus/Upac - chapter 2.md +++ b/doc/rus/Upac - chapter 2.md @@ -1,3 +1,9 @@ + + # **UPAC — единый проектный документ.** Проектный документ. From 78ebc22f504a3a1753a94c331ff03edc10769b26 Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 06:25:40 +0400 Subject: [PATCH 13/68] fix: Moved the third chapter --- doc/rus/Upac - chapter 3.md | 61 +++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 doc/rus/Upac - chapter 3.md diff --git a/doc/rus/Upac - chapter 3.md b/doc/rus/Upac - chapter 3.md new file mode 100644 index 00000000..cdb8bb01 --- /dev/null +++ b/doc/rus/Upac - chapter 3.md @@ -0,0 +1,61 @@ + + +# **UPAC — единый проектный документ.** + +Проектный документ. +Ветка проекта: **`lib-rs`**, крейт `lib-rust/`. + +## **§3.** Структура диска. + +Данный параграф описывает, что физически лежит на дисках развёрнутой системы. + +### Карта + +``` +[блочный девайс, GPT] +│ +├── ESP (FAT32) (1) +│ ├── EFI/Linux/upac-from.efi (2) +│ ├── EFI/Linux/upac-to.efi (2) +│ └── loader/entries/*.conf (3) +│ +├── deployment-раздел → /sysroot (4) +│ ├── composefs/ (5) +│ │ ├── meta.json (6) +│ │ ├── objects// (7) +│ │ ├── images/ (8) +│ │ │ ├── → ../objects//… (9) +│ │ │ └── refs/<имя> → ../images/ (10) +│ │ └── streams/ (11) +│ │ ├── → ../objects//… +│ │ └── refs/<имя> +│ └── state/deploy// (12) +│ ├── meta.json (12) +│ └── etc-upper/{upper, work} (13) +│ +├── /var-раздел → /var (14) +└── /home-раздел → /home (15) +``` + +### Легенда карты + +- **(1)** ESP (EFI System Partition) — отдельный FAT-раздел, который читает прошивка UEFI, монтируется в `/boot` или `/efi`; +- **(2)** `upac-from.efi` / `upac-to.efi` — два фиксированных слота под UKI для direct-UKI загрузки. Операция пишет новый UKI в неактивный слот, переключение — через `BootNext`; +- **(3)** `loader/entries/*.conf` — BLS-записи для машин с менеджером загрузок: systemd-boot и подобных. Требуется наличие поддержки чтения через BLS записи. +- **(4)** deployment-раздел — физический корень со всем содержимым системы, во время исполнения системы для изменений монтируется в `/sysroot`. Требование к файловой системе — поддержка **fs-verity**. К примеру этот механизм поддерживают ext4, btrfs, xfs; +- **(5)** `composefs/` — репозиторий composefs: контент-адресное хранилище всех файлов и образов. Дефолтный путь composefs для system-режима; +- **(6)** `meta.json` — метаданные репы: версия формата + алгоритм fs-verity (`fsverity--`); +- **(7)** `objects/` — контент-адресное хранилищи, в котором объекты разложены по подкаталогам из первых 2 hex-символов хеша. Одинаковое содержимое хранится один раз; +- **(8)** `images/` — EROFS-образы: контент-адресные слепки деревьев `/usr` **и** `/etc`. Несут метаданные дерева, данные файлов берутся из `objects/`; +- **(9)** `` — образ = симлинк на объект в `objects/`, определятеся по хешу образа; +- **(10)** `refs/<имя>` — человекочитаемый именованный указатель на образ; +- **(11)** `streams/` — splitstream'ы - импортированные слои/коммиты, тоже симлинки в `objects/` с дополнением своих refs; +- **(12)** `state/deploy//` — запись деплоя, в котором **ключ = `usr-digest`**. Внутри храниться`meta.json`. +- **(13)** `etc-upper/upper` - **живой `/etc`**: не вошедшие в деплой правки как upper-слой overlayfs над текущим `working_etc`. Запечатывается в `etc-digest` при смене `/usr` или по `upac commit`; +- **(14)** `etc-upper/work` — **живой `/etc`**: `work` — служебный каталог overlayfs; +- **(15)** `/var` - каталог вынесен на отдельный раздел диска, благодаря чему все меняющиеся данные, логи и базы данных сохраняются напрямую и не теряются при откате системы; +- **(16)** `/home` — пользовательские данные: отдельный каталог с пользовательскими данными, вне версионирования. From bb4ce544a4ad430030fe36139e2ca6b9dab50fd3 Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 06:26:39 +0400 Subject: [PATCH 14/68] fix: Added new missing definitions --- doc/rus/Upac - chapter 0.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/doc/rus/Upac - chapter 0.md b/doc/rus/Upac - chapter 0.md index 0f141d6c..6e6419be 100644 --- a/doc/rus/Upac - chapter 0.md +++ b/doc/rus/Upac - chapter 0.md @@ -22,15 +22,16 @@ SPDX-License-Identifier: CC-BY-SA-4.0 - **Файловая система** — способ, которым данные разложены на диске так, чтобы ОС видела их как файлы и папки; - **Диск / блочный девайс** — устройство хранения (физическое или виртуальное); - **Раздел (partition)** — выделенная часть диска, на которой живёт одна ФС; -- **MBR (Master Boot Record)** — это устаревший формат структуры загрузочного сектора, применяемый с 1983 года, который хранит первичный код загрузчика и таблицу разделов, что огранивает накопитель максимум 4 основными разделами и объёмом до 2 ТБ; -- **GPT (GUID Partition Table)** — это стандарт таблицы разделов накопителя, который позволяет создавать много разделов и работать с дисками объёмом больше 2 терабайт; +- **MBR (master Boot Record)** — это устаревший формат структуры загрузочного сектора, применяемый с 1983 года, который хранит первичный код загрузчика и таблицу разделов, что огранивает накопитель максимум 4 основными разделами и объёмом до 2 ТБ; +- **GPT (guid partition Table)** — это стандарт таблицы разделов накопителя, который позволяет создавать много разделов и работать с дисками объёмом больше 2 терабайт; - **Монтирование (mount)** — *«подключение»* ФС раздела в точку дерева каталогов, после чего содержимое файловой системы доступно по этому пути; - **Ядро (kernel)** — главная программа операционной системы, которая выступает посредником между приложениями и железом компьютера, распределяя ресурсы (память, процессор, устройства). - **Initramfs** — это временная файловая система в оперативной памяти (RAM), которая содержит нужные драйверы и программы, чтобы ядро могло найти, расшифровать или подготовить основной диск и запустить с него главную систему; +- **Unified Kernel Image (uki)** — это единый исполняемый файл формата EFI, который объединяет в один неразрывный модуль ядро Linux, образ инициализации (initramfs), параметров командной строки ядра и EFI-загрузчик - **Прошивка (firmware)** — это базовое программное обеспечение, вшитое в энергонезависимую память микросхемы на материнской плате или устройстве: видеокарте, SSD, контроллере, служащая главным мостом между физическим железом и операционной системой, инициализируя компоненты при включении и предоставляя низкоуровневые инструкции для управления ими; -- **BIOS (Basic Input Output System)** — это устаревший стандарт прошивки 1975 года, который не умеет читать файловые системы и считывает загрузочный код из первого 512-байтного сектора диска (MBR), из-за чего опрашивает железо последовательно, работает медленно и ограничен накопителями до 2 ТБ; -- **UEFI (Unified Extensible Firmware Interface)** — это современный стандарт прошивки 2005 года, который умеет полноценно читать файловую систему FAT32 на специальном разделе (ESP) и напрямую запускать из неё .efi-файлы, что позволяет работать с GPT-дисками любого объёма, опрашивать железо параллельно и проверять подписи кода через Secure Boot; -- **ESP (EFI System Partition)** — системный раздел, обычно с файловой системой вида FAT32, который считывается напрямую прошивкой UEFI при включении компьютера. На нём хранятся файлы bootloader формата .efi, ключи Secure Boot и запускные компоненты, необходимые для первой фазы старта железа. +- **BIOS (basic input output system)** — это устаревший стандарт прошивки 1975 года, который не умеет читать файловые системы и считывает загрузочный код из первого 512-байтного сектора диска (MBR), из-за чего опрашивает железо последовательно, работает медленно и ограничен накопителями до 2 ТБ; +- **UEFI (unified extensible firmware interface)** — это современный стандарт прошивки 2005 года, который умеет полноценно читать файловую систему FAT32 на специальном разделе (ESP) и напрямую запускать из неё .efi-файлы, что позволяет работать с GPT-дисками любого объёма, опрашивать железо параллельно и проверять подписи кода через Secure Boot; +- **ESP (efi system partition)** — системный раздел, обычно с файловой системой вида FAT32, который считывается напрямую прошивкой UEFI при включении компьютера. На нём хранятся файлы bootloader формата .efi, ключи Secure Boot и запускные компоненты, необходимые для первой фазы старта железа. - **Secure Boot** — это функция прошивки UEFI, которая блокирует запуск любого стороннего кода при включении ПК с помощью проверки подписи этого кода; - **Загрузчик (bootloader)** — это программа, которую запускает прошивка платы (UEFI/BIOS), чтобы дать пользователю выбор ОС, подгрузить нужные параметры и запустить ядро с диска.; - **Cmdline (командная строка ядра)** — это текстовая строка с инструкциями и настройками, которые загрузчик передаёт ядру при старте, чтобы определить режим работы системы (например, указать корневой диск, включить отладку или отключить драйвер); @@ -42,8 +43,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0 - **Пакетный менеджер** — это программа, которая автоматически скачивает, устанавливает, обновляет и удаляет программы из репозитория, а также сама находит и ставить все необходимые для их работы программы (зависимости); - **Атомарность** — свойство неделимости: операция либо применяется целиком, либо не применяется вовсе, без промежуточных полуразобранных состояний; - **Immutable (неизменяемая) система** — это архитектура операционной системы, в которой системные файлы защищены от изменений во время работы, а любые обновления применяются атомарно и ставясь рядом, позволяя в любой момент мгновенно откатиться к прежнему рабочему состоянию. -- **POSIX (Portable Operating System Interface)** — это семейство стандартов IEEE и ISO, определяющее единый программный интерфейс (API), системные утилиты и поведение командной строки для Unix-подобных операционных систем; -- **FHS (Filesystem Hierarchy Standard)** — это стандарт, который определяет структуру, название и назначение основных каталогов в Unix-подобных операционных системах; +- **POSIX (portable operating system interface)** — это семейство стандартов IEEE и ISO, определяющее единый программный интерфейс (API), системные утилиты и поведение командной строки для Unix-подобных операционных систем; +- **FHS (filesystem hierarchy standard)** — это стандарт, который определяет структуру, название и назначение основных каталогов в Unix-подобных операционных системах; ### **Пункт 2.** Определение используемых системой папок. From 6a87811439a62da0179c84fa2ebb56c710f0d5cd Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 06:56:01 +0400 Subject: [PATCH 15/68] new: Added a file for managing Cargo --- .cargo/config.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..a3bb2f3a --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --quiet --manifest-path xtask/Cargo.toml --" From 0e7e91be3f1cdf432df6e6e39a1f98a1a53c994b Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 08:42:19 +0400 Subject: [PATCH 16/68] Fix: Partially ported chapter 5 (up to, but not including, 5.6) --- doc/rus/Upac - chapter 5.md | 116 ++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 doc/rus/Upac - chapter 5.md diff --git a/doc/rus/Upac - chapter 5.md b/doc/rus/Upac - chapter 5.md new file mode 100644 index 00000000..5c6f4a89 --- /dev/null +++ b/doc/rus/Upac - chapter 5.md @@ -0,0 +1,116 @@ + + +## **§5.** Механизмы работы + +Отдельное описание механизмов, которые реализует ядро (`lib/`). + +### **§5.1** Механизм слияния конфигов в каталоге `/etc`. + +Поскольку каталог `/etc` в загруженной системе — `overlay`, состоящая из `lower` = неизменяемого etc-digest, в режиме `ro`, и upper = незадокументированные в системе правки, в режиме `rw`. + +Каталог `/etc` версионируется контент-адресно: каждый снимок файловой системы = `etc-digest` (см. **§5.7**). Задача механизма merge — при смене изменения каталога `/usr` построить новый каталог `/etc`: перенести правки конфигов пользователя, подтянуть новые стандартные конфигурационные файлы пакетов и запечатать результат в первый снимок `etc-digest` нового каталога `/usr`. + +Механизм библиотечный, выполняется на стадии `merge`, до того как новый снимок станет загрузочным. + +**Три входа (3-way):** +- **base** — текущие файлы `/etc`, соотвествующие текущем файлам в `/usr`, из которох собрана загруженная система; +- **new** — новые файлы `/etc`, разворачиваемого каталога `/usr`, которые включают новые стандартные конфигурационные файлы пакетов; +- **live** — текущее состояние конфигурационных файлов `/etc` пользователя, объеденяющего запечатанный в снимке `working_etc` и незапечатанные в снимке правки пользователя текущей, рабочей системы. + +**Классификация по файлу:** +- Если пользователь файл НЕ менял, то есть `live == base`, то результат идёт **новый стандартный конфигурационный файл** пакета; +- Если пользователь файл правил И новый дефолт совпал со старым, то есть пакет файл не менял, то версия файла пользователя сохраняется; +- Если пользователь файл правил И новый стандартный кинфигурационный файл изменился, создав конфликт, то переноится файл с версией пользователя, а новый файл с стандартными настройками кладётся рядом как `<имя_файла>.upac-new`, НО он будет исключён из будущей классификации — это не *«файл пользователя»*. + +**Конфликты, описанные в последнем пункте решаются через специальный хук, и не блокируют прохождение операции.** Информирование пользователя о новых стандартных конфигурационных файлах пакета `.upac-new` через механизм вызова хуков-сообщений, передаваемых в CLI. + +**Результат работы механизма** запечатывается в новый снимок `etc-digest`, который становится `working_etc` нового системного снимка; Живой upper слой `/etc` из overlayfs стартует пустым. Неизменённые файлы дедуплицируются composefs на уровне объектов, так что `etc-digest` — полный слепок `/etc` без дублирования содержимого. + +По команде `upac commit new` этим механизмом запечатывается текущее состояние `/etc` без изменения файлов в каталоге `/usr` — новый `etc-digest` работает под тем же `/usr`. + +### **§5.2** Механизм загрузки новой версии системы и откат на предыдущий рабочий вариант в случае неуспеха загрузки. + +Откат системы построен на разовом выборе новой опции загрузки и позднем подтверждении успешности запуска. Отдельного счётчика попыток запустить систему нет — образ, не загрузившийся с первого раза по любой возможной причине, автоматически откатывается на предыдущий успешный вариант запуска. + +**Механизм разового выбора.** У загрузчика/прошивки есть пара опций запуска: *«разовая опция загрузки / постоянная опция загрузки»*. К примеру для UKI-direct запуска это `BootNext` / `BootOrder`, для загрузчика systemd-boot — `LoaderEntryOneShot` / `LoaderEntryDefault`, для grub — `grub-reboot` / постоянная опция в конфиге. Разовую переменную прошивка/загрузчик использует всего один раз при любой загрузке, поэтому она сама по себе и есть однопопыточный автооткат. + +**Постановка и загрузка:** + +1. При снимке D' пишется загрузочная запись с `composefs.digest=D'. Для UKI-direct загрузки в неактивный слот `upac-to.efi`, для BLS-confing совместимых загрузчиков через в конфигурационный файл через встроенный в composefs механизм `BootconfigParser`; +2. После перезагрузки: загрузчик грузит D' один раз, разовая опция загрузки удаляется. Initramfs монтирует digest из cmdline (overlay composefs), затем pivot и PID1. +3. Система дошла до полной успешной загрузки — поздний хук / init-юнит делает D' постоянным для загрузки и помечает **пару рабочей**: обновляет `working_etc` для текущего `/usr` (См. **§5.7**); +4. Если подтверждение не сработало, то есть система не запустилась до конечного состояния раньше по любой причине — разовая переменная уже удаления из памятти загрузчика/прошивки, следующая загрузка запускает постоянную опцию запуска, то есть на прошлый снимок системы. Это автооткат; + +**D' — это usr-digest, а не составная пара.** `composefs.digest=D'` несёт именно `usr-digest` — то же самое значение, которым именуется каталог `state/deploy//`, и которое `open_tree()` (См. модуль composefs, **§7**) принимает напрямую. Этим же способом в загруженной системе узнаётся *"какой снимок системы сейчас активен"* без отдельного файла-указателя на диске. Каталога `etc` из пары намеренно **НЕТ** в cmdline — как только `usr-digest` известен, из файла в его каталоге `state/deploy//meta.json` читается `working_etc` (См. **§5.7**), который имеет информацию о текущим подтверждённым `etc-digest`. + +**Почему нераспознанный ядром параметр вроде `composefs.digest=` вообще доживает до `/proc/cmdline`?** + +`/proc/cmdline` — это не отфильтрованный список параметров, которые понимает только ядро, а сырая, нетронутая строка, которую загрузчик или UKI передали ядру. Когда парсер аргументов ядра встречает незнакомый параметр, он его не выкидывает, вместо этого печатая *"Unknown kernel command line parameters ..., will be passed to user space"* и оставляет строку как есть, для `/proc/cmdline` и cmdline самого PID1. + +Это штатное, задокументированное поведение ядра, на работе которого основана работа многих userspace программ. + +**Эшелоны отката**. Описание каждого уровеня ошибки загрузки что этот уровень ловит: + +1. Если ядро или initramfs не смогли запуститься — прошивка/загрузчик сами уходят в постоянную загрузочную запись; +2. Если ядро и initramfs запустились, но PID1 не встал — подтверждение успешности запуска не пришло, следующая загрузка откатывается; +3. Если PID1 успешно встал, но сервисы/сеть/GUI мертвы — пользователь может просто перезапустить систему или вызывать команду `upac commit rollback`; + +Если система формально дошла до рабочего состояния и подтвердила это, но какие-то подсистемы или инструменты не встали или работают неверно — доступен ручной откат: `upac commit rollback` из живой системы либо меню прошивки/загрузчика. + +**Осознанные ограничения такого механизма:** + +- Только одна попытка: битый атомарный образ детерминированно битый, повторять попытки его запуска смысла не несут; +- Автоподтверждение доказывает лишь *«запуск успешно дошёл до определнного момента»*, а не *«пользователю хорошо»* — более глубокие поломки откатываются вручную через команду `upac commit rollback`; +- Чистый *«висяк»*: PID1 жив, но завис, без паники и ребута, требует ручной перезагрузки через физическое взаимодействие, чтобы сработала разовая переменная. + +### **§5.3** Постановка на запуск снимка системы (stage). + +На входе — готовый образ D', уже лежащий в каталоге-репозитории: `images/D'`. +На выход — снимок системы, готовый к разовой загрузке. +Связывает операции (См. **§5.4**) с загрузкой (См. **§5.2**). + +1. Слияние каталога `/etc` (См. **§5.1**): merge запечатывает каталог `etc-digest` для D' и объявляет его `working_etc`. Слой недокументированных пользовательских правок upper (`etc-upper/`) стартует пустым; +2. Персистентные разделы/каталоги (`/var`, `/home`) — реальные, монтируются как есть, не трогаются; +3. Запись загрузочной записи с `composefs.digest=D'`: + - UKI-direct — собрать и подписать UKI, записать в неактивный слот `upac-to.efi`; + - Менеджер — `BootconfigParser` пишет BLS-conf в `loader/entries/`; +4. Поставить D' разовым входом следующей загрузки (См. **§5.2**): UKI-direct — `BootNext` на слот, менеджер — `LoaderEntryOneShot` / `grub-reboot` и прочее опции конретной реализации для конретного менеджера. + +Дальше — перезагрузка и пункт **§5.2**. + +### **§5.4** Механизм операций над файловой системой: добавление, удаление, обновление, переименование и т.д. + +Все операции одной формы: +1. Изменить дерево файлов; +2. Закоммитить новый образ; +3. Отдать образ в постановку (**§5.3**). + +Старый образ не трогается до переключения (Свойство атомарности). Здесь работают декодеры и резолвер, и здесь пишется БД пакетов. + +Общий конвейер операции: + +1. Сформировать новое дерево файлов из текущего; +2. Закоммитить дерево файлов новым образом D' в репозиторий (`objects/` + `images/D'`), зашить БД пакетов внутрь образа; +3. Передать D' в постановку образа (См. **§5.3**); +4. Лёгкая очистка старых неиспользуемых образов (См. **§5.5**). + +### **§5.5** Сборка мусора. + +Имеет два уровня: деплои (что держим) и объекты (что вымести). Политику удержания задаёт пользователь. Движок object-sweep — composefs. + +**Незыблемые пины** (никогда не удаляются): + +- Активный (загруженный) деплой; +- Цель отката (постоянный или предыдущий деплой); +- Поставленный, но не подтверждённый деплой (разовый запуск). + +Плюс пользовательские ручные пины (закреплённые вручную деплои) и последние N в пределах заданной пользователем глубины. + +**Триггеры запуска очистки:** + +1. **Лёгкий очистка деплоев внутренней стадией операции** после каждой изменяющей файловую систему операции: снять реф образа и удалить `state/deploy//` для деплоев за пределами политики сохранения. Колличество операций записи на диск мало, потому операция дёшевая, пины держат нужное; +2. **Тяжёлыая очистка объектов запускается только вручную**, командой `upac package gc`: программа пройдётеся по каталогу `objects/` и `streams/` и выместит недостижимые, то есть объекты, на которые никто не ссылается через механизм composefs `ObjectCollector`. From eeeaf90420356e21804bdffabc32294218408092 Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 08:44:07 +0400 Subject: [PATCH 17/68] fix: Added missing definitions fix: Updated inaccurate definitions --- doc/rus/Upac - chapter 0.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/doc/rus/Upac - chapter 0.md b/doc/rus/Upac - chapter 0.md index 6e6419be..785bf9a1 100644 --- a/doc/rus/Upac - chapter 0.md +++ b/doc/rus/Upac - chapter 0.md @@ -19,13 +19,16 @@ SPDX-License-Identifier: CC-BY-SA-4.0 - **Расширение файла (file extension)** — это суффикс в конце имени файла, отделенный точкой, который указывает операционной системе и программам на формат данных, содержащихся в файле, и определяет, с помощью какого приложения его следует открывать или обрабатывать; - **Каталог (папка)** — контейнер для файлов и других каталогов; - **Путь** — это адрес файла или каталога, который указывает его точное расположение в файловой системе. -- **Файловая система** — способ, которым данные разложены на диске так, чтобы ОС видела их как файлы и папки; +- **Файловая система (ФС)** — это порядок и правила, по которым операционная система организует, записывает, находит и хранит файлы на диске или флешке, например, EXT4, Btrfs, NTFS. +- Реф (референс / ссылка) — это точный адрес или указатель в файловой системе, который ссылается на конкретный файл, состояние или срез данных, позволяя обращаться к ним без дублирования; - **Диск / блочный девайс** — устройство хранения (физическое или виртуальное); - **Раздел (partition)** — выделенная часть диска, на которой живёт одна ФС; - **MBR (master Boot Record)** — это устаревший формат структуры загрузочного сектора, применяемый с 1983 года, который хранит первичный код загрузчика и таблицу разделов, что огранивает накопитель максимум 4 основными разделами и объёмом до 2 ТБ; - **GPT (guid partition Table)** — это стандарт таблицы разделов накопителя, который позволяет создавать много разделов и работать с дисками объёмом больше 2 терабайт; - **Монтирование (mount)** — *«подключение»* ФС раздела в точку дерева каталогов, после чего содержимое файловой системы доступно по этому пути; -- **Ядро (kernel)** — главная программа операционной системы, которая выступает посредником между приложениями и железом компьютера, распределяя ресурсы (память, процессор, устройства). +- **Ядро (kernel)** — главная программа операционной системы, которая выступает посредником между приложениями и железом компьютера, распределяя ресурсы (память, процессор, устройства); +- Userspace (пространство пользователя) - это область памяти, где работают все обычные программы и приложения: браузеры, текстовые редакторы, интерфейс системы, изолированная от прямого доступа к области памаяти для управления аппаратным обеспечением ради безопасности всей системы; +- Kernelspace (пространство ядра) - это защищённая область памяти, в которой работает ядро оперативной системы и драйверы; имеет полный и прямой доступ к процессору, оперативной памяти и всему оборудованию компьютера; - **Initramfs** — это временная файловая система в оперативной памяти (RAM), которая содержит нужные драйверы и программы, чтобы ядро могло найти, расшифровать или подготовить основной диск и запустить с него главную систему; - **Unified Kernel Image (uki)** — это единый исполняемый файл формата EFI, который объединяет в один неразрывный модуль ядро Linux, образ инициализации (initramfs), параметров командной строки ядра и EFI-загрузчик - **Прошивка (firmware)** — это базовое программное обеспечение, вшитое в энергонезависимую память микросхемы на материнской плате или устройстве: видеокарте, SSD, контроллере, служащая главным мостом между физическим железом и операционной системой, инициализируя компоненты при включении и предоставляя низкоуровневые инструкции для управления ими; @@ -42,9 +45,21 @@ SPDX-License-Identifier: CC-BY-SA-4.0 - **Цифровая подпись** — это зашифрованная метка-штамп от разработчика, прикреплённая к пакету: гарантирует авторство, то есть факт принадлежность текущего кода конретному лицу, и целостность, то есть отсуствие изменений программы после её создания разработчиком; - **Пакетный менеджер** — это программа, которая автоматически скачивает, устанавливает, обновляет и удаляет программы из репозитория, а также сама находит и ставить все необходимые для их работы программы (зависимости); - **Атомарность** — свойство неделимости: операция либо применяется целиком, либо не применяется вовсе, без промежуточных полуразобранных состояний; -- **Immutable (неизменяемая) система** — это архитектура операционной системы, в которой системные файлы защищены от изменений во время работы, а любые обновления применяются атомарно и ставясь рядом, позволяя в любой момент мгновенно откатиться к прежнему рабочему состоянию. +- **Immutable (неизменяемая) система** — это архитектура операционной системы, в которой системные файлы защищены от изменений во время работы, а любые обновления применяются атомарно и ставясь рядом, позволяя в любой момент мгновенно откатиться к прежнему рабочему состоянию; +- Персистентный (или персистентность) — это свойство данных или настроек сохраняться даже после выключения компьютера, перезагрузки системы или закрытия программы; - **POSIX (portable operating system interface)** — это семейство стандартов IEEE и ISO, определяющее единый программный интерфейс (API), системные утилиты и поведение командной строки для Unix-подобных операционных систем; - **FHS (filesystem hierarchy standard)** — это стандарт, который определяет структуру, название и назначение основных каталогов в Unix-подобных операционных системах; +- Скрипт (сценарий) — это небольшая программа или последовательность команд, чаще всего реализованный в виде текстового файла с инструкциями, который система или другая программа выполняет пошагово для автоматизации повседневных задач; +- Hook (хук / перехватчик) - это автоматический обработчик, который запускается в определенный момент работы программы, например, до или после установки пакета, чтобы выполнить заданную команду или скрипт; +- Триггер (переключатель / запуск) — это автоматический условие-сигнал, который при наступлении определенного события сразу запускает заранее прописанное действие или программу; +- БД (база данных / database) — это организованное хранилище информации на диске, из которого программы могут быстро находить, добавлять, изменять и безопасно сохранять нужные данные; +- Коммит (commit / фиксация) - это сохранённая точка или *«снимок»* состояния в системе контроля версий, например, Git, который фиксирует все изменения в файлах на данный момент и позволяет при необходимости к ним вернуться; +- Деплой (deployment / развёртывание) — это процесс перенесения, настройки и запуска готовой программы, образа системы, дерева файлов на реальный рабочий сервер или устройство, где развернутое содержимое становится доступным для использования; +- CLI (command line interface / интерфейс командной строки) - это текстовый способ управления программой, когда пользователь вводит команды в терминале с клавиатуры вместо нажатия на кнопки мышкой. +- Pivot (pivot root / сменяемый корень) - это операция переключения работающей системы с временного начального диска, например, initramfs при загрузке, на основной реальный раздел диска, который становится новым корнем `/`; +- PID 1 (process ID 1 / главный процесс системы) - это первый и самый главный процесс, который запускается ядром Linux при включении компьютера, например, systemd, отвечающий за запуск всех остальных программ и управляет всей работой системы до её выключения; +- Юнит (unit / единица оборудования или службы) — это базовый элемент управления в системном менеджере, например, systemd, представляющий собой текстовый файл с настройками для запуска и контроля конкретной службы, задачи, устройства или точки монтирования; +- Движок (engine) — это базовая программа или подсистема, которая выполняет всю сложную внутреннюю работу: расчёты, логику, обработку данных, чтобы упростить разработку и не создавать эти функции с нуля для каждого нового приложения. ### **Пункт 2.** Определение используемых системой папок. From 24d7cd67fa66c2be28881b4cb90843fe1377c0aa Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 09:07:59 +0400 Subject: [PATCH 18/68] fix: Moved chapter 6. --- doc/rus/Upac - chapter 6.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 doc/rus/Upac - chapter 6.md diff --git a/doc/rus/Upac - chapter 6.md b/doc/rus/Upac - chapter 6.md new file mode 100644 index 00000000..d9e1c0d8 --- /dev/null +++ b/doc/rus/Upac - chapter 6.md @@ -0,0 +1,36 @@ + + +## **§6.** FFI и границы взаимодействия составных частей. + +Upac-lib (`lib`) — вся основаная логика работы программы и стабильный C-ABI. Декодеры и загрузочные плагины, грузятся именно из её управления. +Upac-cli и GUI программы на основе библиотеки (в будущем) — тонкие обёртки: только итропретируют ввод и выводят события. + +``` Схема потоков управления во время выполнения программы. +CLI ─┐ +GUI ─┼──(C-ABI)──▶ lib ──(dlopen)──▶ decoders/* (разбор форматов + резолв) +… ─┘ │ + └──(вызовы)──▶ composefs (repo / image / mount / boot) + +lib ──(хуки)──▶ CLI/GUI (прогресс, события, конфликты /etc) +``` + +### Описание направлений схемы: + +- **Внешний вызов → lib:** команда с аргументами (что делать) + токен отмены; +- **Lib → decoders:** путь к пакету; обратно — файлы, метаданные, зависимости; +- **Lib → composefs:** примитивы, к примеру commit образа, mount, prune (очистка), запись загрузочных записей; +- **Lib → внешний вызов (хуки):** прогресс операции, события подтверждения, конфликты `.upac-new`. + +**Правила границы:** +1. *«Трогает состояние / нужен полный доступ к системным файлам / должно быть атомарно»* → `lib`; +2. *«Показывает или собирает ввод»* → внешний вызов, внешнее управление. CLI и GUI — равноправные тонкие обёртки над одной библотекой. + +**Два публичных контракта у `lib`.** + +Помимо стабильного C-ABI (Реализованного через `export` и меназим ОС `dlopen`), Rust-слой сам по себе тоже публичен для прямой статической линковки: `orchestrator`, `scripts`, `plugin`, `composefs`, `database`, `deploy`, `errors`, `lock`. + +Приватными остаются `export`, поскольку сам C-ABI не нужно дёргать при статической линковке, а так же внутрении механизмы реализации логики внешних типов и функций, к примеру `Cursor` внутри `orchestrator`. Внутри `composefs` дополнительно урезаются для внешнего экспорта механизмы: `repository::open`/`repository::open_tree`, поскольку они не часть публичного контракта — единственная точка получения открытого репозитория/дерева файлов с внешней стороны API — модуль `deploy::Deploy`, для того, чтобы репозиторий нельзя было открыть в обход поднятого sysroot, приведя систему не в консистентное состояние. From baac28e968ec2d5ad8e20451c38818b6a50a0a12 Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 09:09:01 +0400 Subject: [PATCH 19/68] fix: Added new definitions --- doc/rus/Upac - chapter 0.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/rus/Upac - chapter 0.md b/doc/rus/Upac - chapter 0.md index 785bf9a1..2e9b3b4e 100644 --- a/doc/rus/Upac - chapter 0.md +++ b/doc/rus/Upac - chapter 0.md @@ -88,4 +88,11 @@ SPDX-License-Identifier: CC-BY-SA-4.0 - **`seq` (sequence / порядковый номер)** — это строго увеличивающийся счетчик, присваиваемый каждому новому деплою, который определяет точную хронологию версий системы и служит гарантированным ориентиром при автоматическом переключении или откате на предыдущие состояния независимо от системных часов; - **Пин (pin / закрепление)** — это флаг защиты в метаданных деплоя, который блокирует удаление конкретной версии системы при автоматической очистке (сборке мусора), гарантируя сохранение текущей рабочей версии, базовой точки отката или явно отмеченных пользователем состояний; - **Откат (rollback)** — это операция мгновенного возврата к заведомо рабочему состоянию системы, которая может выполняться как целиком: переключением загрузчика на предыдущий атомарный деплой с его версиями /usr и /etc, так и точечно: сбросом изменений в слое конфигураций /etc к оригинальному состоянию; -- **Сборка мусора (garbage Collection / gc)** — это автоматический или ручной процесс очистки хранилища, который находит и удаляет старые слои файловой системы, файлы и объекты CAS, больше не используемые ни одним активным, текущим или закрепленным (pinned) деплоем, освобождая дисковое пространство без риска повредить рабочую систему. +- **Сборка мусора (garbage Collection / gc)** — это автоматический или ручной процесс очистки хранилища, который находит и удаляет старые слои файловой системы, файлы и объекты CAS, больше не используемые ни одним активным, текущим или закрепленным (pinned) деплоем, освобождая дисковое пространство без риска повредить рабочую систему; +- Токен (token / маркер) — это компактный фрагмент данных (строка или число), который служит цифровым пропуском или ключом для подтверждения прав, передачи данных или безопасного доступа к системе; +- Линковка (linking / компоновка) — это этап сборки программы, на котором компоновщик (linker) объединяет скомпилированные объектные файлы и внешние библиотеки в один готовый исполняемый файл или динамическую библиотеку, связывая обращения к функциям и переменным с их реальными адресами; +- API (application programming interface / интерфейс программирования) - это набор правил и функций на уровне исходного кода, к примеру заголовочные файлы, имена функций, параметры, определяющий, как программы взаимодействуют друг с другом при сборке в исполняемый файл; +- ABI (application binary interface / двоичный интерфейс) - это набор правил на уровне машинного кода, к примеру соглашения о вызовах, размер и выравнивание типов в памяти, номера системных вызовов, определяющий, как собранные из исхожного кода бинарники и библиотеки взаимодействуют друг с другом во время выполнения программы; +- FFI (foreign function interface / интерфейс внешних функций) — это механизм, который позволяет программе, написанной на одном языке программирования, напрямую вызывать функции и использовать библиотеки, написанные на другом языке; +- Обёртка (wrapper) — это промежуточный слой кода, который скрывает сложное внутреннее устройство и предоставляет более удобный, безопасный или подходящий под конкретный язык интерфейс; +- CLI-обёртка (cli wrapper) — это программа с интерфейсом командной строки, которая принимает команды и флаги от пользователя в терминале, транслирует их в вызовы функции внутренней библиотеки и возвращает результат обратно в консоль. From 1b5e175cb4469e768afd2e75c8a803092f4c6c83 Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 09:32:33 +0400 Subject: [PATCH 20/68] fix: Fixed file header containing a duplicate year --- doc/rus/Upac - chapter 1.md | 2 +- doc/rus/Upac - chapter 2.md | 2 +- doc/rus/Upac - chapter 3.md | 2 +- doc/rus/Upac - chapter 5.md | 2 +- doc/rus/Upac - chapter 6.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/rus/Upac - chapter 1.md b/doc/rus/Upac - chapter 1.md index 38ad2ed4..d1c9a2f1 100644 --- a/doc/rus/Upac - chapter 1.md +++ b/doc/rus/Upac - chapter 1.md @@ -1,5 +1,5 @@ diff --git a/doc/rus/Upac - chapter 2.md b/doc/rus/Upac - chapter 2.md index 764aea01..8e334e78 100644 --- a/doc/rus/Upac - chapter 2.md +++ b/doc/rus/Upac - chapter 2.md @@ -1,5 +1,5 @@ diff --git a/doc/rus/Upac - chapter 3.md b/doc/rus/Upac - chapter 3.md index cdb8bb01..03f6c474 100644 --- a/doc/rus/Upac - chapter 3.md +++ b/doc/rus/Upac - chapter 3.md @@ -1,5 +1,5 @@ diff --git a/doc/rus/Upac - chapter 5.md b/doc/rus/Upac - chapter 5.md index 5c6f4a89..4d255b43 100644 --- a/doc/rus/Upac - chapter 5.md +++ b/doc/rus/Upac - chapter 5.md @@ -1,5 +1,5 @@ diff --git a/doc/rus/Upac - chapter 6.md b/doc/rus/Upac - chapter 6.md index d9e1c0d8..74a2d079 100644 --- a/doc/rus/Upac - chapter 6.md +++ b/doc/rus/Upac - chapter 6.md @@ -1,5 +1,5 @@ From c02ed72eb761d3b4b9f6e01a0bd22a5793a6c6e6 Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 09:33:16 +0400 Subject: [PATCH 21/68] fix: Fixed file header containing a duplicate year fix: Added new definitions fix: Removed unnecessary duplicates --- doc/rus/Upac - chapter 0.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/doc/rus/Upac - chapter 0.md b/doc/rus/Upac - chapter 0.md index 2e9b3b4e..cc17fe82 100644 --- a/doc/rus/Upac - chapter 0.md +++ b/doc/rus/Upac - chapter 0.md @@ -1,5 +1,5 @@ @@ -75,7 +75,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0 - **Контент-адресное хранилище (cas)** — это метод хранения данных, где адрес файла определяется хешем его содержимого, что обеспечивает автоматическую дедупликацию одинаковых файлов, гарантирует защищенность от подмены и позволяет моментально проверять целостность данных; - **Образ (image)** — это неизменяемый (ro) снимок файловой системы в конкретной версии, который разворачивается как единое целое и гарантирует одинаковое состояние ОС на любых устройствах; -- **Деплой (deployment)** — это конкретная развёрнутая версия системы на диске, состоящая из первичного неизменяемого слоя операционной системы в виде исполняемых файлов (`/usr`) и связанного с ним слоя конфигураций (`/etc`), где точка загрузки выбирается по хешу системного образа, а нужная версия настроек подтягивается автоматически из его метаданных; +- Дистрибутив (distribution / distro / дистр) — это готовая к использованию операционная система, собираемая на базе ядра с добавлением системного окружения, утилит, системных сервисов, базовых программ и менеджера пакетов; - **Ref (ссылка)** — это человекочитаемое имя, которое указывает на конкретный хеш образа и обновляется при выходе новых версий образа системы; - **OverlayFS (lower / upper)** — это виртуальная файловая система, которая объединяет слой только для чтения (lower, базовый образ) и записываемый слой (upper, изменения), создавая для пользователя единую папку, где системные файлы остаются неприкосновенными, а любые правки сохраняются отдельно; - **fs-verity** — это встроенный в ядро Linux механизм защиты целостности файлов, который делает файл неизменяемым (ro) и при каждом чтении проверяет его блоки через дерево Меркла, мгновенно блокируя доступ при малейшем повреждении или подмене данных; @@ -95,4 +95,9 @@ SPDX-License-Identifier: CC-BY-SA-4.0 - ABI (application binary interface / двоичный интерфейс) - это набор правил на уровне машинного кода, к примеру соглашения о вызовах, размер и выравнивание типов в памяти, номера системных вызовов, определяющий, как собранные из исхожного кода бинарники и библиотеки взаимодействуют друг с другом во время выполнения программы; - FFI (foreign function interface / интерфейс внешних функций) — это механизм, который позволяет программе, написанной на одном языке программирования, напрямую вызывать функции и использовать библиотеки, написанные на другом языке; - Обёртка (wrapper) — это промежуточный слой кода, который скрывает сложное внутреннее устройство и предоставляет более удобный, безопасный или подходящий под конкретный язык интерфейс; -- CLI-обёртка (cli wrapper) — это программа с интерфейсом командной строки, которая принимает команды и флаги от пользователя в терминале, транслирует их в вызовы функции внутренней библиотеки и возвращает результат обратно в консоль. +- CLI-обёртка (cli wrapper) — это программа с интерфейсом командной строки, которая принимает команды и флаги от пользователя в терминале, транслирует их в вызовы функции внутренней библиотеки и возвращает результат обратно в консоль; +- Маппинг (mapping / сопоставление) — это процесс связывания или преобразования данных из одной структуры, формата или адресного пространства в другое по заданным правилам; +- Comptime (compile-time / время компиляции) - это этап, на котором исходный код программы проверяется, анализируется и преобразуется компилятором в машинный код. Все вычисления, проверки типов и макросы, выполняемые на этом этапе, происходят до запуска программы и не нагружают конечную программу; +- Runtime (время выполнения / среда выполнения) - это этап, когда скомпилированная/собранная программа непосредственно выполняется процессором в операционной системе; +- Race condition (состояние гонки) — это ошибка проектирования многопоточных или параллельных систем, при которой результат выполнения программы зависит от неконтролируемого порядка или времени выполнения сторонних процессов или потоков; +- TOCTOU (time-of-Check to time-of-Use) — это уязвимость состояния гонки (race condition), возникающая в системе, когда состояние ресурса, например, файла или прав, проверяется в один момент времени, но изменяется сторонним процессом до того, как система успевает его использовать. From 22ef966e40e9faf9adebb089087342484305b01e Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 09:34:02 +0400 Subject: [PATCH 22/68] fix: Chapter 7 moved --- doc/rus/Upac - chapter 7.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 doc/rus/Upac - chapter 7.md diff --git a/doc/rus/Upac - chapter 7.md b/doc/rus/Upac - chapter 7.md new file mode 100644 index 00000000..db53a436 --- /dev/null +++ b/doc/rus/Upac - chapter 7.md @@ -0,0 +1,37 @@ + + +## **§7.** Модули программы. + +***ВНИМАНИЕ:*** Ориентир для проектирования, уточняется по ходу разработки! + +**`lib/`** — ядро программы и FFI (реальная раскладка модулей, поддерживается в актуальном виде по ходу разработки): + +- `export` — C-ABI: точки входа всех команд, версия ABI, отмена, освобождение ответов; +- `orchestrator` — общий движок (См. **§5.9**–**§5.11**): `Stage`/`ConcurrentStage`, `Cursor`, `RollbackGuard`, и два оркестратора за одним трейтом `Orchestrator` — `SequentialOrchestrator` (линейный, держит системный лок файл) и `ParallelOrchestrator` (параллельные стадии, используется для хуков в **§5.8**); +- `mutated` / `unmutated` — тела команд, по подмодулю на каждую. Собираются из своего pipeline каждой стадии через `orchestrator`; +- `scripts` — пункт **§5.8**: TOML-формат хук-файла (`HookFile`), примитивы (`Primitive`/`TouchFile`/`MoveFile`/`CreateSymlink`, каждый `impl Step { execute, rollback }`), матчинг нативных триггеров (`Operation`/`Timing`). `HookStage::run()` полностью подключён для нативных триггеров: get-or-build общего `tokio`-рантайма через `Context`, далее проверка подписи и парсинг хук-файлов (`load_hooks`, через `upac-pki`), фильтрация по `NativeTrigger`, параллельный запуск совпавших хуков через `ParallelOrchestrator` (`HookFile` сам `impl ConcurrentStage`, исполняет свои `steps` и учитывает `critical`). Вписан в pipeline всех mutated команд (Pre/Post обработка хуков каждой); +- `plugin` — загрузка декодеров. В данный момент реализован только подмодуль `decoder` (`dlopen`, проверка версии ABI, `decode`/`match_triggers`) — родительский каталог `plugin` зарезервирован под другие виды плагинов на будущее, пока таковых нет. Там же `manifest` (`DecoderManifest`, `load_decoder_manifests()` — читает декларативные файлы для описания декодерв в каталоге `/etc/upac.d/decoders/*.toml`, без сканирования и/или проверки `.so`) и `triggers` (`build_trigger_table()` — строит таблицу native-триггер→хук под конкретный декодер из загруженных `HookFile`, разрешая конфликты `priority` жёсткой ошибкой операции). Пока никуда не подключено — нужна реальная точка вызова, завязанная на ещё не написанные тела стадий каждой команды; +- `composefs` — доступ к composefs-репозиторию. `Repository`: `open(path) -> Repository` (открытие по пути через `Repository::open_path`), `open_tree(repository, name) -> FileSystem` (читает образ через `Repository::open_image` + `erofs::reader::erofs_to_filesystem`) — обе доступны только внутри библиотеки, наружу отдаётся только `deploy::Deploy`. `error::RepoError` — маппинг `RepositoryOpenError`/`ImageError`/`anyhow::Error` (последнее нужно, потому что `ensure_object`/`ensure_object_from_file`/`commit_image` и т.п. в самом composefs возвращают `anyhow::Result` — деталей ошибки оттуда не достать, только факт неудачи). `file::FileHandle` — держатель/указатель на путь в дереве, три `impl`-блока по логике "трогает CAS или нет": конструкторы (`new` — слепой, для вставки нового; `from_tree` — с проверкой, что путь уже существует), дерево без CAS (`insert_in_tree`/`update_in_tree`/`rename_in_tree`/`remove_in_tree`/`symlink_in_tree`/`hardlink_in_tree`/`stat_in_tree`/`symlink_target_in_tree`/`list_in_tree`), файл через CAS (`insert_file` берёт уже открытый `&File` — не байты, чтобы путь резолвился ровно один раз и не было TOCTOU-гонки на подмену файла между чтением и вставкой в CAS; `replace_file` — алиас на `insert_file`, т.к. `Directory::insert` в composefs уже сам upsert-ит; `read_file` — резолвит inline/external и тянет байты из `Repository::read_object` при необходимости); +- `deploy` — постановка деплоя (См. **§5.3**): находит блочное устройство под `/` через `MountInfo`, реальный тип ФС — через `rsblkid::probe::Probe` (не `None`, иначе `mount(2)` падает `EINVAL` — тип ФС ядру нужен явно для любого монтирования, кроме bind/remount), `unshare(CLONE_NEWNS)` + обязательный `MS_REC | MS_PRIVATE`-remount `/` перед реальным монтированием (без этого шага mount-события всё равно утекают в хостовую таблицу через shared propagation, унаследованную от родительского namespace), монтирует раздел в `/sysroot`. `deploy(usr_digest) -> PathBuf` — один компонент пути (`state/deploy//`, см. **§3**). `open_repository()` / `open_tree(name)` — единственная публичная точка доступа к composefs-репозиторию текущего деплоя (См. выше); +- `database` — БД пакетов (реализация через redb) внутри образа. При сборке пишется через свой in-memory `StorageBackend`, в runtime читается `ReadOnlyDatabase` с файла в образе. Там же `record` — `DeployRecord`/`EtcHistoryEntry` (Поля см. **§3** п.12: `usr_digest`/`subject`/`message`/`seq`/`timestamp`/`etc_history`/`working_etc`), физически не часть redb-БД (отдельный `meta.json` на sysroot, не внутри образа), но живёт здесь же по смыслу — *"как наши типы персистятся"* общая забота `database`, независимо от формата. Сериализация — `#[derive(JsonCodec)]` (по образцу `RedbCodec`, тот же по-полевой codegen, только в `serde_json::Value` вместо байт-layout'а); `DeployRecord::write`/`read` пишут/читают файл, `write` — атомарно (tmp-файл в той же директории + `fsync` + `rename`). Своя ошибка `error::DeployRecordError` (отдельно от `DatabaseError` — разные форматы хранения); +- `types` — доменные типы (`Version`, `PackageMeta`, `Dependency`, `Targets`...) и per-commands `StateId`-enums (`states`); +- `errors` / `lock` — вынесены из `types` в свои топ-уровневые публичные модули: `CommonError` (обёртка над `HookError`/`DecoderError`/`RepoError`/`DatabaseError`/`SysrootError`/`LockError`/`DeployRecordError` — все они теперь тоже публичны, каждый под своим модулем выше) и `Lock`/`LockError` (эксклюзивный системный лок файл, смю **§5.9**); + +***Ещё не начато:*** `etc_merge` (3-way слияние `/etc`, §5.1), `boot` (загрузочной записи, разовый вход/подтверждение/откат, см. **§5.2**; берёт `composefs-boot`, grub через `blscfg` — отдельных плагинов нет, только BLS-совместимые загрузчики by desing), `gc` (политика удержания и очистки, см. **§5.5**), и само построение графа зависимостей пакетов на стороне `lib` (decoder сейчас только отдаёт сырой список зависимостей пакета через `decode` — граф ещё никто не обходит, да и сетевого слоя для скачивания пакетов тоже нет). + +Конфиг времени сборки: имена таблиц, пути деплоя, адрес лока — это `lib.toml` + `build.rs`, генерирующие простые константы, а не отдельный крейт `derive-static` — эта идея заменена. Возможно замена в будущем. + +**`cli/`** — тонкая обёртка над библиотекой: + +- `args` — разбор аргументов; +- `commands` — по модулю на команду; +- `render` — рендер прогресса, событий и конфликтов из хуков; +- `ffi` — привязка к C-ABI ядра. + +Возможны изменения в следвии дальнейшей адапации посте стабилизации кода библиотеки. + +**`decoders/`** — плагины (По одному на формат упаковки и сжатия пакета: alpm / deb / rpm / xbps и т.д. Загружает и вызывает их `lib` (модуль `decoder`, внутри родительской папки `plugin`), **НЕ** CLI или другой внешний код. Какой плагин загружать под какой формат — решается по декларативному манифесту (`/etc/upac.d/decoders/*.toml`, см. **§5.8**). По умолчанию — динамические `.so`, опционально мейнтейнеры дистрибутива собирают статикой линковкой. From 4a6d8e66c5f4fc95922fb2f65af432531195d60f Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 09:38:52 +0400 Subject: [PATCH 23/68] fix: File corrections due to changes in the repository --- REUSE.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/REUSE.toml b/REUSE.toml index 053d98ec..7d5a4f9d 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -10,6 +10,16 @@ path = ".github/**" SPDX-FileCopyrightText = "JustPav" SPDX-License-Identifier = "GPL-3.0-only" +[[annotations]] +path = ".cargo/**" +SPDX-FileCopyrightText = "JustPav" +SPDX-License-Identifier = "CC-BY-SA-4.0" + +[[annotations]] +path = ["**/Cargo.lock"] +SPDX-FileCopyrightText = "JustPav" +SPDX-License-Identifier = "CC-BY-SA-4.0" + [[annotations]] path = "doc/**" SPDX-FileCopyrightText = "JustPav" From 26b78d8be5e2700e10ad0b64eade45eb15839433 Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 19:49:54 +0400 Subject: [PATCH 24/68] fix: Moved missing items --- doc/rus/Upac - chapter 5.md | 117 ++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/doc/rus/Upac - chapter 5.md b/doc/rus/Upac - chapter 5.md index 4d255b43..773133d6 100644 --- a/doc/rus/Upac - chapter 5.md +++ b/doc/rus/Upac - chapter 5.md @@ -114,3 +114,120 @@ SPDX-License-Identifier: CC-BY-SA-4.0 1. **Лёгкий очистка деплоев внутренней стадией операции** после каждой изменяющей файловую систему операции: снять реф образа и удалить `state/deploy//` для деплоев за пределами политики сохранения. Колличество операций записи на диск мало, потому операция дёшевая, пины держат нужное; 2. **Тяжёлыая очистка объектов запускается только вручную**, командой `upac package gc`: программа пройдётеся по каталогу `objects/` и `streams/` и выместит недостижимые, то есть объекты, на которые никто не ссылается через механизм composefs `ObjectCollector`. + +### **§5.6.** Механизм создание и развёртывания OCI (в планах на разработку). + +OCI в параграфе — переносимый формат артефакта-образа, а не сетевой протокол. + +**Направления вхождения образа:** + +- **Импорт** — взять OCI-образ и развернуть его как хост-систему; +- **Экспорт** — произвести переносимый OCI-образ-артефакт из деплоя, создав готовую *«эталонная копия»*. + +**Варианты доставки образа:** + +1. **Зз внешнего репозитория** — используется та же сетевая подсистема, что и для доставки пакетов. Применяется по умолчанию; +2. **Команда из локального файла** — используя `--file <образ>` берётся локальный файл и разворачивается напрямую. + +Для развора образа на несколько машин используются только эти пути доставки. Отдельного механизма под разворачивание на несколько устройвст пока не предусмотрено. Возможна реализация в виде плагина. + +Разворачивание образа основывается на механизме `composefs-oci`: `create_filesystem` (слои → образ), `generate_boot_image`, `pull_image`. + +### **§5.7** Механизм истории развертываний образов и откат на N деплоев назад. + +История развертываний хранится НЕ в образе (что сломало бы контент-адресность), а на разделе в режиме чтения и записи (`rw`). Источник правды — сами каталоги `state/deploy//`, отдельного журнала не предусмотрено. + +**Две оси сохранения истории:** + +- **Каталог `/usr`** - линейная история деплоев. Каждый различный каталог `/usr` = одной записи, ключом в которой является `usr-digest`. `seq` — порядок рождения записей: монотонный, один на digest. Имеет встроенную метку для понимания последующего за ним элемента массива в виде `state/next-seq`. Повторное прибытие к существующему `usr-digest` **переключает** на его запись в отметке текущего, а не заводит дубль — так под-история `/etc` этого `/usr` при возврате к старому `/usr` цела. Запись несёт свое **сообщение к коммиту**: `subject`: короткий, обязательный, опциональный длинный `message` — коммит-сообщение операции, породившей этот `/usr`; +- **Каталог `/etc`** — под-история внутри `/usr`. `meta.json` записи несёт `etc_history` — упорядоченный список записей типа: `{etc_digest, subject, message}`, созданных под этим `/usr`. При смене `/usr` и по команде `upac commit`, см. **§5.1**. Каждая запись несёт свои `subject` и опциональный `message`. Первая запись создаётся автоматически при смене `/usr` (См. **§5.1**), наследуя subject и message самого `/usr`-события — последующие явные `upac commit` получают собственные, независимые subject и message. + +**Активный деплой** - это загруженный `composefs.digest` и загрузочный дефолт. **НЕ** `max(seq)`: после переключения на старую запись её `seq` остаётся прежним, неизменным. + +**Варианты отката:** + +- **Аварийный (`/usr`)** — на каждый N-й существующий деплой в порядке `seq` (считается по факту наличия, **не** арифметикой `seq−N`. Допускается сгоревшие номера, их мы просто пропускаем. Восстанавливается **пара**: целевой `usr-digest` + его `working_etc` (последний подтверждённый коммит подпунта) — правки конфига благодаря этому не теряются полностью; +- **Конфига (`/etc`)** — `upac commit rollback --etc` на прошлый `etc-digest` из `etc_history` текущего `/usr`. Работает та же ранговая механика и своя глубина удержания. + +Авторитетом и источником правды для работы механизма откатов является **`seq`**. Временные метки в виде **`timestamp`** в `meta.json` — только для отображения (к примеру `upac commit history`), поскольку дрейф или перевод часов могли бы с лёгкостью сломать историю и сделать откат системсы невозможной. + +Связь с работой механизма очистки (См. **§5.5**): глубина удержания ссылки на образ по каждой оси (конифги или системные файлы) обязана быть **включительной** для чистла N. + +### **§5.8** Механизм хуков и определения нахождения декодеров (работа pre/post-триггеров). + +**Хук** — декларативный **подписанный** файл: описывает сам триггер, приоритет хука над остальными таким же триггерами и композицию из **примитивов**; +**Примитив** — закрытый набор низкоуровневых действий, зашитых в внутри `lib`. К примеру запуск процесса, touch (создание) или move (перемещение) файла и т.п. — единственное, что требует правки кода `lib` для добовления каких-либо новых примитивов; +**Подпись** — защита от произвольного неподписанного хук-файла, поскольку примитивы достаточно привилегированные в системе прав, чтобы не доверять файлу без подписи. + +**Таблица соответствий.** + +Хук-файл отдельно несёт таблицу: для декодера `D` (см. **§6** — плагин формата пакета: deb, rpm, и т.д.) этот хук покрывает его НАТИВНОЕ для родного формата пакета имя триггера (к примеру, у deb есть тригер `update-mime-database`, который будет переводиться декодером в универсальный формат). Совместимость с чужими trigger-конвенциями так же описывается в файле, в то время как декодеру просто передают готовую таблицу соотвествий, из которых он определяет те, которые необходимо исполнить и те, которые не удовлетворяются, передавая всё в вызывающуую сторону. + +**Приоритет хук-файла.** + +**Приоритет** — обычное знаковое целое (дефолт 0), используемый ТОЛЬКО чтобы разрешать конфликт, когда несколько разных хук-файлов заявляют одно и то же нативное имя триггера (один и тот же ключ `k` в таблице соответствий). В конфликте побеждает больший `priority`. однако если находится равенство — это неразрешимый конфликт, и `lib` сразу возращает критическую ошибку и отменяет операцию. Автоматический выбор не предусмотрен by desing. Несопоставившиеся записи (нативного триггера хука просто нет в конкретном пакете) вообще не нуждаются в отдельном репортинге — это нормальный, ожидаемый исход для большинства хуков на большинстве пакетов, а не ошибка. Никакой очерёдности исполнения `priority` не задаёт, поскольку все нужные к исполнению хуки триггера выполняются конкурентно (параллельно). + +Стоит ли всё же прокидывать *нефатальное* предупреждение через `MessageHook` для какого-то из этих случаев (конфликт, или хук, который структурно никогда ни с чем не сматчится, иные случае, которые возникнут в дальнейшем) — пока открытый вопрос. + +**Критичность хук-файла.** + +**Критичность** — поле (`critical = true/false`). Отвечает за отметку критических для операций хук-файлов, при провале выполнения которых происходит провал и отмена всей операции. + +**Разделение труда:** + +- **`lib`** — единственная сторона, что читает хук-файлы с диска, проверяет подпись, разбирает композицию примитивов и таблицу соответствий. Исполняет композицию через свои примитивы; +- **Декодер (плагин)** — получает от `lib` таблицу соответствий уже как готовую карту формата key:value **под свой `D`** (то есть формат пакета deb не получает чужих записей тригеров, например под rpm формат), где key — нативное имя триггера decoder'а, value — наше имемя хука. Декодер сам сопоставляет её с нативными именами триггеров, которые вычитал из пакета (например, deb-декодер сам читает `Triggers-Interest` пакета), и через FFI отдаёт `lib` обратно список хуков на исполнение (необходимые value; + +**Разрешение декодера.** + +Декодеры находятся через декларативные подписанные TOML-манифесты в `/etc/upac.d/decoders/` (на один декодер приходится **ровно** один `format`, `extensions`, `library`): `format` — каноничная идентичность формата пакета, та же строка, что и ключ `D` в таблице соответствий хука, `extensions` перечисляет файловые варианты, в которых этот формат реально поставляется (например, alpm-пакеты имеют расширение файла `pkg.tar`/`pkg.tar.gz`/`pkg.tar.xz`/`pkg.tar.zst`), `library` называет имя `.so`, который нужно открыть. Само открытие библиотекой плагина происходит лениво, то есть **только** и **только тогда**, когда файл **требуется** для исполнения (распаковки формата). При дублирующимся `format` у двух манифестов возникает жёсткая ошибка на этапе загрузки манифестов, та же логика, что и у равенства priority выше. + +**Формат и подпись хук-файла.** + +Хук-файл — TOML, лежит в `/etc/upac.d/hooks/` (путь зашивается константой из файла `lib.toml` на этапе сборки библиотеки). Подпись строиться по цепочке из 2 уровней доверия: root CA (офлайн-ключ, подписывает только следующий уровень), далее signing-сертификат на trust-domain, который и подписывает байты хук-файла напрямую, файл же `.sig` несёт и подпись, и сам signing-сертификат целиком — проверка самодостаточна. + +**Root — конфигурируемый файл**, не зашит в собранную версию программы: дистр/OEM или пользователь подключает свой root без пересборки `upac`. + +**Схема подписи** — алгоритм шифрования Ed25519 поверх X.509-сертификатов. + +**Модель исполнения.** + +Запуск хуков — асинхронный, но целиком внутри `lib`: FFI остаётся полностью синхронным. Область применения — только конкурентный запуск N независимых хуков **внутри одной стадии исполнения команды. + +### **§5.9** Механизм отмена провалившейся операции. + +Работает через механизм `CancelToken` (токен отмены), работающий по принципу атомарного флага, который создаётся вызывающей стороной (CLI/GUI) и передаётся в `lib` указателем через каждый запрос. + +**`Lock`** — механизм взаимного исключения между rw-операциями, работающий на основе bind на abstract Unix-адрес, строго известный и фиксированный в `lib.toml`, общий для всех rw-вызовов). + +### **§5.10** Механизм передачи прогресса операции. + +Используется тот же канал доставки сообщений, что и пункте **§5.9**. Содержит: +- `stage` - порядковый номер стадии в формате `u16`; +- `phase` - какой под-шаг внутри стадии конкретной стадии в формате `u16`; +- `subject` - строка обекта, определяющая **над** чем сейчас ведётся работа (файл, хук, или иной объект); +- `current`/`total` - счётчик элементов. `0`, если не применимо/неизвестно. + +Механизм `MessageHook::send` принимает один самоописывающий параметр вместо прежних раздельных event/data — ничего отдельно распаковывать не нужно. + +### **§5.11** Механизм оркестровки стадий. + +Механизм, которым команды на самом деле исполняются: линейный список стадий плюс движок, который через них проходит, — сюда же подключаются оба хук-канала из пунктов **§5.9** и **§5.10**. + +**Стадия — всегда плоская структура.** + +Одна стадия делает ровно одну атомарную единицу работы за вызов. Каждый вызов сама решает, что дальше — вперёд к следующей стадии, повторить саму себя (например, обработать ещё один файл из уже начатого списка), или прыгнуть НАЗАД к более ранней стадии по её ТИПУ (не по числовому индексу для отсуствия поломок при измненеи списка стадий). Через такой прыжок назад группа из нескольких стадий (например, "проверить пакет → распаковать → зарегистрировать") может повториться как единое целое — движок при прыжке ищет ближайшую подходящую по типу стадию, идя назад от текущей позиции. Если такой стадии нет — это баг сборки pipeline, а не пользовательский ввод, и он возвращается как обычная ошибка, прерывая операцию. + +**Каждый вызов стадии сам приносит свой откат.** + +Стадия не копит состояние между вызовами — при каждом вызове она создаёт свой, самостоятельный объект отката изменений на диске, несущий ровно те данные, что нужны, чтобы отменить именно то, что сделал ЭТОТ вызов (даже если та же стадия вызывалась перед этим много раз с другими данными — каждый вызов остаётся независимым). Если стадии нечего откатывать в этот раз — она всё равно обязана вернуть такой объект, просто в *"пустом"* режиме: это гарантируется на уровне компилятора (конструктор *"пустого"* отката). + +**Движок** держит линейный список стадий. Эксклюзивность работы для разницы rw и ro команд достигается выбором МЕТОДА запуска: в режиме rw он держит системный лок файл на всё время работы, в то время как другой вовсе его не создаёт. При любом отказе (ошибка стадии, отмена, ненайденный прыжок назад) разворачивает все накопленные к этому моменту объекты отката, в обратном порядке, не останавливаясь, если один из них сам не смог откатиться — пропуск, поскольку механизм предполагает по возможности откатываеть всё. Каждый успешный вызов стадии отдаёт движку сразу две отдельные вещи — конструктор прогресса (см. **§5.10**, который движок сам же создал и передал стадии до вызова) и свой объект отката. + +**Ошибки работы движка:** +- Pipeline не смог даже начаться (например, лок файл говорит о работе другой rw операции); +- Конкретная стадия по счёту N упала — команда, вызвавшая движок, различает их, потому что для первого случая никакого номера стадии просто не существует. + +**Валидация пайплайна, до первого вызова.** + +Каждая стадия может декларировать, что она `requires` (требует) из общего контекста и что `provides` (возращает) в него (по типу). Перед запуском движок один раз проходит весь список и проверяет, что требования каждой стадии удовлетворены тем, что уже лежит в контексте операции, плюс тем, что предоставили более ранние стадии — недостающая зависимость падает сразу, до реального запуска любой стадии, а не всплывает где-то глубоко внутри более поздней стадии. Проверка единообразна для всех команд (учитывается так же и exclusive-, и concurrent-путь запуска). From baec92fc4646fcd8696da46abd005bce32140553 Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 19:50:23 +0400 Subject: [PATCH 25/68] fix: Added missing definitions --- doc/rus/Upac - chapter 0.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/doc/rus/Upac - chapter 0.md b/doc/rus/Upac - chapter 0.md index cc17fe82..fdbb6eee 100644 --- a/doc/rus/Upac - chapter 0.md +++ b/doc/rus/Upac - chapter 0.md @@ -59,7 +59,12 @@ SPDX-License-Identifier: CC-BY-SA-4.0 - Pivot (pivot root / сменяемый корень) - это операция переключения работающей системы с временного начального диска, например, initramfs при загрузке, на основной реальный раздел диска, который становится новым корнем `/`; - PID 1 (process ID 1 / главный процесс системы) - это первый и самый главный процесс, который запускается ядром Linux при включении компьютера, например, systemd, отвечающий за запуск всех остальных программ и управляет всей работой системы до её выключения; - Юнит (unit / единица оборудования или службы) — это базовый элемент управления в системном менеджере, например, systemd, представляющий собой текстовый файл с настройками для запуска и контроля конкретной службы, задачи, устройства или точки монтирования; -- Движок (engine) — это базовая программа или подсистема, которая выполняет всю сложную внутреннюю работу: расчёты, логику, обработку данных, чтобы упростить разработку и не создавать эти функции с нуля для каждого нового приложения. +- Движок (engine) — это базовая программа или подсистема, которая выполняет всю сложную внутреннюю работу: расчёты, логику, обработку данных, чтобы упростить разработку и не создавать эти функции с нуля для каждого нового приложения; +- OCI (open container initiative) — это открытый отраслевой стандарт для формата образов контейнеров и среды их выполнения (runtime), гарантирующий, что контейнеры будут одинаково запускаться на любых платформах и движках (к примеру Docker, Podman, Kubernetes/CRI-O); +- CA (certificate authority / центр сертификации) - это орган или сервис, который выпускает и подписывает цифровые SSL/TLS-сертификаты. Подпись CA подтверждает, что публичный ключ действительно принадлежит указанному домену, серверу или пользователю; +- Root CA (корневой центр сертификации) - это самый верхний, главный уровень в цепи доверия (Chain of Trust). Root CA владеет самоподписанным корневым сертификатом, который заранее вшит в операционные системы и браузеры как полностью доверенный, подверждая подписанные от его имени остальные сертификаты; +- Открытый ключ (public key / публичный ключ) - это ключ, который доступен публично и используется для шифрования данных или проверки цифровой подписи. Его можно свободно передавать кому угодно; +- Закрытый ключ (private Key / приватный ключ) - это секретный ключ, который известен только владельцу и используется для расшифровки данных или создания цифровой подписи. Потеря или утечка этого ключа компрометирует всю защиту. ### **Пункт 2.** Определение используемых системой папок. @@ -100,4 +105,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0 - Comptime (compile-time / время компиляции) - это этап, на котором исходный код программы проверяется, анализируется и преобразуется компилятором в машинный код. Все вычисления, проверки типов и макросы, выполняемые на этом этапе, происходят до запуска программы и не нагружают конечную программу; - Runtime (время выполнения / среда выполнения) - это этап, когда скомпилированная/собранная программа непосредственно выполняется процессором в операционной системе; - Race condition (состояние гонки) — это ошибка проектирования многопоточных или параллельных систем, при которой результат выполнения программы зависит от неконтролируемого порядка или времени выполнения сторонних процессов или потоков; -- TOCTOU (time-of-Check to time-of-Use) — это уязвимость состояния гонки (race condition), возникающая в системе, когда состояние ресурса, например, файла или прав, проверяется в один момент времени, но изменяется сторонним процессом до того, как система успевает его использовать. +- TOCTOU (time-of-Check to time-of-Use) — это уязвимость состояния гонки (race condition), возникающая в системе, когда состояние ресурса, например, файла или прав, проверяется в один момент времени, но изменяется сторонним процессом до того, как система успевает его использовать; +- OEM (original equipment manufacturer / оригинальный производитель оборудования) — это компания, которая производит детали, компоненты или готовые устройства, продаваемые затем под брендом другой компании либо используемые ею для сборки своей продукции; +- Ed25519 — это современная и высокоскоростная схема цифровой подписи на основе эллиптических кривых (EdDSA), использующая кривую Curve25519; +- X.509 — это общепринятый международный стандарт (ITU-T / RFC 5280) для структуры цифровых сертификатов с открытым ключом (PKI). From 21274030e0dafdf7ef1d99dbfb6a748fbadb1491 Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 19:50:48 +0400 Subject: [PATCH 26/68] fix: Removed old documentation file --- doc/UPAC project note.md | 455 --------------------------------------- 1 file changed, 455 deletions(-) delete mode 100644 doc/UPAC project note.md diff --git a/doc/UPAC project note.md b/doc/UPAC project note.md deleted file mode 100644 index d358f83d..00000000 --- a/doc/UPAC project note.md +++ /dev/null @@ -1,455 +0,0 @@ -# UPAC — единый проектный документ - -Проектный документ. -Ветка проекта: **`lib-rs`**, крейт `lib-rust/`. - ---- - -## 0. Вступление и определения - -Этот параграф объясняет термины, чтобы документ читался даже без опыта в системном программировании. Дальше по тексту слова из него используются без пояснений. - -### 0.1 Базовые понятия - -- **Файл** — именованный кусок данных на диске. -- **Каталог (папка)** — контейнер для файлов и других каталогов. -- **Путь** — адрес файла в дереве каталогов, например `/usr/bin/bash`. -- **Файловая система (ФС)** — способ, которым данные разложены на диске так, чтобы ОС видела их как файлы и папки. -- **Диск / блочный девайс** — устройство хранения (физическое или виртуальное). -- **Раздел (partition)** — выделенная часть диска, на которой живёт одна ФС. -- **GPT** — современная схема разметки диска на разделы. -- **Монтирование (mount)** — «подключение» ФС раздела в точку дерева каталогов; после этого её содержимое доступно по этому пути. -- **Ядро (kernel)** — сердцевина ОС: управляет железом, памятью и процессами. -- **initramfs** — крошечная временная ФС, которую ядро поднимает первой, чтобы подготовить и смонтировать настоящий корень системы. -- **Загрузчик (bootloader)** — программа, которую запускает прошивка (UEFI) и которая, в свою очередь, запускает ядро. -- **cmdline (командная строка ядра)** — набор параметров, передаваемых ядру при старте. -- **Хеш / дайджест** — короткий «отпечаток» содержимого: одинаковые данные дают одинаковый хеш, любое изменение — другой. -- **Пакет** — упакованный набор файлов (программа + её данные + метаданные), устанавливаемый как единое целое. -- **Пакетный менеджер** — программа, которая устанавливает, обновляет и удаляет пакеты. -- **Атомарность** — свойство «всё или ничего»: операция либо применяется целиком, либо не применяется вовсе, без промежуточных полуразобранных состояний. -- **Immutable (неизменяемая система)** — система, чья основная часть смонтирована только для чтения и не меняется на ходу. - -### 0.2 Папки системы - -Что за что отвечает — и как ложится на иерархию «пункт → подпункт». - -- **`/usr`** — база системы: библиотеки, бинарники, всё, что отвечает за работу и загрузку. Неизменяемая. **Главный пункт** — ось аварийных откатов. -- **`/etc`** — конфигурация системы. **Подпункт** — привязан к своему `/usr`, версионируется отдельно и при откате следует за своим пунктом. -- **`/var`** — изменяемый рантайм-стейт: логи, данные, БД. Персистентный, не версионируется, на откате не теряется. -- **`/home`** — данные пользователей. Персистентный, вне версионирования. -- **`/boot` и ESP** — раздел, который читает прошивка UEFI: загрузочные записи и ядро/initramfs (UKI). Отсюда система стартует до появления настоящего корня. -- **`/sysroot`** — точка, куда монтируется физический корневой раздел; поверх него собирается настоящий `/`. - -### 0.3 Жаргон проекта - -Термины, которыми оперирует остальной документ. - -- **Контент-адресное хранилище (CAS)** — хранилище, где файл адресуется по хешу своего содержимого, а не по имени; одинаковые файлы хранятся один раз (дедупликация). -- **Образ (image)** — цельный контент-адресный слепок дерева файлов (`/usr` или `/etc`) в конкретной версии. -- **Дайджест (digest)** — хеш образа, его идентичность. -- **Деплой (deployment)** — конкретная развёрнутая версия системы; в этой модели = пара `(usr-digest, etc-digest)`, но пара асимметричная: `usr` первичен и адресуем снаружи — именно его называет cmdline при загрузке (`composefs.digest=`, §5.2) и именно им именуется `state/deploy//` (§3, §5.7); `etc` вторичен и достаётся уже ИЗНУТРИ записи этого usr (поле `working_etc`, §5.7), отдельным параметром в cmdline никогда не передаётся. Cmdline говорит "какой usr", а "какой etc" следует уже из его записи, а не напрямую из cmdline. -- **Ref** — человекочитаемый именованный указатель на образ. -- **overlay (lower / upper)** — наложение писабельного слоя (upper) поверх слоя только для чтения (lower); так живой `/etc` лежит поверх образа. -- **fs-verity** — механизм ядра, криптографически заверяющий содержимое файла и ловящий любое его изменение. -- **Разовый вход (one-shot)** — загрузочная запись, которую прошивка/загрузчик выбирает ровно один раз, после чего возвращается к постоянному дефолту (основа автооткла, §5.2). -- **`base` / `new` / `live`** — три входа 3-way merge `/etc` (§5.1): старый дефолт, новый дефолт, текущее состояние пользователя. -- **`.upac-new`** — новый дефолт конфига, положенный рядом при конфликте, чтобы не затирать правку пользователя. -- **`seq`** — монотонный порядковый номер деплоя; авторитет порядка истории и откатов. -- **Пин** — деплой, защищённый от сборки мусора (активный, цель отката, закреплённый пользователем). -- **Откат (rollback)** — возврат к предыдущей версии: пункта (`/usr`) при аварии либо подпункта (`/etc`) отдельно. - ---- - -## 1. Постановка задачи - ---- - -**Проблема состояния.** -На обычной Linux-системе установка, обновление и удаление софта меняют работающую систему *на месте* — правят её файлы прямо там, где она сейчас живёт. Из-за этого у системы в любой момент нет единого проверяемого состояния: она представляет собой груду по отдельности изменённых файлов. Следствия три: прерванное или неудачное изменение оставляет систему в сломанном полусостоянии; нельзя воспроизвести или доказать конкретное «заведомо рабочее» состояние; и нет чистого способа вернуться назад. - -**Решение проблемы состояния.** -UPAC рассматривает каждое состояние системы как **цельный, контент-адресный, проверяемый образ**. Любая операция (установка / обновление / удаление) не трогает работающую систему, а **строит из текущего образа новый**. Переключение на новый образ — один атомарный шаг, а прежний образ остаётся нетронутым. Отсюда прямо следуют три свойства, отвечающие на проблему: операция либо прошла целиком, либо система осталась прежней (**атомарность**); любое состояние опознаётся и заверяется по хешу (**воспроизводимость и проверяемость**); откат — это просто загрузка предыдущего образа (**обратимость**). При этом раскладка диска, загрузчик и ядро остаются полностью под контролем пользователя. - ---- - -**Проблема доступа.** -Базовое дерево системы (`/usr` и т.д.) неизменяемо и находится под управлением менеджера. Если пользователь хочет просто добавить туда свои файлы — например, положить обои или ассеты, которые пакет ожидает в `/usr`, — он не может их туда просто скопировать. Приходится заворачивать пару файлов в полноценный пакет (метаданные, сборка, установка) ради самого факта их размещения. Барьер на добавление своего в управляемое дерево неоправданно высок. - -**Решение проблемы доступа.** -UPAC позволяет добавлять произвольные пользовательские файлы в управляемое дерево (включая `/usr`) напрямую, одной командой, без написания пакета. Файл попадает в собираемый образ как полноценное содержимое, но пользователю не нужен весь конвейер упаковки. - ---- - -**Проблема совместимости.** -Под одно и то же ядро Linux существует множество несовместимых форматов пакетов (deb, rpm, pkg.tar и т.д.) и дистрибутивов. Программа, собранная под один формат, не ставится в другой без плясок; пользователь заперт в экосистеме своего пакетного формата, хотя ядро и ABI у всех общие. - -**Решение проблемы совместимости.** -UPAC не привязан к одному формату пакетов. Разбор конкретного формата вынесен в отдельные бэкенды (по одному на формат), которые приводят пакет к общему внутреннему представлению — дереву файлов плюс метаданным. За счёт этого один менеджер ставит пакеты разных форматов на одну систему, и формат перестаёт быть границей совместимости. - ---- - -**Проблема управления.** -Даже когда файл уже в системе, его нельзя привязать к пакету как пользовательский — так, чтобы менеджер его отслеживал и подчищал вместе с пакетом. Особенно это больно в `/usr`: добавленные вручную файлы остаются «сиротами» вне учёта — их не видно при удалении и не почистить автоматически. - -**Решение проблемы управления.** -UPAC позволяет прикреплять файл к пакету как пользовательский, с полноценным учётом в базе. Такой файл наследует жизненный цикл пакета: отслеживается, показывается в его составе и удаляется вместе с ним, где бы он ни лежал (включая `/usr`). Пользовательские добавления перестают быть неучтённым мусором. - ---- - -## 2. Non-goals - -То, что от менеджера такого рода резонно ожидать, но UPAC осознанно НЕ делает — чтобы очертить границы и снять будущие «а сделайте ещё вот это». - -- **Не дистрибутив.** UPAC — менеджер пакетов и механизм деплоя, а не ОС. Он не везёт кураторскую репу, дефолтный набор софта или релиз-цикл; он управляет тем контентом, на который его натравили. -- **Не менеджер конфигурации.** UPAC сводит `/etc` и сохраняет правки пользователя через обновления, но не генерирует и не навязывает конфиг-политику — это не Ansible и не NixOS-модули. Он сохраняет и примиряет, а не сочиняет. -- **Не рантайм контейнеров.** UPAC использует те же кирпичи, что и контейнеры (composefs, OCI), но разворачивает хост-систему, а не контейнеры. Он не заменяет docker/podman. -- **Никаких изменений на месте — by design.** Любое изменение системы порождает новый образ; горячей подмены файлов на живой системе нет даже опцией. Это прямое следствие принципа проекта (см. §1, «Проблема состояния»). -- **Не сервер репозиториев.** UPAC — только клиент к готовым внешним репам (зеркала дистрибутивов, OCI-реестры и т.п.); своего репозитория или сервера он не поднимает. Единственная локальная альтернатива репе — доставка образа файлом (`--file`). -- **Не чинит файловую систему и диск.** UPAC отвечает за корректность своих операций (проверка пакетов, атомарность образов, целостность репы) и через fs-verity **обнаруживает** порчу контента, отказываясь грузить повреждённый деплой. Но восстановление самой ФС, битых блоков, деградировавшего носителя или ошибок железа — вне его зоны: это задача `fsck`, SMART и замены диска. Развал системы из-за битого диска — не отказ UPAC. - ---- - -## 3. Структура диска - -С этого места документ описывает уже конкретную реализацию. Её ядро — зависимость **composefs**: она даёт контент-адресное хранилище, сборку проверяемого образа и его монтирование. Данный параграф описывает, что физически лежит на дисках развёрнутой системы. - -### Карта - -``` -[блочный девайс, GPT] -│ -├── ESP (FAT32) (1) -│ ├── EFI/Linux/upac-from.efi (2) -│ ├── EFI/Linux/upac-to.efi (2) -│ └── loader/entries/*.conf (3) -│ -├── deployment-раздел → /sysroot (4) -│ ├── composefs/ (5) -│ │ ├── meta.json (6) -│ │ ├── objects// (7) -│ │ ├── images/ (8) -│ │ │ ├── → ../objects//… (9) -│ │ │ └── refs/<имя> → ../images/ (10) -│ │ └── streams/ (11) -│ │ ├── → ../objects//… -│ │ └── refs/<имя> -│ └── state/deploy// (12) -│ ├── meta.json (12) -│ └── etc-upper/{upper, work} (13) -│ -├── /var-раздел → /var (14) -└── /home-раздел → /home (15) -``` - -### Легенда - -- **(1)** ESP (EFI System Partition) — отдельный FAT-раздел, который читает прошивка UEFI; монтируется в `/boot` (или `/efi`). Здесь лежит всё, что нужно для старта до появления настоящего корня. -- **(2)** `upac-from.efi` / `upac-to.efi` — два фиксированных слота под UKI (подписанный образ kernel + initramfs + cmdline) для direct-UKI загрузки. Операция пишет новый UKI в неактивный слот, переключение — через `BootNext`. -- **(3)** `loader/entries/*.conf` — BLS-записи для машин с бут-менеджером (systemd-boot и пр.), альтернатива direct-UKI. Читаются/пишутся через `BootconfigParser`. -- **(4)** deployment-раздел — физический корень со всем содержимым системы; в рантайме монтируется в `/sysroot`. Требование к ФС — поддержка **fs-verity** (ext4 / btrfs / xfs). Настоящий корень `/` собирается поверх образа монтированием (overlay); разложенного дерева файлов на диске нет. -- **(5)** `composefs/` — репозиторий composefs: контент-адресное хранилище всех файлов и образов. Дефолтный путь composefs для system-режима. -- **(6)** `meta.json` — метаданные репы: версия формата + алгоритм fs-verity (`fsverity--`). -- **(7)** `objects/` — контент-адресный стор; объекты разложены по подкаталогам из первых 2 hex-символов хеша. Одинаковое содержимое хранится один раз (дедупликация). -- **(8)** `images/` — EROFS-образы: контент-адресные слепки деревьев `/usr` **и** `/etc` (деплой ссылается на пару). Несут метаданные дерева, данные файлов берутся из `objects/`. -- **(9)** `` — образ = симлинк на объект в `objects/`. Дайджест образа = его идентичность (он же в бут-cmdline). -- **(10)** `refs/<имя>` — человекочитаемый именованный указатель на образ. -- **(11)** `streams/` — splitstream'ы (импортированные слои/коммиты как источник контента), тоже симлинки в `objects/` + свои refs. -- **(12)** `state/deploy//` — запись деплоя, **ключ = `usr-digest`** (одна на различный `/usr`, дедуп). `meta.json` несёт: `usr_digest`, `subject` (короткое обязательное сообщение коммита для этого `/usr`, например от install/uninstall/update) + опциональный длинный `message`, `seq` (порядок рождения, авторитет отката), `timestamp` (для показа), `etc_history` (упорядоченный список записей `{etc_digest, subject, message}` этого `/usr` — каждая `/etc`-запись несёт свои subject+message, первая запись при создании нового `/usr` наследует subject+message самого `/usr`-события) и `working_etc` (рабочий подпункт, ставит boot-confirm). Повторное прибытие к тому же `usr-digest` = переключение на эту запись, не дубль. См. §5.7. -- **(13)** `etc-upper/{upper, work}` — **живой `/etc`**: недокоммиченные правки как upper-слой overlayfs над текущим `working_etc` (§5.1). Запечатывается в `etc-digest` при смене `/usr` или по `upac commit`; `work` — служебный каталог overlayfs. -- **(14)** `/var` — персистентный рантайм-стейт (логи, данные, БД): **отдельный реальный раздел**, не версионируется и на откате не теряется. Пер-digest overlay для `/var` из референса composefs здесь НЕ используется — монтируется реальный раздел. -- **(15)** `/home` — пользовательские данные: отдельный персистентный раздел, вне версионирования. - ---- - -## 4. Структура репозитория - -Целевая раскладка репозитория проекта. - -### Карта - -``` -upac/ -├── Cargo.toml (1) -├── rustfmt.toml -├── README.md -├── CHANGELOG.md -├── LICENSE -├── SECURE.MD (2) -├── .github/ (3) -├── .gitignore -├── doc/ (4) -├── lib/ (5) -├── derive-static/ (6) -├── cli/ (7) -├── decoders/ (8) -│ ├── alpm/ -│ ├── deb/ -│ ├── rpm/ -│ └── xbps/ -└── tests/ (9) -``` - -### Легенда - -- **(1)** `Cargo.toml` — воркспейс (`lib`, `derive-static`, `cli`). -- **(2)** `SECURE.MD` — политика безопасности проекта. -- **(3)** `.github/` — конфигурация CI. -- **(4)** `doc/` — проектные документы (эта заметка и пр.). -- **(5)** `lib/` — Rust-ядро библиотеки. -- **(6)** `derive-static/` — proc-macro крейт: переменные из файла констант. -- **(7)** `cli/` — CLI-фронтенд. -- **(8)** `decoders/` — плагины-декодеры форматов пакетов, по одному на формат. -- **(9)** `tests/` — интеграционные тесты. - ---- - -## 5. Механизмы - -Отдельные механизмы, которые реализует ядро (`lib/`) дополнительно или используя механизмы composefs. - -### 5.1 Слияние конфигов (`/etc`) - -Живой `/etc` в рантайме — `overlay(lower = закоммиченный etc-digest, ro; upper = недокоммиченные правки, rw)`. `/etc` версионируется контент-адресно: каждый снимок = `etc-digest` (см. §5.7). Задача merge — при смене `/usr` построить новый `/etc` (перенести правки пользователя, подтянуть новые дефолты пакетов) и запечатать результат в первый `etc-digest` нового `/usr`. - -Механизм библиотечный, выполняется на стадии `merge`, до того как новый деплой станет загрузочным. - -**Три входа (3-way):** -- **base** — дефолты `/etc` текущего `/usr` (из которого собрана живая система); -- **new** — дефолты `/etc` нового, разворачиваемого `/usr`; -- **live** — текущее живое `/etc` пользователя (закоммиченный `working_etc` + недокоммиченный upper прошлого деплоя). - -**Классификация по файлу:** -- пользователь файл НЕ трогал (`live == base`) → в результат идёт **новый дефолт** пакета; -- пользователь правил, новый дефолт совпал со старым (пакет файл не менял) → версия пользователя сохранена; -- пользователь правил И новый дефолт изменился (конфликт) → версия пользователя остаётся рабочей, а новый дефолт кладётся рядом как `<файл>.upac-new` (исключён из будущей классификации — это не «файл пользователя»). - -**Конфликты — через хук, не блокируют.** Операция не встаёт: деплой проходит, а `.upac-new`-файлы сигналят пользователю событием-хуком (в CLI), что есть что примирить. - -**Результат** запечатывается в новый `etc-digest`, который становится `working_etc` нового деплоя; его живой upper стартует пустым. Неизменённые файлы дедуплицируются composefs на уровне объектов, так что `etc-digest` — полный слепок `/etc` без дублирования содержимого. - -По `upac commit` тем же механизмом запечатывается текущее живое `/etc` без смены `/usr` — новый `etc-digest` под тем же `/usr`. - -### 5.2 Загрузка системы и откат в случае неуспеха - -Откат построен на разовом выборе загрузки и позднем подтверждении успешности запуска; отдельного счётчика попыток нет — образ, не загрузившийся с первого раза по любой причине, повторно не перезапускается. - -**Механизм разового выбора.** У загрузчика/прошивки есть пара «разовый вход / постоянный дефолт»: UKI-direct — `BootNext` / `BootOrder`; systemd-boot — `LoaderEntryOneShot` / `LoaderEntryDefault`; grub — `grub-reboot` / постоянный дефолт в конфиге. Разовую переменную прошивка/загрузчик гасит при любой загрузке, поэтому она сама по себе и есть однопопыточный автооткат. - -**Постановка и загрузка:** - -1. При деплое D' пишется загрузочная запись с `composefs.digest=D'` (UKI в неактивный слот `upac-to.efi` либо BLS-conf через `BootconfigParser`), но постоянным дефолтом она НЕ делается — ставится разовым входом следующей загрузки; постоянный дефолт остаётся на прошлом рабочем деплое. -2. Перезагрузка: загрузчик грузит D' один раз, разовая переменная гасится. initramfs монтирует digest из cmdline (overlay composefs), затем pivot и PID1. -3. Система дошла до здорового состояния — поздний хук / init-юнит делает D' постоянным дефолтом и помечает **пару рабочей**: обновляет `working_etc` текущего `/usr` (§5.7). Это и есть подтверждение. -4. Подтверждение не сработало (система легла раньше по любой причине) — разовая переменная уже погашена, следующая загрузка идёт в постоянный дефолт, то есть на прошлый деплой. Это автооткат. - -**D' — это usr-digest, а не составная пара.** `composefs.digest=D'` несёт именно `usr-digest` — то же самое значение, которым именуется `state/deploy//`, и которое `open_tree()` (см. модуль composefs, §7) принимает напрямую, без всякой трансляции. Этим же способом в рантайме узнаётся "какой деплой сейчас активен" без отдельного файла-указателя на диске (§5.7 это тоже отмечает: "активный деплой — отдельный указатель, загруженный `composefs.digest` / дефолт загрузчика"): читаем `/proc/cmdline`, достаём `composefs.digest`, это и есть usr-digest. `etc` из пары намеренно НЕ в cmdline — как только usr-digest известен, из его `state/deploy//meta.json` читается `working_etc` (§5.7), который называет текущий подтверждённый `etc-digest`. - -**Почему нераспознанный параметр вроде `composefs.digest=` вообще доживает до `/proc/cmdline`.** `/proc/cmdline` — не отфильтрованный список параметров, которые понимает ядро, а сырая, нетронутая строка, которую загрузчик передал ядру. Когда парсер аргументов ядра встречает незнакомый параметр, он его не выкидывает — печатает что-то вроде "Unknown kernel command line parameters ..., will be passed to user space" и оставляет строку как есть, для `/proc/cmdline` и cmdline самого PID1. Это штатное, задокументированное поведение ядра, на которое и так все полагаются — ровно так же работают `systemd.*`, dracut'овские `rd.*`, `luks.uuid=` и собственный `ostree=` у OSTree: ни один из них тоже не параметр ядра, все они чисто userspace, и все доживают до `/proc/cmdline` тем же способом. - -**Эшелоны отката** (какой уровень что ловит): - -1. Ядро или initramfs не встали — прошивка сама уходит в постоянный дефолт (разовая переменная погашена) = прошлый деплой. -2. Загрузился, но PID1 не встал — подтверждение не пришло, следующий бут откатывается; на менеджере прошлый деплой можно выбрать и вручную в меню. -3. PID1 встал, но сервисы/сеть/GUI мертвы — откат из живой системы командой `upac rollback` либо ребут в меню. -4. Полный кирпич — меню прошивки либо Live-USB + `upac rollback --root`. - -Если система формально дошла до рабочего состояния и подтвердилась, но какие-то подсистемы или инструменты не встали или работают неверно — доступен ручной откат: `upac rollback` из живой системы либо меню прошивки/загрузчика. - -**Ограничения (осознанные):** - -- одна попытка, не N: битый атомарный образ детерминированно битый, повторять смысла нет; -- автоподтверждение доказывает «дошёл до здорового таргета», а не «пользователю хорошо» — более глубокие поломки откатываются вручную через `upac rollback`; -- чистый висяк (PID1 жив, но завис, без паники и ребута) требует ручного power-cycle, чтобы сработала разовая переменная. - - -### 5.3 Постановка деплоя (stage) - -На вход — образ D', уже лежащий в репе (`images/D'`); на выход — деплой, готовый к разовой загрузке. Связывает операции (§5.4) с загрузкой (§5.2). - -1. Слияние `/etc` (§5.1): merge запечатывает `etc-digest` для D' и ставит его `working_etc`; живой upper (`etc-upper/`) стартует пустым. -2. Персистентные разделы (`/var`, `/home`) — реальные, монтируются как есть, не трогаются. -3. Запись загрузочной записи с `composefs.digest=D'`: - - UKI-direct — собрать и подписать UKI (kernel + initramfs + cmdline), записать в неактивный слот `upac-to.efi`; - - менеджер — `BootconfigParser` пишет BLS-conf (`options composefs.digest=D'`) в `loader/entries/`. -4. Поставить D' разовым входом следующей загрузки (§5.2): UKI-direct — `BootNext` на слот; менеджер — `LoaderEntryOneShot` / `grub-reboot`. Постоянный дефолт не трогаем — остаётся прошлый деплой. - -Дальше — перезагрузка и §5.2 (загрузка, подтверждение либо автооткат). - -### 5.4 Операции: добавление / удаление / обновление - -Все три — одна форма: изменить дерево → закоммитить новый образ → отдать в постановку деплоя (§5.3). Старый образ не трогается до переключения (атомарность). Здесь работают декодеры и резолвер, и здесь пишется БД пакетов. - -Общий конвейер: - -1. Сформировать новое дерево из текущего (различие — по операции, ниже). -2. Закоммитить дерево новым образом D' в репу (`objects/` + `images/D'`); БД пакетов пишется внутрь образа. -3. Передать D' в постановку деплоя (§5.3). -4. Лёгкий деплой-прунинг (§5.5) завершающей стадией. - -Различие в шаге 1: - -- **add (установка):** декодер разбирает пакет → резолвер добавляет зависимости → новое дерево = текущее + файлы пакета(ов). -- **remove (удаление):** новое дерево = текущее − файлы пакета − привязанные пользовательские файлы. -- **update (обновление):** декодер разбирает новую версию → новое дерево = текущее с заменёнными файлами пакета; слияние `/etc` (§5.1) на постановке донесёт новые дефолты. - -### 5.5 Сборка мусора (GC) - -Два уровня: деплои (что держим) и объекты (что вымести). Политику удержания задаёт пользователь; движок object-sweep — composefs. - -**Незыблемые пины** (никогда не удаляются): - -- активный (загруженный) деплой; -- цель отката (постоянный дефолт); -- поставленный-но-не-подтверждённый деплой (разовый вход). - -Плюс пользовательские ручные пины (закреплённые деплои) и последние N в пределах заданной пользователем глубины. - -**Триггеры:** - -1. **Лёгкий деплой-прунинг — внутренней стадией** после каждой mutated-операции: снять реф образа и удалить `state/deploy//` для деплоев за пределами политики. Дёшево, пины держат нужное. -2. **Тяжёлый object-sweep — только вручную**, командой `upac gc`: пройтись по `objects/` и `streams/` и вымести недостижимое (composefs `ObjectCollector`). -3. Ни в бут/подтверждение, ни по таймеру GC не вешается. - -### 5.6 OCI (в планах) - -Раздел — задел на будущее. OCI здесь — переносимый формат артефакта-образа, а не сетевой протокол; своего сетевого стека сверх репы UPAC не поднимает. - -**Направления:** - -- **импорт** — взять OCI-образ и развернуть его как хост-систему; -- **экспорт** — произвести переносимый OCI-образ-артефакт из деплоя (готовая «эталонная копия»). - -**Доставка образа — два существующих пути:** - -1. **из внешней репы** (клиентом; дефолт) — тот же механизм, что и для пакетов; -2. **`--file <образ>`** — локальный файл, берётся и разворачивается напрямую. - -Fleet-деплой (эталон → парк машин) едет теми же двумя путями: образ в репе → машины тянут, либо раздача файлом. Отдельного транспорта под парк и push в registry нет. - -Кирпичи (`composefs-oci`): `create_filesystem` (слои → образ), `generate_boot_image`, `pull_image`. - -### 5.7 История и откат на N деплоев - -История хранится НЕ в образе (сломало бы контент-адресность) и не в ESP, а на writable-разделе. Источник правды — сами каталоги `state/deploy//`, отдельного журнала нет. - -**Две оси.** - -- **`/usr` — линейная история деплоев.** Каждый различный `/usr` = одна запись, ключ `usr-digest`. `seq` — порядок рождения записей (монотонный, один на digest; high-water-mark в `state/next-seq`, запись tmp+rename). Повторное прибытие к существующему `usr-digest` **переключает** на его запись, а не заводит дубль — так под-история `/etc` этого `/usr` при возврате цела. Запись несёт свой **commit-message**: `subject` (короткий, обязательный) + опциональный длинный `message` — коммит-сообщение операции, породившей этот `/usr` (install/uninstall/update). -- **`/etc` — под-история внутри `/usr`.** `meta.json` записи несёт `etc_history` — упорядоченный список записей `{etc_digest, subject, message}`, снятых под этим `/usr` (при смене `/usr` и по `upac commit`, §5.1). Каждая запись несёт свои `subject`+опциональный `message`; первая запись, созданная автоматическим мёрджем при смене `/usr` (§5.1), наследует subject+message самого `/usr`-события — последующие явные `upac commit` получают собственные, независимые subject+message. - -**Активный деплой — отдельный указатель** (загруженный `composefs.digest` / бут-дефолт), а не `max(seq)`: после переключения на старую запись её `seq` остаётся прежним. - -**Откат:** - -- **аварийный (`/usr`)** — на N-й существующий деплой в порядке `seq` (по факту наличия, **не** арифметикой `seq−N`: GC, пины и сгоревшие номера оставляют дырки, их пропускаем). Восстанавливается **пара**: целевой `usr-digest` + его `working_etc` (последний подтверждённый подпункт) — правки конфига не теряются. -- **конфига (`/etc`)** — `upac rollback --etc` на прошлый `etc-digest` из `etc_history` текущего `/usr`; та же ранговая механика и своя глубина удержания. - -**`seq`** авторитетен для порядка и откатов; **`timestamp`** в `meta.json` — только для отображения (`upac history`), порядок всегда по `seq`, чтобы дрейф или перевод часов не переставили историю. - -Связь с GC (§5.5): глубина удержания по каждой оси обязана быть **≥ максимального N отката** этой оси, иначе история короче обещания — деплой на позиции N уже выметен. - -### 5.8 Хуки (pre/post-триггеры) - -Хук — не код, а декларативный **подписанный** файл: описывает триггер, приоритет и композицию из **примитивов**. Примитив — закрытый набор низкоуровневых действий, зашитых в `lib` (запуск процесса, touch/move файла и т.п.) — единственное, что требует правки кода `lib`. Хук как единица — не `enum` и нигде в коде не перечислен: он целиком описан данными в файле, `lib` — просто генерический исполнитель этой композиции. Подпись — защита от произвольного неподписанного хук-файла (примитивы достаточно привилегированные, чтобы не доверять файлу без неё). - -**Таблица соответствий.** Хук-файл отдельно несёт таблицу: для декодера `D` (§6 — плагин формата пакета: deb, rpm, native, …) этот хук покрывает такое-то его НАТИВНОЕ имя триггера (например, у deb это `Triggers-Interest: update-mime-database`). Так совместимость с чужими trigger-конвенциями — тоже данные в файле, а не хардкод внутри декодера. - -**Приоритет и критичность.** `priority` — обычное знаковое целое (дефолт 0), нужен ТОЛЬКО чтобы разрешать конфликт, когда несколько разных хук-файлов заявляют одно и то же нативное имя триггера (один и тот же ключ `k` в таблице соответствий) — побеждает больший `priority`; равенство — неразрешимый конфликт, и `lib` сразу падает с ошибкой, а не выбирает молча (`build_trigger_table` разрешает это ещё до отправки таблицы декодеру — равенство приоритетов — жёсткий `Err`, та же логика, что и у двух декодеров на один формат пакета, см. ниже). Несопоставившиеся записи (нативного триггера хука просто нет в конкретном пакете) вообще не нуждаются в отдельном репортинге — это нормальный, ожидаемый исход для большинства хуков на большинстве пакетов, а не ошибка. Стоит ли всё же прокидывать *нефатальное* предупреждение через `MessageHook` для какого-то из этих случаев (конфликт, или хук, который структурно никогда ни с чем не сматчится) — пока открытый вопрос. Никакой очерёдности исполнения `priority` не задаёт — все сматчившиеся хуки одной точки триггера выполняются конкурентно, без ступеней. Критичность (abort vs best-effort при падении хука) — поле в самом хук-файле (`critical = true/false`), не свойство примитива: примитив нейтрален, а решение "падение = abort всей операции или просто предупреждение" знает только автор хука, подписавший файл (доверие уже обеспечено цепочкой CA, дополнительное вето на уровне примитива не нужно). - -**Разделение труда:** - -- **`lib`** — единственная сторона, что читает хук-файлы с диска, проверяет подпись, разбирает композицию примитивов и таблицу соответствий. Исполняет композицию через свои примитивы. -- **Декодер (плагин)** — сам хук-файлы не парсит. Получает от `lib` таблицу соответствий уже как готовую map k:v **под свой `D`** (deb не получает чужих записей, например под rpm/native), где k — нативное имя триггера decoder'а, v — наш хук. Декодер сам сопоставляет её с нативными именами триггеров, которые вычитал из пакета (например, deb-декодер сам читает `Triggers-Interest` пакета), и через FFI отдаёт `lib` обратно список хуков на исполнение (сматчившиеся v). Matching остаётся на стороне декодера, но на входе у него уже готовые данные от `lib`, а не сырой хук-файл. - -**Разрешение декодера.** Декодеры находятся через декларативные TOML-манифесты в `/etc/upac.d/decoders/` (один на декодер — `format`, `extensions`, `library`), никогда не через сканирование или dlopen-проверку `.so`-файлов напрямую: `format` — каноничная идентичность формата пакета, та же строка, что и ключ `D` в таблице соответствий хука (один декодер никогда не покрывает больше одного формата); `extensions` перечисляет файловые варианты, в которых этот формат реально поставляется (например, alpm-пакеты как `pkg.tar`/`pkg.tar.gz`/`pkg.tar.xz`/`pkg.tar.zst` — один формат, несколько файловых форм); `library` называет `.so`, который нужно `dlopen`-нуть при первом использовании, лениво, только когда реально понадобилось обработать пакет этого формата. Дублирующийся `format` у двух манифестов — жёсткая ошибка на этапе загрузки манифестов, та же логика, что и у равенства priority выше. Раз до реального разрешения shared-library дело не доходит без надобности, декодерам не нужно (и они не должны) самим репортить свою идентичность через FFI — единственный источник истины — манифест. - -**Формат и подпись хук-файла.** Хук-файл — TOML, лежит в `/etc/upac.d/hooks/` (путь зашит константой в стиле `Lib.toml`, §"типы"). Подпись — отдельный sidecar-файл (`name.hook` + `name.hook.sig`), не поле внутри TOML (иначе пришлось бы фиксировать канонизацию байт при re-serialize/re-parse). Подпись — по цепочке из 2 уровней: root CA (офлайн-ключ, подписывает только следующий уровень) → signing-сертификат на trust-domain (например "upac-core", либо свой на дистр/мейнтейнера), который и подписывает байты `.hook`-файла напрямую; отдельного leaf-уровня нет — лишняя ротация ключей без выгоды на таком узком контуре. `.sig` несёт и подпись, и сам signing-сертификат целиком — проверка самодостаточна (root + `.sig`, ничего искать отдельно не нужно). **Root — конфигурируемый файл**, не зашит в бинарь: это и есть весь смысл системы — дистр/OEM подключает свой root без пересборки `upac`. Схема подписи — Ed25519 поверх X.509-сертификатов, через крейт `x509-cert` (чистый Rust, RustCrypto — без зависимости от OpenSSL/CMS). Реализовано в отдельном крейте `upac-pki` (`lib/pki/`, LGPL, без зависимости от `upac-abi`/`upac-lib`) — `RootIdentity`/`SigningIdentity` (генерация), `HookSignature::sign`/`verify`, трейт `Identity` (`to_bytes`/`from_bytes`) для сохранения/загрузки пары ключ+сертификат между отдельными запусками процесса. От него зависят и `upac-lib` (проверка), и `upac-sign-cli` (подпись) — так байтовый формат `.sig` не может разъехаться между двумя сторонами. - -**Модель исполнения.** Запуск хуков — асинхронный, но целиком внутри `lib`: FFI остаётся синхронным (`extern "C" fn` вызывает обычную функцию, та поднимает `tokio`-рантайм и делает `.block_on(...)`). Область применения — только конкурентный запуск N независимых хуков одной точки триггера **внутри одного `Stage::run()`**; сам `Orchestrator` async не становится — стадии обязаны идти линейно. Для CPU-bound работы (хэширование дерева при add/remove/update, §5.4) — `rayon`/обычные потоки, не async: локальный `tokio::fs` на Linux без io_uring сам блокирующий под капотом (`spawn_blocking`), так что здесь async не даёт выгоды. `tokio-uring` (io_uring) сознательно не берём: другая, небезопасная при отмене модель владения буфером, Linux-only, моложе и настороженно воспринимается с security-стороны — пересмотреть, только если профайлинг реально покажет syscall-overhead на огромных деревьях. - -### 5.9 Отмена операции - -Второй, независимый от §5.8 хук-канал — не декларативные pre/post-триггеры, а системный сигнал отмены. `CancelToken` (`#[repr(C)]`, atomic-флаг) создаётся вызывающей стороной (CLI/GUI) и передаётся в `lib` указателем через каждый запрос. - -**`Lock`** — чистый механизм взаимного исключения между rw-операциями, никак не связан с отменой (bind на abstract Unix-адрес, фиксированный, общий для всех rw-вызовов: занят — `EADDRINUSE` → `LockError::Busy`, свободен — можно работать). Держит его сам `Orchestrator` на весь срок rw-операции; ro его не берёт вообще. - -**Доступ к отмене — явный параметр, без обёрток.** `&CancelToken` передаётся напрямую в `Stage::run`/`Orchestrator::run`, каждая стадия читает `.is_cancelled()` сама, в том числе внутри своих циклов. Никакой отдельной обёртки, объединяющей `Lock` и токен, нет — это два независимых, никак не связанных друг с другом механизма, ровно как и задумано в начале этого раздела. - -### 5.10 Прогресс операции - -Тот же хук-канал, что и §5.9 (`MessageHook`), но не отмена, а отслеживание прогресса. Смысл `data` — не абстрактный довесок "для информации", а именно трекинг того, что происходит ВНУТРИ стадии: каждое событие — переход в именованный под-шаг (фазу) мини-FSM текущей стадии, а не generic процент выполнения. - -Payload — один общий `#[repr(C)]`-тип на все события, не отдельный вид под каждое (тэгированный union был бы неудобен на C-ABI, да и пока не с чем сверять форму — тела стадий ещё не написаны): `stage` (какая стадия, как раньше — `StateId as u32`), `phase` (какой под-шаг внутри стадии; смысл — за самой стадией, как и `StateId`), `subject` (borrowed-строка, к какому объекту относится — пакет, файл, хук), `current`/`total` (счётчик элементов, `0`, если не применимо/неизвестно). `MessageHook::send` принимает один самоописывающий параметр вместо прежних раздельных event/data — ничего отдельно распаковывать не нужно. `Orchestrator` создаёт билдер (с уже проставленным `stage`, из своего индекса в пайплайне — см. §5.11) и передаёт его стадии параметром; стадия дозаполняет `phase`/`subject`/`progress()` и возвращает билдер обратно — собирает (`.build()`) и шлёт в хук уже сам `Orchestrator`. - -### 5.11 Оркестрация стадий - -Механизм, которым mutating-команды (позже и read-only) на самом деле исполняются: линейный список стадий плюс движок, который через них проходит, — сюда же подключаются оба хук-канала из §5.9/§5.10. - -**Стадия — всегда плоская.** Одна стадия делает ровно одну атомарную единицу работы за вызов, никогда не крутит цикл внутри себя. Каждый вызов сама решает, что дальше — вперёд к следующей стадии, повторить саму себя (например, обработать ещё один файл из уже начатого списка), или прыгнуть НАЗАД к более ранней стадии по её ТИПУ (не по числовому индексу — так вставка новых стадий где-то в пайплайне ничего не ломает). Через такой прыжок назад группа из нескольких стадий (например, "проверить пакет → распаковать → зарегистрировать") может повториться как единое целое — движок при прыжке ищет ближайшую подходящую по типу стадию, идя назад от текущей позиции; если такой стадии нет — это баг сборки пайплайна, а не пользовательский ввод, и он возвращается как обычная ошибка, не падением. - -**Каждый вызов стадии сам приносит свой откат.** Стадия не копит состояние между вызовами — при каждом вызове она создаёт свой, самостоятельный объект отката, несущий ровно те данные, что нужны, чтобы отменить именно то, что сделал ЭТОТ вызов (даже если та же стадия вызывалась перед этим много раз с другими данными — каждый вызов остаётся независимым). Если стадии нечего откатывать в этот раз — она всё равно обязана вернуть такой объект, просто в "пустом" режиме: это гарантируется на уровне компилятора (конструктор "пустого" отката — обязательная часть контракта), не конвенцией, которую можно забыть. Настоящий, содержательный конструктор отката (с данными, специфичными для конкретной стадии) в общий контракт не входит — у каждой стадии он свой, с собственной сигнатурой. - -**Движок** держит линейный список стадий; эксклюзивность — не хранимый где-то режим, а выбор МЕТОДА запуска: один держит системный лок (не связан с отменой из §5.9, отдельный механизм эксклюзивности между rw-операциями) на всё время прогона, другой не трогает его вовсе — для read-only. При любом отказе (ошибка стадии, отмена, ненайденный прыжок назад) разворачивает все накопленные к этому моменту объекты отката, в обратном порядке, не останавливаясь, если один из них сам не смог откатиться — по возможности откатывается всё, а не бросается на полпути. Каждый успешный вызов стадии отдаёт движку сразу две отдельные вещи — билдер прогресса (§5.10, который движок сам же создал и передал стадии до вызова) и свой объект отката: первое пересылается в хук как есть, второе копится в стеке для возможного разворота. - -**Отказ движка** — двух разных видов: либо пайплайн не смог даже начаться (например, лок занят), либо конкретная стадия по счёту N упала — команда, вызвавшая движок, различает их, потому что для первого случая никакого номера стадии просто не существует. - -**Валидация пайплайна, до первого вызова.** Каждая стадия может декларировать, что она `requires` из общего контекста и что `provides` в него (по типу); перед запуском движок один раз проходит весь список и проверяет, что требования каждой стадии удовлетворены тем, что уже лежит в контексте, плюс тем, что предоставили более ранние стадии — недостающая зависимость падает сразу, до реального запуска любой стадии, а не всплывает где-то глубоко внутри более поздней стадии невнятной паникой или `None`. Проверка единообразна для всех команд (и exclusive-, и concurrent-путь запуска). На момент написания ни одна стадия не декларирует реальных зависимостей (тела стадий ещё не написаны), так что проверка сейчас везде no-op — она включается сама, по мере того как тела стадий начнут декларировать настоящие зависимости. - ---- - -## 6. FFI и границы - -`lib` — вся логика и стабильный C-ABI. CLI и (в будущем) GUI — тонкие фронты: только парсят ввод и рендерят события. Декодеры и бут — плагины, которые грузит `lib`. Ниже — только потоки, без структур: код за FFI живёт своей жизнью. - -``` -CLI ─┐ -GUI ─┼──(C-ABI)──▶ lib ──(dlopen)──▶ decoders/* (разбор форматов + резолв) -… ─┘ │ - └──(вызовы)──▶ composefs (repo / image / mount / boot) - -lib ──(хук-колбэки)──▶ CLI/GUI (прогресс, события, конфликты /etc) -``` - -Что по стрелкам идёт: - -- **фронт → lib:** команда с аргументами (что делать) + токен отмены. -- **lib → decoders:** путь к пакету; обратно — файлы, метаданные, зависимости. -- **декодеры:** динамические `.so` по умолчанию (добавить формат = положить плагин); опционально — статическая сборка одним бинарём для мейнтейнеров дистра. Владеет загрузкой и вызовом всегда `lib`, не CLI. -- **lib → composefs:** примитивы (commit образа, mount, prune, запись бут-записей). -- **lib → фронт (хуки):** прогресс операции, события подтверждения, конфликты `.upac-new`. - -**Правило границы:** «трогает состояние / нужен рут / должно быть атомарно» → `lib`; «показывает или собирает ввод» → фронт. CLI и GUI — равноправные тонкие фронты над одним FFI, логика не дублируется. - -**Два публичных контракта у `lib`.** Помимо стабильного C-ABI (`export`, через `dlopen`), Rust-слой сам по себе тоже публичен для прямой статической линковки: `orchestrator`, `scripts`, `plugin`, `composefs`, `database`, `deploy`, `errors`, `lock` — всё `pub mod`, без единого `pub use`-реэкспорта (доступ только полным путём, напр. `crate::database::error::DatabaseError`). Приватными остаются `export` (сам C-ABI не нужно дёргать из Rust), `mutated`/`unmutated` (сборка пайплайнов 15 команд — внутренняя механика) и `types` (доменная модель); `Cursor` внутри `orchestrator` тоже приватен — это шаговая механика `SequentialOrchestrator`, а не то, с чем работает внешний код. Внутри `composefs` дополнительный вырез: `repository::open`/`repository::open_tree` — `pub(crate)`, не часть публичного контракта — единственная точка получения открытого `Repository`/дерева наружу — `deploy::Deploy` (`open_repository`/`open_tree`), чтобы репозиторий нельзя было открыть в обход поднятого sysroot-маунта. - ---- - -## 7. Планируемые модули - -Ориентир для проектирования, уточняется по ходу. - -**`lib/`** — ядро и FFI (реальная раскладка модулей, поддерживается в актуальном виде по ходу разработки): - -- `export` — C-ABI: точки входа всех 15 команд, версия ABI, отмена, освобождение ответов. -- `orchestrator` — общий движок (§5.9–§5.11): `Stage`/`ConcurrentStage`, `Cursor`, `RollbackGuard`, и два оркестратора за одним трейтом `Orchestrator` — `SequentialOrchestrator` (линейный, держит системный лок) и `ParallelOrchestrator` (параллельные стадии, используется для хуков §5.8). -- `mutated` / `unmutated` — сами 15 команд, по подмодулю на команду, каждый просто собирает свой пайплайн стадий через `orchestrator`. -- `scripts` — §5.8: TOML-формат хук-файла (`HookFile`), примитивы (`Primitive`/`TouchFile`/`MoveFile`/`CreateSymlink`, каждый `impl Step { execute, rollback }`), матчинг нативных триггеров (`Operation`/`Timing`). `HookStage::run()` полностью подключён для нативных триггеров: get-or-build общего `tokio`-рантайма через `Context`, загрузка + проверка подписи + парсинг хук-файлов (`load_hooks`, через `upac-pki`), фильтрация по `NativeTrigger`, параллельный запуск совпавших хуков через `ParallelOrchestrator` (`HookFile` сам `impl ConcurrentStage`, исполняет свои `steps` и учитывает `critical`), вписан в пайплайны всех 6 мутирующих команд (Pre/Post-обёртка вокруг каждой). Осталось: матчинг по compatibility-таблице против нативных имён триггеров декодера (`HookFile.triggers` парсится, но пока не используется) и разрешение конфликтов по `priority`. -- `plugin` — загрузка декодеров; сейчас конкретно только подмодуль `decoder` (dlopen, проверка версии ABI, `decode`/`match_triggers`) — родительская `plugin` зарезервирована под другие виды плагинов на будущее, пока таких нет. Там же `manifest` (`DecoderManifest`, `load_decoder_manifests()` — читает декларативные дескрипторы `/etc/upac.d/decoders/*.toml`, без сканирования/проверки `.so`) и `triggers` (`build_trigger_table()` — строит таблицу native-триггер→хук под конкретный декодер из загруженных `HookFile`, разрешая конфликты `priority` жёсткой ошибкой). Пока никуда не подключено — нужна реальная точка вызова, завязанная на ещё не написанные тела стадий install/update. -- `composefs` — доступ к composefs-репе. `repository` (`pub(crate)`, см. §6): `open(path) -> Repository` (открытие по пути через `Repository::open_path`), `open_tree(repository, name) -> FileSystem` (читает образ через `Repository::open_image` + `erofs::reader::erofs_to_filesystem`) — обе доступны только внутри крейта, наружу репозиторий отдаёт только `deploy::Deploy`. `error::RepoError` — маппинг `RepositoryOpenError`/`ImageError`/`anyhow::Error` (последнее нужно, потому что `ensure_object`/`ensure_object_from_file`/`commit_image` и т.п. в самом composefs возвращают `anyhow::Result` — деталей ошибки оттуда не достать, только факт неудачи). `file::FileHandle` — хендл на путь в дереве, три `impl`-блока по логике "трогает CAS или нет": конструкторы (`new` — слепой, для вставки нового; `from_tree` — с проверкой, что путь уже существует), дерево без CAS (`insert_in_tree`/`update_in_tree`/`rename_in_tree`/`remove_in_tree`/`symlink_in_tree`/`hardlink_in_tree`/`stat_in_tree`/`symlink_target_in_tree`/`list_in_tree`), файл через CAS (`insert_file` берёт уже открытый `&File` — не байты, чтобы путь резолвился ровно один раз и не было TOCTOU-гонки на подмену файла между чтением и вставкой в CAS; `replace_file` — алиас на `insert_file`, т.к. `Directory::insert` в composefs уже сам upsert-ит; `read_file` — резолвит inline/external и тянет байты из `Repository::read_object` при необходимости). -- `deploy` — постановка деплоя (§5.3): находит блочное устройство под `/` через `MountInfo`, реальный тип ФС — через `rsblkid::probe::Probe` (не `None`, иначе `mount(2)` падает `EINVAL` — тип ФС ядру нужен явно для любого монтирования, кроме bind/remount), `unshare(CLONE_NEWNS)` + обязательный `MS_REC | MS_PRIVATE`-ремонт `/` перед реальным монтированием (без этого шага mount-события всё равно утекают в хостовую таблицу через shared propagation, унаследованную от родительского namespace), монтирует раздел в `/sysroot`. `deploy(usr_digest) -> PathBuf` — один компонент пути (`state/deploy//`, §3), без OSTree-шной пары `(os, checksum)`. `open_repository()` / `open_tree(name)` — единственная публичная точка доступа к composefs-репе текущего деплоя (см. выше). -- `database` — БД пакетов (redb) внутри образа; при сборке пишется через свой in-memory `StorageBackend`, в рантайме читается `ReadOnlyDatabase` с файла в образе. Там же `record` — `DeployRecord`/`EtcHistoryEntry` (поля §3 п.12: `usr_digest`/`subject`/`message`/`seq`/`timestamp`/`etc_history`/`working_etc`), физически не часть redb-БД (отдельный `meta.json` на sysroot, не внутри образа), но живёт здесь же по смыслу — "как наши типы персистятся" общая забота `database`, независимо от формата. Сериализация — `#[derive(JsonCodec)]` (по образцу `RedbCodec`, тот же по-полевой codegen, только в `serde_json::Value` вместо байт-layout'а); `DeployRecord::write`/`read` пишут/читают файл, `write` — атомарно (tmp-файл в той же директории + `fsync` + `rename`, та же дисциплина что у самого composefs для его `meta.json`). Своя ошибка `error::DeployRecordError` (отдельно от `DatabaseError` — разные форматы хранения, не растягиваем одну ошибку на оба). -- `types` — доменные типы (`Version`, `PackageMeta`, `Dependency`, `Targets`...) и per-команда `StateId`-энумы (`states`); приватный целиком, наружу не торчит ни через C-ABI, ни через прямой Rust API. -- `errors` / `lock` — вынесены из `types` в свои топ-уровневые публичные модули: `CommonError` (обёртка над `HookError`/`DecoderError`/`RepoError`/`DatabaseError`/`SysrootError`/`LockError`/`DeployRecordError` — все они теперь тоже публичны, каждый под своим модулем выше) и `Lock`/`LockError` (эксклюзивный системный лок, §5.9). Публичными их сделала необходимость `E: From` для внешнего кода, который хочет собрать свой пайплайн через `orchestrator` при статической линковке. - -Ещё не начато: `etc_merge` (3-way слияние `/etc`, §5.1), `boot` (бут-записи, разовый вход/подтверждение/откат, §5.2; берёт `composefs-boot`, grub через `blscfg` — отдельных бут-плагинов нет, только BLS-совместимые загрузчики по дизайну), `gc` (политика удержания и прунинг, §5.5), и сам резолв графа зависимостей на стороне `lib` (decoder сейчас только отдаёт сырой список зависимостей пакета через `decode` — граф ещё никто не обходит, да и сетевого слоя для скачивания пакетов тоже нет). - -Конфиг времени сборки (имена таблиц, пути деплоя, адрес лока) — это `Lib.toml` + `build.rs`, генерирующие простые константы, а не отдельный крейт `derive-static` — эта идея заменена. - -**`cli/`** — тонкий фронт: - -- `args` — разбор аргументов; -- `commands` — по модулю на команду (install / remove / update / rollback / gc / …); -- `render` — рендер прогресса, событий и конфликтов из хуков; -- `ffi` — привязка к C-ABI ядра. - -**`decoders/`** — плагины (`.so`, по одному на формат: alpm / deb / rpm / xbps). Грузит и вызывает их `lib` (модуль `decoder`, внутри родительской папки `plugin`, зарезервированной под другие виды плагинов в будущем), не CLI. Какой `.so` грузить под какой формат — решается по декларативному манифесту (`/etc/upac.d/decoders/*.toml`, см. §5.8), а не сканированием самой папки `decoders/` — `lib` никогда не пробует `.so` вслепую только чтобы спросить, что это. По умолчанию — динамические `.so`; опционально мейнтейнеры дистра собирают статикой (один бинарь + вкомпиленные декодеры). Каждый экспортирует `decode` (пакет → файлы, метаданные и зависимости, всё одним вызовом — `resolve` слили в него же, раз к этому моменту decoder уже всё распарсил) и `match_triggers` (матчинг компат-таблицы из §5.8). From 6222d83848888e3fb880c29464e8eeaa61e92b52 Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 20:01:43 +0400 Subject: [PATCH 27/68] New: Added a mechanism for the automatic generation and updating of the repository map in the documentation --- xtask/Cargo.toml | 20 +++++ xtask/src/main.rs | 225 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 xtask/Cargo.toml create mode 100644 xtask/src/main.rs diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 00000000..2df19d73 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2026 JustPav +# +# SPDX-License-Identifier: GPL-3.0-only + +[package] +name = "upac-xtask" +version = "0.1.0" + +edition = "2021" +publish = false + +[[bin]] +name = "xtask" +path = "src/main.rs" + +[workspace] +# Empty on purpose: this marks xtask as its OWN workspace root, so Cargo's +# implicit member auto-discovery doesn't pull it into the root workspace +# (which would otherwise force it onto the same MSRV/edition as every +# other crate here). diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 00000000..9583b860 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,225 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// +// SPDX-License-Identifier: GPL-3.0-only + +//! `cargo xtask gen-tree [--check] [--depth N]` +//! +//! Walks the repository from its root and renders an ASCII directory tree +//! (dirs first, alphabetical, Unicode box-drawing characters — matching the +//! style already used in `doc/`). The tree is then spliced into every +//! markdown file that contains a `` / `` +//! marker pair, replacing everything between the markers (inclusive) with a +//! freshly generated fenced code block. +//! +//! `--check` doesn't write anything: it exits non-zero if any tracked file +//! would change, which is what CI should call. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; + +const IGNORED_DIRS: &[&str] = &[".git", "target", "node_modules", ".zig-cache", "zig-out", "zig-pkg"]; + +const MARKER_START: &str = ""; +const MARKER_END: &str = ""; + +fn main() -> ExitCode { + let mut args = std::env::args().skip(1); + let cmd = args.next().unwrap_or_default(); + + if cmd != "gen-tree" { + eprintln!("usage: cargo xtask gen-tree [--check] [--depth N]"); + return ExitCode::FAILURE; + } + + let mut check_only = false; + let mut depth: usize = 2; + + let rest: Vec = args.collect(); + let mut i = 0; + while i < rest.len() { + match rest[i].as_str() { + "--check" => check_only = true, + "--depth" => { + i += 1; + depth = rest.get(i).and_then(|s| s.parse().ok()).unwrap_or_else(|| { + eprintln!("--depth needs an integer argument"); + std::process::exit(2); + }); + } + other => { + eprintln!("unknown argument: {other}"); + return ExitCode::FAILURE; + } + } + i += 1; + } + + let repo_root = repo_root(); + let root_name = repo_root + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| "./".to_string()); + + let mut tree_text = format!("{root_name}/\n"); + render_dir(&repo_root, "", depth, &mut tree_text); + let tree_text = tree_text.trim_end().to_string(); + + let targets = find_marked_files(&repo_root); + if targets.is_empty() { + eprintln!( + "no file under {} contains {MARKER_START} / {MARKER_END} — nothing to do", + repo_root.display() + ); + return ExitCode::FAILURE; + } + + let mut any_stale = false; + for path in targets { + let original = fs::read_to_string(&path).expect("read doc file"); + let updated = match splice(&original, &tree_text) { + Ok(u) => u, + Err(e) => { + eprintln!("{}: {e}", path.display()); + return ExitCode::FAILURE; + } + }; + + if original == updated { + continue; + } + + any_stale = true; + let rel = path.strip_prefix(&repo_root).unwrap_or(&path); + if check_only { + println!("stale: {}", rel.display()); + } else { + fs::write(&path, updated).expect("write doc file"); + println!("updated: {}", rel.display()); + } + } + + if check_only { + if any_stale { + eprintln!("repo tree in docs is out of date — run `cargo xtask gen-tree`"); + return ExitCode::FAILURE; + } + println!("repo tree in docs is up to date"); + } else if !any_stale { + println!("repo tree in docs was already up to date"); + } + + ExitCode::SUCCESS +} + +fn repo_root() -> PathBuf { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + manifest_dir + .parent() + .expect("xtask must live directly under the repo root") + .to_path_buf() +} + +fn render_dir(dir: &Path, prefix: &str, depth: usize, out: &mut String) { + if depth == 0 { + return; + } + + let mut entries: Vec<_> = match fs::read_dir(dir) { + Ok(rd) => rd.filter_map(|e| e.ok()).collect(), + Err(_) => return, + }; + entries.retain(|e| { + let name = e.file_name(); + let name = name.to_string_lossy(); + !IGNORED_DIRS.contains(&name.as_ref()) + }); + + entries.sort_by(|a, b| { + let a_is_dir = a.path().is_dir(); + let b_is_dir = b.path().is_dir(); + b_is_dir.cmp(&a_is_dir).then_with(|| { + a.file_name() + .to_string_lossy() + .to_lowercase() + .cmp(&b.file_name().to_string_lossy().to_lowercase()) + }) + }); + + let last_idx = entries.len().saturating_sub(1); + for (idx, entry) in entries.iter().enumerate() { + let is_last = idx == last_idx; + let connector = if is_last { "└── " } else { "├── " }; + let name = entry.file_name().to_string_lossy().into_owned(); + let is_dir = entry.path().is_dir(); + + if is_dir { + out.push_str(&format!("{prefix}{connector}{name}/\n")); + let child_prefix = format!("{prefix}{}", if is_last { " " } else { "│ " }); + render_dir(&entry.path(), &child_prefix, depth - 1, out); + } else { + out.push_str(&format!("{prefix}{connector}{name}\n")); + } + } +} + +fn find_marked_files(root: &Path) -> Vec { + let mut found = Vec::new(); + walk(root, &mut found); + found +} + +fn walk(dir: &Path, found: &mut Vec) { + let entries = match fs::read_dir(dir) { + Ok(rd) => rd, + Err(_) => return, + }; + for entry in entries.filter_map(|e| e.ok()) { + let path = entry.path(); + let name = entry.file_name(); + let name = name.to_string_lossy(); + if IGNORED_DIRS.contains(&name.as_ref()) { + continue; + } + if path.is_dir() { + walk(&path, found); + continue; + } + + // Only markdown is a splice target — this also keeps xtask's own + // source (which necessarily mentions the marker strings) out of it. + let is_markdown = path + .extension() + .map(|ext| ext.eq_ignore_ascii_case("md")) + .unwrap_or(false); + if !is_markdown { + continue; + } + + if let Ok(content) = fs::read_to_string(&path) { + if content.contains(MARKER_START) && content.contains(MARKER_END) { + found.push(path); + } + } + } +} + +fn splice(original: &str, tree_text: &str) -> Result { + let start = original + .find(MARKER_START) + .ok_or_else(|| format!("missing {MARKER_START}"))?; + let end = original + .find(MARKER_END) + .ok_or_else(|| format!("missing {MARKER_END}"))?; + if end < start { + return Err(format!("{MARKER_END} appears before {MARKER_START}")); + } + if original[start + MARKER_START.len()..].matches(MARKER_START).count() > 0 { + return Err(format!("more than one {MARKER_START} in file")); + } + + let end = end + MARKER_END.len(); + let block = format!("{MARKER_START}\n```text\n{tree_text}\n```\n{MARKER_END}"); + + Ok(format!("{}{}{}", &original[..start], block, &original[end..])) +} From 14080193e7ea53db7f4880795f8941ba249cfb03 Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 20:19:41 +0400 Subject: [PATCH 28/68] fix: Moved item 4 --- doc/rus/Upac - chapter 4.md | 87 +++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 doc/rus/Upac - chapter 4.md diff --git a/doc/rus/Upac - chapter 4.md b/doc/rus/Upac - chapter 4.md new file mode 100644 index 00000000..3415a492 --- /dev/null +++ b/doc/rus/Upac - chapter 4.md @@ -0,0 +1,87 @@ + + +## 4. Структура репозитория проекта. + +### Карта файлов в репозитории. + + +``` +upac/ +├── .cargo/ +│ └── config.toml +├── .claude/ +│ └── settings.local.json +├── .github/ +│ ├── workflows/ +│ └── PULL_REQUEST_TEMPLATE.md +├── decoders/ +│ ├── alpm/ +│ ├── deb/ +│ ├── rpm/ +│ └── xbps/ +├── doc/ +│ ├── rus/ +│ └── UPAC project note.en.md +├── lib/ +│ ├── abi/ +│ ├── lib/ +│ ├── macro/ +│ └── pki/ +├── LICENSES/ +│ ├── CC-BY-SA-4.0.txt +│ ├── GPL-3.0-only.txt +│ └── LGPL-3.0-or-later.txt +├── user/ +│ ├── sign-cli/ +│ └── upac-cli/ +├── xtask/ +│ ├── src/ +│ ├── Cargo.lock +│ └── Cargo.toml +├── .gitignore +├── Cargo.lock +├── Cargo.toml +├── CONTRIBUTING.md +├── LICENSE +├── README.md +├── REUSE.toml +├── rust-toolchain.toml +├── rustfmt.toml +└── SECURITY.md +``` + + +--- + + +### Легенда карты файлов в репозитории. + +- **(1)** `.cargo/`` - папка для упроавления менеджером сборки проекта Cargo; +- **(2)** `config.toml` - файл, задающий параметры сборки проекта; +- **(3)** `.github/` - папка для управления файлами для автоматической работы github удалённого репозитория; +- **(4)** `workflows/` - папка для управления файлами для создания автоматической работы с строго заданными данными на вход и на выход в github удалённого репозитория; +- **(5)** `PULL_REQUEST_TEMPLATE.md` - шаблон для создания pull реквеста на guthub; +- **(6)** `decoders/` - папка с плагины-декодерами форматов пакетов, по одному на формат (описание каждого не прилагается); +- **(7)** `doc` - папка с документацией проекта. Имеется папка с русскоязычной документацией и англоязычной; +- **(8)** `lib/` — Rust-ядро библиотеки; +- **(9)** `abi/` — Rust библиотека для работы с FFI; +- **(10)** `lib/` — основаная логика работы библиотеки; +- **(11)** `marco/` — библоитека процедурно-генерируемых макросов для основного кода библиотеки; +- **(12)** `pli/` — библоитека работы с генерацией и проверкой всех уровений сертификатов; +- **(13)** `LICENSES/` — папка с лицензиями для работы reuse; +- **(14)** `user/` — папка с утилитами пользователя; +- **(15)** `sign-cli/` — CLI для работы с сертификатами (их подпись, проверка); +- **(16)** `upac-cli/` — CLI для работы с главной библиотекой, осуществления основных операций; +- **(17)** `xtask/` — скрипт для работы автоматического обновления карты репозитория в документации на основе Cargo; +- **(18)** `.gitignore` — файл конфигурации игнорируемых файлов в системе контроля версий Git; +- **(19)** `Cargo.toml` — файл workspace, управляющего остальными вложенными проектами; +- **(20)** `CONTRIBUTING.md` — файл для описания включения в работу по контрибьюции проекта; +- **(21)** `README.md` — краткий файл-справочник проекта; +- **(22)** `REUSE.toml` — файл конфигурации утилиты для проверки лицензирования reuse; +- **(23)** `rust-toolchain.toml` — файл конфигурации версии сборочной системы Cargo проекта; +- **(24)** `rustfmt.toml` — файл конфигурации форматирования файлов с исходным кодом проекта; +- **(25)** `SECURITY.md` — файл описания работы с уязвимостями в коде программы и их отслеживанием в проекте, а так же справочной инфрмацией по этому поводу; From d8f984796ed2d6d4732ea83a1b6df0acf65788c4 Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 20:20:44 +0400 Subject: [PATCH 29/68] fix: Added header for reuse --- doc/UPAC project note.en.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/UPAC project note.en.md b/doc/UPAC project note.en.md index 911d7156..a9edc3ed 100644 --- a/doc/UPAC project note.en.md +++ b/doc/UPAC project note.en.md @@ -1,3 +1,9 @@ + + # UPAC — Project Document Project document. From f175f484b6d2e4733ebb0c950d24338bfd1beee7 Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 20:29:36 +0400 Subject: [PATCH 30/68] fix: Removed unnecessary characters from filenames --- doc/rus/{Upac - chapter 0.md => Upac chapter 0.md} | 0 doc/rus/{Upac - chapter 1.md => Upac chapter 1.md} | 0 doc/rus/{Upac - chapter 2.md => Upac chapter 2.md} | 0 doc/rus/{Upac - chapter 3.md => Upac chapter 3.md} | 0 doc/rus/{Upac - chapter 4.md => Upac chapter 4.md} | 0 doc/rus/{Upac - chapter 5.md => Upac chapter 5.md} | 0 doc/rus/{Upac - chapter 6.md => Upac chapter 6.md} | 0 doc/rus/{Upac - chapter 7.md => Upac chapter 7.md} | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename doc/rus/{Upac - chapter 0.md => Upac chapter 0.md} (100%) rename doc/rus/{Upac - chapter 1.md => Upac chapter 1.md} (100%) rename doc/rus/{Upac - chapter 2.md => Upac chapter 2.md} (100%) rename doc/rus/{Upac - chapter 3.md => Upac chapter 3.md} (100%) rename doc/rus/{Upac - chapter 4.md => Upac chapter 4.md} (100%) rename doc/rus/{Upac - chapter 5.md => Upac chapter 5.md} (100%) rename doc/rus/{Upac - chapter 6.md => Upac chapter 6.md} (100%) rename doc/rus/{Upac - chapter 7.md => Upac chapter 7.md} (100%) diff --git a/doc/rus/Upac - chapter 0.md b/doc/rus/Upac chapter 0.md similarity index 100% rename from doc/rus/Upac - chapter 0.md rename to doc/rus/Upac chapter 0.md diff --git a/doc/rus/Upac - chapter 1.md b/doc/rus/Upac chapter 1.md similarity index 100% rename from doc/rus/Upac - chapter 1.md rename to doc/rus/Upac chapter 1.md diff --git a/doc/rus/Upac - chapter 2.md b/doc/rus/Upac chapter 2.md similarity index 100% rename from doc/rus/Upac - chapter 2.md rename to doc/rus/Upac chapter 2.md diff --git a/doc/rus/Upac - chapter 3.md b/doc/rus/Upac chapter 3.md similarity index 100% rename from doc/rus/Upac - chapter 3.md rename to doc/rus/Upac chapter 3.md diff --git a/doc/rus/Upac - chapter 4.md b/doc/rus/Upac chapter 4.md similarity index 100% rename from doc/rus/Upac - chapter 4.md rename to doc/rus/Upac chapter 4.md diff --git a/doc/rus/Upac - chapter 5.md b/doc/rus/Upac chapter 5.md similarity index 100% rename from doc/rus/Upac - chapter 5.md rename to doc/rus/Upac chapter 5.md diff --git a/doc/rus/Upac - chapter 6.md b/doc/rus/Upac chapter 6.md similarity index 100% rename from doc/rus/Upac - chapter 6.md rename to doc/rus/Upac chapter 6.md diff --git a/doc/rus/Upac - chapter 7.md b/doc/rus/Upac chapter 7.md similarity index 100% rename from doc/rus/Upac - chapter 7.md rename to doc/rus/Upac chapter 7.md From c66746d54712fb4ef268435d4d5515a92dbe18b6 Mon Sep 17 00:00:00 2001 From: JustPav Date: Tue, 11 Aug 2026 20:34:19 +0400 Subject: [PATCH 31/68] Fix: Fixed the design section, corrected text inconsistencies, and updated the documentation link --- README.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0cc4d387..24f1f46e 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,23 @@ A modular package management library for Linux systems with composefs-based atom The library is intentionally split into independent components: decoders handle format-specific unpacking, the core library handles installation and database operations, and everything crosses the FFI boundary through a shared `upac-abi` crate. +It covers the disk layout, the deploy/rollback model, the `/etc` merge, GC, the FFI boundary, and the planned module structure. + ## 📖 Design -Full architecture and design decisions live in the project design note: +Full architecture and design decisions live in the project design notes: -- [`doc/UPAC_project_note.en.md`]() — English (canonical) -- [`doc/UPAC_project_note.md`]() — Russian +- [`Project note en.md`]() — English (canonical); -It covers the disk layout, the deploy/rollback model, the `/etc` merge, GC, the FFI boundary, and the planned module structure. +For russian: +1. [`Вступление и определения`](); +2. [`Постановка задач`](); +3. [`Определение того, чем проект НЕ является (Non-goals)`](); +4. [`Структура диска`](); +5. [`Структура репозитория проекта`](); +6. [`Механизмы работы`](); +7. [`FFI и границы взаимодействия составных частей`](); +8. [`Модули программы`](). ## 🚀 Usage From b4de78ef95613a1ac3d6d57d1ef63ac7be86d0a0 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 03:14:09 +0400 Subject: [PATCH 32/68] fix: Added a new test Co-Authored-By: Claude Sonnet 5 --- lib/lib/tests/deploy_digest.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 lib/lib/tests/deploy_digest.rs diff --git a/lib/lib/tests/deploy_digest.rs b/lib/lib/tests/deploy_digest.rs new file mode 100644 index 00000000..3c7d3e54 --- /dev/null +++ b/lib/lib/tests/deploy_digest.rs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 JustPav +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +use upac::deploy::digest::current_usr_digest; +use upac::deploy::error::SysrootError; + +#[test] +fn current_usr_digest_fails_when_param_is_absent_from_cmdline() { + let result = current_usr_digest(); + + assert!(matches!(result, Err(SysrootError::CurrentUsrDigestNotFound))); +} From 04e10bcb8c14bb2a3641b85e24ee9cad4817312f Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 03:14:43 +0400 Subject: [PATCH 33/68] fix: Fixed structure visibility Co-Authored-By: Claude Sonnet 5 --- lib/pki/src/generate.rs | 8 ++++---- lib/pki/src/signature.rs | 8 ++++---- lib/pki/tests/signature.rs | 15 +++++++-------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/lib/pki/src/generate.rs b/lib/pki/src/generate.rs index 54f5279b..e6c57180 100644 --- a/lib/pki/src/generate.rs +++ b/lib/pki/src/generate.rs @@ -24,8 +24,8 @@ pub trait Identity: Sized { } pub struct RootIdentity { - pub issuer: Issuer<'static, KeyPair>, - pub certificate: Certificate, + pub(crate) issuer: Issuer<'static, KeyPair>, + pub(crate) certificate: Certificate, } impl Identity for RootIdentity { @@ -47,8 +47,8 @@ impl Identity for RootIdentity { } pub struct SigningIdentity { - pub key_pair: KeyPair, - pub certificate: Certificate, + pub(crate) key_pair: KeyPair, + pub(crate) certificate: Certificate, } impl Identity for SigningIdentity { diff --git a/lib/pki/src/signature.rs b/lib/pki/src/signature.rs index ae5b7f6b..91982a30 100644 --- a/lib/pki/src/signature.rs +++ b/lib/pki/src/signature.rs @@ -29,7 +29,7 @@ impl CertificateKind { } } -pub struct RootCertificate(pub Certificate); +pub struct RootCertificate(pub(crate) Certificate); impl RootCertificate { pub fn to_bytes(&self) -> Result, PkiError> { @@ -42,9 +42,9 @@ impl RootCertificate { } pub struct HookSignature { - pub certificate_kind: CertificateKind, - pub certificate: Certificate, - pub signature: Signature, + pub(crate) certificate_kind: CertificateKind, + pub(crate) certificate: Certificate, + pub(crate) signature: Signature, } impl HookSignature { diff --git a/lib/pki/tests/signature.rs b/lib/pki/tests/signature.rs index 84a6a27b..d4d9298f 100644 --- a/lib/pki/tests/signature.rs +++ b/lib/pki/tests/signature.rs @@ -3,22 +3,21 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later -use der::{Decode, Encode}; -use x509_cert::Certificate; - use upac_pki::error::PkiError; -use upac_pki::generate::{Identity, SigningIdentity, generate_root, generate_signing_cert}; +use upac_pki::generate::{Identity, RootIdentity, SigningIdentity, generate_root, generate_signing_cert}; use upac_pki::signature::{HookSignature, RootCertificate}; -fn root_certificate_of(certificate: &Certificate) -> RootCertificate { - RootCertificate(Certificate::from_der(&certificate.to_der().unwrap()).unwrap()) +fn root_certificate_of(root: &RootIdentity) -> RootCertificate { + let certificate_der = root.to_bytes().unwrap().certificate_der; + + RootCertificate::from_bytes(&certificate_der).unwrap() } fn signing_identity() -> (SigningIdentity, RootCertificate) { let root = generate_root("upac test root").unwrap(); let signing = generate_signing_cert("upac test signer", &root).unwrap(); - (signing, root_certificate_of(&root.certificate)) + (signing, root_certificate_of(&root)) } #[test] @@ -48,7 +47,7 @@ fn verify_fails_with_unrelated_root() { let hook_bytes = b"#!/bin/sh\necho hook"; let signature = HookSignature::sign(hook_bytes, &signing).unwrap(); - let result = signature.verify(hook_bytes, &root_certificate_of(&unrelated_root.certificate)); + let result = signature.verify(hook_bytes, &root_certificate_of(&unrelated_root)); assert_eq!(result, Err(PkiError::InvalidSignature)); } From aebaf3806d45591348737fb64c8253af624865a8 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 03:43:10 +0400 Subject: [PATCH 34/68] fix: Renamed usr_digest to prefix_digest new: Added NoRollback structure to implement guaranteed absence of rollback new: Implemented fetching stages in list_packages and search_meta Co-Authored-By: Claude Sonnet 5 --- lib/lib/lib.toml | 9 +++-- lib/lib/src/database/record.rs | 2 +- lib/lib/src/deploy/digest.rs | 6 +-- lib/lib/src/deploy/error.rs | 6 +-- lib/lib/src/deploy/mod.rs | 4 +- lib/lib/src/errors.rs | 11 ++++++ lib/lib/src/orchestrator/stage.rs | 16 ++++++++ lib/lib/src/types/mod.rs | 4 ++ lib/lib/src/unmutated/list_packages/error.rs | 7 +++- .../src/unmutated/list_packages/fetching.rs | 23 ++++++++++-- lib/lib/src/unmutated/search_meta/error.rs | 7 +++- lib/lib/src/unmutated/search_meta/mod.rs | 3 +- .../src/unmutated/search_meta/searching.rs | 37 +++++++++++++++++-- lib/lib/tests/database_record.rs | 4 +- lib/lib/tests/deploy_digest.rs | 8 ++-- 15 files changed, 119 insertions(+), 28 deletions(-) diff --git a/lib/lib/lib.toml b/lib/lib/lib.toml index 22c771d5..e316343b 100644 --- a/lib/lib/lib.toml +++ b/lib/lib/lib.toml @@ -25,16 +25,17 @@ database_path = "share/upac/packages.redb" # records + composefs repo, see doc §3). Changing a name means existing # installs stop finding their deploys/repo on upgrade. # -# usr_digest_cmdline_param names the kernel cmdline parameter written at boot -# entry creation (§5.2, "composefs.digest=D'") and read back at runtime to -# resolve the currently booted usr-digest — one name shared by both sides. +# prefix_digest_cmdline_param names the kernel cmdline parameter written at +# boot entry creation (§5.2, "composefs.digest=D'") and read back at runtime +# to resolve the currently booted deploy's prefix_digest — one name shared by +# both sides. [deployment] root_dir = "/" deploys_dir = "state/deploy" repo_dir = "composefs" sysroot_dir = "sysroot" record_filename = "meta.json" -usr_digest_cmdline_param = "composefs.digest" +prefix_digest_cmdline_param = "composefs.digest" # Name of the abstract Unix socket address upac uses to hold its exclusive # process lock (bind() on this address — a second concurrent upac gets diff --git a/lib/lib/src/database/record.rs b/lib/lib/src/database/record.rs index c8eefff5..b75b8133 100644 --- a/lib/lib/src/database/record.rs +++ b/lib/lib/src/database/record.rs @@ -21,7 +21,7 @@ pub struct EtcHistoryEntry { #[derive(Debug, Clone, PartialEq, Eq, JsonCodec)] pub struct DeployRecord { - pub usr_digest: String, + pub prefix_digest: String, pub subject: String, pub message: Option, pub seq: u64, diff --git a/lib/lib/src/deploy/digest.rs b/lib/lib/src/deploy/digest.rs index b354ba8f..ac463a6c 100644 --- a/lib/lib/src/deploy/digest.rs +++ b/lib/lib/src/deploy/digest.rs @@ -6,11 +6,11 @@ use linux_kernel_cmdline::utf8::CmdlineOwned; use crate::deploy::error::SysrootError; -use crate::types::deployment::USR_DIGEST_CMDLINE_PARAM; +use crate::types::deployment::PREFIX_DIGEST_CMDLINE_PARAM; -pub fn current_usr_digest() -> Result { +pub fn current_prefix_digest() -> Result { let cmdline = CmdlineOwned::from_proc()?; - let digest = cmdline.require_value_of(USR_DIGEST_CMDLINE_PARAM)?; + let digest = cmdline.require_value_of(PREFIX_DIGEST_CMDLINE_PARAM)?; Ok(digest.to_owned()) } diff --git a/lib/lib/src/deploy/error.rs b/lib/lib/src/deploy/error.rs index 02e65343..a98d5029 100644 --- a/lib/lib/src/deploy/error.rs +++ b/lib/lib/src/deploy/error.rs @@ -20,7 +20,7 @@ pub enum SysrootError { RepoDirNotFound, ProbeUnavailable, FilesystemTypeNotFound, - CurrentUsrDigestNotFound, + CurrentPrefixDigestNotFound, System(Errno), } @@ -56,7 +56,7 @@ impl From for SysrootError { impl From for SysrootError { fn from(_: anyhow::Error) -> Self { - SysrootError::CurrentUsrDigestNotFound + SysrootError::CurrentPrefixDigestNotFound } } @@ -71,7 +71,7 @@ impl From for ErrorKind { SysrootError::RepoDirNotFound => ErrorKind::NotFound, SysrootError::ProbeUnavailable => ErrorKind::Unexpected, SysrootError::FilesystemTypeNotFound => ErrorKind::NotFound, - SysrootError::CurrentUsrDigestNotFound => ErrorKind::NotFound, + SysrootError::CurrentPrefixDigestNotFound => ErrorKind::NotFound, SysrootError::System(_) => ErrorKind::Unexpected, } } diff --git a/lib/lib/src/deploy/mod.rs b/lib/lib/src/deploy/mod.rs index c6df154f..aed62ade 100644 --- a/lib/lib/src/deploy/mod.rs +++ b/lib/lib/src/deploy/mod.rs @@ -80,8 +80,8 @@ impl Deploy { Ok(Self { sysroot, deploy, repo }) } - pub fn deploy(&self, usr_digest: &str) -> PathBuf { - self.deploy.join(usr_digest) + pub fn deploy(&self, prefix_digest: &str) -> PathBuf { + self.deploy.join(prefix_digest) } pub fn repo(&self) -> &Path { diff --git a/lib/lib/src/errors.rs b/lib/lib/src/errors.rs index c035d602..93044a02 100644 --- a/lib/lib/src/errors.rs +++ b/lib/lib/src/errors.rs @@ -36,6 +36,17 @@ macro_rules! database_error_from { } pub(crate) use database_error_from; +macro_rules! repo_error_from { + ($name:ident) => { + impl From for $name { + fn from(error: RepoError) -> Self { + $name::Common(CommonError::Repo(error)) + } + } + }; +} +pub(crate) use repo_error_from; + macro_rules! sysroot_error_from { ($name:ident) => { impl From for $name { diff --git a/lib/lib/src/orchestrator/stage.rs b/lib/lib/src/orchestrator/stage.rs index d12a7c1c..9d9a9e08 100644 --- a/lib/lib/src/orchestrator/stage.rs +++ b/lib/lib/src/orchestrator/stage.rs @@ -54,3 +54,19 @@ pub trait ConcurrentStage: Send + 'static { self: Box, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, Box), E>; } + +pub struct NoRollback; + +impl RollbackGuard for NoRollback { + fn new_none(_result: StageResult) -> Self { + NoRollback + } + + fn rollback(&mut self) -> Result<(), ErrorKind> { + Ok(()) + } + + fn result(&self) -> StageResult { + StageResult::Advance + } +} diff --git a/lib/lib/src/types/mod.rs b/lib/lib/src/types/mod.rs index 4ca761a0..978ed7e4 100644 --- a/lib/lib/src/types/mod.rs +++ b/lib/lib/src/types/mod.rs @@ -180,6 +180,10 @@ pub struct TmpPath(pub String); as_str_method!(TmpPath); +pub struct Search(pub String); + +as_str_method!(Search); + #[cfg(test)] mod tests { use super::*; diff --git a/lib/lib/src/unmutated/list_packages/error.rs b/lib/lib/src/unmutated/list_packages/error.rs index e0b9ca80..85d62b81 100644 --- a/lib/lib/src/unmutated/list_packages/error.rs +++ b/lib/lib/src/unmutated/list_packages/error.rs @@ -5,9 +5,12 @@ use upac_abi::error::ErrorKind; +use crate::composefs::error::RepoError; use crate::database::error::DatabaseError; use crate::deploy::error::SysrootError; -use crate::errors::{CommonError, common_error_from, database_error_from, lock_error_from, sysroot_error_from}; +use crate::errors::{ + CommonError, common_error_from, database_error_from, lock_error_from, repo_error_from, sysroot_error_from, +}; use crate::lock::LockError; #[derive(Debug, Clone, PartialEq, Eq)] @@ -19,6 +22,8 @@ common_error_from!(ListPackagesError); database_error_from!(ListPackagesError); +repo_error_from!(ListPackagesError); + sysroot_error_from!(ListPackagesError); lock_error_from!(ListPackagesError); diff --git a/lib/lib/src/unmutated/list_packages/fetching.rs b/lib/lib/src/unmutated/list_packages/fetching.rs index fc95485e..de3cbfde 100644 --- a/lib/lib/src/unmutated/list_packages/fetching.rs +++ b/lib/lib/src/unmutated/list_packages/fetching.rs @@ -5,16 +5,33 @@ use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use crate::composefs::file::FileHandle; +use crate::database::meta::MetaStore; +use crate::database::{InMemory, MemoryDatabase}; +use crate::deploy::digest::current_prefix_digest; +use crate::deploy::{Deploy, DeployMode}; use crate::orchestrator::Context; -use crate::orchestrator::stage::{RollbackGuard, Stage}; +use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage}; +use crate::types::database::DATABASE_PATH; use crate::unmutated::list_packages::ListPackagesError; pub struct FetchingStage; impl Stage for FetchingStage { fn run( - &self, _context: &mut Context, _cancel: &CancelToken, _progress: ProgressEventBuilder, + &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, Box), ListPackagesError> { - todo!() + let prefix_digest = current_prefix_digest()?; + + let deploy = Deploy::new(DeployMode::ReadOnly)?; + let repository = deploy.open_repository()?; + let tree = deploy.open_tree(&prefix_digest)?; + + let database_bytes = FileHandle::new(DATABASE_PATH).read_file(&repository, &tree)?; + let database = MemoryDatabase::open_in_memory(database_bytes)?; + + context.put(database.list_packages_metas()?); + + Ok((progress, Box::new(NoRollback))) } } diff --git a/lib/lib/src/unmutated/search_meta/error.rs b/lib/lib/src/unmutated/search_meta/error.rs index 1f71993c..afdedd2b 100644 --- a/lib/lib/src/unmutated/search_meta/error.rs +++ b/lib/lib/src/unmutated/search_meta/error.rs @@ -5,9 +5,12 @@ use upac_abi::error::ErrorKind; +use crate::composefs::error::RepoError; use crate::database::error::DatabaseError; use crate::deploy::error::SysrootError; -use crate::errors::{CommonError, common_error_from, database_error_from, lock_error_from, sysroot_error_from}; +use crate::errors::{ + CommonError, common_error_from, database_error_from, lock_error_from, repo_error_from, sysroot_error_from, +}; use crate::lock::LockError; #[derive(Debug, Clone, PartialEq, Eq)] @@ -19,6 +22,8 @@ common_error_from!(SearchMetaError); database_error_from!(SearchMetaError); +repo_error_from!(SearchMetaError); + sysroot_error_from!(SearchMetaError); lock_error_from!(SearchMetaError); diff --git a/lib/lib/src/unmutated/search_meta/mod.rs b/lib/lib/src/unmutated/search_meta/mod.rs index 6a086885..caa19824 100644 --- a/lib/lib/src/unmutated/search_meta/mod.rs +++ b/lib/lib/src/unmutated/search_meta/mod.rs @@ -14,8 +14,8 @@ pub use self::error::SearchMetaError; use self::searching::SearchingStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use crate::types::PackageMeta; use crate::types::states::SearchMetaStateId; +use crate::types::{PackageMeta, Search}; mod error; mod searching; @@ -54,6 +54,7 @@ fn assemble() -> SequentialOrchestrator { pub fn run(data: SearchMetaData) -> Result<(Vec,), (SearchMetaStateId, SearchMetaError)> { let mut context = Context::new(); + context.put(Search(data.search.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = assemble(); diff --git a/lib/lib/src/unmutated/search_meta/searching.rs b/lib/lib/src/unmutated/search_meta/searching.rs index 1cab7959..dc558254 100644 --- a/lib/lib/src/unmutated/search_meta/searching.rs +++ b/lib/lib/src/unmutated/search_meta/searching.rs @@ -5,16 +5,47 @@ use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use crate::composefs::file::FileHandle; +use crate::database::meta::MetaStore; +use crate::database::{InMemory, MemoryDatabase}; +use crate::deploy::digest::current_prefix_digest; +use crate::deploy::{Deploy, DeployMode}; +use crate::errors::CommonError; use crate::orchestrator::Context; -use crate::orchestrator::stage::{RollbackGuard, Stage}; +use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage}; +use crate::types::Search; +use crate::types::database::DATABASE_PATH; use crate::unmutated::search_meta::SearchMetaError; pub struct SearchingStage; impl Stage for SearchingStage { fn run( - &self, _context: &mut Context, _cancel: &CancelToken, _progress: ProgressEventBuilder, + &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, Box), SearchMetaError> { - todo!() + let search = context.get::().ok_or(CommonError::MissingResult)?; + let needle = search.as_ref().to_lowercase(); + + let prefix_digest = current_prefix_digest()?; + + let deploy = Deploy::new(DeployMode::ReadOnly)?; + let repository = deploy.open_repository()?; + + let tree = deploy.open_tree(&prefix_digest)?; + + let database_bytes = FileHandle::new(DATABASE_PATH).read_file(&repository, &tree)?; + let database = MemoryDatabase::open_in_memory(database_bytes)?; + + let matches: Vec<_> = database + .list_packages_metas()? + .into_iter() + .filter(|meta| { + meta.name.to_lowercase().contains(&needle) || meta.description.to_lowercase().contains(&needle) + }) + .collect(); + + context.put(matches); + + Ok((progress, Box::new(NoRollback))) } } diff --git a/lib/lib/tests/database_record.rs b/lib/lib/tests/database_record.rs index e6c0ca39..d946bbb5 100644 --- a/lib/lib/tests/database_record.rs +++ b/lib/lib/tests/database_record.rs @@ -18,7 +18,7 @@ fn scratch_dir(name: &str) -> PathBuf { fn sample_record() -> DeployRecord { DeployRecord { - usr_digest: "usr-digest-abc123".to_string(), + prefix_digest: "usr-digest-abc123".to_string(), subject: "install firefox".to_string(), message: Some("long-form commit message".to_string()), seq: 7, @@ -84,7 +84,7 @@ fn deploy_record_from_json_fails_on_non_object() { #[test] fn deploy_record_from_json_fails_on_missing_field() { let mut object = sample_record().to_json(); - object.as_object_mut().unwrap().remove("usr_digest"); + object.as_object_mut().unwrap().remove("prefix_digest"); assert!(matches!( DeployRecord::from_json(&object), diff --git a/lib/lib/tests/deploy_digest.rs b/lib/lib/tests/deploy_digest.rs index 3c7d3e54..b48032f9 100644 --- a/lib/lib/tests/deploy_digest.rs +++ b/lib/lib/tests/deploy_digest.rs @@ -3,12 +3,12 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later -use upac::deploy::digest::current_usr_digest; +use upac::deploy::digest::current_prefix_digest; use upac::deploy::error::SysrootError; #[test] -fn current_usr_digest_fails_when_param_is_absent_from_cmdline() { - let result = current_usr_digest(); +fn current_prefix_digest_fails_when_param_is_absent_from_cmdline() { + let result = current_prefix_digest(); - assert!(matches!(result, Err(SysrootError::CurrentUsrDigestNotFound))); + assert!(matches!(result, Err(SysrootError::CurrentPrefixDigestNotFound))); } From 965e5166311515c7e553d2ef860844faa07a8738 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 03:46:34 +0400 Subject: [PATCH 35/68] fix: Documentation updated --- doc/rus/Upac chapter 7.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/rus/Upac chapter 7.md b/doc/rus/Upac chapter 7.md index db53a436..22fcff0c 100644 --- a/doc/rus/Upac chapter 7.md +++ b/doc/rus/Upac chapter 7.md @@ -16,8 +16,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0 - `scripts` — пункт **§5.8**: TOML-формат хук-файла (`HookFile`), примитивы (`Primitive`/`TouchFile`/`MoveFile`/`CreateSymlink`, каждый `impl Step { execute, rollback }`), матчинг нативных триггеров (`Operation`/`Timing`). `HookStage::run()` полностью подключён для нативных триггеров: get-or-build общего `tokio`-рантайма через `Context`, далее проверка подписи и парсинг хук-файлов (`load_hooks`, через `upac-pki`), фильтрация по `NativeTrigger`, параллельный запуск совпавших хуков через `ParallelOrchestrator` (`HookFile` сам `impl ConcurrentStage`, исполняет свои `steps` и учитывает `critical`). Вписан в pipeline всех mutated команд (Pre/Post обработка хуков каждой); - `plugin` — загрузка декодеров. В данный момент реализован только подмодуль `decoder` (`dlopen`, проверка версии ABI, `decode`/`match_triggers`) — родительский каталог `plugin` зарезервирован под другие виды плагинов на будущее, пока таковых нет. Там же `manifest` (`DecoderManifest`, `load_decoder_manifests()` — читает декларативные файлы для описания декодерв в каталоге `/etc/upac.d/decoders/*.toml`, без сканирования и/или проверки `.so`) и `triggers` (`build_trigger_table()` — строит таблицу native-триггер→хук под конкретный декодер из загруженных `HookFile`, разрешая конфликты `priority` жёсткой ошибкой операции). Пока никуда не подключено — нужна реальная точка вызова, завязанная на ещё не написанные тела стадий каждой команды; - `composefs` — доступ к composefs-репозиторию. `Repository`: `open(path) -> Repository` (открытие по пути через `Repository::open_path`), `open_tree(repository, name) -> FileSystem` (читает образ через `Repository::open_image` + `erofs::reader::erofs_to_filesystem`) — обе доступны только внутри библиотеки, наружу отдаётся только `deploy::Deploy`. `error::RepoError` — маппинг `RepositoryOpenError`/`ImageError`/`anyhow::Error` (последнее нужно, потому что `ensure_object`/`ensure_object_from_file`/`commit_image` и т.п. в самом composefs возвращают `anyhow::Result` — деталей ошибки оттуда не достать, только факт неудачи). `file::FileHandle` — держатель/указатель на путь в дереве, три `impl`-блока по логике "трогает CAS или нет": конструкторы (`new` — слепой, для вставки нового; `from_tree` — с проверкой, что путь уже существует), дерево без CAS (`insert_in_tree`/`update_in_tree`/`rename_in_tree`/`remove_in_tree`/`symlink_in_tree`/`hardlink_in_tree`/`stat_in_tree`/`symlink_target_in_tree`/`list_in_tree`), файл через CAS (`insert_file` берёт уже открытый `&File` — не байты, чтобы путь резолвился ровно один раз и не было TOCTOU-гонки на подмену файла между чтением и вставкой в CAS; `replace_file` — алиас на `insert_file`, т.к. `Directory::insert` в composefs уже сам upsert-ит; `read_file` — резолвит inline/external и тянет байты из `Repository::read_object` при необходимости); -- `deploy` — постановка деплоя (См. **§5.3**): находит блочное устройство под `/` через `MountInfo`, реальный тип ФС — через `rsblkid::probe::Probe` (не `None`, иначе `mount(2)` падает `EINVAL` — тип ФС ядру нужен явно для любого монтирования, кроме bind/remount), `unshare(CLONE_NEWNS)` + обязательный `MS_REC | MS_PRIVATE`-remount `/` перед реальным монтированием (без этого шага mount-события всё равно утекают в хостовую таблицу через shared propagation, унаследованную от родительского namespace), монтирует раздел в `/sysroot`. `deploy(usr_digest) -> PathBuf` — один компонент пути (`state/deploy//`, см. **§3**). `open_repository()` / `open_tree(name)` — единственная публичная точка доступа к composefs-репозиторию текущего деплоя (См. выше); -- `database` — БД пакетов (реализация через redb) внутри образа. При сборке пишется через свой in-memory `StorageBackend`, в runtime читается `ReadOnlyDatabase` с файла в образе. Там же `record` — `DeployRecord`/`EtcHistoryEntry` (Поля см. **§3** п.12: `usr_digest`/`subject`/`message`/`seq`/`timestamp`/`etc_history`/`working_etc`), физически не часть redb-БД (отдельный `meta.json` на sysroot, не внутри образа), но живёт здесь же по смыслу — *"как наши типы персистятся"* общая забота `database`, независимо от формата. Сериализация — `#[derive(JsonCodec)]` (по образцу `RedbCodec`, тот же по-полевой codegen, только в `serde_json::Value` вместо байт-layout'а); `DeployRecord::write`/`read` пишут/читают файл, `write` — атомарно (tmp-файл в той же директории + `fsync` + `rename`). Своя ошибка `error::DeployRecordError` (отдельно от `DatabaseError` — разные форматы хранения); +- `deploy` — постановка деплоя (См. **§5.3**): находит блочное устройство под `/` через `MountInfo`, реальный тип ФС — через `rsblkid::probe::Probe` (не `None`, иначе `mount(2)` падает `EINVAL` — тип ФС ядру нужен явно для любого монтирования, кроме bind/remount), `unshare(CLONE_NEWNS)` + обязательный `MS_REC | MS_PRIVATE`-remount `/` перед реальным монтированием (без этого шага mount-события всё равно утекают в хостовую таблицу через shared propagation, унаследованную от родительского namespace), монтирует раздел в `/sysroot`. `deploy(prefix_digest) -> PathBuf` — один компонент пути (`state/deploy//`, см. **§3**). `open_repository()` / `open_tree(name)` — единственная публичная точка доступа к composefs-репозиторию текущего деплоя (См. выше); +- `database` — БД пакетов (реализация через redb) внутри образа. При сборке пишется через свой in-memory `StorageBackend`, в runtime читается `ReadOnlyDatabase` с файла в образе. Там же `record` — `DeployRecord`/`EtcHistoryEntry` (Поля см. **§3** п.12: `prefix_digest`/`subject`/`message`/`seq`/`timestamp`/`etc_history`/`working_etc`), физически не часть redb-БД (отдельный `meta.json` на sysroot, не внутри образа), но живёт здесь же по смыслу — *"как наши типы персистятся"* общая забота `database`, независимо от формата. Сериализация — `#[derive(JsonCodec)]` (по образцу `RedbCodec`, тот же по-полевой codegen, только в `serde_json::Value` вместо байт-layout'а); `DeployRecord::write`/`read` пишут/читают файл, `write` — атомарно (tmp-файл в той же директории + `fsync` + `rename`). Своя ошибка `error::DeployRecordError` (отдельно от `DatabaseError` — разные форматы хранения); - `types` — доменные типы (`Version`, `PackageMeta`, `Dependency`, `Targets`...) и per-commands `StateId`-enums (`states`); - `errors` / `lock` — вынесены из `types` в свои топ-уровневые публичные модули: `CommonError` (обёртка над `HookError`/`DecoderError`/`RepoError`/`DatabaseError`/`SysrootError`/`LockError`/`DeployRecordError` — все они теперь тоже публичны, каждый под своим модулем выше) и `Lock`/`LockError` (эксклюзивный системный лок файл, смю **§5.9**); From c93512e6a5810f746ad1a74fe048156e93e2b2a3 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 03:47:02 +0400 Subject: [PATCH 36/68] fix: Fixed an old usr_digest reference Co-Authored-By: Claude Sonnet 5 --- lib/lib/lib.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/lib/lib.toml b/lib/lib/lib.toml index e316343b..eade73eb 100644 --- a/lib/lib/lib.toml +++ b/lib/lib/lib.toml @@ -10,7 +10,7 @@ # # database_path is where the redb file itself lives inside the mounted /usr # tree (relative to that tree's root, e.g. read via FileHandle::read_file -# after Deploy::open_tree(usr_digest), see doc §7). Changing it means +# after Deploy::open_tree(prefix_digest), see doc §7). Changing it means # already-built images stop finding their own embedded DB on read. [database] packages_table_name = "packages" From 58c6ea5510d45ebdb3c16194f39037cac3bc4930 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 04:50:11 +0400 Subject: [PATCH 37/68] new: Added a function to retrieve the list of deployments new: Implemented the first stage of the list_prefix command Co-Authored-By: Claude Sonnet 5 --- lib/lib/src/deploy/mod.rs | 20 +++++++++++- lib/lib/src/errors.rs | 11 +++++++ lib/lib/src/unmutated/list_prefix/error.rs | 8 +++-- lib/lib/src/unmutated/list_prefix/fetching.rs | 32 +++++++++++++++++-- 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/lib/lib/src/deploy/mod.rs b/lib/lib/src/deploy/mod.rs index aed62ade..19d587b8 100644 --- a/lib/lib/src/deploy/mod.rs +++ b/lib/lib/src/deploy/mod.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later -use std::fs::{create_dir_all, remove_dir}; +use std::fs::{create_dir_all, read_dir, remove_dir}; use std::path::{Path, PathBuf}; use composefs::repository::Repository; @@ -84,6 +84,24 @@ impl Deploy { self.deploy.join(prefix_digest) } + pub fn deploys(&self) -> Result, SysrootError> { + let mut digests = Vec::new(); + + for entry in read_dir(&self.deploy)? { + let entry = entry?; + + if !entry.file_type()?.is_dir() { + continue; + } + + if let Some(digest) = entry.file_name().to_str() { + digests.push(digest.to_owned()); + } + } + + Ok(digests) + } + pub fn repo(&self) -> &Path { &self.repo } diff --git a/lib/lib/src/errors.rs b/lib/lib/src/errors.rs index 93044a02..19d14999 100644 --- a/lib/lib/src/errors.rs +++ b/lib/lib/src/errors.rs @@ -69,6 +69,17 @@ macro_rules! lock_error_from { } pub(crate) use lock_error_from; +macro_rules! deploy_record_error_from { + ($name:ident) => { + impl From for $name { + fn from(error: DeployRecordError) -> Self { + $name::Common(CommonError::DeployRecord(error)) + } + } + }; +} +pub(crate) use deploy_record_error_from; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum CommonError { OutOfMemory, diff --git a/lib/lib/src/unmutated/list_prefix/error.rs b/lib/lib/src/unmutated/list_prefix/error.rs index 65afba2f..dfa17682 100644 --- a/lib/lib/src/unmutated/list_prefix/error.rs +++ b/lib/lib/src/unmutated/list_prefix/error.rs @@ -5,9 +5,11 @@ use upac_abi::error::ErrorKind; -use crate::database::error::DatabaseError; +use crate::database::error::{DatabaseError, DeployRecordError}; use crate::deploy::error::SysrootError; -use crate::errors::{CommonError, common_error_from, database_error_from, lock_error_from, sysroot_error_from}; +use crate::errors::{ + CommonError, common_error_from, database_error_from, deploy_record_error_from, lock_error_from, sysroot_error_from, +}; use crate::lock::LockError; #[derive(Debug, Clone, PartialEq, Eq)] @@ -19,6 +21,8 @@ common_error_from!(ListPrefixError); database_error_from!(ListPrefixError); +deploy_record_error_from!(ListPrefixError); + sysroot_error_from!(ListPrefixError); lock_error_from!(ListPrefixError); diff --git a/lib/lib/src/unmutated/list_prefix/fetching.rs b/lib/lib/src/unmutated/list_prefix/fetching.rs index 4522a7f3..1a2e94a2 100644 --- a/lib/lib/src/unmutated/list_prefix/fetching.rs +++ b/lib/lib/src/unmutated/list_prefix/fetching.rs @@ -5,16 +5,42 @@ use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use crate::database::error::DeployRecordError; +use crate::database::record::DeployRecord; +use crate::deploy::{Deploy, DeployMode}; use crate::orchestrator::Context; -use crate::orchestrator::stage::{RollbackGuard, Stage}; +use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage}; +use crate::types::PrefixEntry; use crate::unmutated::list_prefix::ListPrefixError; pub struct FetchingStage; impl Stage for FetchingStage { fn run( - &self, _context: &mut Context, _cancel: &CancelToken, _progress: ProgressEventBuilder, + &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, Box), ListPrefixError> { - todo!() + let deploy = Deploy::new(DeployMode::ReadOnly)?; + + let mut entries = Vec::new(); + + for prefix_digest in deploy.deploys()? { + let record = match DeployRecord::read(&deploy.deploy(&prefix_digest)) { + Ok(record) => record, + Err(DeployRecordError::NotFound) => continue, + Err(error) => return Err(error.into()), + }; + + entries.push(PrefixEntry { + prefix_digest: record.prefix_digest, + subject: record.subject, + message: record.message, + timestamp: record.timestamp, + working_config: Some(record.working_etc), + }); + } + + context.put(entries); + + Ok((progress, Box::new(NoRollback))) } } From eec539ae75ec5d9d4706c993ee58ef79d56af092 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 05:17:02 +0400 Subject: [PATCH 38/68] New: Added separate diff requests for the `prefix` and `config` directories Co-Authored-By: Claude Sonnet 5 --- lib/abi/src/error.rs | 3 ++- lib/abi/src/request.rs | 18 +++++++++++++++--- lib/abi/src/response.rs | 38 ++++++++++++++++++++++++++++++++------ 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/lib/abi/src/error.rs b/lib/abi/src/error.rs index 3d084614..6ed79902 100644 --- a/lib/abi/src/error.rs +++ b/lib/abi/src/error.rs @@ -19,7 +19,8 @@ pub enum ErrorDomain { ListCommit, ListPrefix, ListHistory, - DiffFiles, + DiffFilesPrefix, + DiffFilesConfig, DiffPackages, Diff, SearchMeta, diff --git a/lib/abi/src/request.rs b/lib/abi/src/request.rs index ee691cf0..1eacf4c8 100644 --- a/lib/abi/src/request.rs +++ b/lib/abi/src/request.rs @@ -137,7 +137,19 @@ pub struct CListHistoryRequest { #[repr(C)] #[derive(CValidate)] -pub struct CDiffFilesRequest { +pub struct CDiffFilesPrefixRequest { + pub struct_size: usize, + pub base: CRequestBase, + + #[optional] + pub from_prefix_digest: CSlice, + #[optional] + pub to_prefix_digest: CSlice, +} + +#[repr(C)] +#[derive(CValidate)] +pub struct CDiffFilesConfigRequest { pub struct_size: usize, pub base: CRequestBase, @@ -154,9 +166,9 @@ pub struct CDiffPackagesRequest { pub base: CRequestBase, #[optional] - pub from_commit_hash: CSlice, + pub from_prefix_digest: CSlice, #[optional] - pub to_commit_hash: CSlice, + pub to_prefix_digest: CSlice, } #[repr(C)] diff --git a/lib/abi/src/response.rs b/lib/abi/src/response.rs index 7b539779..b8a317ec 100644 --- a/lib/abi/src/response.rs +++ b/lib/abi/src/response.rs @@ -22,7 +22,7 @@ pub struct CDiffPackageEntry { #[repr(C)] #[derive(CFree, CValidate)] -pub struct CDiffFileEntry { +pub struct CDiffPrefixFileEntry { pub struct_size: usize, pub path: CSlice, @@ -31,6 +31,17 @@ pub struct CDiffFileEntry { pub is_user: bool, } +#[repr(C)] +#[derive(CFree, CValidate)] +pub struct CDiffConfigFileEntry { + pub struct_size: usize, + + pub path: CSlice, + pub kind: DiffKind, + #[optional] + pub package_name: CSlice, +} + #[repr(C)] #[derive(CFree, CValidate)] pub struct CCommitEntry { @@ -172,12 +183,27 @@ impl CListHistoryResponse { } #[repr(C)] -pub struct CDiffFilesResponse { +pub struct CDiffFilesPrefixResponse { + pub struct_size: usize, + pub files: CVec, +} + +impl CDiffFilesPrefixResponse { + /// # Safety + /// Must be called at most once. Assumes every buffer reachable from `self` was allocated by + /// this library (via `CVec::from_owned`/`CSlice::from_owned`), not hand-constructed by the caller. + pub unsafe fn free(&self) { + unsafe { free_cvec_owning(&self.files, |entry| entry.free()) }; + } +} + +#[repr(C)] +pub struct CDiffFilesConfigResponse { pub struct_size: usize, - pub files: CVec, + pub files: CVec, } -impl CDiffFilesResponse { +impl CDiffFilesConfigResponse { /// # Safety /// Must be called at most once. Assumes every buffer reachable from `self` was allocated by /// this library (via `CVec::from_owned`/`CSlice::from_owned`), not hand-constructed by the caller. @@ -204,7 +230,7 @@ impl CDiffPackagesResponse { #[repr(C)] pub struct CDiffResponse { pub struct_size: usize, - pub files: CVec, + pub files: CVec, pub diff_packages: CVec, } @@ -225,7 +251,7 @@ pub struct CUnmutatedResponse { pub struct_size: usize, pub metas: CVec, - pub files: CVec, + pub files: CVec, pub commits: CVec, pub diff_packages: CVec, } From 6bc373395136443b26d758898500653d0d35d311 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 05:18:03 +0400 Subject: [PATCH 39/68] new: Distinguished between `diff` commands for `prefix` and `config` directories new: Added new internal mirror types for these commands Co-Authored-By: Claude Sonnet 5 --- lib/lib/src/export/unmutated/diff.rs | 4 +- lib/lib/src/export/unmutated/diff_files.rs | 47 ------------ .../src/export/unmutated/diff_files_config.rs | 53 +++++++++++++ .../src/export/unmutated/diff_files_prefix.rs | 53 +++++++++++++ lib/lib/src/export/unmutated/mod.rs | 3 +- lib/lib/src/types/mod.rs | 15 +++- lib/lib/src/types/states.rs | 26 ++++++- lib/lib/src/unmutated/diff/mod.rs | 6 +- .../comparing.rs | 6 +- .../error.rs | 16 ++-- .../{diff_files => diff_files_config}/mod.rs | 28 +++---- .../preparing.rs | 6 +- .../unmutated/diff_files_prefix/comparing.rs | 20 +++++ .../src/unmutated/diff_files_prefix/error.rs | 32 ++++++++ .../src/unmutated/diff_files_prefix/mod.rs | 75 +++++++++++++++++++ .../unmutated/diff_files_prefix/preparing.rs | 20 +++++ lib/lib/src/unmutated/diff_packages/mod.rs | 8 +- lib/lib/src/unmutated/mod.rs | 3 +- 18 files changed, 329 insertions(+), 92 deletions(-) delete mode 100644 lib/lib/src/export/unmutated/diff_files.rs create mode 100644 lib/lib/src/export/unmutated/diff_files_config.rs create mode 100644 lib/lib/src/export/unmutated/diff_files_prefix.rs rename lib/lib/src/unmutated/{diff_files => diff_files_config}/comparing.rs (78%) rename lib/lib/src/unmutated/{diff_files => diff_files_config}/error.rs (56%) rename lib/lib/src/unmutated/{diff_files => diff_files_config}/mod.rs (68%) rename lib/lib/src/unmutated/{diff_files => diff_files_config}/preparing.rs (78%) create mode 100644 lib/lib/src/unmutated/diff_files_prefix/comparing.rs create mode 100644 lib/lib/src/unmutated/diff_files_prefix/error.rs create mode 100644 lib/lib/src/unmutated/diff_files_prefix/mod.rs create mode 100644 lib/lib/src/unmutated/diff_files_prefix/preparing.rs diff --git a/lib/lib/src/export/unmutated/diff.rs b/lib/lib/src/export/unmutated/diff.rs index 3f42a8d6..2036a1ac 100644 --- a/lib/lib/src/export/unmutated/diff.rs +++ b/lib/lib/src/export/unmutated/diff.rs @@ -8,7 +8,7 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CDiffRequest; -use upac_abi::response::{CDiffFileEntry, CDiffPackageEntry, CDiffResponse}; +use upac_abi::response::{CDiffPackageEntry, CDiffPrefixFileEntry, CDiffResponse}; use upac_abi::types::{COwned, CVec}; use crate::export::{try_convert_abi, write_error}; @@ -27,7 +27,7 @@ pub unsafe extern "C" fn diff(request_c: CDiffRequest, response_out: *mut CDiffR unsafe { *response_out = CDiffResponse { struct_size: size_of::(), - files: CVec::from_owned(files.into_iter().map(CDiffFileEntry::from).collect()), + files: CVec::from_owned(files.into_iter().map(CDiffPrefixFileEntry::from).collect()), diff_packages: CVec::from_owned( diff_packages.into_iter().map(CDiffPackageEntry::from).collect(), ), diff --git a/lib/lib/src/export/unmutated/diff_files.rs b/lib/lib/src/export/unmutated/diff_files.rs deleted file mode 100644 index 77c551e0..00000000 --- a/lib/lib/src/export/unmutated/diff_files.rs +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 JustPav -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -use std::mem::size_of; -use std::panic::{AssertUnwindSafe, catch_unwind}; - -use upac_abi::error::{CError, ErrorKind}; -use upac_abi::request::CDiffFilesRequest; -use upac_abi::response::{CDiffFileEntry, CDiffFilesResponse}; -use upac_abi::types::{COwned, CVec}; - -use crate::export::{try_convert_abi, write_error}; -use crate::types::states::DiffFilesStateId; -use crate::unmutated::diff_files::DiffFilesData; - -#[unsafe(no_mangle)] -pub unsafe extern "C" fn diff_files( - request_c: CDiffFilesRequest, response_out: *mut CDiffFilesResponse, err_out: *mut CError, -) -> i32 { - let diff_files_data = try_convert_abi!(DiffFilesData::try_from(&request_c), err_out, DiffFilesStateId); - - let result = catch_unwind(AssertUnwindSafe(|| crate::unmutated::diff_files::run(diff_files_data))); - - match result { - Ok(Ok((files,))) => { - if !response_out.is_null() { - unsafe { - *response_out = CDiffFilesResponse { - struct_size: size_of::(), - files: CVec::from_owned(files.into_iter().map(CDiffFileEntry::from).collect()), - }; - } - } - 0 - } - Ok(Err((state, error))) => { - unsafe { write_error(err_out, state, ErrorKind::from(error)) }; - -1 - } - Err(_) => { - unsafe { write_error(err_out, DiffFilesStateId::Setup, ErrorKind::Unexpected) }; - -1 - } - } -} diff --git a/lib/lib/src/export/unmutated/diff_files_config.rs b/lib/lib/src/export/unmutated/diff_files_config.rs new file mode 100644 index 00000000..6799afaf --- /dev/null +++ b/lib/lib/src/export/unmutated/diff_files_config.rs @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 JustPav +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +use std::mem::size_of; +use std::panic::{AssertUnwindSafe, catch_unwind}; + +use upac_abi::error::{CError, ErrorKind}; +use upac_abi::request::CDiffFilesConfigRequest; +use upac_abi::response::{CDiffConfigFileEntry, CDiffFilesConfigResponse}; +use upac_abi::types::{COwned, CVec}; + +use crate::export::{try_convert_abi, write_error}; +use crate::types::states::DiffFilesConfigStateId; +use crate::unmutated::diff_files_config::DiffFilesConfigData; + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn diff_files_config( + request_c: CDiffFilesConfigRequest, response_out: *mut CDiffFilesConfigResponse, err_out: *mut CError, +) -> i32 { + let diff_files_config_data = try_convert_abi!( + DiffFilesConfigData::try_from(&request_c), + err_out, + DiffFilesConfigStateId + ); + + let result = catch_unwind(AssertUnwindSafe(|| { + crate::unmutated::diff_files_config::run(diff_files_config_data) + })); + + match result { + Ok(Ok((files,))) => { + if !response_out.is_null() { + unsafe { + *response_out = CDiffFilesConfigResponse { + struct_size: size_of::(), + files: CVec::from_owned(files.into_iter().map(CDiffConfigFileEntry::from).collect()), + }; + } + } + 0 + } + Ok(Err((state, error))) => { + unsafe { write_error(err_out, state, ErrorKind::from(error)) }; + -1 + } + Err(_) => { + unsafe { write_error(err_out, DiffFilesConfigStateId::Setup, ErrorKind::Unexpected) }; + -1 + } + } +} diff --git a/lib/lib/src/export/unmutated/diff_files_prefix.rs b/lib/lib/src/export/unmutated/diff_files_prefix.rs new file mode 100644 index 00000000..0840d71c --- /dev/null +++ b/lib/lib/src/export/unmutated/diff_files_prefix.rs @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 JustPav +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +use std::mem::size_of; +use std::panic::{AssertUnwindSafe, catch_unwind}; + +use upac_abi::error::{CError, ErrorKind}; +use upac_abi::request::CDiffFilesPrefixRequest; +use upac_abi::response::{CDiffFilesPrefixResponse, CDiffPrefixFileEntry}; +use upac_abi::types::{COwned, CVec}; + +use crate::export::{try_convert_abi, write_error}; +use crate::types::states::DiffFilesPrefixStateId; +use crate::unmutated::diff_files_prefix::DiffFilesPrefixData; + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn diff_files_prefix( + request_c: CDiffFilesPrefixRequest, response_out: *mut CDiffFilesPrefixResponse, err_out: *mut CError, +) -> i32 { + let diff_files_prefix_data = try_convert_abi!( + DiffFilesPrefixData::try_from(&request_c), + err_out, + DiffFilesPrefixStateId + ); + + let result = catch_unwind(AssertUnwindSafe(|| { + crate::unmutated::diff_files_prefix::run(diff_files_prefix_data) + })); + + match result { + Ok(Ok((files,))) => { + if !response_out.is_null() { + unsafe { + *response_out = CDiffFilesPrefixResponse { + struct_size: size_of::(), + files: CVec::from_owned(files.into_iter().map(CDiffPrefixFileEntry::from).collect()), + }; + } + } + 0 + } + Ok(Err((state, error))) => { + unsafe { write_error(err_out, state, ErrorKind::from(error)) }; + -1 + } + Err(_) => { + unsafe { write_error(err_out, DiffFilesPrefixStateId::Setup, ErrorKind::Unexpected) }; + -1 + } + } +} diff --git a/lib/lib/src/export/unmutated/mod.rs b/lib/lib/src/export/unmutated/mod.rs index ff46d9ae..2dc7226f 100644 --- a/lib/lib/src/export/unmutated/mod.rs +++ b/lib/lib/src/export/unmutated/mod.rs @@ -4,7 +4,8 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pub mod diff; -pub mod diff_files; +pub mod diff_files_config; +pub mod diff_files_prefix; pub mod diff_packages; pub mod list_commit; pub mod list_history; diff --git a/lib/lib/src/types/mod.rs b/lib/lib/src/types/mod.rs index 978ed7e4..a622a942 100644 --- a/lib/lib/src/types/mod.rs +++ b/lib/lib/src/types/mod.rs @@ -10,7 +10,8 @@ use upac_abi::decoder::CDependency; use upac_abi::error::ErrorKind; use upac_abi::package::{CPackageMeta, CUnpackedPackage, CVersion}; use upac_abi::response::{ - CCommitEntry, CDiffFileEntry, CDiffPackageEntry, CHistoryEntry, CPrefixEntry, CSearchFileEntry, + CCommitEntry, CDiffConfigFileEntry, CDiffPackageEntry, CDiffPrefixFileEntry, CHistoryEntry, CPrefixEntry, + CSearchFileEntry, }; use upac_abi::types::{CBorrowed, COwned, CSlice, CVec}; use upac_macro::{CTryToRust, RedbCodec, RustToC}; @@ -151,15 +152,23 @@ pub struct HistoryEntry { pub config_history: Vec, } -// ── DiffFileEntry ─────────────────────────────────────────────────────────── +// ── DiffPrefixFileEntry ───────────────────────────────────────────────────── #[derive(Debug, Clone, RustToC)] -pub struct DiffFileEntry { +pub struct DiffPrefixFileEntry { pub path: String, pub kind: DiffKind, pub package_name: String, pub is_user: bool, } +// ── DiffConfigFileEntry ───────────────────────────────────────────────────── +#[derive(Debug, Clone, RustToC)] +pub struct DiffConfigFileEntry { + pub path: String, + pub kind: DiffKind, + pub package_name: Option, +} + // ── DiffPackageEntry ──────────────────────────────────────────────────────── #[derive(Debug, Clone, RustToC)] pub struct DiffPackageEntry { diff --git a/lib/lib/src/types/states.rs b/lib/lib/src/types/states.rs index f29b8e1d..c1913ace 100644 --- a/lib/lib/src/types/states.rs +++ b/lib/lib/src/types/states.rs @@ -165,16 +165,34 @@ impl CommandState for ListHistoryStateId { #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq, FromStageIndex)] -pub enum DiffFilesStateId { +pub enum DiffFilesPrefixStateId { Preparing = 0, Comparing = 1, Done = 2, Setup = 3, } -impl CommandState for DiffFilesStateId { - const DOMAIN: ErrorDomain = ErrorDomain::DiffFiles; - const VALIDATION: Self = DiffFilesStateId::Setup; +impl CommandState for DiffFilesPrefixStateId { + const DOMAIN: ErrorDomain = ErrorDomain::DiffFilesPrefix; + const VALIDATION: Self = DiffFilesPrefixStateId::Setup; + + fn as_u32(self) -> u32 { + self as u32 + } +} + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromStageIndex)] +pub enum DiffFilesConfigStateId { + Preparing = 0, + Comparing = 1, + Done = 2, + Setup = 3, +} + +impl CommandState for DiffFilesConfigStateId { + const DOMAIN: ErrorDomain = ErrorDomain::DiffFilesConfig; + const VALIDATION: Self = DiffFilesConfigStateId::Setup; fn as_u32(self) -> u32 { self as u32 diff --git a/lib/lib/src/unmutated/diff/mod.rs b/lib/lib/src/unmutated/diff/mod.rs index 1a398569..60fb3bac 100644 --- a/lib/lib/src/unmutated/diff/mod.rs +++ b/lib/lib/src/unmutated/diff/mod.rs @@ -16,7 +16,7 @@ use self::preparing::PreparingStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; use crate::types::states::DiffStateId; -use crate::types::{DiffFileEntry, DiffPackageEntry}; +use crate::types::{DiffPackageEntry, DiffPrefixFileEntry}; mod comparing; mod error; @@ -56,7 +56,7 @@ fn assemble() -> SequentialOrchestrator { SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]) } -pub fn run(data: DiffData) -> Result<(Vec, Vec), (DiffStateId, DiffError)> { +pub fn run(data: DiffData) -> Result<(Vec, Vec), (DiffStateId, DiffError)> { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); @@ -68,7 +68,7 @@ pub fn run(data: DiffData) -> Result<(Vec, Vec) data.cancel_token, DiffStateId, DiffError, - Vec, + Vec, Vec ) } diff --git a/lib/lib/src/unmutated/diff_files/comparing.rs b/lib/lib/src/unmutated/diff_files_config/comparing.rs similarity index 78% rename from lib/lib/src/unmutated/diff_files/comparing.rs rename to lib/lib/src/unmutated/diff_files_config/comparing.rs index ff614420..8a321e66 100644 --- a/lib/lib/src/unmutated/diff_files/comparing.rs +++ b/lib/lib/src/unmutated/diff_files_config/comparing.rs @@ -7,14 +7,14 @@ use upac_abi::hook::{CancelToken, ProgressEventBuilder}; use crate::orchestrator::Context; use crate::orchestrator::stage::{RollbackGuard, Stage}; -use crate::unmutated::diff_files::DiffFilesError; +use crate::unmutated::diff_files_config::DiffFilesConfigError; pub struct ComparingStage; -impl Stage for ComparingStage { +impl Stage for ComparingStage { fn run( &self, _context: &mut Context, _cancel: &CancelToken, _progress: ProgressEventBuilder, - ) -> Result<(ProgressEventBuilder, Box), DiffFilesError> { + ) -> Result<(ProgressEventBuilder, Box), DiffFilesConfigError> { todo!() } } diff --git a/lib/lib/src/unmutated/diff_files/error.rs b/lib/lib/src/unmutated/diff_files_config/error.rs similarity index 56% rename from lib/lib/src/unmutated/diff_files/error.rs rename to lib/lib/src/unmutated/diff_files_config/error.rs index c24f5e2f..274dd1af 100644 --- a/lib/lib/src/unmutated/diff_files/error.rs +++ b/lib/lib/src/unmutated/diff_files_config/error.rs @@ -11,22 +11,22 @@ use crate::errors::{CommonError, common_error_from, database_error_from, lock_er use crate::lock::LockError; #[derive(Debug, Clone, PartialEq, Eq)] -pub enum DiffFilesError { +pub enum DiffFilesConfigError { Common(CommonError), } -common_error_from!(DiffFilesError); +common_error_from!(DiffFilesConfigError); -database_error_from!(DiffFilesError); +database_error_from!(DiffFilesConfigError); -sysroot_error_from!(DiffFilesError); +sysroot_error_from!(DiffFilesConfigError); -lock_error_from!(DiffFilesError); +lock_error_from!(DiffFilesConfigError); -impl From for ErrorKind { - fn from(error: DiffFilesError) -> Self { +impl From for ErrorKind { + fn from(error: DiffFilesConfigError) -> Self { match error { - DiffFilesError::Common(common_error) => common_error.into(), + DiffFilesConfigError::Common(common_error) => common_error.into(), } } } diff --git a/lib/lib/src/unmutated/diff_files/mod.rs b/lib/lib/src/unmutated/diff_files_config/mod.rs similarity index 68% rename from lib/lib/src/unmutated/diff_files/mod.rs rename to lib/lib/src/unmutated/diff_files_config/mod.rs index 4ab53fe9..ac68afd6 100644 --- a/lib/lib/src/unmutated/diff_files/mod.rs +++ b/lib/lib/src/unmutated/diff_files_config/mod.rs @@ -7,22 +7,22 @@ use std::os::raw::c_void; use upac_abi::error::ErrorKind; use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; -use upac_abi::request::CDiffFilesRequest; +use upac_abi::request::CDiffFilesConfigRequest; -pub use self::error::DiffFilesError; +pub use self::error::DiffFilesConfigError; use self::comparing::ComparingStage; use self::preparing::PreparingStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use crate::types::DiffFileEntry; -use crate::types::states::DiffFilesStateId; +use crate::types::DiffConfigFileEntry; +use crate::types::states::DiffFilesConfigStateId; mod comparing; mod error; mod preparing; -pub struct DiffFilesData<'a> { +pub struct DiffFilesConfigData<'a> { pub from_commit_hash: Option<&'a str>, pub to_commit_hash: Option<&'a str>, @@ -32,15 +32,15 @@ pub struct DiffFilesData<'a> { pub cancel_token: &'a CancelToken, } -impl<'a> TryFrom<&'a CDiffFilesRequest> for DiffFilesData<'a> { +impl<'a> TryFrom<&'a CDiffFilesConfigRequest> for DiffFilesConfigData<'a> { type Error = ErrorKind; - fn try_from(request: &'a CDiffFilesRequest) -> Result { + fn try_from(request: &'a CDiffFilesConfigRequest) -> Result { unsafe { request.validate()? }; let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; - Ok(DiffFilesData { + Ok(DiffFilesConfigData { from_commit_hash: (&request.from_commit_hash).try_into()?, to_commit_hash: (&request.to_commit_hash).try_into()?, @@ -52,11 +52,13 @@ impl<'a> TryFrom<&'a CDiffFilesRequest> for DiffFilesData<'a> { } } -fn assemble() -> SequentialOrchestrator { +fn assemble() -> SequentialOrchestrator { SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]) } -pub fn run(data: DiffFilesData) -> Result<(Vec,), (DiffFilesStateId, DiffFilesError)> { +pub fn run( + data: DiffFilesConfigData, +) -> Result<(Vec,), (DiffFilesConfigStateId, DiffFilesConfigError)> { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); @@ -66,8 +68,8 @@ pub fn run(data: DiffFilesData) -> Result<(Vec,), (DiffFilesState orchestrator, context, data.cancel_token, - DiffFilesStateId, - DiffFilesError, - Vec + DiffFilesConfigStateId, + DiffFilesConfigError, + Vec ) } diff --git a/lib/lib/src/unmutated/diff_files/preparing.rs b/lib/lib/src/unmutated/diff_files_config/preparing.rs similarity index 78% rename from lib/lib/src/unmutated/diff_files/preparing.rs rename to lib/lib/src/unmutated/diff_files_config/preparing.rs index 29f081cb..9309b665 100644 --- a/lib/lib/src/unmutated/diff_files/preparing.rs +++ b/lib/lib/src/unmutated/diff_files_config/preparing.rs @@ -7,14 +7,14 @@ use upac_abi::hook::{CancelToken, ProgressEventBuilder}; use crate::orchestrator::Context; use crate::orchestrator::stage::{RollbackGuard, Stage}; -use crate::unmutated::diff_files::DiffFilesError; +use crate::unmutated::diff_files_config::DiffFilesConfigError; pub struct PreparingStage; -impl Stage for PreparingStage { +impl Stage for PreparingStage { fn run( &self, _context: &mut Context, _cancel: &CancelToken, _progress: ProgressEventBuilder, - ) -> Result<(ProgressEventBuilder, Box), DiffFilesError> { + ) -> Result<(ProgressEventBuilder, Box), DiffFilesConfigError> { todo!() } } diff --git a/lib/lib/src/unmutated/diff_files_prefix/comparing.rs b/lib/lib/src/unmutated/diff_files_prefix/comparing.rs new file mode 100644 index 00000000..00792211 --- /dev/null +++ b/lib/lib/src/unmutated/diff_files_prefix/comparing.rs @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 JustPav +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +use upac_abi::hook::{CancelToken, ProgressEventBuilder}; + +use crate::orchestrator::Context; +use crate::orchestrator::stage::{RollbackGuard, Stage}; +use crate::unmutated::diff_files_prefix::DiffFilesPrefixError; + +pub struct ComparingStage; + +impl Stage for ComparingStage { + fn run( + &self, _context: &mut Context, _cancel: &CancelToken, _progress: ProgressEventBuilder, + ) -> Result<(ProgressEventBuilder, Box), DiffFilesPrefixError> { + todo!() + } +} diff --git a/lib/lib/src/unmutated/diff_files_prefix/error.rs b/lib/lib/src/unmutated/diff_files_prefix/error.rs new file mode 100644 index 00000000..9f645d10 --- /dev/null +++ b/lib/lib/src/unmutated/diff_files_prefix/error.rs @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 JustPav +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +use upac_abi::error::ErrorKind; + +use crate::database::error::DatabaseError; +use crate::deploy::error::SysrootError; +use crate::errors::{CommonError, common_error_from, database_error_from, lock_error_from, sysroot_error_from}; +use crate::lock::LockError; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DiffFilesPrefixError { + Common(CommonError), +} + +common_error_from!(DiffFilesPrefixError); + +database_error_from!(DiffFilesPrefixError); + +sysroot_error_from!(DiffFilesPrefixError); + +lock_error_from!(DiffFilesPrefixError); + +impl From for ErrorKind { + fn from(error: DiffFilesPrefixError) -> Self { + match error { + DiffFilesPrefixError::Common(common_error) => common_error.into(), + } + } +} diff --git a/lib/lib/src/unmutated/diff_files_prefix/mod.rs b/lib/lib/src/unmutated/diff_files_prefix/mod.rs new file mode 100644 index 00000000..9ea2a284 --- /dev/null +++ b/lib/lib/src/unmutated/diff_files_prefix/mod.rs @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 JustPav +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +use std::os::raw::c_void; + +use upac_abi::error::ErrorKind; +use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::request::CDiffFilesPrefixRequest; + +pub use self::error::DiffFilesPrefixError; + +use self::comparing::ComparingStage; +use self::preparing::PreparingStage; + +use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; +use crate::types::DiffPrefixFileEntry; +use crate::types::states::DiffFilesPrefixStateId; + +mod comparing; +mod error; +mod preparing; + +pub struct DiffFilesPrefixData<'a> { + pub from_prefix_digest: Option<&'a str>, + pub to_prefix_digest: Option<&'a str>, + + pub hook_message: Option, + pub hook_message_context: *mut c_void, + + pub cancel_token: &'a CancelToken, +} + +impl<'a> TryFrom<&'a CDiffFilesPrefixRequest> for DiffFilesPrefixData<'a> { + type Error = ErrorKind; + + fn try_from(request: &'a CDiffFilesPrefixRequest) -> Result { + unsafe { request.validate()? }; + + let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + + Ok(DiffFilesPrefixData { + from_prefix_digest: (&request.from_prefix_digest).try_into()?, + to_prefix_digest: (&request.to_prefix_digest).try_into()?, + + hook_message: request.base.on_hook, + hook_message_context: request.base.hook_ctx, + + cancel_token, + }) + } +} + +fn assemble() -> SequentialOrchestrator { + SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]) +} + +pub fn run( + data: DiffFilesPrefixData, +) -> Result<(Vec,), (DiffFilesPrefixStateId, DiffFilesPrefixError)> { + let mut context = Context::new(); + context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); + + let orchestrator = assemble(); + + run_unmutated!( + orchestrator, + context, + data.cancel_token, + DiffFilesPrefixStateId, + DiffFilesPrefixError, + Vec + ) +} diff --git a/lib/lib/src/unmutated/diff_files_prefix/preparing.rs b/lib/lib/src/unmutated/diff_files_prefix/preparing.rs new file mode 100644 index 00000000..39c31d63 --- /dev/null +++ b/lib/lib/src/unmutated/diff_files_prefix/preparing.rs @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 JustPav +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +use upac_abi::hook::{CancelToken, ProgressEventBuilder}; + +use crate::orchestrator::Context; +use crate::orchestrator::stage::{RollbackGuard, Stage}; +use crate::unmutated::diff_files_prefix::DiffFilesPrefixError; + +pub struct PreparingStage; + +impl Stage for PreparingStage { + fn run( + &self, _context: &mut Context, _cancel: &CancelToken, _progress: ProgressEventBuilder, + ) -> Result<(ProgressEventBuilder, Box), DiffFilesPrefixError> { + todo!() + } +} diff --git a/lib/lib/src/unmutated/diff_packages/mod.rs b/lib/lib/src/unmutated/diff_packages/mod.rs index d9820b05..bd3f93c8 100644 --- a/lib/lib/src/unmutated/diff_packages/mod.rs +++ b/lib/lib/src/unmutated/diff_packages/mod.rs @@ -23,8 +23,8 @@ mod error; mod preparing; pub struct DiffPackagesData<'a> { - pub from_commit_hash: Option<&'a str>, - pub to_commit_hash: Option<&'a str>, + pub from_prefix_digest: Option<&'a str>, + pub to_prefix_digest: Option<&'a str>, pub hook_message: Option, pub hook_message_context: *mut c_void, @@ -41,8 +41,8 @@ impl<'a> TryFrom<&'a CDiffPackagesRequest> for DiffPackagesData<'a> { let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; Ok(DiffPackagesData { - from_commit_hash: (&request.from_commit_hash).try_into()?, - to_commit_hash: (&request.to_commit_hash).try_into()?, + from_prefix_digest: (&request.from_prefix_digest).try_into()?, + to_prefix_digest: (&request.to_prefix_digest).try_into()?, hook_message: request.base.on_hook, hook_message_context: request.base.hook_ctx, diff --git a/lib/lib/src/unmutated/mod.rs b/lib/lib/src/unmutated/mod.rs index ff46d9ae..2dc7226f 100644 --- a/lib/lib/src/unmutated/mod.rs +++ b/lib/lib/src/unmutated/mod.rs @@ -4,7 +4,8 @@ // SPDX-License-Identifier: LGPL-3.0-or-later pub mod diff; -pub mod diff_files; +pub mod diff_files_config; +pub mod diff_files_prefix; pub mod diff_packages; pub mod list_commit; pub mod list_history; From 68f10c9f99e41c47a3a33f91703fcb8f994f87b7 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 05:37:21 +0400 Subject: [PATCH 40/68] fix: Fixed formatting --- lib/macro/src/common.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/macro/src/common.rs b/lib/macro/src/common.rs index 784c7ffa..59c166aa 100644 --- a/lib/macro/src/common.rs +++ b/lib/macro/src/common.rs @@ -19,7 +19,8 @@ pub(crate) const VALIDATABLE_COMPOSITES: &[&str] = &[ "CPackageMeta", "CUnpackedPackage", "CPackageInfo", - "CDiffFileEntry", + "CDiffPrefixFileEntry", + "CDiffConfigFileEntry", "CCommitEntry", "CRequestBase", "CDependency", From a01395a5c2ad49e25f8e80a01271524794f021f5 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 05:37:28 +0400 Subject: [PATCH 41/68] fix: Fixed formatting --- lib/abi/src/response.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/abi/src/response.rs b/lib/abi/src/response.rs index b8a317ec..93108078 100644 --- a/lib/abi/src/response.rs +++ b/lib/abi/src/response.rs @@ -18,6 +18,7 @@ pub struct CDiffPackageEntry { pub name: CSlice, pub kind: DiffKind, pub version: CVersion, + pub files: CVec, } #[repr(C)] @@ -230,7 +231,7 @@ impl CDiffPackagesResponse { #[repr(C)] pub struct CDiffResponse { pub struct_size: usize, - pub files: CVec, + pub unattached_files: CVec, pub diff_packages: CVec, } @@ -240,7 +241,7 @@ impl CDiffResponse { /// this library (via `CVec::from_owned`/`CSlice::from_owned`), not hand-constructed by the caller. pub unsafe fn free(&self) { unsafe { - free_cvec_owning(&self.files, |entry| entry.free()); + free_cvec_owning(&self.unattached_files, |entry| entry.free()); free_cvec_owning(&self.diff_packages, |entry| entry.free()); } } From df20b2161d2268f86389ed4b7342e05ae09ecc9e Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 05:37:59 +0400 Subject: [PATCH 42/68] new: External diff functions stabilized Co-Authored-By: Claude Sonnet 5 --- lib/lib/src/export/unmutated/diff.rs | 6 ++++-- lib/lib/src/types/mod.rs | 5 +++++ lib/lib/src/unmutated/diff/mod.rs | 6 +++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/lib/src/export/unmutated/diff.rs b/lib/lib/src/export/unmutated/diff.rs index 2036a1ac..fc53b058 100644 --- a/lib/lib/src/export/unmutated/diff.rs +++ b/lib/lib/src/export/unmutated/diff.rs @@ -22,12 +22,14 @@ pub unsafe extern "C" fn diff(request_c: CDiffRequest, response_out: *mut CDiffR let result = catch_unwind(AssertUnwindSafe(|| crate::unmutated::diff::run(diff_data))); match result { - Ok(Ok((files, diff_packages))) => { + Ok(Ok((diff_packages, unattached_files))) => { if !response_out.is_null() { unsafe { *response_out = CDiffResponse { struct_size: size_of::(), - files: CVec::from_owned(files.into_iter().map(CDiffPrefixFileEntry::from).collect()), + unattached_files: CVec::from_owned( + unattached_files.into_iter().map(CDiffPrefixFileEntry::from).collect(), + ), diff_packages: CVec::from_owned( diff_packages.into_iter().map(CDiffPackageEntry::from).collect(), ), diff --git a/lib/lib/src/types/mod.rs b/lib/lib/src/types/mod.rs index a622a942..1fa06d19 100644 --- a/lib/lib/src/types/mod.rs +++ b/lib/lib/src/types/mod.rs @@ -175,6 +175,11 @@ pub struct DiffPackageEntry { pub name: String, pub kind: DiffKind, pub version: Version, + + // Only this package's own files. A changed file with no package to + // attach to is not here — it's in `diff::run()`'s separate + // unattached-files return value. + pub files: Vec, } pub struct Targets(pub Vec); diff --git a/lib/lib/src/unmutated/diff/mod.rs b/lib/lib/src/unmutated/diff/mod.rs index 60fb3bac..d4d603ad 100644 --- a/lib/lib/src/unmutated/diff/mod.rs +++ b/lib/lib/src/unmutated/diff/mod.rs @@ -56,7 +56,7 @@ fn assemble() -> SequentialOrchestrator { SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]) } -pub fn run(data: DiffData) -> Result<(Vec, Vec), (DiffStateId, DiffError)> { +pub fn run(data: DiffData) -> Result<(Vec, Vec), (DiffStateId, DiffError)> { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); @@ -68,7 +68,7 @@ pub fn run(data: DiffData) -> Result<(Vec, Vec, - Vec + Vec, + Vec ) } From 6f9a2d3ac306ff66013c736b7e6a5d314c6295cc Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 06:27:51 +0400 Subject: [PATCH 43/68] fix: Documentation chapters 0 through 5 have been translated Co-Authored-By: Claude Sonnet 5 --- doc/eng/Upac chapter 0.md | 111 ++++++++++++++++++ doc/eng/Upac chapter 1.md | 59 ++++++++++ doc/eng/Upac chapter 2.md | 21 ++++ doc/eng/Upac chapter 3.md | 61 ++++++++++ doc/eng/Upac chapter 4.md | 87 ++++++++++++++ doc/eng/Upac chapter 5.md | 234 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 573 insertions(+) create mode 100644 doc/eng/Upac chapter 0.md create mode 100644 doc/eng/Upac chapter 1.md create mode 100644 doc/eng/Upac chapter 2.md create mode 100644 doc/eng/Upac chapter 3.md create mode 100644 doc/eng/Upac chapter 4.md create mode 100644 doc/eng/Upac chapter 5.md diff --git a/doc/eng/Upac chapter 0.md b/doc/eng/Upac chapter 0.md new file mode 100644 index 00000000..3d43599e --- /dev/null +++ b/doc/eng/Upac chapter 0.md @@ -0,0 +1,111 @@ + + +# **UPAC — the unified project document.** + +Project document. +Project branch: **`lib-rs`**, crate `lib-rust/`. + +## **§0.** Introduction and definitions. + +This paragraph defines the terms used further in the text without explanation. + +### **Item 1.** Basic concepts. + +- **File** — a named area of memory in which a certain amount of information is stored; +- **File extension** — a suffix at the end of a file name, separated by a dot, which tells the operating system and programs the format of the data contained in the file and determines which application should be used to open or process it; +- **Directory (folder)** — a container for files and other directories; +- **Path** — the address of a file or directory that specifies its exact location in the file system. +- **File system (FS)** — the order and rules by which the operating system organizes, writes, finds, and stores files on a disk or flash drive, for example EXT4, Btrfs, NTFS. +- Ref (reference / link) — an exact address or pointer in the file system that references a specific file, state, or data slice, allowing it to be accessed without duplication; +- **Disk / block device** — a storage device (physical or virtual); +- **Partition** — a dedicated part of a disk on which a single FS resides; +- **MBR (Master Boot Record)** — an outdated boot sector structure format, in use since 1983, which stores the primary bootloader code and the partition table, limiting the drive to a maximum of 4 primary partitions and a capacity of up to 2 TB; +- **GPT (GUID Partition Table)** — a drive partition table standard that allows creating many partitions and working with disks larger than 2 terabytes; +- **Mounting (mount)** — the *"attaching"* of a partition's FS to a point in the directory tree, after which the contents of the file system become available at that path; +- **Kernel** — the main program of the operating system, which acts as an intermediary between applications and the computer's hardware, distributing resources (memory, processor, devices); +- Userspace — the area of memory in which all regular programs and applications run: browsers, text editors, the system interface, isolated from direct access to the memory area used to manage hardware, for the sake of the security of the whole system; +- Kernelspace — a protected area of memory in which the operating system kernel and drivers run; it has full and direct access to the processor, RAM, and all of the computer's hardware; +- **Initramfs** — a temporary file system in RAM (random-access memory) that contains the drivers and programs needed for the kernel to find, decrypt, or prepare the main disk and boot the main system from it; +- **Unified Kernel Image (UKI)** — a single executable file in EFI format that combines the Linux kernel, the initialization image (initramfs), the kernel command-line parameters, and the EFI bootloader into one indivisible module; +- **Firmware** — basic software written into the non-volatile memory of a chip on a motherboard or device — a graphics card, an SSD, a controller — serving as the main bridge between the physical hardware and the operating system, initializing components at power-on and providing low-level instructions for controlling them; +- **BIOS (Basic Input Output System)** — an outdated 1975 firmware standard that cannot read file systems and reads boot code from the disk's first 512-byte sector (MBR), which makes it poll hardware sequentially, work slowly, and be limited to drives of up to 2 TB; +- **UEFI (Unified Extensible Firmware Interface)** — a modern 2005 firmware standard capable of fully reading the FAT32 file system on a special partition (ESP) and launching .efi files from it directly, which allows it to work with GPT disks of any size, poll hardware in parallel, and verify code signatures via Secure Boot; +- **ESP (EFI System Partition)** — a system partition, usually with a FAT32 file system, that is read directly by the UEFI firmware when the computer is powered on. It stores bootloader files in .efi format, Secure Boot keys, and the startup components needed for the first phase of the hardware boot. +- **Secure Boot** — a UEFI firmware feature that blocks the execution of any third-party code at PC power-on by verifying that code's signature; +- **Bootloader** — a program launched by the board's firmware (UEFI/BIOS) to give the user a choice of OS, load the required parameters, and start the kernel from disk; +- **Cmdline (kernel command line)** — a text string of instructions and settings that the bootloader passes to the kernel at startup to determine the system's operating mode (for example, to specify the root disk, enable debugging, or disable a driver); +- **Hash function** — an algorithm that converts any amount of data into a sequence of fixed length. Identical data always produces an identical code, while any different data produces, for all practical purposes, a guaranteed unique one; +- **Hash (digest)** — a unique short sequence obtained by processing data with a hash function. If the data has not changed, the hash is always identical; if even one element of the sequence changes, the resulting code changes completely; +- **Package** — an archive of a program's files and its metadata: a list of files the system needs for it to work, and installation instructions, which the package manager installs, updates, or removes as a single unit; +- **Repository** — a package store in which program files sit alongside their signatures and a single index/catalog. This lets the package manager automatically find the required versions, verify their authenticity, and download the correct dependencies; +- **Digital signature** — an encrypted stamp from the developer, attached to a package: it guarantees authorship, i.e. the fact that the current code belongs to a specific party, and integrity, i.e. the absence of any changes to the program since its creation by the developer; +- **Package manager** — a program that automatically downloads, installs, updates, and removes programs from a repository, and that also finds and installs, by itself, all the programs required for them to work (dependencies); +- **Atomicity** — the property of indivisibility: an operation either applies in full or does not apply at all, with no intermediate, half-applied states; +- **Immutable system** — an operating system architecture in which system files are protected from modification while the system is running, and any updates are applied atomically and placed alongside the existing state, allowing an instant rollback to the previous working state at any time; +- Persistent (or persistence) — the property of data or settings being preserved even after the computer is turned off, the system is rebooted, or the program is closed; +- **POSIX (Portable Operating System Interface)** — a family of IEEE and ISO standards defining a unified programming interface (API), system utilities, and command-line behavior for Unix-like operating systems; +- **FHS (Filesystem Hierarchy Standard)** — a standard defining the structure, naming, and purpose of the main directories in Unix-like operating systems; +- Script — a small program or sequence of commands, most often implemented as a text file of instructions, that the system or another program executes step by step to automate everyday tasks; +- Hook — an automatic handler that runs at a specific point during a program's operation, for example before or after a package is installed, to execute a given command or script; +- Trigger — an automatic condition-signal that, when a specific event occurs, immediately launches a predefined action or program; +- DB (database) — an organized store of information on disk from which programs can quickly find, add, modify, and safely save the data they need; +- Commit — a saved point, or *"snapshot,"* of state in a version control system, for example Git, which records all changes to files at a given moment and allows returning to them if necessary; +- Deploy (deployment) — the process of transferring, configuring, and launching a finished program, system image, or file tree onto a real working server or device, where the deployed content becomes available for use; +- CLI (command line interface) — a text-based way of controlling a program, in which the user enters commands into a terminal from the keyboard instead of clicking buttons with a mouse. +- Pivot (pivot root) — the operation of switching a running system from a temporary initial disk, for example initramfs during boot, to the main, real disk partition, which becomes the new root `/`; +- PID 1 (process ID 1) — the first and most important process launched by the Linux kernel at power-on, for example systemd, responsible for starting all other programs and managing the entire operation of the system until it shuts down; +- Unit — a basic control element in a system manager, for example systemd, representing a text file of settings for starting and controlling a specific service, task, device, or mount point; +- Engine — a base program or subsystem that performs all the complex internal work — calculations, logic, data processing — so as to simplify development and avoid recreating these functions from scratch for every new application; +- OCI (Open Container Initiative) — an open industry standard for the container image format and its runtime environment, guaranteeing that containers run identically on any platform and engine (for example Docker, Podman, Kubernetes/CRI-O); +- CA (certificate authority) — a body or service that issues and signs digital SSL/TLS certificates. A CA's signature confirms that a public key genuinely belongs to the specified domain, server, or user; +- Root CA — the topmost, principal level in the chain of trust. A Root CA owns a self-signed root certificate that is pre-embedded in operating systems and browsers as fully trusted, and that vouches for the other certificates signed on its behalf; +- Public key — a key that is publicly available and used to encrypt data or verify a digital signature. It can be freely given to anyone; +- Private key — a secret key known only to its owner, used to decrypt data or create a digital signature. Loss or leakage of this key compromises the entire protection. + +### **Item 2.** Definitions of the folders used by the system. + +- **`/`** — the topmost directory in the file system hierarchy (FHS), from which all other folders and mounted disks branch off; +- **`/usr`** — the system folder containing executable files (programs): binaries, libraries, and other system resources. On immutable systems this directory is mounted read-only (ro), and it is updated entirely, as a full replacement of one version with another, which is exactly what allows the whole operating system to be rolled back instantly on failure; +- **`/etc`** — the directory of configuration files. On immutable systems it remains writable so the user can change settings, and when the /usr layer is updated or rolled back its contents are automatically merged to keep the settings consistent; +- **`/var`** — the directory for mutable data that programs create and update while running: logs, databases, cache, print queues. This folder is always open for writing and is fully preserved across any system updates or rollbacks; +- **`/home`** — the directory for users' personal files: documents, downloads, projects, and their individual program settings. This folder is fully isolated from the OS files, is writable, and is not affected by system updates or rollbacks; +- **`/boot` (or `/efi`)** — a directory or separate partition holding the files needed for the operating system to continue booting: the compressed kernel file (vmlinuz), the RAM disk initialization image (initramfs), or unified boot executable images (UKI); +- **`/sysroot`** — a temporary mount directory for the physical disk at an early stage of boot, while initramfs is running, or in atomic systems. A disk partition is mounted here so that the needed directories can subsequently be selected to boot the required version of the operating system, turning it into the system root `/`; + +### **Item 3.** Definitions of project-specific terminology. + +- **Content-addressed storage (CAS)** — a data storage method in which a file's address is determined by the hash of its content, which provides automatic deduplication of identical files, guarantees protection against tampering, and allows data integrity to be verified instantly; +- **Image** — an immutable (ro) snapshot of a file system at a specific version, deployed as a single unit and guaranteeing an identical OS state on any device; +- Distribution (distro) — a ready-to-use operating system, built on top of a kernel with the addition of a system environment, utilities, system services, basic programs, and a package manager; +- **Ref** — a human-readable name that points to a specific image hash and is updated whenever a new version of the system image is released; +- **OverlayFS (lower / upper)** — a virtual file system that combines a read-only layer (lower, the base image) and a writable layer (upper, changes), presenting the user with a single folder in which system files stay untouched while any edits are stored separately; +- **fs-verity** — a file integrity protection mechanism built into the Linux kernel that makes a file immutable (ro) and, on every read, verifies its blocks via a Merkle tree, instantly blocking access at the slightest corruption or tampering of the data; +- **One-shot (one-shot / BootNext)** — a single-use UEFI boot entry that is activated for exactly one system startup, allowing a new update to be tested and automatically rolled back to the previous working version on failure; +- **Three-way merge (3-way merge)** — an algorithm for automatically merging text files that compares two changed versions against their common ancestor in order to preserve the user's edits and apply system updates without conflicts; +- **`base`** — the original configuration file from the previous (current) version of the system, which serves as an untouched reference for computing the changes made by both the developers and the user; +- **`new`** — the fresh version of the original configuration file from the incoming system update, containing the current settings from the developers; +- **`live`** — the current configuration file in the running system, containing the user's manual edits and individual settings; +- **`.upac-new`** — the file extension assigned to new system settings when an unresolvable conflict occurs during a three-way merge, in order to keep them alongside the original file without overwriting the user's manual edits; +- **`seq` (sequence number)** — a strictly increasing counter assigned to every new deploy, which fixes the exact chronology of system versions and serves as a guaranteed reference point when automatically switching or rolling back to previous states, independent of the system clock; +- **Pin** — a protection flag in a deploy's metadata that blocks the removal of a specific system version during automatic cleanup (garbage collection), guaranteeing the preservation of the current working version, the base rollback point, or states the user has explicitly marked; +- **Rollback** — an operation that instantly returns the system to a known-working state, which can be performed either wholly, by switching the bootloader to the previous atomic deploy along with its /usr and /etc versions, or selectively, by resetting the changes in the /etc configuration layer to their original state; +- **Garbage collection (GC)** — an automatic or manual storage cleanup process that finds and removes old file system layers, files, and CAS objects no longer used by any active, current, or pinned deploy, freeing disk space without risking damage to the working system; +- Token — a compact piece of data (a string or number) that serves as a digital pass or key for confirming rights, transferring data, or securely accessing the system; +- Linking — the build step in which the linker combines compiled object files and external libraries into a single finished executable file or dynamic library, resolving references to functions and variables to their real addresses; +- API (application programming interface) — a set of rules and functions at the source code level — for example header files, function names, parameters — that defines how programs interact with each other when built into an executable file; +- ABI (application binary interface) — a set of rules at the machine code level — for example calling conventions, the size and alignment of types in memory, system call numbers — that defines how binaries and libraries built from source code interact with each other while the program is running; +- FFI (foreign function interface) — a mechanism that lets a program written in one programming language directly call functions and use libraries written in another language; +- Wrapper — an intermediate layer of code that hides complex internal machinery and provides a more convenient, safer, or language-appropriate interface; +- CLI wrapper — a program with a command-line interface that accepts commands and flags from the user in a terminal, translates them into calls to an internal library's functions, and returns the result back to the console; +- Mapping — the process of linking or converting data from one structure, format, or address space into another according to defined rules; +- Comptime (compile time) — the stage at which a program's source code is checked, analyzed, and converted by the compiler into machine code. All computations, type checks, and macros carried out at this stage happen before the program runs and do not add any load to the finished program; +- Runtime — the stage at which the compiled/built program is directly executed by the processor within the operating system; +- Race condition — a design flaw in multithreaded or concurrent systems in which the result of the program's execution depends on the uncontrolled order or timing of execution of third-party processes or threads; +- TOCTOU (time-of-check to time-of-use) — a race-condition vulnerability that arises in a system when the state of a resource, for example a file or a set of permissions, is checked at one point in time but is changed by a third-party process before the system manages to use it; +- OEM (original equipment manufacturer) — a company that manufactures parts, components, or finished devices that are then sold under another company's brand, or that are used by that company to assemble its own products; +- Ed25519 — a modern, high-speed elliptic-curve digital signature scheme (EdDSA) that uses the Curve25519 curve; +- X.509 — a widely accepted international standard (ITU-T / RFC 5280) for the structure of public-key digital certificates (PKI). diff --git a/doc/eng/Upac chapter 1.md b/doc/eng/Upac chapter 1.md new file mode 100644 index 00000000..c007dcef --- /dev/null +++ b/doc/eng/Upac chapter 1.md @@ -0,0 +1,59 @@ + + +# **UPAC — the unified project document.** + +Project document. +Project branch: **`lib-rs`**, crate `lib-rust/`. + +--- + +## **§1.** Problem statement. + +--- + +**State problem.** + +On a typical Linux system, installing, updating, and removing software changes the running system *in place* — it edits its files right where they currently live. Because of this, at any given moment the system has no single, verifiable state: it is a pile of individually modified files. There are three consequences: +1. An interrupted or failed change leaves the system in a broken, half-finished state; +2. There is no way to reproduce or prove a specific *"known-working"* state; +3. There is no clean way to go back. + +**Solution to the state problem.** + +UPAC treats every system state as a **whole, content-addressed, verifiable image**. Any operation — install, update, remove — does not touch the running system, but instead **builds a new image from the current one**. Switching to the new image is a single atomic step, as a result of which the previous image remains untouched. This directly gives rise to three properties that answer the problem: +1. **The system has the property of atomicity**: an operation either completes in full, or the system remains as it was; +2. **The system has the property of reproducibility and verifiability**: every state is identified and attested by its hash, and a rollback is simply booting the previous image. The disk layout, bootloader, and kernel, meanwhile, remain fully under the user's control. + +--- + +**Access problem.** + +The system's base tree (`/usr`) is immutable and is managed by the package manager. If a user simply wants to add their own files there — for example, to drop in wallpapers or assets that a package expects under `/usr` — they cannot just copy them in. A couple of files have to be wrapped into a full-blown package: metadata, build, install, just for the sake of placing them there. The barrier to adding one's own content to the managed tree is unjustifiably high. + +**Solution to the access problem.** + +UPAC allows adding arbitrary user files to the managed tree (`/usr`) directly, with a single command, without writing a package. The file becomes full-fledged content of the image being built, but the user does not need the whole packaging pipeline. + +--- + +**Compatibility problem.** + +For one and the same Linux kernel there exist many incompatible package formats, for example: deb, rpm, pkg.tar, and so on. A program built for one package format will not install through another package manager, which locks the user into the ecosystem of their package format, even though the kernel and the ABI are shared by all of them. + +**Solution to the compatibility problem.** + +UPAC is not tied to a single package format. Parsing a specific format is delegated to separate backends — one per format — which bring the package to a common internal representation: a file tree and metadata. This lets a single manager install packages of different formats on one system, and the format stops being a compatibility boundary. + +--- + +**Management problem.** + +Even when a file is already on the system, it cannot be attached to a package as a user file — in a way that lets the manager track it and clean it up together with the package. This is especially painful under `/usr`: manually added files remain *"orphans"* outside of any tracking — they are invisible on removal and cannot be cleaned up automatically. + +**Solution to the management problem.** + +UPAC allows attaching a file to a package as a user file, with full accounting in the database. Such a file inherits the package's lifecycle: it is tracked, shown as part of the package, and removed along with it. diff --git a/doc/eng/Upac chapter 2.md b/doc/eng/Upac chapter 2.md new file mode 100644 index 00000000..86b27b48 --- /dev/null +++ b/doc/eng/Upac chapter 2.md @@ -0,0 +1,21 @@ + + +# **UPAC — the unified project document.** + +Project document. +Project branch: **`lib-rs`**, crate `lib-rust/`. + +## **§2.** Defining what the project is NOT (Non-goals). + +--- + +- **Not a distribution:** UPAC is a package manager and a deployment mechanism, not an operating system. It does not ship a curated repo, a default set of software, or a release cycle — it only manages whatever content it is pointed at; +- **Not a configuration manager:** UPAC reconciles `/etc` and preserves the user's edits across updates, but it does not generate or enforce config policy — it is not Ansible and not NixOS modules. It preserves and reconciles, it does not generate; +- **Not a container runtime:** UPAC uses the same building blocks as containers — composefs, OCI — but it deploys the host system, not containers. It is not a replacement for docker/podman; +- **No in-place changes — by design:** Any change to the system produces a new image; there is no hot-swapping of files on a live system, not even as an option. This is a direct consequence of the project's principles; +- **Not a repository server:** UPAC is only a client for ready-made external repos: distribution mirrors, OCI registries, and the like — it does not stand up its own repository or server. The only local alternative to a repo is delivering an image as a file: `--file`; +- **Does not repair the file system or the disk:** UPAC is responsible for the correctness of its own operations: package verification, image atomicity, repo integrity, and, via fs-verity, it **detects** content corruption, refusing to boot a damaged deploy. But recovering the file system itself, bad blocks, a degraded storage medium, or hardware errors is outside its scope: that is the job of `fsck`, SMART, and replacing the disk. A system falling apart because of a failing disk is not a failure of UPAC. diff --git a/doc/eng/Upac chapter 3.md b/doc/eng/Upac chapter 3.md new file mode 100644 index 00000000..5b1c085c --- /dev/null +++ b/doc/eng/Upac chapter 3.md @@ -0,0 +1,61 @@ + + +# **UPAC — the unified project document.** + +Project document. +Project branch: **`lib-rs`**, crate `lib-rust/`. + +## **§3.** Disk structure. + +This paragraph describes what physically resides on the disks of a deployed system. + +### Map + +``` +[block device, GPT] +│ +├── ESP (FAT32) (1) +│ ├── EFI/Linux/upac-from.efi (2) +│ ├── EFI/Linux/upac-to.efi (2) +│ └── loader/entries/*.conf (3) +│ +├── deployment partition → /sysroot (4) +│ ├── composefs/ (5) +│ │ ├── meta.json (6) +│ │ ├── objects// (7) +│ │ ├── images/ (8) +│ │ │ ├── → ../objects//… (9) +│ │ │ └── refs/ → ../images/ (10) +│ │ └── streams/ (11) +│ │ ├── → ../objects//… +│ │ └── refs/ +│ └── state/deploy// (12) +│ ├── meta.json (12) +│ └── etc-upper/{upper, work} (13) +│ +├── /var partition → /var (14) +└── /home partition → /home (15) +``` + +### Map legend + +- **(1)** ESP (EFI System Partition) — a separate FAT partition read by the UEFI firmware, mounted at `/boot` or `/efi`; +- **(2)** `upac-from.efi` / `upac-to.efi` — two fixed UKI slots for direct-UKI boot. An operation writes the new UKI into the inactive slot; the switch happens via `BootNext`; +- **(3)** `loader/entries/*.conf` — BLS entries for machines with a boot manager: systemd-boot and similar. Requires support for reading BLS entries. +- **(4)** deployment partition — the physical root holding all of the system's content; while the system is running it is mounted at `/sysroot` for changes. The file system requirement is **fs-verity** support. For example, this mechanism is supported by ext4, btrfs, xfs; +- **(5)** `composefs/` — the composefs repository: the content-addressed store for all files and images. The default composefs path for system mode; +- **(6)** `meta.json` — repo metadata: format version + fs-verity algorithm (`fsverity--`); +- **(7)** `objects/` — the content-addressed store, in which objects are laid out into subdirectories named after the first 2 hex characters of the hash. Identical content is stored once; +- **(8)** `images/` — EROFS images: content-addressed snapshots of the `/usr` **and** `/etc` trees. They carry the tree's metadata; file data is taken from `objects/`; +- **(9)** `` — an image = a symlink to an object in `objects/`, determined by the image's hash; +- **(10)** `refs/` — a human-readable named pointer to an image; +- **(11)** `streams/` — splitstreams: imported layers/commits, also symlinks into `objects/`, with their own refs added; +- **(12)** `state/deploy//` — a deploy record, in which the **key = `usr-digest`**. It holds `meta.json` inside. +- **(13)** `etc-upper/upper` — **live `/etc`**: edits not included in the deploy, as the overlayfs upper layer over the current `working_etc`. It is sealed into `etc-digest` when `/usr` changes or on `upac commit`; +- **(14)** `etc-upper/work` — **live `/etc`**: `work` — the overlayfs service directory; +- **(15)** `/var` — the directory is placed on a separate disk partition, thanks to which all changing data, logs, and databases are preserved directly and are not lost on a system rollback; +- **(16)** `/home` — user data: a separate directory holding user data, outside of versioning. diff --git a/doc/eng/Upac chapter 4.md b/doc/eng/Upac chapter 4.md new file mode 100644 index 00000000..37df9c22 --- /dev/null +++ b/doc/eng/Upac chapter 4.md @@ -0,0 +1,87 @@ + + +## 4. Project repository structure. + +### Map of files in the repository. + + +``` +upac/ +├── .cargo/ +│ └── config.toml +├── .claude/ +│ └── settings.local.json +├── .github/ +│ ├── workflows/ +│ └── PULL_REQUEST_TEMPLATE.md +├── decoders/ +│ ├── alpm/ +│ ├── deb/ +│ ├── rpm/ +│ └── xbps/ +├── doc/ +│ ├── rus/ +│ └── UPAC project note.en.md +├── lib/ +│ ├── abi/ +│ ├── lib/ +│ ├── macro/ +│ └── pki/ +├── LICENSES/ +│ ├── CC-BY-SA-4.0.txt +│ ├── GPL-3.0-only.txt +│ └── LGPL-3.0-or-later.txt +├── user/ +│ ├── sign-cli/ +│ └── upac-cli/ +├── xtask/ +│ ├── src/ +│ ├── Cargo.lock +│ └── Cargo.toml +├── .gitignore +├── Cargo.lock +├── Cargo.toml +├── CONTRIBUTING.md +├── LICENSE +├── README.md +├── REUSE.toml +├── rust-toolchain.toml +├── rustfmt.toml +└── SECURITY.md +``` + + +--- + + +### Legend for the file map in the repository. + +- **(1)** `.cargo/` - a folder for managing the project's Cargo build manager; +- **(2)** `config.toml` - a file that sets the project's build parameters; +- **(3)** `.github/` - a folder for managing the files behind the GitHub remote repository's automation; +- **(4)** `workflows/` - a folder for managing the files that create automated jobs with strictly defined input and output data on the GitHub remote repository; +- **(5)** `PULL_REQUEST_TEMPLATE.md` - a template for creating a pull request on GitHub; +- **(6)** `decoders/` - a folder with package format decoder plugins, one per format (no individual description is included); +- **(7)** `doc` - the project's documentation folder. There is a folder with Russian-language documentation and one with English-language documentation; +- **(8)** `lib/` — the Rust core of the library; +- **(9)** `abi/` — a Rust library for working with FFI; +- **(10)** `lib/` — the library's main working logic; +- **(11)** `macro/` — a library of procedurally-generated macros for the library's main code; +- **(12)** `pki/` — a library for generating and verifying all levels of certificates; +- **(13)** `LICENSES/` — a folder with licenses for reuse to work with; +- **(14)** `user/` — a folder with user-facing utilities; +- **(15)** `sign-cli/` — a CLI for working with certificates (signing them, verifying them); +- **(16)** `upac-cli/` — a CLI for working with the main library, carrying out the core operations; +- **(17)** `xtask/` — a script for automatically updating the repository map in the documentation, based on Cargo; +- **(18)** `.gitignore` — the configuration file for files ignored by the Git version control system; +- **(19)** `Cargo.toml` — the workspace file that manages the other nested projects; +- **(20)** `CONTRIBUTING.md` — a file describing how to get involved in contributing to the project; +- **(21)** `README.md` — the project's brief reference file; +- **(22)** `REUSE.toml` — the configuration file for the reuse licensing-check utility; +- **(23)** `rust-toolchain.toml` — the configuration file for the project's Cargo build system version; +- **(24)** `rustfmt.toml` — the configuration file for formatting the project's source code files; +- **(25)** `SECURITY.md` — a file describing how vulnerabilities in the program's code are handled and tracked in the project, along with reference information on the subject; diff --git a/doc/eng/Upac chapter 5.md b/doc/eng/Upac chapter 5.md new file mode 100644 index 00000000..ec57ed4e --- /dev/null +++ b/doc/eng/Upac chapter 5.md @@ -0,0 +1,234 @@ + + +## **§5.** Operating mechanisms + +A separate description of the mechanisms implemented by the core (`lib/`). + +### **§5.1** The mechanism for merging configs in the `/etc` directory. + +In a booted system, the `/etc` directory is an `overlay` consisting of `lower` = the immutable etc-digest, in `ro` mode, and `upper` = edits not recorded by the system, in `rw` mode. + +The `/etc` directory is versioned content-addressably: every file system snapshot = an `etc-digest` (see **§5.7**). The job of the merge mechanism is, whenever the `/usr` directory changes, to build a new `/etc` directory: carry over the user's config edits, pull in the packages' new standard configuration files, and seal the result into the first `etc-digest` snapshot of the new `/usr` directory. + +The mechanism lives in the library and runs at the `merge` stage, before the new snapshot becomes bootable. + +**Three inputs (3-way):** +- **base** — the current `/etc` files corresponding to the current `/usr` files that the booted system was built from; +- **new** — the new `/etc` files of the `/usr` directory being deployed, which include the packages' new standard configuration files; +- **live** — the current state of the user's `/etc` configuration files, combining the `working_etc` sealed into the snapshot and the current running system's user edits not yet sealed into a snapshot. + +**Per-file classification:** +- If the user did NOT modify the file, i.e. `live == base`, the result is the package's **new standard configuration file**; +- If the user edited the file AND the new default matches the old one, i.e. the package did not change the file, the user's version of the file is kept; +- If the user edited the file AND the new standard configuration file has changed, creating a conflict, the user's version of the file is carried over, and the new file with the standard settings is placed alongside it as `.upac-new`, BUT it is excluded from future classification — it is not a *"user file."* + +**Conflicts described in the last item are resolved via a special hook, and do not block the operation from proceeding.** The user is informed about the package's new standard `.upac-new` configuration files through the message-hook call mechanism, passed on to the CLI. + +**The mechanism's result** is sealed into a new `etc-digest` snapshot, which becomes the `working_etc` of the new system snapshot; the live overlayfs upper layer of `/etc` starts out empty. Unchanged files are deduplicated by composefs at the object level, so `etc-digest` is a complete snapshot of `/etc` without duplicating content. + +On the `upac commit new` command, this mechanism seals the current state of `/etc` without changing the files in the `/usr` directory — the new `etc-digest` runs under the same `/usr`. + +### **§5.2** The mechanism for booting a new system version and rolling back to the previous working version if the boot fails. + +The system rollback is built on a one-shot selection of the new boot option and a late confirmation that the startup succeeded. There is no separate counter of boot attempts — an image that fails to boot on the first try, for whatever reason, is automatically rolled back to the previous successful boot option. + +**The one-shot selection mechanism.** The bootloader/firmware has a pair of boot options: a *"one-shot boot option / persistent boot option."* For example, for UKI-direct boot this is `BootNext` / `BootOrder`; for the systemd-boot bootloader — `LoaderEntryOneShot` / `LoaderEntryDefault`; for grub — `grub-reboot` / the persistent option in the config. The firmware/bootloader uses the one-shot variable exactly once on any boot, which is precisely why it is, by itself, a single-attempt auto-rollback. + +**Staging and booting:** + +1. For snapshot D' a boot entry is written with `composefs.digest=D'`. For UKI-direct boot, into the inactive `upac-to.efi` slot; for bootloaders compatible with BLS config, into the configuration file, via the `BootconfigParser` mechanism built into composefs; +2. After the reboot: the bootloader boots D' once, and the one-shot boot option is removed. Initramfs mounts the digest from the cmdline (composefs overlay), followed by pivot and PID1. +3. The system reaches a fully successful boot — a late hook / init unit makes D' the persistent boot option and marks the **pair as working**: it updates `working_etc` for the current `/usr` (see **§5.7**); +4. If confirmation did not happen, i.e. for whatever reason the system did not reach its final state — the one-shot variable has already been removed from the bootloader's/firmware's memory, so the next boot starts the persistent boot option, i.e. the previous system snapshot. This is the auto-rollback; + +**D' is the usr-digest, not a composite pair.** `composefs.digest=D'` carries precisely the `usr-digest` — the same value that names the `state/deploy//` directory, and that `open_tree()` (see the composefs module, **§7**) accepts directly. This is the same way the booted system finds out *"which system snapshot is currently active"* without a separate pointer file on disk. The `etc` directory from the pair is deliberately **NOT** present in the cmdline — as soon as `usr-digest` is known, `working_etc` is read from the file in its `state/deploy//meta.json` directory (see **§5.7**), which carries information about the currently confirmed `etc-digest`. + +**Why does a kernel parameter unrecognized by the kernel, such as `composefs.digest=`, even survive into `/proc/cmdline`?** + +`/proc/cmdline` is not a filtered list of parameters that only the kernel understands, but the raw, untouched string that the bootloader or the UKI passed to the kernel. When the kernel's argument parser encounters an unfamiliar parameter, it does not discard it; instead, it prints *"Unknown kernel command line parameters ..., will be passed to user space"* and leaves the string as-is, for `/proc/cmdline` and for PID1's own cmdline. + +This is standard, documented kernel behavior, and many userspace programs rely on it. + +**Rollback echelons.** A description of each boot-failure level and what that level catches: + +1. If the kernel or initramfs failed to start — the firmware/bootloader itself falls back to the persistent boot entry; +2. If the kernel and initramfs started, but PID1 failed to come up — no boot-success confirmation arrives, and the next boot rolls back; +3. If PID1 came up successfully, but services/network/GUI are dead — the user can simply restart the system or invoke the `upac commit rollback` command; + +If the system formally reaches a working state and confirms it, but some subsystems or tools failed to come up or are working incorrectly — a manual rollback is available: `upac commit rollback` from the live system, or the firmware/bootloader menu. + +**Deliberate limitations of this mechanism:** + +- Only one attempt: a broken atomic image is deterministically broken, so repeated attempts to boot it are pointless; +- Auto-confirmation only proves that *"the boot successfully reached a certain point,"* not that *"things are good for the user"* — deeper breakage is rolled back manually via the `upac commit rollback` command; +- A plain *"hang"*: PID1 is alive but frozen, with no panic and no reboot, requiring a manual restart through physical interaction so that the one-shot variable takes effect. + +### **§5.3** Staging a system snapshot for boot (stage). + +Input: a ready-made image D', already sitting in the repository directory: `images/D'`. +Output: a system snapshot ready for a one-shot boot. +It links operations (see **§5.4**) with booting (see **§5.2**). + +1. Merging the `/etc` directory (see **§5.1**): merge seals the `etc-digest` directory for D' and declares it `working_etc`. The layer of undocumented user edits, upper (`etc-upper/`), starts out empty; +2. Persistent partitions/directories (`/var`, `/home`) — real, mounted as-is, untouched; +3. Writing the boot entry with `composefs.digest=D'`: + - UKI-direct — assemble and sign the UKI, write it into the inactive `upac-to.efi` slot; + - Manager — `BootconfigParser` writes a BLS conf into `loader/entries/`; +4. Set D' as the one-shot entry for the next boot (see **§5.2**): UKI-direct — `BootNext` on the slot; manager — `LoaderEntryOneShot` / `grub-reboot` and other options specific to a particular manager's implementation. + +After this: reboot, and item **§5.2**. + +### **§5.4** The mechanism for file system operations: add, remove, update, rename, and so on. + +All operations follow one form: +1. Change the file tree; +2. Commit the new image; +3. Hand the image off to staging (**§5.3**). + +The old image is not touched until the switchover (the atomicity property). This is where the decoders and the resolver operate, and where the package DB is written. + +The general operation pipeline: + +1. Build the new file tree from the current one; +2. Commit the file tree as the new image D' into the repository (`objects/` + `images/D'`), embedding the package DB inside the image; +3. Pass D' to image staging (see **§5.3**); +4. Light cleanup of old, unused images (see **§5.5**). + +### **§5.5** Garbage collection. + +It has two levels: deploys (what to keep) and objects (what to sweep away). The retention policy is set by the user. The object-sweep engine is composefs. + +**Immutable pins** (never removed): + +- The active (booted) deploy; +- The rollback target (the persistent or previous deploy); +- A staged but unconfirmed deploy (one-shot boot). + +Plus the user's manual pins (manually pinned deploys) and the last N within the depth set by the user. + +**Cleanup triggers:** + +1. **Light deploy cleanup by an internal operation stage** after every operation that changes the file system: drop the image's ref and remove `state/deploy//` for deploys outside the retention policy. The number of disk write operations is small, so the operation is cheap, and the pins hold onto what's needed; +2. **Heavy object cleanup is only run manually**, via the `upac package gc` command: the program walks the `objects/` and `streams/` directories and sweeps away unreachable ones, i.e. objects that nothing references, using the composefs `ObjectCollector` mechanism. + +### **§5.6.** The mechanism for creating and deploying OCI (planned for development). + +In this paragraph, OCI is a portable image-artifact format, not a network protocol. + +**Directions of image entry:** + +- **Import** — take an OCI image and deploy it as the host system; +- **Export** — produce a portable OCI image artifact from a deploy, creating a ready-made *"reference copy."* + +**Image delivery options:** + +1. **From an external repository** — the same networking subsystem used for delivering packages is used here. Applied by default; +2. **From a local file, via a command** — using `--file `, a local file is taken and deployed directly. + +Only these delivery paths are used for deploying an image to multiple machines. There is no separate mechanism yet for deploying to multiple devices. Implementation as a plugin is possible. + +Deploying an image relies on the `composefs-oci` mechanism: `create_filesystem` (layers → image), `generate_boot_image`, `pull_image`. + +### **§5.7** The mechanism for the deployment history of images and rolling back N deploys. + +The deployment history is stored NOT inside the image (which would break content-addressability), but on the read-write (`rw`) partition. The source of truth is the `state/deploy//` directories themselves; there is no separate log. + +**Two axes of history preservation:** + +- **The `/usr` directory** — a linear deploy history. Each distinct `/usr` directory = one record, whose key is the `usr-digest`. `seq` is the order in which records are born: monotonic, one per digest. It has a built-in marker for finding the array element following it, in the form of `state/next-seq`. Re-arriving at an existing `usr-digest` **switches** to its record as the current one, rather than creating a duplicate — so this `/usr`'s `/etc` sub-history stays intact when returning to an old `/usr`. A record carries its own **commit message**: `subject` — short, required, and an optional long `message` — the commit message of the operation that produced this `/usr`; +- **The `/etc` directory** — a sub-history within `/usr`. The record's `meta.json` carries `etc_history` — an ordered list of records of the type `{etc_digest, subject, message}`, created under this `/usr`. On `/usr` change and on the `upac commit` command, see **§5.1**. Each record carries its own `subject` and an optional `message`. The first record is created automatically when `/usr` changes (see **§5.1**), inheriting the subject and message of the `/usr` event itself — later, explicit `upac commit` calls get their own, independent subject and message. + +**The active deploy** is the booted `composefs.digest` and the boot default. It is **NOT** `max(seq)`: after switching to an old record, its `seq` stays the same, unchanged. + +**Rollback variants:** + +- **Emergency (`/usr`)** — to every N-th existing deploy in `seq` order (counted by actual presence, **not** by `seq−N` arithmetic). Burned numbers are allowed; we simply skip them. The **pair** is restored: the target `usr-digest` + its `working_etc` (the last confirmed commit of the sub-item) — thanks to this, config edits are not entirely lost; +- **Config (`/etc`)** — `upac commit rollback --etc` to the previous `etc-digest` from the `etc_history` of the current `/usr`. The same rank-based mechanics apply, with its own retention depth. + +The authority and source of truth for the rollback mechanism's operation is **`seq`**. Timestamps, in the form of **`timestamp`** in `meta.json`, are for display only (for example, `upac commit history`), since clock drift or a clock change could easily break the history and make the system rollback impossible. + +Connection with the cleanup mechanism (see **§5.5**): the retention depth of an image reference on each axis (configs or system files) must be **inclusive** with respect to the number N. + +### **§5.8** The mechanism for hooks and for locating decoders (the operation of pre/post triggers). + +**Hook** — a declarative, **signed** file: it describes the trigger itself, the hook's priority over other hooks with the same trigger, and a composition of **primitives**; +**Primitive** — a closed set of low-level actions built into `lib`. For example, running a process, touch (creating) or move (moving) a file, and so on — the only thing that requires editing `lib`'s code to add any new primitives; +**Signature** — protection against an arbitrary, unsigned hook file, since primitives are privileged enough within the permissions system that an unsigned file cannot be trusted. + +**Correspondence table.** + +A hook file separately carries a table: for decoder `D` (see **§6** — a package format plugin: deb, rpm, etc.), this hook covers the trigger name that is NATIVE to that package format (for example, deb has the `update-mime-database` trigger, which the decoder will translate into the universal format). Compatibility with other trigger conventions is also described in the file, while the decoder is simply handed the ready-made correspondence table, from which it determines which ones need to be executed and which are not satisfied, passing everything on to the calling side. + +**Hook file priority.** + +**Priority** is an ordinary signed integer (default 0), used ONLY to resolve a conflict when several different hook files claim the same native trigger name (the same key `k` in the correspondence table). In a conflict, the higher `priority` wins; however, if there is a tie, this is an unresolvable conflict, and `lib` immediately returns a critical error and cancels the operation. Automatic selection is not provided, by design. Unmatched entries (the hook's native trigger simply does not exist in a particular package) do not need any separate reporting at all — this is a normal, expected outcome for most hooks on most packages, not an error. `priority` does not define any execution order, since all trigger hooks that need to run execute concurrently (in parallel). + +Whether a non-fatal warning should still be surfaced through `MessageHook` for any of these cases (a conflict, or a hook that structurally can never match anything, or other cases that come up later) — remains an open question for now. + +**Hook file criticality.** + +**Criticality** — a field (`critical = true/false`). It marks hook files that are critical to the operation, whose execution failure causes the entire operation to fail and be canceled. + +**Division of labor:** + +- **`lib`** — the only party that reads hook files from disk, verifies the signature, and parses the composition of primitives and the correspondence table. It executes the composition through its own primitives; +- **The decoder (plugin)** — receives from `lib` the correspondence table already as a ready-made key:value map **for its own `D`** (i.e. the deb package format does not receive other formats' trigger entries, for example rpm's), where the key is the decoder's native trigger name and the value is our hook name. The decoder itself matches it against the native trigger names it read from the package (for example, the deb decoder reads the package's `Triggers-Interest` itself), and hands back to `lib`, through FFI, the list of hooks to execute (the required values); + +**Decoder resolution.** + +Decoders are located through declarative, signed TOML manifests in `/etc/upac.d/decoders/` (each decoder gets exactly one `format`, `extensions`, `library`): `format` is the canonical identity of the package format, the same string as the key `D` in the hook correspondence table; `extensions` lists the file variants in which this format is actually shipped (for example, alpm packages have the file extension `pkg.tar`/`pkg.tar.gz`/`pkg.tar.xz`/`pkg.tar.zst`); `library` names the `.so` that needs to be opened. The plugin library itself is opened lazily, that is, only and exactly when the file is required for execution (unpacking the format). A duplicate `format` between two manifests produces a hard error at manifest-loading time, the same logic as for the priority tie above. + +**Hook file format and signature.** + +A hook file is TOML, and lives in `/etc/upac.d/hooks/` (the path is baked in as a constant from the `lib.toml` file at the library's build time). The signature is built as a chain of 2 trust levels: the root CA (an offline key, which only signs the next level), then a signing certificate for the trust domain, which directly signs the hook file's bytes; the `.sig` file itself carries both the signature and the entire signing certificate — verification is self-contained. + +**The root is a configurable file**, not baked into the compiled version of the program: a distro/OEM, or the user, plugs in their own root without rebuilding `upac`. + +**The signature scheme** — the Ed25519 encryption algorithm on top of X.509 certificates. + +**Execution model.** + +Running hooks is asynchronous, but entirely inside `lib`: the FFI remains fully synchronous. The scope of application is only the concurrent execution of N independent hooks within a single stage of a command's execution. + +### **§5.9** The mechanism for canceling a failed operation. + +It works through the `CancelToken` (cancel token) mechanism, operating on the principle of an atomic flag, which is created by the calling side (CLI/GUI) and passed into `lib` as a pointer with every request. + +**`Lock`** — a mutual-exclusion mechanism between rw operations, working on the basis of a bind to an abstract Unix address, strictly known and fixed in `lib.toml`, shared by all rw calls). + +### **§5.10** The mechanism for reporting operation progress. + +It uses the same message-delivery channel as in item **§5.9**. It contains: +- `stage` - the stage's ordinal number, in `u16` format; +- `phase` - which sub-step within the specific stage, in `u16` format; +- `subject` - the object string, identifying what is currently being worked on (a file, a hook, or another object); +- `current`/`total` - an element counter. `0` if not applicable/unknown. + +The `MessageHook::send` mechanism accepts a single, self-describing parameter instead of the former separate event/data — there is nothing to unpack separately. + +### **§5.11** The stage orchestration mechanism. + +The mechanism by which commands are actually executed: a linear list of stages plus an engine that walks through them — both hook channels from items **§5.9** and **§5.10** are also plugged in here. + +**A stage is always a flat structure.** + +A single stage performs exactly one atomic unit of work per call. Each call itself decides what happens next — moving forward to the next stage, repeating itself (for example, processing one more file from an already-started list), or jumping BACK to an earlier stage by its TYPE (not by numeric index, so that changing the stage list doesn't break things). Through such a backward jump, a group of several stages (for example, *"verify package → unpack → register"*) can be repeated as a single unit — on a jump, the engine looks for the nearest matching stage by type, going backward from the current position. If no such stage exists, this is a pipeline-assembly bug, not user input, and it is returned as an ordinary error, aborting the operation. + +**Every stage call brings its own rollback.** + +A stage does not accumulate state between calls — on every call it creates its own, self-contained rollback object for the changes made on disk, carrying exactly the data needed to undo exactly what THIS call did (even if the same stage was called many times before with different data — each call remains independent). If a stage has nothing to roll back this time, it must still return such an object, simply in *"empty"* mode: this is guaranteed at the compiler level (an *"empty"* rollback constructor). + +**The engine** holds a linear list of stages. Exclusivity for the difference between rw and ro commands is achieved by choosing the launch METHOD: in rw mode, it holds a system lock file for the entire duration of the run, whereas the other mode does not create it at all. On any failure (a stage error, a cancellation, a backward jump that finds nothing), it unwinds every rollback object accumulated up to that point, in reverse order, without stopping if one of them itself fails to roll back — it is skipped, since the mechanism aims to roll back everything it can. Every successful stage call hands the engine two separate things at once — the progress constructor (see **§5.10**, which the engine itself created and passed to the stage before the call) and its own rollback object. + +**Engine failures:** + +- The pipeline failed to even start (for example, the lock file indicates another rw operation is running); +- A specific stage, number N, failed — the command that invoked the engine distinguishes between these two, because in the first case there is simply no stage number at all. + +**Pipeline validation, before the first call.** + +Every stage can declare what it `requires` from the shared context and what it `provides` back into it (by type). Before running, the engine walks the entire list once and checks that each stage's requirements are satisfied by what is already sitting in the operation's context, plus what earlier stages have provided — a missing dependency fails immediately, before any stage actually runs, rather than surfacing somewhere deep inside a later stage. The check is uniform across all commands (it also accounts for both the exclusive and the concurrent launch paths). From 192b7890ffde321257a4e5d1e79c67767528902d Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 08:14:36 +0400 Subject: [PATCH 44/68] fix: Added the remaining chapters Co-Authored-By: Claude Sonnet 5 --- doc/eng/Upac chapter 6.md | 36 ++++++++++++++++++++++++++++++++++++ doc/eng/Upac chapter 7.md | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 doc/eng/Upac chapter 6.md create mode 100644 doc/eng/Upac chapter 7.md diff --git a/doc/eng/Upac chapter 6.md b/doc/eng/Upac chapter 6.md new file mode 100644 index 00000000..bf29590b --- /dev/null +++ b/doc/eng/Upac chapter 6.md @@ -0,0 +1,36 @@ + + +## **§6.** FFI and the boundaries of interaction between the components. + +Upac-lib (`lib`) is all of the program's core logic and the stable C-ABI. Decoders and boot plugins are loaded under its control. +Upac-cli and GUI programs built on the library (in the future) are thin wrappers: they only interpret input and emit events. + +``` Diagram of control flow during the program's runtime. +CLI ─┐ +GUI ─┼──(C-ABI)──▶ lib ──(dlopen)──▶ decoders/* (format parsing + resolve) +… ─┘ │ + └──(calls)──▶ composefs (repo / image / mount / boot) + +lib ──(hooks)──▶ CLI/GUI (progress, events, /etc conflicts) +``` + +### Description of the diagram's directions: + +- **External call → lib:** a command with arguments (what to do) + a cancel token; +- **Lib → decoders:** the path to the package; in return — files, metadata, dependencies; +- **Lib → composefs:** primitives, for example committing an image, mount, prune (cleanup), writing boot entries; +- **Lib → external call (hooks):** operation progress, confirmation events, `.upac-new` conflicts. + +**Boundary rules:** +1. *"Touches state / needs full access to system files / must be atomic"* → `lib`; +2. *"Displays or gathers input"* → external call, external control. CLI and GUI are equal, thin wrappers over one library. + +**`lib` has two public contracts.** + +Besides the stable C-ABI (implemented via `export` and the OS's `dlopen` mechanism), the Rust layer itself is also public for direct static linking: `orchestrator`, `scripts`, `plugin`, `composefs`, `database`, `deploy`, `errors`, `lock`. + +`export` remains private, since the C-ABI itself does not need to be invoked under static linking, as do the internal mechanisms implementing the logic of external types and functions, for example `Cursor` inside `orchestrator`. Inside `composefs`, the mechanisms `repository::open`/`repository::open_tree` are additionally cut from external export, since they are not part of the public contract — the sole point for obtaining an open repository/file tree from outside the API is the `deploy::Deploy` module, so that the repository cannot be opened bypassing an already-mounted sysroot, which would leave the system in an inconsistent state. diff --git a/doc/eng/Upac chapter 7.md b/doc/eng/Upac chapter 7.md new file mode 100644 index 00000000..3dd02e43 --- /dev/null +++ b/doc/eng/Upac chapter 7.md @@ -0,0 +1,37 @@ + + +## **§7.** Program modules. + +***ATTENTION:*** A design reference point, refined as development proceeds! + +**`lib/`** — the program's core and FFI (the actual module layout, kept up to date as development proceeds): + +- `export` — C-ABI: entry points for all commands, the ABI version, cancellation, freeing responses; +- `orchestrator` — the shared engine (see **§5.9**–**§5.11**): `Stage`/`ConcurrentStage`, `Cursor`, `RollbackGuard`, and two orchestrators behind one `Orchestrator` trait — `SequentialOrchestrator` (linear, holds the system lock file) and `ParallelOrchestrator` (parallel stages, used for hooks in **§5.8**); +- `mutated` / `unmutated` — command bodies, one submodule per command. Each is assembled from its own pipeline of stages via `orchestrator`; +- `scripts` — item **§5.8**: the hook file's TOML format (`HookFile`), primitives (`Primitive`/`TouchFile`/`MoveFile`/`CreateSymlink`, each `impl Step { execute, rollback }`), native trigger matching (`Operation`/`Timing`). `HookStage::run()` is fully wired up for native triggers: get-or-build the shared `tokio` runtime via `Context`, then signature verification and hook-file parsing (`load_hooks`, via `upac-pki`), filtering by `NativeTrigger`, and parallel execution of matched hooks via `ParallelOrchestrator` (`HookFile` itself `impl ConcurrentStage`, executing its own `steps` and honoring `critical`). It is wired into the pipeline of every mutating command (Pre/Post hook processing for each); +- `plugin` — decoder loading. At the moment only the `decoder` submodule is implemented (`dlopen`, ABI version check, `decode`/`match_triggers`) — the parent `plugin` directory is reserved for other kinds of plugins in the future, of which there are none yet. Also here: `manifest` (`DecoderManifest`, `load_decoder_manifests()` — reads the declarative files describing decoders in the `/etc/upac.d/decoders/*.toml` directory, without scanning and/or checking the `.so`), and `triggers` (`build_trigger_table()` — builds the native-trigger→hook table for a specific decoder from the loaded `HookFile`s, resolving `priority` conflicts with a hard operation error). Not wired up anywhere yet — it needs a real call site, tied to the not-yet-written stage bodies of each command; +- `composefs` — access to the composefs repository. `Repository`: `open(path) -> Repository` (opening by path via `Repository::open_path`), `open_tree(repository, name) -> FileSystem` (reads the image via `Repository::open_image` + `erofs::reader::erofs_to_filesystem`) — both are available only inside the library; only `deploy::Deploy` is exposed outward. `error::RepoError` — a mapping of `RepositoryOpenError`/`ImageError`/`anyhow::Error` (the last one is needed because `ensure_object`/`ensure_object_from_file`/`commit_image` and so on in composefs itself return `anyhow::Result` — no error detail can be extracted from it, only the fact of failure). `file::FileHandle` — a holder/pointer to a path in the tree, three `impl` blocks organized by "does it touch CAS or not" logic: constructors (`new` — blind, for inserting something new; `from_tree` — with a check that the path already exists), tree operations without CAS (`insert_in_tree`/`update_in_tree`/`rename_in_tree`/`remove_in_tree`/`symlink_in_tree`/`hardlink_in_tree`/`stat_in_tree`/`symlink_target_in_tree`/`list_in_tree`), file operations through CAS (`insert_file` takes an already-open `&File` — not bytes, so that the path is resolved exactly once and there is no TOCTOU race from swapping the file out between reading it and inserting it into CAS; `replace_file` — an alias for `insert_file`, since composefs's `Directory::insert` already upserts by itself; `read_file` — resolves inline/external and pulls bytes from `Repository::read_object` when needed); +- `deploy` — deploy staging (see **§5.3**): finds the block device under `/` via `MountInfo`, the actual FS type via `rsblkid::probe::Probe` (not `None`, otherwise `mount(2)` fails with `EINVAL` — the kernel needs the FS type explicitly for any mount except bind/remount), `unshare(CLONE_NEWNS)` + a mandatory `MS_REC | MS_PRIVATE` remount of `/` before the actual mount (without this step, mount events still leak into the host's table via shared propagation inherited from the parent namespace), and mounts the partition at `/sysroot`. `deploy(prefix_digest) -> PathBuf` — a single path component (`state/deploy//`, see **§3**). `open_repository()` / `open_tree(name)` — the sole public access point to the composefs repository of the current deploy (see above); +- `database` — the package DB (implemented via redb) inside the image. At build time it is written through its own in-memory `StorageBackend`; at runtime a `ReadOnlyDatabase` is read from a file inside the image. Also here: `record` — `DeployRecord`/`EtcHistoryEntry` (for the fields see **§3** item 12: `prefix_digest`/`subject`/`message`/`seq`/`timestamp`/`etc_history`/`working_etc`), which is not physically part of the redb DB (a separate `meta.json` on the sysroot, not inside the image), but lives here in spirit — *"how our types are persisted"* is `database`'s shared concern, regardless of the format. Serialization uses `#[derive(JsonCodec)]` (modeled on `RedbCodec`, the same field-by-field codegen, just into `serde_json::Value` instead of a byte layout); `DeployRecord::write`/`read` write/read the file, with `write` being atomic (a tmp file in the same directory + `fsync` + `rename`). It has its own error, `error::DeployRecordError` (separate from `DatabaseError` — different storage formats); +- `types` — domain types (`Version`, `PackageMeta`, `Dependency`, `Targets`...) and per-command `StateId` enums (`states`); +- `errors` / `lock` — split out of `types` into their own top-level public modules: `CommonError` (a wrapper over `HookError`/`DecoderError`/`RepoError`/`DatabaseError`/`SysrootError`/`LockError`/`DeployRecordError` — all of these are now also public, each under its own module above) and `Lock`/`LockError` (the exclusive system lock file, see **§5.9**); + +***Not yet started:*** `etc_merge` (the 3-way `/etc` merge, §5.1), `boot` (the boot entry, one-shot boot/confirmation/rollback, see **§5.2**; it relies on `composefs-boot`, and grub via `blscfg` — there are no separate plugins, only BLS-compatible bootloaders, by design), `gc` (the retention and cleanup policy, see **§5.5**), and the actual construction of the package dependency graph on the `lib` side (right now the decoder only hands back a raw list of a package's dependencies via `decode` — nothing walks the graph yet, and there's no networking layer for downloading packages either). + +Build-time config: table names, deploy paths, the lock address — these come from `lib.toml` + `build.rs`, which generate simple constants, rather than from a separate `derive-static` crate — that idea was replaced. A further replacement is possible in the future. + +**`cli/`** — a thin wrapper over the library: + +- `args` — argument parsing; +- `commands` — one module per command; +- `render` — rendering progress, events, and conflicts from hooks; +- `ffi` — the binding to the core's C-ABI. + +Changes are possible as a result of further adaptation once the library's code stabilizes. + +**`decoders/`** — plugins (one per package packaging/compression format: alpm / deb / rpm / xbps, etc.). They are loaded and invoked by `lib` (the `decoder` module, inside the parent `plugin` folder), **NOT** by the CLI or other external code. Which plugin to load for which format is decided by the declarative manifest (`/etc/upac.d/decoders/*.toml`, see **§5.8**). By default they are dynamic `.so` files; distribution maintainers can optionally build them with static linking instead. From d2a9e6bff6315283555d65c6dd39b0ab12f9e2a9 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 08:20:29 +0400 Subject: [PATCH 45/68] fix: Removed obsolete documentation file fix: Fixed English documentation menu in README --- README.md | 10 +- doc/UPAC project note.en.md | 460 ------------------------------------ 2 files changed, 9 insertions(+), 461 deletions(-) delete mode 100644 doc/UPAC project note.en.md diff --git a/README.md b/README.md index 24f1f46e..01cd298a 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,15 @@ It covers the disk layout, the deploy/rollback model, the `/etc` merge, GC, the Full architecture and design decisions live in the project design notes: -- [`Project note en.md`]() — English (canonical); +For english (canonical): +1. [`Introduction and definitions`](); +2. [`Problem statement`](); +3. [`Defining what the project is NOT (Non-goals)`](); +4. [`Disk structure`](); +5. [`Project repository structure`](); +6. [`Operating mechanisms`](); +7. [`FFI and the boundaries of interaction between the components`](); +8. [`Program modules`](). For russian: 1. [`Вступление и определения`](); diff --git a/doc/UPAC project note.en.md b/doc/UPAC project note.en.md deleted file mode 100644 index a9edc3ed..00000000 --- a/doc/UPAC project note.en.md +++ /dev/null @@ -1,460 +0,0 @@ - - -# UPAC — Project Document - -Project document. -Project branch: **`lib-rs`**, crate `lib-rust/`. - ---- - -## 0. Introduction and definitions - -This section explains terms so the document reads even without a systems-programming background. Later on, words from it are used without explanation. - -### 0.1 Basic concepts - -- **File** — a named piece of data on disk. -- **Directory (folder)** — a container for files and other directories. -- **Path** — a file's address in the directory tree, e.g. `/usr/bin/bash`. -- **Filesystem (FS)** — the way data is laid out on disk so the OS sees it as files and folders. -- **Disk / block device** — a storage device (physical or virtual). -- **Partition** — a dedicated slice of a disk that holds one filesystem. -- **GPT** — the modern disk partitioning scheme. -- **Mount** — "attaching" a partition's filesystem at a point in the directory tree; afterwards its contents are reachable at that path. -- **Kernel** — the core of the OS: manages hardware, memory and processes. -- **initramfs** — a tiny temporary filesystem the kernel brings up first, to prepare and mount the real system root. -- **Bootloader** — the program the firmware (UEFI) launches, which in turn launches the kernel. -- **cmdline (kernel command line)** — the set of parameters passed to the kernel at start. -- **Hash / digest** — a short "fingerprint" of content: identical data yields the same hash, any change yields a different one. -- **Package** — a bundled set of files (a program plus its data and metadata), installed as a unit. -- **Package manager** — the program that installs, updates and removes packages. -- **Atomicity** — an "all or nothing" property: an operation either applies in full or not at all, with no half-applied intermediate states. -- **Immutable (system)** — a system whose core part is mounted read-only and is not changed in place. - -### 0.2 System folders - -What each is responsible for — and how it maps onto the "main item → sub-item" hierarchy. - -- **`/usr`** — the system base: libraries, binaries, everything responsible for running and booting. Immutable. The **main item** — the axis of failure rollbacks. -- **`/etc`** — system configuration. The **sub-item** — bound to its `/usr`, versioned separately, and on rollback it follows its main item. -- **`/var`** — mutable runtime state: logs, data, DB. Persistent, not versioned, not lost on rollback. -- **`/home`** — user data. Persistent, outside versioning. -- **`/boot` and ESP** — the partition read by the UEFI firmware: boot entries and the kernel/initramfs (UKI). The system starts here before the real root appears. -- **`/sysroot`** — the point where the physical root partition is mounted; the real `/` is assembled over it. - -### 0.3 Project jargon - -Terms the rest of the document operates with. - -- **Content-addressed store (CAS)** — storage where a file is addressed by the hash of its content, not by name; identical files are stored once (deduplication). -- **Image** — a whole content-addressed snapshot of a file tree (`/usr` or `/etc`) at a specific version. -- **Digest** — an image's hash, its identity. -- **Deployment** — a specific deployed version of the system; in this model = a pair `(usr-digest, etc-digest)`, but an asymmetric one: `usr` is primary and externally addressable — it's what the boot cmdline names (`composefs.digest=`, §5.2) and what keys `state/deploy//` (§3, §5.7); `etc` is secondary and resolved from *inside* that usr's own record (its `working_etc` field, §5.7), never carried as its own cmdline parameter. Cmdline tells you "which usr"; "which etc" follows from that record, not from cmdline directly. -- **Ref** — a human-readable named pointer to an image. -- **overlay (lower / upper)** — stacking a writable layer (upper) over a read-only one (lower); this is how the live `/etc` sits over the image. -- **fs-verity** — a kernel feature that cryptographically attests a file's content and catches any change to it. -- **One-shot entry** — a boot entry the firmware/bootloader selects exactly once, after which it reverts to the persistent default (the basis of auto-rollback, §5.2). -- **`base` / `new` / `live`** — the three inputs of the 3-way `/etc` merge (§5.1): the old default, the new default, the user's current state. -- **`.upac-new`** — a new config default placed alongside on a conflict, so as not to overwrite the user's edit. -- **`seq`** — a monotonic deploy sequence number; the authority for history order and rollbacks. -- **Pin** — a deploy protected from garbage collection (active, rollback target, user-pinned). -- **Rollback** — returning to a previous version: of the main item (`/usr`) on a failure, or of the sub-item (`/etc`) separately. - ---- - -## 1. Problem statement - ---- - -**The state problem.** -On an ordinary Linux system, installing, updating and removing software changes the running system *in place* — editing its files right where it currently lives. Because of this the system has no single verifiable state at any moment: it is a heap of individually mutated files. Three consequences follow: an interrupted or failed change leaves the system in a broken half-state; you cannot reproduce or prove a specific "known-good" state; and there is no clean way to go back. - -**Solution to the state problem.** -UPAC treats every system state as a **whole, content-addressed, verifiable image**. Any operation (install / update / remove) does not touch the running system but **builds a new image from the current one**. Switching to the new image is a single atomic step, and the previous image stays intact. Three properties follow directly and answer the problem: an operation either completed in full or the system stayed as it was (**atomicity**); any state is identified and attested by hash (**reproducibility and verifiability**); rollback is simply booting the previous image (**reversibility**). Meanwhile disk layout, bootloader and kernel stay fully under the user's control. - ---- - -**The access problem.** -The system's base tree (`/usr`, etc.) is immutable and manager-owned. If a user just wants to add their own files there — say drop wallpapers or assets a package expects in `/usr` — they cannot simply copy them in. They have to wrap a couple of files into a full package (metadata, build, install) just to place them. The barrier to adding your own files to the managed tree is unreasonably high. - -**Solution to the access problem.** -UPAC lets you add arbitrary user files into the managed tree (including `/usr`) directly, with a single command, without authoring a package. The file lands in the built image as first-class content, but the user needs none of the packaging pipeline. - ---- - -**The compatibility problem.** -For one and the same Linux kernel there are many incompatible package formats (deb, rpm, pkg.tar, etc.) and distributions. Software built for one format won't install into another without contortions; the user is locked into the ecosystem of their package format, even though the kernel and ABI are shared by all. - -**Solution to the compatibility problem.** -UPAC is not tied to a single package format. Parsing a specific format is moved into separate backends (one per format) that bring a package to a common internal representation — a file tree plus metadata. Thanks to this a single manager installs packages of different formats onto one system, and the format stops being a compatibility boundary. - ---- - -**The management problem.** -Even once a file is in the system, it cannot be attached to a package as a user file — so the manager would track it and clean it up together with the package. This hurts especially in `/usr`: manually added files remain "orphans" outside any accounting — invisible on removal and not cleaned automatically. - -**Solution to the management problem.** -UPAC lets you attach a file to a package as a user file, fully accounted for in the database. Such a file inherits the package's lifecycle: it is tracked, shown as part of the package, and removed together with it, wherever it lives (including `/usr`). User additions stop being untracked clutter. - ---- - -## 2. Non-goals - -Things reasonably expected of a manager of this kind that UPAC deliberately does NOT do — to draw the boundaries and head off future "just add this too". - -- **Not a distribution.** UPAC is a package manager and a deployment mechanism, not an OS. It does not ship a curated repo, a default software set or a release cycle; it manages whatever content it is pointed at. -- **Not a configuration manager.** UPAC merges `/etc` and preserves user edits across updates, but it does not generate or enforce config policy — it is not Ansible or NixOS modules. It preserves and reconciles, it does not author. -- **Not a container runtime.** UPAC uses the same building blocks as containers (composefs, OCI) but deploys a host system, not containers. It does not replace docker/podman. -- **No in-place changes — by design.** Any change to the system produces a new image; there is no hot in-place file replacement on the live system, not even as an option. This follows directly from the project's principle (see §1, "The state problem"). -- **Not a repository server.** UPAC is only a client to existing external repos (distro mirrors, OCI registries, etc.); it does not stand up its own repository or server. The only local alternative to a repo is delivering the image as a file (`--file`). -- **Does not fix the filesystem or the disk.** UPAC owns the correctness of its own operations (package verification, atomic images, repo integrity) and via fs-verity **detects** content corruption, refusing to boot a corrupted deploy. But repairing the FS itself, bad blocks, a degraded drive or hardware errors is out of scope: that's the job of `fsck`, SMART and disk replacement. System breakage caused by a bad disk is not a UPAC failure. - ---- - -## 3. Disk structure - -From here on the document describes the concrete implementation. Its core is the **composefs** dependency: it provides the content-addressed store, building a verifiable image, and mounting it. This section describes what physically lives on the disks of a deployed system. - -### Map - -``` -[block device, GPT] -│ -├── ESP (FAT32) (1) -│ ├── EFI/Linux/upac-from.efi (2) -│ ├── EFI/Linux/upac-to.efi (2) -│ └── loader/entries/*.conf (3) -│ -├── deployment partition → /sysroot (4) -│ ├── composefs/ (5) -│ │ ├── meta.json (6) -│ │ ├── objects// (7) -│ │ ├── images/ (8) -│ │ │ ├── → ../objects//… (9) -│ │ │ └── refs/ → ../images/ (10) -│ │ └── streams/ (11) -│ │ ├── → ../objects//… -│ │ └── refs/ -│ └── state/deploy// (12) -│ ├── meta.json (12) -│ └── etc-upper/{upper, work} (13) -│ -├── /var partition → /var (14) -└── /home partition → /home (15) -``` - -### Legend - -- **(1)** ESP (EFI System Partition) — a separate FAT partition read by the UEFI firmware; mounted at `/boot` (or `/efi`). It holds everything needed to start up before the real root appears. -- **(2)** `upac-from.efi` / `upac-to.efi` — two fixed UKI slots (a signed kernel + initramfs + cmdline image) for direct-UKI boot. An operation writes the new UKI to the inactive slot; switching goes through `BootNext`. -- **(3)** `loader/entries/*.conf` — BLS entries for machines with a boot manager (systemd-boot, etc.), an alternative to direct-UKI. Read/written via `BootconfigParser`. -- **(4)** deployment partition — the physical root with all the system's content; at runtime mounted at `/sysroot`. FS requirement — **fs-verity** support (ext4 / btrfs / xfs). The real root `/` is assembled over the image by mounting (overlay); there is no unpacked file tree on disk. -- **(5)** `composefs/` — the composefs repository: the content-addressed store of all files and images. The composefs default path in system mode. -- **(6)** `meta.json` — repo metadata: format version + fs-verity algorithm (`fsverity--`). -- **(7)** `objects/` — the content-addressed store; objects are laid out under subdirectories from the first 2 hex chars of the hash. Identical content is stored once (deduplication). -- **(8)** `images/` — EROFS images: content-addressed snapshots of `/usr` **and** `/etc` trees (a deploy references the pair). They carry the tree metadata, file data is taken from `objects/`. -- **(9)** `` — an image = a symlink to an object in `objects/`. The image digest = its identity (also in the boot cmdline). -- **(10)** `refs/` — a human-readable named pointer to an image. -- **(11)** `streams/` — splitstreams (imported layers/commits as a content source), also symlinks into `objects/` plus their own refs. -- **(12)** `state/deploy//` — a deploy record, **keyed by `usr-digest`** (one per distinct `/usr`, deduped). `meta.json` carries: `usr_digest`, `seq` (birth order, the rollback authority), `timestamp` (for display), `etc_history` (an ordered list of this `/usr`'s `etc-digest`s) and `working_etc` (the working sub-item, set by boot-confirm). Re-arriving at the same `usr-digest` = switching to this record, not a duplicate. See §5.7. -- **(13)** `etc-upper/{upper, work}` — the **live `/etc`**: uncommitted edits as an overlayfs upper layer over the current `working_etc` (§5.1). Sealed into an `etc-digest` on a `/usr` change or via `upac commit`; `work` is the overlayfs service directory. -- **(14)** `/var` — persistent runtime state (logs, data, DB): a **separate real partition**, not versioned and not lost on rollback. The composefs reference per-digest overlay for `/var` is NOT used here — a real partition is mounted. -- **(15)** `/home` — user data: a separate persistent partition, outside versioning. - ---- - -## 4. Repository structure - -The target layout of the project repository. - -### Map - -``` -upac/ -├── Cargo.toml (1) -├── rustfmt.toml -├── README.md -├── CHANGELOG.md -├── LICENSE -├── SECURE.MD (2) -├── .github/ (3) -├── .gitignore -├── doc/ (4) -├── lib/ (5) -├── derive-static/ (6) -├── cli/ (7) -├── decoders/ (8) -│ ├── alpm/ -│ ├── deb/ -│ ├── rpm/ -│ └── xbps/ -└── tests/ (9) -``` - -### Legend - -- **(1)** `Cargo.toml` — the workspace (`lib`, `derive-static`, `cli`). -- **(2)** `SECURE.MD` — the project security policy. -- **(3)** `.github/` — CI configuration. -- **(4)** `doc/` — project documents (this note, etc.). -- **(5)** `lib/` — the Rust core of the library. -- **(6)** `derive-static/` — a proc-macro crate: variables from a constants file. -- **(7)** `cli/` — the CLI frontend. -- **(8)** `decoders/` — package-format decoder plugins, one per format. -- **(9)** `tests/` — integration tests. - ---- - -## 5. Mechanisms - -Separate mechanisms the core (`lib/`) implements, either additionally or by using composefs mechanisms. - -### 5.1 Config merge (`/etc`) - -The live `/etc` at runtime is `overlay(lower = the committed etc-digest, ro; upper = uncommitted edits, rw)`. `/etc` is versioned content-addressed: each snapshot = an `etc-digest` (see §5.7). The merge's job — on a `/usr` change, build the new `/etc` (carry over the user's edits, pull in the new package defaults) and seal the result as the first `etc-digest` of the new `/usr`. - -The mechanism is library-side and runs at the `merge` stage, before the new deploy becomes bootable. - -**Three inputs (3-way):** -- **base** — the `/etc` defaults of the current `/usr` (the one the live system was built from); -- **new** — the `/etc` defaults of the new `/usr` being deployed; -- **live** — the user's current live `/etc` (the committed `working_etc` + the previous deploy's uncommitted upper). - -**Per-file classification:** -- the user did NOT touch the file (`live == base`) → the **new default** goes into the result; -- the user edited it, and the new default equals the old one (the package didn't change the file) → the user's version is preserved; -- the user edited it AND the new default changed (a conflict) → the user's version stays live, and the new default is placed alongside as `.upac-new` (excluded from future classification — it is not "the user's file"). - -**Conflicts — via a hook, non-blocking.** The operation does not stall: the deploy goes through, and `.upac-new` files signal to the user via a hook event (in the CLI) that there is something to reconcile. - -**The result** is sealed into a new `etc-digest`, which becomes the new deploy's `working_etc`; its live upper starts empty. Unchanged files are deduplicated by composefs at the object level, so an `etc-digest` is a full snapshot of `/etc` without duplicating content. - -On `upac commit` the same mechanism seals the current live `/etc` without a `/usr` change — a new `etc-digest` under the same `/usr`. - -### 5.2 System boot and rollback on failure - -Rollback is built on a one-shot boot choice plus a late confirmation that the system started successfully; there is no separate attempt counter — an image that failed to boot the first time, for whatever reason, is not retried. - -**The one-shot choice mechanism.** The bootloader/firmware has a pair "one-shot entry / persistent default": UKI-direct — `BootNext` / `BootOrder`; systemd-boot — `LoaderEntryOneShot` / `LoaderEntryDefault`; grub — `grub-reboot` / the persistent default in its config. The firmware/bootloader clears the one-shot variable on any boot, so it is itself a single-attempt auto-rollback. - -**Staging and boot:** - -1. On deploying D' a boot entry with `composefs.digest=D'` is written (a UKI to the inactive `upac-to.efi` slot, or a BLS conf via `BootconfigParser`), but it is NOT made the persistent default — it is set as the one-shot entry for the next boot; the persistent default stays on the previous working deploy. -2. Reboot: the bootloader boots D' once, the one-shot variable is cleared. initramfs mounts the digest from the cmdline (composefs overlay), then pivot and PID1. -3. The system reached a healthy state — a late hook / init unit makes D' the persistent default and marks the **pair as working**: it updates the current `/usr`'s `working_etc` (§5.7). This is the confirmation. -4. The confirmation did not fire (the system went down earlier, for any reason) — the one-shot variable is already cleared, so the next boot goes to the persistent default, i.e. the previous deploy. This is the auto-rollback. - -**D' is the usr-digest, not a combined pair.** `composefs.digest=D'` carries exactly the `usr-digest` — the same value that keys `state/deploy//` and that `open_tree()` (see composefs module, §7) takes directly, with no translation step. This is also how "the currently running deploy" gets resolved at runtime with no separate pointer file on disk (§5.7 calls this out too: "the active deploy is a separate pointer — the booted `composefs.digest` / boot default"): read `/proc/cmdline`, pull `composefs.digest`, that's the usr-digest. The `etc` side of the pair is deliberately NOT in cmdline — once the usr-digest is known, its `state/deploy//meta.json` is read for `working_etc` (§5.7), which names the currently confirmed `etc-digest`. - -**Why an unrecognized parameter like `composefs.digest=` survives in `/proc/cmdline` at all.** `/proc/cmdline` is not a filtered view of parameters the kernel understands — it's the raw, untouched string the bootloader handed the kernel. When the kernel's argument parser meets a parameter it doesn't recognize, it does not drop it; it logs "Unknown kernel command line parameters ..., will be passed to user space" and leaves the string intact for `/proc/cmdline` and PID 1's own cmdline. This is standard, relied-upon kernel behavior — it's exactly how `systemd.*`, dracut's `rd.*`, `luks.uuid=`, and OSTree's own `ostree=` already work: none of them are kernel parameters either, all of them are userspace-only, all of them survive the same way. - -**Rollback tiers** (which level catches what): - -1. Kernel or initramfs did not come up — the firmware itself goes to the persistent default (one-shot cleared) = the previous deploy. -2. Booted, but PID1 did not come up — the confirmation did not arrive, the next boot rolls back; on a manager the previous deploy can also be picked manually from the menu. -3. PID1 came up, but services/network/GUI are dead — rollback from the live system via `upac rollback`, or reboot to the menu. -4. Full brick — the firmware menu, or a Live-USB + `upac rollback --root`. - -If the system formally reached a working state and confirmed, but some subsystems or tools did not come up or work incorrectly — a manual rollback is available: `upac rollback` from the live system, or the firmware/bootloader menu. - -**Limitations (deliberate):** - -- one attempt, not N: a broken atomic image is deterministically broken, retrying makes no sense; -- auto-confirmation proves "reached a healthy target", not "the user is happy" — deeper breakage is rolled back manually via `upac rollback`; -- a pure hang (PID1 alive but stuck, no panic and no reboot) needs a manual power-cycle for the one-shot variable to take effect. - -### 5.3 Deployment staging (stage) - -Input — image D', already in the repo (`images/D'`); output — a deploy ready for a one-shot boot. It links operations (§5.4) to boot (§5.2). - -1. `/etc` merge (§5.1): the merge seals an `etc-digest` for D' and sets it as `working_etc`; the live upper (`etc-upper/`) starts empty. -2. Persistent partitions (`/var`, `/home`) — real, mounted as-is, untouched. -3. Write the boot entry with `composefs.digest=D'`: - - UKI-direct — build and sign the UKI (kernel + initramfs + cmdline), write it to the inactive `upac-to.efi` slot; - - manager — `BootconfigParser` writes a BLS conf (`options composefs.digest=D'`) into `loader/entries/`. -4. Set D' as the one-shot entry for the next boot (§5.2): UKI-direct — `BootNext` on the slot; manager — `LoaderEntryOneShot` / `grub-reboot`. The persistent default is not touched — it stays on the previous deploy. - -Then — reboot and §5.2 (boot, confirmation or auto-rollback). - -### 5.4 Operations: add / remove / update - -All three are one shape: change the tree → commit a new image → hand it to deployment staging (§5.3). The old image is not touched until the switch (atomicity). This is where decoders and the resolver work, and where the package DB is written. - -Common pipeline: - -1. Build a new tree from the current one (the difference is per-operation, below). -2. Commit the tree as a new image D' into the repo (`objects/` + `images/D'`); the package DB is written inside the image. -3. Hand D' to deployment staging (§5.3). -4. Light deploy-prune (§5.5) as the final stage. - -Difference in step 1: - -- **add (install):** the decoder parses the package → the resolver adds dependencies → new tree = current + the package(s) files. -- **remove:** new tree = current − the package's files − attached user files. -- **update:** the decoder parses the new version → new tree = current with the package's files replaced; the `/etc` merge (§5.1) at staging carries in the new defaults. - -### 5.5 Garbage collection (GC) - -Two levels: deployments (what we keep) and objects (what to sweep). The retention policy is set by the user; the object-sweep engine is composefs. - -**Immutable pins** (never removed): - -- the active (booted) deploy; -- the rollback target (the persistent default); -- the staged-but-unconfirmed deploy (the one-shot entry). - -Plus the user's manual pins (pinned deploys) and the last N within the user-set depth. - -**Triggers:** - -1. **Light deploy-prune — as an internal stage** after each mutating operation: drop the image ref and remove `state/deploy//` for deploys outside the policy. Cheap, the pins hold what's needed. -2. **Heavy object-sweep — manual only**, via the `upac gc` command: walk `objects/` and `streams/` and sweep the unreachable (composefs `ObjectCollector`). -3. GC is never hung on the boot/confirm path, nor on a timer. - -### 5.6 OCI (planned) - -This section is future work. OCI here is a portable image-artifact format, not a network protocol; UPAC does not stand up its own network stack beyond the repo. - -**Directions:** - -- **import** — take an OCI image and deploy it as a host system; -- **export** — produce a portable OCI image artifact from a deploy (a ready "reference copy"). - -**Image delivery — two existing paths:** - -1. **from an external repo** (as a client; the default) — the same mechanism as for packages; -2. **`--file `** — a local file, taken and deployed directly. - -Fleet deploy (a reference image → a fleet of machines) travels by the same two paths: image in the repo → machines pull, or handed out as a file. There is no separate fleet transport and no registry push. - -Building blocks (`composefs-oci`): `create_filesystem` (layers → image), `generate_boot_image`, `pull_image`. - -### 5.7 History and rollback by N deploys - -History is stored NOT in the image (it would break content-addressing) nor in ESP, but on the writable partition. The source of truth is the `state/deploy//` directories themselves; there is no separate journal. - -**Two axes.** - -- **`/usr` — the linear deploy history.** Each distinct `/usr` = one record, keyed by `usr-digest`. `seq` is the birth order of records (monotonic, one per digest; high-water-mark in `state/next-seq`, written tmp+rename). Re-arriving at an existing `usr-digest` **switches** to its record rather than creating a duplicate — so that `/usr`'s `/etc` sub-history stays intact when you return. The record carries its own **commit message**: `subject` (short, required) + an optional long `message` — the commit message of the operation that gave birth to this `/usr` (install/uninstall/update). -- **`/etc` — a sub-history within `/usr`.** The record's `meta.json` carries `etc_history` — an ordered list of `{etc_digest, subject, message}` records taken under this `/usr` (on a `/usr` change and via `upac commit`, §5.1). Each record carries its own `subject` + optional `message`; the first record, created by the automatic merge on a `/usr` change (§5.1), inherits the subject+message of that `/usr` event itself — later explicit `upac commit` calls get their own, independent subject+message. - -**The active deploy is a separate pointer** (the booted `composefs.digest` / boot default), not `max(seq)`: after switching to an old record, its `seq` stays as it was. - -**Rollback:** - -- **failure (`/usr`)** — to the Nth existing deploy in `seq` order (by actual presence, **not** by arithmetic `seq−N`: GC, pins and burned numbers leave gaps in `seq`, which we skip). The **pair** is restored: the target `usr-digest` + its `working_etc` (the last confirmed sub-item) — config edits are not lost. -- **config (`/etc`)** — `upac rollback --etc` to a previous `etc-digest` from the current `/usr`'s `etc_history`; the same rank-based mechanics and its own retention depth. - -**`seq`** is authoritative for order and rollbacks; **`timestamp`** in `meta.json` is for display only (`upac history`), the order is always by `seq`, so that clock drift or a time change does not reorder history. - -Relation to GC (§5.5): the retention depth on each axis must be **≥ the max rollback N** for that axis, otherwise history is shorter than the promise — the deploy at position N is already swept. - -### 5.8 Hooks (pre/post triggers) - -A hook is not code but a declarative, **signed** file: it describes a trigger, a priority, and a composition of **primitives**. A primitive is a closed set of low-level actions baked into `lib` (spawning a process, touch/move a file, etc.) — the only thing that requires a code change in `lib`. The hook as a unit is not an `enum` and is not enumerated anywhere in code: it is entirely described by data in the file, and `lib` is just a generic executor of that composition. The signature guards against an arbitrary, unsigned hook file being dropped in (primitives are privileged enough that a file without one can't be trusted). - -**Compatibility table.** The hook file separately carries a table: for decoder `D` (§6 — a package-format plugin: deb, rpm, native, …) this hook covers such-and-such of its NATIVE trigger name (e.g. deb's `Triggers-Interest: update-mime-database`). This way, compatibility with a foreign trigger convention is also data in the file, not hardcoded inside the decoder. - -**Priority and criticality.** `priority` is a plain signed integer (default 0), needed ONLY to resolve a conflict when several different hook files claim the same native trigger name (the same key `k` in the compatibility table) — the higher `priority` wins; an exact tie is an unresolvable conflict, and `lib` fails outright rather than silently picking one (`build_trigger_table` resolves this before a table is ever sent to a decoder — a tie is a hard `Err`, same treatment as two decoders claiming the same package format, see below). Unmatched entries (a hook's native trigger name simply isn't present in this particular package) need no separate reporting at all — not being matched is the normal, expected outcome for most hooks on most packages, not an error. Whether a *non-fatal* warning should still surface through `MessageHook` for either case (a conflict, or a hook that structurally never matches anything) is still an open question. `priority` sets no execution ordering — all hooks matched to a single trigger point run concurrently, with no tiers. Criticality (abort vs. best-effort on hook failure) is a field on the hook file itself (`critical = true/false`), not a property of the primitive: a primitive is neutral, and whether "failure means abort the whole operation, or just warn" is known only to the hook's author, who already signed the file (trust is already established through the CA chain, no extra primitive-level veto is needed). - -**Division of labor:** - -- **`lib`** — the only side that reads hook files off disk, verifies the signature, and parses the primitive composition and the compatibility table. It executes the composition through its own primitives. -- **Decoder (plugin)** — does not parse hook files itself. It receives from `lib` the compatibility table already as a ready k:v map **scoped to its own `D`** (a deb decoder never gets entries meant for rpm/native, for instance), where k is the decoder's native trigger name and v is our hook. The decoder itself matches this against the native trigger names it read out of the package (e.g. the deb decoder reads the package's own `Triggers-Interest`), and hands back to `lib` over FFI the list of hooks to execute (the matched v's). Matching stays on the decoder's side, but its input is already-parsed data from `lib`, not a raw hook file. - -**Decoder resolution.** Decoders are found via declarative TOML manifests in `/etc/upac.d/decoders/` (one per decoder — `format`, `extensions`, `library`), never by scanning or dlopen-probing `.so` files directly: `format` is the canonical package-format identity, the same string used as the key `D` in a hook's compatibility table (one decoder never covers more than one format); `extensions` lists the file variants that format actually ships as (e.g. alpm packages as `pkg.tar`/`pkg.tar.gz`/`pkg.tar.xz`/`pkg.tar.zst` — one format, several file shapes); `library` names the `.so` to `dlopen` on first use, lazily, only once a package of that format is actually being processed. A duplicate `format` across two manifests is a hard error at manifest-load time, same treatment as the priority-tie case above. Since none of this touches actual shared-library resolution, decoders don't need to (and don't) self-report their own identity over FFI — the manifest is the sole source of truth. - -**Hook file format and signing.** The hook file is TOML, living in `/etc/upac.d/hooks/` (the path is baked in as a constant, `Lib.toml`-style). The signature is a separate sidecar file (`name.hook` + `name.hook.sig`), not a field inside the TOML (otherwise byte canonicalization on re-serialize/re-parse would have to be pinned down). The signature chains through 2 tiers: a root CA (offline key, only ever signs the next tier) → a signing certificate per trust domain (e.g. "upac-core", or one per distro/maintainer), which directly signs the `.hook` file's bytes; there is no separate leaf tier — extra key rotation with no real benefit at this narrow a scope. The `.sig` file carries both the signature and the signing certificate itself in full — verification is self-contained (root + `.sig`, nothing else to look up). **The root is a configurable file**, not baked into the binary — that's the whole point of this system: a distro/OEM plugs in its own root without rebuilding `upac`. The signature scheme is Ed25519 over X.509 certificates, via the `x509-cert` crate (pure Rust, RustCrypto — no OpenSSL/CMS dependency). Implemented in its own crate, `upac-pki` (`lib/pki/`, LGPL, no dependency on `upac-abi`/`upac-lib`) — `RootIdentity`/`SigningIdentity` (generation), `HookSignature::sign`/`verify`, and the `Identity` trait (`to_bytes`/`from_bytes`) for saving/loading key+cert pairs across process invocations. Depended on by both `upac-lib` (verification) and `upac-sign-cli` (signing), so the `.sig` byte format can't drift between the two sides. - -**Execution model.** Hook execution is asynchronous, but entirely inside `lib`: the FFI stays synchronous (the `extern "C" fn` calls a plain function, which spins up a `tokio` runtime and does `.block_on(...)`). The scope is limited to concurrently running the N independent hooks of a single trigger point **inside one `Stage::run()`**; the `Orchestrator` itself does not become async — stages must still run linearly. For CPU-bound work (hashing the tree on add/remove/update, §5.4) — `rayon`/plain threads, not async: local `tokio::fs` on Linux without io_uring is itself blocking under the hood (`spawn_blocking`), so async gives no benefit there. `tokio-uring` (io_uring) is deliberately not adopted: it's a different, cancellation-unsafe buffer-ownership model, Linux-only, younger, and viewed cautiously from a security standpoint — revisit only if profiling actually shows syscall overhead dominating on huge trees. - -### 5.9 Operation cancellation - -A second hook channel, independent of §5.8 — not declarative pre/post triggers, but a system-level cancellation signal. `CancelToken` (`#[repr(C)]`, an atomic flag) is created by the calling side (CLI/GUI) and passed into `lib` as a pointer with every request. - -**`Lock`** is a pure mutual-exclusion mechanism between rw operations, with no connection to cancellation at all (bind on a fixed abstract Unix address shared by all rw calls: taken — `EADDRINUSE` → `LockError::Busy`; free — proceed). The `Orchestrator` itself holds it for the whole rw operation; ro doesn't take it at all. - -**Access to cancellation is an explicit parameter, no wrapper.** `&CancelToken` is passed directly into `Stage::run`/`Orchestrator::run`, and each stage reads `.is_cancelled()` itself, including inside its own loops. There is no separate wrapper combining `Lock` and the token — these are two independent mechanisms with no relation to each other, exactly as intended at the start of this section. - -### 5.10 Operation progress - -The same hook channel as §5.9 (`MessageHook`), but for progress tracking rather than cancellation. The point of `data` isn't an abstract "extra info" tack-on — it's specifically tracking what's happening INSIDE a stage: each event is a transition into a named sub-step (phase) of the current stage's mini-FSM, not a generic completion percentage. - -The payload is one common `#[repr(C)]` type for every event, not a separate shape per event (a tagged union would be awkward over a C-ABI, and there's nothing yet to check the shape against — stage bodies aren't written): `stage` (which stage, as before — `StateId as u32`), `phase` (which sub-step within the stage; the meaning is owned by the stage itself, like `StateId`), `subject` (a borrowed string — what this concerns: a package, a file, a hook), `current`/`total` (an item counter, `0` if not applicable/unknown). `MessageHook::send` takes one self-describing parameter instead of the former separate event/data pair — nothing to unpack separately. The `Orchestrator` creates the builder (with `stage` already set, from its own index in the pipeline — see §5.11) and passes it to the stage as a parameter; the stage fills in `phase`/`subject`/`progress()` and hands the builder back — the `Orchestrator` itself finalizes it (`.build()`) and sends it to the hook. - -### 5.11 Stage orchestration - -The mechanism that actually runs mutating commands (and later read-only ones too): a linear list of stages plus an engine that walks through them — both hook channels from §5.9/§5.10 plug in here. - -**A stage is always flat.** One stage does exactly one atomic unit of work per call, never loops internally. Each call itself decides what happens next — advance to the next stage, repeat itself (e.g. process one more file from an already-started list), or jump BACK to an earlier stage by its TYPE (not by a numeric index — so inserting new stages anywhere in the pipeline never breaks it). Through such a backward jump, a group of several stages (e.g. "verify package → unpack → register") can repeat as a whole — on a jump, the engine searches backward from the current position for the nearest stage of that type; if no such stage exists, that's a pipeline-assembly bug, not user input, and it comes back as an ordinary error, not a crash. - -**Every stage call brings its own rollback.** A stage doesn't accumulate state between calls — each call constructs its own, independent rollback object, carrying exactly the data needed to undo what THAT call did (even if the same stage was called many times before with different data — each call stays independent). If a stage has nothing to roll back this time, it must still return such an object, just in an "empty" mode: this is guaranteed at the compiler level (the "empty" rollback constructor is a mandatory part of the contract), not a convention that can be forgotten. The real, data-carrying rollback constructor (specific to each stage) is not part of the shared contract — every stage has its own, with its own signature. - -**The engine** holds the linear stage list; exclusivity isn't a stored mode but a choice of which METHOD you call to run it: one holds the system lock (unrelated to the cancellation in §5.9, a separate mutual-exclusion mechanism between rw operations) for the whole run, the other never touches it at all — for read-only. On any failure (a stage error, cancellation, an unresolved backward jump) it unwinds every rollback object collected so far, in reverse order, without stopping if one of them fails to roll back itself — everything that can be undone gets attempted, rather than bailing out halfway. Every successful stage call hands the engine two separate things at once — the progress builder (§5.10, which the engine itself created and passed to the stage before the call) and its own rollback object: the former is passed straight through to the hook as-is, the latter is accumulated on the stack for a possible unwind. - -**Engine failure** comes in two distinct kinds: either the pipeline couldn't even start (e.g. the lock was busy), or a specific stage at position N failed — the command that invoked the engine tells them apart, because in the first case no stage number exists at all. - -**Pipeline validation, before the first call.** Each stage can declare what it `requires` from the shared context and what it `provides` into it (by type); before running, the engine walks the whole list once and checks every stage's requirements are satisfied by what's already in the context plus what earlier stages provide — a missing dependency fails fast, before any stage actually runs, instead of surfacing as a confusing panic or `None` deep inside some later stage. This check is uniform across every command (both the exclusive and concurrent run paths). As of this writing no stage declares real requirements yet (stage bodies are still unwritten), so the check is currently a no-op everywhere — it activates automatically as stage bodies start declaring their real dependencies. - ---- - -## 6. FFI and boundaries - -`lib` holds all the logic and a stable C-ABI. CLI and (in the future) GUI are thin frontends: they only parse input and render events. Decoders are plugins that `lib` loads. Below are only the flows, without structs: the code behind the FFI lives its own life. - -``` -CLI ─┐ -GUI ─┼──(C-ABI)──▶ lib ──(dlopen)──▶ decoders/* (format parsing + resolve) -… ─┘ │ - └──(calls)───▶ composefs (repo / image / mount / boot) - -lib ──(hook callbacks)──▶ CLI/GUI (progress, events, /etc conflicts) -``` - -What travels along the arrows: - -- **frontend → lib:** a command with arguments (what to do) + a cancel token. -- **lib → decoders:** the path to a package; back — files, metadata, dependencies. -- **decoders:** dynamic `.so` by default (add a format = drop a plugin); optionally — a static single-binary build for distro maintainers. Loading and calling them is always owned by `lib`, not the CLI. -- **lib → composefs:** primitives (commit an image, mount, prune, write boot entries). -- **lib → frontend (hooks):** operation progress, confirmation events, `.upac-new` conflicts. - -**Boundary rule:** "touches state / needs root / must be atomic" → `lib`; "presents or collects input" → frontend. CLI and GUI are equal thin frontends over one FFI, with no logic duplicated. - -**`lib` has two public contracts.** Besides the stable C-ABI (`export`, via `dlopen`), the Rust layer itself is also public for direct static linking: `orchestrator`, `scripts`, `plugin`, `composefs`, `database`, `deploy`, `errors`, `lock` — all `pub mod`, with zero `pub use` re-exports (access only through the full path, e.g. `crate::database::error::DatabaseError`). What stays private: `export` (no reason to call the C-ABI from Rust), `mutated`/`unmutated` (assembling the 15 commands' pipelines — internal machinery), and `types` (the domain model); `Cursor` inside `orchestrator` is private too — it's `SequentialOrchestrator`'s stepping mechanism, not something external code touches. One more carve-out inside `composefs`: `repository::open`/`repository::open_tree` are `pub(crate)`, not part of the public contract — the only public way to get an open `Repository`/tree is `deploy::Deploy` (`open_repository`/`open_tree`), so the repository can't be opened bypassing the mounted sysroot. - ---- - -## 7. Planned modules - -A design guide, refined as we go. - -**`lib/`** — core and FFI (actual module layout, kept in sync as it's built): - -- `export` — the C-ABI: entry points for all 15 commands, ABI version, cancel, response freeing. -- `orchestrator` — the generic engine (§5.9–§5.11): `Stage`/`ConcurrentStage`, `Cursor`, `RollbackGuard`, and two orchestrators behind one `Orchestrator` trait — `SequentialOrchestrator` (linear, holds the system lock) and `ParallelOrchestrator` (concurrent stages, used for §5.8 hooks). -- `mutated` / `unmutated` — the 15 commands themselves, one submodule per command, each just assembling its own stage pipeline via `orchestrator`. -- `scripts` — §5.8: the hook TOML format (`HookFile`), primitives (`Primitive`/`TouchFile`/`MoveFile`/`CreateSymlink`, each `impl Step { execute, rollback }`), native trigger matching (`Operation`/`Timing`). `HookStage::run()` is fully wired for native triggers: get-or-build the shared `tokio` runtime off `Context`, load + verify + parse hook files (`load_hooks`, via `upac-pki`), filter by `NativeTrigger`, run the matched hooks concurrently through a `ParallelOrchestrator` (`HookFile` itself `impl ConcurrentStage`, executing its `steps` and respecting `critical`), and it's wired into all 6 mutating commands' pipelines (Pre/Post around each). Still open: the compatibility-table matching against a decoder's native trigger names (`HookFile.triggers` is parsed but not yet consulted) and `priority`-based conflict resolution. -- `plugin` — decoder loading; concretely just the `decoder` submodule today (dlopen, ABI-version check, `decode`/`match_triggers`) — the `plugin` parent is reserved for other plugin kinds later, none exist yet. Also holds `manifest` (`DecoderManifest`, `load_decoder_manifests()` — reads the declarative `/etc/upac.d/decoders/*.toml` descriptors, no `.so` scanning/probing) and `triggers` (`build_trigger_table()` — builds a decoder-scoped native-trigger→hook table out of loaded `HookFile`s, resolving `priority` conflicts with a hard error). Not yet wired to anything — still needs the actual call site, tied to the not-yet-written install/update stage bodies. -- `composefs` — access to the composefs repo. `repository` (`pub(crate)`, see §6): `open(path) -> Repository` (opens by path via `Repository::open_path`), `open_tree(repository, name) -> FileSystem` (reads an image via `Repository::open_image` + `erofs::reader::erofs_to_filesystem`) — both crate-internal only, the repository is only ever handed out externally via `deploy::Deploy`. `error::RepoError` — maps `RepositoryOpenError`/`ImageError`/`anyhow::Error` (the last one needed because `ensure_object`/`ensure_object_from_file`/`commit_image` etc. in composefs itself return `anyhow::Result` — no structured detail to recover from that, just the failure itself). `file::FileHandle` — a handle bound to a path in the tree, three `impl` blocks split by "touches CAS or not": constructors (`new` — blind, for inserting something new; `from_tree` — checks the path already exists), tree-only ops with no CAS (`insert_in_tree`/`update_in_tree`/`rename_in_tree`/`remove_in_tree`/`symlink_in_tree`/`hardlink_in_tree`/`stat_in_tree`/`symlink_target_in_tree`/`list_in_tree`), file ops through CAS (`insert_file` takes an already-open `&File`, not raw bytes, so the path is resolved exactly once and there's no TOCTOU race between reading the source and landing it in CAS; `replace_file` is an alias for `insert_file` since composefs's own `Directory::insert` already upserts; `read_file` resolves inline vs. external and pulls bytes from `Repository::read_object` when needed). -- `deploy` — deployment staging (§5.3): finds the block device backing `/` via `MountInfo`, the real filesystem type via `rsblkid::probe::Probe` (not `None` — `mount(2)` fails `EINVAL` otherwise, the kernel needs an explicit fstype for anything other than a bind/remount), `unshare(CLONE_NEWNS)` plus a mandatory `MS_REC | MS_PRIVATE` remount of `/` before the real mount (without it, mount events still leak into the host's mount table via propagation inherited from the parent namespace), then mounts the partition at `/sysroot`. `deploy(usr_digest) -> PathBuf` — a single path component (`state/deploy//`, §3), no more OSTree-style `(os, checksum)` pair. `open_repository()` / `open_tree(name)` — the sole public entry point into the current deployment's composefs repo (see above). -- `database` — the package DB (redb) inside the image; built via a custom in-memory `StorageBackend`, read at runtime with `ReadOnlyDatabase` from the file in the image. Also here: `record` — `DeployRecord`/`EtcHistoryEntry` (fields per §3 point 12: `usr_digest`/`subject`/`message`/`seq`/`timestamp`/`etc_history`/`working_etc`), physically not part of the redb DB (a standalone `meta.json` on the sysroot, not inside the image), but living here anyway since "how our types persist" is `database`'s concern regardless of format. Serialization is `#[derive(JsonCodec)]` (modeled on `RedbCodec` — same per-field codegen, just emitting `serde_json::Value` instead of a byte layout); `DeployRecord::write`/`read` write/read the file, `write` atomically (tmpfile in the same directory + `fsync` + `rename`, the same discipline composefs itself uses for its own `meta.json`). Its own error type, `error::DeployRecordError` (kept separate from `DatabaseError` — different storage formats, not stretching one error type over both). -- `types` — domain types (`Version`, `PackageMeta`, `Dependency`, `Targets`, ...) and per-command `StateId` enums (`states`); fully private, not reachable through either the C-ABI or the direct Rust API. -- `errors` / `lock` — split out of `types` into their own top-level public modules: `CommonError` (wraps `HookError`/`DecoderError`/`RepoError`/`DatabaseError`/`SysrootError`/`LockError`/`DeployRecordError` — all of them public too now, one under each module listed above) and `Lock`/`LockError` (the exclusive system lock, §5.9). Made public because external code implementing `E: From` to build its own pipeline via `orchestrator` under static linking needs to name `CommonError`. - -Not started yet: `etc_merge` (the 3-way `/etc` merge, §5.1), `boot` (boot entries, one-shot/confirmation/rollback, §5.2; uses `composefs-boot`, grub via `blscfg` — no separate boot plugins, only BLS-capable bootloaders by design), `gc` (retention policy and pruning, §5.5), and the actual dependency-graph resolution on `lib`'s side (the decoder only reports a package's raw dependency list today, via `decode` — nothing walks the graph yet, and there's no network layer to fetch resolved packages either). - -Build-time config (table names, deploy paths, the lock address) is `Lib.toml` + `build.rs`, generating plain constants — not a separate `derive-static` crate; that idea was superseded. - -**`cli/`** — a thin frontend: - -- `args` — argument parsing; -- `commands` — one module per command (install / remove / update / rollback / gc / …); -- `render` — rendering progress, events and conflicts from hooks; -- `ffi` — binding to the core's C-ABI. - -**`decoders/`** — plugins (`.so`, one per format: alpm / deb / rpm / xbps). Loaded and called by `lib` (the `decoder` module, inside a `plugin` parent reserved for other plugin kinds later), not the CLI. Which `.so` to load for which format is resolved from a declarative manifest (`/etc/upac.d/decoders/*.toml`, see §5.8), not by scanning the `decoders/` directory itself — `lib` never probes a `.so` just to ask what it is. By default — dynamic `.so`; optionally distro maintainers build statically (a single binary + linked-in decoders). Each exports `decode` (package → files, metadata, and dependencies, all in one call — `resolve` was folded into it, since the decoder already has everything parsed by then) and `match_triggers` (the §5.8 compatibility-table matching). From 7e8277540d77691e50f1e831c60b4e37b169a127 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 08:57:52 +0400 Subject: [PATCH 46/68] fix: fixed incorrect links in documents new: added link to Codeberg in README --- Cargo.toml | 2 +- README.md | 7 ++++--- SECURITY.md | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 912f9e24..5557d81b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = ["user/upac-cli", "user/sign-cli", "lib/macro", "lib/abi", "lib/lib" , version = "0.1.5" edition = "2024" license = "GPL-3.0-only" -repository = "https://github.com/justpav05/upac" +repository = "https://github.com/SmoothTeam/upac" authors = ["justpav05 "] [workspace.dependencies] diff --git a/README.md b/README.md index 01cd298a..0f95b2db 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # 📦 Upac -[![GitHub](https://img.shields.io/badge/GitHub-justpav05%2Fupac-181717?logo=github)](https://github.com/justpav05/upac) -[![Version](https://img.shields.io/badge/version-0.1.5-green)](https://github.com/justpav05/upac/releases) -[![REUSE status](https://api.reuse.software/badge/github.com/justpav05/upac)](https://api.reuse.software/info/github.com/justpav05/upac) +[![GitHub](https://img.shields.io/badge/GitHub-SmoothTeam%2Fupac-181717?logo=github)](https://github.com/SmoothTeam/upac) +[![Codeberg](https://img.shields.io/badge/Codeberg-justpav05%2Fupac-2185D0?logo=codeberg)](https://codeberg.org/justpav05/upac) +[![Version](https://img.shields.io/badge/version-0.1.5-green)](https://github.com/SmoothTeam/upac/releases) +[![REUSE status](https://api.reuse.software/badge/github.com/SmoothTeam/upac)](https://api.reuse.software/info/github.com/SmoothTeam/upac) [![lib: LGPL-3.0-or-later](https://img.shields.io/badge/lib-LGPL--3.0--or--later-blue.svg)](LICENSES/LGPL-3.0-or-later.txt) [![cli: GPL-3.0-only](https://img.shields.io/badge/cli-GPL--3.0--only-blue.svg)](LICENSES/GPL-3.0-only.txt) diff --git a/SECURITY.md b/SECURITY.md index 7d814301..3450d473 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -13,7 +13,7 @@ only the latest commit on `main` is supported. Security fixes are not backported Please use one of the following instead of opening a public issue: -- GitHub's [private vulnerability reporting](https://github.com/justpav05/upac/security/advisories/new) +- GitHub's [private vulnerability reporting](https://github.com/SmoothTeam/upac/security/advisories/new) (Security tab → "Report a vulnerability") - Email: aksenovpaveldmitrievich@gmail.com From 5afe67222afc96c646dd0b519af8c3e0fb42445c Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 09:32:12 +0400 Subject: [PATCH 47/68] fix: Fixed names Co-Authored-By: Claude Sonnet 5 --- lib/abi/src/request.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/abi/src/request.rs b/lib/abi/src/request.rs index 1eacf4c8..bc76b8ff 100644 --- a/lib/abi/src/request.rs +++ b/lib/abi/src/request.rs @@ -178,9 +178,9 @@ pub struct CDiffRequest { pub base: CRequestBase, #[optional] - pub from_commit_hash: CSlice, + pub from_prefix_digest: CSlice, #[optional] - pub to_commit_hash: CSlice, + pub to_prefix_digest: CSlice, } #[repr(C)] From bbd0d8ca612e403af2e9bb5b35dc1f2e5d11d502 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 09:32:32 +0400 Subject: [PATCH 48/68] fix: Removed linear function assemble Co-Authored-By: Claude Sonnet 5 --- lib/lib/src/unmutated/diff/error.rs | 7 ++++++- lib/lib/src/unmutated/diff/mod.rs | 14 +++++--------- lib/lib/src/unmutated/diff_files_config/mod.rs | 6 +----- lib/lib/src/unmutated/diff_files_prefix/mod.rs | 6 +----- lib/lib/src/unmutated/diff_packages/mod.rs | 6 +----- lib/lib/src/unmutated/list_commit/mod.rs | 6 +----- lib/lib/src/unmutated/list_history/mod.rs | 6 +----- lib/lib/src/unmutated/list_packages/mod.rs | 6 +----- lib/lib/src/unmutated/list_prefix/mod.rs | 6 +----- lib/lib/src/unmutated/search_files/mod.rs | 6 +----- lib/lib/src/unmutated/search_meta/mod.rs | 6 +----- 11 files changed, 20 insertions(+), 55 deletions(-) diff --git a/lib/lib/src/unmutated/diff/error.rs b/lib/lib/src/unmutated/diff/error.rs index e8ac8567..bd598091 100644 --- a/lib/lib/src/unmutated/diff/error.rs +++ b/lib/lib/src/unmutated/diff/error.rs @@ -5,9 +5,12 @@ use upac_abi::error::ErrorKind; +use crate::composefs::error::RepoError; use crate::database::error::DatabaseError; use crate::deploy::error::SysrootError; -use crate::errors::{CommonError, common_error_from, database_error_from, lock_error_from, sysroot_error_from}; +use crate::errors::{ + CommonError, common_error_from, database_error_from, lock_error_from, repo_error_from, sysroot_error_from, +}; use crate::lock::LockError; #[derive(Debug, Clone, PartialEq, Eq)] @@ -19,6 +22,8 @@ common_error_from!(DiffError); database_error_from!(DiffError); +repo_error_from!(DiffError); + sysroot_error_from!(DiffError); lock_error_from!(DiffError); diff --git a/lib/lib/src/unmutated/diff/mod.rs b/lib/lib/src/unmutated/diff/mod.rs index d4d603ad..a4880640 100644 --- a/lib/lib/src/unmutated/diff/mod.rs +++ b/lib/lib/src/unmutated/diff/mod.rs @@ -23,8 +23,8 @@ mod error; mod preparing; pub struct DiffData<'a> { - pub from_commit_hash: Option<&'a str>, - pub to_commit_hash: Option<&'a str>, + pub from_prefix_digest: Option<&'a str>, + pub to_prefix_digest: Option<&'a str>, pub hook_message: Option, pub hook_message_context: *mut c_void, @@ -41,8 +41,8 @@ impl<'a> TryFrom<&'a CDiffRequest> for DiffData<'a> { let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; Ok(DiffData { - from_commit_hash: (&request.from_commit_hash).try_into()?, - to_commit_hash: (&request.to_commit_hash).try_into()?, + from_prefix_digest: (&request.from_prefix_digest).try_into()?, + to_prefix_digest: (&request.to_prefix_digest).try_into()?, hook_message: request.base.on_hook, hook_message_context: request.base.hook_ctx, @@ -52,15 +52,11 @@ impl<'a> TryFrom<&'a CDiffRequest> for DiffData<'a> { } } -fn assemble() -> SequentialOrchestrator { - SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]) -} - pub fn run(data: DiffData) -> Result<(Vec, Vec), (DiffStateId, DiffError)> { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); - let orchestrator = assemble(); + let orchestrator = SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]); run_unmutated!( orchestrator, diff --git a/lib/lib/src/unmutated/diff_files_config/mod.rs b/lib/lib/src/unmutated/diff_files_config/mod.rs index ac68afd6..c7a53d42 100644 --- a/lib/lib/src/unmutated/diff_files_config/mod.rs +++ b/lib/lib/src/unmutated/diff_files_config/mod.rs @@ -52,17 +52,13 @@ impl<'a> TryFrom<&'a CDiffFilesConfigRequest> for DiffFilesConfigData<'a> { } } -fn assemble() -> SequentialOrchestrator { - SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]) -} - pub fn run( data: DiffFilesConfigData, ) -> Result<(Vec,), (DiffFilesConfigStateId, DiffFilesConfigError)> { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); - let orchestrator = assemble(); + let orchestrator = SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]); run_unmutated!( orchestrator, diff --git a/lib/lib/src/unmutated/diff_files_prefix/mod.rs b/lib/lib/src/unmutated/diff_files_prefix/mod.rs index 9ea2a284..38c61ef0 100644 --- a/lib/lib/src/unmutated/diff_files_prefix/mod.rs +++ b/lib/lib/src/unmutated/diff_files_prefix/mod.rs @@ -52,17 +52,13 @@ impl<'a> TryFrom<&'a CDiffFilesPrefixRequest> for DiffFilesPrefixData<'a> { } } -fn assemble() -> SequentialOrchestrator { - SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]) -} - pub fn run( data: DiffFilesPrefixData, ) -> Result<(Vec,), (DiffFilesPrefixStateId, DiffFilesPrefixError)> { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); - let orchestrator = assemble(); + let orchestrator = SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]); run_unmutated!( orchestrator, diff --git a/lib/lib/src/unmutated/diff_packages/mod.rs b/lib/lib/src/unmutated/diff_packages/mod.rs index bd3f93c8..61dfb231 100644 --- a/lib/lib/src/unmutated/diff_packages/mod.rs +++ b/lib/lib/src/unmutated/diff_packages/mod.rs @@ -52,15 +52,11 @@ impl<'a> TryFrom<&'a CDiffPackagesRequest> for DiffPackagesData<'a> { } } -fn assemble() -> SequentialOrchestrator { - SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]) -} - pub fn run(data: DiffPackagesData) -> Result<(Vec,), (DiffPackagesStateId, DiffPackagesError)> { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); - let orchestrator = assemble(); + let orchestrator = SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]); run_unmutated!( orchestrator, diff --git a/lib/lib/src/unmutated/list_commit/mod.rs b/lib/lib/src/unmutated/list_commit/mod.rs index c0ac128d..2fa97931 100644 --- a/lib/lib/src/unmutated/list_commit/mod.rs +++ b/lib/lib/src/unmutated/list_commit/mod.rs @@ -48,15 +48,11 @@ impl<'a> TryFrom<&'a CListCommitRequest> for ListCommitData<'a> { } } -fn assemble() -> SequentialOrchestrator { - SequentialOrchestrator::new(vec![Box::new(FetchingStage)]) -} - pub fn run(data: ListCommitData) -> Result<(Vec,), (ListCommitStateId, ListCommitError)> { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); - let orchestrator = assemble(); + let orchestrator = SequentialOrchestrator::new(vec![Box::new(FetchingStage)]); run_unmutated!( orchestrator, diff --git a/lib/lib/src/unmutated/list_history/mod.rs b/lib/lib/src/unmutated/list_history/mod.rs index a390d542..bdf3d7ec 100644 --- a/lib/lib/src/unmutated/list_history/mod.rs +++ b/lib/lib/src/unmutated/list_history/mod.rs @@ -44,15 +44,11 @@ impl<'a> TryFrom<&'a CListHistoryRequest> for ListHistoryData<'a> { } } -fn assemble() -> SequentialOrchestrator { - SequentialOrchestrator::new(vec![Box::new(FetchingStage)]) -} - pub fn run(data: ListHistoryData) -> Result<(Vec,), (ListHistoryStateId, ListHistoryError)> { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); - let orchestrator = assemble(); + let orchestrator = SequentialOrchestrator::new(vec![Box::new(FetchingStage)]); run_unmutated!( orchestrator, diff --git a/lib/lib/src/unmutated/list_packages/mod.rs b/lib/lib/src/unmutated/list_packages/mod.rs index 9d043bed..c7d9c137 100644 --- a/lib/lib/src/unmutated/list_packages/mod.rs +++ b/lib/lib/src/unmutated/list_packages/mod.rs @@ -44,15 +44,11 @@ impl<'a> TryFrom<&'a CListPackagesRequest> for ListPackagesData<'a> { } } -fn assemble() -> SequentialOrchestrator { - SequentialOrchestrator::new(vec![Box::new(FetchingStage)]) -} - pub fn run(data: ListPackagesData) -> Result<(Vec,), (ListPackagesStateId, ListPackagesError)> { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); - let orchestrator = assemble(); + let orchestrator = SequentialOrchestrator::new(vec![Box::new(FetchingStage)]); run_unmutated!( orchestrator, diff --git a/lib/lib/src/unmutated/list_prefix/mod.rs b/lib/lib/src/unmutated/list_prefix/mod.rs index 72d3d095..7873ac5d 100644 --- a/lib/lib/src/unmutated/list_prefix/mod.rs +++ b/lib/lib/src/unmutated/list_prefix/mod.rs @@ -44,15 +44,11 @@ impl<'a> TryFrom<&'a CListPrefixRequest> for ListPrefixData<'a> { } } -fn assemble() -> SequentialOrchestrator { - SequentialOrchestrator::new(vec![Box::new(FetchingStage)]) -} - pub fn run(data: ListPrefixData) -> Result<(Vec,), (ListPrefixStateId, ListPrefixError)> { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); - let orchestrator = assemble(); + let orchestrator = SequentialOrchestrator::new(vec![Box::new(FetchingStage)]); run_unmutated!( orchestrator, diff --git a/lib/lib/src/unmutated/search_files/mod.rs b/lib/lib/src/unmutated/search_files/mod.rs index 9e6d1c60..1e25bd14 100644 --- a/lib/lib/src/unmutated/search_files/mod.rs +++ b/lib/lib/src/unmutated/search_files/mod.rs @@ -48,15 +48,11 @@ impl<'a> TryFrom<&'a CSearchFilesRequest> for SearchFilesData<'a> { } } -fn assemble() -> SequentialOrchestrator { - SequentialOrchestrator::new(vec![Box::new(SearchingStage)]) -} - pub fn run(data: SearchFilesData) -> Result<(Vec,), (SearchFilesStateId, SearchFilesError)> { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); - let orchestrator = assemble(); + let orchestrator = SequentialOrchestrator::new(vec![Box::new(SearchingStage)]); run_unmutated!( orchestrator, diff --git a/lib/lib/src/unmutated/search_meta/mod.rs b/lib/lib/src/unmutated/search_meta/mod.rs index caa19824..d370bd75 100644 --- a/lib/lib/src/unmutated/search_meta/mod.rs +++ b/lib/lib/src/unmutated/search_meta/mod.rs @@ -48,16 +48,12 @@ impl<'a> TryFrom<&'a CSearchMetaRequest> for SearchMetaData<'a> { } } -fn assemble() -> SequentialOrchestrator { - SequentialOrchestrator::new(vec![Box::new(SearchingStage)]) -} - pub fn run(data: SearchMetaData) -> Result<(Vec,), (SearchMetaStateId, SearchMetaError)> { let mut context = Context::new(); context.put(Search(data.search.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); - let orchestrator = assemble(); + let orchestrator = SequentialOrchestrator::new(vec![Box::new(SearchingStage)]); run_unmutated!( orchestrator, From 880dc53484e94ddc1cd0b7c39508c3eac3aeaf63 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 09:38:38 +0400 Subject: [PATCH 49/68] fix: Moved the assemble function --- lib/lib/src/mutated/commit/mod.rs | 24 +++++++++--------- lib/lib/src/mutated/files/mod.rs | 28 ++++++++++----------- lib/lib/src/mutated/installer/mod.rs | 30 +++++++++++------------ lib/lib/src/mutated/rollback/mod.rs | 28 ++++++++++----------- lib/lib/src/mutated/uninstaller/mod.rs | 34 +++++++++++++------------- lib/lib/src/mutated/update/mod.rs | 30 +++++++++++------------ 6 files changed, 87 insertions(+), 87 deletions(-) diff --git a/lib/lib/src/mutated/commit/mod.rs b/lib/lib/src/mutated/commit/mod.rs index 5ce6271b..419d5430 100644 --- a/lib/lib/src/mutated/commit/mod.rs +++ b/lib/lib/src/mutated/commit/mod.rs @@ -56,18 +56,6 @@ impl<'a> TryFrom<&'a CCommitRequest> for CommitData<'a> { } } -fn assemble() -> SequentialOrchestrator { - SequentialOrchestrator::new(vec![ - Box::new(HookStage { - trigger: NativeTrigger::pre(Operation::Commit), - }), - Box::new(TransactionStage), - Box::new(HookStage { - trigger: NativeTrigger::post(Operation::Commit), - }), - ]) -} - pub fn run(data: CommitData) -> Result<(), (CommitStateId, CommitError)> { let mut context = Context::new(); context.put(TmpPath(data.tmp_path.to_owned())); @@ -81,3 +69,15 @@ pub fn run(data: CommitData) -> Result<(), (CommitStateId, CommitError)> { result } + +fn assemble() -> SequentialOrchestrator { + SequentialOrchestrator::new(vec![ + Box::new(HookStage { + trigger: NativeTrigger::pre(Operation::Commit), + }), + Box::new(TransactionStage), + Box::new(HookStage { + trigger: NativeTrigger::post(Operation::Commit), + }), + ]) +} diff --git a/lib/lib/src/mutated/files/mod.rs b/lib/lib/src/mutated/files/mod.rs index 5df534b1..b854b1b2 100644 --- a/lib/lib/src/mutated/files/mod.rs +++ b/lib/lib/src/mutated/files/mod.rs @@ -71,20 +71,6 @@ impl<'a> TryFrom<&'a CFilesRequest> for FilesData<'a> { } } -fn assemble() -> SequentialOrchestrator { - SequentialOrchestrator::new(vec![ - Box::new(HookStage { - trigger: NativeTrigger::pre(Operation::Files), - }), - Box::new(TransactionStage), - Box::new(CheckoutStage), - Box::new(SwapStage), - Box::new(HookStage { - trigger: NativeTrigger::post(Operation::Files), - }), - ]) -} - pub fn run(data: FilesData) -> Result<(), (FilesStateId, FilesError)> { let mut context = Context::new(); context.put(TmpPath(data.tmp_path.to_owned())); @@ -98,3 +84,17 @@ pub fn run(data: FilesData) -> Result<(), (FilesStateId, FilesError)> { result } + +fn assemble() -> SequentialOrchestrator { + SequentialOrchestrator::new(vec![ + Box::new(HookStage { + trigger: NativeTrigger::pre(Operation::Files), + }), + Box::new(TransactionStage), + Box::new(CheckoutStage), + Box::new(SwapStage), + Box::new(HookStage { + trigger: NativeTrigger::post(Operation::Files), + }), + ]) +} diff --git a/lib/lib/src/mutated/installer/mod.rs b/lib/lib/src/mutated/installer/mod.rs index c07f4c5f..9b9c43c2 100644 --- a/lib/lib/src/mutated/installer/mod.rs +++ b/lib/lib/src/mutated/installer/mod.rs @@ -68,6 +68,21 @@ impl<'a> TryFrom<&'a CInstallRequest> for InstallData<'a> { } } +pub fn run(data: InstallData) -> Result<(), (InstallStateId, InstallError)> { + let mut context = Context::new(); + context.put(data.packages); + context.put(TmpPath(data.tmp_path.to_owned())); + context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); + + let orchestrator = assemble(); + + let result = run_mutating!(orchestrator, context, data.cancel_token, InstallStateId, InstallError); + + data.cancel_token.reset(); + + result +} + fn assemble() -> SequentialOrchestrator { SequentialOrchestrator::new(vec![ Box::new(HookStage { @@ -83,18 +98,3 @@ fn assemble() -> SequentialOrchestrator { }), ]) } - -pub fn run(data: InstallData) -> Result<(), (InstallStateId, InstallError)> { - let mut context = Context::new(); - context.put(data.packages); - context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); - - let orchestrator = assemble(); - - let result = run_mutating!(orchestrator, context, data.cancel_token, InstallStateId, InstallError); - - data.cancel_token.reset(); - - result -} diff --git a/lib/lib/src/mutated/rollback/mod.rs b/lib/lib/src/mutated/rollback/mod.rs index 34244d63..e62b2a41 100644 --- a/lib/lib/src/mutated/rollback/mod.rs +++ b/lib/lib/src/mutated/rollback/mod.rs @@ -58,20 +58,6 @@ impl<'a> TryFrom<&'a CRollbackRequest> for RollbackData<'a> { } } -fn assemble() -> SequentialOrchestrator { - SequentialOrchestrator::new(vec![ - Box::new(HookStage { - trigger: NativeTrigger::pre(Operation::Rollback), - }), - Box::new(MergeStage), - Box::new(CheckoutStage), - Box::new(SwapStage), - Box::new(HookStage { - trigger: NativeTrigger::post(Operation::Rollback), - }), - ]) -} - pub fn run(data: RollbackData) -> Result<(), (RollbackStateId, RollbackError)> { let mut context = Context::new(); context.put(TmpPath(data.tmp_path.to_owned())); @@ -85,3 +71,17 @@ pub fn run(data: RollbackData) -> Result<(), (RollbackStateId, RollbackError)> { result } + +fn assemble() -> SequentialOrchestrator { + SequentialOrchestrator::new(vec![ + Box::new(HookStage { + trigger: NativeTrigger::pre(Operation::Rollback), + }), + Box::new(MergeStage), + Box::new(CheckoutStage), + Box::new(SwapStage), + Box::new(HookStage { + trigger: NativeTrigger::post(Operation::Rollback), + }), + ]) +} diff --git a/lib/lib/src/mutated/uninstaller/mod.rs b/lib/lib/src/mutated/uninstaller/mod.rs index 9cd81445..be4f5f90 100644 --- a/lib/lib/src/mutated/uninstaller/mod.rs +++ b/lib/lib/src/mutated/uninstaller/mod.rs @@ -92,23 +92,6 @@ impl<'a> TryFrom<&'a CUninstallRequest> for UninstallData<'a> { } } -fn assemble() -> SequentialOrchestrator { - SequentialOrchestrator::new(vec![ - Box::new(HookStage { - trigger: NativeTrigger::pre(Operation::Uninstall), - }), - Box::new(PreparationStage), - Box::new(BuildStage), - Box::new(CommitStage), - Box::new(ConfigMergeStage), - Box::new(PrepareBootStage), - Box::new(BootOptionStage), - Box::new(HookStage { - trigger: NativeTrigger::post(Operation::Uninstall), - }), - ]) -} - pub fn run(data: UninstallData) -> Result<(), (UninstallStateId, UninstallError)> { let deploy = Deploy::new(DeployMode::ReadWrite).map_err(|error| (UninstallStateId::Setup, UninstallError::from(error)))?; @@ -144,3 +127,20 @@ pub fn run(data: UninstallData) -> Result<(), (UninstallStateId, UninstallError) result } + +fn assemble() -> SequentialOrchestrator { + SequentialOrchestrator::new(vec![ + Box::new(HookStage { + trigger: NativeTrigger::pre(Operation::Uninstall), + }), + Box::new(PreparationStage), + Box::new(BuildStage), + Box::new(CommitStage), + Box::new(ConfigMergeStage), + Box::new(PrepareBootStage), + Box::new(BootOptionStage), + Box::new(HookStage { + trigger: NativeTrigger::post(Operation::Uninstall), + }), + ]) +} diff --git a/lib/lib/src/mutated/update/mod.rs b/lib/lib/src/mutated/update/mod.rs index 7ad956e2..2f258acc 100644 --- a/lib/lib/src/mutated/update/mod.rs +++ b/lib/lib/src/mutated/update/mod.rs @@ -68,6 +68,21 @@ impl<'a> TryFrom<&'a CUpdateRequest> for UpdateData<'a> { } } +pub fn run(data: UpdateData) -> Result<(), (UpdateStateId, UpdateError)> { + let mut context = Context::new(); + context.put(data.packages); + context.put(TmpPath(data.tmp_path.to_owned())); + context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); + + let orchestrator = assemble(); + + let result = run_mutating!(orchestrator, context, data.cancel_token, UpdateStateId, UpdateError); + + data.cancel_token.reset(); + + result +} + fn assemble() -> SequentialOrchestrator { SequentialOrchestrator::new(vec![ Box::new(HookStage { @@ -83,18 +98,3 @@ fn assemble() -> SequentialOrchestrator { }), ]) } - -pub fn run(data: UpdateData) -> Result<(), (UpdateStateId, UpdateError)> { - let mut context = Context::new(); - context.put(data.packages); - context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); - - let orchestrator = assemble(); - - let result = run_mutating!(orchestrator, context, data.cancel_token, UpdateStateId, UpdateError); - - data.cancel_token.reset(); - - result -} From e3f4a2e0b70250e2a0f5566b03af572513e9e97d Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 09:47:09 +0400 Subject: [PATCH 50/68] fix: Renamed `etc history` to `config history` new: Added initial implementation of the `list_history` command stage new: Added required type Co-Authored-By: Claude Sonnet 5 --- lib/lib/src/database/record.rs | 2 +- lib/lib/src/types/mod.rs | 2 + lib/lib/src/unmutated/list_history/error.rs | 8 +++- .../src/unmutated/list_history/fetching.rs | 43 +++++++++++++++++-- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/lib/lib/src/database/record.rs b/lib/lib/src/database/record.rs index b75b8133..46bb3ccf 100644 --- a/lib/lib/src/database/record.rs +++ b/lib/lib/src/database/record.rs @@ -26,7 +26,7 @@ pub struct DeployRecord { pub message: Option, pub seq: u64, pub timestamp: u64, - pub etc_history: Vec, + pub config_history: Vec, pub working_etc: String, } diff --git a/lib/lib/src/types/mod.rs b/lib/lib/src/types/mod.rs index 1fa06d19..745c6515 100644 --- a/lib/lib/src/types/mod.rs +++ b/lib/lib/src/types/mod.rs @@ -198,6 +198,8 @@ pub struct Search(pub String); as_str_method!(Search); +pub struct RequestedPrefixDigest(pub Option); + #[cfg(test)] mod tests { use super::*; diff --git a/lib/lib/src/unmutated/list_history/error.rs b/lib/lib/src/unmutated/list_history/error.rs index d3827912..276cff2d 100644 --- a/lib/lib/src/unmutated/list_history/error.rs +++ b/lib/lib/src/unmutated/list_history/error.rs @@ -5,9 +5,11 @@ use upac_abi::error::ErrorKind; -use crate::database::error::DatabaseError; +use crate::database::error::{DatabaseError, DeployRecordError}; use crate::deploy::error::SysrootError; -use crate::errors::{CommonError, common_error_from, database_error_from, lock_error_from, sysroot_error_from}; +use crate::errors::{ + CommonError, common_error_from, database_error_from, deploy_record_error_from, lock_error_from, sysroot_error_from, +}; use crate::lock::LockError; #[derive(Debug, Clone, PartialEq, Eq)] @@ -19,6 +21,8 @@ common_error_from!(ListHistoryError); database_error_from!(ListHistoryError); +deploy_record_error_from!(ListHistoryError); + sysroot_error_from!(ListHistoryError); lock_error_from!(ListHistoryError); diff --git a/lib/lib/src/unmutated/list_history/fetching.rs b/lib/lib/src/unmutated/list_history/fetching.rs index bf1d37b8..58f9c069 100644 --- a/lib/lib/src/unmutated/list_history/fetching.rs +++ b/lib/lib/src/unmutated/list_history/fetching.rs @@ -5,16 +5,53 @@ use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use crate::database::error::DeployRecordError; +use crate::database::record::DeployRecord; +use crate::deploy::{Deploy, DeployMode}; use crate::orchestrator::Context; -use crate::orchestrator::stage::{RollbackGuard, Stage}; +use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage}; +use crate::types::{CommitEntry, HistoryEntry}; use crate::unmutated::list_history::ListHistoryError; pub struct FetchingStage; impl Stage for FetchingStage { fn run( - &self, _context: &mut Context, _cancel: &CancelToken, _progress: ProgressEventBuilder, + &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, Box), ListHistoryError> { - todo!() + let deploy = Deploy::new(DeployMode::ReadOnly)?; + + let mut entries = Vec::new(); + + for prefix_digest in deploy.deploys()? { + let record = match DeployRecord::read(&deploy.deploy(&prefix_digest)) { + Ok(record) => record, + Err(DeployRecordError::NotFound) => continue, + Err(error) => return Err(error.into()), + }; + + let config_history = record + .config_history + .into_iter() + .map(|entry| CommitEntry { + config_digest: entry.etc_digest, + subject: entry.subject, + message: entry.message, + }) + .collect(); + + entries.push(HistoryEntry { + prefix_digest: record.prefix_digest, + subject: record.subject, + message: record.message, + timestamp: record.timestamp, + working_config: Some(record.working_etc), + config_history, + }); + } + + context.put(entries); + + Ok((progress, Box::new(NoRollback))) } } From b87a3d99d188462128668bfcdad51fcf13928afd Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 19:23:14 +0400 Subject: [PATCH 51/68] fix: Renamed 'etc' to 'config' new: Implemented command new: Added new types Co-Authored-By: Claude Sonnet 5 --- lib/lib/src/types/mod.rs | 10 ++++++ lib/lib/src/unmutated/list_commit/error.rs | 8 +++-- lib/lib/src/unmutated/list_commit/fetching.rs | 35 +++++++++++++++++-- lib/lib/src/unmutated/list_commit/mod.rs | 3 +- lib/lib/tests/database_record.rs | 2 +- 5 files changed, 51 insertions(+), 7 deletions(-) diff --git a/lib/lib/src/types/mod.rs b/lib/lib/src/types/mod.rs index 745c6515..2796e822 100644 --- a/lib/lib/src/types/mod.rs +++ b/lib/lib/src/types/mod.rs @@ -200,6 +200,16 @@ as_str_method!(Search); pub struct RequestedPrefixDigest(pub Option); +pub struct RequestedPrefixDigestRange { + pub from: Option, + pub to: Option, +} + +pub struct DiffPackagesSnapshot { + pub from: Vec, + pub to: Vec, +} + #[cfg(test)] mod tests { use super::*; diff --git a/lib/lib/src/unmutated/list_commit/error.rs b/lib/lib/src/unmutated/list_commit/error.rs index 5b8687f6..667de2f6 100644 --- a/lib/lib/src/unmutated/list_commit/error.rs +++ b/lib/lib/src/unmutated/list_commit/error.rs @@ -5,9 +5,11 @@ use upac_abi::error::ErrorKind; -use crate::database::error::DatabaseError; +use crate::database::error::{DatabaseError, DeployRecordError}; use crate::deploy::error::SysrootError; -use crate::errors::{CommonError, common_error_from, database_error_from, lock_error_from, sysroot_error_from}; +use crate::errors::{ + CommonError, common_error_from, database_error_from, deploy_record_error_from, lock_error_from, sysroot_error_from, +}; use crate::lock::LockError; #[derive(Debug, Clone, PartialEq, Eq)] @@ -19,6 +21,8 @@ common_error_from!(ListCommitError); database_error_from!(ListCommitError); +deploy_record_error_from!(ListCommitError); + sysroot_error_from!(ListCommitError); lock_error_from!(ListCommitError); diff --git a/lib/lib/src/unmutated/list_commit/fetching.rs b/lib/lib/src/unmutated/list_commit/fetching.rs index 167a4e3d..a562e949 100644 --- a/lib/lib/src/unmutated/list_commit/fetching.rs +++ b/lib/lib/src/unmutated/list_commit/fetching.rs @@ -5,16 +5,45 @@ use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use crate::database::record::DeployRecord; +use crate::deploy::digest::current_prefix_digest; +use crate::deploy::{Deploy, DeployMode}; +use crate::errors::CommonError; use crate::orchestrator::Context; -use crate::orchestrator::stage::{RollbackGuard, Stage}; +use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage}; +use crate::types::{CommitEntry, RequestedPrefixDigest}; use crate::unmutated::list_commit::ListCommitError; pub struct FetchingStage; impl Stage for FetchingStage { fn run( - &self, _context: &mut Context, _cancel: &CancelToken, _progress: ProgressEventBuilder, + &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, Box), ListCommitError> { - todo!() + let requested = context + .get::() + .ok_or(CommonError::MissingResult)?; + + let prefix_digest = match &requested.0 { + Some(prefix_digest) => prefix_digest.clone(), + None => current_prefix_digest()?, + }; + + let deploy = Deploy::new(DeployMode::ReadOnly)?; + let record = DeployRecord::read(&deploy.deploy(&prefix_digest))?; + + let entries: Vec = record + .config_history + .into_iter() + .map(|entry| CommitEntry { + config_digest: entry.etc_digest, + subject: entry.subject, + message: entry.message, + }) + .collect(); + + context.put(entries); + + Ok((progress, Box::new(NoRollback))) } } diff --git a/lib/lib/src/unmutated/list_commit/mod.rs b/lib/lib/src/unmutated/list_commit/mod.rs index 2fa97931..66e37af4 100644 --- a/lib/lib/src/unmutated/list_commit/mod.rs +++ b/lib/lib/src/unmutated/list_commit/mod.rs @@ -14,8 +14,8 @@ pub use self::error::ListCommitError; use self::fetching::FetchingStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use crate::types::CommitEntry; use crate::types::states::ListCommitStateId; +use crate::types::{CommitEntry, RequestedPrefixDigest}; mod error; mod fetching; @@ -50,6 +50,7 @@ impl<'a> TryFrom<&'a CListCommitRequest> for ListCommitData<'a> { pub fn run(data: ListCommitData) -> Result<(Vec,), (ListCommitStateId, ListCommitError)> { let mut context = Context::new(); + context.put(RequestedPrefixDigest(data.prefix_digest.map(str::to_owned))); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = SequentialOrchestrator::new(vec![Box::new(FetchingStage)]); diff --git a/lib/lib/tests/database_record.rs b/lib/lib/tests/database_record.rs index d946bbb5..c534916f 100644 --- a/lib/lib/tests/database_record.rs +++ b/lib/lib/tests/database_record.rs @@ -23,7 +23,7 @@ fn sample_record() -> DeployRecord { message: Some("long-form commit message".to_string()), seq: 7, timestamp: 1_754_000_000, - etc_history: vec![ + config_history: vec![ EtcHistoryEntry { etc_digest: "etc-digest-1".to_string(), subject: "first etc".to_string(), From d364edfd7bf8780c8f15ddc9da3e799f08f41041 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 19:26:34 +0400 Subject: [PATCH 52/68] New: A new command has been implemented Co-Authored-By: Claude Sonnet 5 --- .../src/unmutated/diff_packages/comparing.rs | 55 ++++++++++++++++++- lib/lib/src/unmutated/diff_packages/error.rs | 7 ++- lib/lib/src/unmutated/diff_packages/mod.rs | 6 +- .../src/unmutated/diff_packages/preparing.rs | 40 +++++++++++++- 4 files changed, 100 insertions(+), 8 deletions(-) diff --git a/lib/lib/src/unmutated/diff_packages/comparing.rs b/lib/lib/src/unmutated/diff_packages/comparing.rs index c2e30781..86bdb8f9 100644 --- a/lib/lib/src/unmutated/diff_packages/comparing.rs +++ b/lib/lib/src/unmutated/diff_packages/comparing.rs @@ -3,18 +3,67 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later +use std::collections::HashMap; + +use upac_abi::DiffKind; use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use crate::errors::CommonError; use crate::orchestrator::Context; -use crate::orchestrator::stage::{RollbackGuard, Stage}; +use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage}; +use crate::types::{DiffPackageEntry, DiffPackagesSnapshot}; use crate::unmutated::diff_packages::DiffPackagesError; pub struct ComparingStage; impl Stage for ComparingStage { fn run( - &self, _context: &mut Context, _cancel: &CancelToken, _progress: ProgressEventBuilder, + &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, Box), DiffPackagesError> { - todo!() + let snapshot = context.take::().ok_or(CommonError::MissingResult)?; + + let from: HashMap<_, _> = snapshot + .from + .into_iter() + .map(|meta| ((meta.name.clone(), meta.arch.clone(), meta.arch_sub.clone()), meta)) + .collect(); + let mut to: HashMap<_, _> = snapshot + .to + .into_iter() + .map(|meta| ((meta.name.clone(), meta.arch.clone(), meta.arch_sub.clone()), meta)) + .collect(); + + let mut entries = Vec::new(); + + for (identity, from_meta) in from { + match to.remove(&identity) { + Some(to_meta) if to_meta.sha256 != from_meta.sha256 => entries.push(DiffPackageEntry { + name: to_meta.name, + kind: DiffKind::Modified, + version: to_meta.version, + files: Vec::new(), + }), + Some(_) => {} + None => entries.push(DiffPackageEntry { + name: from_meta.name, + kind: DiffKind::Removed, + version: from_meta.version, + files: Vec::new(), + }), + } + } + + for (_identity, to_meta) in to { + entries.push(DiffPackageEntry { + name: to_meta.name, + kind: DiffKind::Added, + version: to_meta.version, + files: Vec::new(), + }); + } + + context.put(entries); + + Ok((progress, Box::new(NoRollback))) } } diff --git a/lib/lib/src/unmutated/diff_packages/error.rs b/lib/lib/src/unmutated/diff_packages/error.rs index 8fe6b9a5..2b8782a6 100644 --- a/lib/lib/src/unmutated/diff_packages/error.rs +++ b/lib/lib/src/unmutated/diff_packages/error.rs @@ -5,9 +5,12 @@ use upac_abi::error::ErrorKind; +use crate::composefs::error::RepoError; use crate::database::error::DatabaseError; use crate::deploy::error::SysrootError; -use crate::errors::{CommonError, common_error_from, database_error_from, lock_error_from, sysroot_error_from}; +use crate::errors::{ + CommonError, common_error_from, database_error_from, lock_error_from, repo_error_from, sysroot_error_from, +}; use crate::lock::LockError; #[derive(Debug, Clone, PartialEq, Eq)] @@ -19,6 +22,8 @@ common_error_from!(DiffPackagesError); database_error_from!(DiffPackagesError); +repo_error_from!(DiffPackagesError); + sysroot_error_from!(DiffPackagesError); lock_error_from!(DiffPackagesError); diff --git a/lib/lib/src/unmutated/diff_packages/mod.rs b/lib/lib/src/unmutated/diff_packages/mod.rs index 61dfb231..6471d263 100644 --- a/lib/lib/src/unmutated/diff_packages/mod.rs +++ b/lib/lib/src/unmutated/diff_packages/mod.rs @@ -15,8 +15,8 @@ use self::comparing::ComparingStage; use self::preparing::PreparingStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use crate::types::DiffPackageEntry; use crate::types::states::DiffPackagesStateId; +use crate::types::{DiffPackageEntry, RequestedPrefixDigestRange}; mod comparing; mod error; @@ -54,6 +54,10 @@ impl<'a> TryFrom<&'a CDiffPackagesRequest> for DiffPackagesData<'a> { pub fn run(data: DiffPackagesData) -> Result<(Vec,), (DiffPackagesStateId, DiffPackagesError)> { let mut context = Context::new(); + context.put(RequestedPrefixDigestRange { + from: data.from_prefix_digest.map(str::to_owned), + to: data.to_prefix_digest.map(str::to_owned), + }); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]); diff --git a/lib/lib/src/unmutated/diff_packages/preparing.rs b/lib/lib/src/unmutated/diff_packages/preparing.rs index 5d108d54..b9b5eb66 100644 --- a/lib/lib/src/unmutated/diff_packages/preparing.rs +++ b/lib/lib/src/unmutated/diff_packages/preparing.rs @@ -5,16 +5,50 @@ use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use crate::composefs::file::FileHandle; +use crate::database::meta::MetaStore; +use crate::database::{InMemory, MemoryDatabase}; +use crate::deploy::digest::current_prefix_digest; +use crate::deploy::{Deploy, DeployMode}; +use crate::errors::CommonError; use crate::orchestrator::Context; -use crate::orchestrator::stage::{RollbackGuard, Stage}; +use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage}; +use crate::types::database::DATABASE_PATH; +use crate::types::{DiffPackagesSnapshot, RequestedPrefixDigestRange}; use crate::unmutated::diff_packages::DiffPackagesError; pub struct PreparingStage; impl Stage for PreparingStage { fn run( - &self, _context: &mut Context, _cancel: &CancelToken, _progress: ProgressEventBuilder, + &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, Box), DiffPackagesError> { - todo!() + let requested = context + .get::() + .ok_or(CommonError::MissingResult)?; + + let from_prefix_digest = match &requested.from { + Some(prefix_digest) => prefix_digest.clone(), + None => current_prefix_digest()?, + }; + let to_prefix_digest = match &requested.to { + Some(prefix_digest) => prefix_digest.clone(), + None => current_prefix_digest()?, + }; + + let deploy = Deploy::new(DeployMode::ReadOnly)?; + let repository = deploy.open_repository()?; + + let from_tree = deploy.open_tree(&from_prefix_digest)?; + let from_bytes = FileHandle::new(DATABASE_PATH).read_file(&repository, &from_tree)?; + let from = MemoryDatabase::open_in_memory(from_bytes)?.list_packages_metas()?; + + let to_tree = deploy.open_tree(&to_prefix_digest)?; + let to_bytes = FileHandle::new(DATABASE_PATH).read_file(&repository, &to_tree)?; + let to = MemoryDatabase::open_in_memory(to_bytes)?.list_packages_metas()?; + + context.put(DiffPackagesSnapshot { from, to }); + + Ok((progress, Box::new(NoRollback))) } } From f9d80460a6f6270f3899cf588fb6b095b9c37e27 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 19:58:58 +0400 Subject: [PATCH 53/68] New: automatic cleanup added for the new type Co-Authored-By: Claude Sonnet 5 --- lib/macro/src/common.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/macro/src/common.rs b/lib/macro/src/common.rs index 59c166aa..7cbaf516 100644 --- a/lib/macro/src/common.rs +++ b/lib/macro/src/common.rs @@ -21,6 +21,7 @@ pub(crate) const VALIDATABLE_COMPOSITES: &[&str] = &[ "CPackageInfo", "CDiffPrefixFileEntry", "CDiffConfigFileEntry", + "CDiffUntrackedFileEntry", "CCommitEntry", "CRequestBase", "CDependency", From c2039a2ba4172c13ead43e70571bdaac810051a4 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 19:59:49 +0400 Subject: [PATCH 54/68] new: added a new structure for diff commands Co-Authored-By: Claude Sonnet 5 --- lib/abi/src/response.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/abi/src/response.rs b/lib/abi/src/response.rs index 93108078..ab0a1168 100644 --- a/lib/abi/src/response.rs +++ b/lib/abi/src/response.rs @@ -43,6 +43,15 @@ pub struct CDiffConfigFileEntry { pub package_name: CSlice, } +#[repr(C)] +#[derive(CFree, CValidate)] +pub struct CDiffUntrackedFileEntry { + pub struct_size: usize, + + pub path: CSlice, + pub kind: DiffKind, +} + #[repr(C)] #[derive(CFree, CValidate)] pub struct CCommitEntry { @@ -231,8 +240,8 @@ impl CDiffPackagesResponse { #[repr(C)] pub struct CDiffResponse { pub struct_size: usize, - pub unattached_files: CVec, pub diff_packages: CVec, + pub unattached_files: CVec, } impl CDiffResponse { From 1bc8155351e802db23dac1f5b6b92be64ba028a1 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 20:01:37 +0400 Subject: [PATCH 55/68] new: Implemented two new commands new: Added types for two new commands Co-Authored-By: Claude Sonnet 5 --- lib/lib/src/composefs/diff.rs | 157 ++++++++++++++++++ lib/lib/src/composefs/mod.rs | 1 + lib/lib/src/export/unmutated/diff.rs | 11 +- lib/lib/src/types/mod.rs | 15 +- lib/lib/src/unmutated/diff/mod.rs | 6 +- .../unmutated/diff_files_prefix/comparing.rs | 58 ++++++- .../src/unmutated/diff_files_prefix/error.rs | 7 +- .../src/unmutated/diff_files_prefix/mod.rs | 14 +- .../unmutated/diff_files_prefix/preparing.rs | 50 +++++- .../src/unmutated/diff_packages/comparing.rs | 4 +- 10 files changed, 303 insertions(+), 20 deletions(-) create mode 100644 lib/lib/src/composefs/diff.rs diff --git a/lib/lib/src/composefs/diff.rs b/lib/lib/src/composefs/diff.rs new file mode 100644 index 00000000..5c7749b4 --- /dev/null +++ b/lib/lib/src/composefs/diff.rs @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 JustPav +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +use std::cmp::Ordering; +use std::path::{Path, PathBuf}; + +use composefs::tree::{Directory, FileSystem, Inode, Leaf, LeafContent, RegularFile}; +use upac_abi::DiffKind; + +use crate::composefs::repository::ObjectID; + +macro_rules! advance_subtree { + ($differ:expr, $prefix:expr, $entries:expr, $next:expr, $name:expr, $inode:expr, $side:expr) => {{ + $differ.mark_subtree(&$prefix.join($name), $inode, $side); + $next = $entries.next(); + }}; +} + +#[derive(Clone, Copy)] +enum Side { + From, + To, +} + +impl Side { + fn kind(self) -> DiffKind { + match self { + Side::From => DiffKind::Removed, + Side::To => DiffKind::Added, + } + } +} + +pub struct TreeDiff<'a> { + from_leaves: &'a [Leaf], + to_leaves: &'a [Leaf], + changes: Vec<(String, DiffKind)>, +} + +impl<'a> TreeDiff<'a> { + pub fn run(from: &'a FileSystem, to: &'a FileSystem) -> Vec<(String, DiffKind)> { + let mut differ = Self { + from_leaves: &from.leaves, + to_leaves: &to.leaves, + changes: Vec::new(), + }; + + differ.compare_directories(&PathBuf::new(), &from.root, &to.root); + + differ.changes + } + + fn compare_directories(&mut self, prefix: &Path, from_dir: &Directory, to_dir: &Directory) { + let mut from_entries = from_dir.sorted_entries(); + let mut to_entries = to_dir.sorted_entries(); + + let mut from_next = from_entries.next(); + let mut to_next = to_entries.next(); + + loop { + match (from_next, to_next) { + (Some((from_name, from_inode)), Some((to_name, to_inode))) => match from_name.cmp(to_name) { + Ordering::Less => { + advance_subtree!(self, prefix, from_entries, from_next, from_name, from_inode, Side::From) + } + Ordering::Greater => { + advance_subtree!(self, prefix, to_entries, to_next, to_name, to_inode, Side::To) + } + Ordering::Equal => { + self.compare_matched_entry(&prefix.join(from_name), from_inode, to_inode); + from_next = from_entries.next(); + to_next = to_entries.next(); + } + }, + (Some((from_name, from_inode)), None) => { + advance_subtree!(self, prefix, from_entries, from_next, from_name, from_inode, Side::From) + } + (None, Some((to_name, to_inode))) => { + advance_subtree!(self, prefix, to_entries, to_next, to_name, to_inode, Side::To) + } + (None, None) => break, + } + } + } + + fn compare_matched_entry(&mut self, path: &Path, from_inode: &Inode, to_inode: &Inode) { + match (from_inode, to_inode) { + (Inode::Directory(from_sub), Inode::Directory(to_sub)) => { + self.compare_directories(path, from_sub, to_sub); + } + (Inode::Leaf(from_id, _), Inode::Leaf(to_id, _)) => { + let from_leaf = &self.from_leaves[from_id.0]; + let to_leaf = &self.to_leaves[to_id.0]; + + if Self::is_regular_or_symlink(from_leaf) + && Self::is_regular_or_symlink(to_leaf) + && !Self::content_matches(from_leaf, to_leaf) + { + self.changes.push((Self::path_to_string(path), DiffKind::Modified)); + } + } + (from_inode, to_inode) => { + self.mark_subtree(path, from_inode, Side::From); + self.mark_subtree(path, to_inode, Side::To); + } + } + } + + fn mark_subtree(&mut self, path: &Path, inode: &Inode, side: Side) { + match inode { + Inode::Leaf(id, _) => { + let leaf = &self.leaves(side)[id.0]; + + if Self::is_regular_or_symlink(leaf) { + self.changes.push((Self::path_to_string(path), side.kind())); + } + } + Inode::Directory(dir) => { + for (name, child) in dir.sorted_entries() { + self.mark_subtree(&path.join(name), child, side); + } + } + } + } + + fn leaves(&self, side: Side) -> &'a [Leaf] { + match side { + Side::From => self.from_leaves, + Side::To => self.to_leaves, + } + } + + fn is_regular_or_symlink(leaf: &Leaf) -> bool { + matches!(leaf.content, LeafContent::Regular(_) | LeafContent::Symlink(_)) + } + + fn content_matches(from: &Leaf, to: &Leaf) -> bool { + match (&from.content, &to.content) { + ( + LeafContent::Regular(RegularFile::External(from_id, from_size)), + LeafContent::Regular(RegularFile::External(to_id, to_size)), + ) => from_id == to_id && from_size == to_size, + ( + LeafContent::Regular(RegularFile::Inline(from_bytes)), + LeafContent::Regular(RegularFile::Inline(to_bytes)), + ) => from_bytes == to_bytes, + (LeafContent::Symlink(from_target), LeafContent::Symlink(to_target)) => from_target == to_target, + _ => false, + } + } + + fn path_to_string(path: &Path) -> String { + path.to_string_lossy().into_owned() + } +} diff --git a/lib/lib/src/composefs/mod.rs b/lib/lib/src/composefs/mod.rs index 9ac8f2f2..dfcfd088 100644 --- a/lib/lib/src/composefs/mod.rs +++ b/lib/lib/src/composefs/mod.rs @@ -3,6 +3,7 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later +pub mod diff; pub mod error; pub mod file; pub mod repository; diff --git a/lib/lib/src/export/unmutated/diff.rs b/lib/lib/src/export/unmutated/diff.rs index fc53b058..16053476 100644 --- a/lib/lib/src/export/unmutated/diff.rs +++ b/lib/lib/src/export/unmutated/diff.rs @@ -8,7 +8,7 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CDiffRequest; -use upac_abi::response::{CDiffPackageEntry, CDiffPrefixFileEntry, CDiffResponse}; +use upac_abi::response::{CDiffPackageEntry, CDiffResponse, CDiffUntrackedFileEntry}; use upac_abi::types::{COwned, CVec}; use crate::export::{try_convert_abi, write_error}; @@ -27,12 +27,15 @@ pub unsafe extern "C" fn diff(request_c: CDiffRequest, response_out: *mut CDiffR unsafe { *response_out = CDiffResponse { struct_size: size_of::(), - unattached_files: CVec::from_owned( - unattached_files.into_iter().map(CDiffPrefixFileEntry::from).collect(), - ), diff_packages: CVec::from_owned( diff_packages.into_iter().map(CDiffPackageEntry::from).collect(), ), + unattached_files: CVec::from_owned( + unattached_files + .into_iter() + .map(CDiffUntrackedFileEntry::from) + .collect(), + ), }; } } diff --git a/lib/lib/src/types/mod.rs b/lib/lib/src/types/mod.rs index 2796e822..1399031d 100644 --- a/lib/lib/src/types/mod.rs +++ b/lib/lib/src/types/mod.rs @@ -10,8 +10,8 @@ use upac_abi::decoder::CDependency; use upac_abi::error::ErrorKind; use upac_abi::package::{CPackageMeta, CUnpackedPackage, CVersion}; use upac_abi::response::{ - CCommitEntry, CDiffConfigFileEntry, CDiffPackageEntry, CDiffPrefixFileEntry, CHistoryEntry, CPrefixEntry, - CSearchFileEntry, + CCommitEntry, CDiffConfigFileEntry, CDiffPackageEntry, CDiffPrefixFileEntry, CDiffUntrackedFileEntry, + CHistoryEntry, CPrefixEntry, CSearchFileEntry, }; use upac_abi::types::{CBorrowed, COwned, CSlice, CVec}; use upac_macro::{CTryToRust, RedbCodec, RustToC}; @@ -182,6 +182,17 @@ pub struct DiffPackageEntry { pub files: Vec, } +// ── DiffUntrackedFileEntry ────────────────────────────────────────────────── +// A changed /usr file that belongs to no package at all — not package-owned, +// not attached as a user file. By design this shouldn't normally happen +// (every /usr file is meant to come with a package), but if it does, it's +// surfaced here rather than silently dropped. No package_name: there is none. +#[derive(Debug, Clone, RustToC)] +pub struct DiffUntrackedFileEntry { + pub path: String, + pub kind: DiffKind, +} + pub struct Targets(pub Vec); impl Targets { diff --git a/lib/lib/src/unmutated/diff/mod.rs b/lib/lib/src/unmutated/diff/mod.rs index a4880640..6799842e 100644 --- a/lib/lib/src/unmutated/diff/mod.rs +++ b/lib/lib/src/unmutated/diff/mod.rs @@ -16,7 +16,7 @@ use self::preparing::PreparingStage; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; use crate::types::states::DiffStateId; -use crate::types::{DiffPackageEntry, DiffPrefixFileEntry}; +use crate::types::{DiffPackageEntry, DiffUntrackedFileEntry}; mod comparing; mod error; @@ -52,7 +52,7 @@ impl<'a> TryFrom<&'a CDiffRequest> for DiffData<'a> { } } -pub fn run(data: DiffData) -> Result<(Vec, Vec), (DiffStateId, DiffError)> { +pub fn run(data: DiffData) -> Result<(Vec, Vec), (DiffStateId, DiffError)> { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); @@ -65,6 +65,6 @@ pub fn run(data: DiffData) -> Result<(Vec, Vec, - Vec + Vec ) } diff --git a/lib/lib/src/unmutated/diff_files_prefix/comparing.rs b/lib/lib/src/unmutated/diff_files_prefix/comparing.rs index 00792211..e300ca08 100644 --- a/lib/lib/src/unmutated/diff_files_prefix/comparing.rs +++ b/lib/lib/src/unmutated/diff_files_prefix/comparing.rs @@ -3,18 +3,68 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later +use upac_abi::DiffKind; use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use crate::database::MemoryDatabase; +use crate::database::files::FileStore; +use crate::database::meta::MetaStore; +use crate::errors::CommonError; use crate::orchestrator::Context; -use crate::orchestrator::stage::{RollbackGuard, Stage}; -use crate::unmutated::diff_files_prefix::DiffFilesPrefixError; +use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage}; +use crate::types::DiffPrefixFileEntry; +use crate::unmutated::diff_files_prefix::{DiffFilesPrefixError, DiffFilesPrefixSnapshot}; pub struct ComparingStage; impl Stage for ComparingStage { fn run( - &self, _context: &mut Context, _cancel: &CancelToken, _progress: ProgressEventBuilder, + &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, Box), DiffFilesPrefixError> { - todo!() + let snapshot = context + .take::() + .ok_or(CommonError::MissingResult)?; + + let mut entries = Vec::new(); + + for (path, kind) in snapshot.changed { + let database = match kind { + DiffKind::Removed => &snapshot.from_database, + DiffKind::Added | DiffKind::Modified => &snapshot.to_database, + }; + + if let Some(entry) = Self::attribute(database, &path, kind)? { + entries.push(entry); + } + } + + context.put(entries); + + Ok((progress, Box::new(NoRollback))) + } +} + +impl ComparingStage { + fn attribute( + database: &MemoryDatabase, path: &str, kind: DiffKind, + ) -> Result, DiffFilesPrefixError> { + let Some(uuid) = database.find_file_owner(path)? else { + return Ok(None); + }; + let Some(meta) = database.get_package_meta(uuid)? else { + return Ok(None); + }; + let is_user = database + .list_files(uuid)? + .into_iter() + .find(|entry| entry.path == path) + .is_some_and(|entry| entry.is_user); + + Ok(Some(DiffPrefixFileEntry { + path: path.to_owned(), + kind, + package_name: meta.name, + is_user, + })) } } diff --git a/lib/lib/src/unmutated/diff_files_prefix/error.rs b/lib/lib/src/unmutated/diff_files_prefix/error.rs index 9f645d10..2fb834ae 100644 --- a/lib/lib/src/unmutated/diff_files_prefix/error.rs +++ b/lib/lib/src/unmutated/diff_files_prefix/error.rs @@ -5,9 +5,12 @@ use upac_abi::error::ErrorKind; +use crate::composefs::error::RepoError; use crate::database::error::DatabaseError; use crate::deploy::error::SysrootError; -use crate::errors::{CommonError, common_error_from, database_error_from, lock_error_from, sysroot_error_from}; +use crate::errors::{ + CommonError, common_error_from, database_error_from, lock_error_from, repo_error_from, sysroot_error_from, +}; use crate::lock::LockError; #[derive(Debug, Clone, PartialEq, Eq)] @@ -19,6 +22,8 @@ common_error_from!(DiffFilesPrefixError); database_error_from!(DiffFilesPrefixError); +repo_error_from!(DiffFilesPrefixError); + sysroot_error_from!(DiffFilesPrefixError); lock_error_from!(DiffFilesPrefixError); diff --git a/lib/lib/src/unmutated/diff_files_prefix/mod.rs b/lib/lib/src/unmutated/diff_files_prefix/mod.rs index 38c61ef0..76819f9d 100644 --- a/lib/lib/src/unmutated/diff_files_prefix/mod.rs +++ b/lib/lib/src/unmutated/diff_files_prefix/mod.rs @@ -5,6 +5,7 @@ use std::os::raw::c_void; +use upac_abi::DiffKind; use upac_abi::error::ErrorKind; use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; use upac_abi::request::CDiffFilesPrefixRequest; @@ -14,14 +15,21 @@ pub use self::error::DiffFilesPrefixError; use self::comparing::ComparingStage; use self::preparing::PreparingStage; +use crate::database::MemoryDatabase; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use crate::types::DiffPrefixFileEntry; use crate::types::states::DiffFilesPrefixStateId; +use crate::types::{DiffPrefixFileEntry, RequestedPrefixDigestRange}; mod comparing; mod error; mod preparing; +struct DiffFilesPrefixSnapshot { + changed: Vec<(String, DiffKind)>, + from_database: MemoryDatabase, + to_database: MemoryDatabase, +} + pub struct DiffFilesPrefixData<'a> { pub from_prefix_digest: Option<&'a str>, pub to_prefix_digest: Option<&'a str>, @@ -56,6 +64,10 @@ pub fn run( data: DiffFilesPrefixData, ) -> Result<(Vec,), (DiffFilesPrefixStateId, DiffFilesPrefixError)> { let mut context = Context::new(); + context.put(RequestedPrefixDigestRange { + from: data.from_prefix_digest.map(str::to_owned), + to: data.to_prefix_digest.map(str::to_owned), + }); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]); diff --git a/lib/lib/src/unmutated/diff_files_prefix/preparing.rs b/lib/lib/src/unmutated/diff_files_prefix/preparing.rs index 39c31d63..922636ff 100644 --- a/lib/lib/src/unmutated/diff_files_prefix/preparing.rs +++ b/lib/lib/src/unmutated/diff_files_prefix/preparing.rs @@ -5,16 +5,58 @@ use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use crate::composefs::diff::TreeDiff; +use crate::composefs::file::FileHandle; +use crate::database::InMemory; +use crate::database::MemoryDatabase; +use crate::deploy::digest::current_prefix_digest; +use crate::deploy::{Deploy, DeployMode}; +use crate::errors::CommonError; use crate::orchestrator::Context; -use crate::orchestrator::stage::{RollbackGuard, Stage}; -use crate::unmutated::diff_files_prefix::DiffFilesPrefixError; +use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage}; +use crate::types::RequestedPrefixDigestRange; +use crate::types::database::DATABASE_PATH; +use crate::unmutated::diff_files_prefix::{DiffFilesPrefixError, DiffFilesPrefixSnapshot}; pub struct PreparingStage; impl Stage for PreparingStage { fn run( - &self, _context: &mut Context, _cancel: &CancelToken, _progress: ProgressEventBuilder, + &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, Box), DiffFilesPrefixError> { - todo!() + let requested = context + .get::() + .ok_or(CommonError::MissingResult)?; + + let from_prefix_digest = match &requested.from { + Some(prefix_digest) => prefix_digest.clone(), + None => current_prefix_digest()?, + }; + let to_prefix_digest = match &requested.to { + Some(prefix_digest) => prefix_digest.clone(), + None => current_prefix_digest()?, + }; + + let deploy = Deploy::new(DeployMode::ReadOnly)?; + let repository = deploy.open_repository()?; + + let from_tree = deploy.open_tree(&from_prefix_digest)?; + let to_tree = deploy.open_tree(&to_prefix_digest)?; + + let changed = TreeDiff::run(&from_tree, &to_tree); + + let from_bytes = FileHandle::new(DATABASE_PATH).read_file(&repository, &from_tree)?; + let from_database = MemoryDatabase::open_in_memory(from_bytes)?; + + let to_bytes = FileHandle::new(DATABASE_PATH).read_file(&repository, &to_tree)?; + let to_database = MemoryDatabase::open_in_memory(to_bytes)?; + + context.put(DiffFilesPrefixSnapshot { + changed, + from_database, + to_database, + }); + + Ok((progress, Box::new(NoRollback))) } } diff --git a/lib/lib/src/unmutated/diff_packages/comparing.rs b/lib/lib/src/unmutated/diff_packages/comparing.rs index 86bdb8f9..a407eb98 100644 --- a/lib/lib/src/unmutated/diff_packages/comparing.rs +++ b/lib/lib/src/unmutated/diff_packages/comparing.rs @@ -20,7 +20,9 @@ impl Stage for ComparingStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, Box), DiffPackagesError> { - let snapshot = context.take::().ok_or(CommonError::MissingResult)?; + let snapshot = context + .take::() + .ok_or(CommonError::MissingResult)?; let from: HashMap<_, _> = snapshot .from From 02d6922c476210f5054a223052b3bf50e864e726 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 20:39:16 +0400 Subject: [PATCH 56/68] fix: Fixed 'etc' naming in config new: Implemented a new set of commands Co-Authored-By: Claude Sonnet 5 --- lib/abi/src/request.rs | 6 +- lib/lib/src/database/record.rs | 2 +- lib/lib/src/mutated/rollback/mod.rs | 4 +- lib/lib/src/types/mod.rs | 5 ++ .../unmutated/diff_files_config/comparing.rs | 44 +++++++++- .../src/unmutated/diff_files_config/error.rs | 14 +++- .../src/unmutated/diff_files_config/mod.rs | 22 +++-- .../unmutated/diff_files_config/preparing.rs | 81 ++++++++++++++++++- lib/lib/src/unmutated/list_commit/fetching.rs | 2 +- .../src/unmutated/list_history/fetching.rs | 2 +- lib/lib/tests/database_record.rs | 4 +- user/upac-cli/src/commands/commit/rollback.rs | 4 +- user/upac-cli/src/commands/package/diff.rs | 8 +- user/upac-cli/src/ffi/request.rs | 32 ++++---- 14 files changed, 183 insertions(+), 47 deletions(-) diff --git a/lib/abi/src/request.rs b/lib/abi/src/request.rs index bc76b8ff..e0351e12 100644 --- a/lib/abi/src/request.rs +++ b/lib/abi/src/request.rs @@ -74,7 +74,7 @@ pub struct CRollbackRequest { pub base: CRequestBase, pub tmp_path: CSlice, - pub commit_hash: CSlice, + pub config_digest: CSlice, } #[repr(C)] @@ -154,9 +154,9 @@ pub struct CDiffFilesConfigRequest { pub base: CRequestBase, #[optional] - pub from_commit_hash: CSlice, + pub from_config_digest: CSlice, #[optional] - pub to_commit_hash: CSlice, + pub to_config_digest: CSlice, } #[repr(C)] diff --git a/lib/lib/src/database/record.rs b/lib/lib/src/database/record.rs index 46bb3ccf..5c98c539 100644 --- a/lib/lib/src/database/record.rs +++ b/lib/lib/src/database/record.rs @@ -14,7 +14,7 @@ use crate::types::deployment::RECORD_FILENAME; #[derive(Debug, Clone, PartialEq, Eq, JsonCodec)] pub struct EtcHistoryEntry { - pub etc_digest: String, + pub config_digest: String, pub subject: String, pub message: Option, } diff --git a/lib/lib/src/mutated/rollback/mod.rs b/lib/lib/src/mutated/rollback/mod.rs index e62b2a41..7ca6672d 100644 --- a/lib/lib/src/mutated/rollback/mod.rs +++ b/lib/lib/src/mutated/rollback/mod.rs @@ -27,7 +27,7 @@ mod merge; mod swap; pub struct RollbackData<'a> { - pub commit_hash: &'a str, + pub config_digest: &'a str, pub tmp_path: &'a str, @@ -46,7 +46,7 @@ impl<'a> TryFrom<&'a CRollbackRequest> for RollbackData<'a> { let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; Ok(RollbackData { - commit_hash: (&request.commit_hash).try_into()?, + config_digest: (&request.config_digest).try_into()?, tmp_path: (&request.tmp_path).try_into()?, diff --git a/lib/lib/src/types/mod.rs b/lib/lib/src/types/mod.rs index 1399031d..78f5a306 100644 --- a/lib/lib/src/types/mod.rs +++ b/lib/lib/src/types/mod.rs @@ -216,6 +216,11 @@ pub struct RequestedPrefixDigestRange { pub to: Option, } +pub struct RequestedConfigDigestRange { + pub from: Option, + pub to: Option, +} + pub struct DiffPackagesSnapshot { pub from: Vec, pub to: Vec, diff --git a/lib/lib/src/unmutated/diff_files_config/comparing.rs b/lib/lib/src/unmutated/diff_files_config/comparing.rs index 8a321e66..3c22f26d 100644 --- a/lib/lib/src/unmutated/diff_files_config/comparing.rs +++ b/lib/lib/src/unmutated/diff_files_config/comparing.rs @@ -3,18 +3,54 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later +use upac_abi::DiffKind; use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use crate::database::MemoryDatabase; +use crate::database::files::FileStore; +use crate::database::meta::MetaStore; +use crate::errors::CommonError; use crate::orchestrator::Context; -use crate::orchestrator::stage::{RollbackGuard, Stage}; -use crate::unmutated::diff_files_config::DiffFilesConfigError; +use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage}; +use crate::types::DiffConfigFileEntry; +use crate::unmutated::diff_files_config::{DiffFilesConfigError, DiffFilesConfigSnapshot}; pub struct ComparingStage; impl Stage for ComparingStage { fn run( - &self, _context: &mut Context, _cancel: &CancelToken, _progress: ProgressEventBuilder, + &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, Box), DiffFilesConfigError> { - todo!() + let snapshot = context.take::().ok_or(CommonError::MissingResult)?; + + let mut entries = Vec::new(); + + for (path, kind) in snapshot.changed { + let database = match kind { + DiffKind::Removed => &snapshot.from_database, + DiffKind::Added | DiffKind::Modified => &snapshot.to_database, + }; + + let package_name = Self::attribute(database, &path)?; + + entries.push(DiffConfigFileEntry { path, kind, package_name }); + } + + context.put(entries); + + Ok((progress, Box::new(NoRollback))) + } +} + +impl ComparingStage { + fn attribute(database: &MemoryDatabase, path: &str) -> Result, DiffFilesConfigError> { + let Some(uuid) = database.find_file_owner(path)? else { + return Ok(None); + }; + let Some(meta) = database.get_package_meta(uuid)? else { + return Ok(None); + }; + + Ok(Some(meta.name)) } } diff --git a/lib/lib/src/unmutated/diff_files_config/error.rs b/lib/lib/src/unmutated/diff_files_config/error.rs index 274dd1af..5a738eb8 100644 --- a/lib/lib/src/unmutated/diff_files_config/error.rs +++ b/lib/lib/src/unmutated/diff_files_config/error.rs @@ -5,20 +5,29 @@ use upac_abi::error::ErrorKind; -use crate::database::error::DatabaseError; +use crate::composefs::error::RepoError; +use crate::database::error::{DatabaseError, DeployRecordError}; use crate::deploy::error::SysrootError; -use crate::errors::{CommonError, common_error_from, database_error_from, lock_error_from, sysroot_error_from}; +use crate::errors::{ + CommonError, common_error_from, database_error_from, deploy_record_error_from, lock_error_from, repo_error_from, + sysroot_error_from, +}; use crate::lock::LockError; #[derive(Debug, Clone, PartialEq, Eq)] pub enum DiffFilesConfigError { Common(CommonError), + ConfigDigestNotFound(String), } common_error_from!(DiffFilesConfigError); database_error_from!(DiffFilesConfigError); +deploy_record_error_from!(DiffFilesConfigError); + +repo_error_from!(DiffFilesConfigError); + sysroot_error_from!(DiffFilesConfigError); lock_error_from!(DiffFilesConfigError); @@ -27,6 +36,7 @@ impl From for ErrorKind { fn from(error: DiffFilesConfigError) -> Self { match error { DiffFilesConfigError::Common(common_error) => common_error.into(), + DiffFilesConfigError::ConfigDigestNotFound(_) => ErrorKind::NotFound, } } } diff --git a/lib/lib/src/unmutated/diff_files_config/mod.rs b/lib/lib/src/unmutated/diff_files_config/mod.rs index c7a53d42..4c8bf96e 100644 --- a/lib/lib/src/unmutated/diff_files_config/mod.rs +++ b/lib/lib/src/unmutated/diff_files_config/mod.rs @@ -5,6 +5,7 @@ use std::os::raw::c_void; +use upac_abi::DiffKind; use upac_abi::error::ErrorKind; use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; use upac_abi::request::CDiffFilesConfigRequest; @@ -14,17 +15,24 @@ pub use self::error::DiffFilesConfigError; use self::comparing::ComparingStage; use self::preparing::PreparingStage; +use crate::database::MemoryDatabase; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use crate::types::DiffConfigFileEntry; use crate::types::states::DiffFilesConfigStateId; +use crate::types::{DiffConfigFileEntry, RequestedConfigDigestRange}; mod comparing; mod error; mod preparing; +struct DiffFilesConfigSnapshot { + changed: Vec<(String, DiffKind)>, + from_database: MemoryDatabase, + to_database: MemoryDatabase, +} + pub struct DiffFilesConfigData<'a> { - pub from_commit_hash: Option<&'a str>, - pub to_commit_hash: Option<&'a str>, + pub from_config_digest: Option<&'a str>, + pub to_config_digest: Option<&'a str>, pub hook_message: Option, pub hook_message_context: *mut c_void, @@ -41,8 +49,8 @@ impl<'a> TryFrom<&'a CDiffFilesConfigRequest> for DiffFilesConfigData<'a> { let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; Ok(DiffFilesConfigData { - from_commit_hash: (&request.from_commit_hash).try_into()?, - to_commit_hash: (&request.to_commit_hash).try_into()?, + from_config_digest: (&request.from_config_digest).try_into()?, + to_config_digest: (&request.to_config_digest).try_into()?, hook_message: request.base.on_hook, hook_message_context: request.base.hook_ctx, @@ -56,6 +64,10 @@ pub fn run( data: DiffFilesConfigData, ) -> Result<(Vec,), (DiffFilesConfigStateId, DiffFilesConfigError)> { let mut context = Context::new(); + context.put(RequestedConfigDigestRange { + from: data.from_config_digest.map(str::to_owned), + to: data.to_config_digest.map(str::to_owned), + }); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]); diff --git a/lib/lib/src/unmutated/diff_files_config/preparing.rs b/lib/lib/src/unmutated/diff_files_config/preparing.rs index 9309b665..73cc4b55 100644 --- a/lib/lib/src/unmutated/diff_files_config/preparing.rs +++ b/lib/lib/src/unmutated/diff_files_config/preparing.rs @@ -5,16 +5,89 @@ use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use crate::composefs::diff::TreeDiff; +use crate::composefs::file::FileHandle; +use crate::database::error::DeployRecordError; +use crate::database::record::DeployRecord; +use crate::database::{InMemory, MemoryDatabase}; +use crate::deploy::digest::current_prefix_digest; +use crate::deploy::{Deploy, DeployMode}; +use crate::errors::CommonError; use crate::orchestrator::Context; -use crate::orchestrator::stage::{RollbackGuard, Stage}; -use crate::unmutated::diff_files_config::DiffFilesConfigError; +use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage}; +use crate::types::RequestedConfigDigestRange; +use crate::types::database::DATABASE_PATH; +use crate::unmutated::diff_files_config::{DiffFilesConfigError, DiffFilesConfigSnapshot}; pub struct PreparingStage; impl Stage for PreparingStage { fn run( - &self, _context: &mut Context, _cancel: &CancelToken, _progress: ProgressEventBuilder, + &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, Box), DiffFilesConfigError> { - todo!() + let requested = context.get::().ok_or(CommonError::MissingResult)?; + + let deploy = Deploy::new(DeployMode::ReadOnly)?; + + let (from_config_digest, from_prefix_digest) = Self::resolve(&deploy, requested.from.as_ref())?; + let (to_config_digest, to_prefix_digest) = Self::resolve(&deploy, requested.to.as_ref())?; + + let repository = deploy.open_repository()?; + + let from_config_tree = deploy.open_tree(&from_config_digest)?; + let to_config_tree = deploy.open_tree(&to_config_digest)?; + + let changed = TreeDiff::run(&from_config_tree, &to_config_tree); + + let from_prefix_tree = deploy.open_tree(&from_prefix_digest)?; + let from_bytes = FileHandle::new(DATABASE_PATH).read_file(&repository, &from_prefix_tree)?; + let from_database = MemoryDatabase::open_in_memory(from_bytes)?; + + let to_prefix_tree = deploy.open_tree(&to_prefix_digest)?; + let to_bytes = FileHandle::new(DATABASE_PATH).read_file(&repository, &to_prefix_tree)?; + let to_database = MemoryDatabase::open_in_memory(to_bytes)?; + + context.put(DiffFilesConfigSnapshot { + changed, + from_database, + to_database, + }); + + Ok((progress, Box::new(NoRollback))) + } +} + +impl PreparingStage { + // Resolves a requested (possibly absent) config_digest to the config_digest + // itself plus the prefix_digest of the deploy it belongs to — needed because + // the /usr package database used for file attribution lives under the prefix, + // not under the standalone /etc image the config_digest names. + fn resolve(deploy: &Deploy, requested: Option<&String>) -> Result<(String, String), DiffFilesConfigError> { + match requested { + Some(config_digest) => { + for prefix_digest in deploy.deploys()? { + let record = match DeployRecord::read(&deploy.deploy(&prefix_digest)) { + Ok(record) => record, + Err(DeployRecordError::NotFound) => continue, + Err(error) => return Err(error.into()), + }; + + let owns_config_digest = record.working_etc == *config_digest + || record.config_history.iter().any(|entry| entry.config_digest == *config_digest); + + if owns_config_digest { + return Ok((config_digest.clone(), prefix_digest)); + } + } + + Err(DiffFilesConfigError::ConfigDigestNotFound(config_digest.clone())) + } + None => { + let prefix_digest = current_prefix_digest()?; + let record = DeployRecord::read(&deploy.deploy(&prefix_digest))?; + + Ok((record.working_etc, prefix_digest)) + } + } } } diff --git a/lib/lib/src/unmutated/list_commit/fetching.rs b/lib/lib/src/unmutated/list_commit/fetching.rs index a562e949..0e1448b7 100644 --- a/lib/lib/src/unmutated/list_commit/fetching.rs +++ b/lib/lib/src/unmutated/list_commit/fetching.rs @@ -36,7 +36,7 @@ impl Stage for FetchingStage { .config_history .into_iter() .map(|entry| CommitEntry { - config_digest: entry.etc_digest, + config_digest: entry.config_digest, subject: entry.subject, message: entry.message, }) diff --git a/lib/lib/src/unmutated/list_history/fetching.rs b/lib/lib/src/unmutated/list_history/fetching.rs index 58f9c069..ae1cfd23 100644 --- a/lib/lib/src/unmutated/list_history/fetching.rs +++ b/lib/lib/src/unmutated/list_history/fetching.rs @@ -34,7 +34,7 @@ impl Stage for FetchingStage { .config_history .into_iter() .map(|entry| CommitEntry { - config_digest: entry.etc_digest, + config_digest: entry.config_digest, subject: entry.subject, message: entry.message, }) diff --git a/lib/lib/tests/database_record.rs b/lib/lib/tests/database_record.rs index c534916f..f25972ad 100644 --- a/lib/lib/tests/database_record.rs +++ b/lib/lib/tests/database_record.rs @@ -25,12 +25,12 @@ fn sample_record() -> DeployRecord { timestamp: 1_754_000_000, config_history: vec![ EtcHistoryEntry { - etc_digest: "etc-digest-1".to_string(), + config_digest: "etc-digest-1".to_string(), subject: "first etc".to_string(), message: None, }, EtcHistoryEntry { - etc_digest: "etc-digest-2".to_string(), + config_digest: "etc-digest-2".to_string(), subject: "second etc".to_string(), message: Some("with a message".to_string()), }, diff --git a/user/upac-cli/src/commands/commit/rollback.rs b/user/upac-cli/src/commands/commit/rollback.rs index a7c9a1f0..1255d3d2 100644 --- a/user/upac-cli/src/commands/commit/rollback.rs +++ b/user/upac-cli/src/commands/commit/rollback.rs @@ -18,10 +18,10 @@ pub struct Args { } pub fn run(args: Args, ctx: CommandContext) -> Result<()> { - let commit_hash = CString::new(args.commit)?; + let config_digest = CString::new(args.commit)?; let request = CMutatedRequest::for_rollback( - &commit_hash, + &config_digest, &ctx.config.paths.repo_path, &ctx.config.paths.root_path, &ctx.config.ostree.branch, diff --git a/user/upac-cli/src/commands/package/diff.rs b/user/upac-cli/src/commands/package/diff.rs index f6b4e80c..94b2d325 100644 --- a/user/upac-cli/src/commands/package/diff.rs +++ b/user/upac-cli/src/commands/package/diff.rs @@ -21,16 +21,16 @@ pub struct Args { } pub fn run(args: Args, ctx: CommandContext) -> Result<()> { - let from_commit_hash = CString::new(args.from)?; - let to_commit_hash = CString::new(args.to)?; + let from_config_digest = CString::new(args.from)?; + let to_config_digest = CString::new(args.to)?; let mut response = CUnmutatedResponse::empty(); let request = CUnmutatedRequest::for_diff( &ctx.config.paths.repo_path, &ctx.tmp_path, - &from_commit_hash, - &to_commit_hash, + &from_config_digest, + &to_config_digest, cancel_token_ptr(), ); diff --git a/user/upac-cli/src/ffi/request.rs b/user/upac-cli/src/ffi/request.rs index 1cbc33ce..bccc1ad3 100644 --- a/user/upac-cli/src/ffi/request.rs +++ b/user/upac-cli/src/ffi/request.rs @@ -45,7 +45,7 @@ pub struct CMutatedRequest { packages_count: usize, uninstall_packages: *const CPackageInfo, uninstall_packages_len: usize, - commit_hash: CSlice, + config_digest: CSlice, message: CSlice, files: *const CSlice, files_len: usize, @@ -71,7 +71,7 @@ impl CMutatedRequest { packages_count: 0, uninstall_packages: null(), uninstall_packages_len: 0, - commit_hash: CSlice::empty(), + config_digest: CSlice::empty(), message: CSlice::empty(), files: null(), files_len: 0, @@ -122,11 +122,11 @@ impl CMutatedRequest { #[allow(clippy::too_many_arguments)] pub fn for_rollback( - commit_hash: &CString, repo_path: &CString, root_path: &CString, branch: &CString, on_hook: Option, + config_digest: &CString, repo_path: &CString, root_path: &CString, branch: &CString, on_hook: Option, hook_ctx: *mut c_void, cancel_token: *mut CancelToken, ) -> Self { let mut req = Self::base(repo_path, root_path, branch, on_hook, hook_ctx, cancel_token); - req.commit_hash = CSlice::from_cstring(commit_hash); + req.config_digest = CSlice::from_cstring(config_digest); req } @@ -156,8 +156,8 @@ pub struct CUnmutatedRequest { root_path: CSlice, tmp_path: CSlice, branch: CSlice, - from_commit_hash: CSlice, - to_commit_hash: CSlice, + from_config_digest: CSlice, + to_config_digest: CSlice, search: CSlice, symlinks: *const CSlice, symlinks_len: usize, @@ -175,8 +175,8 @@ impl CUnmutatedRequest { root_path: CSlice::from_cstring(root_path), tmp_path: CSlice::empty(), branch: CSlice::from_cstring(branch), - from_commit_hash: CSlice::empty(), - to_commit_hash: CSlice::empty(), + from_config_digest: CSlice::empty(), + to_config_digest: CSlice::empty(), search: CSlice::empty(), symlinks: null(), symlinks_len: 0, @@ -192,8 +192,8 @@ impl CUnmutatedRequest { root_path: CSlice::from_cstring(root_path), tmp_path: CSlice::empty(), branch: CSlice::empty(), - from_commit_hash: CSlice::empty(), - to_commit_hash: CSlice::empty(), + from_config_digest: CSlice::empty(), + to_config_digest: CSlice::empty(), search: CSlice::empty(), symlinks: null(), symlinks_len: 0, @@ -212,8 +212,8 @@ impl CUnmutatedRequest { root_path: CSlice::empty(), tmp_path: CSlice::from_cstring(tmp_path), branch: CSlice::empty(), - from_commit_hash: CSlice::from_cstring(from_commit), - to_commit_hash: CSlice::from_cstring(to_commit), + from_config_digest: CSlice::from_cstring(from_commit), + to_config_digest: CSlice::from_cstring(to_commit), search: CSlice::empty(), symlinks: null(), symlinks_len: 0, @@ -229,8 +229,8 @@ impl CUnmutatedRequest { root_path: CSlice::from_cstring(root_path), tmp_path: CSlice::empty(), branch: CSlice::empty(), - from_commit_hash: CSlice::empty(), - to_commit_hash: CSlice::empty(), + from_config_digest: CSlice::empty(), + to_config_digest: CSlice::empty(), search: CSlice::from_cstring(query), symlinks: null(), symlinks_len: 0, @@ -249,8 +249,8 @@ impl CUnmutatedRequest { root_path: CSlice::from_cstring(root_path), tmp_path: CSlice::empty_str(), branch: CSlice::from_cstring(branch), - from_commit_hash: CSlice::empty(), - to_commit_hash: CSlice::empty(), + from_config_digest: CSlice::empty(), + to_config_digest: CSlice::empty(), search: CSlice::empty(), symlinks: if symlinks.is_empty() { null() } else { symlinks.as_ptr() }, symlinks_len: symlinks.len(), From c6c2c4b3accd0438927b2cdfc8dc081f8e4e08dd Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 20:40:07 +0400 Subject: [PATCH 57/68] new: Added a separate type for package kind fix: The old diff kind type is now file diff kind Co-Authored-By: Claude Sonnet 5 --- lib/abi/src/lib.rs | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/lib/abi/src/lib.rs b/lib/abi/src/lib.rs index e2564999..46f80e30 100644 --- a/lib/abi/src/lib.rs +++ b/lib/abi/src/lib.rs @@ -18,18 +18,46 @@ pub const ABI_VERSION: u32 = 2; #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DiffKind { +pub enum FileDiffKind { Added = 0, Removed = 1, Modified = 2, } -impl DiffKind { - pub fn from_u8(version: u8) -> Result { +impl FileDiffKind { + pub fn from_u8(version: u8) -> Result { match version { - 0 => Ok(DiffKind::Added), - 1 => Ok(DiffKind::Removed), - 2 => Ok(DiffKind::Modified), + 0 => Ok(FileDiffKind::Added), + 1 => Ok(FileDiffKind::Removed), + 2 => Ok(FileDiffKind::Modified), + _ => Err(ErrorKind::InvalidEntry), + } + } +} + +// A package's own metadata can be Added/Removed/Modified — or unchanged while +// one of its own files changed underneath it (e.g. a hand-edited is_user file), +// which FileDiffKind's three variants can't represent. Kept separate rather +// than adding a fourth variant to FileDiffKind, since every file-level +// consumer (DiffPrefixFileEntry/DiffConfigFileEntry/DiffUntrackedFileEntry) is +// already a complete, correct 3-way split — a package-only concept doesn't +// belong there. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageDiffKind { + Added = 0, + Removed = 1, + Modified = 2, + FilesChanged = 3, +} + +impl PackageDiffKind { + pub fn from_u8(version: u8) -> Result { + match version { + 0 => Ok(PackageDiffKind::Added), + 1 => Ok(PackageDiffKind::Removed), + 2 => Ok(PackageDiffKind::Modified), + 3 => Ok(PackageDiffKind::FilesChanged), _ => Err(ErrorKind::InvalidEntry), } } From 61a95155ba322b86ba70bcfff10593ac8c7b5b95 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 20:40:59 +0400 Subject: [PATCH 58/68] fix: rename diff kind to file diff kind Co-Authored-By: Claude Sonnet 5 --- lib/abi/src/request.rs | 2 +- lib/abi/src/response.rs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/abi/src/request.rs b/lib/abi/src/request.rs index e0351e12..72bca247 100644 --- a/lib/abi/src/request.rs +++ b/lib/abi/src/request.rs @@ -7,7 +7,7 @@ use std::os::raw::c_void; use upac_macro::CValidate; -use crate::DiffKind; +use crate::FileDiffKind; use crate::error::ErrorKind; use crate::hook::{CancelToken, HookMessageFn}; use crate::package::{CPackageInfo, CUnpackedPackage}; diff --git a/lib/abi/src/response.rs b/lib/abi/src/response.rs index ab0a1168..d203298c 100644 --- a/lib/abi/src/response.rs +++ b/lib/abi/src/response.rs @@ -5,18 +5,18 @@ use upac_macro::{CFree, CValidate}; -use crate::DiffKind; use crate::error::ErrorKind; use crate::memory::{free_cslice, free_cvec_owning}; use crate::package::{CPackageMeta, CVersion}; use crate::types::{CSlice, CVec, check_size}; +use crate::{FileDiffKind, PackageDiffKind}; #[repr(C)] #[derive(CFree)] pub struct CDiffPackageEntry { pub struct_size: usize, pub name: CSlice, - pub kind: DiffKind, + pub kind: PackageDiffKind, pub version: CVersion, pub files: CVec, } @@ -27,7 +27,7 @@ pub struct CDiffPrefixFileEntry { pub struct_size: usize, pub path: CSlice, - pub kind: DiffKind, + pub kind: FileDiffKind, pub package_name: CSlice, pub is_user: bool, } @@ -38,7 +38,7 @@ pub struct CDiffConfigFileEntry { pub struct_size: usize, pub path: CSlice, - pub kind: DiffKind, + pub kind: FileDiffKind, #[optional] pub package_name: CSlice, } @@ -49,7 +49,7 @@ pub struct CDiffUntrackedFileEntry { pub struct_size: usize, pub path: CSlice, - pub kind: DiffKind, + pub kind: FileDiffKind, } #[repr(C)] From 18f7bf67649aabcb5c65bc0ffe470a40761b4d01 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 20:41:12 +0400 Subject: [PATCH 59/68] fix: rename diff kind to file diff kind Co-Authored-By: Claude Sonnet 5 --- lib/abi/src/request.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/abi/src/request.rs b/lib/abi/src/request.rs index 72bca247..cdc7568d 100644 --- a/lib/abi/src/request.rs +++ b/lib/abi/src/request.rs @@ -100,7 +100,7 @@ pub struct CFilesRequest { #[optional] pub message: CSlice, pub files: CVec, - pub file_kind: DiffKind, + pub file_kind: FileDiffKind, pub file_package: *const CPackageInfo, } From eec133aef8639fcaa3e52798d68e426d46236376 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 20:41:20 +0400 Subject: [PATCH 60/68] fix: rename diff kind to file diff kind Co-Authored-By: Claude Sonnet 5 --- lib/macro/src/common.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/macro/src/common.rs b/lib/macro/src/common.rs index 7cbaf516..3987286a 100644 --- a/lib/macro/src/common.rs +++ b/lib/macro/src/common.rs @@ -12,7 +12,7 @@ pub(crate) const PRIMITIVES: &[&str] = &[ "u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128", "isize", "bool", "f32", "f64", ]; -pub(crate) const SHARED_TYPES: &[&str] = &["DiffKind"]; +pub(crate) const SHARED_TYPES: &[&str] = &["FileDiffKind", "PackageDiffKind"]; pub(crate) const VALIDATABLE_COMPOSITES: &[&str] = &[ "CVersion", From c5c36588e7793059755b8a0bf47eb096526e645f Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 20:43:03 +0400 Subject: [PATCH 61/68] fix: rename diff kind to file diff kind and add new package diff to use Co-Authored-By: Claude Sonnet 5 --- lib/lib/src/composefs/diff.rs | 14 +++++++------- lib/lib/src/mutated/files/mod.rs | 4 ++-- lib/lib/src/types/mod.rs | 10 +++++----- .../src/unmutated/diff_files_config/comparing.rs | 6 +++--- lib/lib/src/unmutated/diff_files_config/mod.rs | 4 ++-- .../src/unmutated/diff_files_prefix/comparing.rs | 8 ++++---- lib/lib/src/unmutated/diff_files_prefix/mod.rs | 4 ++-- lib/lib/src/unmutated/diff_packages/comparing.rs | 8 ++++---- 8 files changed, 29 insertions(+), 29 deletions(-) diff --git a/lib/lib/src/composefs/diff.rs b/lib/lib/src/composefs/diff.rs index 5c7749b4..246f5685 100644 --- a/lib/lib/src/composefs/diff.rs +++ b/lib/lib/src/composefs/diff.rs @@ -7,7 +7,7 @@ use std::cmp::Ordering; use std::path::{Path, PathBuf}; use composefs::tree::{Directory, FileSystem, Inode, Leaf, LeafContent, RegularFile}; -use upac_abi::DiffKind; +use upac_abi::FileDiffKind; use crate::composefs::repository::ObjectID; @@ -25,10 +25,10 @@ enum Side { } impl Side { - fn kind(self) -> DiffKind { + fn kind(self) -> FileDiffKind { match self { - Side::From => DiffKind::Removed, - Side::To => DiffKind::Added, + Side::From => FileDiffKind::Removed, + Side::To => FileDiffKind::Added, } } } @@ -36,11 +36,11 @@ impl Side { pub struct TreeDiff<'a> { from_leaves: &'a [Leaf], to_leaves: &'a [Leaf], - changes: Vec<(String, DiffKind)>, + changes: Vec<(String, FileDiffKind)>, } impl<'a> TreeDiff<'a> { - pub fn run(from: &'a FileSystem, to: &'a FileSystem) -> Vec<(String, DiffKind)> { + pub fn run(from: &'a FileSystem, to: &'a FileSystem) -> Vec<(String, FileDiffKind)> { let mut differ = Self { from_leaves: &from.leaves, to_leaves: &to.leaves, @@ -98,7 +98,7 @@ impl<'a> TreeDiff<'a> { && Self::is_regular_or_symlink(to_leaf) && !Self::content_matches(from_leaf, to_leaf) { - self.changes.push((Self::path_to_string(path), DiffKind::Modified)); + self.changes.push((Self::path_to_string(path), FileDiffKind::Modified)); } } (from_inode, to_inode) => { diff --git a/lib/lib/src/mutated/files/mod.rs b/lib/lib/src/mutated/files/mod.rs index b854b1b2..fd3b4b95 100644 --- a/lib/lib/src/mutated/files/mod.rs +++ b/lib/lib/src/mutated/files/mod.rs @@ -5,7 +5,7 @@ use std::os::raw::c_void; -use upac_abi::DiffKind; +use upac_abi::FileDiffKind; use upac_abi::error::ErrorKind; use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; use upac_abi::package::CPackageInfo; @@ -30,7 +30,7 @@ mod transaction; pub struct FilesData<'a> { pub files: Vec<&'a str>, - pub file_kind: DiffKind, + pub file_kind: FileDiffKind, pub file_package: &'a CPackageInfo, pub tmp_path: &'a str, diff --git a/lib/lib/src/types/mod.rs b/lib/lib/src/types/mod.rs index 78f5a306..295cc768 100644 --- a/lib/lib/src/types/mod.rs +++ b/lib/lib/src/types/mod.rs @@ -5,7 +5,6 @@ use std::mem::size_of; -use upac_abi::DiffKind; use upac_abi::decoder::CDependency; use upac_abi::error::ErrorKind; use upac_abi::package::{CPackageMeta, CUnpackedPackage, CVersion}; @@ -14,6 +13,7 @@ use upac_abi::response::{ CHistoryEntry, CPrefixEntry, CSearchFileEntry, }; use upac_abi::types::{CBorrowed, COwned, CSlice, CVec}; +use upac_abi::{FileDiffKind, PackageDiffKind}; use upac_macro::{CTryToRust, RedbCodec, RustToC}; include!(concat!(env!("OUT_DIR"), "/layout.rs")); @@ -156,7 +156,7 @@ pub struct HistoryEntry { #[derive(Debug, Clone, RustToC)] pub struct DiffPrefixFileEntry { pub path: String, - pub kind: DiffKind, + pub kind: FileDiffKind, pub package_name: String, pub is_user: bool, } @@ -165,7 +165,7 @@ pub struct DiffPrefixFileEntry { #[derive(Debug, Clone, RustToC)] pub struct DiffConfigFileEntry { pub path: String, - pub kind: DiffKind, + pub kind: FileDiffKind, pub package_name: Option, } @@ -173,7 +173,7 @@ pub struct DiffConfigFileEntry { #[derive(Debug, Clone, RustToC)] pub struct DiffPackageEntry { pub name: String, - pub kind: DiffKind, + pub kind: PackageDiffKind, pub version: Version, // Only this package's own files. A changed file with no package to @@ -190,7 +190,7 @@ pub struct DiffPackageEntry { #[derive(Debug, Clone, RustToC)] pub struct DiffUntrackedFileEntry { pub path: String, - pub kind: DiffKind, + pub kind: FileDiffKind, } pub struct Targets(pub Vec); diff --git a/lib/lib/src/unmutated/diff_files_config/comparing.rs b/lib/lib/src/unmutated/diff_files_config/comparing.rs index 3c22f26d..c24b68d4 100644 --- a/lib/lib/src/unmutated/diff_files_config/comparing.rs +++ b/lib/lib/src/unmutated/diff_files_config/comparing.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later -use upac_abi::DiffKind; +use upac_abi::FileDiffKind; use upac_abi::hook::{CancelToken, ProgressEventBuilder}; use crate::database::MemoryDatabase; @@ -27,8 +27,8 @@ impl Stage for ComparingStage { for (path, kind) in snapshot.changed { let database = match kind { - DiffKind::Removed => &snapshot.from_database, - DiffKind::Added | DiffKind::Modified => &snapshot.to_database, + FileDiffKind::Removed => &snapshot.from_database, + FileDiffKind::Added | FileDiffKind::Modified => &snapshot.to_database, }; let package_name = Self::attribute(database, &path)?; diff --git a/lib/lib/src/unmutated/diff_files_config/mod.rs b/lib/lib/src/unmutated/diff_files_config/mod.rs index 4c8bf96e..34ff6fb0 100644 --- a/lib/lib/src/unmutated/diff_files_config/mod.rs +++ b/lib/lib/src/unmutated/diff_files_config/mod.rs @@ -5,7 +5,7 @@ use std::os::raw::c_void; -use upac_abi::DiffKind; +use upac_abi::FileDiffKind; use upac_abi::error::ErrorKind; use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; use upac_abi::request::CDiffFilesConfigRequest; @@ -25,7 +25,7 @@ mod error; mod preparing; struct DiffFilesConfigSnapshot { - changed: Vec<(String, DiffKind)>, + changed: Vec<(String, FileDiffKind)>, from_database: MemoryDatabase, to_database: MemoryDatabase, } diff --git a/lib/lib/src/unmutated/diff_files_prefix/comparing.rs b/lib/lib/src/unmutated/diff_files_prefix/comparing.rs index e300ca08..92821ed7 100644 --- a/lib/lib/src/unmutated/diff_files_prefix/comparing.rs +++ b/lib/lib/src/unmutated/diff_files_prefix/comparing.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later -use upac_abi::DiffKind; +use upac_abi::FileDiffKind; use upac_abi::hook::{CancelToken, ProgressEventBuilder}; use crate::database::MemoryDatabase; @@ -29,8 +29,8 @@ impl Stage for ComparingStage { for (path, kind) in snapshot.changed { let database = match kind { - DiffKind::Removed => &snapshot.from_database, - DiffKind::Added | DiffKind::Modified => &snapshot.to_database, + FileDiffKind::Removed => &snapshot.from_database, + FileDiffKind::Added | FileDiffKind::Modified => &snapshot.to_database, }; if let Some(entry) = Self::attribute(database, &path, kind)? { @@ -46,7 +46,7 @@ impl Stage for ComparingStage { impl ComparingStage { fn attribute( - database: &MemoryDatabase, path: &str, kind: DiffKind, + database: &MemoryDatabase, path: &str, kind: FileDiffKind, ) -> Result, DiffFilesPrefixError> { let Some(uuid) = database.find_file_owner(path)? else { return Ok(None); diff --git a/lib/lib/src/unmutated/diff_files_prefix/mod.rs b/lib/lib/src/unmutated/diff_files_prefix/mod.rs index 76819f9d..ede5fc12 100644 --- a/lib/lib/src/unmutated/diff_files_prefix/mod.rs +++ b/lib/lib/src/unmutated/diff_files_prefix/mod.rs @@ -5,7 +5,7 @@ use std::os::raw::c_void; -use upac_abi::DiffKind; +use upac_abi::FileDiffKind; use upac_abi::error::ErrorKind; use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; use upac_abi::request::CDiffFilesPrefixRequest; @@ -25,7 +25,7 @@ mod error; mod preparing; struct DiffFilesPrefixSnapshot { - changed: Vec<(String, DiffKind)>, + changed: Vec<(String, FileDiffKind)>, from_database: MemoryDatabase, to_database: MemoryDatabase, } diff --git a/lib/lib/src/unmutated/diff_packages/comparing.rs b/lib/lib/src/unmutated/diff_packages/comparing.rs index a407eb98..afd521d4 100644 --- a/lib/lib/src/unmutated/diff_packages/comparing.rs +++ b/lib/lib/src/unmutated/diff_packages/comparing.rs @@ -5,7 +5,7 @@ use std::collections::HashMap; -use upac_abi::DiffKind; +use upac_abi::PackageDiffKind; use upac_abi::hook::{CancelToken, ProgressEventBuilder}; use crate::errors::CommonError; @@ -41,14 +41,14 @@ impl Stage for ComparingStage { match to.remove(&identity) { Some(to_meta) if to_meta.sha256 != from_meta.sha256 => entries.push(DiffPackageEntry { name: to_meta.name, - kind: DiffKind::Modified, + kind: PackageDiffKind::Modified, version: to_meta.version, files: Vec::new(), }), Some(_) => {} None => entries.push(DiffPackageEntry { name: from_meta.name, - kind: DiffKind::Removed, + kind: PackageDiffKind::Removed, version: from_meta.version, files: Vec::new(), }), @@ -58,7 +58,7 @@ impl Stage for ComparingStage { for (_identity, to_meta) in to { entries.push(DiffPackageEntry { name: to_meta.name, - kind: DiffKind::Added, + kind: PackageDiffKind::Added, version: to_meta.version, files: Vec::new(), }); From 90d9ae42a6582f0fa460a88b2dc54bb35806211f Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 20:44:40 +0400 Subject: [PATCH 62/68] fix: fix formating Co-Authored-By: Claude Sonnet 5 --- lib/lib/src/unmutated/diff_files_config/comparing.rs | 10 ++++++++-- lib/lib/src/unmutated/diff_files_config/preparing.rs | 9 +++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/lib/src/unmutated/diff_files_config/comparing.rs b/lib/lib/src/unmutated/diff_files_config/comparing.rs index c24b68d4..c439b4ae 100644 --- a/lib/lib/src/unmutated/diff_files_config/comparing.rs +++ b/lib/lib/src/unmutated/diff_files_config/comparing.rs @@ -21,7 +21,9 @@ impl Stage for ComparingStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, Box), DiffFilesConfigError> { - let snapshot = context.take::().ok_or(CommonError::MissingResult)?; + let snapshot = context + .take::() + .ok_or(CommonError::MissingResult)?; let mut entries = Vec::new(); @@ -33,7 +35,11 @@ impl Stage for ComparingStage { let package_name = Self::attribute(database, &path)?; - entries.push(DiffConfigFileEntry { path, kind, package_name }); + entries.push(DiffConfigFileEntry { + path, + kind, + package_name, + }); } context.put(entries); diff --git a/lib/lib/src/unmutated/diff_files_config/preparing.rs b/lib/lib/src/unmutated/diff_files_config/preparing.rs index 73cc4b55..feca8559 100644 --- a/lib/lib/src/unmutated/diff_files_config/preparing.rs +++ b/lib/lib/src/unmutated/diff_files_config/preparing.rs @@ -25,7 +25,9 @@ impl Stage for PreparingStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, Box), DiffFilesConfigError> { - let requested = context.get::().ok_or(CommonError::MissingResult)?; + let requested = context + .get::() + .ok_or(CommonError::MissingResult)?; let deploy = Deploy::new(DeployMode::ReadOnly)?; @@ -73,7 +75,10 @@ impl PreparingStage { }; let owns_config_digest = record.working_etc == *config_digest - || record.config_history.iter().any(|entry| entry.config_digest == *config_digest); + || record + .config_history + .iter() + .any(|entry| entry.config_digest == *config_digest); if owns_config_digest { return Ok((config_digest.clone(), prefix_digest)); From 7384efe040f6d41dcd9dd4e85c742e4b55145428 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 20:51:08 +0400 Subject: [PATCH 63/68] fix: tests moved to a separate file Co-Authored-By: Claude Sonnet 5 --- lib/lib/src/types/mod.rs | 49 +++----------------------------------- lib/lib/src/types/tests.rs | 47 ++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 46 deletions(-) create mode 100644 lib/lib/src/types/tests.rs diff --git a/lib/lib/src/types/mod.rs b/lib/lib/src/types/mod.rs index 295cc768..4a643884 100644 --- a/lib/lib/src/types/mod.rs +++ b/lib/lib/src/types/mod.rs @@ -20,6 +20,9 @@ include!(concat!(env!("OUT_DIR"), "/layout.rs")); pub mod states; +#[cfg(test)] +mod tests; + macro_rules! as_str_method { ($name:ty) => { impl AsRef for $name { @@ -225,49 +228,3 @@ pub struct DiffPackagesSnapshot { pub from: Vec, pub to: Vec, } - -#[cfg(test)] -mod tests { - use super::*; - - fn sample_version() -> Version { - Version { - epoch: 1, - parts: vec![2, 5, 0], - pre: Some("rc1".to_owned()), - release: 3, - } - } - - #[test] - fn version_redb_round_trip_preserves_value() { - let original = sample_version(); - - let mut buf = Vec::new(); - Version::encode_into(&mut buf, &original); - - let mut offset = 0; - let restored = Version::decode_from(&buf, &mut offset); - - assert_eq!(restored, original); - assert_eq!(offset, buf.len()); - } - - #[test] - fn file_entry_redb_round_trip_preserves_value() { - let original = FileEntry { - path: "/usr/bin/up".to_owned(), - is_user: false, - }; - - let mut buf = Vec::new(); - FileEntry::encode_into(&mut buf, &original); - - let mut offset = 0; - let restored = FileEntry::decode_from(&buf, &mut offset); - - assert_eq!(restored.path, original.path); - assert_eq!(restored.is_user, original.is_user); - assert_eq!(offset, buf.len()); - } -} diff --git a/lib/lib/src/types/tests.rs b/lib/lib/src/types/tests.rs new file mode 100644 index 00000000..9f8f9c32 --- /dev/null +++ b/lib/lib/src/types/tests.rs @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 JustPav +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +use super::*; + +fn sample_version() -> Version { + Version { + epoch: 1, + parts: vec![2, 5, 0], + pre: Some("rc1".to_owned()), + release: 3, + } +} + +#[test] +fn version_redb_round_trip_preserves_value() { + let original = sample_version(); + + let mut buf = Vec::new(); + Version::encode_into(&mut buf, &original); + + let mut offset = 0; + let restored = Version::decode_from(&buf, &mut offset); + + assert_eq!(restored, original); + assert_eq!(offset, buf.len()); +} + +#[test] +fn file_entry_redb_round_trip_preserves_value() { + let original = FileEntry { + path: "/usr/bin/up".to_owned(), + is_user: false, + }; + + let mut buf = Vec::new(); + FileEntry::encode_into(&mut buf, &original); + + let mut offset = 0; + let restored = FileEntry::decode_from(&buf, &mut offset); + + assert_eq!(restored.path, original.path); + assert_eq!(restored.is_user, original.is_user); + assert_eq!(offset, buf.len()); +} From 3b720d8d9a21011dd17d3531cbdeccffebff4478 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 20:57:45 +0400 Subject: [PATCH 64/68] fix: updated header in README --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 0f95b2db..31c111a4 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,32 @@ # 📦 Upac +### Links for repositories: + +--- + [![GitHub](https://img.shields.io/badge/GitHub-SmoothTeam%2Fupac-181717?logo=github)](https://github.com/SmoothTeam/upac) [![Codeberg](https://img.shields.io/badge/Codeberg-justpav05%2Fupac-2185D0?logo=codeberg)](https://codeberg.org/justpav05/upac) + +--- + +### General information: + +--- + [![Version](https://img.shields.io/badge/version-0.1.5-green)](https://github.com/SmoothTeam/upac/releases) [![REUSE status](https://api.reuse.software/badge/github.com/SmoothTeam/upac)](https://api.reuse.software/info/github.com/SmoothTeam/upac) + +--- + +### Licensing: + +--- + [![lib: LGPL-3.0-or-later](https://img.shields.io/badge/lib-LGPL--3.0--or--later-blue.svg)](LICENSES/LGPL-3.0-or-later.txt) [![cli: GPL-3.0-only](https://img.shields.io/badge/cli-GPL--3.0--only-blue.svg)](LICENSES/GPL-3.0-only.txt) +--- + > **⚠️ Branch in progress.** This branch (`lib-rs`) is a from-scratch rewrite of upac's core library in Rust, built around [composefs](https://github.com/containers/composefs) instead of OSTree. The FFI/orchestration engine is done; the actual command bodies, the hook system, and packaging are still being implemented — expect gaps and `todo!()`s. A modular package management library for Linux systems with composefs-based atomic deploys. From d2bbad0cfbfa7a677abdb0dbb5f41f7d7c014d24 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 21:02:00 +0400 Subject: [PATCH 65/68] fix: updated header in README --- README.md | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 31c111a4..d87c6740 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,10 @@ # 📦 Upac -### Links for repositories: +### Links for repositories: [![GitHub](https://img.shields.io/badge/GitHub-SmoothTeam%2Fupac-181717?logo=github)](https://github.com/SmoothTeam/upac) [![Codeberg](https://img.shields.io/badge/Codeberg-justpav05%2Fupac-2185D0?logo=codeberg)](https://codeberg.org/justpav05/upac) ---- +### General information: [![Version](https://img.shields.io/badge/version-0.1.5-green)](https://github.com/SmoothTeam/upac/releases) [![REUSE status](https://api.reuse.software/badge/github.com/SmoothTeam/upac)](https://api.reuse.software/info/github.com/SmoothTeam/upac) -[![GitHub](https://img.shields.io/badge/GitHub-SmoothTeam%2Fupac-181717?logo=github)](https://github.com/SmoothTeam/upac) -[![Codeberg](https://img.shields.io/badge/Codeberg-justpav05%2Fupac-2185D0?logo=codeberg)](https://codeberg.org/justpav05/upac) - ---- - -### General information: - ---- - -[![Version](https://img.shields.io/badge/version-0.1.5-green)](https://github.com/SmoothTeam/upac/releases) -[![REUSE status](https://api.reuse.software/badge/github.com/SmoothTeam/upac)](https://api.reuse.software/info/github.com/SmoothTeam/upac) - ---- - -### Licensing: - ---- - -[![lib: LGPL-3.0-or-later](https://img.shields.io/badge/lib-LGPL--3.0--or--later-blue.svg)](LICENSES/LGPL-3.0-or-later.txt) -[![cli: GPL-3.0-only](https://img.shields.io/badge/cli-GPL--3.0--only-blue.svg)](LICENSES/GPL-3.0-only.txt) - ---- +### Licensing: [![lib: LGPL-3.0-or-later](https://img.shields.io/badge/lib-LGPL--3.0--or--later-blue.svg)](LICENSES/LGPL-3.0-or-later.txt) [![cli: GPL-3.0-only](https://img.shields.io/badge/cli-GPL--3.0--only-blue.svg)](LICENSES/GPL-3.0-only.txt) > **⚠️ Branch in progress.** This branch (`lib-rs`) is a from-scratch rewrite of upac's core library in Rust, built around [composefs](https://github.com/containers/composefs) instead of OSTree. The FFI/orchestration engine is done; the actual command bodies, the hook system, and packaging are still being implemented — expect gaps and `todo!()`s. From a4ad1312e375434e38bfe76a6a66654a5ee3c2dd Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 21:03:37 +0400 Subject: [PATCH 66/68] fix: updated header in README Co-Authored-By: Claude Sonnet 5 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d87c6740..b7618c3e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # 📦 Upac -### Links for repositories: [![GitHub](https://img.shields.io/badge/GitHub-SmoothTeam%2Fupac-181717?logo=github)](https://github.com/SmoothTeam/upac) [![Codeberg](https://img.shields.io/badge/Codeberg-justpav05%2Fupac-2185D0?logo=codeberg)](https://codeberg.org/justpav05/upac) +Links for repositories: [![GitHub](https://img.shields.io/badge/GitHub-SmoothTeam%2Fupac-181717?logo=github)](https://github.com/SmoothTeam/upac) [![Codeberg](https://img.shields.io/badge/Codeberg-justpav05%2Fupac-2185D0?logo=codeberg)](https://codeberg.org/justpav05/upac) -### General information: [![Version](https://img.shields.io/badge/version-0.1.5-green)](https://github.com/SmoothTeam/upac/releases) [![REUSE status](https://api.reuse.software/badge/github.com/SmoothTeam/upac)](https://api.reuse.software/info/github.com/SmoothTeam/upac) +General information: [![Version](https://img.shields.io/badge/version-0.1.5-green)](https://github.com/SmoothTeam/upac/releases) [![REUSE status](https://api.reuse.software/badge/github.com/SmoothTeam/upac)](https://api.reuse.software/info/github.com/SmoothTeam/upac) -### Licensing: [![lib: LGPL-3.0-or-later](https://img.shields.io/badge/lib-LGPL--3.0--or--later-blue.svg)](LICENSES/LGPL-3.0-or-later.txt) [![cli: GPL-3.0-only](https://img.shields.io/badge/cli-GPL--3.0--only-blue.svg)](LICENSES/GPL-3.0-only.txt) +Licensing: [![lib: LGPL-3.0-or-later](https://img.shields.io/badge/lib-LGPL--3.0--or--later-blue.svg)](LICENSES/LGPL-3.0-or-later.txt) [![cli: GPL-3.0-only](https://img.shields.io/badge/cli-GPL--3.0--only-blue.svg)](LICENSES/GPL-3.0-only.txt) > **⚠️ Branch in progress.** This branch (`lib-rs`) is a from-scratch rewrite of upac's core library in Rust, built around [composefs](https://github.com/containers/composefs) instead of OSTree. The FFI/orchestration engine is done; the actual command bodies, the hook system, and packaging are still being implemented — expect gaps and `todo!()`s. From aa2bd51207f0035146279d5c5ddc0223d9702275 Mon Sep 17 00:00:00 2001 From: JustPav Date: Thu, 13 Aug 2026 21:07:02 +0400 Subject: [PATCH 67/68] new: new types implemented new: new commands implemented Co-Authored-By: Claude Sonnet 5 --- lib/lib/src/database/attribution.rs | 36 ++++++++++++++++++ lib/lib/src/database/mod.rs | 1 + .../unmutated/diff_files_config/comparing.rs | 21 ++-------- .../unmutated/diff_files_prefix/comparing.rs | 38 ++++--------------- 4 files changed, 49 insertions(+), 47 deletions(-) create mode 100644 lib/lib/src/database/attribution.rs diff --git a/lib/lib/src/database/attribution.rs b/lib/lib/src/database/attribution.rs new file mode 100644 index 00000000..1421e2f7 --- /dev/null +++ b/lib/lib/src/database/attribution.rs @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 JustPav +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +use super::error::DatabaseError; +use super::files::FileStore; +use super::meta::MetaStore; + +use crate::types::{FileEntry, PackageMeta}; + +pub struct FileAttribution { + pub package_meta: PackageMeta, + pub file_entry: FileEntry, +} + +pub trait FileAttribute: FileStore + MetaStore { + fn attribute_file(&self, path: &str) -> Result, DatabaseError> { + let Some(uuid) = self.find_file_owner(path)? else { + return Ok(None); + }; + let Some(package_meta) = self.get_package_meta(uuid)? else { + return Ok(None); + }; + let Some(file_entry) = self.list_files(uuid)?.into_iter().find(|entry| entry.path == path) else { + return Ok(None); + }; + + Ok(Some(FileAttribution { + package_meta, + file_entry, + })) + } +} + +impl FileAttribute for T {} diff --git a/lib/lib/src/database/mod.rs b/lib/lib/src/database/mod.rs index 1e1ddac2..c014f007 100644 --- a/lib/lib/src/database/mod.rs +++ b/lib/lib/src/database/mod.rs @@ -21,6 +21,7 @@ use crate::types::database::{ use self::error::DatabaseError; +pub mod attribution; pub mod error; pub mod files; pub mod meta; diff --git a/lib/lib/src/unmutated/diff_files_config/comparing.rs b/lib/lib/src/unmutated/diff_files_config/comparing.rs index c439b4ae..10788ef7 100644 --- a/lib/lib/src/unmutated/diff_files_config/comparing.rs +++ b/lib/lib/src/unmutated/diff_files_config/comparing.rs @@ -6,9 +6,7 @@ use upac_abi::FileDiffKind; use upac_abi::hook::{CancelToken, ProgressEventBuilder}; -use crate::database::MemoryDatabase; -use crate::database::files::FileStore; -use crate::database::meta::MetaStore; +use crate::database::attribution::FileAttribute; use crate::errors::CommonError; use crate::orchestrator::Context; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage}; @@ -33,7 +31,9 @@ impl Stage for ComparingStage { FileDiffKind::Added | FileDiffKind::Modified => &snapshot.to_database, }; - let package_name = Self::attribute(database, &path)?; + let package_name = database + .attribute_file(&path)? + .map(|attribution| attribution.package_meta.name); entries.push(DiffConfigFileEntry { path, @@ -47,16 +47,3 @@ impl Stage for ComparingStage { Ok((progress, Box::new(NoRollback))) } } - -impl ComparingStage { - fn attribute(database: &MemoryDatabase, path: &str) -> Result, DiffFilesConfigError> { - let Some(uuid) = database.find_file_owner(path)? else { - return Ok(None); - }; - let Some(meta) = database.get_package_meta(uuid)? else { - return Ok(None); - }; - - Ok(Some(meta.name)) - } -} diff --git a/lib/lib/src/unmutated/diff_files_prefix/comparing.rs b/lib/lib/src/unmutated/diff_files_prefix/comparing.rs index 92821ed7..880523a6 100644 --- a/lib/lib/src/unmutated/diff_files_prefix/comparing.rs +++ b/lib/lib/src/unmutated/diff_files_prefix/comparing.rs @@ -6,9 +6,7 @@ use upac_abi::FileDiffKind; use upac_abi::hook::{CancelToken, ProgressEventBuilder}; -use crate::database::MemoryDatabase; -use crate::database::files::FileStore; -use crate::database::meta::MetaStore; +use crate::database::attribution::FileAttribute; use crate::errors::CommonError; use crate::orchestrator::Context; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage}; @@ -33,8 +31,13 @@ impl Stage for ComparingStage { FileDiffKind::Added | FileDiffKind::Modified => &snapshot.to_database, }; - if let Some(entry) = Self::attribute(database, &path, kind)? { - entries.push(entry); + if let Some(attribution) = database.attribute_file(&path)? { + entries.push(DiffPrefixFileEntry { + path, + kind, + package_name: attribution.package_meta.name, + is_user: attribution.file_entry.is_user, + }); } } @@ -43,28 +46,3 @@ impl Stage for ComparingStage { Ok((progress, Box::new(NoRollback))) } } - -impl ComparingStage { - fn attribute( - database: &MemoryDatabase, path: &str, kind: FileDiffKind, - ) -> Result, DiffFilesPrefixError> { - let Some(uuid) = database.find_file_owner(path)? else { - return Ok(None); - }; - let Some(meta) = database.get_package_meta(uuid)? else { - return Ok(None); - }; - let is_user = database - .list_files(uuid)? - .into_iter() - .find(|entry| entry.path == path) - .is_some_and(|entry| entry.is_user); - - Ok(Some(DiffPrefixFileEntry { - path: path.to_owned(), - kind, - package_name: meta.name, - is_user, - })) - } -} From 786d79190b71b01d1d4937ce5625d81fed3954b7 Mon Sep 17 00:00:00 2001 From: JustPav Date: Fri, 14 Aug 2026 16:17:43 +0400 Subject: [PATCH 68/68] new: implemented the diff command Co-Authored-By: Claude Sonnet 5 --- lib/lib/src/unmutated/diff/comparing.rs | 116 +++++++++++++++++++++++- lib/lib/src/unmutated/diff/mod.rs | 16 +++- lib/lib/src/unmutated/diff/preparing.rs | 54 ++++++++++- 3 files changed, 177 insertions(+), 9 deletions(-) diff --git a/lib/lib/src/unmutated/diff/comparing.rs b/lib/lib/src/unmutated/diff/comparing.rs index 1515e1c8..5d47741f 100644 --- a/lib/lib/src/unmutated/diff/comparing.rs +++ b/lib/lib/src/unmutated/diff/comparing.rs @@ -3,18 +3,126 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later +use std::collections::HashMap; + use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::{FileDiffKind, PackageDiffKind}; +use crate::database::attribution::FileAttribute; +use crate::errors::CommonError; use crate::orchestrator::Context; -use crate::orchestrator::stage::{RollbackGuard, Stage}; -use crate::unmutated::diff::DiffError; +use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage}; +use crate::types::{DiffPackageEntry, DiffPrefixFileEntry, DiffUntrackedFileEntry, PackageMeta, Version}; +use crate::unmutated::diff::{DiffError, DiffSnapshot}; + +type PackageIdentity = (String, String, Option); pub struct ComparingStage; impl Stage for ComparingStage { fn run( - &self, _context: &mut Context, _cancel: &CancelToken, _progress: ProgressEventBuilder, + &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, Box), DiffError> { - todo!() + let snapshot = context.take::().ok_or(CommonError::MissingResult)?; + + let mut packages = Self::diff_packages(snapshot.from_packages, snapshot.to_packages); + let mut unattached_files = Vec::new(); + + for (path, kind) in snapshot.changed_files { + let database = match kind { + FileDiffKind::Removed => &snapshot.from_database, + FileDiffKind::Added | FileDiffKind::Modified => &snapshot.to_database, + }; + + match database.attribute_file(&path)? { + Some(attribution) => { + let identity = Self::identity(&attribution.package_meta); + + let entry = packages.entry(identity).or_insert_with(|| DiffPackageEntry { + name: attribution.package_meta.name.clone(), + kind: PackageDiffKind::FilesChanged, + version: attribution.package_meta.version.clone(), + files: Vec::new(), + }); + + entry.files.push(DiffPrefixFileEntry { + path, + kind, + package_name: attribution.package_meta.name, + is_user: attribution.file_entry.is_user, + }); + } + None => unattached_files.push(DiffUntrackedFileEntry { path, kind }), + } + } + + context.put(packages.into_values().collect::>()); + context.put(unattached_files); + + Ok((progress, Box::new(NoRollback))) + } +} + +impl ComparingStage { + fn identity(meta: &PackageMeta) -> PackageIdentity { + (meta.name.clone(), meta.arch.clone(), meta.arch_sub.clone()) + } + + fn diff_packages(from: Vec, to: Vec) -> HashMap { + let from: HashMap<_, _> = from.into_iter().map(|meta| (Self::identity(&meta), meta)).collect(); + let mut to: HashMap<_, _> = to.into_iter().map(|meta| (Self::identity(&meta), meta)).collect(); + + let mut packages = HashMap::new(); + + for (identity, from_meta) in from { + match to.remove(&identity) { + Some(to_meta) if to_meta.sha256 != from_meta.sha256 => { + Self::insert( + &mut packages, + identity, + to_meta.name, + PackageDiffKind::Modified, + to_meta.version, + ); + } + Some(_) => {} + None => { + Self::insert( + &mut packages, + identity, + from_meta.name, + PackageDiffKind::Removed, + from_meta.version, + ); + } + } + } + + for (identity, to_meta) in to { + Self::insert( + &mut packages, + identity, + to_meta.name, + PackageDiffKind::Added, + to_meta.version, + ); + } + + packages + } + + fn insert( + packages: &mut HashMap, identity: PackageIdentity, name: String, + kind: PackageDiffKind, version: Version, + ) { + packages.insert( + identity, + DiffPackageEntry { + name, + kind, + version, + files: Vec::new(), + }, + ); } } diff --git a/lib/lib/src/unmutated/diff/mod.rs b/lib/lib/src/unmutated/diff/mod.rs index 6799842e..6b7a065f 100644 --- a/lib/lib/src/unmutated/diff/mod.rs +++ b/lib/lib/src/unmutated/diff/mod.rs @@ -5,6 +5,7 @@ use std::os::raw::c_void; +use upac_abi::FileDiffKind; use upac_abi::error::ErrorKind; use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; use upac_abi::request::CDiffRequest; @@ -14,14 +15,23 @@ pub use self::error::DiffError; use self::comparing::ComparingStage; use self::preparing::PreparingStage; +use crate::database::MemoryDatabase; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; use crate::types::states::DiffStateId; -use crate::types::{DiffPackageEntry, DiffUntrackedFileEntry}; +use crate::types::{DiffPackageEntry, DiffUntrackedFileEntry, PackageMeta, RequestedPrefixDigestRange}; mod comparing; mod error; mod preparing; +struct DiffSnapshot { + from_packages: Vec, + to_packages: Vec, + changed_files: Vec<(String, FileDiffKind)>, + from_database: MemoryDatabase, + to_database: MemoryDatabase, +} + pub struct DiffData<'a> { pub from_prefix_digest: Option<&'a str>, pub to_prefix_digest: Option<&'a str>, @@ -54,6 +64,10 @@ impl<'a> TryFrom<&'a CDiffRequest> for DiffData<'a> { pub fn run(data: DiffData) -> Result<(Vec, Vec), (DiffStateId, DiffError)> { let mut context = Context::new(); + context.put(RequestedPrefixDigestRange { + from: data.from_prefix_digest.map(str::to_owned), + to: data.to_prefix_digest.map(str::to_owned), + }); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]); diff --git a/lib/lib/src/unmutated/diff/preparing.rs b/lib/lib/src/unmutated/diff/preparing.rs index 9729b8eb..1851f519 100644 --- a/lib/lib/src/unmutated/diff/preparing.rs +++ b/lib/lib/src/unmutated/diff/preparing.rs @@ -5,16 +5,62 @@ use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use crate::composefs::diff::TreeDiff; +use crate::composefs::file::FileHandle; +use crate::database::meta::MetaStore; +use crate::database::{InMemory, MemoryDatabase}; +use crate::deploy::digest::current_prefix_digest; +use crate::deploy::{Deploy, DeployMode}; +use crate::errors::CommonError; use crate::orchestrator::Context; -use crate::orchestrator::stage::{RollbackGuard, Stage}; -use crate::unmutated::diff::DiffError; +use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage}; +use crate::types::RequestedPrefixDigestRange; +use crate::types::database::DATABASE_PATH; +use crate::unmutated::diff::{DiffError, DiffSnapshot}; pub struct PreparingStage; impl Stage for PreparingStage { fn run( - &self, _context: &mut Context, _cancel: &CancelToken, _progress: ProgressEventBuilder, + &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, Box), DiffError> { - todo!() + let requested = context + .get::() + .ok_or(CommonError::MissingResult)?; + + let from_prefix_digest = match &requested.from { + Some(prefix_digest) => prefix_digest.clone(), + None => current_prefix_digest()?, + }; + let to_prefix_digest = match &requested.to { + Some(prefix_digest) => prefix_digest.clone(), + None => current_prefix_digest()?, + }; + + let deploy = Deploy::new(DeployMode::ReadOnly)?; + let repository = deploy.open_repository()?; + + let from_tree = deploy.open_tree(&from_prefix_digest)?; + let to_tree = deploy.open_tree(&to_prefix_digest)?; + + let changed_files = TreeDiff::run(&from_tree, &to_tree); + + let from_bytes = FileHandle::new(DATABASE_PATH).read_file(&repository, &from_tree)?; + let from_database = MemoryDatabase::open_in_memory(from_bytes)?; + let from_packages = from_database.list_packages_metas()?; + + let to_bytes = FileHandle::new(DATABASE_PATH).read_file(&repository, &to_tree)?; + let to_database = MemoryDatabase::open_in_memory(to_bytes)?; + let to_packages = to_database.list_packages_metas()?; + + context.put(DiffSnapshot { + from_packages, + to_packages, + changed_files, + from_database, + to_database, + }); + + Ok((progress, Box::new(NoRollback))) } }