Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

crates/dwarves-slsa/src/spec.rs

//! The release spec: what to build, from which pinned commits, in which image.
//! It is the build's `externalParameters`.

use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

use crate::{bail, Result};

pub const DEFAULT_FORGE: &str = "ssh://git@git.gunnar.rs:2222";
pub const DEFAULT_WEB: &str = "https://git.gunnar.rs";

fn default_forge() -> String {
    DEFAULT_FORGE.to_string()
}

fn default_web() -> String {
    DEFAULT_WEB.to_string()
}

/// One pinned repository.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Repo {
    pub name: String,
    pub org: String,
    /// The full commit id (40 hex for sha1 repositories, 64 for sha256 ones).
    pub commit: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub branch: Option<String>,
}

/// One crate to package, in dependency order.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CrateStep {
    pub name: String,
    pub version: String,
    pub repo: String,
    /// The crate's directory in the repository.
    pub path: String,
}

/// One binary, built from a crate of the release AS PACKAGED (what `cargo install
/// --locked` builds).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BinaryStep {
    #[serde(rename = "crate")]
    pub crate_name: String,
    pub bin: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub features: Vec<String>,
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub no_default_features: bool,
}

/// One binary built from the release's SOURCES (a workspace member, as a Build Thing
/// variant builds it), not from a packaged crate.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceBinary {
    /// The workspace's manifest in the snapshot (`gunnar/Cargo.toml`).
    pub manifest: String,
    pub package: String,
    pub bin: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub features: Vec<String>,
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub no_default_features: bool,
}

/// ★ One example of a workspace run from the release's SOURCES (`cargo run --release
/// --locked --offline --example <example> -- <args>`) in a sealed, network-off build
/// container; the files it writes into `{out}` are the release's subjects (Heimdall's
/// verify-bundle guest: its `dist` example builds the three wasm components). The
/// example's own cargo inherits the vendored, offline cargo home.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceExample {
    /// The workspace's manifest in the snapshot (`heimdall/guests/verify-bundle/Cargo.toml`).
    pub manifest: String,
    pub example: String,
    /// The example's arguments; `{out}` (alone or as `{out}/<path>`) is the directory
    /// whose files become subjects.
    #[serde(default)]
    pub args: Vec<String>,
    /// Every file the example must write, relative to `{out}` (and to the release's
    /// `out/`): exactly these become subjects.
    pub outputs: Vec<String>,
}

/// The placeholder of a [`WorkspaceExample`]'s output directory in its arguments.
pub const OUT_PLACEHOLDER: &str = "{out}";

/// ★ A release.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReleaseSpec {
    /// The release's name (`holger-2.0.1`).
    pub name: String,
    /// The build image BY DIGEST (`name@sha256:<64 hex>`).
    pub image: String,
    /// Where the runner clones from.
    #[serde(default = "default_forge")]
    pub forge: String,
    /// The repositories' public base (the provenance URIs).
    #[serde(default = "default_web")]
    pub web: String,
    /// Where an org's repositories are cloned from instead of `forge` (release mode:
    /// the runner's config sets it; a request cannot).
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub forge_by_org: BTreeMap<String, String>,
    #[serde(default, rename = "repo")]
    pub repos: Vec<Repo>,
    /// The workspaces whose locks are vendored and recorded (`holger/Cargo.toml`).
    #[serde(default, rename = "lock")]
    pub locks: Vec<String>,
    #[serde(default, rename = "crate")]
    pub crates: Vec<CrateStep>,
    #[serde(default, rename = "binary")]
    pub binaries: Vec<BinaryStep>,
    #[serde(default, rename = "workspace_binary")]
    pub workspace_binaries: Vec<WorkspaceBinary>,
    #[serde(default, rename = "workspace_example", skip_serializing_if = "Vec::is_empty")]
    pub workspace_examples: Vec<WorkspaceExample>,
}

fn is_hex(s: &str) -> bool {
    s.bytes()
        .all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c))
}

/// A repository, crate or bin name: letters, digits, `-`, `_`, `.`, not starting with `.`.
fn safe_name(s: &str) -> bool {
    !s.is_empty()
        && !s.starts_with('.')
        && s.bytes()
            .all(|c| c.is_ascii_alphanumeric() || b"-_.+".contains(&c))
}

/// A relative path that stays inside the snapshot.
fn safe_rel(p: &str) -> bool {
    !p.is_empty()
        && !p.starts_with('/')
        && !p.contains('\\')
        && p.split('/')
            .all(|c| !c.is_empty() && c != ".." && c != "." && !c.contains(char::is_whitespace))
}

impl ReleaseSpec {
    /// Parse and [`validate`](Self::validate).
    pub fn parse(text: &str) -> Result<Self> {
        let s: Self =
            toml::from_str(text).map_err(|e| crate::Error(format!("release spec: {e}")))?;
        s.validate()?;
        Ok(s)
    }

