jacquardSnapshot

← snapshot

7128 bytes
//! Dependency-graph proof of the privacy boundary.
//!
//! The rendezvous registry's claim is that nothing but similarity digests ever
//! leaves an organisation. Half of that claim is a trait bound
//! (`jac_core::DigestSafe`); this is the other half, and the stronger one: a
//! rendezvous-plane crate cannot *name* a content type, because the dependency
//! graph never lets it reach one.
//!
//! Each workspace member declares its plane in `Cargo.toml`:
//!
//! ```toml
//! [package.metadata.jacquard]
//! plane = "content"              # neutral | content | rendezvous
//! forbids = ["jac-rendezvous"]   # additional crates this one may never reach
//! ```
//!
//! Two rules are enforced:
//!
//! 1. **Privacy.** A `rendezvous` crate may not transitively reach a `content` crate. A
//!    `neutral` crate may not reach `content` or `rendezvous`: neutral crates are the
//!    shared vocabulary, safe to link anywhere, and a neutral crate that reaches upward
//!    is not neutral any more.
//! 2. **Blinding.** A crate may not transitively reach anything in its `forbids` list.
//!    This is how "the registry cannot see the cloth" becomes a fact the build checks
//!    rather than a convention.

use std::collections::{BTreeMap, BTreeSet, VecDeque};

use anyhow::{Context as _, Result, bail};
use cargo_metadata::{DependencyKind, MetadataCommand, Node, Package, PackageId};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Plane {
    Neutral,
    Content,
    Rendezvous,
}

impl Plane {
    fn parse(s: &str) -> Result<Self> {
        Ok(match s {
            "neutral" => Self::Neutral,
            "content" => Self::Content,
            "rendezvous" => Self::Rendezvous,
            other => bail!("unknown plane `{other}`"),
        })
    }
}

#[derive(Debug)]
struct Declared {
    plane: Plane,
    forbids: Vec<String>,
}

pub(crate) fn run() -> Result<()> {
    let metadata = MetadataCommand::new()
        .manifest_path(crate::workspace_root()?.join("Cargo.toml"))
        .exec()
        .context("failed to run `cargo metadata`")?;

    let by_id: BTreeMap<&PackageId, &Package> =
        metadata.packages.iter().map(|p| (&p.id, p)).collect();

    let resolve = metadata.resolve.as_ref().context(
        "`cargo metadata` returned no resolve graph; the boundary proof cannot be made \
         without one and must not be skipped",
    )?;
    let nodes: BTreeMap<&PackageId, &Node> = resolve.nodes.iter().map(|n| (&n.id, n)).collect();

    // Declared plane per workspace member. Crates from the registry carry no
    // declaration and are treated as neutral: they cannot name our types.
    let mut declared: BTreeMap<&PackageId, Declared> = BTreeMap::new();
    for id in &metadata.workspace_members {
        let pkg = by_id
            .get(id)
            .with_context(|| format!("workspace member {id} not in packages"))?;
        declared.insert(id, read_declaration(pkg)?);
    }

    let mut violations = Vec::new();

    for (id, decl) in &declared {
        let pkg = by_id.get(*id).context("member vanished from package map")?;
        let reachable = reachable_from(id, &nodes);

        for dep in &reachable {
            let Some(dep_decl) = declared.get(dep) else {
                continue;
            };
            let dep_name = by_id.get(dep).map_or("<unknown>", |p| p.name.as_str());
            let broken = match decl.plane {
                // The privacy rule: digests-only crates never reach content.
                Plane::Rendezvous => dep_decl.plane == Plane::Content,
                // Shared vocabulary reaches nothing above itself.
                Plane::Neutral => dep_decl.plane != Plane::Neutral,
                Plane::Content => false,
            };
            if broken {
                violations.push(format!(
                    "privacy: {:?}-plane crate `{}` transitively links {:?}-plane crate `{}`",
                    decl.plane, pkg.name, dep_decl.plane, dep_name
                ));
            }
        }

        for forbidden in &decl.forbids {
            let hit = reachable
                .iter()
                .filter_map(|d| by_id.get(d))
                .any(|p| p.name.as_str() == forbidden.as_str());
            if hit {
                violations.push(format!(
                    "blinding: crate `{}` declares it may never reach `{}`, but the dependency \
                     graph says it does",
                    pkg.name, forbidden
                ));
            }
        }
    }

    if !violations.is_empty() {
        for v in &violations {
            eprintln!("  {v}");
        }
        bail!(
            "{} boundary violation(s); the privacy argument does not hold",
            violations.len()
        );
    }

    let rendezvous: Vec<&str> = declared
        .iter()
        .filter(|(_, d)| d.plane == Plane::Rendezvous)
        .filter_map(|(id, _)| by_id.get(id).map(|p| p.name.as_str()))
        .collect();
    eprintln!(
        "boundary: ok ({} member(s) checked, digest-only: {})",
        declared.len(),
        if rendezvous.is_empty() {
            "<none yet>".to_owned()
        } else {
            rendezvous.join(", ")
        }
    );
    Ok(())
}

fn read_declaration(pkg: &Package) -> Result<Declared> {
    let node = pkg.metadata.get("jacquard").with_context(|| {
        format!(
            "crate `{}` does not declare `[package.metadata.jacquard] plane`. Every workspace \
             member must declare its plane; an undeclared crate cannot be proven anything.",
            pkg.name
        )
    })?;

    let plane = node
        .get("plane")
        .and_then(|v| v.as_str())
        .with_context(|| format!("crate `{}` has no `plane` key", pkg.name))?;

    let forbids = node
        .get("forbids")
        .and_then(|v| v.as_array())
        .map(|a| {
            a.iter()
                .filter_map(|v| v.as_str().map(str::to_owned))
                .collect()
        })
        .unwrap_or_default();

    Ok(Declared {
        plane: Plane::parse(plane)?,
        forbids,
    })
}

/// Transitive closure over normal and build dependencies.
///
/// Dev-dependencies are excluded deliberately: a test may legitimately link a
/// content crate to assert a property about the boundary, and that edge never
/// reaches a shipped artifact.
fn reachable_from<'a>(
    start: &'a PackageId,
    nodes: &BTreeMap<&'a PackageId, &'a Node>,
) -> BTreeSet<&'a PackageId> {
    let mut seen = BTreeSet::new();
    let mut queue = VecDeque::from([start]);

    while let Some(id) = queue.pop_front() {
        let Some(node) = nodes.get(id) else { continue };
        for dep in &node.deps {
            let is_linked = dep
                .dep_kinds
                .iter()
                .any(|k| matches!(k.kind, DependencyKind::Normal | DependencyKind::Build));
            if !is_linked {
                continue;
            }
            let Some((next, _)) = nodes.get_key_value(&dep.pkg) else {
                continue;
            };
            if seen.insert(*next) {
                queue.push_back(*next);
            }
        }
    }
    seen
}