heddle_object_model/object/manifest/
binding.rs1use std::fmt;
30
31use crate::object::{ContentHash, SpoolId};
32
33pub const MANIFEST_BINDING_MAGIC: [u8; 4] = *b"WPMB";
35pub const MANIFEST_BINDING_VERSION: u8 = 1;
37
38#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
44pub enum ManifestFacet {
45 Content,
46 Governance,
47 Membership,
48 Children,
49 Named(String),
50}
51
52impl ManifestFacet {
53 pub fn token(&self) -> &str {
56 match self {
57 Self::Content => "content",
58 Self::Governance => "governance",
59 Self::Membership => "membership",
60 Self::Children => "children",
61 Self::Named(token) => token.as_str(),
62 }
63 }
64
65 pub fn parse(token: impl AsRef<str>) -> Result<Self, ManifestFacetParseError> {
67 let token = token.as_ref();
68 match token {
69 "content" => return Ok(Self::Content),
70 "governance" => return Ok(Self::Governance),
71 "membership" => return Ok(Self::Membership),
72 "children" => return Ok(Self::Children),
73 _ => {}
74 }
75 if !valid_facet_token(token) {
76 return Err(ManifestFacetParseError(token.to_string()));
77 }
78 Ok(Self::Named(token.to_string()))
79 }
80
81 pub fn well_known() -> [Self; 4] {
83 [
84 Self::Content,
85 Self::Governance,
86 Self::Membership,
87 Self::Children,
88 ]
89 }
90}
91
92fn valid_facet_token(token: &str) -> bool {
93 !token.is_empty()
94 && token.len() <= 64
95 && token
96 .bytes()
97 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'-' | b'_'))
98 && token.as_bytes()[0].is_ascii_alphanumeric()
99 && token.as_bytes()[token.len() - 1].is_ascii_alphanumeric()
100}
101
102impl fmt::Display for ManifestFacet {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 f.write_str(self.token())
105 }
106}
107
108#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
109#[error("invalid facet token '{0}'")]
110pub struct ManifestFacetParseError(String);
111
112#[repr(u8)]
117#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
118pub enum ManifestOwnerKind {
119 State = 0,
120 StateAttachment = 1,
121}
122
123impl ManifestOwnerKind {
124 pub fn to_byte(self) -> u8 {
125 self as u8
126 }
127
128 pub fn from_byte(byte: u8) -> Option<Self> {
129 match byte {
130 0 => Some(Self::State),
131 1 => Some(Self::StateAttachment),
132 _ => None,
133 }
134 }
135}
136
137#[derive(Clone, Debug, PartialEq, Eq)]
139pub struct ManifestBinding {
140 pub spool: SpoolId,
141 pub facet: ManifestFacet,
142 pub owner_kind: ManifestOwnerKind,
143 pub owner_hash: ContentHash,
144 pub owner_decoded_size: u64,
145 pub content_root: ContentHash,
146}
147
148impl ManifestBinding {
149 pub fn encode(&self) -> Vec<u8> {
151 let spool = self.spool.as_str();
152 let facet = self.facet.token();
153 let mut out =
154 Vec::with_capacity(4 + 1 + 2 + spool.len() + 2 + facet.len() + 1 + 32 + 8 + 32);
155 out.extend_from_slice(&MANIFEST_BINDING_MAGIC);
156 out.push(MANIFEST_BINDING_VERSION);
157 out.extend_from_slice(&(spool.len() as u16).to_be_bytes());
158 out.extend_from_slice(spool.as_bytes());
159 out.extend_from_slice(&(facet.len() as u16).to_be_bytes());
160 out.extend_from_slice(facet.as_bytes());
161 out.push(self.owner_kind.to_byte());
162 out.extend_from_slice(self.owner_hash.as_bytes());
163 out.extend_from_slice(&self.owner_decoded_size.to_be_bytes());
164 out.extend_from_slice(self.content_root.as_bytes());
165 out
166 }
167
168 pub fn address(&self) -> ContentHash {
170 ContentHash::compute(&self.encode())
171 }
172
173 pub fn decode(bytes: &[u8]) -> Result<Self, ManifestBindingDecodeError> {
176 let binding = Self::decode_inner(bytes)?;
177 if binding.encode() != bytes {
178 return Err(ManifestBindingDecodeError::NonCanonicalEncoding);
179 }
180 Ok(binding)
181 }
182
183 fn decode_inner(bytes: &[u8]) -> Result<Self, ManifestBindingDecodeError> {
184 let mut reader = BindingReader { bytes, pos: 0 };
185
186 if reader.take(4)? != MANIFEST_BINDING_MAGIC {
187 return Err(ManifestBindingDecodeError::BadMagic);
188 }
189 let version = reader.u8()?;
190 if version != MANIFEST_BINDING_VERSION {
191 return Err(ManifestBindingDecodeError::UnsupportedVersion(version));
192 }
193
194 let spool_token = reader.short_string()?;
195 let spool = SpoolId::parse(spool_token.clone())
196 .map_err(|_| ManifestBindingDecodeError::InvalidSpoolId(spool_token))?;
197 let facet_token = reader.short_string()?;
198 let facet = ManifestFacet::parse(&facet_token)
199 .map_err(|_| ManifestBindingDecodeError::InvalidFacet(facet_token))?;
200
201 let owner_byte = reader.u8()?;
202 let owner_kind = ManifestOwnerKind::from_byte(owner_byte)
203 .ok_or(ManifestBindingDecodeError::UnknownOwnerKind(owner_byte))?;
204 let owner_hash = ContentHash::from_bytes(reader.hash()?);
205 let owner_decoded_size = reader.u64()?;
206 let content_root = ContentHash::from_bytes(reader.hash()?);
207
208 if reader.pos != bytes.len() {
209 return Err(ManifestBindingDecodeError::TrailingBytes);
210 }
211
212 Ok(Self {
213 spool,
214 facet,
215 owner_kind,
216 owner_hash,
217 owner_decoded_size,
218 content_root,
219 })
220 }
221}
222
223struct BindingReader<'a> {
224 bytes: &'a [u8],
225 pos: usize,
226}
227
228impl<'a> BindingReader<'a> {
229 fn take(&mut self, len: usize) -> Result<&'a [u8], ManifestBindingDecodeError> {
230 let end = self
231 .pos
232 .checked_add(len)
233 .ok_or(ManifestBindingDecodeError::Truncated)?;
234 let slice = self
235 .bytes
236 .get(self.pos..end)
237 .ok_or(ManifestBindingDecodeError::Truncated)?;
238 self.pos = end;
239 Ok(slice)
240 }
241
242 fn u8(&mut self) -> Result<u8, ManifestBindingDecodeError> {
243 Ok(self.take(1)?[0])
244 }
245
246 fn u16(&mut self) -> Result<u16, ManifestBindingDecodeError> {
247 let bytes = self.take(2)?;
248 Ok(u16::from_be_bytes([bytes[0], bytes[1]]))
249 }
250
251 fn u64(&mut self) -> Result<u64, ManifestBindingDecodeError> {
252 let bytes = self.take(8)?;
253 let mut arr = [0u8; 8];
254 arr.copy_from_slice(bytes);
255 Ok(u64::from_be_bytes(arr))
256 }
257
258 fn hash(&mut self) -> Result<[u8; 32], ManifestBindingDecodeError> {
259 let bytes = self.take(32)?;
260 let mut arr = [0u8; 32];
261 arr.copy_from_slice(bytes);
262 Ok(arr)
263 }
264
265 fn short_string(&mut self) -> Result<String, ManifestBindingDecodeError> {
266 let len = usize::from(self.u16()?);
267 std::str::from_utf8(self.take(len)?)
268 .map(str::to_string)
269 .map_err(|_| ManifestBindingDecodeError::InvalidUtf8)
270 }
271}
272
273#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
274pub enum ManifestBindingDecodeError {
275 #[error("binding does not start with the WPMB magic")]
276 BadMagic,
277 #[error("unsupported binding version {0}")]
278 UnsupportedVersion(u8),
279 #[error("unknown manifest owner kind {0}")]
280 UnknownOwnerKind(u8),
281 #[error("binding bytes are truncated")]
282 Truncated,
283 #[error("binding has trailing bytes after its declared content")]
284 TrailingBytes,
285 #[error("spool id or facet token is not valid UTF-8")]
286 InvalidUtf8,
287 #[error("invalid spool id: {0}")]
288 InvalidSpoolId(String),
289 #[error("invalid facet token: {0}")]
290 InvalidFacet(String),
291 #[error("binding bytes are a non-canonical spelling of their own content")]
292 NonCanonicalEncoding,
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 fn binding() -> ManifestBinding {
300 ManifestBinding {
301 spool: SpoolId::parse("acme/api-v2").unwrap(),
302 facet: ManifestFacet::Content,
303 owner_kind: ManifestOwnerKind::State,
304 owner_hash: ContentHash::from_bytes([7; 32]),
305 owner_decoded_size: 4096,
306 content_root: ContentHash::from_bytes([9; 32]),
307 }
308 }
309
310 #[test]
311 fn binding_round_trips_and_is_hash_stable() {
312 let binding = binding();
313 let encoded = binding.encode();
314 let decoded = ManifestBinding::decode(&encoded).unwrap();
315 assert_eq!(decoded, binding);
316 assert_eq!(decoded.encode(), encoded);
317 assert_eq!(decoded.address(), binding.address());
318 }
319
320 #[test]
321 fn every_spool_carries_all_four_facets_and_they_bind_distinctly() {
322 let addresses: Vec<ContentHash> = ManifestFacet::well_known()
325 .into_iter()
326 .map(|facet| ManifestBinding { facet, ..binding() }.address())
327 .collect();
328 let unique: std::collections::BTreeSet<_> = addresses.iter().collect();
329 assert_eq!(unique.len(), 4, "facets must not collide in binding space");
330 }
331
332 #[test]
333 fn a_named_token_normalizes_to_its_well_known_facet() {
334 assert_eq!(
335 ManifestFacet::parse("children").unwrap(),
336 ManifestFacet::Children
337 );
338 let named = ManifestBinding {
341 facet: ManifestFacet::parse("content").unwrap(),
342 ..binding()
343 };
344 assert_eq!(named.address(), binding().address());
345 }
346
347 #[test]
348 fn owner_identity_is_outside_the_content_root() {
349 let parent = binding();
352 let child = ManifestBinding {
353 owner_hash: ContentHash::from_bytes([11; 32]),
354 ..parent.clone()
355 };
356 assert_eq!(child.content_root, parent.content_root);
357 assert_ne!(child.address(), parent.address());
358 }
359
360 #[test]
361 fn decode_rejects_each_corruption_class() {
362 let encoded = binding().encode();
363
364 let mut bad_magic = encoded.clone();
365 bad_magic[0] = b'Q';
366 assert_eq!(
367 ManifestBinding::decode(&bad_magic).unwrap_err(),
368 ManifestBindingDecodeError::BadMagic
369 );
370
371 let mut bad_version = encoded.clone();
372 bad_version[4] = 4;
373 assert_eq!(
374 ManifestBinding::decode(&bad_version).unwrap_err(),
375 ManifestBindingDecodeError::UnsupportedVersion(4)
376 );
377
378 let mut trailing = encoded.clone();
379 trailing.push(0);
380 assert_eq!(
381 ManifestBinding::decode(&trailing).unwrap_err(),
382 ManifestBindingDecodeError::TrailingBytes
383 );
384
385 assert_eq!(
386 ManifestBinding::decode(&encoded[..encoded.len() - 1]).unwrap_err(),
387 ManifestBindingDecodeError::Truncated
388 );
389
390 let owner_kind_at = 4 + 1 + 2 + "acme/api-v2".len() + 2 + "content".len();
392 let mut bad_owner = encoded;
393 bad_owner[owner_kind_at] = 5;
394 assert_eq!(
395 ManifestBinding::decode(&bad_owner).unwrap_err(),
396 ManifestBindingDecodeError::UnknownOwnerKind(5)
397 );
398 }
399
400 #[test]
401 fn invalid_facet_tokens_are_rejected() {
402 for token in ["", "Content", "-bad", "bad-", "with space"] {
403 assert!(
404 ManifestFacet::parse(token).is_err(),
405 "accepted facet token {token:?}"
406 );
407 }
408 }
409}