Skip to main content

heddle_thread_api/publication/
staging.rs

1//! Shared actual-artifact validation for hosted and owned-device publication.
2//! Original signatures and causal closure are necessary but do not authorize
3//! their authors, current audience, hosted executors, or destination policy.
4use std::io::Read;
5
6use super::PublicationOriginals;
7use crate::{
8    contract::*,
9    fetch::{Error, ValidatedSourceArtifacts},
10};
11
12/// Call on a bounded disk worker after the complete upload. The directory must
13/// contain only the uploaded `source.pack` and `source.idx`; validation never
14/// falls back to objects already installed in a shared store. Dropping either
15/// the input on failure or the result on cancellation removes its scratch.
16pub fn validate_source_artifacts(
17    directory: tempfile::TempDir,
18    opening: &PublishContentOpen,
19    originals: PublicationOriginals,
20) -> Result<ValidatedSourceArtifacts, Error> {
21    originals.validate_bounds().map_err(preparation)?;
22    let thread = opening
23        .thread
24        .as_ref()
25        .ok_or(Error::Invalid("publication Thread absent"))?;
26    let revision = opening
27        .revision
28        .as_ref()
29        .ok_or(Error::Invalid("publication revision absent"))?;
30    if opening.packs.len() != 2
31        || opening.packs[0].kind != pack_extent::Kind::NativePack as i32
32        || opening.packs[1].kind != pack_extent::Kind::NativeIndex as i32
33    {
34        return Err(Error::Invalid("ordered native pack and index required"));
35    }
36    let mut total = 0_u64;
37    for (extent, name) in opening.packs.iter().zip(["source.pack", "source.idx"]) {
38        let address = extent
39            .pack
40            .as_ref()
41            .ok_or(Error::Invalid("artifact address absent"))?;
42        total = total
43            .checked_add(extent.length)
44            .ok_or(Error::Invalid("artifact length overflow"))?;
45        if address.algorithm != "blake3"
46            || address.digest.len() != 32
47            || extent.offset != 0
48            || extent.length == 0
49            || extent.extent_digest.as_ref() != Some(address)
50            || total > 256 * 1024 * 1024
51        {
52            return Err(Error::Invalid("invalid or oversized publication artifact"));
53        }
54        let mut file = std::fs::File::open(directory.path().join(name))?;
55        if file.metadata()?.len() != extent.length {
56            return Err(Error::Invalid("uploaded artifact length differs"));
57        }
58        let mut hasher = blake3::Hasher::new();
59        let mut buffer = [0_u8; 64 * 1024];
60        loop {
61            let read = file.read(&mut buffer)?;
62            if read == 0 {
63                break;
64            }
65            hasher.update(&buffer[..read]);
66        }
67        if hasher.finalize().as_bytes().as_slice() != address.digest {
68            return Err(Error::Invalid("uploaded artifact digest differs"));
69        }
70    }
71    let selected = thread
72        .id
73        .as_ref()
74        .ok_or(Error::Invalid("Thread identity absent"))?;
75    let mut genesis = None;
76    let mut dependencies = Vec::new();
77    for wrapper in originals.geneses {
78        let record = wrapper
79            .genesis
80            .as_ref()
81            .ok_or(Error::Invalid("original genesis absent"))?;
82        let candidate = heddle_object_model::object::thread_replication::ThreadGenesis::decode(
83            &record.canonical_record,
84        )
85        .map_err(preparation)?;
86        if candidate.id().map_err(preparation)?.as_bytes().as_slice() == selected.value {
87            if genesis.replace(wrapper).is_some() {
88                return Err(Error::Invalid("duplicate selected genesis"));
89            }
90        } else {
91            dependencies.push(wrapper);
92        }
93    }
94    let mut operations = Vec::new();
95    let mut receipts = Vec::new();
96    for batch in originals.operations {
97        for received in crate::authority_admission::match_batch(&batch)? {
98            operations.push(received.original);
99            receipts.extend(received.authority_admission);
100        }
101    }
102    crate::fetch::validate_artifacts(
103        directory,
104        thread,
105        revision,
106        &genesis.ok_or(Error::Invalid("original selected genesis absent"))?,
107        operations,
108        dependencies,
109        receipts,
110    )
111}
112fn preparation(error: impl std::fmt::Display) -> Error {
113    Error::Preparation(error.to_string())
114}
115
116/// Structurally verified artifact closure and explicit proposed acceptances.
117/// No acceptance is a current-authority token. The host must authorize every
118/// selected subject and preserve ordinary receipt, claim, and owner-cutoff gates.
119pub struct ProposedSourceArtifacts {
120    artifacts: ValidatedSourceArtifacts,
121    acceptances: super::PublicationAcceptancePlan,
122}
123
124impl ProposedSourceArtifacts {
125    pub fn artifacts(&self) -> &ValidatedSourceArtifacts {
126        &self.artifacts
127    }
128    pub fn acceptances(&self) -> &super::PublicationAcceptancePlan {
129        &self.acceptances
130    }
131    pub fn into_parts(self) -> (ValidatedSourceArtifacts, super::PublicationAcceptancePlan) {
132        (self.artifacts, self.acceptances)
133    }
134}
135
136pub fn validate_proposed_source_artifacts(
137    directory: tempfile::TempDir,
138    opening: &PublishContentClientFrame,
139    originals: PublicationOriginals,
140    spool_genesis: heddle_object_model::object::ContentHash,
141) -> Result<ProposedSourceArtifacts, Error> {
142    let (originals, acceptances) =
143        super::proposed_publication(opening, originals, spool_genesis).map_err(preparation)?;
144    let Some(publish_content_client_frame::Body::Open(open)) = &opening.body else {
145        return Err(Error::Invalid("publication Open required"));
146    };
147    let artifacts = validate_source_artifacts(directory, open, originals)?;
148    Ok(ProposedSourceArtifacts {
149        artifacts,
150        acceptances,
151    })
152}