Skip to main content

treetop_bundle/
archive.rs

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/// Default and configured archive limits.
25#[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/// Signature verification details for a validated bundle.
94#[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
112/// A decoded bundle whose signatures, hashes, Cedar, schema, and labels are valid.
113pub 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    /// Build a complete engine without modifying any application state.
198    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/// An in-memory gzip-compressed Treetop bundle archive.
235#[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    /// Validate and re-sign an archive, replacing its existing signature.
280    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        // Preserve the logical identity by construction and guard it explicitly.
295        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}