Skip to main content

fidius_host/
package.rs

1// Copyright 2026 Colliery, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Host-side package integration: manifest loading, discovery, and building.
16
17use std::path::{Path, PathBuf};
18
19use ed25519_dalek::{Signature, Verifier, VerifyingKey};
20use fidius_core::package::{PackageError, PackageManifest};
21use serde::de::DeserializeOwned;
22
23/// Load and validate a package manifest against a host-defined schema.
24///
25/// This is the primary entry point for host applications working with packages.
26/// The type parameter `M` is the host's metadata schema — if the manifest's
27/// `[metadata]` section doesn't deserialize into `M`, this returns an error.
28///
29/// # Example
30///
31/// ```ignore
32/// #[derive(Deserialize)]
33/// struct MySchema {
34///     category: String,
35///     min_host_version: String,
36/// }
37///
38/// let manifest = load_package_manifest::<MySchema>(Path::new("./packages/blur/"))?;
39/// assert_eq!(manifest.metadata.category, "image-processing");
40/// ```
41pub fn load_package_manifest<M: DeserializeOwned>(
42    dir: &Path,
43) -> Result<PackageManifest<M>, PackageError> {
44    fidius_core::package::load_manifest(dir)
45}
46
47/// Discover packages in a directory.
48///
49/// Scans `dir` for subdirectories containing a `package.toml` file.
50/// Returns the paths to each package directory found.
51pub fn discover_packages(dir: &Path) -> Result<Vec<PathBuf>, PackageError> {
52    let mut packages = Vec::new();
53
54    if !dir.is_dir() {
55        return Ok(packages);
56    }
57
58    let entries = std::fs::read_dir(dir).map_err(PackageError::Io)?;
59
60    for entry in entries {
61        let entry = entry.map_err(PackageError::Io)?;
62        let path = entry.path();
63        if path.is_dir() && path.join("package.toml").exists() {
64            packages.push(path);
65        }
66    }
67
68    packages.sort();
69    Ok(packages)
70}
71
72/// Verify a source package's signature against trusted public keys.
73///
74/// Recomputes the package digest from files on disk and verifies the
75/// `package.sig` signature against the provided trusted keys.
76///
77/// # Errors
78///
79/// - `PackageError::SignatureNotFound` — if `package.sig` doesn't exist
80/// - `PackageError::SignatureInvalid` — if no trusted key verifies the signature
81pub fn verify_package(dir: &Path, trusted_keys: &[VerifyingKey]) -> Result<(), PackageError> {
82    let sig_path = dir.join("package.sig");
83    if !sig_path.exists() {
84        return Err(PackageError::SignatureNotFound {
85            path: dir.display().to_string(),
86        });
87    }
88
89    let sig_bytes: [u8; 64] =
90        std::fs::read(&sig_path)?
91            .try_into()
92            .map_err(|_| PackageError::SignatureInvalid {
93                path: dir.display().to_string(),
94            })?;
95
96    let signature = Signature::from_bytes(&sig_bytes);
97    let digest = fidius_core::package::package_digest(dir)?;
98
99    for key in trusted_keys {
100        if key.verify(&digest, &signature).is_ok() {
101            return Ok(());
102        }
103    }
104
105    Err(PackageError::SignatureInvalid {
106        path: dir.display().to_string(),
107    })
108}
109
110/// Extract a `.fid` archive and validate its contents.
111///
112/// Delegates to [`fidius_core::package::unpack_package`], which applies strict
113/// safety defaults: entries with `..` components, absolute paths, symlinks, or
114/// hardlinks are rejected, and the cumulative decompressed size is capped to
115/// guard against decompression bombs. Emits a `tracing::warn!` if the unpacked
116/// package has no `package.sig`.
117///
118/// For archives that legitimately exceed the default caps, call
119/// [`fidius_core::package::unpack_package_with_options`] directly.
120///
121/// # Example
122///
123/// ```ignore
124/// let pkg_dir = unpack_fid(Path::new("blur-filter-1.0.0.fid"), Path::new("./plugins/"))?;
125/// let manifest = load_package_manifest::<MySchema>(&pkg_dir)?;
126/// ```
127pub fn unpack_fid(archive: &Path, dest: &Path) -> Result<PathBuf, PackageError> {
128    let pkg_dir = fidius_core::package::unpack_package(archive, dest)?;
129
130    if !pkg_dir.join("package.sig").exists() {
131        #[cfg(feature = "tracing")]
132        tracing::warn!(
133            package = %pkg_dir.display(),
134            "unpacked package is unsigned (no package.sig found)"
135        );
136    }
137
138    Ok(pkg_dir)
139}
140
141/// Build a package by running `cargo build` inside the package directory.
142///
143/// Returns the path to the compiled cdylib on success.
144pub fn build_package(dir: &Path, release: bool) -> Result<PathBuf, PackageError> {
145    let cargo_toml = dir.join("Cargo.toml");
146    if !cargo_toml.exists() {
147        return Err(PackageError::BuildFailed(format!(
148            "Cargo.toml not found in {}",
149            dir.display()
150        )));
151    }
152
153    let mut cmd = std::process::Command::new("cargo");
154    cmd.arg("build").arg("--manifest-path").arg(&cargo_toml);
155    if release {
156        cmd.arg("--release");
157    }
158
159    let output = cmd.output().map_err(PackageError::Io)?;
160
161    if !output.status.success() {
162        let stderr = String::from_utf8_lossy(&output.stderr);
163        return Err(PackageError::BuildFailed(stderr.to_string()));
164    }
165
166    let profile = if release { "release" } else { "debug" };
167    let target_dir = dir.join("target").join(profile);
168
169    // Find the cdylib in the target directory
170    let dylib_ext = if cfg!(target_os = "macos") {
171        "dylib"
172    } else if cfg!(target_os = "windows") {
173        "dll"
174    } else {
175        "so"
176    };
177
178    // Look for any file with the right extension
179    if let Ok(entries) = std::fs::read_dir(&target_dir) {
180        for entry in entries.flatten() {
181            let path = entry.path();
182            if path.extension().and_then(|e| e.to_str()) == Some(dylib_ext) {
183                return Ok(path);
184            }
185        }
186    }
187
188    Err(PackageError::BuildFailed(format!(
189        "build succeeded but no .{} file found in {}",
190        dylib_ext,
191        target_dir.display()
192    )))
193}