wsb 0.275.7622

Workspace tools for every project under $PRJ: git across submodules, template stamping, GitHub, and the workbench
Documentation
//! What arrives from a peer is checked, not trusted.

use super::*;
use crate::peer::converge::{Entry, EntryKind};

fn entry(bytes: &[u8]) -> Entry {
    Entry {
        kind: EntryKind::File,
        hash: crate::peer::converge::hash_bytes(bytes),
        size: bytes.len() as u64,
        executable: false,
    }
}

/// TEST9957: a write is verified against the entry it claims to be.
///
/// A peer is authenticated, which says who it is — not that every byte
/// survived, and not that its own bookkeeping is right. A write whose content
/// does not hash to its entry would put a hash in the baseline that nothing on
/// disk has, and every later round would see a change that cannot be resolved.
#[test]
fn test9957_a_write_must_match_its_own_entry() {
    let content = b"fn main() {}\n";
    let honest = Write {
        path: "src/main.rs".into(),
        entry: entry(content),
        content: crate::peer::identity::base64_encode(content),
    };
    assert_eq!(honest.decoded().expect("verified"), content);

    // The bytes changed and the entry did not.
    let tampered = Write {
        content: crate::peer::identity::base64_encode(b"fn main() { evil() }\n"),
        ..honest.clone()
    };
    let error = tampered.decoded().expect_err("a mismatched write must be refused");
    assert!(error.contains("does not match the hash"), "{error}");

    // The size is checked too: a hash collision is not the only way to lie,
    // and a size that disagrees is a sign the entry was assembled by hand.
    let wrong_size = Write {
        entry: Entry {
            size: 9999,
            ..entry(content)
        },
        ..honest.clone()
    };
    let error = wrong_size.decoded().expect_err("a mismatched size must be refused");
    assert!(error.contains("bytes"), "{error}");

    // And content that is not base64 at all is a refusal, not a panic.
    let malformed = Write {
        content: "!!!not base64!!!".into(),
        ..honest
    };
    assert!(malformed.decoded().is_err());
}

/// TEST9958: two daemons holding the same baseline compute the same digest,
/// and any difference changes it.
///
/// The digest is how a receiver knows the sender planned against the state it
/// actually shares. If it missed a field — the executable bit, the kind — a
/// transfer planned against a different baseline would be applied as though it
/// were the same one.
#[test]
fn test9958_the_baseline_digest_covers_everything_that_matters() {
    let mut baseline = crate::peer::converge::Baseline::new();
    baseline.insert("a.rs".into(), entry(b"a"));
    baseline.insert("b.rs".into(), entry(b"b"));

    // Order of insertion must not matter: two daemons learn paths in different
    // orders and must still agree.
    let mut other = crate::peer::converge::Baseline::new();
    other.insert("b.rs".into(), entry(b"b"));
    other.insert("a.rs".into(), entry(b"a"));
    assert_eq!(baseline_digest(&baseline), baseline_digest(&other));

    // Every field that decides convergence changes it.
    for mutate in [
        // content
        |b: &mut crate::peer::converge::Baseline| {
            b.insert("a.rs".into(), entry(b"different"));
        },
        // the executable bit
        |b: &mut crate::peer::converge::Baseline| {
            let mut e = entry(b"a");
            e.executable = true;
            b.insert("a.rs".into(), e);
        },
        // the kind
        |b: &mut crate::peer::converge::Baseline| {
            let mut e = entry(b"a");
            e.kind = EntryKind::Symlink;
            b.insert("a.rs".into(), e);
        },
        // membership
        |b: &mut crate::peer::converge::Baseline| {
            b.remove("b.rs");
        },
        // a path with the same content under another name
        |b: &mut crate::peer::converge::Baseline| {
            b.insert("c.rs".into(), entry(b"a"));
        },
    ] {
        let mut changed = baseline.clone();
        mutate(&mut changed);
        assert_ne!(
            baseline_digest(&baseline),
            baseline_digest(&changed),
            "a baseline difference did not change the digest"
        );
    }
}

/// TEST9959: every message round-trips through its wire form.
///
/// The two daemons are separate processes, so every type here crosses serde.
/// A variant that does not survive the trip is a state one side can express
/// and the other cannot read.
#[test]
fn test9959_every_message_survives_the_wire() {
    let epoch = crate::peer::compatibility::Epoch {
        repository: crate::peer::model::RepositoryId::root(),
        root_commit: "abc".into(),
        branch: Some("feature/peer".into()),
        head: Some("def".into()),
        git_state: crate::peer::compatibility::GitState::Settled,
    };
    let worktree = crate::peer::model::SharedWorktreeId::parse("ab12@main").expect("id");

    let requests = vec![
        Request::Inventory,
        Request::Report {
            worktree: worktree.clone(),
            repositories: vec![crate::peer::model::RepositoryId::root()],
        },
        Request::Fetch {
            worktree: worktree.clone(),
            repository: crate::peer::model::RepositoryId::root(),
            epoch: epoch.clone(),
            paths: vec!["a.rs".into()],
        },
        Request::Apply {
            worktree: worktree.clone(),
            repository: crate::peer::model::RepositoryId::root(),
            epoch: epoch.clone(),
            writes: vec![Write {
                path: "a.rs".into(),
                entry: entry(b"a"),
                content: crate::peer::identity::base64_encode(b"a"),
            }],
            deletions: vec!["gone.rs".into()],
            found: [("gone.rs".to_string(), entry(b"old"))].into_iter().collect(),
            baseline_digest: "digest".into(),
        },
        Request::Confirm {
            worktree: worktree.clone(),
            repository: crate::peer::model::RepositoryId::root(),
            baseline_digest: "digest".into(),
        },
        Request::Acknowledge {
            worktree,
            repository: crate::peer::model::RepositoryId::root(),
            epoch,
            paths: vec!["a.rs".into()],
            baseline_digest: "digest".into(),
        },
    ];
    for request in requests {
        let text = serde_json::to_string(&request).expect("encode");
        let back: Request = serde_json::from_str(&text).expect("decode");
        assert_eq!(request, back);
    }

    let responses = vec![
        Response::Inventory { worktrees: vec![] },
        Response::Report {
            worktree_present: Some(true),
            repositories: vec![],
        },
        Response::Fetched { writes: vec![] },
        Response::Applied {
            applied: vec!["a.rs".into()],
        },
        Response::Acknowledged,
        Response::Refused {
            code: RefusalCode::EpochMoved,
            detail: "the branch changed".into(),
        },
    ];
    for response in responses {
        let text = serde_json::to_string(&response).expect("encode");
        let back: Response = serde_json::from_str(&text).expect("decode");
        assert_eq!(response, back);
    }

    // Deletions and writes are separate fields on purpose: an empty file and a
    // deleted file must never be the same message.
    let empty_write = Request::Apply {
        worktree: crate::peer::model::SharedWorktreeId::parse("ab12@main").expect("id"),
        repository: crate::peer::model::RepositoryId::root(),
        epoch: crate::peer::compatibility::Epoch {
            repository: crate::peer::model::RepositoryId::root(),
            root_commit: "abc".into(),
            branch: Some("main".into()),
            head: Some("def".into()),
            git_state: crate::peer::compatibility::GitState::Settled,
        },
        writes: vec![Write {
            path: "empty".into(),
            entry: entry(b""),
            content: String::new(),
        }],
        deletions: vec![],
        found: Default::default(),
        baseline_digest: "d".into(),
    };
    let text = serde_json::to_string(&empty_write).expect("encode");
    assert!(text.contains("\"deletions\":[]"), "{text}");
}