Skip to main content

ProductId

Struct ProductId 

Source
pub struct ProductId(/* private fields */);
Expand description

A validated opaque product identifier owned by the consumer.

Implementations§

Source§

impl ProductId

Source

pub fn new(value: impl Into<String>) -> Result<Self>

Creates a product identifier after rejecting empty or unsafe control-character input.

Examples found in repository?
examples/receipts.rs (line 52)
35fn build(
36    new: &[u8],
37    old: Option<&[u8]>,
38) -> Result<(PathBuf, InstallPlan), Box<dyn std::error::Error>> {
39    let base = temp_root("eggup-receipts");
40    let inputs = base.join("inputs");
41    let root = base.join("install");
42    fs::create_dir_all(&inputs)?;
43    fs::create_dir_all(root.join("bin"))?;
44    fs::write(inputs.join("app"), new)?;
45    if let Some(old) = old {
46        fs::write(root.join("bin/app"), old)?;
47    }
48    let digest = eggup_core::hash_file(&inputs.join("app"))?;
49    let member = ArtifactMember::new(MemberId::new("main")?, inputs.join("app"), "bin/app")?
50        .with_integrity(IntegrityRequirement::Sha256(digest));
51    let plan = InstallPlan::new(
52        ProductId::new("demo")?,
53        ReleaseId::new("5.0.0")?,
54        &root,
55        ArtifactSet::single(member)?,
56    )?;
57    // Leak the base via the plan's root; caller cleans `root.parent()`.
58    Ok((base, plan))
59}
More examples
Hide additional examples
examples/custom_validator.rs (line 57)
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}
examples/one_member.rs (line 45)
30fn main() -> Result<(), Box<dyn std::error::Error>> {
31    let base = temp_root("eggup-quickstart");
32    let (inputs, root) = (base.join("inputs"), base.join("install"));
33    fs::create_dir_all(inputs.join("bin"))?;
34    fs::create_dir_all(root.join("bin"))?;
35    fs::write(inputs.join("app"), b"new-app")?;
36
37    // 1. Describe one member, with the SHA-256 you expect to verify.
38    let source = inputs.join("app");
39    let digest = eggup_core::hash_file(&source)?;
40    let member = ArtifactMember::new(MemberId::new("main")?, source, "bin/app")?
41        .with_integrity(IntegrityRequirement::Sha256(digest));
42
43    // 2. Name the product, release, install root, and artifact set.
44    let plan = InstallPlan::new(
45        ProductId::new("demo")?,
46        ReleaseId::new("1.0.0")?,
47        &root,
48        ArtifactSet::single(member)?,
49    )?;
50
51    // 3. Stage, verify, validate, then commit with proven ownership.
52    let receipt = plan
53        .prepare()?
54        .verify_integrity()?
55        .validate(&AllValidators::new())?
56        .commit(CommitOwnership::new(
57            &ExistingAsOwnedVerifier,
58            AbsentPolicy::AllowCreate,
59        ))?;
60
61    assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
62    assert_eq!(fs::read(root.join("bin/app"))?, b"new-app");
63    let _ = fs::remove_dir_all(&base);
64    println!("committed: {:?}", receipt.disposition());
65    Ok(())
66}
examples/ownership.rs (line 37)
24fn main() -> Result<(), Box<dyn std::error::Error>> {
25    let base = temp_root("eggup-ownership");
26    let inputs = base.join("inputs");
27    let root = base.join("install");
28    fs::create_dir_all(&inputs)?;
29    fs::create_dir_all(root.join("bin"))?;
30    fs::write(inputs.join("app"), b"new-app")?;
31    fs::write(root.join("bin/app"), b"old-app")?;
32    let new_digest = eggup_core::hash_file(&inputs.join("app"))?;
33    let old_digest = eggup_core::hash_file(&root.join("bin/app"))?;
34    let member = ArtifactMember::new(MemberId::new("main")?, inputs.join("app"), "bin/app")?
35        .with_integrity(IntegrityRequirement::Sha256(new_digest));
36    let plan = InstallPlan::new(
37        ProductId::new("demo")?,
38        ReleaseId::new("4.0.0")?,
39        &root,
40        ArtifactSet::single(member)?,
41    )?;
42    // Prove the live file is the expected old deployment; deny creation of
43    // absent destinations (replacement-only policy).
44    let verifier = ExactDigestVerifier::new(vec![(MemberId::new("main")?, old_digest)]);
45    let receipt = plan
46        .prepare()?
47        .verify_integrity()?
48        .validate(&AllValidators::new())?
49        .commit(CommitOwnership::new(&verifier, AbsentPolicy::DenyCreate))?;
50    assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
51    assert_eq!(fs::read(root.join("bin/app"))?, b"new-app");
52    let _ = fs::remove_dir_all(&base);
53    println!("ownership-verified commit: {:?}", receipt.disposition());
54    Ok(())
55}
examples/multi_member.rs (line 47)
24fn main() -> Result<(), Box<dyn std::error::Error>> {
25    let base = temp_root("eggup-multi-member");
26    let inputs = base.join("inputs");
27    let root = base.join("install");
28    fs::create_dir_all(&inputs)?;
29    fs::create_dir_all(root.join("bin"))?;
30    fs::write(inputs.join("main"), b"new-main")?;
31    fs::write(inputs.join("helper"), b"new-helper")?;
32    fs::write(root.join("bin/main"), b"old-main")?;
33    fs::write(root.join("bin/helper"), b"old-helper")?;
34    let main_digest = eggup_core::hash_file(&inputs.join("main"))?;
35    let helper_digest = eggup_core::hash_file(&inputs.join("helper"))?;
36    let members = ArtifactSet::new(vec![
37        ArtifactMember::new(MemberId::new("main")?, inputs.join("main"), "bin/main")?
38            .with_integrity(IntegrityRequirement::Sha256(main_digest)),
39        ArtifactMember::new(
40            MemberId::new("helper")?,
41            inputs.join("helper"),
42            "bin/helper",
43        )?
44        .with_integrity(IntegrityRequirement::Sha256(helper_digest)),
45    ])?;
46    let plan = InstallPlan::new(
47        ProductId::new("bundle")?,
48        ReleaseId::new("2.0.0")?,
49        &root,
50        members,
51    )?;
52    let receipt = plan
53        .prepare()?
54        .verify_integrity()?
55        .validate(&AllValidators::new())?
56        .commit(CommitOwnership::new(
57            &ExistingAsOwnedVerifier,
58            AbsentPolicy::AllowCreate,
59        ))?;
60    assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
61    assert_eq!(fs::read(root.join("bin/main"))?, b"new-main");
62    assert_eq!(fs::read(root.join("bin/helper"))?, b"new-helper");
63    let _ = fs::remove_dir_all(&base);
64    println!("multi-member commit: {:?}", receipt.disposition());
65    Ok(())
66}
examples/stale_lock_recovery.rs (line 95)
89fn main() -> Result<(), Box<dyn std::error::Error>> {
90    let root =
91        std::env::temp_dir().join(format!("eggup-stale-lock-example-{}", std::process::id()));
92    let _ = std::fs::remove_dir_all(&root);
93    std::fs::create_dir_all(&root)?;
94
95    let product = ProductId::new("example-daemon")?;
96    let release = ReleaseId::new("2026.10.05")?;
97    let lock_path = root.join(".eggup-mutation.lock");
98
99    // A crashed updater left this record behind.
100    std::fs::write(
101        &lock_path,
102        "pid=4242 nonce=9 product=example-daemon release=2026.10.05\n",
103    )?;
104
105    println!("default acquisition never recovers:");
106    let error = MutationLock::acquire(&root, &product, &release).expect_err("record exists");
107    println!("  {error}\n");
108
109    // The consumer supervises pid 4242, can no longer see it, and the deployment
110    // is declared idle. That combination is its proof.
111    let conclusive = DeploymentVerifier {
112        evidence: DeploymentEvidence {
113            product: product.clone(),
114            supervised: vec![4242],
115            alive: Vec::new(),
116            declared_idle: true,
117        },
118    };
119    match MutationLock::acquire_with_recovery(&root, &product, &release, &conclusive) {
120        Ok(lock) => {
121            println!("recovered after caller proof:");
122            println!("  lock record: {}", lock.path().display());
123            println!("  our own record is removed when it is dropped");
124        }
125        Err(error) => println!("retained the record: {error}"),
126    }
127
128    // Contrast: the same record without the deployment's out-of-band statement is
129    // ambiguous, so it is left exactly where it is.
130    let _ = std::fs::remove_file(&lock_path);
131    std::fs::write(
132        &lock_path,
133        "pid=5150 nonce=1 product=example-daemon release=2026.10.05\n",
134    )?;
135    let cautious = DeploymentVerifier {
136        evidence: DeploymentEvidence {
137            product: product.clone(),
138            supervised: vec![5150],
139            alive: Vec::new(),
140            declared_idle: false,
141        },
142    };
143    let error = MutationLock::acquire_with_recovery(&root, &product, &release, &cautious)
144        .expect_err("no out-of-band proof");
145    println!("\nwithout out-of-band proof:");
146    println!("  {error}");
147    println!("  record still present: {}", lock_path.exists());
148
149    // And a record belonging to a different deployment is never claimed.
150    let _ = std::fs::remove_file(&lock_path);
151    std::fs::write(
152        &lock_path,
153        "pid=6060 nonce=1 product=somebody-else release=2026.10.05\n",
154    )?;
155    let foreign = DeploymentVerifier {
156        evidence: DeploymentEvidence {
157            product: ProductId::new("example-daemon")?,
158            supervised: vec![6060],
159            alive: Vec::new(),
160            declared_idle: true,
161        },
162    };
163    let error = MutationLock::acquire_with_recovery(&root, &product, &release, &foreign)
164        .expect_err("foreign record");
165    println!("\nfor another deployment's record:");
166    println!("  {error}");
167    println!("  record still present: {}", lock_path.exists());
168
169    let _ = std::fs::remove_dir_all(&root);
170    Ok(())
171}
Source

