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