zlayer-builder 0.13.0

Dockerfile parsing and buildah-based container image building
Documentation
//! Cross-search: when the package resolver can't map a Linux package to a
//! Homebrew/Chocolatey equivalent, fire a lightweight "unfulfilled request" to
//! `ZPackageIndex` (`{base}/linux/request`). A server-side hourly scheduler
//! drains the queue and resolves the mapping (Repology), so a FUTURE native
//! sandbox/HCS build resolves the package without the VM fallback. The build
//! itself does zero apt/buildah work — this is one cheap HMAC POST.
//!
//! There is exactly one HMAC signer in the workspace
//! ([`zlayer_toolchain::package_index::sign`]) and the endpoint base lives in
//! [`zlayer_types::package_index::PackageIndexConfig`]; this module reuses both
//! rather than re-deriving either.

use zlayer_toolchain::package_index::sign;
use zlayer_types::package_index::PackageIndexConfig;

/// Build-time reposync HMAC secret; `None` disables the POST. The single home
/// for the *value* — the signing *algorithm* lives in `zlayer-toolchain`.
const REPOSYNC_HMAC_SECRET: Option<&str> = option_env!("ZLAYER_REPOSYNC_HMAC_SECRET");

/// Fire-and-forget: report a Linux package the resolver could not map, so the
/// index can backfill it. `manager` is `"apt"`|`"apk"`; `distro` e.g.
/// `"debian-12"`. No-op when the HMAC secret isn't baked in. Never
/// blocks/fails the build.
pub fn report_unfulfilled(distro: &str, manager: &str, name: &str) {
    let Some(secret) = REPOSYNC_HMAC_SECRET.filter(|s| !s.is_empty()) else {
        return;
    };
    let (distro, manager, name) = (distro.to_string(), manager.to_string(), name.to_string());
    let endpoint = PackageIndexConfig::from_env().linux_request_url();
    tokio::spawn(async move {
        // JSON-escape via serde_json to be safe with package names.
        let payload =
            serde_json::json!({ "distro": distro, "manager": manager, "name": name }).to_string();
        let signature = sign(secret, payload.as_bytes());
        let _ = reqwest::Client::new()
            .post(&endpoint)
            .header("x-reposync-signature", signature)
            .header("content-type", "application/json")
            .body(payload)
            .send()
            .await;
    });
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn shared_signer_matches_reposync_reference_vector() {
        // Same reference vector the toolchain signer is tested against:
        //   crypto.createHmac('sha256','test-secret').update('{"foo":"bar"}').digest('hex')
        // Proves the DRY consolidation onto zlayer_toolchain::package_index::sign
        // preserves the exact signature the index expects.
        let sig = sign("test-secret", br#"{"foo":"bar"}"#);
        assert_eq!(
            sig,
            "sha256=9b1abf7d901bda91325d00f6b397fb0dc257937939b27d4dc67848ab9e08f6c0"
        );
    }

    #[test]
    fn payload_shape_is_stable() {
        // The request body is a flat {distro, manager, name} object; serde_json
        // escapes package names safely (no manual string interpolation).
        let payload = serde_json::json!({
            "distro": "debian-12",
            "manager": "apt",
            "name": "lib\"weird\"-dev",
        })
        .to_string();
        let parsed: serde_json::Value = serde_json::from_str(&payload).expect("valid JSON");
        assert_eq!(parsed["distro"], "debian-12");
        assert_eq!(parsed["manager"], "apt");
        assert_eq!(parsed["name"], "lib\"weird\"-dev");
    }

    #[test]
    fn endpoint_derives_from_index_base() {
        // The request endpoint is the PackageIndexConfig-derived URL, not a
        // hardcoded const.
        assert_eq!(
            PackageIndexConfig::default().linux_request_url(),
            "https://packages.zlayer.dev/linux/request"
        );
    }
}