pub fn as_str(&self) -> &str

Returns the identifier’s stable string representation.

Examples found in repository?
examples/stale_lock_recovery.rs (line 61)
57    fn classify(&self, observed: &LockObservation) -> StaleLockDecision {
58        // The record must belong to this consumer's domain before its fields
59        // mean anything. Core observed the bytes; attributing them is the
60        // caller's job.
61        if observed.product() != Some(self.evidence.product.as_str()) {
62            return StaleLockDecision::Unknown;
63        }
64        // An unrecognised record format carries no attributable evidence.
65        if !observed.format_known() {
66            return StaleLockDecision::Unknown;
67        }
68        let Some(pid) = observed.pid() else {
69            return StaleLockDecision::Unknown;
70        };
71        // A supervised process this consumer can still see is plainly active.
72        if self.evidence.alive.contains(&pid) {
73            return StaleLockDecision::Active;
74        }
75        // A pid this consumer never supervised is ambiguous: the number may now
76        // belong to an unrelated process entirely.
77        if !self.evidence.supervised.contains(&pid) {
78            return StaleLockDecision::Unknown;
79        }
80        // Conclusive only with the deployment's own out-of-band statement.
81        if self.evidence.declared_idle {
82            StaleLockDecision::ProvenStale
83        } else {
84            StaleLockDecision::Unknown
85        }
86    }

Trait Implementations§

Source§

impl Clone for ProductId

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for ProductId

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Display for ProductId

Source§

fn fmt(&self, formatter: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Eq for ProductId

Source§

impl Hash for ProductId

Source§

fn hash<__H: Hasher>(&self, state: &mut __H)

Feeds this value into the given Hasher. Read more
1.3.0 · Source§

fn hash_slice<H>(data: &[Self], state: &mut H)
where H: Hasher, Self: Sized,

Feeds a slice of this type into the given Hasher. Read more
Source§

impl PartialEq for ProductId

Source§

fn eq(&self, other: &Self) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for ProductId

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T> ToString for T
where T: Display + ?Sized,

Source§

fn to_string(&self) -> String

Converts the given value to a String. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.