diff --git a/.github/workflows/ci-zig.yml b/.github/workflows/ci-zig.yml deleted file mode 100644 index 9987928..0000000 --- a/.github/workflows/ci-zig.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Zig CI -on: - push: - branches: [main] - pull_request: - branches: [main] - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.head.ref || github.ref_name }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - build: - name: Zig Build - runs-on: ubuntu-latest - strategy: - matrix: - component: ["alpm", "rpm", "deb", "xbps"] - steps: - - uses: actions/checkout@v4 - - - name: Install dependencies - run: sudo apt-get update && sudo apt-get install -y libarchive-dev pkg-config - - - name: Setup Zig - uses: mlugg/setup-zig@v2 - with: - version: 0.16.0 - - - name: Check formatting (${{ matrix.component }}) - working-directory: decoders/${{ matrix.component }} - run: zig fmt --check . - - - name: Build (${{ matrix.component }}) - working-directory: decoders/${{ matrix.component }} - run: zig build diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 32635ce..120f950 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,7 +56,7 @@ Every new source file needs an SPDX header matching its directory's license, e.g ```rust // SPDX-FileCopyrightText: 2026 // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception ``` For files where a comment header doesn't make sense (e.g. Markdown, TOML), add an annotation to diff --git a/Cargo.toml b/Cargo.toml index 038b8ca..b13a157 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ [workspace] resolver = "3" -members = ["user/upac-cli", "user/sign-cli", "user/setup-cli", "lib/macro", "lib/abi", "lib/lib" , "lib/pki", "lib/types", "lib/setup", "booters/uki", "booters/systemd-boot", "booters/grub", "booters/refind"] +members = ["user/upac-cli", "user/sign-cli", "user/setup-cli", "lib/macro", "lib/abi", "lib/lib" , "lib/pki", "lib/types", "lib/setup", "booters/uki", "booters/systemd-boot", "booters/grub", "booters/refind", "decoders/alpm", "decoders/deb", "decoders/rpm", "decoders/xbps"] [workspace.package] version = "0.2.0" edition = "2024" rust-version = "1.98" -license = "LGPL-3.0-or-later" +license = "LGPL-3.0-or-later WITH LGPL-3.0-linking-exception" license-file = "LICENSES/LGPL-3.0-or-later" readme = "README.md" @@ -38,6 +38,10 @@ upac-uki = { path = "booters/uki", default-features = false } upac-systemd-boot = { path = "booters/systemd-boot", default-features = false } upac-grub = { path = "booters/grub", default-features = false } upac-refind = { path = "booters/refind", default-features = false } +upac-decoders-alpm = { path = "decoders/alpm", package = "alpm", default-features = false } +upac-decoders-deb = { path = "decoders/deb", package = "deb", default-features = false } +upac-decoders-rpm = { path = "decoders/rpm", package = "rpm", default-features = false } +upac-decoders-xbps = { path = "decoders/xbps", package = "xbps", default-features = false } composefs-oci = { version = "0.9.0", features = ["composefs-boot", "boot", "tar"] } nix = { version = "0.31.3", features = ["mount", "sched", "fs", "net"] } @@ -60,6 +64,12 @@ composefs-boot = "0.9.0" indicatif = "0.18.4" efivar = "2.0.0" tempfile = "3" +ar = "0.9.0" +cpio = "0.4.1" +flate2 = "1.1.9" +tar = "0.4.46" +xz2 = "0.1.7" +zstd = "0.13.3" [profile.dev] opt-level = 0 diff --git a/LICENSES/LGPL-3.0-linking-exception.txt b/LICENSES/LGPL-3.0-linking-exception.txt new file mode 100644 index 0000000..186456f --- /dev/null +++ b/LICENSES/LGPL-3.0-linking-exception.txt @@ -0,0 +1,16 @@ +As a special exception to the GNU Lesser General Public License version 3 +("LGPL3"), the copyright holders of this Library give you permission to +convey to a third party a Combined Work that links statically or dynamically +to this Library without providing any Minimal Corresponding Source or +Minimal Application Code as set out in 4d or providing the installation +information set out in section 4e, provided that you comply with the other +provisions of LGPL3 and provided that you meet, for the Application the +terms and conditions of the license(s) which apply to the Application. + +Except as stated in this special exception, the provisions of LGPL3 will +continue to comply in full to this Library. If you modify this Library, you +may apply this exception to your version of this Library, but you are not +obliged to do so. If you do not wish to do so, delete this exception +statement from your version. This exception does not (and cannot) modify any +license terms which apply to the Application, with which you must still +comply. diff --git a/README.md b/README.md index 235cbd7..f2610ce 100644 --- a/README.md +++ b/README.md @@ -103,13 +103,13 @@ The core library exposes a C-compatible ABI through `libupac.so`. All strings cr ### Decoders (`decoders/`) -Decoders are separate shared libraries, still written in [Zig](https://ziglang.org/), that handle format-specific package unpacking. Each decoder receives a package path, an output directory, and a SHA-256 checksum; it verifies the checksum, extracts the package, parses the metadata, and returns a `PackageMeta` struct. +Decoders are separate shared libraries that handle format-specific package unpacking. Each decoder receives a package path, an output directory, and a SHA-256 checksum; it verifies the checksum, extracts the package, parses the metadata, and returns a `PackageMeta` struct, its dependencies, and any declarative (package-format-native) trigger names it declares. `alpm`, `deb` and `rpm` are written in [Rust](https://www.rust-lang.org/) (and can also be statically linked into `upac-lib` via the `builtin-alpm`/`builtin-deb`/`builtin-rpm` Cargo features); `xbps` is still [Zig](https://ziglang.org/) and is planned for the same Rust rewrite. | Decoder | Formats | Distributions | |---|---|---| -| **`libupac-alpm.so`** | `.pkg.tar.zst`, `.pkg.tar.xz`, `.pkg.tar.gz` | Arch Linux, Manjaro, etc. | -| **`libupac-rpm.so`** | `.rpm` | Fedora, RHEL, openSUSE, etc. | -| **`libupac-deb.so`** | `.deb` | Debian, Ubuntu, etc. | +| **`libupac_decoder_alpm.so`** | `.pkg.tar.zst`, `.pkg.tar.xz`, `.pkg.tar.gz` | Arch Linux, Manjaro, etc. | +| **`libupac_decoder_deb.so`** | `.deb` | Debian, Ubuntu, etc. | +| **`libupac_decoder_rpm.so`** | `.rpm` | Fedora, RHEL, openSUSE, etc. | | **`libupac-xbps.so`** | `.xbps` | Void Linux | Adding support for a new package format means writing a new decoder `.so` — the core library does not need to change. @@ -135,11 +135,14 @@ cargo build --workspace ### Static linking By default `up` dlopens `libupac.so` at startup, and `upac-lib` in turn dlopens boot-plugin -`.so`s (`booters/{uki,systemd-boot,grub,refind}`) described by on-disk manifests — this is the -`dynamic-plugins` feature, on by default on both `upac-cli` and `upac-lib`. +`.so`s (`booters/{uki,systemd-boot,grub,refind}`) and decoder `.so`s (`decoders/{alpm,deb}`, the +two currently written in Rust) described by on-disk manifests — this is the `dynamic-plugins` +feature, on by default on both `upac-cli` and `upac-lib`. Each crate also has a `static-link`/`builtin-*` axis for producing self-contained binaries with -no dlopen at all: +no dlopen at all — `builtin-uki`/`builtin-systemd-boot`/`builtin-grub`/`builtin-refind` (bundled +as `builtin-all`) for boot plugins, `builtin-alpm`/`builtin-deb`/`builtin-rpm` for decoders (no +bundle yet, only three decoders are Rust so far): ```sh # libupac.so with uki+systemd-boot+grub compiled in, `up` still dlopens it @@ -160,13 +163,21 @@ cargo hack build -p upac-cli -p upac-lib --feature-powerset \ --at-least-one-of dynamic-plugins,static-link ``` -Decoder static linking (the Zig side) isn't implemented yet — `builtin-decoders` exists as a -placeholder feature on `upac-lib` only. +Decoder static linking for the remaining Zig decoder (`xbps`) isn't implemented — `builtin-decoders` +still works, just without an `xbps` counterpart until it's rewritten in Rust. ### Build a decoder +`alpm`/`deb`/`rpm` are normal Rust workspace members, built along with everything else: + +```sh +cargo build -p alpm -p deb -p rpm +``` + +`xbps` is still Zig: + ```sh -cd decoders/alpm +cd decoders/xbps zig build ``` diff --git a/ROADMAP.md b/ROADMAP.md index 8ff0e63..ff28d58 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -19,7 +19,7 @@ Upac was originally OSTree-based. The project is migrated to composefs-backed at the read-only surface (`unmutated/`) and the composefs/database layers are done and tested. **Every mutating command's entire pipeline is real end-to-end** — install/update/uninstall/rollback/commit/files/gc all have genuine `transaction`/`merge`/`checkout`/`swap` bodies (no -`todo!()` anywhere in `lib/lib/src`), including the boot-plugin subsystem (pluggable UKI/systemd-boot/grub/rEFInd one-shot reboot selection), the real §5.1 3-way `/etc` merge with `.upac-new` conflict handling, and deploy retention/pinning. Package unpacking itself (`upac-lib`'s own decoder-plugin invocation, checksum, output-dir bookkeeping) is done (`PackageUnpacker` in `lib/lib/src/plugin/decoder/unpack.rs`), wired into both install's and update's `PreparationStage`. +`todo!()` anywhere in `lib/lib/src`), including the boot-plugin subsystem (pluggable UKI/systemd-boot/grub/rEFInd one-shot reboot selection), the real §5.1 3-way `/etc` merge with `.upac-new` conflict handling, and deploy retention/pinning. Package unpacking itself (`upac-lib`'s own decoder-plugin invocation, checksum, output-dir bookkeeping) is done (`PackageUnpacker` in `lib/lib/src/plugin/decoder/unpack.rs`), wired into both install's and update's `PreparationStage`. **Declarative (package-format-native) triggers are also fully wired**: `PackageUnpacker` carries each decoded package's `DeclarativeTrigger{format, triggers}` through `Context` as its own key (separate from `PackageTemp`, so `TransactionStage`'s `context.take::>()` doesn't remove it), install/update persist it into the new `packages_triggers` database table (`database::triggers`, keyed by the package's `Uuid`) alongside `PackageMeta`, uninstall reads it back from there (no re-`decode()`), and `HookStage` — via a new `Timing::Declarative` position on `PipelineTrigger`, firing right before each command's Post hooks — matches the stored trigger names against `build_trigger_table()`'s per-format table and runs whatever hooks matched. What's still explicitly out of scope for this phase, tracked separately below: network-based package fetching (§6) and the first-boot bootstrap installer (§5). @@ -39,13 +39,19 @@ resolution, etc. all live in `upac-lib`). `upac-lib`'s own paths are still baked in at build time via `lib.toml`. Separately, a genuine *runtime* config now exists: `upac_types::settings::RuntimeSettings` reads a shared `/etc/upac.d/upac.toml` (currently `[gc] retention_depth` and `[progress]` indicatif templates), parsed independently by both `upac-lib` and `upac-cli` (both link `upac_types` directly) — best-effort, missing/malformed file falls back to defaults. -## 4. Decoders (Zig, `decoders/`) — **in progress**. +## 4. Decoders (`decoders/`) — **in progress**. -Per-format decoder shared libraries (deb/rpm/alpm/xbps) exist with real backend logic; see `decoders/*/` and the README's Decoders section for the current contract (`package_path` + `output_dir` + SHA-256 checksum in, `PackageMeta` + dependencies out). Remaining work, not near-term: +Per-format decoder shared libraries (alpm/deb/rpm/xbps) exist with real backend logic; see `decoders/*/` and the README's Decoders section for the current contract (`package_path` + `output_dir` + SHA-256 checksum in, `PackageMeta` + dependencies + declarative triggers out). -- **Packaging pipeline.** Decoder manifest TOML files exist for all 4 decoders -(`decoders/{alpm,deb,rpm,xbps}/upac-*.toml`), but there's no PKGBUILD/spec/etc packaging, Mime types for alpm/xbps (`application/x-alpm-package`/`application/x-xbps-package`) are unofficial and vendor-prefixed (no shared-mime-info registration exists for either format, unlike deb/rpm). `up mime sync` is the mechanism meant to populate desktop integration once decoders are actually installed if any problems arise with automatic updating—not written yet either. -- **Full rewrite to Rust.** The 4 decoders are considered legacy: static linking was never started (separate mechanism from the Rust boot plugins' Cargo-feature approach — no Zig-side equivalent designed), and `parseVersion`/`CVersion` still populate the pre-`Ver`-brick 4-field shape, out of sync with the simplified Rust/C-ABI `{epoch, raw}`. Rather than reconciling that drift in Zig, the plan is a full rewrite of the decoders in Rust later — do not invest in fixing/extending the Zig side in the meantime. +**`alpm`, `deb` and `rpm` are done and rewritten in Rust** (`decoders/{alpm,deb,rpm}`, packages `alpm`/`deb`/`rpm`, `[lib] name = "upac_decoder_{alpm,deb,rpm}"` — decoder-before-format naming) — `xbps` is still the legacy Zig implementation and hasn't started its rewrite. `alpm`'s pipeline is `verify` (SHA-256) → `extract` (tar + gzip/xz/zstd, reads `.PKGINFO`/`.INSTALL` into memory) → `pkginfo::PkgInfo::parse` (`PackageMeta` + `Vec`) → `triggers::scan` (declarative trigger function names, matched against the shared `upac_types::DecoderTrigger` enum — `PreInstall`/`PostInstall`/`PreUpgrade`/`PostUpgrade`/`PreRemove`/`PostRemove`, the same 6-point set every Rust decoder maps its own native function/scriptlet names onto) — 12 tests (`decoders/alpm/tests/{pkginfo,triggers}.rs`; caught and fixed a real bug where a declarative-trigger name appearing as an indented call inside another function's body was misdetected as a top-level declaration). `deb`'s pipeline is `verify` (SHA-256) → `extract` (outer `ar` archive + nested `control.tar.*`/`data.tar.*`, gzip/xz/zstd) → `control::ControlFile::parse` (Debian control file `Key: value` fields + `Depends:` parsing — comma-separated AND groups, `|` OR-groups resolved by taking the first alternative) → `triggers::scan` (presence of the 4 maintainer-script files `preinst`/`postinst`/`prerm`/`postrm` — since dpkg has no separate install-vs-upgrade script, each file maps to two `DecoderTrigger` positions) — 11 tests (`decoders/deb/tests/{control,triggers}.rs`); license text is pulled from the payload's `usr/share/doc/*/copyright` DEP-5 file. `rpm`'s pipeline is `verify` (SHA-256) → `header::read` (own hand-rolled binary parser, generic over `Read + Seek`: skips the 96-byte lead, skips the signature section incl. its 8-byte alignment padding, reads the main header's tag index + data store) → `extract` (payload is `cpio` newc/crc format, not tar — dispatched by the header's own `PayloadFormat`/`PayloadCompressor` tags, gzip/xz/zstd, with a path-traversal guard hand-rolled since there's no `tar`-crate-style built-in protection for `cpio` entries) → `meta::build` (`PackageMeta` + `Vec` — `Requires*` array tags mapped via RPM's own `RPMSENSE_{LESS,GREATER,EQUAL}` bits to `CONSTRAINT_*`, `rpmlib()`-internal pseudo-dependencies filtered out) → `triggers::scan` (presence of the 4 scriptlet tags `%pre`/`%post`/`%preun`/`%postun` — same file-has-no-install-vs-upgrade-split reasoning as `deb`'s maintainer scripts) — 13 tests (`decoders/rpm/tests/{meta,triggers}.rs`, built against synthetic in-memory RPM header byte blobs, no real `.rpm` fixture file needed). Format-specific constants (archive entry names, `.PKGINFO`/control field keys, RPM header tag IDs, lifecycle script/function/scriptlet names) live in a shared `decoders/decoder.toml`, generated per-decoder-crate via `build.rs` — same pattern as `booters/booter.toml`, except each crate's `build.rs` now only reads its **own** section (`[alpm]`/`[deb]`/`[rpm]`/`[xbps]`) rather than generating a module for every section in the shared file. The dependency-version-constraint longest-prefix-match logic (`<=`/`>=`/etc. → `CONSTRAINT_*` bitflags) is shared as `upac_abi::decoder::parse_constraint_prefix`, with each decoder supplying its own operator table (syntax differs per format — alpm has bare `<`/`>`, deb only has `<<`/`>>`); `rpm` doesn't use this at all — its own `REQUIREFLAGS` tag is already a clean bitflag, no string parsing needed. + +All three Rust decoders can also be **statically linked** into `upac-lib`, mirroring the boot-plugin `builtin-*` Cargo-feature mechanism but adapted for extension-based dispatch (a decoder is selected by the package file's extension, not a runtime `probe()` call): `builtin-alpm`/`builtin-deb`/`builtin-rpm` features (`dep:upac-decoders-{alpm,deb,rpm}` + shared `builtin-decoders` gate) compile the decoder in as an `rlib`, with `format`/`extensions` also compiled in as constants (generated from each crate's own `upac-{alpm,deb,rpm}.toml` manifest via `build.rs`) — a `builtin-decoders`-only build touches no `/etc/upac.d/decoders/*.toml` manifest on disk at all. See `lib/lib/src/plugin/decoder/mod.rs`'s `static_decoders()`. + +Remaining work, not near-term: + +- **Packaging pipeline.** Decoder manifest TOML files exist for all 4 decoders +(`decoders/{alpm,deb,rpm,xbps}/upac-*.toml`), but there's no PKGBUILD/spec/etc packaging, Mime types for alpm/xbps (`application/x-alpm-package`/`application/x-xbps-package`) are unofficial and vendor-prefixed (no shared-mime-info registration exists for either format, unlike deb/rpm). `up mime sync` (populates desktop/mime-type integration from installed decoder manifests) is already fully implemented — see §1 — this item is just "no decoder is actually packaged/installed yet for it to sync against." +- **Full rewrite to Rust, `xbps` remaining.** Still legacy Zig: static linking was never started for it (no Zig-side equivalent of the Cargo-feature `builtin-*` mechanism), and its `parseVersion`/`CVersion` still populate the pre-`Ver`-brick 4-field shape, out of sync with the simplified Rust/C-ABI `{epoch, raw}`. Rather than reconciling that drift in Zig, the plan (per `alpm`/`deb`/`rpm`'s own precedent) is a full rewrite in Rust — do not invest in fixing/extending the Zig side in the meantime. Once rewritten, its `.so` output will be underscore-named (`libupac_decoder_xbps.so`, standard Rust `cdylib` naming) rather than the current hyphenated `libupac-xbps.so` — a cosmetic-only mismatch within the decoder family until a packaging step renames the artifact, not a functional issue. ## 5. Bootstrap / installer concerns — **done**. diff --git a/TODO.md b/TODO.md index e437a7a..6d96618 100644 --- a/TODO.md +++ b/TODO.md @@ -7,3 +7,4 @@ Near-term, concrete items. See `ROADMAP.md` for the bigger picture. - `user/upac-cli/data/` (`.desktop`, `upac-mime.xml`, `.policy`) reference `Icon=upac`/`icon_name=upac`, but there's no actual icon asset (SVG/PNG) yet, and no install step wiring it into `/usr/share/icons/hicolor/...`. Needs real artwork before packaging. + diff --git a/booters/booter.toml b/booters/booter.toml index 572c1dc..1b03a88 100644 --- a/booters/booter.toml +++ b/booters/booter.toml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2026 JustPav # SPDX-FileCopyrightText: 2026 SmoothTeam # -# SPDX-License-Identifier: LGPL-3.0-or-later +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception # Shared source of truth for external protocol constants used by more than one boot plugin — # none of this is upac's own ABI, so it doesn't belong in any single plugin crate. Each plugin's diff --git a/booters/grub/Cargo.toml b/booters/grub/Cargo.toml index fffc631..8ea7971 100644 --- a/booters/grub/Cargo.toml +++ b/booters/grub/Cargo.toml @@ -1,15 +1,25 @@ # SPDX-FileCopyrightText: 2026 JustPav # SPDX-FileCopyrightText: 2026 SmoothTeam # -# SPDX-License-Identifier: LGPL-3.0-or-later +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception [package] name = "upac-grub" +description = "GRUB (BLS via blscfg) boot plugin for upac: one-shot/persistent reboot selection via grub-reboot/grub-set-default" version.workspace = true + edition.workspace = true -repository.workspace = true +rust-version.workspace = true + license.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true + +keywords.workspace = true +categories.workspace = true + [lints] workspace = true diff --git a/booters/grub/build.rs b/booters/grub/build.rs index cbac554..23be7f0 100644 --- a/booters/grub/build.rs +++ b/booters/grub/build.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::env::var; use std::error::Error; diff --git a/booters/grub/src/backend.rs b/booters/grub/src/backend.rs index b2f0443..dc1ce59 100644 --- a/booters/grub/src/backend.rs +++ b/booters/grub/src/backend.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::io::ErrorKind as IoErrorKind; use std::path::Path; diff --git a/booters/grub/src/error.rs b/booters/grub/src/error.rs index a6f1627..5b1c7b2 100644 --- a/booters/grub/src/error.rs +++ b/booters/grub/src/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::io::{Error as IoError, ErrorKind as IoErrorKind}; diff --git a/booters/grub/src/lib.rs b/booters/grub/src/lib.rs index 35ab9e7..8dfcd1e 100644 --- a/booters/grub/src/lib.rs +++ b/booters/grub/src/lib.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::str::from_utf8; diff --git a/booters/refind/Cargo.toml b/booters/refind/Cargo.toml index 3e7574a..a09de84 100644 --- a/booters/refind/Cargo.toml +++ b/booters/refind/Cargo.toml @@ -1,15 +1,25 @@ # SPDX-FileCopyrightText: 2026 JustPav # SPDX-FileCopyrightText: 2026 SmoothTeam # -# SPDX-License-Identifier: LGPL-3.0-or-later +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception [package] name = "upac-refind" +description = "rEFInd boot plugin for upac: one-shot/persistent reboot selection via the PreviousBoot EFI variable" version.workspace = true + edition.workspace = true -repository.workspace = true +rust-version.workspace = true + license.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true + +keywords.workspace = true +categories.workspace = true + [lints] workspace = true @@ -20,8 +30,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] upac-abi = { workspace = true } efivar = { workspace = true } - -uuid = "1.24.1" +uuid = { workspace = true } [build-dependencies] toml = { workspace = true } diff --git a/booters/refind/build.rs b/booters/refind/build.rs index cbac554..23be7f0 100644 --- a/booters/refind/build.rs +++ b/booters/refind/build.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::env::var; use std::error::Error; diff --git a/booters/refind/src/backend.rs b/booters/refind/src/backend.rs index 933092c..91fe427 100644 --- a/booters/refind/src/backend.rs +++ b/booters/refind/src/backend.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; use std::str::FromStr; diff --git a/booters/refind/src/error.rs b/booters/refind/src/error.rs index 8fec260..70f99d1 100644 --- a/booters/refind/src/error.rs +++ b/booters/refind/src/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::any::Any; diff --git a/booters/refind/src/lib.rs b/booters/refind/src/lib.rs index c10d685..26a62fe 100644 --- a/booters/refind/src/lib.rs +++ b/booters/refind/src/lib.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::str::from_utf8; diff --git a/booters/systemd-boot/Cargo.toml b/booters/systemd-boot/Cargo.toml index 961fbff..e502800 100644 --- a/booters/systemd-boot/Cargo.toml +++ b/booters/systemd-boot/Cargo.toml @@ -1,15 +1,25 @@ # SPDX-FileCopyrightText: 2026 JustPav # SPDX-FileCopyrightText: 2026 SmoothTeam # -# SPDX-License-Identifier: LGPL-3.0-or-later +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception [package] name = "upac-systemd-boot" +description = "systemd-boot (BLS) boot plugin for upac: one-shot/persistent reboot selection via LoaderEntryOneShot/LoaderEntryDefault" version.workspace = true + edition.workspace = true -repository.workspace = true +rust-version.workspace = true + license.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true + +keywords.workspace = true +categories.workspace = true + [lints] workspace = true @@ -20,8 +30,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] upac-abi = { workspace = true } efivar = { workspace = true } - -uuid = "1.24.1" +uuid = { workspace = true } [build-dependencies] toml = { workspace = true } diff --git a/booters/systemd-boot/build.rs b/booters/systemd-boot/build.rs index cbac554..23be7f0 100644 --- a/booters/systemd-boot/build.rs +++ b/booters/systemd-boot/build.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::env::var; use std::error::Error; diff --git a/booters/systemd-boot/src/backend.rs b/booters/systemd-boot/src/backend.rs index bfb431c..a511db1 100644 --- a/booters/systemd-boot/src/backend.rs +++ b/booters/systemd-boot/src/backend.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; use std::str::FromStr; diff --git a/booters/systemd-boot/src/error.rs b/booters/systemd-boot/src/error.rs index 41002a2..ace8c74 100644 --- a/booters/systemd-boot/src/error.rs +++ b/booters/systemd-boot/src/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::any::Any; diff --git a/booters/systemd-boot/src/lib.rs b/booters/systemd-boot/src/lib.rs index 5803b9d..b11082c 100644 --- a/booters/systemd-boot/src/lib.rs +++ b/booters/systemd-boot/src/lib.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::str::from_utf8; diff --git a/booters/uki/Cargo.toml b/booters/uki/Cargo.toml index 969e463..8337fc1 100644 --- a/booters/uki/Cargo.toml +++ b/booters/uki/Cargo.toml @@ -1,15 +1,25 @@ # SPDX-FileCopyrightText: 2026 JustPav # SPDX-FileCopyrightText: 2026 SmoothTeam # -# SPDX-License-Identifier: LGPL-3.0-or-later +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception [package] name = "upac-uki" +description = "UKI-direct boot plugin for upac: one-shot/persistent reboot selection via UEFI BootNext/BootOrder" version.workspace = true + edition.workspace = true -repository.workspace = true +rust-version.workspace = true + license.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true + +keywords.workspace = true +categories.workspace = true + [lints] workspace = true @@ -20,8 +30,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] upac-abi = { workspace = true } efivar = { workspace = true } - -uuid = "1.24.1" +uuid = { workspace = true } [build-dependencies] toml = { workspace = true } diff --git a/booters/uki/build.rs b/booters/uki/build.rs index cbac554..23be7f0 100644 --- a/booters/uki/build.rs +++ b/booters/uki/build.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::env::var; use std::error::Error; diff --git a/booters/uki/src/backend.rs b/booters/uki/src/backend.rs index 6978c1e..9a57cbb 100644 --- a/booters/uki/src/backend.rs +++ b/booters/uki/src/backend.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; use std::path::Path; diff --git a/booters/uki/src/error.rs b/booters/uki/src/error.rs index 1dee258..ea030a0 100644 --- a/booters/uki/src/error.rs +++ b/booters/uki/src/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::any::Any; diff --git a/booters/uki/src/lib.rs b/booters/uki/src/lib.rs index cadab14..1140ec7 100644 --- a/booters/uki/src/lib.rs +++ b/booters/uki/src/lib.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::str::from_utf8; diff --git a/decoders/alpm/Cargo.toml b/decoders/alpm/Cargo.toml new file mode 100644 index 0000000..7e1b315 --- /dev/null +++ b/decoders/alpm/Cargo.toml @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: 2026 JustPav +# SPDX-FileCopyrightText: 2026 SmoothTeam +# +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +[package] +name = "alpm" +description = "ALPM (.pkg.tar.*) package decoder plugin for upac, implementing pacman's package format" +version.workspace = true + +edition.workspace = true +rust-version.workspace = true + +license.workspace = true + +readme.workspace = true +homepage.workspace = true +repository.workspace = true + +keywords.workspace = true +categories.workspace = true + +[lints] +workspace = true + +[lib] +name = "upac_decoder_alpm" +crate-type = ["cdylib", "rlib"] + +[dependencies] +upac-abi = { workspace = true } +upac-types = { workspace = true } + +flate2 = { workspace = true } +sha2 = { workspace = true } +tar = { workspace = true } +xz2 = { workspace = true } +zstd = { workspace = true } + +[build-dependencies] +toml = { workspace = true } + +[features] +default = ["cdylib"] +cdylib = [] diff --git a/decoders/alpm/build.rs b/decoders/alpm/build.rs new file mode 100644 index 0000000..078d586 --- /dev/null +++ b/decoders/alpm/build.rs @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::env::var; +use std::error::Error; +use std::fs::{read_to_string, write}; +use std::path::Path; + +use toml::{Value, from_str}; + +fn main() -> Result<(), Box> { + let manifest_dir = var("CARGO_MANIFEST_DIR")?; + + let mut generated = String::new(); + generated.push_str(&generate_decoder_toml(&manifest_dir)?); + generated.push_str(&generate_manifest_module(&manifest_dir)?); + + let out = Path::new(&var("OUT_DIR")?).join("layout.rs"); + write(out, generated)?; + + Ok(()) +} + +fn generate_decoder_toml(manifest_dir: &str) -> Result> { + let source = Path::new(manifest_dir).join("../decoder.toml"); + + println!("cargo:rerun-if-changed={}", source.display()); + + let raw = read_to_string(&source)?; + let config: Value = from_str(&raw)?; + + let section = "alpm"; + let entries = config + .get(section) + .and_then(Value::as_table) + .ok_or_else(|| format!("decoder.toml: [{section}] must be a table"))?; + + let mut generated = String::new(); + generated.push_str(&format!("pub mod {section} {{\n")); + + for (key, value) in entries { + let rendered = if let Some(value) = value.as_str() { + format!("&str = {value:?}") + } else if let Some(value) = value.as_integer() { + format!("u32 = {value}") + } else { + return Err(format!("decoder.toml: {section}.{key} must be a string or integer").into()); + }; + + generated.push_str(&format!(" pub const {}: {rendered};\n", key.to_uppercase())); + } + + generated.push_str("}\n"); + + Ok(generated) +} + +/// Compiles this crate's own deployable manifest (`format`/`extensions`) into constants, so a +/// `builtin-alpm` build can dispatch by format without reading `upac-alpm.toml` from disk at +/// runtime — `library`/`mime` are runtime-deployment-only fields, not needed here. +fn generate_manifest_module(manifest_dir: &str) -> Result> { + let source = Path::new(manifest_dir).join("upac-alpm.toml"); + + println!("cargo:rerun-if-changed={}", source.display()); + + let raw = read_to_string(&source)?; + let config: Value = from_str(&raw)?; + + let format = config + .get("format") + .and_then(Value::as_str) + .ok_or("upac-alpm.toml: format must be a string")?; + + let extensions = config + .get("extensions") + .and_then(Value::as_array) + .ok_or("upac-alpm.toml: extensions must be an array")? + .iter() + .map(|entry| { + entry + .as_str() + .ok_or("upac-alpm.toml: extensions entries must be strings") + }) + .collect::, _>>()?; + + let mut generated = String::new(); + generated.push_str("pub mod manifest {\n"); + generated.push_str(&format!(" pub const FORMAT: &str = {format:?};\n")); + generated.push_str(" pub const EXTENSIONS: &[&str] = &[\n"); + for extension in extensions { + generated.push_str(&format!(" {extension:?},\n")); + } + generated.push_str(" ];\n"); + generated.push_str("}\n"); + + Ok(generated) +} diff --git a/decoders/alpm/build.zig b/decoders/alpm/build.zig deleted file mode 100644 index c5a675b..0000000 --- a/decoders/alpm/build.zig +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ───────────────────────────────────────────────────────────────────── -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - const strip = b.option(bool, "strip", "Strip debug symbols") orelse false; - const stack_check = b.option(bool, "stack-check", "Check for stack overflows") orelse false; - - // ── C libs ──────────────────────────────────────────────────────────────── - const translated_libs = b.addTranslateC(.{ - .root_source_file = b.path("src/imports.h"), - .target = target, - .optimize = optimize, - }); - translated_libs.link_libc = true; - translated_libs.linkSystemLibrary("archive", .{}); - - const c_libs_module = translated_libs.createModule(); - - // ── Config ZON modules ──────────────────────────────────────────────────── - const upac_meta_fields = b.createModule(.{ .root_source_file = b.path("config/meta_fields.zon") }); - - // ── Types ───────────────────────────────────────────────────────────────── - const upac_backend_types = b.createModule(.{ - .root_source_file = b.path("src/types/types.zig"), - .target = target, - .optimize = optimize, - }); - upac_backend_types.addImport("upac-meta-fields", upac_meta_fields); - - // ── FFI ───────────────────────────────────────────────────────────────── - const upac_backend_ffi = b.createModule(.{ - .root_source_file = b.path("src/ffi.zig"), - .target = target, - .optimize = optimize, - }); - - upac_backend_ffi.addImport("upac-backend-types", upac_backend_types); - - // ── Root ────────────────────────────────────────────────────────────────── - const upac_backend_root = b.createModule(.{ - .root_source_file = b.path("src/symbols.zig"), - .target = target, - .optimize = optimize, - }); - - upac_backend_root.strip = strip; - upac_backend_root.stack_check = stack_check; - - // ── Shared library ──────────────────────────────────────────────────────── - const shared_lib = b.addLibrary(.{ - .name = "upac-alpm", - .linkage = .dynamic, - .root_module = upac_backend_root, - }); - - shared_lib.root_module.link_libc = true; - - shared_lib.root_module.addImport("c-libs", c_libs_module); - shared_lib.root_module.addImport("upac-backend-types", upac_backend_types); - shared_lib.root_module.addImport("upac-backend-ffi", upac_backend_ffi); - - shared_lib.root_module.strip = strip; - shared_lib.root_module.stack_check = stack_check; - shared_lib.bundle_compiler_rt = false; - shared_lib.link_gc_sections = false; - - b.installArtifact(shared_lib); -} diff --git a/decoders/alpm/build.zig.zon b/decoders/alpm/build.zig.zon deleted file mode 100644 index 226b4f8..0000000 --- a/decoders/alpm/build.zig.zon +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -.{ - .name = .upac_alpm, - .version = "0.1.4", - .minimum_zig_version = "0.16.0", - .fingerprint = 0x37e4a72abb77041d, - .dependencies = .{}, - .paths = .{ "build.zig", "build.zig.zon", "src" }, -} diff --git a/decoders/alpm/config/meta_fields.zon b/decoders/alpm/config/meta_fields.zon deleted file mode 100644 index 3c1537b..0000000 --- a/decoders/alpm/config/meta_fields.zon +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -.{ - .Package = "pkgname", - .Version = "pkgver", - .@"Installed-Size" = "size", - .Architecture = "arch", - .Description = "pkgdesc", - .License = "license", - .Homepage = "url", - .Maintainer = "packager", -} diff --git a/decoders/alpm/src/backend/backend.zig b/decoders/alpm/src/backend/backend.zig deleted file mode 100644 index 81d66ae..0000000 --- a/decoders/alpm/src/backend/backend.zig +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ───────────────────────────────────────────────────────────────────── -pub const std = @import("std"); - -pub const c_libs = @import("c-libs"); - -const types = @import("upac-backend-types"); -const BackendError = types.BackendError; -const StateId = types.StateId; -const PackageMeta = types.PackageMeta; -const PrepareData = types.PrepareData; -const PrepareResult = types.PrepareResult; -const CancelToken = types.CancelToken; - -const verifying = @import("verifying/verifying.zig"); -const unpacking = @import("unpacking/unpacking.zig"); -const parsing = @import("parsing/parsing.zig"); -// ── BackendMachine ──────────────────────────────────────────────────────────── -pub const BackendMachine = struct { - data: PrepareData, - - meta: ?PackageMeta = null, - package_info: ?[]u8 = null, - - temp_package_path: ?[:0]const u8 = null, - - allocator: std.mem.Allocator, - io: std.Io, - - pub fn deinit(self: *BackendMachine) void { - if (self.package_info) |content| self.allocator.free(content); - if (self.temp_package_path) |path| self.allocator.free(path); - } - - pub fn hook(self: *BackendMachine, event: StateId) BackendError!void { - const cb = self.data.on_hook orelse return; - if (cb(@intFromEnum(event), null, self.data.hook_ctx) == .cancel) return BackendError.Cancelled; - } - - pub fn run(data: PrepareData, allocator: std.mem.Allocator) BackendError!PrepareResult { - var state = StateId.verifying; - var machine = BackendMachine{ - .data = data, - - .io = std.Io.Threaded.global_single_threaded.io(), - .allocator = allocator, - }; - defer machine.deinit(); - - while (state != .done) { - machine.hook(state) catch |err| return err; - switch (state) { - .verifying => { - verifying.run(&machine) catch |err| return err; - state = .extracting; - }, - .extracting => { - unpacking.run(&machine) catch |err| return err; - state = .reading_meta; - }, - .reading_meta => { - parsing.run(&machine) catch |err| return err; - state = .done; - }, - .done, .special_step => {}, - } - } - - const temp_package_path = machine.temp_package_path orelse return BackendError.TempDirFailed; - machine.temp_package_path = null; - - return PrepareResult{ - .meta = machine.meta orelse return BackendError.MetadataNotFound, - .temp_path = temp_package_path, - }; - } -}; diff --git a/decoders/alpm/src/backend/parsing/parsing.zig b/decoders/alpm/src/backend/parsing/parsing.zig deleted file mode 100644 index 8b3cf5f..0000000 --- a/decoders/alpm/src/backend/parsing/parsing.zig +++ /dev/null @@ -1,151 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ─────────────────────────────────────────────────────────────────── -const std = @import("std"); - -const package_meta_field_map = @import("upac-backend-types").buildFieldMap(); - -const types = @import("upac-backend-types"); -const BackendError = types.BackendError; - -const PackageMeta = types.PackageMeta; -const RawMeta = types.RawMeta; - -const backend = @import("../backend.zig"); -const Machine = backend.BackendMachine; - -const parseVersion = @import("utils.zig").parseVersion; -// ── ParsingState ────────────────────────────────────────────────────────────── -const ParsingState = enum { - parse_pkginfo, - build_meta, - cleanup_junk, - done, -}; - -// ── ParsingMachine ──────────────────────────────────────────────────────────── -const ParsingMachine = struct { - backend: *Machine, - raw_meta: RawMeta = .{}, - - fn stateFailed(self: *ParsingMachine, err: BackendError) BackendError { - self.raw_meta.deinit(self.backend.allocator); - - return err; - } -}; - -// ── Trampoline ──────────────────────────────────────────────────────────────── -pub fn run(machine: *Machine) BackendError!void { - var parsing = ParsingMachine{ .backend = machine }; - - var state = ParsingState.parse_pkginfo; - while (state != .done) { - if (machine.data.cancel_token.isCancelled()) return parsing.stateFailed(BackendError.Cancelled); - state = switch (state) { - .parse_pkginfo => try stateParsePkginfo(&parsing), - .build_meta => try stateBuildMeta(&parsing), - .cleanup_junk => stateCleanupJunk(&parsing), - .done => unreachable, - }; - } -} - -// ── States ──────────────────────────────────────────────────────────────────── -fn stateParsePkginfo(machine: *ParsingMachine) BackendError!ParsingState { - const pkginfo_content = machine.backend.package_info orelse return machine.stateFailed(BackendError.MetadataNotFound); - defer { - machine.backend.allocator.free(pkginfo_content); - machine.backend.package_info = null; - } - - var lines = std.mem.splitScalar(u8, pkginfo_content, '\n'); - while (lines.next()) |line| { - const trimmed_line = std.mem.trim(u8, line, " \t\r"); - if (trimmed_line.len == 0 or trimmed_line[0] == '#') continue; - - const separator_index = std.mem.indexOf(u8, trimmed_line, " = ") orelse continue; - - const key = std.mem.trim(u8, trimmed_line[0..separator_index], " \t"); - const value = std.mem.trim(u8, trimmed_line[separator_index + 3 ..], " \t"); - - const field = package_meta_field_map.get(key) orelse continue; - switch (field) { - .Package => machine.raw_meta.name = machine.backend.allocator.dupe(u8, value) catch return machine.stateFailed(BackendError.OutOfMemory), - .Version => machine.raw_meta.version_str = machine.backend.allocator.dupe(u8, value) catch return machine.stateFailed(BackendError.OutOfMemory), - .@"Installed-Size" => machine.raw_meta.installed_size = std.fmt.parseInt(u64, value, 10) catch 0, - .Architecture => machine.raw_meta.arch = machine.backend.allocator.dupe(u8, value) catch return machine.stateFailed(BackendError.OutOfMemory), - .Description => machine.raw_meta.description = machine.backend.allocator.dupe(u8, value) catch return machine.stateFailed(BackendError.OutOfMemory), - .License => machine.raw_meta.license = machine.backend.allocator.dupe(u8, value) catch return machine.stateFailed(BackendError.OutOfMemory), - .Homepage => machine.raw_meta.url = machine.backend.allocator.dupe(u8, value) catch return machine.stateFailed(BackendError.OutOfMemory), - .Maintainer => machine.raw_meta.maintainer = machine.backend.allocator.dupe(u8, value) catch return machine.stateFailed(BackendError.OutOfMemory), - } - } - - return .build_meta; -} - -fn stateBuildMeta(machine: *ParsingMachine) BackendError!ParsingState { - var sha256: [32]u8 = undefined; - - _ = std.fmt.hexToBytes(&sha256, machine.backend.data.checksum) catch return machine.stateFailed(BackendError.InvalidPackage); - - const raw_version_str = machine.raw_meta.version_str orelse return machine.stateFailed(BackendError.MetadataNotFound); - defer machine.backend.allocator.free(raw_version_str); - machine.raw_meta.version_str = null; - - const parsed_version = parseVersion(machine.backend.allocator, raw_version_str, '-') catch return machine.stateFailed(BackendError.InvalidPackage); - errdefer parsed_version.deinit(machine.backend.allocator); - - const package_name = machine.raw_meta.name orelse return machine.stateFailed(BackendError.MetadataNotFound); - machine.raw_meta.name = null; - errdefer machine.backend.allocator.free(package_name); - - const arch = machine.raw_meta.arch orelse machine.backend.allocator.dupe(u8, "any") catch return machine.stateFailed(BackendError.OutOfMemory); - machine.raw_meta.arch = null; - errdefer machine.backend.allocator.free(arch); - - const maintainer = machine.raw_meta.maintainer orelse machine.backend.allocator.dupe(u8, "") catch return machine.stateFailed(BackendError.OutOfMemory); - machine.raw_meta.maintainer = null; - errdefer machine.backend.allocator.free(maintainer); - - const description = machine.raw_meta.description orelse machine.backend.allocator.dupe(u8, "") catch return machine.stateFailed(BackendError.OutOfMemory); - machine.raw_meta.description = null; - errdefer machine.backend.allocator.free(description); - - const license = machine.raw_meta.license; - machine.raw_meta.license = null; - - const url = machine.raw_meta.url; - machine.raw_meta.url = null; - - machine.backend.meta = PackageMeta{ - .name = package_name, - .version = parsed_version, - .arch = arch, - .arch_sub = null, - .maintainer = maintainer, - .description = description, - .license = license, - .url = url, - .sha256 = sha256, - .installed_size = machine.raw_meta.installed_size, - }; - - return .cleanup_junk; -} - -fn stateCleanupJunk(machine: *ParsingMachine) ParsingState { - const temp_package_path = machine.backend.temp_package_path orelse return .done; - - var temp_dir = std.Io.Dir.openDirAbsolute(machine.backend.io, temp_package_path, .{}) catch return .done; - defer temp_dir.close(machine.backend.io); - - const junk_filenames = [_][]const u8{ ".BUILDINFO", ".MTREE", ".INSTALL", ".CHANGELOG" }; - for (junk_filenames) |junk_filename| temp_dir.deleteFile(machine.backend.io, junk_filename) catch {}; - - return .done; -} diff --git a/decoders/alpm/src/backend/parsing/utils.zig b/decoders/alpm/src/backend/parsing/utils.zig deleted file mode 100644 index 21cfa35..0000000 --- a/decoders/alpm/src/backend/parsing/utils.zig +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ─────────────────────────────────────────────────────────────────── -const std = @import("std"); - -const types = @import("upac-backend-types"); -const Version = types.Version; -const BackendError = types.BackendError; - -// Parses a version string "epoch:X.Y.Z-release" where release_sep separates the release suffix. -// epoch prefix "N:" is optional. If the release suffix is non-numeric, it becomes the pre-release tag. -pub fn parseVersion(allocator: std.mem.Allocator, version_str: []const u8, release_sep: u8) BackendError!Version { - var epoch: u32 = 0; - var release: u32 = 1; - var pre: ?[]const u8 = null; - var remaining = version_str; - var version_parts = std.ArrayList(u32).empty; - errdefer version_parts.deinit(allocator); - - if (std.mem.indexOf(u8, remaining, ":")) |colon_idx| { - if (std.fmt.parseInt(u32, remaining[0..colon_idx], 10)) |parsed_epoch| { - epoch = parsed_epoch; - remaining = remaining[colon_idx + 1 ..]; - } else |_| {} - } - - if (std.mem.lastIndexOf(u8, remaining, &[_]u8{release_sep})) |sep_idx| { - const release_str = remaining[sep_idx + 1 ..]; - if (std.fmt.parseInt(u32, release_str, 10)) |parsed_release| { - release = parsed_release; - } else |_| { - var digit_end: usize = 0; - while (digit_end < release_str.len and release_str[digit_end] >= '0' and release_str[digit_end] <= '9') : (digit_end += 1) {} - - if (digit_end > 0) release = std.fmt.parseInt(u32, release_str[0..digit_end], 10) catch 1; - if (release_str.len > 0) pre = allocator.dupe(u8, release_str) catch return BackendError.AllocZFailed; - } - remaining = remaining[0..sep_idx]; - } - - var part_iter = std.mem.splitScalar(u8, remaining, '.'); - while (part_iter.next()) |part_str| { - var digit_end: usize = 0; - while (digit_end < part_str.len and part_str[digit_end] >= '0' and part_str[digit_end] <= '9') : (digit_end += 1) {} - const part_value = if (digit_end > 0) std.fmt.parseInt(u32, part_str[0..digit_end], 10) catch 0 else 0; - version_parts.append(allocator, part_value) catch return BackendError.AllocZFailed; - } - - if (version_parts.items.len == 0) version_parts.append(allocator, 0) catch return BackendError.AllocZFailed; - - return Version{ - .epoch = epoch, - .parts = version_parts.toOwnedSlice(allocator) catch return BackendError.AllocZFailed, - .pre = pre, - .release = release, - }; -} diff --git a/decoders/alpm/src/backend/unpacking/unpacking.zig b/decoders/alpm/src/backend/unpacking/unpacking.zig deleted file mode 100644 index fba322b..0000000 --- a/decoders/alpm/src/backend/unpacking/unpacking.zig +++ /dev/null @@ -1,220 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ─────────────────────────────────────────────────────────────────── -const std = @import("std"); - -const c_libs = @import("c-libs"); - -const BackendError = @import("upac-backend-types").BackendError; - -const backend = @import("../backend.zig"); -const Machine = backend.BackendMachine; - -// ── UnpackingState ──────────────────────────────────────────────────────────── -const UnpackingState = enum { - create_temp_dir, - open_archive, - next_entry, - write_blocks, - close_archive, - done, -}; - -// ── UnpackingMachine ────────────────────────────────────────────────────────── -const UnpackingMachine = struct { - backend: *Machine, - - file: ?std.Io.File = null, - archive_reader: ?*c_libs.archive = null, - archive_writer: ?*c_libs.archive = null, - - old_package_dir: ?std.Io.Dir = null, - - fn stateFailed(self: *UnpackingMachine, err: BackendError) BackendError { - if (self.archive_reader) |reader| { - _ = c_libs.archive_read_free(reader); - self.archive_reader = null; - } - - if (self.archive_writer) |writer| { - _ = c_libs.archive_write_free(writer); - self.archive_writer = null; - } - - if (self.old_package_dir) |dir| { - std.Io.Threaded.fchdir(dir.handle) catch {}; - dir.close(self.backend.io); - self.old_package_dir = null; - } - - if (self.file) |file| { - file.close(self.backend.io); - self.file = null; - } - - if (self.backend.temp_package_path) |temp_path| { - std.Io.Dir.cwd().deleteTree(self.backend.io, temp_path) catch {}; - - self.backend.allocator.free(temp_path); - self.backend.temp_package_path = null; - } - - return err; - } -}; - -// ── Trampoline ──────────────────────────────────────────────────────────────── -pub fn run(machine: *Machine) BackendError!void { - var unpacking = UnpackingMachine{ .backend = machine }; - - var state = UnpackingState.create_temp_dir; - while (state != .done) { - if (machine.data.cancel_token.isCancelled()) return unpacking.stateFailed(BackendError.Cancelled); - state = switch (state) { - .create_temp_dir => try stateCreateTempDir(&unpacking), - .open_archive => try stateOpenArchive(&unpacking), - .next_entry => try stateNextEntry(&unpacking), - .write_blocks => try stateWriteBlocks(&unpacking), - .close_archive => stateCloseArchive(&unpacking), - .done => unreachable, - }; - } -} - -// ── States ──────────────────────────────────────────────────────────────────── -fn stateCreateTempDir(machine: *UnpackingMachine) BackendError!UnpackingState { - var temp_dir_name_buf: [256]u8 = undefined; - - const timestamp: i64 = @intCast(@divTrunc(std.Io.Clock.real.now(machine.backend.io).nanoseconds, std.time.ns_per_ms)); - - const temp_package_dir_name = std.fmt.bufPrintZ(&temp_dir_name_buf, "upac-installed-{d}", .{timestamp}) catch return machine.stateFailed(BackendError.AllocZFailed); - - const temp_package_path = std.Io.Dir.path.joinZ(machine.backend.allocator, &.{ - std.mem.span(machine.backend.data.temp_path_c), - temp_package_dir_name, - }) catch return machine.stateFailed(BackendError.AllocZFailed); - - std.Io.Dir.createDirAbsolute(machine.backend.io, temp_package_path, .default_dir) catch return machine.stateFailed(BackendError.TempDirFailed); - - machine.backend.temp_package_path = temp_package_path; - - return .open_archive; -} - -fn stateOpenArchive(machine: *UnpackingMachine) BackendError!UnpackingState { - const package_path = std.mem.span(machine.backend.data.package_path_c); - const temp_package_path = machine.backend.temp_package_path orelse return machine.stateFailed(BackendError.TempDirFailed); - - const file = std.Io.Dir.openFileAbsolute(machine.backend.io, package_path, .{}) catch return machine.stateFailed(BackendError.ReadFailed); - machine.file = file; - - const archive_reader = c_libs.archive_read_new() orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - machine.archive_reader = archive_reader; - - _ = c_libs.archive_read_support_format_tar(archive_reader); - _ = c_libs.archive_read_support_filter_zstd(archive_reader); - _ = c_libs.archive_read_support_filter_xz(archive_reader); - _ = c_libs.archive_read_support_filter_gzip(archive_reader); - - if (c_libs.archive_read_open_fd(archive_reader, file.handle, 16384) != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveOpenFailed); - - const archive_writer = c_libs.archive_write_disk_new() orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - machine.archive_writer = archive_writer; - - _ = c_libs.archive_write_disk_set_options(archive_writer, c_libs.ARCHIVE_EXTRACT_TIME | - c_libs.ARCHIVE_EXTRACT_PERM | - c_libs.ARCHIVE_EXTRACT_FFLAGS); - _ = c_libs.archive_write_disk_set_standard_lookup(archive_writer); - - const old_package_dir = std.Io.Dir.cwd().openDir(machine.backend.io, ".", .{}) catch return machine.stateFailed(BackendError.ReadFailed); - machine.old_package_dir = old_package_dir; - - std.Io.Threaded.chdir(temp_package_path) catch return machine.stateFailed(BackendError.TempDirFailed); - - return .next_entry; -} - -fn stateNextEntry(machine: *UnpackingMachine) BackendError!UnpackingState { - var archive_entry: ?*c_libs.archive_entry = undefined; - - const archive_reader = machine.archive_reader orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - - const archive_writer = machine.archive_writer orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - - const read_result = c_libs.archive_read_next_header(archive_reader, &archive_entry); - if (read_result == c_libs.ARCHIVE_EOF) return .close_archive; - if (read_result != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveReadFailed); - - const entry = archive_entry orelse return machine.stateFailed(BackendError.ArchiveReadFailed); - - const entry_pathname = c_libs.archive_entry_pathname(entry); - const is_pkginfo = entry_pathname != null and std.mem.eql(u8, std.mem.span(entry_pathname), ".PKGINFO"); - - if (is_pkginfo) { - const pkginfo_size: usize = @intCast(c_libs.archive_entry_size(entry)); - const pkginfo_buf = machine.backend.allocator.alloc(u8, pkginfo_size) catch return machine.stateFailed(BackendError.OutOfMemory); - - if (c_libs.archive_read_data(archive_reader, pkginfo_buf.ptr, pkginfo_size) < 0) { - machine.backend.allocator.free(pkginfo_buf); - return machine.stateFailed(BackendError.ArchiveReadFailed); - } - - machine.backend.package_info = pkginfo_buf; - return .next_entry; - } - - if (c_libs.archive_write_header(archive_writer, entry) != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveExtractFailed); - - return .write_blocks; -} - -fn stateWriteBlocks(machine: *UnpackingMachine) BackendError!UnpackingState { - var block_size: usize = 0; - var block_offset: i64 = 0; - var data_block: ?*const anyopaque = null; - - const archive_reader = machine.archive_reader orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - const archive_writer = machine.archive_writer orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - - const block_result = c_libs.archive_read_data_block(archive_reader, &data_block, &block_size, &block_offset); - - if (block_result == c_libs.ARCHIVE_EOF) { - if (c_libs.archive_write_finish_entry(archive_writer) != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveExtractFailed); - return .next_entry; - } - - if (block_result != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveReadFailed); - - if (c_libs.archive_write_data_block(archive_writer, data_block, block_size, block_offset) != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveExtractFailed); - - return .write_blocks; -} - -fn stateCloseArchive(machine: *UnpackingMachine) UnpackingState { - if (machine.archive_reader) |reader| { - _ = c_libs.archive_read_free(reader); - machine.archive_reader = null; - } - - if (machine.archive_writer) |writer| { - _ = c_libs.archive_write_free(writer); - machine.archive_writer = null; - } - - if (machine.old_package_dir) |dir| { - std.Io.Threaded.fchdir(dir.handle) catch {}; - - dir.close(machine.backend.io); - machine.old_package_dir = null; - } - - if (machine.file) |file| { - file.close(machine.backend.io); - machine.file = null; - } - - return .done; -} diff --git a/decoders/alpm/src/backend/verifying/verifying.zig b/decoders/alpm/src/backend/verifying/verifying.zig deleted file mode 100644 index 11c84bb..0000000 --- a/decoders/alpm/src/backend/verifying/verifying.zig +++ /dev/null @@ -1,114 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ─────────────────────────────────────────────────────────────────── -const std = @import("std"); - -const BackendError = @import("upac-backend-types").BackendError; - -const backend = @import("../backend.zig"); -const Machine = backend.BackendMachine; - -// ── VerifyingState ──────────────────────────────────────────────────────────── -const VerifyingState = enum { - check_file, - check_temp_dir, - hash, - compare, - done, -}; - -// ── VerifyingMachine ────────────────────────────────────────────────────────── -const VerifyingMachine = struct { - backend: *Machine, - - file: ?std.Io.File = null, - - digest_checksum: [32]u8 = undefined, - - fn stateFailed(self: *VerifyingMachine, err: BackendError) BackendError { - if (self.file) |file| { - file.close(self.backend.io); - self.file = null; - } - - return err; - } -}; - -// ── Trampoline ──────────────────────────────────────────────────────────────── -pub fn run(machine: *Machine) BackendError!void { - var verifying = VerifyingMachine{ .backend = machine }; - - var state = VerifyingState.check_file; - while (state != .done) { - if (machine.data.cancel_token.isCancelled()) return verifying.stateFailed(BackendError.Cancelled); - state = switch (state) { - .check_file => try stateCheckFile(&verifying), - .check_temp_dir => try stateCheckTempDir(&verifying), - .hash => try stateHash(&verifying), - .compare => try stateCompare(&verifying), - .done => unreachable, - }; - } -} - -// ── States ──────────────────────────────────────────────────────────────────── -fn stateCheckFile(machine: *VerifyingMachine) BackendError!VerifyingState { - const package_path = std.mem.span(machine.backend.data.package_path_c); - - std.Io.Dir.accessAbsolute(machine.backend.io, package_path, .{}) catch return machine.stateFailed(BackendError.ReadFailed); - - return .check_temp_dir; -} - -fn stateCheckTempDir(machine: *VerifyingMachine) BackendError!VerifyingState { - const temp_path = std.mem.span(machine.backend.data.temp_path_c); - - std.Io.Dir.accessAbsolute(machine.backend.io, temp_path, .{}) catch return machine.stateFailed(BackendError.TempDirFailed); - - return .hash; -} - -fn stateHash(machine: *VerifyingMachine) BackendError!VerifyingState { - var package_reader_buf: [65536]u8 = undefined; - var package_hasher = std.crypto.hash.sha2.Sha256.init(.{}); - - const package_path = std.mem.span(machine.backend.data.package_path_c); - - const file = std.Io.Dir.openFileAbsolute(machine.backend.io, package_path, .{}) catch return machine.stateFailed(BackendError.ReadFailed); - machine.file = file; - - var package_read_bufs_vector = [1][]u8{package_reader_buf[0..]}; - while (true) { - const bytes_read = file.readStreaming(machine.backend.io, &package_read_bufs_vector) catch |err| { - if (err == error.EndOfStream) break; - return machine.stateFailed(BackendError.ReadFailed); - }; - - if (bytes_read == 0) break; - - package_hasher.update(package_reader_buf[0..bytes_read]); - } - - package_hasher.final(&machine.digest_checksum); - - return .compare; -} - -fn stateCompare(machine: *VerifyingMachine) BackendError!VerifyingState { - var checksum_as_bytes: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined; - - _ = std.fmt.hexToBytes(&checksum_as_bytes, machine.backend.data.checksum) catch return machine.stateFailed(BackendError.InvalidPackage); - - if (!std.mem.eql(u8, &machine.digest_checksum, &checksum_as_bytes)) return machine.stateFailed(BackendError.ChecksumMismatch); - - if (machine.file) |file| { - file.close(machine.backend.io); - machine.file = null; - } - - return .done; -} diff --git a/decoders/alpm/src/error.rs b/decoders/alpm/src/error.rs new file mode 100644 index 0000000..2074897 --- /dev/null +++ b/decoders/alpm/src/error.rs @@ -0,0 +1,40 @@ +// 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; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecodeError { + InvalidRequest, + Io(IoErrorKind), + ChecksumMismatch, + UnsupportedFormat, + MissingPkgInfo, + MalformedPkgInfo, + 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::MissingPkgInfo => -5, + DecodeError::MalformedPkgInfo => -6, + DecodeError::InvalidUtf8 => -7, + DecodeError::Cancelled => -8, + } + } +} diff --git a/decoders/alpm/src/extract.rs b/decoders/alpm/src/extract.rs new file mode 100644 index 0000000..fc75a0d --- /dev/null +++ b/decoders/alpm/src/extract.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::File; +use std::io::Read; + +use flate2::read::GzDecoder; +use tar::Archive; +use xz2::read::XzDecoder; +use zstd::stream::read::Decoder as ZstdDecoder; + +use upac_abi::hook::CancelToken; + +use crate::alpm::{BUILDINFO_ENTRY, CHANGELOG_ENTRY, INSTALL_ENTRY, MTREE_ENTRY, PKGINFO_ENTRY}; +use crate::error::DecodeError; + +const JUNK_ENTRIES: [&str; 3] = [BUILDINFO_ENTRY, MTREE_ENTRY, CHANGELOG_ENTRY]; + +pub struct ExtractedMetadata { + pub pkginfo: String, + pub install: Option, +} + +pub fn extract(package_path: &str, output_dir: &str, cancel: &CancelToken) -> Result { + let file = File::open(package_path)?; + let reader = open_reader(package_path, file)?; + + let mut archive = Archive::new(reader); + + let mut pkginfo = None; + let mut install = None; + + for entry in archive.entries()? { + if cancel.is_cancelled() { + return Err(DecodeError::Cancelled); + } + + let mut entry = entry?; + let entry_path = entry.path()?.to_string_lossy().into_owned(); + + if entry_path == PKGINFO_ENTRY { + pkginfo = Some(read_entry_to_string(&mut entry)?); + continue; + } + + if entry_path == INSTALL_ENTRY { + install = Some(read_entry_to_string(&mut entry)?); + continue; + } + + if JUNK_ENTRIES.contains(&entry_path.as_str()) { + continue; + } + + entry.unpack_in(output_dir)?; + } + + pkginfo + .map(|pkginfo| ExtractedMetadata { pkginfo, install }) + .ok_or(DecodeError::MissingPkgInfo) +} + +fn read_entry_to_string(entry: &mut tar::Entry<'_, R>) -> Result { + let mut bytes = Vec::new(); + entry.read_to_end(&mut bytes)?; + + String::from_utf8(bytes).map_err(|_| DecodeError::InvalidUtf8) +} + +fn open_reader(package_path: &str, file: File) -> Result, DecodeError> { + if package_path.ends_with(".pkg.tar.zst") { + Ok(Box::new(ZstdDecoder::new(file)?)) + } else if package_path.ends_with(".pkg.tar.xz") { + Ok(Box::new(XzDecoder::new(file))) + } else if package_path.ends_with(".pkg.tar.gz") { + Ok(Box::new(GzDecoder::new(file))) + } else if package_path.ends_with(".pkg.tar") { + Ok(Box::new(file)) + } else { + Err(DecodeError::UnsupportedFormat) + } +} diff --git a/decoders/alpm/src/ffi.zig b/decoders/alpm/src/ffi.zig deleted file mode 100644 index 579a76e..0000000 --- a/decoders/alpm/src/ffi.zig +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -pub const std = @import("std"); - -const types = @import("upac-backend-types"); -const StateId = types.StateId; -const BackendError = types.BackendError; - -const HookFn = types.HookFn; -const CancelToken = types.CancelToken; - -pub const ABI_VERSION: u32 = 2; - -// ── FFI types ────────────────────────────────────────────────────────────────── -pub const CSlice = extern struct { - ptr: [*c]const u8, - len: usize, - - pub fn toSlice(self: CSlice) []const u8 { - const not_null_ptr = self.ptr orelse return ""; - return not_null_ptr[0..self.len]; - } - - pub fn asZ(self: CSlice) [*c]const u8 { - return self.ptr; - } - - pub fn fromSlice(slice: ?[]const u8) CSlice { - const not_null_slice = slice orelse return .{ .ptr = null, .len = 0 }; - return .{ .ptr = @ptrCast(not_null_slice.ptr), .len = not_null_slice.len }; - } - - pub fn validate(self: CSlice) !void { - if (self.ptr == null) return error.InvalidEntry; - if (self.ptr[self.len] != 0) return error.InvalidEntry; - if (std.mem.len(self.ptr) != self.len) return error.InvalidEntry; - } -}; - -pub const CVersionParts = extern struct { - ptr: [*]u32, - len: usize, - - pub fn toSlice(self: CVersionParts) []u32 { - return self.ptr[0..self.len]; - } -}; - -pub const CVersion = extern struct { - struct_size: usize = @sizeOf(CVersion), - - epoch: u32, - release: u32, - parts: CVersionParts, - pre: CSlice, -}; - -pub const CPackageMeta = extern struct { - struct_size: usize = @sizeOf(CPackageMeta), - - name: CSlice, - version: CVersion, - arch: CSlice, - arch_sub: CSlice, - maintainer: CSlice, - description: CSlice, - license: CSlice, - url: CSlice, - sha256: [32]u8, - installed_size: u64 = 0, - - pub fn free(self: *CPackageMeta, allocator: std.mem.Allocator) void { - inline for (std.meta.fields(CPackageMeta)) |field| { - if (field.type == CSlice) { - const slice = @field(self, field.name); - if (slice.ptr != null) allocator.free(slice.toSlice()); - } - } - if (self.version.parts.len > 0) allocator.free(self.version.parts.toSlice()); - if (self.version.pre.ptr != null) allocator.free(self.version.pre.toSlice()); - allocator.destroy(self); - } -}; - -pub const CPrepareRequest = extern struct { - struct_size: usize = @sizeOf(CPrepareRequest), - checksum: CSlice, - - package_path: CSlice, - temp_dir_path: CSlice, - - on_hook: ?*const HookFn = null, - hook_ctx: ?*anyopaque = null, - - cancel_token: ?*const CancelToken = null, - - pub fn validate(req: CPrepareRequest) !void { - if (req.struct_size != @sizeOf(CPrepareRequest)) return error.AbiMismatch; - - try req.package_path.validate(); - try req.temp_dir_path.validate(); - try req.checksum.validate(); - } -}; - -pub fn dupeToCSlice(allocator: std.mem.Allocator, slice: []const u8) BackendError!CSlice { - const duped = allocator.dupeZ(u8, slice) catch return BackendError.AllocZFailed; - return CSlice.fromSlice(duped); -} - -pub fn dupeRequiredToCSlice(allocator: std.mem.Allocator, slice: []const u8) BackendError!CSlice { - if (slice.len == 0) return BackendError.InvalidPackage; - return dupeToCSlice(allocator, slice); -} diff --git a/decoders/alpm/src/lib.rs b/decoders/alpm/src/lib.rs new file mode 100644 index 0000000..62913e3 --- /dev/null +++ b/decoders/alpm/src/lib.rs @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::str::from_utf8; + +use upac_abi::ABI_VERSION; +use upac_abi::decoder::{CDecodeRequest, CDecodeResponse, CDependency}; +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 crate::error::DecodeError; +use crate::pkginfo::PkgInfo; + +pub mod error; +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 +} + +/// # Safety +/// `request`, if non-null, must point to a valid, initialized `CDecodeRequest` for the duration +/// of the call. `response_out`, if non-null, must point to writable, uninitialized +/// `CDecodeResponse` storage that this function fully initializes on success. +#[cfg_attr(feature = "cdylib", unsafe(no_mangle))] +pub unsafe extern "C" fn decode(request: *const CDecodeRequest, response_out: *mut CDecodeResponse) -> i32 { + if request.is_null() || response_out.is_null() { + return DecodeError::InvalidRequest.code(); + } + + match decode_package(unsafe { &*request }) { + Ok(response) => { + unsafe { response_out.write(response) }; + 0 + } + Err(error) => error.code(), + } +} + +/// # Safety +/// `response`, if non-null, must point to a `CDecodeResponse` produced by this crate's own +/// `decode`, not yet freed. +unsafe extern "C" fn free_decode_response(response: *mut CDecodeResponse) { + if response.is_null() { + return; + } + + let response = unsafe { &*response }; + + unsafe { + response.meta.free(); + + free_cvec_owning(&response.dependencies, |dependency| { + free_cslice(&dependency.name); + dependency.version.free(); + }); + + free_cvec_owning(&response.declarative_triggers, |slice| free_cslice(slice)); + } +} + +fn decode_package(request: &CDecodeRequest) -> Result { + let package_path = + from_utf8(unsafe { request.package_path.as_slice() }).map_err(|_| DecodeError::InvalidRequest)?; + let output_dir = from_utf8(unsafe { request.output_dir.as_slice() }).map_err(|_| DecodeError::InvalidRequest)?; + let cancel = unsafe { request.cancel_token.as_ref() }.ok_or(DecodeError::InvalidRequest)?; + + verify::verify(package_path, request.checksum, cancel)?; + + let extracted = extract::extract(package_path, output_dir, cancel)?; + let declarative_triggers = triggers::scan(extracted.install.as_deref().unwrap_or("")); + + let pkg_info = PkgInfo::parse(&extracted.pkginfo, request.checksum)?; + + Ok(build_response(pkg_info, declarative_triggers)) +} + +fn build_response(pkg_info: PkgInfo, declarative_triggers: Vec) -> CDecodeResponse { + let PkgInfo { meta, dependencies } = pkg_info; + + 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, + } +} diff --git a/decoders/alpm/src/pkginfo.rs b/decoders/alpm/src/pkginfo.rs new file mode 100644 index 0000000..c9dbfcd --- /dev/null +++ b/decoders/alpm/src/pkginfo.rs @@ -0,0 +1,117 @@ +// 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::HashMap; + +use upac_abi::decoder::{ + CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS, parse_constraint_prefix, +}; + +use upac_types::{Dependency, PackageMeta, Version}; + +use crate::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, +}; +use crate::error::DecodeError; + +const OPERATORS: [(&[u8], u8); 5] = [ + (b"<=", CONSTRAINT_LESS | CONSTRAINT_EQUAL), + (b">=", CONSTRAINT_GREATER | CONSTRAINT_EQUAL), + (b"<", CONSTRAINT_LESS), + (b">", CONSTRAINT_GREATER), + (b"=", CONSTRAINT_EQUAL), +]; + +#[derive(Debug)] +pub struct PkgInfo { + pub meta: PackageMeta, + pub dependencies: Vec, +} + +impl PkgInfo { + pub fn parse(content: &str, sha256: [u8; 32]) -> Result { + let mut fields: HashMap<&str, String> = HashMap::new(); + let mut dependencies = Vec::new(); + + for line in content.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + + let Some((key, value)) = line.split_once(" = ") else { + continue; + }; + + if key == PKGINFO_DEPEND_KEY { + dependencies.push(Self::parse_dependency(value)); + } else { + fields.insert(key, value.to_owned()); + } + } + + let name = fields.remove(PKGINFO_NAME_KEY).ok_or(DecodeError::MalformedPkgInfo)?; + let version = fields + .remove(PKGINFO_VERSION_KEY) + .ok_or(DecodeError::MalformedPkgInfo)?; + + let raw_version = match fields.remove(PKGINFO_RELEASE_KEY) { + Some(release) => format!("{version}-{release}"), + None => version, + }; + + let epoch = fields + .get(PKGINFO_EPOCH_KEY) + .and_then(|epoch| epoch.parse().ok()) + .unwrap_or(0); + + let installed_size = fields + .get(PKGINFO_SIZE_KEY) + .and_then(|size| size.parse().ok()) + .unwrap_or(0); + + let meta = PackageMeta { + name, + version: Version { + epoch, + raw: raw_version, + }, + arch: fields.remove(PKGINFO_ARCH_KEY).unwrap_or_else(|| "any".to_owned()), + arch_sub: None, + maintainer: fields.remove(PKGINFO_MAINTAINER_KEY).unwrap_or_default(), + description: fields.remove(PKGINFO_DESCRIPTION_KEY).unwrap_or_default(), + license: fields.remove(PKGINFO_LICENSE_KEY), + url: fields.remove(PKGINFO_URL_KEY), + sha256, + installed_size, + }; + + Ok(PkgInfo { meta, dependencies }) + } + + fn parse_dependency(raw: &str) -> Dependency { + let bytes = raw.as_bytes(); + + for index in 0..bytes.len() { + let Some((constraint, operator_len)) = parse_constraint_prefix(&bytes[index..], &OPERATORS) else { + continue; + }; + + return Dependency { + name: raw[..index].to_owned(), + constraint, + version: Version::parse(&raw[index + operator_len..]), + }; + } + + Dependency { + name: raw.to_owned(), + constraint: CONSTRAINT_ANY, + version: Version::default(), + } + } +} diff --git a/decoders/alpm/src/symbols.zig b/decoders/alpm/src/symbols.zig deleted file mode 100644 index 8aa1f6c..0000000 --- a/decoders/alpm/src/symbols.zig +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ─────────────────────────────────────────────────────────────────── -const std = @import("std"); - -const types = @import("upac-backend-types"); -const BackendErrorCode = types.BackendErrorCode; -const fromError = types.fromError; -const BackendError = types.BackendError; -const PrepareData = types.PrepareData; - -const ffi = @import("upac-backend-ffi"); -const CPrepareRequest = ffi.CPrepareRequest; -const CPackageMeta = ffi.CPackageMeta; -const CVersion = ffi.CVersion; -const CSlice = ffi.CSlice; - -const dupeToCSlice = ffi.dupeToCSlice; -const dupeRequiredToCSlice = ffi.dupeRequiredToCSlice; - -const BackendMachine = @import("backend/backend.zig").BackendMachine; - -// ── FFI exports ─────────────────────────────────────────────────────────────── -pub export fn prepare(request_c: *const CPrepareRequest, out_meta: **CPackageMeta, out_temp_path: *CSlice) callconv(.c) i32 { - request_c.validate() catch |err| return @intFromEnum(fromError(err)); - - const cancel_token = request_c.cancel_token orelse return @intFromEnum(BackendErrorCode.invalid_entry); - - const prepare_data = PrepareData{ - .package_path_c = request_c.package_path.asZ(), - .temp_path_c = request_c.temp_dir_path.asZ(), - .checksum = request_c.checksum.toSlice(), - .on_hook = request_c.on_hook, - .hook_ctx = request_c.hook_ctx, - .cancel_token = cancel_token, - }; - - var result = BackendMachine.run(prepare_data, std.heap.c_allocator) catch |err| return @intFromEnum(fromError(err)); - defer result.meta.deinit(std.heap.c_allocator); - - const version_parts_copy = std.heap.c_allocator.dupe(u32, result.meta.version.parts) catch return @intFromEnum(BackendErrorCode.alloc_failed); - - const out_meta_ptr = std.heap.c_allocator.create(CPackageMeta) catch { - std.heap.c_allocator.free(version_parts_copy); - return @intFromEnum(BackendErrorCode.alloc_failed); - }; - - out_meta_ptr.* = CPackageMeta{ - .name = dupeRequiredToCSlice(std.heap.c_allocator, result.meta.name) catch return @intFromEnum(fromError(BackendError.InvalidPackage)), - .version = CVersion{ - .epoch = result.meta.version.epoch, - .release = result.meta.version.release, - .parts = .{ .ptr = version_parts_copy.ptr, .len = version_parts_copy.len }, - .pre = CSlice.fromSlice(if (result.meta.version.pre) |pre| - std.heap.c_allocator.dupeZ(u8, pre) catch return @intFromEnum(BackendErrorCode.alloc_failed) - else - null), - }, - .arch = dupeToCSlice(std.heap.c_allocator, result.meta.arch) catch return @intFromEnum(fromError(BackendError.AllocZFailed)), - .arch_sub = CSlice.fromSlice(null), - .maintainer = dupeToCSlice(std.heap.c_allocator, result.meta.maintainer) catch return @intFromEnum(fromError(BackendError.AllocZFailed)), - .description = dupeToCSlice(std.heap.c_allocator, result.meta.description) catch return @intFromEnum(fromError(BackendError.AllocZFailed)), - .license = CSlice.fromSlice(if (result.meta.license) |lic| - std.heap.c_allocator.dupeZ(u8, lic) catch return @intFromEnum(BackendErrorCode.alloc_failed) - else - null), - .url = CSlice.fromSlice(if (result.meta.url) |url| - std.heap.c_allocator.dupeZ(u8, url) catch return @intFromEnum(BackendErrorCode.alloc_failed) - else - null), - .sha256 = result.meta.sha256, - .installed_size = result.meta.installed_size, - }; - - out_meta.* = out_meta_ptr; - out_temp_path.* = dupeToCSlice(std.heap.c_allocator, result.temp_path) catch return @intFromEnum(fromError(BackendError.AllocZFailed)); - - return @intFromEnum(BackendErrorCode.ok); -} - -pub export fn cleanup(path_c: CSlice) callconv(.c) void { - const io = std.Io.Threaded.global_single_threaded.io(); - const path = path_c.toSlice(); - - std.Io.Dir.cwd().deleteTree(io, path) catch {}; - - std.heap.c_allocator.free(path); -} - -pub export fn free_meta(package_meta_c: *CPackageMeta) callconv(.c) void { - package_meta_c.free(std.heap.c_allocator); -} - -pub export fn version_abi() callconv(.c) u32 { - return ffi.ABI_VERSION; -} diff --git a/decoders/alpm/src/triggers.rs b/decoders/alpm/src/triggers.rs new file mode 100644 index 0000000..82e4a9c --- /dev/null +++ b/decoders/alpm/src/triggers.rs @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_types::DecoderTrigger; + +use crate::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 + .into_iter() + .map(native_name) + .filter(|name| declares_function(content, name)) + .map(str::to_owned) + .collect() +} + +fn native_name(trigger: DecoderTrigger) -> &'static str { + match trigger { + DecoderTrigger::PreInstall => PRE_INSTALL_FN, + DecoderTrigger::PostInstall => POST_INSTALL_FN, + DecoderTrigger::PreUpgrade => PRE_UPGRADE_FN, + DecoderTrigger::PostUpgrade => POST_UPGRADE_FN, + DecoderTrigger::PreRemove => PRE_REMOVE_FN, + DecoderTrigger::PostRemove => POST_REMOVE_FN, + } +} + +fn declares_function(content: &str, name: &str) -> bool { + content.lines().any(|line| { + if line.starts_with(char::is_whitespace) { + return false; + } + + let Some(rest) = line.strip_prefix(name) else { + return false; + }; + + rest.trim_start().starts_with('(') + }) +} diff --git a/decoders/alpm/src/types/errors.zig b/decoders/alpm/src/types/errors.zig deleted file mode 100644 index ed8661b..0000000 --- a/decoders/alpm/src/types/errors.zig +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -pub const BackendErrorCode = enum(i32) { - ok = 0, - checksum_mismatch = 1, - extraction_failed = 2, - metadata_not_found = 3, - invalid_package = 4, - archive_open_failed = 5, - archive_read_failed = 6, - archive_extract_failed = 7, - temp_dir_failed = 8, - alloc_failed = 9, - cancelled = 10, - read_failed = 11, - invalid_entry = 12, - abi_mismatch = 13, - unexpected = 99, -}; - -pub fn fromError(err: anyerror) BackendErrorCode { - return switch (err) { - error.ChecksumMismatch => .checksum_mismatch, - error.ExtractionFailed => .extraction_failed, - error.MetadataNotFound => .metadata_not_found, - error.InvalidPackage => .invalid_package, - error.ArchiveOpenFailed => .archive_open_failed, - error.ArchiveReadFailed => .archive_read_failed, - error.ArchiveExtractFailed => .archive_extract_failed, - error.TempDirFailed => .temp_dir_failed, - error.AllocZFailed, error.OutOfMemory => .alloc_failed, - error.Cancelled => .cancelled, - error.ReadFailed => .read_failed, - error.InvalidEntry => .invalid_entry, - error.AbiMismatch => .abi_mismatch, - else => .unexpected, - }; -} diff --git a/decoders/alpm/src/types/types.zig b/decoders/alpm/src/types/types.zig deleted file mode 100644 index 7b32d2e..0000000 --- a/decoders/alpm/src/types/types.zig +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -pub const std = @import("std"); - -const meta_fields = @import("upac-meta-fields"); - -const errors = @import("errors.zig"); -pub const BackendErrorCode = errors.BackendErrorCode; -pub const fromError = errors.fromError; - -pub const PackageMetaField = enum { - Package, - Version, - @"Installed-Size", - Architecture, - Description, - License, - Homepage, - Maintainer, -}; - -// Listing specific backend errors when working with archives and metadata -pub const BackendError = error{ - ChecksumMismatch, - ExtractionFailed, - MetadataNotFound, - InvalidPackage, - ReadFailed, - ArchiveOpenFailed, - ArchiveReadFailed, - ArchiveExtractFailed, - OutOfMemory, - TempDirFailed, - AllocZFailed, - Cancelled, -}; - -// ── Inner FSM types ─────────────────────────────────────────────────────── -pub const StateId = enum(u8) { - verifying = 0, - extracting = 1, - reading_meta = 2, - special_step = 3, - - done = 4, -}; - -// ── Version ───────────────────────────────────────────────────────────────── -pub const Version = struct { - epoch: u32 = 0, - parts: []const u32, - pre: ?[]const u8 = null, - release: u32 = 1, - - pub fn deinit(self: *const Version, allocator: std.mem.Allocator) void { - allocator.free(self.parts); - if (self.pre) |pre| allocator.free(pre); - } -}; - -pub const PrepareResult = struct { - meta: PackageMeta, - temp_path: [:0]const u8, -}; - -pub const HookResponse = enum(u8) { - proceed = 0, - cancel = 1, -}; - -pub const HookFn = fn (event: u32, data: ?*const anyopaque, ctx: ?*anyopaque) callconv(.c) HookResponse; - -pub const PrepareData = struct { - package_path_c: [*:0]const u8, - temp_path_c: [*:0]const u8, - - checksum: []const u8, - - on_hook: ?*const HookFn = null, - hook_ctx: ?*anyopaque = null, - - cancel_token: *const CancelToken, -}; - -// ── Public types ──────────────────────────────────────────────────────────── -pub const CancelToken = extern struct { - _flag: u8, - _hook: ?*const fn (ctx: ?*anyopaque) callconv(.c) void = null, - _hook_ctx: ?*anyopaque = null, - - pub fn isCancelled(self: *const CancelToken) bool { - return @atomicLoad(u8, &self._flag, .acquire) != 0; - } -}; - -pub const RawMeta = struct { - name: ?[]const u8 = null, - version_str: ?[]const u8 = null, - arch: ?[]const u8 = null, - description: ?[]const u8 = null, - url: ?[]const u8 = null, - maintainer: ?[]const u8 = null, - license: ?[]const u8 = null, - installed_size: u64 = 0, - - pub fn deinit(self: *RawMeta, allocator: std.mem.Allocator) void { - if (self.name) |value| allocator.free(value); - if (self.version_str) |value| allocator.free(value); - if (self.arch) |value| allocator.free(value); - if (self.description) |value| allocator.free(value); - if (self.url) |value| allocator.free(value); - if (self.maintainer) |value| allocator.free(value); - if (self.license) |value| allocator.free(value); - } -}; - -// Main structure containing package metadata -pub const PackageMeta = struct { - name: []const u8, - version: Version, - arch: []const u8, - arch_sub: ?[]const u8, - maintainer: []const u8, - description: []const u8, - license: ?[]const u8, - url: ?[]const u8, - sha256: [32]u8, - installed_size: u64, - - pub fn deinit(self: *PackageMeta, allocator: std.mem.Allocator) void { - allocator.free(self.name); - allocator.free(self.arch); - allocator.free(self.maintainer); - allocator.free(self.description); - self.version.deinit(allocator); - - if (self.arch_sub) |sub| allocator.free(sub); - if (self.license) |license| allocator.free(license); - if (self.url) |url| allocator.free(url); - } -}; - -pub fn buildFieldMap() std.StaticStringMap(PackageMetaField) { - return std.StaticStringMap(PackageMetaField).initComptime(.{ - .{ meta_fields.Package, .Package }, - .{ meta_fields.Version, .Version }, - .{ meta_fields.@"Installed-Size", .@"Installed-Size" }, - .{ meta_fields.Architecture, .Architecture }, - .{ meta_fields.Description, .Description }, - .{ meta_fields.License, .License }, - .{ meta_fields.Homepage, .Homepage }, - .{ meta_fields.Maintainer, .Maintainer }, - }); -} diff --git a/decoders/alpm/src/verify.rs b/decoders/alpm/src/verify.rs new file mode 100644 index 0000000..704d458 --- /dev/null +++ b/decoders/alpm/src/verify.rs @@ -0,0 +1,42 @@ +// 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::hook::CancelToken; + +use crate::error::DecodeError; + +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 new file mode 100644 index 0000000..13b78a6 --- /dev/null +++ b/decoders/alpm/tests/pkginfo.rs @@ -0,0 +1,125 @@ +// 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::decoder::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS}; + +use upac_decoder_alpm::error::DecodeError; +use upac_decoder_alpm::pkginfo::PkgInfo; + +const CHECKSUM: [u8; 32] = [7; 32]; + +#[test] +fn parses_minimal_pkginfo_with_defaults() { + let content = "pkgname = foo\npkgver = 1.2.3\n"; + + let pkg_info = PkgInfo::parse(content, CHECKSUM).unwrap(); + + assert_eq!(pkg_info.meta.name, "foo"); + assert_eq!(pkg_info.meta.version.raw, "1.2.3"); + assert_eq!(pkg_info.meta.version.epoch, 0); + assert_eq!(pkg_info.meta.arch, "any"); + assert_eq!(pkg_info.meta.maintainer, ""); + assert_eq!(pkg_info.meta.description, ""); + assert_eq!(pkg_info.meta.license, None); + assert_eq!(pkg_info.meta.url, None); + assert_eq!(pkg_info.meta.sha256, CHECKSUM); + assert!(pkg_info.dependencies.is_empty()); +} + +#[test] +fn combines_pkgver_and_pkgrel_into_the_raw_version() { + let content = "pkgname = foo\npkgver = 1.2.3\npkgrel = 2\n"; + + let pkg_info = PkgInfo::parse(content, CHECKSUM).unwrap(); + + assert_eq!(pkg_info.meta.version.raw, "1.2.3-2"); +} + +#[test] +fn parses_epoch_separately_from_the_version() { + let content = "pkgname = foo\npkgver = 1.2.3\nepoch = 2\n"; + + let pkg_info = PkgInfo::parse(content, CHECKSUM).unwrap(); + + assert_eq!(pkg_info.meta.version.epoch, 2); + assert_eq!(pkg_info.meta.version.raw, "1.2.3"); +} + +#[test] +fn parses_all_fields_and_ignores_comments_and_blank_lines() { + let content = "# a comment\n\npkgname = foo\npkgver = 1.2.3\narch = x86_64\npkgdesc = A test package\nurl = \ + https://example.com\npackager = Jane \nlicense = MIT\nsize = 4096\n"; + + let pkg_info = PkgInfo::parse(content, CHECKSUM).unwrap(); + + assert_eq!(pkg_info.meta.arch, "x86_64"); + assert_eq!(pkg_info.meta.description, "A test package"); + assert_eq!(pkg_info.meta.url, Some("https://example.com".to_owned())); + assert_eq!(pkg_info.meta.maintainer, "Jane "); + assert_eq!(pkg_info.meta.license, Some("MIT".to_owned())); + assert_eq!(pkg_info.meta.installed_size, 4096); +} + +#[test] +fn missing_pkgname_is_malformed() { + let content = "pkgver = 1.2.3\n"; + + let result = PkgInfo::parse(content, CHECKSUM); + + assert_eq!(result.unwrap_err(), DecodeError::MalformedPkgInfo); +} + +#[test] +fn missing_pkgver_is_malformed() { + let content = "pkgname = foo\n"; + + let result = PkgInfo::parse(content, CHECKSUM); + + assert_eq!(result.unwrap_err(), DecodeError::MalformedPkgInfo); +} + +#[test] +fn parses_dependencies_with_every_constraint_operator() { + let content = "pkgname = foo\npkgver = 1.2.3\ndepend = bash\ndepend = glibc>=2.36\ndepend = openssl<=3\ndepend = \ + python=3.12\ndepend = zlib<2\ndepend = curl>7\n"; + + let pkg_info = PkgInfo::parse(content, CHECKSUM).unwrap(); + + let dependencies = pkg_info.dependencies; + assert_eq!(dependencies.len(), 6); + + assert_eq!(dependencies[0].name, "bash"); + assert_eq!(dependencies[0].constraint, CONSTRAINT_ANY); + + assert_eq!(dependencies[1].name, "glibc"); + assert_eq!(dependencies[1].constraint, CONSTRAINT_GREATER | CONSTRAINT_EQUAL); + assert_eq!(dependencies[1].version.raw, "2.36"); + + assert_eq!(dependencies[2].name, "openssl"); + assert_eq!(dependencies[2].constraint, CONSTRAINT_LESS | CONSTRAINT_EQUAL); + assert_eq!(dependencies[2].version.raw, "3"); + + assert_eq!(dependencies[3].name, "python"); + assert_eq!(dependencies[3].constraint, CONSTRAINT_EQUAL); + assert_eq!(dependencies[3].version.raw, "3.12"); + + assert_eq!(dependencies[4].name, "zlib"); + assert_eq!(dependencies[4].constraint, CONSTRAINT_LESS); + assert_eq!(dependencies[4].version.raw, "2"); + + assert_eq!(dependencies[5].name, "curl"); + assert_eq!(dependencies[5].constraint, CONSTRAINT_GREATER); + assert_eq!(dependencies[5].version.raw, "7"); +} + +#[test] +fn parses_a_dependency_version_with_its_own_epoch() { + let content = "pkgname = foo\npkgver = 1.2.3\ndepend = python>=2:3.10\n"; + + let pkg_info = PkgInfo::parse(content, CHECKSUM).unwrap(); + + assert_eq!(pkg_info.dependencies[0].version.epoch, 2); + assert_eq!(pkg_info.dependencies[0].version.raw, "3.10"); +} diff --git a/decoders/alpm/tests/triggers.rs b/decoders/alpm/tests/triggers.rs new file mode 100644 index 0000000..6aee41b --- /dev/null +++ b/decoders/alpm/tests/triggers.rs @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use upac_decoder_alpm::triggers; + +#[test] +fn finds_no_triggers_in_empty_content() { + let triggers = triggers::scan(""); + + assert!(triggers.is_empty()); +} + +#[test] +fn finds_all_declared_lifecycle_functions_in_declaration_order() { + let content = "post_remove() {\n :\n}\n\npre_install() {\n :\n}\n\npost_install ( ) {\n :\n}\n"; + + let triggers = triggers::scan(content); + + assert_eq!(triggers, vec!["pre_install", "post_install", "post_remove"]); +} + +#[test] +fn ignores_names_that_are_not_function_declarations() { + let content = "# pre_install is mentioned here but not declared\necho pre_install\n"; + + let triggers = triggers::scan(content); + + assert!(triggers.is_empty()); +} + +#[test] +fn ignores_indented_occurrences() { + let content = "post_install() {\n pre_install()\n}\n"; + + let triggers = triggers::scan(content); + + assert_eq!(triggers, vec!["post_install"]); +} diff --git a/decoders/alpm/upac-alpm.toml b/decoders/alpm/upac-alpm.toml index 66d09d0..29cb19a 100644 --- a/decoders/alpm/upac-alpm.toml +++ b/decoders/alpm/upac-alpm.toml @@ -1,15 +1,17 @@ # SPDX-FileCopyrightText: 2026 JustPav # SPDX-FileCopyrightText: 2026 SmoothTeam # -# SPDX-License-Identifier: LGPL-3.0-or-later +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -# Declarative decoder manifest (doc §5.8/§6). Not installed by `zig build` — +# Declarative decoder manifest (doc §5.8/§6). Not installed by `cargo build` — # there is no packaging pipeline (PKGBUILD/spec/etc) in this repo yet; this # file is the canonical source a future package build copies to -# /etc/upac.d/decoders/upac-alpm.toml. +# /etc/upac.d/decoders/upac-alpm.toml. `library` matches this crate's +# `[lib] name = "upac_decoder_alpm"` (Cargo lib-name-to-filename convention: +# `lib` prefix + name verbatim, underscores kept as-is). format = "alpm" extensions = ["pkg.tar", "pkg.tar.gz", "pkg.tar.xz", "pkg.tar.zst"] -library = "/usr/lib/upac/decoders/libupac-alpm.so" +library = "/usr/lib/upac/decoders/libupac_decoder_alpm.so" # No mime type for alpm packages is registered in shared-mime-info (verified # against freedesktop.org.xml) — this is the same unofficial vendor-prefixed diff --git a/decoders/deb/Cargo.toml b/decoders/deb/Cargo.toml new file mode 100644 index 0000000..f12d855 --- /dev/null +++ b/decoders/deb/Cargo.toml @@ -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 + +[package] +name = "deb" +description = "Debian (.deb) package decoder plugin for upac, implementing dpkg's package format" +version.workspace = true + +edition.workspace = true +rust-version.workspace = true + +license.workspace = true + +readme.workspace = true +homepage.workspace = true +repository.workspace = true + +keywords.workspace = true +categories.workspace = true + +[lints] +workspace = true + +[lib] +name = "upac_decoder_deb" +crate-type = ["cdylib", "rlib"] + +[dependencies] +upac-abi = { workspace = true } +upac-types = { workspace = true } + +ar = { workspace = true } +flate2 = { workspace = true } +sha2 = { workspace = true } +tar = { workspace = true } +xz2 = { workspace = true } +zstd = { workspace = true } + +[build-dependencies] +toml = { workspace = true } + +[features] +default = ["cdylib"] +cdylib = [] diff --git a/decoders/deb/build.rs b/decoders/deb/build.rs new file mode 100644 index 0000000..3e5c702 --- /dev/null +++ b/decoders/deb/build.rs @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::env::var; +use std::error::Error; +use std::fs::{read_to_string, write}; +use std::path::Path; + +use toml::{Value, from_str}; + +fn main() -> Result<(), Box> { + let manifest_dir = var("CARGO_MANIFEST_DIR")?; + + let mut generated = String::new(); + generated.push_str(&generate_decoder_toml(&manifest_dir)?); + generated.push_str(&generate_manifest_module(&manifest_dir)?); + + let out = Path::new(&var("OUT_DIR")?).join("layout.rs"); + write(out, generated)?; + + Ok(()) +} + +fn generate_decoder_toml(manifest_dir: &str) -> Result> { + let source = Path::new(manifest_dir).join("../decoder.toml"); + + println!("cargo:rerun-if-changed={}", source.display()); + + let raw = read_to_string(&source)?; + let config: Value = from_str(&raw)?; + + let section = "deb"; + let entries = config + .get(section) + .and_then(Value::as_table) + .ok_or_else(|| format!("decoder.toml: [{section}] must be a table"))?; + + let mut generated = String::new(); + generated.push_str(&format!("pub mod {section} {{\n")); + + for (key, value) in entries { + let rendered = if let Some(value) = value.as_str() { + format!("&str = {value:?}") + } else if let Some(value) = value.as_integer() { + format!("u32 = {value}") + } else { + return Err(format!("decoder.toml: {section}.{key} must be a string or integer").into()); + }; + + generated.push_str(&format!(" pub const {}: {rendered};\n", key.to_uppercase())); + } + + generated.push_str("}\n"); + + Ok(generated) +} + +/// Compiles this crate's own deployable manifest (`format`/`extensions`) into constants, so a +/// `builtin-deb` build can dispatch by format without reading `upac-deb.toml` from disk at +/// runtime — `library`/`mime` are runtime-deployment-only fields, not needed here. +fn generate_manifest_module(manifest_dir: &str) -> Result> { + let source = Path::new(manifest_dir).join("upac-deb.toml"); + + println!("cargo:rerun-if-changed={}", source.display()); + + let raw = read_to_string(&source)?; + let config: Value = from_str(&raw)?; + + let format = config + .get("format") + .and_then(Value::as_str) + .ok_or("upac-deb.toml: format must be a string")?; + + let extensions = config + .get("extensions") + .and_then(Value::as_array) + .ok_or("upac-deb.toml: extensions must be an array")? + .iter() + .map(|entry| { + entry + .as_str() + .ok_or("upac-deb.toml: extensions entries must be strings") + }) + .collect::, _>>()?; + + let mut generated = String::new(); + generated.push_str("pub mod manifest {\n"); + generated.push_str(&format!(" pub const FORMAT: &str = {format:?};\n")); + generated.push_str(" pub const EXTENSIONS: &[&str] = &[\n"); + for extension in extensions { + generated.push_str(&format!(" {extension:?},\n")); + } + generated.push_str(" ];\n"); + generated.push_str("}\n"); + + Ok(generated) +} diff --git a/decoders/deb/build.zig b/decoders/deb/build.zig deleted file mode 100644 index 98f720a..0000000 --- a/decoders/deb/build.zig +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ───────────────────────────────────────────────────────────────────── -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - const strip = b.option(bool, "strip", "Strip debug symbols") orelse false; - const stack_check = b.option(bool, "stack-check", "Check for stack overflows") orelse false; - - // ── C libs ──────────────────────────────────────────────────────────────── - const translated_libs = b.addTranslateC(.{ - .root_source_file = b.path("src/imports.h"), - .target = target, - .optimize = optimize, - }); - translated_libs.link_libc = true; - translated_libs.linkSystemLibrary("archive", .{}); - - const c_libs_module = translated_libs.createModule(); - - // ── Config ZON modules ──────────────────────────────────────────────────── - const upac_meta_fields = b.createModule(.{ .root_source_file = b.path("config/meta_fields.zon") }); - - // ── Types ───────────────────────────────────────────────────────────────── - const upac_backend_types = b.createModule(.{ - .root_source_file = b.path("src/types/types.zig"), - .target = target, - .optimize = optimize, - }); - upac_backend_types.addImport("upac-meta-fields", upac_meta_fields); - - // ── FFI ───────────────────────────────────────────────────────────────── - const upac_backend_ffi = b.createModule(.{ - .root_source_file = b.path("src/ffi.zig"), - .target = target, - .optimize = optimize, - }); - - upac_backend_ffi.addImport("upac-backend-types", upac_backend_types); - - // ── Root ────────────────────────────────────────────────────────────────── - const upac_backend_root = b.createModule(.{ - .root_source_file = b.path("src/symbols.zig"), - .target = target, - .optimize = optimize, - }); - - upac_backend_root.strip = strip; - upac_backend_root.stack_check = stack_check; - - // ── Shared library ──────────────────────────────────────────────────────── - const shared_lib = b.addLibrary(.{ - .name = "upac-deb", - .linkage = .dynamic, - .root_module = upac_backend_root, - }); - - shared_lib.root_module.link_libc = true; - - shared_lib.root_module.addImport("c-libs", c_libs_module); - shared_lib.root_module.addImport("upac-backend-types", upac_backend_types); - shared_lib.root_module.addImport("upac-backend-ffi", upac_backend_ffi); - shared_lib.root_module.addImport("upac-meta-fields", upac_meta_fields); - - shared_lib.root_module.strip = strip; - shared_lib.root_module.stack_check = stack_check; - shared_lib.bundle_compiler_rt = false; - shared_lib.link_gc_sections = false; - - b.installArtifact(shared_lib); -} diff --git a/decoders/deb/build.zig.zon b/decoders/deb/build.zig.zon deleted file mode 100644 index be09366..0000000 --- a/decoders/deb/build.zig.zon +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -.{ - .name = .upac_deb, - .version = "0.1.4", - .fingerprint = 0x1746a24a2a0dd22e, - .minimum_zig_version = "0.16.0", - .dependencies = .{}, - .paths = .{ "build.zig", "build.zig.zon", "src" }, -} diff --git a/decoders/deb/config/meta_fields.zon b/decoders/deb/config/meta_fields.zon deleted file mode 100644 index 86b98e3..0000000 --- a/decoders/deb/config/meta_fields.zon +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -.{ - .Package = "name", - .Version = "version", - .@"Installed-Size" = "size", - .Architecture = "arch", - .Description = "description", - .Homepage = "url", - .Maintainer = "packager", -} diff --git a/decoders/deb/src/backend/backend.zig b/decoders/deb/src/backend/backend.zig deleted file mode 100644 index 0eea61f..0000000 --- a/decoders/deb/src/backend/backend.zig +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ─────────────────────────────────────────────────────────────────── -pub const std = @import("std"); - -const types = @import("upac-backend-types"); -pub const BackendError = types.BackendError; -pub const StateId = types.StateId; -pub const PackageMeta = types.PackageMeta; -pub const PrepareData = types.PrepareData; -pub const PrepareResult = types.PrepareResult; -pub const CancelToken = types.CancelToken; - -const verifying = @import("verifying/verifying.zig"); -const unpacking = @import("unpacking/unpacking.zig"); -const parsing = @import("parsing/parsing.zig"); - -// ── BackendMachine ──────────────────────────────────────────────────────────── -pub const BackendMachine = struct { - data: PrepareData, - - meta: ?PackageMeta = null, - temp_package_path: ?[:0]const u8 = null, - - allocator: std.mem.Allocator, - io: std.Io, - - pub fn deinit(self: *BackendMachine) void { - if (self.temp_package_path) |path| self.allocator.free(path); - } - - pub fn hook(self: *BackendMachine, event: StateId) BackendError!void { - const cb = self.data.on_hook orelse return; - if (cb(@intFromEnum(event), null, self.data.hook_ctx) == .cancel) return BackendError.Cancelled; - } - - pub fn run(data: PrepareData, allocator: std.mem.Allocator) BackendError!PrepareResult { - var state = StateId.verifying; - var machine = BackendMachine{ - .data = data, - - .io = std.Io.Threaded.global_single_threaded.io(), - .allocator = allocator, - }; - defer machine.deinit(); - - while (state != .done) { - machine.hook(state) catch |err| return err; - switch (state) { - .verifying => { - verifying.run(&machine) catch |err| return err; - state = .extracting; - }, - .extracting => { - unpacking.run(&machine) catch |err| return err; - state = .reading_meta; - }, - .reading_meta => { - parsing.run(&machine) catch |err| return err; - state = .done; - }, - .done, .special_step => {}, - } - } - - const temp_package_path = machine.temp_package_path orelse return BackendError.TempDirFailed; - machine.temp_package_path = null; - - return PrepareResult{ - .meta = machine.meta orelse return BackendError.MetadataNotFound, - .temp_path = temp_package_path, - }; - } -}; diff --git a/decoders/deb/src/backend/parsing/parsing.zig b/decoders/deb/src/backend/parsing/parsing.zig deleted file mode 100644 index 4f6468c..0000000 --- a/decoders/deb/src/backend/parsing/parsing.zig +++ /dev/null @@ -1,450 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ─────────────────────────────────────────────────────────────────── -const std = @import("std"); - -const c_libs = @import("c-libs"); - -const types = @import("upac-backend-types"); - -const BackendError = types.BackendError; - -const PackageMeta = types.PackageMeta; -const RawMeta = types.RawMeta; - -const control_field_map = types.control_field_map; - -const backend = @import("../backend.zig"); -const Machine = backend.BackendMachine; - -const utils = @import("utils.zig"); -const parseLicenseFromCopyright = utils.parseLicenseFromCopyright; -const parseVersion = utils.parseVersion; - -// ── ParsingState ────────────────────────────────────────────────────────────── -const ParsingState = enum { - open_archive, - find_tars, - open_control_archive, - scan_control_files, - verify_md5sums, - open_data_archive, - scan_copyright_files, - parse_control, - build_meta, - done, -}; - -// ── ParsingMachine ──────────────────────────────────────────────────────────── -const ParsingMachine = struct { - backend: *Machine, - - archive_reader: ?*c_libs.archive = null, - - control_tar_buf: ?[]u8 = null, - control_content: ?[]u8 = null, - control_inner_reader: ?*c_libs.archive = null, - - data_tar_buf: ?[]u8 = null, - data_inner_reader: ?*c_libs.archive = null, - - md5sums_content: ?[]u8 = null, - - copyright_content: ?[]u8 = null, - - raw_meta: RawMeta = .{}, - - fn stateFailed(self: *ParsingMachine, err: BackendError) BackendError { - if (self.archive_reader) |reader| { - _ = c_libs.archive_read_free(reader); - self.archive_reader = null; - } - - if (self.control_inner_reader) |reader| { - _ = c_libs.archive_read_free(reader); - self.control_inner_reader = null; - } - - if (self.control_tar_buf) |buf| { - self.backend.allocator.free(buf); - self.control_tar_buf = null; - } - - if (self.data_inner_reader) |reader| { - _ = c_libs.archive_read_free(reader); - self.data_inner_reader = null; - } - - if (self.data_tar_buf) |buf| { - self.backend.allocator.free(buf); - self.data_tar_buf = null; - } - - if (self.control_content) |content| { - self.backend.allocator.free(content); - self.control_content = null; - } - - if (self.md5sums_content) |content| { - self.backend.allocator.free(content); - self.md5sums_content = null; - } - - if (self.copyright_content) |content| { - self.backend.allocator.free(content); - self.copyright_content = null; - } - - self.raw_meta.deinit(self.backend.allocator); - - return err; - } -}; - -// ── Trampoline ──────────────────────────────────────────────────────────────── -pub fn run(machine: *Machine) BackendError!void { - var parsing = ParsingMachine{ .backend = machine }; - - var state = ParsingState.open_archive; - while (state != .done) { - if (machine.data.cancel_token.isCancelled()) return parsing.stateFailed(BackendError.Cancelled); - state = switch (state) { - .open_archive => try stateOpenArchive(&parsing), - .find_tars => try stateFindTars(&parsing), - .open_control_archive => try stateOpenControlArchive(&parsing), - .scan_control_files => try stateScanControlFiles(&parsing), - .verify_md5sums => try stateVerifyMd5sums(&parsing), - .open_data_archive => try stateOpenDataArchive(&parsing), - .scan_copyright_files => try stateScanCopyrightFiles(&parsing), - .parse_control => try stateParseControl(&parsing), - .build_meta => try stateBuildMeta(&parsing), - .done => unreachable, - }; - } -} - -// ── States ──────────────────────────────────────────────────────────────────── -fn stateOpenArchive(machine: *ParsingMachine) BackendError!ParsingState { - const archive_reader = c_libs.archive_read_new() orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - machine.archive_reader = archive_reader; - - _ = c_libs.archive_read_support_format_ar(archive_reader); - _ = c_libs.archive_read_support_filter_all(archive_reader); - - if (c_libs.archive_read_open_filename(archive_reader, machine.backend.data.package_path_c, 16384) != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveOpenFailed); - - return .find_tars; -} - -fn stateFindTars(machine: *ParsingMachine) BackendError!ParsingState { - const archive_reader = machine.archive_reader orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - - var outer_entry: ?*c_libs.archive_entry = null; - const result = c_libs.archive_read_next_header(archive_reader, &outer_entry); - - if (result == c_libs.ARCHIVE_EOF) { - _ = c_libs.archive_read_free(archive_reader); - machine.archive_reader = null; - - if (machine.control_tar_buf == null) return machine.stateFailed(BackendError.InvalidPackage); - - return .open_control_archive; - } - if (result != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveReadFailed); - - const entry_name = std.mem.span(c_libs.archive_entry_pathname(outer_entry)); - const raw_size = c_libs.archive_entry_size(outer_entry); - - if (std.mem.startsWith(u8, entry_name, "control.tar") and machine.control_tar_buf == null) { - if (raw_size <= 0) return machine.stateFailed(BackendError.InvalidPackage); - const entry_size: usize = @intCast(raw_size); - const control_buf = machine.backend.allocator.alloc(u8, entry_size) catch return machine.stateFailed(BackendError.OutOfMemory); - machine.control_tar_buf = control_buf; - if (c_libs.archive_read_data(archive_reader, control_buf.ptr, entry_size) < 0) return machine.stateFailed(BackendError.ArchiveReadFailed); - } else if (std.mem.startsWith(u8, entry_name, "data.tar") and machine.data_tar_buf == null) { - if (raw_size > 0) { - const entry_size: usize = @intCast(raw_size); - const data_buf = machine.backend.allocator.alloc(u8, entry_size) catch return machine.stateFailed(BackendError.OutOfMemory); - machine.data_tar_buf = data_buf; - if (c_libs.archive_read_data(archive_reader, data_buf.ptr, entry_size) < 0) return machine.stateFailed(BackendError.ArchiveReadFailed); - } - } - - if (machine.control_tar_buf != null and machine.data_tar_buf != null) { - _ = c_libs.archive_read_free(archive_reader); - machine.archive_reader = null; - return .open_control_archive; - } - - return .find_tars; -} - -fn stateOpenControlArchive(machine: *ParsingMachine) BackendError!ParsingState { - const control_tar_buf = machine.control_tar_buf orelse return machine.stateFailed(BackendError.InvalidPackage); - - const inner_reader = c_libs.archive_read_new() orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - machine.control_inner_reader = inner_reader; - - _ = c_libs.archive_read_support_format_tar(inner_reader); - _ = c_libs.archive_read_support_filter_all(inner_reader); - - if (c_libs.archive_read_open_memory(inner_reader, control_tar_buf.ptr, control_tar_buf.len) != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveOpenFailed); - - return .scan_control_files; -} - -fn stateScanControlFiles(machine: *ParsingMachine) BackendError!ParsingState { - const inner_reader = machine.control_inner_reader orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - - var inner_entry: ?*c_libs.archive_entry = null; - const result = c_libs.archive_read_next_header(inner_reader, &inner_entry); - - if (result == c_libs.ARCHIVE_EOF) { - _ = c_libs.archive_read_free(inner_reader); - machine.control_inner_reader = null; - if (machine.control_tar_buf) |buf| { - machine.backend.allocator.free(buf); - machine.control_tar_buf = null; - } - if (machine.control_content == null) return machine.stateFailed(BackendError.InvalidPackage); - return .verify_md5sums; - } - if (result != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveReadFailed); - - const entry_name = std.mem.span(c_libs.archive_entry_pathname(inner_entry)); - const name = if (std.mem.startsWith(u8, entry_name, "./")) entry_name[2..] else entry_name; - const raw_size = c_libs.archive_entry_size(inner_entry); - - if (raw_size > 0) { - const entry_size: usize = @intCast(raw_size); - if (std.mem.eql(u8, name, "control") and machine.control_content == null) { - const content = machine.backend.allocator.alloc(u8, entry_size) catch return machine.stateFailed(BackendError.OutOfMemory); - machine.control_content = content; - if (c_libs.archive_read_data(inner_reader, content.ptr, entry_size) < 0) return machine.stateFailed(BackendError.ArchiveReadFailed); - } else if (std.mem.eql(u8, name, "md5sums") and machine.md5sums_content == null) { - const content = machine.backend.allocator.alloc(u8, entry_size) catch return machine.stateFailed(BackendError.OutOfMemory); - machine.md5sums_content = content; - if (c_libs.archive_read_data(inner_reader, content.ptr, entry_size) < 0) return machine.stateFailed(BackendError.ArchiveReadFailed); - } - } - - if (machine.control_content != null and machine.md5sums_content != null) { - _ = c_libs.archive_read_free(inner_reader); - machine.control_inner_reader = null; - if (machine.control_tar_buf) |buf| { - machine.backend.allocator.free(buf); - machine.control_tar_buf = null; - } - return .verify_md5sums; - } - - return .scan_control_files; -} - -fn stateVerifyMd5sums(machine: *ParsingMachine) BackendError!ParsingState { - var io_buf: [4096]u8 = undefined; - - const md5sums_content = machine.md5sums_content orelse return .open_data_archive; - defer machine.backend.allocator.free(md5sums_content); - machine.md5sums_content = null; - - const temp_package_path = machine.backend.temp_package_path orelse return machine.stateFailed(BackendError.TempDirFailed); - - var temp_dir = std.Io.Dir.openDirAbsolute(machine.backend.io, temp_package_path, .{}) catch return machine.stateFailed(BackendError.ReadFailed); - defer temp_dir.close(machine.backend.io); - - var lines = std.mem.splitScalar(u8, md5sums_content, '\n'); - while (lines.next()) |line| { - var hasher = std.crypto.hash.Md5.init(.{}); - - const trimmed_line = std.mem.trim(u8, line, " \t\r"); - if (trimmed_line.len == 0) continue; - - var tokens = std.mem.tokenizeAny(u8, trimmed_line, " \t"); - const expected_hex = tokens.next() orelse continue; - - const file_path = std.mem.trim(u8, tokens.rest(), " \t"); - - const file = temp_dir.openFile(machine.backend.io, file_path, .{}) catch continue; - defer file.close(machine.backend.io); - - while (true) { - const iov = [1][]u8{io_buf[0..]}; - const bytes_read = file.readStreaming(machine.backend.io, &iov) catch |err| { - if (err == error.EndOfStream) break; - - return machine.stateFailed(BackendError.ReadFailed); - }; - - if (bytes_read == 0) break; - hasher.update(io_buf[0..bytes_read]); - } - - var digest_hash: [std.crypto.hash.Md5.digest_length]u8 = undefined; - hasher.final(&digest_hash); - const actual_hex = std.fmt.bytesToHex(digest_hash, .lower); - - if (!std.mem.eql(u8, &actual_hex, expected_hex)) return machine.stateFailed(BackendError.ChecksumMismatch); - } - - return .open_data_archive; -} - -fn stateOpenDataArchive(machine: *ParsingMachine) BackendError!ParsingState { - const data_tar_buf = machine.data_tar_buf orelse return .parse_control; - - const inner_reader = c_libs.archive_read_new() orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - machine.data_inner_reader = inner_reader; - - _ = c_libs.archive_read_support_format_tar(inner_reader); - _ = c_libs.archive_read_support_filter_all(inner_reader); - - if (c_libs.archive_read_open_memory(inner_reader, data_tar_buf.ptr, data_tar_buf.len) != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveOpenFailed); - - return .scan_copyright_files; -} - -fn stateScanCopyrightFiles(machine: *ParsingMachine) BackendError!ParsingState { - const inner_reader = machine.data_inner_reader orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - - var inner_entry: ?*c_libs.archive_entry = null; - const result = c_libs.archive_read_next_header(inner_reader, &inner_entry); - - if (result == c_libs.ARCHIVE_EOF) { - _ = c_libs.archive_read_free(inner_reader); - machine.data_inner_reader = null; - if (machine.data_tar_buf) |buf| { - machine.backend.allocator.free(buf); - machine.data_tar_buf = null; - } - return .parse_control; - } - if (result != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveReadFailed); - - const entry_name = std.mem.span(c_libs.archive_entry_pathname(inner_entry)); - const name = if (std.mem.startsWith(u8, entry_name, "./")) entry_name[2..] else entry_name; - - if (!std.mem.startsWith(u8, name, "usr/share/doc/") or !std.mem.endsWith(u8, name, "/copyright")) return .scan_copyright_files; - - const raw_size = c_libs.archive_entry_size(inner_entry); - if (raw_size <= 0) { - _ = c_libs.archive_read_free(inner_reader); - machine.data_inner_reader = null; - if (machine.data_tar_buf) |buf| { - machine.backend.allocator.free(buf); - machine.data_tar_buf = null; - } - return .parse_control; - } - - const entry_size: usize = @intCast(raw_size); - const content = machine.backend.allocator.alloc(u8, entry_size) catch return machine.stateFailed(BackendError.OutOfMemory); - machine.copyright_content = content; - if (c_libs.archive_read_data(inner_reader, content.ptr, entry_size) < 0) return machine.stateFailed(BackendError.ArchiveReadFailed); - - _ = c_libs.archive_read_free(inner_reader); - machine.data_inner_reader = null; - if (machine.data_tar_buf) |buf| { - machine.backend.allocator.free(buf); - machine.data_tar_buf = null; - } - - return .parse_control; -} - -fn stateParseControl(machine: *ParsingMachine) BackendError!ParsingState { - const control_content = machine.control_content orelse return machine.stateFailed(BackendError.InvalidPackage); - machine.control_content = null; - defer machine.backend.allocator.free(control_content); - - var lines = std.mem.splitScalar(u8, control_content, '\n'); - while (lines.next()) |line| { - const trimmed = std.mem.trim(u8, line, " \t\r"); - if (trimmed.len == 0) continue; - - const separator_index = std.mem.indexOf(u8, trimmed, ": ") orelse continue; - const key = trimmed[0..separator_index]; - const value = std.mem.trim(u8, trimmed[separator_index + 2 ..], " \t"); - - const field_kind = control_field_map.get(key) orelse continue; - switch (field_kind) { - .name => machine.raw_meta.name = machine.backend.allocator.dupe(u8, value) catch return machine.stateFailed(BackendError.OutOfMemory), - .version => machine.raw_meta.version = machine.backend.allocator.dupe(u8, value) catch return machine.stateFailed(BackendError.OutOfMemory), - .arch => machine.raw_meta.arch = machine.backend.allocator.dupe(u8, value) catch return machine.stateFailed(BackendError.OutOfMemory), - .size => machine.raw_meta.size = std.fmt.parseInt(u32, value, 10) catch 0, - .description => machine.raw_meta.description = machine.backend.allocator.dupe(u8, value) catch return machine.stateFailed(BackendError.OutOfMemory), - .url => machine.raw_meta.url = machine.backend.allocator.dupe(u8, value) catch return machine.stateFailed(BackendError.OutOfMemory), - .packager => machine.raw_meta.packager = machine.backend.allocator.dupe(u8, value) catch return machine.stateFailed(BackendError.OutOfMemory), - .license => {}, - } - } - - if (machine.copyright_content) |copyright| { - machine.raw_meta.license = parseLicenseFromCopyright(copyright, machine.backend.allocator) catch return machine.stateFailed(BackendError.OutOfMemory); - machine.backend.allocator.free(copyright); - machine.copyright_content = null; - } - - return .build_meta; -} - -fn stateBuildMeta(machine: *ParsingMachine) BackendError!ParsingState { - var sha256: [32]u8 = undefined; - - _ = std.fmt.hexToBytes(&sha256, machine.backend.data.checksum) catch return machine.stateFailed(BackendError.InvalidPackage); - - const raw_version_str = machine.raw_meta.version orelse return machine.stateFailed(BackendError.MetadataNotFound); - defer machine.backend.allocator.free(raw_version_str); - machine.raw_meta.version = null; - - const parsed_version = parseVersion(machine.backend.allocator, raw_version_str) catch return machine.stateFailed(BackendError.InvalidPackage); - errdefer parsed_version.deinit(machine.backend.allocator); - - const package_name = machine.raw_meta.name orelse return machine.stateFailed(BackendError.MetadataNotFound); - machine.raw_meta.name = null; - errdefer machine.backend.allocator.free(package_name); - - const package_arch = machine.raw_meta.arch orelse machine.backend.allocator.dupe(u8, "") catch return machine.stateFailed(BackendError.OutOfMemory); - machine.raw_meta.arch = null; - errdefer machine.backend.allocator.free(package_arch); - - const package_description = machine.raw_meta.description orelse machine.backend.allocator.dupe(u8, "") catch return machine.stateFailed(BackendError.OutOfMemory); - machine.raw_meta.description = null; - errdefer machine.backend.allocator.free(package_description); - - const package_url = machine.raw_meta.url orelse machine.backend.allocator.dupe(u8, "") catch return machine.stateFailed(BackendError.OutOfMemory); - machine.raw_meta.url = null; - errdefer machine.backend.allocator.free(package_url); - - const package_packager = machine.raw_meta.packager orelse machine.backend.allocator.dupe(u8, "") catch return machine.stateFailed(BackendError.OutOfMemory); - machine.raw_meta.packager = null; - errdefer machine.backend.allocator.free(package_packager); - - const package_author = machine.backend.allocator.dupe(u8, package_packager) catch return machine.stateFailed(BackendError.OutOfMemory); - errdefer machine.backend.allocator.free(package_author); - - const package_license = machine.raw_meta.license orelse machine.backend.allocator.dupe(u8, "") catch return machine.stateFailed(BackendError.OutOfMemory); - machine.raw_meta.license = null; - errdefer machine.backend.allocator.free(package_license); - - machine.backend.meta = PackageMeta{ - .name = package_name, - .version = parsed_version, - .arch = package_arch, - .author = package_author, - .description = package_description, - .license = package_license, - .url = package_url, - .packager = package_packager, - .checksum = sha256, - .size = machine.raw_meta.size, - .installed_at = @intCast(@divTrunc(std.Io.Clock.real.now(machine.backend.io).nanoseconds, std.time.ns_per_s)), - }; - - machine.raw_meta.deinit(machine.backend.allocator); - - return .done; -} diff --git a/decoders/deb/src/backend/parsing/utils.zig b/decoders/deb/src/backend/parsing/utils.zig deleted file mode 100644 index 003413c..0000000 --- a/decoders/deb/src/backend/parsing/utils.zig +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ─────────────────────────────────────────────────────────────────── -const std = @import("std"); - -const BackendError = @import("upac-backend-types").BackendError; -const Version = @import("upac-backend-types").Version; - -pub fn parseLicenseFromCopyright(content: []const u8, allocator: std.mem.Allocator) std.mem.Allocator.Error![]const u8 { - var lines = std.mem.splitScalar(u8, content, '\n'); - while (lines.next()) |line| { - const trimmed_line = std.mem.trim(u8, line, " \t\r"); - - if (!std.mem.startsWith(u8, trimmed_line, "License:")) continue; - - const value = std.mem.trim(u8, trimmed_line["License:".len..], " \t\r"); - if (value.len == 0) continue; - - return allocator.dupe(u8, value); - } - - return allocator.dupe(u8, ""); -} - -// Parses a deb version string: [epoch:]upstream[-revision][~pre] -pub fn parseVersion(allocator: std.mem.Allocator, version_str: []const u8) BackendError!Version { - var remaining = version_str; - - var epoch: u32 = 0; - if (std.mem.indexOf(u8, remaining, ":")) |colon_idx| { - epoch = std.fmt.parseInt(u32, remaining[0..colon_idx], 10) catch 0; - remaining = remaining[colon_idx + 1 ..]; - } - - var release: u32 = 0; - if (std.mem.lastIndexOf(u8, remaining, "-")) |dash_idx| { - release = std.fmt.parseInt(u32, remaining[dash_idx + 1 ..], 10) catch 0; - remaining = remaining[0..dash_idx]; - } - - var pre: ?[]const u8 = null; - if (std.mem.indexOf(u8, remaining, "~")) |tilde_idx| { - pre = allocator.dupe(u8, remaining[tilde_idx + 1 ..]) catch return BackendError.AllocZFailed; - remaining = remaining[0..tilde_idx]; - } - errdefer if (pre) |p| allocator.free(p); - - var parts_list = std.ArrayList(u32).empty; - defer parts_list.deinit(allocator); - - var iter = std.mem.splitScalar(u8, remaining, '.'); - while (iter.next()) |part_str| { - if (part_str.len == 0) continue; - const part = std.fmt.parseInt(u32, part_str, 10) catch return BackendError.InvalidPackage; - parts_list.append(allocator, part) catch return BackendError.AllocZFailed; - } - - if (parts_list.items.len == 0) return BackendError.InvalidPackage; - - const parts_owned = parts_list.toOwnedSlice(allocator) catch return BackendError.AllocZFailed; - - return Version{ - .epoch = epoch, - .parts = parts_owned, - .pre = pre, - .release = release, - }; -} diff --git a/decoders/deb/src/backend/unpacking/unpacking.zig b/decoders/deb/src/backend/unpacking/unpacking.zig deleted file mode 100644 index 3a26bd7..0000000 --- a/decoders/deb/src/backend/unpacking/unpacking.zig +++ /dev/null @@ -1,254 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ─────────────────────────────────────────────────────────────────── -const std = @import("std"); - -const c_libs = @import("c-libs"); - -const types = @import("upac-backend-types"); -const BackendError = types.BackendError; - -const backend = @import("../backend.zig"); -const Machine = backend.BackendMachine; - -// ── UnpackingState ──────────────────────────────────────────────────────────── -const UnpackingState = enum { - create_temp_dir, - open_outer_archive, - find_data_tar, - open_inner_archive, - next_entry, - write_blocks, - close_archives, - done, -}; - -// ── UnpackingMachine ────────────────────────────────────────────────────────── -const UnpackingMachine = struct { - backend: *Machine, - - inner_reader: ?*c_libs.archive = null, - outer_reader: ?*c_libs.archive = null, - - data_tar_buf: ?[]u8 = null, - - archive_writer: ?*c_libs.archive = null, - - old_package_dir: ?std.Io.Dir = null, - - fn stateFailed(self: *UnpackingMachine, err: BackendError) BackendError { - if (self.outer_reader) |reader| { - _ = c_libs.archive_read_free(reader); - self.outer_reader = null; - } - - if (self.inner_reader) |reader| { - _ = c_libs.archive_read_free(reader); - self.inner_reader = null; - } - - if (self.data_tar_buf) |buf| { - self.backend.allocator.free(buf); - self.data_tar_buf = null; - } - - if (self.archive_writer) |writer| { - _ = c_libs.archive_write_free(writer); - self.archive_writer = null; - } - - if (self.old_package_dir) |old_dir| { - std.Io.Threaded.fchdir(old_dir.handle) catch {}; - old_dir.close(self.backend.io); - self.old_package_dir = null; - } - - if (self.backend.temp_package_path) |temp_path| { - std.Io.Dir.cwd().deleteTree(self.backend.io, temp_path) catch {}; - self.backend.allocator.free(temp_path); - self.backend.temp_package_path = null; - } - - return err; - } -}; - -// ── Trampoline ──────────────────────────────────────────────────────────────── -pub fn run(machine: *Machine) BackendError!void { - var unpacking = UnpackingMachine{ .backend = machine }; - - var state = UnpackingState.create_temp_dir; - while (state != .done) { - if (machine.data.cancel_token.isCancelled()) return unpacking.stateFailed(BackendError.Cancelled); - state = switch (state) { - .create_temp_dir => try stateCreateTempDir(&unpacking), - .open_outer_archive => try stateOpenOuterArchive(&unpacking), - .find_data_tar => try stateFindDataTar(&unpacking), - .open_inner_archive => try stateOpenInnerArchive(&unpacking), - .next_entry => try stateNextEntry(&unpacking), - .write_blocks => try stateWriteBlocks(&unpacking), - .close_archives => stateCloseArchives(&unpacking), - .done => unreachable, - }; - } -} - -// ── States ──────────────────────────────────────────────────────────────────── -fn stateCreateTempDir(machine: *UnpackingMachine) BackendError!UnpackingState { - var temp_dir_name_buf: [256]u8 = undefined; - - const temp_path = std.mem.span(machine.backend.data.temp_path_c); - const timestamp: i64 = @intCast(@divTrunc(std.Io.Clock.real.now(machine.backend.io).nanoseconds, std.time.ns_per_ms)); - - const temp_package_dir_name = std.fmt.bufPrintZ(&temp_dir_name_buf, "upac-installed-{d}", .{timestamp}) catch return machine.stateFailed(BackendError.AllocZFailed); - - const temp_package_path = std.Io.Dir.path.joinZ(machine.backend.allocator, &.{ temp_path, temp_package_dir_name }) catch return machine.stateFailed(BackendError.AllocZFailed); - - std.Io.Dir.createDirAbsolute(machine.backend.io, temp_package_path, .default_dir) catch return machine.stateFailed(BackendError.TempDirFailed); - - machine.backend.temp_package_path = temp_package_path; - - return .open_outer_archive; -} - -fn stateOpenOuterArchive(machine: *UnpackingMachine) BackendError!UnpackingState { - const temp_package_path = machine.backend.temp_package_path orelse return machine.stateFailed(BackendError.TempDirFailed); - - const outer_reader = c_libs.archive_read_new() orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - machine.outer_reader = outer_reader; - - _ = c_libs.archive_read_support_format_ar(outer_reader); - _ = c_libs.archive_read_support_filter_all(outer_reader); - - if (c_libs.archive_read_open_filename(outer_reader, machine.backend.data.package_path_c, 16384) != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveOpenFailed); - - const archive_writer = c_libs.archive_write_disk_new() orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - machine.archive_writer = archive_writer; - - _ = c_libs.archive_write_disk_set_options(archive_writer, c_libs.ARCHIVE_EXTRACT_TIME | - c_libs.ARCHIVE_EXTRACT_PERM | - c_libs.ARCHIVE_EXTRACT_FFLAGS); - _ = c_libs.archive_write_disk_set_standard_lookup(archive_writer); - - const old_package_dir = std.Io.Dir.cwd().openDir(machine.backend.io, ".", .{}) catch return machine.stateFailed(BackendError.ReadFailed); - machine.old_package_dir = old_package_dir; - - std.Io.Threaded.chdir(temp_package_path) catch return machine.stateFailed(BackendError.TempDirFailed); - - return .find_data_tar; -} - -fn stateFindDataTar(machine: *UnpackingMachine) BackendError!UnpackingState { - const outer_reader = machine.outer_reader orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - - var outer_entry: ?*c_libs.archive_entry = null; - const result = c_libs.archive_read_next_header(outer_reader, &outer_entry); - - if (result == c_libs.ARCHIVE_EOF) return machine.stateFailed(BackendError.InvalidPackage); - if (result != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveReadFailed); - - const entry_name = std.mem.span(c_libs.archive_entry_pathname(outer_entry)); - if (!std.mem.startsWith(u8, entry_name, "data.tar")) return .find_data_tar; - - const raw_size = c_libs.archive_entry_size(outer_entry); - if (raw_size <= 0) return machine.stateFailed(BackendError.InvalidPackage); - - const data_size: usize = @intCast(raw_size); - const data_buf = machine.backend.allocator.alloc(u8, data_size) catch return machine.stateFailed(BackendError.OutOfMemory); - machine.data_tar_buf = data_buf; - - if (c_libs.archive_read_data(outer_reader, data_buf.ptr, data_size) < 0) return machine.stateFailed(BackendError.ArchiveReadFailed); - - return .open_inner_archive; -} - -fn stateOpenInnerArchive(machine: *UnpackingMachine) BackendError!UnpackingState { - const data_buf = machine.data_tar_buf orelse return machine.stateFailed(BackendError.InvalidPackage); - - if (machine.outer_reader) |reader| { - _ = c_libs.archive_read_free(reader); - machine.outer_reader = null; - } - - const inner_reader = c_libs.archive_read_new() orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - machine.inner_reader = inner_reader; - - _ = c_libs.archive_read_support_format_tar(inner_reader); - _ = c_libs.archive_read_support_filter_all(inner_reader); - - if (c_libs.archive_read_open_memory(inner_reader, data_buf.ptr, data_buf.len) != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveOpenFailed); - - return .next_entry; -} - -fn stateNextEntry(machine: *UnpackingMachine) BackendError!UnpackingState { - var archive_entry: ?*c_libs.archive_entry = undefined; - - const inner_reader = machine.inner_reader orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - const archive_writer = machine.archive_writer orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - - const read_result = c_libs.archive_read_next_header(inner_reader, &archive_entry); - - if (read_result == c_libs.ARCHIVE_EOF) return .close_archives; - if (read_result != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveReadFailed); - - const entry = archive_entry orelse return machine.stateFailed(BackendError.ArchiveReadFailed); - - if (c_libs.archive_write_header(archive_writer, entry) != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveExtractFailed); - - return .write_blocks; -} - -fn stateWriteBlocks(machine: *UnpackingMachine) BackendError!UnpackingState { - var block_size: usize = 0; - var block_offset: i64 = 0; - var data_block: ?*const anyopaque = null; - - const inner_reader = machine.inner_reader orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - const archive_writer = machine.archive_writer orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - - const block_result = c_libs.archive_read_data_block(inner_reader, &data_block, &block_size, &block_offset); - if (block_result == c_libs.ARCHIVE_EOF) { - if (c_libs.archive_write_finish_entry(archive_writer) != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveExtractFailed); - - return .next_entry; - } - if (block_result != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveReadFailed); - - if (c_libs.archive_write_data_block(archive_writer, data_block, block_size, block_offset) != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveExtractFailed); - - return .write_blocks; -} - -fn stateCloseArchives(machine: *UnpackingMachine) UnpackingState { - if (machine.inner_reader) |reader| { - _ = c_libs.archive_read_free(reader); - machine.inner_reader = null; - } - - if (machine.outer_reader) |reader| { - _ = c_libs.archive_read_free(reader); - machine.outer_reader = null; - } - - if (machine.data_tar_buf) |buf| { - machine.backend.allocator.free(buf); - machine.data_tar_buf = null; - } - - if (machine.archive_writer) |writer| { - _ = c_libs.archive_write_free(writer); - machine.archive_writer = null; - } - - if (machine.old_package_dir) |old_dir| { - std.Io.Threaded.fchdir(old_dir.handle) catch {}; - old_dir.close(machine.backend.io); - machine.old_package_dir = null; - } - - return .done; -} diff --git a/decoders/deb/src/backend/verifying/verifying.zig b/decoders/deb/src/backend/verifying/verifying.zig deleted file mode 100644 index 34a2b1c..0000000 --- a/decoders/deb/src/backend/verifying/verifying.zig +++ /dev/null @@ -1,116 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ─────────────────────────────────────────────────────────────────── -const std = @import("std"); - -const types = @import("upac-backend-types"); - -const BackendError = types.BackendError; - -const backend = @import("../backend.zig"); -const Machine = backend.BackendMachine; - -// ── VerifyingState ──────────────────────────────────────────────────────────── -const VerifyingState = enum { - check_file, - check_temp_dir, - hash, - compare, - done, -}; - -// ── VerifyingMachine ────────────────────────────────────────────────────────── -const VerifyingMachine = struct { - backend: *Machine, - - file: ?std.Io.File = null, - - digest_checksum: [32]u8 = undefined, - - fn stateFailed(self: *VerifyingMachine, err: BackendError) BackendError { - if (self.file) |file| { - file.close(self.backend.io); - self.file = null; - } - - return err; - } -}; - -// ── Trampoline ──────────────────────────────────────────────────────────────── -pub fn run(machine: *Machine) BackendError!void { - var verifying = VerifyingMachine{ .backend = machine }; - - var state = VerifyingState.check_file; - while (state != .done) { - if (machine.data.cancel_token.isCancelled()) return verifying.stateFailed(BackendError.Cancelled); - state = switch (state) { - .check_file => try stateCheckFile(&verifying), - .check_temp_dir => try stateCheckTempDir(&verifying), - .hash => try stateHash(&verifying), - .compare => try stateCompare(&verifying), - .done => unreachable, - }; - } -} - -// ── States ──────────────────────────────────────────────────────────────────── -fn stateCheckFile(machine: *VerifyingMachine) BackendError!VerifyingState { - const package_path = std.mem.span(machine.backend.data.package_path_c); - - std.Io.Dir.accessAbsolute(machine.backend.io, package_path, .{}) catch return machine.stateFailed(BackendError.ReadFailed); - - return .check_temp_dir; -} - -fn stateCheckTempDir(machine: *VerifyingMachine) BackendError!VerifyingState { - const temp_path = std.mem.span(machine.backend.data.temp_path_c); - - std.Io.Dir.accessAbsolute(machine.backend.io, temp_path, .{}) catch return machine.stateFailed(BackendError.TempDirFailed); - - return .hash; -} - -fn stateHash(machine: *VerifyingMachine) BackendError!VerifyingState { - var package_reader_buf: [65536]u8 = undefined; - var package_hasher = std.crypto.hash.sha2.Sha256.init(.{}); - - const package_path = std.mem.span(machine.backend.data.package_path_c); - - const file = std.Io.Dir.openFileAbsolute(machine.backend.io, package_path, .{}) catch return machine.stateFailed(BackendError.ReadFailed); - machine.file = file; - - var package_read_bufs_vector = [1][]u8{package_reader_buf[0..]}; - while (true) { - const bytes_read = file.readStreaming(machine.backend.io, &package_read_bufs_vector) catch |err| { - if (err == error.EndOfStream) break; - return machine.stateFailed(BackendError.ReadFailed); - }; - - if (bytes_read == 0) break; - - package_hasher.update(package_reader_buf[0..bytes_read]); - } - - package_hasher.final(&machine.digest_checksum); - - return .compare; -} - -fn stateCompare(machine: *VerifyingMachine) BackendError!VerifyingState { - var checksum_as_bytes: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined; - - _ = std.fmt.hexToBytes(&checksum_as_bytes, machine.backend.data.checksum) catch return machine.stateFailed(BackendError.InvalidPackage); - - if (!std.mem.eql(u8, &machine.digest_checksum, &checksum_as_bytes)) return machine.stateFailed(BackendError.ChecksumMismatch); - - if (machine.file) |file| { - file.close(machine.backend.io); - machine.file = null; - } - - return .done; -} diff --git a/decoders/deb/src/control.rs b/decoders/deb/src/control.rs new file mode 100644 index 0000000..2b8d8ea --- /dev/null +++ b/decoders/deb/src/control.rs @@ -0,0 +1,120 @@ +// 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::HashMap; + +use upac_abi::decoder::{ + CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS, parse_constraint_prefix, +}; + +use upac_types::{Dependency, PackageMeta, Version}; + +use crate::deb::{ + CONTROL_ARCH_KEY, CONTROL_DEPENDS_KEY, CONTROL_DESCRIPTION_KEY, CONTROL_INSTALLED_SIZE_KEY, CONTROL_MAINTAINER_KEY, + CONTROL_NAME_KEY, CONTROL_URL_KEY, CONTROL_VERSION_KEY, +}; +use crate::error::DecodeError; + +const OPERATORS: [(&[u8], u8); 5] = [ + (b"<<", CONSTRAINT_LESS), + (b"<=", CONSTRAINT_LESS | CONSTRAINT_EQUAL), + (b">>", CONSTRAINT_GREATER), + (b">=", CONSTRAINT_GREATER | CONSTRAINT_EQUAL), + (b"=", CONSTRAINT_EQUAL), +]; + +#[derive(Debug)] +pub struct ControlFile { + pub meta: PackageMeta, + pub dependencies: Vec, +} + +impl ControlFile { + pub fn parse(content: &str, license: Option, sha256: [u8; 32]) -> Result { + let mut fields: HashMap<&str, String> = HashMap::new(); + let mut dependencies = Vec::new(); + + for line in content.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + + let Some((key, value)) = line.split_once(": ") else { + continue; + }; + + if key == CONTROL_DEPENDS_KEY { + dependencies.extend(Self::parse_depends(value)); + } else { + fields.insert(key, value.to_owned()); + } + } + + let name = fields.remove(CONTROL_NAME_KEY).ok_or(DecodeError::MalformedControl)?; + let raw_version = fields + .remove(CONTROL_VERSION_KEY) + .ok_or(DecodeError::MalformedControl)?; + + let installed_size = fields + .get(CONTROL_INSTALLED_SIZE_KEY) + .and_then(|size| size.parse().ok()) + .unwrap_or(0); + + let meta = PackageMeta { + name, + version: Version::parse(&raw_version), + arch: fields.remove(CONTROL_ARCH_KEY).unwrap_or_else(|| "all".to_owned()), + arch_sub: None, + maintainer: fields.remove(CONTROL_MAINTAINER_KEY).unwrap_or_default(), + description: fields.remove(CONTROL_DESCRIPTION_KEY).unwrap_or_default(), + license, + url: fields.remove(CONTROL_URL_KEY), + sha256, + installed_size, + }; + + Ok(ControlFile { meta, dependencies }) + } + + fn parse_depends(value: &str) -> Vec { + value + .split(',') + .filter_map(|group| group.split('|').next()) + .map(Self::parse_dependency) + .collect() + } + + fn parse_dependency(raw: &str) -> Dependency { + let raw = raw.trim(); + let bytes = raw.as_bytes(); + + for index in 0..bytes.len() { + let Some((constraint, operator_len)) = parse_constraint_prefix(&bytes[index..], &OPERATORS) else { + continue; + }; + + let name = raw[..index].trim().trim_end_matches('(').trim().to_owned(); + + let version_part = raw[index + operator_len..].trim(); + let version_str = match version_part.find(')') { + Some(close_index) => &version_part[..close_index], + None => version_part, + }; + + return Dependency { + name, + constraint, + version: Version::parse(version_str), + }; + } + + Dependency { + name: raw.to_owned(), + constraint: CONSTRAINT_ANY, + version: Version::default(), + } + } +} diff --git a/decoders/deb/src/error.rs b/decoders/deb/src/error.rs new file mode 100644 index 0000000..7db21a5 --- /dev/null +++ b/decoders/deb/src/error.rs @@ -0,0 +1,40 @@ +// 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; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecodeError { + InvalidRequest, + Io(IoErrorKind), + ChecksumMismatch, + UnsupportedFormat, + MissingControl, + MalformedControl, + 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::MissingControl => -5, + DecodeError::MalformedControl => -6, + DecodeError::InvalidUtf8 => -7, + DecodeError::Cancelled => -8, + } + } +} diff --git a/decoders/deb/src/extract.rs b/decoders/deb/src/extract.rs new file mode 100644 index 0000000..bd14446 --- /dev/null +++ b/decoders/deb/src/extract.rs @@ -0,0 +1,173 @@ +// 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::{self, File}; +use std::io::{Cursor, Read}; +use std::path::Path; + +use ar::Archive as ArArchive; +use flate2::read::GzDecoder; +use tar::Archive as TarArchive; +use xz2::read::XzDecoder; +use zstd::stream::read::Decoder as ZstdDecoder; + +use upac_abi::hook::CancelToken; + +use crate::deb::{ + CONTROL_ENTRY, CONTROL_TAR_PREFIX, COPYRIGHT_DIR_PREFIX, COPYRIGHT_ENTRY_SUFFIX, DATA_TAR_PREFIX, POSTINST_FILE, + POSTRM_FILE, PREINST_FILE, PRERM_FILE, +}; +use crate::error::DecodeError; + +const SCRIPT_FILES: [&str; 4] = [PREINST_FILE, POSTINST_FILE, PRERM_FILE, POSTRM_FILE]; + +pub struct ExtractedMetadata { + pub control: String, + pub scripts_present: Vec, + pub license: Option, +} + +pub fn extract(package_path: &str, output_dir: &str, cancel: &CancelToken) -> Result { + let file = File::open(package_path)?; + let mut outer = ArArchive::new(file); + + let mut control_tar: Option<(String, Vec)> = None; + let mut data_tar: Option<(String, Vec)> = None; + + while let Some(entry) = outer.next_entry() { + if cancel.is_cancelled() { + return Err(DecodeError::Cancelled); + } + + let mut entry = entry?; + let name = String::from_utf8(entry.header().identifier().to_vec()).map_err(|_| DecodeError::InvalidUtf8)?; + + if name.starts_with(CONTROL_TAR_PREFIX) && control_tar.is_none() { + let mut bytes = Vec::new(); + entry.read_to_end(&mut bytes)?; + control_tar = Some((name, bytes)); + } else if name.starts_with(DATA_TAR_PREFIX) && data_tar.is_none() { + let mut bytes = Vec::new(); + entry.read_to_end(&mut bytes)?; + data_tar = Some((name, bytes)); + } + } + + let (control_name, control_bytes) = control_tar.ok_or(DecodeError::MissingControl)?; + let (data_name, data_bytes) = data_tar.ok_or(DecodeError::MissingControl)?; + + let (control, scripts_present) = extract_control(control_bytes, &control_name, cancel)?; + let license = extract_data(data_bytes, &data_name, output_dir, cancel)?; + + Ok(ExtractedMetadata { + control, + scripts_present, + license, + }) +} + +fn extract_control( + bytes: Vec, member_name: &str, cancel: &CancelToken, +) -> Result<(String, Vec), DecodeError> { + let reader = open_tar_reader(member_name, bytes)?; + let mut archive = TarArchive::new(reader); + + let mut control = None; + let mut scripts_present = Vec::new(); + + for entry in archive.entries()? { + if cancel.is_cancelled() { + return Err(DecodeError::Cancelled); + } + + let mut entry = entry?; + let entry_path = entry.path()?.to_string_lossy().into_owned(); + let normalized = entry_path.strip_prefix("./").unwrap_or(&entry_path); + + if normalized == CONTROL_ENTRY { + control = Some(read_entry_to_string(&mut entry)?); + } else if SCRIPT_FILES.contains(&normalized) { + scripts_present.push(normalized.to_owned()); + } + } + + control + .map(|control| (control, scripts_present)) + .ok_or(DecodeError::MissingControl) +} + +fn extract_data( + bytes: Vec, member_name: &str, output_dir: &str, cancel: &CancelToken, +) -> Result, DecodeError> { + let reader = open_tar_reader(member_name, bytes)?; + let mut archive = TarArchive::new(reader); + + let mut license = None; + + for entry in archive.entries()? { + if cancel.is_cancelled() { + return Err(DecodeError::Cancelled); + } + + let mut entry = entry?; + let entry_path = entry.path()?.to_string_lossy().into_owned(); + let normalized = entry_path.strip_prefix("./").unwrap_or(&entry_path); + + if license.is_none() + && normalized.starts_with(COPYRIGHT_DIR_PREFIX) + && normalized.ends_with(COPYRIGHT_ENTRY_SUFFIX) + { + let mut bytes = Vec::new(); + entry.read_to_end(&mut bytes)?; + + let target = Path::new(output_dir).join(normalized); + if let Some(parent) = target.parent() { + fs::create_dir_all(parent)?; + } + fs::write(&target, &bytes)?; + + license = String::from_utf8(bytes) + .ok() + .and_then(|content| parse_license_from_copyright(&content)); + continue; + } + + entry.unpack_in(output_dir)?; + } + + Ok(license) +} + +fn parse_license_from_copyright(content: &str) -> Option { + content + .lines() + .map(str::trim) + .find_map(|line| line.strip_prefix("License:")) + .map(|value| value.trim().to_owned()) + .filter(|value| !value.is_empty()) +} + +fn read_entry_to_string(entry: &mut tar::Entry<'_, R>) -> Result { + let mut bytes = Vec::new(); + entry.read_to_end(&mut bytes)?; + + String::from_utf8(bytes).map_err(|_| DecodeError::InvalidUtf8) +} + +fn open_tar_reader(member_name: &str, bytes: Vec) -> Result, DecodeError> { + let cursor = Cursor::new(bytes); + + if member_name.ends_with(".zst") { + Ok(Box::new(ZstdDecoder::new(cursor)?)) + } else if member_name.ends_with(".xz") { + Ok(Box::new(XzDecoder::new(cursor))) + } else if member_name.ends_with(".gz") { + Ok(Box::new(GzDecoder::new(cursor))) + } else if member_name == CONTROL_TAR_PREFIX || member_name == DATA_TAR_PREFIX { + Ok(Box::new(cursor)) + } else { + Err(DecodeError::UnsupportedFormat) + } +} diff --git a/decoders/deb/src/ffi.zig b/decoders/deb/src/ffi.zig deleted file mode 100644 index c45ed5f..0000000 --- a/decoders/deb/src/ffi.zig +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -pub const std = @import("std"); - -const types = @import("upac-backend-types"); -const BackendError = types.BackendError; -const HookFn = types.HookFn; -const CancelToken = types.CancelToken; - -pub const ABI_VERSION: u32 = 2; - -// ── FFI types ───────────────────────────────────────────────────────────────── -pub const CSlice = extern struct { - ptr: [*c]const u8, - len: usize, - - pub fn toSlice(self: CSlice) []const u8 { - const not_null_ptr = self.ptr orelse return ""; - return not_null_ptr[0..self.len]; - } - - pub fn asZ(self: CSlice) [*c]const u8 { - return self.ptr; - } - - pub fn fromSlice(slice: ?[]const u8) CSlice { - const not_null_slice = slice orelse return .{ .ptr = null, .len = 0 }; - return .{ .ptr = @ptrCast(not_null_slice.ptr), .len = not_null_slice.len }; - } - - pub fn validate(self: CSlice) !void { - if (self.ptr == null) return error.InvalidEntry; - if (self.ptr[self.len] != 0) return error.InvalidEntry; - if (std.mem.len(self.ptr) != self.len) return error.InvalidEntry; - } -}; - -pub const CVersionParts = extern struct { - ptr: [*]u32, - len: usize, - - pub fn toSlice(self: CVersionParts) []u32 { - return self.ptr[0..self.len]; - } -}; - -pub const CVersion = extern struct { - struct_size: usize = @sizeOf(CVersion), - - epoch: u32, - release: u32, - parts: CVersionParts, - pre: CSlice, - - pub fn deinit(self: CVersion, allocator: std.mem.Allocator) void { - allocator.free(self.parts.toSlice()); - if (self.pre.ptr != null) allocator.free(self.pre.toSlice()); - } -}; - -pub const CPackageMeta = extern struct { - struct_size: usize = @sizeOf(CPackageMeta), - - name: CSlice, - version: CVersion, - arch: CSlice, - arch_sub: CSlice, - maintainer: CSlice, - description: CSlice, - license: CSlice, - url: CSlice, - sha256: [32]u8, - installed_size: u64 = 0, - - pub fn free(self: *CPackageMeta, allocator: std.mem.Allocator) void { - inline for (std.meta.fields(CPackageMeta)) |field| { - if (field.type == CSlice) { - const slice = @field(self, field.name); - if (slice.ptr != null) allocator.free(slice.toSlice()); - } - } - self.version.deinit(allocator); - allocator.destroy(self); - } -}; - -pub const CPrepareRequest = extern struct { - struct_size: usize = @sizeOf(CPrepareRequest), - checksum: CSlice, - - package_path: CSlice, - temp_dir: CSlice, - - on_hook: ?*const HookFn = null, - hook_ctx: ?*anyopaque = null, - - cancel_token: ?*const CancelToken = null, - - pub fn validate(req: CPrepareRequest) !void { - if (req.struct_size != @sizeOf(CPrepareRequest)) return error.AbiMismatch; - try req.package_path.validate(); - try req.temp_dir.validate(); - try req.checksum.validate(); - } -}; - -pub fn dupeToCSlice(allocator: std.mem.Allocator, slice: []const u8) BackendError!CSlice { - const duped = allocator.dupeZ(u8, slice) catch return BackendError.AllocZFailed; - return CSlice.fromSlice(duped); -} - -pub fn dupeRequiredToCSlice(allocator: std.mem.Allocator, slice: []const u8) BackendError!CSlice { - if (slice.len == 0) return BackendError.InvalidPackage; - return dupeToCSlice(allocator, slice); -} diff --git a/decoders/deb/src/imports.h b/decoders/deb/src/imports.h deleted file mode 100644 index 85797f3..0000000 --- a/decoders/deb/src/imports.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 JustPav - * SPDX-FileCopyrightText: 2026 SmoothTeam - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ - -#include -#include diff --git a/decoders/deb/src/lib.rs b/decoders/deb/src/lib.rs new file mode 100644 index 0000000..a2f7f18 --- /dev/null +++ b/decoders/deb/src/lib.rs @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::str::from_utf8; + +use upac_abi::ABI_VERSION; +use upac_abi::decoder::{CDecodeRequest, CDecodeResponse, CDependency}; +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 crate::control::ControlFile; +use crate::error::DecodeError; + +pub mod control; +pub mod error; +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 +} + +/// # Safety +/// `request`, if non-null, must point to a valid, initialized `CDecodeRequest` for the duration +/// of the call. `response_out`, if non-null, must point to writable, uninitialized +/// `CDecodeResponse` storage that this function fully initializes on success. +#[cfg_attr(feature = "cdylib", unsafe(no_mangle))] +pub unsafe extern "C" fn decode(request: *const CDecodeRequest, response_out: *mut CDecodeResponse) -> i32 { + if request.is_null() || response_out.is_null() { + return DecodeError::InvalidRequest.code(); + } + + match decode_package(unsafe { &*request }) { + Ok(response) => { + unsafe { response_out.write(response) }; + 0 + } + Err(error) => error.code(), + } +} + +/// # Safety +/// `response`, if non-null, must point to a `CDecodeResponse` produced by this crate's own +/// `decode`, not yet freed. +unsafe extern "C" fn free_decode_response(response: *mut CDecodeResponse) { + if response.is_null() { + return; + } + + let response = unsafe { &*response }; + + unsafe { + response.meta.free(); + + free_cvec_owning(&response.dependencies, |dependency| { + free_cslice(&dependency.name); + dependency.version.free(); + }); + + free_cvec_owning(&response.declarative_triggers, |slice| free_cslice(slice)); + } +} + +fn decode_package(request: &CDecodeRequest) -> Result { + let package_path = + from_utf8(unsafe { request.package_path.as_slice() }).map_err(|_| DecodeError::InvalidRequest)?; + let output_dir = from_utf8(unsafe { request.output_dir.as_slice() }).map_err(|_| DecodeError::InvalidRequest)?; + let cancel = unsafe { request.cancel_token.as_ref() }.ok_or(DecodeError::InvalidRequest)?; + + verify::verify(package_path, request.checksum, cancel)?; + + let extracted = extract::extract(package_path, output_dir, cancel)?; + let declarative_triggers = triggers::scan(&extracted.scripts_present); + + let control = ControlFile::parse(&extracted.control, extracted.license, request.checksum)?; + + Ok(build_response(control, declarative_triggers)) +} + +fn build_response(control: ControlFile, declarative_triggers: Vec) -> CDecodeResponse { + let ControlFile { meta, dependencies } = control; + + 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, + } +} diff --git a/decoders/deb/src/symbols.zig b/decoders/deb/src/symbols.zig deleted file mode 100644 index d159449..0000000 --- a/decoders/deb/src/symbols.zig +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ─────────────────────────────────────────────────────────────────── -const std = @import("std"); - -const types = @import("upac-backend-types"); -const BackendErrorCode = types.BackendErrorCode; -const fromError = types.fromError; -const BackendError = types.BackendError; -const PrepareData = types.PrepareData; - -const ffi = @import("upac-backend-ffi"); -const CPrepareRequest = ffi.CPrepareRequest; -const CPackageMeta = ffi.CPackageMeta; -const CVersion = ffi.CVersion; -const CVersionParts = ffi.CVersionParts; -const CSlice = ffi.CSlice; - -const dupeToCSlice = ffi.dupeToCSlice; -const dupeRequiredToCSlice = ffi.dupeRequiredToCSlice; - -const BackendMachine = @import("backend/backend.zig").BackendMachine; - -// ── FFI exports ─────────────────────────────────────────────────────────────── -pub export fn prepare(request_c: *const CPrepareRequest, out_meta: **CPackageMeta, out_temp_path: *CSlice) callconv(.c) i32 { - request_c.validate() catch |err| return @intFromEnum(fromError(err)); - - const cancel_token = request_c.cancel_token orelse return @intFromEnum(BackendErrorCode.invalid_entry); - - const prepare_data = PrepareData{ - .package_path_c = request_c.package_path.asZ(), - .temp_path_c = request_c.temp_dir.asZ(), - .checksum = request_c.checksum.toSlice(), - .on_hook = request_c.on_hook, - .hook_ctx = request_c.hook_ctx, - .cancel_token = cancel_token, - }; - - var result = BackendMachine.run(prepare_data, std.heap.c_allocator) catch |err| return @intFromEnum(fromError(err)); - defer result.meta.deinit(std.heap.c_allocator); - - const version_parts_copy = std.heap.c_allocator.dupe(u32, result.meta.version.parts) catch return @intFromEnum(BackendErrorCode.alloc_failed); - - const out_meta_ptr = std.heap.c_allocator.create(CPackageMeta) catch { - std.heap.c_allocator.free(version_parts_copy); - return @intFromEnum(BackendErrorCode.alloc_failed); - }; - - out_meta_ptr.* = CPackageMeta{ - .name = dupeRequiredToCSlice(std.heap.c_allocator, result.meta.name) catch return @intFromEnum(fromError(BackendError.InvalidPackage)), - .version = CVersion{ - .epoch = result.meta.version.epoch, - .release = result.meta.version.release, - .parts = .{ .ptr = version_parts_copy.ptr, .len = version_parts_copy.len }, - .pre = CSlice.fromSlice(if (result.meta.version.pre) |pre| - std.heap.c_allocator.dupeZ(u8, pre) catch return @intFromEnum(BackendErrorCode.alloc_failed) - else - null), - }, - .arch = dupeToCSlice(std.heap.c_allocator, result.meta.arch) catch return @intFromEnum(fromError(BackendError.AllocZFailed)), - .arch_sub = CSlice.fromSlice(null), - .maintainer = dupeToCSlice(std.heap.c_allocator, result.meta.author) catch return @intFromEnum(fromError(BackendError.AllocZFailed)), - .description = dupeToCSlice(std.heap.c_allocator, result.meta.description) catch return @intFromEnum(fromError(BackendError.AllocZFailed)), - .license = dupeToCSlice(std.heap.c_allocator, result.meta.license) catch return @intFromEnum(fromError(BackendError.AllocZFailed)), - .url = dupeToCSlice(std.heap.c_allocator, result.meta.url) catch return @intFromEnum(fromError(BackendError.AllocZFailed)), - .sha256 = result.meta.checksum, - .installed_size = @as(u64, result.meta.size), - }; - - out_meta.* = out_meta_ptr; - out_temp_path.* = dupeToCSlice(std.heap.c_allocator, result.temp_path) catch return @intFromEnum(fromError(BackendError.AllocZFailed)); - - return @intFromEnum(BackendErrorCode.ok); -} - -pub export fn cleanup(path_c: CSlice) callconv(.c) void { - const path = path_c.toSlice(); - const io = std.Io.Threaded.global_single_threaded.io(); - - std.Io.Dir.cwd().deleteTree(io, path) catch {}; - - std.heap.c_allocator.free(path); -} - -pub export fn free_meta(package_meta_c: *CPackageMeta) callconv(.c) void { - package_meta_c.free(std.heap.c_allocator); -} - -pub export fn version_abi() callconv(.c) u32 { - return ffi.ABI_VERSION; -} diff --git a/decoders/deb/src/triggers.rs b/decoders/deb/src/triggers.rs new file mode 100644 index 0000000..212f717 --- /dev/null +++ b/decoders/deb/src/triggers.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_types::DecoderTrigger; + +use crate::deb::{POSTINST_FILE, POSTRM_FILE, PREINST_FILE, PRERM_FILE}; + +pub fn scan(scripts_present: &[String]) -> Vec { + let mut names: Vec = Vec::new(); + + for trigger in DecoderTrigger::ALL { + let name = native_name(trigger); + let declared = scripts_present.iter().any(|script| script == name); + let already_added = names.iter().any(|existing| existing == name); + + if declared && !already_added { + names.push(name.to_owned()); + } + } + + names +} + +fn native_name(trigger: DecoderTrigger) -> &'static str { + match trigger { + DecoderTrigger::PreInstall | DecoderTrigger::PreUpgrade => PREINST_FILE, + DecoderTrigger::PostInstall | DecoderTrigger::PostUpgrade => POSTINST_FILE, + DecoderTrigger::PreRemove => PRERM_FILE, + DecoderTrigger::PostRemove => POSTRM_FILE, + } +} diff --git a/decoders/deb/src/types/types.zig b/decoders/deb/src/types/types.zig deleted file mode 100644 index 3daf9c1..0000000 --- a/decoders/deb/src/types/types.zig +++ /dev/null @@ -1,133 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -pub const std = @import("std"); - -const meta_fields = @import("upac-meta-fields"); - -const errors = @import("errors.zig"); -pub const BackendErrorCode = errors.BackendErrorCode; -pub const BackendError = errors.BackendError; -pub const fromError = errors.fromError; - -// ── StateId ─────────────────────────────────────────────────────────────────── -pub const StateId = enum(u8) { - verifying = 0, - reading_meta = 1, - extracting = 2, - special_step = 3, - - done = 4, -}; - -// ── Version ───────────────────────────────────────────────────────────────── -pub const Version = struct { - epoch: u32 = 0, - parts: []const u32, - pre: ?[]const u8 = null, - release: u32 = 1, - - pub fn deinit(self: *const Version, allocator: std.mem.Allocator) void { - allocator.free(self.parts); - if (self.pre) |pre| allocator.free(pre); - } -}; - -// ── Hook ────────────────────────────────────────────────────────────────────── -pub const HookResponse = enum(u8) { - proceed = 0, - cancel = 1, -}; - -pub const HookFn = fn (event: u32, data: ?*const anyopaque, ctx: ?*anyopaque) callconv(.c) HookResponse; - -// ── CancelToken ─────────────────────────────────────────────────────────────── -pub const CancelToken = extern struct { - _flag: u8, - _hook: ?*const fn (ctx: ?*anyopaque) callconv(.c) void = null, - _hook_ctx: ?*anyopaque = null, - - pub fn isCancelled(self: *const CancelToken) bool { - return @atomicLoad(u8, &self._flag, .acquire) != 0; - } -}; - -// ── control_field_map ───────────────────────────────────────────────────────── -const RawMetaField = std.meta.FieldEnum(RawMeta); - -pub const control_field_map = blk: { - const zon_fields = std.meta.fields(@TypeOf(meta_fields)); - var entries: [zon_fields.len]struct { []const u8, RawMetaField } = undefined; - for (zon_fields, 0..) |field, index| { - const raw_meta_field_name = @field(meta_fields, field.name); - entries[index] = .{ field.name, @field(RawMetaField, raw_meta_field_name) }; - } - break :blk std.StaticStringMap(RawMetaField).initComptime(entries); -}; - -// ── PrepareData ─────────────────────────────────────────────────────────────── -pub const PrepareData = struct { - package_path_c: [*:0]const u8, - temp_path_c: [*:0]const u8, - checksum: []const u8, - on_hook: ?*const HookFn = null, - hook_ctx: ?*anyopaque = null, - cancel_token: *const CancelToken, -}; - -// ── RawMeta ─────────────────────────────────────────────────────────────────── -pub const RawMeta = struct { - name: ?[]const u8 = null, - version: ?[]const u8 = null, - arch: ?[]const u8 = null, - size: u32 = 0, - description: ?[]const u8 = null, - url: ?[]const u8 = null, - packager: ?[]const u8 = null, - license: ?[]const u8 = null, - - pub fn deinit(self: *RawMeta, allocator: std.mem.Allocator) void { - if (self.name) |value| allocator.free(value); - if (self.version) |value| allocator.free(value); - if (self.arch) |value| allocator.free(value); - if (self.description) |value| allocator.free(value); - if (self.url) |value| allocator.free(value); - if (self.packager) |value| allocator.free(value); - if (self.license) |value| allocator.free(value); - } -}; - -// ── PackageMeta ─────────────────────────────────────────────────────────────── -pub const PackageMeta = struct { - name: []const u8, - version: Version, - arch: []const u8, - author: []const u8, - description: []const u8, - license: []const u8, - url: []const u8, - packager: []const u8, - checksum: [32]u8, - size: u32, - installed_at: i64, - - pub fn deinit(self: *PackageMeta, allocator: std.mem.Allocator) void { - allocator.free(self.name); - allocator.free(self.arch); - allocator.free(self.author); - allocator.free(self.description); - allocator.free(self.license); - allocator.free(self.url); - allocator.free(self.packager); - - self.version.deinit(allocator); - } -}; - -// ── PrepareResult ───────────────────────────────────────────────────────────── -pub const PrepareResult = struct { - meta: PackageMeta, - temp_path: [:0]const u8, -}; diff --git a/decoders/deb/src/verify.rs b/decoders/deb/src/verify.rs new file mode 100644 index 0000000..704d458 --- /dev/null +++ b/decoders/deb/src/verify.rs @@ -0,0 +1,42 @@ +// 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::hook::CancelToken; + +use crate::error::DecodeError; + +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/deb/tests/control.rs b/decoders/deb/tests/control.rs new file mode 100644 index 0000000..b612b61 --- /dev/null +++ b/decoders/deb/tests/control.rs @@ -0,0 +1,117 @@ +// 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::decoder::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS}; + +use upac_decoder_deb::control::ControlFile; +use upac_decoder_deb::error::DecodeError; + +const CHECKSUM: [u8; 32] = [7; 32]; + +#[test] +fn parses_minimal_control_with_defaults() { + let content = "Package: foo\nVersion: 1.2.3\n"; + + let control = ControlFile::parse(content, None, CHECKSUM).unwrap(); + + assert_eq!(control.meta.name, "foo"); + assert_eq!(control.meta.version.raw, "1.2.3"); + assert_eq!(control.meta.version.epoch, 0); + assert_eq!(control.meta.arch, "all"); + assert_eq!(control.meta.maintainer, ""); + assert_eq!(control.meta.description, ""); + assert_eq!(control.meta.license, None); + assert_eq!(control.meta.url, None); + assert_eq!(control.meta.sha256, CHECKSUM); + assert!(control.dependencies.is_empty()); +} + +#[test] +fn parses_epoch_out_of_the_version_string() { + let content = "Package: foo\nVersion: 2:1.2.3-1\n"; + + let control = ControlFile::parse(content, None, CHECKSUM).unwrap(); + + assert_eq!(control.meta.version.epoch, 2); + assert_eq!(control.meta.version.raw, "1.2.3-1"); +} + +#[test] +fn parses_all_fields_and_ignores_blank_lines() { + let content = "Package: foo\n\nVersion: 1.2.3\nArchitecture: amd64\nDescription: A test package\nHomepage: \ + https://example.com\nMaintainer: Jane \nInstalled-Size: 4096\n"; + + let control = ControlFile::parse(content, Some("MIT".to_owned()), CHECKSUM).unwrap(); + + assert_eq!(control.meta.arch, "amd64"); + assert_eq!(control.meta.description, "A test package"); + assert_eq!(control.meta.url, Some("https://example.com".to_owned())); + assert_eq!(control.meta.maintainer, "Jane "); + assert_eq!(control.meta.license, Some("MIT".to_owned())); + assert_eq!(control.meta.installed_size, 4096); +} + +#[test] +fn missing_package_is_malformed() { + let content = "Version: 1.2.3\n"; + + let result = ControlFile::parse(content, None, CHECKSUM); + + assert_eq!(result.unwrap_err(), DecodeError::MalformedControl); +} + +#[test] +fn missing_version_is_malformed() { + let content = "Package: foo\n"; + + let result = ControlFile::parse(content, None, CHECKSUM); + + assert_eq!(result.unwrap_err(), DecodeError::MalformedControl); +} + +#[test] +fn parses_dependencies_with_every_constraint_operator() { + let content = "Package: foo\nVersion: 1.2.3\nDepends: bash, libc6 (>= 2.36), libssl (<= 3), libfoo (= 1.0), \ + zlib1g (<< 2), libbar (>> 7)\n"; + + let control = ControlFile::parse(content, None, CHECKSUM).unwrap(); + + let dependencies = control.dependencies; + assert_eq!(dependencies.len(), 6); + + assert_eq!(dependencies[0].name, "bash"); + assert_eq!(dependencies[0].constraint, CONSTRAINT_ANY); + + assert_eq!(dependencies[1].name, "libc6"); + assert_eq!(dependencies[1].constraint, CONSTRAINT_GREATER | CONSTRAINT_EQUAL); + assert_eq!(dependencies[1].version.raw, "2.36"); + + assert_eq!(dependencies[2].name, "libssl"); + assert_eq!(dependencies[2].constraint, CONSTRAINT_LESS | CONSTRAINT_EQUAL); + assert_eq!(dependencies[2].version.raw, "3"); + + assert_eq!(dependencies[3].name, "libfoo"); + assert_eq!(dependencies[3].constraint, CONSTRAINT_EQUAL); + assert_eq!(dependencies[3].version.raw, "1.0"); + + assert_eq!(dependencies[4].name, "zlib1g"); + assert_eq!(dependencies[4].constraint, CONSTRAINT_LESS); + assert_eq!(dependencies[4].version.raw, "2"); + + assert_eq!(dependencies[5].name, "libbar"); + assert_eq!(dependencies[5].constraint, CONSTRAINT_GREATER); + assert_eq!(dependencies[5].version.raw, "7"); +} + +#[test] +fn picks_the_first_alternative_in_an_or_group() { + let content = "Package: foo\nVersion: 1.2.3\nDepends: libfoo | libbar (>= 2.0)\n"; + + let control = ControlFile::parse(content, None, CHECKSUM).unwrap(); + + assert_eq!(control.dependencies.len(), 1); + assert_eq!(control.dependencies[0].name, "libfoo"); + assert_eq!(control.dependencies[0].constraint, CONSTRAINT_ANY); +} diff --git a/decoders/deb/tests/triggers.rs b/decoders/deb/tests/triggers.rs new file mode 100644 index 0000000..3fcd706 --- /dev/null +++ b/decoders/deb/tests/triggers.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_decoder_deb::triggers; + +#[test] +fn finds_no_triggers_when_no_scripts_are_present() { + let triggers = triggers::scan(&[]); + + assert!(triggers.is_empty()); +} + +#[test] +fn a_bare_preinst_covers_both_install_and_upgrade_positions() { + let scripts = vec!["preinst".to_owned()]; + + let triggers = triggers::scan(&scripts); + + assert_eq!(triggers, vec!["preinst"]); +} + +#[test] +fn finds_all_four_maintainer_scripts() { + let scripts = vec![ + "preinst".to_owned(), + "postinst".to_owned(), + "prerm".to_owned(), + "postrm".to_owned(), + ]; + + let mut triggers = triggers::scan(&scripts); + triggers.sort(); + + assert_eq!(triggers, vec!["postinst", "postrm", "preinst", "prerm"]); +} + +#[test] +fn ignores_names_that_are_not_declared_scripts() { + let scripts = vec!["control".to_owned(), "md5sums".to_owned()]; + + let triggers = triggers::scan(&scripts); + + assert!(triggers.is_empty()); +} diff --git a/decoders/deb/upac-deb.toml b/decoders/deb/upac-deb.toml index b020522..8122bfd 100644 --- a/decoders/deb/upac-deb.toml +++ b/decoders/deb/upac-deb.toml @@ -1,15 +1,17 @@ # SPDX-FileCopyrightText: 2026 JustPav # SPDX-FileCopyrightText: 2026 SmoothTeam # -# SPDX-License-Identifier: LGPL-3.0-or-later +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -# Declarative decoder manifest (doc §5.8/§6). Not installed by `zig build` — +# Declarative decoder manifest (doc §5.8/§6). Not installed by `cargo build` — # there is no packaging pipeline (PKGBUILD/spec/etc) in this repo yet; this # file is the canonical source a future package build copies to -# /etc/upac.d/decoders/upac-deb.toml. +# /etc/upac.d/decoders/upac-deb.toml. `library` matches this crate's +# `[lib] name = "upac_decoder_deb"` (Cargo lib-name-to-filename convention: +# `lib` prefix + name verbatim, underscores kept as-is). format = "deb" extensions = ["deb"] -library = "/usr/lib/upac/decoders/libupac-deb.so" +library = "/usr/lib/upac/decoders/libupac_decoder_deb.so" # Registered in shared-mime-info (freedesktop.org.xml). mime = "application/vnd.debian.binary-package" diff --git a/decoders/decoder.toml b/decoders/decoder.toml new file mode 100644 index 0000000..b35437b --- /dev/null +++ b/decoders/decoder.toml @@ -0,0 +1,105 @@ +# SPDX-FileCopyrightText: 2026 JustPav +# SPDX-FileCopyrightText: 2026 SmoothTeam +# +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +# Shared source of truth for external package-format constants used by decoder plugins. Each +# decoder's own build.rs reads this same file and generates its own private layout module; +# nothing here is a Cargo dependency between decoders, just a shared data file. + +# Fixed metadata entry names inside an ALPM (.pkg.tar.*) archive, per pacman's own package +# format — never deployment-configurable, never change without a new package format entirely. +[alpm] +pkginfo_entry = ".PKGINFO" +install_entry = ".INSTALL" +buildinfo_entry = ".BUILDINFO" +mtree_entry = ".MTREE" +changelog_entry = ".CHANGELOG" + +# .PKGINFO's own "key = value" field names, per pacman's package format. +pkginfo_name_key = "pkgname" +pkginfo_version_key = "pkgver" +pkginfo_release_key = "pkgrel" +pkginfo_epoch_key = "epoch" +pkginfo_description_key = "pkgdesc" +pkginfo_url_key = "url" +pkginfo_maintainer_key = "packager" +pkginfo_size_key = "size" +pkginfo_arch_key = "arch" +pkginfo_license_key = "license" +pkginfo_depend_key = "depend" + +# Fixed .INSTALL lifecycle scriptlet function names, per pacman's package format — matched +# against declared hook triggers (doc §5.8), never executed. +pre_install_fn = "pre_install" +post_install_fn = "post_install" +pre_upgrade_fn = "pre_upgrade" +post_upgrade_fn = "post_upgrade" +pre_remove_fn = "pre_remove" +post_remove_fn = "post_remove" + +# Fixed archive-layout identifiers inside a .deb (Unix `ar`) archive, per dpkg's own package +# format — never deployment-configurable, never change without a new package format entirely. +[deb] +control_tar_prefix = "control.tar" +data_tar_prefix = "data.tar" +control_entry = "control" +copyright_dir_prefix = "usr/share/doc/" +copyright_entry_suffix = "/copyright" + +# `control`'s own "Key: value" field names, per dpkg's control file format. +control_name_key = "Package" +control_version_key = "Version" +control_installed_size_key = "Installed-Size" +control_arch_key = "Architecture" +control_description_key = "Description" +control_url_key = "Homepage" +control_maintainer_key = "Maintainer" +control_depends_key = "Depends" + +# Fixed maintainer-script filenames, per dpkg's package format — dpkg has no separate +# install-vs-upgrade script, so each one maps to two declarative trigger positions (doc §5.8). +preinst_file = "preinst" +postinst_file = "postinst" +prerm_file = "prerm" +postrm_file = "postrm" + +# Fixed offsets inside an RPM package's binary layout, per rpm's own file format (lead + +# signature header + main header + cpio payload) — never deployment-configurable, never +# change without a new package format entirely. +[rpm] +# Native scriptlet identifiers, matched against declared hook triggers (doc §5.8), never +# executed — RPM has no separate script files (unlike dpkg), so these just label the 4 +# scriptlet tag positions below, named after their spec-file macro (%pre/%post/%preun/%postun). +prein_name = "pre" +postin_name = "post" +preun_name = "preun" +postun_name = "postun" + +lead_size = 96 + +# Fixed RPM header tag IDs, per rpm's own stable tag namespace (lib/rpmtag.h) — never change +# without a new package format entirely. +name_tag = 1000 +version_tag = 1001 +release_tag = 1002 +summary_tag = 1004 +size_tag = 1009 +license_tag = 1014 +packager_tag = 1015 +url_tag = 1020 +arch_tag = 1022 +prein_tag = 1023 +postin_tag = 1024 +preun_tag = 1025 +postun_tag = 1026 +provide_name_tag = 1047 +require_name_tag = 1049 +require_version_tag = 1050 +require_flags_tag = 1048 +payload_format_tag = 1124 +payload_compressor_tag = 1125 + +# Reserved for xbps's own archive-layout/field-key constants, once xbps gets its Rust rewrite +# (still Zig today, see decoders/xbps-zig). +[xbps] diff --git a/decoders/rpm/Cargo.toml b/decoders/rpm/Cargo.toml new file mode 100644 index 0000000..155d05a --- /dev/null +++ b/decoders/rpm/Cargo.toml @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: 2026 JustPav +# SPDX-FileCopyrightText: 2026 SmoothTeam +# +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +[package] +name = "rpm" +description = "RPM (.rpm) package decoder plugin for upac, implementing rpm's package format" +version.workspace = true + +edition.workspace = true +rust-version.workspace = true + +license.workspace = true + +readme.workspace = true +homepage.workspace = true +repository.workspace = true + +keywords.workspace = true +categories.workspace = true + +[lints] +workspace = true + +[lib] +name = "upac_decoder_rpm" +crate-type = ["cdylib", "rlib"] + +[dependencies] +upac-abi = { workspace = true } +upac-types = { workspace = true } + +cpio = { workspace = true } +flate2 = { workspace = true } +sha2 = { workspace = true } +xz2 = { workspace = true } +zstd = { workspace = true } + +[build-dependencies] +toml = { workspace = true } + +[features] +default = ["cdylib"] +cdylib = [] diff --git a/decoders/rpm/build.rs b/decoders/rpm/build.rs new file mode 100644 index 0000000..fba6d00 --- /dev/null +++ b/decoders/rpm/build.rs @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::env::var; +use std::error::Error; +use std::fs::{read_to_string, write}; +use std::path::Path; + +use toml::{Value, from_str}; + +fn main() -> Result<(), Box> { + let manifest_dir = var("CARGO_MANIFEST_DIR")?; + + let mut generated = String::new(); + generated.push_str(&generate_decoder_toml(&manifest_dir)?); + generated.push_str(&generate_manifest_module(&manifest_dir)?); + + let out = Path::new(&var("OUT_DIR")?).join("layout.rs"); + write(out, generated)?; + + Ok(()) +} + +fn generate_decoder_toml(manifest_dir: &str) -> Result> { + let source = Path::new(manifest_dir).join("../decoder.toml"); + + println!("cargo:rerun-if-changed={}", source.display()); + + let raw = read_to_string(&source)?; + let config: Value = from_str(&raw)?; + + let section = "rpm"; + let entries = config + .get(section) + .and_then(Value::as_table) + .ok_or_else(|| format!("decoder.toml: [{section}] must be a table"))?; + + let mut generated = String::new(); + generated.push_str(&format!("pub mod {section} {{\n")); + + for (key, value) in entries { + let rendered = if let Some(value) = value.as_str() { + format!("&str = {value:?}") + } else if let Some(value) = value.as_integer() { + format!("u32 = {value}") + } else { + return Err(format!("decoder.toml: {section}.{key} must be a string or integer").into()); + }; + + generated.push_str(&format!(" pub const {}: {rendered};\n", key.to_uppercase())); + } + + generated.push_str("}\n"); + + Ok(generated) +} + +/// Compiles this crate's own deployable manifest (`format`/`extensions`) into constants, so a +/// `builtin-rpm` build can dispatch by format without reading `upac-rpm.toml` from disk at +/// runtime — `library`/`mime` are runtime-deployment-only fields, not needed here. +fn generate_manifest_module(manifest_dir: &str) -> Result> { + let source = Path::new(manifest_dir).join("upac-rpm.toml"); + + println!("cargo:rerun-if-changed={}", source.display()); + + let raw = read_to_string(&source)?; + let config: Value = from_str(&raw)?; + + let format = config + .get("format") + .and_then(Value::as_str) + .ok_or("upac-rpm.toml: format must be a string")?; + + let extensions = config + .get("extensions") + .and_then(Value::as_array) + .ok_or("upac-rpm.toml: extensions must be an array")? + .iter() + .map(|entry| { + entry + .as_str() + .ok_or("upac-rpm.toml: extensions entries must be strings") + }) + .collect::, _>>()?; + + let mut generated = String::new(); + generated.push_str("pub mod manifest {\n"); + generated.push_str(&format!(" pub const FORMAT: &str = {format:?};\n")); + generated.push_str(" pub const EXTENSIONS: &[&str] = &[\n"); + for extension in extensions { + generated.push_str(&format!(" {extension:?},\n")); + } + generated.push_str(" ];\n"); + generated.push_str("}\n"); + + Ok(generated) +} diff --git a/decoders/rpm/build.zig b/decoders/rpm/build.zig deleted file mode 100644 index 9f8b570..0000000 --- a/decoders/rpm/build.zig +++ /dev/null @@ -1,65 +0,0 @@ -// ── Imports ───────────────────────────────────────────────────────────────────── -const std = @import("std"); - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - const strip = b.option(bool, "strip", "Strip debug symbols") orelse false; - const stack_check = b.option(bool, "stack-check", "Check for stack overflows") orelse false; - - // ── C libs ──────────────────────────────────────────────────────────────── - const translated_libs = b.addTranslateC(.{ - .root_source_file = b.path("src/imports.h"), - .target = target, - .optimize = optimize, - }); - translated_libs.link_libc = true; - translated_libs.linkSystemLibrary("archive", .{}); - - const c_libs_module = translated_libs.createModule(); - - // ── Types ───────────────────────────────────────────────────────────────── - const upac_backend_types = b.createModule(.{ - .root_source_file = b.path("src/types/types.zig"), - .target = target, - .optimize = optimize, - }); - - // ── FFI ───────────────────────────────────────────────────────────────── - const upac_backend_ffi = b.createModule(.{ - .root_source_file = b.path("src/ffi.zig"), - .target = target, - .optimize = optimize, - }); - - upac_backend_ffi.addImport("upac-backend-types", upac_backend_types); - - // ── Root ────────────────────────────────────────────────────────────────── - const upac_backend_root = b.createModule(.{ - .root_source_file = b.path("src/symbols.zig"), - .target = target, - .optimize = optimize, - }); - - upac_backend_root.strip = strip; - upac_backend_root.stack_check = stack_check; - - // ── Shared library ──────────────────────────────────────────────────────── - const shared_lib = b.addLibrary(.{ - .name = "upac-rpm", - .linkage = .dynamic, - .root_module = upac_backend_root, - }); - - shared_lib.root_module.addImport("c-libs", c_libs_module); - shared_lib.root_module.addImport("upac-backend-types", upac_backend_types); - shared_lib.root_module.addImport("upac-backend-ffi", upac_backend_ffi); - - shared_lib.root_module.strip = strip; - shared_lib.root_module.stack_check = stack_check; - shared_lib.bundle_compiler_rt = false; - shared_lib.link_gc_sections = false; - - b.installArtifact(shared_lib); -} diff --git a/decoders/rpm/build.zig.license b/decoders/rpm/build.zig.license deleted file mode 100644 index 50143aa..0000000 --- a/decoders/rpm/build.zig.license +++ /dev/null @@ -1,4 +0,0 @@ -SPDX-FileCopyrightText: 2026 JustPav -SPDX-FileCopyrightText: 2026 SmoothTeam - -SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/decoders/rpm/build.zig.zon b/decoders/rpm/build.zig.zon deleted file mode 100644 index 8ed9ac9..0000000 --- a/decoders/rpm/build.zig.zon +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -.{ - .name = .upac_rpm, - .version = "0.1.4", - .fingerprint = 0xa8e7860db0e12a85, - .minimum_zig_version = "0.16.0", - - .dependencies = .{}, - .paths = .{ "build.zig", "build.zig.zon", "src" }, -} diff --git a/decoders/rpm/src/backend/backend.zig b/decoders/rpm/src/backend/backend.zig deleted file mode 100644 index 9c0ef01..0000000 --- a/decoders/rpm/src/backend/backend.zig +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ───────────────────────────────────────────────────────────────────── -pub const std = @import("std"); - -const types = @import("upac-backend-types"); -const BackendError = types.BackendError; -const StateId = types.StateId; -const PackageMeta = types.PackageMeta; -const PrepareData = types.PrepareData; -const PrepareResult = types.PrepareResult; -const CancelToken = types.CancelToken; - -const verifying = @import("verifying/verifying.zig"); -const unpacking = @import("unpacking/unpacking.zig"); -const parsing = @import("parsing/parsing.zig"); - -// ── BackendMachine ──────────────────────────────────────────────────────────── -pub const BackendMachine = struct { - data: PrepareData, - - meta: ?PackageMeta = null, - temp_package_path: ?[:0]const u8 = null, - - allocator: std.mem.Allocator, - io: std.Io, - - pub fn deinit(self: *BackendMachine) void { - if (self.temp_package_path) |path| self.allocator.free(path); - } - - pub fn hook(self: *BackendMachine, event: StateId) BackendError!void { - const cb = self.data.on_hook orelse return; - if (cb(@intFromEnum(event), null, self.data.hook_ctx) == .cancel) return BackendError.Cancelled; - } - - pub fn run(data: PrepareData, allocator: std.mem.Allocator) BackendError!PrepareResult { - var state = StateId.verifying; - var machine = BackendMachine{ - .data = data, - - .io = std.Io.Threaded.global_single_threaded.io(), - .allocator = allocator, - }; - defer machine.deinit(); - - while (state != .done) { - machine.hook(state) catch |err| return err; - switch (state) { - .verifying => { - verifying.run(&machine) catch |err| return err; - state = .extracting; - }, - .extracting => { - unpacking.run(&machine) catch |err| return err; - state = .reading_meta; - }, - .reading_meta => { - parsing.run(&machine) catch |err| return err; - state = .done; - }, - .done, .special_step => {}, - } - } - - const temp_package_path = machine.temp_package_path orelse return BackendError.TempDirFailed; - machine.temp_package_path = null; - - return PrepareResult{ - .meta = machine.meta orelse return BackendError.MetadataNotFound, - .temp_path = temp_package_path, - }; - } -}; diff --git a/decoders/rpm/src/backend/parsing/parsing.zig b/decoders/rpm/src/backend/parsing/parsing.zig deleted file mode 100644 index 029d43c..0000000 --- a/decoders/rpm/src/backend/parsing/parsing.zig +++ /dev/null @@ -1,251 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ─────────────────────────────────────────────────────────────────── -const std = backend.std; - -const types = @import("upac-backend-types"); -const rpm_lead_size = types.rpm_lead_size; - -const BackendError = types.BackendError; - -const PackageMeta = types.PackageMeta; -const RawMeta = types.RawMeta; - -const RpmTag = types.RpmTag; - -const backend = @import("../backend.zig"); -const Machine = backend.BackendMachine; - -const utils = @import("utils.zig"); -const readExact = utils.readExact; -const readString = utils.readString; -const parseVersion = utils.parseVersion; - -// ── Constants ───────────────────────────────────────────────────────────────── -const header_magic = [3]u8{ 0x8E, 0xAD, 0xE8 }; - -// ── ParsingState ────────────────────────────────────────────────────────────── -const ParsingState = enum { - skip_lead, - skip_signature, - read_header_index, - read_data_store, - extract_tags, - build_meta, - done, -}; - -// ── ParsingMachine ──────────────────────────────────────────────────────────── -const ParsingMachine = struct { - backend: *Machine, - - file: ?std.Io.File = null, - - index_bytes: ?[]u8 = null, - data_block: ?[]u8 = null, - - tag_count: u32 = 0, - data_size: u32 = 0, - - raw_meta: RawMeta = .{}, - - fn stateFailed(self: *ParsingMachine, err: BackendError) BackendError { - if (self.file) |file| { - file.close(self.backend.io); - self.file = null; - } - - if (self.index_bytes) |bytes| { - self.backend.allocator.free(bytes); - self.index_bytes = null; - } - - if (self.data_block) |block| { - self.backend.allocator.free(block); - self.data_block = null; - } - - self.raw_meta.deinit(self.backend.allocator); - return err; - } -}; - -// ── Trampoline ──────────────────────────────────────────────────────────────── -pub fn run(machine: *Machine) BackendError!void { - var parsing = ParsingMachine{ .backend = machine }; - - var state = ParsingState.skip_lead; - while (state != .done) { - if (machine.data.cancel_token.isCancelled()) return parsing.stateFailed(BackendError.Cancelled); - state = switch (state) { - .skip_lead => try stateSkipLead(&parsing), - .skip_signature => try stateSkipSignature(&parsing), - .read_header_index => try stateReadHeaderIndex(&parsing), - .read_data_store => try stateReadDataStore(&parsing), - .extract_tags => try stateExtractTags(&parsing), - .build_meta => try stateBuildMeta(&parsing), - .done => unreachable, - }; - } -} - -// ── States ──────────────────────────────────────────────────────────────────── -fn stateSkipLead(machine: *ParsingMachine) BackendError!ParsingState { - const package_path = std.mem.span(machine.backend.data.package_path_c); - - const file = std.Io.Dir.openFileAbsolute(machine.backend.io, package_path, .{}) catch return machine.stateFailed(BackendError.ReadFailed); - machine.file = file; - - machine.backend.io.vtable.fileSeekTo(machine.backend.io.userdata, file, rpm_lead_size) catch return machine.stateFailed(BackendError.ReadFailed); - - return .skip_signature; -} - -fn stateSkipSignature(machine: *ParsingMachine) BackendError!ParsingState { - var section_header_buf: [16]u8 = undefined; - - const file = machine.file orelse return machine.stateFailed(BackendError.ReadFailed); - - readExact(machine.backend.io, file, §ion_header_buf) catch return machine.stateFailed(BackendError.InvalidPackage); - - if (!std.mem.eql(u8, section_header_buf[0..3], &header_magic)) return machine.stateFailed(BackendError.InvalidPackage); - - const signature_tag_count = std.mem.readInt(u32, section_header_buf[8..12], .big); - const signature_data_size = std.mem.readInt(u32, section_header_buf[12..16], .big); - - const signature_total_size: u64 = @as(u64, signature_tag_count) * 16 + signature_data_size; - machine.backend.io.vtable.fileSeekBy(machine.backend.io.userdata, file, @intCast(signature_total_size)) catch return machine.stateFailed(BackendError.ReadFailed); - - const alignment_remainder = signature_total_size % 8; - if (alignment_remainder != 0) machine.backend.io.vtable.fileSeekBy(machine.backend.io.userdata, file, @intCast(8 - alignment_remainder)) catch return machine.stateFailed(BackendError.ReadFailed); - - return .read_header_index; -} - -fn stateReadHeaderIndex(machine: *ParsingMachine) BackendError!ParsingState { - var section_header_buf: [16]u8 = undefined; - - const file = machine.file orelse return machine.stateFailed(BackendError.ReadFailed); - - readExact(machine.backend.io, file, §ion_header_buf) catch return machine.stateFailed(BackendError.InvalidPackage); - - if (!std.mem.eql(u8, section_header_buf[0..3], &header_magic)) return machine.stateFailed(BackendError.InvalidPackage); - - machine.tag_count = std.mem.readInt(u32, section_header_buf[8..12], .big); - machine.data_size = std.mem.readInt(u32, section_header_buf[12..16], .big); - - const index_bytes = machine.backend.allocator.alloc(u8, @as(usize, machine.tag_count) * 16) catch return machine.stateFailed(BackendError.OutOfMemory); - machine.index_bytes = index_bytes; - - readExact(machine.backend.io, file, index_bytes) catch return machine.stateFailed(BackendError.InvalidPackage); - - return .read_data_store; -} - -fn stateReadDataStore(machine: *ParsingMachine) BackendError!ParsingState { - const file = machine.file orelse return machine.stateFailed(BackendError.ReadFailed); - - const data_block = machine.backend.allocator.alloc(u8, machine.data_size) catch return machine.stateFailed(BackendError.OutOfMemory); - machine.data_block = data_block; - - readExact(machine.backend.io, file, data_block) catch return machine.stateFailed(BackendError.InvalidPackage); - - file.close(machine.backend.io); - machine.file = null; - - return .extract_tags; -} - -fn stateExtractTags(machine: *ParsingMachine) BackendError!ParsingState { - const index_bytes = machine.index_bytes orelse return machine.stateFailed(BackendError.InvalidPackage); - machine.index_bytes = null; - defer machine.backend.allocator.free(index_bytes); - - const data_block = machine.data_block orelse return machine.stateFailed(BackendError.InvalidPackage); - machine.data_block = null; - defer machine.backend.allocator.free(data_block); - - var tag_index: u32 = 0; - while (tag_index < machine.tag_count) : (tag_index += 1) { - const entry_bytes = index_bytes[tag_index * 16 ..][0..16]; - const tag_id = std.mem.readInt(u32, entry_bytes[0..4], .big); - const data_offset = std.mem.readInt(u32, entry_bytes[8..12], .big); - - const tag: RpmTag = @enumFromInt(tag_id); - switch (tag) { - .name => machine.raw_meta.name = readString(machine.backend.allocator, data_block, data_offset) catch return machine.stateFailed(BackendError.InvalidPackage), - .version => machine.raw_meta.version = readString(machine.backend.allocator, data_block, data_offset) catch return machine.stateFailed(BackendError.InvalidPackage), - .release => machine.raw_meta.release = readString(machine.backend.allocator, data_block, data_offset) catch return machine.stateFailed(BackendError.InvalidPackage), - .summary => machine.raw_meta.summary = readString(machine.backend.allocator, data_block, data_offset) catch return machine.stateFailed(BackendError.InvalidPackage), - .arch => machine.raw_meta.arch = readString(machine.backend.allocator, data_block, data_offset) catch return machine.stateFailed(BackendError.InvalidPackage), - .license => machine.raw_meta.license = readString(machine.backend.allocator, data_block, data_offset) catch return machine.stateFailed(BackendError.InvalidPackage), - .url => machine.raw_meta.url = readString(machine.backend.allocator, data_block, data_offset) catch return machine.stateFailed(BackendError.InvalidPackage), - .packager => machine.raw_meta.packager = readString(machine.backend.allocator, data_block, data_offset) catch return machine.stateFailed(BackendError.InvalidPackage), - .size => { - if (data_offset + 4 <= data_block.len) { - machine.raw_meta.size = @intCast(std.mem.readInt(i32, data_block[data_offset..][0..4], .big)); - } - }, - _ => {}, - } - } - - return .build_meta; -} - -fn stateBuildMeta(machine: *ParsingMachine) BackendError!ParsingState { - var sha256: [32]u8 = undefined; - - _ = std.fmt.hexToBytes(&sha256, machine.backend.data.checksum) catch return machine.stateFailed(BackendError.InvalidPackage); - - const raw_version_str = machine.raw_meta.version orelse return machine.stateFailed(BackendError.MetadataNotFound); - defer machine.backend.allocator.free(raw_version_str); - machine.raw_meta.version = null; - - const parsed_version = parseVersion(machine.backend.allocator, raw_version_str, machine.raw_meta.release) catch return machine.stateFailed(BackendError.InvalidPackage); - errdefer parsed_version.deinit(machine.backend.allocator); - - const package_name = machine.raw_meta.name orelse return machine.stateFailed(BackendError.MetadataNotFound); - machine.raw_meta.name = null; - errdefer machine.backend.allocator.free(package_name); - - const package_arch = machine.raw_meta.arch orelse return machine.stateFailed(BackendError.MetadataNotFound); - machine.raw_meta.arch = null; - errdefer machine.backend.allocator.free(package_arch); - - const package_author = machine.backend.allocator.dupe(u8, machine.raw_meta.packager orelse "") catch return machine.stateFailed(BackendError.OutOfMemory); - errdefer machine.backend.allocator.free(package_author); - - const package_description = machine.backend.allocator.dupe(u8, machine.raw_meta.summary orelse "") catch return machine.stateFailed(BackendError.OutOfMemory); - errdefer machine.backend.allocator.free(package_description); - - const package_license = machine.backend.allocator.dupe(u8, machine.raw_meta.license orelse "") catch return machine.stateFailed(BackendError.OutOfMemory); - errdefer machine.backend.allocator.free(package_license); - - const package_url = machine.backend.allocator.dupe(u8, machine.raw_meta.url orelse "") catch return machine.stateFailed(BackendError.OutOfMemory); - errdefer machine.backend.allocator.free(package_url); - - const package_packager = machine.backend.allocator.dupe(u8, machine.raw_meta.packager orelse "") catch return machine.stateFailed(BackendError.OutOfMemory); - errdefer machine.backend.allocator.free(package_packager); - - machine.backend.meta = PackageMeta{ - .name = package_name, - .version = parsed_version, - .arch = package_arch, - .author = package_author, - .description = package_description, - .license = package_license, - .url = package_url, - .packager = package_packager, - .checksum = sha256, - .size = machine.raw_meta.size, - .installed_at = @intCast(@divTrunc(std.Io.Clock.real.now(machine.backend.io).nanoseconds, std.time.ns_per_s)), - }; - - machine.raw_meta.deinit(machine.backend.allocator); - - return .done; -} diff --git a/decoders/rpm/src/backend/parsing/utils.zig b/decoders/rpm/src/backend/parsing/utils.zig deleted file mode 100644 index 707e430..0000000 --- a/decoders/rpm/src/backend/parsing/utils.zig +++ /dev/null @@ -1,237 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ───────────────────────────────────────────────────────────────────── -const std = @import("std"); - -const types = @import("upac-backend-types"); -const BackendError = types.BackendError; -const Version = types.Version; - -// ── Contains RPM magic bytes and header magic bytes ───────────────────────────────────────────────────────────── -const rpm_magic: [4]u8 = .{ 0xED, 0xAB, 0xEE, 0xDB }; -const header_magic: [3]u8 = .{ 0x8E, 0xAD, 0xE8 }; - -// Represents an RPM tag, identified by its numeric tag ID -const RpmTag = enum(u32) { - name = 1000, - version = 1001, - release = 1002, - summary = 1004, - description = 1005, - license = 1014, - packager = 1015, - url = 1020, - arch = 1022, - size = 1023, - _, -}; - -const TagEntry = struct { - tag: u32, - tag_type: u32, - offset: u32, - count: u32, -}; - -const SectionHeader = struct { tag_count: u32, data_size: u32 }; - -// ── Public types ──────────────────────────────────────────────────────────── -// Contains metadata extracted from the RPM package header -pub const RpmHeader = struct { - name: ?[]const u8 = null, - version: ?[]const u8 = null, - size: u32 = 0, - release: ?[]const u8 = null, - summary: ?[]const u8 = null, - arch: ?[]const u8 = null, - license: ?[]const u8 = null, - url: ?[]const u8 = null, - packager: ?[]const u8 = null, - - pub fn deinit(self: *RpmHeader, allocator: std.mem.Allocator) void { - if (self.name) |value| allocator.free(value); - if (self.version) |value| allocator.free(value); - if (self.release) |value| allocator.free(value); - if (self.summary) |value| allocator.free(value); - if (self.arch) |value| allocator.free(value); - if (self.license) |value| allocator.free(value); - if (self.url) |value| allocator.free(value); - if (self.packager) |value| allocator.free(value); - } -}; - -// ── Parser ──────────────────────────────────────────────────────────────────── -pub fn parseHeader(allocator: std.mem.Allocator, io: std.Io, file: std.Io.File) !RpmHeader { - try verifyMagic(io, file); - try skipLeadSection(io, file); - try skipSignatureSection(io, file); - return readHeaderSection(allocator, io, file); -} - -// ── Internal functions ──────────────────────────────────────────────────────── - -// Reads exactly buf.len bytes; returns error.UnexpectedEOF on short read. -pub fn readExact(io: std.Io, file: std.Io.File, buf: []u8) !void { - var total: usize = 0; - while (total < buf.len) { - const iov = [1][]u8{buf[total..]}; - const bytes_read_count = file.readStreaming(io, &iov) catch |err| { - if (err == error.EndOfStream) return error.UnexpectedEOF; - return err; - }; - if (bytes_read_count == 0) return error.UnexpectedEOF; - total += bytes_read_count; - } -} - -fn verifyMagic(io: std.Io, file: std.Io.File) !void { - var magic_buffer: [4]u8 = undefined; - try readExact(io, file, &magic_buffer); - if (!std.mem.eql(u8, &magic_buffer, &rpm_magic)) return error.InvalidRpmMagic; -} - -// Skips the obsolete Lead section (96 bytes minus the 4 already read for magic) -fn skipLeadSection(io: std.Io, file: std.Io.File) !void { - try io.vtable.fileSeekBy(io.userdata, file, 96 - 4); -} - -// Reads the 16-byte section intro; checks magic and returns tag_count + data_size. -fn readSectionHeader(io: std.Io, file: std.Io.File, comptime err: anyerror) !SectionHeader { - var buf: [16]u8 = undefined; - try readExact(io, file, &buf); - if (!std.mem.eql(u8, buf[0..3], &header_magic)) return err; - return .{ - .tag_count = std.mem.readInt(u32, buf[8..12], .big), - .data_size = std.mem.readInt(u32, buf[12..16], .big), - }; -} - -// Skips the digital signature section, including its 8-byte alignment padding. -fn skipSignatureSection(io: std.Io, file: std.Io.File) !void { - const header = try readSectionHeader(io, file, error.InvalidSignatureMagic); - - const tags_size: u64 = @as(u64, header.tag_count) * 16; - const data_size: u64 = header.data_size; - try io.vtable.fileSeekBy(io.userdata, file, @intCast(tags_size + data_size)); - - // Pad to 8-byte boundary (only index+data counts, header is already 8-aligned) - const remainder = (tags_size + data_size) % 8; - if (remainder != 0) try io.vtable.fileSeekBy(io.userdata, file, @intCast(8 - remainder)); -} - -// Reads the main header section, extracting the tag table and data block. -fn readHeaderSection(allocator: std.mem.Allocator, io: std.Io, file: std.Io.File) !RpmHeader { - const header = try readSectionHeader(io, file, error.InvalidHeaderMagic); - - // Read all tag index entries as a flat byte slice, then parse in-place. - const index_bytes = try allocator.alloc(u8, @as(usize, header.tag_count) * 16); - defer allocator.free(index_bytes); - try readExact(io, file, index_bytes); - - // Read the data store. - const data_block = try allocator.alloc(u8, header.data_size); - defer allocator.free(data_block); - try readExact(io, file, data_block); - - var rpm_header = RpmHeader{}; - errdefer rpm_header.deinit(allocator); - - var i: usize = 0; - while (i < header.tag_count) : (i += 1) { - const e = index_bytes[i * 16 ..][0..16]; - const tag = std.mem.readInt(u32, e[0..4], .big); - const offset = std.mem.readInt(u32, e[8..12], .big); - - const rpm_tag = blk: { - inline for (std.meta.fields(RpmTag)) |field| { - if (field.value == tag) break :blk @as(RpmTag, @field(RpmTag, field.name)); - } - break :blk null; - } orelse continue; - switch (rpm_tag) { - .name => rpm_header.name = try readString(allocator, data_block, offset), - .version => rpm_header.version = try readString(allocator, data_block, offset), - .release => rpm_header.release = try readString(allocator, data_block, offset), - .summary => rpm_header.summary = try readString(allocator, data_block, offset), - .arch => rpm_header.arch = try readString(allocator, data_block, offset), - .license => rpm_header.license = try readString(allocator, data_block, offset), - .url => rpm_header.url = try readString(allocator, data_block, offset), - .packager => rpm_header.packager = try readString(allocator, data_block, offset), - .size => { - if (offset + 4 <= data_block.len) { - rpm_header.size = @intCast(std.mem.readInt(i32, data_block[offset..][0..4], .big)); - } - }, - else => {}, - } - } - - return rpm_header; -} - -// Parses an RPM version from separate version and release strings. -// version_str: e.g. "3.6.2" or "1:3.6.2" (epoch prefix) -// release_str: e.g. "2" or "2.fc40" (only the leading number matters) -pub fn parseVersion(allocator: std.mem.Allocator, version_str: []const u8, release_str: ?[]const u8) BackendError!Version { - var remaining = version_str; - - var epoch: u32 = 0; - if (std.mem.indexOf(u8, remaining, ":")) |colon_idx| { - epoch = std.fmt.parseInt(u32, remaining[0..colon_idx], 10) catch 0; - remaining = remaining[colon_idx + 1 ..]; - } - - var pre: ?[]const u8 = null; - if (std.mem.indexOf(u8, remaining, "~")) |tilde_idx| { - pre = allocator.dupe(u8, remaining[tilde_idx + 1 ..]) catch return BackendError.AllocZFailed; - remaining = remaining[0..tilde_idx]; - } - errdefer if (pre) |p| allocator.free(p); - - var parts_list = std.ArrayList(u32).empty; - defer parts_list.deinit(allocator); - - var iter = std.mem.splitScalar(u8, remaining, '.'); - while (iter.next()) |segment| { - if (segment.len == 0) continue; - - const digits_end = for (segment, 0..) |ch, i| { - if (ch < '0' or ch > '9') break i; - } else segment.len; - - if (digits_end == 0) continue; - - const num = std.fmt.parseInt(u32, segment[0..digits_end], 10) catch continue; - parts_list.append(allocator, num) catch return BackendError.AllocZFailed; - - if (digits_end < segment.len and pre == null) { - pre = allocator.dupe(u8, segment[digits_end..]) catch return BackendError.AllocZFailed; - } - } - - if (parts_list.items.len == 0) return BackendError.InvalidPackage; - - const parts_owned = parts_list.toOwnedSlice(allocator) catch return BackendError.AllocZFailed; - errdefer allocator.free(parts_owned); - - var release: u32 = 1; - if (release_str) |rel| { - release = std.fmt.parseInt(u32, rel, 10) catch blk: { - const dot_idx = std.mem.indexOfScalar(u8, rel, '.') orelse rel.len; - break :blk std.fmt.parseInt(u32, rel[0..dot_idx], 10) catch 1; - }; - } - - return Version{ .epoch = epoch, .parts = parts_owned, .pre = pre, .release = release }; -} - -// Reads a null-terminated string from a data block at a specified offset. -pub fn readString(allocator: std.mem.Allocator, data_block: []const u8, offset: u32) ![]const u8 { - if (offset >= data_block.len) return error.InvalidTagOffset; - const start = data_block[offset..]; - const end = std.mem.indexOfScalar(u8, start, 0) orelse return error.UnterminatedString; - return allocator.dupe(u8, start[0..end]); -} diff --git a/decoders/rpm/src/backend/unpacking/unpacking.zig b/decoders/rpm/src/backend/unpacking/unpacking.zig deleted file mode 100644 index 06f8fd7..0000000 --- a/decoders/rpm/src/backend/unpacking/unpacking.zig +++ /dev/null @@ -1,202 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ─────────────────────────────────────────────────────────────────── -const std = @import("std"); - -const c_libs = @import("c-libs"); - -const types = @import("upac-backend-types"); - -const BackendError = types.BackendError; - -const backend = @import("../backend.zig"); -const Machine = backend.BackendMachine; - -// ── UnpackingState ──────────────────────────────────────────────────────────── -const UnpackingState = enum { - create_temp_dir, - open_archive, - next_entry, - write_blocks, - close_archive, - done, -}; - -// ── UnpackingMachine ────────────────────────────────────────────────────────── -const UnpackingMachine = struct { - backend: *Machine, - - file: ?std.Io.File = null, - - archive_reader: ?*c_libs.archive = null, - archive_writer: ?*c_libs.archive = null, - - old_package_dir: ?std.Io.Dir = null, - - fn stateFailed(self: *UnpackingMachine, err: BackendError) BackendError { - if (self.archive_reader) |reader| { - _ = c_libs.archive_read_free(reader); - self.archive_reader = null; - } - - if (self.archive_writer) |writer| { - _ = c_libs.archive_write_free(writer); - self.archive_writer = null; - } - - if (self.old_package_dir) |old_dir| { - std.Io.Threaded.fchdir(old_dir.handle) catch {}; - old_dir.close(self.backend.io); - self.old_package_dir = null; - } - - if (self.file) |file| { - file.close(self.backend.io); - self.file = null; - } - - if (self.backend.temp_package_path) |temp_path| { - std.Io.Dir.cwd().deleteTree(self.backend.io, temp_path) catch {}; - - self.backend.allocator.free(temp_path); - self.backend.temp_package_path = null; - } - - return err; - } -}; - -// ── Trampoline ──────────────────────────────────────────────────────────────── -pub fn run(machine: *Machine) BackendError!void { - var unpacking = UnpackingMachine{ .backend = machine }; - - var state = UnpackingState.create_temp_dir; - while (state != .done) { - if (machine.data.cancel_token.isCancelled()) return unpacking.stateFailed(BackendError.Cancelled); - state = switch (state) { - .create_temp_dir => try stateCreateTempDir(&unpacking), - .open_archive => try stateOpenArchive(&unpacking), - .next_entry => try stateNextEntry(&unpacking), - .write_blocks => try stateWriteBlocks(&unpacking), - .close_archive => stateCloseArchive(&unpacking), - .done => unreachable, - }; - } -} - -// ── States ──────────────────────────────────────────────────────────────────── -fn stateCreateTempDir(machine: *UnpackingMachine) BackendError!UnpackingState { - var temp_dir_name_buf: [256]u8 = undefined; - - const temp_path = std.mem.span(machine.backend.data.temp_path_c); - - const timestamp: i64 = @intCast(@divTrunc(std.Io.Clock.real.now(machine.backend.io).nanoseconds, std.time.ns_per_ms)); - - const temp_package_dir_name = std.fmt.bufPrintZ(&temp_dir_name_buf, "upac-installed-{d}", .{timestamp}) catch return machine.stateFailed(BackendError.AllocZFailed); - - const temp_package_path = std.Io.Dir.path.joinZ(machine.backend.allocator, &.{ temp_path, temp_package_dir_name }) catch return machine.stateFailed(BackendError.AllocZFailed); - - std.Io.Dir.createDirAbsolute(machine.backend.io, temp_package_path, .default_dir) catch return machine.stateFailed(BackendError.TempDirFailed); - - machine.backend.temp_package_path = temp_package_path; - - return .open_archive; -} - -fn stateOpenArchive(machine: *UnpackingMachine) BackendError!UnpackingState { - const package_path = std.mem.span(machine.backend.data.package_path_c); - const temp_package_path = machine.backend.temp_package_path orelse return machine.stateFailed(BackendError.TempDirFailed); - - const file = std.Io.Dir.openFileAbsolute(machine.backend.io, package_path, .{}) catch return machine.stateFailed(BackendError.ReadFailed); - machine.file = file; - - const archive_reader = c_libs.archive_read_new() orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - machine.archive_reader = archive_reader; - - _ = c_libs.archive_read_support_format_all(archive_reader); - _ = c_libs.archive_read_support_filter_all(archive_reader); - - if (c_libs.archive_read_open_fd(archive_reader, file.handle, 16384) != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveOpenFailed); - - const archive_writer = c_libs.archive_write_disk_new() orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - machine.archive_writer = archive_writer; - - _ = c_libs.archive_write_disk_set_options(archive_writer, c_libs.ARCHIVE_EXTRACT_TIME | - c_libs.ARCHIVE_EXTRACT_PERM | - c_libs.ARCHIVE_EXTRACT_FFLAGS); - _ = c_libs.archive_write_disk_set_standard_lookup(archive_writer); - - const old_package_dir = std.Io.Dir.cwd().openDir(machine.backend.io, ".", .{}) catch return machine.stateFailed(BackendError.ReadFailed); - machine.old_package_dir = old_package_dir; - - std.Io.Threaded.chdir(temp_package_path) catch return machine.stateFailed(BackendError.TempDirFailed); - - return .next_entry; -} - -fn stateNextEntry(machine: *UnpackingMachine) BackendError!UnpackingState { - var archive_entry: ?*c_libs.archive_entry = undefined; - - const archive_reader = machine.archive_reader orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - const archive_writer = machine.archive_writer orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - - const read_result = c_libs.archive_read_next_header(archive_reader, &archive_entry); - - if (read_result == c_libs.ARCHIVE_EOF) return .close_archive; - if (read_result != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveReadFailed); - - const entry = archive_entry orelse return machine.stateFailed(BackendError.ArchiveReadFailed); - - if (c_libs.archive_write_header(archive_writer, entry) != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveExtractFailed); - - return .write_blocks; -} - -fn stateWriteBlocks(machine: *UnpackingMachine) BackendError!UnpackingState { - var block_size: usize = 0; - var block_offset: i64 = 0; - var data_block: ?*const anyopaque = null; - - const archive_reader = machine.archive_reader orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - const archive_writer = machine.archive_writer orelse return machine.stateFailed(BackendError.ArchiveOpenFailed); - - const block_result = c_libs.archive_read_data_block(archive_reader, &data_block, &block_size, &block_offset); - if (block_result == c_libs.ARCHIVE_EOF) { - if (c_libs.archive_write_finish_entry(archive_writer) != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveExtractFailed); - - return .next_entry; - } - if (block_result != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveReadFailed); - - if (c_libs.archive_write_data_block(archive_writer, data_block, block_size, block_offset) != c_libs.ARCHIVE_OK) return machine.stateFailed(BackendError.ArchiveExtractFailed); - - return .write_blocks; -} - -fn stateCloseArchive(machine: *UnpackingMachine) UnpackingState { - if (machine.archive_reader) |reader| { - _ = c_libs.archive_read_free(reader); - machine.archive_reader = null; - } - - if (machine.archive_writer) |writer| { - _ = c_libs.archive_write_free(writer); - machine.archive_writer = null; - } - - if (machine.old_package_dir) |old_dir| { - std.Io.Threaded.fchdir(old_dir.handle) catch {}; - old_dir.close(machine.backend.io); - machine.old_package_dir = null; - } - - if (machine.file) |file| { - file.close(machine.backend.io); - machine.file = null; - } - - return .done; -} diff --git a/decoders/rpm/src/backend/verifying/verifying.zig b/decoders/rpm/src/backend/verifying/verifying.zig deleted file mode 100644 index 4df02cb..0000000 --- a/decoders/rpm/src/backend/verifying/verifying.zig +++ /dev/null @@ -1,137 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ─────────────────────────────────────────────────────────────────── -const std = @import("std"); - -const types = @import("upac-backend-types"); -const rpm_lead_magic = types.rpm_lead_magic; -const rpm_lead_size = types.rpm_lead_size; - -const BackendError = types.BackendError; - -const backend = @import("../backend.zig"); -const Machine = backend.BackendMachine; - -// ── VerifyingState ──────────────────────────────────────────────────────────── -const VerifyingState = enum { - check_file, - check_temp_dir, - hash, - compare, - validate_lead, - done, -}; - -// ── VerifyingMachine ────────────────────────────────────────────────────────── -const VerifyingMachine = struct { - backend: *Machine, - - file: ?std.Io.File = null, - - digest_checksum: [32]u8 = undefined, - - fn stateFailed(self: *VerifyingMachine, err: BackendError) BackendError { - if (self.file) |file| { - file.close(self.backend.io); - self.file = null; - } - - return err; - } -}; - -// ── Trampoline ──────────────────────────────────────────────────────────────── -pub fn run(machine: *Machine) BackendError!void { - var verifying = VerifyingMachine{ .backend = machine }; - - var state = VerifyingState.check_file; - while (state != .done) { - if (machine.data.cancel_token.isCancelled()) return verifying.stateFailed(BackendError.Cancelled); - state = switch (state) { - .check_file => try stateCheckFile(&verifying), - .check_temp_dir => try stateCheckTempDir(&verifying), - .hash => try stateHash(&verifying), - .compare => try stateCompare(&verifying), - .validate_lead => try stateValidateLead(&verifying), - .done => unreachable, - }; - } -} - -// ── States ──────────────────────────────────────────────────────────────────── -fn stateCheckFile(machine: *VerifyingMachine) BackendError!VerifyingState { - const package_path = std.mem.span(machine.backend.data.package_path_c); - - std.Io.Dir.accessAbsolute(machine.backend.io, package_path, .{}) catch return machine.stateFailed(BackendError.ReadFailed); - - return .check_temp_dir; -} - -fn stateCheckTempDir(machine: *VerifyingMachine) BackendError!VerifyingState { - const temp_path = std.mem.span(machine.backend.data.temp_path_c); - - std.Io.Dir.accessAbsolute(machine.backend.io, temp_path, .{}) catch return machine.stateFailed(BackendError.TempDirFailed); - - return .hash; -} - -fn stateHash(machine: *VerifyingMachine) BackendError!VerifyingState { - var package_reader_buf: [65536]u8 = undefined; - var package_hasher = std.crypto.hash.sha2.Sha256.init(.{}); - - const package_path = std.mem.span(machine.backend.data.package_path_c); - - const file = std.Io.Dir.openFileAbsolute(machine.backend.io, package_path, .{}) catch return machine.stateFailed(BackendError.ReadFailed); - machine.file = file; - - var package_read_bufs_vector = [1][]u8{package_reader_buf[0..]}; - while (true) { - const bytes_read = file.readStreaming(machine.backend.io, &package_read_bufs_vector) catch |err| { - if (err == error.EndOfStream) break; - return machine.stateFailed(BackendError.ReadFailed); - }; - - if (bytes_read == 0) break; - - package_hasher.update(package_reader_buf[0..bytes_read]); - } - - package_hasher.final(&machine.digest_checksum); - - return .compare; -} - -fn stateCompare(machine: *VerifyingMachine) BackendError!VerifyingState { - var checksum_as_bytes: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined; - - _ = std.fmt.hexToBytes(&checksum_as_bytes, machine.backend.data.checksum) catch return machine.stateFailed(BackendError.InvalidPackage); - - if (!std.mem.eql(u8, &machine.digest_checksum, &checksum_as_bytes)) return machine.stateFailed(BackendError.ChecksumMismatch); - - if (machine.file) |file| { - file.close(machine.backend.io); - machine.file = null; - } - - return .validate_lead; -} - -fn stateValidateLead(machine: *VerifyingMachine) BackendError!VerifyingState { - const package_path = std.mem.span(machine.backend.data.package_path_c); - - const file = std.Io.Dir.openFileAbsolute(machine.backend.io, package_path, .{}) catch return machine.stateFailed(BackendError.ReadFailed); - defer file.close(machine.backend.io); - - var lead_buf: [rpm_lead_size]u8 = undefined; - var lead_iov = [1][]u8{lead_buf[0..]}; - const bytes_read = file.readStreaming(machine.backend.io, &lead_iov) catch return machine.stateFailed(BackendError.ReadFailed); - - if (bytes_read < rpm_lead_size) return machine.stateFailed(BackendError.InvalidPackage); - if (!std.mem.eql(u8, lead_buf[0..4], &rpm_lead_magic)) return machine.stateFailed(BackendError.InvalidPackage); - if (lead_buf[4] < 3) return machine.stateFailed(BackendError.InvalidPackage); - - return .done; -} diff --git a/decoders/rpm/src/error.rs b/decoders/rpm/src/error.rs new file mode 100644 index 0000000..1aa4850 --- /dev/null +++ b/decoders/rpm/src/error.rs @@ -0,0 +1,40 @@ +// 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; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecodeError { + InvalidRequest, + Io(IoErrorKind), + ChecksumMismatch, + UnsupportedFormat, + MissingHeader, + MalformedHeader, + 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::MissingHeader => -5, + DecodeError::MalformedHeader => -6, + DecodeError::InvalidUtf8 => -7, + DecodeError::Cancelled => -8, + } + } +} diff --git a/decoders/rpm/src/extract.rs b/decoders/rpm/src/extract.rs new file mode 100644 index 0000000..7de4a7a --- /dev/null +++ b/decoders/rpm/src/extract.rs @@ -0,0 +1,106 @@ +// 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::{self, File}; +use std::io::Read; +use std::os::unix::fs::symlink; +use std::path::{Component, Path}; + +use cpio::newc::Reader as CpioReader; +use flate2::read::GzDecoder; +use xz2::read::XzDecoder; +use zstd::stream::read::Decoder as ZstdDecoder; + +use upac_abi::hook::CancelToken; + +use crate::error::DecodeError; +use crate::header::Header; +use crate::rpm::{PAYLOAD_COMPRESSOR_TAG, PAYLOAD_FORMAT_TAG}; + +const MODE_TYPE_MASK: u32 = 0o170000; +const MODE_TYPE_DIRECTORY: u32 = 0o040000; +const MODE_TYPE_REGULAR: u32 = 0o100000; +const MODE_TYPE_SYMLINK: u32 = 0o120000; + +pub fn extract(file: File, header: &Header, output_dir: &str, cancel: &CancelToken) -> Result<(), DecodeError> { + let format = header.string(PAYLOAD_FORMAT_TAG)?.unwrap_or_else(|| "cpio".to_owned()); + if format != "cpio" { + return Err(DecodeError::UnsupportedFormat); + } + + let compressor = header + .string(PAYLOAD_COMPRESSOR_TAG)? + .unwrap_or_else(|| "gzip".to_owned()); + + let mut reader = open_decompressor(&compressor, file)?; + + loop { + if cancel.is_cancelled() { + return Err(DecodeError::Cancelled); + } + + let mut entry_reader = CpioReader::new(reader)?; + let entry = entry_reader.entry().clone(); + + if entry.is_trailer() { + break; + } + + let relative_path = entry.name().trim_start_matches("./"); + if relative_path.is_empty() { + reader = entry_reader.finish()?; + continue; + } + + if Path::new(relative_path) + .components() + .any(|component| matches!(component, Component::ParentDir)) + { + return Err(DecodeError::MalformedHeader); + } + + let target_path = Path::new(output_dir).join(relative_path); + + reader = match entry.mode() & MODE_TYPE_MASK { + MODE_TYPE_DIRECTORY => { + fs::create_dir_all(&target_path)?; + entry_reader.finish()? + } + MODE_TYPE_SYMLINK => { + let mut link_target = Vec::new(); + entry_reader.read_to_end(&mut link_target)?; + let link_target = String::from_utf8(link_target).map_err(|_| DecodeError::InvalidUtf8)?; + + if let Some(parent) = target_path.parent() { + fs::create_dir_all(parent)?; + } + symlink(link_target, &target_path)?; + + entry_reader.finish()? + } + MODE_TYPE_REGULAR => { + if let Some(parent) = target_path.parent() { + fs::create_dir_all(parent)?; + } + + let mut out = File::create(&target_path)?; + entry_reader.to_writer(&mut out)? + } + _ => entry_reader.finish()?, + }; + } + + Ok(()) +} + +fn open_decompressor(compressor: &str, file: File) -> Result, DecodeError> { + match compressor { + "gzip" => Ok(Box::new(GzDecoder::new(file))), + "xz" => Ok(Box::new(XzDecoder::new(file))), + "zstd" => Ok(Box::new(ZstdDecoder::new(file)?)), + "none" => Ok(Box::new(file)), + _ => Err(DecodeError::UnsupportedFormat), + } +} diff --git a/decoders/rpm/src/ffi.zig b/decoders/rpm/src/ffi.zig deleted file mode 100644 index f42f1ad..0000000 --- a/decoders/rpm/src/ffi.zig +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -pub const std = @import("std"); - -const types = @import("upac-backend-types"); -const BackendError = types.BackendError; -const HookFn = types.HookFn; -const CancelToken = types.CancelToken; - -pub const ABI_VERSION: u32 = 2; - -// ── FFI types ───────────────────────────────────────────────────────────────── -pub const CSlice = extern struct { - ptr: [*c]const u8, - len: usize, - - pub fn toSlice(self: CSlice) []const u8 { - const not_null_ptr = self.ptr orelse return ""; - return not_null_ptr[0..self.len]; - } - - pub fn asZ(self: CSlice) [*c]const u8 { - return self.ptr; - } - - pub fn fromSlice(slice: ?[]const u8) CSlice { - const not_null_slice = slice orelse return .{ .ptr = null, .len = 0 }; - return .{ .ptr = @ptrCast(not_null_slice.ptr), .len = not_null_slice.len }; - } - - pub fn validate(self: CSlice) !void { - if (self.ptr == null) return error.InvalidEntry; - if (self.ptr[self.len] != 0) return error.InvalidEntry; - if (std.mem.len(self.ptr) != self.len) return error.InvalidEntry; - } -}; - -pub const CVersionParts = extern struct { - ptr: [*]u32, - len: usize, - - pub fn toSlice(self: CVersionParts) []u32 { - return self.ptr[0..self.len]; - } -}; - -pub const CVersion = extern struct { - struct_size: usize = @sizeOf(CVersion), - epoch: u32, - release: u32, - parts: CVersionParts, - pre: CSlice, - - pub fn deinit(self: CVersion, allocator: std.mem.Allocator) void { - allocator.free(self.parts.toSlice()); - if (self.pre.ptr != null) allocator.free(self.pre.toSlice()); - } -}; - -pub const CPackageMeta = extern struct { - struct_size: usize = @sizeOf(CPackageMeta), - - name: CSlice, - version: CVersion, - arch: CSlice, - arch_sub: CSlice, - maintainer: CSlice, - description: CSlice, - license: CSlice, - url: CSlice, - sha256: [32]u8, - installed_size: u64 = 0, - - pub fn free(self: *CPackageMeta, allocator: std.mem.Allocator) void { - inline for (std.meta.fields(CPackageMeta)) |field| { - if (field.type == CSlice) { - const slice = @field(self, field.name); - if (slice.ptr != null) allocator.free(slice.toSlice()); - } - } - self.version.deinit(allocator); - allocator.destroy(self); - } -}; - -pub const CPrepareRequest = extern struct { - struct_size: usize = @sizeOf(CPrepareRequest), - checksum: CSlice, - - package_path: CSlice, - temp_dir: CSlice, - - on_hook: ?*const HookFn = null, - hook_ctx: ?*anyopaque = null, - - cancel_token: ?*const CancelToken = null, - - pub fn validate(req: CPrepareRequest) !void { - if (req.struct_size != @sizeOf(CPrepareRequest)) return error.AbiMismatch; - try req.package_path.validate(); - try req.temp_dir.validate(); - try req.checksum.validate(); - } -}; - -pub fn dupeToCSlice(allocator: std.mem.Allocator, slice: []const u8) BackendError!CSlice { - const duped = allocator.dupeZ(u8, slice) catch return BackendError.AllocZFailed; - return CSlice.fromSlice(duped); -} - -pub fn dupeRequiredToCSlice(allocator: std.mem.Allocator, slice: []const u8) BackendError!CSlice { - if (slice.len == 0) return BackendError.InvalidPackage; - return dupeToCSlice(allocator, slice); -} diff --git a/decoders/rpm/src/header.rs b/decoders/rpm/src/header.rs new file mode 100644 index 0000000..e251b79 --- /dev/null +++ b/decoders/rpm/src/header.rs @@ -0,0 +1,168 @@ +// 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::{Read, Seek, SeekFrom}; + +use crate::error::DecodeError; +use crate::rpm::LEAD_SIZE; + +const LEAD_MAGIC: [u8; 4] = [0xED, 0xAB, 0xEE, 0xDB]; +const SECTION_MAGIC: [u8; 3] = [0x8E, 0xAD, 0xE8]; + +struct SectionHeader { + tag_count: u32, + data_size: u32, +} + +struct TagEntry { + tag: u32, + offset: u32, + count: u32, +} + +pub struct Header { + entries: Vec, + data: Vec, +} + +pub fn read(reader: &mut R) -> Result { + skip_lead(reader)?; + skip_signature(reader)?; + read_main_header(reader) +} + +impl Header { + pub fn string(&self, tag: u32) -> Result, DecodeError> { + let Some(entry) = self.find(tag) else { return Ok(None) }; + + read_string_at(&self.data, entry.offset as usize).map(Some) + } + + pub fn string_array(&self, tag: u32) -> Result, DecodeError> { + let Some(entry) = self.find(tag) else { + return Ok(Vec::new()); + }; + + let mut values = Vec::with_capacity(entry.count as usize); + let mut cursor = entry.offset as usize; + for _ in 0..entry.count { + let value = read_string_at(&self.data, cursor)?; + cursor += value.len() + 1; + values.push(value); + } + + Ok(values) + } + + pub fn int32(&self, tag: u32) -> Result, DecodeError> { + let Some(entry) = self.find(tag) else { return Ok(None) }; + + read_i32_at(&self.data, entry.offset as usize).map(Some) + } + + pub fn int32_array(&self, tag: u32) -> Result, DecodeError> { + let Some(entry) = self.find(tag) else { + return Ok(Vec::new()); + }; + + (0..entry.count as usize) + .map(|index| read_i32_at(&self.data, entry.offset as usize + index * 4)) + .collect() + } + + pub fn contains(&self, tag: u32) -> bool { + self.find(tag).is_some() + } + + fn find(&self, tag: u32) -> Option<&TagEntry> { + self.entries.iter().find(|entry| entry.tag == tag) + } +} + +fn skip_lead(reader: &mut R) -> Result<(), DecodeError> { + let mut magic = [0u8; 4]; + reader + .read_exact(&mut magic) + .map_err(|_| DecodeError::UnsupportedFormat)?; + + if magic != LEAD_MAGIC { + return Err(DecodeError::UnsupportedFormat); + } + + reader.seek(SeekFrom::Start(u64::from(LEAD_SIZE)))?; + + Ok(()) +} + +fn skip_signature(reader: &mut R) -> Result<(), DecodeError> { + let header = read_section_header(reader)?; + + let total_size = u64::from(header.tag_count) * 16 + u64::from(header.data_size); + reader.seek(SeekFrom::Current(total_size as i64))?; + + let remainder = total_size % 8; + if remainder != 0 { + reader.seek(SeekFrom::Current((8 - remainder) as i64))?; + } + + Ok(()) +} + +fn read_main_header(reader: &mut R) -> Result { + let header = read_section_header(reader)?; + + let mut index_bytes = vec![0u8; header.tag_count as usize * 16]; + reader + .read_exact(&mut index_bytes) + .map_err(|_| DecodeError::MalformedHeader)?; + + let mut data = vec![0u8; header.data_size as usize]; + reader.read_exact(&mut data).map_err(|_| DecodeError::MalformedHeader)?; + + let entries = index_bytes + .as_chunks::<16>() + .0 + .iter() + .map(|chunk| TagEntry { + tag: u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]), + offset: u32::from_be_bytes([chunk[8], chunk[9], chunk[10], chunk[11]]), + count: u32::from_be_bytes([chunk[12], chunk[13], chunk[14], chunk[15]]), + }) + .collect(); + + Ok(Header { entries, data }) +} + +fn read_section_header(reader: &mut R) -> Result { + let mut buffer = [0u8; 16]; + reader + .read_exact(&mut buffer) + .map_err(|_| DecodeError::MalformedHeader)?; + + if buffer[0..3] != SECTION_MAGIC { + return Err(DecodeError::MalformedHeader); + } + + Ok(SectionHeader { + tag_count: u32::from_be_bytes([buffer[8], buffer[9], buffer[10], buffer[11]]), + data_size: u32::from_be_bytes([buffer[12], buffer[13], buffer[14], buffer[15]]), + }) +} + +fn read_string_at(data: &[u8], offset: usize) -> Result { + let slice = data.get(offset..).ok_or(DecodeError::MalformedHeader)?; + let end = slice + .iter() + .position(|&byte| byte == 0) + .ok_or(DecodeError::MalformedHeader)?; + + String::from_utf8(slice[..end].to_vec()).map_err(|_| DecodeError::InvalidUtf8) +} + +fn read_i32_at(data: &[u8], offset: usize) -> Result { + let bytes = data.get(offset..offset + 4).ok_or(DecodeError::MalformedHeader)?; + + Ok(i32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])) +} diff --git a/decoders/rpm/src/imports.h b/decoders/rpm/src/imports.h deleted file mode 100644 index 85797f3..0000000 --- a/decoders/rpm/src/imports.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 JustPav - * SPDX-FileCopyrightText: 2026 SmoothTeam - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ - -#include -#include diff --git a/decoders/rpm/src/lib.rs b/decoders/rpm/src/lib.rs new file mode 100644 index 0000000..3f419b7 --- /dev/null +++ b/decoders/rpm/src/lib.rs @@ -0,0 +1,116 @@ +// 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::str::from_utf8; + +use upac_abi::ABI_VERSION; +use upac_abi::decoder::{CDecodeRequest, CDecodeResponse, CDependency}; +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 crate::error::DecodeError; +use crate::meta::Meta; + +pub mod error; +pub mod header; +pub mod meta; +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 +} + +/// # Safety +/// `request`, if non-null, must point to a valid, initialized `CDecodeRequest` for the duration +/// of the call. `response_out`, if non-null, must point to writable, uninitialized +/// `CDecodeResponse` storage that this function fully initializes on success. +#[cfg_attr(feature = "cdylib", unsafe(no_mangle))] +pub unsafe extern "C" fn decode(request: *const CDecodeRequest, response_out: *mut CDecodeResponse) -> i32 { + if request.is_null() || response_out.is_null() { + return DecodeError::InvalidRequest.code(); + } + + match decode_package(unsafe { &*request }) { + Ok(response) => { + unsafe { response_out.write(response) }; + 0 + } + Err(error) => error.code(), + } +} + +/// # Safety +/// `response`, if non-null, must point to a `CDecodeResponse` produced by this crate's own +/// `decode`, not yet freed. +unsafe extern "C" fn free_decode_response(response: *mut CDecodeResponse) { + if response.is_null() { + return; + } + + let response = unsafe { &*response }; + + unsafe { + response.meta.free(); + + free_cvec_owning(&response.dependencies, |dependency| { + free_cslice(&dependency.name); + dependency.version.free(); + }); + + free_cvec_owning(&response.declarative_triggers, |slice| free_cslice(slice)); + } +} + +fn decode_package(request: &CDecodeRequest) -> Result { + let package_path = + from_utf8(unsafe { request.package_path.as_slice() }).map_err(|_| DecodeError::InvalidRequest)?; + let output_dir = from_utf8(unsafe { request.output_dir.as_slice() }).map_err(|_| DecodeError::InvalidRequest)?; + let cancel = unsafe { request.cancel_token.as_ref() }.ok_or(DecodeError::InvalidRequest)?; + + verify::verify(package_path, request.checksum, cancel)?; + + let mut file = File::open(package_path)?; + let header = header::read(&mut file)?; + + extract::extract(file, &header, output_dir, cancel)?; + + let declarative_triggers = triggers::scan(&header); + let built = meta::build(&header, request.checksum)?; + + Ok(build_response(built, declarative_triggers)) +} + +fn build_response(built: Meta, declarative_triggers: Vec) -> CDecodeResponse { + let Meta { meta, dependencies } = built; + + 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, + } +} diff --git a/decoders/rpm/src/meta.rs b/decoders/rpm/src/meta.rs new file mode 100644 index 0000000..441cb41 --- /dev/null +++ b/decoders/rpm/src/meta.rs @@ -0,0 +1,95 @@ +// 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::decoder::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS}; +use upac_types::{Dependency, PackageMeta, Version}; + +use crate::error::DecodeError; +use crate::header::Header; +use crate::rpm::{ + ARCH_TAG, LICENSE_TAG, NAME_TAG, PACKAGER_TAG, RELEASE_TAG, REQUIRE_FLAGS_TAG, REQUIRE_NAME_TAG, + REQUIRE_VERSION_TAG, SIZE_TAG, SUMMARY_TAG, URL_TAG, VERSION_TAG, +}; + +const SENSE_LESS: i32 = 0x02; +const SENSE_GREATER: i32 = 0x04; +const SENSE_EQUAL: i32 = 0x08; +const SENSE_RPMLIB: i32 = 0x0100_0000; + +#[derive(Debug)] +pub struct Meta { + pub meta: PackageMeta, + pub dependencies: Vec, +} + +pub fn build(header: &Header, sha256: [u8; 32]) -> Result { + let name = header.string(NAME_TAG)?.ok_or(DecodeError::MalformedHeader)?; + let version = header.string(VERSION_TAG)?.ok_or(DecodeError::MalformedHeader)?; + + let raw_version = match header.string(RELEASE_TAG)? { + Some(release) => format!("{version}-{release}"), + None => version, + }; + + let arch = header.string(ARCH_TAG)?.ok_or(DecodeError::MalformedHeader)?; + let installed_size = header.int32(SIZE_TAG)?.unwrap_or(0).max(0) as u64; + + let meta = PackageMeta { + name, + version: Version::parse(&raw_version), + arch, + arch_sub: None, + maintainer: header.string(PACKAGER_TAG)?.unwrap_or_default(), + description: header.string(SUMMARY_TAG)?.unwrap_or_default(), + license: header.string(LICENSE_TAG)?, + url: header.string(URL_TAG)?, + sha256, + installed_size, + }; + + Ok(Meta { + meta, + dependencies: parse_dependencies(header)?, + }) +} + +fn parse_dependencies(header: &Header) -> Result, DecodeError> { + let names = header.string_array(REQUIRE_NAME_TAG)?; + let versions = header.string_array(REQUIRE_VERSION_TAG)?; + let flags = header.int32_array(REQUIRE_FLAGS_TAG)?; + + let mut dependencies = Vec::with_capacity(names.len()); + for (index, name) in names.into_iter().enumerate() { + let flag = flags.get(index).copied().unwrap_or(0); + if flag & SENSE_RPMLIB != 0 { + continue; + } + + let raw_version = versions.get(index).cloned().unwrap_or_default(); + + dependencies.push(Dependency { + name, + constraint: sense_to_constraint(flag), + version: Version::parse(&raw_version), + }); + } + + Ok(dependencies) +} + +fn sense_to_constraint(flag: i32) -> u8 { + let mut constraint = 0; + if flag & SENSE_LESS != 0 { + constraint |= CONSTRAINT_LESS; + } + if flag & SENSE_GREATER != 0 { + constraint |= CONSTRAINT_GREATER; + } + if flag & SENSE_EQUAL != 0 { + constraint |= CONSTRAINT_EQUAL; + } + + if constraint == 0 { CONSTRAINT_ANY } else { constraint } +} diff --git a/decoders/rpm/src/triggers.rs b/decoders/rpm/src/triggers.rs new file mode 100644 index 0000000..6b39233 --- /dev/null +++ b/decoders/rpm/src/triggers.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_types::DecoderTrigger; + +use crate::header::Header; +use crate::rpm::{POSTIN_NAME, POSTIN_TAG, POSTUN_NAME, POSTUN_TAG, PREIN_NAME, PREIN_TAG, PREUN_NAME, PREUN_TAG}; + +pub fn scan(header: &Header) -> Vec { + let mut names: Vec = Vec::new(); + + for trigger in DecoderTrigger::ALL { + let (tag, name) = native(trigger); + let already_added = names.iter().any(|existing| existing == name); + + if header.contains(tag) && !already_added { + names.push(name.to_owned()); + } + } + + names +} + +fn native(trigger: DecoderTrigger) -> (u32, &'static str) { + match trigger { + DecoderTrigger::PreInstall | DecoderTrigger::PreUpgrade => (PREIN_TAG, PREIN_NAME), + DecoderTrigger::PostInstall | DecoderTrigger::PostUpgrade => (POSTIN_TAG, POSTIN_NAME), + DecoderTrigger::PreRemove => (PREUN_TAG, PREUN_NAME), + DecoderTrigger::PostRemove => (POSTUN_TAG, POSTUN_NAME), + } +} diff --git a/decoders/rpm/src/types/errors.zig b/decoders/rpm/src/types/errors.zig deleted file mode 100644 index 8acb8f6..0000000 --- a/decoders/rpm/src/types/errors.zig +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -pub const BackendErrorCode = enum(i32) { - ok = 0, - checksum_mismatch = 1, - extraction_failed = 2, - metadata_not_found = 3, - invalid_package = 4, - archive_open_failed = 5, - archive_read_failed = 6, - archive_extract_failed = 7, - temp_dir_failed = 8, - alloc_failed = 9, - cancelled = 10, - read_failed = 11, - invalid_entry = 12, - abi_mismatch = 13, - unexpected = 99, -}; - -pub const BackendError = error{ - ChecksumMismatch, - ExtractionFailed, - MetadataNotFound, - InvalidPackage, - ReadFailed, - ArchiveOpenFailed, - ArchiveReadFailed, - ArchiveExtractFailed, - OutOfMemory, - TempDirFailed, - AllocZFailed, - Cancelled, -}; - -pub fn fromError(err: anyerror) BackendErrorCode { - return switch (err) { - BackendError.ChecksumMismatch => .checksum_mismatch, - BackendError.ExtractionFailed => .extraction_failed, - BackendError.MetadataNotFound => .metadata_not_found, - BackendError.InvalidPackage => .invalid_package, - BackendError.ArchiveOpenFailed => .archive_open_failed, - BackendError.ArchiveReadFailed => .archive_read_failed, - BackendError.ArchiveExtractFailed => .archive_extract_failed, - BackendError.TempDirFailed => .temp_dir_failed, - BackendError.AllocZFailed, BackendError.OutOfMemory => .alloc_failed, - BackendError.Cancelled => .cancelled, - BackendError.ReadFailed => .read_failed, - error.InvalidEntry => .invalid_entry, - error.AbiMismatch => .abi_mismatch, - else => .unexpected, - }; -} diff --git a/decoders/rpm/src/types/types.zig b/decoders/rpm/src/types/types.zig deleted file mode 100644 index 6f49230..0000000 --- a/decoders/rpm/src/types/types.zig +++ /dev/null @@ -1,137 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -pub const std = @import("std"); - -const errors = @import("errors.zig"); -pub const BackendErrorCode = errors.BackendErrorCode; -pub const BackendError = errors.BackendError; -pub const fromError = errors.fromError; - -pub const rpm_lead_magic = [4]u8{ 0xED, 0xAB, 0xEE, 0xDB }; -pub const rpm_lead_size = 96; - -// ── StateId ─────────────────────────────────────────────────────────────────── -pub const StateId = enum(u8) { - verifying = 0, - reading_meta = 1, - extracting = 2, - special_step = 3, - - done = 4, -}; - -// ── RpmTag ──────────────────────────────────────────────────────────────────── -pub const RpmTag = enum(u32) { - name = 1000, - version = 1001, - release = 1002, - summary = 1004, - license = 1014, - packager = 1015, - url = 1020, - arch = 1022, - size = 1023, - _, -}; - -// ── Version ───────────────────────────────────────────────────────────────── -pub const Version = struct { - epoch: u32 = 0, - parts: []const u32, - pre: ?[]const u8 = null, - release: u32 = 1, - - pub fn deinit(self: *const Version, allocator: std.mem.Allocator) void { - allocator.free(self.parts); - if (self.pre) |pre| allocator.free(pre); - } -}; - -// ── Hook ────────────────────────────────────────────────────────────────────── -pub const HookResponse = enum(u8) { - proceed = 0, - cancel = 1, -}; - -pub const HookFn = fn (event: u32, data: ?*const anyopaque, ctx: ?*anyopaque) callconv(.c) HookResponse; - -// ── CancelToken ─────────────────────────────────────────────────────────────── -pub const CancelToken = extern struct { - _flag: u8, - _hook: ?*const fn (ctx: ?*anyopaque) callconv(.c) void = null, - _hook_ctx: ?*anyopaque = null, - - pub fn isCancelled(self: *const CancelToken) bool { - return @atomicLoad(u8, &self._flag, .acquire) != 0; - } -}; - -// ── PrepareData ─────────────────────────────────────────────────────────────── -pub const PrepareData = struct { - package_path_c: [*:0]const u8, - temp_path_c: [*:0]const u8, - checksum: []const u8, - on_hook: ?*const HookFn = null, - hook_ctx: ?*anyopaque = null, - cancel_token: *const CancelToken, -}; - -// ── RawMeta ─────────────────────────────────────────────────────────────────── -pub const RawMeta = struct { - name: ?[]const u8 = null, - version: ?[]const u8 = null, - release: ?[]const u8 = null, - arch: ?[]const u8 = null, - summary: ?[]const u8 = null, - license: ?[]const u8 = null, - url: ?[]const u8 = null, - packager: ?[]const u8 = null, - size: u32 = 0, - - pub fn deinit(self: *RawMeta, allocator: std.mem.Allocator) void { - if (self.name) |value| allocator.free(value); - if (self.version) |value| allocator.free(value); - if (self.release) |value| allocator.free(value); - if (self.arch) |value| allocator.free(value); - if (self.summary) |value| allocator.free(value); - if (self.license) |value| allocator.free(value); - if (self.url) |value| allocator.free(value); - if (self.packager) |value| allocator.free(value); - } -}; - -// ── PackageMeta ─────────────────────────────────────────────────────────────── -pub const PackageMeta = struct { - name: []const u8, - version: Version, - arch: []const u8, - author: []const u8, - description: []const u8, - license: []const u8, - url: []const u8, - packager: []const u8, - checksum: [32]u8, - size: u32, - installed_at: i64, - - pub fn deinit(self: *PackageMeta, allocator: std.mem.Allocator) void { - allocator.free(self.name); - allocator.free(self.arch); - allocator.free(self.author); - allocator.free(self.description); - allocator.free(self.license); - allocator.free(self.url); - allocator.free(self.packager); - - self.version.deinit(allocator); - } -}; - -// ── PrepareResult ───────────────────────────────────────────────────────────── -pub const PrepareResult = struct { - meta: PackageMeta, - temp_path: [:0]const u8, -}; diff --git a/decoders/rpm/src/verify.rs b/decoders/rpm/src/verify.rs new file mode 100644 index 0000000..704d458 --- /dev/null +++ b/decoders/rpm/src/verify.rs @@ -0,0 +1,42 @@ +// 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::hook::CancelToken; + +use crate::error::DecodeError; + +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/rpm/tests/meta.rs b/decoders/rpm/tests/meta.rs new file mode 100644 index 0000000..1653bb1 --- /dev/null +++ b/decoders/rpm/tests/meta.rs @@ -0,0 +1,233 @@ +// 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::Cursor; + +use upac_abi::decoder::{CONSTRAINT_ANY, CONSTRAINT_EQUAL, CONSTRAINT_GREATER, CONSTRAINT_LESS}; +use upac_decoder_rpm::error::DecodeError; +use upac_decoder_rpm::header::{self, Header}; +use upac_decoder_rpm::meta; +use upac_decoder_rpm::rpm::{ + ARCH_TAG, LICENSE_TAG, NAME_TAG, PACKAGER_TAG, RELEASE_TAG, REQUIRE_FLAGS_TAG, REQUIRE_NAME_TAG, + REQUIRE_VERSION_TAG, SIZE_TAG, SUMMARY_TAG, URL_TAG, VERSION_TAG, +}; + +const CHECKSUM: [u8; 32] = [7; 32]; + +enum RawValue<'a> { + Str(&'a str), + StrArray(&'a [&'a str]), + I32(i32), + I32Array(&'a [i32]), +} + +fn build_header(entries: &[(u32, RawValue)]) -> Header { + let mut index_bytes = Vec::new(); + let mut data_block: Vec = Vec::new(); + + for (tag, value) in entries { + let offset = data_block.len() as u32; + let count: u32 = match value { + RawValue::Str(text) => { + data_block.extend_from_slice(text.as_bytes()); + data_block.push(0); + 1 + } + RawValue::StrArray(items) => { + for item in *items { + data_block.extend_from_slice(item.as_bytes()); + data_block.push(0); + } + items.len() as u32 + } + RawValue::I32(value) => { + data_block.extend_from_slice(&value.to_be_bytes()); + 1 + } + RawValue::I32Array(items) => { + for item in *items { + data_block.extend_from_slice(&item.to_be_bytes()); + } + items.len() as u32 + } + }; + + index_bytes.extend_from_slice(&tag.to_be_bytes()); + index_bytes.extend_from_slice(&0u32.to_be_bytes()); + index_bytes.extend_from_slice(&offset.to_be_bytes()); + index_bytes.extend_from_slice(&count.to_be_bytes()); + } + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&[0xED, 0xAB, 0xEE, 0xDB]); + bytes.resize(96, 0); + + bytes.extend_from_slice(§ion_header(0, 0)); + bytes.extend_from_slice(§ion_header(entries.len() as u32, data_block.len() as u32)); + bytes.extend_from_slice(&index_bytes); + bytes.extend_from_slice(&data_block); + + let mut cursor = Cursor::new(bytes); + header::read(&mut cursor).unwrap() +} + +fn section_header(tag_count: u32, data_size: u32) -> [u8; 16] { + let mut section = [0u8; 16]; + section[0..3].copy_from_slice(&[0x8E, 0xAD, 0xE8]); + section[3] = 0x01; + section[8..12].copy_from_slice(&tag_count.to_be_bytes()); + section[12..16].copy_from_slice(&data_size.to_be_bytes()); + section +} + +#[test] +fn parses_minimal_meta_with_defaults() { + let header = build_header(&[ + (NAME_TAG, RawValue::Str("foo")), + (VERSION_TAG, RawValue::Str("1.2.3")), + (ARCH_TAG, RawValue::Str("x86_64")), + ]); + + let built = meta::build(&header, CHECKSUM).unwrap(); + + assert_eq!(built.meta.name, "foo"); + assert_eq!(built.meta.version.raw, "1.2.3"); + assert_eq!(built.meta.version.epoch, 0); + assert_eq!(built.meta.arch, "x86_64"); + assert_eq!(built.meta.maintainer, ""); + assert_eq!(built.meta.description, ""); + assert_eq!(built.meta.license, None); + assert_eq!(built.meta.url, None); + assert_eq!(built.meta.installed_size, 0); + assert_eq!(built.meta.sha256, CHECKSUM); + assert!(built.dependencies.is_empty()); +} + +#[test] +fn release_is_joined_into_raw_version() { + let header = build_header(&[ + (NAME_TAG, RawValue::Str("foo")), + (VERSION_TAG, RawValue::Str("1.2.3")), + (RELEASE_TAG, RawValue::Str("2.fc40")), + (ARCH_TAG, RawValue::Str("x86_64")), + ]); + + let built = meta::build(&header, CHECKSUM).unwrap(); + + assert_eq!(built.meta.version.raw, "1.2.3-2.fc40"); +} + +#[test] +fn epoch_prefix_is_parsed_out_of_the_version() { + let header = build_header(&[ + (NAME_TAG, RawValue::Str("foo")), + (VERSION_TAG, RawValue::Str("2:1.2.3")), + (ARCH_TAG, RawValue::Str("x86_64")), + ]); + + let built = meta::build(&header, CHECKSUM).unwrap(); + + assert_eq!(built.meta.version.epoch, 2); + assert_eq!(built.meta.version.raw, "1.2.3"); +} + +#[test] +fn parses_all_optional_fields() { + let header = build_header(&[ + (NAME_TAG, RawValue::Str("foo")), + (VERSION_TAG, RawValue::Str("1.2.3")), + (RELEASE_TAG, RawValue::Str("1")), + (ARCH_TAG, RawValue::Str("x86_64")), + (SUMMARY_TAG, RawValue::Str("A test package")), + (LICENSE_TAG, RawValue::Str("MIT")), + (PACKAGER_TAG, RawValue::Str("Jane ")), + (URL_TAG, RawValue::Str("https://example.com")), + (SIZE_TAG, RawValue::I32(4096)), + ]); + + let built = meta::build(&header, CHECKSUM).unwrap(); + + assert_eq!(built.meta.description, "A test package"); + assert_eq!(built.meta.license, Some("MIT".to_owned())); + assert_eq!(built.meta.maintainer, "Jane "); + assert_eq!(built.meta.url, Some("https://example.com".to_owned())); + assert_eq!(built.meta.installed_size, 4096); +} + +#[test] +fn missing_name_is_malformed() { + let header = build_header(&[ + (VERSION_TAG, RawValue::Str("1.2.3")), + (ARCH_TAG, RawValue::Str("x86_64")), + ]); + + let result = meta::build(&header, CHECKSUM); + + assert_eq!(result.unwrap_err(), DecodeError::MalformedHeader); +} + +#[test] +fn missing_version_is_malformed() { + let header = build_header(&[(NAME_TAG, RawValue::Str("foo")), (ARCH_TAG, RawValue::Str("x86_64"))]); + + let result = meta::build(&header, CHECKSUM); + + assert_eq!(result.unwrap_err(), DecodeError::MalformedHeader); +} + +#[test] +fn missing_arch_is_malformed() { + let header = build_header(&[(NAME_TAG, RawValue::Str("foo")), (VERSION_TAG, RawValue::Str("1.2.3"))]); + + let result = meta::build(&header, CHECKSUM); + + assert_eq!(result.unwrap_err(), DecodeError::MalformedHeader); +} + +#[test] +fn parses_dependencies_with_all_constraint_operators() { + let header = build_header(&[ + (NAME_TAG, RawValue::Str("foo")), + (VERSION_TAG, RawValue::Str("1.2.3")), + (ARCH_TAG, RawValue::Str("x86_64")), + ( + REQUIRE_NAME_TAG, + RawValue::StrArray(&["libc", "glibc", "bash", "coreutils", "sh"]), + ), + ( + REQUIRE_VERSION_TAG, + RawValue::StrArray(&["2.34", "2.34", "5.0", "", ""]), + ), + (REQUIRE_FLAGS_TAG, RawValue::I32Array(&[0x02, 0x0A, 0x04, 0x00, 0x08])), + ]); + + let built = meta::build(&header, CHECKSUM).unwrap(); + + assert_eq!(built.dependencies.len(), 5); + assert_eq!(built.dependencies[0].constraint, CONSTRAINT_LESS); + assert_eq!(built.dependencies[1].constraint, CONSTRAINT_LESS | CONSTRAINT_EQUAL); + assert_eq!(built.dependencies[2].constraint, CONSTRAINT_GREATER); + assert_eq!(built.dependencies[3].constraint, CONSTRAINT_ANY); + assert_eq!(built.dependencies[4].constraint, CONSTRAINT_EQUAL); + assert_eq!(built.dependencies[0].version.raw, "2.34"); + assert_eq!(built.dependencies[3].name, "coreutils"); +} + +#[test] +fn filters_out_rpmlib_internal_dependencies() { + let header = build_header(&[ + (NAME_TAG, RawValue::Str("foo")), + (VERSION_TAG, RawValue::Str("1.2.3")), + (ARCH_TAG, RawValue::Str("x86_64")), + (REQUIRE_NAME_TAG, RawValue::StrArray(&["rpmlib(PayloadIsXz)", "bash"])), + (REQUIRE_VERSION_TAG, RawValue::StrArray(&["4.14.3-1", ""])), + (REQUIRE_FLAGS_TAG, RawValue::I32Array(&[0x0100_0000 | 0x08, 0x00])), + ]); + + let built = meta::build(&header, CHECKSUM).unwrap(); + + assert_eq!(built.dependencies.len(), 1); + assert_eq!(built.dependencies[0].name, "bash"); +} diff --git a/decoders/rpm/tests/triggers.rs b/decoders/rpm/tests/triggers.rs new file mode 100644 index 0000000..bc87bc2 --- /dev/null +++ b/decoders/rpm/tests/triggers.rs @@ -0,0 +1,98 @@ +// 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::Cursor; + +use upac_decoder_rpm::header::{self, Header}; +use upac_decoder_rpm::rpm::{NAME_TAG, POSTIN_TAG, POSTUN_TAG, PREIN_TAG, PREUN_TAG}; +use upac_decoder_rpm::triggers; + +enum RawValue<'a> { + Str(&'a str), +} + +fn build_header(entries: &[(u32, RawValue)]) -> Header { + let mut index_bytes = Vec::new(); + let mut data_block: Vec = Vec::new(); + + for (tag, value) in entries { + let offset = data_block.len() as u32; + let count: u32 = match value { + RawValue::Str(text) => { + data_block.extend_from_slice(text.as_bytes()); + data_block.push(0); + 1 + } + }; + + index_bytes.extend_from_slice(&tag.to_be_bytes()); + index_bytes.extend_from_slice(&0u32.to_be_bytes()); + index_bytes.extend_from_slice(&offset.to_be_bytes()); + index_bytes.extend_from_slice(&count.to_be_bytes()); + } + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&[0xED, 0xAB, 0xEE, 0xDB]); + bytes.resize(96, 0); + + bytes.extend_from_slice(§ion_header(0, 0)); + bytes.extend_from_slice(§ion_header(entries.len() as u32, data_block.len() as u32)); + bytes.extend_from_slice(&index_bytes); + bytes.extend_from_slice(&data_block); + + let mut cursor = Cursor::new(bytes); + header::read(&mut cursor).unwrap() +} + +fn section_header(tag_count: u32, data_size: u32) -> [u8; 16] { + let mut section = [0u8; 16]; + section[0..3].copy_from_slice(&[0x8E, 0xAD, 0xE8]); + section[3] = 0x01; + section[8..12].copy_from_slice(&tag_count.to_be_bytes()); + section[12..16].copy_from_slice(&data_size.to_be_bytes()); + section +} + +#[test] +fn finds_no_triggers_when_no_scriptlets_are_present() { + let header = build_header(&[]); + + let triggers = triggers::scan(&header); + + assert!(triggers.is_empty()); +} + +#[test] +fn a_bare_prein_covers_both_install_and_upgrade_positions() { + let header = build_header(&[(PREIN_TAG, RawValue::Str("echo hi"))]); + + let triggers = triggers::scan(&header); + + assert_eq!(triggers, vec!["pre"]); +} + +#[test] +fn finds_all_four_scriptlets() { + let header = build_header(&[ + (PREIN_TAG, RawValue::Str("a")), + (POSTIN_TAG, RawValue::Str("b")), + (PREUN_TAG, RawValue::Str("c")), + (POSTUN_TAG, RawValue::Str("d")), + ]); + + let mut triggers = triggers::scan(&header); + triggers.sort(); + + assert_eq!(triggers, vec!["post", "postun", "pre", "preun"]); +} + +#[test] +fn ignores_tags_that_are_not_scriptlets() { + let header = build_header(&[(NAME_TAG, RawValue::Str("foo"))]); + + let triggers = triggers::scan(&header); + + assert!(triggers.is_empty()); +} diff --git a/decoders/rpm/upac-rpm.toml b/decoders/rpm/upac-rpm.toml index 7fb8a26..6fc301c 100644 --- a/decoders/rpm/upac-rpm.toml +++ b/decoders/rpm/upac-rpm.toml @@ -1,15 +1,14 @@ # SPDX-FileCopyrightText: 2026 JustPav # SPDX-FileCopyrightText: 2026 SmoothTeam # -# SPDX-License-Identifier: LGPL-3.0-or-later +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -# Declarative decoder manifest (doc §5.8/§6). Not installed by `zig build` — -# there is no packaging pipeline (PKGBUILD/spec/etc) in this repo yet; this -# file is the canonical source a future package build copies to -# /etc/upac.d/decoders/upac-rpm.toml. +# Declarative decoder manifest (doc §5.8/§6). Not installed by any packaging +# pipeline yet — this file is the canonical source a future package build +# copies to /etc/upac.d/decoders/upac-rpm.toml. format = "rpm" extensions = ["rpm"] -library = "/usr/lib/upac/decoders/libupac-rpm.so" +library = "/usr/lib/upac/decoders/libupac_decoder_rpm.so" # Registered in shared-mime-info (freedesktop.org.xml). mime = "application/x-rpm" diff --git a/decoders/xbps/build.zig b/decoders/xbps-zig/build.zig similarity index 97% rename from decoders/xbps/build.zig rename to decoders/xbps-zig/build.zig index 420a009..ca84311 100644 --- a/decoders/xbps/build.zig +++ b/decoders/xbps-zig/build.zig @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception // ── Imports ───────────────────────────────────────────────────────────────────── const std = @import("std"); diff --git a/decoders/xbps/build.zig.zon b/decoders/xbps-zig/build.zig.zon similarity index 79% rename from decoders/xbps/build.zig.zon rename to decoders/xbps-zig/build.zig.zon index 0addff2..7bae770 100644 --- a/decoders/xbps/build.zig.zon +++ b/decoders/xbps-zig/build.zig.zon @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception .{ .name = .upac_xbps, diff --git a/decoders/xbps/config/meta_fields.zon b/decoders/xbps-zig/config/meta_fields.zon similarity index 79% rename from decoders/xbps/config/meta_fields.zon rename to decoders/xbps-zig/config/meta_fields.zon index c724b25..3a21848 100644 --- a/decoders/xbps/config/meta_fields.zon +++ b/decoders/xbps-zig/config/meta_fields.zon @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception .{ .pkgname = "name", diff --git a/decoders/xbps/src/backend/backend.zig b/decoders/xbps-zig/src/backend/backend.zig similarity index 97% rename from decoders/xbps/src/backend/backend.zig rename to decoders/xbps-zig/src/backend/backend.zig index db87467..e607a95 100644 --- a/decoders/xbps/src/backend/backend.zig +++ b/decoders/xbps-zig/src/backend/backend.zig @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception // ── Imports ─────────────────────────────────────────────────────────────────── pub const std = @import("std"); diff --git a/decoders/xbps/src/backend/parsing/parsing.zig b/decoders/xbps-zig/src/backend/parsing/parsing.zig similarity index 99% rename from decoders/xbps/src/backend/parsing/parsing.zig rename to decoders/xbps-zig/src/backend/parsing/parsing.zig index ebe2a70..93480ae 100644 --- a/decoders/xbps/src/backend/parsing/parsing.zig +++ b/decoders/xbps-zig/src/backend/parsing/parsing.zig @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception // ── Imports ─────────────────────────────────────────────────────────────────── const std = @import("std"); diff --git a/decoders/xbps/src/backend/parsing/utils.zig b/decoders/xbps-zig/src/backend/parsing/utils.zig similarity index 97% rename from decoders/xbps/src/backend/parsing/utils.zig rename to decoders/xbps-zig/src/backend/parsing/utils.zig index 6147098..6476b12 100644 --- a/decoders/xbps/src/backend/parsing/utils.zig +++ b/decoders/xbps-zig/src/backend/parsing/utils.zig @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception // ── Imports ───────────────────────────────────────────────────────────────────── const std = @import("std"); diff --git a/decoders/xbps/src/backend/unpacking/unpacking.zig b/decoders/xbps-zig/src/backend/unpacking/unpacking.zig similarity index 99% rename from decoders/xbps/src/backend/unpacking/unpacking.zig rename to decoders/xbps-zig/src/backend/unpacking/unpacking.zig index dd872e5..9c0abb3 100644 --- a/decoders/xbps/src/backend/unpacking/unpacking.zig +++ b/decoders/xbps-zig/src/backend/unpacking/unpacking.zig @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception // ── Imports ─────────────────────────────────────────────────────────────────── const std = @import("std"); diff --git a/decoders/xbps/src/backend/verifying/verifying.zig b/decoders/xbps-zig/src/backend/verifying/verifying.zig similarity index 98% rename from decoders/xbps/src/backend/verifying/verifying.zig rename to decoders/xbps-zig/src/backend/verifying/verifying.zig index cb1be10..ceee646 100644 --- a/decoders/xbps/src/backend/verifying/verifying.zig +++ b/decoders/xbps-zig/src/backend/verifying/verifying.zig @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception // ── Imports ─────────────────────────────────────────────────────────────────── const std = backend.std; diff --git a/decoders/xbps/src/ffi.zig b/decoders/xbps-zig/src/ffi.zig similarity index 97% rename from decoders/xbps/src/ffi.zig rename to decoders/xbps-zig/src/ffi.zig index b558319..30a0a33 100644 --- a/decoders/xbps/src/ffi.zig +++ b/decoders/xbps-zig/src/ffi.zig @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception pub const std = @import("std"); diff --git a/decoders/alpm/src/imports.h b/decoders/xbps-zig/src/imports.h similarity index 64% rename from decoders/alpm/src/imports.h rename to decoders/xbps-zig/src/imports.h index 85797f3..64ed074 100644 --- a/decoders/alpm/src/imports.h +++ b/decoders/xbps-zig/src/imports.h @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: 2026 JustPav * SPDX-FileCopyrightText: 2026 SmoothTeam * - * SPDX-License-Identifier: LGPL-3.0-or-later + * SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception */ #include diff --git a/decoders/rpm/src/symbols.zig b/decoders/xbps-zig/src/symbols.zig similarity index 98% rename from decoders/rpm/src/symbols.zig rename to decoders/xbps-zig/src/symbols.zig index d159449..6aa89a3 100644 --- a/decoders/rpm/src/symbols.zig +++ b/decoders/xbps-zig/src/symbols.zig @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception // ── Imports ─────────────────────────────────────────────────────────────────── const std = @import("std"); diff --git a/decoders/deb/src/types/errors.zig b/decoders/xbps-zig/src/types/errors.zig similarity index 95% rename from decoders/deb/src/types/errors.zig rename to decoders/xbps-zig/src/types/errors.zig index 8acb8f6..75a60ff 100644 --- a/decoders/deb/src/types/errors.zig +++ b/decoders/xbps-zig/src/types/errors.zig @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception pub const BackendErrorCode = enum(i32) { ok = 0, diff --git a/decoders/xbps/src/types/types.zig b/decoders/xbps-zig/src/types/types.zig similarity index 98% rename from decoders/xbps/src/types/types.zig rename to decoders/xbps-zig/src/types/types.zig index 3be4592..17a176c 100644 --- a/decoders/xbps/src/types/types.zig +++ b/decoders/xbps-zig/src/types/types.zig @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception pub const std = @import("std"); diff --git a/decoders/xbps-zig/upac-xbps.toml b/decoders/xbps-zig/upac-xbps.toml new file mode 100644 index 0000000..74b6f12 --- /dev/null +++ b/decoders/xbps-zig/upac-xbps.toml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 JustPav +# SPDX-FileCopyrightText: 2026 SmoothTeam +# +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +# Declarative decoder manifest (doc §5.8/§6). Not installed by `zig build` — +# there is no packaging pipeline (PKGBUILD/spec/etc) in this repo yet; this +# file is the canonical source a future package build copies to +# /etc/upac.d/decoders/upac-xbps.toml. +format = "xbps" +extensions = ["xbps"] +library = "/usr/lib/upac/decoders/libupac-xbps.so" + +# No mime type for xbps packages is registered in shared-mime-info (verified +# against freedesktop.org.xml) — this is the same unofficial vendor-prefixed +# convention used elsewhere for formats without a registered type. +mime = "application/x-xbps-package" diff --git a/decoders/xbps/Cargo.toml b/decoders/xbps/Cargo.toml new file mode 100644 index 0000000..76b6d6f --- /dev/null +++ b/decoders/xbps/Cargo.toml @@ -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 + +[package] +name = "xbps" +description = "XBPS (.xbps) package decoder plugin for upac, implementing Void Linux's xbps package format" +version.workspace = true + +edition.workspace = true +rust-version.workspace = true + +license.workspace = true + +readme.workspace = true +homepage.workspace = true +repository.workspace = true + +keywords.workspace = true +categories.workspace = true + +[lints] +workspace = true + +[lib] +name = "upac_decoder_xbps" +crate-type = ["cdylib", "rlib"] + +[dependencies] +upac-abi = { workspace = true } +upac-types = { workspace = true } + +ar = { workspace = true } +flate2 = { workspace = true } +sha2 = { workspace = true } +tar = { workspace = true } +xz2 = { workspace = true } +zstd = { workspace = true } + +[build-dependencies] +toml = { workspace = true } + +[features] +default = ["cdylib"] +cdylib = [] diff --git a/decoders/xbps/build.rs b/decoders/xbps/build.rs new file mode 100644 index 0000000..d3fd8c0 --- /dev/null +++ b/decoders/xbps/build.rs @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::env::var; +use std::error::Error; +use std::fs::{read_to_string, write}; +use std::path::Path; + +use toml::{Value, from_str}; + +fn main() -> Result<(), Box> { + let manifest_dir = var("CARGO_MANIFEST_DIR")?; + + let mut generated = String::new(); + generated.push_str(&generate_decoder_toml(&manifest_dir)?); + generated.push_str(&generate_manifest_module(&manifest_dir)?); + + let out = Path::new(&var("OUT_DIR")?).join("layout.rs"); + write(out, generated)?; + + Ok(()) +} + +fn generate_decoder_toml(manifest_dir: &str) -> Result> { + let source = Path::new(manifest_dir).join("../decoder.toml"); + + println!("cargo:rerun-if-changed={}", source.display()); + + let raw = read_to_string(&source)?; + let config: Value = from_str(&raw)?; + + let section = "xbps"; + let entries = config + .get(section) + .and_then(Value::as_table) + .ok_or_else(|| format!("decoder.toml: [{section}] must be a table"))?; + + let mut generated = String::new(); + generated.push_str(&format!("pub mod {section} {{\n")); + + for (key, value) in entries { + let rendered = if let Some(value) = value.as_str() { + format!("&str = {value:?}") + } else if let Some(value) = value.as_integer() { + format!("u32 = {value}") + } else { + return Err(format!("decoder.toml: {section}.{key} must be a string or integer").into()); + }; + + generated.push_str(&format!(" pub const {}: {rendered};\n", key.to_uppercase())); + } + + generated.push_str("}\n"); + + Ok(generated) +} + +/// Compiles this crate's own deployable manifest (`format`/`extensions`) into constants, so a +/// `builtin-xbps` build can dispatch by format without reading `upac-xbps.toml` from disk at +/// runtime — `library`/`mime` are runtime-deployment-only fields, not needed here. +fn generate_manifest_module(manifest_dir: &str) -> Result> { + let source = Path::new(manifest_dir).join("upac-xbps.toml"); + + println!("cargo:rerun-if-changed={}", source.display()); + + let raw = read_to_string(&source)?; + let config: Value = from_str(&raw)?; + + let format = config + .get("format") + .and_then(Value::as_str) + .ok_or("upac-xbps.toml: format must be a string")?; + + let extensions = config + .get("extensions") + .and_then(Value::as_array) + .ok_or("upac-xbps.toml: extensions must be an array")? + .iter() + .map(|entry| { + entry + .as_str() + .ok_or("upac-xbps.toml: extensions entries must be strings") + }) + .collect::, _>>()?; + + let mut generated = String::new(); + generated.push_str("pub mod manifest {\n"); + generated.push_str(&format!(" pub const FORMAT: &str = {format:?};\n")); + generated.push_str(" pub const EXTENSIONS: &[&str] = &[\n"); + for extension in extensions { + generated.push_str(&format!(" {extension:?},\n")); + } + generated.push_str(" ];\n"); + generated.push_str("}\n"); + + Ok(generated) +} diff --git a/decoders/xbps/src/imports.h b/decoders/xbps/src/imports.h deleted file mode 100644 index 85797f3..0000000 --- a/decoders/xbps/src/imports.h +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 JustPav - * SPDX-FileCopyrightText: 2026 SmoothTeam - * - * SPDX-License-Identifier: LGPL-3.0-or-later - */ - -#include -#include diff --git a/decoders/xbps/src/lib.rs b/decoders/xbps/src/lib.rs new file mode 100644 index 0000000..7de1d50 --- /dev/null +++ b/decoders/xbps/src/lib.rs @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception diff --git a/decoders/xbps/src/symbols.zig b/decoders/xbps/src/symbols.zig deleted file mode 100644 index d159449..0000000 --- a/decoders/xbps/src/symbols.zig +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -// ── Imports ─────────────────────────────────────────────────────────────────── -const std = @import("std"); - -const types = @import("upac-backend-types"); -const BackendErrorCode = types.BackendErrorCode; -const fromError = types.fromError; -const BackendError = types.BackendError; -const PrepareData = types.PrepareData; - -const ffi = @import("upac-backend-ffi"); -const CPrepareRequest = ffi.CPrepareRequest; -const CPackageMeta = ffi.CPackageMeta; -const CVersion = ffi.CVersion; -const CVersionParts = ffi.CVersionParts; -const CSlice = ffi.CSlice; - -const dupeToCSlice = ffi.dupeToCSlice; -const dupeRequiredToCSlice = ffi.dupeRequiredToCSlice; - -const BackendMachine = @import("backend/backend.zig").BackendMachine; - -// ── FFI exports ─────────────────────────────────────────────────────────────── -pub export fn prepare(request_c: *const CPrepareRequest, out_meta: **CPackageMeta, out_temp_path: *CSlice) callconv(.c) i32 { - request_c.validate() catch |err| return @intFromEnum(fromError(err)); - - const cancel_token = request_c.cancel_token orelse return @intFromEnum(BackendErrorCode.invalid_entry); - - const prepare_data = PrepareData{ - .package_path_c = request_c.package_path.asZ(), - .temp_path_c = request_c.temp_dir.asZ(), - .checksum = request_c.checksum.toSlice(), - .on_hook = request_c.on_hook, - .hook_ctx = request_c.hook_ctx, - .cancel_token = cancel_token, - }; - - var result = BackendMachine.run(prepare_data, std.heap.c_allocator) catch |err| return @intFromEnum(fromError(err)); - defer result.meta.deinit(std.heap.c_allocator); - - const version_parts_copy = std.heap.c_allocator.dupe(u32, result.meta.version.parts) catch return @intFromEnum(BackendErrorCode.alloc_failed); - - const out_meta_ptr = std.heap.c_allocator.create(CPackageMeta) catch { - std.heap.c_allocator.free(version_parts_copy); - return @intFromEnum(BackendErrorCode.alloc_failed); - }; - - out_meta_ptr.* = CPackageMeta{ - .name = dupeRequiredToCSlice(std.heap.c_allocator, result.meta.name) catch return @intFromEnum(fromError(BackendError.InvalidPackage)), - .version = CVersion{ - .epoch = result.meta.version.epoch, - .release = result.meta.version.release, - .parts = .{ .ptr = version_parts_copy.ptr, .len = version_parts_copy.len }, - .pre = CSlice.fromSlice(if (result.meta.version.pre) |pre| - std.heap.c_allocator.dupeZ(u8, pre) catch return @intFromEnum(BackendErrorCode.alloc_failed) - else - null), - }, - .arch = dupeToCSlice(std.heap.c_allocator, result.meta.arch) catch return @intFromEnum(fromError(BackendError.AllocZFailed)), - .arch_sub = CSlice.fromSlice(null), - .maintainer = dupeToCSlice(std.heap.c_allocator, result.meta.author) catch return @intFromEnum(fromError(BackendError.AllocZFailed)), - .description = dupeToCSlice(std.heap.c_allocator, result.meta.description) catch return @intFromEnum(fromError(BackendError.AllocZFailed)), - .license = dupeToCSlice(std.heap.c_allocator, result.meta.license) catch return @intFromEnum(fromError(BackendError.AllocZFailed)), - .url = dupeToCSlice(std.heap.c_allocator, result.meta.url) catch return @intFromEnum(fromError(BackendError.AllocZFailed)), - .sha256 = result.meta.checksum, - .installed_size = @as(u64, result.meta.size), - }; - - out_meta.* = out_meta_ptr; - out_temp_path.* = dupeToCSlice(std.heap.c_allocator, result.temp_path) catch return @intFromEnum(fromError(BackendError.AllocZFailed)); - - return @intFromEnum(BackendErrorCode.ok); -} - -pub export fn cleanup(path_c: CSlice) callconv(.c) void { - const path = path_c.toSlice(); - const io = std.Io.Threaded.global_single_threaded.io(); - - std.Io.Dir.cwd().deleteTree(io, path) catch {}; - - std.heap.c_allocator.free(path); -} - -pub export fn free_meta(package_meta_c: *CPackageMeta) callconv(.c) void { - package_meta_c.free(std.heap.c_allocator); -} - -pub export fn version_abi() callconv(.c) u32 { - return ffi.ABI_VERSION; -} diff --git a/decoders/xbps/src/types/errors.zig b/decoders/xbps/src/types/errors.zig deleted file mode 100644 index 8acb8f6..0000000 --- a/decoders/xbps/src/types/errors.zig +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-FileCopyrightText: 2026 JustPav -// SPDX-FileCopyrightText: 2026 SmoothTeam -// -// SPDX-License-Identifier: LGPL-3.0-or-later - -pub const BackendErrorCode = enum(i32) { - ok = 0, - checksum_mismatch = 1, - extraction_failed = 2, - metadata_not_found = 3, - invalid_package = 4, - archive_open_failed = 5, - archive_read_failed = 6, - archive_extract_failed = 7, - temp_dir_failed = 8, - alloc_failed = 9, - cancelled = 10, - read_failed = 11, - invalid_entry = 12, - abi_mismatch = 13, - unexpected = 99, -}; - -pub const BackendError = error{ - ChecksumMismatch, - ExtractionFailed, - MetadataNotFound, - InvalidPackage, - ReadFailed, - ArchiveOpenFailed, - ArchiveReadFailed, - ArchiveExtractFailed, - OutOfMemory, - TempDirFailed, - AllocZFailed, - Cancelled, -}; - -pub fn fromError(err: anyerror) BackendErrorCode { - return switch (err) { - BackendError.ChecksumMismatch => .checksum_mismatch, - BackendError.ExtractionFailed => .extraction_failed, - BackendError.MetadataNotFound => .metadata_not_found, - BackendError.InvalidPackage => .invalid_package, - BackendError.ArchiveOpenFailed => .archive_open_failed, - BackendError.ArchiveReadFailed => .archive_read_failed, - BackendError.ArchiveExtractFailed => .archive_extract_failed, - BackendError.TempDirFailed => .temp_dir_failed, - BackendError.AllocZFailed, BackendError.OutOfMemory => .alloc_failed, - BackendError.Cancelled => .cancelled, - BackendError.ReadFailed => .read_failed, - error.InvalidEntry => .invalid_entry, - error.AbiMismatch => .abi_mismatch, - else => .unexpected, - }; -} diff --git a/decoders/xbps/upac-xbps.toml b/decoders/xbps/upac-xbps.toml index de08bdb..09943db 100644 --- a/decoders/xbps/upac-xbps.toml +++ b/decoders/xbps/upac-xbps.toml @@ -1,15 +1,14 @@ # SPDX-FileCopyrightText: 2026 JustPav # SPDX-FileCopyrightText: 2026 SmoothTeam # -# SPDX-License-Identifier: LGPL-3.0-or-later +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -# Declarative decoder manifest (doc §5.8/§6). Not installed by `zig build` — -# there is no packaging pipeline (PKGBUILD/spec/etc) in this repo yet; this -# file is the canonical source a future package build copies to -# /etc/upac.d/decoders/upac-xbps.toml. +# Declarative decoder manifest (doc §5.8/§6). Not installed by any packaging +# pipeline yet — this file is the canonical source a future package build +# copies to /etc/upac.d/decoders/upac-xbps.toml. format = "xbps" extensions = ["xbps"] -library = "/usr/lib/upac/decoders/libupac-xbps.so" +library = "/usr/lib/upac/decoders/libupac_decoder_xbps.so" # No mime type for xbps packages is registered in shared-mime-info (verified # against freedesktop.org.xml) — this is the same unofficial vendor-prefixed diff --git a/doc/eng/Upac chapter 7.md b/doc/eng/Upac chapter 7.md index 1cf8976..ac9559c 100644 --- a/doc/eng/Upac chapter 7.md +++ b/doc/eng/Upac chapter 7.md @@ -14,8 +14,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0 - `export` — C-ABI: entry points for all commands, the ABI version, cancellation, freeing responses; - `orchestrator` — the shared engine (see **§5.9**–**§5.11**): `Stage`/`ConcurrentStage`, `Cursor`, `RollbackGuard`, and two orchestrators behind one `Orchestrator` trait — `SequentialOrchestrator` (linear, holds the system lock file) and `ParallelOrchestrator` (parallel stages, used for hooks in **§5.8**); - `mutated` / `unmutated` — command bodies, one submodule per command, kept flat even for related families (e.g. `diff`/`diff_packages`/`diff_prefix`/`diff_config`, or `search_meta`/`search_files`) — no grouping subfolder, since the shared name prefix already sorts/groups them in any directory listing. Each is assembled from its own pipeline of stages via `orchestrator`; -- `scripts` — item **§5.8**: the hook file's TOML format (`HookFile`), primitives (`Primitive`/`TouchFile`/`MoveFile`/`CreateSymlink`, each `impl Step { execute, rollback }`), native trigger matching (`Operation`/`Timing`). `HookStage::run()` is fully wired up for native triggers: get-or-build the shared `tokio` runtime via `Context`, then signature verification and hook-file parsing (`load_hooks`, via `upac-pki`), filtering by `NativeTrigger`, and parallel execution of matched hooks via `ParallelOrchestrator` (`HookFile` itself `impl ConcurrentStage`, executing its own `steps` and honoring `critical`). It is wired into the pipeline of every mutating command (Pre/Post hook processing for each); -- `plugin` — two independent plugin kinds, both loaded and invoked only by `lib`, never directly by the CLI. `decoder`: `dlopen`, ABI version check, `decode`/`match_triggers`; `manifest` (`DecoderManifest`, `load_decoder_manifests()` — reads the declarative files describing decoders in the `/etc/upac.d/decoders/*.toml` directory, without scanning and/or checking the `.so`) and `triggers` (`build_trigger_table()` — builds the native-trigger→hook table for a specific decoder from the loaded `HookFile`s, resolving `priority` conflicts with a hard operation error); wired into a real call site via `PackageUnpacker` (`plugin/decoder/unpack.rs`), used by both `install`'s and `update`'s `PreparationStage`. `boot`: a second, symmetric plugin kind for one-shot reboot selection — `resolve_boot_plugin` either loads a caller-named plugin directly by manifest name (an explicit choice is trusted as-is, never re-probed) or, in auto mode, loads every installed plugin and probes each one, hard-erroring on zero or more than one claimant (same "ambiguous/duplicate is a hard error" precedent as decoder formats). Each plugin implements a small four-symbol C-ABI contract (`probe`/`set_one_shot`/`confirm_boot`/`abi_version`, see `upac-abi`'s `boot` module) and can be loaded either dynamically (default, `dlopen`) or compiled directly into `upac-lib` via Cargo features (`builtin-uki`/`builtin-systemd-boot`/`builtin-grub`/`builtin-refind`) for distributions that prefer static linking — see `booters/` below; +- `scripts` — item **§5.8**: the hook file's TOML format (`HookFile`), primitives (`Primitive`/`TouchFile`/`MoveFile`/`CreateSymlink`, each `impl Step { execute, rollback }`), pipeline trigger matching (`Operation`/`Timing`, where `Timing` has three positions — `Pre`, `Post`, and `Declarative`). `HookStage::run()` is fully wired up for both: get-or-build the shared `tokio` runtime via `Context`, then signature verification and hook-file parsing (`load_hooks`, via `upac-pki`); for `Pre`/`Post` it filters hooks by `PipelineTrigger` equality, while for `Declarative` it instead reads the pipeline's `Vec` out of `Context`, builds a per-format `build_trigger_table()` for each format seen, and matches each package's declared trigger names against it — either way, the matched hooks run in parallel via `ParallelOrchestrator` (`HookFile` itself `impl ConcurrentStage`, executing its own `steps` and honoring `critical`). It is wired into the pipeline of every mutating command: `Pre` first, then the command's own transaction/merge/checkout/swap stages, then `Declarative` (once real system state has actually changed), then `Post`; +- `plugin` — two independent plugin kinds, both loaded and invoked only by `lib`, never directly by the CLI. `decoder`: `dlopen`, ABI version check, `decode` (its response also carries each package's own declarative triggers alongside its dependencies, surfaced by `Decoder`/`PackageUnpacker` as a separate `upac_types::DeclarativeTrigger{format, triggers}` value, kept apart from `PackageTemp` so a later stage's `context.take::>()` doesn't remove it); `manifest` (`DecoderManifest`, `load_decoder_manifests()` — reads the declarative files describing decoders in the `/etc/upac.d/decoders/*.toml` directory, without scanning and/or checking the `.so`) and `triggers` (`build_trigger_table()` — builds the declarative-trigger→hook table for a specific decoder from the loaded `HookFile`s, resolving `priority` conflicts with a hard operation error). Matching a decoded package's declarative triggers against this table, and firing the resulting hooks, is fully wired: install/update persist each package's `DeclarativeTrigger` into the database (`packages_triggers` table, keyed by the package's `Uuid`, alongside `PackageMeta`) as part of their own `TransactionStage`; uninstall reads it back out of the database during `PreparationStage` (no re-`decode()` — the original package archive may no longer even be available) and removes the row during its own `TransactionStage`; and `HookStage`'s `Declarative` position (above) does the actual table-matching and hook firing for all three commands. `boot`: a second, symmetric plugin kind for one-shot reboot selection — `resolve_boot_plugin` either loads a caller-named plugin directly by manifest name (an explicit choice is trusted as-is, never re-probed) or, in auto mode, loads every installed plugin and probes each one, hard-erroring on zero or more than one claimant (same "ambiguous/duplicate is a hard error" precedent as decoder formats). Each plugin implements a small four-symbol C-ABI contract (`probe`/`set_one_shot`/`confirm_boot`/`abi_version`, see `upac-abi`'s `boot` module) and can be loaded either dynamically (default, `dlopen`) or compiled directly into `upac-lib` via Cargo features (`builtin-uki`/`builtin-systemd-boot`/`builtin-grub`/`builtin-refind`) for distributions that prefer static linking — see `booters/` below; - `config` — item **§5.1**: the real 3-way `/etc` merge, `merge_config(base, new, live, allow_conflict_files)`. Classifies every path per the doc's rules and writes `.upac-new` sidecars for genuine conflicts; `allow_conflict_files` is an `install`/`update`-level ABI override (`--no-conflict-files` in the CLI, default on) that skips writing the sidecar while still tracking the conflict — the doc's own 3-way classification never needed a flag, this is purely an operational knob layered on top. Conflicts are reported through the existing `MessageHook` as ordinary progress events (`subject` = the conflicting path) — fire-and-forget, per the doc's explicit "does not block the operation from proceeding" wording; there is no separate two-way/blocking hook for this, and none is planned; - `boot` — item **§5.2**/**§5.3**: `write_boot_entry`, which stages a UKI or BLS boot resource from a committed composefs image onto the ESP (fixed `upac-to` slot for UKI-direct, content-addressed `` filename for BLS-style loaders — `composefs-boot`'s own `write_boot_simple` dispatches by resource type, `Type1`/`Type2`). This module only writes the on-disk entry; the actual one-shot NVRAM selection is delegated entirely to whichever plugin `plugin::boot::resolve_boot_plugin` resolves (see above) — `boot` and `plugin::boot` are deliberately separate modules for two different concerns (staging the entry vs. selecting it for next boot); - `composefs` — access to the composefs repository. `Repository`: `open(path) -> Repository` (opening by path via `Repository::open_path`), `open_tree(repository, name) -> FileSystem` (reads the image via `Repository::open_image` + `erofs::reader::erofs_to_filesystem`), and their write-side counterparts `commit_tree(repository, tree) -> ObjectID` (validates + `mkfs_erofs` + `write_image`) and `object_id_from_hex` — all four are available only inside the library; only `deploy::Deploy` is exposed outward. `error::RepoError` — a mapping of `RepositoryOpenError`/`ImageError`/`anyhow::Error` (the last one is needed because `ensure_object`/`ensure_object_from_file`/`commit_image` and so on in composefs itself return `anyhow::Result` — no error detail can be extracted from it, only the fact of failure). `file::FileHandle` — a holder/pointer to a path in the tree, three `impl` blocks organized by "does it touch CAS or not" logic: constructors (`new` — blind, for inserting something new; `from_tree` — with a check that the path already exists), tree operations without CAS (`insert_in_tree`/`update_in_tree`/`rename_in_tree`/`remove_in_tree`/`symlink_in_tree`/`hardlink_in_tree`/`stat_in_tree`/`symlink_target_in_tree`/`list_in_tree`/`import_directory`), file operations through CAS (`insert_file` takes an already-open `&File` — not bytes, so that the path is resolved exactly once and there is no TOCTOU race from swapping the file out between reading it and inserting it into CAS; `replace_file` — an alias for `insert_file`, since composefs's `Directory::insert` already upserts by itself; `read_file` — resolves inline/external and pulls bytes from `Repository::read_object` when needed; `copy_from_tree` — a cross-tree leaf copy, used by the 3-way merge above); diff --git a/doc/rus/Upac chapter 7.md b/doc/rus/Upac chapter 7.md index e6a150f..c63ae70 100644 --- a/doc/rus/Upac chapter 7.md +++ b/doc/rus/Upac chapter 7.md @@ -14,8 +14,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0 - `export` — C-ABI: точки входа всех команд, версия ABI, отмена, освобождение ответов; - `orchestrator` — общий движок (См. **§5.9**–**§5.11**): `Stage`/`ConcurrentStage`, `Cursor`, `RollbackGuard`, и два оркестратора за одним трейтом `Orchestrator` — `SequentialOrchestrator` (линейный, держит системный лок файл) и `ParallelOrchestrator` (параллельные стадии, используется для хуков в **§5.8**); - `mutated` / `unmutated` — тела команд, по подмодулю на каждую, вложенность плоская даже для родственных семей (например, `diff`/`diff_packages`/`diff_prefix`/`diff_config` или `search_meta`/`search_files`) — без папки-группы, поскольку общий префикс имени и так сортирует/группирует их в любом листинге директории. Собираются из своего pipeline каждой стадии через `orchestrator`; -- `scripts` — пункт **§5.8**: TOML-формат хук-файла (`HookFile`), примитивы (`Primitive`/`TouchFile`/`MoveFile`/`CreateSymlink`, каждый `impl Step { execute, rollback }`), матчинг нативных триггеров (`Operation`/`Timing`). `HookStage::run()` полностью подключён для нативных триггеров: get-or-build общего `tokio`-рантайма через `Context`, далее проверка подписи и парсинг хук-файлов (`load_hooks`, через `upac-pki`), фильтрация по `NativeTrigger`, параллельный запуск совпавших хуков через `ParallelOrchestrator` (`HookFile` сам `impl ConcurrentStage`, исполняет свои `steps` и учитывает `critical`). Вписан в pipeline всех mutated команд (Pre/Post обработка хуков каждой); -- `plugin` — два независимых вида плагинов, оба загружаются и вызываются только `lib`, никогда напрямую CLI. `decoder`: `dlopen`, проверка версии ABI, `decode`/`match_triggers`; `manifest` (`DecoderManifest`, `load_decoder_manifests()` — читает декларативные файлы для описания декодеров в каталоге `/etc/upac.d/decoders/*.toml`, без сканирования и/или проверки `.so`) и `triggers` (`build_trigger_table()` — строит таблицу native-триггер→хук под конкретный декодер из загруженных `HookFile`, разрешая конфликты `priority` жёсткой ошибкой операции); подключён к реальной точке вызова через `PackageUnpacker` (`plugin/decoder/unpack.rs`), используется в `PreparationStage` и `install`, и `update`. `boot`: второй, симметричный вид плагинов для одноразового выбора перезагрузки — `resolve_boot_plugin` либо загружает названный вызывающей стороной плагин напрямую по имени манифеста (явный выбор доверяется как есть, повторный probe не выполняется), либо в авто-режиме загружает все установленные плагины и опрашивает (`probe`) каждый, с жёсткой ошибкой при нуле или более чем одном заявившем о себе плагине (тот же прецедент "неоднозначность/дубликат — жёсткая ошибка", что и у форматов декодеров). Каждый плагин реализует небольшой контракт из четырёх C-ABI символов (`probe`/`set_one_shot`/`confirm_boot`/`abi_version`, см. модуль `boot` в `upac-abi`) и может грузиться либо динамически (по умолчанию, `dlopen`), либо компилироваться напрямую в `upac-lib` через Cargo-фичи (`builtin-uki`/`builtin-systemd-boot`/`builtin-grub`/`builtin-refind`) для дистрибутивов, предпочитающих статическую линковку — см. `booters/` ниже; +- `scripts` — пункт **§5.8**: TOML-формат хук-файла (`HookFile`), примитивы (`Primitive`/`TouchFile`/`MoveFile`/`CreateSymlink`, каждый `impl Step { execute, rollback }`), матчинг pipeline-триггеров (`Operation`/`Timing`, где у `Timing` три позиции — `Pre`, `Post` и `Declarative`). `HookStage::run()` полностью подключён для обеих: get-or-build общего `tokio`-рантайма через `Context`, далее проверка подписи и парсинг хук-файлов (`load_hooks`, через `upac-pki`); для `Pre`/`Post` фильтрация идёт по равенству `PipelineTrigger`, а для `Declarative` вместо этого из `Context` читается `Vec` пайплайна, для каждого встреченного формата строится своя `build_trigger_table()`, и имена задекларированных триггеров каждого пакета сверяются с ней — в обоих случаях совпавшие хуки запускаются параллельно через `ParallelOrchestrator` (`HookFile` сам `impl ConcurrentStage`, исполняет свои `steps` и учитывает `critical`). Вписан в pipeline всех mutated команд: сначала `Pre`, затем собственные стадии команды (transaction/merge/checkout/swap), затем `Declarative` (когда реальное состояние системы уже изменилось), затем `Post`; +- `plugin` — два независимых вида плагинов, оба загружаются и вызываются только `lib`, никогда напрямую CLI. `decoder`: `dlopen`, проверка версии ABI, `decode` (в ответе также приходят декларативные триггеры конкретного пакета, наравне с его зависимостями; `Decoder`/`PackageUnpacker` выносят их отдельным значением `upac_types::DeclarativeTrigger{format, triggers}`, отдельно от `PackageTemp`, чтобы более позднее `context.take::>()` его не забрало); `manifest` (`DecoderManifest`, `load_decoder_manifests()` — читает декларативные файлы для описания декодеров в каталоге `/etc/upac.d/decoders/*.toml`, без сканирования и/или проверки `.so`) и `triggers` (`build_trigger_table()` — строит таблицу декларативный-триггер→хук под конкретный декодер из загруженных `HookFile`, разрешая конфликты `priority` жёсткой ошибкой операции). Сверка декларативных триггеров декодированного пакета с этой таблицей и реальный запуск совпавших хуков полностью подключены: install/update сохраняют `DeclarativeTrigger` каждого пакета в базу (таблица `packages_triggers`, ключ — `Uuid` пакета, наравне с `PackageMeta`) в рамках собственной `TransactionStage`; uninstall читает их обратно из базы на `PreparationStage` (без повторного `decode()` — исходный архив пакета к этому моменту может уже отсутствовать) и удаляет строку на своей `TransactionStage`; а собственно сверку по таблице и запуск хуков для всех трёх команд выполняет позиция `Declarative` у `HookStage` (см. выше). `boot`: второй, симметричный вид плагинов для одноразового выбора перезагрузки — `resolve_boot_plugin` либо загружает названный вызывающей стороной плагин напрямую по имени манифеста (явный выбор доверяется как есть, повторный probe не выполняется), либо в авто-режиме загружает все установленные плагины и опрашивает (`probe`) каждый, с жёсткой ошибкой при нуле или более чем одном заявившем о себе плагине (тот же прецедент "неоднозначность/дубликат — жёсткая ошибка", что и у форматов декодеров). Каждый плагин реализует небольшой контракт из четырёх C-ABI символов (`probe`/`set_one_shot`/`confirm_boot`/`abi_version`, см. модуль `boot` в `upac-abi`) и может грузиться либо динамически (по умолчанию, `dlopen`), либо компилироваться напрямую в `upac-lib` через Cargo-фичи (`builtin-uki`/`builtin-systemd-boot`/`builtin-grub`/`builtin-refind`) для дистрибутивов, предпочитающих статическую линковку — см. `booters/` ниже; - `config` — пункт **§5.1**: реальное 3-way слияние `/etc`, `merge_config(base, new, live, allow_conflict_files)`. Классифицирует каждый путь по правилам доктрины и пишет `.upac-new`-сайдкары при настоящих конфликтах; `allow_conflict_files` — это override уровня ABI для `install`/`update` (`--no-conflict-files` в CLI, по умолчанию включено), который пропускает запись сайдкара, но всё равно отслеживает конфликт — сама 3-way классификация из доктрины никогда не нуждалась во флаге, это чисто эксплуатационная ручка поверх неё. О конфликтах сообщается через уже существующий `MessageHook` обычными progress-событиями (`subject` = конфликтующий путь) — fire-and-forget, согласно явной формулировке доктрины "does not block the operation from proceeding"; отдельного двустороннего/блокирующего хука для этого нет и не планируется; - `boot` — пункт **§5.2**/**§5.3**: `write_boot_entry`, который выкладывает UKI- или BLS-загрузочный ресурс из закоммиченного composefs-образа на ESP (фиксированный слот `upac-to` для UKI-direct, content-addressed имя `` для BLS-совместимых загрузчиков — сам `write_boot_simple` из `composefs-boot` диспетчеризует по типу ресурса, `Type1`/`Type2`). Этот модуль только пишет запись на диск; сам одноразовый выбор в NVRAM целиком делегирован тому плагину, который резолвит `plugin::boot::resolve_boot_plugin` (см. выше) — `boot` и `plugin::boot` намеренно разные модули под две разные заботы (выкладка записи vs. её выбор на следующую загрузку); - `composefs` — доступ к composefs-репозиторию. `Repository`: `open(path) -> Repository` (открытие по пути через `Repository::open_path`), `open_tree(repository, name) -> FileSystem` (читает образ через `Repository::open_image` + `erofs::reader::erofs_to_filesystem`), и их write-side пара `commit_tree(repository, tree) -> ObjectID` (валидация + `mkfs_erofs` + `write_image`) и `object_id_from_hex` — все четыре доступны только внутри библиотеки, наружу отдаётся только `deploy::Deploy`. `error::RepoError` — маппинг `RepositoryOpenError`/`ImageError`/`anyhow::Error` (последнее нужно, потому что `ensure_object`/`ensure_object_from_file`/`commit_image` и т.п. в самом composefs возвращают `anyhow::Result` — деталей ошибки оттуда не достать, только факт неудачи). `file::FileHandle` — держатель/указатель на путь в дереве, три `impl`-блока по логике "трогает CAS или нет": конструкторы (`new` — слепой, для вставки нового; `from_tree` — с проверкой, что путь уже существует), дерево без CAS (`insert_in_tree`/`update_in_tree`/`rename_in_tree`/`remove_in_tree`/`symlink_in_tree`/`hardlink_in_tree`/`stat_in_tree`/`symlink_target_in_tree`/`list_in_tree`/`import_directory`), файл через CAS (`insert_file` берёт уже открытый `&File` — не байты, чтобы путь резолвился ровно один раз и не было TOCTOU-гонки на подмену файла между чтением и вставкой в CAS; `replace_file` — алиас на `insert_file`, т.к. `Directory::insert` в composefs уже сам upsert-ит; `read_file` — резолвит inline/external и тянет байты из `Repository::read_object` при необходимости; `copy_from_tree` — междеревянное копирование листа, используется 3-way слиянием выше); diff --git a/lib/abi/Cargo.toml b/lib/abi/Cargo.toml index d537b3c..31eee84 100644 --- a/lib/abi/Cargo.toml +++ b/lib/abi/Cargo.toml @@ -1,17 +1,25 @@ # SPDX-FileCopyrightText: 2026 JustPav # SPDX-FileCopyrightText: 2026 SmoothTeam # -# SPDX-License-Identifier: LGPL-3.0-or-later +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception [package] name = "upac-abi" description = "C-ABI types, error codes, and conversions shared between upac-lib and its consumers" - version.workspace = true + edition.workspace = true -repository.workspace = true +rust-version.workspace = true + license.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true + +keywords.workspace = true +categories.workspace = true + include = ["/src"] [lints] diff --git a/lib/abi/src/boot.rs b/lib/abi/src/boot.rs index 173ace3..1483c5b 100644 --- a/lib/abi/src/boot.rs +++ b/lib/abi/src/boot.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_macro::CNew; diff --git a/lib/abi/src/decoder.rs b/lib/abi/src/decoder.rs index 19c06a5..0a23996 100644 --- a/lib/abi/src/decoder.rs +++ b/lib/abi/src/decoder.rs @@ -1,9 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later - -use std::slice::from_raw_parts; +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_macro::{CNew, CValidate}; @@ -23,7 +21,17 @@ pub type DecodeFn = unsafe extern "C" fn(request: *const CDecodeRequest, respons pub type FreeDecodeResponseFn = unsafe extern "C" fn(response: *mut CDecodeResponse); -pub type MatchTriggersFn = unsafe extern "C" fn(table: *const CTriggerTable, matches: *mut CTriggerMatches) -> i32; +/// Matches `token` against `operators`, longest operator first, returning the matched +/// constraint bitflags and how many bytes the operator itself consumed. Each package format +/// declares its own `operators` table (dependency version-constraint syntax differs per format — +/// e.g. alpm has bare `<`/`>`, deb only has `<<`/`>>`), so this only supplies the shared +/// longest-prefix-match logic, not any fixed operator set. +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())) +} #[repr(C)] #[derive(CNew)] @@ -46,6 +54,7 @@ pub struct CDecodeResponse { pub meta: CPackageMeta, pub dependencies: CVec, + pub declarative_triggers: CVec, pub free: FreeDecodeResponseFn, } @@ -65,44 +74,3 @@ pub struct CDependency { pub constraint: u8, pub version: CVersion, } - -#[repr(C)] -#[derive(CValidate)] -pub struct CTriggerEntry { - pub struct_size: usize, - pub name: CSlice, - pub hook_id: u16, -} - -#[repr(C)] -#[derive(CValidate)] -pub struct CTriggerTable { - pub struct_size: usize, - - pub entries: CVec, -} - -#[repr(C)] -#[derive(CNew)] -pub struct CTriggerMatches { - pub struct_size: usize, - - pub ids: *mut u16, - pub capacity: usize, - pub len: usize, -} - -impl CTriggerMatches { - /// # Safety - /// `ids` must point to `capacity` writable `u16` slots, and `len` must have been written by the - /// decoder (or left at `0`) before this is called. - pub unsafe fn matched(&self) -> &[u16] { - if self.ids.is_null() { - return &[]; - } - - let len = self.len.min(self.capacity); - - unsafe { from_raw_parts(self.ids, len) } - } -} diff --git a/lib/abi/src/error.rs b/lib/abi/src/error.rs index 1d34168..53ec0d7 100644 --- a/lib/abi/src/error.rs +++ b/lib/abi/src/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::ffi::FromBytesWithNulError; use std::str::Utf8Error; diff --git a/lib/abi/src/hook.rs b/lib/abi/src/hook.rs index ddd3c69..e7836f0 100644 --- a/lib/abi/src/hook.rs +++ b/lib/abi/src/hook.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// 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; diff --git a/lib/abi/src/lib.rs b/lib/abi/src/lib.rs index 042700a..3a89c2c 100644 --- a/lib/abi/src/lib.rs +++ b/lib/abi/src/lib.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use self::error::ErrorKind; diff --git a/lib/abi/src/memory.rs b/lib/abi/src/memory.rs index f1c9cd7..bee6dd9 100644 --- a/lib/abi/src/memory.rs +++ b/lib/abi/src/memory.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; use std::ptr::null_mut; diff --git a/lib/abi/src/package.rs b/lib/abi/src/package.rs index 5a1db80..2c594f7 100644 --- a/lib/abi/src/package.rs +++ b/lib/abi/src/package.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_macro::{CFree, CNew, CValidate}; diff --git a/lib/abi/src/request.rs b/lib/abi/src/request.rs index fadc8e5..6c2518d 100644 --- a/lib/abi/src/request.rs +++ b/lib/abi/src/request.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; diff --git a/lib/abi/src/response.rs b/lib/abi/src/response.rs index c87d46b..762b4fb 100644 --- a/lib/abi/src/response.rs +++ b/lib/abi/src/response.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_macro::{CFree, CNew, CValidate}; diff --git a/lib/abi/src/setup.rs b/lib/abi/src/setup.rs index 0acc7a7..14c63ff 100644 --- a/lib/abi/src/setup.rs +++ b/lib/abi/src/setup.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_macro::{CNew, CValidate}; diff --git a/lib/abi/src/types.rs b/lib/abi/src/types.rs index 7207240..124b61d 100644 --- a/lib/abi/src/types.rs +++ b/lib/abi/src/types.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::ffi::CStr; use std::mem::{forget, size_of}; diff --git a/lib/abi/tests/types.rs b/lib/abi/tests/types.rs index 958e77b..a8145f7 100644 --- a/lib/abi/tests/types.rs +++ b/lib/abi/tests/types.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::ptr::{null, null_mut}; diff --git a/lib/abi/tests/validate.rs b/lib/abi/tests/validate.rs index b81ba8a..3a50614 100644 --- a/lib/abi/tests/validate.rs +++ b/lib/abi/tests/validate.rs @@ -1,16 +1,16 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::mem::size_of; use std::ptr::null; -use upac_abi::decoder::{CDependency, CTriggerEntry, CTriggerTable}; +use upac_abi::decoder::CDependency; use upac_abi::error::ErrorKind; -use upac_abi::memory::{free_cslice, free_cvec}; +use upac_abi::memory::free_cslice; use upac_abi::package::{CPackageInfo, CPackageMeta, CVersion}; -use upac_abi::types::{COwned, CSlice, CVec}; +use upac_abi::types::{COwned, CSlice}; fn valid_version() -> CVersion { CVersion { @@ -110,50 +110,3 @@ fn dependency_validate_rejects_invalid_nested_version() { dependency.version.free(); } } - -#[test] -fn trigger_table_validate_ok_for_valid_entries() { - let table = CTriggerTable { - struct_size: size_of::(), - entries: CVec::from_owned(vec![ - CTriggerEntry { - struct_size: size_of::(), - name: CSlice::from_owned(b"pre-install".to_vec()), - hook_id: 0, - }, - CTriggerEntry { - struct_size: size_of::(), - name: CSlice::from_owned(b"post-install".to_vec()), - hook_id: 1, - }, - ]), - }; - - assert!(unsafe { table.validate() }.is_ok()); - unsafe { - for entry in table.entries.as_slice() { - free_cslice(&entry.name); - } - free_cvec(&table.entries); - } -} - -#[test] -fn trigger_table_validate_rejects_malformed_entry() { - let table = CTriggerTable { - struct_size: size_of::(), - entries: CVec::from_owned(vec![CTriggerEntry { - struct_size: 0, - name: CSlice::from_owned(b"pre-install".to_vec()), - hook_id: 0, - }]), - }; - - assert_eq!(unsafe { table.validate() }, Err(ErrorKind::AbiMismatch)); - unsafe { - for entry in table.entries.as_slice() { - free_cslice(&entry.name); - } - free_cvec(&table.entries); - } -} diff --git a/lib/lib/Cargo.toml b/lib/lib/Cargo.toml index 4280007..774cb97 100644 --- a/lib/lib/Cargo.toml +++ b/lib/lib/Cargo.toml @@ -1,17 +1,25 @@ # SPDX-FileCopyrightText: 2026 JustPav # SPDX-FileCopyrightText: 2026 SmoothTeam # -# SPDX-License-Identifier: LGPL-3.0-or-later +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception [package] name = "upac-lib" description = "Core library implementing upac's package management logic (composefs-based atomic deploys) and exposing it via a C ABI" - version.workspace = true + edition.workspace = true -repository.workspace = true +rust-version.workspace = true + license.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true + +keywords.workspace = true +categories.workspace = true + include = ["/src", "lib.toml", "build.rs"] [lints] @@ -30,6 +38,9 @@ upac-uki = { workspace = true, optional = true } upac-systemd-boot = { workspace = true, optional = true } upac-grub = { workspace = true, optional = true } upac-refind = { workspace = true, optional = true } +upac-decoders-alpm = { workspace = true, optional = true } +upac-decoders-deb = { workspace = true, optional = true } +upac-decoders-rpm = { workspace = true, optional = true } composefs = { workspace = true } composefs-oci = { workspace = true } @@ -53,7 +64,7 @@ twox-hash = "2.1.3" rsblkid = "0.4" rsmount = "0.2" regex = "1" -quick-xml = "0.41.0" +quick-xml = "0.42.0" mime = "0.3.17" hex = "0.4" xattr = "1.6" @@ -74,6 +85,10 @@ builtin-systemd-boot = ["dep:upac-systemd-boot", "builtin-booters"] builtin-grub = ["dep:upac-grub", "builtin-booters"] builtin-refind = ["dep:upac-refind", "builtin-booters"] +builtin-alpm = ["dep:upac-decoders-alpm", "builtin-decoders"] +builtin-deb = ["dep:upac-decoders-deb", "builtin-decoders"] +builtin-rpm = ["dep:upac-decoders-rpm", "builtin-decoders"] + builtin-all = [ "builtin-uki", "builtin-systemd-boot", "builtin-grub", "builtin-refind", ] diff --git a/lib/lib/build.rs b/lib/lib/build.rs index 09acc5d..781f2c9 100644 --- a/lib/lib/build.rs +++ b/lib/lib/build.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::env::var; use std::error::Error; diff --git a/lib/lib/lib.toml b/lib/lib/lib.toml index 03f4850..fd205ef 100644 --- a/lib/lib/lib.toml +++ b/lib/lib/lib.toml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2026 JustPav # SPDX-FileCopyrightText: 2026 SmoothTeam # -# SPDX-License-Identifier: LGPL-3.0-or-later +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception # Names of the redb tables inside the package database embedded in the image. # Changing a name means already-built images stop finding their tables when @@ -15,9 +15,11 @@ [database] packages_table_name = "packages" packages_by_name_table_name = "packages_by_name" +packages_triggers_table_name = "packages_triggers" files_table_name = "files" files_by_path_table_name = "files_by_path" packages_meta_type_name = "upac::PackageMeta" +packages_triggers_type_name = "upac::DeclarativeTrigger" files_entry_type_name = "upac::FileEntry" database_path = "share/upac/packages.redb" diff --git a/lib/lib/src/boot/error.rs b/lib/lib/src/boot/error.rs index fbb15d0..fe9e277 100644 --- a/lib/lib/src/boot/error.rs +++ b/lib/lib/src/boot/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use anyhow::Error as AnyhowError; diff --git a/lib/lib/src/boot/mod.rs b/lib/lib/src/boot/mod.rs index cf50f9d..9a966d8 100644 --- a/lib/lib/src/boot/mod.rs +++ b/lib/lib/src/boot/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::path::Path; diff --git a/lib/lib/src/composefs/diff.rs b/lib/lib/src/composefs/diff.rs index df5d903..7dac4af 100644 --- a/lib/lib/src/composefs/diff.rs +++ b/lib/lib/src/composefs/diff.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::cmp::Ordering; use std::path::{Path, PathBuf}; diff --git a/lib/lib/src/composefs/error.rs b/lib/lib/src/composefs/error.rs index f34822f..19f6c28 100644 --- a/lib/lib/src/composefs/error.rs +++ b/lib/lib/src/composefs/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::io::Error as IoError; diff --git a/lib/lib/src/composefs/file.rs b/lib/lib/src/composefs/file.rs index 8169d8f..41ef296 100644 --- a/lib/lib/src/composefs/file.rs +++ b/lib/lib/src/composefs/file.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::collections::BTreeMap; use std::ffi::OsStr; diff --git a/lib/lib/src/composefs/mod.rs b/lib/lib/src/composefs/mod.rs index 54bf54c..db1d988 100644 --- a/lib/lib/src/composefs/mod.rs +++ b/lib/lib/src/composefs/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception pub mod diff; pub mod error; diff --git a/lib/lib/src/composefs/overlay.rs b/lib/lib/src/composefs/overlay.rs index 5c5821b..8be8934 100644 --- a/lib/lib/src/composefs/overlay.rs +++ b/lib/lib/src/composefs/overlay.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::{File, Metadata, read_dir, read_link}; use std::os::unix::fs::{FileTypeExt, MetadataExt}; diff --git a/lib/lib/src/composefs/repository.rs b/lib/lib/src/composefs/repository.rs index 754343d..3eed822 100644 --- a/lib/lib/src/composefs/repository.rs +++ b/lib/lib/src/composefs/repository.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::File; use std::io::Read; diff --git a/lib/lib/src/config/merge.rs b/lib/lib/src/config/merge.rs index 86f8d96..192e342 100644 --- a/lib/lib/src/config/merge.rs +++ b/lib/lib/src/config/merge.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::collections::BTreeMap; use std::path::Path; diff --git a/lib/lib/src/config/mod.rs b/lib/lib/src/config/mod.rs index 0bacc48..177dfc9 100644 --- a/lib/lib/src/config/mod.rs +++ b/lib/lib/src/config/mod.rs @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// 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 8bcc9cf..020dbe7 100644 --- a/lib/lib/src/database/attribution.rs +++ b/lib/lib/src/database/attribution.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use super::error::DatabaseError; use super::files::FileStore; diff --git a/lib/lib/src/database/error.rs b/lib/lib/src/database/error.rs index 4197647..9522007 100644 --- a/lib/lib/src/database/error.rs +++ b/lib/lib/src/database/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::io::Error as IoError; diff --git a/lib/lib/src/database/files.rs b/lib/lib/src/database/files.rs index d0d7d7c..34e7e48 100644 --- a/lib/lib/src/database/files.rs +++ b/lib/lib/src/database/files.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use redb::{ReadableDatabase, ReadableTable, TypeName, Value as RedbValue}; @@ -10,6 +10,7 @@ use twox_hash::xxhash3_64::Hasher as XxHasher; use uuid::Uuid; use upac_types::FileEntry; +use upac_types::codec::RedbCodable; use super::error::DatabaseError; use super::{FILES_UUID_HASH_TABLE, FILES_UUID_TABLE, MemoryDatabase, ReadableSource}; @@ -169,7 +170,7 @@ impl RedbValue for StoredFileEntry { { let mut offset = 0; - StoredFileEntry(FileEntry::decode_from(data, &mut offset)) + StoredFileEntry(FileEntry::redb_decode(data, &mut offset)) } fn as_bytes<'a, 'b: 'a>(value: &'a StoredFileEntry) -> Vec @@ -178,7 +179,7 @@ impl RedbValue for StoredFileEntry { { let mut buf = Vec::new(); - FileEntry::encode_into(&mut buf, &value.0); + value.0.redb_encode(&mut buf); buf } diff --git a/lib/lib/src/database/meta.rs b/lib/lib/src/database/meta.rs index 237474f..875a803 100644 --- a/lib/lib/src/database/meta.rs +++ b/lib/lib/src/database/meta.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use redb::{ReadableDatabase, ReadableTable, TypeName, Value as RedbValue}; @@ -10,7 +10,7 @@ use twox_hash::xxhash3_64::Hasher as XxHasher; use uuid::Uuid; use upac_types::PackageMeta; -use upac_types::codec::{write_len_prefixed, write_opt_str}; +use upac_types::codec::{RedbCodable, write_len_prefixed, write_opt_str}; use super::error::DatabaseError; use super::{MemoryDatabase, PACKAGES_HASH_TABLE, PACKAGES_UUID_TABLE, ReadableSource}; @@ -159,7 +159,7 @@ impl RedbValue for StoredPackageMeta { { let mut offset = 0; - StoredPackageMeta(PackageMeta::decode_from(data, &mut offset)) + StoredPackageMeta(PackageMeta::redb_decode(data, &mut offset)) } fn as_bytes<'a, 'b: 'a>(value: &'a StoredPackageMeta) -> Vec @@ -168,7 +168,7 @@ impl RedbValue for StoredPackageMeta { { let mut buf = Vec::new(); - PackageMeta::encode_into(&mut buf, &value.0); + value.0.redb_encode(&mut buf); buf } diff --git a/lib/lib/src/database/mod.rs b/lib/lib/src/database/mod.rs index 5f103b5..5bfcfdf 100644 --- a/lib/lib/src/database/mod.rs +++ b/lib/lib/src/database/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::io::{Error as IoError, ErrorKind}; use std::path::Path; @@ -16,21 +16,26 @@ 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; pub mod attribution; pub mod error; pub mod files; pub mod meta; pub mod record; +pub mod triggers; pub(crate) const PACKAGES_UUID_TABLE: TableDefinition = TableDefinition::new(PACKAGES_TABLE_NAME); pub(crate) const PACKAGES_HASH_TABLE: TableDefinition = TableDefinition::new(PACKAGES_BY_NAME_TABLE_NAME); +pub(crate) const PACKAGES_TRIGGERS_TABLE: TableDefinition = + TableDefinition::new(PACKAGES_TRIGGERS_TABLE_NAME); pub(crate) const FILES_UUID_TABLE: TableDefinition<(Uuid, u64), StoredFileEntry> = TableDefinition::new(FILES_TABLE_NAME); diff --git a/lib/lib/src/database/record.rs b/lib/lib/src/database/record.rs index c0200c2..f161b75 100644 --- a/lib/lib/src/database/record.rs +++ b/lib/lib/src/database/record.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::{File, read_to_string}; use std::io::{ErrorKind as IoErrorKind, Read}; diff --git a/lib/lib/src/database/triggers.rs b/lib/lib/src/database/triggers.rs new file mode 100644 index 0000000..dfa033f --- /dev/null +++ b/lib/lib/src/database/triggers.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 redb::{ReadableDatabase, TypeName, Value as RedbValue}; + +use uuid::Uuid; + +use upac_types::DeclarativeTrigger; +use upac_types::codec::RedbCodable; + +use super::error::DatabaseError; +use super::{MemoryDatabase, PACKAGES_TRIGGERS_TABLE, ReadableSource}; + +use crate::layout::database::PACKAGES_TRIGGERS_TYPE_NAME; + +pub trait TriggerStore { + fn get_declarative_triggers(&self, uuid: Uuid) -> Result, DatabaseError>; +} + +pub trait TriggerStoreMut: TriggerStore { + fn set_declarative_triggers(&mut self, uuid: Uuid, trigger: &DeclarativeTrigger) -> Result<(), DatabaseError>; + fn remove_declarative_triggers(&mut self, uuid: Uuid) -> Result<(), DatabaseError>; +} + +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)?; + + Ok(triggers.get(uuid)?.map(|guard| guard.value().0)) + } +} + +impl TriggerStoreMut for MemoryDatabase { + fn set_declarative_triggers(&mut self, uuid: Uuid, trigger: &DeclarativeTrigger) -> Result<(), DatabaseError> { + let transaction = self.database.begin_write()?; + + transaction + .open_table(PACKAGES_TRIGGERS_TABLE)? + .insert(uuid, StoredTriggers::from_ref(trigger))?; + + transaction.commit()?; + Ok(()) + } + + fn remove_declarative_triggers(&mut self, uuid: Uuid) -> Result<(), DatabaseError> { + let transaction = self.database.begin_write()?; + + transaction.open_table(PACKAGES_TRIGGERS_TABLE)?.remove(uuid)?; + + transaction.commit()?; + Ok(()) + } +} + +#[derive(Debug)] +#[repr(transparent)] +pub(crate) struct StoredTriggers(pub(crate) DeclarativeTrigger); + +impl StoredTriggers { + fn from_ref(trigger: &DeclarativeTrigger) -> &StoredTriggers { + // SAFETY: `StoredTriggers` is `#[repr(transparent)]` over `DeclarativeTrigger`, so the two + // share identical layout and this reference cast is sound. + unsafe { &*(trigger as *const DeclarativeTrigger as *const StoredTriggers) } + } +} + +impl RedbValue for StoredTriggers { + type AsBytes<'a> = Vec; + type SelfType<'a> = StoredTriggers; + + fn fixed_width() -> Option { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> StoredTriggers + where + Self: 'a, + { + let mut offset = 0; + + StoredTriggers(DeclarativeTrigger::redb_decode(data, &mut offset)) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a StoredTriggers) -> Vec + where + Self: 'b, + { + let mut buf = Vec::new(); + + value.0.redb_encode(&mut buf); + buf + } + + fn type_name() -> TypeName { + TypeName::new(PACKAGES_TRIGGERS_TYPE_NAME) + } +} diff --git a/lib/lib/src/deploy/digest.rs b/lib/lib/src/deploy/digest.rs index 1084e10..e75816e 100644 --- a/lib/lib/src/deploy/digest.rs +++ b/lib/lib/src/deploy/digest.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use linux_kernel_cmdline::utf8::CmdlineOwned; diff --git a/lib/lib/src/deploy/error.rs b/lib/lib/src/deploy/error.rs index dcc3547..c31ecca 100644 --- a/lib/lib/src/deploy/error.rs +++ b/lib/lib/src/deploy/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::io::Error as IoError; diff --git a/lib/lib/src/deploy/esp.rs b/lib/lib/src/deploy/esp.rs index 9333ad9..46f9650 100644 --- a/lib/lib/src/deploy/esp.rs +++ b/lib/lib/src/deploy/esp.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::path::PathBuf; diff --git a/lib/lib/src/deploy/mod.rs b/lib/lib/src/deploy/mod.rs index 7f7a3fc..91d82b0 100644 --- a/lib/lib/src/deploy/mod.rs +++ b/lib/lib/src/deploy/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::{create_dir_all, read_dir, remove_dir}; use std::path::{Path, PathBuf}; diff --git a/lib/lib/src/deploy/retention.rs b/lib/lib/src/deploy/retention.rs index 9d110f2..6add97b 100644 --- a/lib/lib/src/deploy/retention.rs +++ b/lib/lib/src/deploy/retention.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::collections::HashSet; use std::fs::remove_dir_all; diff --git a/lib/lib/src/errors.rs b/lib/lib/src/errors.rs index ed8809e..6efc073 100644 --- a/lib/lib/src/errors.rs +++ b/lib/lib/src/errors.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::io::ErrorKind as IoErrorKind; diff --git a/lib/lib/src/export/mod.rs b/lib/lib/src/export/mod.rs index 626695c..c28aa52 100644 --- a/lib/lib/src/export/mod.rs +++ b/lib/lib/src/export/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// 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}; diff --git a/lib/lib/src/export/mutated/commit.rs b/lib/lib/src/export/mutated/commit.rs index 51e1aa4..cd590ea 100644 --- a/lib/lib/src/export/mutated/commit.rs +++ b/lib/lib/src/export/mutated/commit.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/mutated/files.rs b/lib/lib/src/export/mutated/files.rs index cd36759..9593e1e 100644 --- a/lib/lib/src/export/mutated/files.rs +++ b/lib/lib/src/export/mutated/files.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/mutated/gc.rs b/lib/lib/src/export/mutated/gc.rs index fd433c5..25713a9 100644 --- a/lib/lib/src/export/mutated/gc.rs +++ b/lib/lib/src/export/mutated/gc.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/mutated/installer.rs b/lib/lib/src/export/mutated/installer.rs index c2ea299..6844f40 100644 --- a/lib/lib/src/export/mutated/installer.rs +++ b/lib/lib/src/export/mutated/installer.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/mutated/mime.rs b/lib/lib/src/export/mutated/mime.rs index b22a7b1..9a26bcc 100644 --- a/lib/lib/src/export/mutated/mime.rs +++ b/lib/lib/src/export/mutated/mime.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/mutated/mod.rs b/lib/lib/src/export/mutated/mod.rs index cb4cb78..72fb068 100644 --- a/lib/lib/src/export/mutated/mod.rs +++ b/lib/lib/src/export/mutated/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception pub mod commit; pub mod files; diff --git a/lib/lib/src/export/mutated/pin.rs b/lib/lib/src/export/mutated/pin.rs index 1017adc..9e07965 100644 --- a/lib/lib/src/export/mutated/pin.rs +++ b/lib/lib/src/export/mutated/pin.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/mutated/rollback.rs b/lib/lib/src/export/mutated/rollback.rs index f4d773b..d557072 100644 --- a/lib/lib/src/export/mutated/rollback.rs +++ b/lib/lib/src/export/mutated/rollback.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/mutated/uninstaller.rs b/lib/lib/src/export/mutated/uninstaller.rs index f7e9ecc..ce2b5a1 100644 --- a/lib/lib/src/export/mutated/uninstaller.rs +++ b/lib/lib/src/export/mutated/uninstaller.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/mutated/update.rs b/lib/lib/src/export/mutated/update.rs index 8def3ae..3a76719 100644 --- a/lib/lib/src/export/mutated/update.rs +++ b/lib/lib/src/export/mutated/update.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/unmutated/diff.rs b/lib/lib/src/export/unmutated/diff.rs index 3118bee..e51f0b3 100644 --- a/lib/lib/src/export/unmutated/diff.rs +++ b/lib/lib/src/export/unmutated/diff.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/unmutated/diff_config.rs b/lib/lib/src/export/unmutated/diff_config.rs index 2a24517..5d33e01 100644 --- a/lib/lib/src/export/unmutated/diff_config.rs +++ b/lib/lib/src/export/unmutated/diff_config.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/unmutated/diff_packages.rs b/lib/lib/src/export/unmutated/diff_packages.rs index ec3b021..3544a92 100644 --- a/lib/lib/src/export/unmutated/diff_packages.rs +++ b/lib/lib/src/export/unmutated/diff_packages.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/unmutated/diff_prefix.rs b/lib/lib/src/export/unmutated/diff_prefix.rs index cba28fb..904d786 100644 --- a/lib/lib/src/export/unmutated/diff_prefix.rs +++ b/lib/lib/src/export/unmutated/diff_prefix.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/unmutated/list_config.rs b/lib/lib/src/export/unmutated/list_config.rs index 4efe101..c0cd50f 100644 --- a/lib/lib/src/export/unmutated/list_config.rs +++ b/lib/lib/src/export/unmutated/list_config.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/unmutated/list_history.rs b/lib/lib/src/export/unmutated/list_history.rs index 1f41af6..ea8a0a6 100644 --- a/lib/lib/src/export/unmutated/list_history.rs +++ b/lib/lib/src/export/unmutated/list_history.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/unmutated/list_packages.rs b/lib/lib/src/export/unmutated/list_packages.rs index b297aff..442aa11 100644 --- a/lib/lib/src/export/unmutated/list_packages.rs +++ b/lib/lib/src/export/unmutated/list_packages.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/unmutated/list_prefix.rs b/lib/lib/src/export/unmutated/list_prefix.rs index 5bb0725..5a2adbc 100644 --- a/lib/lib/src/export/unmutated/list_prefix.rs +++ b/lib/lib/src/export/unmutated/list_prefix.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/unmutated/mod.rs b/lib/lib/src/export/unmutated/mod.rs index f961618..fa473ce 100644 --- a/lib/lib/src/export/unmutated/mod.rs +++ b/lib/lib/src/export/unmutated/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception pub mod diff; pub mod diff_config; diff --git a/lib/lib/src/export/unmutated/search_files.rs b/lib/lib/src/export/unmutated/search_files.rs index 35944d4..6e4ff0e 100644 --- a/lib/lib/src/export/unmutated/search_files.rs +++ b/lib/lib/src/export/unmutated/search_files.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/unmutated/search_in_meta.rs b/lib/lib/src/export/unmutated/search_in_meta.rs index 8d0fc1b..20ee402 100644 --- a/lib/lib/src/export/unmutated/search_in_meta.rs +++ b/lib/lib/src/export/unmutated/search_in_meta.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; 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 ce68956..1c2e1aa 100644 --- a/lib/lib/src/export/unmutated/search_in_package_files.rs +++ b/lib/lib/src/export/unmutated/search_in_package_files.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/export/unmutated/search_meta.rs b/lib/lib/src/export/unmutated/search_meta.rs index 46e456a..e8b211c 100644 --- a/lib/lib/src/export/unmutated/search_meta.rs +++ b/lib/lib/src/export/unmutated/search_meta.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::panic::{AssertUnwindSafe, catch_unwind}; diff --git a/lib/lib/src/fs.rs b/lib/lib/src/fs.rs index 11f279d..14175dc 100644 --- a/lib/lib/src/fs.rs +++ b/lib/lib/src/fs.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs; use std::io::{Error as IoError, ErrorKind as IoErrorKind, Write as IoWrite}; diff --git a/lib/lib/src/lib.rs b/lib/lib/src/lib.rs index 50b3e55..1677a24 100644 --- a/lib/lib/src/lib.rs +++ b/lib/lib/src/lib.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception mod mutated; mod search; diff --git a/lib/lib/src/lock.rs b/lib/lib/src/lock.rs index b7de754..89b32c0 100644 --- a/lib/lib/src/lock.rs +++ b/lib/lib/src/lock.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::fd::{AsRawFd, OwnedFd}; diff --git a/lib/lib/src/mutated/commit/error.rs b/lib/lib/src/mutated/commit/error.rs index 4c9766b..fb62291 100644 --- a/lib/lib/src/mutated/commit/error.rs +++ b/lib/lib/src/mutated/commit/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/mutated/commit/mod.rs b/lib/lib/src/mutated/commit/mod.rs index 6b0cea0..10eba37 100644 --- a/lib/lib/src/mutated/commit/mod.rs +++ b/lib/lib/src/mutated/commit/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; @@ -17,7 +17,7 @@ use crate::deploy::retention::RetentionStage; use crate::deploy::{Deploy, DeployMode}; use crate::orchestrator::{Context, Orchestrator, SequentialOrchestrator, run_mutating}; use crate::scripts::HookStage; -use crate::scripts::native::{NativeTrigger, Operation}; +use crate::scripts::pipeline::{Operation, PipelineTrigger}; use upac_types::TmpPath; use upac_types::states::CommitStateId; @@ -84,11 +84,11 @@ pub fn run(data: CommitData) -> Result<(), (CommitStateId, CommitError)> { fn assemble() -> SequentialOrchestrator { SequentialOrchestrator::new(vec![ Box::new(HookStage { - trigger: NativeTrigger::pre(Operation::Commit), + trigger: PipelineTrigger::pre(Operation::Commit), }), Box::new(TransactionStage), Box::new(HookStage { - trigger: NativeTrigger::post(Operation::Commit), + trigger: PipelineTrigger::post(Operation::Commit), }), Box::new(RetentionStage), ]) diff --git a/lib/lib/src/mutated/commit/transaction.rs b/lib/lib/src/mutated/commit/transaction.rs index 76f3e26..ec524b2 100644 --- a/lib/lib/src/mutated/commit/transaction.rs +++ b/lib/lib/src/mutated/commit/transaction.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use composefs::fsverity::FsVerityHashValue; use composefs::repository::ImportContext; diff --git a/lib/lib/src/mutated/files/checkout.rs b/lib/lib/src/mutated/files/checkout.rs index 363656a..5bf4af4 100644 --- a/lib/lib/src/mutated/files/checkout.rs +++ b/lib/lib/src/mutated/files/checkout.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/mutated/files/error.rs b/lib/lib/src/mutated/files/error.rs index c2171a6..49bf891 100644 --- a/lib/lib/src/mutated/files/error.rs +++ b/lib/lib/src/mutated/files/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/mutated/files/mod.rs b/lib/lib/src/mutated/files/mod.rs index 5471d64..9bdf7f2 100644 --- a/lib/lib/src/mutated/files/mod.rs +++ b/lib/lib/src/mutated/files/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; @@ -22,7 +22,7 @@ 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::native::{NativeTrigger, Operation}; +use crate::scripts::pipeline::{Operation, PipelineTrigger}; use upac_types::TmpPath; use upac_types::states::FilesStateId; @@ -150,13 +150,13 @@ pub fn run(data: FilesData) -> Result<(), (FilesStateId, FilesError)> { fn assemble() -> SequentialOrchestrator { SequentialOrchestrator::new(vec![ Box::new(HookStage { - trigger: NativeTrigger::pre(Operation::Files), + trigger: PipelineTrigger::pre(Operation::Files), }), Box::new(TransactionStage), Box::new(CheckoutStage), Box::new(SwapStage), Box::new(HookStage { - trigger: NativeTrigger::post(Operation::Files), + trigger: PipelineTrigger::post(Operation::Files), }), Box::new(RetentionStage), ]) diff --git a/lib/lib/src/mutated/files/swap.rs b/lib/lib/src/mutated/files/swap.rs index dd797ef..c5c6126 100644 --- a/lib/lib/src/mutated/files/swap.rs +++ b/lib/lib/src/mutated/files/swap.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/mutated/files/transaction.rs b/lib/lib/src/mutated/files/transaction.rs index bc938bc..f65abb5 100644 --- a/lib/lib/src/mutated/files/transaction.rs +++ b/lib/lib/src/mutated/files/transaction.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::{File, copy, create_dir_all, read_link, remove_file, symlink_metadata, write}; use std::os::unix::fs::symlink; diff --git a/lib/lib/src/mutated/gc/cleaning.rs b/lib/lib/src/mutated/gc/cleaning.rs index 4a24097..dedf99b 100644 --- a/lib/lib/src/mutated/gc/cleaning.rs +++ b/lib/lib/src/mutated/gc/cleaning.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/mutated/gc/error.rs b/lib/lib/src/mutated/gc/error.rs index 68dafe2..48defe5 100644 --- a/lib/lib/src/mutated/gc/error.rs +++ b/lib/lib/src/mutated/gc/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/mutated/gc/mod.rs b/lib/lib/src/mutated/gc/mod.rs index 642cb53..51e129c 100644 --- a/lib/lib/src/mutated/gc/mod.rs +++ b/lib/lib/src/mutated/gc/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; diff --git a/lib/lib/src/mutated/installer/checkout.rs b/lib/lib/src/mutated/installer/checkout.rs index 6026b6a..3c6693b 100644 --- a/lib/lib/src/mutated/installer/checkout.rs +++ b/lib/lib/src/mutated/installer/checkout.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/mutated/installer/error.rs b/lib/lib/src/mutated/installer/error.rs index 33083f2..93d8611 100644 --- a/lib/lib/src/mutated/installer/error.rs +++ b/lib/lib/src/mutated/installer/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/mutated/installer/fetching.rs b/lib/lib/src/mutated/installer/fetching.rs index 315f40d..2f278d8 100644 --- a/lib/lib/src/mutated/installer/fetching.rs +++ b/lib/lib/src/mutated/installer/fetching.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/mutated/installer/merge.rs b/lib/lib/src/mutated/installer/merge.rs index d837e83..d2049af 100644 --- a/lib/lib/src/mutated/installer/merge.rs +++ b/lib/lib/src/mutated/installer/merge.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::create_dir_all; diff --git a/lib/lib/src/mutated/installer/mod.rs b/lib/lib/src/mutated/installer/mod.rs index 0cf52e9..e29554f 100644 --- a/lib/lib/src/mutated/installer/mod.rs +++ b/lib/lib/src/mutated/installer/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; @@ -26,7 +26,7 @@ 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::native::{NativeTrigger, Operation}; +use crate::scripts::pipeline::{Operation, PipelineTrigger}; use upac_types::TmpPath; use upac_types::states::InstallStateId; @@ -122,7 +122,7 @@ pub fn run(data: InstallData) -> Result<(), (InstallStateId, InstallError)> { fn assemble() -> SequentialOrchestrator { SequentialOrchestrator::new(vec![ Box::new(HookStage { - trigger: NativeTrigger::pre(Operation::Install), + trigger: PipelineTrigger::pre(Operation::Install), }), Box::new(FetchingStage), Box::new(PreparationStage), @@ -131,7 +131,10 @@ fn assemble() -> SequentialOrchestrator { Box::new(CheckoutStage), Box::new(SwapStage), Box::new(HookStage { - trigger: NativeTrigger::post(Operation::Install), + trigger: PipelineTrigger::declarative(Operation::Install), + }), + Box::new(HookStage { + trigger: PipelineTrigger::post(Operation::Install), }), Box::new(RetentionStage), ]) diff --git a/lib/lib/src/mutated/installer/preparation.rs b/lib/lib/src/mutated/installer/preparation.rs index 52c4641..7ab4985 100644 --- a/lib/lib/src/mutated/installer/preparation.rs +++ b/lib/lib/src/mutated/installer/preparation.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; @@ -23,10 +23,11 @@ impl Stage for PreparationStage { let tmp_path = context.get::().ok_or(CommonError::MissingResult)?; let mut unpacker = PackageUnpacker::new().map_err(CommonError::Decoder)?; - let packages = unpacker + let (packages, declarative_triggers) = unpacker .unpack_all(&package_paths, tmp_path.as_ref(), cancel) .map_err(CommonError::Decoder)?; context.put(packages); + context.put(declarative_triggers); Ok((progress, Box::new(NoRollback::new_none(StageResult::Advance)))) } diff --git a/lib/lib/src/mutated/installer/swap.rs b/lib/lib/src/mutated/installer/swap.rs index fc4ce4a..2fc919f 100644 --- a/lib/lib/src/mutated/installer/swap.rs +++ b/lib/lib/src/mutated/installer/swap.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/mutated/installer/transaction.rs b/lib/lib/src/mutated/installer/transaction.rs index 63c51d6..c0a9492 100644 --- a/lib/lib/src/mutated/installer/transaction.rs +++ b/lib/lib/src/mutated/installer/transaction.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::{File, write}; use std::path::{Path, PathBuf}; @@ -13,13 +13,14 @@ use composefs::tree::FileSystem; use upac_abi::hook::{CancelToken, ProgressEventBuilder}; -use upac_types::{FileEntry, FileEntryScope, PackageTemp, TmpPath}; +use upac_types::{DeclarativeTrigger, FileEntry, FileEntryScope, PackageTemp, TmpPath}; use crate::composefs::error::RepoError; use crate::composefs::file::FileHandle; use crate::composefs::repository::{ObjectID, commit_tree}; use crate::database::files::FileStoreMut; use crate::database::meta::MetaStoreMut; +use crate::database::triggers::TriggerStoreMut; use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; @@ -79,7 +80,12 @@ impl Stage for TransactionStage { .progress(index as u64, packages_total); context.send_progress(&progress); - self.import_package(package, &mut package_data)?; + let trigger = context + .get::>() + .and_then(|triggers| triggers.get(index)) + .ok_or(CommonError::MissingResult)?; + + self.import_package(package, trigger, &mut package_data)?; } let database_bytes = database.into_bytes()?; @@ -104,7 +110,9 @@ impl Stage for TransactionStage { } impl TransactionStage { - fn import_package(&self, package: &PackageTemp, package_data: &mut ImportPackageData) -> Result<(), InstallError> { + fn import_package( + &self, package: &PackageTemp, trigger: &DeclarativeTrigger, package_data: &mut ImportPackageData, + ) -> Result<(), InstallError> { let source_root = Path::new(&package.temp_package_path); let usr_source = source_root.join("usr"); @@ -134,6 +142,7 @@ impl TransactionStage { }; let uuid = package_data.database.insert_package_meta(&package.meta)?; + package_data.database.set_declarative_triggers(uuid, trigger)?; for path in imported { let entry = FileEntry { diff --git a/lib/lib/src/mutated/mime/error.rs b/lib/lib/src/mutated/mime/error.rs index c8b26c9..a410613 100644 --- a/lib/lib/src/mutated/mime/error.rs +++ b/lib/lib/src/mutated/mime/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// 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; diff --git a/lib/lib/src/mutated/mime/mod.rs b/lib/lib/src/mutated/mime/mod.rs index f49335b..05357be 100644 --- a/lib/lib/src/mutated/mime/mod.rs +++ b/lib/lib/src/mutated/mime/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; diff --git a/lib/lib/src/mutated/mime/preparing.rs b/lib/lib/src/mutated/mime/preparing.rs index 7347a98..7773cd6 100644 --- a/lib/lib/src/mutated/mime/preparing.rs +++ b/lib/lib/src/mutated/mime/preparing.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs; diff --git a/lib/lib/src/mutated/mime/rendering.rs b/lib/lib/src/mutated/mime/rendering.rs index 1eac118..ffb487b 100644 --- a/lib/lib/src/mutated/mime/rendering.rs +++ b/lib/lib/src/mutated/mime/rendering.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::collections::HashMap; use std::io::Result as IoResult; diff --git a/lib/lib/src/mutated/mime/writing.rs b/lib/lib/src/mutated/mime/writing.rs index 7492d87..9d6738a 100644 --- a/lib/lib/src/mutated/mime/writing.rs +++ b/lib/lib/src/mutated/mime/writing.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::path::Path; use std::process::Command; diff --git a/lib/lib/src/mutated/mod.rs b/lib/lib/src/mutated/mod.rs index cb4cb78..72fb068 100644 --- a/lib/lib/src/mutated/mod.rs +++ b/lib/lib/src/mutated/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception pub mod commit; pub mod files; diff --git a/lib/lib/src/mutated/pin/error.rs b/lib/lib/src/mutated/pin/error.rs index 8a73e3b..f45a30c 100644 --- a/lib/lib/src/mutated/pin/error.rs +++ b/lib/lib/src/mutated/pin/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/mutated/pin/mod.rs b/lib/lib/src/mutated/pin/mod.rs index e6365a6..77675f8 100644 --- a/lib/lib/src/mutated/pin/mod.rs +++ b/lib/lib/src/mutated/pin/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; diff --git a/lib/lib/src/mutated/pin/stage.rs b/lib/lib/src/mutated/pin/stage.rs index 09603d0..201a91e 100644 --- a/lib/lib/src/mutated/pin/stage.rs +++ b/lib/lib/src/mutated/pin/stage.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::mem::replace; diff --git a/lib/lib/src/mutated/rollback/checkout.rs b/lib/lib/src/mutated/rollback/checkout.rs index ef11216..9cb1bd8 100644 --- a/lib/lib/src/mutated/rollback/checkout.rs +++ b/lib/lib/src/mutated/rollback/checkout.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/mutated/rollback/error.rs b/lib/lib/src/mutated/rollback/error.rs index 9bcc5cd..1b5aa05 100644 --- a/lib/lib/src/mutated/rollback/error.rs +++ b/lib/lib/src/mutated/rollback/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/mutated/rollback/merge.rs b/lib/lib/src/mutated/rollback/merge.rs index 6a1698f..02d3d3c 100644 --- a/lib/lib/src/mutated/rollback/merge.rs +++ b/lib/lib/src/mutated/rollback/merge.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/mutated/rollback/mod.rs b/lib/lib/src/mutated/rollback/mod.rs index 2d1f458..dc911db 100644 --- a/lib/lib/src/mutated/rollback/mod.rs +++ b/lib/lib/src/mutated/rollback/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; @@ -20,7 +20,7 @@ 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::native::{NativeTrigger, Operation}; +use crate::scripts::pipeline::{Operation, PipelineTrigger}; use upac_types::TmpPath; use upac_types::states::RollbackStateId; @@ -94,13 +94,13 @@ pub fn run(data: RollbackData) -> Result<(), (RollbackStateId, RollbackError)> { fn assemble() -> SequentialOrchestrator { SequentialOrchestrator::new(vec![ Box::new(HookStage { - trigger: NativeTrigger::pre(Operation::Rollback), + trigger: PipelineTrigger::pre(Operation::Rollback), }), Box::new(MergeStage), Box::new(CheckoutStage), Box::new(SwapStage), Box::new(HookStage { - trigger: NativeTrigger::post(Operation::Rollback), + trigger: PipelineTrigger::post(Operation::Rollback), }), Box::new(RetentionStage), ]) diff --git a/lib/lib/src/mutated/rollback/swap.rs b/lib/lib/src/mutated/rollback/swap.rs index 20f2705..8181f54 100644 --- a/lib/lib/src/mutated/rollback/swap.rs +++ b/lib/lib/src/mutated/rollback/swap.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/mutated/uninstaller/checkout.rs b/lib/lib/src/mutated/uninstaller/checkout.rs index b1c6191..f6a4eca 100644 --- a/lib/lib/src/mutated/uninstaller/checkout.rs +++ b/lib/lib/src/mutated/uninstaller/checkout.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/mutated/uninstaller/error.rs b/lib/lib/src/mutated/uninstaller/error.rs index e5f339d..d84f523 100644 --- a/lib/lib/src/mutated/uninstaller/error.rs +++ b/lib/lib/src/mutated/uninstaller/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/mutated/uninstaller/merge.rs b/lib/lib/src/mutated/uninstaller/merge.rs index 3d1b858..412e3b8 100644 --- a/lib/lib/src/mutated/uninstaller/merge.rs +++ b/lib/lib/src/mutated/uninstaller/merge.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::create_dir_all; diff --git a/lib/lib/src/mutated/uninstaller/mod.rs b/lib/lib/src/mutated/uninstaller/mod.rs index f21ea17..00dd0eb 100644 --- a/lib/lib/src/mutated/uninstaller/mod.rs +++ b/lib/lib/src/mutated/uninstaller/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; @@ -17,7 +17,7 @@ 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::native::{NativeTrigger, Operation}; +use crate::scripts::pipeline::{Operation, PipelineTrigger}; use upac_types::states::UninstallStateId; use upac_types::{PackageEntry, Targets, TmpPath}; @@ -154,7 +154,7 @@ pub fn run(data: UninstallData) -> Result<(), (UninstallStateId, UninstallError) fn assemble() -> SequentialOrchestrator { SequentialOrchestrator::new(vec![ Box::new(HookStage { - trigger: NativeTrigger::pre(Operation::Uninstall), + trigger: PipelineTrigger::pre(Operation::Uninstall), }), Box::new(PreparationStage), Box::new(TransactionStage), @@ -162,7 +162,10 @@ fn assemble() -> SequentialOrchestrator { Box::new(CheckoutStage), Box::new(SwapStage), Box::new(HookStage { - trigger: NativeTrigger::post(Operation::Uninstall), + trigger: PipelineTrigger::declarative(Operation::Uninstall), + }), + Box::new(HookStage { + trigger: PipelineTrigger::post(Operation::Uninstall), }), Box::new(RetentionStage), ]) diff --git a/lib/lib/src/mutated/uninstaller/preparation.rs b/lib/lib/src/mutated/uninstaller/preparation.rs index f84d3dc..19949c7 100644 --- a/lib/lib/src/mutated/uninstaller/preparation.rs +++ b/lib/lib/src/mutated/uninstaller/preparation.rs @@ -1,14 +1,15 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; -use upac_types::Targets; +use upac_types::{DeclarativeTrigger, Targets}; use crate::composefs::file::FileHandle; use crate::database::meta::MetaStore; +use crate::database::triggers::TriggerStore; use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; @@ -35,14 +36,20 @@ impl Stage for PreparationStage { let database = MemoryDatabase::open_in_memory(database_bytes)?; let mut uuids = Vec::new(); + let mut declarative_triggers: Vec = Vec::new(); for entry in &targets.0 { let uuid = database .find_package_uuid(&entry.name, &entry.arch, entry.arch_sub.as_deref())? .ok_or(UninstallError::PackageNotFound)?; uuids.push(uuid); + + if let Some(trigger) = database.get_declarative_triggers(uuid)? { + declarative_triggers.push(trigger); + } } context.put(PackageUuidsToRemove(uuids)); + context.put(declarative_triggers); Ok((progress, Box::new(NoRollback))) } diff --git a/lib/lib/src/mutated/uninstaller/swap.rs b/lib/lib/src/mutated/uninstaller/swap.rs index 3b540bb..384387e 100644 --- a/lib/lib/src/mutated/uninstaller/swap.rs +++ b/lib/lib/src/mutated/uninstaller/swap.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/mutated/uninstaller/transaction.rs b/lib/lib/src/mutated/uninstaller/transaction.rs index b283d34..10a0eb4 100644 --- a/lib/lib/src/mutated/uninstaller/transaction.rs +++ b/lib/lib/src/mutated/uninstaller/transaction.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::{File, write}; @@ -21,6 +21,7 @@ use crate::composefs::file::FileHandle; use crate::composefs::repository::{ObjectID, commit_tree}; use crate::database::files::{FileStore, FileStoreMut}; use crate::database::meta::{MetaStore, MetaStoreMut}; +use crate::database::triggers::TriggerStoreMut; use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; @@ -153,6 +154,7 @@ impl TransactionStage { package_data .database .remove_package_meta(&meta.name, &meta.arch, meta.arch_sub.as_deref())?; + package_data.database.remove_declarative_triggers(uuid)?; Ok(()) } diff --git a/lib/lib/src/mutated/update/checkout.rs b/lib/lib/src/mutated/update/checkout.rs index 587aedb..94138ca 100644 --- a/lib/lib/src/mutated/update/checkout.rs +++ b/lib/lib/src/mutated/update/checkout.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/mutated/update/error.rs b/lib/lib/src/mutated/update/error.rs index 607aded..e7db861 100644 --- a/lib/lib/src/mutated/update/error.rs +++ b/lib/lib/src/mutated/update/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/mutated/update/fetching.rs b/lib/lib/src/mutated/update/fetching.rs index feecdbf..846a9fb 100644 --- a/lib/lib/src/mutated/update/fetching.rs +++ b/lib/lib/src/mutated/update/fetching.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/mutated/update/merge.rs b/lib/lib/src/mutated/update/merge.rs index 1a172cb..5597f14 100644 --- a/lib/lib/src/mutated/update/merge.rs +++ b/lib/lib/src/mutated/update/merge.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::create_dir_all; diff --git a/lib/lib/src/mutated/update/mod.rs b/lib/lib/src/mutated/update/mod.rs index 0b36647..626de9d 100644 --- a/lib/lib/src/mutated/update/mod.rs +++ b/lib/lib/src/mutated/update/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; @@ -26,7 +26,7 @@ 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::native::{NativeTrigger, Operation}; +use crate::scripts::pipeline::{Operation, PipelineTrigger}; use upac_types::TmpPath; use upac_types::states::UpdateStateId; @@ -127,7 +127,7 @@ pub fn run(data: UpdateData) -> Result<(), (UpdateStateId, UpdateError)> { fn assemble() -> SequentialOrchestrator { SequentialOrchestrator::new(vec![ Box::new(HookStage { - trigger: NativeTrigger::pre(Operation::Update), + trigger: PipelineTrigger::pre(Operation::Update), }), Box::new(FetchingStage), Box::new(PreparationStage), @@ -136,7 +136,10 @@ fn assemble() -> SequentialOrchestrator { Box::new(CheckoutStage), Box::new(SwapStage), Box::new(HookStage { - trigger: NativeTrigger::post(Operation::Update), + trigger: PipelineTrigger::declarative(Operation::Update), + }), + Box::new(HookStage { + trigger: PipelineTrigger::post(Operation::Update), }), Box::new(RetentionStage), ]) diff --git a/lib/lib/src/mutated/update/preparation.rs b/lib/lib/src/mutated/update/preparation.rs index 04e8fa6..012bd66 100644 --- a/lib/lib/src/mutated/update/preparation.rs +++ b/lib/lib/src/mutated/update/preparation.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; @@ -23,10 +23,11 @@ impl Stage for PreparationStage { let tmp_path = context.get::().ok_or(CommonError::MissingResult)?; let mut unpacker = PackageUnpacker::new().map_err(CommonError::Decoder)?; - let packages = unpacker + let (packages, declarative_triggers) = unpacker .unpack_all(&package_paths, tmp_path.as_ref(), cancel) .map_err(CommonError::Decoder)?; context.put(packages); + context.put(declarative_triggers); Ok((progress, Box::new(NoRollback::new_none(StageResult::Advance)))) } diff --git a/lib/lib/src/mutated/update/swap.rs b/lib/lib/src/mutated/update/swap.rs index 98e8310..933f106 100644 --- a/lib/lib/src/mutated/update/swap.rs +++ b/lib/lib/src/mutated/update/swap.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/mutated/update/transaction.rs b/lib/lib/src/mutated/update/transaction.rs index b0596b4..4f0d793 100644 --- a/lib/lib/src/mutated/update/transaction.rs +++ b/lib/lib/src/mutated/update/transaction.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::{File, write}; use std::path::{Path, PathBuf}; @@ -13,13 +13,14 @@ use composefs::tree::FileSystem; use upac_abi::hook::{CancelToken, ProgressEventBuilder}; -use upac_types::{FileEntry, FileEntryScope, PackageTemp, TmpPath}; +use upac_types::{DeclarativeTrigger, FileEntry, FileEntryScope, PackageTemp, TmpPath}; use crate::composefs::error::RepoError; use crate::composefs::file::FileHandle; use crate::composefs::repository::{ObjectID, commit_tree}; use crate::database::files::{FileStore, FileStoreMut}; use crate::database::meta::{MetaStore, MetaStoreMut}; +use crate::database::triggers::TriggerStoreMut; use crate::database::{InMemory, MemoryDatabase}; use crate::deploy::Deploy; use crate::deploy::digest::current_prefix_digest; @@ -91,7 +92,12 @@ impl Stage for TransactionStage { .progress(index as u64, packages_total); context.send_progress(&progress); - self.update_package(package, &mut package_data)?; + let trigger = context + .get::>() + .and_then(|triggers| triggers.get(index)) + .ok_or(CommonError::MissingResult)?; + + self.update_package(package, trigger, &mut package_data)?; } let database_bytes = database.into_bytes()?; @@ -117,7 +123,9 @@ impl Stage for TransactionStage { } impl TransactionStage { - fn update_package(&self, package: &PackageTemp, package_data: &mut UpdatePackageData) -> Result<(), UpdateError> { + fn update_package( + &self, package: &PackageTemp, trigger: &DeclarativeTrigger, package_data: &mut UpdatePackageData, + ) -> Result<(), UpdateError> { let uuid = package_data .database .find_package_uuid(&package.meta.name, &package.meta.arch, package.meta.arch_sub.as_deref())? @@ -188,6 +196,7 @@ impl TransactionStage { }; package_data.database.update_package_meta(&package.meta)?; + package_data.database.set_declarative_triggers(uuid, trigger)?; for path in imported { let entry = FileEntry { diff --git a/lib/lib/src/orchestrator/cursor.rs b/lib/lib/src/orchestrator/cursor.rs index 9b69dd1..9714602 100644 --- a/lib/lib/src/orchestrator/cursor.rs +++ b/lib/lib/src/orchestrator/cursor.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::any::TypeId; diff --git a/lib/lib/src/orchestrator/error.rs b/lib/lib/src/orchestrator/error.rs index c93d9c3..307fb29 100644 --- a/lib/lib/src/orchestrator/error.rs +++ b/lib/lib/src/orchestrator/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use crate::lock::LockError; diff --git a/lib/lib/src/orchestrator/mod.rs b/lib/lib/src/orchestrator/mod.rs index 55ddb9b..393b815 100644 --- a/lib/lib/src/orchestrator/mod.rs +++ b/lib/lib/src/orchestrator/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::any::{Any, TypeId}; use std::collections::{HashMap, HashSet}; diff --git a/lib/lib/src/orchestrator/stage.rs b/lib/lib/src/orchestrator/stage.rs index 5768692..3512b9c 100644 --- a/lib/lib/src/orchestrator/stage.rs +++ b/lib/lib/src/orchestrator/stage.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::any::{Any, TypeId}; diff --git a/lib/lib/src/plugin/boot/error.rs b/lib/lib/src/plugin/boot/error.rs index 6e770b1..ca7434f 100644 --- a/lib/lib/src/plugin/boot/error.rs +++ b/lib/lib/src/plugin/boot/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// 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; diff --git a/lib/lib/src/plugin/boot/manifest.rs b/lib/lib/src/plugin/boot/manifest.rs index 5ae051e..9493ad2 100644 --- a/lib/lib/src/plugin/boot/manifest.rs +++ b/lib/lib/src/plugin/boot/manifest.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::collections::HashMap; use std::fs; diff --git a/lib/lib/src/plugin/boot/mod.rs b/lib/lib/src/plugin/boot/mod.rs index 47bab47..a25ab1e 100644 --- a/lib/lib/src/plugin/boot/mod.rs +++ b/lib/lib/src/plugin/boot/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::mem::MaybeUninit; diff --git a/lib/lib/src/plugin/decoder/error.rs b/lib/lib/src/plugin/decoder/error.rs index 307ba37..441582e 100644 --- a/lib/lib/src/plugin/decoder/error.rs +++ b/lib/lib/src/plugin/decoder/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// 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; diff --git a/lib/lib/src/plugin/decoder/manifest.rs b/lib/lib/src/plugin/decoder/manifest.rs index 05c25a7..9a4a7e3 100644 --- a/lib/lib/src/plugin/decoder/manifest.rs +++ b/lib/lib/src/plugin/decoder/manifest.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::collections::HashMap; use std::fs; diff --git a/lib/lib/src/plugin/decoder/mod.rs b/lib/lib/src/plugin/decoder/mod.rs index 99e1eeb..b4a4cab 100644 --- a/lib/lib/src/plugin/decoder/mod.rs +++ b/lib/lib/src/plugin/decoder/mod.rs @@ -1,12 +1,14 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_types::{Dependency, PackageMeta}; -#[cfg(feature = "dynamic-plugins")] +#[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; @@ -15,19 +17,29 @@ use libloading::Library; use upac_abi::ABI_VERSION; #[cfg(feature = "dynamic-plugins")] -use upac_abi::decoder::{ - AbiVersionFn, CDecodeRequest, CDecodeResponse, CTriggerMatches, CTriggerTable, DecodeFn, MatchTriggersFn, -}; +use upac_abi::decoder::AbiVersionFn; -#[cfg(feature = "dynamic-plugins")] +#[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(feature = "dynamic-plugins")] +#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] use upac_abi::types::{CBorrowed, CSlice}; -#[cfg(feature = "dynamic-plugins")] +#[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}; + pub mod error; pub mod manifest; pub mod triggers; @@ -36,11 +48,12 @@ pub mod unpack; /// A package decoded by a decoder plugin. /// /// Plain owned data — available in every build configuration, including ones -/// without `dynamic-plugins`, so that callers and error types elsewhere in the -/// crate keep compiling. +/// 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")] @@ -50,19 +63,26 @@ unsafe fn load_symbol(library: &Library, name: &str) -> Result, +} + +#[cfg(feature = "builtin-decoders")] +impl Decoder { + fn from_static(decode: DecodeFn) -> Self { + Decoder { + decode, + + #[cfg(feature = "dynamic-plugins")] + _library: None, + } + } } #[cfg(feature = "dynamic-plugins")] @@ -72,7 +92,6 @@ impl Decoder { let abi_version: AbiVersionFn = unsafe { load_symbol(&library, "abi_version")? }; let decode: DecodeFn = unsafe { load_symbol(&library, "decode")? }; - let match_triggers: MatchTriggersFn = unsafe { load_symbol(&library, "match_triggers")? }; let got = unsafe { abi_version() }; if got != ABI_VERSION { @@ -84,11 +103,13 @@ impl Decoder { Ok(Decoder { decode, - match_triggers, - _library: library, + _library: Some(library), }) } +} +#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] +impl Decoder { pub fn decode( &self, package_path: &str, output_dir: &str, checksum: [u8; 32], cancel: &CancelToken, ) -> Result { @@ -117,22 +138,54 @@ impl Decoder { .map(Dependency::try_from) .collect::, _>>()?; - Ok(DecodedPackage { meta, dependencies }) + 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, + }) } +} - pub fn match_triggers(&self, table: &CTriggerTable) -> Result, DecoderError> { - let capacity = unsafe { table.entries.as_slice() }.len(); - let mut ids = vec![0u16; capacity]; - - let mut matches = CTriggerMatches::new(ids.as_mut_ptr(), capacity, 0); - - let code = unsafe { (self.match_triggers)(table, &mut matches) }; - if code != 0 { - return Err(DecoderError::Failed(code)); - } - - ids.truncate(matches.len.min(matches.capacity)); - - Ok(ids) - } +/// 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), + )); + + decoders } diff --git a/lib/lib/src/plugin/decoder/triggers.rs b/lib/lib/src/plugin/decoder/triggers.rs index 72df8d0..fc83646 100644 --- a/lib/lib/src/plugin/decoder/triggers.rs +++ b/lib/lib/src/plugin/decoder/triggers.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::collections::HashMap; diff --git a/lib/lib/src/plugin/decoder/unpack.rs b/lib/lib/src/plugin/decoder/unpack.rs index 3f7832c..57f715a 100644 --- a/lib/lib/src/plugin/decoder/unpack.rs +++ b/lib/lib/src/plugin/decoder/unpack.rs @@ -1,61 +1,64 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later - -use std::collections::HashMap; +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::CancelToken; -use upac_types::PackageTemp; +use upac_types::{DeclarativeTrigger, PackageTemp}; -use crate::layout::decoders; use crate::plugin::decoder::error::DecoderError; -use crate::plugin::decoder::manifest::{DecoderManifest, load_decoder_manifests}; -#[cfg(feature = "dynamic-plugins")] +#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] use std::fs::{File, create_dir_all, remove_dir_all}; -#[cfg(feature = "dynamic-plugins")] +#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] use std::io::Read; -#[cfg(feature = "dynamic-plugins")] +#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] use std::path::Path; -#[cfg(feature = "dynamic-plugins")] +#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] use sha2::{Digest, Sha256}; -#[cfg(feature = "dynamic-plugins")] +#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] use crate::plugin::decoder::Decoder; +#[cfg(feature = "dynamic-plugins")] +use std::collections::HashMap; + +#[cfg(feature = "dynamic-plugins")] +use crate::layout::decoders; +#[cfg(feature = "dynamic-plugins")] +use crate::plugin::decoder::manifest::{DecoderManifest, load_decoder_manifests}; + +#[cfg(all(not(feature = "dynamic-plugins"), feature = "builtin-decoders"))] +use crate::plugin::decoder::static_decoders; + pub struct PackageUnpacker { #[cfg(feature = "dynamic-plugins")] manifests: HashMap, #[cfg(feature = "dynamic-plugins")] decoders: HashMap, + + #[cfg(all(not(feature = "dynamic-plugins"), feature = "builtin-decoders"))] + decoders: Vec<(&'static str, &'static [&'static str], Decoder)>, } -#[cfg(feature = "dynamic-plugins")] +#[cfg(any(feature = "dynamic-plugins", feature = "builtin-decoders"))] impl PackageUnpacker { - pub fn new() -> Result { - let manifests = load_decoder_manifests(decoders::DECODERS_DIR, decoders::MANIFEST_EXTENSION)?; - - Ok(Self { - manifests, - decoders: HashMap::new(), - }) - } - pub fn unpack_all( &mut self, package_paths: &[String], tmp_path: &str, cancel: &CancelToken, - ) -> Result, DecoderError> { + ) -> Result<(Vec, Vec), DecoderError> { let mut packages = Vec::with_capacity(package_paths.len()); + let mut declarative_triggers = Vec::with_capacity(package_paths.len()); let mut output_dirs = Vec::with_capacity(package_paths.len()); for (index, package_path) in package_paths.iter().enumerate() { match self.unpack_one(package_path, index, tmp_path, cancel) { - Ok(package) => { + Ok((package, trigger)) => { output_dirs.push(package.temp_package_path.clone()); packages.push(package); + declarative_triggers.push(trigger); } Err(error) => { for output_dir in output_dirs.into_iter().rev() { @@ -67,12 +70,12 @@ impl PackageUnpacker { } } - Ok(packages) + Ok((packages, declarative_triggers)) } fn unpack_one( &mut self, package_path: &str, index: usize, tmp_path: &str, cancel: &CancelToken, - ) -> Result { + ) -> Result<(PackageTemp, DeclarativeTrigger), DecoderError> { let format = self.format_for(package_path)?; let checksum = checksum_of_file(package_path)?; @@ -89,9 +92,27 @@ impl PackageUnpacker { let _ = remove_dir_all(&output_dir); })?; - Ok(PackageTemp { - meta: decoded.meta, - temp_package_path: output_dir, + Ok(( + PackageTemp { + meta: decoded.meta, + temp_package_path: output_dir, + }, + DeclarativeTrigger { + format, + triggers: decoded.declarative_triggers, + }, + )) + } +} + +#[cfg(feature = "dynamic-plugins")] +impl PackageUnpacker { + pub fn new() -> Result { + let manifests = load_decoder_manifests(decoders::DECODERS_DIR, decoders::MANIFEST_EXTENSION)?; + + Ok(Self { + manifests, + decoders: HashMap::new(), }) } @@ -122,26 +143,55 @@ impl PackageUnpacker { } } -#[cfg(not(feature = "dynamic-plugins"))] +/// 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 { - /// Always fails: this build contains no decoder loading path. pub fn new() -> Result { - let _ = (decoders::DECODERS_DIR, decoders::MANIFEST_EXTENSION); - let _: Option _> = - None:: Result, DecoderError>>; - let _ = load_decoder_manifests; + Ok(Self { + decoders: static_decoders(), + }) + } + + fn format_for(&self, package_path: &str) -> Result { + let extension = Path::new(package_path) + .extension() + .and_then(|extension| extension.to_str()) + .ok_or_else(|| DecoderError::UnknownFormat(package_path.to_owned()))?; + self.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 + .iter() + .find(|(name, _, _)| *name == format) + .map(|(_, _, decoder)| decoder) + .ok_or_else(|| DecoderError::UnknownFormat(format.to_owned())) + } +} + +#[cfg(all(not(feature = "dynamic-plugins"), not(feature = "builtin-decoders")))] +impl PackageUnpacker { + /// Always fails: this build contains no decoder loading path. + pub fn new() -> Result { Err(DecoderError::NoDecoders) } pub fn unpack_all( &mut self, _package_paths: &[String], _tmp_path: &str, _cancel: &CancelToken, - ) -> Result, DecoderError> { + ) -> Result<(Vec, Vec), DecoderError> { Err(DecoderError::NoDecoders) } } -#[cfg(feature = "dynamic-plugins")] +#[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(); diff --git a/lib/lib/src/plugin/mod.rs b/lib/lib/src/plugin/mod.rs index 410ff32..da50095 100644 --- a/lib/lib/src/plugin/mod.rs +++ b/lib/lib/src/plugin/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception pub mod boot; pub mod decoder; diff --git a/lib/lib/src/scripts/error.rs b/lib/lib/src/scripts/error.rs index c32a456..b01bf6d 100644 --- a/lib/lib/src/scripts/error.rs +++ b/lib/lib/src/scripts/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// 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; diff --git a/lib/lib/src/scripts/file.rs b/lib/lib/src/scripts/file.rs index 9b6c97c..1bf68da 100644 --- a/lib/lib/src/scripts/file.rs +++ b/lib/lib/src/scripts/file.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::collections::HashMap; @@ -12,7 +12,7 @@ use upac_abi::hook::ProgressEventBuilder; use crate::errors::CommonError; use crate::orchestrator::stage::{ConcurrentStage, RollbackGuard}; use crate::scripts::error::HookError; -use crate::scripts::native::{NativeTrigger, Operation, Timing}; +use crate::scripts::pipeline::{Operation, PipelineTrigger, Timing}; use crate::scripts::primitive::{Primitive, Step}; #[derive(Debug, Clone, Deserialize)] @@ -42,9 +42,9 @@ impl HookFile { Ok(file) } - pub fn native_trigger(&self) -> Option { + pub fn pipeline_trigger(&self) -> Option { match (self.operation, self.timing) { - (Some(operation), Some(timing)) => Some(NativeTrigger { operation, timing }), + (Some(operation), Some(timing)) => Some(PipelineTrigger { operation, timing }), _ => None, } } diff --git a/lib/lib/src/scripts/load.rs b/lib/lib/src/scripts/load.rs index 22dcffd..98943b3 100644 --- a/lib/lib/src/scripts/load.rs +++ b/lib/lib/src/scripts/load.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs; use std::str::from_utf8; diff --git a/lib/lib/src/scripts/mod.rs b/lib/lib/src/scripts/mod.rs index f16418e..00ce6fa 100644 --- a/lib/lib/src/scripts/mod.rs +++ b/lib/lib/src/scripts/mod.rs @@ -1,27 +1,32 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +use std::collections::{HashMap, HashSet}; use upac_abi::hook::{CancelToken, ProgressEventBuilder}; +use upac_types::DeclarativeTrigger; + use crate::errors::CommonError; use crate::layout::hooks::{HOOK_EXTENSION, HOOKS_DIR, ROOT_CERT_PATH, SIGNATURE_EXTENSION}; use crate::orchestrator::stage::{ConcurrentStage, RollbackGuard, Stage}; use crate::orchestrator::{Context, Orchestrator, ParallelOrchestrator}; +use crate::plugin::decoder::triggers::build_trigger_table; use crate::scripts::error::HookError; use crate::scripts::load::load_hooks; -use crate::scripts::native::NativeTrigger; +use crate::scripts::pipeline::{PipelineTrigger, Timing}; use crate::scripts::primitive::Primitive; pub mod error; pub mod file; pub mod load; -pub mod native; +pub mod pipeline; pub mod primitive; pub struct HookStage { - pub trigger: NativeTrigger, + pub trigger: PipelineTrigger, } impl + Send + 'static> Stage for HookStage { @@ -33,11 +38,43 @@ impl + Send + 'static> Stage for HookStage { let hooks = load_hooks(HOOKS_DIR, ROOT_CERT_PATH, HOOK_EXTENSION, SIGNATURE_EXTENSION).map_err(CommonError::from)?; - let matched: Vec>> = hooks - .into_iter() - .filter(|hook_file| hook_file.native_trigger() == Some(self.trigger)) - .map(|hook_file| Box::new(hook_file) as Box>) - .collect(); + let matched: Vec>> = if self.trigger.timing == Timing::Declarative { + let packages = context + .get::>() + .ok_or(CommonError::MissingResult)?; + + let mut tables = HashMap::new(); + for package in packages { + if !tables.contains_key(&package.format) { + let table = build_trigger_table(&hooks, &package.format).map_err(CommonError::from)?; + tables.insert(package.format.clone(), table); + } + } + + let mut matched_ids = HashSet::new(); + for package in packages { + let table = &tables[&package.format]; + + for trigger_name in &package.triggers { + if let Some(entry) = table.iter().find(|entry| &entry.name == trigger_name) { + matched_ids.insert(entry.hook_id); + } + } + } + + hooks + .into_iter() + .enumerate() + .filter(|(index, _)| matched_ids.contains(&(*index as u16))) + .map(|(_, hook_file)| Box::new(hook_file) as Box>) + .collect() + } else { + hooks + .into_iter() + .filter(|hook_file| hook_file.pipeline_trigger() == Some(self.trigger)) + .map(|hook_file| Box::new(hook_file) as Box>) + .collect() + }; ParallelOrchestrator::new(matched, runtime) .run_concurrent(context, cancel) diff --git a/lib/lib/src/scripts/native.rs b/lib/lib/src/scripts/pipeline.rs similarity index 73% rename from lib/lib/src/scripts/native.rs rename to lib/lib/src/scripts/pipeline.rs index 4b39d77..2dd895c 100644 --- a/lib/lib/src/scripts/native.rs +++ b/lib/lib/src/scripts/pipeline.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use serde::Deserialize; @@ -21,15 +21,16 @@ pub enum Operation { pub enum Timing { Pre, Post, + Declarative, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct NativeTrigger { +pub struct PipelineTrigger { pub operation: Operation, pub timing: Timing, } -impl NativeTrigger { +impl PipelineTrigger { pub fn pre(operation: Operation) -> Self { Self { operation, @@ -43,4 +44,11 @@ impl NativeTrigger { timing: Timing::Post, } } + + pub fn declarative(operation: Operation) -> Self { + Self { + operation, + timing: Timing::Declarative, + } + } } diff --git a/lib/lib/src/scripts/primitive.rs b/lib/lib/src/scripts/primitive.rs index aacf055..a49321f 100644 --- a/lib/lib/src/scripts/primitive.rs +++ b/lib/lib/src/scripts/primitive.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::{File, remove_file, rename}; use std::os::unix::fs::symlink; diff --git a/lib/lib/src/search.rs b/lib/lib/src/search.rs index 8a06fcc..d2b6740 100644 --- a/lib/lib/src/search.rs +++ b/lib/lib/src/search.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use regex::Regex; diff --git a/lib/lib/src/unmutated/diff/comparing.rs b/lib/lib/src/unmutated/diff/comparing.rs index 6180211..2eda9d1 100644 --- a/lib/lib/src/unmutated/diff/comparing.rs +++ b/lib/lib/src/unmutated/diff/comparing.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::collections::HashMap; diff --git a/lib/lib/src/unmutated/diff/error.rs b/lib/lib/src/unmutated/diff/error.rs index 4d5b892..b0af659 100644 --- a/lib/lib/src/unmutated/diff/error.rs +++ b/lib/lib/src/unmutated/diff/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/unmutated/diff/mod.rs b/lib/lib/src/unmutated/diff/mod.rs index 4623cfa..611a7d1 100644 --- a/lib/lib/src/unmutated/diff/mod.rs +++ b/lib/lib/src/unmutated/diff/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; diff --git a/lib/lib/src/unmutated/diff/preparing.rs b/lib/lib/src/unmutated/diff/preparing.rs index f01f487..6248089 100644 --- a/lib/lib/src/unmutated/diff/preparing.rs +++ b/lib/lib/src/unmutated/diff/preparing.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::DiffFileSource; use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/unmutated/diff_config/comparing.rs b/lib/lib/src/unmutated/diff_config/comparing.rs index 681c17f..4ac7634 100644 --- a/lib/lib/src/unmutated/diff_config/comparing.rs +++ b/lib/lib/src/unmutated/diff_config/comparing.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::FileDiffKind; use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/unmutated/diff_config/error.rs b/lib/lib/src/unmutated/diff_config/error.rs index d47cd31..639655b 100644 --- a/lib/lib/src/unmutated/diff_config/error.rs +++ b/lib/lib/src/unmutated/diff_config/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/unmutated/diff_config/mod.rs b/lib/lib/src/unmutated/diff_config/mod.rs index e477c7f..f956a7f 100644 --- a/lib/lib/src/unmutated/diff_config/mod.rs +++ b/lib/lib/src/unmutated/diff_config/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; diff --git a/lib/lib/src/unmutated/diff_config/preparing.rs b/lib/lib/src/unmutated/diff_config/preparing.rs index 941935e..9bd53b6 100644 --- a/lib/lib/src/unmutated/diff_config/preparing.rs +++ b/lib/lib/src/unmutated/diff_config/preparing.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/unmutated/diff_packages/comparing.rs b/lib/lib/src/unmutated/diff_packages/comparing.rs index a3e0187..030ab89 100644 --- a/lib/lib/src/unmutated/diff_packages/comparing.rs +++ b/lib/lib/src/unmutated/diff_packages/comparing.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::collections::HashMap; diff --git a/lib/lib/src/unmutated/diff_packages/error.rs b/lib/lib/src/unmutated/diff_packages/error.rs index 733d8d2..46f9e7e 100644 --- a/lib/lib/src/unmutated/diff_packages/error.rs +++ b/lib/lib/src/unmutated/diff_packages/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/unmutated/diff_packages/mod.rs b/lib/lib/src/unmutated/diff_packages/mod.rs index 79824f1..fc80c1f 100644 --- a/lib/lib/src/unmutated/diff_packages/mod.rs +++ b/lib/lib/src/unmutated/diff_packages/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; diff --git a/lib/lib/src/unmutated/diff_packages/preparing.rs b/lib/lib/src/unmutated/diff_packages/preparing.rs index 0d2faa3..0490eec 100644 --- a/lib/lib/src/unmutated/diff_packages/preparing.rs +++ b/lib/lib/src/unmutated/diff_packages/preparing.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/unmutated/diff_prefix/comparing.rs b/lib/lib/src/unmutated/diff_prefix/comparing.rs index c0228d2..b748f08 100644 --- a/lib/lib/src/unmutated/diff_prefix/comparing.rs +++ b/lib/lib/src/unmutated/diff_prefix/comparing.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; use upac_abi::{DiffFileSource, FileDiffKind}; diff --git a/lib/lib/src/unmutated/diff_prefix/error.rs b/lib/lib/src/unmutated/diff_prefix/error.rs index 3a1b76d..130e0bd 100644 --- a/lib/lib/src/unmutated/diff_prefix/error.rs +++ b/lib/lib/src/unmutated/diff_prefix/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/unmutated/diff_prefix/mod.rs b/lib/lib/src/unmutated/diff_prefix/mod.rs index 40719de..9c97b14 100644 --- a/lib/lib/src/unmutated/diff_prefix/mod.rs +++ b/lib/lib/src/unmutated/diff_prefix/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; diff --git a/lib/lib/src/unmutated/diff_prefix/preparing.rs b/lib/lib/src/unmutated/diff_prefix/preparing.rs index 6a4130e..8eaea34 100644 --- a/lib/lib/src/unmutated/diff_prefix/preparing.rs +++ b/lib/lib/src/unmutated/diff_prefix/preparing.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/unmutated/list_config/error.rs b/lib/lib/src/unmutated/list_config/error.rs index 295006b..3cb2a22 100644 --- a/lib/lib/src/unmutated/list_config/error.rs +++ b/lib/lib/src/unmutated/list_config/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/unmutated/list_config/fetching.rs b/lib/lib/src/unmutated/list_config/fetching.rs index 7812dd8..3581595 100644 --- a/lib/lib/src/unmutated/list_config/fetching.rs +++ b/lib/lib/src/unmutated/list_config/fetching.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/unmutated/list_config/mod.rs b/lib/lib/src/unmutated/list_config/mod.rs index c48f2db..84a5dd7 100644 --- a/lib/lib/src/unmutated/list_config/mod.rs +++ b/lib/lib/src/unmutated/list_config/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; diff --git a/lib/lib/src/unmutated/list_history/error.rs b/lib/lib/src/unmutated/list_history/error.rs index 41b23f8..ea6cb7a 100644 --- a/lib/lib/src/unmutated/list_history/error.rs +++ b/lib/lib/src/unmutated/list_history/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/unmutated/list_history/fetching.rs b/lib/lib/src/unmutated/list_history/fetching.rs index b626a38..4f9e67a 100644 --- a/lib/lib/src/unmutated/list_history/fetching.rs +++ b/lib/lib/src/unmutated/list_history/fetching.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/unmutated/list_history/mod.rs b/lib/lib/src/unmutated/list_history/mod.rs index 30d4505..f9bac4b 100644 --- a/lib/lib/src/unmutated/list_history/mod.rs +++ b/lib/lib/src/unmutated/list_history/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; diff --git a/lib/lib/src/unmutated/list_packages/error.rs b/lib/lib/src/unmutated/list_packages/error.rs index ef81d1b..849773d 100644 --- a/lib/lib/src/unmutated/list_packages/error.rs +++ b/lib/lib/src/unmutated/list_packages/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/unmutated/list_packages/fetching.rs b/lib/lib/src/unmutated/list_packages/fetching.rs index 79e0695..2478e4d 100644 --- a/lib/lib/src/unmutated/list_packages/fetching.rs +++ b/lib/lib/src/unmutated/list_packages/fetching.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/unmutated/list_packages/mod.rs b/lib/lib/src/unmutated/list_packages/mod.rs index 560ebcd..bd33bbc 100644 --- a/lib/lib/src/unmutated/list_packages/mod.rs +++ b/lib/lib/src/unmutated/list_packages/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; diff --git a/lib/lib/src/unmutated/list_prefix/error.rs b/lib/lib/src/unmutated/list_prefix/error.rs index 97c8054..5791728 100644 --- a/lib/lib/src/unmutated/list_prefix/error.rs +++ b/lib/lib/src/unmutated/list_prefix/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/unmutated/list_prefix/fetching.rs b/lib/lib/src/unmutated/list_prefix/fetching.rs index 888e78c..5328292 100644 --- a/lib/lib/src/unmutated/list_prefix/fetching.rs +++ b/lib/lib/src/unmutated/list_prefix/fetching.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/unmutated/list_prefix/mod.rs b/lib/lib/src/unmutated/list_prefix/mod.rs index 156381a..a784440 100644 --- a/lib/lib/src/unmutated/list_prefix/mod.rs +++ b/lib/lib/src/unmutated/list_prefix/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; diff --git a/lib/lib/src/unmutated/mod.rs b/lib/lib/src/unmutated/mod.rs index 7b79e54..e315fb5 100644 --- a/lib/lib/src/unmutated/mod.rs +++ b/lib/lib/src/unmutated/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception pub mod diff; pub mod diff_config; diff --git a/lib/lib/src/unmutated/search_files/error.rs b/lib/lib/src/unmutated/search_files/error.rs index bae9e28..327879a 100644 --- a/lib/lib/src/unmutated/search_files/error.rs +++ b/lib/lib/src/unmutated/search_files/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/unmutated/search_files/mod.rs b/lib/lib/src/unmutated/search_files/mod.rs index db78317..9fbd871 100644 --- a/lib/lib/src/unmutated/search_files/mod.rs +++ b/lib/lib/src/unmutated/search_files/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; diff --git a/lib/lib/src/unmutated/search_files/searching.rs b/lib/lib/src/unmutated/search_files/searching.rs index 88824bc..1630bde 100644 --- a/lib/lib/src/unmutated/search_files/searching.rs +++ b/lib/lib/src/unmutated/search_files/searching.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/unmutated/search_in_meta/error.rs b/lib/lib/src/unmutated/search_in_meta/error.rs index e561cb2..cd58832 100644 --- a/lib/lib/src/unmutated/search_in_meta/error.rs +++ b/lib/lib/src/unmutated/search_in_meta/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/unmutated/search_in_meta/mod.rs b/lib/lib/src/unmutated/search_in_meta/mod.rs index c890aa0..acb5bfc 100644 --- a/lib/lib/src/unmutated/search_in_meta/mod.rs +++ b/lib/lib/src/unmutated/search_in_meta/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; diff --git a/lib/lib/src/unmutated/search_in_meta/searching.rs b/lib/lib/src/unmutated/search_in_meta/searching.rs index bbc0fff..6808e9c 100644 --- a/lib/lib/src/unmutated/search_in_meta/searching.rs +++ b/lib/lib/src/unmutated/search_in_meta/searching.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; 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 f8b60e3..9a5fe78 100644 --- a/lib/lib/src/unmutated/search_in_package_files/error.rs +++ b/lib/lib/src/unmutated/search_in_package_files/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; 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 4a50dfc..4869b7b 100644 --- a/lib/lib/src/unmutated/search_in_package_files/mod.rs +++ b/lib/lib/src/unmutated/search_in_package_files/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; 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 27ab8d5..a01a632 100644 --- a/lib/lib/src/unmutated/search_in_package_files/searching.rs +++ b/lib/lib/src/unmutated/search_in_package_files/searching.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/src/unmutated/search_meta/error.rs b/lib/lib/src/unmutated/search_meta/error.rs index ff91ce5..b0ae8d8 100644 --- a/lib/lib/src/unmutated/search_meta/error.rs +++ b/lib/lib/src/unmutated/search_meta/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::ErrorKind; diff --git a/lib/lib/src/unmutated/search_meta/mod.rs b/lib/lib/src/unmutated/search_meta/mod.rs index 4ae0da9..1c14122 100644 --- a/lib/lib/src/unmutated/search_meta/mod.rs +++ b/lib/lib/src/unmutated/search_meta/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; diff --git a/lib/lib/src/unmutated/search_meta/searching.rs b/lib/lib/src/unmutated/search_meta/searching.rs index de9b698..870ee79 100644 --- a/lib/lib/src/unmutated/search_meta/searching.rs +++ b/lib/lib/src/unmutated/search_meta/searching.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::hook::{CancelToken, ProgressEventBuilder}; diff --git a/lib/lib/tests/composefs_file.rs b/lib/lib/tests/composefs_file.rs index fb551a0..6cf1148 100644 --- a/lib/lib/tests/composefs_file.rs +++ b/lib/lib/tests/composefs_file.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::{File, create_dir_all, write}; use std::os::unix::fs::symlink; diff --git a/lib/lib/tests/composefs_overlay.rs b/lib/lib/tests/composefs_overlay.rs index 2a59236..f1e20b7 100644 --- a/lib/lib/tests/composefs_overlay.rs +++ b/lib/lib/tests/composefs_overlay.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::{File, create_dir_all, write}; diff --git a/lib/lib/tests/composefs_repository.rs b/lib/lib/tests/composefs_repository.rs index aa2fdd6..26a13a2 100644 --- a/lib/lib/tests/composefs_repository.rs +++ b/lib/lib/tests/composefs_repository.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::{File, write}; use std::io::Read; diff --git a/lib/lib/tests/config_merge.rs b/lib/lib/tests/config_merge.rs index 02d6fe0..c8383bb 100644 --- a/lib/lib/tests/config_merge.rs +++ b/lib/lib/tests/config_merge.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::{File, write}; diff --git a/lib/lib/tests/database_record.rs b/lib/lib/tests/database_record.rs index a23f91c..a99eed6 100644 --- a/lib/lib/tests/database_record.rs +++ b/lib/lib/tests/database_record.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use tempfile::{Builder, TempDir}; use upac::database::error::DeployRecordError; diff --git a/lib/lib/tests/deploy_digest.rs b/lib/lib/tests/deploy_digest.rs index b138ce5..eeb1966 100644 --- a/lib/lib/tests/deploy_digest.rs +++ b/lib/lib/tests/deploy_digest.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac::deploy::digest::current_prefix_digest; use upac::deploy::error::SysrootError; diff --git a/lib/lib/tests/orchestrator.rs b/lib/lib/tests/orchestrator.rs index a3631b3..097190a 100644 --- a/lib/lib/tests/orchestrator.rs +++ b/lib/lib/tests/orchestrator.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::any::TypeId; use std::sync::atomic::{AtomicUsize, Ordering}; diff --git a/lib/lib/tests/plugin_decoder.rs b/lib/lib/tests/plugin_decoder.rs index 8d6ebfe..6ffae21 100644 --- a/lib/lib/tests/plugin_decoder.rs +++ b/lib/lib/tests/plugin_decoder.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::collections::HashMap; use std::fs::write; diff --git a/lib/lib/tests/scripts_hook.rs b/lib/lib/tests/scripts_hook.rs index 29c9c12..2b431bb 100644 --- a/lib/lib/tests/scripts_hook.rs +++ b/lib/lib/tests/scripts_hook.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::{read_link, write}; use std::path::{Path, PathBuf}; @@ -10,7 +10,7 @@ use tempfile::{Builder, TempDir}; use upac::scripts::error::HookError; use upac::scripts::file::HookFile; use upac::scripts::load::load_hooks; -use upac::scripts::native::{NativeTrigger, Operation, Timing}; +use upac::scripts::pipeline::{Operation, PipelineTrigger, Timing}; use upac::scripts::primitive::Step; use upac_pki::generate::{Identity, SigningIdentity, generate_root, generate_signing_cert}; use upac_pki::signature::HookSignature; @@ -37,17 +37,20 @@ fn write_signed_hook(dir: &Path, name: &str, hook_toml: &str, signing: &SigningI } #[test] -fn hook_file_parse_succeeds_for_native_trigger() { +fn hook_file_parse_succeeds_for_pipeline_trigger() { let hook_file = HookFile::parse("operation = \"install\"\ntiming = \"pre\"\n").unwrap(); - assert_eq!(hook_file.native_trigger(), Some(NativeTrigger::pre(Operation::Install))); + assert_eq!( + hook_file.pipeline_trigger(), + Some(PipelineTrigger::pre(Operation::Install)) + ); } #[test] fn hook_file_parse_succeeds_for_trigger_map() { let hook_file = HookFile::parse("[triggers]\ndeb = [\"postinst\"]\n").unwrap(); - assert_eq!(hook_file.native_trigger(), None); + assert_eq!(hook_file.pipeline_trigger(), None); assert_eq!(hook_file.triggers.get("deb").unwrap(), &vec!["postinst".to_owned()]); } @@ -80,17 +83,17 @@ fn hook_file_parse_fails_on_malformed_toml() { } #[test] -fn native_trigger_pre_and_post_set_correct_timing() { +fn pipeline_trigger_pre_and_post_set_correct_timing() { assert_eq!( - NativeTrigger::pre(Operation::Update), - NativeTrigger { + PipelineTrigger::pre(Operation::Update), + PipelineTrigger { operation: Operation::Update, timing: Timing::Pre, } ); assert_eq!( - NativeTrigger::post(Operation::Update), - NativeTrigger { + PipelineTrigger::post(Operation::Update), + PipelineTrigger { operation: Operation::Update, timing: Timing::Post, } @@ -233,7 +236,10 @@ fn load_hooks_returns_matching_hook_for_signed_valid_file() { .unwrap(); assert_eq!(hooks.len(), 1); - assert_eq!(hooks[0].native_trigger(), Some(NativeTrigger::pre(Operation::Install))); + assert_eq!( + hooks[0].pipeline_trigger(), + Some(PipelineTrigger::pre(Operation::Install)) + ); } #[test] diff --git a/lib/macro/Cargo.toml b/lib/macro/Cargo.toml index 18f5d49..31751de 100644 --- a/lib/macro/Cargo.toml +++ b/lib/macro/Cargo.toml @@ -1,17 +1,25 @@ # SPDX-FileCopyrightText: 2026 JustPav # SPDX-FileCopyrightText: 2026 SmoothTeam # -# SPDX-License-Identifier: LGPL-3.0-or-later +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception [package] name = "upac-macro" description = "Derive macros for C-ABI struct plumbing used internally by upac-lib and upac-abi" - version.workspace = true + edition.workspace = true -repository.workspace = true +rust-version.workspace = true + license.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true + +keywords.workspace = true +categories.workspace = true + include = ["/src"] [lints] diff --git a/lib/macro/src/c_free/mod.rs b/lib/macro/src/c_free/mod.rs index 2f64b3f..e3df09a 100644 --- a/lib/macro/src/c_free/mod.rs +++ b/lib/macro/src/c_free/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception //! `#[derive(CFree)]` — generates an unsafe `free()` that releases every //! owned buffer a C-ABI struct holds. This is the reflection-over-fields diff --git a/lib/macro/src/c_new/mod.rs b/lib/macro/src/c_new/mod.rs index 18097f5..a40f027 100644 --- a/lib/macro/src/c_new/mod.rs +++ b/lib/macro/src/c_new/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception //! `#[derive(CNew)]` — generates a `new(...)` constructor for a C-ABI struct. Every field except //! `struct_size` becomes a parameter, in declaration order; `struct_size` itself is computed via diff --git a/lib/macro/src/c_to_rust/mod.rs b/lib/macro/src/c_to_rust/mod.rs index 7b7cccd..96bed9c 100644 --- a/lib/macro/src/c_to_rust/mod.rs +++ b/lib/macro/src/c_to_rust/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception //! `#[derive(CToRust)]` — generates `impl From<&CRust> for Rust`, converting //! a C-ABI struct into an owned Rust domain type without validation diff --git a/lib/macro/src/c_try_to_rust/mod.rs b/lib/macro/src/c_try_to_rust/mod.rs index 8d0eab2..8858e25 100644 --- a/lib/macro/src/c_try_to_rust/mod.rs +++ b/lib/macro/src/c_try_to_rust/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception //! `#[derive(CTryToRust)]` — generates `impl TryFrom<&CRust> for Rust`, //! validating the C-ABI struct first and then converting it into an owned diff --git a/lib/macro/src/c_validate/mod.rs b/lib/macro/src/c_validate/mod.rs index 8571548..0076080 100644 --- a/lib/macro/src/c_validate/mod.rs +++ b/lib/macro/src/c_validate/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception //! `#[derive(CValidate)]` — generates an unsafe `validate()` that checks //! `struct_size` and every field, driven by `#[optional]`/`#[non_empty]` diff --git a/lib/macro/src/common.rs b/lib/macro/src/common.rs index 90a83cb..9bf290c 100644 --- a/lib/macro/src/common.rs +++ b/lib/macro/src/common.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception //! Consts and type-introspection helpers shared by more than one derive //! macro in this crate. @@ -29,7 +29,6 @@ pub(crate) const VALIDATABLE_COMPOSITES: &[&str] = &[ "CHistoryEntry", "CRequestBase", "CDependency", - "CTriggerEntry", "CSetupBase", "CPartitionMount", "CPartitionSpec", diff --git a/lib/macro/src/from_stage_index/mod.rs b/lib/macro/src/from_stage_index/mod.rs index c55c0e4..d32912d 100644 --- a/lib/macro/src/from_stage_index/mod.rs +++ b/lib/macro/src/from_stage_index/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception //! `#[derive(FromStageIndex)]` — generates `from_stage_index(usize) -> Self` //! for a fieldless enum, mapping an orchestrator stage index to the variant diff --git a/lib/macro/src/json_codec/mod.rs b/lib/macro/src/json_codec/mod.rs index 81e5a5a..a6d3d5d 100644 --- a/lib/macro/src/json_codec/mod.rs +++ b/lib/macro/src/json_codec/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception //! `#[derive(JsonCodec)]` — generates `to_json()`/`from_json()` for //! storing a struct as a `serde_json::Value` (used for on-disk records diff --git a/lib/macro/src/lib.rs b/lib/macro/src/lib.rs index c73b353..9db7cd0 100644 --- a/lib/macro/src/lib.rs +++ b/lib/macro/src/lib.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception //! Proc-macro crate for UPAC. Each derive reflects over a struct's (or //! enum's) fields at compile time to generate boilerplate that would diff --git a/lib/macro/src/redb_codec/mod.rs b/lib/macro/src/redb_codec/mod.rs index 5698be0..85ae95b 100644 --- a/lib/macro/src/redb_codec/mod.rs +++ b/lib/macro/src/redb_codec/mod.rs @@ -1,126 +1,19 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception -//! `#[derive(RedbCodec)]` — generates `encode_into()`/`decode_from()` for -//! storing a struct as a `redb` value via the crate's own byte layout. +//! `#[derive(RedbCodec)]` — generates an `upac_types::codec::RedbCodable` impl for storing a +//! struct as a `redb` value via the crate's own byte layout. Field dispatch is uniform: every +//! field just calls its own type's `RedbCodable` impl, resolved by the real Rust compiler (not +//! this macro) from the field's declared type — so `String`/`u32`/`u64`/`bool`/`Option`/ +//! `Vec`/any other `RedbCodable` composite all just work without this macro ever needing to +//! special-case a field's type by name. use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; -use syn::{Data, DeriveInput, Error, Fields, Ident, PathSegment, Type, TypeArray, parse_macro_input}; - -fn array_codec(ident: &Ident, ty: &Type, array: &TypeArray) -> (TokenStream2, TokenStream2) { - let len = &array.len; - - let encode = quote! { - buf.extend_from_slice(&value.#ident); - }; - let decode = quote! { - let #ident: #ty = data[*offset..*offset + (#len)].try_into().unwrap(); - *offset += #len; - }; - - (encode, decode) -} - -fn string_codec(ident: &Ident) -> (TokenStream2, TokenStream2) { - ( - quote! { crate::codec::write_len_prefixed(buf, value.#ident.as_bytes()); }, - quote! { let #ident = crate::codec::read_str(data, offset); }, - ) -} - -fn u32_codec(ident: &Ident) -> (TokenStream2, TokenStream2) { - ( - quote! { crate::codec::write_u32(buf, value.#ident); }, - quote! { let #ident = crate::codec::read_u32(data, offset); }, - ) -} - -fn u64_codec(ident: &Ident) -> (TokenStream2, TokenStream2) { - ( - quote! { crate::codec::write_u64(buf, value.#ident); }, - quote! { let #ident = crate::codec::read_u64(data, offset); }, - ) -} - -fn bool_codec(ident: &Ident) -> (TokenStream2, TokenStream2) { - ( - quote! { crate::codec::write_bool(buf, value.#ident); }, - quote! { let #ident = crate::codec::read_bool(data, offset); }, - ) -} - -fn option_codec(ident: &Ident) -> (TokenStream2, TokenStream2) { - ( - quote! { crate::codec::write_opt_str(buf, value.#ident.as_deref()); }, - quote! { let #ident = crate::codec::read_opt_str(data, offset); }, - ) -} - -fn vec_codec(ident: &Ident) -> (TokenStream2, TokenStream2) { - ( - quote! { crate::codec::write_vec_u32(buf, &value.#ident); }, - quote! { let #ident = crate::codec::read_vec_u32(data, offset); }, - ) -} - -fn composite_codec(ident: &Ident, ty: &Type) -> (TokenStream2, TokenStream2) { - ( - quote! { #ty::encode_into(buf, &value.#ident); }, - quote! { let #ident = #ty::decode_from(data, offset); }, - ) -} - -fn field_path_codec(ident: &Ident, segment: &PathSegment, ty: &Type) -> (TokenStream2, TokenStream2) { - match segment.ident.to_string().as_str() { - "String" => string_codec(ident), - "u32" => u32_codec(ident), - "u64" => u64_codec(ident), - "bool" => bool_codec(ident), - "Option" => option_codec(ident), - "Vec" => vec_codec(ident), - _ => composite_codec(ident, ty), - } -} - -fn field_codec(ident: &Ident, ty: &Type) -> (TokenStream2, TokenStream2) { - if let Type::Array(array) = ty { - return array_codec(ident, ty, array); - } - - let Type::Path(type_path) = ty else { - let error = quote! { compile_error!("RedbCodec: unsupported field type"); }; - return (error.clone(), error); - }; - - let Some(segment) = type_path.path.segments.last() else { - let error = quote! { compile_error!("RedbCodec: unsupported field type"); }; - return (error.clone(), error); - }; - - field_path_codec(ident, segment, ty) -} - -fn codec_impl(name: &Ident, encodes: &[TokenStream2], decodes: &[TokenStream2], names: &[Ident]) -> TokenStream2 { - quote! { - impl #name { - pub fn encode_into(buf: &mut Vec, value: &#name) { - #(#encodes)* - } - - pub fn decode_from(data: &[u8], offset: &mut usize) -> #name { - #(#decodes)* - - #name { - #(#names),* - } - } - } - } -} +use syn::{Data, DeriveInput, Error, Fields, Ident, Type, TypeArray, parse_macro_input}; pub(crate) fn expand(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); @@ -161,3 +54,46 @@ pub(crate) fn expand(input: TokenStream) -> TokenStream { codec_impl(name, &encodes, &decodes, &names).into() } + +fn codec_impl(name: &Ident, encodes: &[TokenStream2], decodes: &[TokenStream2], names: &[Ident]) -> TokenStream2 { + quote! { + impl crate::codec::RedbCodable for #name { + fn redb_encode(&self, buf: &mut Vec) { + #(#encodes)* + } + + fn redb_decode(data: &[u8], offset: &mut usize) -> #name { + #(#decodes)* + + #name { + #(#names),* + } + } + } + } +} + +fn array_codec(ident: &Ident, ty: &Type, array: &TypeArray) -> (TokenStream2, TokenStream2) { + let len = &array.len; + + let encode = quote! { + buf.extend_from_slice(&self.#ident); + }; + let decode = quote! { + let #ident: #ty = data[*offset..*offset + (#len)].try_into().unwrap(); + *offset += #len; + }; + + (encode, decode) +} + +fn field_codec(ident: &Ident, ty: &Type) -> (TokenStream2, TokenStream2) { + if let Type::Array(array) = ty { + return array_codec(ident, ty, array); + } + + let encode = quote! { crate::codec::RedbCodable::redb_encode(&self.#ident, buf); }; + let decode = quote! { let #ident: #ty = crate::codec::RedbCodable::redb_decode(data, offset); }; + + (encode, decode) +} diff --git a/lib/macro/src/rust_to_c/mod.rs b/lib/macro/src/rust_to_c/mod.rs index ae65bfa..cc13f0d 100644 --- a/lib/macro/src/rust_to_c/mod.rs +++ b/lib/macro/src/rust_to_c/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception //! `#[derive(RustToC)]` — generates `impl From for CRust`, converting //! an owned Rust domain type into its C-ABI mirror (outbound direction). diff --git a/lib/macro/src/stage_key/mod.rs b/lib/macro/src/stage_key/mod.rs index 753d29a..8c12c28 100644 --- a/lib/macro/src/stage_key/mod.rs +++ b/lib/macro/src/stage_key/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception //! `#[derive(StageKey)]` — generates `stage_key(&self) -> &'static str` for a //! fieldless enum, converting each variant's PascalCase name into a diff --git a/lib/pki/Cargo.toml b/lib/pki/Cargo.toml index c10245a..d4e1d76 100644 --- a/lib/pki/Cargo.toml +++ b/lib/pki/Cargo.toml @@ -1,17 +1,25 @@ # SPDX-FileCopyrightText: 2026 JustPav # SPDX-FileCopyrightText: 2026 SmoothTeam # -# SPDX-License-Identifier: LGPL-3.0-or-later +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception [package] name = "upac-pki" description = "Ed25519/X.509 signing and verification for upac hook files" - version.workspace = true + edition.workspace = true -repository.workspace = true +rust-version.workspace = true + license.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true + +keywords.workspace = true +categories.workspace = true + include = ["/src"] [lints] diff --git a/lib/pki/src/error.rs b/lib/pki/src/error.rs index 590489a..9c70a17 100644 --- a/lib/pki/src/error.rs +++ b/lib/pki/src/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::array::TryFromSliceError; use std::fmt::{Display, Formatter, Result}; diff --git a/lib/pki/src/generate.rs b/lib/pki/src/generate.rs index 94aca93..42862ca 100644 --- a/lib/pki/src/generate.rs +++ b/lib/pki/src/generate.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use der::pem::LineEnding; use der::{Decode, DecodePem, Encode, EncodePem}; diff --git a/lib/pki/src/lib.rs b/lib/pki/src/lib.rs index bac4aee..0863275 100644 --- a/lib/pki/src/lib.rs +++ b/lib/pki/src/lib.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception pub mod error; pub mod generate; diff --git a/lib/pki/src/signature.rs b/lib/pki/src/signature.rs index 877696e..0de299a 100644 --- a/lib/pki/src/signature.rs +++ b/lib/pki/src/signature.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use der::pem::LineEnding; use der::{Decode, DecodePem, Encode, EncodePem}; diff --git a/lib/pki/tests/signature.rs b/lib/pki/tests/signature.rs index 8f8b8fc..041ccc0 100644 --- a/lib/pki/tests/signature.rs +++ b/lib/pki/tests/signature.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_pki::error::PkiError; use upac_pki::generate::{Identity, RootIdentity, SigningIdentity, generate_root, generate_signing_cert}; diff --git a/lib/setup/Cargo.toml b/lib/setup/Cargo.toml index 4bb9d75..03b5fd1 100644 --- a/lib/setup/Cargo.toml +++ b/lib/setup/Cargo.toml @@ -1,16 +1,25 @@ # SPDX-FileCopyrightText: 2026 JustPav # SPDX-FileCopyrightText: 2026 SmoothTeam # -# SPDX-License-Identifier: LGPL-3.0-or-later +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception [package] name = "upac-setup" description = "First-boot bootstrap installer: initial disk layout for a blank system" version.workspace = true + edition.workspace = true -repository.workspace = true +rust-version.workspace = true + license.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true + +keywords.workspace = true +categories.workspace = true + [lints] workspace = true diff --git a/lib/setup/build.rs b/lib/setup/build.rs index 09acc5d..781f2c9 100644 --- a/lib/setup/build.rs +++ b/lib/setup/build.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::env::var; use std::error::Error; diff --git a/lib/setup/lib.toml b/lib/setup/lib.toml index cf8e559..e421d77 100644 --- a/lib/setup/lib.toml +++ b/lib/setup/lib.toml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2026 JustPav # SPDX-FileCopyrightText: 2026 SmoothTeam # -# SPDX-License-Identifier: LGPL-3.0-or-later +# 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. diff --git a/lib/setup/src/data.rs b/lib/setup/src/data.rs index 15f4af0..5d04c77 100644 --- a/lib/setup/src/data.rs +++ b/lib/setup/src/data.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::os::raw::c_void; diff --git a/lib/setup/src/error.rs b/lib/setup/src/error.rs index 5198706..ce7cf2e 100644 --- a/lib/setup/src/error.rs +++ b/lib/setup/src/error.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::io::{Error as IoError, ErrorKind as IoErrorKind}; diff --git a/lib/setup/src/format.rs b/lib/setup/src/format.rs index 701434f..95d3631 100644 --- a/lib/setup/src/format.rs +++ b/lib/setup/src/format.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::ffi::OsStr; use std::fs::OpenOptions; diff --git a/lib/setup/src/genesis/deploy.rs b/lib/setup/src/genesis/deploy.rs index f089f54..93ed3bd 100644 --- a/lib/setup/src/genesis/deploy.rs +++ b/lib/setup/src/genesis/deploy.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::create_dir_all; diff --git a/lib/setup/src/genesis/entry.rs b/lib/setup/src/genesis/entry.rs index e0eb50e..e1fd4be 100644 --- a/lib/setup/src/genesis/entry.rs +++ b/lib/setup/src/genesis/entry.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::File; use std::io::Read; diff --git a/lib/setup/src/genesis/meta.rs b/lib/setup/src/genesis/meta.rs index 4e97b82..75514f6 100644 --- a/lib/setup/src/genesis/meta.rs +++ b/lib/setup/src/genesis/meta.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::path::Path; diff --git a/lib/setup/src/genesis/mod.rs b/lib/setup/src/genesis/mod.rs index fd14158..17beff2 100644 --- a/lib/setup/src/genesis/mod.rs +++ b/lib/setup/src/genesis/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::path::{Path, PathBuf}; diff --git a/lib/setup/src/genesis/trees.rs b/lib/setup/src/genesis/trees.rs index 15a00ea..25f0c29 100644 --- a/lib/setup/src/genesis/trees.rs +++ b/lib/setup/src/genesis/trees.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::env::temp_dir; use std::fs::{File, write}; diff --git a/lib/setup/src/lib.rs b/lib/setup/src/lib.rs index 388b36d..47c23a0 100644 --- a/lib/setup/src/lib.rs +++ b/lib/setup/src/lib.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception pub mod data; pub mod error; diff --git a/lib/setup/src/meta.rs b/lib/setup/src/meta.rs index 0df5393..5141aa7 100644 --- a/lib/setup/src/meta.rs +++ b/lib/setup/src/meta.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// 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; diff --git a/lib/setup/src/partition.rs b/lib/setup/src/partition.rs index 718eb7e..f7875d9 100644 --- a/lib/setup/src/partition.rs +++ b/lib/setup/src/partition.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::{File, OpenOptions}; use std::path::{Path, PathBuf}; diff --git a/lib/setup/src/target.rs b/lib/setup/src/target.rs index 52cb64d..0000ada 100644 --- a/lib/setup/src/target.rs +++ b/lib/setup/src/target.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::create_dir_all; use std::path::{Path, PathBuf}; diff --git a/lib/types/Cargo.toml b/lib/types/Cargo.toml index 9135248..38eac82 100644 --- a/lib/types/Cargo.toml +++ b/lib/types/Cargo.toml @@ -1,17 +1,25 @@ # SPDX-FileCopyrightText: 2026 JustPav # SPDX-FileCopyrightText: 2026 SmoothTeam # -# SPDX-License-Identifier: LGPL-3.0-or-later +# SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception [package] name = "upac-types" description = "Rust-native domain types shared between upac-lib and its direct Rust consumers, independent of the C ABI" - version.workspace = true + edition.workspace = true -repository.workspace = true +rust-version.workspace = true + license.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true + +keywords.workspace = true +categories.workspace = true + include = ["/src"] [lints] diff --git a/lib/types/src/codec.rs b/lib/types/src/codec.rs index c73c3a2..1045aee 100644 --- a/lib/types/src/codec.rs +++ b/lib/types/src/codec.rs @@ -1,7 +1,91 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception + +pub trait RedbCodable: Sized { + fn redb_encode(&self, buf: &mut Vec); + fn redb_decode(data: &[u8], offset: &mut usize) -> Self; +} + +impl RedbCodable for String { + fn redb_encode(&self, buf: &mut Vec) { + write_len_prefixed(buf, self.as_bytes()); + } + + fn redb_decode(data: &[u8], offset: &mut usize) -> Self { + read_str(data, offset) + } +} + +impl RedbCodable for u32 { + fn redb_encode(&self, buf: &mut Vec) { + write_u32(buf, *self); + } + + fn redb_decode(data: &[u8], offset: &mut usize) -> Self { + read_u32(data, offset) + } +} + +impl RedbCodable for u64 { + fn redb_encode(&self, buf: &mut Vec) { + write_u64(buf, *self); + } + + fn redb_decode(data: &[u8], offset: &mut usize) -> Self { + read_u64(data, offset) + } +} + +impl RedbCodable for bool { + fn redb_encode(&self, buf: &mut Vec) { + write_bool(buf, *self); + } + + fn redb_decode(data: &[u8], offset: &mut usize) -> Self { + read_bool(data, offset) + } +} + +impl RedbCodable for Option { + fn redb_encode(&self, buf: &mut Vec) { + match self { + Some(value) => { + buf.push(1); + value.redb_encode(buf); + } + None => buf.push(0), + } + } + + fn redb_decode(data: &[u8], offset: &mut usize) -> Self { + let flag = data[*offset]; + *offset += 1; + + if flag == 1 { + Some(T::redb_decode(data, offset)) + } else { + None + } + } +} + +impl RedbCodable for Vec { + fn redb_encode(&self, buf: &mut Vec) { + write_u32(buf, self.len() as u32); + + for element in self { + element.redb_encode(buf); + } + } + + fn redb_decode(data: &[u8], offset: &mut usize) -> Self { + let len = read_u32(data, offset); + + (0..len).map(|_| T::redb_decode(data, offset)).collect() + } +} pub(crate) fn write_bool(buf: &mut Vec, value: bool) { buf.push(u8::from(value)); @@ -63,10 +147,3 @@ pub(crate) fn read_len_prefixed<'a>(data: &'a [u8], offset: &mut usize) -> &'a [ pub(crate) fn read_str(data: &[u8], offset: &mut usize) -> String { String::from_utf8_lossy(read_len_prefixed(data, offset)).into_owned() } - -pub(crate) fn read_opt_str(data: &[u8], offset: &mut usize) -> Option { - let flag = data[*offset]; - *offset += 1; - - if flag == 1 { Some(read_str(data, offset)) } else { None } -} diff --git a/lib/types/src/lib.rs b/lib/types/src/lib.rs index 6cce16d..8c02356 100644 --- a/lib/types/src/lib.rs +++ b/lib/types/src/lib.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::cmp::Ordering; @@ -20,6 +20,8 @@ use upac_abi::{DiffFileSource, FileDiffKind, FsKind, PackageDiffKind}; use upac_macro::{CTryToRust, RedbCodec, RustToC}; +use crate::codec::RedbCodable; + pub mod codec; pub mod settings; pub mod states; @@ -157,6 +159,33 @@ pub struct PackageTemp { 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 { @@ -172,7 +201,7 @@ pub struct PackageMeta { pub installed_size: u64, } -#[derive(Debug, Clone, CTryToRust)] +#[derive(Debug, Clone, CTryToRust, RustToC)] pub struct Dependency { pub name: String, pub constraint: u8, @@ -195,12 +224,12 @@ pub enum FileEntryScope { Config = 1, } -impl FileEntryScope { - pub fn encode_into(buf: &mut Vec, value: &FileEntryScope) { - buf.push(*value as u8); +impl RedbCodable for FileEntryScope { + fn redb_encode(&self, buf: &mut Vec) { + buf.push(*self as u8); } - pub fn decode_from(data: &[u8], offset: &mut usize) -> FileEntryScope { + fn redb_decode(data: &[u8], offset: &mut usize) -> FileEntryScope { let value = data[*offset]; *offset += 1; diff --git a/lib/types/src/settings.rs b/lib/types/src/settings.rs index 4b18fb4..978833d 100644 --- a/lib/types/src/settings.rs +++ b/lib/types/src/settings.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use std::fs::read_to_string; diff --git a/lib/types/src/states.rs b/lib/types/src/states.rs index 2f998ac..78c5ed8 100644 --- a/lib/types/src/states.rs +++ b/lib/types/src/states.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use upac_abi::error::{CommandState, ErrorDomain}; diff --git a/lib/types/src/tests.rs b/lib/types/src/tests.rs index fed9973..31f0982 100644 --- a/lib/types/src/tests.rs +++ b/lib/types/src/tests.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: LGPL-3.0-or-later WITH LGPL-3.0-linking-exception use super::*; @@ -17,10 +17,10 @@ fn version_redb_round_trip_preserves_value() { let original = sample_version(); let mut buf = Vec::new(); - Version::encode_into(&mut buf, &original); + original.redb_encode(&mut buf); let mut offset = 0; - let restored = Version::decode_from(&buf, &mut offset); + let restored = Version::redb_decode(&buf, &mut offset); assert_eq!(restored, original); assert_eq!(offset, buf.len()); @@ -145,10 +145,10 @@ fn file_entry_redb_round_trip_preserves_value() { }; let mut buf = Vec::new(); - FileEntry::encode_into(&mut buf, &original); + original.redb_encode(&mut buf); let mut offset = 0; - let restored = FileEntry::decode_from(&buf, &mut offset); + let restored = FileEntry::redb_decode(&buf, &mut offset); assert_eq!(restored.path, original.path); assert_eq!(restored.is_user, original.is_user); diff --git a/lib/types/tests/conversions.rs b/lib/types/tests/conversions.rs index 6bbaeb8..7718d89 100644 --- a/lib/types/tests/conversions.rs +++ b/lib/types/tests/conversions.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 JustPav // SPDX-FileCopyrightText: 2026 SmoothTeam // -// SPDX-License-Identifier: LGPL-3.0-or-later +// 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}; diff --git a/user/setup-cli/Cargo.toml b/user/setup-cli/Cargo.toml index 462d410..ea0b30d 100644 --- a/user/setup-cli/Cargo.toml +++ b/user/setup-cli/Cargo.toml @@ -7,8 +7,17 @@ name = "upac-setup-cli" description = "First-boot bootstrap installer CLI, driving upac-setup" version.workspace = true + edition.workspace = true +rust-version.workspace = true + +readme.workspace = true +homepage.workspace = true repository.workspace = true + +keywords.workspace = true +categories.workspace = true + license = "GPL-3.0-only" [lints] diff --git a/user/sign-cli/Cargo.toml b/user/sign-cli/Cargo.toml index 36accc7..036d391 100644 --- a/user/sign-cli/Cargo.toml +++ b/user/sign-cli/Cargo.toml @@ -7,8 +7,17 @@ name = "upac-sign-cli" description = "CLI tool for signing upac hook files (and other artifacts) with Ed25519 certificates" version.workspace = true + edition.workspace = true +rust-version.workspace = true + +readme.workspace = true +homepage.workspace = true repository.workspace = true + +keywords.workspace = true +categories.workspace = true + license = "GPL-3.0-only" [lints] diff --git a/user/upac-cli/Cargo.toml b/user/upac-cli/Cargo.toml index 4c3da4f..3c85605 100644 --- a/user/upac-cli/Cargo.toml +++ b/user/upac-cli/Cargo.toml @@ -7,8 +7,17 @@ name = "upac-cli" description = "Package manager for installing any type of package in Linux, as well as registering binary file rollbacks based on composefs, written in Rust" version.workspace = true + edition.workspace = true +rust-version.workspace = true + +readme.workspace = true +homepage.workspace = true repository.workspace = true + +keywords.workspace = true +categories.workspace = true + license = "GPL-3.0-only" [lints] diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index e2eecb8..9d62e33 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -5,11 +5,16 @@ [package] name = "upac-xtask" +description = "Developer tooling for upac's own build: repo-internal style lints and other checks, invoked via `cargo xtask`" version = "0.1.0" edition = "2024" publish = false +[lib] +name = "upac_xtask" +path = "src/lib.rs" + [[bin]] name = "xtask" path = "src/main.rs" diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs new file mode 100644 index 0000000..5a26560 --- /dev/null +++ b/xtask/src/lib.rs @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: GPL-3.0-only + +pub mod error; +pub mod gen_tree; +pub mod lint_style; diff --git a/xtask/src/lint_style/cargo_toml_dependency_order.rs b/xtask/src/lint_style/cargo_toml_dependency_order.rs index 2abd61f..29ffa69 100644 --- a/xtask/src/lint_style/cargo_toml_dependency_order.rs +++ b/xtask/src/lint_style/cargo_toml_dependency_order.rs @@ -24,7 +24,7 @@ struct Entry { key_count: usize, } -pub(super) fn check(path: &Path, contents: &str) -> Vec { +pub fn check(path: &Path, contents: &str) -> Vec { let entries = dependency_entries(contents); let mut violations = Vec::new(); diff --git a/xtask/src/lint_style/cargo_toml_package_order.rs b/xtask/src/lint_style/cargo_toml_package_order.rs index daa85a1..6b522c0 100644 --- a/xtask/src/lint_style/cargo_toml_package_order.rs +++ b/xtask/src/lint_style/cargo_toml_package_order.rs @@ -15,7 +15,7 @@ struct Field { is_workspace: bool, } -pub(super) fn check(path: &Path, contents: &str) -> Vec { +pub fn check(path: &Path, contents: &str) -> Vec { let Some(fields) = package_fields(contents) else { return Vec::new(); }; diff --git a/xtask/src/lint_style/extern_fn_position.rs b/xtask/src/lint_style/extern_fn_position.rs index b57a461..1e402d2 100644 --- a/xtask/src/lint_style/extern_fn_position.rs +++ b/xtask/src/lint_style/extern_fn_position.rs @@ -16,7 +16,7 @@ enum Tier { Private, } -pub(super) fn check(path: &Path, contents: &str) -> Vec { +pub fn check(path: &Path, contents: &str) -> Vec { let tiers: Vec<(usize, Tier)> = contents .lines() .enumerate() diff --git a/xtask/src/lint_style/macro_visibility_adjacency.rs b/xtask/src/lint_style/macro_visibility_adjacency.rs index 5ad71dd..cf9b772 100644 --- a/xtask/src/lint_style/macro_visibility_adjacency.rs +++ b/xtask/src/lint_style/macro_visibility_adjacency.rs @@ -14,7 +14,7 @@ struct MacroBlock<'a> { end_line: usize, } -pub(super) fn check(path: &Path, contents: &str) -> Vec { +pub fn check(path: &Path, contents: &str) -> Vec { let lines: Vec<&str> = contents.lines().collect(); let macros = find_macro_blocks(&lines); diff --git a/xtask/src/lint_style/mod.rs b/xtask/src/lint_style/mod.rs index facac80..84ff39b 100644 --- a/xtask/src/lint_style/mod.rs +++ b/xtask/src/lint_style/mod.rs @@ -15,13 +15,13 @@ use std::process::ExitCode; use crate::error::XtaskError; -mod cargo_toml_dependency_order; -mod cargo_toml_package_order; -mod extern_fn_position; -mod macro_visibility_adjacency; -mod no_pub_use_reexport; -mod toml_config_field_order; -mod violation; +pub mod cargo_toml_dependency_order; +pub mod cargo_toml_package_order; +pub mod extern_fn_position; +pub mod macro_visibility_adjacency; +pub mod no_pub_use_reexport; +pub mod toml_config_field_order; +pub mod violation; mod walk; pub fn run() -> Result { diff --git a/xtask/src/lint_style/no_pub_use_reexport.rs b/xtask/src/lint_style/no_pub_use_reexport.rs index bcc38f1..79aabb3 100644 --- a/xtask/src/lint_style/no_pub_use_reexport.rs +++ b/xtask/src/lint_style/no_pub_use_reexport.rs @@ -10,7 +10,7 @@ use crate::lint_style::violation::Violation; const RULE: &str = "no-pub-use-reexport"; -pub(super) fn check(path: &Path, contents: &str) -> Vec { +pub fn check(path: &Path, contents: &str) -> Vec { let public_modules = public_module_names(contents); let mut violations = Vec::new(); diff --git a/xtask/src/lint_style/toml_config_field_order.rs b/xtask/src/lint_style/toml_config_field_order.rs index b898e6a..2a0bbee 100644 --- a/xtask/src/lint_style/toml_config_field_order.rs +++ b/xtask/src/lint_style/toml_config_field_order.rs @@ -16,7 +16,7 @@ enum Kind { Number, } -pub(super) fn check(path: &Path, contents: &str) -> Vec { +pub fn check(path: &Path, contents: &str) -> Vec { let lines: Vec<&str> = contents.lines().collect(); let mut violations = Vec::new(); diff --git a/xtask/src/main.rs b/xtask/src/main.rs index a14818e..4f675d8 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -7,11 +7,8 @@ use std::process::ExitCode; use clap::{Parser, Subcommand}; -use self::error::XtaskError; - -mod error; -mod gen_tree; -mod lint_style; +use upac_xtask::error::XtaskError; +use upac_xtask::{gen_tree, lint_style}; #[derive(Parser)] #[command(name = "xtask")] diff --git a/xtask/tests/lint_style.rs b/xtask/tests/lint_style.rs new file mode 100644 index 0000000..dc707e8 --- /dev/null +++ b/xtask/tests/lint_style.rs @@ -0,0 +1,199 @@ +// SPDX-FileCopyrightText: 2026 JustPav +// SPDX-FileCopyrightText: 2026 SmoothTeam +// +// SPDX-License-Identifier: GPL-3.0-only + +use std::path::Path; + +use upac_xtask::lint_style::{ + cargo_toml_dependency_order, cargo_toml_package_order, extern_fn_position, macro_visibility_adjacency, + no_pub_use_reexport, toml_config_field_order, +}; + +mod no_pub_use_reexport_rule { + use super::*; + + #[test] + fn allows_reexport_of_a_private_module() { + let contents = "mod foo;\npub use self::foo::Bar;\n"; + + let violations = no_pub_use_reexport::check(Path::new("lib.rs"), contents); + + assert!(violations.is_empty()); + } + + #[test] + fn flags_reexport_of_an_already_pub_module() { + let contents = "pub mod foo;\npub use self::foo::Bar;\n"; + + let violations = no_pub_use_reexport::check(Path::new("lib.rs"), contents); + + assert_eq!(violations.len(), 1); + assert_eq!(violations[0].rule, "no-pub-use-reexport"); + assert_eq!(violations[0].line, 2); + } +} + +mod extern_fn_position_rule { + use super::*; + + #[test] + fn allows_extern_fns_before_pub_and_private_fns() { + let contents = "unsafe extern \"C\" fn a() {}\npub fn b() {}\nfn c() {}\n"; + + let violations = extern_fn_position::check(Path::new("lib.rs"), contents); + + assert!(violations.is_empty()); + } + + #[test] + fn flags_extern_fn_after_pub_fn() { + let contents = "pub fn a() {}\nunsafe extern \"C\" fn b() {}\n"; + + let violations = extern_fn_position::check(Path::new("lib.rs"), contents); + + assert_eq!(violations.len(), 1); + assert!(violations[0].message.contains("extern fns go first")); + } + + #[test] + fn flags_pub_fn_after_private_fn_when_an_extern_fn_is_present() { + let contents = "unsafe extern \"C\" fn a() {}\nfn b() {}\npub fn c() {}\n"; + + let violations = extern_fn_position::check(Path::new("lib.rs"), contents); + + assert_eq!(violations.len(), 1); + assert!(violations[0].message.contains("pub fns go before private ones")); + } + + #[test] + fn ignores_pub_private_ordering_when_no_extern_fn_is_present() { + let contents = "fn a() {}\npub fn b() {}\n"; + + let violations = extern_fn_position::check(Path::new("lib.rs"), contents); + + assert!(violations.is_empty()); + } +} + +mod macro_visibility_adjacency_rule { + use super::*; + + #[test] + fn allows_use_immediately_after_the_macros_closing_brace() { + let contents = "macro_rules! foo {\n () => {};\n}\npub(crate) use foo;\n"; + + let violations = macro_visibility_adjacency::check(Path::new("lib.rs"), contents); + + assert!(violations.is_empty()); + } + + #[test] + fn flags_use_separated_from_the_macros_closing_brace() { + let contents = "macro_rules! foo {\n () => {};\n}\n\npub(crate) use foo;\n"; + + let violations = macro_visibility_adjacency::check(Path::new("lib.rs"), contents); + + assert_eq!(violations.len(), 1); + assert_eq!(violations[0].rule, "macro-visibility-adjacency"); + } +} + +mod cargo_toml_dependency_order_rule { + use super::*; + + #[test] + fn allows_upac_then_workspace_bracketed_then_bracketed_then_bare() { + let contents = "[dependencies]\nupac-abi = { workspace = true }\n\nfoo = { workspace = true }\n\nbar = { \ + version = \"1\", features = [\"x\"] }\nbaz = { version = \"1\" }\n\nqux = \"1\"\n"; + + let violations = cargo_toml_dependency_order::check(Path::new("Cargo.toml"), contents); + + assert!(violations.is_empty()); + } + + #[test] + fn flags_bare_dependency_before_a_bracketed_one() { + let contents = "[dependencies]\nqux = \"1\"\nbar = { version = \"1\" }\n"; + + let violations = cargo_toml_dependency_order::check(Path::new("Cargo.toml"), contents); + + assert_eq!(violations.len(), 1); + assert!(violations[0].message.contains("out of its expected group")); + } + + #[test] + fn flags_ascending_key_count_among_bracketed_dependencies() { + let contents = + "[dependencies]\nbar = { version = \"1\" }\nbaz = { version = \"1\", features = [\"x\"] }\n"; + + let violations = cargo_toml_dependency_order::check(Path::new("Cargo.toml"), contents); + + assert_eq!(violations.len(), 1); + assert!(violations[0].message.contains("descending key count")); + } +} + +mod cargo_toml_package_order_rule { + use super::*; + + #[test] + fn allows_workspace_fields_before_a_trailing_custom_override() { + let contents = "[package]\nname = \"foo\"\nversion.workspace = true\n\nreadme.workspace = true\n\nlicense \ + = \"GPL-3.0-only\"\n"; + + let violations = cargo_toml_package_order::check(Path::new("Cargo.toml"), contents); + + assert!(violations.is_empty()); + } + + #[test] + fn flags_name_not_being_the_first_field() { + let contents = "[package]\nversion.workspace = true\nname = \"foo\"\n"; + + let violations = cargo_toml_package_order::check(Path::new("Cargo.toml"), contents); + + assert!(violations.iter().any(|violation| violation.message.contains("must be the first field"))); + } + + #[test] + fn flags_workspace_field_after_a_custom_override() { + let contents = "[package]\nname = \"foo\"\nlicense = \"GPL-3.0-only\"\nreadme.workspace = true\n"; + + let violations = cargo_toml_package_order::check(Path::new("Cargo.toml"), contents); + + assert!(violations.iter().any(|violation| violation.message.contains("workspace fields go first"))); + } +} + +mod toml_config_field_order_rule { + use super::*; + + #[test] + fn allows_bool_then_string_then_number() { + let contents = "[section]\nflag = true\nname = \"foo\"\ncount = 1\n"; + + let violations = toml_config_field_order::check(Path::new("lib.toml"), contents); + + assert!(violations.is_empty()); + } + + #[test] + fn flags_bool_after_string() { + let contents = "[section]\nname = \"foo\"\nflag = true\n"; + + let violations = toml_config_field_order::check(Path::new("lib.toml"), contents); + + assert_eq!(violations.len(), 1); + assert!(violations[0].message.contains("bool")); + } + + #[test] + fn resets_ordering_per_section() { + let contents = "[a]\ncount = 1\n\n[b]\nflag = true\n"; + + let violations = toml_config_field_order::check(Path::new("lib.toml"), contents); + + assert!(violations.is_empty()); + } +}