    /// ★ Refuse what cannot be pinned or would reach outside the snapshot: an image
    /// not by digest, a short or non-hex commit, an unsafe or doubled repository
    /// name, a crate of an unpinned repository, a path that is absolute or climbs
    /// with `..`, no lock, nothing to build, a binary of a crate not in the release.
    pub fn validate(&self) -> Result<()> {
        if !safe_name(&self.name) {
            bail!(
                "release name {:?}: letters, digits, '-', '_', '.' only",
                self.name
            );
        }
        match self.image.rsplit_once("@sha256:") {
            Some((n, d)) if !n.is_empty() && d.len() == 64 && is_hex(d) => {}
            _ => bail!(
                "image {:?} is not pinned by digest (name@sha256:<64 hex>)",
                self.image
            ),
        }
        let mut seen = std::collections::BTreeSet::new();
        for r in &self.repos {
            if !safe_name(&r.name) || !safe_name(&r.org) {
                bail!(
                    "repository name {:?}/{:?}: letters, digits, '-', '_', '.' only",
                    r.org,
                    r.name
                );
            }
            if !seen.insert(r.name.as_str()) {
                bail!("repository {:?} is pinned twice", r.name);
            }
            if !((r.commit.len() == 40 || r.commit.len() == 64) && is_hex(&r.commit)) {
                bail!(
                    "repository {}: commit {:?} is not a full commit id (40 or 64 lowercase hex)",
                    r.name,
                    r.commit
                );
            }
        }
        if self.locks.is_empty() {
            bail!("no lock: name the workspaces whose Cargo.lock the release resolves from");
        }
        for l in &self.locks {
            if !safe_rel(l) || !l.ends_with("Cargo.toml") {
                bail!("lock {l:?}: a relative path to a Cargo.toml inside the snapshot");
            }
            let repo = l.split('/').next().unwrap_or("");
            if self.repo(repo).is_none() {
                bail!("lock {l:?}: repository {repo:?} is not pinned");
            }
        }
        if self.crates.is_empty() && self.workspace_binaries.is_empty() && self.workspace_examples.is_empty() {
            bail!("nothing to build: no crate, no workspace binary and no workspace example");
        }
        let mut outputs = std::collections::BTreeSet::new();
        for w in &self.workspace_examples {
            if !safe_rel(&w.manifest) || !w.manifest.ends_with("/Cargo.toml") {
                bail!("workspace example {:?}: manifest {:?} is not a relative path to a Cargo.toml inside the snapshot", w.example, w.manifest);
            }
            let repo = w.manifest.split('/').next().unwrap_or("");
            if self.repo(repo).is_none() {
                bail!("workspace example {:?}: repository {repo:?} is not pinned", w.example);
            }
            if !safe_name(&w.example) {
                bail!("workspace example {:?}: the example is a name only", w.example);
            }
            for a in &w.args {
                if let Some(rest) = a.strip_prefix(OUT_PLACEHOLDER) {
                    if !(rest.is_empty() || rest.strip_prefix('/').is_some_and(safe_rel)) {
                        bail!("workspace example {:?}: argument {a:?}: {OUT_PLACEHOLDER} alone or {OUT_PLACEHOLDER}/<relative path>", w.example);
                    }
                } else if a.contains('\0') || a.split('/').any(|c| c == "..") {
                    bail!("workspace example {:?}: argument {a:?} climbs with '..'", w.example);
                }
            }
            if w.outputs.is_empty() {
                bail!("workspace example {:?}: no output declared (the files it writes into {OUT_PLACEHOLDER})", w.example);
            }
            for o in &w.outputs {
                if !safe_rel(o) {
                    bail!("workspace example {:?}: output {o:?} is not a relative path inside out/", w.example);
                }
                if !outputs.insert(o.as_str()) {
                    bail!("output {o:?} is declared twice");
                }
            }
        }
        for w in &self.workspace_binaries {
            if !safe_rel(&w.manifest) || !w.manifest.ends_with("Cargo.toml") {
                bail!("workspace binary {}: manifest {:?} is not a relative path to a Cargo.toml inside the snapshot", w.bin, w.manifest);
            }
            let repo = w.manifest.split('/').next().unwrap_or("");
            if self.repo(repo).is_none() {
                bail!(
                    "workspace binary {}: repository {repo:?} is not pinned",
                    w.bin
                );
            }
            if !safe_name(&w.package)
                || !safe_name(&w.bin)
                || w.features.iter().any(|f| !safe_name(&f.replace('/', "_")))
            {
                bail!(
                    "workspace binary {:?}: package, bin and feature names are names only",
                    w.bin
                );
            }
        }
        let mut steps = std::collections::BTreeSet::new();
        for c in &self.crates {
            if !safe_name(&c.name) || !safe_name(&c.version) {
                bail!(
                    "crate {:?} {:?}: not a crate name and version",
                    c.name,
                    c.version
                );
            }
            if self.repo(&c.repo).is_none() {
                bail!("crate {}: repository {:?} is not pinned", c.name, c.repo);
            }
            if !safe_rel(&c.path) {
                bail!(
                    "crate {}: path {:?} is not a relative path inside the repository",
                    c.name,
                    c.path
                );
            }
            if !steps.insert(c.name.as_str()) {
                bail!("crate {} is a step twice", c.name);
            }
        }
        for b in &self.binaries {
            if !steps.contains(b.crate_name.as_str()) {
                bail!("binary {}: crate {:?} is not a crate of this release (binaries are built from the packaged crates)", b.bin, b.crate_name);
            }
            if !safe_name(&b.bin) || b.features.iter().any(|f| !safe_name(&f.replace('/', "_"))) {
                bail!("binary {:?}: bin and feature names are names only", b.bin);
            }
        }
        for f in std::iter::once(&self.forge).chain(self.forge_by_org.values()) {
            if !f.starts_with("ssh://") && !f.starts_with("https://") {
                bail!("forge {f:?}: ssh:// or https://");
            }
        }
        Ok(())
    }

