Skip to main content

custom_validator/
custom_validator.rs

1//! Custom candidate validator.
2//!
3//! Shows how a consumer supplies its own identity check without changing
4//! Eggup. The validator below requires the staged file to contain an exact
5//! marker. Run with `cargo run -p eggup-core --example custom_validator`.
6
7use eggup_core::{
8    AbsentPolicy, ArtifactMember, ArtifactSet, CandidateValidator, CommitOwnership,
9    ExistingAsOwnedVerifier, InstallPlan, IntegrityRequirement, MemberId, ProductId, ReleaseId,
10    TransactionDisposition, VerifiedTransaction,
11};
12use std::fs;
13use std::path::PathBuf;
14use std::time::{SystemTime, UNIX_EPOCH};
15
16#[derive(Debug)]
17struct ContainsMarker {
18    member: MemberId,
19    marker: Vec<u8>,
20}
21
22impl CandidateValidator for ContainsMarker {
23    fn validate(&self, transaction: &VerifiedTransaction) -> eggup_core::Result<()> {
24        let path = transaction.staged_path(&self.member)?;
25        let bytes = fs::read(&path).map_err(|e| {
26            eggup_core::Error::CandidateExecution(format!("reading candidate: {e}"))
27        })?;
28        if bytes.windows(self.marker.len()).any(|w| w == self.marker) {
29            Ok(())
30        } else {
31            Err(eggup_core::Error::CandidateExecution(
32                "marker not found in candidate".into(),
33            ))
34        }
35    }
36}
37
38fn temp_root(prefix: &str) -> PathBuf {
39    let nanos = SystemTime::now()
40        .duration_since(UNIX_EPOCH)
41        .map(|d| d.as_nanos())
42        .unwrap_or(0);
43    std::env::temp_dir().join(format!("{prefix}-{}-{nanos}", std::process::id()))
44}
45
46fn main() -> Result<(), Box<dyn std::error::Error>> {
47    let base = temp_root("eggup-custom-validator");
48    let inputs = base.join("inputs");
49    let root = base.join("install");
50    fs::create_dir_all(&inputs)?;
51    fs::create_dir_all(root.join("bin"))?;
52    fs::write(inputs.join("app"), b"release-marker-42")?;
53    let digest = eggup_core::hash_file(&inputs.join("app"))?;
54    let member = ArtifactMember::new(MemberId::new("main")?, inputs.join("app"), "bin/app")?
55        .with_integrity(IntegrityRequirement::Sha256(digest));
56    let plan = InstallPlan::new(
57        ProductId::new("demo")?,
58        ReleaseId::new("3.0.0")?,
59        &root,
60        ArtifactSet::single(member)?,
61    )?;
62    let validator = ContainsMarker {
63        member: MemberId::new("main")?,
64        marker: b"marker-42".to_vec(),
65    };
66    let receipt = plan
67        .prepare()?
68        .verify_integrity()?
69        .validate(&validator)?
70        .commit(CommitOwnership::new(
71            &ExistingAsOwnedVerifier,
72            AbsentPolicy::AllowCreate,
73        ))?;
74    assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
75    let _ = fs::remove_dir_all(&base);
76    println!("custom validator commit: {:?}", receipt.disposition());
77    Ok(())
78}