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