    /// The hex digest of the image.
    pub fn image_digest(&self) -> &str {
        self.image
            .rsplit_once("@sha256:")
            .map(|(_, d)| d)
            .unwrap_or("")
    }

    /// The image's repository name, without the digest.
    pub fn image_name(&self) -> &str {
        self.image
            .rsplit_once("@sha256:")
            .map(|(n, _)| n)
            .unwrap_or(&self.image)
    }

    pub fn repo(&self, name: &str) -> Option<&Repo> {
        self.repos.iter().find(|r| r.name == name)
    }

    /// `<forge>/<org>/<name>.git` (the org's own forge if `forge_by_org` names one).
    pub fn clone_url(&self, r: &Repo) -> String {
        format!(
            "{}/{}/{}.git",
            self.forge_by_org
                .get(&r.org)
                .unwrap_or(&self.forge)
                .trim_end_matches('/'),
            r.org,
            r.name
        )
    }

    /// `<web>/<org>/<name>`.
    pub fn web_url(&self, r: &Repo) -> String {
        format!("{}/{}/{}", self.web.trim_end_matches('/'), r.org, r.name)
    }

    pub fn to_toml(&self) -> Result<String> {
        toml::to_string(self).map_err(|e| crate::Error(format!("release spec: {e}")))
    }

    /// The provenance's `externalParameters`.
    pub fn external_parameters(&self) -> Value {
        json!({
            "release": self.name,
            "repositories": self.repos.iter().map(|r| json!({
                "name": r.name,
                "repository": self.web_url(r),
                "commit": r.commit,
            })).collect::<Vec<_>>(),
            "locks": self.locks,
            "crates": self.crates,
            "binaries": self.binaries,
            "workspaceBinaries": self.workspace_binaries,
            "workspaceExamples": self.workspace_examples,
        })
    }

    /// ★ A spec from the publish scripts' own tables: the STEPS lines
    /// (`n crate version repo path verify`) and a pins file (`repo full-sha`).
    /// `orgs` names a repository's org where it is not `default_org`.
    #[allow(clippy::too_many_arguments)]
    pub fn from_tables(
        name: &str,
        image: &str,
        steps: &str,
        pins: &str,
        orgs: &BTreeMap<String, String>,
        default_org: &str,
        locks: Vec<String>,
        binaries: Vec<BinaryStep>,
    ) -> Result<Self> {
        let mut repos = Vec::new();
        for l in pins
            .lines()
            .map(str::trim)
            .filter(|l| !l.is_empty() && !l.starts_with('#'))
        {
            let f: Vec<&str> = l.split_whitespace().collect();
            if f.len() < 2 {
                bail!("pins line {l:?}: '<repo> <full sha>'");
            }
            let org = orgs.get(f[0]).map(String::as_str).unwrap_or(default_org);
            repos.push(Repo {
                name: f[0].into(),
                org: org.into(),
                commit: f[1].into(),
                branch: None,
            });
        }
        let mut crates = Vec::new();
        for l in steps
            .lines()
            .map(str::trim)
            .filter(|l| !l.is_empty() && !l.starts_with('#'))
        {
            let f: Vec<&str> = l.split_whitespace().collect();
            if f.len() < 5 || f[0].parse::<u32>().is_err() {
                bail!("steps line {l:?}: 'n crate version repo path [verify]'");
            }
            if !repos.iter().any(|r: &Repo| r.name == f[3]) {
                bail!(
                    "step {} names repository {:?}, which the pins file has not pinned",
                    f[1],
                    f[3]
                );
            }
            crates.push(CrateStep {
                name: f[1].into(),
                version: f[2].into(),
                repo: f[3].into(),
                path: f[4].into(),
            });
        }
        let s = Self {
            name: name.into(),
            image: image.into(),
            forge: default_forge(),
            web: default_web(),
            forge_by_org: BTreeMap::new(),
            repos,
            locks,
            crates,
            binaries,
            workspace_binaries: Vec::new(),
            workspace_examples: Vec::new(),
        };
        s.validate()?;
        Ok(s)
    }
}