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::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/// 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        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/// Signature verification details for a validated bundle.
96#[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
114/// A decoded bundle whose signatures, hashes, Cedar, schema, and labels are valid.
115pub 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    /// Build a complete engine without modifying any application state.
200    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/// An in-memory gzip-compressed Treetop bundle archive.
237#[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    /// Validate and re-sign an archive, replacing its existing signature.
304    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}