diff --git a/TODO.md b/TODO.md index 816c6356..e8c2b4df 100644 --- a/TODO.md +++ b/TODO.md @@ -10,69 +10,53 @@ Near-term, concrete items. See `ROADMAP.md` for the bigger picture. ## upac-lib -Test-coverage pass in progress, going file by file through the non-command core first -(`errors.rs`/`lock.rs`/`search.rs`/`fs.rs`/`orchestrator/*`/`database/*` done), commands -(`mutated`/`unmutated`) last. Remaining core files not yet visited: `deploy/{error,retention,mod}.rs` -(`esp.rs` skipped — real mount), `scripts/{error,file,load,pipeline,primitive}.rs`, -`plugin/decoder/{error,unpack,mod}.rs`, `plugin/boot/{error,manifest,mod}.rs`, -`composefs/{diff,error,mod}.rs`, `config/mod.rs`, `boot/{error,mod}.rs`. - -`boot/mod.rs`'s UKI staging only ever writes the single fixed `upac-to` slot -(`layout::boot::UPAC_TO_SLOT`) — doc chapter 3's disk-layout map still describes a two-slot -`upac-from.efi`/`upac-to.efi` A/B scheme, but no code anywhere writes/reads an `upac-from` slot. -Real unfinished A/B swap, not just a stale doc — needs a decision (implement the second slot, or -formally drop it and fix the doc to match the single-slot design `lib.toml`'s own comment already -argues for). - -Genesis (`up-sp`) now installs the actual bootloader binary onto a fresh ESP for systemd-boot and -rEFInd (`StageBootStage::run`, `lib/setup/lib.toml`'s `[genesis]` source paths) — confirmed working -via a live VM test for systemd-boot; rEFInd wired the same way but not yet VM-verified. grub is NOT -handled — a real `grub-install`-equivalent (target-specific generated `grubx64.efi`, not a plain -file copy) is out of scope for now; either shell out to `grub-install` against the mounted ESP, or -explicitly document grub as unsupported for genesis whole-disk mode. - -`StageBootStage` picks which ESP loader binary to copy via a `match input.boot_plugin.as_deref()` -against literal `"systemd-boot"`/`"refind"` strings — this bypasses the actual dynamic boot-plugin -system (`resolve_boot_plugin`/`BootPluginManifest`/`static_plugins`), which is supposed to be the -one place plugin names are known. Adding a 5th booter plugin would require editing this match by -hand instead of just dropping in a new plugin. The correct fix is extending the `Booter` ABI itself -with a 4th function (e.g. `esp_loader_source() -> CSlice`, empty for uki/grub) so genesis asks the -already-resolved plugin for its own install-time source path instead of hardcoding names — but that -means bumping `BOOT_ABI_VERSION` and touching all 4 `booters/*` crates, so deliberately deferred; -the hardcoded match stays as a known, scoped limitation until then. - -**Genesis-produced disks don't actually boot into the installed system yet** — found via a live -QEMU/OVMF test (systemd-boot now starts, finds the BLS entry, loads kernel+initramfs — that part -works after the bootloader-binary fix above). Two separate gaps, both required: -1. `partition.rs`'s `LINUX_PARTITION_TYPE_GUID` (`0fc63daf-8483-4772-8e79-3d69d8477de4`, generic - "Linux filesystem data") should be the discoverable-root GUID - (`4f68bce3-e8cd-4db1-96e7-fbcaf984b709`, "Linux root x86-64") so `systemd-gpt-auto-generator` - can find the deploy partition at all instead of hanging on `/dev/gpt-auto-root`. -2. Even with (1) fixed, a plain partition mount isn't how composefs systems boot — nothing in this - project resolves `composefs.digest=` (the kernel cmdline param `write_boot_entry` already - writes) against the on-disk repository, mounts the erofs image with fs-verity, and overlays - `state/deploy//etc/`. **Found a real, existing upstream tool for exactly this**: - `composefs-setup-root` (crates.io, same `composefs-rs` project/version as our `composefs`/ - `composefs-boot` deps) — a Rust binary, not something we'd write ourselves. Our on-disk layout - already matches its hardcoded expectations (`composefs/`, `state/deploy//`) after - renaming `etc-upper` → `etc` (done, `lib.toml`'s `config_dir_name`). What's still missing: the - actual boot-time integration — the live VM's initramfs is systemd-based (mkinitcpio's `systemd` - hook, not classic busybox-style hooks), so this needs a systemd unit ordered between - `sysroot.mount` and `initrd-switch-root.target` (same role as ostree's - `ostree-prepare-root.service`), not a classic mkinitcpio hook script. Also unresolved: whether - upac needs to ship/package this integration itself, or whether it's expected to already exist - on the source distro (same assumption as the systemd-boot/rEFInd binary copy above) — needs - checking whether Arch/AUR already has a package for this. - -**Genesis tracks the entire bootstrapped system as a single synthetic "rootfs" package**, not -per-package (`ReadMetaStage` reads one `meta.toml`, `ImportTreesStage` imports all of source's -`usr`/`etc` wholesale). Found while reasoning about the `composefs-setup-root` hook: if it needs to -already be installed on the source system (via pacman) for genesis to pick it up, its files still -end up attributed to the one fake "rootfs" package in our database — no real per-package -provenance for anything baked into the source image, unlike a `pacstrap`-then-`up install` flow -would give. Decision made: genesis should eventually be rewritten to install real, individually -decoded packages through the same pipeline `up install` uses, instead of importing a pre-built -directory wholesale — no special-casing even for the kernel package. This is a genesis rewrite, not -a patch; deliberately deferred until after a dedicated code-cleanup/macro-consolidation pass -(reduce duplicated lines, extract shared macros) elsewhere in the codebase first. - +Test-coverage pass in progress. The entire non-command core is covered (`errors.rs`/`lock.rs`/ +`search.rs`/`fs.rs`/`orchestrator/*`/`database/*`/`deploy/*`/`scripts/*`/`composefs/*`/`config/*`/ +`boot/*`/`plugin/decoder/{error,manifest,triggers}.rs`/`plugin/boot/{error,manifest}.rs`), except +`plugin/decoder/unpack.rs`/`plugin/decoder/mod.rs`/`plugin/boot/mod.rs` (need a real dlopen'd/ +`builtin-*` plugin) and `deploy/esp.rs` (real mount table) — both explicit, justified skips. Every +`mutated`/`unmutated` command's own `Error` enum is also now covered (inline tests next to +each `error.rs`, since `mutated`/`unmutated` aren't `pub`) — only each variant's own logic, not the +macro-generated `Common(...)` delegation shared with `errors.rs`'s already-tested `CommonError`. +Remaining: the `Stage::run()` bodies themselves — each needs a real composefs `Repository`/`Deploy`/ +database in context, likely out of scope for unit tests unless a pure-logic helper turns out to be +extractable. + +**`genesis`'s `system/` mechanism is done**: `ImportSystemStage` requires `/system/` (a +literal 1:1 mirror of the target's real `/usr`, sibling to the package archives — +`EnumeratePackagesStage` already skips it, it only looks at files) to contain +`lib/systemd/system/composefs-setup-root.service` (hard error, `SetupError:: +ComposefsSetupRootUnitNotFound`, if missing), imports the whole tree into `PrefixTree`, and creates +the unit's `*.target.wants/` enablement symlink itself. This is also how a built `up`/`upac-lib`/ +booters gets onto a genesis'd disk at all — genesis never installs itself automatically, whoever +assembles `--source` has to place it under `system/` too, same assumption already made for the +systemd-boot/rEFInd binaries. Confirmed `composefs-setup-root`'s own hardcoded expectations already +match upac's on-disk layout exactly (repo at `composefs/`, per-deploy state at `state/deploy//`, +`composefs=` cmdline karg) — no restructuring was needed, only the unit + the `system/` plumbing. +Still unresolved: whether upac ships/packages the `composefs-setup-root` binary itself or expects it +to already exist on the source distro (same open question as the systemd-boot/rEFInd binaries). + +**Booter ABI redesign — decided this session, execution in progress file-by-file under direct +supervision (no batch edits).** Four canonical plugin responsibilities: + +1. Plugin sets itself for one-time boot (`set_one_shot`) — done. +2. Plugin sets itself for persistent boot (`confirm_boot`) — done, including UKI's `to.efi`↔ + `from.efi` file swap (needed a new `esp_mount_point` parameter on `confirm_boot`, added this + session; the swap only fires when the confirmed `entry_name` is the `to` slot specifically). +3. Plugin installs itself onto the ESP (`install`) — done for grub (real `grub-install + --removable --no-nvram` + a minimal `blscfg` `grub.cfg`), no-op for the other 3. +4. Plugin declares where its own pre-built loader binary lives in the source package tree + (`esp_loader_source`) — done, stays a separate passive query (only genesis can reach the + composefs tree to copy the bytes out, plugins can't do this step themselves). + +**`write_boot_entry` must search only for the resource type the selected plugin needs, not +autonomously scan everything and guess.** Right now (`lib/lib/src/boot/mod.rs`) it calls +`get_boot_resources` unconditionally, takes whichever single boot resource exists in the tree +(Type1/Type2/`UsrLibModulesVmLinuz`), and only errors if more than one is found total — completely +independent of which plugin was actually selected. This is the case in all 6 call sites: +`lib/lib/src/boot/mod.rs` itself, `mutated/{files,installer,uninstaller,update,rollback}/checkout.rs`, +and `lib/setup/src/genesis/entry.rs`. Needs to take the (now always-explicit) plugin name and require +specifically: `uki` → `Type2` only; `grub`/`systemd-boot`/`rEFInd` → `Type1`/`UsrLibModulesVmLinuz` +only — hard error if that type isn't present, even if a different type is. This also means +`resolve_boot_plugin` must run before `write_boot_entry` everywhere — today the 5 ordinary +`checkout.rs` stages call it after (only genesis already has the order right). diff --git a/booters/booter.toml b/booters/booter.toml index af32a191..9336646b 100644 --- a/booters/booter.toml +++ b/booters/booter.toml @@ -8,42 +8,74 @@ # own build.rs reads this same file and generates its own private layout::boot module; nothing # here is a Cargo dependency between plugins, just a shared data file. # -# efi_sysfs_path is the fixed Linux kernel sysfs mount point for UEFI firmware (never -# distro/deployment-configurable, unlike the ESP mount point). efivarfs_path is the fixed -# subdirectory where the kernel exposes individual EFI variables as files (`efivar`'s own crate -# never surfaces this path, so plugins that need to clear the immutable attribute on an existing -# variable file — kernel efivarfs sets it after first write, and `efivar` never clears it back off -# before overwriting — have to reconstruct the exact same path themselves). sd_boot_loader_guid is -# systemd's vendor GUID for its own boot-loader-interface EFI variables (LoaderInfo, -# LoaderEntryOneShot, LoaderEntryDefault), per systemd's BOOT_LOADER_INTERFACE.md. boot_next_var/ -# boot_order_var are UEFI global-namespace variables, no vendor GUID needed — boot_order_var is -# only needed to clear its immutable attribute before `VarManager::set_boot_order` touches it -# (that method writes the variable internally, so the uki plugin can't intercept the write() call -# itself the way it can for boot_next_var). +# efivarfs_path is the fixed subdirectory where the kernel exposes individual EFI variables as +# files (`efivar`'s own crate never surfaces this path, so plugins that need to clear the +# immutable attribute on an existing variable file — kernel efivarfs sets it after first write, +# and `efivar` never clears it back off before overwriting — have to reconstruct the exact same +# path themselves). sd_boot_loader_guid is systemd's vendor GUID for its own boot-loader-interface +# EFI variables (LoaderEntryOneShot, LoaderEntryDefault), per systemd's BOOT_LOADER_INTERFACE.md. +# boot_next_var/boot_order_var are UEFI global-namespace variables, no vendor GUID needed — +# boot_order_var is only needed to clear its immutable attribute before +# `VarManager::set_boot_order` touches it (that method writes the variable internally, so the uki +# plugin can't intercept the write() call itself the way it can for boot_next_var). [boot] -efi_sysfs_path = "/sys/firmware/efi" efivarfs_path = "/sys/firmware/efi/efivars" sd_boot_loader_guid = "4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" boot_next_var = "BootNext" boot_order_var = "BootOrder" -loader_info_var = "LoaderInfo" loader_entry_one_shot_var = "LoaderEntryOneShot" loader_entry_default_var = "LoaderEntryDefault" +# efi_linux_dir is the fixed UEFI-style (backslash) directory composefs-boot's own +# `write_t2_simple` always writes UKI images under (`EFI/Linux/`, hardcoded upstream, not +# configurable) — used to build the file-path device-path segment when genesis registers a +# UEFI Boot#### entry for a UKI slot, so the entry's path always matches where the image +# actually lands. efi_linux_real_path is the same directory spelled as a real, mounted- +# filesystem-relative path (forward slashes, joined via `Path::join`) — used by +# `Uki::confirm_boot` to find `to.efi`/`from.efi` on the real, mounted ESP; the two can't share +# a definition (see `lib/setup/lib.toml`'s own `genesis.efi_linux_dir` for the same split, +# there for the same reason: different crate, different path flavor needed). +# +# to_slot/from_slot must stay in sync with `upac-lib`'s own `lib.toml` +# (`boot.upac_uki_to_slot`/`upac_uki_from_slot`) — two separate crates, can't share a single +# Rust constant, so each carries its own literal copy. `Uki::confirm_boot` only performs the +# to/from file swap when the confirmed `entry_name` is `to_slot` specifically (confirming a +# fallback boot into `from_slot` means `to_slot` may be broken — nothing should be promoted). +[uki] +efi_linux_dir = "\\EFI\\Linux\\" +efi_linux_real_path = "EFI/Linux" +to_slot = "upac-to" +from_slot = "upac-from" + # grub has no EFI-variable-based one-shot mechanism — it's file-based (grubenv), driven through # grub's own grub-reboot/grub-set-default tools rather than a hand-rolled binary-format writer. # Both the grubenv location and the tool names differ across distro packaging: Debian/Ubuntu/Arch # ship grub-reboot/grub-set-default writing /boot/grub/grubenv, while Fedora/RHEL keep the # "grub-*" names reserved for legacy GRUB Legacy and ship grub2-reboot/grub2-set-default writing # /boot/grub2/grubenv instead. *_primary/*_fallback are tried in that order. +# +# install_bin_primary/install_bin_fallback: same Debian/Arch-vs-Fedora naming split as the +# reboot/set-default tools above, for the one-time `grub-install` genesis runs against a freshly +# partitioned ESP. install_target is grub's own `--target` platform name (this project is +# UEFI/x86_64-only throughout, matching systemd_boot/refind's own *_x64 binary names below). +# install_bootloader_id is grub's `--bootloader-id`, used only to name the install's own +# NVRAM-independent EFI/BOOT/BOOTX64.EFI fallback copy (see backend.rs's `--removable --no-nvram` +# — genesis relies on the same UEFI firmware fallback path uki/systemd-boot/refind's binaries +# already occupy, not a Boot#### entry, so a fresh disk boots without any NVRAM setup). [grub] -grubenv_primary = "/boot/grub/grubenv" -grubenv_fallback = "/boot/grub2/grubenv" reboot_bin_primary = "grub-reboot" reboot_bin_fallback = "grub2-reboot" + set_default_bin_primary = "grub-set-default" set_default_bin_fallback = "grub2-set-default" +install_bin_primary = "grub-install" +install_bin_fallback = "grub2-install" + +install_target = "x86_64-efi" + +install_bootloader_id = "upac" + # rEFInd has no separate one-shot/persistent pair of variables like systemd-boot's # LoaderEntryOneShot/LoaderEntryDefault — it has exactly one, PreviousBoot, which it writes itself # just before launching any OS and reads back as the default on its next launch. Selecting an diff --git a/booters/grub/Cargo.toml b/booters/grub/Cargo.toml index 9cf8f60d..365c7f82 100644 --- a/booters/grub/Cargo.toml +++ b/booters/grub/Cargo.toml @@ -28,7 +28,8 @@ name = "upac_boot_grub" crate-type = ["cdylib", "rlib"] [dependencies] -upac-abi = { workspace = true } +upac-abi = { workspace = true } +upac-types = { workspace = true } [build-dependencies] toml = { workspace = true } diff --git a/booters/grub/build.rs b/booters/grub/build.rs index 23be7f05..1e56aec4 100644 --- a/booters/grub/build.rs +++ b/booters/grub/build.rs @@ -19,26 +19,25 @@ fn main() -> Result<(), Box> { let raw = read_to_string(&source)?; let config: Value = from_str(&raw)?; - let mut generated = String::new(); - - let sections = config.as_table().ok_or("booter.toml: root must be a table")?; - for (section, entries) in sections { - generated.push_str(&format!("pub mod {section} {{\n")); + let section = "grub"; + let entries = config + .get(section) + .and_then(Value::as_table) + .ok_or_else(|| format!("booter.toml: [{section}] must be a table"))?; - let entries = entries - .as_table() - .ok_or_else(|| format!("booter.toml: [{section}] must be a table"))?; - for (key, value) in entries { - let value = value - .as_str() - .ok_or_else(|| format!("booter.toml: {section}.{key} must be a string"))?; + let mut generated = String::new(); + generated.push_str(&format!("pub mod {section} {{\n")); - generated.push_str(&format!(" pub const {}: &str = {value:?};\n", key.to_uppercase())); - } + for (key, value) in entries { + let value = value + .as_str() + .ok_or_else(|| format!("booter.toml: {section}.{key} must be a string"))?; - generated.push_str("}\n"); + generated.push_str(&format!(" pub const {}: &str = {value:?};\n", key.to_uppercase())); } + generated.push_str("}\n"); + let out = Path::new(&var("OUT_DIR")?).join("layout.rs"); write(out, generated)?; diff --git a/booters/grub/src/backend.rs b/booters/grub/src/backend.rs index dc1ce59d..9a6867bc 100644 --- a/booters/grub/src/backend.rs +++ b/booters/grub/src/backend.rs @@ -3,18 +3,21 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception +use std::fs::{create_dir_all, write}; use std::io::ErrorKind as IoErrorKind; use std::path::Path; use std::process::Command; -use upac_abi::boot::Booter; +use upac_types::traits::Booter; -use crate::error::GrubError; -use crate::grub::{ - GRUBENV_FALLBACK, GRUBENV_PRIMARY, REBOOT_BIN_FALLBACK, REBOOT_BIN_PRIMARY, SET_DEFAULT_BIN_FALLBACK, - SET_DEFAULT_BIN_PRIMARY, +use super::error::GrubError; +use super::grub::{ + INSTALL_BIN_FALLBACK, INSTALL_BIN_PRIMARY, INSTALL_BOOTLOADER_ID, INSTALL_TARGET, REBOOT_BIN_FALLBACK, + REBOOT_BIN_PRIMARY, SET_DEFAULT_BIN_FALLBACK, SET_DEFAULT_BIN_PRIMARY, }; +const GRUB_CFG_CONTENTS: &str = "insmod blscfg\nblscfg\n"; + pub struct Grub; impl Booter for Grub { @@ -24,23 +27,55 @@ impl Booter for Grub { Ok(Grub) } - fn probes() -> bool { - Path::new(GRUBENV_PRIMARY).exists() || Path::new(GRUBENV_FALLBACK).exists() + fn set_one_shot(&mut self, entry_name: &str) -> Result<(), GrubError> { + self.run_first_available([REBOOT_BIN_PRIMARY, REBOOT_BIN_FALLBACK], &[entry_name]) } - fn set_one_shot(&mut self, entry_name: &str) -> Result<(), GrubError> { - self.run_first_available([REBOOT_BIN_PRIMARY, REBOOT_BIN_FALLBACK], entry_name) + fn confirm_boot(&mut self, entry_name: &str, esp_mount_point: &str) -> Result<(), GrubError> { + let _ = esp_mount_point; + + self.run_first_available([SET_DEFAULT_BIN_PRIMARY, SET_DEFAULT_BIN_FALLBACK], &[entry_name]) } - fn confirm_boot(&mut self, entry_name: &str) -> Result<(), GrubError> { - self.run_first_available([SET_DEFAULT_BIN_PRIMARY, SET_DEFAULT_BIN_FALLBACK], entry_name) + fn install( + &mut self, esp_mount_point: &str, esp_partition_number: u32, esp_starting_lba: u64, esp_ending_lba: u64, + esp_unique_partition_guid: [u8; 16], to_slot: &str, from_slot: &str, + ) -> Result<(), GrubError> { + let _ = ( + esp_partition_number, + esp_starting_lba, + esp_ending_lba, + esp_unique_partition_guid, + to_slot, + from_slot, + ); + + self.run_first_available( + [INSTALL_BIN_PRIMARY, INSTALL_BIN_FALLBACK], + &[ + &format!("--target={INSTALL_TARGET}"), + &format!("--efi-directory={esp_mount_point}"), + &format!("--boot-directory={esp_mount_point}"), + &format!("--bootloader-id={INSTALL_BOOTLOADER_ID}"), + "--removable", + "--no-nvram", + ], + )?; + + let grub_cfg = Path::new(esp_mount_point).join("grub").join("grub.cfg"); + if let Some(parent) = grub_cfg.parent() { + create_dir_all(parent)?; + } + write(&grub_cfg, GRUB_CFG_CONTENTS)?; + + Ok(()) } } impl Grub { - fn run_first_available(&self, candidates: [&str; 2], entry_name: &str) -> Result<(), GrubError> { + fn run_first_available(&self, candidates: [&str; 2], args: &[&str]) -> Result<(), GrubError> { for candidate in candidates { - match Command::new(candidate).arg(entry_name).status() { + match Command::new(candidate).args(args).status() { Ok(status) if status.success() => return Ok(()), Ok(_) => return Err(GrubError::Unexpected), Err(error) if error.kind() == IoErrorKind::NotFound => continue, diff --git a/booters/grub/src/lib.rs b/booters/grub/src/lib.rs index 8dfcd1ea..9ead2b24 100644 --- a/booters/grub/src/lib.rs +++ b/booters/grub/src/lib.rs @@ -3,89 +3,128 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::str::from_utf8; - use upac_abi::BOOT_ABI_VERSION; -use upac_abi::boot::{Booter, CBootPluginRequest}; use upac_abi::error::ErrorKind; -use upac_abi::types::CBorrowed; +use upac_abi::request::{ + CBootPluginConfirmSuccsesBootRequest, CBootPluginInstallRequest, CBootPluginSetOneShotRequest, +}; + +use upac_types::request::{BootPluginConfirmSuccsesBootRequest, BootPluginInstallRequest, BootPluginSetOneShotRequest}; +use upac_types::traits::Booter; -use crate::backend::Grub; -use crate::error::GrubError; +use self::backend::Grub; +use self::error::GrubError; mod backend; mod error; include!(concat!(env!("OUT_DIR"), "/layout.rs")); -/// # Safety -/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::AbiVersionFn`. -#[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn abi_version() -> u32 { - BOOT_ABI_VERSION +macro_rules! write_error { + ($err_out:expr, $error:expr) => { + if !$err_out.is_null() { + unsafe { *$err_out = $error.into() }; + } + }; } /// # Safety -/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::ProbeFn`. +/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::BootPluginAbiVersionFn`. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn probe() -> i32 { - i32::from(Grub::probes()) +pub unsafe extern "C" fn boot_abi_version() -> u32 { + BOOT_ABI_VERSION } /// # Safety -/// `request`, if non-null, must point to a valid, initialized `CBootPluginRequest` for the +/// `request`, if non-null, must point to a valid, initialized `CBootPluginSetOneShotRequest` for the /// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn set_one_shot(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32 { +pub unsafe extern "C" fn set_one_shot(request: *const CBootPluginSetOneShotRequest, err_out: *mut ErrorKind) -> i32 { if request.is_null() { - write_error(err_out, GrubError::InvalidRequest); + write_error!(err_out, GrubError::InvalidRequest); + return -1; } - let result = entry_name_from_request(unsafe { &*request }) - .and_then(|entry_name| Grub::new().and_then(|mut grub| grub.set_one_shot(&entry_name))); + let result = BootPluginSetOneShotRequest::try_from(unsafe { &*request }) + .map_err(|_| GrubError::InvalidRequest) + .and_then(|request| Grub::new().and_then(|mut grub| grub.set_one_shot(&request.entry_name))); match result { Ok(()) => 0, + Err(error) => { - write_error(err_out, error); + write_error!(err_out, error); + -1 } } } /// # Safety -/// `request`, if non-null, must point to a valid, initialized `CBootPluginRequest` for the +/// `request`, if non-null, must point to a valid, initialized `CBootPluginConfirmSuccsesBootRequest` for the /// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn confirm_boot(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32 { +pub unsafe extern "C" fn confirm_boot( + request: *const CBootPluginConfirmSuccsesBootRequest, err_out: *mut ErrorKind, +) -> i32 { if request.is_null() { - write_error(err_out, GrubError::InvalidRequest); + write_error!(err_out, GrubError::InvalidRequest); + return -1; } - let result = entry_name_from_request(unsafe { &*request }) - .and_then(|entry_name| Grub::new().and_then(|mut grub| grub.confirm_boot(&entry_name))); + let result = BootPluginConfirmSuccsesBootRequest::try_from(unsafe { &*request }) + .map_err(|_| GrubError::InvalidRequest) + .and_then(|request| { + Grub::new().and_then(|mut grub| grub.confirm_boot(&request.entry_name, &request.esp_mount_point)) + }); match result { Ok(()) => 0, + Err(error) => { - write_error(err_out, error); + write_error!(err_out, error); + -1 } } } -fn entry_name_from_request(request: &CBootPluginRequest) -> Result { - let bytes = unsafe { request.entry_name.as_borrowed() }; +/// # Safety +/// `request`, if non-null, must point to a valid, initialized `CBootPluginInstallRequest` for the +/// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. +#[cfg_attr(feature = "cdylib", unsafe(no_mangle))] +pub unsafe extern "C" fn install(request: *const CBootPluginInstallRequest, err_out: *mut ErrorKind) -> i32 { + if request.is_null() { + write_error!(err_out, GrubError::InvalidRequest); - from_utf8(bytes) - .map(str::to_owned) + return -1; + } + + let result = BootPluginInstallRequest::try_from(unsafe { &*request }) .map_err(|_| GrubError::InvalidRequest) -} + .and_then(|request| { + Grub::new().and_then(|mut grub| { + grub.install( + &request.esp_mount_point, + request.esp_partition_number, + request.esp_starting_lba, + request.esp_ending_lba, + request.esp_unique_partition_guid, + &request.to_slot, + &request.from_slot, + ) + }) + }); -fn write_error(err_out: *mut ErrorKind, error: GrubError) { - if !err_out.is_null() { - unsafe { *err_out = error.into() }; + match result { + Ok(()) => 0, + + Err(error) => { + write_error!(err_out, error); + + -1 + } } } diff --git a/booters/refind/Cargo.toml b/booters/refind/Cargo.toml index 77204ac9..571cc6b9 100644 --- a/booters/refind/Cargo.toml +++ b/booters/refind/Cargo.toml @@ -29,6 +29,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] upac-abi = { workspace = true } +upac-types = { workspace = true } nix = { workspace = true, features = ["ioctl"] } diff --git a/booters/refind/build.rs b/booters/refind/build.rs index 23be7f05..e0d9eec8 100644 --- a/booters/refind/build.rs +++ b/booters/refind/build.rs @@ -20,27 +20,33 @@ fn main() -> Result<(), Box> { let config: Value = from_str(&raw)?; let mut generated = String::new(); + generated.push_str(&generate_section(&config, "boot")?); + generated.push_str(&generate_section(&config, "refind")?); - let sections = config.as_table().ok_or("booter.toml: root must be a table")?; - for (section, entries) in sections { - generated.push_str(&format!("pub mod {section} {{\n")); + let out = Path::new(&var("OUT_DIR")?).join("layout.rs"); + write(out, generated)?; + + Ok(()) +} - let entries = entries - .as_table() - .ok_or_else(|| format!("booter.toml: [{section}] must be a table"))?; - for (key, value) in entries { - let value = value - .as_str() - .ok_or_else(|| format!("booter.toml: {section}.{key} must be a string"))?; +fn generate_section(config: &Value, section: &str) -> Result> { + let entries = config + .get(section) + .and_then(Value::as_table) + .ok_or_else(|| format!("booter.toml: [{section}] must be a table"))?; - generated.push_str(&format!(" pub const {}: &str = {value:?};\n", key.to_uppercase())); - } + let mut generated = String::new(); + generated.push_str(&format!("pub mod {section} {{\n")); + + for (key, value) in entries { + let value = value + .as_str() + .ok_or_else(|| format!("booter.toml: {section}.{key} must be a string"))?; - generated.push_str("}\n"); + generated.push_str(&format!(" pub const {}: &str = {value:?};\n", key.to_uppercase())); } - let out = Path::new(&var("OUT_DIR")?).join("layout.rs"); - write(out, generated)?; + generated.push_str("}\n"); - Ok(()) + Ok(generated) } diff --git a/booters/refind/src/backend.rs b/booters/refind/src/backend.rs index 68745535..d1abd87e 100644 --- a/booters/refind/src/backend.rs +++ b/booters/refind/src/backend.rs @@ -16,17 +16,25 @@ use uuid::Uuid; use nix::{ioctl_read, ioctl_write_ptr}; -use upac_abi::boot::Booter; +use upac_types::traits::Booter; -use crate::boot::EFIVARFS_PATH; -use crate::error::RefindError; -use crate::refind::{PREVIOUS_BOOT_GUID, PREVIOUS_BOOT_VAR}; +use super::boot::EFIVARFS_PATH; +use super::error::RefindError; +use super::refind::{PREVIOUS_BOOT_GUID, PREVIOUS_BOOT_VAR}; const FS_IMMUTABLE_FL: c_long = 0x0000_0010; ioctl_read!(fs_ioc_getflags, b'f', 1, c_long); ioctl_write_ptr!(fs_ioc_setflags, b'f', 2, c_long); +macro_rules! encode_utf16_null { + ($value:expr) => {{ + let mut bytes: Vec = $value.encode_utf16().flat_map(u16::to_le_bytes).collect(); + bytes.extend_from_slice(&[0x00, 0x00]); + bytes + }}; +} + pub struct Refind { manager: Box, } @@ -40,26 +48,32 @@ impl Booter for Refind { }) } - fn probes() -> bool { - let Ok(manager) = catch_unwind(AssertUnwindSafe(efivar::system)) else { - return false; - }; - let Ok(guid) = Uuid::from_str(PREVIOUS_BOOT_GUID) else { - return false; - }; - - manager - .exists(&Variable::new_with_vendor(PREVIOUS_BOOT_VAR, guid)) - .unwrap_or(false) - } - fn set_one_shot(&mut self, entry_name: &str) -> Result<(), RefindError> { self.write_previous_boot(entry_name) } - fn confirm_boot(&mut self, entry_name: &str) -> Result<(), RefindError> { + fn confirm_boot(&mut self, entry_name: &str, esp_mount_point: &str) -> Result<(), RefindError> { + let _ = esp_mount_point; + self.write_previous_boot(entry_name) } + + fn install( + &mut self, esp_mount_point: &str, esp_partition_number: u32, esp_starting_lba: u64, esp_ending_lba: u64, + esp_unique_partition_guid: [u8; 16], to_slot: &str, from_slot: &str, + ) -> Result<(), RefindError> { + let _ = ( + esp_mount_point, + esp_partition_number, + esp_starting_lba, + esp_ending_lba, + esp_unique_partition_guid, + to_slot, + from_slot, + ); + + Ok(()) + } } impl Refind { @@ -70,7 +84,7 @@ impl Refind { Self::clear_immutable(&variable); self.manager - .write(&variable, VariableFlags::default(), &encode_utf16_null(entry_name))?; + .write(&variable, VariableFlags::default(), &encode_utf16_null!(entry_name))?; Ok(()) } @@ -95,10 +109,3 @@ impl Refind { } } } - -fn encode_utf16_null(value: &str) -> Vec { - let mut bytes: Vec = value.encode_utf16().flat_map(u16::to_le_bytes).collect(); - bytes.extend_from_slice(&[0x00, 0x00]); - - bytes -} diff --git a/booters/refind/src/lib.rs b/booters/refind/src/lib.rs index 26a62fed..7bd4f6a7 100644 --- a/booters/refind/src/lib.rs +++ b/booters/refind/src/lib.rs @@ -3,89 +3,98 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::str::from_utf8; - use upac_abi::BOOT_ABI_VERSION; -use upac_abi::boot::{Booter, CBootPluginRequest}; use upac_abi::error::ErrorKind; -use upac_abi::types::CBorrowed; +use upac_abi::request::{ + CBootPluginConfirmSuccsesBootRequest, CBootPluginInstallRequest, CBootPluginSetOneShotRequest, +}; + +use upac_types::request::{BootPluginConfirmSuccsesBootRequest, BootPluginSetOneShotRequest}; +use upac_types::traits::Booter; -use crate::backend::Refind; -use crate::error::RefindError; +use self::backend::Refind; +use self::error::RefindError; mod backend; mod error; include!(concat!(env!("OUT_DIR"), "/layout.rs")); -/// # Safety -/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::AbiVersionFn`. -#[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn abi_version() -> u32 { - BOOT_ABI_VERSION +macro_rules! write_error { + ($err_out:expr, $error:expr) => { + if !$err_out.is_null() { + unsafe { *$err_out = $error.into() }; + } + }; } /// # Safety -/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::ProbeFn`. +/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::BootPluginAbiVersionFn`. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn probe() -> i32 { - i32::from(Refind::probes()) +pub unsafe extern "C" fn boot_abi_version() -> u32 { + BOOT_ABI_VERSION } /// # Safety -/// `request`, if non-null, must point to a valid, initialized `CBootPluginRequest` for the +/// `request`, if non-null, must point to a valid, initialized `CBootPluginSetOneShotRequest` for the /// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn set_one_shot(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32 { +pub unsafe extern "C" fn set_one_shot(request: *const CBootPluginSetOneShotRequest, err_out: *mut ErrorKind) -> i32 { if request.is_null() { - write_error(err_out, RefindError::InvalidRequest); + write_error!(err_out, RefindError::InvalidRequest); + return -1; } - let result = entry_name_from_request(unsafe { &*request }) - .and_then(|entry_name| Refind::new().and_then(|mut refind| refind.set_one_shot(&entry_name))); + let result = BootPluginSetOneShotRequest::try_from(unsafe { &*request }) + .map_err(|_| RefindError::InvalidRequest) + .and_then(|request| Refind::new().and_then(|mut refind| refind.set_one_shot(&request.entry_name))); match result { Ok(()) => 0, + Err(error) => { - write_error(err_out, error); + write_error!(err_out, error); + -1 } } } /// # Safety -/// `request`, if non-null, must point to a valid, initialized `CBootPluginRequest` for the +/// `request`, if non-null, must point to a valid, initialized `CBootPluginConfirmSuccsesBootRequest` for the /// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn confirm_boot(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32 { +pub unsafe extern "C" fn confirm_boot( + request: *const CBootPluginConfirmSuccsesBootRequest, err_out: *mut ErrorKind, +) -> i32 { if request.is_null() { - write_error(err_out, RefindError::InvalidRequest); + write_error!(err_out, RefindError::InvalidRequest); + return -1; } - let result = entry_name_from_request(unsafe { &*request }) - .and_then(|entry_name| Refind::new().and_then(|mut refind| refind.confirm_boot(&entry_name))); + let result = BootPluginConfirmSuccsesBootRequest::try_from(unsafe { &*request }) + .map_err(|_| RefindError::InvalidRequest) + .and_then(|request| { + Refind::new().and_then(|mut refind| refind.confirm_boot(&request.entry_name, &request.esp_mount_point)) + }); match result { Ok(()) => 0, + Err(error) => { - write_error(err_out, error); + write_error!(err_out, error); + -1 } } } -fn entry_name_from_request(request: &CBootPluginRequest) -> Result { - let bytes = unsafe { request.entry_name.as_borrowed() }; - - from_utf8(bytes) - .map(str::to_owned) - .map_err(|_| RefindError::InvalidRequest) -} - -fn write_error(err_out: *mut ErrorKind, error: RefindError) { - if !err_out.is_null() { - unsafe { *err_out = error.into() }; - } +/// # Safety +/// Touches no pointers — rEFInd has nothing to install onto a pre-existing ESP, always succeeds +/// (its binary is copied from the source package tree via `esp_loader_source` instead). +#[cfg_attr(feature = "cdylib", unsafe(no_mangle))] +pub unsafe extern "C" fn install(_request: *const CBootPluginInstallRequest, _err_out: *mut ErrorKind) -> i32 { + 0 } diff --git a/booters/systemd-boot/Cargo.toml b/booters/systemd-boot/Cargo.toml index d2e1514f..1dd6f9ef 100644 --- a/booters/systemd-boot/Cargo.toml +++ b/booters/systemd-boot/Cargo.toml @@ -29,6 +29,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] upac-abi = { workspace = true } +upac-types = { workspace = true } nix = { workspace = true, features = ["ioctl"] } efivar = { workspace = true } diff --git a/booters/systemd-boot/build.rs b/booters/systemd-boot/build.rs index 23be7f05..62c06f36 100644 --- a/booters/systemd-boot/build.rs +++ b/booters/systemd-boot/build.rs @@ -19,26 +19,25 @@ fn main() -> Result<(), Box> { let raw = read_to_string(&source)?; let config: Value = from_str(&raw)?; - let mut generated = String::new(); - - let sections = config.as_table().ok_or("booter.toml: root must be a table")?; - for (section, entries) in sections { - generated.push_str(&format!("pub mod {section} {{\n")); + let section = "boot"; + let entries = config + .get(section) + .and_then(Value::as_table) + .ok_or_else(|| format!("booter.toml: [{section}] must be a table"))?; - let entries = entries - .as_table() - .ok_or_else(|| format!("booter.toml: [{section}] must be a table"))?; - for (key, value) in entries { - let value = value - .as_str() - .ok_or_else(|| format!("booter.toml: {section}.{key} must be a string"))?; + let mut generated = String::new(); + generated.push_str(&format!("pub mod {section} {{\n")); - generated.push_str(&format!(" pub const {}: &str = {value:?};\n", key.to_uppercase())); - } + for (key, value) in entries { + let value = value + .as_str() + .ok_or_else(|| format!("booter.toml: {section}.{key} must be a string"))?; - generated.push_str("}\n"); + generated.push_str(&format!(" pub const {}: &str = {value:?};\n", key.to_uppercase())); } + generated.push_str("}\n"); + let out = Path::new(&var("OUT_DIR")?).join("layout.rs"); write(out, generated)?; diff --git a/booters/systemd-boot/src/backend.rs b/booters/systemd-boot/src/backend.rs index 39b9fab4..56ec0938 100644 --- a/booters/systemd-boot/src/backend.rs +++ b/booters/systemd-boot/src/backend.rs @@ -16,62 +16,74 @@ use nix::{ioctl_read, ioctl_write_ptr}; use uuid::Uuid; -use upac_abi::boot::Booter; +use upac_types::traits::Booter; -use crate::boot::{ - EFIVARFS_PATH, LOADER_ENTRY_DEFAULT_VAR, LOADER_ENTRY_ONE_SHOT_VAR, LOADER_INFO_VAR, SD_BOOT_LOADER_GUID, -}; -use crate::error::BlsError; +use super::boot::{EFIVARFS_PATH, LOADER_ENTRY_DEFAULT_VAR, LOADER_ENTRY_ONE_SHOT_VAR, SD_BOOT_LOADER_GUID}; +use super::error::SystemdBootError; const FS_IMMUTABLE_FL: c_long = 0x0000_0010; ioctl_read!(fs_ioc_getflags, b'f', 1, c_long); ioctl_write_ptr!(fs_ioc_setflags, b'f', 2, c_long); -pub struct Bls { +macro_rules! encode_utf16_null { + ($value:expr) => {{ + let mut bytes: Vec = $value.encode_utf16().flat_map(u16::to_le_bytes).collect(); + bytes.extend_from_slice(&[0x00, 0x00]); + bytes + }}; +} + +pub struct SystemdBoot { manager: Box, } -impl Booter for Bls { - type Error = BlsError; +impl Booter for SystemdBoot { + type Error = SystemdBootError; - fn new() -> Result { + fn new() -> Result { Ok(Self { manager: catch_unwind(AssertUnwindSafe(efivar::system))?, }) } - fn probes() -> bool { - let Ok(manager) = catch_unwind(AssertUnwindSafe(efivar::system)) else { - return false; - }; - let Ok(guid) = Uuid::from_str(SD_BOOT_LOADER_GUID) else { - return false; - }; - - manager - .exists(&Variable::new_with_vendor(LOADER_INFO_VAR, guid)) - .unwrap_or(false) - } - - fn set_one_shot(&mut self, entry_name: &str) -> Result<(), BlsError> { + fn set_one_shot(&mut self, entry_name: &str) -> Result<(), SystemdBootError> { self.write_loader_variable(LOADER_ENTRY_ONE_SHOT_VAR, entry_name) } - fn confirm_boot(&mut self, entry_name: &str) -> Result<(), BlsError> { + fn confirm_boot(&mut self, entry_name: &str, esp_mount_point: &str) -> Result<(), SystemdBootError> { + let _ = esp_mount_point; + self.write_loader_variable(LOADER_ENTRY_DEFAULT_VAR, entry_name) } + + fn install( + &mut self, esp_mount_point: &str, esp_partition_number: u32, esp_starting_lba: u64, esp_ending_lba: u64, + esp_unique_partition_guid: [u8; 16], to_slot: &str, from_slot: &str, + ) -> Result<(), SystemdBootError> { + let _ = ( + esp_mount_point, + esp_partition_number, + esp_starting_lba, + esp_ending_lba, + esp_unique_partition_guid, + to_slot, + from_slot, + ); + + Ok(()) + } } -impl Bls { - fn write_loader_variable(&mut self, name: &str, entry_name: &str) -> Result<(), BlsError> { +impl SystemdBoot { + fn write_loader_variable(&mut self, name: &str, entry_name: &str) -> Result<(), SystemdBootError> { let guid = Uuid::from_str(SD_BOOT_LOADER_GUID)?; let variable = Variable::new_with_vendor(name, guid); Self::clear_immutable(&variable); self.manager - .write(&variable, VariableFlags::default(), &encode_utf16_null(entry_name))?; + .write(&variable, VariableFlags::default(), &encode_utf16_null!(entry_name))?; Ok(()) } @@ -96,10 +108,3 @@ impl Bls { } } } - -fn encode_utf16_null(value: &str) -> Vec { - let mut bytes: Vec = value.encode_utf16().flat_map(u16::to_le_bytes).collect(); - bytes.extend_from_slice(&[0x00, 0x00]); - - bytes -} diff --git a/booters/systemd-boot/src/error.rs b/booters/systemd-boot/src/error.rs index ace8c742..72804775 100644 --- a/booters/systemd-boot/src/error.rs +++ b/booters/systemd-boot/src/error.rs @@ -12,41 +12,41 @@ use uuid::Error as UuidError; use upac_abi::error::ErrorKind; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BlsError { +pub enum SystemdBootError { EfiUnavailable, PermissionDenied, InvalidRequest, Unexpected, } -impl From for BlsError { +impl From for SystemdBootError { fn from(error: EfivarError) -> Self { match error { - EfivarError::PermissionDenied { .. } => BlsError::PermissionDenied, - _ => BlsError::Unexpected, + EfivarError::PermissionDenied { .. } => SystemdBootError::PermissionDenied, + _ => SystemdBootError::Unexpected, } } } -impl From for BlsError { +impl From for SystemdBootError { fn from(_: UuidError) -> Self { - BlsError::Unexpected + SystemdBootError::Unexpected } } -impl From> for BlsError { +impl From> for SystemdBootError { fn from(_: Box) -> Self { - BlsError::EfiUnavailable + SystemdBootError::EfiUnavailable } } -impl From for ErrorKind { - fn from(error: BlsError) -> Self { +impl From for ErrorKind { + fn from(error: SystemdBootError) -> Self { match error { - BlsError::EfiUnavailable => ErrorKind::NotInitialized, - BlsError::PermissionDenied => ErrorKind::PermissionDenied, - BlsError::InvalidRequest => ErrorKind::InvalidEntry, - BlsError::Unexpected => ErrorKind::Unexpected, + SystemdBootError::EfiUnavailable => ErrorKind::NotInitialized, + SystemdBootError::PermissionDenied => ErrorKind::PermissionDenied, + SystemdBootError::InvalidRequest => ErrorKind::InvalidEntry, + SystemdBootError::Unexpected => ErrorKind::Unexpected, } } } diff --git a/booters/systemd-boot/src/lib.rs b/booters/systemd-boot/src/lib.rs index b11082c6..09b3f616 100644 --- a/booters/systemd-boot/src/lib.rs +++ b/booters/systemd-boot/src/lib.rs @@ -3,89 +3,98 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::str::from_utf8; - use upac_abi::BOOT_ABI_VERSION; -use upac_abi::boot::{Booter, CBootPluginRequest}; use upac_abi::error::ErrorKind; -use upac_abi::types::CBorrowed; +use upac_abi::request::{ + CBootPluginConfirmSuccsesBootRequest, CBootPluginInstallRequest, CBootPluginSetOneShotRequest, +}; + +use upac_types::request::{BootPluginConfirmSuccsesBootRequest, BootPluginSetOneShotRequest}; +use upac_types::traits::Booter; -use crate::backend::Bls; -use crate::error::BlsError; +use self::backend::SystemdBoot; +use self::error::SystemdBootError; mod backend; mod error; include!(concat!(env!("OUT_DIR"), "/layout.rs")); -/// # Safety -/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::AbiVersionFn`. -#[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn abi_version() -> u32 { - BOOT_ABI_VERSION +macro_rules! write_error { + ($err_out:expr, $error:expr) => { + if !$err_out.is_null() { + unsafe { *$err_out = $error.into() }; + } + }; } /// # Safety -/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::ProbeFn`. +/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::AbiVersionFn`. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn probe() -> i32 { - i32::from(Bls::probes()) +pub unsafe extern "C" fn boot_abi_version() -> u32 { + BOOT_ABI_VERSION } /// # Safety -/// `request`, if non-null, must point to a valid, initialized `CBootPluginRequest` for the +/// `request`, if non-null, must point to a valid, initialized `CBootPluginSetOneShotRequest` for the /// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn set_one_shot(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32 { +pub unsafe extern "C" fn set_one_shot(request: *const CBootPluginSetOneShotRequest, err_out: *mut ErrorKind) -> i32 { if request.is_null() { - write_error(err_out, BlsError::InvalidRequest); + write_error!(err_out, SystemdBootError::InvalidRequest); return -1; } - let result = entry_name_from_request(unsafe { &*request }) - .and_then(|entry_name| Bls::new().and_then(|mut bls| bls.set_one_shot(&entry_name))); + let result = BootPluginSetOneShotRequest::try_from(unsafe { &*request }) + .map_err(|_| SystemdBootError::InvalidRequest) + .and_then(|request| SystemdBoot::new().and_then(|mut systemd| systemd.set_one_shot(&request.entry_name))); match result { Ok(()) => 0, + Err(error) => { - write_error(err_out, error); + write_error!(err_out, error); + -1 } } } /// # Safety -/// `request`, if non-null, must point to a valid, initialized `CBootPluginRequest` for the +/// `request`, if non-null, must point to a valid, initialized `CConfirmBootRequest` for the /// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn confirm_boot(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32 { +pub unsafe extern "C" fn confirm_boot( + request: *const CBootPluginConfirmSuccsesBootRequest, err_out: *mut ErrorKind, +) -> i32 { if request.is_null() { - write_error(err_out, BlsError::InvalidRequest); + write_error!(err_out, SystemdBootError::InvalidRequest); + return -1; } - let result = entry_name_from_request(unsafe { &*request }) - .and_then(|entry_name| Bls::new().and_then(|mut bls| bls.confirm_boot(&entry_name))); + let result = BootPluginConfirmSuccsesBootRequest::try_from(unsafe { &*request }) + .map_err(|_| SystemdBootError::InvalidRequest) + .and_then(|request| { + SystemdBoot::new() + .and_then(|mut systemd| systemd.confirm_boot(&request.entry_name, &request.esp_mount_point)) + }); match result { Ok(()) => 0, + Err(error) => { - write_error(err_out, error); + write_error!(err_out, error); + -1 } } } -fn entry_name_from_request(request: &CBootPluginRequest) -> Result { - let bytes = unsafe { request.entry_name.as_borrowed() }; - - from_utf8(bytes) - .map(str::to_owned) - .map_err(|_| BlsError::InvalidRequest) -} - -fn write_error(err_out: *mut ErrorKind, error: BlsError) { - if !err_out.is_null() { - unsafe { *err_out = error.into() }; - } +/// # Safety +/// Touches no pointers — systemd-boot has nothing to install onto a pre-existing ESP, always +/// succeeds (its binary is copied from the source package tree via `esp_loader_source` instead). +#[cfg_attr(feature = "cdylib", unsafe(no_mangle))] +pub unsafe extern "C" fn install(_request: *const CBootPluginInstallRequest, _err_out: *mut ErrorKind) -> i32 { + 0 } diff --git a/booters/uki/Cargo.toml b/booters/uki/Cargo.toml index dfbe34a8..1fd695db 100644 --- a/booters/uki/Cargo.toml +++ b/booters/uki/Cargo.toml @@ -29,6 +29,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] upac-abi = { workspace = true } +upac-types = { workspace = true } nix = { workspace = true, features = ["ioctl"] } efivar = { workspace = true } diff --git a/booters/uki/build.rs b/booters/uki/build.rs index 23be7f05..b65b8a71 100644 --- a/booters/uki/build.rs +++ b/booters/uki/build.rs @@ -20,27 +20,33 @@ fn main() -> Result<(), Box> { let config: Value = from_str(&raw)?; let mut generated = String::new(); + generated.push_str(&generate_section(&config, "boot")?); + generated.push_str(&generate_section(&config, "uki")?); - let sections = config.as_table().ok_or("booter.toml: root must be a table")?; - for (section, entries) in sections { - generated.push_str(&format!("pub mod {section} {{\n")); + let out = Path::new(&var("OUT_DIR")?).join("layout.rs"); + write(out, generated)?; + + Ok(()) +} - let entries = entries - .as_table() - .ok_or_else(|| format!("booter.toml: [{section}] must be a table"))?; - for (key, value) in entries { - let value = value - .as_str() - .ok_or_else(|| format!("booter.toml: {section}.{key} must be a string"))?; +fn generate_section(config: &Value, section: &str) -> Result> { + let entries = config + .get(section) + .and_then(Value::as_table) + .ok_or_else(|| format!("booter.toml: [{section}] must be a table"))?; - generated.push_str(&format!(" pub const {}: &str = {value:?};\n", key.to_uppercase())); - } + let mut generated = String::new(); + generated.push_str(&format!("pub mod {section} {{\n")); + + for (key, value) in entries { + let value = value + .as_str() + .ok_or_else(|| format!("booter.toml: {section}.{key} must be a string"))?; - generated.push_str("}\n"); + generated.push_str(&format!(" pub const {}: &str = {value:?};\n", key.to_uppercase())); } - let out = Path::new(&var("OUT_DIR")?).join("layout.rs"); - write(out, generated)?; + generated.push_str("}\n"); - Ok(()) + Ok(generated) } diff --git a/booters/uki/src/backend.rs b/booters/uki/src/backend.rs index 8b1262ef..b32b2f17 100644 --- a/booters/uki/src/backend.rs +++ b/booters/uki/src/backend.rs @@ -3,26 +3,27 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::fs::OpenOptions; +use std::fs::{OpenOptions, copy}; use std::os::fd::AsRawFd; use std::os::raw::c_long; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::path::Path; -use std::str::FromStr; use efivar::VarManager; +use efivar::boot::{ + BootEntry, BootEntryAttributes, BootVarName, EFIHardDrive, EFIHardDriveType, FilePath, FilePathList, +}; use efivar::efi::{Variable, VariableFlags}; use nix::{ioctl_read, ioctl_write_ptr}; use uuid::Uuid; -use upac_abi::boot::Booter; +use upac_types::traits::Booter; -use crate::boot::{BOOT_NEXT_VAR, BOOT_ORDER_VAR, EFI_SYSFS_PATH, EFIVARFS_PATH, LOADER_INFO_VAR, SD_BOOT_LOADER_GUID}; -use crate::error::UkiError; -use crate::grub::{GRUBENV_FALLBACK, GRUBENV_PRIMARY}; -use crate::refind::{PREVIOUS_BOOT_GUID, PREVIOUS_BOOT_VAR}; +use super::boot::{BOOT_NEXT_VAR, BOOT_ORDER_VAR, EFIVARFS_PATH}; +use super::error::UkiError; +use super::uki::{EFI_LINUX_DIR, EFI_LINUX_REAL_PATH, FROM_SLOT, TO_SLOT}; const FS_IMMUTABLE_FL: c_long = 0x0000_0010; @@ -42,22 +43,6 @@ impl Booter for Uki { }) } - fn probes() -> bool { - if !Path::new(EFI_SYSFS_PATH).exists() { - return false; - } - if Path::new(GRUBENV_PRIMARY).exists() || Path::new(GRUBENV_FALLBACK).exists() { - return false; - } - - let Ok(manager) = catch_unwind(AssertUnwindSafe(efivar::system)) else { - return false; - }; - - !efi_variable_exists(manager.as_ref(), LOADER_INFO_VAR, SD_BOOT_LOADER_GUID) - && !efi_variable_exists(manager.as_ref(), PREVIOUS_BOOT_VAR, PREVIOUS_BOOT_GUID) - } - fn set_one_shot(&mut self, entry_name: &str) -> Result<(), UkiError> { let id = self.find_boot_id(entry_name)?; @@ -70,7 +55,7 @@ impl Booter for Uki { Ok(()) } - fn confirm_boot(&mut self, entry_name: &str) -> Result<(), UkiError> { + fn confirm_boot(&mut self, entry_name: &str, esp_mount_point: &str) -> Result<(), UkiError> { let id = self.find_boot_id(entry_name)?; let mut order = self.manager.get_boot_order()?; @@ -80,20 +65,55 @@ impl Booter for Uki { Self::clear_immutable(&Variable::new(BOOT_ORDER_VAR)); self.manager.set_boot_order(order)?; + if entry_name == TO_SLOT { + let efi_linux = Path::new(esp_mount_point).join(EFI_LINUX_REAL_PATH); + let to_path = efi_linux.join(format!("{TO_SLOT}.efi")); + let from_path = efi_linux.join(format!("{FROM_SLOT}.efi")); + copy(&to_path, &from_path)?; + } + + Ok(()) + } + + fn install( + &mut self, esp_mount_point: &str, esp_partition_number: u32, esp_starting_lba: u64, esp_ending_lba: u64, + esp_unique_partition_guid: [u8; 16], to_slot: &str, from_slot: &str, + ) -> Result<(), UkiError> { + let _ = esp_mount_point; + + let partition_size = esp_ending_lba - esp_starting_lba + 1; + let partition_sig = Uuid::from_bytes_le(esp_unique_partition_guid); + + self.register_slot( + esp_partition_number, + esp_starting_lba, + partition_size, + partition_sig, + to_slot, + )?; + self.register_slot( + esp_partition_number, + esp_starting_lba, + partition_size, + partition_sig, + from_slot, + )?; + Ok(()) } } impl Uki { fn find_boot_id(&self, slot_filename: &str) -> Result { + let slot_file_name = format!("{}.efi", slot_filename.to_lowercase()); + for (entry, _var) in self.manager.get_boot_entries()? { let entry = entry?; - let matches = entry.entry.file_path_list.as_ref().is_some_and(|list| { - list.file_path - .path - .to_lowercase() - .ends_with(&slot_filename.to_lowercase()) - }); + let matches = entry + .entry + .file_path_list + .as_ref() + .is_some_and(|list| list.file_path.path.to_lowercase().ends_with(&slot_file_name)); if matches { return Ok(entry.id); @@ -103,6 +123,42 @@ impl Uki { Err(UkiError::EntryNotFound) } + fn register_slot( + &mut self, partition_number: u32, partition_start: u64, partition_size: u64, partition_sig: Uuid, + slot_filename: &str, + ) -> Result { + let id = self.free_boot_id()?; + + let entry = BootEntry { + attributes: BootEntryAttributes::LOAD_OPTION_ACTIVE, + description: slot_filename.to_owned(), + file_path_list: Some(FilePathList { + file_path: FilePath { + path: format!("{EFI_LINUX_DIR}{slot_filename}.efi"), + }, + hard_drive: EFIHardDrive { + partition_number, + partition_start, + partition_size, + partition_sig, + format: 0x02, + sig_type: EFIHardDriveType::Gpt, + }, + }), + optional_data: Vec::new(), + }; + + self.manager.add_boot_entry(id, entry)?; + + Ok(id) + } + + fn free_boot_id(&self) -> Result { + (0..u16::MAX) + .find(|id| !self.manager.exists(&Variable::new(&id.boot_var_name())).unwrap_or(true)) + .ok_or(UkiError::NoFreeBootId) + } + fn clear_immutable(variable: &Variable) { let Ok(file) = OpenOptions::new() .read(true) @@ -123,11 +179,3 @@ impl Uki { } } } - -fn efi_variable_exists(manager: &dyn VarManager, name: &str, guid: &str) -> bool { - let Ok(guid) = Uuid::from_str(guid) else { - return false; - }; - - manager.exists(&Variable::new_with_vendor(name, guid)).unwrap_or(false) -} diff --git a/booters/uki/src/error.rs b/booters/uki/src/error.rs index ea030a01..714a23da 100644 --- a/booters/uki/src/error.rs +++ b/booters/uki/src/error.rs @@ -4,6 +4,7 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::any::Any; +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; use efivar::Error as EfivarError; @@ -14,6 +15,7 @@ pub enum UkiError { EfiUnavailable, PermissionDenied, EntryNotFound, + NoFreeBootId, InvalidRequest, Unexpected, } @@ -27,6 +29,16 @@ impl From for UkiError { } } +impl From for UkiError { + fn from(error: IoError) -> Self { + match error.kind() { + IoErrorKind::NotFound => UkiError::EntryNotFound, + IoErrorKind::PermissionDenied => UkiError::PermissionDenied, + _ => UkiError::Unexpected, + } + } +} + impl From> for UkiError { fn from(_: Box) -> Self { UkiError::EfiUnavailable @@ -39,6 +51,7 @@ impl From for ErrorKind { UkiError::EfiUnavailable => ErrorKind::NotInitialized, UkiError::PermissionDenied => ErrorKind::PermissionDenied, UkiError::EntryNotFound => ErrorKind::NotFound, + UkiError::NoFreeBootId => ErrorKind::OutOfMemory, UkiError::InvalidRequest => ErrorKind::InvalidEntry, UkiError::Unexpected => ErrorKind::Unexpected, } diff --git a/booters/uki/src/lib.rs b/booters/uki/src/lib.rs index 1140ec7c..4dc80b16 100644 --- a/booters/uki/src/lib.rs +++ b/booters/uki/src/lib.rs @@ -3,89 +3,98 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::str::from_utf8; - use upac_abi::BOOT_ABI_VERSION; -use upac_abi::boot::{Booter, CBootPluginRequest}; use upac_abi::error::ErrorKind; -use upac_abi::types::CBorrowed; +use upac_abi::request::{ + CBootPluginConfirmSuccsesBootRequest, CBootPluginInstallRequest, CBootPluginSetOneShotRequest, +}; + +use upac_types::request::{BootPluginConfirmSuccsesBootRequest, BootPluginSetOneShotRequest}; +use upac_types::traits::Booter; -use crate::backend::Uki; -use crate::error::UkiError; +use self::backend::Uki; +use self::error::UkiError; mod backend; mod error; include!(concat!(env!("OUT_DIR"), "/layout.rs")); -/// # Safety -/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::AbiVersionFn`. -#[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn abi_version() -> u32 { - BOOT_ABI_VERSION +macro_rules! write_error { + ($err_out:expr, $error:expr) => { + if !$err_out.is_null() { + unsafe { *$err_out = $error.into() }; + } + }; } /// # Safety -/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::ProbeFn`. +/// Touches no pointers — `unsafe extern "C"` only to match `upac_abi::boot::AbiVersionFn`. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn probe() -> i32 { - i32::from(Uki::probes()) +pub unsafe extern "C" fn boot_abi_version() -> u32 { + BOOT_ABI_VERSION } /// # Safety -/// `request`, if non-null, must point to a valid, initialized `CBootPluginRequest` for the +/// `request`, if non-null, must point to a valid, initialized `CBootPluginSetOneShotRequest` for the /// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn set_one_shot(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32 { +pub unsafe extern "C" fn set_one_shot(request: *const CBootPluginSetOneShotRequest, err_out: *mut ErrorKind) -> i32 { if request.is_null() { - write_error(err_out, UkiError::InvalidRequest); + write_error!(err_out, UkiError::InvalidRequest); + return -1; } - let result = entry_name_from_request(unsafe { &*request }) - .and_then(|entry_name| Uki::new().and_then(|mut uki| uki.set_one_shot(&entry_name))); + let result = BootPluginSetOneShotRequest::try_from(unsafe { &*request }) + .map_err(|_| UkiError::InvalidRequest) + .and_then(|request| Uki::new().and_then(|mut uki| uki.set_one_shot(&request.entry_name))); match result { Ok(()) => 0, + Err(error) => { - write_error(err_out, error); + write_error!(err_out, error); + -1 } } } /// # Safety -/// `request`, if non-null, must point to a valid, initialized `CBootPluginRequest` for the +/// `request`, if non-null, must point to a valid, initialized `CBootPluginConfirmSuccsesBootRequest` for the /// duration of the call. `err_out`, if non-null, must point to writable `ErrorKind` storage. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn confirm_boot(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32 { +pub unsafe extern "C" fn confirm_boot( + request: *const CBootPluginConfirmSuccsesBootRequest, err_out: *mut ErrorKind, +) -> i32 { if request.is_null() { - write_error(err_out, UkiError::InvalidRequest); + write_error!(err_out, UkiError::InvalidRequest); + return -1; } - let result = entry_name_from_request(unsafe { &*request }) - .and_then(|entry_name| Uki::new().and_then(|mut uki| uki.confirm_boot(&entry_name))); + let result = BootPluginConfirmSuccsesBootRequest::try_from(unsafe { &*request }) + .map_err(|_| UkiError::InvalidRequest) + .and_then(|request| { + Uki::new().and_then(|mut uki| uki.confirm_boot(&request.entry_name, &request.esp_mount_point)) + }); match result { Ok(()) => 0, + Err(error) => { - write_error(err_out, error); + write_error!(err_out, error); + -1 } } } -fn entry_name_from_request(request: &CBootPluginRequest) -> Result { - let bytes = unsafe { request.entry_name.as_borrowed() }; - - from_utf8(bytes) - .map(str::to_owned) - .map_err(|_| UkiError::InvalidRequest) -} - -fn write_error(err_out: *mut ErrorKind, error: UkiError) { - if !err_out.is_null() { - unsafe { *err_out = error.into() }; - } +/// # Safety +/// Touches no pointers — uki has nothing to install onto a pre-existing ESP, always succeeds +/// (its binary is copied from the source package tree via `esp_loader_source` instead). +#[cfg_attr(feature = "cdylib", unsafe(no_mangle))] +pub unsafe extern "C" fn install(_request: *const CBootPluginInstallRequest, _err_out: *mut ErrorKind) -> i32 { + 0 } diff --git a/decoders/alpm/Cargo.toml b/decoders/alpm/Cargo.toml index fcd01bcb..38cdab9e 100644 --- a/decoders/alpm/Cargo.toml +++ b/decoders/alpm/Cargo.toml @@ -32,7 +32,6 @@ upac-abi = { workspace = true } upac-types = { workspace = true } flate2 = { workspace = true } -sha2 = { workspace = true } tar = { workspace = true } xz2 = { workspace = true } zstd = { workspace = true } diff --git a/decoders/alpm/src/extract.rs b/decoders/alpm/src/extract.rs index e755930d..6c937975 100644 --- a/decoders/alpm/src/extract.rs +++ b/decoders/alpm/src/extract.rs @@ -11,11 +11,12 @@ use tar::Archive; use xz2::read::XzDecoder; use zstd::stream::read::Decoder as ZstdDecoder; -use upac_abi::decoder::DecodeError; use upac_abi::hook::CancelToken; + use upac_types::decoder::read_to_string; +use upac_types::error::DecodeError; -use crate::alpm::{BUILDINFO_ENTRY, CHANGELOG_ENTRY, INSTALL_ENTRY, MTREE_ENTRY, PKGINFO_ENTRY}; +use super::alpm::{BUILDINFO_ENTRY, CHANGELOG_ENTRY, INSTALL_ENTRY, MTREE_ENTRY, PKGINFO_ENTRY}; const JUNK_ENTRIES: [&str; 3] = [BUILDINFO_ENTRY, MTREE_ENTRY, CHANGELOG_ENTRY]; diff --git a/decoders/alpm/src/lib.rs b/decoders/alpm/src/lib.rs index 2a0385c9..27bff897 100644 --- a/decoders/alpm/src/lib.rs +++ b/decoders/alpm/src/lib.rs @@ -5,13 +5,14 @@ use std::str::from_utf8; -use upac_abi::ABI_VERSION; -use upac_abi::decoder::{CDecodeRequest, CDecodeResponse, CDependency, DecodeError}; +use upac_abi::DECODER_ABI_VERSION; use upac_abi::memory::{free_cslice, free_cvec_owning}; -use upac_abi::package::CPackageMeta; -use upac_abi::types::COwned; -use upac_abi::types::{CSlice, CVec}; -use upac_types::decoder::{DecodeMeta, DecodedMeta}; +use upac_abi::request::CDecodeRequest; +use upac_abi::response::CDecodeResponse; + +use upac_types::decoder::{build_decode_response, verify}; +use upac_types::error::DecodeError; +use upac_types::traits::DecodeMeta; use crate::extract::ExtractedMetadata; use crate::pkginfo::PkgInfo; @@ -20,15 +21,14 @@ pub mod pkginfo; pub mod triggers; mod extract; -mod verify; include!(concat!(env!("OUT_DIR"), "/layout.rs")); /// # Safety /// Touches no pointers. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] -pub unsafe extern "C" fn abi_version() -> u32 { - ABI_VERSION +pub unsafe extern "C" fn decode_abi_version() -> u32 { + DECODER_ABI_VERSION } /// # Safety @@ -78,34 +78,16 @@ fn decode_package(request: &CDecodeRequest) -> Result) -> CDecodeResponse { - let DecodedMeta { meta, dependencies } = decoded; - - let dependencies = dependencies.into_iter().map(CDependency::from).collect::>(); - - let declarative_triggers = declarative_triggers - .into_iter() - .map(|trigger| CSlice::from_owned(trigger.into_bytes())) - .collect::>(); - - CDecodeResponse { - struct_size: size_of::(), - - meta: CPackageMeta::from(meta), - - dependencies: CVec::from_owned(dependencies), - declarative_triggers: CVec::from_owned(declarative_triggers), - - free: free_decode_response, - } + Ok(build_decode_response( + decoded, + declarative_triggers, + free_decode_response, + )) } diff --git a/decoders/alpm/src/pkginfo.rs b/decoders/alpm/src/pkginfo.rs index 5128ccb5..f79bbf9b 100644 --- a/decoders/alpm/src/pkginfo.rs +++ b/decoders/alpm/src/pkginfo.rs @@ -5,13 +5,14 @@ use std::collections::HashMap; -use upac_abi::decoder::{ - CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS, DecodeError, parse_constraint_prefix, -}; -use upac_types::decoder::{DecodeMeta, DecodedMeta}; -use upac_types::{Dependency, PackageMeta, Version}; +use upac_abi::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS}; + +use upac_types::decoder::parse_constraint_prefix; +use upac_types::error::DecodeError; +use upac_types::package::{DecodedPackageMeta, PackageDependency, PackageMeta, Version}; +use upac_types::traits::DecodeMeta; -use crate::alpm::{ +use super::alpm::{ PKGINFO_ARCH_KEY, PKGINFO_DEPEND_KEY, PKGINFO_DESCRIPTION_KEY, PKGINFO_EPOCH_KEY, PKGINFO_LICENSE_KEY, PKGINFO_MAINTAINER_KEY, PKGINFO_NAME_KEY, PKGINFO_RELEASE_KEY, PKGINFO_SIZE_KEY, PKGINFO_URL_KEY, PKGINFO_VERSION_KEY, @@ -43,7 +44,7 @@ const OPERATORS: [(&[u8], u8); 5] = [ pub struct PkgInfo<'a>(pub &'a str); impl DecodeMeta for PkgInfo<'_> { - fn decode(&self, sha256: [u8; 32]) -> Result { + fn decode(&self, sha256: [u8; 32]) -> Result { let (mut fields, dependencies) = self.parse_fields(); let name = required_field!(fields, PKGINFO_NAME_KEY); @@ -73,12 +74,12 @@ impl DecodeMeta for PkgInfo<'_> { installed_size, }; - Ok(DecodedMeta { meta, dependencies }) + Ok(DecodedPackageMeta { meta, dependencies }) } } impl PkgInfo<'_> { - fn parse_fields(&self) -> (HashMap<&str, String>, Vec) { + fn parse_fields(&self) -> (HashMap<&str, String>, Vec) { let mut fields: HashMap<&str, String> = HashMap::new(); let mut dependencies = Vec::new(); @@ -102,7 +103,7 @@ impl PkgInfo<'_> { (fields, dependencies) } - fn parse_dependency(value: &str) -> Dependency { + fn parse_dependency(value: &str) -> PackageDependency { let bytes = value.as_bytes(); for index in 0..bytes.len() { @@ -110,14 +111,14 @@ impl PkgInfo<'_> { continue; }; - return Dependency { + return PackageDependency { name: value[..index].to_owned(), constraint, version: Version::parse(&value[index + operator_len..]), }; } - Dependency { + PackageDependency { name: value.to_owned(), constraint: CONSTRAINT_ANY, version: Version::default(), diff --git a/decoders/alpm/src/triggers.rs b/decoders/alpm/src/triggers.rs index 82e4a9cf..abaf1315 100644 --- a/decoders/alpm/src/triggers.rs +++ b/decoders/alpm/src/triggers.rs @@ -3,9 +3,9 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_types::DecoderTrigger; +use upac_types::decoder::DecoderTrigger; -use crate::alpm::{POST_INSTALL_FN, POST_REMOVE_FN, POST_UPGRADE_FN, PRE_INSTALL_FN, PRE_REMOVE_FN, PRE_UPGRADE_FN}; +use super::alpm::{POST_INSTALL_FN, POST_REMOVE_FN, POST_UPGRADE_FN, PRE_INSTALL_FN, PRE_REMOVE_FN, PRE_UPGRADE_FN}; pub fn scan(content: &str) -> Vec { DecoderTrigger::ALL diff --git a/decoders/alpm/src/verify.rs b/decoders/alpm/src/verify.rs deleted file mode 100644 index aae6636c..00000000 --- a/decoders/alpm/src/verify.rs +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs::File; -use std::io::{BufReader, Read}; - -use sha2::{Digest, Sha256}; - -use upac_abi::decoder::DecodeError; -use upac_abi::hook::CancelToken; - -const READ_CHUNK_SIZE: usize = 65536; - -pub fn verify(package_path: &str, expected_checksum: [u8; 32], cancel: &CancelToken) -> Result<(), DecodeError> { - let file = File::open(package_path)?; - let mut reader = BufReader::new(file); - - let mut hasher = Sha256::new(); - let mut buffer = [0u8; READ_CHUNK_SIZE]; - - loop { - if cancel.is_cancelled() { - return Err(DecodeError::Cancelled); - } - - let bytes_read = reader.read(&mut buffer)?; - if bytes_read == 0 { - break; - } - - hasher.update(&buffer[..bytes_read]); - } - - if hasher.finalize().as_slice() != expected_checksum.as_slice() { - return Err(DecodeError::ChecksumMismatch); - } - - Ok(()) -} diff --git a/decoders/alpm/tests/pkginfo.rs b/decoders/alpm/tests/pkginfo.rs index bbc0dae2..0d5a54f2 100644 --- a/decoders/alpm/tests/pkginfo.rs +++ b/decoders/alpm/tests/pkginfo.rs @@ -3,9 +3,12 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::decoder::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS, DecodeError}; +use upac_abi::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS}; + use upac_decoder_alpm::pkginfo::PkgInfo; -use upac_types::decoder::DecodeMeta; + +use upac_types::error::DecodeError; +use upac_types::traits::DecodeMeta; const CHECKSUM: [u8; 32] = [7; 32]; diff --git a/decoders/deb/src/lib.rs b/decoders/deb/src/lib.rs index 7be841ba..6c6c29ec 100644 --- a/decoders/deb/src/lib.rs +++ b/decoders/deb/src/lib.rs @@ -5,7 +5,7 @@ use std::str::from_utf8; -use upac_abi::ABI_VERSION; +use upac_abi::DECODER_ABI_VERSION; use upac_abi::decoder::{CDecodeRequest, CDecodeResponse, CDependency, DecodeError}; use upac_abi::memory::{free_cslice, free_cvec_owning}; use upac_abi::package::CPackageMeta; @@ -28,7 +28,7 @@ include!(concat!(env!("OUT_DIR"), "/layout.rs")); /// Touches no pointers. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] pub unsafe extern "C" fn abi_version() -> u32 { - ABI_VERSION + DECODER_ABI_VERSION } /// # Safety diff --git a/decoders/rpm/src/lib.rs b/decoders/rpm/src/lib.rs index 6cd8b29e..10deca54 100644 --- a/decoders/rpm/src/lib.rs +++ b/decoders/rpm/src/lib.rs @@ -6,7 +6,7 @@ use std::fs::File; use std::str::from_utf8; -use upac_abi::ABI_VERSION; +use upac_abi::DECODER_ABI_VERSION; use upac_abi::decoder::{CDecodeRequest, CDecodeResponse, CDependency, DecodeError}; use upac_abi::memory::{free_cslice, free_cvec_owning}; use upac_abi::package::CPackageMeta; @@ -27,7 +27,7 @@ include!(concat!(env!("OUT_DIR"), "/layout.rs")); /// Touches no pointers. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] pub unsafe extern "C" fn abi_version() -> u32 { - ABI_VERSION + DECODER_ABI_VERSION } /// # Safety diff --git a/decoders/xbps/src/lib.rs b/decoders/xbps/src/lib.rs index 5f385374..e501fccf 100644 --- a/decoders/xbps/src/lib.rs +++ b/decoders/xbps/src/lib.rs @@ -5,7 +5,7 @@ use std::str::from_utf8; -use upac_abi::ABI_VERSION; +use upac_abi::DECODER_ABI_VERSION; use upac_abi::decoder::{CDecodeRequest, CDecodeResponse, CDependency, DecodeError}; use upac_abi::memory::{free_cslice, free_cvec_owning}; use upac_abi::package::CPackageMeta; @@ -28,7 +28,7 @@ include!(concat!(env!("OUT_DIR"), "/layout.rs")); /// Touches no pointers. #[cfg_attr(feature = "cdylib", unsafe(no_mangle))] pub unsafe extern "C" fn abi_version() -> u32 { - ABI_VERSION + DECODER_ABI_VERSION } /// # Safety diff --git a/lib/abi/src/boot.rs b/lib/abi/src/boot.rs deleted file mode 100644 index 1483c5bc..00000000 --- a/lib/abi/src/boot.rs +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use upac_macro::CNew; - -use crate::error::ErrorKind; -use crate::types::CSlice; - -pub type AbiVersionFn = unsafe extern "C" fn() -> u32; - -pub type ProbeFn = unsafe extern "C" fn() -> i32; - -pub type SetOneShotFn = unsafe extern "C" fn(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32; - -pub type ConfirmBootFn = unsafe extern "C" fn(request: *const CBootPluginRequest, err_out: *mut ErrorKind) -> i32; - -pub trait Booter: Sized { - type Error; - - fn new() -> Result; - fn probes() -> bool; - fn set_one_shot(&mut self, entry_name: &str) -> Result<(), Self::Error>; - fn confirm_boot(&mut self, entry_name: &str) -> Result<(), Self::Error>; -} - -#[repr(C)] -#[derive(CNew)] -pub struct CBootPluginRequest { - pub struct_size: usize, - - pub entry_name: CSlice, -} diff --git a/lib/abi/src/decoder.rs b/lib/abi/src/decoder.rs deleted file mode 100644 index 5ea575db..00000000 --- a/lib/abi/src/decoder.rs +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::io::Error as IoError; -use std::io::ErrorKind as IoErrorKind; - -use upac_macro::{CNew, CValidate}; - -use crate::error::ErrorKind; -use crate::hook::CancelToken; -use crate::package::{CPackageMeta, CVersion}; -use crate::types::{CSlice, CVec, check_size}; - -pub const CONSTRAINT_LESS: u8 = 0b001; -pub const CONSTRAINT_EQUAL: u8 = 0b010; -pub const CONSTRAINT_GREATER: u8 = 0b100; -pub const CONSTRAINT_ANY: u8 = CONSTRAINT_LESS | CONSTRAINT_EQUAL | CONSTRAINT_GREATER; - -pub type AbiVersionFn = unsafe extern "C" fn() -> u32; - -pub type DecodeFn = unsafe extern "C" fn(request: *const CDecodeRequest, response_out: *mut CDecodeResponse) -> i32; - -pub type FreeDecodeResponseFn = unsafe extern "C" fn(response: *mut CDecodeResponse); - -pub fn parse_constraint_prefix(token: &[u8], operators: &[(&[u8], u8)]) -> Option<(u8, usize)> { - operators - .iter() - .find(|(operator, _)| token.starts_with(operator)) - .map(|(operator, constraint)| (*constraint, operator.len())) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DecodeError { - InvalidRequest, - Io(IoErrorKind), - ChecksumMismatch, - UnsupportedFormat, - MissingMetadata, - MalformedMetadata, - InvalidUtf8, - Cancelled, -} - -impl From for DecodeError { - fn from(error: IoError) -> Self { - DecodeError::Io(error.kind()) - } -} - -impl DecodeError { - pub fn code(self) -> i32 { - match self { - DecodeError::InvalidRequest => -1, - DecodeError::Io(_) => -2, - DecodeError::ChecksumMismatch => -3, - DecodeError::UnsupportedFormat => -4, - DecodeError::MissingMetadata => -5, - DecodeError::MalformedMetadata => -6, - DecodeError::InvalidUtf8 => -7, - DecodeError::Cancelled => -8, - } - } -} - -#[repr(C)] -#[derive(CNew)] -pub struct CDecodeRequest { - pub struct_size: usize, - - pub package_path: CSlice, - pub output_dir: CSlice, - - pub checksum: [u8; 32], - - pub cancel_token: *mut CancelToken, -} - -#[repr(C)] -#[derive(CValidate)] -pub struct CDecodeResponse { - pub struct_size: usize, - - pub meta: CPackageMeta, - - pub dependencies: CVec, - pub declarative_triggers: CVec, - - pub free: FreeDecodeResponseFn, -} - -impl Drop for CDecodeResponse { - fn drop(&mut self) { - unsafe { (self.free)(self) }; - } -} - -#[repr(C)] -#[derive(CValidate)] -pub struct CDependency { - pub struct_size: usize, - - pub name: CSlice, - pub constraint: u8, - pub version: CVersion, -} diff --git a/lib/abi/src/error.rs b/lib/abi/src/error.rs index 53ec0d75..80f9e942 100644 --- a/lib/abi/src/error.rs +++ b/lib/abi/src/error.rs @@ -69,10 +69,3 @@ pub struct CError { pub state: u32, pub error: ErrorKind, } - -pub trait CommandState: Copy { - const DOMAIN: ErrorDomain; - const VALIDATION: Self; - - fn as_u32(self) -> u32; -} diff --git a/lib/abi/src/hook.rs b/lib/abi/src/hook.rs index 0fda2595..9fc48a10 100644 --- a/lib/abi/src/hook.rs +++ b/lib/abi/src/hook.rs @@ -3,16 +3,10 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::ffi::CString; -use std::mem::size_of; -use std::os::raw::c_void; -use std::ptr::null; use std::sync::atomic::{AtomicU8, Ordering}; use crate::types::CSlice; -pub type HookMessageFn = unsafe extern "C" fn(event: *const CProgressEvent, ctx: *mut c_void) -> HookAck; - #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HookAck { @@ -30,93 +24,6 @@ pub struct CProgressEvent { pub total: u64, } -pub struct ProgressEventBuilder { - stage: u32, - phase: u32, - subject: Option, - current: u64, - total: u64, -} - -impl ProgressEventBuilder { - pub fn new(stage: u32) -> Self { - Self { - stage, - phase: 0, - subject: None, - current: 0, - total: 0, - } - } - - pub fn stage(&self) -> u32 { - self.stage - } - - pub fn phase(mut self, phase: u32) -> Self { - self.phase = phase; - self - } - - pub fn subject(mut self, subject: impl Into) -> Self { - self.subject = CString::new(subject.into()).ok(); - self - } - - pub fn progress(mut self, current: u64, total: u64) -> Self { - self.current = current; - self.total = total; - self - } - - pub fn build(&self) -> CProgressEvent { - let subject = match &self.subject { - Some(subject) => CSlice { - ptr: subject.as_ptr().cast(), - len: subject.as_bytes().len(), - }, - None => CSlice { ptr: null(), len: 0 }, - }; - - CProgressEvent { - struct_size: size_of::(), - stage: self.stage, - phase: self.phase, - subject, - current: self.current, - total: self.total, - } - } -} - -pub trait MessageHook { - fn send(&self, event: &CProgressEvent) -> HookAck; -} - -pub struct Message { - hook_message: Option, - hook_message_context: *mut c_void, -} - -impl Message { - pub fn new(hook_message: Option, hook_message_context: *mut c_void) -> Self { - Self { - hook_message, - hook_message_context, - } - } -} - -impl MessageHook for Message { - fn send(&self, event: &CProgressEvent) -> HookAck { - let Some(hook_message) = self.hook_message else { - return HookAck::Delivered; - }; - - unsafe { hook_message(event as *const CProgressEvent, self.hook_message_context) } - } -} - #[repr(C)] pub struct CancelToken { cancelled: AtomicU8, diff --git a/lib/abi/src/lib.rs b/lib/abi/src/lib.rs index 3a89c2ce..e78a3a13 100644 --- a/lib/abi/src/lib.rs +++ b/lib/abi/src/lib.rs @@ -3,21 +3,49 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception +use std::ffi::c_void; + use self::error::ErrorKind; +use self::hook::{CProgressEvent, HookAck}; +use self::request::{ + CBootPluginConfirmSuccsesBootRequest, CBootPluginInstallRequest, CBootPluginSetOneShotRequest, CDecodeRequest, +}; +use self::response::CDecodeResponse; -pub mod boot; -pub mod decoder; pub mod error; pub mod hook; pub mod memory; pub mod package; pub mod request; pub mod response; -pub mod setup; pub mod types; -pub const ABI_VERSION: u32 = 2; -pub const BOOT_ABI_VERSION: u32 = 1; +pub const LIB_ABI_VERSION: u32 = 2; +pub const BOOT_ABI_VERSION: u32 = 2; +pub const DECODER_ABI_VERSION: u32 = 2; + +pub const CONSTRAINT_LESS: u8 = 0b001; +pub const CONSTRAINT_EQUAL: u8 = 0b010; +pub const CONSTRAINT_GREATER: u8 = 0b100; +pub const CONSTRAINT_ANY: u8 = CONSTRAINT_LESS | CONSTRAINT_EQUAL | CONSTRAINT_GREATER; + +pub type BootPluginAbiVersionFn = unsafe extern "C" fn() -> u32; + +pub type DecodePluginAbiVersionFn = unsafe extern "C" fn() -> u32; + +pub type HookMessageFn = unsafe extern "C" fn(event: *const CProgressEvent, ctx: *mut c_void) -> HookAck; + +pub type SetOneShotFn = + unsafe extern "C" fn(request: *const CBootPluginSetOneShotRequest, err_out: *mut ErrorKind) -> i32; + +pub type ConfirmBootFn = + unsafe extern "C" fn(request: *const CBootPluginConfirmSuccsesBootRequest, err_out: *mut ErrorKind) -> i32; + +pub type InstallFn = unsafe extern "C" fn(request: *const CBootPluginInstallRequest, err_out: *mut ErrorKind) -> i32; + +pub type DecodeFn = unsafe extern "C" fn(request: *const CDecodeRequest, response_out: *mut CDecodeResponse) -> i32; + +pub type FreeDecodeResponseFn = unsafe extern "C" fn(response: *mut CDecodeResponse); #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -38,13 +66,6 @@ impl FileDiffKind { } } -// 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 { @@ -66,11 +87,6 @@ impl PackageDiffKind { } } -// Distinguishes which tree a DiffPrefixFileEntry/DiffUntrackedFileEntry came -// from when both /usr and /etc changes are folded into one list (the combined -// diff command). Standalone diff_prefix/diff_config don't need it to -// disambiguate (the command itself already implies the axis), but reuse the -// same entry types and set it to a fixed value. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DiffFileSource { @@ -88,10 +104,6 @@ impl DiffFileSource { } } -// Filesystem chosen for the deployment partition (needs fs-verity support, see -// doc chapter 3 §(4)) or any extra mount upac-setup formats/mounts. Appending -// a new variant later (e.g. bcachefs) is a plain additive change here — every -// consumer already goes through from_u8, so nothing needs pre-reserving. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FsKind { diff --git a/lib/abi/src/package.rs b/lib/abi/src/package.rs index 2c594f70..f5562bd3 100644 --- a/lib/abi/src/package.rs +++ b/lib/abi/src/package.rs @@ -10,7 +10,7 @@ use crate::memory::free_cslice; use crate::types::{CSlice, check_size}; #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CVersion { pub struct_size: usize, @@ -20,7 +20,7 @@ pub struct CVersion { } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CPackageMeta { pub struct_size: usize, pub name: CSlice, @@ -48,3 +48,13 @@ pub struct CPackageInfo { #[optional] pub arch_sub: CSlice, } + +#[repr(C)] +#[derive(CFree, CNew, CValidate)] +pub struct CPackageDependency { + pub struct_size: usize, + + pub name: CSlice, + pub constraint: u8, + pub version: CVersion, +} diff --git a/lib/abi/src/request.rs b/lib/abi/src/request.rs index 6c2518df..e25f57de 100644 --- a/lib/abi/src/request.rs +++ b/lib/abi/src/request.rs @@ -7,8 +7,9 @@ use std::os::raw::c_void; use upac_macro::{CNew, CValidate}; +use super::HookMessageFn; use crate::error::ErrorKind; -use crate::hook::{CancelToken, HookMessageFn}; +use crate::hook::CancelToken; use crate::package::CPackageInfo; use crate::types::{CSlice, CVec, check_size}; use crate::{DiffFileSource, FileDiffKind}; @@ -30,6 +31,8 @@ pub struct CInstallRequest { pub struct_size: usize, pub base: CRequestBase, + pub boot_plugin: CSlice, + pub tmp_path: CSlice, pub subject: CSlice, @@ -37,8 +40,7 @@ pub struct CInstallRequest { pub message: CSlice, pub packages: CVec, - #[optional] - pub boot_plugin: CSlice, + pub allow_conflict_files: bool, } @@ -48,6 +50,8 @@ pub struct CUpdateRequest { pub struct_size: usize, pub base: CRequestBase, + pub boot_plugin: CSlice, + pub tmp_path: CSlice, pub subject: CSlice, @@ -55,8 +59,7 @@ pub struct CUpdateRequest { pub message: CSlice, pub packages: CVec, - #[optional] - pub boot_plugin: CSlice, + pub allow_downgrade: bool, pub allow_conflict_files: bool, } @@ -67,13 +70,14 @@ pub struct CUninstallRequest { pub struct_size: usize, pub base: CRequestBase, + pub boot_plugin: CSlice, + pub tmp_path: CSlice, pub subject: CSlice, #[optional] pub message: CSlice, pub packages: CVec, - #[optional] - pub boot_plugin: CSlice, + pub purge: bool, } @@ -83,10 +87,10 @@ pub struct CRollbackRequest { pub struct_size: usize, pub base: CRequestBase, + pub boot_plugin: CSlice, + pub tmp_path: CSlice, pub config_digest: CSlice, - #[optional] - pub boot_plugin: CSlice, } #[repr(C)] @@ -107,16 +111,17 @@ pub struct CFilesRequest { pub struct_size: usize, pub base: CRequestBase, + pub boot_plugin: CSlice, + pub tmp_path: CSlice, pub subject: CSlice, #[optional] pub message: CSlice, pub files: CVec, pub file_kind: FileDiffKind, - pub scope: DiffFileSource, pub file_package: *const CPackageInfo, - #[optional] - pub boot_plugin: CSlice, + + pub scope: DiffFileSource, } #[repr(C)] @@ -268,3 +273,47 @@ pub struct CSearchInPackageFilesRequest { pub search: CSlice, pub is_regex: bool, } + +#[repr(C)] +#[derive(CNew, CValidate)] +pub struct CDecodeRequest { + pub struct_size: usize, + + pub package_path: CSlice, + pub output_dir: CSlice, + + pub checksum: [u8; 32], + + pub cancel_token: *mut CancelToken, +} + +#[repr(C)] +#[derive(CNew, CValidate)] +pub struct CBootPluginSetOneShotRequest { + pub struct_size: usize, + + pub entry_name: CSlice, +} + +#[repr(C)] +#[derive(CNew, CValidate)] +pub struct CBootPluginConfirmSuccsesBootRequest { + pub struct_size: usize, + + pub entry_name: CSlice, + pub esp_mount_point: CSlice, +} + +#[repr(C)] +#[derive(CNew, CValidate)] +pub struct CBootPluginInstallRequest { + pub struct_size: usize, + + pub esp_mount_point: CSlice, + pub esp_partition_number: u32, + pub esp_starting_lba: u64, + pub esp_ending_lba: u64, + pub esp_unique_partition_guid: [u8; 16], + pub to_slot: CSlice, + pub from_slot: CSlice, +} diff --git a/lib/abi/src/response.rs b/lib/abi/src/response.rs index 762b4fb2..61757160 100644 --- a/lib/abi/src/response.rs +++ b/lib/abi/src/response.rs @@ -5,16 +5,18 @@ use upac_macro::{CFree, CNew, CValidate}; +use crate::FreeDecodeResponseFn; use crate::error::ErrorKind; use crate::memory::{free_cslice, free_cvec_owning}; -use crate::package::{CPackageMeta, CVersion}; +use crate::package::{CPackageDependency, CPackageMeta, CVersion}; use crate::types::{CSlice, CVec, check_size}; use crate::{DiffFileSource, FileDiffKind, PackageDiffKind}; #[repr(C)] -#[derive(CFree)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffPackageEntry { pub struct_size: usize, + pub name: CSlice, pub kind: PackageDiffKind, pub version: CVersion, @@ -22,7 +24,7 @@ pub struct CDiffPackageEntry { } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffFileEntryCommon { pub struct_size: usize, @@ -31,7 +33,7 @@ pub struct CDiffFileEntryCommon { } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffPrefixFileEntry { pub struct_size: usize, @@ -42,7 +44,7 @@ pub struct CDiffPrefixFileEntry { } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffConfigFileEntry { pub struct_size: usize, @@ -52,7 +54,7 @@ pub struct CDiffConfigFileEntry { } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffUntrackedFileEntry { pub struct_size: usize, @@ -61,7 +63,7 @@ pub struct CDiffUntrackedFileEntry { } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CConfigCommitEntry { pub struct_size: usize, @@ -72,28 +74,28 @@ pub struct CConfigCommitEntry { } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CListConfigResponse { pub struct_size: usize, pub commits: CVec, } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CListPackagesResponse { pub struct_size: usize, pub metas: CVec, } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CSearchMetaResponse { pub struct_size: usize, pub metas: CVec, } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CSearchFileEntry { pub struct_size: usize, @@ -103,28 +105,28 @@ pub struct CSearchFileEntry { } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CSearchFilesResponse { pub struct_size: usize, pub files: CVec, } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CSearchInMetaResponse { pub struct_size: usize, pub metas: CVec, } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CSearchInPackageFilesResponse { pub struct_size: usize, pub files: CVec, } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CPrefixEntry { pub struct_size: usize, @@ -138,14 +140,14 @@ pub struct CPrefixEntry { } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CListPrefixResponse { pub struct_size: usize, pub prefixes: CVec, } #[repr(C)] -#[derive(CFree, CValidate)] +#[derive(CFree, CNew, CValidate)] pub struct CHistoryEntry { pub struct_size: usize, @@ -160,35 +162,35 @@ pub struct CHistoryEntry { } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CListHistoryResponse { pub struct_size: usize, pub history: CVec, } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffPrefixResponse { pub struct_size: usize, pub files: CVec, } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffConfigResponse { pub struct_size: usize, pub files: CVec, } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffPackagesResponse { pub struct_size: usize, pub diff_packages: CVec, } #[repr(C)] -#[derive(CFree, CNew)] +#[derive(CFree, CNew, CValidate)] pub struct CDiffResponse { pub struct_size: usize, pub diff_packages: CVec, @@ -196,12 +198,20 @@ pub struct CDiffResponse { } #[repr(C)] -#[derive(CFree)] -pub struct CUnmutatedResponse { +#[derive(CValidate, CNew)] +pub struct CDecodeResponse { pub struct_size: usize, - pub metas: CVec, - pub files: CVec, - pub commits: CVec, - pub diff_packages: CVec, + pub meta: CPackageMeta, + + pub dependencies: CVec, + pub declarative_triggers: CVec, + + pub free: FreeDecodeResponseFn, +} + +impl Drop for CDecodeResponse { + fn drop(&mut self) { + unsafe { (self.free)(self) }; + } } diff --git a/lib/abi/src/setup.rs b/lib/abi/src/setup.rs deleted file mode 100644 index a24a3b95..00000000 --- a/lib/abi/src/setup.rs +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use upac_macro::{CNew, CValidate}; - -use crate::FsKind; -use crate::error::ErrorKind; -use crate::request::CRequestBase; -use crate::types::{CSlice, CVec, check_size}; - -#[repr(C)] -#[derive(CNew, CValidate)] -pub struct CSetupBase { - pub struct_size: usize, - - pub base: CRequestBase, - - #[optional] - pub mount_point: CSlice, - #[non_empty] - pub source: CSlice, - #[optional] - pub meta_filename: CSlice, - pub empty_config: bool, - pub pinned: bool, - #[optional] - pub boot_plugin: CSlice, -} - -#[repr(C)] -#[derive(CValidate)] -pub struct CPartitionMount { - pub struct_size: usize, - - #[non_empty] - pub mount_path: CSlice, - #[non_empty] - pub device_path: CSlice, - pub fs_kind: FsKind, -} - -#[repr(C)] -#[derive(CValidate)] -pub struct CPartitionSpec { - pub struct_size: usize, - - #[non_empty] - pub mount_path: CSlice, - pub size_mib: u64, - pub fs_kind: FsKind, -} - -#[repr(C)] -#[derive(CNew, CValidate)] -pub struct CSetupExistingRequest { - pub struct_size: usize, - - pub base: CSetupBase, - - #[non_empty] - pub esp_device: CSlice, - #[non_empty] - pub deploy_device: CSlice, - pub deploy_fs: FsKind, - pub extra_mounts: CVec, -} - -#[repr(C)] -#[derive(CValidate)] -pub struct CGptLayout { - pub struct_size: usize, - - pub esp_size_mib: u64, - pub deploy_fs: FsKind, - pub deploy_size_mib: u64, - pub extra_partitions: CVec, - pub force_wipe: bool, -} - -#[repr(C)] -#[derive(CValidate)] -pub struct CBtrfsOptions { - pub struct_size: usize, - - pub node_size: u32, - pub sector_size: u32, -} - -#[repr(C)] -#[derive(CNew, CValidate)] -pub struct CSetupWholeDiskRequest { - pub struct_size: usize, - - pub base: CSetupBase, - - #[non_empty] - pub device_path: CSlice, - pub gpt: CGptLayout, - pub btrfs: CBtrfsOptions, -} diff --git a/lib/abi/src/types.rs b/lib/abi/src/types.rs index 124b61d5..778b2a31 100644 --- a/lib/abi/src/types.rs +++ b/lib/abi/src/types.rs @@ -40,6 +40,13 @@ pub trait COwned { unsafe fn into_owned(self) -> Self::Owned; } +pub trait CValidatable { + /// # Safety + /// Same contract as the inherent `validate()` this forwards to — the receiver must be a + /// freshly-received C-ABI struct that hasn't yet been trusted for reads. + unsafe fn validate(&self) -> Result<(), ErrorKind>; +} + #[repr(C)] #[derive(Clone, Copy)] pub struct CSlice { diff --git a/lib/abi/tests/hook.rs b/lib/abi/tests/hook.rs index 3f8ae6f1..a0536ade 100644 --- a/lib/abi/tests/hook.rs +++ b/lib/abi/tests/hook.rs @@ -3,18 +3,7 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::mem::size_of; -use std::os::raw::c_void; -use std::ptr::{addr_of_mut, null_mut}; - -use upac_abi::hook::{CProgressEvent, CancelToken, HookAck, Message, MessageHook, ProgressEventBuilder}; - -unsafe extern "C" fn record_stage_and_retry(event: *const CProgressEvent, ctx: *mut c_void) -> HookAck { - unsafe { - *ctx.cast::() = (*event).stage; - } - HookAck::Retry -} +use upac_abi::hook::CancelToken; #[test] fn cancel_token_starts_not_cancelled() { @@ -48,53 +37,3 @@ fn cancel_token_reset_clears_a_cancellation() { assert!(!token.is_cancelled()); } - -#[test] -fn progress_event_builder_defaults() { - let event = ProgressEventBuilder::new(3).build(); - - assert_eq!(event.struct_size, size_of::()); - assert_eq!(event.stage, 3); - assert_eq!(event.phase, 0); - assert_eq!(event.current, 0); - assert_eq!(event.total, 0); - assert!(event.subject.ptr.is_null()); -} - -#[test] -fn progress_event_builder_stage_accessor_matches_the_constructor() { - let builder = ProgressEventBuilder::new(7); - - assert_eq!(builder.stage(), 7); -} - -#[test] -fn progress_event_builder_applies_phase_subject_and_progress() { - let builder = ProgressEventBuilder::new(1).phase(2).subject("foo.txt").progress(3, 10); - let event = builder.build(); - - assert_eq!(event.phase, 2); - assert_eq!(event.current, 3); - assert_eq!(event.total, 10); - assert_eq!(<&str>::try_from(&event.subject).unwrap(), "foo.txt"); -} - -#[test] -fn message_send_with_no_hook_returns_delivered() { - let message = Message::new(None, null_mut()); - let event = ProgressEventBuilder::new(0).build(); - - assert_eq!(message.send(&event), HookAck::Delivered); -} - -#[test] -fn message_send_with_a_hook_forwards_the_event_and_context() { - let mut recorded_stage: u32 = 0; - let message = Message::new(Some(record_stage_and_retry), addr_of_mut!(recorded_stage).cast()); - let event = ProgressEventBuilder::new(9).build(); - - let ack = message.send(&event); - - assert_eq!(ack, HookAck::Retry); - assert_eq!(recorded_stage, 9); -} diff --git a/lib/abi/tests/validate.rs b/lib/abi/tests/validate.rs index 693536d2..efeddc3c 100644 --- a/lib/abi/tests/validate.rs +++ b/lib/abi/tests/validate.rs @@ -6,18 +6,16 @@ use std::mem::size_of; use std::ptr::{null, null_mut}; -use upac_abi::decoder::CDependency; use upac_abi::error::ErrorKind; use upac_abi::memory::free_cslice; -use upac_abi::package::{CPackageInfo, CPackageMeta, CVersion}; +use upac_abi::package::{CPackageDependency, CPackageInfo, CPackageMeta, CVersion}; use upac_abi::request::CRequestBase; use upac_abi::response::{ CConfigCommitEntry, CDiffConfigFileEntry, CDiffFileEntryCommon, CDiffPrefixFileEntry, CDiffUntrackedFileEntry, CHistoryEntry, CPrefixEntry, CSearchFileEntry, }; -use upac_abi::setup::{CBtrfsOptions, CGptLayout, CPartitionMount, CPartitionSpec, CSetupBase}; use upac_abi::types::{COwned, CSlice, CVec}; -use upac_abi::{DiffFileSource, FileDiffKind, FsKind}; +use upac_abi::{DiffFileSource, FileDiffKind}; fn valid_version() -> CVersion { CVersion { @@ -103,8 +101,8 @@ fn package_info_validate_rejects_missing_required_field() { #[test] fn dependency_validate_rejects_invalid_nested_version() { - let mut dependency = CDependency { - struct_size: size_of::(), + let mut dependency = CPackageDependency { + struct_size: size_of::(), name: CSlice::from_owned(b"glibc".to_vec()), constraint: 0b010, version: valid_version(), @@ -382,146 +380,3 @@ fn request_base_validate_rejects_wrong_struct_size() { assert_eq!(unsafe { base.validate() }, Err(ErrorKind::AbiMismatch)); } - -fn valid_setup_base() -> CSetupBase { - CSetupBase { - struct_size: size_of::(), - base: valid_request_base(), - mount_point: CSlice { ptr: null(), len: 0 }, - source: CSlice::from_owned(b"/mnt/source".to_vec()), - meta_filename: CSlice { ptr: null(), len: 0 }, - empty_config: false, - pinned: false, - boot_plugin: CSlice { ptr: null(), len: 0 }, - } -} - -#[test] -fn setup_base_validate_ok_with_all_optionals_absent() { - let base = valid_setup_base(); - - assert!(unsafe { base.validate() }.is_ok()); - unsafe { free_cslice(&base.source) }; -} - -#[test] -fn setup_base_validate_rejects_missing_required_source() { - let mut base = valid_setup_base(); - unsafe { free_cslice(&base.source) }; - base.source = CSlice { ptr: null(), len: 0 }; - - assert_eq!(unsafe { base.validate() }, Err(ErrorKind::InvalidEntry)); -} - -fn valid_partition_mount() -> CPartitionMount { - CPartitionMount { - struct_size: size_of::(), - mount_path: CSlice::from_owned(b"/boot".to_vec()), - device_path: CSlice::from_owned(b"/dev/sda1".to_vec()), - fs_kind: FsKind::Ext4, - } -} - -#[test] -fn partition_mount_validate_ok_for_well_formed() { - let mount = valid_partition_mount(); - - assert!(unsafe { mount.validate() }.is_ok()); - unsafe { - free_cslice(&mount.mount_path); - free_cslice(&mount.device_path); - } -} - -#[test] -fn partition_mount_validate_rejects_missing_mount_path() { - let mut mount = valid_partition_mount(); - unsafe { free_cslice(&mount.mount_path) }; - mount.mount_path = CSlice { ptr: null(), len: 0 }; - - assert_eq!(unsafe { mount.validate() }, Err(ErrorKind::InvalidEntry)); - unsafe { free_cslice(&mount.device_path) }; -} - -fn valid_partition_spec() -> CPartitionSpec { - CPartitionSpec { - struct_size: size_of::(), - mount_path: CSlice::from_owned(b"/boot".to_vec()), - size_mib: 512, - fs_kind: FsKind::Ext4, - } -} - -#[test] -fn partition_spec_validate_ok_for_well_formed() { - let spec = valid_partition_spec(); - - assert!(unsafe { spec.validate() }.is_ok()); - unsafe { free_cslice(&spec.mount_path) }; -} - -#[test] -fn partition_spec_validate_rejects_missing_mount_path() { - let mut spec = valid_partition_spec(); - unsafe { free_cslice(&spec.mount_path) }; - spec.mount_path = CSlice { ptr: null(), len: 0 }; - - assert_eq!(unsafe { spec.validate() }, Err(ErrorKind::InvalidEntry)); -} - -fn valid_gpt_layout() -> CGptLayout { - CGptLayout { - struct_size: size_of::(), - esp_size_mib: 512, - deploy_fs: FsKind::Ext4, - deploy_size_mib: 8192, - extra_partitions: CVec { - ptr: null_mut(), - len: 0, - }, - force_wipe: false, - } -} - -#[test] -fn gpt_layout_validate_ok_with_no_extra_partitions() { - assert!(unsafe { valid_gpt_layout().validate() }.is_ok()); -} - -#[test] -fn gpt_layout_validate_rejects_an_invalid_extra_partition() { - let mut bad_spec = valid_partition_spec(); - bad_spec.struct_size = 0; - let mut specs = vec![bad_spec]; - - let mut layout = valid_gpt_layout(); - layout.extra_partitions = CVec { - ptr: specs.as_mut_ptr(), - len: specs.len(), - }; - - assert_eq!(unsafe { layout.validate() }, Err(ErrorKind::AbiMismatch)); - - unsafe { free_cslice(&specs[0].mount_path) }; -} - -fn valid_btrfs_options() -> CBtrfsOptions { - CBtrfsOptions { - struct_size: size_of::(), - node_size: 0, - sector_size: 0, - } -} - -#[test] -fn btrfs_options_validate_ok_for_well_formed() { - assert!(unsafe { valid_btrfs_options().validate() }.is_ok()); -} - -#[test] -fn btrfs_options_validate_rejects_wrong_struct_size() { - let mut options = valid_btrfs_options(); - options.struct_size = 0; - - assert_eq!(unsafe { options.validate() }, Err(ErrorKind::AbiMismatch)); -} diff --git a/lib/lib/lib.toml b/lib/lib/lib.toml index 92de957a..f41c4b9a 100644 --- a/lib/lib/lib.toml +++ b/lib/lib/lib.toml @@ -145,17 +145,21 @@ update_mime_database_bin = "update-mime-database" update_desktop_database_bin = "update-desktop-database" shared_mime_info_xmlns = "http://www.freedesktop.org/standards/shared-mime-info" -# ESP discovery and the fixed UKI-direct staging slot name (§5.2/§5.3). esp_mount_primary/ +# ESP discovery and the fixed UKI-direct staging slots (§5.2/§5.3). esp_mount_primary/ # esp_mount_fallback are tried in that order against the running system's mount table (same # rsmount::tables::MountInfo mechanism Deploy::device_path() already uses) — chapter 3 documents -# the ESP as mounted at either. upac_to_slot is the fixed stem a UKI-direct image is always -# staged under (`\EFI\Linux\upac-to.efi`): the corresponding UEFI Boot#### entry is pre-registered -# once, out of scope of this pipeline, and CheckoutStage only ever overwrites that same file's -# content — a content-addressed name would require a new Boot#### NVRAM entry each time, which -# nothing here creates. BLS-style images (systemd-boot/grub/refind) use the prefix_digest itself -# as the entry name instead — content-addressed, no fixed slot needed, since those loaders rescan -# their entries directory fresh every boot. +# the ESP as mounted at either. upac_uki_to_slot is the fixed stem the corresponding UEFI Boot#### +# entry is pre-registered against once, out of scope of this pipeline (`\EFI\Linux\upac-to.efi`) — +# a content-addressed name would require a new Boot#### NVRAM entry each time, which nothing here +# creates. upac_uki_from_slot is NOT a second independently-bootable slot — it's a staging name: +# `write_boot_entry` writes the new image there first, then atomically renames it over +# upac_uki_to_slot, so a crash mid-write never leaves the one file Boot#### actually points at +# truncated (the upstream `composefs-boot` writer itself just does a plain, non-atomic +# `fs::write`). BLS-style images (systemd-boot/grub/refind) use the prefix_digest itself as the +# entry name instead — content-addressed, no staging needed, since those loaders rescan their +# entries directory fresh every boot. [boot] esp_mount_primary = "/efi" esp_mount_fallback = "/boot" upac_uki_to_slot = "upac-to" +upac_uki_from_slot = "upac-from" diff --git a/lib/lib/src/boot/mod.rs b/lib/lib/src/boot/mod.rs index f064caca..9cfe143b 100644 --- a/lib/lib/src/boot/mod.rs +++ b/lib/lib/src/boot/mod.rs @@ -9,6 +9,7 @@ use std::path::Path; use composefs::generic_tree::Stat; use composefs::repository::Repository; use composefs::tree::{Directory, FileSystem, Inode}; + use composefs_boot::bootloader::{BootEntry, get_boot_resources}; use composefs_boot::cmdline::ComposefsCmdline; use composefs_boot::write_boot::write_boot_simple; diff --git a/lib/lib/src/composefs/overlay.rs b/lib/lib/src/composefs/overlay.rs index 8be89349..1d0b4f09 100644 --- a/lib/lib/src/composefs/overlay.rs +++ b/lib/lib/src/composefs/overlay.rs @@ -10,9 +10,10 @@ use std::path::{Path, PathBuf}; use composefs::repository::{ImportContext, Repository}; use composefs::tree::{FileSystem, Inode}; -use crate::composefs::error::RepoError; -use crate::composefs::file::{FileHandle, stat_from_metadata}; -use crate::composefs::repository::ObjectID; +use super::error::RepoError; +use super::file::{FileHandle, stat_from_metadata}; +use super::repository::ObjectID; + use crate::layout::deployment::OVERLAY_OPAQUE_XATTR; pub fn apply_overlay_upper( diff --git a/lib/lib/src/composefs/repository.rs b/lib/lib/src/composefs/repository.rs index 3eed822a..727a8633 100644 --- a/lib/lib/src/composefs/repository.rs +++ b/lib/lib/src/composefs/repository.rs @@ -15,7 +15,7 @@ use composefs::tree::FileSystem; use nix::fcntl::AT_FDCWD; -use crate::composefs::error::RepoError; +use super::error::RepoError; pub type ObjectID = Sha256HashValue; @@ -23,6 +23,14 @@ pub fn init(path: &Path) -> Result<(Repository, bool), RepoError> { Ok(Repository::init_path(AT_FDCWD, path, RepositoryConfig::default())?) } +pub fn init_insecure(path: &Path) -> Result<(Repository, bool), RepoError> { + Ok(Repository::init_path( + AT_FDCWD, + path, + RepositoryConfig::default().set_insecure(), + )?) +} + pub(crate) fn open(path: &Path) -> Result, RepoError> { Ok(Repository::open_path(AT_FDCWD, path)?) } diff --git a/lib/lib/src/config/merge.rs b/lib/lib/src/config.rs similarity index 100% rename from lib/lib/src/config/merge.rs rename to lib/lib/src/config.rs diff --git a/lib/lib/src/config/mod.rs b/lib/lib/src/config/mod.rs deleted file mode 100644 index 177dfc90..00000000 --- a/lib/lib/src/config/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -pub mod merge; diff --git a/lib/lib/src/database/attribution.rs b/lib/lib/src/database/attribution.rs index 020dbe78..5516d4b0 100644 --- a/lib/lib/src/database/attribution.rs +++ b/lib/lib/src/database/attribution.rs @@ -3,12 +3,13 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception +use upac_types::entry::FileEntry; +use upac_types::package::PackageMeta; + use super::error::DatabaseError; use super::files::FileStore; use super::meta::MetaStore; -use upac_types::{FileEntry, PackageMeta}; - pub struct FileAttribution { pub package_meta: PackageMeta, pub file_entry: FileEntry, diff --git a/lib/lib/src/database/files.rs b/lib/lib/src/database/files.rs index 34e7e48b..b32e7181 100644 --- a/lib/lib/src/database/files.rs +++ b/lib/lib/src/database/files.rs @@ -9,11 +9,11 @@ use twox_hash::xxhash3_64::Hasher as XxHasher; use uuid::Uuid; -use upac_types::FileEntry; use upac_types::codec::RedbCodable; +use upac_types::entry::FileEntry; use super::error::DatabaseError; -use super::{FILES_UUID_HASH_TABLE, FILES_UUID_TABLE, MemoryDatabase, ReadableSource}; +use super::{FILES_UUID_HASH_TABLE, FILES_UUID_TABLE, MemoryDatabase, ReadTransactionExt, ReadableSource}; use crate::layout::database::FILES_ENTRY_TYPE_NAME; @@ -37,14 +37,18 @@ pub trait FileStoreMut: FileStore { impl FileStore for T { fn find_file_owner(&self, path: &str) -> Result, DatabaseError> { let transaction = self.source().begin_read()?; - let by_path = transaction.open_table(FILES_UUID_HASH_TABLE)?; + let Some(by_path) = transaction.open_table_or_none(FILES_UUID_HASH_TABLE)? else { + return Ok(None); + }; Ok(by_path.get(Self::path_hash(path))?.map(|guard| guard.value())) } fn list_package_files(&self, uuid: Uuid) -> Result, DatabaseError> { let transaction = self.source().begin_read()?; - let files = transaction.open_table(FILES_UUID_TABLE)?; + let Some(files) = transaction.open_table_or_none(FILES_UUID_TABLE)? else { + return Ok(Vec::new()); + }; let mut out = Vec::new(); for entry in files.range((uuid, 0u64)..)? { @@ -63,7 +67,9 @@ impl FileStore for T { fn list_files(&self) -> Result, DatabaseError> { let transaction = self.source().begin_read()?; - let files = transaction.open_table(FILES_UUID_TABLE)?; + let Some(files) = transaction.open_table_or_none(FILES_UUID_TABLE)? else { + return Ok(Vec::new()); + }; let mut out = Vec::new(); for entry in files.iter()? { diff --git a/lib/lib/src/database/meta.rs b/lib/lib/src/database/meta.rs index 875a8039..36abfb06 100644 --- a/lib/lib/src/database/meta.rs +++ b/lib/lib/src/database/meta.rs @@ -9,11 +9,11 @@ use twox_hash::xxhash3_64::Hasher as XxHasher; use uuid::Uuid; -use upac_types::PackageMeta; use upac_types::codec::{RedbCodable, write_len_prefixed, write_opt_str}; +use upac_types::package::PackageMeta; use super::error::DatabaseError; -use super::{MemoryDatabase, PACKAGES_HASH_TABLE, PACKAGES_UUID_TABLE, ReadableSource}; +use super::{MemoryDatabase, PACKAGES_HASH_TABLE, PACKAGES_UUID_TABLE, ReadTransactionExt, ReadableSource}; use crate::layout::database::PACKAGES_META_TYPE_NAME; @@ -52,21 +52,27 @@ pub trait MetaStoreMut: MetaStore { impl MetaStore for T { fn find_package_uuid(&self, name: &str, arch: &str, arch_sub: Option<&str>) -> Result, DatabaseError> { let transaction = self.source().begin_read()?; - let by_name = transaction.open_table(PACKAGES_HASH_TABLE)?; + let Some(by_name) = transaction.open_table_or_none(PACKAGES_HASH_TABLE)? else { + return Ok(None); + }; Self::lookup_uuid(&by_name, name, arch, arch_sub) } fn get_package_meta(&self, uuid: Uuid) -> Result, DatabaseError> { let transaction = self.source().begin_read()?; - let packages = transaction.open_table(PACKAGES_UUID_TABLE)?; + let Some(packages) = transaction.open_table_or_none(PACKAGES_UUID_TABLE)? else { + return Ok(None); + }; Ok(packages.get(uuid)?.map(|guard| guard.value().0)) } fn list_packages_metas(&self) -> Result, DatabaseError> { let transaction = self.source().begin_read()?; - let packages = transaction.open_table(PACKAGES_UUID_TABLE)?; + let Some(packages) = transaction.open_table_or_none(PACKAGES_UUID_TABLE)? else { + return Ok(Vec::new()); + }; let mut out = Vec::new(); for entry in packages.iter()? { diff --git a/lib/lib/src/database/mod.rs b/lib/lib/src/database/mod.rs index 5bfcfdf9..63246ebb 100644 --- a/lib/lib/src/database/mod.rs +++ b/lib/lib/src/database/mod.rs @@ -8,22 +8,22 @@ use std::path::Path; use std::sync::{Arc, PoisonError, RwLock}; use redb::{ - Builder, Database as RedbDatabase, ReadOnlyDatabase as RedbReadOnlyDatabase, ReadableDatabase, StorageBackend, - TableDefinition, + Builder, Database as RedbDatabase, Key, ReadOnlyDatabase as RedbReadOnlyDatabase, ReadOnlyTable, ReadTransaction, + ReadableDatabase, StorageBackend, TableDefinition, TableError, Value, }; use uuid::Uuid; -use crate::layout::database::{ - FILES_BY_PATH_TABLE_NAME, FILES_TABLE_NAME, PACKAGES_BY_NAME_TABLE_NAME, PACKAGES_TABLE_NAME, - PACKAGES_TRIGGERS_TABLE_NAME, -}; - use self::error::DatabaseError; use self::files::StoredFileEntry; use self::meta::StoredPackageMeta; use self::triggers::StoredTriggers; +use crate::layout::database::{ + FILES_BY_PATH_TABLE_NAME, FILES_TABLE_NAME, PACKAGES_BY_NAME_TABLE_NAME, PACKAGES_TABLE_NAME, + PACKAGES_TRIGGERS_TABLE_NAME, +}; + pub mod attribution; pub mod error; pub mod files; @@ -122,6 +122,24 @@ impl ReadableSource for ReadOnlyDatabase { } } +pub(crate) trait ReadTransactionExt { + fn open_table_or_none( + &self, definition: TableDefinition, + ) -> Result>, DatabaseError>; +} + +impl ReadTransactionExt for ReadTransaction { + fn open_table_or_none( + &self, definition: TableDefinition, + ) -> Result>, DatabaseError> { + match self.open_table(definition) { + Ok(table) => Ok(Some(table)), + Err(TableError::TableDoesNotExist(_)) => Ok(None), + Err(error) => Err(error.into()), + } + } +} + #[derive(Debug, Clone, Default)] pub struct SharedMemoryBackend(Arc>>); diff --git a/lib/lib/src/database/triggers.rs b/lib/lib/src/database/triggers.rs index dfa033f8..544e59f8 100644 --- a/lib/lib/src/database/triggers.rs +++ b/lib/lib/src/database/triggers.rs @@ -7,11 +7,11 @@ use redb::{ReadableDatabase, TypeName, Value as RedbValue}; use uuid::Uuid; -use upac_types::DeclarativeTrigger; use upac_types::codec::RedbCodable; +use upac_types::decoder::DeclarativeTrigger; use super::error::DatabaseError; -use super::{MemoryDatabase, PACKAGES_TRIGGERS_TABLE, ReadableSource}; +use super::{MemoryDatabase, PACKAGES_TRIGGERS_TABLE, ReadTransactionExt, ReadableSource}; use crate::layout::database::PACKAGES_TRIGGERS_TYPE_NAME; @@ -27,7 +27,9 @@ pub trait TriggerStoreMut: TriggerStore { impl TriggerStore for T { fn get_declarative_triggers(&self, uuid: Uuid) -> Result, DatabaseError> { let transaction = self.source().begin_read()?; - let triggers = transaction.open_table(PACKAGES_TRIGGERS_TABLE)?; + let Some(triggers) = transaction.open_table_or_none(PACKAGES_TRIGGERS_TABLE)? else { + return Ok(None); + }; Ok(triggers.get(uuid)?.map(|guard| guard.value().0)) } diff --git a/lib/lib/src/deploy/digest.rs b/lib/lib/src/deploy/digest.rs index e75816e9..4aa915d4 100644 --- a/lib/lib/src/deploy/digest.rs +++ b/lib/lib/src/deploy/digest.rs @@ -5,7 +5,8 @@ use linux_kernel_cmdline::utf8::CmdlineOwned; -use crate::deploy::error::SysrootError; +use super::SysrootError; + use crate::layout::deployment::PREFIX_DIGEST_CMDLINE_PARAM; pub fn current_prefix_digest() -> Result { diff --git a/lib/lib/src/deploy/esp.rs b/lib/lib/src/deploy/esp.rs deleted file mode 100644 index 5caae6cb..00000000 --- a/lib/lib/src/deploy/esp.rs +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::path::PathBuf; - -use rsmount::tables::MountInfo; - -use crate::deploy::error::SysrootError; -use crate::layout::boot::{ESP_MOUNT_FALLBACK, ESP_MOUNT_PRIMARY}; - -pub fn find_esp_mount() -> Result { - let mut mount_table = MountInfo::new()?; - mount_table.import_mountinfo()?; - - for candidate_for_mount in [ESP_MOUNT_PRIMARY, ESP_MOUNT_FALLBACK] { - if mount_table.find_target(candidate_for_mount).is_some() { - return Ok(PathBuf::from(candidate_for_mount)); - } - } - - Err(SysrootError::EspNotFound) -} diff --git a/lib/lib/src/deploy/mod.rs b/lib/lib/src/deploy/mod.rs index d4df4582..c0b10df2 100644 --- a/lib/lib/src/deploy/mod.rs +++ b/lib/lib/src/deploy/mod.rs @@ -20,22 +20,26 @@ use rsblkid::utils::evaluation::find_canonical_device_name_from_path; use rsmount::tables::MountInfo; -use self::error::SysrootError; - use upac_types::settings::RuntimeSettings; +use self::digest::current_prefix_digest; +use self::error::SysrootError; + use crate::composefs::error::RepoError; use crate::composefs::repository::{self, ObjectID}; use crate::database::record::DeployRecord; -use crate::deploy::digest::current_prefix_digest; use crate::errors::CommonError; +use crate::layout::boot::{ESP_MOUNT_FALLBACK, ESP_MOUNT_PRIMARY}; use crate::layout::deployment::{DEPLOYS_DIR, NEXT_SEQ_PATH, REPO_DIR, ROOT_DIR, SYSROOT_DIR}; pub mod digest; pub mod error; -pub mod esp; pub mod retention; +#[cfg(test)] +#[path = "../../tests/inline/deploy.rs"] +mod tests; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DeployMode { ReadOnly, @@ -214,3 +218,27 @@ impl Drop for Deploy { let _ = remove_dir(&self.sysroot); } } + +pub fn find_esp_mount() -> Result { + let mut mount_table = MountInfo::new()?; + mount_table.import_mountinfo()?; + + for candidate_for_mount in [ESP_MOUNT_PRIMARY, ESP_MOUNT_FALLBACK] { + if mount_table.find_target(candidate_for_mount).is_some() { + return Ok(PathBuf::from(candidate_for_mount)); + } + } + + Err(SysrootError::EspNotFound) +} + +#[cfg(test)] +impl Deploy { + pub(crate) fn for_testing(deploy_dir: PathBuf) -> Self { + Deploy { + sysroot: deploy_dir.clone(), + deploy: deploy_dir, + repo: PathBuf::new(), + } + } +} diff --git a/lib/lib/src/deploy/retention.rs b/lib/lib/src/deploy/retention.rs index ff4add29..5c02d7f7 100644 --- a/lib/lib/src/deploy/retention.rs +++ b/lib/lib/src/deploy/retention.rs @@ -3,13 +3,15 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::deploy::Deploy; +use upac_types::hook::ProgressEventBuilder; + +use super::Deploy; use crate::errors::CommonError; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; pub struct RetentionStage; diff --git a/lib/lib/src/export/mod.rs b/lib/lib/src/export/mod.rs index c28aa52f..632f9636 100644 --- a/lib/lib/src/export/mod.rs +++ b/lib/lib/src/export/mod.rs @@ -3,10 +3,11 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::ABI_VERSION; -use upac_abi::error::{CError, CommandState, ErrorKind}; +use upac_abi::LIB_ABI_VERSION; +use upac_abi::error::{CError, ErrorKind}; use upac_abi::hook::CancelToken; -use upac_abi::response::CUnmutatedResponse; + +use upac_types::error::CommandState; pub mod mutated; pub mod unmutated; @@ -14,8 +15,8 @@ pub mod unmutated; /// # Safety /// Touches no pointers — `unsafe extern "C"` only to match the ABI calling convention. #[unsafe(no_mangle)] -pub unsafe extern "C" fn version_abi() -> u32 { - ABI_VERSION +pub unsafe extern "C" fn lib_abi_version() -> u32 { + LIB_ABI_VERSION } /// # Safety @@ -29,17 +30,6 @@ pub unsafe extern "C" fn cancel(token: *mut CancelToken) { unsafe { (*token).cancel() }; } -/// # Safety -/// `response`, if non-null, must point to a valid `CUnmutatedResponse` produced by this library -/// that has not already been freed. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn free_response(response: *mut CUnmutatedResponse) { - if response.is_null() { - return; - } - unsafe { (*response).free() }; -} - pub(crate) unsafe fn write_error(err_out: *mut CError, state: S, error: ErrorKind) { if !err_out.is_null() { unsafe { diff --git a/lib/lib/src/export/mutated/commit.rs b/lib/lib/src/export/mutated/commit.rs index cd590eae..e49fe1ca 100644 --- a/lib/lib/src/export/mutated/commit.rs +++ b/lib/lib/src/export/mutated/commit.rs @@ -11,7 +11,7 @@ use upac_abi::request::CCommitRequest; use upac_types::states::CommitStateId; use crate::export::{try_convert_abi, write_error}; -use crate::mutated::commit::CommitData; +use crate::mutated::commit::{CommitData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,14 +20,16 @@ use crate::mutated::commit::CommitData; pub unsafe extern "C" fn commit(request_c: CCommitRequest, err_out: *mut CError) -> i32 { let commit_data = try_convert_abi!(CommitData::try_from(&request_c), err_out, CommitStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::commit::run(commit_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(commit_data))); match result { Ok(Ok(())) => 0, + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, CommitStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/mutated/files.rs b/lib/lib/src/export/mutated/files.rs index 9593e1ea..0c076217 100644 --- a/lib/lib/src/export/mutated/files.rs +++ b/lib/lib/src/export/mutated/files.rs @@ -11,7 +11,7 @@ use upac_abi::request::CFilesRequest; use upac_types::states::FilesStateId; use crate::export::{try_convert_abi, write_error}; -use crate::mutated::files::FilesData; +use crate::mutated::files::{FilesData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,7 +20,7 @@ use crate::mutated::files::FilesData; pub unsafe extern "C" fn files(request_c: CFilesRequest, err_out: *mut CError) -> i32 { let files_data = try_convert_abi!(FilesData::try_from(&request_c), err_out, FilesStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::files::run(files_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(files_data))); match result { Ok(Ok(())) => 0, diff --git a/lib/lib/src/export/mutated/gc.rs b/lib/lib/src/export/mutated/gc.rs index 25713a94..5843e4c6 100644 --- a/lib/lib/src/export/mutated/gc.rs +++ b/lib/lib/src/export/mutated/gc.rs @@ -11,7 +11,7 @@ use upac_abi::request::CGcRequest; use upac_types::states::GcStateId; use crate::export::{try_convert_abi, write_error}; -use crate::mutated::gc::GcData; +use crate::mutated::gc::{GcData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,14 +20,16 @@ use crate::mutated::gc::GcData; pub unsafe extern "C" fn gc(request_c: CGcRequest, err_out: *mut CError) -> i32 { let gc_data = try_convert_abi!(GcData::try_from(&request_c), err_out, GcStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::gc::run(gc_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(gc_data))); match result { Ok(Ok(())) => 0, + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, GcStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/mutated/installer.rs b/lib/lib/src/export/mutated/installer.rs index 6844f40b..25fc7824 100644 --- a/lib/lib/src/export/mutated/installer.rs +++ b/lib/lib/src/export/mutated/installer.rs @@ -11,7 +11,7 @@ use upac_abi::request::CInstallRequest; use upac_types::states::InstallStateId; use crate::export::{try_convert_abi, write_error}; -use crate::mutated::installer::InstallData; +use crate::mutated::installer::{InstallData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,14 +20,16 @@ use crate::mutated::installer::InstallData; pub unsafe extern "C" fn install(request_c: CInstallRequest, err_out: *mut CError) -> i32 { let install_data = try_convert_abi!(InstallData::try_from(&request_c), err_out, InstallStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::installer::run(install_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(install_data))); match result { Ok(Ok(())) => 0, + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, InstallStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/mutated/mime.rs b/lib/lib/src/export/mutated/mime.rs index 9a26bcc6..5db3fa6c 100644 --- a/lib/lib/src/export/mutated/mime.rs +++ b/lib/lib/src/export/mutated/mime.rs @@ -11,7 +11,7 @@ use upac_abi::request::CMimeSyncRequest; use upac_types::states::MimeStateId; use crate::export::{try_convert_abi, write_error}; -use crate::mutated::mime::MimeData; +use crate::mutated::mime::{MimeData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,14 +20,16 @@ use crate::mutated::mime::MimeData; pub unsafe extern "C" fn mime(request_c: CMimeSyncRequest, err_out: *mut CError) -> i32 { let mime_data = try_convert_abi!(MimeData::try_from(&request_c), err_out, MimeStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::mime::run(mime_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(mime_data))); match result { Ok(Ok(())) => 0, + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, MimeStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/mutated/pin.rs b/lib/lib/src/export/mutated/pin.rs index 9e07965e..dd96ed26 100644 --- a/lib/lib/src/export/mutated/pin.rs +++ b/lib/lib/src/export/mutated/pin.rs @@ -11,7 +11,7 @@ use upac_abi::request::CPinRequest; use upac_types::states::PinStateId; use crate::export::{try_convert_abi, write_error}; -use crate::mutated::pin::PinData; +use crate::mutated::pin::{PinData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,14 +20,16 @@ use crate::mutated::pin::PinData; pub unsafe extern "C" fn pin_deploy(request_c: CPinRequest, err_out: *mut CError) -> i32 { let pin_data = try_convert_abi!(PinData::try_from(&request_c), err_out, PinStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::pin::run(pin_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(pin_data))); match result { Ok(Ok(())) => 0, + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, PinStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/mutated/rollback.rs b/lib/lib/src/export/mutated/rollback.rs index d5570724..614c43ac 100644 --- a/lib/lib/src/export/mutated/rollback.rs +++ b/lib/lib/src/export/mutated/rollback.rs @@ -11,7 +11,7 @@ use upac_abi::request::CRollbackRequest; use upac_types::states::RollbackStateId; use crate::export::{try_convert_abi, write_error}; -use crate::mutated::rollback::RollbackData; +use crate::mutated::rollback::{RollbackData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,14 +20,16 @@ use crate::mutated::rollback::RollbackData; pub unsafe extern "C" fn rollback(request_c: CRollbackRequest, err_out: *mut CError) -> i32 { let rollback_data = try_convert_abi!(RollbackData::try_from(&request_c), err_out, RollbackStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::rollback::run(rollback_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(rollback_data))); match result { Ok(Ok(())) => 0, + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, RollbackStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/mutated/uninstaller.rs b/lib/lib/src/export/mutated/uninstaller.rs index ce2b5a14..f7f6627c 100644 --- a/lib/lib/src/export/mutated/uninstaller.rs +++ b/lib/lib/src/export/mutated/uninstaller.rs @@ -11,7 +11,7 @@ use upac_abi::request::CUninstallRequest; use upac_types::states::UninstallStateId; use crate::export::{try_convert_abi, write_error}; -use crate::mutated::uninstaller::UninstallData; +use crate::mutated::uninstaller::{UninstallData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,14 +20,16 @@ use crate::mutated::uninstaller::UninstallData; pub unsafe extern "C" fn uninstall(request_c: CUninstallRequest, err_out: *mut CError) -> i32 { let uninstall_data = try_convert_abi!(UninstallData::try_from(&request_c), err_out, UninstallStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::uninstaller::run(uninstall_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(uninstall_data))); match result { Ok(Ok(())) => 0, + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, UninstallStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/mutated/update.rs b/lib/lib/src/export/mutated/update.rs index 3a767191..23d90aae 100644 --- a/lib/lib/src/export/mutated/update.rs +++ b/lib/lib/src/export/mutated/update.rs @@ -11,7 +11,7 @@ use upac_abi::request::CUpdateRequest; use upac_types::states::UpdateStateId; use crate::export::{try_convert_abi, write_error}; -use crate::mutated::update::UpdateData; +use crate::mutated::update::{UpdateData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -20,14 +20,16 @@ use crate::mutated::update::UpdateData; pub unsafe extern "C" fn update(request_c: CUpdateRequest, err_out: *mut CError) -> i32 { let update_data = try_convert_abi!(UpdateData::try_from(&request_c), err_out, UpdateStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::mutated::update::run(update_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(update_data))); match result { Ok(Ok(())) => 0, + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, UpdateStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/diff.rs b/lib/lib/src/export/unmutated/diff.rs index e51f0b34..db6554e0 100644 --- a/lib/lib/src/export/unmutated/diff.rs +++ b/lib/lib/src/export/unmutated/diff.rs @@ -7,13 +7,12 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CDiffRequest; -use upac_abi::response::{CDiffPackageEntry, CDiffResponse, CDiffUntrackedFileEntry}; -use upac_abi::types::{COwned, CVec}; +use upac_abi::response::CDiffResponse; use upac_types::states::DiffStateId; use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::diff::DiffData; +use crate::unmutated::diff::{DiffData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -23,29 +22,21 @@ use crate::unmutated::diff::DiffData; pub unsafe extern "C" fn diff(request_c: CDiffRequest, response_out: *mut CDiffResponse, err_out: *mut CError) -> i32 { let diff_data = try_convert_abi!(DiffData::try_from(&request_c), err_out, DiffStateId); - let result = catch_unwind(AssertUnwindSafe(|| crate::unmutated::diff::run(diff_data))); + let result = catch_unwind(AssertUnwindSafe(|| run(diff_data))); match result { - Ok(Ok((diff_packages, unattached_files))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CDiffResponse::new( - CVec::from_owned(diff_packages.into_iter().map(CDiffPackageEntry::from).collect()), - CVec::from_owned( - unattached_files - .into_iter() - .map(CDiffUntrackedFileEntry::from) - .collect(), - ), - ); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, DiffStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/diff_config.rs b/lib/lib/src/export/unmutated/diff_config.rs index 5d33e01d..f6cdaaf8 100644 --- a/lib/lib/src/export/unmutated/diff_config.rs +++ b/lib/lib/src/export/unmutated/diff_config.rs @@ -7,13 +7,12 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CDiffConfigRequest; -use upac_abi::response::{CDiffConfigFileEntry, CDiffConfigResponse}; -use upac_abi::types::{COwned, CVec}; +use upac_abi::response::CDiffConfigResponse; use upac_types::states::DiffConfigStateId; use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::diff_config::DiffConfigData; +use crate::unmutated::diff_config::{DiffConfigData, run}; /// # Safety /// Any borrowed byte-slice fields inside `request_c` must remain valid for the duration of the @@ -25,25 +24,21 @@ pub unsafe extern "C" fn diff_config( ) -> i32 { let diff_config_data = try_convert_abi!(DiffConfigData::try_from(&request_c), err_out, DiffConfigStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::diff_config::run(diff_config_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(diff_config_data))); match result { - Ok(Ok((files,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CDiffConfigResponse::new(CVec::from_owned( - files.into_iter().map(CDiffConfigFileEntry::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, DiffConfigStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/diff_packages.rs b/lib/lib/src/export/unmutated/diff_packages.rs index 3544a928..f9a8c920 100644 --- a/lib/lib/src/export/unmutated/diff_packages.rs +++ b/lib/lib/src/export/unmutated/diff_packages.rs @@ -7,11 +7,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CDiffPackagesRequest; -use upac_abi::response::{CDiffPackageEntry, CDiffPackagesResponse}; -use upac_abi::types::{COwned, CVec}; +use upac_abi::response::CDiffPackagesResponse; use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::diff_packages::DiffPackagesData; +use crate::unmutated::diff_packages::{DiffPackagesData, run}; use upac_types::states::DiffPackagesStateId; @@ -25,25 +24,21 @@ pub unsafe extern "C" fn diff_packages( ) -> i32 { let diff_packages_data = try_convert_abi!(DiffPackagesData::try_from(&request_c), err_out, DiffPackagesStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::diff_packages::run(diff_packages_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(diff_packages_data))); match result { - Ok(Ok((diff_packages,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CDiffPackagesResponse::new(CVec::from_owned( - diff_packages.into_iter().map(CDiffPackageEntry::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, DiffPackagesStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/diff_prefix.rs b/lib/lib/src/export/unmutated/diff_prefix.rs index 904d7863..57bda4d4 100644 --- a/lib/lib/src/export/unmutated/diff_prefix.rs +++ b/lib/lib/src/export/unmutated/diff_prefix.rs @@ -7,11 +7,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CDiffPrefixRequest; -use upac_abi::response::{CDiffPrefixFileEntry, CDiffPrefixResponse}; -use upac_abi::types::{COwned, CVec}; +use upac_abi::response::CDiffPrefixResponse; use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::diff_prefix::DiffPrefixData; +use crate::unmutated::diff_prefix::{DiffPrefixData, run}; use upac_types::states::DiffPrefixStateId; @@ -25,25 +24,21 @@ pub unsafe extern "C" fn diff_prefix( ) -> i32 { let diff_prefix_data = try_convert_abi!(DiffPrefixData::try_from(&request_c), err_out, DiffPrefixStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::diff_prefix::run(diff_prefix_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(diff_prefix_data))); match result { - Ok(Ok((files,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CDiffPrefixResponse::new(CVec::from_owned( - files.into_iter().map(CDiffPrefixFileEntry::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, DiffPrefixStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/list_config.rs b/lib/lib/src/export/unmutated/list_config.rs index c0cd50f9..056c2f9a 100644 --- a/lib/lib/src/export/unmutated/list_config.rs +++ b/lib/lib/src/export/unmutated/list_config.rs @@ -7,11 +7,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CListConfigRequest; -use upac_abi::response::{CConfigCommitEntry, CListConfigResponse}; -use upac_abi::types::{COwned, CVec}; +use upac_abi::response::CListConfigResponse; use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::list_config::ListConfigData; +use crate::unmutated::list_config::{ListConfigData, run}; use upac_types::states::ListConfigStateId; @@ -25,25 +24,21 @@ pub unsafe extern "C" fn list_config( ) -> i32 { let list_config_data = try_convert_abi!(ListConfigData::try_from(&request_c), err_out, ListConfigStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::list_config::run(list_config_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(list_config_data))); match result { - Ok(Ok((commits,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CListConfigResponse::new(CVec::from_owned( - commits.into_iter().map(CConfigCommitEntry::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, ListConfigStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/list_history.rs b/lib/lib/src/export/unmutated/list_history.rs index ea8a0a63..ed3f0fdb 100644 --- a/lib/lib/src/export/unmutated/list_history.rs +++ b/lib/lib/src/export/unmutated/list_history.rs @@ -7,11 +7,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CListHistoryRequest; -use upac_abi::response::{CHistoryEntry, CListHistoryResponse}; -use upac_abi::types::{COwned, CVec}; +use upac_abi::response::CListHistoryResponse; use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::list_history::ListHistoryData; +use crate::unmutated::list_history::{ListHistoryData, run}; use upac_types::states::ListHistoryStateId; @@ -25,25 +24,21 @@ pub unsafe extern "C" fn list_history( ) -> i32 { let list_history_data = try_convert_abi!(ListHistoryData::try_from(&request_c), err_out, ListHistoryStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::list_history::run(list_history_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(list_history_data))); match result { - Ok(Ok((history,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CListHistoryResponse::new(CVec::from_owned( - history.into_iter().map(CHistoryEntry::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, ListHistoryStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/list_packages.rs b/lib/lib/src/export/unmutated/list_packages.rs index 442aa115..dbf0dfc5 100644 --- a/lib/lib/src/export/unmutated/list_packages.rs +++ b/lib/lib/src/export/unmutated/list_packages.rs @@ -6,13 +6,11 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; -use upac_abi::package::CPackageMeta; use upac_abi::request::CListPackagesRequest; use upac_abi::response::CListPackagesResponse; -use upac_abi::types::{COwned, CVec}; use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::list_packages::ListPackagesData; +use crate::unmutated::list_packages::{ListPackagesData, run}; use upac_types::states::ListPackagesStateId; @@ -26,25 +24,21 @@ pub unsafe extern "C" fn list_packages( ) -> i32 { let list_packages_data = try_convert_abi!(ListPackagesData::try_from(&request_c), err_out, ListPackagesStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::list_packages::run(list_packages_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(list_packages_data))); match result { - Ok(Ok((metas,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CListPackagesResponse::new(CVec::from_owned( - metas.into_iter().map(CPackageMeta::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, ListPackagesStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/list_prefix.rs b/lib/lib/src/export/unmutated/list_prefix.rs index 5a2adbc0..2e519f2a 100644 --- a/lib/lib/src/export/unmutated/list_prefix.rs +++ b/lib/lib/src/export/unmutated/list_prefix.rs @@ -7,11 +7,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CListPrefixRequest; -use upac_abi::response::{CListPrefixResponse, CPrefixEntry}; -use upac_abi::types::{COwned, CVec}; +use upac_abi::response::CListPrefixResponse; use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::list_prefix::ListPrefixData; +use crate::unmutated::list_prefix::{ListPrefixData, run}; use upac_types::states::ListPrefixStateId; @@ -25,25 +24,21 @@ pub unsafe extern "C" fn list_prefix( ) -> i32 { let list_prefix_data = try_convert_abi!(ListPrefixData::try_from(&request_c), err_out, ListPrefixStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::list_prefix::run(list_prefix_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(list_prefix_data))); match result { - Ok(Ok((prefixes,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CListPrefixResponse::new(CVec::from_owned( - prefixes.into_iter().map(CPrefixEntry::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, ListPrefixStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/search_files.rs b/lib/lib/src/export/unmutated/search_files.rs index 6e4ff0e6..7c29dd83 100644 --- a/lib/lib/src/export/unmutated/search_files.rs +++ b/lib/lib/src/export/unmutated/search_files.rs @@ -7,11 +7,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CSearchFilesRequest; -use upac_abi::response::{CSearchFileEntry, CSearchFilesResponse}; -use upac_abi::types::{COwned, CVec}; +use upac_abi::response::CSearchFilesResponse; use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::search_files::SearchFilesData; +use crate::unmutated::search_files::{SearchFilesData, run}; use upac_types::states::SearchFilesStateId; @@ -25,25 +24,21 @@ pub unsafe extern "C" fn search_files( ) -> i32 { let search_files_data = try_convert_abi!(SearchFilesData::try_from(&request_c), err_out, SearchFilesStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::search_files::run(search_files_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(search_files_data))); match result { - Ok(Ok((files,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CSearchFilesResponse::new(CVec::from_owned( - files.into_iter().map(CSearchFileEntry::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, SearchFilesStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/search_in_meta.rs b/lib/lib/src/export/unmutated/search_in_meta.rs index 20ee402a..a0396b77 100644 --- a/lib/lib/src/export/unmutated/search_in_meta.rs +++ b/lib/lib/src/export/unmutated/search_in_meta.rs @@ -6,13 +6,11 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; -use upac_abi::package::CPackageMeta; use upac_abi::request::CSearchInMetaRequest; use upac_abi::response::CSearchInMetaResponse; -use upac_abi::types::{COwned, CVec}; use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::search_in_meta::SearchInMetaData; +use crate::unmutated::search_in_meta::{SearchInMetaData, run}; use upac_types::states::SearchInMetaStateId; @@ -26,25 +24,21 @@ pub unsafe extern "C" fn search_in_meta( ) -> i32 { let search_in_meta_data = try_convert_abi!(SearchInMetaData::try_from(&request_c), err_out, SearchInMetaStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::search_in_meta::run(search_in_meta_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(search_in_meta_data))); match result { - Ok(Ok((metas,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CSearchInMetaResponse::new(CVec::from_owned( - metas.into_iter().map(CPackageMeta::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, SearchInMetaStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/search_in_package_files.rs b/lib/lib/src/export/unmutated/search_in_package_files.rs index 1c2e1aa3..0dedaa3c 100644 --- a/lib/lib/src/export/unmutated/search_in_package_files.rs +++ b/lib/lib/src/export/unmutated/search_in_package_files.rs @@ -7,11 +7,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; use upac_abi::request::CSearchInPackageFilesRequest; -use upac_abi::response::{CSearchFileEntry, CSearchInPackageFilesResponse}; -use upac_abi::types::{COwned, CVec}; +use upac_abi::response::CSearchInPackageFilesResponse; use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::search_in_package_files::SearchInPackageFilesData; +use crate::unmutated::search_in_package_files::{SearchInPackageFilesData, run}; use upac_types::states::SearchInPackageFilesStateId; @@ -29,25 +28,21 @@ pub unsafe extern "C" fn search_in_package_files( SearchInPackageFilesStateId ); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::search_in_package_files::run(search_in_package_files_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(search_in_package_files_data))); match result { - Ok(Ok((files,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = CSearchInPackageFilesResponse::new(CVec::from_owned( - files.into_iter().map(CSearchFileEntry::from).collect(), - )); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, SearchInPackageFilesStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/export/unmutated/search_meta.rs b/lib/lib/src/export/unmutated/search_meta.rs index e8b211c6..8c140513 100644 --- a/lib/lib/src/export/unmutated/search_meta.rs +++ b/lib/lib/src/export/unmutated/search_meta.rs @@ -6,13 +6,11 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use upac_abi::error::{CError, ErrorKind}; -use upac_abi::package::CPackageMeta; use upac_abi::request::CSearchMetaRequest; use upac_abi::response::CSearchMetaResponse; -use upac_abi::types::{COwned, CVec}; use crate::export::{try_convert_abi, write_error}; -use crate::unmutated::search_meta::SearchMetaData; +use crate::unmutated::search_meta::{SearchMetaData, run}; use upac_types::states::SearchMetaStateId; @@ -26,24 +24,21 @@ pub unsafe extern "C" fn search_meta( ) -> i32 { let search_meta_data = try_convert_abi!(SearchMetaData::try_from(&request_c), err_out, SearchMetaStateId); - let result = catch_unwind(AssertUnwindSafe(|| { - crate::unmutated::search_meta::run(search_meta_data) - })); + let result = catch_unwind(AssertUnwindSafe(|| run(search_meta_data))); match result { - Ok(Ok((metas,))) => { + Ok(Ok(response)) => { if !response_out.is_null() { - unsafe { - *response_out = - CSearchMetaResponse::new(CVec::from_owned(metas.into_iter().map(CPackageMeta::from).collect())); - } + unsafe { *response_out = response.into() }; } 0 } + Ok(Err((state, error))) => { unsafe { write_error(err_out, state, ErrorKind::from(error)) }; -1 } + Err(_) => { unsafe { write_error(err_out, SearchMetaStateId::Setup, ErrorKind::Unexpected) }; -1 diff --git a/lib/lib/src/lock.rs b/lib/lib/src/lock.rs index 89b32c03..0c2bc6ee 100644 --- a/lib/lib/src/lock.rs +++ b/lib/lib/src/lock.rs @@ -10,7 +10,7 @@ use nix::sys::socket::{AddressFamily, SockFlag, SockType, UnixAddr, bind, socket use upac_abi::error::ErrorKind; -use crate::layout::runtime; +use crate::layout::runtime::LOCK_ADDRESS; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LockError { @@ -51,7 +51,7 @@ pub struct Lock { impl Lock { pub fn acquire() -> Result { let socket = socket(AddressFamily::Unix, SockType::Stream, SockFlag::SOCK_CLOEXEC, None)?; - let address = UnixAddr::new_abstract(runtime::LOCK_ADDRESS.as_bytes())?; + let address = UnixAddr::new_abstract(LOCK_ADDRESS.as_bytes())?; bind(socket.as_raw_fd(), &address)?; diff --git a/lib/lib/src/mutated/commit/mod.rs b/lib/lib/src/mutated/commit/mod.rs index 10eba377..3f139059 100644 --- a/lib/lib/src/mutated/commit/mod.rs +++ b/lib/lib/src/mutated/commit/mod.rs @@ -5,27 +5,34 @@ use std::os::raw::c_void; +use upac_types::TmpPath; +use upac_types::hook::Message; +use upac_types::states::CommitStateId; +use upac_types::traits::MessageHook; + +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CCommitRequest; -pub use self::error::CommitError; - use self::transaction::TransactionStage; use crate::deploy::retention::RetentionStage; use crate::deploy::{Deploy, DeployMode}; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; use crate::scripts::HookStage; use crate::scripts::pipeline::{Operation, PipelineTrigger}; -use upac_types::TmpPath; -use upac_types::states::CommitStateId; + +pub use self::error::CommitError; mod error; mod transaction; -pub(crate) struct Subject(pub String); -pub(crate) struct CommitMessage(pub Option); +pub(crate) struct CommitInfo { + pub subject: String, + pub message: Option, +} pub struct CommitData<'a> { pub tmp_path: &'a str, @@ -45,7 +52,7 @@ impl<'a> TryFrom<&'a CCommitRequest> for CommitData<'a> { fn try_from(request: &'a CCommitRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(CommitData { tmp_path: (&request.tmp_path).try_into()?, @@ -68,8 +75,10 @@ pub fn run(data: CommitData) -> Result<(), (CommitStateId, CommitError)> { let mut context = Context::new(); context.put(deploy); context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Subject(data.subject.to_owned())); - context.put(CommitMessage(data.message.map(str::to_owned))); + context.put(CommitInfo { + subject: data.subject.to_owned(), + message: data.message.map(str::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/commit/transaction.rs b/lib/lib/src/mutated/commit/transaction.rs index f7c3bbd9..230c452e 100644 --- a/lib/lib/src/mutated/commit/transaction.rs +++ b/lib/lib/src/mutated/commit/transaction.rs @@ -6,7 +6,10 @@ use composefs::fsverity::FsVerityHashValue; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; +use upac_types::hook::ProgressEventBuilder; + +use super::{CommitError, CommitInfo}; use crate::composefs::overlay::apply_overlay_upper; use crate::composefs::repository::commit_tree; @@ -14,9 +17,8 @@ use crate::database::record::DeployRecord; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::deployment::CONFIG_DIR_NAME; -use crate::mutated::commit::{CommitError, CommitMessage, Subject}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; pub struct TransactionStage; @@ -25,8 +27,7 @@ impl Stage for TransactionStage { &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), CommitError> { let deploy = ctx_get!(context, Deploy); - let subject = ctx_get!(context, Subject); - let message = ctx_get!(context, CommitMessage); + let commit_info = ctx_get!(context, CommitInfo); let repository = deploy.open_repository()?; @@ -49,8 +50,8 @@ impl Stage for TransactionStage { written.extend(record_deploy.update_working_config( ¤t_record_dir, new_config_digest, - subject.0.clone(), - message.0.clone(), + commit_info.subject.clone(), + commit_info.message.clone(), )?); Ok((progress, StageResult::Advance, Box::new(written))) diff --git a/lib/lib/src/mutated/files/apply.rs b/lib/lib/src/mutated/files/apply.rs index c8c7d1ff..122adf48 100644 --- a/lib/lib/src/mutated/files/apply.rs +++ b/lib/lib/src/mutated/files/apply.rs @@ -11,10 +11,13 @@ use composefs::generic_tree::Stat; use composefs::repository::{ImportContext, Repository}; use composefs::tree::FileSystem; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_abi::{DiffFileSource, FileDiffKind}; -use upac_types::{FileEntry, FileEntryScope}; +use upac_types::entry::{FileEntry, FileEntryScope}; +use upac_types::hook::ProgressEventBuilder; + +use super::{ApplyTarget, FileProgress, FilesError, RequestedFileOperation, WorkingState}; use crate::composefs::error::RepoError; use crate::composefs::file::{FileHandle, stat_from_metadata}; @@ -23,12 +26,8 @@ use crate::database::files::FileStoreMut; use crate::deploy::Deploy; use crate::errors::CommonError; use crate::layout::deployment::LIVE_ETC_DIR; -use crate::mutated::files::{ - EtcUpperDir, FilesError, PendingFiles, RequestedFileKind, RequestedFileScope, TargetUuid, TotalFiles, - WorkingDatabase, WorkingTree, -}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct ApplyFileStage; @@ -36,33 +35,30 @@ impl Stage for ApplyFileStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), FilesError> { - let mut pending_files = ctx_take!(context, PendingFiles); - let mut woking_files_tree = ctx_take!(context, WorkingTree); - let mut woking_database = ctx_take!(context, WorkingDatabase); - let mut import_ctx = ctx_take!(context, ImportContext); - - let config_upper_dir = ctx_get!(context, EtcUpperDir); - let uuid = ctx_get!(context, TargetUuid); - let file_kind = ctx_get!(context, RequestedFileKind); - let scope = ctx_get!(context, RequestedFileScope); - let total_files = ctx_get!(context, TotalFiles); + let mut file_progress = ctx_take!(context, FileProgress); + let mut woking_state = ctx_take!(context, WorkingState); + let mut imported_ctx = ctx_take!(context, ImportContext); + + let apply_target = ctx_get!(context, ApplyTarget); + let file_operation = ctx_get!(context, RequestedFileOperation); + let deploy = ctx_get!(context, Deploy); - let path = pending_files.0.pop_front().ok_or(CommonError::MissingResult)?; + let path = file_progress.pending.pop_front().ok_or(CommonError::MissingResult)?; - match scope.0 { + match file_operation.scope { DiffFileSource::Prefix => { let repository = deploy.open_repository()?; - match file_kind.0 { + match file_operation.kind { FileDiffKind::Removed => { - FileHandle::new(&path).remove_in_tree(&mut woking_files_tree.0)?; - woking_database.0.remove_user_file(uuid.0, &path)?; + FileHandle::new(&path).remove_in_tree(&mut woking_state.tree)?; + woking_state.database.remove_user_file(apply_target.uuid, &path)?; } FileDiffKind::Added | FileDiffKind::Modified => { - Self::add_file(&path, &repository, &mut woking_files_tree.0, &mut import_ctx)?; - woking_database.0.insert_package_file( - uuid.0, + Self::add_file(&path, &repository, &mut woking_state.tree, &mut imported_ctx)?; + woking_state.database.insert_package_file( + apply_target.uuid, &FileEntry { path: path.clone(), is_user: true, @@ -72,15 +68,15 @@ impl Stage for ApplyFileStage { } } } - DiffFileSource::Config => match file_kind.0 { + DiffFileSource::Config => match file_operation.kind { FileDiffKind::Removed => { - remove_file(config_upper_dir.0.join(&path)).map_err(RepoError::from)?; - woking_database.0.remove_user_file(uuid.0, &path)?; + remove_file(apply_target.config_upper_dir.join(&path)).map_err(RepoError::from)?; + woking_state.database.remove_user_file(apply_target.uuid, &path)?; } FileDiffKind::Added | FileDiffKind::Modified => { - Self::add_config_file(&path, &config_upper_dir.0)?; - woking_database.0.insert_package_file( - uuid.0, + Self::add_config_file(&path, &apply_target.config_upper_dir)?; + woking_state.database.insert_package_file( + apply_target.uuid, &FileEntry { path: path.clone(), is_user: true, @@ -91,20 +87,19 @@ impl Stage for ApplyFileStage { }, } - let remaining = pending_files.0.len() as u64; - let processed = total_files.0 - remaining; - progress = progress.subject(path).progress(processed, total_files.0); + let remaining = file_progress.pending.len() as u64; + let processed = file_progress.total - remaining; + progress = progress.subject(path).progress(processed, file_progress.total); - let result = if pending_files.0.is_empty() { + let result = if file_progress.pending.is_empty() { StageResult::Advance } else { StageResult::Repeat }; - context.put(pending_files); - context.put(woking_files_tree); - context.put(woking_database); - context.put(import_ctx); + context.put(file_progress); + context.put(woking_state); + context.put(imported_ctx); Ok((progress, result, Box::new(NoRollback))) } @@ -112,7 +107,8 @@ impl Stage for ApplyFileStage { impl ApplyFileStage { fn add_file( - path: &str, repository: &Repository, tree: &mut FileSystem, import_ctx: &mut ImportContext, + path: &str, repository: &Repository, tree: &mut FileSystem, + imported_ctx: &mut ImportContext, ) -> Result<(), FilesError> { let source_path = Path::new(path); let metadata = symlink_metadata(source_path).map_err(RepoError::from)?; @@ -143,17 +139,17 @@ impl ApplyFileStage { tree, &File::open(source_path).map_err(RepoError::from)?, stat, - import_ctx, + imported_ctx, )?; } Ok(()) } - fn add_config_file(path: &str, etc_upper_dir: &Path) -> Result<(), FilesError> { + fn add_config_file(path: &str, config_upper_dir: &Path) -> Result<(), FilesError> { let live_path = Path::new(LIVE_ETC_DIR).join(path); let metadata = symlink_metadata(&live_path).map_err(RepoError::from)?; - let dest_path = etc_upper_dir.join(path); + let dest_path = config_upper_dir.join(path); if let Some(parent) = dest_path.parent() { create_dir_all(parent).map_err(RepoError::from)?; diff --git a/lib/lib/src/mutated/files/checkout.rs b/lib/lib/src/mutated/files/checkout.rs index 88e5a318..5acff554 100644 --- a/lib/lib/src/mutated/files/checkout.rs +++ b/lib/lib/src/mutated/files/checkout.rs @@ -3,17 +3,16 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; +use upac_types::hook::ProgressEventBuilder; use crate::boot::write_boot_entry; use crate::composefs::repository::object_id_from_hex; -use crate::deploy::Deploy; -use crate::deploy::esp::find_esp_mount; -use crate::layout::boot_plugins::{BOOT_PLUGINS_DIR, MANIFEST_EXTENSION}; +use crate::deploy::{Deploy, find_esp_mount}; use crate::mutated::files::{FilesError, NewPrefixDigest, RequestedBootPlugin, ResolvedBootEntry}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::plugin::boot::resolve_boot_plugin; +use crate::plugin::boot::BootPlugins; pub struct CheckoutStage; @@ -22,21 +21,19 @@ impl Stage for CheckoutStage { &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), FilesError> { let new_prefix = ctx_get!(context, NewPrefixDigest); + let deploy = ctx_get!(context, Deploy); + let requested_boot_plugins = ctx_get!(context, RequestedBootPlugin); let repository = deploy.open_repository()?; - let deploy_tree = deploy.open_tree(&new_prefix.0)?; - let digest = object_id_from_hex(&new_prefix.0)?; + let deploy_tree = deploy.open_tree(&new_prefix)?; + let digest = object_id_from_hex(&new_prefix)?; let esp_mount = find_esp_mount()?; - let entry_name = write_boot_entry(&repository, &deploy_tree, digest, &esp_mount, &new_prefix.0)?; + let entry_name = write_boot_entry(&repository, &deploy_tree, digest, &esp_mount, &new_prefix)?; - let plugin = resolve_boot_plugin( - BOOT_PLUGINS_DIR, - MANIFEST_EXTENSION, - requested_boot_plugins.0.as_deref(), - )?; + let plugin = BootPlugins::new()?.load(&requested_boot_plugins)?; context.put(ResolvedBootEntry { plugin, entry_name }); diff --git a/lib/lib/src/mutated/files/commit.rs b/lib/lib/src/mutated/files/commit.rs index 60a05111..227f9dab 100644 --- a/lib/lib/src/mutated/files/commit.rs +++ b/lib/lib/src/mutated/files/commit.rs @@ -10,9 +10,12 @@ use composefs::fsverity::FsVerityHashValue; use composefs::generic_tree::Stat; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_types::TmpPath; +use upac_types::hook::ProgressEventBuilder; + +use super::{CommitInfo, FilesError, NewPrefixDigest, WorkingState}; use crate::composefs::error::RepoError; use crate::composefs::file::FileHandle; @@ -23,9 +26,8 @@ use crate::database::record::DeployRecord; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::database::{DATABASE_PATH, FILES_SCRATCH_FILENAME}; -use crate::mutated::files::{CommitMessage, FilesError, NewPrefixDigest, Subject, WorkingDatabase, WorkingTree}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct CommitTransactionStage; @@ -33,19 +35,18 @@ impl Stage for CommitTransactionStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), FilesError> { - let working_tree = ctx_take!(context, WorkingTree); - let working_database = ctx_take!(context, WorkingDatabase); - let mut import_ctx = ctx_take!(context, ImportContext); + let working_state = ctx_take!(context, WorkingState); + let mut imported_ctx = ctx_take!(context, ImportContext); + let commit_info = ctx_get!(context, CommitInfo); let tmp_path = ctx_get!(context, TmpPath); + let deploy = ctx_get!(context, Deploy); - let subject = ctx_get!(context, Subject); - let message = ctx_get!(context, CommitMessage); let repository = deploy.open_repository()?; - let mut tree = working_tree.0; + let mut tree = working_state.tree; - let database_bytes = working_database.0.into_bytes()?; + let database_bytes = working_state.database.into_bytes()?; let database_scratch_path = Path::new(tmp_path.as_ref()).join(FILES_SCRATCH_FILENAME); write(&database_scratch_path, &database_bytes).map_err(RepoError::from)?; @@ -54,7 +55,7 @@ impl Stage for CommitTransactionStage { &mut tree, &File::open(&database_scratch_path).map_err(RepoError::from)?, Stat::uninitialized(), - &mut import_ctx, + &mut imported_ctx, )?; let digest = commit_tree(&repository, tree)?; @@ -71,8 +72,8 @@ impl Stage for CommitTransactionStage { let record = DeployRecord { prefix_digest: new_prefix.clone(), - subject: subject.0.clone(), - message: message.0.clone(), + subject: commit_info.subject.clone(), + message: commit_info.message.clone(), seq: DeployRecord::allocate_seq(&deploy.next_seq_path())?, timestamp: DeployRecord::now_secs(), config_history: current_record.config_history.clone(), diff --git a/lib/lib/src/mutated/files/error.rs b/lib/lib/src/mutated/files/error.rs index 49bf8919..0f5a6c64 100644 --- a/lib/lib/src/mutated/files/error.rs +++ b/lib/lib/src/mutated/files/error.rs @@ -16,6 +16,10 @@ use crate::errors::{ use crate::lock::LockError; use crate::plugin::boot::error::BootPluginError; +#[cfg(test)] +#[path = "../../../tests/inline/mutated_files_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum FilesError { PackageNotFound, diff --git a/lib/lib/src/mutated/files/mod.rs b/lib/lib/src/mutated/files/mod.rs index 0e169a25..4bde5b85 100644 --- a/lib/lib/src/mutated/files/mod.rs +++ b/lib/lib/src/mutated/files/mod.rs @@ -11,13 +11,19 @@ use composefs::tree::FileSystem; use uuid::Uuid; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::package::CPackageInfo; use upac_abi::request::CFilesRequest; use upac_abi::{DiffFileSource, FileDiffKind}; -pub use self::error::FilesError; +use upac_types::TmpPath; +use upac_types::hook::Message; +use upac_types::states::FilesStateId; +use upac_types::traits::MessageHook; + +use upac_macro::ContextValue; use self::apply::ApplyFileStage; use self::checkout::CheckoutStage; @@ -29,12 +35,13 @@ use crate::composefs::repository::ObjectID; use crate::database::MemoryDatabase; use crate::deploy::retention::RetentionStage; use crate::deploy::{Deploy, DeployMode}; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; use crate::plugin::boot::BootPlugin; use crate::scripts::HookStage; use crate::scripts::pipeline::{Operation, PipelineTrigger}; -use upac_types::TmpPath; -use upac_types::states::FilesStateId; + +pub use self::error::FilesError; mod apply; mod checkout; @@ -43,28 +50,45 @@ mod error; mod open; mod swap; -pub(crate) struct RequestedFileKind(pub FileDiffKind); -pub(crate) struct RequestedFileScope(pub DiffFileSource); +pub(crate) struct RequestedFileOperation { + pub kind: FileDiffKind, + pub scope: DiffFileSource, +} pub(crate) struct RequestedFilePackage { pub name: String, pub arch: String, pub arch_sub: Option, } + +#[derive(ContextValue)] pub(crate) struct NewPrefixDigest(pub String); -pub(crate) struct Subject(pub String); -pub(crate) struct CommitMessage(pub Option); -pub(crate) struct RequestedBootPlugin(pub Option); + +pub(crate) struct CommitInfo { + pub subject: String, + pub message: Option, +} + +#[derive(ContextValue)] +pub(crate) struct RequestedBootPlugin(pub String); pub(crate) struct ResolvedBootEntry { pub plugin: BootPlugin, pub entry_name: String, } -pub(crate) struct PendingFiles(pub VecDeque); -pub(crate) struct TotalFiles(pub u64); -pub(crate) struct WorkingTree(pub FileSystem); -pub(crate) struct WorkingDatabase(pub MemoryDatabase); -pub(crate) struct TargetUuid(pub Uuid); -pub(crate) struct EtcUpperDir(pub PathBuf); +pub(crate) struct FileProgress { + pub pending: VecDeque, + pub total: u64, +} + +pub(crate) struct WorkingState { + pub tree: FileSystem, + pub database: MemoryDatabase, +} + +pub(crate) struct ApplyTarget { + pub uuid: Uuid, + pub config_upper_dir: PathBuf, +} pub struct FilesPackage<'a> { pub name: &'a str, @@ -87,11 +111,13 @@ impl<'a> TryFrom<&'a CPackageInfo> for FilesPackage<'a> { } pub struct FilesData<'a> { + pub scope: DiffFileSource, + pub files: Vec<&'a str>, pub file_kind: FileDiffKind, - pub scope: DiffFileSource, pub file_package: FilesPackage<'a>, - pub boot_plugin: Option<&'a str>, + + pub boot_plugin: &'a str, pub tmp_path: &'a str, @@ -111,13 +137,16 @@ impl<'a> TryFrom<&'a CFilesRequest> for FilesData<'a> { unsafe { request.validate()? }; let file_package = unsafe { request.file_package.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(FilesData { + scope: request.scope, + files: Vec::try_from(&request.files)?, file_kind: request.file_kind, - scope: request.scope, file_package: FilesPackage::try_from(file_package)?, + boot_plugin: (&request.boot_plugin).try_into()?, tmp_path: (&request.tmp_path).try_into()?, @@ -144,17 +173,21 @@ pub fn run(data: FilesData) -> Result<(), (FilesStateId, FilesError)> { .map(|path| (*path).to_owned()) .collect::>(), ); - context.put(RequestedFileKind(data.file_kind)); - context.put(RequestedFileScope(data.scope)); + context.put(RequestedFileOperation { + kind: data.file_kind, + scope: data.scope, + }); context.put(RequestedFilePackage { name: data.file_package.name.to_owned(), arch: data.file_package.arch.to_owned(), arch_sub: data.file_package.arch_sub.map(str::to_owned), }); context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Subject(data.subject.to_owned())); - context.put(CommitMessage(data.message.map(str::to_owned))); - context.put(RequestedBootPlugin(data.boot_plugin.map(str::to_owned))); + context.put(CommitInfo { + subject: data.subject.to_owned(), + message: data.message.map(str::to_owned), + }); + context.put(RequestedBootPlugin(data.boot_plugin.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/open.rs b/lib/lib/src/mutated/files/open.rs index 4f26c6f8..ece80f06 100644 --- a/lib/lib/src/mutated/files/open.rs +++ b/lib/lib/src/mutated/files/open.rs @@ -7,7 +7,10 @@ use std::collections::VecDeque; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; +use upac_types::hook::ProgressEventBuilder; + +use super::{ApplyTarget, FileProgress, FilesError, RequestedFilePackage, WorkingState}; use crate::composefs::file::FileHandle; use crate::database::meta::MetaStore; @@ -16,11 +19,8 @@ use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::database::DATABASE_PATH; use crate::layout::deployment::CONFIG_DIR_NAME; -use crate::mutated::files::{ - EtcUpperDir, FilesError, PendingFiles, RequestedFilePackage, TargetUuid, TotalFiles, WorkingDatabase, WorkingTree, -}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct OpenTransactionStage; @@ -50,13 +50,10 @@ impl Stage for OpenTransactionStage { let total = files.len() as u64; let pending: VecDeque<_> = files.into_iter().collect(); - context.put(WorkingTree(tree)); - context.put(WorkingDatabase(database)); + context.put(WorkingState { tree, database }); context.put(ImportContext::default()); - context.put(EtcUpperDir(config_upper_dir)); - context.put(TargetUuid(uuid)); - context.put(PendingFiles(pending)); - context.put(TotalFiles(total)); + context.put(ApplyTarget { uuid, config_upper_dir }); + context.put(FileProgress { pending, total }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/files/swap.rs b/lib/lib/src/mutated/files/swap.rs index 1df78d41..fa25e6e2 100644 --- a/lib/lib/src/mutated/files/swap.rs +++ b/lib/lib/src/mutated/files/swap.rs @@ -3,11 +3,15 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::mutated::files::{FilesError, ResolvedBootEntry}; +use upac_types::hook::ProgressEventBuilder; +use upac_types::request::BootPluginSetOneShotRequest; + +use super::{FilesError, ResolvedBootEntry}; + +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; pub struct SwapStage; @@ -17,7 +21,9 @@ impl Stage for SwapStage { ) -> Result<(ProgressEventBuilder, StageResult, Box), FilesError> { let resolved = ctx_take!(context, ResolvedBootEntry); - resolved.plugin.set_one_shot(&resolved.entry_name)?; + resolved.plugin.set_one_shot(BootPluginSetOneShotRequest { + entry_name: resolved.entry_name, + })?; Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/gc/cleaning.rs b/lib/lib/src/mutated/gc/cleaning.rs index fd2be858..5f96ab39 100644 --- a/lib/lib/src/mutated/gc/cleaning.rs +++ b/lib/lib/src/mutated/gc/cleaning.rs @@ -3,13 +3,15 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; +use upac_types::hook::ProgressEventBuilder; + +use super::{CollectedRoots, GcError}; use crate::composefs::repository::gc; use crate::deploy::Deploy; -use crate::mutated::gc::{CollectedRoots, GcError}; +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; pub struct CleaningStage; @@ -21,7 +23,7 @@ impl Stage for CleaningStage { let deploy = ctx_take!(context, Deploy); let repository = deploy.open_repository()?; - let root_refs: Vec<&str> = roots.0.iter().map(String::as_str).collect(); + let root_refs: Vec<&str> = roots.iter().map(String::as_str).collect(); gc(&repository, &root_refs)?; Ok((progress, StageResult::Advance, Box::new(NoRollback))) diff --git a/lib/lib/src/mutated/gc/collect.rs b/lib/lib/src/mutated/gc/collect.rs index f407c33a..aa6c8470 100644 --- a/lib/lib/src/mutated/gc/collect.rs +++ b/lib/lib/src/mutated/gc/collect.rs @@ -3,14 +3,16 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; +use upac_types::hook::ProgressEventBuilder; + +use super::{CollectedRoots, DeployProgress, GcError}; use crate::database::record::DeployRecord; use crate::deploy::Deploy; use crate::errors::CommonError; -use crate::mutated::gc::{CollectedRoots, GcError, PendingDeploys, TotalDeploys}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct CollectRootsStage; @@ -18,35 +20,36 @@ impl Stage for CollectRootsStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), GcError> { - let mut pending_deploys = ctx_take!(context, PendingDeploys); + let mut deploy_progress = ctx_take!(context, DeployProgress); let mut roots = ctx_take!(context, CollectedRoots); - let total_deploys = ctx_get!(context, TotalDeploys); let deploy = ctx_get!(context, Deploy); - let prefix_digest = pending_deploys.0.pop_front().ok_or(CommonError::MissingResult)?; + let prefix_digest = deploy_progress.pending.pop_front().ok_or(CommonError::MissingResult)?; let record = DeployRecord::read(&deploy.deploy(&prefix_digest))?; - roots.0.push(record.prefix_digest); + roots.push(record.prefix_digest); if !record.working_config.is_empty() { - roots.0.push(record.working_config); + roots.push(record.working_config); } for entry in record.config_history { - roots.0.push(entry.config_digest); + roots.push(entry.config_digest); } - let remaining = pending_deploys.0.len() as u64; - let processed = total_deploys.0 - remaining; - progress = progress.subject(prefix_digest).progress(processed, total_deploys.0); + let remaining = deploy_progress.pending.len() as u64; + let processed = deploy_progress.total - remaining; + progress = progress + .subject(prefix_digest) + .progress(processed, deploy_progress.total); - let stage_result = if pending_deploys.0.is_empty() { + let stage_result = if deploy_progress.pending.is_empty() { StageResult::Advance } else { StageResult::Repeat }; - context.put(pending_deploys); + context.put(deploy_progress); context.put(roots); Ok((progress, stage_result, Box::new(NoRollback))) diff --git a/lib/lib/src/mutated/gc/mod.rs b/lib/lib/src/mutated/gc/mod.rs index 811caea3..be15a483 100644 --- a/lib/lib/src/mutated/gc/mod.rs +++ b/lib/lib/src/mutated/gc/mod.rs @@ -6,27 +6,39 @@ use std::collections::VecDeque; use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CGcRequest; -pub use self::error::GcError; +use upac_types::hook::Message; +use upac_types::traits::MessageHook; + +use upac_types::states::GcStateId; + +use upac_macro::ContextValue; use self::cleaning::CleaningStage; use self::collect::CollectRootsStage; use self::pruning::PruneStage; use crate::deploy::{Deploy, DeployMode}; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; -use upac_types::states::GcStateId; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; + +pub use self::error::GcError; mod cleaning; mod collect; mod error; mod pruning; -pub(crate) struct PendingDeploys(pub VecDeque); -pub(crate) struct TotalDeploys(pub u64); +pub(crate) struct DeployProgress { + pub pending: VecDeque, + pub total: u64, +} + +#[derive(ContextValue)] pub(crate) struct CollectedRoots(pub Vec); pub struct GcData<'a> { @@ -42,7 +54,7 @@ impl<'a> TryFrom<&'a CGcRequest> for GcData<'a> { fn try_from(request: &'a CGcRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(GcData { hook_message: request.base.on_hook, diff --git a/lib/lib/src/mutated/gc/pruning.rs b/lib/lib/src/mutated/gc/pruning.rs index 50497216..d1f89117 100644 --- a/lib/lib/src/mutated/gc/pruning.rs +++ b/lib/lib/src/mutated/gc/pruning.rs @@ -5,12 +5,13 @@ use std::collections::VecDeque; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; +use upac_types::hook::ProgressEventBuilder; use crate::deploy::Deploy; -use crate::mutated::gc::{CollectedRoots, GcError, PendingDeploys, TotalDeploys}; +use crate::mutated::gc::{CollectedRoots, DeployProgress, GcError}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; pub struct PruneStage; @@ -23,11 +24,10 @@ impl Stage for PruneStage { deploy.prune_deploys()?; let deploys = deploy.deploys()?; - let total_deploys_count = deploys.len() as u64; + let total = deploys.len() as u64; let pending: VecDeque<_> = deploys.into_iter().collect(); - context.put(PendingDeploys(pending)); - context.put(TotalDeploys(total_deploys_count)); + context.put(DeployProgress { pending, total }); context.put(CollectedRoots(Vec::new())); Ok((progress, StageResult::Advance, Box::new(NoRollback))) diff --git a/lib/lib/src/mutated/installer/checkout.rs b/lib/lib/src/mutated/installer/checkout.rs index b0d379f1..c2677cfa 100644 --- a/lib/lib/src/mutated/installer/checkout.rs +++ b/lib/lib/src/mutated/installer/checkout.rs @@ -3,17 +3,19 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{InstallError, NewState, RequestedBootPlugin, ResolvedBootEntry}; use crate::boot::write_boot_entry; use crate::composefs::repository::object_id_from_hex; use crate::deploy::Deploy; -use crate::deploy::esp::find_esp_mount; -use crate::layout::boot_plugins::{BOOT_PLUGINS_DIR, MANIFEST_EXTENSION}; -use crate::mutated::installer::{InstallError, NewPrefixDigest, RequestedBootPlugin, ResolvedBootEntry}; +use crate::deploy::find_esp_mount; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::plugin::boot::resolve_boot_plugin; +use crate::plugin::boot::BootPlugins; pub struct CheckoutStage; @@ -21,18 +23,18 @@ impl Stage for CheckoutStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), InstallError> { - let new_prefix = ctx_get!(context, NewPrefixDigest); + let new_state = ctx_get!(context, NewState); let deploy = ctx_get!(context, Deploy); - let requested = ctx_get!(context, RequestedBootPlugin); + let requested_boot_plugins = ctx_get!(context, RequestedBootPlugin); let repository = deploy.open_repository()?; - let tree = deploy.open_tree(&new_prefix.0)?; - let digest = object_id_from_hex(&new_prefix.0)?; + let tree = deploy.open_tree(&new_state.prefix_digest)?; + let digest = object_id_from_hex(&new_state.prefix_digest)?; let esp_mount = find_esp_mount()?; - let entry_name = write_boot_entry(&repository, &tree, digest, &esp_mount, &new_prefix.0)?; + let entry_name = write_boot_entry(&repository, &tree, digest, &esp_mount, &new_state.prefix_digest)?; - let plugin = resolve_boot_plugin(BOOT_PLUGINS_DIR, MANIFEST_EXTENSION, requested.0.as_deref())?; + let plugin = BootPlugins::new()?.load(&requested_boot_plugins)?; context.put(ResolvedBootEntry { plugin, entry_name }); diff --git a/lib/lib/src/mutated/installer/commit.rs b/lib/lib/src/mutated/installer/commit.rs index 3711861b..571ef599 100644 --- a/lib/lib/src/mutated/installer/commit.rs +++ b/lib/lib/src/mutated/installer/commit.rs @@ -10,9 +10,12 @@ use composefs::fsverity::FsVerityHashValue; use composefs::generic_tree::Stat; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_types::TmpPath; +use upac_types::hook::ProgressEventBuilder; + +use super::{ImportedState, InstallError, NewState}; use crate::composefs::error::RepoError; use crate::composefs::file::FileHandle; @@ -20,11 +23,8 @@ use crate::composefs::repository::commit_tree; use crate::database::InMemory; use crate::deploy::Deploy; use crate::layout::database::{DATABASE_PATH, INSTALLER_SCRATCH_FILENAME}; -use crate::mutated::installer::{ - ImportedConfigDefaults, ImportedDatabase, ImportedTree, InstallError, NewConfigDefaults, NewPrefixDigest, -}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct CommitTransactionStage; @@ -32,19 +32,18 @@ impl Stage for CommitTransactionStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), InstallError> { - let imported_tree = ctx_take!(context, ImportedTree); - let config_defaults = ctx_take!(context, ImportedConfigDefaults); - let database = ctx_take!(context, ImportedDatabase); - let mut import_ctx = ctx_take!(context, ImportContext); + let imported_state = ctx_take!(context, ImportedState); + let mut imported_ctx = ctx_take!(context, ImportContext); let tmp_path = ctx_get!(context, TmpPath); let deploy = ctx_get!(context, Deploy); let repository = deploy.open_repository()?; - let mut tree = imported_tree.0; + let mut tree = imported_state.tree; - let database_bytes = database.0.into_bytes()?; + let database_bytes = imported_state.database.into_bytes()?; let database_scratch_path = Path::new(tmp_path.as_ref()).join(INSTALLER_SCRATCH_FILENAME); + write(&database_scratch_path, &database_bytes).map_err(RepoError::from)?; FileHandle::new(DATABASE_PATH).insert_file( @@ -52,13 +51,15 @@ impl Stage for CommitTransactionStage { &mut tree, &File::open(&database_scratch_path).map_err(RepoError::from)?, Stat::uninitialized(), - &mut import_ctx, + &mut imported_ctx, )?; let digest = commit_tree(&repository, tree)?; - context.put(NewPrefixDigest(digest.to_hex())); - context.put(NewConfigDefaults(config_defaults.0)); + context.put(NewState { + prefix_digest: digest.to_hex(), + config_defaults: imported_state.config_defaults, + }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/installer/fetching.rs b/lib/lib/src/mutated/installer/fetching.rs index 98603bef..95d0ed27 100644 --- a/lib/lib/src/mutated/installer/fetching.rs +++ b/lib/lib/src/mutated/installer/fetching.rs @@ -3,10 +3,13 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::mutated::installer::InstallError; -use crate::orchestrator::Context; +use upac_types::hook::ProgressEventBuilder; + +use super::InstallError; + +use crate::orchestrator::context::Context; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; pub struct FetchingStage; diff --git a/lib/lib/src/mutated/installer/import.rs b/lib/lib/src/mutated/installer/import.rs index a50a3dec..c0cbe766 100644 --- a/lib/lib/src/mutated/installer/import.rs +++ b/lib/lib/src/mutated/installer/import.rs @@ -7,9 +7,12 @@ use std::path::Path; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use upac_types::{FileEntry, FileEntryScope}; +use upac_types::entry::{FileEntry, FileEntryScope}; +use upac_types::hook::ProgressEventBuilder; + +use super::{ImportedState, InstallError, InstallProgress}; use crate::composefs::file::import_if_dir; use crate::database::files::FileStoreMut; @@ -17,11 +20,8 @@ use crate::database::meta::MetaStoreMut; use crate::database::triggers::TriggerStoreMut; use crate::deploy::Deploy; use crate::errors::CommonError; -use crate::mutated::installer::{ - ImportedConfigDefaults, ImportedDatabase, ImportedTree, InstallError, PendingPackages, TotalPackages, -}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct ImportPackageStage; @@ -29,16 +29,13 @@ impl Stage for ImportPackageStage { fn run( &self, context: &mut Context, cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), InstallError> { - let mut pending_packages = ctx_take!(context, PendingPackages); - let mut imported_tree = ctx_take!(context, ImportedTree); - let mut config_defaults = ctx_take!(context, ImportedConfigDefaults); - let mut database = ctx_take!(context, ImportedDatabase); - let mut import_ctx = ctx_take!(context, ImportContext); + let mut install_progress = ctx_take!(context, InstallProgress); + let mut imported_state = ctx_take!(context, ImportedState); + let mut imported_ctx = ctx_take!(context, ImportContext); - let total = ctx_get!(context, TotalPackages); let deploy = ctx_get!(context, Deploy); - let (package, trigger) = pending_packages.0.pop_front().ok_or(CommonError::MissingResult)?; + let (package, trigger) = install_progress.pending.pop_front().ok_or(CommonError::MissingResult)?; let repository = deploy.open_repository()?; let source_root = Path::new(&package.temp_package_path); @@ -46,26 +43,26 @@ impl Stage for ImportPackageStage { let prefix_source = source_root.join("usr"); let imported = import_if_dir!( &repository, - &mut imported_tree.0, + &mut imported_state.tree, &prefix_source, - &mut import_ctx, + &mut imported_ctx, cancel ); let config_source = source_root.join("etc"); let imported_config = import_if_dir!( &repository, - &mut config_defaults.0, + &mut imported_state.config_defaults, &config_source, - &mut import_ctx, + &mut imported_ctx, cancel ); - let uuid = database.0.insert_package_meta(&package.meta)?; - database.0.set_declarative_triggers(uuid, &trigger)?; + let uuid = imported_state.database.insert_package_meta(&package.meta)?; + imported_state.database.set_declarative_triggers(uuid, &trigger)?; for path in imported { - database.0.insert_package_file( + imported_state.database.insert_package_file( uuid, &FileEntry { path: path.to_string_lossy().into_owned(), @@ -76,7 +73,7 @@ impl Stage for ImportPackageStage { } for path in imported_config { - database.0.insert_package_file( + imported_state.database.insert_package_file( uuid, &FileEntry { path: path.to_string_lossy().into_owned(), @@ -86,21 +83,21 @@ impl Stage for ImportPackageStage { )?; } - let remaining = pending_packages.0.len() as u64; - let processed = total.0 - remaining; - progress = progress.subject(package.meta.name.clone()).progress(processed, total.0); + let remaining = install_progress.pending.len() as u64; + let processed = install_progress.total - remaining; + progress = progress + .subject(package.meta.name.clone()) + .progress(processed, install_progress.total); - let stage_result = if pending_packages.0.is_empty() { + let stage_result = if install_progress.pending.is_empty() { StageResult::Advance } else { StageResult::Repeat }; - context.put(pending_packages); - context.put(imported_tree); - context.put(config_defaults); - context.put(database); - context.put(import_ctx); + context.put(install_progress); + context.put(imported_state); + context.put(imported_ctx); Ok((progress, stage_result, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/installer/merge.rs b/lib/lib/src/mutated/installer/merge.rs index 906fcaf2..2cf91ae3 100644 --- a/lib/lib/src/mutated/installer/merge.rs +++ b/lib/lib/src/mutated/installer/merge.rs @@ -8,21 +8,22 @@ use std::fs::create_dir_all; use composefs::fsverity::FsVerityHashValue; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{CommitInfo, InstallError, NewState}; use crate::composefs::overlay::{apply_overlay_upper, apply_tree_overlay}; use crate::composefs::repository::commit_tree; -use crate::config::merge::merge_config; +use crate::config::merge_config; use crate::database::error::DeployRecordError; use crate::database::record::DeployRecord; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::deployment::CONFIG_DIR_NAME; -use crate::mutated::installer::{ - AllowConflictFiles, CommitMessage, InstallError, NewConfigDefaults, NewPrefixDigest, Subject, -}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct MergeStage; @@ -30,13 +31,10 @@ impl Stage for MergeStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), InstallError> { - let new_config_defaults = ctx_take!(context, NewConfigDefaults); + let new_state = ctx_get!(context, NewState); - let new_prefix = ctx_get!(context, NewPrefixDigest); let deploy = ctx_get!(context, Deploy); - let subject = ctx_get!(context, Subject); - let message = ctx_get!(context, CommitMessage); - let allow_conflict_files = ctx_get!(context, AllowConflictFiles); + let commit_info = ctx_get!(context, CommitInfo); let repository = deploy.open_repository()?; @@ -56,13 +54,13 @@ impl Stage for MergeStage { let mut new_config_layout = base_config_layout.clone(); - apply_tree_overlay(&mut new_config_layout, &new_config_defaults.0)?; + apply_tree_overlay(&mut new_config_layout, &new_state.config_defaults)?; let merge_result = merge_config( &base_config_layout, &new_config_layout, &live_config_layout, - allow_conflict_files.0, + commit_info.allow_conflict_files, )?; let new_config_digest = commit_tree(&repository, merge_result.tree)?.to_hex(); @@ -73,16 +71,16 @@ impl Stage for MergeStage { context.send_progress(&progress); } - let new_record_dir = deploy.deploy(&new_prefix.0); + let new_record_dir = deploy.deploy(&new_state.prefix_digest); let mut record_deploy = match DeployRecord::read(&new_record_dir) { Ok(existing) => existing, Err(DeployRecordError::NotFound) => { create_dir_all(&new_record_dir).map_err(DeployRecordError::from)?; DeployRecord { - prefix_digest: new_prefix.0.clone(), - subject: subject.0.clone(), - message: message.0.clone(), + prefix_digest: new_state.prefix_digest.clone(), + subject: commit_info.subject.clone(), + message: commit_info.message.clone(), seq: DeployRecord::allocate_seq(&deploy.next_seq_path())?, timestamp: DeployRecord::now_secs(), config_history: Vec::new(), @@ -97,8 +95,8 @@ impl Stage for MergeStage { written.extend(record_deploy.update_working_config( &new_record_dir, new_config_digest, - subject.0.clone(), - message.0.clone(), + commit_info.subject.clone(), + commit_info.message.clone(), )?); Ok((progress, StageResult::Advance, Box::new(written))) diff --git a/lib/lib/src/mutated/installer/mod.rs b/lib/lib/src/mutated/installer/mod.rs index ca5ec8c3..de660ce4 100644 --- a/lib/lib/src/mutated/installer/mod.rs +++ b/lib/lib/src/mutated/installer/mod.rs @@ -8,13 +8,19 @@ use std::os::raw::c_void; use composefs::tree::FileSystem; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CInstallRequest; -use upac_types::{DeclarativeTrigger, PackageTemp}; +use upac_types::TmpPath; +use upac_types::decoder::DeclarativeTrigger; +use upac_types::hook::Message; +use upac_types::package::PackageTemp; +use upac_types::states::InstallStateId; +use upac_types::traits::MessageHook; -pub use self::error::InstallError; +use upac_macro::ContextValue; use self::checkout::CheckoutStage; use self::commit::CommitTransactionStage; @@ -30,13 +36,14 @@ use crate::database::MemoryDatabase; use crate::deploy::retention::RetentionStage; use crate::deploy::{Deploy, DeployMode}; use crate::errors::CommonError; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; use crate::plugin::boot::BootPlugin; use crate::plugin::decoder::unpack::PackageUnpacker; use crate::scripts::HookStage; use crate::scripts::pipeline::{Operation, PipelineTrigger}; -use upac_types::TmpPath; -use upac_types::states::InstallStateId; + +pub use self::error::InstallError; mod checkout; mod commit; @@ -48,30 +55,47 @@ mod open; mod preparation; mod swap; -pub(crate) struct NewPrefixDigest(pub String); -pub(crate) struct NewConfigDefaults(pub FileSystem); -pub(crate) struct Subject(pub String); -pub(crate) struct CommitMessage(pub Option); -pub(crate) struct RequestedBootPlugin(pub Option); -pub(crate) struct AllowConflictFiles(pub bool); +pub(crate) struct NewState { + pub prefix_digest: String, + pub config_defaults: FileSystem, +} + +pub(crate) struct CommitInfo { + pub subject: String, + pub message: Option, + pub allow_conflict_files: bool, +} + +#[derive(ContextValue)] +pub(crate) struct RequestedBootPlugin(pub String); pub(crate) struct ResolvedBootEntry { pub plugin: BootPlugin, pub entry_name: String, } -pub(crate) struct PendingPackagePaths(pub VecDeque); -pub(crate) struct UnpackerState(pub PackageUnpacker); -pub(crate) struct PendingPackages(pub VecDeque<(PackageTemp, DeclarativeTrigger)>); -pub(crate) struct TotalPackages(pub u64); -pub(crate) struct ImportedTree(pub FileSystem); -pub(crate) struct ImportedConfigDefaults(pub FileSystem); -pub(crate) struct ImportedDatabase(pub MemoryDatabase); +pub(crate) struct UnpackState { + pub pending_paths: VecDeque, + pub unpacker: PackageUnpacker, +} + +pub(crate) struct InstallProgress { + pub pending: VecDeque<(PackageTemp, DeclarativeTrigger)>, + pub total: u64, +} + +pub(crate) struct ImportedState { + pub tree: FileSystem, + pub config_defaults: FileSystem, + pub database: MemoryDatabase, +} pub struct InstallData<'a> { pub packages: Vec<&'a str>, - pub boot_plugin: Option<&'a str>, + pub allow_conflict_files: bool, + pub boot_plugin: &'a str, + pub tmp_path: &'a str, pub subject: &'a str, @@ -89,13 +113,15 @@ impl<'a> TryFrom<&'a CInstallRequest> for InstallData<'a> { fn try_from(request: &'a CInstallRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(InstallData { packages: Vec::try_from(&request.packages)?, - boot_plugin: (&request.boot_plugin).try_into()?, + allow_conflict_files: request.allow_conflict_files, + boot_plugin: (&request.boot_plugin).try_into()?, + tmp_path: (&request.tmp_path).try_into()?, subject: (&request.subject).try_into()?, @@ -119,17 +145,21 @@ pub fn run(data: InstallData) -> Result<(), (InstallStateId, InstallError)> { let mut context = Context::new(); context.put(deploy); - context.put(UnpackerState(unpacker)); - context.put(PendingPackagePaths( - data.packages.iter().map(|path| (*path).to_owned()).collect(), - )); - context.put(PendingPackages(VecDeque::new())); - context.put(TotalPackages(total_packages)); + context.put(UnpackState { + pending_paths: data.packages.iter().map(|path| (*path).to_owned()).collect(), + unpacker, + }); + context.put(InstallProgress { + pending: VecDeque::new(), + total: total_packages, + }); context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Subject(data.subject.to_owned())); - context.put(CommitMessage(data.message.map(str::to_owned))); - context.put(RequestedBootPlugin(data.boot_plugin.map(str::to_owned))); - context.put(AllowConflictFiles(data.allow_conflict_files)); + context.put(CommitInfo { + subject: data.subject.to_owned(), + message: data.message.map(str::to_owned), + allow_conflict_files: data.allow_conflict_files, + }); + context.put(RequestedBootPlugin(data.boot_plugin.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/open.rs b/lib/lib/src/mutated/installer/open.rs index a3d090d1..ab781e00 100644 --- a/lib/lib/src/mutated/installer/open.rs +++ b/lib/lib/src/mutated/installer/open.rs @@ -7,16 +7,19 @@ use composefs::generic_tree::Stat; use composefs::repository::ImportContext; use composefs::tree::FileSystem; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{ImportedState, InstallError}; use crate::composefs::file::FileHandle; use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::database::DATABASE_PATH; -use crate::mutated::installer::{ImportedConfigDefaults, ImportedDatabase, ImportedTree, InstallError}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; pub struct OpenTransactionStage; @@ -33,9 +36,11 @@ impl Stage for OpenTransactionStage { let database_bytes = FileHandle::new(DATABASE_PATH).read_file(&repository, &tree)?; let database = MemoryDatabase::open_in_memory(database_bytes)?; - context.put(ImportedTree(tree)); - context.put(ImportedConfigDefaults(FileSystem::new(Stat::uninitialized()))); - context.put(ImportedDatabase(database)); + context.put(ImportedState { + tree, + config_defaults: FileSystem::new(Stat::uninitialized()), + database, + }); context.put(ImportContext::default()); Ok((progress, StageResult::Advance, Box::new(NoRollback))) diff --git a/lib/lib/src/mutated/installer/preparation.rs b/lib/lib/src/mutated/installer/preparation.rs index 6a0b4dc3..694cfa21 100644 --- a/lib/lib/src/mutated/installer/preparation.rs +++ b/lib/lib/src/mutated/installer/preparation.rs @@ -7,14 +7,16 @@ use std::fs::remove_dir_all; use std::path::PathBuf; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_types::TmpPath; +use upac_types::hook::ProgressEventBuilder; + +use super::{InstallError, InstallProgress, UnpackState}; use crate::errors::CommonError; -use crate::mutated::installer::{InstallError, PendingPackagePaths, PendingPackages, TotalPackages, UnpackerState}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct PreparationStage; @@ -24,38 +26,40 @@ impl Stage for PreparationStage { fn run( &self, context: &mut Context, cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), InstallError> { - let mut pending_paths = ctx_take!(context, PendingPackagePaths); - let mut unpacker = ctx_take!(context, UnpackerState); - let mut pending_packages = ctx_take!(context, PendingPackages); + let mut unpack_state = ctx_take!(context, UnpackState); + let mut install_progress = ctx_take!(context, InstallProgress); let tmp_path = ctx_get!(context, TmpPath); - let total_packages = ctx_get!(context, TotalPackages); - let package_path = pending_paths.0.pop_front().ok_or(CommonError::MissingResult)?; - let index = pending_packages.0.len(); + let package_path = unpack_state + .pending_paths + .pop_front() + .ok_or(CommonError::MissingResult)?; + let index = install_progress.pending.len(); - let (package, trigger) = unpacker - .0 + let (package, trigger) = unpack_state + .unpacker .unpack_one(&package_path, index, tmp_path.as_ref(), cancel) .map_err(CommonError::Decoder)?; let guard = UnpackedPackageDir(PathBuf::from(&package.temp_package_path)); - pending_packages.0.push_back((package, trigger)); + install_progress.pending.push_back((package, trigger)); - let remaining = pending_paths.0.len() as u64; - let processed = total_packages.0 - remaining; - progress = progress.subject(package_path).progress(processed, total_packages.0); + let remaining = unpack_state.pending_paths.len() as u64; + let processed = install_progress.total - remaining; + progress = progress + .subject(package_path) + .progress(processed, install_progress.total); - let result = if pending_paths.0.is_empty() { + let result = if unpack_state.pending_paths.is_empty() { StageResult::Advance } else { StageResult::Repeat }; - context.put(pending_paths); - context.put(unpacker); - context.put(pending_packages); + context.put(unpack_state); + context.put(install_progress); Ok((progress, result, Box::new(guard))) } diff --git a/lib/lib/src/mutated/installer/swap.rs b/lib/lib/src/mutated/installer/swap.rs index d7d53e9d..e4f03ffa 100644 --- a/lib/lib/src/mutated/installer/swap.rs +++ b/lib/lib/src/mutated/installer/swap.rs @@ -3,11 +3,15 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::mutated::installer::{InstallError, ResolvedBootEntry}; +use upac_types::hook::ProgressEventBuilder; +use upac_types::request::BootPluginSetOneShotRequest; + +use super::{InstallError, ResolvedBootEntry}; + +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; pub struct SwapStage; @@ -17,7 +21,9 @@ impl Stage for SwapStage { ) -> Result<(ProgressEventBuilder, StageResult, Box), InstallError> { let resolved = ctx_take!(context, ResolvedBootEntry); - resolved.plugin.set_one_shot(&resolved.entry_name)?; + resolved.plugin.set_one_shot(BootPluginSetOneShotRequest { + entry_name: resolved.entry_name, + })?; Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/mime/error.rs b/lib/lib/src/mutated/mime/error.rs index a410613a..b297164c 100644 --- a/lib/lib/src/mutated/mime/error.rs +++ b/lib/lib/src/mutated/mime/error.rs @@ -11,6 +11,10 @@ use upac_abi::error::ErrorKind; use crate::errors::{CommonError, common_error_from, lock_error_from}; use crate::lock::LockError; +#[cfg(test)] +#[path = "../../../tests/inline/mutated_mime_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum MimeError { Common(CommonError), diff --git a/lib/lib/src/mutated/mime/mod.rs b/lib/lib/src/mutated/mime/mod.rs index c7bbf632..f461045d 100644 --- a/lib/lib/src/mutated/mime/mod.rs +++ b/lib/lib/src/mutated/mime/mod.rs @@ -6,28 +6,38 @@ use std::collections::VecDeque; use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CMimeSyncRequest; -pub use self::error::MimeError; +use upac_types::hook::Message; +use upac_types::states::MimeStateId; +use upac_types::traits::MessageHook; + +use upac_macro::ContextValue; use self::preparing::PreparingStage; use self::rendering::RenderingStage; use self::writing::WritingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; -use upac_types::states::MimeStateId; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; + +pub use self::error::MimeError; mod error; mod preparing; mod rendering; mod writing; +#[derive(ContextValue)] pub(crate) struct DesktopContent(pub String); -pub(crate) struct PendingWrites(pub VecDeque<(&'static str, String)>); -pub(crate) struct TotalWrites(pub u64); +pub(crate) struct WriteProgress { + pub pending: VecDeque<(&'static str, String)>, + pub total: u64, +} pub struct MimeData<'a> { pub hook_message: Option, @@ -42,7 +52,7 @@ impl<'a> TryFrom<&'a CMimeSyncRequest> for MimeData<'a> { fn try_from(request: &'a CMimeSyncRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(MimeData { hook_message: request.base.on_hook, diff --git a/lib/lib/src/mutated/mime/preparing.rs b/lib/lib/src/mutated/mime/preparing.rs index 7b31fa68..0ebe1315 100644 --- a/lib/lib/src/mutated/mime/preparing.rs +++ b/lib/lib/src/mutated/mime/preparing.rs @@ -5,14 +5,17 @@ use std::fs; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{DesktopContent, MimeError}; use crate::errors::CommonError; -use crate::layout::{decoders, mime}; -use crate::mutated::mime::{DesktopContent, MimeError}; -use crate::orchestrator::Context; +use crate::layout::mime; +use crate::orchestrator::context::Context; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::plugin::decoder::manifest::load_decoder_manifests; +use crate::plugin::decoder::manifest::DecoderManifests; pub struct PreparingStage; @@ -20,11 +23,10 @@ impl Stage for PreparingStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), MimeError> { - let manifests = load_decoder_manifests(decoders::DECODERS_DIR, decoders::MANIFEST_EXTENSION) - .map_err(CommonError::Decoder)?; + let manifests = DecoderManifests::new().map_err(CommonError::Decoder)?; let desktop_content = fs::read_to_string(mime::DESKTOP_FILE_PATH)?; - context.put(manifests); + context.put(manifests.0); context.put(DesktopContent(desktop_content)); Ok((progress, StageResult::Advance, Box::new(NoRollback))) diff --git a/lib/lib/src/mutated/mime/rendering.rs b/lib/lib/src/mutated/mime/rendering.rs index d42e4645..03a2a4c6 100644 --- a/lib/lib/src/mutated/mime/rendering.rs +++ b/lib/lib/src/mutated/mime/rendering.rs @@ -9,12 +9,15 @@ use std::io::Result as IoResult; use quick_xml::Writer as XmlWriter; use quick_xml::events::{BytesDecl, BytesText, Event}; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::layout::mime; -use crate::mutated::mime::{DesktopContent, MimeError, PendingWrites, TotalWrites}; +use upac_types::hook::ProgressEventBuilder; + +use super::{DesktopContent, MimeError, WriteProgress}; + +use crate::layout::mime::{DESKTOP_FILE_PATH, MIME_XML_PATH, SHARED_MIME_INFO_XMLNS}; +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; use crate::plugin::decoder::manifest::DecoderManifest; pub struct RenderingStage; @@ -28,15 +31,11 @@ impl Stage for RenderingStage { let mime_xml = Self::render_mime_xml(&manifests)?; let mime_type_line = Self::render_mime_type_line(&manifests); - let desktop_content = Self::rewrite_desktop_mime_type(&desktop_content.0, &mime_type_line)?; + let desktop_content = Self::rewrite_desktop_mime_type(&desktop_content, &mime_type_line)?; - let pending = VecDeque::from([ - (mime::MIME_XML_PATH, mime_xml), - (mime::DESKTOP_FILE_PATH, desktop_content), - ]); + let pending = VecDeque::from([(MIME_XML_PATH, mime_xml), (DESKTOP_FILE_PATH, desktop_content)]); - context.put(PendingWrites(pending)); - context.put(TotalWrites(2)); + context.put(WriteProgress { pending, total: 2 }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } @@ -50,7 +49,7 @@ impl RenderingStage { writer .create_element("mime-info") - .with_attribute(("xmlns", mime::SHARED_MIME_INFO_XMLNS)) + .with_attribute(("xmlns", SHARED_MIME_INFO_XMLNS)) .write_inner_content(|writer| { for manifest in manifests.values() { Self::write_mime_type_element(writer, manifest)?; diff --git a/lib/lib/src/mutated/mime/writing.rs b/lib/lib/src/mutated/mime/writing.rs index 9ce90c1c..40b7bd90 100644 --- a/lib/lib/src/mutated/mime/writing.rs +++ b/lib/lib/src/mutated/mime/writing.rs @@ -6,14 +6,17 @@ use std::path::Path; use std::process::Command; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{MimeError, WriteProgress}; use crate::errors::CommonError; use crate::fs::WrittenFile; -use crate::layout::mime; -use crate::mutated::mime::{MimeError, PendingWrites, TotalWrites}; +use crate::layout::mime::{APPLICATIONS_DIR, MIME_DB_DIR, UPDATE_DESKTOP_DATABASE_BIN, UPDATE_MIME_DATABASE_BIN}; +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct WritingStage; @@ -21,32 +24,28 @@ impl Stage for WritingStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), MimeError> { - let mut pending = ctx_take!(context, PendingWrites); - - let total = ctx_get!(context, TotalWrites); + let mut write_progress = ctx_take!(context, WriteProgress); - let (path, content) = pending.0.pop_front().ok_or(CommonError::MissingResult)?; + let (path, content) = write_progress.pending.pop_front().ok_or(CommonError::MissingResult)?; let written_file = WrittenFile::write(Path::new(path), content.as_bytes())?; - let remaining = pending.0.len() as u64; - let processed = total.0 - remaining; - progress = progress.subject(path.to_owned()).progress(processed, total.0); + let remaining = write_progress.pending.len() as u64; + let processed = write_progress.total - remaining; + progress = progress + .subject(path.to_owned()) + .progress(processed, write_progress.total); - let result = if pending.0.is_empty() { - let _ = Command::new(mime::UPDATE_MIME_DATABASE_BIN) - .arg(mime::MIME_DB_DIR) - .status(); - let _ = Command::new(mime::UPDATE_DESKTOP_DATABASE_BIN) - .arg(mime::APPLICATIONS_DIR) - .status(); + let result = if write_progress.pending.is_empty() { + let _ = Command::new(UPDATE_MIME_DATABASE_BIN).arg(MIME_DB_DIR).status(); + let _ = Command::new(UPDATE_DESKTOP_DATABASE_BIN).arg(APPLICATIONS_DIR).status(); StageResult::Advance } else { StageResult::Repeat }; - context.put(pending); + context.put(write_progress); Ok((progress, result, Box::new(vec![written_file]))) } diff --git a/lib/lib/src/mutated/pin/mod.rs b/lib/lib/src/mutated/pin/mod.rs index 77675f8c..680168d4 100644 --- a/lib/lib/src/mutated/pin/mod.rs +++ b/lib/lib/src/mutated/pin/mod.rs @@ -5,22 +5,32 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CPinRequest; +use upac_types::hook::Message; +use upac_types::traits::MessageHook; -pub use self::error::PinError; +use upac_types::states::PinStateId; + +use upac_macro::ContextValue; use self::stage::SetPinnedStage; use crate::deploy::{Deploy, DeployMode}; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; -use upac_types::states::PinStateId; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; + +pub use self::error::PinError; mod error; mod stage; +#[derive(ContextValue)] pub(crate) struct RequestedPrefixDigest(pub String); + +#[derive(ContextValue)] pub(crate) struct RequestedPinned(pub bool); pub struct PinData<'a> { @@ -39,7 +49,7 @@ impl<'a> TryFrom<&'a CPinRequest> for PinData<'a> { fn try_from(request: &'a CPinRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(PinData { prefix_digest: (&request.prefix_digest).try_into()?, diff --git a/lib/lib/src/mutated/pin/stage.rs b/lib/lib/src/mutated/pin/stage.rs index 059aeeb9..e4933ff2 100644 --- a/lib/lib/src/mutated/pin/stage.rs +++ b/lib/lib/src/mutated/pin/stage.rs @@ -5,13 +5,16 @@ use std::mem::replace; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{PinError, RequestedPinned, RequestedPrefixDigest}; use crate::database::record::DeployRecord; use crate::deploy::Deploy; -use crate::mutated::pin::{PinError, RequestedPinned, RequestedPrefixDigest}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; pub struct SetPinnedStage; @@ -23,7 +26,7 @@ impl Stage for SetPinnedStage { let prefix_digest = ctx_get!(context, RequestedPrefixDigest); let pinned = ctx_get!(context, RequestedPinned); - let record_dir = deploy.deploy(&prefix_digest.0); + let record_dir = deploy.deploy(&prefix_digest); let mut record = DeployRecord::read(&record_dir)?; let mut written = Vec::new(); diff --git a/lib/lib/src/mutated/rollback/checkout.rs b/lib/lib/src/mutated/rollback/checkout.rs index bcaef910..f935cbb4 100644 --- a/lib/lib/src/mutated/rollback/checkout.rs +++ b/lib/lib/src/mutated/rollback/checkout.rs @@ -3,17 +3,18 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{RequestedBootPlugin, ResolvedBootEntry, RollbackError, TargetPrefixDigest}; use crate::boot::write_boot_entry; use crate::composefs::repository::object_id_from_hex; -use crate::deploy::Deploy; -use crate::deploy::esp::find_esp_mount; -use crate::layout::boot_plugins::{BOOT_PLUGINS_DIR, MANIFEST_EXTENSION}; -use crate::mutated::rollback::{RequestedBootPlugin, ResolvedBootEntry, RollbackError, TargetPrefixDigest}; +use crate::deploy::{Deploy, find_esp_mount}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::plugin::boot::resolve_boot_plugin; +use crate::plugin::boot::BootPlugins; pub struct CheckoutStage; @@ -23,16 +24,16 @@ impl Stage for CheckoutStage { ) -> Result<(ProgressEventBuilder, StageResult, Box), RollbackError> { let target = ctx_get!(context, TargetPrefixDigest); let deploy = ctx_get!(context, Deploy); - let requested = ctx_get!(context, RequestedBootPlugin); + let requested_boot_plugins = ctx_get!(context, RequestedBootPlugin); let repository = deploy.open_repository()?; - let tree = deploy.open_tree(&target.0)?; - let digest = object_id_from_hex(&target.0)?; + let tree = deploy.open_tree(&target)?; + let digest = object_id_from_hex(&target)?; let esp_mount = find_esp_mount()?; - let entry_name = write_boot_entry(&repository, &tree, digest, &esp_mount, &target.0)?; + let entry_name = write_boot_entry(&repository, &tree, digest, &esp_mount, &target)?; - let plugin = resolve_boot_plugin(BOOT_PLUGINS_DIR, MANIFEST_EXTENSION, requested.0.as_deref())?; + let plugin = BootPlugins::new()?.load(&requested_boot_plugins)?; context.put(ResolvedBootEntry { plugin, entry_name }); diff --git a/lib/lib/src/mutated/rollback/error.rs b/lib/lib/src/mutated/rollback/error.rs index 1b5aa059..ccd778b9 100644 --- a/lib/lib/src/mutated/rollback/error.rs +++ b/lib/lib/src/mutated/rollback/error.rs @@ -17,6 +17,10 @@ use crate::errors::{ use crate::lock::LockError; use crate::plugin::boot::error::BootPluginError; +#[cfg(test)] +#[path = "../../../tests/inline/mutated_rollback_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum RollbackError { Common(CommonError), diff --git a/lib/lib/src/mutated/rollback/merge.rs b/lib/lib/src/mutated/rollback/merge.rs index ce0d41fe..3761b4ab 100644 --- a/lib/lib/src/mutated/rollback/merge.rs +++ b/lib/lib/src/mutated/rollback/merge.rs @@ -3,13 +3,16 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{RequestedConfigDigest, RollbackError, TargetPrefixDigest}; use crate::database::record::DeployRecord; use crate::deploy::Deploy; -use crate::mutated::rollback::{RequestedConfigDigest, RollbackError, TargetPrefixDigest}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; pub struct MergeStage; @@ -20,7 +23,7 @@ impl Stage for MergeStage { let requested = ctx_get!(context, RequestedConfigDigest); let deploy = ctx_get!(context, Deploy); - let (config_digest, prefix_digest) = DeployRecord::resolve_config_digest(deploy, Some(&requested.0))?; + let (config_digest, prefix_digest) = DeployRecord::resolve_config_digest(deploy, Some(&requested))?; let record_dir = deploy.deploy(&prefix_digest); let mut record = DeployRecord::read(&record_dir)?; diff --git a/lib/lib/src/mutated/rollback/mod.rs b/lib/lib/src/mutated/rollback/mod.rs index dc911dbb..ea993bc0 100644 --- a/lib/lib/src/mutated/rollback/mod.rs +++ b/lib/lib/src/mutated/rollback/mod.rs @@ -5,11 +5,17 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CRollbackRequest; -pub use self::error::RollbackError; +use upac_types::TmpPath; +use upac_types::hook::Message; +use upac_types::states::RollbackStateId; +use upac_types::traits::MessageHook; + +use upac_macro::ContextValue; use self::checkout::CheckoutStage; use self::merge::MergeStage; @@ -17,21 +23,28 @@ use self::swap::SwapStage; use crate::deploy::retention::RetentionStage; use crate::deploy::{Deploy, DeployMode}; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; use crate::plugin::boot::BootPlugin; use crate::scripts::HookStage; use crate::scripts::pipeline::{Operation, PipelineTrigger}; -use upac_types::TmpPath; -use upac_types::states::RollbackStateId; + +pub use self::error::RollbackError; mod checkout; mod error; mod merge; mod swap; +#[derive(ContextValue)] pub(crate) struct RequestedConfigDigest(pub String); -pub(crate) struct RequestedBootPlugin(pub Option); + +#[derive(ContextValue)] pub(crate) struct TargetPrefixDigest(pub String); + +#[derive(ContextValue)] +pub(crate) struct RequestedBootPlugin(pub String); + pub(crate) struct ResolvedBootEntry { pub plugin: BootPlugin, pub entry_name: String, @@ -39,7 +52,8 @@ pub(crate) struct ResolvedBootEntry { pub struct RollbackData<'a> { pub config_digest: &'a str, - pub boot_plugin: Option<&'a str>, + + pub boot_plugin: &'a str, pub tmp_path: &'a str, @@ -55,10 +69,11 @@ impl<'a> TryFrom<&'a CRollbackRequest> for RollbackData<'a> { fn try_from(request: &'a CRollbackRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(RollbackData { config_digest: (&request.config_digest).try_into()?, + boot_plugin: (&request.boot_plugin).try_into()?, tmp_path: (&request.tmp_path).try_into()?, @@ -78,7 +93,7 @@ pub fn run(data: RollbackData) -> Result<(), (RollbackStateId, RollbackError)> { let mut context = Context::new(); context.put(deploy); context.put(RequestedConfigDigest(data.config_digest.to_owned())); - context.put(RequestedBootPlugin(data.boot_plugin.map(str::to_owned))); + context.put(RequestedBootPlugin(data.boot_plugin.to_owned())); context.put(TmpPath(data.tmp_path.to_owned())); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); diff --git a/lib/lib/src/mutated/rollback/swap.rs b/lib/lib/src/mutated/rollback/swap.rs index e55104d8..0c084691 100644 --- a/lib/lib/src/mutated/rollback/swap.rs +++ b/lib/lib/src/mutated/rollback/swap.rs @@ -3,11 +3,15 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::mutated::rollback::{ResolvedBootEntry, RollbackError}; +use upac_types::hook::ProgressEventBuilder; +use upac_types::request::BootPluginSetOneShotRequest; + +use super::{ResolvedBootEntry, RollbackError}; + +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; pub struct SwapStage; @@ -17,7 +21,9 @@ impl Stage for SwapStage { ) -> Result<(ProgressEventBuilder, StageResult, Box), RollbackError> { let resolved = ctx_take!(context, ResolvedBootEntry); - resolved.plugin.set_one_shot(&resolved.entry_name)?; + resolved.plugin.set_one_shot(BootPluginSetOneShotRequest { + entry_name: resolved.entry_name, + })?; Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/uninstaller/checkout.rs b/lib/lib/src/mutated/uninstaller/checkout.rs index 05fe546f..73578955 100644 --- a/lib/lib/src/mutated/uninstaller/checkout.rs +++ b/lib/lib/src/mutated/uninstaller/checkout.rs @@ -3,17 +3,18 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{NewState, RequestedBootPlugin, ResolvedBootEntry, UninstallError}; use crate::boot::write_boot_entry; use crate::composefs::repository::object_id_from_hex; -use crate::deploy::Deploy; -use crate::deploy::esp::find_esp_mount; -use crate::layout::boot_plugins::{BOOT_PLUGINS_DIR, MANIFEST_EXTENSION}; -use crate::mutated::uninstaller::{NewPrefixDigest, RequestedBootPlugin, ResolvedBootEntry, UninstallError}; +use crate::deploy::{Deploy, find_esp_mount}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::plugin::boot::resolve_boot_plugin; +use crate::plugin::boot::BootPlugins; pub struct CheckoutStage; @@ -21,18 +22,18 @@ impl Stage for CheckoutStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UninstallError> { - let new_prefix = ctx_get!(context, NewPrefixDigest); + let new_state = ctx_get!(context, NewState); let deploy = ctx_get!(context, Deploy); - let requested = ctx_get!(context, RequestedBootPlugin); + let requested_boot_plugin = ctx_get!(context, RequestedBootPlugin); let repository = deploy.open_repository()?; - let tree = deploy.open_tree(&new_prefix.0)?; - let digest = object_id_from_hex(&new_prefix.0)?; + let tree = deploy.open_tree(&new_state.prefix_digest)?; + let digest = object_id_from_hex(&new_state.prefix_digest)?; let esp_mount = find_esp_mount()?; - let entry_name = write_boot_entry(&repository, &tree, digest, &esp_mount, &new_prefix.0)?; + let entry_name = write_boot_entry(&repository, &tree, digest, &esp_mount, &new_state.prefix_digest)?; - let plugin = resolve_boot_plugin(BOOT_PLUGINS_DIR, MANIFEST_EXTENSION, requested.0.as_deref())?; + let plugin = BootPlugins::new()?.load(&requested_boot_plugin)?; context.put(ResolvedBootEntry { plugin, entry_name }); diff --git a/lib/lib/src/mutated/uninstaller/commit.rs b/lib/lib/src/mutated/uninstaller/commit.rs index 905767b4..0cfef3b0 100644 --- a/lib/lib/src/mutated/uninstaller/commit.rs +++ b/lib/lib/src/mutated/uninstaller/commit.rs @@ -10,9 +10,12 @@ use composefs::fsverity::FsVerityHashValue; use composefs::generic_tree::Stat; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_types::TmpPath; +use upac_types::hook::ProgressEventBuilder; + +use super::{NewState, UninstallError, WorkingState}; use crate::composefs::error::RepoError; use crate::composefs::file::FileHandle; @@ -20,11 +23,8 @@ use crate::composefs::repository::commit_tree; use crate::database::InMemory; use crate::deploy::Deploy; use crate::layout::database::{DATABASE_PATH, UNINSTALL_SCRATCH_FILENAME}; -use crate::mutated::uninstaller::{ - NewPrefixDigest, RemovedConfigPaths, UninstallError, WorkingDatabase, WorkingRemovedConfigPaths, WorkingTree, -}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct CommitTransactionStage; @@ -32,17 +32,15 @@ impl Stage for CommitTransactionStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UninstallError> { - let working_tree = ctx_take!(context, WorkingTree); - let working_database = ctx_take!(context, WorkingDatabase); - let removed_config_paths = ctx_take!(context, WorkingRemovedConfigPaths); + let working_state = ctx_take!(context, WorkingState); let tmp_path = ctx_get!(context, TmpPath); let deploy = ctx_get!(context, Deploy); let repository = deploy.open_repository()?; - let mut tree = working_tree.0; + let mut tree = working_state.tree; - let database_bytes = working_database.0.into_bytes()?; + let database_bytes = working_state.database.into_bytes()?; let database_scratch_path = Path::new(tmp_path.as_ref()).join(UNINSTALL_SCRATCH_FILENAME); write(&database_scratch_path, &database_bytes).map_err(RepoError::from)?; @@ -56,8 +54,10 @@ impl Stage for CommitTransactionStage { let digest = commit_tree(&repository, tree)?; - context.put(NewPrefixDigest(digest.to_hex())); - context.put(RemovedConfigPaths(removed_config_paths.0)); + context.put(NewState { + prefix_digest: digest.to_hex(), + removed_config_paths: working_state.removed_config_paths, + }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/uninstaller/error.rs b/lib/lib/src/mutated/uninstaller/error.rs index d84f5237..e61c5a42 100644 --- a/lib/lib/src/mutated/uninstaller/error.rs +++ b/lib/lib/src/mutated/uninstaller/error.rs @@ -16,6 +16,10 @@ use crate::errors::{ use crate::lock::LockError; use crate::plugin::boot::error::BootPluginError; +#[cfg(test)] +#[path = "../../../tests/inline/mutated_uninstaller_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum UninstallError { PackageNotFound, diff --git a/lib/lib/src/mutated/uninstaller/merge.rs b/lib/lib/src/mutated/uninstaller/merge.rs index baf84bc1..1498361a 100644 --- a/lib/lib/src/mutated/uninstaller/merge.rs +++ b/lib/lib/src/mutated/uninstaller/merge.rs @@ -8,20 +8,23 @@ use std::fs::create_dir_all; use composefs::fsverity::FsVerityHashValue; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{CommitInfo, NewState, UninstallError}; use crate::composefs::file::FileHandle; use crate::composefs::overlay::apply_overlay_upper; use crate::composefs::repository::commit_tree; -use crate::config::merge::merge_config; +use crate::config::merge_config; use crate::database::error::DeployRecordError; use crate::database::record::DeployRecord; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::deployment::CONFIG_DIR_NAME; -use crate::mutated::uninstaller::{CommitMessage, NewPrefixDigest, RemovedConfigPaths, Subject, UninstallError}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct MergeStage; @@ -29,12 +32,9 @@ impl Stage for MergeStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UninstallError> { - let removed_config_paths = ctx_take!(context, RemovedConfigPaths); - - let new_prefix = ctx_get!(context, NewPrefixDigest); + let new_state = ctx_get!(context, NewState); let deploy = ctx_get!(context, Deploy); - let subject = ctx_get!(context, Subject); - let message = ctx_get!(context, CommitMessage); + let commit_info = ctx_get!(context, CommitInfo); let repository = deploy.open_repository()?; @@ -52,23 +52,23 @@ impl Stage for MergeStage { let mut new = base_config_layout.clone(); - for path in &removed_config_paths.0 { + for path in &new_state.removed_config_paths { FileHandle::new(path).remove_in_tree(&mut new)?; } let merge_result = merge_config(&base_config_layout, &new, &live_config_layout, true)?; let new_config_digest = commit_tree(&repository, merge_result.tree)?.to_hex(); - let new_record_dir = deploy.deploy(&new_prefix.0); + let new_record_dir = deploy.deploy(&new_state.prefix_digest); let mut record = match DeployRecord::read(&new_record_dir) { Ok(existing) => existing, Err(DeployRecordError::NotFound) => { create_dir_all(&new_record_dir).map_err(DeployRecordError::from)?; DeployRecord { - prefix_digest: new_prefix.0.clone(), - subject: subject.0.clone(), - message: message.0.clone(), + prefix_digest: new_state.prefix_digest.clone(), + subject: commit_info.subject.clone(), + message: commit_info.message.clone(), seq: DeployRecord::allocate_seq(&deploy.next_seq_path())?, timestamp: DeployRecord::now_secs(), config_history: Vec::new(), @@ -83,8 +83,8 @@ impl Stage for MergeStage { written.extend(record.update_working_config( &new_record_dir, new_config_digest, - subject.0.clone(), - message.0.clone(), + commit_info.subject.clone(), + commit_info.message.clone(), )?); Ok((progress, StageResult::Advance, Box::new(written))) diff --git a/lib/lib/src/mutated/uninstaller/mod.rs b/lib/lib/src/mutated/uninstaller/mod.rs index 5ac2546a..0153a3d5 100644 --- a/lib/lib/src/mutated/uninstaller/mod.rs +++ b/lib/lib/src/mutated/uninstaller/mod.rs @@ -10,24 +10,19 @@ use composefs::tree::FileSystem; use uuid::Uuid; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::package::CPackageInfo; use upac_abi::request::CUninstallRequest; -use crate::composefs::repository::ObjectID; -use crate::database::MemoryDatabase; -use crate::deploy::retention::RetentionStage; -use crate::deploy::{Deploy, DeployMode}; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; -use crate::plugin::boot::BootPlugin; -use crate::scripts::HookStage; -use crate::scripts::pipeline::{Operation, PipelineTrigger}; - +use upac_types::hook::Message; +use upac_types::package::PackageEntry; use upac_types::states::UninstallStateId; -use upac_types::{PackageEntry, Targets, TmpPath}; +use upac_types::traits::MessageHook; +use upac_types::{TmpPath, UninstallPackagesTargets}; -pub use self::error::UninstallError; +use upac_macro::ContextValue; use self::checkout::CheckoutStage; use self::commit::CommitTransactionStage; @@ -37,6 +32,18 @@ use self::preparation::PreparationStage; use self::remove::RemovePackageStage; use self::swap::SwapStage; +use crate::composefs::repository::ObjectID; +use crate::database::MemoryDatabase; +use crate::deploy::retention::RetentionStage; +use crate::deploy::{Deploy, DeployMode}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; +use crate::plugin::boot::BootPlugin; +use crate::scripts::HookStage; +use crate::scripts::pipeline::{Operation, PipelineTrigger}; + +pub use self::error::UninstallError; + mod checkout; mod commit; mod error; @@ -46,23 +53,39 @@ mod preparation; mod remove; mod swap; +#[derive(ContextValue)] pub(crate) struct PackageUuidsToRemove(pub Vec); -pub(crate) struct NewPrefixDigest(pub String); -pub(crate) struct RemovedConfigPaths(pub Vec); -pub(crate) struct Subject(pub String); -pub(crate) struct CommitMessage(pub Option); -pub(crate) struct RequestedBootPlugin(pub Option); + +pub(crate) struct NewState { + pub prefix_digest: String, + pub removed_config_paths: Vec, +} + +pub(crate) struct CommitInfo { + pub subject: String, + pub message: Option, +} + +#[derive(ContextValue)] pub(crate) struct Purge(pub bool); + +#[derive(ContextValue)] +pub(crate) struct RequestedBootPlugin(pub String); pub(crate) struct ResolvedBootEntry { pub plugin: BootPlugin, pub entry_name: String, } -pub(crate) struct PendingUuids(pub VecDeque); -pub(crate) struct TotalPackages(pub u64); -pub(crate) struct WorkingTree(pub FileSystem); -pub(crate) struct WorkingDatabase(pub MemoryDatabase); -pub(crate) struct WorkingRemovedConfigPaths(pub Vec); +pub(crate) struct RemoveProgress { + pub pending: VecDeque, + pub total: u64, +} + +pub(crate) struct WorkingState { + pub tree: FileSystem, + pub database: MemoryDatabase, + pub removed_config_paths: Vec, +} pub struct UninstallPackage<'a> { pub name: &'a str, @@ -86,9 +109,11 @@ impl<'a> TryFrom<&'a CPackageInfo> for UninstallPackage<'a> { pub struct UninstallData<'a> { pub packages: Vec>, - pub boot_plugin: Option<&'a str>, + pub purge: bool, + pub boot_plugin: &'a str, + pub tmp_path: &'a str, pub subject: &'a str, @@ -106,13 +131,15 @@ impl<'a> TryFrom<&'a CUninstallRequest> for UninstallData<'a> { fn try_from(request: &'a CUninstallRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(UninstallData { packages: Vec::try_from(&request.packages)?, - boot_plugin: (&request.boot_plugin).try_into()?, + purge: request.purge, + boot_plugin: (&request.boot_plugin).try_into()?, + tmp_path: (&request.tmp_path).try_into()?, subject: (&request.subject).try_into()?, @@ -130,7 +157,7 @@ pub fn run(data: UninstallData) -> Result<(), (UninstallStateId, UninstallError) let deploy = Deploy::new(DeployMode::ReadWrite).map_err(|error| (UninstallStateId::Setup, UninstallError::from(error)))?; - let targets = Targets( + let targets = UninstallPackagesTargets( data.packages .iter() .map(|package| PackageEntry { @@ -145,9 +172,11 @@ pub fn run(data: UninstallData) -> Result<(), (UninstallStateId, UninstallError) context.put(targets); context.put(deploy); context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Subject(data.subject.to_owned())); - context.put(CommitMessage(data.message.map(str::to_owned))); - context.put(RequestedBootPlugin(data.boot_plugin.map(str::to_owned))); + context.put(CommitInfo { + subject: data.subject.to_owned(), + message: data.message.map(str::to_owned), + }); + context.put(RequestedBootPlugin(data.boot_plugin.to_owned())); context.put(Purge(data.purge)); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); diff --git a/lib/lib/src/mutated/uninstaller/open.rs b/lib/lib/src/mutated/uninstaller/open.rs index e069c445..ddeb84c2 100644 --- a/lib/lib/src/mutated/uninstaller/open.rs +++ b/lib/lib/src/mutated/uninstaller/open.rs @@ -5,19 +5,19 @@ use std::collections::VecDeque; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{PackageUuidsToRemove, RemoveProgress, UninstallError, WorkingState}; use crate::composefs::file::FileHandle; use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::database::DATABASE_PATH; -use crate::mutated::uninstaller::{ - PackageUuidsToRemove, PendingUuids, TotalPackages, UninstallError, WorkingDatabase, WorkingRemovedConfigPaths, - WorkingTree, -}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct OpenTransactionStage; @@ -36,14 +36,15 @@ impl Stage for OpenTransactionStage { let database_bytes = FileHandle::new(DATABASE_PATH).read_file(&repository, &tree)?; let database = MemoryDatabase::open_in_memory(database_bytes)?; - let total = uuids.0.len() as u64; + let total = uuids.len() as u64; let pending: VecDeque<_> = uuids.0.into_iter().collect(); - context.put(WorkingTree(tree)); - context.put(WorkingDatabase(database)); - context.put(WorkingRemovedConfigPaths(Vec::new())); - context.put(PendingUuids(pending)); - context.put(TotalPackages(total)); + context.put(WorkingState { + tree, + database, + removed_config_paths: Vec::new(), + }); + context.put(RemoveProgress { total, pending }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/uninstaller/preparation.rs b/lib/lib/src/mutated/uninstaller/preparation.rs index d3432c76..2ff9ffd6 100644 --- a/lib/lib/src/mutated/uninstaller/preparation.rs +++ b/lib/lib/src/mutated/uninstaller/preparation.rs @@ -3,9 +3,13 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use upac_types::{DeclarativeTrigger, Targets}; +use upac_types::UninstallPackagesTargets; +use upac_types::decoder::DeclarativeTrigger; +use upac_types::hook::ProgressEventBuilder; + +use super::{PackageUuidsToRemove, UninstallError}; use crate::composefs::file::FileHandle; use crate::database::meta::MetaStore; @@ -14,9 +18,8 @@ use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::database::DATABASE_PATH; -use crate::mutated::uninstaller::{PackageUuidsToRemove, UninstallError}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; pub struct PreparationStage; @@ -24,7 +27,7 @@ impl Stage for PreparationStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UninstallError> { - let targets = ctx_get!(context, Targets); + let targets = ctx_get!(context, UninstallPackagesTargets); let deploy = ctx_get!(context, Deploy); let current_prefix = current_prefix_digest()?; diff --git a/lib/lib/src/mutated/uninstaller/remove.rs b/lib/lib/src/mutated/uninstaller/remove.rs index c7ce000e..5fed6c55 100644 --- a/lib/lib/src/mutated/uninstaller/remove.rs +++ b/lib/lib/src/mutated/uninstaller/remove.rs @@ -3,20 +3,21 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use upac_types::FileEntryScope; +use upac_types::hook::ProgressEventBuilder; + +use upac_types::entry::FileEntryScope; + +use super::{Purge, RemoveProgress, UninstallError, WorkingState}; use crate::composefs::file::FileHandle; use crate::database::files::{FileStore, FileStoreMut}; use crate::database::meta::{MetaStore, MetaStoreMut}; use crate::database::triggers::TriggerStoreMut; use crate::errors::CommonError; -use crate::mutated::uninstaller::{ - PendingUuids, Purge, TotalPackages, UninstallError, WorkingDatabase, WorkingRemovedConfigPaths, WorkingTree, -}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct RemovePackageStage; @@ -24,23 +25,20 @@ impl Stage for RemovePackageStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UninstallError> { - let mut pending = ctx_take!(context, PendingUuids); - let mut woking_tree = ctx_take!(context, WorkingTree); - let mut woking_database = ctx_take!(context, WorkingDatabase); - let mut removed_config_paths = ctx_take!(context, WorkingRemovedConfigPaths); + let mut woking_state = ctx_take!(context, WorkingState); + let mut remove_progress = ctx_take!(context, RemoveProgress); - let total_packages = ctx_get!(context, TotalPackages); let purge = ctx_get!(context, Purge); - let uuid = pending.0.pop_front().ok_or(CommonError::MissingResult)?; + let uuid = remove_progress.pending.pop_front().ok_or(CommonError::MissingResult)?; - let subject = woking_database - .0 + let subject = woking_state + .database .get_package_meta(uuid)? .map(|meta| meta.name) .unwrap_or_default(); - let files = woking_database.0.list_package_files(uuid)?; + let files = woking_state.database.list_package_files(uuid)?; for entry in files { if entry.is_user && !purge.0 { @@ -49,43 +47,41 @@ impl Stage for RemovePackageStage { match entry.scope { FileEntryScope::Prefix => { - FileHandle::new(&entry.path).remove_in_tree(&mut woking_tree.0)?; + FileHandle::new(&entry.path).remove_in_tree(&mut woking_state.tree)?; } FileEntryScope::Config => { - removed_config_paths.0.push(entry.path.clone()); + woking_state.removed_config_paths.push(entry.path.clone()); } } if entry.is_user { - woking_database.0.remove_user_file(uuid, &entry.path)?; + woking_state.database.remove_user_file(uuid, &entry.path)?; } else { - woking_database.0.remove_package_file(uuid, &entry.path)?; + woking_state.database.remove_package_file(uuid, &entry.path)?; } } - let meta = woking_database - .0 + let meta = woking_state + .database .get_package_meta(uuid)? .ok_or(UninstallError::PackageNotFound)?; - woking_database - .0 + woking_state + .database .remove_package_meta(&meta.name, &meta.arch, meta.arch_sub.as_deref())?; - woking_database.0.remove_declarative_triggers(uuid)?; + woking_state.database.remove_declarative_triggers(uuid)?; - let remaining = pending.0.len() as u64; - let processed = total_packages.0 - remaining; - progress = progress.subject(subject).progress(processed, total_packages.0); + let remaining = remove_progress.pending.len() as u64; + let processed = remove_progress.total - remaining; + progress = progress.subject(subject).progress(processed, remove_progress.total); - let result = if pending.0.is_empty() { + let result = if remove_progress.pending.is_empty() { StageResult::Advance } else { StageResult::Repeat }; - context.put(pending); - context.put(woking_tree); - context.put(woking_database); - context.put(removed_config_paths); + context.put(remove_progress); + context.put(woking_state); Ok((progress, result, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/uninstaller/swap.rs b/lib/lib/src/mutated/uninstaller/swap.rs index e48ee0ae..e4f6f896 100644 --- a/lib/lib/src/mutated/uninstaller/swap.rs +++ b/lib/lib/src/mutated/uninstaller/swap.rs @@ -3,11 +3,15 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::mutated::uninstaller::{ResolvedBootEntry, UninstallError}; +use upac_types::hook::ProgressEventBuilder; +use upac_types::request::BootPluginSetOneShotRequest; + +use super::{ResolvedBootEntry, UninstallError}; + +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; pub struct SwapStage; @@ -17,7 +21,9 @@ impl Stage for SwapStage { ) -> Result<(ProgressEventBuilder, StageResult, Box), UninstallError> { let resolved = ctx_take!(context, ResolvedBootEntry); - resolved.plugin.set_one_shot(&resolved.entry_name)?; + resolved.plugin.set_one_shot(BootPluginSetOneShotRequest { + entry_name: resolved.entry_name, + })?; Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/update/checkout.rs b/lib/lib/src/mutated/update/checkout.rs index ffee7e4f..4e5e2aae 100644 --- a/lib/lib/src/mutated/update/checkout.rs +++ b/lib/lib/src/mutated/update/checkout.rs @@ -3,17 +3,18 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{NewState, RequestedBootPlugin, ResolvedBootEntry, UpdateError}; use crate::boot::write_boot_entry; use crate::composefs::repository::object_id_from_hex; -use crate::deploy::Deploy; -use crate::deploy::esp::find_esp_mount; -use crate::layout::boot_plugins::{BOOT_PLUGINS_DIR, MANIFEST_EXTENSION}; -use crate::mutated::update::{NewPrefixDigest, RequestedBootPlugin, ResolvedBootEntry, UpdateError}; +use crate::deploy::{Deploy, find_esp_mount}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::plugin::boot::resolve_boot_plugin; +use crate::plugin::boot::BootPlugins; pub struct CheckoutStage; @@ -21,18 +22,18 @@ impl Stage for CheckoutStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UpdateError> { - let new_prefix = ctx_get!(context, NewPrefixDigest); + let new_state = ctx_get!(context, NewState); let deploy = ctx_get!(context, Deploy); - let requested = ctx_get!(context, RequestedBootPlugin); + let requested_boot_plugin = ctx_get!(context, RequestedBootPlugin); let repository = deploy.open_repository()?; - let tree = deploy.open_tree(&new_prefix.0)?; - let digest = object_id_from_hex(&new_prefix.0)?; + let tree = deploy.open_tree(&new_state.prefix_digest)?; + let digest = object_id_from_hex(&new_state.prefix_digest)?; let esp_mount = find_esp_mount()?; - let entry_name = write_boot_entry(&repository, &tree, digest, &esp_mount, &new_prefix.0)?; + let entry_name = write_boot_entry(&repository, &tree, digest, &esp_mount, &new_state.prefix_digest)?; - let plugin = resolve_boot_plugin(BOOT_PLUGINS_DIR, MANIFEST_EXTENSION, requested.0.as_deref())?; + let plugin = BootPlugins::new()?.load(&requested_boot_plugin)?; context.put(ResolvedBootEntry { plugin, entry_name }); diff --git a/lib/lib/src/mutated/update/commit.rs b/lib/lib/src/mutated/update/commit.rs index 60d1a4f7..42ec74d8 100644 --- a/lib/lib/src/mutated/update/commit.rs +++ b/lib/lib/src/mutated/update/commit.rs @@ -10,9 +10,12 @@ use composefs::fsverity::FsVerityHashValue; use composefs::generic_tree::Stat; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_types::TmpPath; +use upac_types::hook::ProgressEventBuilder; + +use super::{ImportedState, NewState, UpdateError}; use crate::composefs::error::RepoError; use crate::composefs::file::FileHandle; @@ -20,12 +23,8 @@ use crate::composefs::repository::commit_tree; use crate::database::InMemory; use crate::deploy::Deploy; use crate::layout::database::{DATABASE_PATH, UPDATE_SCRATCH_FILENAME}; -use crate::mutated::update::{ - ImportedConfigDefaults, ImportedDatabase, ImportedRemovedConfigPaths, ImportedTree, NewConfigDefaults, - NewPrefixDigest, RemovedConfigPaths, UpdateError, -}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct CommitTransactionStage; @@ -33,19 +32,16 @@ impl Stage for CommitTransactionStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UpdateError> { - let tree = ctx_take!(context, ImportedTree); - let config_defaults = ctx_take!(context, ImportedConfigDefaults); - let database = ctx_take!(context, ImportedDatabase); - let removed_config_paths = ctx_take!(context, ImportedRemovedConfigPaths); + let imported_state = ctx_take!(context, ImportedState); let mut import_ctx = ctx_take!(context, ImportContext); let tmp_path = ctx_get!(context, TmpPath); let deploy = ctx_get!(context, Deploy); let repository = deploy.open_repository()?; - let mut tree = tree.0; + let mut tree = imported_state.tree; - let database_bytes = database.0.into_bytes()?; + let database_bytes = imported_state.database.into_bytes()?; let database_scratch_path = Path::new(tmp_path.as_ref()).join(UPDATE_SCRATCH_FILENAME); write(&database_scratch_path, &database_bytes).map_err(RepoError::from)?; @@ -59,9 +55,11 @@ impl Stage for CommitTransactionStage { let digest = commit_tree(&repository, tree)?; - context.put(NewPrefixDigest(digest.to_hex())); - context.put(NewConfigDefaults(config_defaults.0)); - context.put(RemovedConfigPaths(removed_config_paths.0)); + context.put(NewState { + prefix_digest: digest.to_hex(), + config_defaults: imported_state.config_defaults, + removed_config_paths: imported_state.removed_config_paths, + }); Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/update/error.rs b/lib/lib/src/mutated/update/error.rs index e7db861f..fe023562 100644 --- a/lib/lib/src/mutated/update/error.rs +++ b/lib/lib/src/mutated/update/error.rs @@ -16,6 +16,10 @@ use crate::errors::{ use crate::lock::LockError; use crate::plugin::boot::error::BootPluginError; +#[cfg(test)] +#[path = "../../../tests/inline/mutated_update_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum UpdateError { PackageNotFound, diff --git a/lib/lib/src/mutated/update/fetching.rs b/lib/lib/src/mutated/update/fetching.rs index 77b98cd4..a7653ad2 100644 --- a/lib/lib/src/mutated/update/fetching.rs +++ b/lib/lib/src/mutated/update/fetching.rs @@ -3,10 +3,12 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; use crate::mutated::update::UpdateError; -use crate::orchestrator::Context; +use crate::orchestrator::context::Context; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; pub struct FetchingStage; diff --git a/lib/lib/src/mutated/update/import.rs b/lib/lib/src/mutated/update/import.rs index 510f14b6..3fc816ff 100644 --- a/lib/lib/src/mutated/update/import.rs +++ b/lib/lib/src/mutated/update/import.rs @@ -7,9 +7,10 @@ use std::path::Path; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use upac_types::{FileEntry, FileEntryScope}; +use upac_types::entry::{FileEntry, FileEntryScope}; +use upac_types::hook::ProgressEventBuilder; use crate::composefs::file::{FileHandle, import_if_dir}; use crate::database::files::{FileStore, FileStoreMut}; @@ -17,12 +18,9 @@ use crate::database::meta::{MetaStore, MetaStoreMut}; use crate::database::triggers::TriggerStoreMut; use crate::deploy::Deploy; use crate::errors::CommonError; -use crate::mutated::update::{ - AllowDowngrade, ImportedConfigDefaults, ImportedDatabase, ImportedRemovedConfigPaths, ImportedTree, - PendingPackages, TotalPackages, UpdateError, -}; +use crate::mutated::update::{AllowDowngrade, ImportProgress, ImportedState, UpdateError}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct ImportPackageStage; @@ -30,68 +28,74 @@ impl Stage for ImportPackageStage { fn run( &self, context: &mut Context, cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UpdateError> { - let mut pending = ctx_take!(context, PendingPackages); - let mut tree = ctx_take!(context, ImportedTree); - let mut config_defaults = ctx_take!(context, ImportedConfigDefaults); - let mut database = ctx_take!(context, ImportedDatabase); - let mut removed_config_paths = ctx_take!(context, ImportedRemovedConfigPaths); + let mut import_progress = ctx_take!(context, ImportProgress); + let mut imported_state = ctx_take!(context, ImportedState); let mut import_ctx = ctx_take!(context, ImportContext); - let total = ctx_get!(context, TotalPackages); let allow_downgrade = ctx_get!(context, AllowDowngrade); + let deploy = ctx_get!(context, Deploy); - let (package, trigger) = pending.0.pop_front().ok_or(CommonError::MissingResult)?; + let (package, trigger) = import_progress.pending.pop_front().ok_or(CommonError::MissingResult)?; let repository = deploy.open_repository()?; - let uuid = database - .0 + let uuid = imported_state + .database .find_package_uuid(&package.meta.name, &package.meta.arch, package.meta.arch_sub.as_deref())? .ok_or(UpdateError::PackageNotFound)?; if !allow_downgrade.0 { - let current_meta = database.0.get_package_meta(uuid)?.ok_or(UpdateError::PackageNotFound)?; + let current_meta = imported_state + .database + .get_package_meta(uuid)? + .ok_or(UpdateError::PackageNotFound)?; if package.meta.version < current_meta.version { return Err(UpdateError::DowngradeNotAllowed); } } - let old_files = database.0.list_package_files(uuid)?; + let old_files = imported_state.database.list_package_files(uuid)?; for entry in old_files { match entry.scope { FileEntryScope::Prefix => { - FileHandle::new(&entry.path).remove_in_tree(&mut tree.0)?; + FileHandle::new(&entry.path).remove_in_tree(&mut imported_state.tree)?; } FileEntryScope::Config => { - removed_config_paths.0.push(entry.path.clone()); + imported_state.removed_config_paths.push(entry.path.clone()); } } - database.0.remove_package_file(uuid, &entry.path)?; + imported_state.database.remove_package_file(uuid, &entry.path)?; } let source_root = Path::new(&package.temp_package_path); let usr_source = source_root.join("usr"); - let imported = import_if_dir!(&repository, &mut tree.0, &usr_source, &mut import_ctx, cancel); + let imported = import_if_dir!( + &repository, + &mut imported_state.tree, + &usr_source, + &mut import_ctx, + cancel + ); let config_source = source_root.join("etc"); let imported_config = import_if_dir!( &repository, - &mut config_defaults.0, + &mut imported_state.config_defaults, &config_source, &mut import_ctx, cancel ); - database.0.update_package_meta(&package.meta)?; - database.0.set_declarative_triggers(uuid, &trigger)?; + imported_state.database.update_package_meta(&package.meta)?; + imported_state.database.set_declarative_triggers(uuid, &trigger)?; for path in imported { - database.0.insert_package_file( + imported_state.database.insert_package_file( uuid, &FileEntry { path: path.to_string_lossy().into_owned(), @@ -102,7 +106,7 @@ impl Stage for ImportPackageStage { } for path in imported_config { - database.0.insert_package_file( + imported_state.database.insert_package_file( uuid, &FileEntry { path: path.to_string_lossy().into_owned(), @@ -112,21 +116,20 @@ impl Stage for ImportPackageStage { )?; } - let remaining = pending.0.len() as u64; - let processed = total.0 - remaining; - progress = progress.subject(package.meta.name.clone()).progress(processed, total.0); + let remaining = import_progress.pending.len() as u64; + let processed = import_progress.total - remaining; + progress = progress + .subject(package.meta.name.clone()) + .progress(processed, import_progress.total); - let result = if pending.0.is_empty() { + let result = if import_progress.pending.is_empty() { StageResult::Advance } else { StageResult::Repeat }; - context.put(pending); - context.put(tree); - context.put(config_defaults); - context.put(database); - context.put(removed_config_paths); + context.put(import_progress); + context.put(imported_state); context.put(import_ctx); Ok((progress, result, Box::new(NoRollback))) diff --git a/lib/lib/src/mutated/update/merge.rs b/lib/lib/src/mutated/update/merge.rs index 76c1efd0..bcd3427d 100644 --- a/lib/lib/src/mutated/update/merge.rs +++ b/lib/lib/src/mutated/update/merge.rs @@ -8,22 +8,23 @@ use std::fs::create_dir_all; use composefs::fsverity::FsVerityHashValue; use composefs::repository::ImportContext; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{AllowConflictFiles, CommitInfo, NewState, UpdateError}; use crate::composefs::file::FileHandle; use crate::composefs::overlay::{apply_overlay_upper, apply_tree_overlay}; use crate::composefs::repository::commit_tree; -use crate::config::merge::merge_config; +use crate::config::merge_config; use crate::database::error::DeployRecordError; use crate::database::record::DeployRecord; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::deployment::CONFIG_DIR_NAME; -use crate::mutated::update::{ - AllowConflictFiles, CommitMessage, NewConfigDefaults, NewPrefixDigest, RemovedConfigPaths, Subject, UpdateError, -}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct MergeStage; @@ -31,13 +32,9 @@ impl Stage for MergeStage { fn run( &self, context: &mut Context, _cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UpdateError> { - let new_config_defaults = ctx_take!(context, NewConfigDefaults); - let removed_config_paths = ctx_take!(context, RemovedConfigPaths); - - let new_prefix = ctx_get!(context, NewPrefixDigest); + let new_state = ctx_get!(context, NewState); let deploy = ctx_get!(context, Deploy); - let subject = ctx_get!(context, Subject); - let message = ctx_get!(context, CommitMessage); + let commit_info = ctx_get!(context, CommitInfo); let allow_conflict_files = ctx_get!(context, AllowConflictFiles); let repository = deploy.open_repository()?; @@ -55,10 +52,10 @@ impl Stage for MergeStage { let mut new = base.clone(); - for path in &removed_config_paths.0 { + for path in &new_state.removed_config_paths { FileHandle::new(path).remove_in_tree(&mut new)?; } - apply_tree_overlay(&mut new, &new_config_defaults.0)?; + apply_tree_overlay(&mut new, &new_state.config_defaults)?; let merge_result = merge_config(&base, &new, &live, allow_conflict_files.0)?; let new_config_digest = commit_tree(&repository, merge_result.tree)?.to_hex(); @@ -69,16 +66,16 @@ impl Stage for MergeStage { context.send_progress(&progress); } - let new_record_dir = deploy.deploy(&new_prefix.0); + let new_record_dir = deploy.deploy(&new_state.prefix_digest); let mut record = match DeployRecord::read(&new_record_dir) { Ok(existing) => existing, Err(DeployRecordError::NotFound) => { create_dir_all(&new_record_dir).map_err(DeployRecordError::from)?; DeployRecord { - prefix_digest: new_prefix.0.clone(), - subject: subject.0.clone(), - message: message.0.clone(), + prefix_digest: new_state.prefix_digest.clone(), + subject: commit_info.subject.clone(), + message: commit_info.message.clone(), seq: DeployRecord::allocate_seq(&deploy.next_seq_path())?, timestamp: DeployRecord::now_secs(), config_history: Vec::new(), @@ -93,8 +90,8 @@ impl Stage for MergeStage { written.extend(record.update_working_config( &new_record_dir, new_config_digest, - subject.0.clone(), - message.0.clone(), + commit_info.subject.clone(), + commit_info.message.clone(), )?); Ok((progress, StageResult::Advance, Box::new(written))) diff --git a/lib/lib/src/mutated/update/mod.rs b/lib/lib/src/mutated/update/mod.rs index 4acb0b3a..00205421 100644 --- a/lib/lib/src/mutated/update/mod.rs +++ b/lib/lib/src/mutated/update/mod.rs @@ -8,13 +8,19 @@ use std::os::raw::c_void; use composefs::tree::FileSystem; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CUpdateRequest; -use upac_types::{DeclarativeTrigger, PackageTemp}; +use upac_types::TmpPath; +use upac_types::decoder::DeclarativeTrigger; +use upac_types::hook::Message; +use upac_types::package::PackageTemp; +use upac_types::states::UpdateStateId; +use upac_types::traits::MessageHook; -pub use self::error::UpdateError; +use upac_macro::ContextValue; use self::checkout::CheckoutStage; use self::commit::CommitTransactionStage; @@ -30,13 +36,14 @@ use crate::database::MemoryDatabase; use crate::deploy::retention::RetentionStage; use crate::deploy::{Deploy, DeployMode}; use crate::errors::CommonError; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_mutating}; use crate::plugin::boot::BootPlugin; use crate::plugin::decoder::unpack::PackageUnpacker; use crate::scripts::HookStage; use crate::scripts::pipeline::{Operation, PipelineTrigger}; -use upac_types::TmpPath; -use upac_types::states::UpdateStateId; + +pub use self::error::UpdateError; mod checkout; mod commit; @@ -48,31 +55,51 @@ mod open; mod preparation; mod swap; -pub(crate) struct NewPrefixDigest(pub String); -pub(crate) struct NewConfigDefaults(pub FileSystem); -pub(crate) struct RemovedConfigPaths(pub Vec); -pub(crate) struct Subject(pub String); -pub(crate) struct CommitMessage(pub Option); -pub(crate) struct RequestedBootPlugin(pub Option); +pub(crate) struct NewState { + pub prefix_digest: String, + pub config_defaults: FileSystem, + pub removed_config_paths: Vec, +} + +pub(crate) struct CommitInfo { + pub subject: String, + pub message: Option, +} + +#[derive(ContextValue)] +pub(crate) struct RequestedBootPlugin(pub String); pub(crate) struct ResolvedBootEntry { pub plugin: BootPlugin, pub entry_name: String, } + +#[derive(ContextValue)] pub(crate) struct AllowDowngrade(pub bool); +#[derive(ContextValue)] pub(crate) struct AllowConflictFiles(pub bool); -pub(crate) struct PendingPackagePaths(pub VecDeque); -pub(crate) struct UnpackerState(pub PackageUnpacker); -pub(crate) struct PendingPackages(pub VecDeque<(PackageTemp, DeclarativeTrigger)>); -pub(crate) struct TotalPackages(pub u64); -pub(crate) struct ImportedTree(pub FileSystem); -pub(crate) struct ImportedConfigDefaults(pub FileSystem); -pub(crate) struct ImportedDatabase(pub MemoryDatabase); -pub(crate) struct ImportedRemovedConfigPaths(pub Vec); +pub(crate) struct UnpackState { + pub pending_paths: VecDeque, + pub unpacker: PackageUnpacker, +} + +pub(crate) struct ImportProgress { + pub pending: VecDeque<(PackageTemp, DeclarativeTrigger)>, + pub total: u64, +} + +pub(crate) struct ImportedState { + pub tree: FileSystem, + pub config_defaults: FileSystem, + pub database: MemoryDatabase, + pub removed_config_paths: Vec, +} pub struct UpdateData<'a> { pub packages: Vec<&'a str>, - pub boot_plugin: Option<&'a str>, + + pub boot_plugin: &'a str, + pub allow_downgrade: bool, pub allow_conflict_files: bool, @@ -93,11 +120,13 @@ impl<'a> TryFrom<&'a CUpdateRequest> for UpdateData<'a> { fn try_from(request: &'a CUpdateRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(UpdateData { packages: Vec::try_from(&request.packages)?, + boot_plugin: (&request.boot_plugin).try_into()?, + allow_downgrade: request.allow_downgrade, allow_conflict_files: request.allow_conflict_files, @@ -124,16 +153,20 @@ pub fn run(data: UpdateData) -> Result<(), (UpdateStateId, UpdateError)> { let mut context = Context::new(); context.put(deploy); - context.put(UnpackerState(unpacker)); - context.put(PendingPackagePaths( - data.packages.iter().map(|path| (*path).to_owned()).collect(), - )); - context.put(PendingPackages(VecDeque::new())); - context.put(TotalPackages(total_packages)); + context.put(UnpackState { + pending_paths: data.packages.iter().map(|path| (*path).to_owned()).collect(), + unpacker, + }); + context.put(ImportProgress { + pending: VecDeque::new(), + total: total_packages, + }); context.put(TmpPath(data.tmp_path.to_owned())); - context.put(Subject(data.subject.to_owned())); - context.put(CommitMessage(data.message.map(str::to_owned))); - context.put(RequestedBootPlugin(data.boot_plugin.map(str::to_owned))); + context.put(CommitInfo { + subject: data.subject.to_owned(), + message: data.message.map(str::to_owned), + }); + context.put(RequestedBootPlugin(data.boot_plugin.to_owned())); context.put(AllowDowngrade(data.allow_downgrade)); context.put(AllowConflictFiles(data.allow_conflict_files)); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); diff --git a/lib/lib/src/mutated/update/open.rs b/lib/lib/src/mutated/update/open.rs index d4a82b45..16357813 100644 --- a/lib/lib/src/mutated/update/open.rs +++ b/lib/lib/src/mutated/update/open.rs @@ -7,18 +7,19 @@ use composefs::generic_tree::Stat; use composefs::repository::ImportContext; use composefs::tree::FileSystem; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{ImportedState, UpdateError}; use crate::composefs::file::FileHandle; use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; use crate::layout::database::DATABASE_PATH; -use crate::mutated::update::{ - ImportedConfigDefaults, ImportedDatabase, ImportedRemovedConfigPaths, ImportedTree, UpdateError, -}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; pub struct OpenTransactionStage; @@ -35,10 +36,12 @@ impl Stage for OpenTransactionStage { let database_bytes = FileHandle::new(DATABASE_PATH).read_file(&repository, &tree)?; let database = MemoryDatabase::open_in_memory(database_bytes)?; - context.put(ImportedTree(tree)); - context.put(ImportedConfigDefaults(FileSystem::new(Stat::uninitialized()))); - context.put(ImportedDatabase(database)); - context.put(ImportedRemovedConfigPaths(Vec::new())); + context.put(ImportedState { + tree, + config_defaults: FileSystem::new(Stat::uninitialized()), + database, + removed_config_paths: Vec::new(), + }); context.put(ImportContext::default()); Ok((progress, StageResult::Advance, Box::new(NoRollback))) diff --git a/lib/lib/src/mutated/update/preparation.rs b/lib/lib/src/mutated/update/preparation.rs index 47385be8..f721e19c 100644 --- a/lib/lib/src/mutated/update/preparation.rs +++ b/lib/lib/src/mutated/update/preparation.rs @@ -7,14 +7,16 @@ use std::fs::remove_dir_all; use std::path::PathBuf; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_types::TmpPath; +use upac_types::hook::ProgressEventBuilder; + +use super::{ImportProgress, UnpackState, UpdateError}; use crate::errors::CommonError; -use crate::mutated::update::{PendingPackagePaths, PendingPackages, TotalPackages, UnpackerState, UpdateError}; +use crate::orchestrator::context::{Context, ctx_get, ctx_take}; use crate::orchestrator::stage::{RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get, ctx_take}; pub struct PreparationStage; @@ -24,38 +26,40 @@ impl Stage for PreparationStage { fn run( &self, context: &mut Context, cancel: &CancelToken, mut progress: ProgressEventBuilder, ) -> Result<(ProgressEventBuilder, StageResult, Box), UpdateError> { - let mut pending_paths = ctx_take!(context, PendingPackagePaths); - let mut unpacker = ctx_take!(context, UnpackerState); - let mut pending_packages = ctx_take!(context, PendingPackages); + let mut unpack_state = ctx_take!(context, UnpackState); + let mut import_progress = ctx_take!(context, ImportProgress); let tmp_path = ctx_get!(context, TmpPath); - let total_packages = ctx_get!(context, TotalPackages); - let package_path = pending_paths.0.pop_front().ok_or(CommonError::MissingResult)?; - let index = pending_packages.0.len(); + let package_path = unpack_state + .pending_paths + .pop_front() + .ok_or(CommonError::MissingResult)?; + let index = import_progress.pending.len(); - let (package, trigger) = unpacker - .0 + let (package, trigger) = unpack_state + .unpacker .unpack_one(&package_path, index, tmp_path.as_ref(), cancel) .map_err(CommonError::Decoder)?; let guard = UnpackedPackageDir(PathBuf::from(&package.temp_package_path)); - pending_packages.0.push_back((package, trigger)); + import_progress.pending.push_back((package, trigger)); - let remaining = pending_paths.0.len() as u64; - let processed = total_packages.0 - remaining; - progress = progress.subject(package_path).progress(processed, total_packages.0); + let remaining = unpack_state.pending_paths.len() as u64; + let processed = import_progress.total - remaining; + progress = progress + .subject(package_path) + .progress(processed, import_progress.total); - let result = if pending_paths.0.is_empty() { + let result = if unpack_state.pending_paths.is_empty() { StageResult::Advance } else { StageResult::Repeat }; - context.put(pending_paths); - context.put(unpacker); - context.put(pending_packages); + context.put(unpack_state); + context.put(import_progress); Ok((progress, result, Box::new(guard))) } diff --git a/lib/lib/src/mutated/update/swap.rs b/lib/lib/src/mutated/update/swap.rs index 9d179e71..396d4a65 100644 --- a/lib/lib/src/mutated/update/swap.rs +++ b/lib/lib/src/mutated/update/swap.rs @@ -3,11 +3,15 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::mutated::update::{ResolvedBootEntry, UpdateError}; +use upac_types::hook::ProgressEventBuilder; +use upac_types::request::BootPluginSetOneShotRequest; + +use super::{ResolvedBootEntry, UpdateError}; + +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; pub struct SwapStage; @@ -17,7 +21,9 @@ impl Stage for SwapStage { ) -> Result<(ProgressEventBuilder, StageResult, Box), UpdateError> { let resolved = ctx_take!(context, ResolvedBootEntry); - resolved.plugin.set_one_shot(&resolved.entry_name)?; + resolved.plugin.set_one_shot(BootPluginSetOneShotRequest { + entry_name: resolved.entry_name, + })?; Ok((progress, StageResult::Advance, Box::new(NoRollback))) } diff --git a/lib/lib/src/orchestrator/context.rs b/lib/lib/src/orchestrator/context.rs new file mode 100644 index 00000000..5655a580 --- /dev/null +++ b/lib/lib/src/orchestrator/context.rs @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::any::{Any, TypeId}; +use std::collections::{HashMap, HashSet}; +use std::io::Error as IoError; +use std::sync::Arc; + +use tokio::runtime::Runtime; + +use upac_abi::hook::HookAck; +use upac_types::hook::ProgressEventBuilder; +use upac_types::traits::MessageHook; + +use crate::orchestrator::stage::RollbackGuard; + +macro_rules! ctx_get { + ($context:expr, $ty:ty) => { + $context + .get::<$ty>() + .ok_or($crate::errors::CommonError::MissingResult)? + }; +} +pub(crate) use ctx_get; + +macro_rules! ctx_take { + ($context:expr, $ty:ty) => { + $context + .take::<$ty>() + .ok_or($crate::errors::CommonError::MissingResult)? + }; +} +pub(crate) use ctx_take; + +pub struct Context { + slots: HashMap>, + pub(super) rollback: Vec>, +} + +impl Context { + pub fn new() -> Self { + Self { + slots: HashMap::new(), + rollback: Vec::new(), + } + } + + pub fn put(&mut self, value: T) { + self.slots.insert(TypeId::of::(), Box::new(value)); + } + + pub fn get(&self) -> Option<&T> { + self.slots + .get(&TypeId::of::()) + .and_then(|slot| slot.downcast_ref::()) + } + + pub fn take(&mut self) -> Option { + self.slots + .remove(&TypeId::of::()) + .and_then(|slot| slot.downcast::().ok()) + .map(|boxed| *boxed) + } + + pub fn runtime(&mut self) -> Result, IoError> { + if let Some(runtime) = self.get::>() { + return Ok(Arc::clone(runtime)); + } + + let runtime = Arc::new(Runtime::new()?); + self.put(Arc::clone(&runtime)); + + Ok(runtime) + } + + pub fn send_progress(&self, progress: &ProgressEventBuilder) { + if let Some(hook) = self.get::>() { + let event = progress.build(); + while hook.send(&event) == HookAck::Retry {} + } + } + + pub(super) fn type_ids(&self) -> HashSet { + self.slots.keys().copied().collect() + } + + pub(super) fn unwind(&mut self) { + while let Some(mut guard) = self.rollback.pop() { + let _ = guard.rollback(); + } + } +} + +impl Default for Context { + fn default() -> Self { + Self::new() + } +} diff --git a/lib/lib/src/orchestrator/cursor.rs b/lib/lib/src/orchestrator/cursor.rs index 97146021..2afcc76e 100644 --- a/lib/lib/src/orchestrator/cursor.rs +++ b/lib/lib/src/orchestrator/cursor.rs @@ -8,7 +8,7 @@ use std::any::TypeId; use upac_abi::hook::CancelToken; use crate::errors::CommonError; -use crate::orchestrator::Context; +use crate::orchestrator::context::Context; use crate::orchestrator::stage::{Stage, StageResult}; pub struct Cursor { diff --git a/lib/lib/src/orchestrator/mod.rs b/lib/lib/src/orchestrator/mod.rs index eb5da1b0..e96096be 100644 --- a/lib/lib/src/orchestrator/mod.rs +++ b/lib/lib/src/orchestrator/mod.rs @@ -3,24 +3,25 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::any::{Any, TypeId}; -use std::collections::{HashMap, HashSet}; -use std::io::Error as IoError; +use std::any::TypeId; use std::sync::Arc; use tokio::runtime::Runtime; use tokio::task::JoinSet; -use upac_abi::hook::{CancelToken, HookAck, MessageHook, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; +use upac_types::hook::ProgressEventBuilder; use crate::errors::CommonError; use crate::lock::Lock; +use crate::orchestrator::context::Context; use crate::orchestrator::cursor::Cursor; use crate::orchestrator::error::OrchestratorError; -use crate::orchestrator::stage::{ConcurrentStage, RollbackGuard, Stage, StageResult}; +use crate::orchestrator::stage::{ConcurrentStage, Stage, StageResult}; mod cursor; +pub mod context; pub mod error; pub mod stage; @@ -69,91 +70,8 @@ macro_rules! run_unmutated { } pub(crate) use run_unmutated; -macro_rules! ctx_get { - ($context:expr, $ty:ty) => { - $context - .get::<$ty>() - .ok_or($crate::errors::CommonError::MissingResult)? - }; -} -pub(crate) use ctx_get; - -macro_rules! ctx_take { - ($context:expr, $ty:ty) => { - $context - .take::<$ty>() - .ok_or($crate::errors::CommonError::MissingResult)? - }; -} -pub(crate) use ctx_take; - pub type StagePipelineError = TypeId; -pub struct Context { - slots: HashMap>, - rollback: Vec>, -} - -impl Context { - pub fn new() -> Self { - Self { - slots: HashMap::new(), - rollback: Vec::new(), - } - } - - pub fn put(&mut self, value: T) { - self.slots.insert(TypeId::of::(), Box::new(value)); - } - - pub fn get(&self) -> Option<&T> { - self.slots - .get(&TypeId::of::()) - .and_then(|slot| slot.downcast_ref::()) - } - - pub fn take(&mut self) -> Option { - self.slots - .remove(&TypeId::of::()) - .and_then(|slot| slot.downcast::().ok()) - .map(|boxed| *boxed) - } - - pub fn runtime(&mut self) -> Result, IoError> { - if let Some(runtime) = self.get::>() { - return Ok(Arc::clone(runtime)); - } - - let runtime = Arc::new(Runtime::new()?); - self.put(Arc::clone(&runtime)); - - Ok(runtime) - } - - pub fn send_progress(&self, progress: &ProgressEventBuilder) { - if let Some(hook) = self.get::>() { - let event = progress.build(); - while hook.send(&event) == HookAck::Retry {} - } - } - - fn type_ids(&self) -> HashSet { - self.slots.keys().copied().collect() - } - - fn unwind(&mut self) { - while let Some(mut guard) = self.rollback.pop() { - let _ = guard.rollback(); - } - } -} - -impl Default for Context { - fn default() -> Self { - Self::new() - } -} - pub trait Orchestrator: Sized { fn run_exclusive(self, context: &mut Context, cancel: &CancelToken) -> Result<(), OrchestratorError>; diff --git a/lib/lib/src/orchestrator/stage.rs b/lib/lib/src/orchestrator/stage.rs index 9c7fdcfb..134306a1 100644 --- a/lib/lib/src/orchestrator/stage.rs +++ b/lib/lib/src/orchestrator/stage.rs @@ -6,9 +6,10 @@ use std::any::{Any, TypeId}; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; +use upac_types::hook::ProgressEventBuilder; -use crate::orchestrator::Context; +use crate::orchestrator::context::Context; #[derive(Clone, Copy)] pub enum StageResult { diff --git a/lib/lib/src/plugin/boot/dynamic_link.rs b/lib/lib/src/plugin/boot/dynamic_link.rs new file mode 100644 index 00000000..2723c790 --- /dev/null +++ b/lib/lib/src/plugin/boot/dynamic_link.rs @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use libloading::Library; + +use upac_abi::{BOOT_ABI_VERSION, BootPluginAbiVersionFn, ConfirmBootFn, InstallFn, SetOneShotFn}; + +use super::BootPlugin; +use super::error::BootPluginError; +use super::manifest::BootPluginManifests; + +macro_rules! load_symbol { + ($library:expr, $name:literal) => { + unsafe { load_symbol(&$library, $name)? } + }; +} + +unsafe fn load_symbol(library: &Library, name: &str) -> Result { + unsafe { library.get::(name.as_bytes()) } + .map(|symbol| *symbol) + .map_err(|_| BootPluginError::Symbol) +} + +impl BootPlugin { + pub(super) fn load_plugin(library_name: &str) -> Result { + let library = unsafe { Library::new(library_name) }.map_err(|_| BootPluginError::Load)?; + + let booter_abi_version: BootPluginAbiVersionFn = load_symbol!(library, "boot_abi_version"); + let set_one_shot: SetOneShotFn = load_symbol!(library, "set_one_shot"); + let confirm_boot: ConfirmBootFn = load_symbol!(library, "confirm_boot"); + let install: InstallFn = load_symbol!(library, "install"); + + let got_booter_abi_version = unsafe { booter_abi_version() }; + if got_booter_abi_version != BOOT_ABI_VERSION { + return Err(BootPluginError::AbiMismatch { + got: got_booter_abi_version, + expected: BOOT_ABI_VERSION, + }); + } + + Ok(BootPlugin { + set_one_shot, + confirm_boot, + + install, + _library: Some(library), + }) + } +} + +pub(super) fn load_boot_plugin_dynamic( + manifests: &BootPluginManifests, name: &str, +) -> Result { + let manifest = manifests + .0 + .get(name) + .ok_or_else(|| BootPluginError::UnknownName(name.to_owned()))?; + + BootPlugin::load_plugin(&manifest.library) +} diff --git a/lib/lib/src/plugin/boot/error.rs b/lib/lib/src/plugin/boot/error.rs index ca7434fa..7cc8f7ca 100644 --- a/lib/lib/src/plugin/boot/error.rs +++ b/lib/lib/src/plugin/boot/error.rs @@ -21,7 +21,6 @@ pub enum BootPluginError { DuplicateName(String), UnknownName(String), NoClaimant, - AmbiguousClaim, } impl From for BootPluginError { @@ -48,7 +47,6 @@ impl From for ErrorKind { BootPluginError::DuplicateName(_) => ErrorKind::InvalidEntry, BootPluginError::UnknownName(_) => ErrorKind::NotFound, BootPluginError::NoClaimant => ErrorKind::NotFound, - BootPluginError::AmbiguousClaim => ErrorKind::InvalidEntry, } } } diff --git a/lib/lib/src/plugin/boot/manifest.rs b/lib/lib/src/plugin/boot/manifest.rs index fc801b7f..ce088a7f 100644 --- a/lib/lib/src/plugin/boot/manifest.rs +++ b/lib/lib/src/plugin/boot/manifest.rs @@ -4,12 +4,14 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::collections::HashMap; -use std::fs; +use std::fs::{read_dir, read_to_string}; use std::io::ErrorKind; use serde::Deserialize; -use crate::plugin::boot::error::BootPluginError; +use super::error::BootPluginError; + +use crate::layout::boot_plugins::{BOOT_PLUGINS_DIR, MANIFEST_EXTENSION}; #[derive(Debug, Clone, Deserialize)] pub struct BootPluginManifest { @@ -17,33 +19,35 @@ pub struct BootPluginManifest { pub library: String, } -pub fn load_boot_plugin_manifests( - boot_plugins_dir: &str, manifest_extension: &str, -) -> Result, BootPluginError> { - let mut manifests = HashMap::new(); +pub struct BootPluginManifests(pub HashMap); - let dir = match fs::read_dir(boot_plugins_dir) { - Ok(dir) => dir, - Err(error) if error.kind() == ErrorKind::NotFound => return Ok(manifests), - Err(error) => return Err(error.into()), - }; +impl BootPluginManifests { + pub fn new() -> Result { + let mut manifests = HashMap::new(); - for entry in dir { - let path = entry?.path(); + let dir = match read_dir(BOOT_PLUGINS_DIR) { + Ok(dir) => dir, + Err(error) if error.kind() == ErrorKind::NotFound => return Ok(BootPluginManifests(manifests)), + Err(error) => return Err(error.into()), + }; - if path.extension().and_then(|extension| extension.to_str()) != Some(manifest_extension) { - continue; - } + for entry in dir { + let path = entry?.path(); + + if path.extension().and_then(|extension| extension.to_str()) != Some(MANIFEST_EXTENSION) { + continue; + } - let raw = fs::read_to_string(&path)?; - let manifest: BootPluginManifest = toml::from_str(&raw)?; + let raw = read_to_string(&path)?; + let manifest: BootPluginManifest = toml::from_str(&raw)?; - if manifests.contains_key(&manifest.name) { - return Err(BootPluginError::DuplicateName(manifest.name)); + if manifests.contains_key(&manifest.name) { + return Err(BootPluginError::DuplicateName(manifest.name)); + } + + manifests.insert(manifest.name.clone(), manifest); } - manifests.insert(manifest.name.clone(), manifest); + Ok(BootPluginManifests(manifests)) } - - Ok(manifests) } diff --git a/lib/lib/src/plugin/boot/mod.rs b/lib/lib/src/plugin/boot/mod.rs index eaee6821..ce49c46a 100644 --- a/lib/lib/src/plugin/boot/mod.rs +++ b/lib/lib/src/plugin/boot/mod.rs @@ -5,258 +5,107 @@ use std::mem::MaybeUninit; -use upac_abi::boot::{CBootPluginRequest, ConfirmBootFn, ProbeFn, SetOneShotFn}; use upac_abi::error::ErrorKind; -use upac_abi::types::{CBorrowed, CSlice}; +use upac_abi::request::{ + CBootPluginConfirmSuccsesBootRequest, CBootPluginInstallRequest, CBootPluginSetOneShotRequest, +}; +use upac_abi::{ConfirmBootFn, InstallFn, SetOneShotFn}; -use crate::plugin::boot::error::BootPluginError; +use upac_types::request::{BootPluginConfirmSuccsesBootRequest, BootPluginInstallRequest, BootPluginSetOneShotRequest}; -#[cfg(feature = "dynamic-plugins")] -use libloading::Library; +use self::error::BootPluginError; -#[cfg(feature = "dynamic-plugins")] -use upac_abi::BOOT_ABI_VERSION; +#[cfg(all(feature = "dynamic-plugins", feature = "builtin-booters"))] +compile_error!("dynamic-plugins and builtin-booters are mutually exclusive"); #[cfg(feature = "dynamic-plugins")] -use upac_abi::boot::AbiVersionFn; +use libloading::Library; #[cfg(feature = "dynamic-plugins")] -use crate::plugin::boot::manifest::load_boot_plugin_manifests; - -#[cfg(feature = "builtin-grub")] -use upac_boot_grub::{confirm_boot as grub_confirm_boot, probe as grub_probe, set_one_shot as grub_set_one_shot}; - -#[cfg(feature = "builtin-systemd-boot")] -use upac_boot_systemd_boot::{ - confirm_boot as systemd_boot_confirm_boot, probe as systemd_boot_probe, set_one_shot as systemd_boot_set_one_shot, -}; - -#[cfg(feature = "builtin-uki")] -use upac_boot_uki::{confirm_boot as uki_confirm_boot, probe as uki_probe, set_one_shot as uki_set_one_shot}; - -#[cfg(feature = "builtin-refind")] -use upac_boot_refind::{ - confirm_boot as refind_confirm_boot, probe as refind_probe, set_one_shot as refind_set_one_shot, -}; +use self::manifest::BootPluginManifests; pub mod error; #[cfg(feature = "dynamic-plugins")] pub mod manifest; -#[cfg(feature = "builtin-booters")] -impl BootPlugin { - fn from_static(probe: ProbeFn, set_one_shot: SetOneShotFn, confirm_boot: ConfirmBootFn) -> Self { - BootPlugin { - probe, - set_one_shot, - confirm_boot, - - #[cfg(feature = "dynamic-plugins")] - _library: None, - } - } -} - -/// Resolves a boot plugin by loading shared objects described by on-disk manifests. -/// -/// Built with `dynamic-plugins`: plugins are discovered at runtime from -/// `boot_plugins_dir`. Any plugin compiled in via `builtin-*` is still reachable -/// through [`static_plugins`], but on-disk manifests take part in the same search. #[cfg(feature = "dynamic-plugins")] -pub fn resolve_boot_plugin( - boot_plugins_dir: &str, manifest_extension: &str, requested: Option<&str>, -) -> Result { - let manifests = load_boot_plugin_manifests(boot_plugins_dir, manifest_extension)?; +mod dynamic_link; - match requested { - Some(name) => { - if let Some(manifest) = manifests.get(name) { - return BootPlugin::load(&manifest.library); - } - - #[cfg(feature = "builtin-booters")] - if let Some((_, plugin)) = static_plugins() - .into_iter() - .find(|(plugin_name, _)| *plugin_name == name) - { - return Ok(plugin); - } - - Err(BootPluginError::UnknownName(name.to_owned())) - } - None => { - let mut claimants = Vec::new(); - for manifest in manifests.values() { - let plugin = BootPlugin::load(&manifest.library)?; - if plugin.probes() { - claimants.push(plugin); - } - } - - #[cfg(feature = "builtin-booters")] - for (_, plugin) in static_plugins() { - if plugin.probes() { - claimants.push(plugin); - } - } +#[cfg(feature = "builtin-booters")] +mod static_link; - let mut claimants = claimants.into_iter(); - match (claimants.next(), claimants.next()) { - (Some(plugin), None) => Ok(plugin), - (None, _) => Err(BootPluginError::NoClaimant), - (Some(_), Some(_)) => Err(BootPluginError::AmbiguousClaim), - } - } - } +pub struct BootPlugins { + #[cfg(feature = "dynamic-plugins")] + manifests: BootPluginManifests, } -/// Resolves a boot plugin from the set compiled into this build. -/// -/// Built without `dynamic-plugins`: this binary contains no code path that loads -/// executable objects from disk. `boot_plugins_dir` and `manifest_extension` are -/// accepted to keep the signature stable across build configurations, and ignored. -/// -/// With no `builtin-*` feature enabled the candidate set is empty and every call -/// returns [`BootPluginError::NoClaimant`]. -#[cfg(not(feature = "dynamic-plugins"))] -pub fn resolve_boot_plugin( - _boot_plugins_dir: &str, _manifest_extension: &str, requested: Option<&str>, -) -> Result { - #[cfg(not(feature = "builtin-booters"))] - { - let _ = requested; - Err(BootPluginError::NoClaimant) +impl BootPlugins { + pub fn new() -> Result { + Ok(BootPlugins { + #[cfg(feature = "dynamic-plugins")] + manifests: BootPluginManifests::new()?, + }) } - #[cfg(feature = "builtin-booters")] - { - let plugins = static_plugins(); + pub fn load(&self, name: &str) -> Result { + #[cfg(feature = "dynamic-plugins")] + return dynamic_link::load_boot_plugin_dynamic(&self.manifests, name); - match requested { - Some(name) => plugins - .into_iter() - .find(|(plugin_name, _)| *plugin_name == name) - .map(|(_, plugin)| plugin) - .ok_or_else(|| BootPluginError::UnknownName(name.to_owned())), - None => { - let mut claimants = plugins.into_iter().filter(|(_, plugin)| plugin.probes()); + #[cfg(feature = "builtin-booters")] + return static_link::load_boot_plugin_static(name); - match (claimants.next(), claimants.next()) { - (Some((_, plugin)), None) => Ok(plugin), - (None, _) => Err(BootPluginError::NoClaimant), - (Some(_), Some(_)) => Err(BootPluginError::AmbiguousClaim), - } - } + #[cfg(not(any(feature = "dynamic-plugins", feature = "builtin-booters")))] + { + let _ = name; + Err(BootPluginError::NoClaimant) } } } -/// The boot plugins linked into this build, in probe order. -/// -/// No ABI version check is performed here: these are compiled from the same source -/// tree by the same compiler, so [`BOOT_ABI_VERSION`] matches by construction. -#[cfg(feature = "builtin-booters")] -#[allow( - clippy::vec_init_then_push, - reason = "each push is independently cfg-gated, vec![] can't express that" -)] -fn static_plugins() -> Vec<(&'static str, BootPlugin)> { - let mut plugins = Vec::new(); - - #[cfg(feature = "builtin-uki")] - plugins.push(( - "uki", - BootPlugin::from_static(uki_probe, uki_set_one_shot, uki_confirm_boot), - )); - - #[cfg(feature = "builtin-systemd-boot")] - plugins.push(( - "systemd-boot", - BootPlugin::from_static(systemd_boot_probe, systemd_boot_set_one_shot, systemd_boot_confirm_boot), - )); - - #[cfg(feature = "builtin-grub")] - plugins.push(( - "grub", - BootPlugin::from_static(grub_probe, grub_set_one_shot, grub_confirm_boot), - )); - - #[cfg(feature = "builtin-refind")] - plugins.push(( - "refind", - BootPlugin::from_static(refind_probe, refind_set_one_shot, refind_confirm_boot), - )); - - plugins -} - -#[cfg(feature = "dynamic-plugins")] -unsafe fn load_symbol(library: &Library, name: &str) -> Result { - unsafe { library.get::(name.as_bytes()) } - .map(|symbol| *symbol) - .map_err(|_| BootPluginError::Symbol) -} - pub struct BootPlugin { - probe: ProbeFn, set_one_shot: SetOneShotFn, confirm_boot: ConfirmBootFn, + install: InstallFn, #[cfg(feature = "dynamic-plugins")] _library: Option, } -#[cfg(feature = "dynamic-plugins")] impl BootPlugin { - pub fn load(library_name: &str) -> Result { - let library = unsafe { Library::new(library_name) }.map_err(|_| BootPluginError::Load)?; + pub fn set_one_shot(&self, request: BootPluginSetOneShotRequest) -> Result<(), BootPluginError> { + let request: CBootPluginSetOneShotRequest = request.into(); - let abi_version: AbiVersionFn = unsafe { load_symbol(&library, "abi_version")? }; - let probe: ProbeFn = unsafe { load_symbol(&library, "probe")? }; - let set_one_shot: SetOneShotFn = unsafe { load_symbol(&library, "set_one_shot")? }; - let confirm_boot: ConfirmBootFn = unsafe { load_symbol(&library, "confirm_boot")? }; + let mut error = MaybeUninit::::uninit(); - let got = unsafe { abi_version() }; - if got != BOOT_ABI_VERSION { - return Err(BootPluginError::AbiMismatch { - got, - expected: BOOT_ABI_VERSION, - }); + let response_code = unsafe { (self.set_one_shot)(&request, error.as_mut_ptr()) }; + if response_code != 0 { + return Err(BootPluginError::Reported(unsafe { error.assume_init() })); } - Ok(BootPlugin { - probe, - set_one_shot, - confirm_boot, - _library: Some(library), - }) + Ok(()) } -} -impl BootPlugin { - pub fn probes(&self) -> bool { - unsafe { (self.probe)() == 1 } - } + pub fn confirm_boot(&self, request: BootPluginConfirmSuccsesBootRequest) -> Result<(), BootPluginError> { + let request: CBootPluginConfirmSuccsesBootRequest = request.into(); - pub fn set_one_shot(&self, entry_name: &str) -> Result<(), BootPluginError> { - let request = CBootPluginRequest::new(CSlice::from_borrowed(entry_name.as_bytes())); let mut error = MaybeUninit::::uninit(); - let code = unsafe { (self.set_one_shot)(&request, error.as_mut_ptr()) }; - if code != 0 { + let response_code = unsafe { (self.confirm_boot)(&request, error.as_mut_ptr()) }; + if response_code != 0 { return Err(BootPluginError::Reported(unsafe { error.assume_init() })); } Ok(()) } - pub fn confirm_boot(&self, entry_name: &str) -> Result<(), BootPluginError> { - let request = CBootPluginRequest::new(CSlice::from_borrowed(entry_name.as_bytes())); + pub fn install(&self, request: BootPluginInstallRequest) -> Result<(), BootPluginError> { + let request: CBootPluginInstallRequest = request.into(); + let mut error = MaybeUninit::::uninit(); - let code = unsafe { (self.confirm_boot)(&request, error.as_mut_ptr()) }; - if code != 0 { + let response_code = unsafe { (self.install)(&request, error.as_mut_ptr()) }; + if response_code != 0 { return Err(BootPluginError::Reported(unsafe { error.assume_init() })); } diff --git a/lib/lib/src/plugin/boot/static_link.rs b/lib/lib/src/plugin/boot/static_link.rs new file mode 100644 index 00000000..7dea4df6 --- /dev/null +++ b/lib/lib/src/plugin/boot/static_link.rs @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::{ConfirmBootFn, InstallFn, SetOneShotFn}; + +use super::BootPlugin; +use super::error::BootPluginError; + +#[cfg(feature = "builtin-grub")] +use upac_boot_grub::{confirm_boot as grub_confirm_boot, install as grub_install, set_one_shot as grub_set_one_shot}; + +#[cfg(feature = "builtin-systemd-boot")] +use upac_boot_systemd_boot::{ + confirm_boot as systemd_boot_confirm_boot, install as systemd_boot_install, + set_one_shot as systemd_boot_set_one_shot, +}; + +#[cfg(feature = "builtin-uki")] +use upac_boot_uki::{confirm_boot as uki_confirm_boot, install as uki_install, set_one_shot as uki_set_one_shot}; + +#[cfg(feature = "builtin-refind")] +use upac_boot_refind::{ + confirm_boot as refind_confirm_boot, install as refind_install, set_one_shot as refind_set_one_shot, +}; + +impl BootPlugin { + fn load_plugin_from_static(set_one_shot: SetOneShotFn, confirm_boot: ConfirmBootFn, install: InstallFn) -> Self { + BootPlugin { + set_one_shot, + confirm_boot, + install, + + #[cfg(feature = "dynamic-plugins")] + _library: None, + } + } +} + +pub(super) fn load_boot_plugin_static(name: &str) -> Result { + #[cfg(feature = "builtin-uki")] + if name == "uki" { + return Ok(BootPlugin::load_plugin_from_static( + uki_set_one_shot, + uki_confirm_boot, + uki_install, + )); + } + + #[cfg(feature = "builtin-systemd-boot")] + if name == "systemd-boot" { + return Ok(BootPlugin::load_plugin_from_static( + systemd_boot_set_one_shot, + systemd_boot_confirm_boot, + systemd_boot_install, + )); + } + + #[cfg(feature = "builtin-grub")] + if name == "grub" { + return Ok(BootPlugin::load_plugin_from_static( + grub_set_one_shot, + grub_confirm_boot, + grub_install, + )); + } + + #[cfg(feature = "builtin-refind")] + if name == "refind" { + return Ok(BootPlugin::load_plugin_from_static( + refind_set_one_shot, + refind_confirm_boot, + refind_install, + )); + } + + Err(BootPluginError::UnknownName(name.to_owned())) +} diff --git a/lib/lib/src/plugin/decoder/dynamic_link.rs b/lib/lib/src/plugin/decoder/dynamic_link.rs new file mode 100644 index 00000000..b1175b4d --- /dev/null +++ b/lib/lib/src/plugin/decoder/dynamic_link.rs @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use libloading::Library; + +use upac_abi::{DECODER_ABI_VERSION, DecodeFn, DecodePluginAbiVersionFn}; + +use super::DecoderPlugin; +use super::error::DecoderError; +use super::manifest::DecoderManifests; + +macro_rules! load_symbol { + ($library:expr, $name:literal) => { + unsafe { load_symbol(&$library, $name)? } + }; +} + +unsafe fn load_symbol(library: &Library, name: &str) -> Result { + unsafe { library.get::(name.as_bytes()) } + .map(|symbol| *symbol) + .map_err(|_| DecoderError::Symbol) +} + +impl DecoderPlugin { + pub(super) fn load_plugin(library_name: &str) -> Result { + let library = unsafe { Library::new(library_name) }.map_err(|_| DecoderError::Load)?; + + let abi_version: DecodePluginAbiVersionFn = load_symbol!(library, "decode_abi_version"); + let decode: DecodeFn = load_symbol!(library, "decode"); + + let got_abi_version = unsafe { abi_version() }; + if got_abi_version != DECODER_ABI_VERSION { + return Err(DecoderError::AbiMismatch { + got: got_abi_version, + expected: DECODER_ABI_VERSION, + }); + } + + Ok(DecoderPlugin { + decode, + _library: Some(library), + }) + } +} + +pub(super) fn load_decoder_dynamic(manifests: &DecoderManifests, format: &str) -> Result { + let manifest = manifests + .0 + .get(format) + .ok_or_else(|| DecoderError::UnknownFormat(format.to_owned()))?; + + DecoderPlugin::load_plugin(&manifest.library) +} diff --git a/lib/lib/src/plugin/decoder/manifest.rs b/lib/lib/src/plugin/decoder/manifest.rs index 9a4a7e3a..7c34e007 100644 --- a/lib/lib/src/plugin/decoder/manifest.rs +++ b/lib/lib/src/plugin/decoder/manifest.rs @@ -5,12 +5,14 @@ use std::collections::HashMap; use std::fs; +use std::io::ErrorKind; use std::str::FromStr; use mime::Mime; use serde::Deserialize; +use crate::layout::decoders::{DECODERS_DIR, MANIFEST_EXTENSION}; use crate::plugin::decoder::error::DecoderError; #[derive(Debug, Clone, Deserialize)] @@ -21,29 +23,37 @@ pub struct DecoderManifest { pub mime: String, } -pub fn load_decoder_manifests( - decoders_dir: &str, manifest_extension: &str, -) -> Result, DecoderError> { - let mut manifests = HashMap::new(); +pub struct DecoderManifests(pub HashMap); - for entry in fs::read_dir(decoders_dir)? { - let path = entry?.path(); +impl DecoderManifests { + pub fn new() -> Result { + let mut manifests = HashMap::new(); - if path.extension().and_then(|extension| extension.to_str()) != Some(manifest_extension) { - continue; - } + let dir = match fs::read_dir(DECODERS_DIR) { + Ok(dir) => dir, + Err(error) if error.kind() == ErrorKind::NotFound => return Ok(DecoderManifests(manifests)), + Err(error) => return Err(error.into()), + }; + + for entry in dir { + let path = entry?.path(); + + if path.extension().and_then(|extension| extension.to_str()) != Some(MANIFEST_EXTENSION) { + continue; + } - let raw = fs::read_to_string(&path)?; - let manifest: DecoderManifest = toml::from_str(&raw)?; + let raw = fs::read_to_string(&path)?; + let manifest: DecoderManifest = toml::from_str(&raw)?; - Mime::from_str(&manifest.mime)?; + Mime::from_str(&manifest.mime)?; - if manifests.contains_key(&manifest.format) { - return Err(DecoderError::DuplicateFormat(manifest.format)); + if manifests.contains_key(&manifest.format) { + return Err(DecoderError::DuplicateFormat(manifest.format)); + } + + manifests.insert(manifest.format.clone(), manifest); } - manifests.insert(manifest.format.clone(), manifest); + Ok(DecoderManifests(manifests)) } - - Ok(manifests) } diff --git a/lib/lib/src/plugin/decoder/mod.rs b/lib/lib/src/plugin/decoder/mod.rs index 1add65a9..6ad75850 100644 --- a/lib/lib/src/plugin/decoder/mod.rs +++ b/lib/lib/src/plugin/decoder/mod.rs @@ -3,125 +3,51 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_types::{Dependency, PackageMeta}; - -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] use std::mem::MaybeUninit; -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] -use std::str::from_utf8; -#[cfg(feature = "dynamic-plugins")] -use libloading::Library; +use upac_abi::DecodeFn; +use upac_abi::hook::CancelToken; +use upac_abi::request::CDecodeRequest; +use upac_abi::response::CDecodeResponse; -#[cfg(feature = "dynamic-plugins")] -use upac_abi::ABI_VERSION; +use upac_types::request::DecodeRequest; +use upac_types::response::DecodeResponse; #[cfg(feature = "dynamic-plugins")] -use upac_abi::decoder::AbiVersionFn; - -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] -use upac_abi::decoder::{CDecodeRequest, CDecodeResponse, DecodeFn}; - -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] -use upac_abi::hook::CancelToken; - -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] -use upac_abi::types::{CBorrowed, CSlice}; +use libloading::Library; -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] use crate::plugin::decoder::error::DecoderError; -#[cfg(feature = "builtin-alpm")] -use upac_decoders_alpm::{decode as alpm_decode, manifest as alpm_manifest}; - -#[cfg(feature = "builtin-deb")] -use upac_decoders_deb::{decode as deb_decode, manifest as deb_manifest}; - -#[cfg(feature = "builtin-rpm")] -use upac_decoders_rpm::{decode as rpm_decode, manifest as rpm_manifest}; - -#[cfg(feature = "builtin-xbps")] -use upac_decoders_xbps::{decode as xbps_decode, manifest as xbps_manifest}; +#[cfg(all(feature = "dynamic-plugins", feature = "builtin-decoders"))] +compile_error!("dynamic-plugins and builtin-decoders are mutually exclusive"); +#[cfg(feature = "dynamic-plugins")] +pub mod dynamic_link; pub mod error; pub mod manifest; +#[cfg(feature = "builtin-decoders")] +pub mod static_link; pub mod triggers; pub mod unpack; -/// A package decoded by a decoder plugin. -/// -/// Plain owned data — available in every build configuration, including ones -/// without `dynamic-plugins`/`builtin-decoders`, so that callers and error -/// types elsewhere in the crate keep compiling. -pub struct DecodedPackage { - pub meta: PackageMeta, - pub dependencies: Vec, - pub declarative_triggers: Vec, -} - -#[cfg(feature = "dynamic-plugins")] -unsafe fn load_symbol(library: &Library, name: &str) -> Result { - unsafe { library.get::(name.as_bytes()) } - .map(|symbol| *symbol) - .map_err(|_| DecoderError::Symbol) -} - -/// A decoder plugin, either loaded from a shared object at runtime (`dynamic-plugins`) or -/// compiled directly into this binary (`builtin-decoders`). -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] -pub struct Decoder { +pub struct DecoderPlugin { decode: DecodeFn, #[cfg(feature = "dynamic-plugins")] _library: Option, } -#[cfg(feature = "builtin-decoders")] -impl Decoder { - fn from_static(decode: DecodeFn) -> Self { - Decoder { - decode, - - #[cfg(feature = "dynamic-plugins")] - _library: None, - } - } -} - -#[cfg(feature = "dynamic-plugins")] -impl Decoder { - pub fn load(library_name: &str) -> Result { - let library = unsafe { Library::new(library_name) }.map_err(|_| DecoderError::Load)?; - - let abi_version: AbiVersionFn = unsafe { load_symbol(&library, "abi_version")? }; - let decode: DecodeFn = unsafe { load_symbol(&library, "decode")? }; - - let got = unsafe { abi_version() }; - if got != ABI_VERSION { - return Err(DecoderError::AbiMismatch { - got, - expected: ABI_VERSION, - }); - } - - Ok(Decoder { - decode, - _library: Some(library), - }) - } -} - -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] -impl Decoder { +impl DecoderPlugin { pub fn decode( &self, package_path: &str, output_dir: &str, checksum: [u8; 32], cancel: &CancelToken, - ) -> Result { - let request = CDecodeRequest::new( - CSlice::from_borrowed(package_path.as_bytes()), - CSlice::from_borrowed(output_dir.as_bytes()), + ) -> Result { + let request: CDecodeRequest = DecodeRequest { + package_path: package_path.to_owned(), + output_dir: output_dir.to_owned(), checksum, - cancel as *const CancelToken as *mut CancelToken, - ); + cancel_token: cancel as *const CancelToken as *mut CancelToken, + } + .into(); let mut response = MaybeUninit::::uninit(); @@ -132,70 +58,6 @@ impl Decoder { let response = unsafe { response.assume_init() }; - unsafe { response.validate() }?; - - let meta = PackageMeta::try_from(&response.meta)?; - - let dependencies = unsafe { response.dependencies.as_slice() } - .iter() - .map(Dependency::try_from) - .collect::, _>>()?; - - let declarative_triggers = unsafe { response.declarative_triggers.as_slice() } - .iter() - .map(|trigger| unsafe { trigger.as_borrowed() }) - .map(|bytes| from_utf8(bytes).map(str::to_owned)) - .collect::, _>>() - .map_err(|_| DecoderError::InvalidResponse)?; - - Ok(DecodedPackage { - meta, - dependencies, - declarative_triggers, - }) + Ok(DecodeResponse::try_from(&response)?) } } - -/// The decoders compiled directly into this binary, keyed by format name with their claimed -/// extensions — mirrors `plugin::boot::static_plugins`, adapted for extension-based dispatch -/// (a decoder is selected by the package file's extension, not by a `probe()` call). No ABI -/// version check: compiled from the same source tree by the same compiler, so the decoder's own -/// `ABI_VERSION` matches by construction. -#[cfg(feature = "builtin-decoders")] -#[allow( - clippy::vec_init_then_push, - reason = "each push is independently cfg-gated, vec![] can't express that" -)] -pub(crate) fn static_decoders() -> Vec<(&'static str, &'static [&'static str], Decoder)> { - let mut decoders = Vec::new(); - - #[cfg(feature = "builtin-alpm")] - decoders.push(( - alpm_manifest::FORMAT, - alpm_manifest::EXTENSIONS, - Decoder::from_static(alpm_decode), - )); - - #[cfg(feature = "builtin-deb")] - decoders.push(( - deb_manifest::FORMAT, - deb_manifest::EXTENSIONS, - Decoder::from_static(deb_decode), - )); - - #[cfg(feature = "builtin-rpm")] - decoders.push(( - rpm_manifest::FORMAT, - rpm_manifest::EXTENSIONS, - Decoder::from_static(rpm_decode), - )); - - #[cfg(feature = "builtin-xbps")] - decoders.push(( - xbps_manifest::FORMAT, - xbps_manifest::EXTENSIONS, - Decoder::from_static(xbps_decode), - )); - - decoders -} diff --git a/lib/lib/src/plugin/decoder/static_link.rs b/lib/lib/src/plugin/decoder/static_link.rs new file mode 100644 index 00000000..6e3013c7 --- /dev/null +++ b/lib/lib/src/plugin/decoder/static_link.rs @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::DecodeFn; + +use super::DecoderPlugin; + +#[cfg(feature = "builtin-alpm")] +use upac_decoders_alpm::{decode as alpm_decode, manifest as alpm_manifest}; + +#[cfg(feature = "builtin-deb")] +use upac_decoders_deb::{decode as deb_decode, manifest as deb_manifest}; + +#[cfg(feature = "builtin-rpm")] +use upac_decoders_rpm::{decode as rpm_decode, manifest as rpm_manifest}; + +#[cfg(feature = "builtin-xbps")] +use upac_decoders_xbps::{decode as xbps_decode, manifest as xbps_manifest}; + +impl DecoderPlugin { + fn load_plugin_from_static(decode: DecodeFn) -> Self { + DecoderPlugin { + decode, + + #[cfg(feature = "dynamic-plugins")] + _library: None, + } + } +} + +#[allow( + clippy::vec_init_then_push, + reason = "each push is independently cfg-gated, vec![] can't express that" +)] +pub(super) fn static_decoders() -> Vec<(&'static str, &'static [&'static str], DecoderPlugin)> { + #[allow( + unused_mut, + reason = "mut is only needed when at least one builtin-* decoder feature is enabled" + )] + let mut decoders = Vec::new(); + + #[cfg(feature = "builtin-alpm")] + decoders.push(( + alpm_manifest::FORMAT, + alpm_manifest::EXTENSIONS, + DecoderPlugin::load_plugin_from_static(alpm_decode), + )); + + #[cfg(feature = "builtin-deb")] + decoders.push(( + deb_manifest::FORMAT, + deb_manifest::EXTENSIONS, + DecoderPlugin::load_plugin_from_static(deb_decode), + )); + + #[cfg(feature = "builtin-rpm")] + decoders.push(( + rpm_manifest::FORMAT, + rpm_manifest::EXTENSIONS, + DecoderPlugin::load_plugin_from_static(rpm_decode), + )); + + #[cfg(feature = "builtin-xbps")] + decoders.push(( + xbps_manifest::FORMAT, + xbps_manifest::EXTENSIONS, + DecoderPlugin::load_plugin_from_static(xbps_decode), + )); + + decoders +} diff --git a/lib/lib/src/plugin/decoder/unpack.rs b/lib/lib/src/plugin/decoder/unpack.rs index b69189d0..08ca5df7 100644 --- a/lib/lib/src/plugin/decoder/unpack.rs +++ b/lib/lib/src/plugin/decoder/unpack.rs @@ -5,7 +5,8 @@ use upac_abi::hook::CancelToken; -use upac_types::{DeclarativeTrigger, PackageTemp}; +use upac_types::decoder::DeclarativeTrigger; +use upac_types::package::PackageTemp; use crate::plugin::decoder::error::DecoderError; @@ -20,33 +21,50 @@ use std::path::Path; use sha2::{Digest, Sha256}; #[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] -use crate::plugin::decoder::Decoder; +use crate::plugin::decoder::DecoderPlugin; #[cfg(feature = "dynamic-plugins")] use std::collections::HashMap; #[cfg(feature = "dynamic-plugins")] -use crate::layout::decoders; +use crate::plugin::decoder::dynamic_link::load_decoder_dynamic; #[cfg(feature = "dynamic-plugins")] -use crate::plugin::decoder::manifest::{DecoderManifest, load_decoder_manifests}; +use crate::plugin::decoder::manifest::DecoderManifests; -#[cfg(all(not(feature = "dynamic-plugins"), feature = "builtin-decoders"))] -use crate::plugin::decoder::static_decoders; +#[cfg(feature = "builtin-decoders")] +use crate::plugin::decoder::static_link::static_decoders; + +#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] +fn checksum_of_file(path: &str) -> Result<[u8; 32], DecoderError> { + let mut file = File::open(path)?; + let mut hasher = Sha256::new(); + let mut buffer = [0u8; 65536]; + + loop { + let bytes_read = file.read(&mut buffer)?; + if bytes_read == 0 { + break; + } + hasher.update(&buffer[..bytes_read]); + } + + Ok(hasher.finalize().into()) +} pub struct PackageUnpacker { #[cfg(feature = "dynamic-plugins")] - manifests: HashMap, + manifests: DecoderManifests, #[cfg(feature = "dynamic-plugins")] - decoders: HashMap, + decoders: HashMap, - #[cfg(all(not(feature = "dynamic-plugins"), feature = "builtin-decoders"))] - decoders: Vec<(&'static str, &'static [&'static str], Decoder)>, + #[cfg(feature = "builtin-decoders")] + static_decoders: Vec<(&'static str, &'static [&'static str], DecoderPlugin)>, } #[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] impl PackageUnpacker { - pub(crate) fn unpack_one( + pub fn unpack_one( &mut self, package_path: &str, index: usize, tmp_path: &str, cancel: &CancelToken, ) -> Result<(PackageTemp, DeclarativeTrigger), DecoderError> { let format = self.format_for(package_path)?; @@ -81,11 +99,12 @@ impl PackageUnpacker { #[cfg(feature = "dynamic-plugins")] impl PackageUnpacker { pub fn new() -> Result { - let manifests = load_decoder_manifests(decoders::DECODERS_DIR, decoders::MANIFEST_EXTENSION)?; - Ok(Self { - manifests, + manifests: DecoderManifests::new()?, decoders: HashMap::new(), + + #[cfg(feature = "builtin-decoders")] + static_decoders: Vec::new(), }) } @@ -96,19 +115,16 @@ impl PackageUnpacker { .ok_or_else(|| DecoderError::UnknownFormat(package_path.to_owned()))?; self.manifests + .0 .values() .find(|manifest| manifest.extensions.iter().any(|candidate| candidate == extension)) .map(|manifest| manifest.format.clone()) .ok_or_else(|| DecoderError::UnknownFormat(package_path.to_owned())) } - fn decoder_for(&mut self, format: &str) -> Result<&Decoder, DecoderError> { + fn decoder_for(&mut self, format: &str) -> Result<&DecoderPlugin, DecoderError> { if !self.decoders.contains_key(format) { - let manifest = self - .manifests - .get(format) - .ok_or_else(|| DecoderError::UnknownFormat(format.to_owned()))?; - let decoder = Decoder::load(&manifest.library)?; + let decoder = load_decoder_dynamic(&self.manifests, format)?; self.decoders.insert(format.to_owned(), decoder); } @@ -116,15 +132,17 @@ impl PackageUnpacker { } } -/// Format resolution here never touches disk — the extension/format table comes straight from -/// each builtin decoder's own compiled-in manifest constants (`static_decoders`), not from -/// `/etc/upac.d/decoders/*.toml`. A build with `builtin-decoders` and no `dynamic-plugins` is -/// fully self-contained: no on-disk manifest is required for it to decode anything. #[cfg(all(not(feature = "dynamic-plugins"), feature = "builtin-decoders"))] impl PackageUnpacker { pub fn new() -> Result { Ok(Self { - decoders: static_decoders(), + #[cfg(feature = "dynamic-plugins")] + manifests: DecoderManifests::new()?, + + #[cfg(feature = "dynamic-plugins")] + decoders: HashMap::new(), + + static_decoders: static_decoders(), }) } @@ -134,15 +152,15 @@ impl PackageUnpacker { .and_then(|extension| extension.to_str()) .ok_or_else(|| DecoderError::UnknownFormat(package_path.to_owned()))?; - self.decoders + self.static_decoders .iter() .find(|(_, extensions, _)| extensions.contains(&extension)) .map(|(format, _, _)| (*format).to_owned()) .ok_or_else(|| DecoderError::UnknownFormat(package_path.to_owned())) } - fn decoder_for(&mut self, format: &str) -> Result<&Decoder, DecoderError> { - self.decoders + fn decoder_for(&mut self, format: &str) -> Result<&DecoderPlugin, DecoderError> { + self.static_decoders .iter() .find(|(name, _, _)| *name == format) .map(|(_, _, decoder)| decoder) @@ -157,26 +175,9 @@ impl PackageUnpacker { Err(DecoderError::NoDecoders) } - pub(crate) fn unpack_one( + pub fn unpack_one( &mut self, _package_path: &str, _index: usize, _tmp_path: &str, _cancel: &CancelToken, ) -> Result<(PackageTemp, DeclarativeTrigger), DecoderError> { Err(DecoderError::NoDecoders) } } - -#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] -fn checksum_of_file(path: &str) -> Result<[u8; 32], DecoderError> { - let mut file = File::open(path)?; - let mut hasher = Sha256::new(); - let mut buffer = [0u8; 65536]; - - loop { - let bytes_read = file.read(&mut buffer)?; - if bytes_read == 0 { - break; - } - hasher.update(&buffer[..bytes_read]); - } - - Ok(hasher.finalize().into()) -} diff --git a/lib/lib/src/scripts/file.rs b/lib/lib/src/scripts/file.rs index 50dd8ce8..5e7a2651 100644 --- a/lib/lib/src/scripts/file.rs +++ b/lib/lib/src/scripts/file.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use serde::Deserialize; -use upac_abi::hook::ProgressEventBuilder; +use upac_types::hook::ProgressEventBuilder; use crate::errors::CommonError; use crate::orchestrator::stage::{ConcurrentStage, RollbackGuard, StageResult}; diff --git a/lib/lib/src/scripts/load.rs b/lib/lib/src/scripts/load.rs deleted file mode 100644 index 98943b3b..00000000 --- a/lib/lib/src/scripts/load.rs +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs; -use std::str::from_utf8; - -use upac_pki::signature::{HookSignature, RootCertificate}; - -use crate::scripts::error::HookError; -use crate::scripts::file::HookFile; - -pub fn load_hooks( - hooks_dir: &str, root_cert_path: &str, hook_extension: &str, signature_extension: &str, -) -> Result, HookError> { - let root_bytes = fs::read(root_cert_path)?; - let root_certificate = RootCertificate::from_bytes(&root_bytes)?; - - let mut hooks = Vec::new(); - - for entry in fs::read_dir(hooks_dir)? { - let path = entry?.path(); - - if path.extension().and_then(|extension| extension.to_str()) != Some(hook_extension) { - continue; - } - - let mut signature_path = path.clone().into_os_string(); - signature_path.push("."); - signature_path.push(signature_extension); - - let hook_bytes = fs::read(&path)?; - let signature_bytes = fs::read(&signature_path)?; - - let signature = HookSignature::from_bytes(&signature_bytes)?; - signature.verify(&hook_bytes, &root_certificate)?; - - let hook_text = from_utf8(&hook_bytes)?; - let hook_file = HookFile::parse(hook_text)?; - - hooks.push(hook_file); - } - - Ok(hooks) -} diff --git a/lib/lib/src/scripts/mod.rs b/lib/lib/src/scripts/mod.rs index 13468a37..e06364a1 100644 --- a/lib/lib/src/scripts/mod.rs +++ b/lib/lib/src/scripts/mod.rs @@ -4,24 +4,30 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::collections::{HashMap, HashSet}; +use std::fs::{read, read_dir}; +use std::str::from_utf8; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use upac_types::DeclarativeTrigger; +use upac_types::hook::ProgressEventBuilder; + +use upac_types::decoder::DeclarativeTrigger; + +use upac_pki::signature::{HookSignature, RootCertificate}; use crate::errors::CommonError; use crate::layout::hooks::{HOOK_EXTENSION, HOOKS_DIR, ROOT_CERT_PATH, SIGNATURE_EXTENSION}; +use crate::orchestrator::context::Context; use crate::orchestrator::stage::{ConcurrentStage, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, Orchestrator, ParallelOrchestrator}; +use crate::orchestrator::{Orchestrator, ParallelOrchestrator}; use crate::plugin::decoder::triggers::build_trigger_table; use crate::scripts::error::HookError; -use crate::scripts::load::load_hooks; +use crate::scripts::file::HookFile; use crate::scripts::pipeline::{PipelineTrigger, Timing}; use crate::scripts::primitive::Primitive; pub mod error; pub mod file; -pub mod load; pub mod pipeline; pub mod primitive; @@ -83,3 +89,37 @@ impl + Send + 'static> Stage for HookStage { Ok((progress, StageResult::Advance, Box::new(Vec::::new()))) } } + +pub fn load_hooks( + hooks_dir: &str, root_cert_path: &str, hook_extension: &str, signature_extension: &str, +) -> Result, HookError> { + let root_bytes = read(root_cert_path)?; + let root_certificate = RootCertificate::from_bytes(&root_bytes)?; + + let mut hooks = Vec::new(); + + for entry in read_dir(hooks_dir)? { + let path = entry?.path(); + + if path.extension().and_then(|extension| extension.to_str()) != Some(hook_extension) { + continue; + } + + let mut signature_path = path.clone().into_os_string(); + signature_path.push("."); + signature_path.push(signature_extension); + + let hook_bytes = read(&path)?; + let signature_bytes = read(&signature_path)?; + + let signature = HookSignature::from_bytes(&signature_bytes)?; + signature.verify(&hook_bytes, &root_certificate)?; + + let hook_text = from_utf8(&hook_bytes)?; + let hook_file = HookFile::parse(hook_text)?; + + hooks.push(hook_file); + } + + Ok(hooks) +} diff --git a/lib/lib/src/unmutated/diff/comparing.rs b/lib/lib/src/unmutated/diff/comparing.rs index aaa8fdf5..ff03cddd 100644 --- a/lib/lib/src/unmutated/diff/comparing.rs +++ b/lib/lib/src/unmutated/diff/comparing.rs @@ -5,17 +5,18 @@ use std::collections::HashMap; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_abi::{FileDiffKind, PackageDiffKind}; +use upac_types::entry::{DiffFileEntryCommon, DiffPackageEntry, DiffPrefixFileEntry, DiffUntrackedFileEntry}; +use upac_types::hook::ProgressEventBuilder; +use upac_types::package::{PackageMeta, Version}; + +use super::{DiffError, DiffSnapshot}; + use crate::database::attribution::FileAttribute; +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; -use crate::unmutated::diff::{DiffError, DiffSnapshot}; - -use upac_types::{ - DiffFileEntryCommon, DiffPackageEntry, DiffPrefixFileEntry, DiffUntrackedFileEntry, PackageMeta, Version, -}; type PackageIdentity = (String, String, Option); diff --git a/lib/lib/src/unmutated/diff/error.rs b/lib/lib/src/unmutated/diff/error.rs index b0af6592..e6dfb3c3 100644 --- a/lib/lib/src/unmutated/diff/error.rs +++ b/lib/lib/src/unmutated/diff/error.rs @@ -14,6 +14,10 @@ use crate::errors::{ }; use crate::lock::LockError; +#[cfg(test)] +#[path = "../../../tests/inline/unmutated_diff_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum DiffError { Common(CommonError), diff --git a/lib/lib/src/unmutated/diff/mod.rs b/lib/lib/src/unmutated/diff/mod.rs index 611a7d16..6aba3d83 100644 --- a/lib/lib/src/unmutated/diff/mod.rs +++ b/lib/lib/src/unmutated/diff/mod.rs @@ -5,22 +5,28 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CDiffRequest; use upac_abi::{DiffFileSource, FileDiffKind}; -pub use self::error::DiffError; +use upac_types::entry::{DiffPackageEntry, DiffUntrackedFileEntry}; +use upac_types::hook::Message; +use upac_types::package::PackageMeta; +use upac_types::response::DiffResponse; +use upac_types::states::DiffStateId; +use upac_types::traits::MessageHook; +use upac_types::{RequestedConfigDigestRange, RequestedPrefixDigestRange}; use self::comparing::ComparingStage; use self::preparing::PreparingStage; use crate::database::MemoryDatabase; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use upac_types::states::DiffStateId; -use upac_types::{ - DiffPackageEntry, DiffUntrackedFileEntry, PackageMeta, RequestedConfigDigestRange, RequestedPrefixDigestRange, -}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; + +pub use self::error::DiffError; mod comparing; mod error; @@ -29,7 +35,9 @@ mod preparing; struct DiffSnapshot { from_packages: Vec, to_packages: Vec, + changed_files: Vec<(String, FileDiffKind, DiffFileSource)>, + from_database: MemoryDatabase, to_database: MemoryDatabase, } @@ -52,7 +60,7 @@ impl<'a> TryFrom<&'a CDiffRequest> for DiffData<'a> { fn try_from(request: &'a CDiffRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(DiffData { from_prefix_digest: (&request.from_prefix_digest).try_into()?, @@ -68,21 +76,23 @@ impl<'a> TryFrom<&'a CDiffRequest> for DiffData<'a> { } } -pub fn run(data: DiffData) -> Result<(Vec, Vec), (DiffStateId, DiffError)> { +pub fn run(data: DiffData) -> Result { 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(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)]); - run_unmutated!( + let (diff_packages, unattached_files) = run_unmutated!( orchestrator, context, data.cancel_token, @@ -90,5 +100,10 @@ pub fn run(data: DiffData) -> Result<(Vec, Vec, Vec - ) + )?; + + Ok(DiffResponse { + diff_packages, + unattached_files, + }) } diff --git a/lib/lib/src/unmutated/diff/preparing.rs b/lib/lib/src/unmutated/diff/preparing.rs index faef783f..40ad92d8 100644 --- a/lib/lib/src/unmutated/diff/preparing.rs +++ b/lib/lib/src/unmutated/diff/preparing.rs @@ -4,7 +4,11 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::DiffFileSource; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{DiffError, DiffSnapshot}; use crate::composefs::diff::TreeDiff; use crate::composefs::file::FileHandle; @@ -14,9 +18,8 @@ use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::unmutated::diff::{DiffError, DiffSnapshot}; use upac_types::{RequestedConfigDigestRange, RequestedPrefixDigestRange}; diff --git a/lib/lib/src/unmutated/diff_config/comparing.rs b/lib/lib/src/unmutated/diff_config/comparing.rs index b6ccca0d..87839cf7 100644 --- a/lib/lib/src/unmutated/diff_config/comparing.rs +++ b/lib/lib/src/unmutated/diff_config/comparing.rs @@ -4,14 +4,17 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::FileDiffKind; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{DiffConfigError, DiffConfigSnapshot}; use crate::database::attribution::FileAttribute; +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; -use crate::unmutated::diff_config::{DiffConfigError, DiffConfigSnapshot}; -use upac_types::{DiffConfigFileEntry, DiffFileEntryCommon}; +use upac_types::entry::{DiffConfigFileEntry, DiffFileEntryCommon}; pub struct ComparingStage; diff --git a/lib/lib/src/unmutated/diff_config/error.rs b/lib/lib/src/unmutated/diff_config/error.rs index 639655bb..71b1b220 100644 --- a/lib/lib/src/unmutated/diff_config/error.rs +++ b/lib/lib/src/unmutated/diff_config/error.rs @@ -14,6 +14,10 @@ use crate::errors::{ }; use crate::lock::LockError; +#[cfg(test)] +#[path = "../../../tests/inline/unmutated_diff_config_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum DiffConfigError { Common(CommonError), diff --git a/lib/lib/src/unmutated/diff_config/mod.rs b/lib/lib/src/unmutated/diff_config/mod.rs index f956a7f3..89f68af6 100644 --- a/lib/lib/src/unmutated/diff_config/mod.rs +++ b/lib/lib/src/unmutated/diff_config/mod.rs @@ -6,19 +6,26 @@ use std::os::raw::c_void; use upac_abi::FileDiffKind; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CDiffConfigRequest; -pub use self::error::DiffConfigError; +use upac_types::RequestedConfigDigestRange; +use upac_types::entry::DiffConfigFileEntry; +use upac_types::hook::Message; +use upac_types::response::DiffConfigResponse; +use upac_types::states::DiffConfigStateId; +use upac_types::traits::MessageHook; use self::comparing::ComparingStage; use self::preparing::PreparingStage; use crate::database::MemoryDatabase; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use upac_types::states::DiffConfigStateId; -use upac_types::{DiffConfigFileEntry, RequestedConfigDigestRange}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; + +pub use self::error::DiffConfigError; mod comparing; mod error; @@ -46,7 +53,7 @@ impl<'a> TryFrom<&'a CDiffConfigRequest> for DiffConfigData<'a> { fn try_from(request: &'a CDiffConfigRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(DiffConfigData { from_config_digest: (&request.from_config_digest).try_into()?, @@ -60,7 +67,7 @@ impl<'a> TryFrom<&'a CDiffConfigRequest> for DiffConfigData<'a> { } } -pub fn run(data: DiffConfigData) -> Result<(Vec,), (DiffConfigStateId, DiffConfigError)> { +pub fn run(data: DiffConfigData) -> Result { let mut context = Context::new(); context.put(RequestedConfigDigestRange { from: data.from_config_digest.map(str::to_owned), @@ -70,12 +77,14 @@ pub fn run(data: DiffConfigData) -> Result<(Vec,), (DiffCon let orchestrator = SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]); - run_unmutated!( + let (files,) = run_unmutated!( orchestrator, context, data.cancel_token, DiffConfigStateId, DiffConfigError, Vec - ) + )?; + + Ok(DiffConfigResponse { files }) } diff --git a/lib/lib/src/unmutated/diff_config/preparing.rs b/lib/lib/src/unmutated/diff_config/preparing.rs index 04f20f14..79a4aa11 100644 --- a/lib/lib/src/unmutated/diff_config/preparing.rs +++ b/lib/lib/src/unmutated/diff_config/preparing.rs @@ -3,7 +3,11 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::{DiffConfigError, DiffConfigSnapshot}; use crate::composefs::diff::TreeDiff; use crate::composefs::file::FileHandle; @@ -11,9 +15,8 @@ use crate::database::record::DeployRecord; use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::unmutated::diff_config::{DiffConfigError, DiffConfigSnapshot}; use upac_types::RequestedConfigDigestRange; diff --git a/lib/lib/src/unmutated/diff_packages/comparing.rs b/lib/lib/src/unmutated/diff_packages/comparing.rs index aecf7544..a8ec8960 100644 --- a/lib/lib/src/unmutated/diff_packages/comparing.rs +++ b/lib/lib/src/unmutated/diff_packages/comparing.rs @@ -6,13 +6,16 @@ use std::collections::HashMap; use upac_abi::PackageDiffKind; -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; -use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; -use crate::unmutated::diff_packages::DiffPackagesError; +use upac_types::DiffPackagesSnapshot; +use upac_types::entry::DiffPackageEntry; +use upac_types::hook::ProgressEventBuilder; + +use super::DiffPackagesError; -use upac_types::{DiffPackageEntry, DiffPackagesSnapshot}; +use crate::orchestrator::context::{Context, ctx_take}; +use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; pub struct ComparingStage; diff --git a/lib/lib/src/unmutated/diff_packages/mod.rs b/lib/lib/src/unmutated/diff_packages/mod.rs index fc80c1f7..e302bec9 100644 --- a/lib/lib/src/unmutated/diff_packages/mod.rs +++ b/lib/lib/src/unmutated/diff_packages/mod.rs @@ -5,18 +5,25 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CDiffPackagesRequest; -pub use self::error::DiffPackagesError; +use upac_types::RequestedPrefixDigestRange; +use upac_types::entry::DiffPackageEntry; +use upac_types::hook::Message; +use upac_types::response::DiffPackagesResponse; +use upac_types::states::DiffPackagesStateId; +use upac_types::traits::MessageHook; use self::comparing::ComparingStage; use self::preparing::PreparingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use upac_types::states::DiffPackagesStateId; -use upac_types::{DiffPackageEntry, RequestedPrefixDigestRange}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; + +pub use self::error::DiffPackagesError; mod comparing; mod error; @@ -38,7 +45,7 @@ impl<'a> TryFrom<&'a CDiffPackagesRequest> for DiffPackagesData<'a> { fn try_from(request: &'a CDiffPackagesRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(DiffPackagesData { from_prefix_digest: (&request.from_prefix_digest).try_into()?, @@ -52,7 +59,7 @@ impl<'a> TryFrom<&'a CDiffPackagesRequest> for DiffPackagesData<'a> { } } -pub fn run(data: DiffPackagesData) -> Result<(Vec,), (DiffPackagesStateId, DiffPackagesError)> { +pub fn run(data: DiffPackagesData) -> Result { let mut context = Context::new(); context.put(RequestedPrefixDigestRange { from: data.from_prefix_digest.map(str::to_owned), @@ -62,12 +69,14 @@ pub fn run(data: DiffPackagesData) -> Result<(Vec,), (DiffPack let orchestrator = SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]); - run_unmutated!( + let (diff_packages,) = run_unmutated!( orchestrator, context, data.cancel_token, DiffPackagesStateId, DiffPackagesError, Vec - ) + )?; + + Ok(DiffPackagesResponse { diff_packages }) } diff --git a/lib/lib/src/unmutated/diff_packages/preparing.rs b/lib/lib/src/unmutated/diff_packages/preparing.rs index 04ffb02d..1b085f09 100644 --- a/lib/lib/src/unmutated/diff_packages/preparing.rs +++ b/lib/lib/src/unmutated/diff_packages/preparing.rs @@ -3,7 +3,12 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; +use upac_types::{DiffPackagesSnapshot, RequestedPrefixDigestRange}; + +use super::DiffPackagesError; use crate::composefs::file::FileHandle; use crate::database::meta::MetaStore; @@ -11,11 +16,8 @@ use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::unmutated::diff_packages::DiffPackagesError; - -use upac_types::{DiffPackagesSnapshot, RequestedPrefixDigestRange}; pub struct PreparingStage; diff --git a/lib/lib/src/unmutated/diff_prefix/comparing.rs b/lib/lib/src/unmutated/diff_prefix/comparing.rs index 06db41c3..184ebee8 100644 --- a/lib/lib/src/unmutated/diff_prefix/comparing.rs +++ b/lib/lib/src/unmutated/diff_prefix/comparing.rs @@ -3,15 +3,17 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; use upac_abi::{DiffFileSource, FileDiffKind}; +use upac_types::entry::{DiffFileEntryCommon, DiffPrefixFileEntry}; +use upac_types::hook::ProgressEventBuilder; + +use super::{DiffPrefixError, DiffPrefixSnapshot}; + use crate::database::attribution::FileAttribute; +use crate::orchestrator::context::{Context, ctx_take}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_take}; -use crate::unmutated::diff_prefix::{DiffPrefixError, DiffPrefixSnapshot}; - -use upac_types::{DiffFileEntryCommon, DiffPrefixFileEntry}; pub struct ComparingStage; diff --git a/lib/lib/src/unmutated/diff_prefix/mod.rs b/lib/lib/src/unmutated/diff_prefix/mod.rs index 9c97b14a..8dd5a44c 100644 --- a/lib/lib/src/unmutated/diff_prefix/mod.rs +++ b/lib/lib/src/unmutated/diff_prefix/mod.rs @@ -6,19 +6,26 @@ use std::os::raw::c_void; use upac_abi::FileDiffKind; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CDiffPrefixRequest; -pub use self::error::DiffPrefixError; +use upac_types::RequestedPrefixDigestRange; +use upac_types::entry::DiffPrefixFileEntry; +use upac_types::hook::Message; +use upac_types::response::DiffPrefixResponse; +use upac_types::states::DiffPrefixStateId; +use upac_types::traits::MessageHook; use self::comparing::ComparingStage; use self::preparing::PreparingStage; use crate::database::MemoryDatabase; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use upac_types::states::DiffPrefixStateId; -use upac_types::{DiffPrefixFileEntry, RequestedPrefixDigestRange}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; + +pub use self::error::DiffPrefixError; mod comparing; mod error; @@ -46,7 +53,7 @@ impl<'a> TryFrom<&'a CDiffPrefixRequest> for DiffPrefixData<'a> { fn try_from(request: &'a CDiffPrefixRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(DiffPrefixData { from_prefix_digest: (&request.from_prefix_digest).try_into()?, @@ -60,7 +67,7 @@ impl<'a> TryFrom<&'a CDiffPrefixRequest> for DiffPrefixData<'a> { } } -pub fn run(data: DiffPrefixData) -> Result<(Vec,), (DiffPrefixStateId, DiffPrefixError)> { +pub fn run(data: DiffPrefixData) -> Result { let mut context = Context::new(); context.put(RequestedPrefixDigestRange { from: data.from_prefix_digest.map(str::to_owned), @@ -70,12 +77,14 @@ pub fn run(data: DiffPrefixData) -> Result<(Vec,), (DiffPre let orchestrator = SequentialOrchestrator::new(vec![Box::new(PreparingStage), Box::new(ComparingStage)]); - run_unmutated!( + let (files,) = run_unmutated!( orchestrator, context, data.cancel_token, DiffPrefixStateId, DiffPrefixError, Vec - ) + )?; + + Ok(DiffPrefixResponse { files }) } diff --git a/lib/lib/src/unmutated/diff_prefix/preparing.rs b/lib/lib/src/unmutated/diff_prefix/preparing.rs index d307554b..e385cddb 100644 --- a/lib/lib/src/unmutated/diff_prefix/preparing.rs +++ b/lib/lib/src/unmutated/diff_prefix/preparing.rs @@ -3,7 +3,12 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::RequestedPrefixDigestRange; +use upac_types::hook::ProgressEventBuilder; + +use super::{DiffPrefixError, DiffPrefixSnapshot}; use crate::composefs::diff::TreeDiff; use crate::composefs::file::FileHandle; @@ -12,11 +17,8 @@ use crate::database::MemoryDatabase; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::unmutated::diff_prefix::{DiffPrefixError, DiffPrefixSnapshot}; - -use upac_types::RequestedPrefixDigestRange; pub struct PreparingStage; diff --git a/lib/lib/src/unmutated/list_config/fetching.rs b/lib/lib/src/unmutated/list_config/fetching.rs index daee05d8..156f1375 100644 --- a/lib/lib/src/unmutated/list_config/fetching.rs +++ b/lib/lib/src/unmutated/list_config/fetching.rs @@ -3,16 +3,19 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::RequestedPrefixDigest; +use upac_types::entry::ConfigCommitEntry; +use upac_types::hook::ProgressEventBuilder; + +use super::ListConfigError; use crate::database::record::DeployRecord; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; -use crate::unmutated::list_config::ListConfigError; - -use upac_types::{ConfigCommitEntry, RequestedPrefixDigest}; pub struct FetchingStage; diff --git a/lib/lib/src/unmutated/list_config/mod.rs b/lib/lib/src/unmutated/list_config/mod.rs index 84a5dd78..754736ab 100644 --- a/lib/lib/src/unmutated/list_config/mod.rs +++ b/lib/lib/src/unmutated/list_config/mod.rs @@ -5,17 +5,24 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CListConfigRequest; -pub use self::error::ListConfigError; +use upac_types::RequestedPrefixDigest; +use upac_types::entry::ConfigCommitEntry; +use upac_types::hook::Message; +use upac_types::response::ListConfigResponse; +use upac_types::states::ListConfigStateId; +use upac_types::traits::MessageHook; use self::fetching::FetchingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use upac_types::states::ListConfigStateId; -use upac_types::{ConfigCommitEntry, RequestedPrefixDigest}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; + +pub use self::error::ListConfigError; mod error; mod fetching; @@ -35,7 +42,7 @@ impl<'a> TryFrom<&'a CListConfigRequest> for ListConfigData<'a> { fn try_from(request: &'a CListConfigRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(ListConfigData { prefix_digest: (&request.prefix_digest).try_into()?, @@ -48,19 +55,21 @@ impl<'a> TryFrom<&'a CListConfigRequest> for ListConfigData<'a> { } } -pub fn run(data: ListConfigData) -> Result<(Vec,), (ListConfigStateId, ListConfigError)> { +pub fn run(data: ListConfigData) -> Result { 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)]); - run_unmutated!( + let (commits,) = run_unmutated!( orchestrator, context, data.cancel_token, ListConfigStateId, ListConfigError, Vec - ) + )?; + + Ok(ListConfigResponse { commits }) } diff --git a/lib/lib/src/unmutated/list_history/fetching.rs b/lib/lib/src/unmutated/list_history/fetching.rs index e206bf33..1fe8391a 100644 --- a/lib/lib/src/unmutated/list_history/fetching.rs +++ b/lib/lib/src/unmutated/list_history/fetching.rs @@ -3,15 +3,17 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::entry::{ConfigCommitEntry, HistoryEntry}; +use upac_types::hook::ProgressEventBuilder; + +use super::ListHistoryError; use crate::database::record::DeployRecord; use crate::deploy::{Deploy, DeployMode}; -use crate::orchestrator::Context; +use crate::orchestrator::context::Context; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::unmutated::list_history::ListHistoryError; - -use upac_types::{ConfigCommitEntry, HistoryEntry}; pub struct FetchingStage; diff --git a/lib/lib/src/unmutated/list_history/mod.rs b/lib/lib/src/unmutated/list_history/mod.rs index f9bac4b7..264f320f 100644 --- a/lib/lib/src/unmutated/list_history/mod.rs +++ b/lib/lib/src/unmutated/list_history/mod.rs @@ -5,17 +5,23 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CListHistoryRequest; -pub use self::error::ListHistoryError; +use upac_types::entry::HistoryEntry; +use upac_types::hook::Message; +use upac_types::response::ListHistoryResponse; +use upac_types::states::ListHistoryStateId; +use upac_types::traits::MessageHook; use self::fetching::FetchingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use upac_types::HistoryEntry; -use upac_types::states::ListHistoryStateId; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; + +pub use self::error::ListHistoryError; mod error; mod fetching; @@ -33,7 +39,7 @@ impl<'a> TryFrom<&'a CListHistoryRequest> for ListHistoryData<'a> { fn try_from(request: &'a CListHistoryRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(ListHistoryData { hook_message: request.base.on_hook, @@ -44,18 +50,20 @@ impl<'a> TryFrom<&'a CListHistoryRequest> for ListHistoryData<'a> { } } -pub fn run(data: ListHistoryData) -> Result<(Vec,), (ListHistoryStateId, ListHistoryError)> { +pub fn run(data: ListHistoryData) -> Result { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = SequentialOrchestrator::new(vec![Box::new(FetchingStage)]); - run_unmutated!( + let (history,) = run_unmutated!( orchestrator, context, data.cancel_token, ListHistoryStateId, ListHistoryError, Vec - ) + )?; + + Ok(ListHistoryResponse { history }) } diff --git a/lib/lib/src/unmutated/list_packages/fetching.rs b/lib/lib/src/unmutated/list_packages/fetching.rs index bae1dc0e..41b371a2 100644 --- a/lib/lib/src/unmutated/list_packages/fetching.rs +++ b/lib/lib/src/unmutated/list_packages/fetching.rs @@ -3,7 +3,11 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::ListPackagesError; use crate::composefs::file::FileHandle; use crate::database::meta::MetaStore; @@ -11,9 +15,8 @@ use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; -use crate::orchestrator::Context; +use crate::orchestrator::context::Context; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::unmutated::list_packages::ListPackagesError; pub struct FetchingStage; diff --git a/lib/lib/src/unmutated/list_packages/mod.rs b/lib/lib/src/unmutated/list_packages/mod.rs index bd33bbcd..2201ed22 100644 --- a/lib/lib/src/unmutated/list_packages/mod.rs +++ b/lib/lib/src/unmutated/list_packages/mod.rs @@ -5,17 +5,23 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CListPackagesRequest; -pub use self::error::ListPackagesError; +use upac_types::hook::Message; +use upac_types::package::PackageMeta; +use upac_types::response::ListPackagesResponse; +use upac_types::states::ListPackagesStateId; +use upac_types::traits::MessageHook; use self::fetching::FetchingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use upac_types::PackageMeta; -use upac_types::states::ListPackagesStateId; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; + +pub use self::error::ListPackagesError; mod error; mod fetching; @@ -33,7 +39,7 @@ impl<'a> TryFrom<&'a CListPackagesRequest> for ListPackagesData<'a> { fn try_from(request: &'a CListPackagesRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(ListPackagesData { hook_message: request.base.on_hook, @@ -44,18 +50,20 @@ impl<'a> TryFrom<&'a CListPackagesRequest> for ListPackagesData<'a> { } } -pub fn run(data: ListPackagesData) -> Result<(Vec,), (ListPackagesStateId, ListPackagesError)> { +pub fn run(data: ListPackagesData) -> Result { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = SequentialOrchestrator::new(vec![Box::new(FetchingStage)]); - run_unmutated!( + let (metas,) = run_unmutated!( orchestrator, context, data.cancel_token, ListPackagesStateId, ListPackagesError, Vec - ) + )?; + + Ok(ListPackagesResponse { metas }) } diff --git a/lib/lib/src/unmutated/list_prefix/fetching.rs b/lib/lib/src/unmutated/list_prefix/fetching.rs index 9cefb2ee..a18a943e 100644 --- a/lib/lib/src/unmutated/list_prefix/fetching.rs +++ b/lib/lib/src/unmutated/list_prefix/fetching.rs @@ -3,15 +3,17 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::entry::PrefixEntry; +use upac_types::hook::ProgressEventBuilder; + +use super::ListPrefixError; use crate::database::record::DeployRecord; use crate::deploy::{Deploy, DeployMode}; -use crate::orchestrator::Context; +use crate::orchestrator::context::Context; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::unmutated::list_prefix::ListPrefixError; - -use upac_types::PrefixEntry; pub struct FetchingStage; diff --git a/lib/lib/src/unmutated/list_prefix/mod.rs b/lib/lib/src/unmutated/list_prefix/mod.rs index a7844407..aae03e52 100644 --- a/lib/lib/src/unmutated/list_prefix/mod.rs +++ b/lib/lib/src/unmutated/list_prefix/mod.rs @@ -5,17 +5,23 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CListPrefixRequest; -pub use self::error::ListPrefixError; +use upac_types::entry::PrefixEntry; +use upac_types::hook::Message; +use upac_types::response::ListPrefixResponse; +use upac_types::states::ListPrefixStateId; +use upac_types::traits::MessageHook; use self::fetching::FetchingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; -use upac_types::PrefixEntry; -use upac_types::states::ListPrefixStateId; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; + +pub use self::error::ListPrefixError; mod error; mod fetching; @@ -33,7 +39,7 @@ impl<'a> TryFrom<&'a CListPrefixRequest> for ListPrefixData<'a> { fn try_from(request: &'a CListPrefixRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(ListPrefixData { hook_message: request.base.on_hook, @@ -44,18 +50,20 @@ impl<'a> TryFrom<&'a CListPrefixRequest> for ListPrefixData<'a> { } } -pub fn run(data: ListPrefixData) -> Result<(Vec,), (ListPrefixStateId, ListPrefixError)> { +pub fn run(data: ListPrefixData) -> Result { let mut context = Context::new(); context.put(Box::new(Message::new(data.hook_message, data.hook_message_context)) as Box); let orchestrator = SequentialOrchestrator::new(vec![Box::new(FetchingStage)]); - run_unmutated!( + let (prefixes,) = run_unmutated!( orchestrator, context, data.cancel_token, ListPrefixStateId, ListPrefixError, Vec - ) + )?; + + Ok(ListPrefixResponse { prefixes }) } diff --git a/lib/lib/src/unmutated/search_files/error.rs b/lib/lib/src/unmutated/search_files/error.rs index 327879ae..6fefb3c7 100644 --- a/lib/lib/src/unmutated/search_files/error.rs +++ b/lib/lib/src/unmutated/search_files/error.rs @@ -14,6 +14,10 @@ use crate::errors::{ }; use crate::lock::LockError; +#[cfg(test)] +#[path = "../../../tests/inline/unmutated_search_files_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SearchFilesError { Common(CommonError), diff --git a/lib/lib/src/unmutated/search_files/mod.rs b/lib/lib/src/unmutated/search_files/mod.rs index 9fbd871f..2791043d 100644 --- a/lib/lib/src/unmutated/search_files/mod.rs +++ b/lib/lib/src/unmutated/search_files/mod.rs @@ -5,18 +5,24 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CSearchFilesRequest; -pub use self::error::SearchFilesError; +use upac_types::entry::SearchFileEntry; +use upac_types::hook::Message; +use upac_types::response::SearchFilesResponse; +use upac_types::states::SearchFilesStateId; +use upac_types::traits::MessageHook; use self::searching::SearchingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; use crate::search::Search; -use upac_types::SearchFileEntry; -use upac_types::states::SearchFilesStateId; + +pub use self::error::SearchFilesError; mod error; mod searching; @@ -37,7 +43,7 @@ impl<'a> TryFrom<&'a CSearchFilesRequest> for SearchFilesData<'a> { fn try_from(request: &'a CSearchFilesRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(SearchFilesData { search: (&request.search).try_into()?, @@ -51,7 +57,7 @@ impl<'a> TryFrom<&'a CSearchFilesRequest> for SearchFilesData<'a> { } } -pub fn run(data: SearchFilesData) -> Result<(Vec,), (SearchFilesStateId, SearchFilesError)> { +pub fn run(data: SearchFilesData) -> Result { let search = Search::new(data.search, data.is_regex) .map_err(|error| (SearchFilesStateId::Setup, SearchFilesError::from(error)))?; @@ -61,12 +67,14 @@ pub fn run(data: SearchFilesData) -> Result<(Vec,), (SearchFile let orchestrator = SequentialOrchestrator::new(vec![Box::new(SearchingStage)]); - run_unmutated!( + let (files,) = run_unmutated!( orchestrator, context, data.cancel_token, SearchFilesStateId, SearchFilesError, Vec - ) + )?; + + Ok(SearchFilesResponse { files }) } diff --git a/lib/lib/src/unmutated/search_files/searching.rs b/lib/lib/src/unmutated/search_files/searching.rs index 94a24ca9..5ea5ff4b 100644 --- a/lib/lib/src/unmutated/search_files/searching.rs +++ b/lib/lib/src/unmutated/search_files/searching.rs @@ -3,7 +3,12 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::entry::SearchFileEntry; +use upac_types::hook::ProgressEventBuilder; + +use super::SearchFilesError; use crate::composefs::file::FileHandle; use crate::database::files::FileStore; @@ -12,12 +17,9 @@ use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; use crate::search::Search; -use crate::unmutated::search_files::SearchFilesError; - -use upac_types::SearchFileEntry; pub struct SearchingStage; diff --git a/lib/lib/src/unmutated/search_in_meta/error.rs b/lib/lib/src/unmutated/search_in_meta/error.rs index cd588325..a928244e 100644 --- a/lib/lib/src/unmutated/search_in_meta/error.rs +++ b/lib/lib/src/unmutated/search_in_meta/error.rs @@ -14,6 +14,10 @@ use crate::errors::{ }; use crate::lock::LockError; +#[cfg(test)] +#[path = "../../../tests/inline/unmutated_search_in_meta_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SearchInMetaError { Common(CommonError), diff --git a/lib/lib/src/unmutated/search_in_meta/mod.rs b/lib/lib/src/unmutated/search_in_meta/mod.rs index acb5bfc9..cb622dfc 100644 --- a/lib/lib/src/unmutated/search_in_meta/mod.rs +++ b/lib/lib/src/unmutated/search_in_meta/mod.rs @@ -5,18 +5,24 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CSearchInMetaRequest; -pub use self::error::SearchInMetaError; +use upac_types::hook::Message; +use upac_types::package::{PackageEntry, PackageMeta}; +use upac_types::response::SearchInMetaResponse; +use upac_types::states::SearchInMetaStateId; +use upac_types::traits::MessageHook; use self::searching::SearchingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; use crate::search::Search; -use upac_types::states::SearchInMetaStateId; -use upac_types::{PackageEntry, PackageMeta}; + +pub use self::error::SearchInMetaError; mod error; mod searching; @@ -40,7 +46,7 @@ impl<'a> TryFrom<&'a CSearchInMetaRequest> for SearchInMetaData<'a> { fn try_from(request: &'a CSearchInMetaRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(SearchInMetaData { name: (&request.package.name).try_into()?, @@ -57,7 +63,7 @@ impl<'a> TryFrom<&'a CSearchInMetaRequest> for SearchInMetaData<'a> { } } -pub fn run(data: SearchInMetaData) -> Result<(Vec,), (SearchInMetaStateId, SearchInMetaError)> { +pub fn run(data: SearchInMetaData) -> Result { let search = Search::new(data.search, data.is_regex) .map_err(|error| (SearchInMetaStateId::Setup, SearchInMetaError::from(error)))?; @@ -72,12 +78,14 @@ pub fn run(data: SearchInMetaData) -> Result<(Vec,), (SearchInMetaS let orchestrator = SequentialOrchestrator::new(vec![Box::new(SearchingStage)]); - run_unmutated!( + let (metas,) = run_unmutated!( orchestrator, context, data.cancel_token, SearchInMetaStateId, SearchInMetaError, Vec - ) + )?; + + Ok(SearchInMetaResponse { metas }) } diff --git a/lib/lib/src/unmutated/search_in_meta/searching.rs b/lib/lib/src/unmutated/search_in_meta/searching.rs index 03ece769..7d384981 100644 --- a/lib/lib/src/unmutated/search_in_meta/searching.rs +++ b/lib/lib/src/unmutated/search_in_meta/searching.rs @@ -3,7 +3,12 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; +use upac_types::package::PackageEntry; + +use super::SearchInMetaError; use crate::composefs::file::FileHandle; use crate::database::error::DatabaseError; @@ -12,12 +17,9 @@ use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; use crate::search::Search; -use crate::unmutated::search_in_meta::SearchInMetaError; - -use upac_types::PackageEntry; pub struct SearchingStage; diff --git a/lib/lib/src/unmutated/search_in_package_files/error.rs b/lib/lib/src/unmutated/search_in_package_files/error.rs index 9a5fe780..58dfe107 100644 --- a/lib/lib/src/unmutated/search_in_package_files/error.rs +++ b/lib/lib/src/unmutated/search_in_package_files/error.rs @@ -14,6 +14,10 @@ use crate::errors::{ }; use crate::lock::LockError; +#[cfg(test)] +#[path = "../../../tests/inline/unmutated_search_in_package_files_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SearchInPackageFilesError { Common(CommonError), diff --git a/lib/lib/src/unmutated/search_in_package_files/mod.rs b/lib/lib/src/unmutated/search_in_package_files/mod.rs index 4869b7b4..aedecf1c 100644 --- a/lib/lib/src/unmutated/search_in_package_files/mod.rs +++ b/lib/lib/src/unmutated/search_in_package_files/mod.rs @@ -5,18 +5,25 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CSearchInPackageFilesRequest; -pub use self::error::SearchInPackageFilesError; +use upac_types::entry::SearchFileEntry; +use upac_types::hook::Message; +use upac_types::package::PackageEntry; +use upac_types::response::SearchInPackageFilesResponse; +use upac_types::states::SearchInPackageFilesStateId; +use upac_types::traits::MessageHook; use self::searching::SearchingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; use crate::search::Search; -use upac_types::states::SearchInPackageFilesStateId; -use upac_types::{PackageEntry, SearchFileEntry}; + +pub use self::error::SearchInPackageFilesError; mod error; mod searching; @@ -40,7 +47,7 @@ impl<'a> TryFrom<&'a CSearchInPackageFilesRequest> for SearchInPackageFilesData< fn try_from(request: &'a CSearchInPackageFilesRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(SearchInPackageFilesData { name: (&request.package.name).try_into()?, @@ -59,7 +66,7 @@ impl<'a> TryFrom<&'a CSearchInPackageFilesRequest> for SearchInPackageFilesData< pub fn run( data: SearchInPackageFilesData, -) -> Result<(Vec,), (SearchInPackageFilesStateId, SearchInPackageFilesError)> { +) -> Result { let search = Search::new(data.search, data.is_regex).map_err(|error| { ( SearchInPackageFilesStateId::Setup, @@ -78,12 +85,14 @@ pub fn run( let orchestrator = SequentialOrchestrator::new(vec![Box::new(SearchingStage)]); - run_unmutated!( + let (files,) = run_unmutated!( orchestrator, context, data.cancel_token, SearchInPackageFilesStateId, SearchInPackageFilesError, Vec - ) + )?; + + Ok(SearchInPackageFilesResponse { files }) } diff --git a/lib/lib/src/unmutated/search_in_package_files/searching.rs b/lib/lib/src/unmutated/search_in_package_files/searching.rs index 971c716b..a165c4fc 100644 --- a/lib/lib/src/unmutated/search_in_package_files/searching.rs +++ b/lib/lib/src/unmutated/search_in_package_files/searching.rs @@ -3,7 +3,13 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::entry::SearchFileEntry; +use upac_types::hook::ProgressEventBuilder; +use upac_types::package::PackageEntry; + +use super::SearchInPackageFilesError; use crate::composefs::file::FileHandle; use crate::database::error::DatabaseError; @@ -13,12 +19,9 @@ use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; use crate::search::Search; -use crate::unmutated::search_in_package_files::SearchInPackageFilesError; - -use upac_types::{PackageEntry, SearchFileEntry}; pub struct SearchingStage; diff --git a/lib/lib/src/unmutated/search_meta/error.rs b/lib/lib/src/unmutated/search_meta/error.rs index b0ae8d82..931e207c 100644 --- a/lib/lib/src/unmutated/search_meta/error.rs +++ b/lib/lib/src/unmutated/search_meta/error.rs @@ -14,6 +14,10 @@ use crate::errors::{ }; use crate::lock::LockError; +#[cfg(test)] +#[path = "../../../tests/inline/unmutated_search_meta_error.rs"] +mod tests; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SearchMetaError { Common(CommonError), diff --git a/lib/lib/src/unmutated/search_meta/mod.rs b/lib/lib/src/unmutated/search_meta/mod.rs index 1c14122c..d32c7478 100644 --- a/lib/lib/src/unmutated/search_meta/mod.rs +++ b/lib/lib/src/unmutated/search_meta/mod.rs @@ -5,18 +5,24 @@ use std::os::raw::c_void; +use upac_abi::HookMessageFn; use upac_abi::error::ErrorKind; -use upac_abi::hook::{CancelToken, HookMessageFn, Message, MessageHook}; +use upac_abi::hook::CancelToken; use upac_abi::request::CSearchMetaRequest; -pub use self::error::SearchMetaError; +use upac_types::hook::Message; +use upac_types::package::PackageMeta; +use upac_types::response::SearchMetaResponse; +use upac_types::states::SearchMetaStateId; +use upac_types::traits::MessageHook; use self::searching::SearchingStage; -use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_unmutated}; +use crate::orchestrator::context::Context; +use crate::orchestrator::{Orchestrator, SequentialOrchestrator, run_unmutated}; use crate::search::Search; -use upac_types::PackageMeta; -use upac_types::states::SearchMetaStateId; + +pub use self::error::SearchMetaError; mod error; mod searching; @@ -37,7 +43,7 @@ impl<'a> TryFrom<&'a CSearchMetaRequest> for SearchMetaData<'a> { fn try_from(request: &'a CSearchMetaRequest) -> Result { unsafe { request.validate()? }; - let cancel_token = unsafe { request.base.cancel_token.as_ref() }.ok_or(ErrorKind::InvalidEntry)?; + let cancel_token = unsafe { &*request.base.cancel_token }; Ok(SearchMetaData { search: (&request.search).try_into()?, @@ -51,7 +57,7 @@ impl<'a> TryFrom<&'a CSearchMetaRequest> for SearchMetaData<'a> { } } -pub fn run(data: SearchMetaData) -> Result<(Vec,), (SearchMetaStateId, SearchMetaError)> { +pub fn run(data: SearchMetaData) -> Result { let search = Search::new(data.search, data.is_regex) .map_err(|error| (SearchMetaStateId::Setup, SearchMetaError::from(error)))?; @@ -61,12 +67,14 @@ pub fn run(data: SearchMetaData) -> Result<(Vec,), (SearchMetaState let orchestrator = SequentialOrchestrator::new(vec![Box::new(SearchingStage)]); - run_unmutated!( + let (metas,) = run_unmutated!( orchestrator, context, data.cancel_token, SearchMetaStateId, SearchMetaError, Vec - ) + )?; + + Ok(SearchMetaResponse { metas }) } diff --git a/lib/lib/src/unmutated/search_meta/searching.rs b/lib/lib/src/unmutated/search_meta/searching.rs index 8952ff4e..b63442c5 100644 --- a/lib/lib/src/unmutated/search_meta/searching.rs +++ b/lib/lib/src/unmutated/search_meta/searching.rs @@ -3,7 +3,11 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_abi::hook::CancelToken; + +use upac_types::hook::ProgressEventBuilder; + +use super::SearchMetaError; use crate::composefs::file::FileHandle; use crate::database::meta::MetaStore; @@ -11,10 +15,9 @@ use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::digest::current_prefix_digest; use crate::deploy::{Deploy, DeployMode}; use crate::layout::database::DATABASE_PATH; +use crate::orchestrator::context::{Context, ctx_get}; use crate::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use crate::orchestrator::{Context, ctx_get}; use crate::search::Search; -use crate::unmutated::search_meta::SearchMetaError; pub struct SearchingStage; diff --git a/lib/lib/tests/boot.rs b/lib/lib/tests/boot.rs new file mode 100644 index 00000000..77143339 --- /dev/null +++ b/lib/lib/tests/boot.rs @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::fs::{File, write}; + +use composefs::fsverity::FsVerityHashValue; +use composefs::generic_tree::Stat; +use composefs::repository::{ImportContext, Repository, RepositoryConfig}; +use composefs::tree::FileSystem; +use nix::fcntl::AT_FDCWD; +use tempfile::{Builder, TempDir}; + +use upac::boot::error::BootError; +use upac::boot::write_boot_entry; +use upac::composefs::file::FileHandle; +use upac::composefs::repository::ObjectID; + +fn scratch_dir(name: &str) -> TempDir { + Builder::new().prefix(name).tempdir().unwrap() +} + +fn empty_tree() -> FileSystem { + FileSystem::new(Stat::uninitialized()) +} + +fn open_repository(name: &str) -> (TempDir, Repository) { + let dir = scratch_dir(name); + let (repository, _created) = + Repository::init_path(AT_FDCWD, dir.path(), RepositoryConfig::default().set_insecure()).unwrap(); + + (dir, repository) +} + +fn source_file(dir_name: &str, content: &[u8]) -> File { + let dir = scratch_dir(dir_name); + let path = dir.path().join("source"); + write(&path, content).unwrap(); + + File::open(&path).unwrap() +} + +fn ensure_modules_dir(tree: &mut FileSystem) { + if FileHandle::from_tree(tree, "lib/modules").is_ok() { + return; + } + FileHandle::new("lib") + .insert_in_tree(tree, Stat::uninitialized()) + .unwrap(); + FileHandle::new("lib/modules") + .insert_in_tree(tree, Stat::uninitialized()) + .unwrap(); +} + +fn insert_kernel( + repository: &Repository, tree: &mut FileSystem, ctx: &mut ImportContext, kver: &str, +) { + ensure_modules_dir(tree); + FileHandle::new(format!("lib/modules/{kver}")) + .insert_in_tree(tree, Stat::uninitialized()) + .unwrap(); + FileHandle::new(format!("lib/modules/{kver}/vmlinuz")) + .insert_file( + repository, + tree, + &source_file(&format!("kernel-{kver}"), b"kernel"), + Stat::uninitialized(), + ctx, + ) + .unwrap(); +} + +#[test] +fn write_boot_entry_fails_when_the_tree_has_no_boot_resource() { + let (_scratch, repository) = open_repository("boot-none"); + let tree = empty_tree(); + let esp = scratch_dir("boot-none-esp"); + + let result = write_boot_entry(&repository, &tree, ObjectID::EMPTY, esp.path(), "deadbeef"); + + assert_eq!(result.unwrap_err(), BootError::NoBootResource); +} + +#[test] +fn write_boot_entry_fails_when_the_tree_has_more_than_one_boot_resource() { + let (_scratch, repository) = open_repository("boot-ambiguous"); + let mut ctx = ImportContext::default(); + let mut tree = empty_tree(); + insert_kernel(&repository, &mut tree, &mut ctx, "6.6.0"); + insert_kernel(&repository, &mut tree, &mut ctx, "6.7.0"); + let esp = scratch_dir("boot-ambiguous-esp"); + + let result = write_boot_entry(&repository, &tree, ObjectID::EMPTY, esp.path(), "deadbeef"); + + assert_eq!(result.unwrap_err(), BootError::AmbiguousBootResource); +} diff --git a/lib/lib/tests/boot_error.rs b/lib/lib/tests/boot_error.rs new file mode 100644 index 00000000..b90f8b13 --- /dev/null +++ b/lib/lib/tests/boot_error.rs @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use anyhow::anyhow; + +use upac::boot::error::BootError; + +use upac_abi::error::ErrorKind; + +#[test] +fn anyhow_error_maps_to_unexpected() { + assert_eq!(BootError::from(anyhow!("boom")), BootError::Unexpected); +} + +#[test] +fn every_variant_maps_to_the_documented_error_kind() { + let cases = [ + (BootError::NoBootResource, ErrorKind::NotFound), + (BootError::AmbiguousBootResource, ErrorKind::InvalidEntry), + (BootError::UnsupportedBootResource, ErrorKind::InvalidEntry), + (BootError::Unexpected, ErrorKind::Unexpected), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/composefs_diff.rs b/lib/lib/tests/composefs_diff.rs new file mode 100644 index 00000000..ffbdd29a --- /dev/null +++ b/lib/lib/tests/composefs_diff.rs @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::fs::{File, write}; + +use composefs::generic_tree::Stat; +use composefs::repository::{ImportContext, Repository, RepositoryConfig}; +use composefs::tree::FileSystem; +use nix::fcntl::AT_FDCWD; +use tempfile::{Builder, TempDir}; + +use upac::composefs::diff::TreeDiff; +use upac::composefs::file::FileHandle; +use upac::composefs::repository::ObjectID; + +use upac_abi::FileDiffKind; + +fn scratch_dir(name: &str) -> TempDir { + Builder::new().prefix(name).tempdir().unwrap() +} + +fn empty_tree() -> FileSystem { + FileSystem::new(Stat::uninitialized()) +} + +fn open_repository(name: &str) -> (TempDir, Repository) { + let dir = scratch_dir(name); + let (repository, _created) = + Repository::init_path(AT_FDCWD, dir.path(), RepositoryConfig::default().set_insecure()).unwrap(); + + (dir, repository) +} + +fn source_file(dir_name: &str, content: &[u8]) -> File { + let dir = scratch_dir(dir_name); + let path = dir.path().join("source"); + write(&path, content).unwrap(); + + File::open(&path).unwrap() +} + +fn insert( + repository: &Repository, tree: &mut FileSystem, ctx: &mut ImportContext, path: &str, + content: &[u8], +) { + FileHandle::new(path) + .insert_file( + repository, + tree, + &source_file(&path.replace('/', "-"), content), + Stat::uninitialized(), + ctx, + ) + .unwrap(); +} + +#[test] +fn run_reports_no_changes_for_identical_trees() { + let (_scratch, repository) = open_repository("diff-unchanged"); + let mut ctx = ImportContext::default(); + let mut from = empty_tree(); + let mut to = empty_tree(); + insert(&repository, &mut from, &mut ctx, "file.txt", b"same"); + insert(&repository, &mut to, &mut ctx, "file.txt", b"same"); + + let changes = TreeDiff::run(&from, &to); + + assert!(changes.is_empty()); +} + +#[test] +fn run_reports_added_for_a_file_only_in_to() { + let (_scratch, repository) = open_repository("diff-added"); + let mut ctx = ImportContext::default(); + let from = empty_tree(); + let mut to = empty_tree(); + insert(&repository, &mut to, &mut ctx, "new.txt", b"content"); + + let changes = TreeDiff::run(&from, &to); + + assert_eq!(changes, vec![("new.txt".to_owned(), FileDiffKind::Added)]); +} + +#[test] +fn run_reports_removed_for_a_file_only_in_from() { + let (_scratch, repository) = open_repository("diff-removed"); + let mut ctx = ImportContext::default(); + let mut from = empty_tree(); + let to = empty_tree(); + insert(&repository, &mut from, &mut ctx, "old.txt", b"content"); + + let changes = TreeDiff::run(&from, &to); + + assert_eq!(changes, vec![("old.txt".to_owned(), FileDiffKind::Removed)]); +} + +#[test] +fn run_reports_modified_for_a_file_with_different_content_in_each_tree() { + let (_scratch, repository) = open_repository("diff-modified"); + let mut ctx = ImportContext::default(); + let mut from = empty_tree(); + let mut to = empty_tree(); + insert(&repository, &mut from, &mut ctx, "file.txt", b"first"); + insert(&repository, &mut to, &mut ctx, "file.txt", b"second"); + + let changes = TreeDiff::run(&from, &to); + + assert_eq!(changes, vec![("file.txt".to_owned(), FileDiffKind::Modified)]); +} + +#[test] +fn run_recurses_into_matched_subdirectories() { + let (_scratch, repository) = open_repository("diff-nested"); + let mut ctx = ImportContext::default(); + let mut from = empty_tree(); + let mut to = empty_tree(); + FileHandle::new("dir") + .insert_in_tree(&mut from, Stat::uninitialized()) + .unwrap(); + FileHandle::new("dir") + .insert_in_tree(&mut to, Stat::uninitialized()) + .unwrap(); + insert(&repository, &mut to, &mut ctx, "dir/new.txt", b"content"); + + let changes = TreeDiff::run(&from, &to); + + assert_eq!(changes, vec![("dir/new.txt".to_owned(), FileDiffKind::Added)]); +} + +#[test] +fn run_marks_both_sides_when_a_directory_is_replaced_by_a_regular_file() { + let (_scratch, repository) = open_repository("diff-type-change"); + let mut ctx = ImportContext::default(); + let mut from = empty_tree(); + let mut to = empty_tree(); + FileHandle::new("thing") + .insert_in_tree(&mut from, Stat::uninitialized()) + .unwrap(); + insert(&repository, &mut from, &mut ctx, "thing/child", b"content"); + insert(&repository, &mut to, &mut ctx, "thing", b"content"); + + let changes = TreeDiff::run(&from, &to); + + assert_eq!(changes.len(), 2); + assert!(changes.contains(&("thing".to_owned(), FileDiffKind::Added))); + assert!(changes.contains(&("thing/child".to_owned(), FileDiffKind::Removed))); +} + +#[test] +fn run_ignores_a_bare_directory_present_on_only_one_side() { + let (_scratch, repository) = open_repository("diff-dir-only-side"); + let mut ctx = ImportContext::default(); + let mut from = empty_tree(); + let mut to = empty_tree(); + FileHandle::new("empty-dir") + .insert_in_tree(&mut to, Stat::uninitialized()) + .unwrap(); + insert(&repository, &mut from, &mut ctx, "file.txt", b"content"); + insert(&repository, &mut to, &mut ctx, "file.txt", b"content"); + + let changes = TreeDiff::run(&from, &to); + + assert!(changes.is_empty()); +} diff --git a/lib/lib/tests/composefs_error.rs b/lib/lib/tests/composefs_error.rs new file mode 100644 index 00000000..e3ba4232 --- /dev/null +++ b/lib/lib/tests/composefs_error.rs @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::ffi::OsStr; +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; + +use anyhow::anyhow; + +use composefs::fsverity::Algorithm; +use composefs::generic_tree::ImageError; +use composefs::repository::RepositoryOpenError; + +use hex::FromHexError; + +use upac::composefs::error::RepoError; + +use upac_abi::error::ErrorKind; + +#[test] +fn hex_error_maps_to_invalid_digest() { + let error = FromHexError::OddLength; + + assert_eq!(RepoError::from(error), RepoError::InvalidDigest); +} + +#[test] +fn io_error_maps_by_kind() { + assert_eq!( + RepoError::from(IoError::new(IoErrorKind::NotFound, "missing")), + RepoError::NotFound + ); + assert_eq!( + RepoError::from(IoError::new(IoErrorKind::PermissionDenied, "denied")), + RepoError::AccessDenied + ); + assert_eq!(RepoError::from(IoError::other("other")), RepoError::Unexpected); +} + +#[test] +fn repository_open_error_maps_every_variant() { + assert_eq!( + RepoError::from(RepositoryOpenError::MetadataMissing), + RepoError::NotInitialized + ); + assert_eq!( + RepoError::from(RepositoryOpenError::OldFormatRepository), + RepoError::NotInitialized + ); + assert_eq!( + RepoError::from(RepositoryOpenError::MetadataInvalid( + serde_json::from_str::("not json").unwrap_err() + )), + RepoError::Corrupted + ); + assert_eq!( + RepoError::from(RepositoryOpenError::AlgorithmMismatch { + found: Algorithm::Sha256 { lg_blocksize: 12 }, + expected: Algorithm::Sha512 { lg_blocksize: 12 }, + }), + RepoError::AlgorithmMismatch + ); + assert_eq!( + RepoError::from(RepositoryOpenError::UnsupportedVersion { found: 99 }), + RepoError::UnsupportedVersion + ); + assert_eq!( + RepoError::from(RepositoryOpenError::IncompatibleFeatures(vec!["unknown".to_owned()])), + RepoError::IncompatibleFeatures + ); + assert_eq!( + RepoError::from(RepositoryOpenError::Io(IoError::new(IoErrorKind::NotFound, "missing"))), + RepoError::NotFound + ); +} + +#[test] +fn image_error_maps_every_variant() { + assert_eq!( + RepoError::from(ImageError::InvalidFilename(Box::::from(OsStr::new("..")))), + RepoError::InvalidPath + ); + assert_eq!( + RepoError::from(ImageError::NotFound(Box::::from(OsStr::new("missing")))), + RepoError::NotFound + ); + assert_eq!( + RepoError::from(ImageError::NotADirectory(Box::::from(OsStr::new("file")))), + RepoError::NotADirectory + ); + assert_eq!( + RepoError::from(ImageError::IsADirectory(Box::::from(OsStr::new("dir")))), + RepoError::IsADirectory + ); + assert_eq!( + RepoError::from(ImageError::IsNotRegular(Box::::from(OsStr::new("special")))), + RepoError::NotRegularFile + ); + assert_eq!( + RepoError::from(ImageError::LeafIdOutOfBounds(1, 0)), + RepoError::Unexpected + ); + assert_eq!( + RepoError::from(ImageError::OrphanedLeaves(vec![1])), + RepoError::Unexpected + ); +} + +#[test] +fn anyhow_error_maps_to_unexpected() { + assert_eq!(RepoError::from(anyhow!("boom")), RepoError::Unexpected); +} + +#[test] +fn every_variant_maps_to_the_documented_error_kind() { + let cases = [ + (RepoError::NotInitialized, ErrorKind::NotInitialized), + (RepoError::Corrupted, ErrorKind::ReadFailed), + (RepoError::AlgorithmMismatch, ErrorKind::Unexpected), + (RepoError::UnsupportedVersion, ErrorKind::Unexpected), + (RepoError::IncompatibleFeatures, ErrorKind::Unexpected), + (RepoError::NotFound, ErrorKind::NotFound), + (RepoError::AccessDenied, ErrorKind::PermissionDenied), + (RepoError::InvalidPath, ErrorKind::InvalidPath), + (RepoError::InvalidDigest, ErrorKind::InvalidPath), + (RepoError::NotADirectory, ErrorKind::InvalidEntry), + (RepoError::IsADirectory, ErrorKind::InvalidEntry), + (RepoError::NotRegularFile, ErrorKind::InvalidEntry), + (RepoError::NotASymlink, ErrorKind::InvalidEntry), + (RepoError::Cancelled, ErrorKind::Cancelled), + (RepoError::Unexpected, ErrorKind::Unexpected), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/config_merge.rs b/lib/lib/tests/config_merge.rs index c8383bb7..1769b883 100644 --- a/lib/lib/tests/config_merge.rs +++ b/lib/lib/tests/config_merge.rs @@ -12,7 +12,7 @@ use nix::fcntl::AT_FDCWD; use tempfile::{Builder, TempDir}; use upac::composefs::file::FileHandle; use upac::composefs::repository::ObjectID; -use upac::config::merge::merge_config; +use upac::config::merge_config; fn scratch_dir(name: &str) -> TempDir { Builder::new().prefix(name).tempdir().unwrap() diff --git a/lib/lib/tests/deploy_error.rs b/lib/lib/tests/deploy_error.rs new file mode 100644 index 00000000..47c2f513 --- /dev/null +++ b/lib/lib/tests/deploy_error.rs @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; + +use anyhow::anyhow; + +use nix::errno::Errno; + +use rsblkid::probe::{ProbeBuilderError, ProbeError}; + +use rsmount::errors::MountInfoError; + +use upac::deploy::error::SysrootError; + +use upac_abi::error::ErrorKind; + +#[test] +fn mount_info_error_maps_to_mount_info_unavailable() { + let error = MountInfoError::Creation("boom".to_owned()); + + assert_eq!(SysrootError::from(error), SysrootError::MountInfoUnavailable); +} + +#[test] +fn probe_builder_error_maps_to_probe_unavailable() { + let error = ProbeBuilderError::Required("scan_device".to_owned()); + + assert_eq!(SysrootError::from(error), SysrootError::ProbeUnavailable); +} + +#[test] +fn probe_error_maps_to_probe_unavailable() { + let error = ProbeError::Config("bad config".to_owned()); + + assert_eq!(SysrootError::from(error), SysrootError::ProbeUnavailable); +} + +#[test] +fn io_error_maps_to_sysroot_dir_unavailable() { + let error = IoError::new(IoErrorKind::PermissionDenied, "denied"); + + assert_eq!(SysrootError::from(error), SysrootError::SysrootDirUnavailable); +} + +#[test] +fn errno_maps_to_the_system_variant_with_the_same_errno() { + assert_eq!(SysrootError::from(Errno::ENOSPC), SysrootError::System(Errno::ENOSPC)); +} + +#[test] +fn anyhow_error_maps_to_current_prefix_digest_not_found() { + let error = anyhow!("no current prefix digest"); + + assert_eq!(SysrootError::from(error), SysrootError::CurrentPrefixDigestNotFound); +} + +#[test] +fn every_variant_maps_to_the_documented_error_kind() { + let cases = [ + (SysrootError::MountInfoUnavailable, ErrorKind::Unexpected), + (SysrootError::RootDeviceNotFound, ErrorKind::NotFound), + (SysrootError::CanonicalDeviceNotFound, ErrorKind::NotFound), + (SysrootError::SysrootDirUnavailable, ErrorKind::NotFound), + (SysrootError::DeploysDirNotFound, ErrorKind::NotFound), + (SysrootError::RepoDirNotFound, ErrorKind::NotFound), + (SysrootError::ProbeUnavailable, ErrorKind::Unexpected), + (SysrootError::FilesystemTypeNotFound, ErrorKind::NotFound), + (SysrootError::CurrentPrefixDigestNotFound, ErrorKind::NotFound), + (SysrootError::EspNotFound, ErrorKind::NotFound), + (SysrootError::System(Errno::EIO), ErrorKind::Unexpected), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/deploy.rs b/lib/lib/tests/inline/deploy.rs new file mode 100644 index 00000000..9c0134b6 --- /dev/null +++ b/lib/lib/tests/inline/deploy.rs @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::fs::create_dir_all; +use std::path::Path; + +use tempfile::TempDir; + +use crate::database::record::DeployRecord; + +use super::Deploy; + +fn write_record(deploy_dir: &Path, digest: &str, seq: u64, pinned: bool) { + let record_dir = deploy_dir.join(digest); + create_dir_all(&record_dir).unwrap(); + + let record = DeployRecord { + prefix_digest: digest.to_owned(), + subject: "test".to_owned(), + message: None, + seq, + timestamp: DeployRecord::now_secs(), + config_history: Vec::new(), + working_config: String::new(), + pinned, + }; + record.write(&record_dir).unwrap(); +} + +#[test] +fn prune_deploys_removes_nothing_when_total_is_within_retention_depth() { + let scratch = TempDir::new().unwrap(); + write_record(scratch.path(), "digest-0", 0, false); + write_record(scratch.path(), "digest-1", 1, false); + write_record(scratch.path(), "digest-2", 2, false); + + let deploy = Deploy::for_testing(scratch.path().to_path_buf()); + let removed = deploy.prune_deploys().unwrap(); + + assert!(removed.is_empty()); + assert!(scratch.path().join("digest-0").is_dir()); + assert!(scratch.path().join("digest-1").is_dir()); + assert!(scratch.path().join("digest-2").is_dir()); +} + +#[test] +fn prune_deploys_never_removes_a_pinned_deploy_regardless_of_age() { + let scratch = TempDir::new().unwrap(); + + // Oldest of the bunch, would be beyond any realistic retention depth on its own — + // `pinned: true` must save it anyway. + write_record(scratch.path(), "digest-oldest-pinned", 0, true); + + for seq in 1..=6 { + write_record(scratch.path(), &format!("digest-{seq}"), seq, false); + } + + let deploy = Deploy::for_testing(scratch.path().to_path_buf()); + let removed = deploy.prune_deploys().unwrap(); + + assert!(!removed.contains(&"digest-oldest-pinned".to_owned())); + assert!(scratch.path().join("digest-oldest-pinned").is_dir()); +} + +#[test] +fn prune_deploys_removes_the_oldest_unpinned_deploy_when_the_total_is_large() { + let scratch = TempDir::new().unwrap(); + + // Comfortably more entries than any sane retention depth would keep, so the single oldest, + // unpinned deploy is guaranteed to fall outside it regardless of the real (environment-read) + // `RuntimeSettings::load().gc.retention_depth` value. + for seq in 0..64 { + write_record(scratch.path(), &format!("digest-{seq}"), seq, false); + } + + let deploy = Deploy::for_testing(scratch.path().to_path_buf()); + let removed = deploy.prune_deploys().unwrap(); + + assert!(removed.contains(&"digest-0".to_owned())); + assert!(!scratch.path().join("digest-0").is_dir()); + + // The most recent one is always within any positive retention depth. + assert!(scratch.path().join("digest-63").is_dir()); +} diff --git a/lib/lib/tests/inline/mutated_files_error.rs b/lib/lib/tests/inline/mutated_files_error.rs new file mode 100644 index 00000000..fb4b5268 --- /dev/null +++ b/lib/lib/tests/inline/mutated_files_error.rs @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, FilesError}; + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + (FilesError::PackageNotFound, ErrorKind::NotFound), + (FilesError::Common(CommonError::OutOfMemory), ErrorKind::OutOfMemory), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/mutated_mime_error.rs b/lib/lib/tests/inline/mutated_mime_error.rs new file mode 100644 index 00000000..8c977387 --- /dev/null +++ b/lib/lib/tests/inline/mutated_mime_error.rs @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, IoError, IoErrorKind, MimeError}; + +#[test] +fn io_error_maps_to_io_with_the_same_kind() { + let error = IoError::new(IoErrorKind::PermissionDenied, "denied"); + + assert_eq!(MimeError::from(error), MimeError::Io(IoErrorKind::PermissionDenied)); +} + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + (MimeError::Common(CommonError::OutOfMemory), ErrorKind::OutOfMemory), + (MimeError::Io(IoErrorKind::NotFound), ErrorKind::NotFound), + ( + MimeError::Io(IoErrorKind::PermissionDenied), + ErrorKind::PermissionDenied, + ), + (MimeError::Io(IoErrorKind::Other), ErrorKind::Unexpected), + (MimeError::DesktopFileMalformed, ErrorKind::InvalidEntry), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/mutated_rollback_error.rs b/lib/lib/tests/inline/mutated_rollback_error.rs new file mode 100644 index 00000000..ebce8c79 --- /dev/null +++ b/lib/lib/tests/inline/mutated_rollback_error.rs @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{ + CommonError, ConfigDigestResolveError, DeployRecordError, DeployRecordsError, RollbackError, SysrootError, +}; + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + (RollbackError::Common(CommonError::OutOfMemory), ErrorKind::OutOfMemory), + ( + RollbackError::ConfigDigestNotFound("deadbeef".to_owned()), + ErrorKind::NotFound, + ), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} + +#[test] +fn config_digest_resolve_error_not_found_maps_to_config_digest_not_found() { + let error = ConfigDigestResolveError::NotFound("deadbeef".to_owned()); + + assert_eq!( + RollbackError::from(error), + RollbackError::ConfigDigestNotFound("deadbeef".to_owned()) + ); +} + +#[test] +fn config_digest_resolve_error_records_delegates_to_the_inner_error() { + let sysroot = ConfigDigestResolveError::Records(DeployRecordsError::Sysroot(SysrootError::MountInfoUnavailable)); + let deploy_record = + ConfigDigestResolveError::Records(DeployRecordsError::DeployRecord(DeployRecordError::NotFound)); + + assert_eq!( + RollbackError::from(sysroot), + RollbackError::Common(CommonError::Sysroot(SysrootError::MountInfoUnavailable)) + ); + assert_eq!( + RollbackError::from(deploy_record), + RollbackError::Common(CommonError::DeployRecord(DeployRecordError::NotFound)) + ); +} diff --git a/lib/lib/tests/inline/mutated_uninstaller_error.rs b/lib/lib/tests/inline/mutated_uninstaller_error.rs new file mode 100644 index 00000000..2a5f35e4 --- /dev/null +++ b/lib/lib/tests/inline/mutated_uninstaller_error.rs @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, UninstallError}; + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + (UninstallError::PackageNotFound, ErrorKind::NotFound), + (UninstallError::Common(CommonError::OutOfMemory), ErrorKind::OutOfMemory), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/mutated_update_error.rs b/lib/lib/tests/inline/mutated_update_error.rs new file mode 100644 index 00000000..8ea26983 --- /dev/null +++ b/lib/lib/tests/inline/mutated_update_error.rs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, UpdateError}; + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + (UpdateError::PackageNotFound, ErrorKind::NotFound), + (UpdateError::DowngradeNotAllowed, ErrorKind::InvalidEntry), + (UpdateError::Common(CommonError::OutOfMemory), ErrorKind::OutOfMemory), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/unmutated_diff_config_error.rs b/lib/lib/tests/inline/unmutated_diff_config_error.rs new file mode 100644 index 00000000..4bc6f73b --- /dev/null +++ b/lib/lib/tests/inline/unmutated_diff_config_error.rs @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, ConfigDigestResolveError, DeployRecordsError, DiffConfigError, SysrootError}; + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + ( + DiffConfigError::Common(CommonError::OutOfMemory), + ErrorKind::OutOfMemory, + ), + ( + DiffConfigError::ConfigDigestNotFound("deadbeef".to_owned()), + ErrorKind::NotFound, + ), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} + +#[test] +fn config_digest_resolve_error_not_found_maps_to_config_digest_not_found() { + let error = ConfigDigestResolveError::NotFound("deadbeef".to_owned()); + + assert_eq!( + DiffConfigError::from(error), + DiffConfigError::ConfigDigestNotFound("deadbeef".to_owned()) + ); +} + +#[test] +fn config_digest_resolve_error_records_delegates_to_the_inner_error() { + let error = ConfigDigestResolveError::Records(DeployRecordsError::Sysroot(SysrootError::MountInfoUnavailable)); + + assert_eq!( + DiffConfigError::from(error), + DiffConfigError::Common(CommonError::Sysroot(SysrootError::MountInfoUnavailable)) + ); +} diff --git a/lib/lib/tests/inline/unmutated_diff_error.rs b/lib/lib/tests/inline/unmutated_diff_error.rs new file mode 100644 index 00000000..39870114 --- /dev/null +++ b/lib/lib/tests/inline/unmutated_diff_error.rs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, DiffError}; + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + (DiffError::Common(CommonError::OutOfMemory), ErrorKind::OutOfMemory), + ( + DiffError::ConfigDigestNotFound("deadbeef".to_owned()), + ErrorKind::NotFound, + ), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/unmutated_search_files_error.rs b/lib/lib/tests/inline/unmutated_search_files_error.rs new file mode 100644 index 00000000..0199a101 --- /dev/null +++ b/lib/lib/tests/inline/unmutated_search_files_error.rs @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, SearchFilesError}; + +#[test] +fn regex_error_maps_to_invalid_search_pattern() { + let invalid_pattern = "("; + let error = regex::Regex::new(invalid_pattern).unwrap_err(); + + assert!(matches!( + SearchFilesError::from(error), + SearchFilesError::InvalidSearchPattern(_) + )); +} + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + ( + SearchFilesError::Common(CommonError::OutOfMemory), + ErrorKind::OutOfMemory, + ), + ( + SearchFilesError::InvalidSearchPattern("(".to_owned()), + ErrorKind::InvalidEntry, + ), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/unmutated_search_in_meta_error.rs b/lib/lib/tests/inline/unmutated_search_in_meta_error.rs new file mode 100644 index 00000000..097ba98d --- /dev/null +++ b/lib/lib/tests/inline/unmutated_search_in_meta_error.rs @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, SearchInMetaError}; + +#[test] +fn regex_error_maps_to_invalid_search_pattern() { + let invalid_pattern = "("; + let error = regex::Regex::new(invalid_pattern).unwrap_err(); + + assert!(matches!( + SearchInMetaError::from(error), + SearchInMetaError::InvalidSearchPattern(_) + )); +} + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + ( + SearchInMetaError::Common(CommonError::OutOfMemory), + ErrorKind::OutOfMemory, + ), + ( + SearchInMetaError::InvalidSearchPattern("(".to_owned()), + ErrorKind::InvalidEntry, + ), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/unmutated_search_in_package_files_error.rs b/lib/lib/tests/inline/unmutated_search_in_package_files_error.rs new file mode 100644 index 00000000..e8912241 --- /dev/null +++ b/lib/lib/tests/inline/unmutated_search_in_package_files_error.rs @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, SearchInPackageFilesError}; + +#[test] +fn regex_error_maps_to_invalid_search_pattern() { + let invalid_pattern = "("; + let error = regex::Regex::new(invalid_pattern).unwrap_err(); + + assert!(matches!( + SearchInPackageFilesError::from(error), + SearchInPackageFilesError::InvalidSearchPattern(_) + )); +} + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + ( + SearchInPackageFilesError::Common(CommonError::OutOfMemory), + ErrorKind::OutOfMemory, + ), + ( + SearchInPackageFilesError::InvalidSearchPattern("(".to_owned()), + ErrorKind::InvalidEntry, + ), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/inline/unmutated_search_meta_error.rs b/lib/lib/tests/inline/unmutated_search_meta_error.rs new file mode 100644 index 00000000..591bcc4e --- /dev/null +++ b/lib/lib/tests/inline/unmutated_search_meta_error.rs @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::error::ErrorKind; + +use super::{CommonError, SearchMetaError}; + +#[test] +fn regex_error_maps_to_invalid_search_pattern() { + let invalid_pattern = "("; + let error = regex::Regex::new(invalid_pattern).unwrap_err(); + + assert!(matches!( + SearchMetaError::from(error), + SearchMetaError::InvalidSearchPattern(_) + )); +} + +#[test] +fn every_own_variant_maps_to_the_documented_error_kind() { + let cases = [ + ( + SearchMetaError::Common(CommonError::OutOfMemory), + ErrorKind::OutOfMemory, + ), + ( + SearchMetaError::InvalidSearchPattern("(".to_owned()), + ErrorKind::InvalidEntry, + ), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/orchestrator.rs b/lib/lib/tests/orchestrator.rs index ac0b4555..3b2db695 100644 --- a/lib/lib/tests/orchestrator.rs +++ b/lib/lib/tests/orchestrator.rs @@ -9,9 +9,10 @@ use std::sync::{Arc, Mutex}; use upac::errors::CommonError; use upac::lock::LockError; +use upac::orchestrator::context::Context; use upac::orchestrator::error::OrchestratorError; use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; -use upac::orchestrator::{Context, Orchestrator, SequentialOrchestrator}; +use upac::orchestrator::{Orchestrator, SequentialOrchestrator}; use upac_abi::error::ErrorKind; use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/tests/plugin_boot_error.rs b/lib/lib/tests/plugin_boot_error.rs new file mode 100644 index 00000000..6a931ac4 --- /dev/null +++ b/lib/lib/tests/plugin_boot_error.rs @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; + +use upac::plugin::boot::error::BootPluginError; + +use upac_abi::error::ErrorKind; + +#[test] +fn io_error_maps_to_io_with_the_same_kind() { + let error = IoError::new(IoErrorKind::PermissionDenied, "denied"); + + assert_eq!( + BootPluginError::from(error), + BootPluginError::Io(IoErrorKind::PermissionDenied) + ); +} + +#[test] +fn toml_error_maps_to_manifest() { + let error = toml::from_str::("not valid toml [[[").unwrap_err(); + + assert_eq!(BootPluginError::from(error), BootPluginError::Manifest); +} + +#[test] +fn every_variant_maps_to_the_documented_error_kind() { + let cases = [ + (BootPluginError::Load, ErrorKind::NotFound), + (BootPluginError::Symbol, ErrorKind::AbiMismatch), + ( + BootPluginError::AbiMismatch { got: 1, expected: 2 }, + ErrorKind::AbiMismatch, + ), + ( + BootPluginError::Reported(ErrorKind::PermissionDenied), + ErrorKind::PermissionDenied, + ), + (BootPluginError::Io(IoErrorKind::NotFound), ErrorKind::ReadFailed), + (BootPluginError::Manifest, ErrorKind::InvalidEntry), + ( + BootPluginError::DuplicateName("uki".to_owned()), + ErrorKind::InvalidEntry, + ), + (BootPluginError::UnknownName("uki".to_owned()), ErrorKind::NotFound), + (BootPluginError::NoClaimant, ErrorKind::NotFound), + (BootPluginError::AmbiguousClaim, ErrorKind::InvalidEntry), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/plugin_boot_manifest.rs b/lib/lib/tests/plugin_boot_manifest.rs new file mode 100644 index 00000000..e661da30 --- /dev/null +++ b/lib/lib/tests/plugin_boot_manifest.rs @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::fs::write; + +use tempfile::{Builder, TempDir}; + +use upac::plugin::boot::error::BootPluginError; +use upac::plugin::boot::manifest::load_boot_plugin_manifests; + +fn scratch_dir(name: &str) -> TempDir { + Builder::new().prefix(name).tempdir().unwrap() +} + +#[test] +fn load_boot_plugin_manifests_collects_distinct_names() { + let dir = scratch_dir("distinct-names"); + write( + dir.path().join("uki.boot"), + "name = \"uki\"\nlibrary = \"libupac-uki.so\"\n", + ) + .unwrap(); + write( + dir.path().join("grub.boot"), + "name = \"grub\"\nlibrary = \"libupac-grub.so\"\n", + ) + .unwrap(); + + let manifests = load_boot_plugin_manifests(dir.path().to_str().unwrap(), "boot").unwrap(); + + assert_eq!(manifests.len(), 2); + assert_eq!(manifests["uki"].library, "libupac-uki.so"); + assert_eq!(manifests["grub"].library, "libupac-grub.so"); +} + +#[test] +fn load_boot_plugin_manifests_ignores_non_matching_extension() { + let dir = scratch_dir("ignore-extension"); + write( + dir.path().join("uki.boot"), + "name = \"uki\"\nlibrary = \"libupac-uki.so\"\n", + ) + .unwrap(); + write(dir.path().join("README.md"), b"not a manifest").unwrap(); + + let manifests = load_boot_plugin_manifests(dir.path().to_str().unwrap(), "boot").unwrap(); + + assert_eq!(manifests.len(), 1); +} + +#[test] +fn load_boot_plugin_manifests_fails_on_duplicate_name() { + let dir = scratch_dir("duplicate-name"); + write( + dir.path().join("a.boot"), + "name = \"uki\"\nlibrary = \"libupac-uki-a.so\"\n", + ) + .unwrap(); + write( + dir.path().join("b.boot"), + "name = \"uki\"\nlibrary = \"libupac-uki-b.so\"\n", + ) + .unwrap(); + + let result = load_boot_plugin_manifests(dir.path().to_str().unwrap(), "boot"); + + assert_eq!(result.unwrap_err(), BootPluginError::DuplicateName("uki".to_owned())); +} + +#[test] +fn load_boot_plugin_manifests_fails_on_malformed_toml() { + let dir = scratch_dir("malformed-toml"); + write(dir.path().join("broken.boot"), "not valid toml [[[").unwrap(); + + let result = load_boot_plugin_manifests(dir.path().to_str().unwrap(), "boot"); + + assert_eq!(result.unwrap_err(), BootPluginError::Manifest); +} + +#[test] +fn load_boot_plugin_manifests_treats_a_missing_directory_as_no_manifests() { + let dir = scratch_dir("missing-dir").path().join("does-not-exist"); + + let manifests = load_boot_plugin_manifests(dir.to_str().unwrap(), "boot").unwrap(); + + assert!(manifests.is_empty()); +} diff --git a/lib/lib/tests/plugin_decoder.rs b/lib/lib/tests/plugin_decoder.rs index 6ffae21f..77ddd3e9 100644 --- a/lib/lib/tests/plugin_decoder.rs +++ b/lib/lib/tests/plugin_decoder.rs @@ -144,6 +144,15 @@ fn load_decoder_manifests_fails_on_duplicate_format() { assert_eq!(result.unwrap_err(), DecoderError::DuplicateFormat("deb".to_owned())); } +#[test] +fn load_decoder_manifests_treats_a_missing_directory_as_no_manifests() { + let dir = scratch_dir("missing-dir").path().join("does-not-exist"); + + let manifests = load_decoder_manifests(dir.to_str().unwrap(), "decoder").unwrap(); + + assert!(manifests.is_empty()); +} + #[test] fn load_decoder_manifests_fails_on_malformed_toml() { let dir = scratch_dir("malformed-toml"); diff --git a/lib/lib/tests/plugin_decoder_error.rs b/lib/lib/tests/plugin_decoder_error.rs new file mode 100644 index 00000000..a4fd413f --- /dev/null +++ b/lib/lib/tests/plugin_decoder_error.rs @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; + +use mime::Mime; + +use upac::plugin::decoder::error::DecoderError; + +use upac_abi::error::ErrorKind; + +#[test] +fn abi_error_kind_maps_to_invalid_response() { + let error = ErrorKind::AbiMismatch; + + assert_eq!(DecoderError::from(error), DecoderError::InvalidResponse); +} + +#[test] +fn io_error_maps_to_io_with_the_same_kind() { + let error = IoError::new(IoErrorKind::NotFound, "missing"); + + assert_eq!(DecoderError::from(error), DecoderError::Io(IoErrorKind::NotFound)); +} + +#[test] +fn toml_error_maps_to_manifest() { + let error = toml::from_str::("not valid toml [[[").unwrap_err(); + + assert_eq!(DecoderError::from(error), DecoderError::Manifest); +} + +#[test] +fn mime_parse_error_maps_to_invalid_mime_type() { + let error = "".parse::().unwrap_err(); + + assert_eq!(DecoderError::from(error), DecoderError::InvalidMimeType); +} + +#[test] +fn every_variant_maps_to_the_documented_error_kind() { + let cases = [ + (DecoderError::Load, ErrorKind::NotFound), + (DecoderError::Symbol, ErrorKind::AbiMismatch), + ( + DecoderError::AbiMismatch { got: 1, expected: 2 }, + ErrorKind::AbiMismatch, + ), + (DecoderError::Failed(-1), ErrorKind::Unexpected), + (DecoderError::InvalidResponse, ErrorKind::InvalidEntry), + (DecoderError::Io(IoErrorKind::NotFound), ErrorKind::ReadFailed), + (DecoderError::Manifest, ErrorKind::InvalidEntry), + (DecoderError::DuplicateFormat("deb".to_owned()), ErrorKind::InvalidEntry), + (DecoderError::UnknownFormat("zst".to_owned()), ErrorKind::NotFound), + (DecoderError::InvalidMimeType, ErrorKind::InvalidEntry), + (DecoderError::NoDecoders, ErrorKind::NotFound), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/scripts_error.rs b/lib/lib/tests/scripts_error.rs new file mode 100644 index 00000000..b39b7ec9 --- /dev/null +++ b/lib/lib/tests/scripts_error.rs @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; +use std::str::from_utf8; + +use upac::scripts::error::HookError; + +use upac_abi::error::ErrorKind; + +use upac_pki::error::PkiError; + +#[test] +fn toml_error_maps_to_parse() { + let error = toml::from_str::("not valid toml [[[").unwrap_err(); + + assert_eq!(HookError::from(error), HookError::Parse); +} + +#[test] +fn io_error_maps_to_io_with_the_same_kind() { + let error = IoError::new(IoErrorKind::PermissionDenied, "denied"); + + assert_eq!(HookError::from(error), HookError::Io(IoErrorKind::PermissionDenied)); +} + +#[test] +fn utf8_error_maps_to_encoding() { + let bytes: Vec = vec![0xff, 0xfe]; + let error = from_utf8(&bytes).unwrap_err(); + + assert_eq!(HookError::from(error), HookError::Encoding); +} + +#[test] +fn pki_error_maps_each_variant_directly() { + let cases = [ + (PkiError::Malformed, HookError::MalformedSignature), + (PkiError::InvalidSignature, HookError::InvalidSignature), + (PkiError::Generation, HookError::Parse), + ]; + + for (error, expected) in cases { + assert_eq!(HookError::from(error), expected); + } +} + +#[test] +fn every_variant_maps_to_the_documented_error_kind() { + let cases = [ + (HookError::Parse, ErrorKind::InvalidEntry), + (HookError::InvalidTrigger, ErrorKind::InvalidEntry), + (HookError::NoTrigger, ErrorKind::InvalidEntry), + (HookError::Io(IoErrorKind::NotFound), ErrorKind::ReadFailed), + (HookError::Encoding, ErrorKind::InvalidEntry), + (HookError::MalformedSignature, ErrorKind::InvalidEntry), + (HookError::InvalidSignature, ErrorKind::InvalidEntry), + (HookError::TriggerConflict("deb".to_owned()), ErrorKind::InvalidEntry), + ]; + + for (error, expected) in cases { + assert_eq!(ErrorKind::from(error), expected); + } +} diff --git a/lib/lib/tests/scripts_hook.rs b/lib/lib/tests/scripts_hook.rs index 2b431bb3..28968b0c 100644 --- a/lib/lib/tests/scripts_hook.rs +++ b/lib/lib/tests/scripts_hook.rs @@ -7,11 +7,14 @@ use std::fs::{read_link, write}; use std::path::{Path, PathBuf}; use tempfile::{Builder, TempDir}; +use upac::errors::CommonError; +use upac::orchestrator::stage::{ConcurrentStage, StageResult}; use upac::scripts::error::HookError; use upac::scripts::file::HookFile; use upac::scripts::load::load_hooks; use upac::scripts::pipeline::{Operation, PipelineTrigger, Timing}; use upac::scripts::primitive::Step; +use upac_abi::hook::ProgressEventBuilder; use upac_pki::generate::{Identity, SigningIdentity, generate_root, generate_signing_cert}; use upac_pki::signature::HookSignature; @@ -216,6 +219,80 @@ fn primitive_vec_rollback_guard_unwinds_in_reverse_order() { assert!(!c.exists()); } +#[test] +fn hook_file_run_executes_all_steps_and_returns_advance() { + let dir = scratch_dir("run-advance"); + let a = dir.path().join("a"); + let b = dir.path().join("b"); + + let hook_file = HookFile::parse(&format!( + concat!( + "operation = \"install\"\ntiming = \"pre\"\n\n", + "[[steps]]\ntype = \"touch_file\"\npath = {:?}\n\n", + "[[steps]]\ntype = \"touch_file\"\npath = {:?}\n", + ), + a, b + )) + .unwrap(); + + let (_, result, _guard) = + ConcurrentStage::::run(Box::new(hook_file), ProgressEventBuilder::new(0)).unwrap(); + + assert!(matches!(result, StageResult::Advance)); + assert!(a.exists()); + assert!(b.exists()); +} + +#[test] +fn hook_file_run_rolls_back_and_errors_when_a_critical_step_fails() { + let dir = scratch_dir("run-critical-failure"); + let touched = dir.path().join("touched"); + let missing_from = dir.path().join("does-not-exist"); + let move_to = dir.path().join("move-to"); + + let hook_file = HookFile::parse(&format!( + concat!( + "operation = \"install\"\ntiming = \"pre\"\ncritical = true\n\n", + "[[steps]]\ntype = \"touch_file\"\npath = {:?}\n\n", + "[[steps]]\ntype = \"move_file\"\nfrom = {:?}\nto = {:?}\n", + ), + touched, missing_from, move_to + )) + .unwrap(); + + let result = ConcurrentStage::::run(Box::new(hook_file), ProgressEventBuilder::new(0)); + + assert!(result.is_err()); + assert!(!touched.exists(), "the already-executed step must be rolled back"); +} + +#[test] +fn hook_file_run_stops_early_without_error_when_a_non_critical_step_fails() { + let dir = scratch_dir("run-non-critical-failure"); + let touched = dir.path().join("touched"); + let missing_from = dir.path().join("does-not-exist"); + let move_to = dir.path().join("move-to"); + + let hook_file = HookFile::parse(&format!( + concat!( + "operation = \"install\"\ntiming = \"pre\"\ncritical = false\n\n", + "[[steps]]\ntype = \"touch_file\"\npath = {:?}\n\n", + "[[steps]]\ntype = \"move_file\"\nfrom = {:?}\nto = {:?}\n", + ), + touched, missing_from, move_to + )) + .unwrap(); + + let (_, result, _guard) = + ConcurrentStage::::run(Box::new(hook_file), ProgressEventBuilder::new(0)).unwrap(); + + assert!(matches!(result, StageResult::Advance)); + assert!( + touched.exists(), + "a non-critical failure must not roll back prior steps" + ); +} + #[test] fn load_hooks_returns_matching_hook_for_signed_valid_file() { let hooks_dir = scratch_dir("load-valid"); diff --git a/lib/macro/src/c_try_to_rust/mod.rs b/lib/macro/src/c_try_to_rust/mod.rs index 8858e250..155a5586 100644 --- a/lib/macro/src/c_try_to_rust/mod.rs +++ b/lib/macro/src/c_try_to_rust/mod.rs @@ -30,6 +30,8 @@ fn option_from_c(ident: &Ident, segment: &PathSegment) -> TokenStream2 { if inner_name == "String" { quote! { Option::<&str>::try_from(&value.#ident)?.map(str::to_owned) } + } else if inner_name == "HookMessageFn" { + quote! { value.#ident } } else { quote! { compile_error!("CTryToRust: unsupported Option inner type") } } @@ -40,7 +42,18 @@ fn vec_from_c(ident: &Ident, segment: &PathSegment) -> TokenStream2 { return quote! { compile_error!("CTryToRust: unsupported Vec element type") }; }; - if PRIMITIVES.contains(&inner_name.as_str()) { + if inner_name == "String" { + quote! { + { + unsafe { value.#ident.validate()? }; + unsafe { value.#ident.as_slice() } + .iter() + .map(<&str>::try_from) + .map(|element| element.map(str::to_owned)) + .collect::, ErrorKind>>()? + } + } + } else if PRIMITIVES.contains(&inner_name.as_str()) { quote! { { unsafe { value.#ident.validate()? }; @@ -71,11 +84,26 @@ fn field_path_from_c(ident: &Ident, segment: &PathSegment) -> TokenStream2 { } } +fn ptr_from_c(ident: &Ident) -> TokenStream2 { + quote! { + { + if value.#ident.is_null() { + return Err(ErrorKind::InvalidEntry); + } + value.#ident + } + } +} + fn field_from_c_fallible(ident: &Ident, ty: &Type) -> TokenStream2 { if let Type::Array(_) = ty { return quote! { value.#ident }; } + if let Type::Ptr(_) = ty { + return ptr_from_c(ident); + } + let Type::Path(type_path) = ty else { return quote! { compile_error!("CTryToRust: unsupported field type") }; }; diff --git a/lib/macro/src/c_validate/mod.rs b/lib/macro/src/c_validate/mod.rs index 0076080e..3bce2a22 100644 --- a/lib/macro/src/c_validate/mod.rs +++ b/lib/macro/src/c_validate/mod.rs @@ -9,7 +9,9 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; + use quote::quote; + use syn::{Data, DeriveInput, Error, Field, Fields, Ident, PathSegment, Type, TypePtr, parse_macro_input}; use crate::common::{VALIDATABLE_COMPOSITES, generic_arg, segment_name}; @@ -89,7 +91,17 @@ fn field_ptr_validate(ident: &Ident, ptr: &TypePtr) -> TokenStream2 { return quote! {}; }; - if VALIDATABLE_COMPOSITES.contains(&seg.ident.to_string().as_str()) { + let name = seg.ident.to_string(); + + if name == "CancelToken" { + return quote! { + if self.#ident.is_null() { + return Err(ErrorKind::InvalidEntry); + } + }; + } + + if VALIDATABLE_COMPOSITES.contains(&name.as_str()) { quote! { unsafe { if self.#ident.is_null() { @@ -129,6 +141,12 @@ fn validate_impl(name: &Ident, validations: &[TokenStream2]) -> TokenStream2 { Ok(()) } } + + impl crate::types::CValidatable for #name { + unsafe fn validate(&self) -> Result<(), ErrorKind> { + unsafe { #name::validate(self) } + } + } } } diff --git a/lib/macro/src/common.rs b/lib/macro/src/common.rs index 9bf290c2..7feb428c 100644 --- a/lib/macro/src/common.rs +++ b/lib/macro/src/common.rs @@ -28,12 +28,7 @@ pub(crate) const VALIDATABLE_COMPOSITES: &[&str] = &[ "CPrefixEntry", "CHistoryEntry", "CRequestBase", - "CDependency", - "CSetupBase", - "CPartitionMount", - "CPartitionSpec", - "CGptLayout", - "CBtrfsOptions", + "CPackageDependency", ]; pub(crate) fn generic_arg(segment: &PathSegment) -> Option<&Type> { diff --git a/lib/macro/src/context_value/mod.rs b/lib/macro/src/context_value/mod.rs new file mode 100644 index 00000000..d3e06184 --- /dev/null +++ b/lib/macro/src/context_value/mod.rs @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +//! `#[derive(ContextValue)]` — generates `Deref`/`DerefMut`/`From` for a +//! single-field tuple struct, so an orchestrator `Context` wrapper type can be +//! used (and constructed) like its inner value without hand-written +//! boilerplate for each one. + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{Data, DeriveInput, Error, Fields, parse_macro_input}; + +pub(crate) fn expand(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = &input.ident; + + let field = match &input.data { + Data::Struct(s) => match &s.fields { + Fields::Unnamed(fields) if fields.unnamed.len() == 1 => &fields.unnamed[0], + _ => { + return Error::new_spanned(name, "ContextValue only supports single-field tuple structs") + .to_compile_error() + .into(); + } + }, + _ => { + return Error::new_spanned(name, "ContextValue only supports tuple structs") + .to_compile_error() + .into(); + } + }; + + let ty = &field.ty; + + let expanded: TokenStream2 = quote! { + impl std::ops::Deref for #name { + type Target = #ty; + + fn deref(&self) -> &#ty { + &self.0 + } + } + + impl std::ops::DerefMut for #name { + fn deref_mut(&mut self) -> &mut #ty { + &mut self.0 + } + } + + impl From<#ty> for #name { + fn from(value: #ty) -> Self { + #name(value) + } + } + }; + + expanded.into() +} diff --git a/lib/macro/src/lib.rs b/lib/macro/src/lib.rs index 9db7cd08..2215faa6 100644 --- a/lib/macro/src/lib.rs +++ b/lib/macro/src/lib.rs @@ -12,6 +12,7 @@ //! CTryToRust - C-ABI struct -> Rust domain type, fallible (inbound) //! CToRust - C-ABI struct -> Rust domain type, infallible (inbound) //! CValidate - unsafe validate() checking struct_size + every field +//! ContextValue - Deref/DerefMut/From for a single-field tuple struct //! FromStageIndex - orchestrator stage index -> enum variant (by position) //! StageKey - enum variant -> "stage_snake_case" gettext key (by name) //! RedbCodec - encode_into()/decode_from() for the redb key-value store @@ -29,6 +30,7 @@ mod c_to_rust; mod c_try_to_rust; mod c_validate; mod common; +mod context_value; mod from_stage_index; mod json_codec; mod redb_codec; @@ -65,6 +67,11 @@ pub fn derive_cvalidate(input: TokenStream) -> TokenStream { c_validate::expand(input) } +#[proc_macro_derive(ContextValue)] +pub fn derive_context_value(input: TokenStream) -> TokenStream { + context_value::expand(input) +} + #[proc_macro_derive(FromStageIndex)] pub fn derive_from_stage_index(input: TokenStream) -> TokenStream { from_stage_index::expand(input) diff --git a/lib/macro/src/rust_to_c/mod.rs b/lib/macro/src/rust_to_c/mod.rs index cc13f0de..bc4646fe 100644 --- a/lib/macro/src/rust_to_c/mod.rs +++ b/lib/macro/src/rust_to_c/mod.rs @@ -35,7 +35,9 @@ fn vec_to_c(ident: &Ident, segment: &PathSegment) -> TokenStream2 { return quote! { compile_error!("RustToC: unsupported Vec element type") }; }; - if PRIMITIVES.contains(&inner_name.as_str()) { + if inner_name == "String" { + quote! { CVec::from_owned(value.#ident.into_iter().map(|element| CSlice::from_owned(element.into_bytes())).collect()) } + } else if PRIMITIVES.contains(&inner_name.as_str()) { quote! { CVec::from_owned(value.#ident) } } else { let c_inner = format_ident!("C{inner_name}"); @@ -58,6 +60,10 @@ fn field_to_c(ident: &Ident, ty: &Type) -> TokenStream2 { return quote! { value.#ident }; } + if let Type::Ptr(_) = ty { + return quote! { value.#ident }; + } + let Type::Path(type_path) = ty else { return quote! { compile_error!("RustToC: unsupported field type") }; }; diff --git a/lib/pki/src/error.rs b/lib/pki/src/error.rs index 9c70a175..d05a5fed 100644 --- a/lib/pki/src/error.rs +++ b/lib/pki/src/error.rs @@ -4,9 +4,9 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::array::TryFromSliceError; -use std::fmt::{Display, Formatter, Result}; use der::Error as DerError; + use rcgen::Error as RcgenError; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -16,18 +16,6 @@ pub enum PkiError { Generation, } -impl Display for PkiError { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - match self { - PkiError::Malformed => write!(f, "malformed PKI data"), - PkiError::InvalidSignature => write!(f, "invalid signature"), - PkiError::Generation => write!(f, "certificate generation failed"), - } - } -} - -impl std::error::Error for PkiError {} - impl From for PkiError { fn from(_: DerError) -> Self { PkiError::Malformed diff --git a/lib/pki/src/generate.rs b/lib/pki/src/generate.rs index 42862ca4..18671651 100644 --- a/lib/pki/src/generate.rs +++ b/lib/pki/src/generate.rs @@ -5,14 +5,17 @@ use der::pem::LineEnding; use der::{Decode, DecodePem, Encode, EncodePem}; + use rcgen::{ BasicConstraints, CertificateParams, DistinguishedName, DnType, IsCa, Issuer, KeyPair, KeyUsagePurpose, PKCS_ED25519, }; + use rustls_pki_types::CertificateDer; + use x509_cert::Certificate; -use crate::error::PkiError; +use super::error::PkiError; pub struct SerializedIdentity { pub key_der: Vec, @@ -46,8 +49,11 @@ impl Identity for RootIdentity { fn from_bytes(serialized: &SerializedIdentity) -> Result { let key_pair = KeyPair::try_from(serialized.key_der.as_slice())?; + let certificate_der = CertificateDer::from(serialized.certificate_der.as_slice()); + let issuer = Issuer::from_ca_cert_der(&certificate_der, key_pair)?; + let certificate = Certificate::from_der(&serialized.certificate_der)?; Ok(RootIdentity { issuer, certificate }) @@ -62,7 +68,9 @@ impl Identity for RootIdentity { fn from_pem(pem: &PemIdentity) -> Result { let key_pair = KeyPair::from_pem(&pem.key_pem)?; + let issuer = Issuer::from_ca_cert_pem(&pem.certificate_pem, key_pair)?; + let certificate = Certificate::from_pem(pem.certificate_pem.as_bytes())?; Ok(RootIdentity { issuer, certificate }) @@ -84,6 +92,7 @@ impl Identity for SigningIdentity { fn from_bytes(serialized: &SerializedIdentity) -> Result { let key_pair = KeyPair::try_from(serialized.key_der.as_slice())?; + let certificate = Certificate::from_der(&serialized.certificate_der)?; Ok(SigningIdentity { key_pair, certificate }) @@ -98,6 +107,7 @@ impl Identity for SigningIdentity { fn from_pem(pem: &PemIdentity) -> Result { let key_pair = KeyPair::from_pem(&pem.key_pem)?; + let certificate = Certificate::from_pem(pem.certificate_pem.as_bytes())?; Ok(SigningIdentity { key_pair, certificate }) @@ -114,6 +124,7 @@ pub fn generate_root(common_name: &str) -> Result { params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign]; let key_pair = KeyPair::generate_for(&PKCS_ED25519)?; + let certificate_der = params.self_signed(&key_pair)?; let certificate = Certificate::from_der(certificate_der.der())?; @@ -132,6 +143,7 @@ pub fn generate_signing_cert(common_name: &str, root: &RootIdentity) -> Result Result { let signature_bytes = signing.key_pair.sign(hook_bytes)?; + let signature = Signature::try_from(signature_bytes.as_slice()).map_err(|_| PkiError::Malformed)?; Ok(HookSignature { @@ -126,6 +130,7 @@ impl HookSignature { let verifying_key = Self::extract_verifying_key(issuer_certificate)?; let tbs_der = certificate.tbs_certificate().to_der()?; + let signature = Signature::try_from(certificate.signature().raw_bytes()).map_err(|_| PkiError::Malformed)?; verifying_key @@ -141,6 +146,7 @@ impl HookSignature { .subject_public_key_info() .subject_public_key .raw_bytes(); + let key_bytes: [u8; 32] = key_bytes.try_into()?; VerifyingKey::from_bytes(&key_bytes).map_err(|_| PkiError::Malformed) diff --git a/lib/setup/Cargo.toml b/lib/setup/Cargo.toml index c6a54b60..6b5f70c8 100644 --- a/lib/setup/Cargo.toml +++ b/lib/setup/Cargo.toml @@ -24,7 +24,7 @@ categories.workspace = true workspace = true [dependencies] -upac-lib = { workspace = true, features = ["builtin-all-booters"] } +upac-lib = { workspace = true, features = ["builtin-all-booters", "builtin-all-decoders"] } upac-types = { workspace = true } upac-abi = { workspace = true } upac-macro = { workspace = true } @@ -42,7 +42,6 @@ xz2 = { workspace = true } zstd = { workspace = true } tempfile = { workspace = true } -composefs-setup-root = "0.9.0" sevenz-rust2 = "0.22.2" btrfs-mkfs = "0.13.0" gptman = "3.1.1" diff --git a/lib/setup/lib.toml b/lib/setup/lib.toml index 46b43028..6d454eb2 100644 --- a/lib/setup/lib.toml +++ b/lib/setup/lib.toml @@ -3,11 +3,6 @@ # # SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -# meta.filename is the default name of the per-source_dir package-metadata -# manifest (see meta.rs) — overridable per request via CSetupBase.meta_filename. -[meta] -filename = "meta.toml" - # mount.default_mount_point is where the target deployment partition (and, # under it, the ESP + any extra_mounts) gets mounted when the request's own # mount_point is left empty. @@ -27,8 +22,15 @@ wipefs_bin = "wipefs" # partition.settle_attempts/settle_interval_ms bound how long DiskLayout::create waits for the # kernel (via udev) to create the new partition device nodes after BLKRRPART, before giving up — # the ioctl itself is synchronous, but the /dev/vdaN nodes it triggers are created by udev -# asynchronously afterward. +# asynchronously afterward. esp_label/deploy_label name both the GPT partition entry itself +# (DiskLayout::create) and the filesystem volume label written at format time +# (TargetSysroot::create_whole_disk) — purely informational (`lsblk -f`/`blkid`/other OS +# installers), nothing in this codebase looks a partition up by either. Uppercase by the same +# convention real-world ESP volume labels always use. [partition] +esp_label = "ESP" +deploy_label = "UPAC-DEPLOY" + settle_attempts = 50 settle_interval_ms = 100 @@ -36,15 +38,40 @@ settle_interval_ms = 100 # written (under the OS temp dir) before being embedded into the new /usr # tree — purely transient, read back once right after the write. # -# *_source are the fixed, package-convention paths (source-tree-relative) where each supported -# bootloader ships its own EFI binary — systemd's own packaging always installs to -# lib/systemd/boot/efi/, rEFInd's own packaging always installs to share/refind/. Checked in this -# order (first one found in the source tree wins) since a fresh ESP has no bootloader binary at -# all yet — install/update never need this, the binary is already on the ESP from genesis. # esp_fallback_loader is the fixed "removable media" path UEFI firmware always tries when NVRAM -# has no Boot#### entries at all, which is exactly genesis's situation on a brand-new disk. +# has no Boot#### entries at all, which is exactly genesis's situation on a brand-new disk. The +# resolved boot plugin's own `esp_loader_source()` (see `booters/booter.toml`) says which source +# path — if any — a fresh ESP needs it copied from; genesis only owns the destination. +# efi_linux_dir is a real, mounted-filesystem-relative path (forward slashes, joined via +# `Path::join`) to where `composefs-boot`'s own UKI writer always lands `upac-to.efi`/ +# `upac-from.efi` (`EFI/Linux/`, hardcoded upstream) — used only to seed `upac-from.efi` with the +# same first image on genesis's very first deploy. Distinct from `booters/booter.toml`'s +# `uki.efi_linux_dir`, which is the same directory spelled as a backslash UEFI device-path string +# for NVRAM Boot#### entries, not a filesystem path — the two can't share a definition across the +# crate boundary. +# +# system_dir is a source-relative directory, sibling to the package archives `EnumeratePackagesStage` +# scans (skipped by it automatically — it only looks at files, never directories) — a literal, +# 1:1 mirror of the target's real `/usr` layout (matches `PrefixTree`, see `database_path` in +# `lib/lib/lib.toml`), imported wholesale by `ImportSystemStage`. This is how a built `up`/ +# `upac-lib`/booters (or anything else that must exist outside the normal per-package pipeline) +# gets onto a genesis'd disk — genesis never resolves or installs itself automatically, whoever +# assembles `--source` has to put it there, same assumption already made for the systemd-boot/ +# rEFInd binaries. +# +# composefs_setup_root_unit_path (relative to `system_dir`, i.e. also relative to real `/usr`) is +# where `ImportSystemStage` requires to find a `composefs-setup-root.service` unit — hard error +# (`SetupError::ComposefsSetupRootUnitNotFound`) if missing, since a genesis'd disk cannot boot at +# all without it. composefs_setup_root_wants_path/composefs_setup_root_wants_target: the stage +# also creates the unit's `*.target.wants/` enablement symlink itself (pure systemd mechanics, no +# reason to burden `--source` with it) — see TODO.md for why this specific set of paths matches +# `composefs-setup-root`'s own hardcoded expectations (confirmed by reading its upstream source, +# not guessed). [genesis] scratch_filename = "genesis-packages.redb" -systemd_boot_source = "usr/lib/systemd/boot/efi/systemd-bootx64.efi" -refind_source = "usr/share/refind/refind_x64.efi" esp_fallback_loader = "EFI/BOOT/BOOTX64.EFI" +efi_linux_dir = "EFI/Linux" +system_dir = "system" +composefs_setup_root_unit_path = "lib/systemd/system/composefs-setup-root.service" +composefs_setup_root_wants_path = "lib/systemd/system/initrd-root-fs.target.wants/composefs-setup-root.service" +composefs_setup_root_wants_target = "../composefs-setup-root.service" diff --git a/lib/setup/src/data.rs b/lib/setup/src/data.rs index 8168fa43..b2645e6d 100644 --- a/lib/setup/src/data.rs +++ b/lib/setup/src/data.rs @@ -22,7 +22,6 @@ pub struct SetupExistingData<'data> { pub mount_point: Option<&'data str>, pub source: &'data str, - pub meta_filename: Option<&'data str>, pub empty_config: bool, pub pinned: bool, pub boot_plugin: Option<&'data str>, @@ -55,7 +54,6 @@ impl<'data> TryFrom<&'data CSetupExistingRequest> for SetupExistingData<'data> { mount_point: (&request.base.mount_point).try_into()?, source: (&request.base.source).try_into()?, - meta_filename: (&request.base.meta_filename).try_into()?, empty_config: request.base.empty_config, pinned: request.base.pinned, boot_plugin: (&request.base.boot_plugin).try_into()?, @@ -81,7 +79,6 @@ pub struct SetupWholeDiskData<'data> { pub mount_point: Option<&'data str>, pub source: &'data str, - pub meta_filename: Option<&'data str>, pub empty_config: bool, pub pinned: bool, pub boot_plugin: Option<&'data str>, @@ -122,7 +119,6 @@ impl<'data> TryFrom<&'data CSetupWholeDiskRequest> for SetupWholeDiskData<'data> mount_point: (&request.base.mount_point).try_into()?, source: (&request.base.source).try_into()?, - meta_filename: (&request.base.meta_filename).try_into()?, empty_config: request.base.empty_config, pinned: request.base.pinned, boot_plugin: (&request.base.boot_plugin).try_into()?, diff --git a/lib/setup/src/error.rs b/lib/setup/src/error.rs index 3c3f5da1..2f9930c1 100644 --- a/lib/setup/src/error.rs +++ b/lib/setup/src/error.rs @@ -12,8 +12,6 @@ use gptman::linux::BlockError as GptBlockError; use nix::errno::Errno; -use toml::de::Error as TomlError; - use upac::boot::error::BootError; use upac::composefs::error::RepoError; use upac::database::error::{DatabaseError, DeployRecordError}; @@ -31,7 +29,6 @@ pub enum SetupError { Boot(BootError), BootPlugin(BootPluginError), Io(IoErrorKind), - MetaMalformed, NoSpaceLeft, NotBlockDevice, MkfsFailed, @@ -40,6 +37,7 @@ pub enum SetupError { InvalidPartitionLayout, InvalidFormatParams, RereadFailed(Errno), + ComposefsSetupRootUnitNotFound, Unexpected, } @@ -97,12 +95,6 @@ impl From for SetupError { } } -impl From for SetupError { - fn from(_: TomlError) -> Self { - SetupError::MetaMalformed - } -} - impl From for SetupError { fn from(error: GptError) -> Self { match error { diff --git a/lib/setup/src/genesis/database.rs b/lib/setup/src/genesis/database.rs deleted file mode 100644 index 98c7b69d..00000000 --- a/lib/setup/src/genesis/database.rs +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use upac::database::meta::MetaStoreMut; -use upac::database::{InMemory, MemoryDatabase}; -use upac::orchestrator::Context; -use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use upac_types::PackageMeta; - -use super::ctx_take; - -use crate::error::SetupError; -use crate::types::{GenesisDatabase, PackageUuid}; - -#[cfg(test)] -#[path = "../../tests/inline/database.rs"] -mod tests; - -pub struct CreateDatabaseStage; - -impl Stage for CreateDatabaseStage { - fn run( - &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, - ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { - let meta = ctx_take!(context, PackageMeta); - - let mut database = MemoryDatabase::new_in_memory()?; - let uuid = database.insert_package_meta(&meta)?; - - context.put(GenesisDatabase(database)); - context.put(PackageUuid(uuid)); - - Ok((progress, StageResult::Advance, Box::new(NoRollback))) - } -} diff --git a/lib/setup/src/genesis/entry.rs b/lib/setup/src/genesis/entry.rs index ebeb342c..1e7c8eb0 100644 --- a/lib/setup/src/genesis/entry.rs +++ b/lib/setup/src/genesis/entry.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::fs::{File, copy, create_dir_all}; +use std::fs::{File, copy, create_dir_all, write}; use std::io::Read; use composefs::erofs::reader::erofs_to_filesystem; @@ -12,7 +12,9 @@ use composefs::repository::Repository; use composefs::tree::FileSystem; use upac::boot::write_boot_entry; +use upac::composefs::file::FileHandle; use upac::composefs::repository::ObjectID; +use upac::layout::boot::{UPAC_UKI_FROM_SLOT, UPAC_UKI_TO_SLOT}; use upac::layout::boot_plugins::{BOOT_PLUGINS_DIR, MANIFEST_EXTENSION}; use upac::orchestrator::Context; use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; @@ -23,9 +25,9 @@ use upac_abi::hook::{CancelToken, ProgressEventBuilder}; use super::ctx_get; use crate::error::SetupError; -use crate::layout::genesis::{ESP_FALLBACK_LOADER, REFIND_SOURCE, SYSTEMD_BOOT_SOURCE}; +use crate::layout::genesis::{EFI_LINUX_DIR, ESP_FALLBACK_LOADER}; use crate::target::TargetSysroot; -use crate::types::{GenesisInput, PrefixDigest, ResolvedSourceDir}; +use crate::types::{GenesisInput, PrefixDigest}; pub struct StageBootStage; @@ -36,27 +38,31 @@ impl Stage for StageBootStage { let target = ctx_get!(context, TargetSysroot); let input = ctx_get!(context, GenesisInput); let prefix_digest = ctx_get!(context, PrefixDigest); - let resolved = ctx_get!(context, ResolvedSourceDir); let repository = target.repository(); let prefix_digest_hex = prefix_digest.0.to_hex(); - let candidate = match input.boot_plugin.as_deref() { - Some("systemd-boot") => Some(SYSTEMD_BOOT_SOURCE), - Some("refind") => Some(REFIND_SOURCE), - _ => None, - }; - - if let Some(candidate) = candidate { - let source = resolved.0.join(candidate); - let destination = target.esp_mount_point().join(ESP_FALLBACK_LOADER); - if let Some(parent) = destination.parent() { - create_dir_all(parent)?; + let prefix_tree = Self::reopen_tree(repository, &prefix_digest_hex)?; + + let plugin = resolve_boot_plugin(BOOT_PLUGINS_DIR, MANIFEST_EXTENSION, input.boot_plugin.as_deref())?; + + let esp_mount_point = target.esp_mount_point().to_string_lossy().into_owned(); + plugin.install(&esp_mount_point)?; + + if let Some(candidate) = plugin.esp_loader_source() { + let handle = FileHandle::new(candidate); + if handle.stat_in_tree(&prefix_tree).is_ok() { + let loader_bytes = handle.read_file(repository, &prefix_tree)?; + + let destination = target.esp_mount_point().join(ESP_FALLBACK_LOADER); + if let Some(parent) = destination.parent() { + create_dir_all(parent)?; + } + + write(&destination, &loader_bytes)?; } - copy(&source, &destination)?; } - let prefix_tree = Self::reopen_tree(repository, &prefix_digest_hex)?; let entry_name = write_boot_entry( repository, &prefix_tree, @@ -65,7 +71,40 @@ impl Stage for StageBootStage { &prefix_digest_hex, )?; - let plugin = resolve_boot_plugin(BOOT_PLUGINS_DIR, MANIFEST_EXTENSION, input.boot_plugin.as_deref())?; + // UKI-direct: genesis is the very first deploy, so `upac-from.efi` (the fallback slot) + // has nothing real to hold yet — seed it with the exact same image `write_boot_entry` just + // wrote to `upac-to.efi`. `up install`/`update` never do this: only genesis creates both + // files and both Boot#### entries; every later deploy only ever touches `upac-to.efi`. + if entry_name == UPAC_UKI_TO_SLOT { + let efi_linux = target.esp_mount_point().join(EFI_LINUX_DIR); + let to_path = efi_linux.join(format!("{UPAC_UKI_TO_SLOT}.efi")); + let from_path = efi_linux.join(format!("{UPAC_UKI_FROM_SLOT}.efi")); + copy(&to_path, &from_path)?; + + let geometry = ( + target.esp_partition_number(), + target.esp_starting_lba(), + target.esp_ending_lba(), + target.esp_unique_partition_guid(), + ); + + // Manual mode (pre-existing partitions) has nowhere to read GPT geometry back from — + // registering the two Boot#### entries is skipped there, same as the existing + // "pre-registered once, out of scope of this pipeline" assumption everywhere else. + if let (Some(partition_number), Some(starting_lba), Some(ending_lba), Some(unique_partition_guid)) = + geometry + { + plugin.register_boot_slots( + partition_number, + starting_lba, + ending_lba, + unique_partition_guid.to_bytes_le(), + UPAC_UKI_TO_SLOT, + UPAC_UKI_FROM_SLOT, + )?; + } + } + plugin.set_one_shot(&entry_name)?; Ok((progress, StageResult::Advance, Box::new(NoRollback))) diff --git a/lib/setup/src/genesis/enumerate.rs b/lib/setup/src/genesis/enumerate.rs new file mode 100644 index 00000000..16a2670b --- /dev/null +++ b/lib/setup/src/genesis/enumerate.rs @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::collections::VecDeque; +use std::fs::read_dir; + +use composefs::generic_tree::Stat; +use composefs::repository::ImportContext; +use composefs::tree::FileSystem; + +use tempfile::TempDir; + +use upac::database::{InMemory, MemoryDatabase}; +use upac::errors::CommonError; +use upac::orchestrator::Context; +use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; +use upac::plugin::decoder::unpack::PackageUnpacker; + +use upac_abi::hook::{CancelToken, ProgressEventBuilder}; + +use upac_types::TmpPath; + +use super::ctx_get; + +use crate::error::SetupError; +use crate::types::{ + ConfigTree, GenesisDatabase, PendingPackagePaths, PendingPackages, PrefixTree, ResolvedSourceDir, TotalPackages, + UnpackerState, +}; + +#[cfg(test)] +#[path = "../../tests/inline/enumerate.rs"] +mod tests; + +pub struct EnumeratePackagesStage; + +impl Stage for EnumeratePackagesStage { + fn run( + &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, + ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { + let resolved = ctx_get!(context, ResolvedSourceDir); + + let mut package_paths = Vec::new(); + for entry in read_dir(&resolved.0)? { + let entry = entry?; + + if entry.metadata()?.is_file() { + package_paths.push(entry.path().to_string_lossy().into_owned()); + } + } + + let total = package_paths.len() as u64; + + let unpacker = PackageUnpacker::new().map_err(CommonError::Decoder)?; + let scratch = TempDir::new()?; + let tmp_path = TmpPath(scratch.path().to_string_lossy().into_owned()); + let database = MemoryDatabase::new_in_memory()?; + + context.put(PendingPackagePaths(VecDeque::from(package_paths))); + context.put(TotalPackages(total)); + context.put(UnpackerState(unpacker)); + context.put(tmp_path); + context.put(scratch); + context.put(PendingPackages(VecDeque::new())); + context.put(GenesisDatabase(database)); + context.put(PrefixTree(FileSystem::new(Stat::uninitialized()))); + context.put(ConfigTree(FileSystem::new(Stat::uninitialized()))); + context.put(ImportContext::default()); + + Ok((progress, StageResult::Advance, Box::new(NoRollback))) + } +} diff --git a/lib/setup/src/genesis/files.rs b/lib/setup/src/genesis/files.rs deleted file mode 100644 index aac8d95b..00000000 --- a/lib/setup/src/genesis/files.rs +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use upac::database::files::FileStoreMut; -use upac::orchestrator::Context; -use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use upac_types::{FileEntry, FileEntryScope}; - -use super::{ctx_get, ctx_take}; - -use crate::error::SetupError; -use crate::types::{GenesisDatabase, ImportedConfigPaths, ImportedPrefixPaths, PackageUuid}; - -#[cfg(test)] -#[path = "../../tests/inline/file_entries.rs"] -mod tests; - -pub struct InsertFileEntryStage; - -impl Stage for InsertFileEntryStage { - fn run( - &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, - ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { - let mut prefix_paths = ctx_take!(context, ImportedPrefixPaths); - let mut config_paths = ctx_take!(context, ImportedConfigPaths); - let mut database = ctx_take!(context, GenesisDatabase); - - let uuid = ctx_get!(context, PackageUuid); - - let next = if let Some(path) = prefix_paths.0.pop() { - Some((FileEntryScope::Prefix, path)) - } else { - config_paths.0.pop().map(|path| (FileEntryScope::Config, path)) - }; - - if let Some((scope, path)) = next { - database.0.insert_package_file( - uuid.0, - &FileEntry { - path: path.to_string_lossy().into_owned(), - is_user: false, - scope, - }, - )?; - } - - let done = prefix_paths.0.is_empty() && config_paths.0.is_empty(); - - context.put(prefix_paths); - context.put(config_paths); - context.put(database); - - let result = if done { - StageResult::Advance - } else { - StageResult::Repeat - }; - - Ok((progress, result, Box::new(NoRollback))) - } -} diff --git a/lib/setup/src/genesis/import.rs b/lib/setup/src/genesis/import.rs new file mode 100644 index 00000000..bafb3491 --- /dev/null +++ b/lib/setup/src/genesis/import.rs @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::path::Path; + +use composefs::repository::ImportContext; + +use upac::database::files::FileStoreMut; +use upac::database::meta::MetaStoreMut; +use upac::database::triggers::TriggerStoreMut; +use upac::errors::CommonError; +use upac::orchestrator::Context; +use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; + +use upac_abi::hook::{CancelToken, ProgressEventBuilder}; + +use upac_types::{FileEntry, FileEntryScope}; + +use super::{ctx_get, ctx_take, import_if_dir}; + +use crate::error::SetupError; +use crate::target::TargetSysroot; +use crate::types::{ConfigTree, GenesisDatabase, GenesisInput, PendingPackages, PrefixTree, TotalPackages}; + +// No unit test: needs a real decoded package temp dir + a real composefs `Repository`, same +// untestable-in-isolation shape as `up install`'s own `ImportPackageStage` (lib/lib/src/mutated/installer). +pub struct ImportPackageStage; + +impl Stage for ImportPackageStage { + fn run( + &self, context: &mut Context, cancel: &CancelToken, mut progress: ProgressEventBuilder, + ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { + let mut pending_packages = ctx_take!(context, PendingPackages); + let mut prefix_tree = ctx_take!(context, PrefixTree); + let mut config_tree = ctx_take!(context, ConfigTree); + let mut database = ctx_take!(context, GenesisDatabase); + let mut import_ctx = ctx_take!(context, ImportContext); + + let target = ctx_get!(context, TargetSysroot); + let input = ctx_get!(context, GenesisInput); + let total = ctx_get!(context, TotalPackages); + + let repository = target.repository(); + + let (package, trigger) = pending_packages.0.pop_front().ok_or(CommonError::MissingResult)?; + + let source_root = Path::new(&package.temp_package_path); + + let prefix_source = source_root.join("usr"); + let imported = import_if_dir!(repository, &mut prefix_tree.0, &prefix_source, &mut import_ctx, cancel); + + let config_source = source_root.join("etc"); + let imported_config = if input.empty_config { + Vec::new() + } else { + import_if_dir!(repository, &mut config_tree.0, &config_source, &mut import_ctx, cancel) + }; + + let uuid = database.0.insert_package_meta(&package.meta)?; + database.0.set_declarative_triggers(uuid, &trigger)?; + + for path in imported { + database.0.insert_package_file( + uuid, + &FileEntry { + path: path.to_string_lossy().into_owned(), + is_user: false, + scope: FileEntryScope::Prefix, + }, + )?; + } + + for path in imported_config { + database.0.insert_package_file( + uuid, + &FileEntry { + path: path.to_string_lossy().into_owned(), + is_user: false, + scope: FileEntryScope::Config, + }, + )?; + } + + let remaining = pending_packages.0.len() as u64; + let processed = total.0 - remaining; + progress = progress.subject(package.meta.name.clone()).progress(processed, total.0); + + let result = if pending_packages.0.is_empty() { + StageResult::Advance + } else { + StageResult::Repeat + }; + + context.put(pending_packages); + context.put(prefix_tree); + context.put(config_tree); + context.put(database); + context.put(import_ctx); + + Ok((progress, result, Box::new(NoRollback))) + } +} diff --git a/lib/setup/src/genesis/meta.rs b/lib/setup/src/genesis/meta.rs deleted file mode 100644 index 9c8c81a6..00000000 --- a/lib/setup/src/genesis/meta.rs +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use upac::orchestrator::Context; -use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use super::ctx_get; - -use crate::error::SetupError; -use crate::meta::SourceDir; -use crate::types::{GenesisInput, ResolvedSourceDir}; - -#[cfg(test)] -#[path = "../../tests/inline/meta.rs"] -mod tests; - -pub struct ReadMetaStage; - -impl Stage for ReadMetaStage { - fn run( - &self, context: &mut Context, _cancel: &CancelToken, progress: ProgressEventBuilder, - ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { - let input = ctx_get!(context, GenesisInput); - let resolved = ctx_get!(context, ResolvedSourceDir); - - let source = SourceDir { path: &resolved.0 }; - - let mut meta = source.read(input.meta_filename.as_deref())?; - let (sha256, installed_size) = source.checksum(!input.empty_config)?; - meta.sha256 = sha256; - meta.installed_size = installed_size; - - context.put(meta); - - Ok((progress, StageResult::Advance, Box::new(NoRollback))) - } -} diff --git a/lib/setup/src/genesis/mod.rs b/lib/setup/src/genesis/mod.rs index 8691f413..991c3036 100644 --- a/lib/setup/src/genesis/mod.rs +++ b/lib/setup/src/genesis/mod.rs @@ -13,28 +13,28 @@ use upac_abi::hook::{Message, MessageHook}; use upac_macro::{FromStageIndex, StageKey}; -use self::database::CreateDatabaseStage; use self::deploy::WriteDeployRecordStage; use self::embed::EmbedDatabaseStage; use self::entry::StageBootStage; -use self::files::InsertFileEntryStage; -use self::meta::ReadMetaStage; +use self::enumerate::EnumeratePackagesStage; +use self::import::ImportPackageStage; use self::source::PrepareSourceStage; -use self::trees::ImportTreesStage; +use self::system::ImportSystemStage; +use self::unpack::UnpackPackageStage; use crate::data::{SetupExistingData, SetupWholeDiskData}; use crate::error::SetupError; use crate::target::TargetSysroot; use crate::types::GenesisInput; -mod database; mod deploy; mod embed; mod entry; -mod files; -mod meta; +mod enumerate; +mod import; mod source; -mod trees; +mod system; +mod unpack; macro_rules! ctx_get { ($context:expr, $ty:ty) => { @@ -50,14 +50,32 @@ macro_rules! ctx_take { } pub(crate) use ctx_take; +macro_rules! import_if_dir { + ($repository:expr, $tree:expr, $source:expr, $import_ctx:expr, $cancel:expr) => { + if $source.is_dir() { + upac::composefs::file::FileHandle::new(::std::path::PathBuf::new()).import_directory( + $repository, + $tree, + $source, + $import_ctx, + $cancel, + &mut |_| {}, + )? + } else { + Vec::new() + } + }; +} +pub(crate) use import_if_dir; + #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq, FromStageIndex, StageKey)] pub enum GenesisStage { PrepareSource = 0, - ReadMeta = 1, - ImportTrees = 2, - CreateDatabase = 3, - InsertFileEntry = 4, + EnumeratePackages = 1, + UnpackPackage = 2, + ImportPackage = 3, + ImportSystem = 4, EmbedDatabase = 5, WriteDeployRecord = 6, StageBoot = 7, @@ -72,6 +90,10 @@ impl SetupExistingData<'_> { Path::new(self.esp_device), PathBuf::from(self.mount_point()), &self.extra_mounts, + None, + None, + None, + None, ) .map_err(|error| (GenesisStage::Setup, error))?; @@ -82,10 +104,10 @@ impl SetupExistingData<'_> { let orchestrator = SequentialOrchestrator::new(vec![ Box::new(PrepareSourceStage), - Box::new(ReadMetaStage), - Box::new(ImportTreesStage), - Box::new(CreateDatabaseStage), - Box::new(InsertFileEntryStage), + Box::new(EnumeratePackagesStage), + Box::new(UnpackPackageStage), + Box::new(ImportPackageStage), + Box::new(ImportSystemStage), Box::new(EmbedDatabaseStage), Box::new(WriteDeployRecordStage), Box::new(StageBootStage), @@ -119,10 +141,10 @@ impl SetupWholeDiskData<'_> { let orchestrator = SequentialOrchestrator::new(vec![ Box::new(PrepareSourceStage), - Box::new(ReadMetaStage), - Box::new(ImportTreesStage), - Box::new(CreateDatabaseStage), - Box::new(InsertFileEntryStage), + Box::new(EnumeratePackagesStage), + Box::new(UnpackPackageStage), + Box::new(ImportPackageStage), + Box::new(ImportSystemStage), Box::new(EmbedDatabaseStage), Box::new(WriteDeployRecordStage), Box::new(StageBootStage), diff --git a/lib/setup/src/genesis/system.rs b/lib/setup/src/genesis/system.rs new file mode 100644 index 00000000..1615e08e --- /dev/null +++ b/lib/setup/src/genesis/system.rs @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::path::Path; + +use composefs::generic_tree::Stat; +use composefs::repository::ImportContext; +use composefs::tree::FileSystem; + +use upac::composefs::file::FileHandle; +use upac::composefs::repository::ObjectID; +use upac::orchestrator::Context; +use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; + +use upac_abi::hook::{CancelToken, ProgressEventBuilder}; + +use super::{ctx_get, ctx_take, import_if_dir}; + +use crate::error::SetupError; +use crate::layout::genesis::{ + COMPOSEFS_SETUP_ROOT_UNIT_PATH, COMPOSEFS_SETUP_ROOT_WANTS_PATH, COMPOSEFS_SETUP_ROOT_WANTS_TARGET, SYSTEM_DIR, +}; +use crate::target::TargetSysroot; +use crate::types::{PrefixTree, ResolvedSourceDir}; + +// No unit test: needs a real filesystem tree to import + a real composefs `Repository`, same +// untestable-in-isolation shape as `ImportPackageStage`. +pub struct ImportSystemStage; + +impl Stage for ImportSystemStage { + fn run( + &self, context: &mut Context, cancel: &CancelToken, progress: ProgressEventBuilder, + ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { + let mut prefix_tree = ctx_take!(context, PrefixTree); + let mut import_ctx = ctx_take!(context, ImportContext); + + let resolved = ctx_get!(context, ResolvedSourceDir); + let target = ctx_get!(context, TargetSysroot); + + let repository = target.repository(); + + let system_dir = resolved.0.join(SYSTEM_DIR); + let unit_source = system_dir.join(COMPOSEFS_SETUP_ROOT_UNIT_PATH); + if !unit_source.is_file() { + return Err(SetupError::ComposefsSetupRootUnitNotFound); + } + + import_if_dir!(repository, &mut prefix_tree.0, &system_dir, &mut import_ctx, cancel); + + ensure_ancestor_dirs(COMPOSEFS_SETUP_ROOT_WANTS_PATH, &mut prefix_tree.0)?; + FileHandle::new(COMPOSEFS_SETUP_ROOT_WANTS_PATH).symlink_in_tree( + &mut prefix_tree.0, + COMPOSEFS_SETUP_ROOT_WANTS_TARGET, + Stat::uninitialized(), + )?; + + context.put(prefix_tree); + context.put(import_ctx); + + Ok((progress, StageResult::Advance, Box::new(NoRollback))) + } +} + +fn ensure_ancestor_dirs(path: &str, tree: &mut FileSystem) -> Result<(), SetupError> { + let mut ancestors: Vec<&Path> = Path::new(path) + .ancestors() + .skip(1) + .filter(|ancestor| !ancestor.as_os_str().is_empty()) + .collect(); + ancestors.reverse(); + + for ancestor in ancestors { + let handle = FileHandle::new(ancestor); + if handle.stat_in_tree(tree).is_err() { + handle.insert_in_tree(tree, Stat::uninitialized())?; + } + } + + Ok(()) +} diff --git a/lib/setup/src/genesis/trees.rs b/lib/setup/src/genesis/trees.rs deleted file mode 100644 index 7da31900..00000000 --- a/lib/setup/src/genesis/trees.rs +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs::read_dir; -use std::io::Result as IoResult; -use std::path::{Path, PathBuf}; - -use composefs::generic_tree::Stat; -use composefs::repository::ImportContext; -use composefs::tree::FileSystem; - -use upac::composefs::file::FileHandle; -use upac::orchestrator::Context; -use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use super::ctx_get; - -use crate::error::SetupError; -use crate::target::TargetSysroot; -use crate::types::{ConfigTree, GenesisInput, ImportedConfigPaths, ImportedPrefixPaths, PrefixTree, ResolvedSourceDir}; - -#[cfg(test)] -#[path = "../../tests/inline/trees.rs"] -mod tests; - -macro_rules! import_with_progress { - ($repository:expr, $tree:expr, $source:expr, $import_ctx:expr, $cancel:expr, $context:expr, $stage:expr) => {{ - let total = ImportTreesStage::count_leaf_entries($source).unwrap_or(0); - let mut current = 0u64; - - FileHandle::new(PathBuf::new()).import_directory( - $repository, - $tree, - $source, - $import_ctx, - $cancel, - &mut |path| { - current += 1; - $context.send_progress( - &ProgressEventBuilder::new($stage) - .subject(path.display().to_string()) - .progress(current, total), - ); - }, - )? - }}; -} - -pub struct ImportTreesStage; - -impl Stage for ImportTreesStage { - fn run( - &self, context: &mut Context, cancel: &CancelToken, progress: ProgressEventBuilder, - ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { - let target = ctx_get!(context, TargetSysroot); - let input = ctx_get!(context, GenesisInput); - let resolved = ctx_get!(context, ResolvedSourceDir); - - let repository = target.repository(); - let mut import_ctx = ImportContext::default(); - let stage = progress.stage(); - - let mut prefix_tree = FileSystem::new(Stat::uninitialized()); - let prefix_source = resolved.0.join("usr"); - let imported = if prefix_source.is_dir() { - import_with_progress!( - repository, - &mut prefix_tree, - &prefix_source, - &mut import_ctx, - cancel, - context, - stage - ) - } else { - Vec::new() - }; - - let mut config_tree = FileSystem::new(Stat::uninitialized()); - let config_source = resolved.0.join("etc"); - let imported_config = if !input.empty_config && config_source.is_dir() { - import_with_progress!( - repository, - &mut config_tree, - &config_source, - &mut import_ctx, - cancel, - context, - stage - ) - } else { - Vec::new() - }; - - context.put(PrefixTree(prefix_tree)); - context.put(ConfigTree(config_tree)); - context.put(ImportedPrefixPaths(imported)); - context.put(ImportedConfigPaths(imported_config)); - context.put(import_ctx); - - Ok((progress, StageResult::Advance, Box::new(NoRollback))) - } -} - -impl ImportTreesStage { - fn count_leaf_entries(dir: &Path) -> IoResult { - let mut count = 0; - - for entry in read_dir(dir)? { - let entry = entry?; - - if entry.metadata()?.is_dir() { - count += Self::count_leaf_entries(&entry.path())?; - } else { - count += 1; - } - } - - Ok(count) - } -} diff --git a/lib/setup/src/genesis/unpack.rs b/lib/setup/src/genesis/unpack.rs new file mode 100644 index 00000000..eda699b5 --- /dev/null +++ b/lib/setup/src/genesis/unpack.rs @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac::errors::CommonError; +use upac::orchestrator::Context; +use upac::orchestrator::stage::{NoRollback, RollbackGuard, Stage, StageResult}; + +use upac_abi::hook::{CancelToken, ProgressEventBuilder}; + +use upac_types::TmpPath; + +use super::{ctx_get, ctx_take}; + +use crate::error::SetupError; +use crate::types::{PendingPackagePaths, PendingPackages, TotalPackages, UnpackerState}; + +// No unit test: needs a real decoder + a real package archive to unpack, same untestable-in- +// isolation shape as `up install`'s own `PreparationStage` (lib/lib/src/mutated/installer). +pub struct UnpackPackageStage; + +impl Stage for UnpackPackageStage { + fn run( + &self, context: &mut Context, cancel: &CancelToken, mut progress: ProgressEventBuilder, + ) -> Result<(ProgressEventBuilder, StageResult, Box), SetupError> { + let mut pending_paths = ctx_take!(context, PendingPackagePaths); + let mut unpacker = ctx_take!(context, UnpackerState); + let mut pending_packages = ctx_take!(context, PendingPackages); + + let tmp_path = ctx_get!(context, TmpPath); + let total = ctx_get!(context, TotalPackages); + + let package_path = pending_paths.0.pop_front().ok_or(CommonError::MissingResult)?; + let index = pending_packages.0.len(); + + let (package, trigger) = unpacker + .0 + .unpack_one(&package_path, index, tmp_path.as_ref(), cancel) + .map_err(CommonError::Decoder)?; + + pending_packages.0.push_back((package, trigger)); + + let remaining = pending_paths.0.len() as u64; + let processed = total.0 - remaining; + progress = progress.subject(package_path).progress(processed, total.0); + + let result = if pending_paths.0.is_empty() { + StageResult::Advance + } else { + StageResult::Repeat + }; + + context.put(pending_paths); + context.put(unpacker); + context.put(pending_packages); + + Ok((progress, result, Box::new(NoRollback))) + } +} diff --git a/lib/setup/src/lib.rs b/lib/setup/src/lib.rs index ad3926b7..914201ed 100644 --- a/lib/setup/src/lib.rs +++ b/lib/setup/src/lib.rs @@ -10,7 +10,6 @@ pub mod genesis; pub mod layout { include!(concat!(env!("OUT_DIR"), "/layout.rs")); } -pub mod meta; pub mod partition; pub mod target; mod types; diff --git a/lib/setup/src/meta.rs b/lib/setup/src/meta.rs deleted file mode 100644 index 7d850bf6..00000000 --- a/lib/setup/src/meta.rs +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs::{File, read_dir, read_link, read_to_string}; -use std::io::Read; -use std::path::Path; - -use sha2::{Digest, Sha256}; - -use toml::from_str; - -use upac_types::PackageMeta; - -use crate::error::SetupError; -use crate::layout::meta::FILENAME; - -pub struct SourceDir<'src> { - pub path: &'src Path, -} - -impl SourceDir<'_> { - pub fn read(&self, filename: Option<&str>) -> Result { - let content = read_to_string(self.path.join(filename.unwrap_or(FILENAME)))?; - - Ok(from_str(&content)?) - } - - pub fn checksum(&self, include_config: bool) -> Result<([u8; 32], u64), SetupError> { - let mut accumulator = Accumulator { - hasher: Sha256::new(), - installed_size: 0, - }; - - let sections: &[&str] = if include_config { &["usr", "etc"] } else { &["usr"] }; - - for §ion in sections { - let section_dir = self.path.join(section); - if section_dir.is_dir() { - accumulator.hasher.update(section.as_bytes()); - accumulator.hash_dir(§ion_dir)?; - } - } - - Ok((accumulator.hasher.finalize().into(), accumulator.installed_size)) - } -} - -struct Accumulator { - hasher: Sha256, - installed_size: u64, -} - -impl Accumulator { - fn hash_dir(&mut self, dir: &Path) -> Result<(), SetupError> { - let mut entries = read_dir(dir)?.collect::, _>>()?; - entries.sort_by_key(|entry| entry.file_name()); - - for entry in entries { - let metadata = entry.metadata()?; - self.hasher.update(entry.file_name().as_encoded_bytes()); - - if metadata.is_dir() { - self.hash_dir(&entry.path())?; - } else if metadata.is_symlink() { - self.hasher - .update(read_link(entry.path())?.as_os_str().as_encoded_bytes()); - } else { - let mut file = File::open(entry.path())?; - let mut buffer = [0u8; 65536]; - - loop { - let bytes_read = file.read(&mut buffer)?; - if bytes_read == 0 { - break; - } - self.hasher.update(&buffer[..bytes_read]); - } - - self.installed_size += metadata.len(); - } - } - - Ok(()) - } -} diff --git a/lib/setup/src/partition.rs b/lib/setup/src/partition.rs index 38200392..bf406ec4 100644 --- a/lib/setup/src/partition.rs +++ b/lib/setup/src/partition.rs @@ -17,7 +17,7 @@ use upac_types::PartitionSpec; use crate::error::SetupError; use crate::format::FormatTarget; -use crate::layout::partition::{SETTLE_ATTEMPTS, SETTLE_INTERVAL_MS}; +use crate::layout::partition::{DEPLOY_LABEL, ESP_LABEL, SETTLE_ATTEMPTS, SETTLE_INTERVAL_MS}; #[cfg(test)] #[path = "../tests/inline/partition.rs"] @@ -47,11 +47,11 @@ impl GptTable { fn insert_partition( &mut self, number: u32, partition_type: Uuid, name: &str, size_sectors: u64, - ) -> Result<(), SetupError> { + ) -> Result { let starting_lba = self.0.find_first_place(size_sectors).ok_or(SetupError::NoSpaceLeft)?; let ending_lba = starting_lba + size_sectors - 1; - self.0[number] = GPTPartitionEntry { + let entry = GPTPartitionEntry { partition_type_guid: partition_type.to_bytes_le(), unique_partition_guid: Uuid::new_v4().to_bytes_le(), starting_lba, @@ -60,7 +60,9 @@ impl GptTable { partition_name: name.into(), }; - Ok(()) + self.0[number] = entry.clone(); + + Ok(entry) } fn write_into(&mut self, device: &mut File) -> Result<(), SetupError> { @@ -73,6 +75,9 @@ impl GptTable { pub struct DiskLayout { device_path: PathBuf, esp_partition: u32, + esp_starting_lba: u64, + esp_ending_lba: u64, + esp_unique_partition_guid: Uuid, deploy_partition: u32, extra_partitions: Vec, } @@ -98,10 +103,10 @@ impl DiskLayout { let mut next_number = 1; let esp_partition = next_number; - gpt.insert_partition( + let esp_entry = gpt.insert_partition( esp_partition, ESP_PARTITION_TYPE_GUID, - "ESP", + ESP_LABEL, mib_to_sectors!(esp_size_mib, sector_size), )?; next_number += 1; @@ -110,7 +115,7 @@ impl DiskLayout { gpt.insert_partition( deploy_partition, LINUX_ROOT_X86_64_GUID, - "upac-deploy", + DEPLOY_LABEL, mib_to_sectors!(deploy_size_mib, sector_size), )?; next_number += 1; @@ -134,6 +139,9 @@ impl DiskLayout { let layout = DiskLayout { device_path: device_path.to_owned(), esp_partition, + esp_starting_lba: esp_entry.starting_lba, + esp_ending_lba: esp_entry.ending_lba, + esp_unique_partition_guid: Uuid::from_bytes_le(esp_entry.unique_partition_guid), deploy_partition, extra_partitions: extras, }; @@ -169,6 +177,22 @@ impl DiskLayout { self.partition_path(self.esp_partition) } + pub fn esp_partition_number(&self) -> u32 { + self.esp_partition + } + + pub fn esp_starting_lba(&self) -> u64 { + self.esp_starting_lba + } + + pub fn esp_ending_lba(&self) -> u64 { + self.esp_ending_lba + } + + pub fn esp_unique_partition_guid(&self) -> Uuid { + self.esp_unique_partition_guid + } + pub fn deploy_path(&self) -> PathBuf { self.partition_path(self.deploy_partition) } diff --git a/lib/setup/src/target.rs b/lib/setup/src/target.rs index ca1510b6..f10e526f 100644 --- a/lib/setup/src/target.rs +++ b/lib/setup/src/target.rs @@ -17,11 +17,14 @@ use upac::layout::deployment::{DEPLOYS_DIR, NEXT_SEQ_PATH, REPO_DIR}; use upac_abi::FsKind; +use uuid::Uuid; + use upac_types::PartitionMount; use crate::data::SetupWholeDiskData; use crate::error::SetupError; use crate::format::FormatTarget; +use crate::layout::partition::{DEPLOY_LABEL, ESP_LABEL}; use crate::partition::DiskLayout; pub struct TargetSysroot { @@ -29,12 +32,21 @@ pub struct TargetSysroot { deploy_dir: PathBuf, repository: ManuallyDrop>, mounted: Vec, + esp_partition_number: Option, + esp_starting_lba: Option, + esp_ending_lba: Option, + esp_unique_partition_guid: Option, } impl TargetSysroot { + #[allow( + clippy::too_many_arguments, + reason = "flat ESP-geometry params by design, not grouped into a struct — see partition::DiskLayout" + )] pub fn new( deploy_device: &Path, deploy_fs: FsKind, esp_device: &Path, mount_point: PathBuf, - extra_mounts: &[PartitionMount], + extra_mounts: &[PartitionMount], esp_partition_number: Option, esp_starting_lba: Option, + esp_ending_lba: Option, esp_unique_partition_guid: Option, ) -> Result { create_dir_all(&mount_point)?; @@ -84,6 +96,10 @@ impl TargetSysroot { deploy_dir, repository: ManuallyDrop::new(repository), mounted, + esp_partition_number, + esp_starting_lba, + esp_ending_lba, + esp_unique_partition_guid, }) } @@ -97,16 +113,17 @@ impl TargetSysroot { )?; let esp_path = layout.esp_path(); + FormatTarget { device_path: &esp_path, - label: Some("ESP"), + label: Some(ESP_LABEL), } .format_esp()?; let deploy_path = layout.deploy_path(); FormatTarget { device_path: &deploy_path, - label: Some("upac-deploy"), + label: Some(DEPLOY_LABEL), } .format(data.deploy_fs, data.node_size, data.sector_size, data.force_wipe)?; @@ -133,6 +150,10 @@ impl TargetSysroot { &esp_path, PathBuf::from(data.mount_point()), &extra_mounts, + Some(layout.esp_partition_number()), + Some(layout.esp_starting_lba()), + Some(layout.esp_ending_lba()), + Some(layout.esp_unique_partition_guid()), ) } @@ -151,27 +172,21 @@ impl TargetSysroot { pub fn esp_mount_point(&self) -> PathBuf { self.mount_point.join(ESP_MOUNT_PRIMARY.trim_start_matches('/')) } -} -#[cfg(test)] -impl TargetSysroot { - /// Builds a `TargetSysroot` over a plain directory — no `mount()`, no root required. Only - /// `deploy_dir`/`next_seq_path`/`repository` are meaningful on the result; `Drop` has nothing - /// to unmount since `mounted` stays empty. - pub(crate) fn for_testing(mount_point: PathBuf) -> Result { - create_dir_all(&mount_point)?; + pub fn esp_partition_number(&self) -> Option { + self.esp_partition_number + } - let deploy_dir = mount_point.join(DEPLOYS_DIR); - create_dir_all(&deploy_dir)?; + pub fn esp_starting_lba(&self) -> Option { + self.esp_starting_lba + } - let (repository, _freshly_initialized) = repository::init(&mount_point.join(REPO_DIR))?; + pub fn esp_ending_lba(&self) -> Option { + self.esp_ending_lba + } - Ok(Self { - mount_point, - deploy_dir, - repository: ManuallyDrop::new(repository), - mounted: Vec::new(), - }) + pub fn esp_unique_partition_guid(&self) -> Option { + self.esp_unique_partition_guid } } @@ -192,3 +207,26 @@ impl Drop for TargetSysroot { let _ = umount(base); } } + +#[cfg(test)] +impl TargetSysroot { + pub(crate) fn for_testing(mount_point: PathBuf) -> Result { + create_dir_all(&mount_point)?; + + let deploy_dir = mount_point.join(DEPLOYS_DIR); + create_dir_all(&deploy_dir)?; + + let (repository, _freshly_initialized) = repository::init_insecure(&mount_point.join(REPO_DIR))?; + + Ok(Self { + mount_point, + deploy_dir, + repository: ManuallyDrop::new(repository), + mounted: Vec::new(), + esp_partition_number: None, + esp_starting_lba: None, + esp_ending_lba: None, + esp_unique_partition_guid: None, + }) + } +} diff --git a/lib/setup/src/types.rs b/lib/setup/src/types.rs index 6a04cde3..60adac8a 100644 --- a/lib/setup/src/types.rs +++ b/lib/setup/src/types.rs @@ -3,20 +3,21 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception +use std::collections::VecDeque; use std::path::PathBuf; use composefs::tree::FileSystem; -use uuid::Uuid; - use upac::composefs::repository::ObjectID; use upac::database::MemoryDatabase; +use upac::plugin::decoder::unpack::PackageUnpacker; + +use upac_types::{DeclarativeTrigger, PackageTemp}; use crate::data::{SetupExistingData, SetupWholeDiskData}; pub(crate) struct GenesisInput { pub source: String, - pub meta_filename: Option, pub empty_config: bool, pub pinned: bool, pub boot_plugin: Option, @@ -24,17 +25,19 @@ pub(crate) struct GenesisInput { pub(crate) struct ResolvedSourceDir(pub PathBuf); -pub(crate) struct PrefixTree(pub FileSystem); +pub(crate) struct PendingPackagePaths(pub VecDeque); -pub(crate) struct ConfigTree(pub FileSystem); +pub(crate) struct TotalPackages(pub u64); -pub(crate) struct ImportedPrefixPaths(pub Vec); +pub(crate) struct UnpackerState(pub PackageUnpacker); -pub(crate) struct ImportedConfigPaths(pub Vec); +pub(crate) struct PendingPackages(pub VecDeque<(PackageTemp, DeclarativeTrigger)>); -pub(crate) struct GenesisDatabase(pub MemoryDatabase); +pub(crate) struct PrefixTree(pub FileSystem); + +pub(crate) struct ConfigTree(pub FileSystem); -pub(crate) struct PackageUuid(pub Uuid); +pub(crate) struct GenesisDatabase(pub MemoryDatabase); pub(crate) struct PrefixDigest(pub ObjectID); @@ -44,7 +47,6 @@ impl From<&SetupExistingData<'_>> for GenesisInput { fn from(data: &SetupExistingData<'_>) -> Self { GenesisInput { source: data.source.to_owned(), - meta_filename: data.meta_filename.map(str::to_owned), empty_config: data.empty_config, pinned: data.pinned, boot_plugin: data.boot_plugin.map(str::to_owned), @@ -56,7 +58,6 @@ impl From<&SetupWholeDiskData<'_>> for GenesisInput { fn from(data: &SetupWholeDiskData<'_>) -> Self { GenesisInput { source: data.source.to_owned(), - meta_filename: data.meta_filename.map(str::to_owned), empty_config: data.empty_config, pinned: data.pinned, boot_plugin: data.boot_plugin.map(str::to_owned), diff --git a/lib/setup/tests/data.rs b/lib/setup/tests/data.rs index d8fd8203..7b55ea88 100644 --- a/lib/setup/tests/data.rs +++ b/lib/setup/tests/data.rs @@ -19,7 +19,6 @@ fn existing_data<'data>(cancel_token: &'data CancelToken, mount_point: Option<&' mount_point, source: "/mnt/source", - meta_filename: None, empty_config: false, pinned: false, boot_plugin: None, @@ -47,7 +46,6 @@ fn whole_disk_data<'data>( mount_point, source: "/mnt/source", - meta_filename: None, empty_config: false, pinned: false, boot_plugin: None, diff --git a/lib/setup/tests/inline/database.rs b/lib/setup/tests/inline/database.rs deleted file mode 100644 index 07931780..00000000 --- a/lib/setup/tests/inline/database.rs +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use upac::database::meta::MetaStore; -use upac::orchestrator::Context; -use upac::orchestrator::stage::{Stage, StageResult}; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use upac_types::PackageMeta; - -use crate::types::{GenesisDatabase, PackageUuid}; - -use super::CreateDatabaseStage; - -#[test] -fn run_inserts_package_meta_and_puts_database_and_uuid() { - let mut context = Context::new(); - context.put(PackageMeta { - name: "test-pkg".to_owned(), - ..PackageMeta::default() - }); - - let cancel = CancelToken::new(); - let progress = ProgressEventBuilder::new(0); - - let (_, result, _) = CreateDatabaseStage.run(&mut context, &cancel, progress).unwrap(); - - assert!(matches!(result, StageResult::Advance)); - - let database = context - .take::() - .expect("stage should put GenesisDatabase"); - let uuid = context.get::().expect("stage should put PackageUuid"); - - let meta = database - .0 - .get_package_meta(uuid.0) - .unwrap() - .expect("meta should be retrievable by the uuid the stage produced"); - assert_eq!(meta.name, "test-pkg"); -} - -#[test] -fn run_fails_when_package_meta_missing_from_context() { - let mut context = Context::new(); - let cancel = CancelToken::new(); - let progress = ProgressEventBuilder::new(0); - - let result = CreateDatabaseStage.run(&mut context, &cancel, progress); - - assert!(result.is_err()); -} diff --git a/lib/setup/tests/inline/deploy.rs b/lib/setup/tests/inline/deploy.rs index 3c473a8f..391e6d42 100644 --- a/lib/setup/tests/inline/deploy.rs +++ b/lib/setup/tests/inline/deploy.rs @@ -22,7 +22,6 @@ use super::WriteDeployRecordStage; fn genesis_input(pinned: bool) -> GenesisInput { GenesisInput { source: String::new(), - meta_filename: None, empty_config: false, pinned, boot_plugin: None, diff --git a/lib/setup/tests/inline/enumerate.rs b/lib/setup/tests/inline/enumerate.rs new file mode 100644 index 00000000..779d039c --- /dev/null +++ b/lib/setup/tests/inline/enumerate.rs @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::fs::{create_dir_all, write}; + +use tempfile::TempDir; + +use upac::orchestrator::Context; +use upac::orchestrator::stage::{Stage, StageResult}; + +use upac_abi::hook::{CancelToken, ProgressEventBuilder}; + +use upac_types::TmpPath; + +use crate::types::{ + ConfigTree, GenesisDatabase, PendingPackagePaths, PendingPackages, PrefixTree, ResolvedSourceDir, TotalPackages, + UnpackerState, +}; + +use super::EnumeratePackagesStage; + +#[test] +fn run_lists_only_files_and_initializes_pipeline_state() { + let source = TempDir::new().unwrap(); + write(source.path().join("a.pkg.tar.zst"), b"a").unwrap(); + write(source.path().join("b.pkg.tar.zst"), b"b").unwrap(); + create_dir_all(source.path().join("not-a-package")).unwrap(); + + let mut context = Context::new(); + context.put(ResolvedSourceDir(source.path().to_path_buf())); + + let cancel = CancelToken::new(); + let progress = ProgressEventBuilder::new(0); + + let (_, result, _guard) = EnumeratePackagesStage.run(&mut context, &cancel, progress).unwrap(); + + assert!(matches!(result, StageResult::Advance)); + + let total = context.get::().unwrap(); + assert_eq!(total.0, 2); + + let pending = context.get::().unwrap(); + assert_eq!(pending.0.len(), 2); + + assert!(context.get::().is_some()); + assert!(context.get::().is_some()); + assert!(context.get::().unwrap().0.is_empty()); + assert!(context.get::().is_some()); + assert!(context.get::().is_some()); + assert!(context.get::().is_some()); +} + +#[test] +fn run_with_empty_directory_sets_total_to_zero() { + let source = TempDir::new().unwrap(); + + let mut context = Context::new(); + context.put(ResolvedSourceDir(source.path().to_path_buf())); + + let cancel = CancelToken::new(); + let progress = ProgressEventBuilder::new(0); + + EnumeratePackagesStage.run(&mut context, &cancel, progress).unwrap(); + + let total = context.get::().unwrap(); + assert_eq!(total.0, 0); + + let pending = context.get::().unwrap(); + assert!(pending.0.is_empty()); +} + +#[test] +fn run_fails_when_source_dir_missing_from_context() { + let mut context = Context::new(); + + let cancel = CancelToken::new(); + let progress = ProgressEventBuilder::new(0); + + let result = EnumeratePackagesStage.run(&mut context, &cancel, progress); + + assert!(result.is_err()); +} diff --git a/lib/setup/tests/inline/file_entries.rs b/lib/setup/tests/inline/file_entries.rs deleted file mode 100644 index cc8b91a4..00000000 --- a/lib/setup/tests/inline/file_entries.rs +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::path::PathBuf; - -use upac::database::files::FileStore; -use upac::database::meta::MetaStoreMut; -use upac::database::{InMemory, MemoryDatabase}; -use upac::orchestrator::Context; -use upac::orchestrator::stage::{Stage, StageResult}; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use upac_types::{FileEntryScope, PackageMeta}; - -use crate::types::{GenesisDatabase, ImportedConfigPaths, ImportedPrefixPaths, PackageUuid}; - -use super::InsertFileEntryStage; - -fn context_with(prefix_paths: Vec, config_paths: Vec) -> Context { - let mut database = MemoryDatabase::new_in_memory().unwrap(); - let uuid = database - .insert_package_meta(&PackageMeta { - name: "test-pkg".to_owned(), - ..PackageMeta::default() - }) - .unwrap(); - - let mut context = Context::new(); - context.put(ImportedPrefixPaths(prefix_paths)); - context.put(ImportedConfigPaths(config_paths)); - context.put(GenesisDatabase(database)); - context.put(PackageUuid(uuid)); - context -} - -#[test] -fn run_inserts_one_entry_per_call_and_repeats_until_all_paths_are_recorded() { - let mut context = context_with( - vec![PathBuf::from("usr/bin/a"), PathBuf::from("usr/bin/b")], - vec![PathBuf::from("etc/conf")], - ); - let cancel = CancelToken::new(); - - let (_, first, _) = InsertFileEntryStage - .run(&mut context, &cancel, ProgressEventBuilder::new(0)) - .unwrap(); - assert!(matches!(first, StageResult::Repeat)); - - let (_, second, _) = InsertFileEntryStage - .run(&mut context, &cancel, ProgressEventBuilder::new(0)) - .unwrap(); - assert!(matches!(second, StageResult::Repeat)); - - let (_, third, _) = InsertFileEntryStage - .run(&mut context, &cancel, ProgressEventBuilder::new(0)) - .unwrap(); - assert!(matches!(third, StageResult::Advance)); - - let uuid = context.get::().unwrap(); - let database = context.get::().unwrap(); - let mut files = database.0.list_package_files(uuid.0).unwrap(); - files.sort_by(|a, b| a.path.cmp(&b.path)); - - assert_eq!(files.len(), 3); - assert_eq!(files[0].path, "etc/conf"); - assert_eq!(files[0].scope, FileEntryScope::Config); - assert_eq!(files[1].path, "usr/bin/a"); - assert_eq!(files[1].scope, FileEntryScope::Prefix); - assert_eq!(files[2].path, "usr/bin/b"); - assert_eq!(files[2].scope, FileEntryScope::Prefix); -} - -#[test] -fn run_advances_immediately_when_both_queues_are_empty() { - let mut context = context_with(Vec::new(), Vec::new()); - let cancel = CancelToken::new(); - - let (_, result, _) = InsertFileEntryStage - .run(&mut context, &cancel, ProgressEventBuilder::new(0)) - .unwrap(); - - assert!(matches!(result, StageResult::Advance)); - - let uuid = context.get::().unwrap(); - let database = context.get::().unwrap(); - assert!(database.0.list_package_files(uuid.0).unwrap().is_empty()); -} diff --git a/lib/setup/tests/inline/meta.rs b/lib/setup/tests/inline/meta.rs deleted file mode 100644 index 1ac2341a..00000000 --- a/lib/setup/tests/inline/meta.rs +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs::{create_dir_all, write}; - -use tempfile::TempDir; - -use upac::orchestrator::Context; -use upac::orchestrator::stage::{Stage, StageResult}; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use upac_types::PackageMeta; - -use crate::types::{GenesisInput, ResolvedSourceDir}; - -use super::ReadMetaStage; - -fn genesis_input(empty_config: bool) -> GenesisInput { - GenesisInput { - source: String::new(), - meta_filename: None, - empty_config, - pinned: false, - boot_plugin: None, - } -} - -#[test] -fn run_reads_meta_and_fills_in_sha256_and_installed_size() { - let scratch = TempDir::new().unwrap(); - write( - scratch.path().join("meta.toml"), - "name = \"test-pkg\"\narch = \"x86_64\"\n", - ) - .unwrap(); - create_dir_all(scratch.path().join("usr")).unwrap(); - write(scratch.path().join("usr/a.txt"), b"hello").unwrap(); - - let mut context = Context::new(); - context.put(genesis_input(false)); - context.put(ResolvedSourceDir(scratch.path().to_path_buf())); - - let cancel = CancelToken::new(); - let progress = ProgressEventBuilder::new(0); - - let (_, result, _guard) = ReadMetaStage.run(&mut context, &cancel, progress).unwrap(); - - assert!(matches!(result, StageResult::Advance)); - - let meta = context.get::().unwrap(); - assert_eq!(meta.name, "test-pkg"); - assert_eq!(meta.arch, "x86_64"); - assert_eq!(meta.installed_size, 5); - assert_ne!(meta.sha256, [0u8; 32]); -} - -#[test] -fn run_fails_when_meta_toml_missing() { - let scratch = TempDir::new().unwrap(); - - let mut context = Context::new(); - context.put(genesis_input(false)); - context.put(ResolvedSourceDir(scratch.path().to_path_buf())); - - let cancel = CancelToken::new(); - let progress = ProgressEventBuilder::new(0); - - let result = ReadMetaStage.run(&mut context, &cancel, progress); - - assert!(result.is_err()); -} diff --git a/lib/setup/tests/inline/partition.rs b/lib/setup/tests/inline/partition.rs index 29fa7005..925a3fc5 100644 --- a/lib/setup/tests/inline/partition.rs +++ b/lib/setup/tests/inline/partition.rs @@ -5,12 +5,17 @@ use std::path::PathBuf; +use uuid::Uuid; + use super::DiskLayout; fn layout(device_path: &str, extra_partitions: Vec) -> DiskLayout { DiskLayout { device_path: PathBuf::from(device_path), esp_partition: 1, + esp_starting_lba: 2048, + esp_ending_lba: 2048 + 1024 * 1024 / 512 - 1, + esp_unique_partition_guid: Uuid::new_v4(), deploy_partition: 2, extra_partitions, } diff --git a/lib/setup/tests/inline/trees.rs b/lib/setup/tests/inline/trees.rs deleted file mode 100644 index fa66a80d..00000000 --- a/lib/setup/tests/inline/trees.rs +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs::{create_dir_all, write}; -use std::path::PathBuf; - -use tempfile::TempDir; - -use upac::orchestrator::Context; -use upac::orchestrator::stage::{Stage, StageResult}; - -use upac_abi::hook::{CancelToken, ProgressEventBuilder}; - -use crate::target::TargetSysroot; -use crate::types::{ConfigTree, GenesisInput, ImportedConfigPaths, ImportedPrefixPaths, PrefixTree, ResolvedSourceDir}; - -use super::ImportTreesStage; - -fn genesis_input(empty_config: bool) -> GenesisInput { - GenesisInput { - source: String::new(), - meta_filename: None, - empty_config, - pinned: false, - boot_plugin: None, - } -} - -fn context_with(source_dir: PathBuf, empty_config: bool) -> (Context, TempDir) { - let target_scratch = TempDir::new().unwrap(); - let target = TargetSysroot::for_testing(target_scratch.path().to_path_buf()).unwrap(); - - let mut context = Context::new(); - context.put(target); - context.put(genesis_input(empty_config)); - context.put(ResolvedSourceDir(source_dir)); - - (context, target_scratch) -} - -#[test] -fn run_imports_usr_and_etc_and_records_their_paths() { - let source = TempDir::new().unwrap(); - create_dir_all(source.path().join("usr/bin")).unwrap(); - write(source.path().join("usr/bin/tool"), b"binary").unwrap(); - create_dir_all(source.path().join("etc")).unwrap(); - write(source.path().join("etc/conf"), b"config").unwrap(); - - let (mut context, _target_scratch) = context_with(source.path().to_path_buf(), false); - let cancel = CancelToken::new(); - let progress = ProgressEventBuilder::new(0); - - let (_, result, _guard) = ImportTreesStage.run(&mut context, &cancel, progress).unwrap(); - - assert!(matches!(result, StageResult::Advance)); - - let prefix_paths = context.get::().unwrap(); - assert_eq!(prefix_paths.0, vec![PathBuf::from("bin/tool")]); - - let config_paths = context.get::().unwrap(); - assert_eq!(config_paths.0, vec![PathBuf::from("conf")]); - - assert!(context.get::().is_some()); - assert!(context.get::().is_some()); -} - -#[test] -fn run_skips_etc_when_empty_config_is_true() { - let source = TempDir::new().unwrap(); - create_dir_all(source.path().join("usr")).unwrap(); - write(source.path().join("usr/tool"), b"binary").unwrap(); - create_dir_all(source.path().join("etc")).unwrap(); - write(source.path().join("etc/conf"), b"config").unwrap(); - - let (mut context, _target_scratch) = context_with(source.path().to_path_buf(), true); - let cancel = CancelToken::new(); - let progress = ProgressEventBuilder::new(0); - - ImportTreesStage.run(&mut context, &cancel, progress).unwrap(); - - let config_paths = context.get::().unwrap(); - assert!(config_paths.0.is_empty()); - - let prefix_paths = context.get::().unwrap(); - assert_eq!(prefix_paths.0, vec![PathBuf::from("tool")]); -} - -#[test] -fn run_handles_source_with_neither_usr_nor_etc() { - let source = TempDir::new().unwrap(); - - let (mut context, _target_scratch) = context_with(source.path().to_path_buf(), false); - let cancel = CancelToken::new(); - let progress = ProgressEventBuilder::new(0); - - let (_, result, _guard) = ImportTreesStage.run(&mut context, &cancel, progress).unwrap(); - - assert!(matches!(result, StageResult::Advance)); - assert!(context.get::().unwrap().0.is_empty()); - assert!(context.get::().unwrap().0.is_empty()); -} diff --git a/lib/setup/tests/meta.rs b/lib/setup/tests/meta.rs deleted file mode 100644 index 61ed10d5..00000000 --- a/lib/setup/tests/meta.rs +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use std::fs::{create_dir_all, write}; - -use tempfile::TempDir; - -use upac_setup::meta::SourceDir; - -#[test] -fn read_parses_meta_toml_with_serde_defaults() { - let scratch = TempDir::new().unwrap(); - write( - scratch.path().join("meta.toml"), - "name = \"test-pkg\"\narch = \"x86_64\"\n", - ) - .unwrap(); - - let source = SourceDir { path: scratch.path() }; - let meta = source.read(None).unwrap(); - - assert_eq!(meta.name, "test-pkg"); - assert_eq!(meta.arch, "x86_64"); -} - -#[test] -fn read_honors_explicit_filename_override() { - let scratch = TempDir::new().unwrap(); - write(scratch.path().join("custom.toml"), "name = \"custom-pkg\"\n").unwrap(); - - let source = SourceDir { path: scratch.path() }; - let meta = source.read(Some("custom.toml")).unwrap(); - - assert_eq!(meta.name, "custom-pkg"); -} - -#[test] -fn read_fails_when_file_missing() { - let scratch = TempDir::new().unwrap(); - let source = SourceDir { path: scratch.path() }; - - assert!(source.read(None).is_err()); -} - -#[test] -fn checksum_sums_installed_size_of_usr_files() { - let scratch = TempDir::new().unwrap(); - create_dir_all(scratch.path().join("usr")).unwrap(); - write(scratch.path().join("usr/a.txt"), b"12345").unwrap(); - write(scratch.path().join("usr/b.txt"), b"1234567890").unwrap(); - - let source = SourceDir { path: scratch.path() }; - let (_, installed_size) = source.checksum(false).unwrap(); - - assert_eq!(installed_size, 15); -} - -#[test] -fn checksum_excludes_etc_when_include_config_is_false() { - let scratch = TempDir::new().unwrap(); - create_dir_all(scratch.path().join("usr")).unwrap(); - write(scratch.path().join("usr/a.txt"), b"hello").unwrap(); - create_dir_all(scratch.path().join("etc")).unwrap(); - write(scratch.path().join("etc/b.txt"), b"world").unwrap(); - - let source = SourceDir { path: scratch.path() }; - - let (hash_without_config, size_without_config) = source.checksum(false).unwrap(); - let (hash_with_config, size_with_config) = source.checksum(true).unwrap(); - - assert_ne!(hash_without_config, hash_with_config); - assert_eq!(size_without_config, 5); - assert_eq!(size_with_config, 10); -} - -#[test] -fn checksum_is_deterministic() { - let scratch = TempDir::new().unwrap(); - create_dir_all(scratch.path().join("usr")).unwrap(); - write(scratch.path().join("usr/a.txt"), b"hello").unwrap(); - - let source = SourceDir { path: scratch.path() }; - - let first = source.checksum(false).unwrap(); - let second = source.checksum(false).unwrap(); - - assert_eq!(first, second); -} diff --git a/lib/types/Cargo.toml b/lib/types/Cargo.toml index 38eac821..8b34b0cb 100644 --- a/lib/types/Cargo.toml +++ b/lib/types/Cargo.toml @@ -30,4 +30,5 @@ upac-macro = { workspace = true } upac-abi = { workspace = true } serde = { workspace = true } +sha2 = { workspace = true } toml = { workspace = true } diff --git a/lib/types/src/decoder.rs b/lib/types/src/decoder.rs index 7f7da1ce..5945aa6e 100644 --- a/lib/types/src/decoder.rs +++ b/lib/types/src/decoder.rs @@ -3,11 +3,57 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::io::Read; +use std::fs::File; +use std::io::{BufReader, Read}; -use upac_abi::decoder::DecodeError; +use sha2::{Digest, Sha256}; -use crate::{Dependency, PackageMeta}; +use upac_abi::FreeDecodeResponseFn; +use upac_abi::hook::CancelToken; +use upac_abi::package::{CPackageDependency, CPackageMeta}; +use upac_abi::response::CDecodeResponse; +use upac_abi::types::{COwned, CSlice, CVec}; + +use upac_macro::RedbCodec; + +use super::error::DecodeError; +use super::package::DecodedPackageMeta; + +const VERIFY_CHUNK_SIZE: usize = 65536; + +#[derive(Debug, Clone, RedbCodec)] +pub struct DeclarativeTrigger { + pub format: String, + pub triggers: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecoderTrigger { + PreInstall, + PostInstall, + PreUpgrade, + PostUpgrade, + PreRemove, + PostRemove, +} + +impl DecoderTrigger { + pub const ALL: [DecoderTrigger; 6] = [ + DecoderTrigger::PreInstall, + DecoderTrigger::PostInstall, + DecoderTrigger::PreUpgrade, + DecoderTrigger::PostUpgrade, + DecoderTrigger::PreRemove, + DecoderTrigger::PostRemove, + ]; +} + +pub fn parse_constraint_prefix(token: &[u8], operators: &[(&[u8], u8)]) -> Option<(u8, usize)> { + operators + .iter() + .find(|(operator, _)| token.starts_with(operator)) + .map(|(operator, constraint)| (*constraint, operator.len())) +} pub fn read_to_string(reader: &mut R) -> Result { let mut bytes = Vec::new(); @@ -16,12 +62,52 @@ pub fn read_to_string(reader: &mut R) -> Result { String::from_utf8(bytes).map_err(|_| DecodeError::InvalidUtf8) } -#[derive(Debug)] -pub struct DecodedMeta { - pub meta: PackageMeta, - pub dependencies: Vec, +pub fn verify(package_path: &str, expected_checksum: [u8; 32], cancel: &CancelToken) -> Result<(), DecodeError> { + let file = File::open(package_path)?; + let mut reader = BufReader::new(file); + + let mut hasher = Sha256::new(); + let mut buffer = [0u8; VERIFY_CHUNK_SIZE]; + + loop { + if cancel.is_cancelled() { + return Err(DecodeError::Cancelled); + } + + let bytes_read = reader.read(&mut buffer)?; + if bytes_read == 0 { + break; + } + + hasher.update(&buffer[..bytes_read]); + } + + if hasher.finalize().as_slice() != expected_checksum.as_slice() { + return Err(DecodeError::ChecksumMismatch); + } + + Ok(()) } -pub trait DecodeMeta { - fn decode(&self, sha256: [u8; 32]) -> Result; +pub fn build_decode_response( + decoded: DecodedPackageMeta, declarative_triggers: Vec, free: FreeDecodeResponseFn, +) -> CDecodeResponse { + let DecodedPackageMeta { meta, dependencies } = decoded; + + let dependencies = dependencies + .into_iter() + .map(CPackageDependency::from) + .collect::>(); + + let declarative_triggers = declarative_triggers + .into_iter() + .map(|trigger| CSlice::from_owned(trigger.into_bytes())) + .collect::>(); + + CDecodeResponse::new( + CPackageMeta::from(meta), + CVec::from_owned(dependencies), + CVec::from_owned(declarative_triggers), + free, + ) } diff --git a/lib/types/src/entry.rs b/lib/types/src/entry.rs new file mode 100644 index 00000000..6c34f6f9 --- /dev/null +++ b/lib/types/src/entry.rs @@ -0,0 +1,135 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::mem::size_of; + +use upac_abi::package::CVersion; +use upac_abi::response::{ + CConfigCommitEntry, CDiffConfigFileEntry, CDiffFileEntryCommon, CDiffPackageEntry, CDiffPrefixFileEntry, + CDiffUntrackedFileEntry, CHistoryEntry, CPrefixEntry, CSearchFileEntry, +}; +use upac_abi::types::{COwned, CSlice, CVec}; +use upac_abi::{DiffFileSource, FileDiffKind, PackageDiffKind}; + +use upac_macro::{RedbCodec, RustToC}; + +use crate::codec::RedbCodable; +use crate::package::Version; + +// ── FileEntryScope ────────────────────────────────────────────────────────── +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileEntryScope { + Prefix = 0, + Config = 1, +} + +impl RedbCodable for FileEntryScope { + fn redb_encode(&self, buf: &mut Vec) { + buf.push(*self as u8); + } + + fn redb_decode(data: &[u8], offset: &mut usize) -> FileEntryScope { + let value = data[*offset]; + *offset += 1; + + match value { + 1 => FileEntryScope::Config, + _ => FileEntryScope::Prefix, + } + } +} + +// ── FileEntry ─────────────────────────────────────────────────────────────── +#[derive(Debug, Clone, RedbCodec)] +pub struct FileEntry { + pub path: String, + pub is_user: bool, + pub scope: FileEntryScope, +} + +// ── SearchFileEntry ───────────────────────────────────────────────────────── +#[derive(Debug, Clone, RustToC)] +pub struct SearchFileEntry { + pub path: String, + pub package_name: String, + pub is_user: bool, +} + +// ── PrefixEntry ───────────────────────────────────────────────────────────── +#[derive(Debug, Clone, RustToC)] +pub struct PrefixEntry { + pub prefix_digest: String, + + pub subject: String, + pub message: Option, + + pub timestamp: u64, + + pub working_config: Option, +} + +// ── ConfigCommitEntry ───────────────────────────────────────────────────────────── +#[derive(Debug, Clone, RustToC)] +pub struct ConfigCommitEntry { + pub config_digest: String, + + pub subject: String, + pub message: Option, +} + +// ── HistoryEntry ──────────────────────────────────────────────────────────── +#[derive(Debug, Clone, RustToC)] +pub struct HistoryEntry { + pub prefix_digest: String, + + pub subject: String, + pub message: Option, + + pub timestamp: u64, + + pub working_config: Option, + pub config_history: Vec, +} + +// ── DiffFileEntryCommon ────────────────────────────────────────────────────── +#[derive(Debug, Clone, RustToC)] +pub struct DiffFileEntryCommon { + pub path: String, + pub kind: FileDiffKind, +} + +// ── DiffPrefixFileEntry ───────────────────────────────────────────────────── +#[derive(Debug, Clone, RustToC)] +pub struct DiffPrefixFileEntry { + pub common: DiffFileEntryCommon, + pub source: DiffFileSource, + pub package_name: String, + pub is_user: bool, +} + +// ── DiffConfigFileEntry ───────────────────────────────────────────────────── +#[derive(Debug, Clone, RustToC)] +pub struct DiffConfigFileEntry { + pub common: DiffFileEntryCommon, + pub package_name: Option, +} + +// ── DiffPackageEntry ──────────────────────────────────────────────────────── +#[derive(Debug, Clone, RustToC)] +pub struct DiffPackageEntry { + pub name: String, + pub kind: PackageDiffKind, + pub version: Version, + + pub files: Vec, +} + +// ── DiffUntrackedFileEntry ────────────────────────────────────────────────── +#[derive(Debug, Clone, RustToC)] +pub struct DiffUntrackedFileEntry { + pub common: DiffFileEntryCommon, + pub source: DiffFileSource, +} diff --git a/lib/types/src/error.rs b/lib/types/src/error.rs new file mode 100644 index 00000000..758348a6 --- /dev/null +++ b/lib/types/src/error.rs @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::io::Error as IoError; +use std::io::ErrorKind as IoErrorKind; + +use upac_abi::error::ErrorDomain; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecodeError { + InvalidRequest, + Io(IoErrorKind), + ChecksumMismatch, + UnsupportedFormat, + MissingMetadata, + MalformedMetadata, + InvalidUtf8, + Cancelled, +} + +impl From for DecodeError { + fn from(error: IoError) -> Self { + DecodeError::Io(error.kind()) + } +} + +impl DecodeError { + pub fn code(self) -> i32 { + match self { + DecodeError::InvalidRequest => -1, + DecodeError::Io(_) => -2, + DecodeError::ChecksumMismatch => -3, + DecodeError::UnsupportedFormat => -4, + DecodeError::MissingMetadata => -5, + DecodeError::MalformedMetadata => -6, + DecodeError::InvalidUtf8 => -7, + DecodeError::Cancelled => -8, + } + } +} + +pub trait CommandState: Copy { + const DOMAIN: ErrorDomain; + const VALIDATION: Self; + + fn as_u32(self) -> u32; +} diff --git a/lib/types/src/hook.rs b/lib/types/src/hook.rs new file mode 100644 index 00000000..64bad63a --- /dev/null +++ b/lib/types/src/hook.rs @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::ffi::{CString, c_void}; +use std::mem::size_of; +use std::ptr::null; + +use upac_abi::HookMessageFn; +use upac_abi::hook::{CProgressEvent, HookAck}; +use upac_abi::types::CSlice; + +use crate::traits::MessageHook; + +pub struct ProgressEventBuilder { + stage: u32, + phase: u32, + subject: Option, + current: u64, + total: u64, +} + +impl ProgressEventBuilder { + pub fn new(stage: u32) -> Self { + Self { + stage, + phase: 0, + subject: None, + current: 0, + total: 0, + } + } + + pub fn stage(&self) -> u32 { + self.stage + } + + pub fn phase(mut self, phase: u32) -> Self { + self.phase = phase; + self + } + + pub fn subject(mut self, subject: impl Into) -> Self { + self.subject = CString::new(subject.into()).ok(); + self + } + + pub fn progress(mut self, current: u64, total: u64) -> Self { + self.current = current; + self.total = total; + self + } + + pub fn build(&self) -> CProgressEvent { + let subject = match &self.subject { + Some(subject) => CSlice { + ptr: subject.as_ptr().cast(), + len: subject.as_bytes().len(), + }, + None => CSlice { ptr: null(), len: 0 }, + }; + + CProgressEvent { + struct_size: size_of::(), + stage: self.stage, + phase: self.phase, + subject, + current: self.current, + total: self.total, + } + } +} + +pub struct Message { + hook_message: Option, + hook_message_context: *mut c_void, +} + +impl Message { + pub fn new(hook_message: Option, hook_message_context: *mut c_void) -> Self { + Self { + hook_message, + hook_message_context, + } + } +} + +impl MessageHook for Message { + fn send(&self, event: &CProgressEvent) -> HookAck { + let Some(hook_message) = self.hook_message else { + return HookAck::Delivered; + }; + + unsafe { hook_message(event as *const CProgressEvent, self.hook_message_context) } + } +} diff --git a/lib/types/src/lib.rs b/lib/types/src/lib.rs index 4e9682f7..beefaf38 100644 --- a/lib/types/src/lib.rs +++ b/lib/types/src/lib.rs @@ -3,32 +3,21 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use std::cmp::Ordering; +use upac_abi::FsKind; -use serde::{Deserialize, Deserializer}; - -use upac_abi::decoder::CDependency; -use upac_abi::error::ErrorKind; -use upac_abi::package::{CPackageMeta, CVersion}; -use upac_abi::response::{ - CConfigCommitEntry, CDiffConfigFileEntry, CDiffFileEntryCommon, CDiffPackageEntry, CDiffPrefixFileEntry, - CDiffUntrackedFileEntry, CHistoryEntry, CPrefixEntry, CSearchFileEntry, -}; -use upac_abi::setup::{CBtrfsOptions, CGptLayout, CPartitionMount, CPartitionSpec}; -use upac_abi::types::{COwned, CSlice, CVec}; -use upac_abi::{DiffFileSource, FileDiffKind, FsKind, PackageDiffKind}; - -use upac_macro::{CTryToRust, RedbCodec, RustToC}; - -use crate::codec::RedbCodable; +use self::package::{PackageEntry, PackageMeta}; pub mod codec; pub mod decoder; +pub mod entry; +pub mod error; +pub mod hook; +pub mod package; +pub mod request; +pub mod response; pub mod settings; pub mod states; - -#[cfg(test)] -mod tests; +pub mod traits; macro_rules! as_str_method { ($name:ty) => { @@ -40,309 +29,9 @@ macro_rules! as_str_method { }; } -// ── Version ───────────────────────────────────────────────────────────────── -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -enum VersionToken<'a> { - Alpha(&'a str), - Numeric(u64), -} - -#[derive(Debug, Clone, PartialEq, Eq, CTryToRust, RedbCodec, RustToC)] -pub struct Version { - pub epoch: u32, - pub raw: String, -} - -impl Default for Version { - fn default() -> Self { - Version { - epoch: 0, - raw: "1.0.0".to_owned(), - } - } -} - -impl Version { - pub fn parse(raw: &str) -> Version { - match raw.split_once(':') { - Some((epoch, rest)) => Version { - epoch: epoch.parse().unwrap_or(0), - raw: rest.to_owned(), - }, - None => Version { - epoch: 0, - raw: raw.to_owned(), - }, - } - } -} - -impl<'de> Deserialize<'de> for Version { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let raw = String::deserialize(deserializer)?; - - Ok(Version::parse(&raw)) - } -} - -impl PartialOrd for Version { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Version { - fn cmp(&self, other: &Self) -> Ordering { - if self.epoch != other.epoch { - return self.epoch.cmp(&other.epoch); - } - - let self_tokens = self.tokenize(); - let other_tokens = other.tokenize(); - - let mut self_iter = self_tokens.iter(); - let mut other_iter = other_tokens.iter(); - - loop { - match (self_iter.next(), other_iter.next()) { - (Some(a), Some(b)) => match a.cmp(b) { - Ordering::Equal => continue, - ordering => return ordering, - }, - (Some(VersionToken::Numeric(_)), None) => return Ordering::Greater, - (Some(VersionToken::Alpha(_)), None) => return Ordering::Less, - (None, Some(VersionToken::Numeric(_))) => return Ordering::Less, - (None, Some(VersionToken::Alpha(_))) => return Ordering::Greater, - (None, None) => return Ordering::Equal, - } - } - } -} - -impl Version { - fn tokenize(&self) -> Vec> { - let bytes = self.raw.as_bytes(); - let mut tokens = Vec::new(); - let mut index = 0; - - while index < bytes.len() { - if !bytes[index].is_ascii_alphanumeric() { - index += 1; - continue; - } - - let start = index; - if bytes[index].is_ascii_digit() { - while index < bytes.len() && bytes[index].is_ascii_digit() { - index += 1; - } - let value = self.raw[start..index].parse().unwrap_or(u64::MAX); - tokens.push(VersionToken::Numeric(value)); - } else { - while index < bytes.len() && bytes[index].is_ascii_alphabetic() { - index += 1; - } - tokens.push(VersionToken::Alpha(&self.raw[start..index])); - } - } - - tokens - } -} - -// ── Package ───────────────────────────────────────────────────────────────── -#[derive(Debug, Clone)] -pub struct PackageTemp { - pub meta: PackageMeta, - pub temp_package_path: String, -} - -#[derive(Debug, Clone, RedbCodec)] -pub struct DeclarativeTrigger { - pub format: String, - pub triggers: Vec, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DecoderTrigger { - PreInstall, - PostInstall, - PreUpgrade, - PostUpgrade, - PreRemove, - PostRemove, -} - -impl DecoderTrigger { - pub const ALL: [DecoderTrigger; 6] = [ - DecoderTrigger::PreInstall, - DecoderTrigger::PostInstall, - DecoderTrigger::PreUpgrade, - DecoderTrigger::PostUpgrade, - DecoderTrigger::PreRemove, - DecoderTrigger::PostRemove, - ]; -} - -#[derive(Debug, Clone, Default, Deserialize, CTryToRust, RedbCodec, RustToC)] -#[serde(default)] -pub struct PackageMeta { - pub name: String, - pub version: Version, - pub arch: String, - pub arch_sub: Option, - pub maintainer: String, - pub description: String, - pub license: Option, - pub url: Option, - pub sha256: [u8; 32], - pub installed_size: u64, -} - -#[derive(Debug, Clone, CTryToRust, RustToC)] -pub struct Dependency { - pub name: String, - pub constraint: u8, - pub version: Version, -} - -// ── PackageEntry ──────────────────────────────────────────────────────────── -#[derive(Debug, Clone)] -pub struct PackageEntry { - pub name: String, - pub arch: String, - pub arch_sub: Option, -} - -// ── FileEntryScope ────────────────────────────────────────────────────────── -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FileEntryScope { - Prefix = 0, - Config = 1, -} - -impl RedbCodable for FileEntryScope { - fn redb_encode(&self, buf: &mut Vec) { - buf.push(*self as u8); - } - - fn redb_decode(data: &[u8], offset: &mut usize) -> FileEntryScope { - let value = data[*offset]; - *offset += 1; - - match value { - 1 => FileEntryScope::Config, - _ => FileEntryScope::Prefix, - } - } -} - -// ── FileEntry ─────────────────────────────────────────────────────────────── -#[derive(Debug, Clone, RedbCodec)] -pub struct FileEntry { - pub path: String, - pub is_user: bool, - pub scope: FileEntryScope, -} - -// ── SearchFileEntry ───────────────────────────────────────────────────────── -#[derive(Debug, Clone, RustToC)] -pub struct SearchFileEntry { - pub path: String, - pub package_name: String, - pub is_user: bool, -} - -// ── PrefixEntry ───────────────────────────────────────────────────────────── -#[derive(Debug, Clone, RustToC)] -pub struct PrefixEntry { - pub prefix_digest: String, - - pub subject: String, - pub message: Option, - - pub timestamp: u64, - - pub working_config: Option, -} +pub struct UninstallPackagesTargets(pub Vec); -// ── ConfigCommitEntry ───────────────────────────────────────────────────────────── -#[derive(Debug, Clone, RustToC)] -pub struct ConfigCommitEntry { - pub config_digest: String, - - pub subject: String, - pub message: Option, -} - -// ── HistoryEntry ──────────────────────────────────────────────────────────── -#[derive(Debug, Clone, RustToC)] -pub struct HistoryEntry { - pub prefix_digest: String, - - pub subject: String, - pub message: Option, - - pub timestamp: u64, - - pub working_config: Option, - pub config_history: Vec, -} - -// ── DiffFileEntryCommon ────────────────────────────────────────────────────── -#[derive(Debug, Clone, RustToC)] -pub struct DiffFileEntryCommon { - pub path: String, - pub kind: FileDiffKind, -} - -// ── DiffPrefixFileEntry ───────────────────────────────────────────────────── -#[derive(Debug, Clone, RustToC)] -pub struct DiffPrefixFileEntry { - pub common: DiffFileEntryCommon, - pub source: DiffFileSource, - pub package_name: String, - pub is_user: bool, -} - -// ── DiffConfigFileEntry ───────────────────────────────────────────────────── -#[derive(Debug, Clone, RustToC)] -pub struct DiffConfigFileEntry { - pub common: DiffFileEntryCommon, - pub package_name: Option, -} - -// ── DiffPackageEntry ──────────────────────────────────────────────────────── -#[derive(Debug, Clone, RustToC)] -pub struct DiffPackageEntry { - pub name: String, - pub kind: PackageDiffKind, - 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, -} - -// ── 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 common: DiffFileEntryCommon, - pub source: DiffFileSource, -} - -pub struct Targets(pub Vec); - -impl Targets { +impl UninstallPackagesTargets { pub fn entries(&self) -> &[PackageEntry] { &self.0 } @@ -370,21 +59,21 @@ pub struct DiffPackagesSnapshot { } // ── PartitionMount / PartitionSpec (bootstrap setup) ──────────────────────── -#[derive(Debug, Clone, CTryToRust)] +#[derive(Debug, Clone)] pub struct PartitionMount { pub mount_path: String, pub device_path: String, pub fs_kind: FsKind, } -#[derive(Debug, Clone, CTryToRust)] +#[derive(Debug, Clone)] pub struct PartitionSpec { pub mount_path: String, pub size_mib: u64, pub fs_kind: FsKind, } -#[derive(Debug, Clone, CTryToRust)] +#[derive(Debug, Clone)] pub struct GptLayout { pub esp_size_mib: u64, pub deploy_fs: FsKind, @@ -393,7 +82,7 @@ pub struct GptLayout { pub force_wipe: bool, } -#[derive(Debug, Clone, CTryToRust)] +#[derive(Debug, Clone)] pub struct BtrfsOptions { pub node_size: u32, pub sector_size: u32, diff --git a/lib/types/src/package.rs b/lib/types/src/package.rs new file mode 100644 index 00000000..eb8ad1b8 --- /dev/null +++ b/lib/types/src/package.rs @@ -0,0 +1,178 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::cmp::Ordering; +use std::mem::size_of; + +use serde::{Deserialize, Deserializer}; + +use upac_abi::error::ErrorKind; +use upac_abi::package::{CPackageDependency, CPackageInfo, CPackageMeta, CVersion}; +use upac_abi::types::{COwned, CSlice}; + +use upac_macro::{CTryToRust, RedbCodec, RustToC}; + +// ── Version ───────────────────────────────────────────────────────────────── +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum VersionToken<'a> { + Alpha(&'a str), + Numeric(u64), +} + +#[derive(Debug, Clone, PartialEq, Eq, CTryToRust, RedbCodec, RustToC)] +pub struct Version { + pub epoch: u32, + pub raw: String, +} + +impl Default for Version { + fn default() -> Self { + Version { + epoch: 0, + raw: "1.0.0".to_owned(), + } + } +} + +impl Version { + pub fn parse(raw: &str) -> Version { + match raw.split_once(':') { + Some((epoch, rest)) => Version { + epoch: epoch.parse().unwrap_or(0), + raw: rest.to_owned(), + }, + None => Version { + epoch: 0, + raw: raw.to_owned(), + }, + } + } +} + +impl<'de> Deserialize<'de> for Version { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let raw = String::deserialize(deserializer)?; + + Ok(Version::parse(&raw)) + } +} + +impl PartialOrd for Version { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Version { + fn cmp(&self, other: &Self) -> Ordering { + if self.epoch != other.epoch { + return self.epoch.cmp(&other.epoch); + } + + let self_tokens = self.tokenize(); + let other_tokens = other.tokenize(); + + let mut self_iter = self_tokens.iter(); + let mut other_iter = other_tokens.iter(); + + loop { + match (self_iter.next(), other_iter.next()) { + (Some(a), Some(b)) => match a.cmp(b) { + Ordering::Equal => continue, + ordering => return ordering, + }, + (Some(VersionToken::Numeric(_)), None) => return Ordering::Greater, + (Some(VersionToken::Alpha(_)), None) => return Ordering::Less, + (None, Some(VersionToken::Numeric(_))) => return Ordering::Less, + (None, Some(VersionToken::Alpha(_))) => return Ordering::Greater, + (None, None) => return Ordering::Equal, + } + } + } +} + +impl Version { + fn tokenize(&self) -> Vec> { + let bytes = self.raw.as_bytes(); + let mut tokens = Vec::new(); + let mut index = 0; + + while index < bytes.len() { + if !bytes[index].is_ascii_alphanumeric() { + index += 1; + continue; + } + + let start = index; + if bytes[index].is_ascii_digit() { + while index < bytes.len() && bytes[index].is_ascii_digit() { + index += 1; + } + let value = self.raw[start..index].parse().unwrap_or(u64::MAX); + tokens.push(VersionToken::Numeric(value)); + } else { + while index < bytes.len() && bytes[index].is_ascii_alphabetic() { + index += 1; + } + tokens.push(VersionToken::Alpha(&self.raw[start..index])); + } + } + + tokens + } +} + +// ── Package ───────────────────────────────────────────────────────────────── +#[derive(Debug, Clone)] +pub struct PackageTemp { + pub meta: PackageMeta, + pub temp_package_path: String, +} + +#[derive(Debug, Clone, Default, Deserialize, CTryToRust, RedbCodec, RustToC)] +#[serde(default)] +pub struct PackageMeta { + pub name: String, + pub version: Version, + pub arch: String, + pub arch_sub: Option, + pub maintainer: String, + pub description: String, + pub license: Option, + pub url: Option, + pub sha256: [u8; 32], + pub installed_size: u64, +} + +// ── PackageEntry ──────────────────────────────────────────────────────────── +#[derive(Debug, Clone)] +pub struct PackageEntry { + pub name: String, + pub arch: String, + pub arch_sub: Option, +} + +#[derive(Debug, Clone, CTryToRust, RustToC)] +pub struct PackageInfo { + pub name: String, + pub arch: String, + pub arch_sub: Option, +} + +#[derive(Debug, Clone, CTryToRust, RustToC)] +pub struct PackageDependency { + pub name: String, + pub constraint: u8, + pub version: Version, +} + +#[derive(Debug)] +pub struct DecodedPackageMeta { + pub meta: PackageMeta, + pub dependencies: Vec, +} diff --git a/lib/types/src/request.rs b/lib/types/src/request.rs new file mode 100644 index 00000000..e863fa11 --- /dev/null +++ b/lib/types/src/request.rs @@ -0,0 +1,243 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::mem::size_of; +use std::os::raw::c_void; + +use upac_abi::HookMessageFn; +use upac_abi::error::ErrorKind; +use upac_abi::hook::CancelToken; +use upac_abi::package::CPackageInfo; +use upac_abi::request::{ + CBootPluginConfirmSuccsesBootRequest, CBootPluginInstallRequest, CBootPluginSetOneShotRequest, CCommitRequest, + CDecodeRequest, CDiffConfigRequest, CDiffPackagesRequest, CDiffPrefixRequest, CDiffRequest, CFilesRequest, + CGcRequest, CInstallRequest, CListConfigRequest, CListHistoryRequest, CListPackagesRequest, CListPrefixRequest, + CMimeSyncRequest, CPinRequest, CRequestBase, CRollbackRequest, CSearchFilesRequest, CSearchInMetaRequest, + CSearchInPackageFilesRequest, CSearchMetaRequest, CUninstallRequest, CUpdateRequest, +}; +use upac_abi::types::{COwned, CSlice, CVec}; +use upac_abi::{DiffFileSource, FileDiffKind}; + +use upac_macro::{CTryToRust, RustToC}; + +use super::package::PackageInfo; + +#[derive(Debug, Clone, RustToC)] +pub struct RequestBase { + pub on_hook: Option, + pub hook_ctx: *mut c_void, + pub cancel_token: *mut CancelToken, +} + +#[derive(Debug, Clone, RustToC)] +pub struct InstallRequest { + pub base: RequestBase, + + pub tmp_path: String, + + pub subject: String, + pub message: Option, + pub packages: Vec, + + pub boot_plugin: String, + + pub allow_conflict_files: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct UpdateRequest { + pub base: RequestBase, + pub tmp_path: String, + + pub subject: String, + pub message: Option, + + pub packages: Vec, + + pub boot_plugin: String, + + pub allow_downgrade: bool, + pub allow_conflict_files: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct UninstallRequest { + pub base: RequestBase, + + pub tmp_path: String, + + pub subject: String, + pub message: Option, + + pub packages: Vec, + + pub boot_plugin: String, + + pub purge: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct RollbackRequest { + pub base: RequestBase, + pub tmp_path: String, + pub config_digest: String, + pub boot_plugin: String, +} + +#[derive(Debug, Clone, RustToC)] +pub struct CommitRequest { + pub base: RequestBase, + pub tmp_path: String, + pub subject: String, + pub message: Option, +} + +#[derive(Debug, Clone, RustToC)] +pub struct FilesRequest { + pub base: RequestBase, + + pub tmp_path: String, + + pub subject: String, + pub message: Option, + + pub files: Vec, + pub file_kind: FileDiffKind, + pub file_package: *const CPackageInfo, + + pub boot_plugin: String, + + pub scope: DiffFileSource, +} + +#[derive(Debug, Clone, RustToC)] +pub struct GcRequest { + pub base: RequestBase, +} + +#[derive(Debug, Clone, RustToC)] +pub struct MimeSyncRequest { + pub base: RequestBase, +} + +#[derive(Debug, Clone, RustToC)] +pub struct PinRequest { + pub base: RequestBase, + pub prefix_digest: String, + pub pinned: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct ListPackagesRequest { + pub base: RequestBase, +} + +#[derive(Debug, Clone, RustToC)] +pub struct ListConfigRequest { + pub base: RequestBase, + pub prefix_digest: Option, +} + +#[derive(Debug, Clone, RustToC)] +pub struct ListPrefixRequest { + pub base: RequestBase, +} + +#[derive(Debug, Clone, RustToC)] +pub struct ListHistoryRequest { + pub base: RequestBase, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffPrefixRequest { + pub base: RequestBase, + pub from_prefix_digest: Option, + pub to_prefix_digest: Option, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffConfigRequest { + pub base: RequestBase, + pub from_config_digest: Option, + pub to_config_digest: Option, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffPackagesRequest { + pub base: RequestBase, + pub from_prefix_digest: Option, + pub to_prefix_digest: Option, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffRequest { + pub base: RequestBase, + pub from_prefix_digest: Option, + pub to_prefix_digest: Option, + pub from_config_digest: Option, + pub to_config_digest: Option, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SearchMetaRequest { + pub base: RequestBase, + pub search: String, + pub is_regex: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SearchFilesRequest { + pub base: RequestBase, + pub search: String, + pub is_regex: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SearchInMetaRequest { + pub base: RequestBase, + pub package: PackageInfo, + pub search: String, + pub is_regex: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SearchInPackageFilesRequest { + pub base: RequestBase, + pub package: PackageInfo, + pub search: String, + pub is_regex: bool, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DecodeRequest { + pub package_path: String, + pub output_dir: String, + pub checksum: [u8; 32], + pub cancel_token: *mut CancelToken, +} + +#[derive(Debug, Clone, RustToC, CTryToRust)] +pub struct BootPluginSetOneShotRequest { + pub entry_name: String, +} + +#[derive(Debug, Clone, RustToC, CTryToRust)] +pub struct BootPluginConfirmSuccsesBootRequest { + pub entry_name: String, + + pub esp_mount_point: String, +} + +#[derive(Debug, Clone, RustToC, CTryToRust)] +pub struct BootPluginInstallRequest { + pub esp_mount_point: String, + pub esp_partition_number: u32, + pub esp_starting_lba: u64, + pub esp_ending_lba: u64, + pub esp_unique_partition_guid: [u8; 16], + + pub to_slot: String, + pub from_slot: String, +} diff --git a/lib/types/src/response.rs b/lib/types/src/response.rs new file mode 100644 index 00000000..cf0cc927 --- /dev/null +++ b/lib/types/src/response.rs @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::mem::size_of; + +use upac_abi::error::ErrorKind; +use upac_abi::package::CPackageMeta; +use upac_abi::response::{ + CConfigCommitEntry, CDecodeResponse, CDiffConfigFileEntry, CDiffConfigResponse, CDiffPackageEntry, + CDiffPackagesResponse, CDiffPrefixFileEntry, CDiffPrefixResponse, CDiffResponse, CDiffUntrackedFileEntry, + CHistoryEntry, CListConfigResponse, CListHistoryResponse, CListPackagesResponse, CListPrefixResponse, CPrefixEntry, + CSearchFileEntry, CSearchFilesResponse, CSearchInMetaResponse, CSearchInPackageFilesResponse, CSearchMetaResponse, +}; +use upac_abi::types::{COwned, CVec}; + +use upac_macro::{CTryToRust, RustToC}; + +use super::entry::{ + ConfigCommitEntry, DiffConfigFileEntry, DiffPackageEntry, DiffPrefixFileEntry, DiffUntrackedFileEntry, + HistoryEntry, PrefixEntry, SearchFileEntry, +}; +use super::package::{PackageDependency, PackageMeta}; + +#[derive(Debug, Clone, RustToC)] +pub struct ListConfigResponse { + pub commits: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct ListPackagesResponse { + pub metas: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SearchMetaResponse { + pub metas: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SearchFilesResponse { + pub files: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SearchInMetaResponse { + pub metas: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct SearchInPackageFilesResponse { + pub files: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct ListPrefixResponse { + pub prefixes: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct ListHistoryResponse { + pub history: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffPrefixResponse { + pub files: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffConfigResponse { + pub files: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffPackagesResponse { + pub diff_packages: Vec, +} + +#[derive(Debug, Clone, RustToC)] +pub struct DiffResponse { + pub diff_packages: Vec, + pub unattached_files: Vec, +} + +#[derive(Debug, Clone, CTryToRust)] +pub struct DecodeResponse { + pub meta: PackageMeta, + pub dependencies: Vec, + pub declarative_triggers: Vec, +} diff --git a/lib/types/src/settings.rs b/lib/types/src/settings.rs index 978833db..73e8fbdd 100644 --- a/lib/types/src/settings.rs +++ b/lib/types/src/settings.rs @@ -39,11 +39,18 @@ impl Default for ProgressSettings { } } +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(default)] +pub struct BootSettings { + pub plugin: Option, +} + #[derive(Debug, Clone, Default, Deserialize)] #[serde(default)] pub struct RuntimeSettings { pub gc: GcSettings, pub progress: ProgressSettings, + pub boot: BootSettings, } impl RuntimeSettings { diff --git a/lib/types/src/states.rs b/lib/types/src/states.rs index ed167e8b..bea4cf3c 100644 --- a/lib/types/src/states.rs +++ b/lib/types/src/states.rs @@ -3,10 +3,12 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -use upac_abi::error::{CommandState, ErrorDomain}; +use upac_abi::error::ErrorDomain; use upac_macro::{FromStageIndex, StageKey}; +use crate::error::CommandState; + macro_rules! impl_command_state { ($name:ident, $domain:ident) => { impl CommandState for $name { diff --git a/lib/types/src/tests.rs b/lib/types/src/tests.rs deleted file mode 100644 index 31f09828..00000000 --- a/lib/types/src/tests.rs +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception - -use super::*; - -fn sample_version() -> Version { - Version { - epoch: 1, - raw: "2.5.0-3~rc1".to_owned(), - } -} - -#[test] -fn version_redb_round_trip_preserves_value() { - let original = sample_version(); - - let mut buf = Vec::new(); - original.redb_encode(&mut buf); - - let mut offset = 0; - let restored = Version::redb_decode(&buf, &mut offset); - - assert_eq!(restored, original); - assert_eq!(offset, buf.len()); -} - -#[test] -fn version_ord_equal_versions_compare_equal() { - let a = Version { - epoch: 0, - raw: "1.2.3".to_owned(), - }; - let b = a.clone(); - - assert_eq!(a.cmp(&b), Ordering::Equal); -} - -#[test] -fn version_ord_epoch_dominates_everything_else() { - let low_epoch = Version { - epoch: 0, - raw: "99.99.99".to_owned(), - }; - let high_epoch = Version { - epoch: 1, - raw: "0.0.1".to_owned(), - }; - - assert!(high_epoch > low_epoch); -} - -#[test] -fn version_ord_numeric_segments_compare_numerically() { - let a = Version { - epoch: 0, - raw: "1.9".to_owned(), - }; - let b = Version { - epoch: 0, - raw: "1.10".to_owned(), - }; - - assert!(b > a); -} - -#[test] -fn version_ord_numeric_beats_alpha_at_same_position() { - let release = Version { - epoch: 0, - raw: "1.0".to_owned(), - }; - let pre_release = Version { - epoch: 0, - raw: "1.0a".to_owned(), - }; - - assert!(release > pre_release); -} - -#[test] -fn version_ord_trailing_extra_numeric_is_newer() { - let a = Version { - epoch: 0, - raw: "1.0".to_owned(), - }; - let b = Version { - epoch: 0, - raw: "1.0.1".to_owned(), - }; - - assert!(b > a); -} - -#[test] -fn version_ord_trailing_extra_alpha_is_older() { - let a = Version { - epoch: 0, - raw: "1.0".to_owned(), - }; - let b = Version { - epoch: 0, - raw: "1.0-alpha".to_owned(), - }; - - assert!(b < a); -} - -#[test] -fn version_ord_mixed_format_examples_compare_consistently() { - let semver = Version { - epoch: 0, - raw: "1.23".to_owned(), - }; - let calver_dotted = Version { - epoch: 0, - raw: "26.5.4".to_owned(), - }; - let calver_flat = Version { - epoch: 0, - raw: "20263545".to_owned(), - }; - let alpha_mixed = Version { - epoch: 0, - raw: "1.13pre-1".to_owned(), - }; - let no_suffix = Version { - epoch: 0, - raw: "1.13".to_owned(), - }; - - assert!(calver_dotted > semver); - assert!(calver_flat > calver_dotted); - assert_eq!(alpha_mixed.cmp(&alpha_mixed.clone()), Ordering::Equal); - assert!(no_suffix > alpha_mixed); -} - -#[test] -fn file_entry_redb_round_trip_preserves_value() { - let original = FileEntry { - path: "/usr/bin/up".to_owned(), - is_user: false, - scope: FileEntryScope::Prefix, - }; - - let mut buf = Vec::new(); - original.redb_encode(&mut buf); - - let mut offset = 0; - let restored = FileEntry::redb_decode(&buf, &mut offset); - - assert_eq!(restored.path, original.path); - assert_eq!(restored.is_user, original.is_user); - assert_eq!(restored.scope, original.scope); - assert_eq!(offset, buf.len()); -} diff --git a/lib/types/src/traits.rs b/lib/types/src/traits.rs new file mode 100644 index 00000000..2115ed52 --- /dev/null +++ b/lib/types/src/traits.rs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_abi::hook::{CProgressEvent, HookAck}; + +use crate::error::DecodeError; +use crate::package::DecodedPackageMeta; + +pub trait Booter: Sized { + type Error; + + fn new() -> Result; + fn set_one_shot(&mut self, entry_name: &str) -> Result<(), Self::Error>; + fn confirm_boot(&mut self, entry_name: &str, esp_mount_point: &str) -> Result<(), Self::Error>; + + fn install( + &mut self, esp_mount_point: &str, esp_partition_number: u32, esp_starting_lba: u64, esp_ending_lba: u64, + esp_unique_partition_guid: [u8; 16], to_slot: &str, from_slot: &str, + ) -> Result<(), Self::Error>; +} + +pub trait DecodeMeta { + fn decode(&self, sha256: [u8; 32]) -> Result; +} + +pub trait MessageHook { + fn send(&self, event: &CProgressEvent) -> HookAck; +} diff --git a/lib/types/tests/conversions.rs b/lib/types/tests/conversions.rs index 7718d895..3d56a127 100644 --- a/lib/types/tests/conversions.rs +++ b/lib/types/tests/conversions.rs @@ -4,7 +4,7 @@ // SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::package::{CPackageMeta, CVersion}; -use upac_types::{PackageMeta, Version}; +use upac_types::package::{PackageMeta, Version}; fn sample_version() -> Version { Version { diff --git a/user/setup-cli/i18n/en/upac-setup-cli.ftl b/user/setup-cli/i18n/en/upac-setup-cli.ftl index 8f6b7a9e..fc924160 100644 --- a/user/setup-cli/i18n/en/upac-setup-cli.ftl +++ b/user/setup-cli/i18n/en/upac-setup-cli.ftl @@ -8,7 +8,6 @@ err-deploy-record = Deploy record operation failed err-boot = Boot entry staging failed err-boot-plugin = Boot plugin operation failed err-io = I/O error -err-meta-malformed = Malformed package metadata err-no-space-left = No space left on device err-not-block-device = Not a block device err-mkfs-failed = Filesystem creation failed @@ -22,12 +21,13 @@ err-missing-source = Missing required argument: --source err-invalid-partition-layout = Requested partition sizes don't fit on the disk err-invalid-format-params = Invalid filesystem formatting parameters err-reread-failed = Failed to reread the partition table (device busy?) +err-composefs-setup-root-unit-not-found = composefs-setup-root.service not found under source's system/ directory stage-prepare-source = Preparing source -stage-read-meta = Reading package metadata -stage-import-trees = Importing package tree -stage-create-database = Creating package database -stage-insert-file-entry = Recording file entries +stage-enumerate-packages = Enumerating packages +stage-unpack-package = Unpacking package +stage-import-package = Importing package +stage-import-system = Importing system files stage-embed-database = Embedding package database stage-write-deploy-record = Writing deploy record stage-stage-boot = Staging boot entry diff --git a/user/setup-cli/i18n/ru/upac-setup-cli.ftl b/user/setup-cli/i18n/ru/upac-setup-cli.ftl index 38117c7a..cd6ca91d 100644 --- a/user/setup-cli/i18n/ru/upac-setup-cli.ftl +++ b/user/setup-cli/i18n/ru/upac-setup-cli.ftl @@ -8,7 +8,6 @@ err-deploy-record = Ошибка операции с записью деплоя err-boot = Ошибка подготовки загрузочной записи err-boot-plugin = Ошибка загрузочного плагина err-io = Ошибка ввода-вывода -err-meta-malformed = Повреждённые метаданные пакета err-no-space-left = Не осталось места на устройстве err-not-block-device = Не является блочным устройством err-mkfs-failed = Ошибка создания файловой системы @@ -22,12 +21,13 @@ err-missing-source = Отсутствует обязательный аргум err-invalid-partition-layout = Запрошенные размеры разделов не помещаются на диск err-invalid-format-params = Некорректные параметры форматирования файловой системы err-reread-failed = Не удалось перечитать таблицу разделов (устройство занято?) +err-composefs-setup-root-unit-not-found = composefs-setup-root.service не найден в директории system/ источника stage-prepare-source = Подготовка источника -stage-read-meta = Чтение метаданных пакета -stage-import-trees = Импорт дерева пакета -stage-create-database = Создание базы данных пакета -stage-insert-file-entry = Запись файловых записей +stage-enumerate-packages = Перечисление пакетов +stage-unpack-package = Распаковка пакета +stage-import-package = Импорт пакета +stage-import-system = Импорт системных файлов stage-embed-database = Встраивание базы данных пакета stage-write-deploy-record = Запись записи деплоя stage-stage-boot = Подготовка загрузочной записи diff --git a/user/setup-cli/src/commands/manual.rs b/user/setup-cli/src/commands/manual.rs index 00e013c9..6adb8529 100644 --- a/user/setup-cli/src/commands/manual.rs +++ b/user/setup-cli/src/commands/manual.rs @@ -33,8 +33,6 @@ pub struct Args { #[arg(long)] pub source: String, #[arg(long)] - pub meta_filename: Option, - #[arg(long)] pub empty_config: bool, #[arg(long)] pub pinned: bool, @@ -53,7 +51,6 @@ pub fn run(args: Args, cancel_token: &CancelToken) -> Result<()> { mount_point: args.mount_point.as_deref(), source: &args.source, - meta_filename: args.meta_filename.as_deref(), empty_config: args.empty_config, pinned: args.pinned, boot_plugin: args.boot_plugin.as_deref(), diff --git a/user/setup-cli/src/commands/whole_disk.rs b/user/setup-cli/src/commands/whole_disk.rs index 3b783c4c..73b834ca 100644 --- a/user/setup-cli/src/commands/whole_disk.rs +++ b/user/setup-cli/src/commands/whole_disk.rs @@ -50,8 +50,6 @@ pub struct Args { #[arg(long)] pub source: Option, #[arg(long)] - pub meta_filename: Option, - #[arg(long)] pub empty_config: bool, #[arg(long)] pub pinned: bool, @@ -85,7 +83,6 @@ pub fn run(args: Args, cancel_token: &CancelToken) -> Result<()> { mount_point: args.mount_point.as_deref(), source, - meta_filename: args.meta_filename.as_deref(), empty_config: args.empty_config, pinned: args.pinned, boot_plugin: args.boot_plugin.as_deref(), diff --git a/user/setup-cli/src/errors.rs b/user/setup-cli/src/errors.rs index 5963f9a3..90be2507 100644 --- a/user/setup-cli/src/errors.rs +++ b/user/setup-cli/src/errors.rs @@ -50,7 +50,6 @@ impl Display for LocalizedSetupError { SetupError::Io(kind) => { write!(formatter, "{} ({kind:?})", fl!(LOADER, "err-io")) } - SetupError::MetaMalformed => formatter.write_str(&fl!(LOADER, "err-meta-malformed")), SetupError::NoSpaceLeft => formatter.write_str(&fl!(LOADER, "err-no-space-left")), SetupError::NotBlockDevice => formatter.write_str(&fl!(LOADER, "err-not-block-device")), SetupError::MkfsFailed => formatter.write_str(&fl!(LOADER, "err-mkfs-failed")), @@ -61,6 +60,9 @@ impl Display for LocalizedSetupError { SetupError::RereadFailed(errno) => { write!(formatter, "{} ({errno})", fl!(LOADER, "err-reread-failed")) } + SetupError::ComposefsSetupRootUnitNotFound => { + formatter.write_str(&fl!(LOADER, "err-composefs-setup-root-unit-not-found")) + } SetupError::Unexpected => formatter.write_str(&fl!(LOADER, "err-unexpected")), } } diff --git a/user/setup-cli/tests/inline/errors.rs b/user/setup-cli/tests/inline/errors.rs index da68b73d..f3dede54 100644 --- a/user/setup-cli/tests/inline/errors.rs +++ b/user/setup-cli/tests/inline/errors.rs @@ -27,9 +27,9 @@ fn localized(stage: GenesisStage, error: SetupError) -> String { #[test] fn prefixes_the_message_with_the_localized_failing_stage_name() { - let message = localized(GenesisStage::ImportTrees, SetupError::Unexpected); + let message = localized(GenesisStage::ImportPackage, SetupError::Unexpected); - assert_eq!(message, "Importing package tree: Unexpected error"); + assert_eq!(message, "Importing package: Unexpected error"); } #[test] @@ -49,12 +49,9 @@ fn mount_variant_embeds_the_errno() { #[test] fn repo_variant_embeds_debug_detail() { - let message = localized(GenesisStage::ImportTrees, SetupError::Repo(RepoError::NotFound)); + let message = localized(GenesisStage::ImportPackage, SetupError::Repo(RepoError::NotFound)); - assert_eq!( - message, - "Importing package tree: Repository operation failed (NotFound)" - ); + assert_eq!(message, "Importing package: Repository operation failed (NotFound)"); } #[test] @@ -105,9 +102,9 @@ fn boot_plugin_variant_embeds_debug_detail() { #[test] fn io_variant_embeds_the_error_kind() { - let message = localized(GenesisStage::ReadMeta, SetupError::Io(IoErrorKind::NotFound)); + let message = localized(GenesisStage::UnpackPackage, SetupError::Io(IoErrorKind::NotFound)); - assert_eq!(message, "Reading package metadata: I/O error (NotFound)"); + assert_eq!(message, "Unpacking package: I/O error (NotFound)"); } #[test] @@ -124,7 +121,6 @@ fn reread_failed_variant_embeds_the_errno() { #[test] fn no_payload_variants_use_their_fixed_localized_message() { let cases = [ - (SetupError::MetaMalformed, "Malformed package metadata"), (SetupError::NoSpaceLeft, "No space left on device"), (SetupError::NotBlockDevice, "Not a block device"), (SetupError::MkfsFailed, "Filesystem creation failed"), @@ -144,6 +140,10 @@ fn no_payload_variants_use_their_fixed_localized_message() { SetupError::InvalidFormatParams, "Invalid filesystem formatting parameters", ), + ( + SetupError::ComposefsSetupRootUnitNotFound, + "composefs-setup-root.service not found under source's system/ directory", + ), (SetupError::Unexpected, "Unexpected error"), ]; diff --git a/user/setup-cli/tests/inline/progress.rs b/user/setup-cli/tests/inline/progress.rs index f9be7db1..6c3bda52 100644 --- a/user/setup-cli/tests/inline/progress.rs +++ b/user/setup-cli/tests/inline/progress.rs @@ -45,10 +45,10 @@ fn apply_with_zero_total_stays_on_spinner() { locale::init_for_test(); let mut state = ProgressState::new(); - state.apply(&event(GenesisStage::ReadMeta as u32, 0, 0, empty_slice())); + state.apply(&event(GenesisStage::EnumeratePackages as u32, 0, 0, empty_slice())); assert!(!state.is_bar); - assert_eq!(state.bar.message(), "Reading package metadata"); + assert_eq!(state.bar.message(), "Enumerating packages"); } #[test] @@ -56,7 +56,7 @@ fn apply_with_nonzero_total_switches_to_bar_and_sets_position() { locale::init_for_test(); let mut state = ProgressState::new(); - state.apply(&event(GenesisStage::ImportTrees as u32, 3, 10, empty_slice())); + state.apply(&event(GenesisStage::ImportPackage as u32, 3, 10, empty_slice())); assert!(state.is_bar); assert_eq!(state.bar.length(), Some(10)); @@ -69,9 +69,14 @@ fn apply_includes_subject_in_message_when_present() { let mut state = ProgressState::new(); let subject = CString::new("foo.txt").unwrap(); - state.apply(&event(GenesisStage::ReadMeta as u32, 0, 0, slice_from_cstr(&subject))); + state.apply(&event( + GenesisStage::EnumeratePackages as u32, + 0, + 0, + slice_from_cstr(&subject), + )); - assert_eq!(state.bar.message(), "Reading package metadata: foo.txt"); + assert_eq!(state.bar.message(), "Enumerating packages: foo.txt"); } #[test] diff --git a/user/setup-cli/tests/inline/whole_disk.rs b/user/setup-cli/tests/inline/whole_disk.rs index f1f7068f..5a6a275f 100644 --- a/user/setup-cli/tests/inline/whole_disk.rs +++ b/user/setup-cli/tests/inline/whole_disk.rs @@ -24,7 +24,6 @@ fn valid_args() -> Args { mount_point: None, source: Some("/mnt/source".to_owned()), - meta_filename: None, empty_config: false, pinned: false, boot_plugin: None, diff --git a/user/sign-cli/src/commands/generate_cert.rs b/user/sign-cli/src/commands/generate_cert.rs index 0df15478..490a4d4c 100644 --- a/user/sign-cli/src/commands/generate_cert.rs +++ b/user/sign-cli/src/commands/generate_cert.rs @@ -42,13 +42,16 @@ pub fn run(args: Args) -> Result<()> { certificate_pem: read_to_string(&args.root_cert) .with_context(|| format!("{}: {}", fl!(LOADER, "err-read"), args.root_cert.display()))?, }; + let root = RootIdentity::from_pem(&root_pem).map_err(LocalizedPkiError)?; let signing = generate_signing_cert(&args.common_name, &root).map_err(LocalizedPkiError)?; + let pem = signing.to_pem().map_err(LocalizedPkiError)?; write(&args.key_out, &pem.key_pem) .with_context(|| format!("{}: {}", fl!(LOADER, "err-write"), args.key_out.display()))?; + write(&args.cert_out, &pem.certificate_pem) .with_context(|| format!("{}: {}", fl!(LOADER, "err-write"), args.cert_out.display()))?; diff --git a/user/sign-cli/src/commands/generate_root.rs b/user/sign-cli/src/commands/generate_root.rs index c6b4b106..e0bd589d 100644 --- a/user/sign-cli/src/commands/generate_root.rs +++ b/user/sign-cli/src/commands/generate_root.rs @@ -33,10 +33,12 @@ pub struct Args { pub fn run(args: Args) -> Result<()> { let root = generate_root(&args.common_name).map_err(LocalizedPkiError)?; + let pem = root.to_pem().map_err(LocalizedPkiError)?; write(&args.key_out, &pem.key_pem) .with_context(|| format!("{}: {}", fl!(LOADER, "err-write"), args.key_out.display()))?; + write(&args.cert_out, &pem.certificate_pem) .with_context(|| format!("{}: {}", fl!(LOADER, "err-write"), args.cert_out.display()))?; diff --git a/user/sign-cli/src/commands/sign_hook.rs b/user/sign-cli/src/commands/sign_hook.rs index a0da1cc5..6db1920d 100644 --- a/user/sign-cli/src/commands/sign_hook.rs +++ b/user/sign-cli/src/commands/sign_hook.rs @@ -38,14 +38,18 @@ pub fn run(args: Args) -> Result<()> { let signing_pem = PemIdentity { key_pem: read_to_string(&args.key) .with_context(|| format!("{}: {}", fl!(LOADER, "err-read"), args.key.display()))?, + certificate_pem: read_to_string(&args.cert) .with_context(|| format!("{}: {}", fl!(LOADER, "err-read"), args.cert.display()))?, }; + let signing = SigningIdentity::from_pem(&signing_pem).map_err(LocalizedPkiError)?; let hook_bytes = read(&args.hook).with_context(|| format!("{}: {}", fl!(LOADER, "err-read"), args.hook.display()))?; + let signature = HookSignature::sign(&hook_bytes, &signing).map_err(LocalizedPkiError)?; + let signature_bytes = signature.to_bytes().map_err(LocalizedPkiError)?; write(&args.signature, signature_bytes) diff --git a/user/sign-cli/src/commands/verify_hook.rs b/user/sign-cli/src/commands/verify_hook.rs index 454d75ab..e8321777 100644 --- a/user/sign-cli/src/commands/verify_hook.rs +++ b/user/sign-cli/src/commands/verify_hook.rs @@ -34,12 +34,15 @@ pub struct Args { pub fn run(args: Args) -> Result<()> { let hook_bytes = read(&args.hook).with_context(|| format!("{}: {}", fl!(LOADER, "err-read"), args.hook.display()))?; + let signature_bytes = read(&args.signature).with_context(|| format!("{}: {}", fl!(LOADER, "err-read"), args.signature.display()))?; + let root_cert_pem = read_to_string(&args.root_cert) .with_context(|| format!("{}: {}", fl!(LOADER, "err-read"), args.root_cert.display()))?; let signature = HookSignature::from_bytes(&signature_bytes).map_err(LocalizedPkiError)?; + let root_certificate = RootCertificate::from_pem(&root_cert_pem).map_err(LocalizedPkiError)?; signature diff --git a/user/sign-cli/src/errors.rs b/user/sign-cli/src/errors.rs index 195d9724..f141dc2c 100644 --- a/user/sign-cli/src/errors.rs +++ b/user/sign-cli/src/errors.rs @@ -3,6 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only +use std::error::Error; use std::fmt::{Display, Formatter}; use i18n_embed_fl::fl; @@ -29,4 +30,4 @@ impl Display for LocalizedPkiError { } } -impl std::error::Error for LocalizedPkiError {} +impl Error for LocalizedPkiError {} diff --git a/user/sign-cli/src/main.rs b/user/sign-cli/src/main.rs index bb322b28..a31dc866 100644 --- a/user/sign-cli/src/main.rs +++ b/user/sign-cli/src/main.rs @@ -27,7 +27,6 @@ mod layout { } mod locale; -// ── CLI arguments ───────────────────────────────────────────────────────────── #[derive(Parser)] #[command(name = "up-si", author, version, about)] enum Command { @@ -37,7 +36,6 @@ enum Command { VerifyHook(commands::verify_hook::Args), } -// ── Entry points ─────────────────────────────────────────────────────────────── fn main() -> ExitCode { locale::init(); diff --git a/user/upac-cli/Cargo.toml b/user/upac-cli/Cargo.toml index ad2ab2ad..1b7b62a9 100644 --- a/user/upac-cli/Cargo.toml +++ b/user/upac-cli/Cargo.toml @@ -28,7 +28,7 @@ name = "up" path = "src/main.rs" [dependencies] -upac-lib = { workspace = true, optional = true } +upac-lib = { workspace = true, optional = true, default-features = false } upac-abi = { workspace = true } upac-types = { workspace = true } diff --git a/user/upac-cli/i18n/en/upac-cli.ftl b/user/upac-cli/i18n/en/upac-cli.ftl index 1828014d..110b5851 100644 --- a/user/upac-cli/i18n/en/upac-cli.ftl +++ b/user/upac-cli/i18n/en/upac-cli.ftl @@ -24,6 +24,7 @@ err-invalid-entry = Invalid entry err-pkg-not-found = Package not found err-already-exists = Already exists err-not-initialized = Not initialized +err-boot-plugin-required = No boot plugin specified (use --boot or set [boot] plugin in /etc/upac.d/upac.toml) stage-pre-hooks = Pre-hooks stage-post-hooks = Post-hooks diff --git a/user/upac-cli/i18n/ru/upac-cli.ftl b/user/upac-cli/i18n/ru/upac-cli.ftl index f86983af..94b7515c 100644 --- a/user/upac-cli/i18n/ru/upac-cli.ftl +++ b/user/upac-cli/i18n/ru/upac-cli.ftl @@ -24,6 +24,7 @@ err-invalid-entry = Некорректная запись err-pkg-not-found = Пакет не найден err-already-exists = Уже существует err-not-initialized = Не инициализировано +err-boot-plugin-required = Не указан boot-плагин (используйте --boot или задайте [boot] plugin в /etc/upac.d/upac.toml) stage-pre-hooks = Пре-хуки stage-post-hooks = Пост-хуки diff --git a/user/upac-cli/src/commands/commit/diff.rs b/user/upac-cli/src/commands/commit/diff.rs index 2f8403d2..20998cf2 100644 --- a/user/upac-cli/src/commands/commit/diff.rs +++ b/user/upac-cli/src/commands/commit/diff.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -14,8 +14,11 @@ use colored::Colorize; use upac_abi::FileDiffKind; use upac_abi::request::CDiffConfigRequest; +use upac_types::request::{DiffConfigRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, optional_slice, request_base}; +use crate::types::abi::invoke_with_response; #[derive(ClapArgs)] pub struct Args { @@ -24,14 +27,16 @@ pub struct Args { } pub fn run(args: Args, ctx: CommandContext) -> Result<()> { - let from_config = args.from.as_deref().map(CString::new).transpose()?; - let to_config = args.to.as_deref().map(CString::new).transpose()?; - - let request = CDiffConfigRequest::new( - request_base(), - optional_slice(from_config.as_ref()), - optional_slice(to_config.as_ref()), - ); + let request: CDiffConfigRequest = DiffConfigRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + from_config_digest: args.from, + to_config_digest: args.to, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.diff_config)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/commit/history.rs b/user/upac-cli/src/commands/commit/history.rs index fd763e72..2575e5fc 100644 --- a/user/upac-cli/src/commands/commit/history.rs +++ b/user/upac-cli/src/commands/commit/history.rs @@ -3,6 +3,8 @@ // // SPDX-License-Identifier: GPL-3.0-only +use std::ptr::null_mut; + use anyhow::Result; use chrono::{Local, TimeZone}; @@ -13,14 +15,24 @@ use colored::Colorize; use upac_abi::request::CListHistoryRequest; +use upac_types::request::{ListHistoryRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, request_base}; +use crate::types::abi::invoke_with_response; #[derive(ClapArgs)] pub struct Args {} pub fn run(_args: Args, ctx: CommandContext) -> Result<()> { - let request = CListHistoryRequest::new(request_base()); + let request: CListHistoryRequest = ListHistoryRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.list_history)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/commit/list.rs b/user/upac-cli/src/commands/commit/list.rs index 1e346122..233bc0db 100644 --- a/user/upac-cli/src/commands/commit/list.rs +++ b/user/upac-cli/src/commands/commit/list.rs @@ -3,6 +3,8 @@ // // SPDX-License-Identifier: GPL-3.0-only +use std::ptr::null_mut; + use anyhow::Result; use clap::Args as ClapArgs; @@ -11,14 +13,25 @@ use colored::Colorize; use upac_abi::request::CListConfigRequest; +use upac_types::request::{ListConfigRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{empty_slice, invoke_with_response, request_base}; +use crate::types::abi::invoke_with_response; #[derive(ClapArgs)] pub struct Args {} pub fn run(_args: Args, ctx: CommandContext) -> Result<()> { - let request = CListConfigRequest::new(request_base(), empty_slice()); + let request: CListConfigRequest = ListConfigRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + prefix_digest: None, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.list_config)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/commit/mod.rs b/user/upac-cli/src/commands/commit/mod.rs index 84e67d28..7ab04fbf 100644 --- a/user/upac-cli/src/commands/commit/mod.rs +++ b/user/upac-cli/src/commands/commit/mod.rs @@ -3,7 +3,6 @@ // // SPDX-License-Identifier: GPL-3.0-only -// ── Imports ───────────────────────────────────────────────────────────────── use anyhow::Result; use clap::{Args, Subcommand}; @@ -18,14 +17,12 @@ pub mod pin; pub mod prefixes; pub mod unpin; -// ── Args ───────────────────────────────────────────────────────────────────── #[derive(Args)] pub struct CommitArgs { #[command(subcommand)] pub command: CommitCommand, } -// ── Subcommands ─────────────────────────────────────────────────────────────── #[derive(Subcommand)] pub enum CommitCommand { Diff(diff::Args), @@ -37,7 +34,6 @@ pub enum CommitCommand { Unpin(unpin::Args), } -// ── Dispatch ────────────────────────────────────────────────────────────────── pub fn run(args: CommitArgs, context: CommandContext) -> Result<()> { match args.command { CommitCommand::Diff(args) => diff::run(args, context), diff --git a/user/upac-cli/src/commands/commit/new.rs b/user/upac-cli/src/commands/commit/new.rs index 2d7ccfd3..d8230fee 100644 --- a/user/upac-cli/src/commands/commit/new.rs +++ b/user/upac-cli/src/commands/commit/new.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -11,8 +11,11 @@ use clap::Args as ClapArgs; use upac_abi::request::CCommitRequest; +use upac_types::request::{CommitRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{empty_slice, invoke, request_base, slice_from_cstr}; +use crate::types::abi::invoke; #[derive(ClapArgs)] pub struct Args { @@ -21,14 +24,18 @@ pub struct Args { pub fn run(args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let subject = CString::new(args.message)?; - - let request = CCommitRequest::new( - request_base(), - slice_from_cstr(&ctx.tmp_path), - slice_from_cstr(&subject), - empty_slice(), - ); + + let request: CCommitRequest = CommitRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + tmp_path: ctx.tmp_path.to_string_lossy().into_owned(), + subject: args.message, + message: None, + } + .into(); invoke(|error| unsafe { (symbols.commit)(request, error) }) } diff --git a/user/upac-cli/src/commands/commit/pin.rs b/user/upac-cli/src/commands/commit/pin.rs index 76e32365..573eec8e 100644 --- a/user/upac-cli/src/commands/commit/pin.rs +++ b/user/upac-cli/src/commands/commit/pin.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -11,8 +11,11 @@ use clap::Args as ClapArgs; use upac_abi::request::CPinRequest; +use upac_types::request::{PinRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{invoke, request_base, slice_from_cstr}; +use crate::types::abi::invoke; #[derive(ClapArgs)] pub struct Args { @@ -21,9 +24,17 @@ pub struct Args { pub fn run(args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let prefix_digest = CString::new(args.digest)?; - let request = CPinRequest::new(request_base(), slice_from_cstr(&prefix_digest), true); + let request: CPinRequest = PinRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + prefix_digest: args.digest, + pinned: true, + } + .into(); invoke(|error| unsafe { (symbols.pin_deploy)(request, error) }) } diff --git a/user/upac-cli/src/commands/commit/prefixes.rs b/user/upac-cli/src/commands/commit/prefixes.rs index 5f6614a6..f4c06a99 100644 --- a/user/upac-cli/src/commands/commit/prefixes.rs +++ b/user/upac-cli/src/commands/commit/prefixes.rs @@ -3,6 +3,8 @@ // // SPDX-License-Identifier: GPL-3.0-only +use std::ptr::null_mut; + use anyhow::Result; use chrono::{Local, TimeZone}; @@ -13,14 +15,24 @@ use colored::Colorize; use upac_abi::request::CListPrefixRequest; +use upac_types::request::{ListPrefixRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, request_base}; +use crate::types::abi::invoke_with_response; #[derive(ClapArgs)] pub struct Args {} pub fn run(_args: Args, ctx: CommandContext) -> Result<()> { - let request = CListPrefixRequest::new(request_base()); + let request: CListPrefixRequest = ListPrefixRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.list_prefix)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/commit/unpin.rs b/user/upac-cli/src/commands/commit/unpin.rs index d86621c5..d76b3a7d 100644 --- a/user/upac-cli/src/commands/commit/unpin.rs +++ b/user/upac-cli/src/commands/commit/unpin.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -11,8 +11,11 @@ use clap::Args as ClapArgs; use upac_abi::request::CPinRequest; +use upac_types::request::{PinRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{invoke, request_base, slice_from_cstr}; +use crate::types::abi::invoke; #[derive(ClapArgs)] pub struct Args { @@ -21,9 +24,17 @@ pub struct Args { pub fn run(args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let prefix_digest = CString::new(args.digest)?; - let request = CPinRequest::new(request_base(), slice_from_cstr(&prefix_digest), false); + let request: CPinRequest = PinRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + prefix_digest: args.digest, + pinned: false, + } + .into(); invoke(|error| unsafe { (symbols.pin_deploy)(request, error) }) } diff --git a/user/upac-cli/src/commands/diff.rs b/user/upac-cli/src/commands/diff.rs index 1f8f2116..b63c6e1e 100644 --- a/user/upac-cli/src/commands/diff.rs +++ b/user/upac-cli/src/commands/diff.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -14,9 +14,12 @@ use colored::Colorize; use upac_abi::request::CDiffRequest; use upac_abi::{DiffFileSource, FileDiffKind, PackageDiffKind}; +use upac_types::request::{DiffRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::commands::display::VersionDisplay; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, optional_slice, request_base}; +use crate::types::abi::invoke_with_response; #[derive(ClapArgs)] pub struct Args { @@ -31,18 +34,18 @@ pub struct Args { } pub fn run(args: Args, ctx: CommandContext) -> Result<()> { - let from_prefix = args.from_prefix.as_deref().map(CString::new).transpose()?; - let to_prefix = args.to_prefix.as_deref().map(CString::new).transpose()?; - let from_config = args.from_config.as_deref().map(CString::new).transpose()?; - let to_config = args.to_config.as_deref().map(CString::new).transpose()?; - - let request = CDiffRequest::new( - request_base(), - optional_slice(from_prefix.as_ref()), - optional_slice(to_prefix.as_ref()), - optional_slice(from_config.as_ref()), - optional_slice(to_config.as_ref()), - ); + let request: CDiffRequest = DiffRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + from_prefix_digest: args.from_prefix, + to_prefix_digest: args.to_prefix, + from_config_digest: args.from_config, + to_config_digest: args.to_config, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.diff)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/display.rs b/user/upac-cli/src/commands/display.rs index 0e61509b..fe4db66a 100644 --- a/user/upac-cli/src/commands/display.rs +++ b/user/upac-cli/src/commands/display.rs @@ -10,7 +10,7 @@ use colored::Colorize; use strum::AsRefStr; use upac_abi::package::{CPackageMeta, CVersion}; -use upac_types::Version; +use upac_types::package::Version; use crate::locale::LOADER; diff --git a/user/upac-cli/src/commands/file/add.rs b/user/upac-cli/src/commands/file/add.rs index b769f40b..45e02616 100644 --- a/user/upac-cli/src/commands/file/add.rs +++ b/user/upac-cli/src/commands/file/add.rs @@ -3,19 +3,25 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; - use anyhow::Result; use clap::Args as ClapArgs; +use i18n_embed_fl::fl; + use upac_abi::FileDiffKind; use upac_abi::error::ErrorDomain; -use upac_abi::request::{CFilesRequest, CRequestBase}; +use upac_abi::package::CPackageInfo; +use upac_abi::request::CFilesRequest; + +use upac_types::package::PackageInfo; +use upac_types::request::{FilesRequest, RequestBase}; +use upac_types::settings::RuntimeSettings; use crate::cancel_token_ptr; +use crate::locale::LOADER; use crate::types::CommandContext; -use crate::types::abi::{FileScope, borrowed_vec, invoke, optional_slice, package_info, slice_from_cstr}; +use crate::types::abi::{FileScope, invoke}; use crate::types::progress::{ProgressState, on_progress}; #[derive(ClapArgs)] @@ -39,37 +45,36 @@ pub struct Args { pub fn run(args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let package_name = CString::new(args.package)?; - let package_arch = CString::new(args.arch)?; - let package_arch_sub = args.arch_sub.map(CString::new).transpose()?; - let subject = CString::new("file add")?; - let message = args.message.map(CString::new).transpose()?; - let boot_plugin = args.boot.map(CString::new).transpose()?; - let scope = args.scope.into(); - - let file_cstrings = args - .files - .iter() - .map(|file_path| CString::new(file_path.as_str())) - .collect::, _>>()?; - let file_slices: Vec<_> = file_cstrings.iter().map(slice_from_cstr).collect(); - - let package = package_info(&package_name, &package_arch, package_arch_sub.as_ref()); + let package: CPackageInfo = PackageInfo { + name: args.package, + arch: args.arch, + arch_sub: args.arch_sub, + } + .into(); let mut progress = ProgressState::new(ErrorDomain::Files); - let base = CRequestBase::new(Some(on_progress), progress.ctx_ptr(), cancel_token_ptr()); - let request = CFilesRequest::new( - base, - slice_from_cstr(&ctx.tmp_path), - slice_from_cstr(&subject), - optional_slice(message.as_ref()), - borrowed_vec(&file_slices), - FileDiffKind::Added, - scope, - &package, - optional_slice(boot_plugin.as_ref()), - ); + let boot_plugin = args + .boot + .or_else(|| RuntimeSettings::load().boot.plugin) + .ok_or_else(|| anyhow::anyhow!(fl!(LOADER, "err-boot-plugin-required")))?; + + let request: CFilesRequest = FilesRequest { + base: RequestBase { + on_hook: Some(on_progress), + hook_ctx: progress.ctx_ptr(), + cancel_token: cancel_token_ptr(), + }, + tmp_path: ctx.tmp_path.to_string_lossy().into_owned(), + subject: "file add".to_owned(), + message: args.message, + files: args.files, + file_kind: FileDiffKind::Added, + scope: args.scope.into(), + file_package: &package, + boot_plugin: boot_plugin, + } + .into(); let result = invoke(|error| unsafe { (symbols.files)(request, error) }); progress.finish(); diff --git a/user/upac-cli/src/commands/file/diff.rs b/user/upac-cli/src/commands/file/diff.rs index ebf73eec..9bb53d54 100644 --- a/user/upac-cli/src/commands/file/diff.rs +++ b/user/upac-cli/src/commands/file/diff.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -14,8 +14,11 @@ use colored::Colorize; use upac_abi::FileDiffKind; use upac_abi::request::CDiffPrefixRequest; +use upac_types::request::{DiffPrefixRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, optional_slice, request_base}; +use crate::types::abi::invoke_with_response; #[derive(ClapArgs)] pub struct Args { @@ -24,14 +27,16 @@ pub struct Args { } pub fn run(args: Args, ctx: CommandContext) -> Result<()> { - let from_prefix = args.from.as_deref().map(CString::new).transpose()?; - let to_prefix = args.to.as_deref().map(CString::new).transpose()?; - - let request = CDiffPrefixRequest::new( - request_base(), - optional_slice(from_prefix.as_ref()), - optional_slice(to_prefix.as_ref()), - ); + let request: CDiffPrefixRequest = DiffPrefixRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + from_prefix_digest: args.from, + to_prefix_digest: args.to, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.diff_prefix)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/file/mod.rs b/user/upac-cli/src/commands/file/mod.rs index 027dbe5a..f2ecd687 100644 --- a/user/upac-cli/src/commands/file/mod.rs +++ b/user/upac-cli/src/commands/file/mod.rs @@ -3,7 +3,6 @@ // // SPDX-License-Identifier: GPL-3.0-only -// ── Imports ───────────────────────────────────────────────────────────────── use anyhow::Result; use clap::{Args, Subcommand}; @@ -15,14 +14,12 @@ pub mod diff; pub mod remove; pub mod search; -// ── Args ───────────────────────────────────────────────────────────────────── #[derive(Args)] pub struct FileArgs { #[command(subcommand)] pub command: FileCommand, } -// ── Subcommands ─────────────────────────────────────────────────────────────── #[derive(Subcommand)] pub enum FileCommand { Add(add::Args), @@ -31,7 +28,6 @@ pub enum FileCommand { Search(search::Args), } -// ── Dispatch ────────────────────────────────────────────────────────────────── pub fn run(args: FileArgs, context: CommandContext) -> Result<()> { match args.command { FileCommand::Add(args) => add::run(args, context), diff --git a/user/upac-cli/src/commands/file/remove.rs b/user/upac-cli/src/commands/file/remove.rs index 058d0878..ec8d4ee5 100644 --- a/user/upac-cli/src/commands/file/remove.rs +++ b/user/upac-cli/src/commands/file/remove.rs @@ -3,19 +3,25 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; - use anyhow::Result; use clap::Args as ClapArgs; +use i18n_embed_fl::fl; + use upac_abi::FileDiffKind; use upac_abi::error::ErrorDomain; -use upac_abi::request::{CFilesRequest, CRequestBase}; +use upac_abi::package::CPackageInfo; +use upac_abi::request::CFilesRequest; + +use upac_types::package::PackageInfo; +use upac_types::request::{FilesRequest, RequestBase}; +use upac_types::settings::RuntimeSettings; use crate::cancel_token_ptr; +use crate::locale::LOADER; use crate::types::CommandContext; -use crate::types::abi::{FileScope, borrowed_vec, invoke, optional_slice, package_info, slice_from_cstr}; +use crate::types::abi::{FileScope, invoke}; use crate::types::progress::{ProgressState, on_progress}; #[derive(ClapArgs)] @@ -39,37 +45,36 @@ pub struct Args { pub fn run(args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let package_name = CString::new(args.package)?; - let package_arch = CString::new(args.arch)?; - let package_arch_sub = args.arch_sub.map(CString::new).transpose()?; - let subject = CString::new("file remove")?; - let message = args.message.map(CString::new).transpose()?; - let boot_plugin = args.boot.map(CString::new).transpose()?; - let scope = args.scope.into(); - - let file_cstrings = args - .files - .iter() - .map(|file_path| CString::new(file_path.as_str())) - .collect::, _>>()?; - let file_slices: Vec<_> = file_cstrings.iter().map(slice_from_cstr).collect(); - - let package = package_info(&package_name, &package_arch, package_arch_sub.as_ref()); + let package: CPackageInfo = PackageInfo { + name: args.package, + arch: args.arch, + arch_sub: args.arch_sub, + } + .into(); let mut progress = ProgressState::new(ErrorDomain::Files); - let base = CRequestBase::new(Some(on_progress), progress.ctx_ptr(), cancel_token_ptr()); - let request = CFilesRequest::new( - base, - slice_from_cstr(&ctx.tmp_path), - slice_from_cstr(&subject), - optional_slice(message.as_ref()), - borrowed_vec(&file_slices), - FileDiffKind::Removed, - scope, - &package, - optional_slice(boot_plugin.as_ref()), - ); + let boot_plugin = args + .boot + .or_else(|| RuntimeSettings::load().boot.plugin) + .ok_or_else(|| anyhow::anyhow!(fl!(LOADER, "err-boot-plugin-required")))?; + + let request: CFilesRequest = FilesRequest { + base: RequestBase { + on_hook: Some(on_progress), + hook_ctx: progress.ctx_ptr(), + cancel_token: cancel_token_ptr(), + }, + tmp_path: ctx.tmp_path.to_string_lossy().into_owned(), + subject: "file remove".to_owned(), + message: args.message, + files: args.files, + file_kind: FileDiffKind::Removed, + scope: args.scope.into(), + file_package: &package, + boot_plugin: boot_plugin, + } + .into(); let result = invoke(|error| unsafe { (symbols.files)(request, error) }); progress.finish(); diff --git a/user/upac-cli/src/commands/file/search.rs b/user/upac-cli/src/commands/file/search.rs index f6de5696..d2920abc 100644 --- a/user/upac-cli/src/commands/file/search.rs +++ b/user/upac-cli/src/commands/file/search.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -16,9 +16,13 @@ use i18n_embed_fl::fl; use upac_abi::request::{CSearchFilesRequest, CSearchInPackageFilesRequest}; use upac_abi::response::CSearchFileEntry; +use upac_types::package::PackageInfo; +use upac_types::request::{RequestBase, SearchFilesRequest, SearchInPackageFilesRequest}; + +use crate::cancel_token_ptr; use crate::locale::LOADER; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, package_info, request_base, slice_from_cstr}; +use crate::types::abi::invoke_with_response; #[derive(ClapArgs)] pub struct Args { @@ -34,21 +38,27 @@ pub struct Args { } pub fn run(args: Args, ctx: CommandContext) -> Result<()> { - let query = CString::new(args.query.as_str())?; - match args.package.as_deref() { Some(package) => { let Some(arch) = args.package_arch.as_deref() else { anyhow::bail!(fl!(LOADER, "err-invalid-entry")); }; - let package_name = CString::new(package)?; - let package_arch = CString::new(arch)?; - let package_arch_sub = args.package_arch_sub.as_deref().map(CString::new).transpose()?; - let package = package_info(&package_name, &package_arch, package_arch_sub.as_ref()); - - let request = - CSearchInPackageFilesRequest::new(request_base(), package, slice_from_cstr(&query), args.regex); + let request: CSearchInPackageFilesRequest = SearchInPackageFilesRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + package: PackageInfo { + name: package.to_owned(), + arch: arch.to_owned(), + arch_sub: args.package_arch_sub.clone(), + }, + search: args.query.clone(), + is_regex: args.regex, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.search_in_package_files)(request, out, error) })?; @@ -58,7 +68,16 @@ pub fn run(args: Args, ctx: CommandContext) -> Result<()> { unsafe { response.free() }; } None => { - let request = CSearchFilesRequest::new(request_base(), slice_from_cstr(&query), args.regex); + let request: CSearchFilesRequest = SearchFilesRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + search: args.query.clone(), + is_regex: args.regex, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.search_files)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/gc.rs b/user/upac-cli/src/commands/gc.rs index 842fed52..5b7d60f1 100644 --- a/user/upac-cli/src/commands/gc.rs +++ b/user/upac-cli/src/commands/gc.rs @@ -3,14 +3,19 @@ // // SPDX-License-Identifier: GPL-3.0-only +use std::ptr::null_mut; + use anyhow::Result; use clap::Args as ClapArgs; use upac_abi::request::CGcRequest; +use upac_types::request::{GcRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{invoke, request_base}; +use crate::types::abi::invoke; #[derive(ClapArgs)] pub struct Args {} @@ -18,7 +23,14 @@ pub struct Args {} pub fn run(_args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let request = CGcRequest::new(request_base()); + let request: CGcRequest = GcRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + } + .into(); invoke(|error| unsafe { (symbols.gc)(request, error) }) } diff --git a/user/upac-cli/src/commands/mime/mod.rs b/user/upac-cli/src/commands/mime/mod.rs index 823e9320..aab1f687 100644 --- a/user/upac-cli/src/commands/mime/mod.rs +++ b/user/upac-cli/src/commands/mime/mod.rs @@ -3,7 +3,6 @@ // // SPDX-License-Identifier: GPL-3.0-only -// ── Imports ───────────────────────────────────────────────────────────────── use anyhow::Result; use clap::{Args, Subcommand}; @@ -12,20 +11,17 @@ use crate::types::CommandContext; pub mod sync; -// ── Args ───────────────────────────────────────────────────────────────────── #[derive(Args)] pub struct MimeArgs { #[command(subcommand)] pub command: MimeCommand, } -// ── Subcommands ─────────────────────────────────────────────────────────────── #[derive(Subcommand)] pub enum MimeCommand { Sync(sync::Args), } -// ── Dispatch ────────────────────────────────────────────────────────────────── pub fn run(args: MimeArgs, context: CommandContext) -> Result<()> { match args.command { MimeCommand::Sync(args) => sync::run(args, context), diff --git a/user/upac-cli/src/commands/mime/sync.rs b/user/upac-cli/src/commands/mime/sync.rs index cdb9912b..80b46633 100644 --- a/user/upac-cli/src/commands/mime/sync.rs +++ b/user/upac-cli/src/commands/mime/sync.rs @@ -3,14 +3,19 @@ // // SPDX-License-Identifier: GPL-3.0-only +use std::ptr::null_mut; + use anyhow::Result; use clap::Args as ClapArgs; use upac_abi::request::CMimeSyncRequest; +use upac_types::request::{MimeSyncRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::types::CommandContext; -use crate::types::abi::{invoke, request_base}; +use crate::types::abi::invoke; #[derive(ClapArgs)] pub struct Args {} @@ -18,7 +23,14 @@ pub struct Args {} pub fn run(_args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let request = CMimeSyncRequest::new(request_base()); + let request: CMimeSyncRequest = MimeSyncRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + } + .into(); invoke(|error| unsafe { (symbols.mime)(request, error) }) } diff --git a/user/upac-cli/src/commands/package/diff.rs b/user/upac-cli/src/commands/package/diff.rs index 371ae360..af558016 100644 --- a/user/upac-cli/src/commands/package/diff.rs +++ b/user/upac-cli/src/commands/package/diff.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -14,9 +14,12 @@ use colored::Colorize; use upac_abi::PackageDiffKind; use upac_abi::request::CDiffPackagesRequest; +use upac_types::request::{DiffPackagesRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::commands::display::VersionDisplay; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, optional_slice, request_base}; +use crate::types::abi::invoke_with_response; #[derive(ClapArgs)] pub struct Args { @@ -25,14 +28,16 @@ pub struct Args { } pub fn run(args: Args, ctx: CommandContext) -> Result<()> { - let from_prefix = args.from.as_deref().map(CString::new).transpose()?; - let to_prefix = args.to.as_deref().map(CString::new).transpose()?; - - let request = CDiffPackagesRequest::new( - request_base(), - optional_slice(from_prefix.as_ref()), - optional_slice(to_prefix.as_ref()), - ); + let request: CDiffPackagesRequest = DiffPackagesRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + from_prefix_digest: args.from, + to_prefix_digest: args.to, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.diff_packages)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/package/install.rs b/user/upac-cli/src/commands/package/install.rs index e813b91b..e5c7b0a2 100644 --- a/user/upac-cli/src/commands/package/install.rs +++ b/user/upac-cli/src/commands/package/install.rs @@ -3,7 +3,6 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; use std::fs::canonicalize; use anyhow::Result; @@ -13,18 +12,19 @@ use clap::Args as ClapArgs; use i18n_embed_fl::fl; use upac_abi::error::ErrorDomain; -use upac_abi::request::{CInstallRequest, CRequestBase}; +use upac_abi::request::CInstallRequest; + +use upac_types::request::{InstallRequest, RequestBase}; +use upac_types::settings::RuntimeSettings; use crate::cancel_token_ptr; use crate::locale::LOADER; use crate::types::CommandContext; -use crate::types::abi::{borrowed_vec, invoke, optional_slice, slice_from_cstr}; +use crate::types::abi::invoke; use crate::types::progress::{ProgressState, on_progress}; #[derive(ClapArgs)] pub struct Args { - // Required flag, not a positional: keeps the positional slot free for a future - // name-based network install (e.g. `up pkg install foo`), separate from this local-file path. #[arg(short, long = "file", required = true, num_args = 1..)] pub files: Vec, #[arg(short, long)] @@ -38,31 +38,34 @@ pub struct Args { pub fn run(args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let subject = CString::new("install")?; - let message = args.message.map(CString::new).transpose()?; - let boot_plugin = args.boot.map(CString::new).transpose()?; - - let mut paths = Vec::with_capacity(args.files.len()); + let mut packages = Vec::with_capacity(args.files.len()); for file_path in &args.files { let absolute = canonicalize(file_path).map_err(|_| anyhow::anyhow!("{}: {file_path}", fl!(LOADER, "err-not-found")))?; - paths.push(CString::new(absolute.to_string_lossy().as_ref())?); + packages.push(absolute.to_string_lossy().into_owned()); } - let path_slices: Vec<_> = paths.iter().map(slice_from_cstr).collect(); - let mut progress = ProgressState::new(ErrorDomain::Install); - let base = CRequestBase::new(Some(on_progress), progress.ctx_ptr(), cancel_token_ptr()); - let request = CInstallRequest::new( - base, - slice_from_cstr(&ctx.tmp_path), - slice_from_cstr(&subject), - optional_slice(message.as_ref()), - borrowed_vec(&path_slices), - optional_slice(boot_plugin.as_ref()), - !args.no_conflict_files, - ); + let boot_plugin = args + .boot + .or_else(|| RuntimeSettings::load().boot.plugin) + .ok_or_else(|| anyhow::anyhow!(fl!(LOADER, "err-boot-plugin-required")))?; + + let request: CInstallRequest = InstallRequest { + base: RequestBase { + on_hook: Some(on_progress), + hook_ctx: progress.ctx_ptr(), + cancel_token: cancel_token_ptr(), + }, + tmp_path: ctx.tmp_path.to_string_lossy().into_owned(), + subject: "install".to_owned(), + message: args.message, + packages, + boot_plugin: boot_plugin, + allow_conflict_files: !args.no_conflict_files, + } + .into(); let result = invoke(|error| unsafe { (symbols.install)(request, error) }); progress.finish(); diff --git a/user/upac-cli/src/commands/package/list.rs b/user/upac-cli/src/commands/package/list.rs index 0516797d..8660030c 100644 --- a/user/upac-cli/src/commands/package/list.rs +++ b/user/upac-cli/src/commands/package/list.rs @@ -3,15 +3,20 @@ // // SPDX-License-Identifier: GPL-3.0-only +use std::ptr::null_mut; + use anyhow::Result; use clap::Args as ClapArgs; use upac_abi::request::CListPackagesRequest; +use upac_types::request::{ListPackagesRequest, RequestBase}; + +use crate::cancel_token_ptr; use crate::commands::display::{PackageField, PackageFormatter}; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, request_base}; +use crate::types::abi::invoke_with_response; #[cfg(test)] #[path = "../../../tests/inline/list.rs"] @@ -42,7 +47,14 @@ pub struct Args { } pub fn run(args: Args, ctx: CommandContext) -> Result<()> { - let request = CListPackagesRequest::new(request_base()); + let request: CListPackagesRequest = ListPackagesRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.list_packages)(request, out, error) })?; diff --git a/user/upac-cli/src/commands/package/mod.rs b/user/upac-cli/src/commands/package/mod.rs index 3a28345c..4f421bc5 100644 --- a/user/upac-cli/src/commands/package/mod.rs +++ b/user/upac-cli/src/commands/package/mod.rs @@ -3,7 +3,6 @@ // // SPDX-License-Identifier: GPL-3.0-only -// ── Imports ───────────────────────────────────────────────────────────────── use anyhow::Result; use clap::{Args, Subcommand}; @@ -17,14 +16,12 @@ pub mod remove; pub mod search; pub mod update; -// ── Args ───────────────────────────────────────────────────────────────────── #[derive(Args)] pub struct PkgArgs { #[command(subcommand)] pub command: PkgCommand, } -// ── Subcommands ─────────────────────────────────────────────────────────────── #[derive(Subcommand)] pub enum PkgCommand { Install(install::Args), @@ -36,7 +33,6 @@ pub enum PkgCommand { Search(search::Args), } -// ── Dispatch ────────────────────────────────────────────────────────────────── pub fn run(args: PkgArgs, context: CommandContext) -> Result<()> { match args.command { PkgCommand::Install(args) => install::run(args, context), diff --git a/user/upac-cli/src/commands/package/remove.rs b/user/upac-cli/src/commands/package/remove.rs index eb3f5918..053807b8 100644 --- a/user/upac-cli/src/commands/package/remove.rs +++ b/user/upac-cli/src/commands/package/remove.rs @@ -3,8 +3,8 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; use std::io::{self, Write}; +use std::ptr::null_mut; use anyhow::Result; @@ -15,15 +15,17 @@ use colored::Colorize; use i18n_embed_fl::fl; use upac_abi::error::ErrorDomain; -use upac_abi::request::{CListPackagesRequest, CRequestBase, CUninstallRequest}; +use upac_abi::request::{CListPackagesRequest, CUninstallRequest}; use upac_abi::types::CSlice; +use upac_types::package::PackageInfo; +use upac_types::request::{ListPackagesRequest, RequestBase, UninstallRequest}; +use upac_types::settings::RuntimeSettings; + use crate::cancel_token_ptr; use crate::locale::LOADER; use crate::types::CommandContext; -use crate::types::abi::{ - borrowed_vec, invoke, invoke_with_response, optional_slice, package_info, request_base, slice_from_cstr, -}; +use crate::types::abi::{invoke, invoke_with_response}; use crate::types::progress::{ProgressState, on_progress}; #[cfg(test)] @@ -31,7 +33,6 @@ use crate::types::progress::{ProgressState, on_progress}; mod tests; type InstalledEntry = (String, String, Option); -type ResolvedEntry = (CString, CString, Option); #[derive(ClapArgs)] pub struct Args { @@ -89,12 +90,19 @@ struct RemoveMachine { ctx: CommandContext, installed: Vec, - resolved: Vec, + resolved: Vec, } impl RemoveMachine { fn state_listing(&mut self) -> Result { - let request = CListPackagesRequest::new(request_base()); + let request: CListPackagesRequest = ListPackagesRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (self.ctx.lib.ro.list_packages)(request, out, error) })?; @@ -124,14 +132,12 @@ impl RemoveMachine { .args .names .iter() - .map(|name| { - Ok(( - CString::new(name.as_str())?, - CString::new(arch)?, - self.args.arch_sub.as_deref().map(CString::new).transpose()?, - )) + .map(|name| PackageInfo { + name: name.clone(), + arch: arch.to_owned(), + arch_sub: self.args.arch_sub.clone(), }) - .collect::>()?; + .collect(); Ok(State::Removing) } @@ -142,11 +148,11 @@ impl RemoveMachine { .iter() .map(|name| { let (arch, arch_sub) = find_installed(&self.installed, name)?; - Ok(( - CString::new(name.as_str())?, - CString::new(arch)?, - arch_sub.as_deref().map(CString::new).transpose()?, - )) + Ok(PackageInfo { + name: name.clone(), + arch, + arch_sub, + }) }) .collect::>()?; Ok(State::Removing) @@ -155,28 +161,29 @@ impl RemoveMachine { fn state_removing(&mut self) -> Result { let symbols = self.ctx.lib.require_write()?; - let subject = CString::new("remove")?; - let message = self.args.message.as_deref().map(CString::new).transpose()?; - let boot_plugin = self.args.boot.as_deref().map(CString::new).transpose()?; - - let packages: Vec<_> = self - .resolved - .iter() - .map(|(name, arch, arch_sub)| package_info(name, arch, arch_sub.as_ref())) - .collect(); - let mut progress = ProgressState::new(ErrorDomain::Uninstall); - let base = CRequestBase::new(Some(on_progress), progress.ctx_ptr(), cancel_token_ptr()); - - let request = CUninstallRequest::new( - base, - slice_from_cstr(&self.ctx.tmp_path), - slice_from_cstr(&subject), - optional_slice(message.as_ref()), - borrowed_vec(&packages), - optional_slice(boot_plugin.as_ref()), - self.args.purge, - ); + + let boot_plugin = self + .args + .boot + .clone() + .or_else(|| RuntimeSettings::load().boot.plugin) + .ok_or_else(|| anyhow::anyhow!(fl!(LOADER, "err-boot-plugin-required")))?; + + let request: CUninstallRequest = UninstallRequest { + base: RequestBase { + on_hook: Some(on_progress), + hook_ctx: progress.ctx_ptr(), + cancel_token: cancel_token_ptr(), + }, + tmp_path: self.ctx.tmp_path.to_string_lossy().into_owned(), + subject: "remove".to_owned(), + message: self.args.message.clone(), + packages: std::mem::take(&mut self.resolved), + boot_plugin, + purge: self.args.purge, + } + .into(); let result = invoke(|error| unsafe { (symbols.uninstall)(request, error) }); progress.finish(); diff --git a/user/upac-cli/src/commands/package/search.rs b/user/upac-cli/src/commands/package/search.rs index 68b54ffd..b0eec814 100644 --- a/user/upac-cli/src/commands/package/search.rs +++ b/user/upac-cli/src/commands/package/search.rs @@ -3,7 +3,7 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; @@ -13,10 +13,14 @@ use i18n_embed_fl::fl; use upac_abi::request::{CSearchInMetaRequest, CSearchMetaRequest}; +use upac_types::package::PackageInfo; +use upac_types::request::{RequestBase, SearchInMetaRequest, SearchMetaRequest}; + +use crate::cancel_token_ptr; use crate::commands::display::{PackageField, PackageFormatter}; use crate::locale::LOADER; use crate::types::CommandContext; -use crate::types::abi::{invoke_with_response, package_info, request_base, slice_from_cstr}; +use crate::types::abi::invoke_with_response; #[cfg(test)] #[path = "../../../tests/inline/search.rs"] @@ -56,7 +60,6 @@ pub struct Args { } pub fn run(args: Args, ctx: CommandContext) -> Result<()> { - let query = CString::new(args.query.as_str())?; let extra_fields = build_extra_fields(&args); match args.package.as_deref() { @@ -65,12 +68,21 @@ pub fn run(args: Args, ctx: CommandContext) -> Result<()> { anyhow::bail!(fl!(LOADER, "err-invalid-entry")); }; - let package_name = CString::new(package)?; - let package_arch = CString::new(arch)?; - let package_arch_sub = args.package_arch_sub.as_deref().map(CString::new).transpose()?; - let package = package_info(&package_name, &package_arch, package_arch_sub.as_ref()); - - let request = CSearchInMetaRequest::new(request_base(), package, slice_from_cstr(&query), args.regex); + let request: CSearchInMetaRequest = SearchInMetaRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + package: PackageInfo { + name: package.to_owned(), + arch: arch.to_owned(), + arch_sub: args.package_arch_sub.clone(), + }, + search: args.query.clone(), + is_regex: args.regex, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.search_in_meta)(request, out, error) })?; @@ -83,8 +95,18 @@ pub fn run(args: Args, ctx: CommandContext) -> Result<()> { unsafe { response.free() }; } + None => { - let request = CSearchMetaRequest::new(request_base(), slice_from_cstr(&query), args.regex); + let request: CSearchMetaRequest = SearchMetaRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + search: args.query.clone(), + is_regex: args.regex, + } + .into(); let response = invoke_with_response(|out, error| unsafe { (ctx.lib.ro.search_meta)(request, out, error) })?; PackageFormatter { diff --git a/user/upac-cli/src/commands/package/update.rs b/user/upac-cli/src/commands/package/update.rs index f2d8ae26..f499d0c7 100644 --- a/user/upac-cli/src/commands/package/update.rs +++ b/user/upac-cli/src/commands/package/update.rs @@ -3,7 +3,6 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; use std::fs::canonicalize; use anyhow::Result; @@ -13,18 +12,19 @@ use clap::Args as ClapArgs; use i18n_embed_fl::fl; use upac_abi::error::ErrorDomain; -use upac_abi::request::{CRequestBase, CUpdateRequest}; +use upac_abi::request::CUpdateRequest; + +use upac_types::request::{RequestBase, UpdateRequest}; +use upac_types::settings::RuntimeSettings; use crate::cancel_token_ptr; use crate::locale::LOADER; use crate::types::CommandContext; -use crate::types::abi::{borrowed_vec, invoke, optional_slice, slice_from_cstr}; +use crate::types::abi::invoke; use crate::types::progress::{ProgressState, on_progress}; #[derive(ClapArgs)] pub struct Args { - // Required flag, not a positional: keeps the positional slot free for a future - // name-based network update (e.g. `up pkg update foo`), separate from this local-file path. #[arg(short, long = "file", required = true, num_args = 1..)] pub files: Vec, #[arg(short, long)] @@ -40,32 +40,35 @@ pub struct Args { pub fn run(args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let subject = CString::new("update")?; - let message = args.message.map(CString::new).transpose()?; - let boot_plugin = args.boot.map(CString::new).transpose()?; - - let mut paths = Vec::with_capacity(args.files.len()); + let mut packages = Vec::with_capacity(args.files.len()); for file_path in &args.files { let absolute = canonicalize(file_path).map_err(|_| anyhow::anyhow!("{}: {file_path}", fl!(LOADER, "err-not-found")))?; - paths.push(CString::new(absolute.to_string_lossy().as_ref())?); + packages.push(absolute.to_string_lossy().into_owned()); } - let path_slices: Vec<_> = paths.iter().map(slice_from_cstr).collect(); - let mut progress = ProgressState::new(ErrorDomain::Update); - let base = CRequestBase::new(Some(on_progress), progress.ctx_ptr(), cancel_token_ptr()); - let request = CUpdateRequest::new( - base, - slice_from_cstr(&ctx.tmp_path), - slice_from_cstr(&subject), - optional_slice(message.as_ref()), - borrowed_vec(&path_slices), - optional_slice(boot_plugin.as_ref()), - args.allow_downgrade, - !args.no_conflict_files, - ); + let boot_plugin = args + .boot + .or_else(|| RuntimeSettings::load().boot.plugin) + .ok_or_else(|| anyhow::anyhow!(fl!(LOADER, "err-boot-plugin-required")))?; + + let request: CUpdateRequest = UpdateRequest { + base: RequestBase { + on_hook: Some(on_progress), + hook_ctx: progress.ctx_ptr(), + cancel_token: cancel_token_ptr(), + }, + tmp_path: ctx.tmp_path.to_string_lossy().into_owned(), + subject: "update".to_owned(), + message: args.message, + packages, + boot_plugin: boot_plugin, + allow_downgrade: args.allow_downgrade, + allow_conflict_files: !args.no_conflict_files, + } + .into(); let result = invoke(|error| unsafe { (symbols.update)(request, error) }); progress.finish(); diff --git a/user/upac-cli/src/commands/rollback.rs b/user/upac-cli/src/commands/rollback.rs index c0f80e70..5f74899c 100644 --- a/user/upac-cli/src/commands/rollback.rs +++ b/user/upac-cli/src/commands/rollback.rs @@ -3,16 +3,23 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; +use std::ptr::null_mut; use anyhow::Result; use clap::Args as ClapArgs; +use i18n_embed_fl::fl; + use upac_abi::request::CRollbackRequest; +use upac_types::request::{RequestBase, RollbackRequest}; +use upac_types::settings::RuntimeSettings; + +use crate::cancel_token_ptr; +use crate::locale::LOADER; use crate::types::CommandContext; -use crate::types::abi::{invoke, optional_slice, request_base, slice_from_cstr}; +use crate::types::abi::invoke; #[derive(ClapArgs)] pub struct Args { @@ -23,15 +30,23 @@ pub struct Args { pub fn run(args: Args, ctx: CommandContext) -> Result<()> { let symbols = ctx.lib.require_write()?; - let config_digest = CString::new(args.commit)?; - let boot_plugin = args.boot.map(CString::new).transpose()?; - - let request = CRollbackRequest::new( - request_base(), - slice_from_cstr(&ctx.tmp_path), - slice_from_cstr(&config_digest), - optional_slice(boot_plugin.as_ref()), - ); + + let boot_plugin = args + .boot + .or_else(|| RuntimeSettings::load().boot.plugin) + .ok_or_else(|| anyhow::anyhow!(fl!(LOADER, "err-boot-plugin-required")))?; + + let request: CRollbackRequest = RollbackRequest { + base: RequestBase { + on_hook: None, + hook_ctx: null_mut(), + cancel_token: cancel_token_ptr(), + }, + tmp_path: ctx.tmp_path.to_string_lossy().into_owned(), + config_digest: args.commit, + boot_plugin: boot_plugin, + } + .into(); invoke(|error| unsafe { (symbols.rollback)(request, error) }) } diff --git a/user/upac-cli/src/libcore.rs b/user/upac-cli/src/libcore.rs index a091b154..a0fa14dc 100644 --- a/user/upac-cli/src/libcore.rs +++ b/user/upac-cli/src/libcore.rs @@ -3,13 +3,13 @@ // // SPDX-License-Identifier: GPL-3.0-only -// ── Imports ───────────────────────────────────────────────────────────────── use anyhow::Result; use i18n_embed_fl::fl; use nix::unistd::Uid; +use upac_abi::LIB_ABI_VERSION; use upac_abi::error::CError; use upac_abi::hook::CancelToken; use upac_abi::request::{ @@ -24,16 +24,17 @@ use upac_abi::response::{ CSearchInPackageFilesResponse, CSearchMetaResponse, }; +#[cfg(feature = "dynamic-plugins")] +use libloading::Library; + +use super::types::errors::{AbiMismatch, LibError}; + use crate::locale::LOADER; -use crate::types::errors::{AbiMismatch, LibError}; #[cfg(test)] #[path = "../tests/inline/libcore.rs"] mod tests; -#[cfg(feature = "dynamic-plugins")] -use libloading::Library; - #[cfg(feature = "static-link")] use upac::export::mutated::{ commit::commit, files::files, gc::gc, installer::install, mime::mime, pin::pin_deploy, rollback::rollback, @@ -49,7 +50,6 @@ use upac::export::unmutated::{ #[cfg(feature = "static-link")] use upac::export::{cancel, version_abi}; -// ── Static-link symbol construction ──────────────────────────────────────── #[cfg(feature = "static-link")] impl RoSymbols { fn from_static() -> Self { @@ -111,7 +111,6 @@ impl Lib { } } -// ── Dynamic-link symbol loading ──────────────────────────────────────────── #[cfg(feature = "dynamic-plugins")] pub trait LoadLibrarySymbols: Sized { fn load(lib: &Library) -> Result; @@ -170,10 +169,10 @@ impl Lib { }; let abi_version = unsafe { (lib.version_abi)() }; - if abi_version != upac_abi::ABI_VERSION { + if abi_version != LIB_ABI_VERSION { let err = AbiMismatch { got: abi_version, - expected: upac_abi::ABI_VERSION, + expected: LIB_ABI_VERSION, }; return Err(err.into()); @@ -194,7 +193,6 @@ impl Lib { } } -// ── Read-only symbols ───────────────────────────────────────────────────────── pub struct RoSymbols { pub list_packages: unsafe extern "C" fn(CListPackagesRequest, *mut CListPackagesResponse, *mut CError) -> i32, pub search_meta: unsafe extern "C" fn(CSearchMetaRequest, *mut CSearchMetaResponse, *mut CError) -> i32, @@ -211,7 +209,6 @@ pub struct RoSymbols { unsafe extern "C" fn(CSearchInPackageFilesRequest, *mut CSearchInPackageFilesResponse, *mut CError) -> i32, } -// ── Mutating symbols ────────────────────────────────────────────────────────── pub struct RwSymbols { pub install: unsafe extern "C" fn(CInstallRequest, *mut CError) -> i32, pub update: unsafe extern "C" fn(CUpdateRequest, *mut CError) -> i32, @@ -224,7 +221,6 @@ pub struct RwSymbols { pub pin_deploy: unsafe extern "C" fn(CPinRequest, *mut CError) -> i32, } -// ── Wrapper around either libupac.so or the statically linked upac-lib ────── pub struct Lib { pub ro: RoSymbols, pub rw: RwSymbols, @@ -236,9 +232,6 @@ pub struct Lib { } impl Lib { - /// Gates access to the mutating symbol table behind an effective-root check — call sites for - /// install/update/uninstall/commit/rollback/files/gc/mime go through here instead of reading - /// `self.rw` directly, so the check can't be forgotten at a new call site. pub fn require_write(&self) -> Result<&RwSymbols> { if !Uid::effective().is_root() { anyhow::bail!(fl!(LOADER, "err-requires-root")); diff --git a/user/upac-cli/src/main.rs b/user/upac-cli/src/main.rs index 67254c61..ec4f1e38 100644 --- a/user/upac-cli/src/main.rs +++ b/user/upac-cli/src/main.rs @@ -3,7 +3,6 @@ // // SPDX-License-Identifier: GPL-3.0-only -// ── Imports ───────────────────────────────────────────────────────────────── use std::process::ExitCode; use std::ptr::addr_of_mut; use std::sync::Arc; @@ -18,11 +17,11 @@ use i18n_embed_fl::fl; use upac_abi::hook::CancelToken; -use crate::commands::commit::CommitArgs; -use crate::commands::file::FileArgs; -use crate::commands::package::PkgArgs; -use crate::libcore::Lib; -use crate::types::CommandContext; +use self::commands::commit::CommitArgs; +use self::commands::file::FileArgs; +use self::commands::package::PkgArgs; +use self::libcore::Lib; +use self::types::CommandContext; mod libcore; mod layout { @@ -48,7 +47,6 @@ pub(crate) fn cancel_token_ptr() -> *mut CancelToken { addr_of_mut!(CANCEL_TOKEN) } -// ── CLI arguments ───────────────────────────────────────────────────────────── #[derive(Parser)] #[command(author, version, about)] enum Command { @@ -61,7 +59,6 @@ enum Command { Rollback(commands::rollback::Args), } -// ── Entry points ─────────────────────────────────────────────────────────────── fn main() -> ExitCode { locale::init(); @@ -78,6 +75,7 @@ fn run() -> Result<()> { let lib = Arc::new(Lib::load()?); let lib_cancel = Arc::clone(&lib); + ctrlc::set_handler(move || { unsafe { (lib_cancel.cancel)(cancel_token_ptr()) }; })?; diff --git a/user/upac-cli/src/types/abi.rs b/user/upac-cli/src/types/abi.rs index b64a30da..b13bebae 100644 --- a/user/upac-cli/src/types/abi.rs +++ b/user/upac-cli/src/types/abi.rs @@ -3,20 +3,15 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; use std::mem::MaybeUninit; -use std::ptr::{null, null_mut}; use anyhow::Result; use upac_abi::DiffFileSource; use upac_abi::error::CError; -use upac_abi::package::CPackageInfo; -use upac_abi::request::CRequestBase; -use upac_abi::types::{CBorrowed, CSlice, CVec}; +use upac_abi::types::CValidatable; -use crate::cancel_token_ptr; -use crate::types::errors::LibError; +use crate::types::errors::{InvalidResponse, LibError}; #[cfg(test)] #[path = "../../tests/inline/abi.rs"] @@ -37,47 +32,28 @@ impl From for DiffFileSource { } } -pub fn request_base() -> CRequestBase { - CRequestBase::new(None, null_mut(), cancel_token_ptr()) -} - -pub fn slice_from_cstr(value: &CString) -> CSlice { - CSlice { - ptr: value.as_ptr().cast(), - len: value.as_bytes().len(), - } -} - -pub fn empty_slice() -> CSlice { - CSlice { ptr: null(), len: 0 } -} - -pub fn optional_slice(value: Option<&CString>) -> CSlice { - match value { - Some(value) => slice_from_cstr(value), - None => empty_slice(), - } -} - -pub fn package_info(name: &CString, arch: &CString, arch_sub: Option<&CString>) -> CPackageInfo { - CPackageInfo::new(slice_from_cstr(name), slice_from_cstr(arch), optional_slice(arch_sub)) -} - -pub fn borrowed_vec(items: &[T]) -> CVec { - CVec::from_borrowed(items) -} - pub fn invoke(call: impl FnOnce(*mut CError) -> i32) -> Result<()> { let mut error = MaybeUninit::uninit(); + let code = call(error.as_mut_ptr()); + unsafe { LibError::check(code, error.as_ptr())? }; + Ok(()) } -pub fn invoke_with_response(call: impl FnOnce(*mut R, *mut CError) -> i32) -> Result { +pub fn invoke_with_response(call: impl FnOnce(*mut R, *mut CError) -> i32) -> Result { let mut response = MaybeUninit::zeroed(); + let mut error = MaybeUninit::uninit(); + let code = call(response.as_mut_ptr(), error.as_mut_ptr()); + unsafe { LibError::check(code, error.as_ptr())? }; - Ok(unsafe { response.assume_init() }) + + let response = unsafe { response.assume_init() }; + + unsafe { response.validate() }.map_err(|error| InvalidResponse { error })?; + + Ok(response) } diff --git a/user/upac-cli/src/types/errors.rs b/user/upac-cli/src/types/errors.rs index 3ff422dd..3bab1099 100644 --- a/user/upac-cli/src/types/errors.rs +++ b/user/upac-cli/src/types/errors.rs @@ -43,6 +43,19 @@ impl Display for AbiMismatch { impl Error for AbiMismatch {} +#[derive(Debug)] +pub struct InvalidResponse { + pub error: ErrorKind, +} + +impl Display for InvalidResponse { + fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "{}", error_kind_message(self.error)) + } +} + +impl Error for InvalidResponse {} + pub(crate) struct StageName { domain: ErrorDomain, state: u32, @@ -92,6 +105,24 @@ impl Display for StageName { } } +fn error_kind_message(kind: ErrorKind) -> String { + match kind { + ErrorKind::Unexpected => fl!(LOADER, "err-unexpected"), + ErrorKind::OutOfMemory => fl!(LOADER, "err-oom"), + ErrorKind::NotFound => fl!(LOADER, "err-not-found"), + ErrorKind::AlreadyExists => fl!(LOADER, "err-already-exists"), + ErrorKind::PermissionDenied => fl!(LOADER, "err-permission-denied"), + ErrorKind::InvalidPath => fl!(LOADER, "err-invalid-path"), + ErrorKind::NoSpaceLeft => fl!(LOADER, "err-no-space"), + ErrorKind::Cancelled => fl!(LOADER, "err-cancelled"), + ErrorKind::ReadFailed => fl!(LOADER, "err-read"), + ErrorKind::WriteFailed => fl!(LOADER, "err-write"), + ErrorKind::NotInitialized => fl!(LOADER, "err-not-initialized"), + ErrorKind::AbiMismatch => fl!(LOADER, "err-abi-mismatch"), + ErrorKind::InvalidEntry => fl!(LOADER, "err-invalid-entry"), + } +} + #[derive(Debug)] pub struct LibError { pub error: CError, @@ -99,25 +130,10 @@ pub struct LibError { impl Display for LibError { fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { - let message = match self.error.error { - ErrorKind::Unexpected => fl!(LOADER, "err-unexpected"), - ErrorKind::OutOfMemory => fl!(LOADER, "err-oom"), - ErrorKind::NotFound => fl!(LOADER, "err-not-found"), - ErrorKind::AlreadyExists => fl!(LOADER, "err-already-exists"), - ErrorKind::PermissionDenied => fl!(LOADER, "err-permission-denied"), - ErrorKind::InvalidPath => fl!(LOADER, "err-invalid-path"), - ErrorKind::NoSpaceLeft => fl!(LOADER, "err-no-space"), - ErrorKind::Cancelled => fl!(LOADER, "err-cancelled"), - ErrorKind::ReadFailed => fl!(LOADER, "err-read"), - ErrorKind::WriteFailed => fl!(LOADER, "err-write"), - ErrorKind::NotInitialized => fl!(LOADER, "err-not-initialized"), - ErrorKind::AbiMismatch => fl!(LOADER, "err-abi-mismatch"), - ErrorKind::InvalidEntry => fl!(LOADER, "err-invalid-entry"), - }; write!( formatter, "{} ({:?}: {})", - message, + error_kind_message(self.error.error), self.error.domain, StageName::from(&self.error) ) diff --git a/user/upac-cli/src/types/mod.rs b/user/upac-cli/src/types/mod.rs index 87730c79..3c43f1f5 100644 --- a/user/upac-cli/src/types/mod.rs +++ b/user/upac-cli/src/types/mod.rs @@ -9,7 +9,7 @@ use std::env::temp_dir; use std::ffi::CString; use std::sync::Arc; -use crate::libcore::Lib; +use super::libcore::Lib; pub mod abi; pub mod errors; diff --git a/user/upac-cli/src/types/progress.rs b/user/upac-cli/src/types/progress.rs index 07563e9d..1fc53775 100644 --- a/user/upac-cli/src/types/progress.rs +++ b/user/upac-cli/src/types/progress.rs @@ -35,8 +35,9 @@ pub unsafe extern "C" fn on_progress(event: *const CProgressEvent, ctx: *mut c_v pub struct ProgressState { pub(crate) bar: ProgressBar, - domain: ErrorDomain, pub(crate) is_bar: bool, + + domain: ErrorDomain, settings: ProgressSettings, } diff --git a/user/upac-cli/tests/inline/abi.rs b/user/upac-cli/tests/inline/abi.rs index b6410467..6774d4fc 100644 --- a/user/upac-cli/tests/inline/abi.rs +++ b/user/upac-cli/tests/inline/abi.rs @@ -3,92 +3,13 @@ // // SPDX-License-Identifier: GPL-3.0-only -use std::ffi::CString; - use upac_abi::error::{CError, ErrorDomain, ErrorKind}; -use upac_abi::types::CSlice; +use upac_abi::package::CPackageMeta; +use upac_abi::response::CListPackagesResponse; +use upac_abi::types::{COwned, CVec}; use crate::locale; -use crate::types::abi::{ - borrowed_vec, empty_slice, invoke, invoke_with_response, optional_slice, package_info, request_base, - slice_from_cstr, -}; - -fn as_str(slice: &CSlice) -> &str { - <&str>::try_from(slice).unwrap() -} - -#[test] -fn slice_from_cstr_preserves_the_bytes() { - let value = CString::new("hello").unwrap(); - - assert_eq!(as_str(&slice_from_cstr(&value)), "hello"); -} - -#[test] -fn empty_slice_is_null_and_zero_length() { - let slice = empty_slice(); - - assert!(slice.ptr.is_null()); - assert_eq!(slice.len, 0); -} - -#[test] -fn optional_slice_some_preserves_the_bytes() { - let value = CString::new("hello").unwrap(); - - assert_eq!(as_str(&optional_slice(Some(&value))), "hello"); -} - -#[test] -fn optional_slice_none_is_empty() { - let slice = optional_slice(None); - - assert!(slice.ptr.is_null()); - assert_eq!(slice.len, 0); -} - -#[test] -fn package_info_builds_the_expected_fields() { - let name = CString::new("upac").unwrap(); - let arch = CString::new("x86_64").unwrap(); - let arch_sub = CString::new("v3").unwrap(); - - let info = package_info(&name, &arch, Some(&arch_sub)); - - assert_eq!(as_str(&info.name), "upac"); - assert_eq!(as_str(&info.arch), "x86_64"); - assert_eq!(as_str(&info.arch_sub), "v3"); -} - -#[test] -fn package_info_without_arch_sub_leaves_it_empty() { - let name = CString::new("upac").unwrap(); - let arch = CString::new("x86_64").unwrap(); - - let info = package_info(&name, &arch, None); - - assert!(info.arch_sub.ptr.is_null()); -} - -#[test] -fn borrowed_vec_wraps_the_slice_without_copying() { - let items = [1u32, 2, 3]; - - let vec = borrowed_vec(&items); - - assert_eq!(vec.len, 3); - assert_eq!(vec.ptr, items.as_ptr() as *mut u32); -} - -#[test] -fn request_base_has_no_hook_and_a_non_null_cancel_token() { - let base = request_base(); - - assert!(base.on_hook.is_none()); - assert!(base.hook_ctx.is_null()); - assert!(!base.cancel_token.is_null()); -} +use crate::types::abi::{invoke, invoke_with_response}; #[test] fn invoke_returns_ok_on_a_zero_code() { @@ -112,20 +33,20 @@ fn invoke_propagates_the_localized_error_on_a_nonzero_code() { } #[test] -fn invoke_with_response_returns_the_response_on_a_zero_code() { - let result = invoke_with_response(|response: *mut u32, _error| { - unsafe { *response = 42 }; +fn invoke_with_response_returns_the_validated_response_on_a_zero_code() { + let result = invoke_with_response(|response: *mut CListPackagesResponse, _error| { + unsafe { *response = CListPackagesResponse::new(CVec::from_owned(Vec::::new())) }; 0 }); - assert_eq!(result.unwrap(), 42); + assert!(result.is_ok()); } #[test] fn invoke_with_response_propagates_the_localized_error_on_a_nonzero_code() { locale::init_for_test(); - let result = invoke_with_response(|_response: *mut u32, error| unsafe { + let result = invoke_with_response(|_response: *mut CListPackagesResponse, error| unsafe { *error = CError { domain: ErrorDomain::Install, state: 0, @@ -134,5 +55,12 @@ fn invoke_with_response_propagates_the_localized_error_on_a_nonzero_code() { 1 }); - assert_eq!(result.unwrap_err().to_string(), "File not found (Install: Pre-hooks)"); + assert_eq!(result.err().unwrap().to_string(), "File not found (Install: Pre-hooks)"); +} + +#[test] +fn invoke_with_response_rejects_an_unvalidated_response() { + let result = invoke_with_response(|_response: *mut CListPackagesResponse, _error| 0); + + assert!(result.is_err()); } diff --git a/user/upac-cli/tests/inline/progress.rs b/user/upac-cli/tests/inline/progress.rs index 2a6d7f4b..840bae73 100644 --- a/user/upac-cli/tests/inline/progress.rs +++ b/user/upac-cli/tests/inline/progress.rs @@ -8,10 +8,9 @@ use std::mem::size_of; use upac_abi::error::ErrorDomain; use upac_abi::hook::CProgressEvent; -use upac_abi::types::CSlice; +use upac_abi::types::{CBorrowed, CSlice}; use crate::locale; -use crate::types::abi::{empty_slice, slice_from_cstr}; use crate::types::progress::ProgressState; fn event(stage: u32, current: u64, total: u64, subject: CSlice) -> CProgressEvent { @@ -30,7 +29,7 @@ fn apply_with_zero_total_stays_on_spinner() { locale::init_for_test(); let mut state = ProgressState::new(ErrorDomain::Install); - state.apply(&event(0, 0, 0, empty_slice())); + state.apply(&event(0, 0, 0, CSlice::from_slice(None))); assert!(!state.is_bar); assert_eq!(state.bar.message(), "Pre-hooks"); @@ -41,7 +40,7 @@ fn apply_with_nonzero_total_switches_to_bar_and_sets_position() { locale::init_for_test(); let mut state = ProgressState::new(ErrorDomain::Install); - state.apply(&event(0, 3, 10, empty_slice())); + state.apply(&event(0, 3, 10, CSlice::from_slice(None))); assert!(state.is_bar); assert_eq!(state.bar.length(), Some(10)); @@ -54,7 +53,7 @@ fn apply_includes_subject_in_message_when_present() { let mut state = ProgressState::new(ErrorDomain::Install); let subject = CString::new("foo.txt").unwrap(); - state.apply(&event(0, 0, 0, slice_from_cstr(&subject))); + state.apply(&event(0, 0, 0, CSlice::from_borrowed(subject.as_bytes()))); assert_eq!(state.bar.message(), "Pre-hooks: foo.txt"); }