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