1use crate::signing::{BundleSignature, SignaturePolicy, SigningKey, TrustStore};
2use crate::validation::{BundleParts, ModuleRecord, validate_archive_parts};
3use crate::{
4 BundleError, CEDAR_VERSION, Diagnostic, FORMAT_VERSION, LabelSet, Result, TREETOP_CORE_VERSION,
5};
6use flate2::bufread::GzDecoder;
7use flate2::{Compression, GzBuilder};
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10use sha2::{Digest, Sha256};
11use std::collections::HashSet;
12use std::fs;
13use std::io::{Cursor, Read, Write};
14use std::path::{Component, Path};
15use tar::{Archive, Builder, EntryType, Header};
16use treetop_core::{LabelRegistryBuilder, PolicyEngine};
17
18const MANIFEST_PATH: &str = "manifest.json";
19const SIGNATURE_PATH: &str = "signature.json";
20const POLICIES_PATH: &str = "policies.cedar";
21const SCHEMA_PATH: &str = "schema.json";
22const LABELS_PATH: &str = "labels.json";
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct ArchiveLimits {
27 max_compressed_bytes: usize,
28 max_uncompressed_bytes: usize,
29}
30
31impl ArchiveLimits {
32 pub const DEFAULT_MAX_COMPRESSED_BYTES: usize = 10 * 1024 * 1024;
33 pub const DEFAULT_MAX_UNCOMPRESSED_BYTES: usize = 50 * 1024 * 1024;
34
35 pub fn new(max_compressed_bytes: usize, max_uncompressed_bytes: usize) -> Result<Self> {
36 if max_compressed_bytes == 0 || max_uncompressed_bytes == 0 {
37 return Err(BundleError::Archive(
38 "archive size limits must be greater than zero".to_string(),
39 ));
40 }
41 Ok(Self {
42 max_compressed_bytes,
43 max_uncompressed_bytes,
44 })
45 }
46
47 pub fn max_compressed_bytes(&self) -> usize {
48 self.max_compressed_bytes
49 }
50
51 pub fn max_uncompressed_bytes(&self) -> usize {
52 self.max_uncompressed_bytes
53 }
54}
55
56impl Default for ArchiveLimits {
57 fn default() -> Self {
58 Self {
59 max_compressed_bytes: Self::DEFAULT_MAX_COMPRESSED_BYTES,
60 max_uncompressed_bytes: Self::DEFAULT_MAX_UNCOMPRESSED_BYTES,
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(deny_unknown_fields)]
67struct GeneratorRecord {
68 treetop_bundle: String,
69 treetop_core: String,
70 cedar: String,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(deny_unknown_fields)]
75struct ArtifactRecord {
76 path: String,
77 size: usize,
78 sha256: String,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(deny_unknown_fields)]
83struct ArchiveManifest {
84 format_version: u32,
85 bundle_id: String,
86 name: String,
87 generator: GeneratorRecord,
88 modules: Vec<ModuleRecord>,
89 policy_ids: Vec<String>,
90 artifacts: Vec<ArtifactRecord>,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(deny_unknown_fields)]
96pub struct VerifiedSignature {
97 signed: bool,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 key_id: Option<String>,
100}
101
102impl VerifiedSignature {
103 pub fn is_signed(&self) -> bool {
104 self.signed
105 }
106
107 pub fn key_id(&self) -> Option<&str> {
108 self.key_id.as_deref()
109 }
110}
111
112pub struct ValidatedBundle {
114 format_version: u32,
115 bundle_id: String,
116 name: String,
117 modules: Vec<ModuleRecord>,
118 policies: String,
119 schema_json: Option<Value>,
120 labels: LabelSet,
121 policy_ids: Vec<String>,
122 diagnostics: Vec<Diagnostic>,
123 archive_sha256: String,
124 compressed_size: usize,
125 signature: VerifiedSignature,
126}
127
128impl ValidatedBundle {
129 pub fn format_version(&self) -> u32 {
130 self.format_version
131 }
132
133 pub fn bundle_id(&self) -> &str {
134 &self.bundle_id
135 }
136
137 pub fn name(&self) -> &str {
138 &self.name
139 }
140
141 pub fn module_count(&self) -> usize {
142 self.modules.len()
143 }
144
145 pub fn policies(&self) -> &str {
146 &self.policies
147 }
148
149 pub fn schema_json(&self) -> Option<&Value> {
150 self.schema_json.as_ref()
151 }
152
153 pub fn schema_json_string(&self) -> Result<Option<String>> {
154 self.schema_json
155 .as_ref()
156 .map(canonical_json_bytes)
157 .transpose()
158 .and_then(|value| {
159 value
160 .map(|bytes| {
161 String::from_utf8(bytes)
162 .map_err(|error| BundleError::Serialization(error.to_string()))
163 })
164 .transpose()
165 })
166 }
167
168 pub fn labels(&self) -> &LabelSet {
169 &self.labels
170 }
171
172 pub fn labels_json(&self) -> Result<String> {
173 let bytes = canonical_json_bytes(&self.labels)?;
174 String::from_utf8(bytes).map_err(|error| BundleError::Serialization(error.to_string()))
175 }
176
177 pub fn policy_ids(&self) -> &[String] {
178 &self.policy_ids
179 }
180
181 pub fn diagnostics(&self) -> &[Diagnostic] {
182 &self.diagnostics
183 }
184
185 pub fn archive_sha256(&self) -> &str {
186 &self.archive_sha256
187 }
188
189 pub fn compressed_size(&self) -> usize {
190 self.compressed_size
191 }
192
193 pub fn verified_signature(&self) -> &VerifiedSignature {
194 &self.signature
195 }
196
197 pub fn prepare_engine(&self) -> Result<PolicyEngine> {
199 let mut engine = match &self.schema_json {
200 Some(schema) => {
201 let schema =
202 cedar_policy::Schema::from_json_value(schema.clone()).map_err(|error| {
203 BundleError::Validation(vec![Diagnostic::error(
204 "schema.aggregate_invalid",
205 error.to_string(),
206 )])
207 })?;
208 PolicyEngine::new_from_str_with_schema(&self.policies, schema).map_err(|error| {
209 BundleError::Validation(vec![Diagnostic::error(
210 "policy.engine_prepare",
211 error.to_string(),
212 )])
213 })?
214 }
215 None => PolicyEngine::new_from_str(&self.policies).map_err(|error| {
216 BundleError::Validation(vec![Diagnostic::error(
217 "policy.engine_prepare",
218 error.to_string(),
219 )])
220 })?,
221 };
222 let labelers = self.labels.to_labelers();
223 if !labelers.is_empty() {
224 let mut builder = LabelRegistryBuilder::new();
225 for labeler in labelers {
226 builder = builder.add_labeler(labeler);
227 }
228 engine = engine.with_label_registry(builder.build());
229 }
230 Ok(engine)
231 }
232}
233
234#[derive(Debug, Clone)]
236pub struct BundleArchive {
237 bytes: Vec<u8>,
238}
239
240impl BundleArchive {
241 pub fn from_bytes(bytes: Vec<u8>) -> Self {
242 Self { bytes }
243 }
244
245 pub fn read(path: impl AsRef<Path>, max_compressed_bytes: usize) -> Result<Self> {
246 let path = path.as_ref();
247 let metadata = fs::metadata(path).map_err(|error| BundleError::io(path, error))?;
248 if metadata.len() > max_compressed_bytes as u64 {
249 return Err(BundleError::SizeLimit {
250 kind: "compressed",
251 limit: max_compressed_bytes,
252 });
253 }
254 let bytes = fs::read(path).map_err(|error| BundleError::io(path, error))?;
255 Ok(Self { bytes })
256 }
257
258 pub fn as_bytes(&self) -> &[u8] {
259 &self.bytes
260 }
261
262 pub fn into_bytes(self) -> Vec<u8> {
263 self.bytes
264 }
265
266 pub fn sha256(&self) -> String {
267 sha256_hex(&self.bytes)
268 }
269
270 pub fn validate(
271 &self,
272 signature_policy: SignaturePolicy,
273 trust_store: &TrustStore,
274 limits: ArchiveLimits,
275 ) -> Result<ValidatedBundle> {
276 self.validate_inner(signature_policy, trust_store, limits, true)
277 }
278
279 pub fn resign(&self, key: &SigningKey, limits: ArchiveLimits) -> Result<Self> {
281 let decoded = decode_archive(&self.bytes, limits)?;
282 let (manifest, _, _) = validate_decoded(
283 &decoded,
284 SignaturePolicy::AllowUnsigned,
285 &TrustStore::new(),
286 false,
287 )?;
288 let signature = key.sign_manifest(&decoded.manifest);
289 let signature_bytes = canonical_json_bytes(&signature)?;
290 let artifacts = artifact_entries(&decoded);
291 let bytes = encode_archive(&decoded.manifest, Some(&signature_bytes), &artifacts)?;
292 let rebuilt = Self { bytes };
293
294 let rebuilt_decoded = decode_archive(&rebuilt.bytes, limits)?;
296 let rebuilt_manifest: ArchiveManifest =
297 parse_json(MANIFEST_PATH, &rebuilt_decoded.manifest)?;
298 if rebuilt_manifest.bundle_id != manifest.bundle_id {
299 return Err(BundleError::Archive(
300 "re-signing changed the logical bundle ID".to_string(),
301 ));
302 }
303 Ok(rebuilt)
304 }
305
306 fn validate_inner(
307 &self,
308 signature_policy: SignaturePolicy,
309 trust_store: &TrustStore,
310 limits: ArchiveLimits,
311 verify_signature: bool,
312 ) -> Result<ValidatedBundle> {
313 let decoded = decode_archive(&self.bytes, limits)?;
314 let (manifest, signature, parts) =
315 validate_decoded(&decoded, signature_policy, trust_store, verify_signature)?;
316 Ok(ValidatedBundle {
317 format_version: manifest.format_version,
318 bundle_id: manifest.bundle_id,
319 name: parts.name,
320 modules: parts.modules,
321 policies: parts.policies,
322 schema_json: parts.schema_json,
323 labels: parts.labels,
324 policy_ids: parts.policy_ids,
325 diagnostics: parts.diagnostics,
326 archive_sha256: sha256_hex(&self.bytes),
327 compressed_size: self.bytes.len(),
328 signature,
329 })
330 }
331
332 pub(crate) fn build(parts: BundleParts, key: Option<&SigningKey>) -> Result<Self> {
333 let policies = parts.policies.as_bytes().to_vec();
334 let schema = parts
335 .schema_json
336 .as_ref()
337 .map(canonical_json_bytes)
338 .transpose()?;
339 let labels = canonical_json_bytes(&parts.labels)?;
340
341 let mut artifact_data = vec![(POLICIES_PATH.to_string(), policies)];
342 if let Some(schema) = schema {
343 artifact_data.push((SCHEMA_PATH.to_string(), schema));
344 }
345 artifact_data.push((LABELS_PATH.to_string(), labels));
346 let artifacts = artifact_data
347 .iter()
348 .map(|(path, bytes)| ArtifactRecord {
349 path: path.clone(),
350 size: bytes.len(),
351 sha256: sha256_hex(bytes),
352 })
353 .collect();
354 let mut manifest = ArchiveManifest {
355 format_version: FORMAT_VERSION,
356 bundle_id: String::new(),
357 name: parts.name,
358 generator: GeneratorRecord {
359 treetop_bundle: env!("CARGO_PKG_VERSION").to_string(),
360 treetop_core: TREETOP_CORE_VERSION.to_string(),
361 cedar: CEDAR_VERSION.to_string(),
362 },
363 modules: parts.modules,
364 policy_ids: parts.policy_ids,
365 artifacts,
366 };
367 manifest.bundle_id = compute_bundle_id(&manifest)?;
368 let manifest_bytes = canonical_json_bytes(&manifest)?;
369 let signature_bytes = key
370 .map(|key| canonical_json_bytes(&key.sign_manifest(&manifest_bytes)))
371 .transpose()?;
372 let bytes = encode_archive(&manifest_bytes, signature_bytes.as_deref(), &artifact_data)?;
373 Ok(Self { bytes })
374 }
375}
376
377struct DecodedArchive {
378 manifest: Vec<u8>,
379 signature: Option<Vec<u8>>,
380 policies: Vec<u8>,
381 schema: Option<Vec<u8>>,
382 labels: Vec<u8>,
383}
384
385fn decode_archive(bytes: &[u8], limits: ArchiveLimits) -> Result<DecodedArchive> {
386 if bytes.len() > limits.max_compressed_bytes {
387 return Err(BundleError::SizeLimit {
388 kind: "compressed",
389 limit: limits.max_compressed_bytes,
390 });
391 }
392 let cursor = Cursor::new(bytes);
393 let mut decoder = GzDecoder::new(cursor);
394 let mut uncompressed = Vec::new();
395 decoder
396 .by_ref()
397 .take((limits.max_uncompressed_bytes as u64) + 1)
398 .read_to_end(&mut uncompressed)
399 .map_err(|error| BundleError::Archive(format!("gzip decoding failed: {error}")))?;
400 if uncompressed.len() > limits.max_uncompressed_bytes {
401 return Err(BundleError::SizeLimit {
402 kind: "uncompressed",
403 limit: limits.max_uncompressed_bytes,
404 });
405 }
406 let cursor = decoder.into_inner();
407 if cursor.position() != bytes.len() as u64 {
408 return Err(BundleError::Archive(
409 "concatenated gzip members or trailing bytes are not allowed".to_string(),
410 ));
411 }
412
413 let mut archive = Archive::new(Cursor::new(uncompressed));
414 let mut entries = Vec::new();
415 let archive_entries = archive
416 .entries()
417 .map_err(|error| BundleError::Archive(format!("tar decoding failed: {error}")))?;
418 for entry in archive_entries {
419 let mut entry = entry
420 .map_err(|error| BundleError::Archive(format!("tar entry is invalid: {error}")))?;
421 if entries.len() == 5 {
422 return Err(BundleError::Archive(
423 "bundle archive contains more than five entries".to_string(),
424 ));
425 }
426 if entry.header().entry_type() != EntryType::Regular {
427 return Err(BundleError::Archive(
428 "only regular tar entries are allowed".to_string(),
429 ));
430 }
431 let path = entry
432 .path()
433 .map_err(|error| BundleError::Archive(format!("invalid tar path: {error}")))?
434 .into_owned();
435 if path.is_absolute()
436 || path.components().count() != 1
437 || path.components().any(|component| {
438 matches!(
439 component,
440 Component::ParentDir | Component::RootDir | Component::Prefix(_)
441 )
442 })
443 {
444 return Err(BundleError::Archive(format!(
445 "unsafe tar entry path {}",
446 path.display()
447 )));
448 }
449 let name = path
450 .to_str()
451 .ok_or_else(|| BundleError::Archive("tar path is not UTF-8".to_string()))?
452 .to_string();
453 let mut contents = Vec::new();
454 entry
455 .read_to_end(&mut contents)
456 .map_err(|error| BundleError::Archive(format!("cannot read tar entry: {error}")))?;
457 entries.push((name, contents));
458 }
459
460 let names = entries
461 .iter()
462 .map(|(name, _)| name.as_str())
463 .collect::<Vec<_>>();
464 let valid = matches!(
465 names.as_slice(),
466 [MANIFEST_PATH, POLICIES_PATH, LABELS_PATH]
467 | [MANIFEST_PATH, POLICIES_PATH, SCHEMA_PATH, LABELS_PATH]
468 | [MANIFEST_PATH, SIGNATURE_PATH, POLICIES_PATH, LABELS_PATH]
469 | [
470 MANIFEST_PATH,
471 SIGNATURE_PATH,
472 POLICIES_PATH,
473 SCHEMA_PATH,
474 LABELS_PATH
475 ]
476 );
477 if !valid {
478 return Err(BundleError::Archive(format!(
479 "archive entries are missing, unknown, duplicated, or out of order: {names:?}"
480 )));
481 }
482
483 let mut by_name = entries
484 .into_iter()
485 .collect::<std::collections::BTreeMap<_, _>>();
486 Ok(DecodedArchive {
487 manifest: by_name
488 .remove(MANIFEST_PATH)
489 .expect("validated entry order includes manifest"),
490 signature: by_name.remove(SIGNATURE_PATH),
491 policies: by_name
492 .remove(POLICIES_PATH)
493 .expect("validated entry order includes policies"),
494 schema: by_name.remove(SCHEMA_PATH),
495 labels: by_name
496 .remove(LABELS_PATH)
497 .expect("validated entry order includes labels"),
498 })
499}
500
501fn validate_decoded(
502 decoded: &DecodedArchive,
503 signature_policy: SignaturePolicy,
504 trust_store: &TrustStore,
505 verify_signature: bool,
506) -> Result<(ArchiveManifest, VerifiedSignature, BundleParts)> {
507 let manifest: ArchiveManifest = parse_json(MANIFEST_PATH, &decoded.manifest)?;
508 let signature: Option<BundleSignature> = decoded
509 .signature
510 .as_ref()
511 .map(|bytes| parse_json(SIGNATURE_PATH, bytes))
512 .transpose()?;
513 if let Some(signature) = &signature {
514 signature.validate_format()?;
515 }
516
517 let verified_signature = match signature {
518 Some(signature) if verify_signature => VerifiedSignature {
519 signed: true,
520 key_id: Some(trust_store.verify(&decoded.manifest, &signature)?),
521 },
522 Some(signature) => VerifiedSignature {
523 signed: true,
524 key_id: Some(signature.key_id().to_string()),
525 },
526 None if signature_policy == SignaturePolicy::Required => {
527 return Err(BundleError::Archive("signature_missing".to_string()));
528 }
529 None => VerifiedSignature {
530 signed: false,
531 key_id: None,
532 },
533 };
534
535 validate_manifest(&manifest)?;
536
537 let expected = artifact_entries(decoded);
538 if manifest.artifacts.len() != expected.len() {
539 return Err(BundleError::Archive(
540 "manifest artifact list does not match archive entries".to_string(),
541 ));
542 }
543 for (record, (path, contents)) in manifest.artifacts.iter().zip(&expected) {
544 if record.path != *path
545 || record.size != contents.len()
546 || record.sha256 != sha256_hex(contents)
547 {
548 return Err(BundleError::Archive(format!(
549 "artifact hash or size mismatch for {path}"
550 )));
551 }
552 }
553 if manifest.bundle_id != compute_bundle_id(&manifest)? {
554 return Err(BundleError::Archive(
555 "manifest bundle_id does not match its canonical payload".to_string(),
556 ));
557 }
558
559 let policies = utf8(POLICIES_PATH, &decoded.policies)?;
560 let schema_json = decoded
561 .schema
562 .as_ref()
563 .map(|bytes| parse_json(SCHEMA_PATH, bytes))
564 .transpose()?;
565 let labels_source = utf8(LABELS_PATH, &decoded.labels)?;
566 let labels = LabelSet::from_json_str(&labels_source)?;
567 let parts = validate_archive_parts(
568 manifest.name.clone(),
569 manifest.modules.clone(),
570 policies,
571 schema_json,
572 labels,
573 &manifest.policy_ids,
574 )?;
575 Ok((manifest, verified_signature, parts))
576}
577
578fn validate_manifest(manifest: &ArchiveManifest) -> Result<()> {
579 if manifest.format_version != FORMAT_VERSION {
580 return Err(BundleError::Archive(format!(
581 "unsupported bundle format version {}",
582 manifest.format_version
583 )));
584 }
585 if manifest.name.trim().is_empty() {
586 return Err(BundleError::Archive(
587 "bundle manifest name must not be empty".to_string(),
588 ));
589 }
590 if manifest.generator.treetop_bundle != env!("CARGO_PKG_VERSION")
591 || manifest.generator.treetop_core != TREETOP_CORE_VERSION
592 || manifest.generator.cedar != CEDAR_VERSION
593 {
594 return Err(BundleError::Archive(
595 "bundle generator dependency versions are unsupported".to_string(),
596 ));
597 }
598 let mut module_names = HashSet::new();
599 let mut namespaces: Vec<&str> = Vec::new();
600 let mut assigned_policy_ids = HashSet::new();
601 if manifest.modules.is_empty() {
602 return Err(BundleError::Archive(
603 "manifest must contain at least one module".to_string(),
604 ));
605 }
606 if !manifest
607 .modules
608 .windows(2)
609 .all(|pair| pair[0].name < pair[1].name)
610 {
611 return Err(BundleError::Archive(
612 "manifest modules are not ordered by name".to_string(),
613 ));
614 }
615 if !manifest.policy_ids.windows(2).all(|pair| pair[0] < pair[1]) {
616 return Err(BundleError::Archive(
617 "manifest policy IDs must be sorted and unique".to_string(),
618 ));
619 }
620 let selected_namespaces = manifest
621 .modules
622 .iter()
623 .map(|module| module.namespace.as_str())
624 .collect::<HashSet<_>>();
625 for module in &manifest.modules {
626 if module.name.trim().is_empty()
627 || module.namespace.trim().is_empty()
628 || module
629 .namespace
630 .parse::<cedar_policy::EntityTypeName>()
631 .is_err()
632 {
633 return Err(BundleError::Archive(
634 "manifest contains an invalid module name or namespace".to_string(),
635 ));
636 }
637 if !module_names.insert(module.name.as_str()) {
638 return Err(BundleError::Archive(
639 "manifest contains duplicate module names".to_string(),
640 ));
641 }
642 for existing in &namespaces {
643 if crate::manifest::namespace_owns(existing, &module.namespace)
644 || crate::manifest::namespace_owns(&module.namespace, existing)
645 {
646 return Err(BundleError::Archive(
647 "manifest contains overlapping module namespaces".to_string(),
648 ));
649 }
650 }
651 namespaces.push(module.namespace.as_str());
652 let mut imports = HashSet::new();
653 if module.imports.iter().any(|import| {
654 !selected_namespaces.contains(import.as_str())
655 || import == &module.namespace
656 || !imports.insert(import)
657 }) {
658 return Err(BundleError::Archive(
659 "manifest contains an unresolved, self, or duplicate module import".to_string(),
660 ));
661 }
662 if module
663 .policy_ids
664 .iter()
665 .any(|policy_id| !assigned_policy_ids.insert(policy_id.as_str()))
666 {
667 return Err(BundleError::Archive(
668 "manifest assigns a policy ID more than once".to_string(),
669 ));
670 }
671 }
672 if assigned_policy_ids
673 != manifest
674 .policy_ids
675 .iter()
676 .map(String::as_str)
677 .collect::<HashSet<_>>()
678 {
679 return Err(BundleError::Archive(
680 "manifest module policy assignments do not match policy_ids".to_string(),
681 ));
682 }
683 Ok(())
684}
685
686fn artifact_entries(decoded: &DecodedArchive) -> Vec<(String, Vec<u8>)> {
687 let mut entries = vec![(POLICIES_PATH.to_string(), decoded.policies.clone())];
688 if let Some(schema) = &decoded.schema {
689 entries.push((SCHEMA_PATH.to_string(), schema.clone()));
690 }
691 entries.push((LABELS_PATH.to_string(), decoded.labels.clone()));
692 entries
693}
694
695fn encode_archive(
696 manifest: &[u8],
697 signature: Option<&[u8]>,
698 artifacts: &[(String, Vec<u8>)],
699) -> Result<Vec<u8>> {
700 let mut tar_bytes = Vec::new();
701 {
702 let mut builder = Builder::new(&mut tar_bytes);
703 append_tar_file(&mut builder, MANIFEST_PATH, manifest)?;
704 if let Some(signature) = signature {
705 append_tar_file(&mut builder, SIGNATURE_PATH, signature)?;
706 }
707 for (path, contents) in artifacts {
708 append_tar_file(&mut builder, path, contents)?;
709 }
710 builder
711 .finish()
712 .map_err(|error| BundleError::Archive(format!("tar encoding failed: {error}")))?;
713 }
714 let mut encoder = GzBuilder::new()
715 .mtime(0)
716 .write(Vec::new(), Compression::default());
717 encoder
718 .write_all(&tar_bytes)
719 .map_err(|error| BundleError::Archive(format!("gzip encoding failed: {error}")))?;
720 encoder
721 .finish()
722 .map_err(|error| BundleError::Archive(format!("gzip encoding failed: {error}")))
723}
724
725fn append_tar_file(builder: &mut Builder<&mut Vec<u8>>, path: &str, contents: &[u8]) -> Result<()> {
726 let mut header = Header::new_gnu();
727 header.set_entry_type(EntryType::Regular);
728 header.set_mode(0o644);
729 header.set_uid(0);
730 header.set_gid(0);
731 header.set_mtime(0);
732 header.set_size(contents.len() as u64);
733 header.set_cksum();
734 builder
735 .append_data(&mut header, path, Cursor::new(contents))
736 .map_err(|error| BundleError::Archive(format!("tar encoding failed: {error}")))
737}
738
739fn compute_bundle_id(manifest: &ArchiveManifest) -> Result<String> {
740 let mut payload = serde_json::to_value(manifest)
741 .map_err(|error| BundleError::Serialization(error.to_string()))?;
742 payload
743 .as_object_mut()
744 .expect("ArchiveManifest always serializes as an object")
745 .remove("bundle_id");
746 Ok(sha256_hex(&canonical_json_bytes(&payload)?))
747}
748
749pub(crate) fn canonical_json_bytes(value: &impl Serialize) -> Result<Vec<u8>> {
750 let value = serde_json::to_value(value)
751 .map_err(|error| BundleError::Serialization(error.to_string()))?;
752 let value = sort_json(value);
753 let mut bytes = serde_json::to_vec(&value)
754 .map_err(|error| BundleError::Serialization(error.to_string()))?;
755 bytes.push(b'\n');
756 Ok(bytes)
757}
758
759fn sort_json(value: Value) -> Value {
760 match value {
761 Value::Object(object) => {
762 let sorted = object
763 .into_iter()
764 .map(|(key, value)| (key, sort_json(value)))
765 .collect::<std::collections::BTreeMap<_, _>>();
766 Value::Object(sorted.into_iter().collect::<Map<_, _>>())
767 }
768 Value::Array(values) => Value::Array(values.into_iter().map(sort_json).collect()),
769 other => other,
770 }
771}
772
773fn parse_json<T: for<'de> Deserialize<'de>>(path: &str, bytes: &[u8]) -> Result<T> {
774 let source = std::str::from_utf8(bytes)
775 .map_err(|error| BundleError::Archive(format!("{path} is not UTF-8: {error}")))?;
776 serde_json::from_str(source)
777 .map_err(|error| BundleError::Archive(format!("{path} is invalid JSON: {error}")))
778}
779
780fn utf8(path: &str, bytes: &[u8]) -> Result<String> {
781 String::from_utf8(bytes.to_vec())
782 .map_err(|error| BundleError::Archive(format!("{path} is not UTF-8: {error}")))
783}
784
785fn sha256_hex(bytes: &[u8]) -> String {
786 Sha256::digest(bytes)
787 .iter()
788 .map(|byte| format!("{byte:02x}"))
789 .collect()
790}