heddle_thread_api/publication/
staging.rs1use std::io::Read;
5
6use super::PublicationOriginals;
7use crate::{
8 contract::*,
9 fetch::{Error, ValidatedSourceArtifacts},
10};
11
12pub 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
116pub 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}