use crate::signing::{BundleSignature, SignaturePolicy, SigningKey, TrustStore};
use crate::validation::{BundleParts, ModuleRecord, validate_archive_parts};
use crate::{
BundleError, CEDAR_VERSION, Diagnostic, FORMAT_VERSION, LabelSet, Result, TREETOP_CORE_VERSION,
};
use flate2::bufread::GzDecoder;
use flate2::{Compression, GzBuilder};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
use std::collections::HashSet;
use std::fs::File;
use std::io::{self, Cursor, Read, Write};
use std::path::{Component, Path};
use tar::{Archive, Builder, EntryType, Header};
use treetop_core::{LabelRegistryBuilder, PolicyEngine, PolicyStoreConfig, PolicyStoreLayout};
const MANIFEST_PATH: &str = "manifest.json";
const SIGNATURE_PATH: &str = "signature.json";
const POLICIES_PATH: &str = "policies.cedar";
const SCHEMA_PATH: &str = "schema.json";
const LABELS_PATH: &str = "labels.json";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ArchiveLimits {
max_compressed_bytes: usize,
max_uncompressed_bytes: usize,
}
impl ArchiveLimits {
pub const DEFAULT_MAX_COMPRESSED_BYTES: usize = 10 * 1024 * 1024;
pub const DEFAULT_MAX_UNCOMPRESSED_BYTES: usize = 50 * 1024 * 1024;
pub fn new(max_compressed_bytes: usize, max_uncompressed_bytes: usize) -> Result<Self> {
if max_compressed_bytes == 0 || max_uncompressed_bytes == 0 {
return Err(BundleError::Archive(
"archive size limits must be greater than zero".to_string(),
));
}
limit_plus_one(max_compressed_bytes, "compressed")?;
limit_plus_one(max_uncompressed_bytes, "uncompressed")?;
Ok(Self {
max_compressed_bytes,
max_uncompressed_bytes,
})
}
pub fn max_compressed_bytes(&self) -> usize {
self.max_compressed_bytes
}
pub fn max_uncompressed_bytes(&self) -> usize {
self.max_uncompressed_bytes
}
}
impl Default for ArchiveLimits {
fn default() -> Self {
Self {
max_compressed_bytes: Self::DEFAULT_MAX_COMPRESSED_BYTES,
max_uncompressed_bytes: Self::DEFAULT_MAX_UNCOMPRESSED_BYTES,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct GeneratorRecord {
treetop_bundle: String,
treetop_core: String,
cedar: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct ArtifactRecord {
path: String,
size: usize,
sha256: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct ArchiveManifest {
format_version: u32,
bundle_id: String,
name: String,
generator: GeneratorRecord,
modules: Vec<ModuleRecord>,
policy_ids: Vec<String>,
artifacts: Vec<ArtifactRecord>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VerifiedSignature {
signed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
key_id: Option<String>,
}
impl VerifiedSignature {
pub fn is_signed(&self) -> bool {
self.signed
}
pub fn key_id(&self) -> Option<&str> {
self.key_id.as_deref()
}
}
pub struct ValidatedBundle {
format_version: u32,
bundle_id: String,
name: String,
modules: Vec<ModuleRecord>,
policies: String,
schema_json: Option<Value>,
labels: LabelSet,
policy_ids: Vec<String>,
diagnostics: Vec<Diagnostic>,
archive_sha256: String,
compressed_size: usize,
signature: VerifiedSignature,
}
impl ValidatedBundle {
pub fn format_version(&self) -> u32 {
self.format_version
}
pub fn bundle_id(&self) -> &str {
&self.bundle_id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn module_count(&self) -> usize {
self.modules.len()
}
pub fn policies(&self) -> &str {
&self.policies
}
pub fn schema_json(&self) -> Option<&Value> {
self.schema_json.as_ref()
}
pub fn schema_json_string(&self) -> Result<Option<String>> {
self.schema_json
.as_ref()
.map(canonical_json_bytes)
.transpose()
.and_then(|value| {
value
.map(|bytes| {
String::from_utf8(bytes)
.map_err(|error| BundleError::Serialization(error.to_string()))
})
.transpose()
})
}
pub fn labels(&self) -> &LabelSet {
&self.labels
}
pub fn labels_json(&self) -> Result<String> {
let bytes = canonical_json_bytes(&self.labels)?;
String::from_utf8(bytes).map_err(|error| BundleError::Serialization(error.to_string()))
}
pub fn policy_ids(&self) -> &[String] {
&self.policy_ids
}
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
pub fn archive_sha256(&self) -> &str {
&self.archive_sha256
}
pub fn compressed_size(&self) -> usize {
self.compressed_size
}
pub fn verified_signature(&self) -> &VerifiedSignature {
&self.signature
}
pub fn prepare_engine(&self) -> Result<PolicyEngine> {
self.prepare_engine_with_layout(None)
}
pub fn prepare_engine_with_policy_stores(&self) -> Result<PolicyEngine> {
let stores = self
.modules
.iter()
.filter(|module| module.role == crate::ModuleRole::Ordinary)
.map(|module| PolicyStoreConfig::new(&module.name, &module.namespace))
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(policy_engine_error)?;
let global_policy_ids = self
.modules
.iter()
.filter(|module| module.role == crate::ModuleRole::Global)
.flat_map(|module| module.policy_ids.iter());
let layout = PolicyStoreLayout::new(stores)
.and_then(|layout| layout.with_global_policy_ids(global_policy_ids))
.map_err(policy_engine_error)?;
self.prepare_engine_with_layout(Some(layout))
}
fn prepare_engine_with_layout(
&self,
layout: Option<PolicyStoreLayout>,
) -> Result<PolicyEngine> {
let mut engine = match (&self.schema_json, layout) {
(Some(schema), layout) => {
let schema =
cedar_policy::Schema::from_json_value(schema.clone()).map_err(|error| {
BundleError::Validation(vec![Diagnostic::error(
"schema.aggregate_invalid",
error.to_string(),
)])
})?;
match layout {
Some(layout) => PolicyEngine::new_from_str_with_schema_and_policy_stores(
&self.policies,
schema,
layout,
),
None => PolicyEngine::new_from_str_with_schema(&self.policies, schema),
}
.map_err(policy_engine_error)?
}
(None, Some(layout)) => {
PolicyEngine::new_from_str_with_policy_stores(&self.policies, layout)
.map_err(policy_engine_error)?
}
(None, None) => {
PolicyEngine::new_from_str(&self.policies).map_err(policy_engine_error)?
}
};
let labelers = self.labels.to_labelers();
if !labelers.is_empty() {
let mut builder = LabelRegistryBuilder::new();
for labeler in labelers {
builder = builder.add_labeler(labeler);
}
engine = engine.with_label_registry(builder.build());
}
Ok(engine)
}
}
fn policy_engine_error(error: impl ToString) -> BundleError {
BundleError::Validation(vec![Diagnostic::error(
"policy.engine_prepare",
error.to_string(),
)])
}
#[derive(Debug, Clone)]
pub struct BundleArchive {
bytes: Vec<u8>,
}
impl BundleArchive {
pub fn from_bytes(bytes: Vec<u8>) -> Self {
Self { bytes }
}
pub fn read(path: impl AsRef<Path>, max_compressed_bytes: usize) -> Result<Self> {
let path = path.as_ref();
let file = File::open(path).map_err(|error| BundleError::io(path, error))?;
let metadata = file
.metadata()
.map_err(|error| BundleError::io(path, error))?;
if !metadata.is_file() {
return Err(BundleError::io(
path,
io::Error::new(io::ErrorKind::InvalidInput, "archive is not a regular file"),
));
}
if metadata.len() > max_compressed_bytes as u64 {
return Err(BundleError::SizeLimit {
kind: "compressed",
limit: max_compressed_bytes,
});
}
let mut bytes = Vec::with_capacity(
usize::try_from(metadata.len())
.unwrap_or(max_compressed_bytes)
.min(max_compressed_bytes),
);
file.take(limit_plus_one(max_compressed_bytes, "compressed")?)
.read_to_end(&mut bytes)
.map_err(|error| BundleError::io(path, error))?;
if bytes.len() > max_compressed_bytes {
return Err(BundleError::SizeLimit {
kind: "compressed",
limit: max_compressed_bytes,
});
}
Ok(Self { bytes })
}
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
pub fn into_bytes(self) -> Vec<u8> {
self.bytes
}
pub fn sha256(&self) -> String {
sha256_hex(&self.bytes)
}
pub fn validate(
&self,
signature_policy: SignaturePolicy,
trust_store: &TrustStore,
limits: ArchiveLimits,
) -> Result<ValidatedBundle> {
self.validate_inner(signature_policy, trust_store, limits, true)
}
pub fn resign(&self, key: &SigningKey, limits: ArchiveLimits) -> Result<Self> {
let decoded = decode_archive(&self.bytes, limits)?;
validate_decoded(
&decoded,
SignaturePolicy::AllowUnsigned,
&TrustStore::new(),
false,
)?;
let signature = key.sign_manifest(&decoded.manifest);
let signature_bytes = canonical_json_bytes(&signature)?;
let bytes = encode_archive(
&decoded.manifest,
Some(&signature_bytes),
artifact_entries(&decoded),
)?;
Ok(Self { bytes })
}
fn validate_inner(
&self,
signature_policy: SignaturePolicy,
trust_store: &TrustStore,
limits: ArchiveLimits,
verify_signature: bool,
) -> Result<ValidatedBundle> {
let decoded = decode_archive(&self.bytes, limits)?;
let (manifest, signature, parts) =
validate_decoded(&decoded, signature_policy, trust_store, verify_signature)?;
Ok(ValidatedBundle {
format_version: manifest.format_version,
bundle_id: manifest.bundle_id,
name: parts.name,
modules: parts.modules,
policies: parts.policies,
schema_json: parts.schema_json,
labels: parts.labels,
policy_ids: parts.policy_ids,
diagnostics: parts.diagnostics,
archive_sha256: sha256_hex(&self.bytes),
compressed_size: self.bytes.len(),
signature,
})
}
pub(crate) fn build(parts: BundleParts, key: Option<&SigningKey>) -> Result<Self> {
let policies = parts.policies.as_bytes().to_vec();
let schema = parts
.schema_json
.as_ref()
.map(canonical_json_bytes)
.transpose()?;
let labels = canonical_json_bytes(&parts.labels)?;
let mut artifact_data = vec![(POLICIES_PATH.to_string(), policies)];
if let Some(schema) = schema {
artifact_data.push((SCHEMA_PATH.to_string(), schema));
}
artifact_data.push((LABELS_PATH.to_string(), labels));
let artifacts = artifact_data
.iter()
.map(|(path, bytes)| ArtifactRecord {
path: path.clone(),
size: bytes.len(),
sha256: sha256_hex(bytes),
})
.collect();
let mut manifest = ArchiveManifest {
format_version: FORMAT_VERSION,
bundle_id: String::new(),
name: parts.name,
generator: GeneratorRecord {
treetop_bundle: env!("CARGO_PKG_VERSION").to_string(),
treetop_core: TREETOP_CORE_VERSION.to_string(),
cedar: CEDAR_VERSION.to_string(),
},
modules: parts.modules,
policy_ids: parts.policy_ids,
artifacts,
};
manifest.bundle_id = compute_bundle_id(&manifest)?;
let manifest_bytes = canonical_json_bytes(&manifest)?;
let signature_bytes = key
.map(|key| canonical_json_bytes(&key.sign_manifest(&manifest_bytes)))
.transpose()?;
let bytes = encode_archive(
&manifest_bytes,
signature_bytes.as_deref(),
artifact_data
.iter()
.map(|(path, contents)| (path.as_str(), contents.as_slice())),
)?;
Ok(Self { bytes })
}
}
struct DecodedArchive {
manifest: Vec<u8>,
signature: Option<Vec<u8>>,
policies: Vec<u8>,
schema: Option<Vec<u8>>,
labels: Vec<u8>,
}
fn decode_archive(bytes: &[u8], limits: ArchiveLimits) -> Result<DecodedArchive> {
if bytes.len() > limits.max_compressed_bytes {
return Err(BundleError::SizeLimit {
kind: "compressed",
limit: limits.max_compressed_bytes,
});
}
let cursor = Cursor::new(bytes);
let decoder = GzDecoder::new(cursor);
let limited = decoder.take(limit_plus_one(
limits.max_uncompressed_bytes,
"uncompressed",
)?);
let mut archive = Archive::new(limited);
let mut entries = Vec::new();
{
let archive_entries = archive
.entries()
.map_err(|error| BundleError::Archive(format!("tar decoding failed: {error}")))?;
for entry in archive_entries {
let mut entry = entry
.map_err(|error| BundleError::Archive(format!("tar entry is invalid: {error}")))?;
if entries.len() == 5 {
return Err(BundleError::Archive(
"bundle archive contains more than five entries".to_string(),
));
}
if entry.header().entry_type() != EntryType::Regular {
return Err(BundleError::Archive(
"only regular tar entries are allowed".to_string(),
));
}
let path = entry
.path()
.map_err(|error| BundleError::Archive(format!("invalid tar path: {error}")))?
.into_owned();
if path.is_absolute()
|| path.components().count() != 1
|| path.components().any(|component| {
matches!(
component,
Component::ParentDir | Component::RootDir | Component::Prefix(_)
)
})
{
return Err(BundleError::Archive(format!(
"unsafe tar entry path {}",
path.display()
)));
}
let name = path
.to_str()
.ok_or_else(|| BundleError::Archive("tar path is not UTF-8".to_string()))?
.to_string();
let mut contents = Vec::new();
entry
.read_to_end(&mut contents)
.map_err(|error| BundleError::Archive(format!("cannot read tar entry: {error}")))?;
entries.push((name, contents));
}
}
let mut limited = archive.into_inner();
io::copy(&mut limited, &mut io::sink())
.map_err(|error| BundleError::Archive(format!("gzip decoding failed: {error}")))?;
if limited.limit() == 0 {
return Err(BundleError::SizeLimit {
kind: "uncompressed",
limit: limits.max_uncompressed_bytes,
});
}
let cursor = limited.into_inner().into_inner();
if cursor.position() != bytes.len() as u64 {
return Err(BundleError::Archive(
"concatenated gzip members or trailing bytes are not allowed".to_string(),
));
}
let names = entries
.iter()
.map(|(name, _)| name.as_str())
.collect::<Vec<_>>();
let valid = matches!(
names.as_slice(),
[MANIFEST_PATH, POLICIES_PATH, LABELS_PATH]
| [MANIFEST_PATH, POLICIES_PATH, SCHEMA_PATH, LABELS_PATH]
| [MANIFEST_PATH, SIGNATURE_PATH, POLICIES_PATH, LABELS_PATH]
| [
MANIFEST_PATH,
SIGNATURE_PATH,
POLICIES_PATH,
SCHEMA_PATH,
LABELS_PATH
]
);
if !valid {
return Err(BundleError::Archive(format!(
"archive entries are missing, unknown, duplicated, or out of order: {names:?}"
)));
}
let mut by_name = entries
.into_iter()
.collect::<std::collections::BTreeMap<_, _>>();
Ok(DecodedArchive {
manifest: by_name
.remove(MANIFEST_PATH)
.expect("validated entry order includes manifest"),
signature: by_name.remove(SIGNATURE_PATH),
policies: by_name
.remove(POLICIES_PATH)
.expect("validated entry order includes policies"),
schema: by_name.remove(SCHEMA_PATH),
labels: by_name
.remove(LABELS_PATH)
.expect("validated entry order includes labels"),
})
}
fn validate_decoded(
decoded: &DecodedArchive,
signature_policy: SignaturePolicy,
trust_store: &TrustStore,
verify_signature: bool,
) -> Result<(ArchiveManifest, VerifiedSignature, BundleParts)> {
let manifest: ArchiveManifest = parse_json(MANIFEST_PATH, &decoded.manifest)?;
let signature: Option<BundleSignature> = decoded
.signature
.as_ref()
.map(|bytes| parse_json(SIGNATURE_PATH, bytes))
.transpose()?;
if let Some(signature) = &signature {
signature.validate_format()?;
}
let verified_signature = match signature {
Some(signature) if verify_signature => VerifiedSignature {
signed: true,
key_id: Some(trust_store.verify(&decoded.manifest, &signature)?),
},
Some(signature) => VerifiedSignature {
signed: true,
key_id: Some(signature.key_id().to_string()),
},
None if signature_policy == SignaturePolicy::Required => {
return Err(BundleError::Archive("signature_missing".to_string()));
}
None => VerifiedSignature {
signed: false,
key_id: None,
},
};
validate_manifest(&manifest)?;
let expected = artifact_entries(decoded);
if manifest.artifacts.len() != expected.len() {
return Err(BundleError::Archive(
"manifest artifact list does not match archive entries".to_string(),
));
}
for (record, (path, contents)) in manifest.artifacts.iter().zip(&expected) {
if record.path != *path
|| record.size != contents.len()
|| record.sha256 != sha256_hex(contents)
{
return Err(BundleError::Archive(format!(
"artifact hash or size mismatch for {path}"
)));
}
}
if manifest.bundle_id != compute_bundle_id(&manifest)? {
return Err(BundleError::Archive(
"manifest bundle_id does not match its canonical payload".to_string(),
));
}
let policies = utf8(POLICIES_PATH, &decoded.policies)?;
let schema_json = decoded
.schema
.as_ref()
.map(|bytes| parse_json(SCHEMA_PATH, bytes))
.transpose()?;
let labels_source = utf8(LABELS_PATH, &decoded.labels)?;
let labels = LabelSet::from_json_str(&labels_source)?;
let parts = validate_archive_parts(
manifest.name.clone(),
manifest.modules.clone(),
policies,
schema_json,
labels,
&manifest.policy_ids,
)?;
Ok((manifest, verified_signature, parts))
}
fn validate_manifest(manifest: &ArchiveManifest) -> Result<()> {
if manifest.format_version != FORMAT_VERSION {
return Err(BundleError::Archive(format!(
"unsupported bundle format version {}",
manifest.format_version
)));
}
if manifest.name.trim().is_empty() {
return Err(BundleError::Archive(
"bundle manifest name must not be empty".to_string(),
));
}
if manifest.generator.treetop_bundle != env!("CARGO_PKG_VERSION")
|| manifest.generator.treetop_core != TREETOP_CORE_VERSION
|| manifest.generator.cedar != CEDAR_VERSION
{
return Err(BundleError::Archive(
"bundle generator dependency versions are unsupported".to_string(),
));
}
let mut module_names = HashSet::new();
let mut namespaces: Vec<&str> = Vec::new();
let mut assigned_policy_ids = HashSet::new();
if manifest.modules.is_empty() {
return Err(BundleError::Archive(
"manifest must contain at least one module".to_string(),
));
}
if !manifest
.modules
.windows(2)
.all(|pair| pair[0].name < pair[1].name)
{
return Err(BundleError::Archive(
"manifest modules are not ordered by name".to_string(),
));
}
if !manifest.policy_ids.windows(2).all(|pair| pair[0] < pair[1]) {
return Err(BundleError::Archive(
"manifest policy IDs must be sorted and unique".to_string(),
));
}
let selected_namespaces = manifest
.modules
.iter()
.map(|module| module.namespace.as_str())
.collect::<HashSet<_>>();
for module in &manifest.modules {
if module.name.trim().is_empty()
|| module.namespace.trim().is_empty()
|| module
.namespace
.parse::<cedar_policy::EntityTypeName>()
.is_err()
{
return Err(BundleError::Archive(
"manifest contains an invalid module name or namespace".to_string(),
));
}
if !module_names.insert(module.name.as_str()) {
return Err(BundleError::Archive(
"manifest contains duplicate module names".to_string(),
));
}
namespaces.push(module.namespace.as_str());
let mut imports = HashSet::new();
if module.imports.iter().any(|import| {
!selected_namespaces.contains(import.as_str())
|| import == &module.namespace
|| !imports.insert(import)
}) {
return Err(BundleError::Archive(
"manifest contains an unresolved, self, or duplicate module import".to_string(),
));
}
if module
.policy_ids
.iter()
.any(|policy_id| !assigned_policy_ids.insert(policy_id.as_str()))
{
return Err(BundleError::Archive(
"manifest assigns a policy ID more than once".to_string(),
));
}
}
namespaces.sort_unstable();
if namespaces.windows(2).any(|pair| {
crate::manifest::namespace_owns(pair[0], pair[1])
|| crate::manifest::namespace_owns(pair[1], pair[0])
}) {
return Err(BundleError::Archive(
"manifest contains overlapping module namespaces".to_string(),
));
}
if assigned_policy_ids
!= manifest
.policy_ids
.iter()
.map(String::as_str)
.collect::<HashSet<_>>()
{
return Err(BundleError::Archive(
"manifest module policy assignments do not match policy_ids".to_string(),
));
}
Ok(())
}
fn artifact_entries(decoded: &DecodedArchive) -> Vec<(&'static str, &[u8])> {
let mut entries = vec![(POLICIES_PATH, decoded.policies.as_slice())];
if let Some(schema) = &decoded.schema {
entries.push((SCHEMA_PATH, schema.as_slice()));
}
entries.push((LABELS_PATH, decoded.labels.as_slice()));
entries
}
fn encode_archive<'a>(
manifest: &[u8],
signature: Option<&[u8]>,
artifacts: impl IntoIterator<Item = (&'a str, &'a [u8])>,
) -> Result<Vec<u8>> {
let encoder = GzBuilder::new()
.mtime(0)
.write(Vec::new(), Compression::default());
let mut builder = Builder::new(encoder);
append_tar_file(&mut builder, MANIFEST_PATH, manifest)?;
if let Some(signature) = signature {
append_tar_file(&mut builder, SIGNATURE_PATH, signature)?;
}
for (path, contents) in artifacts {
append_tar_file(&mut builder, path, contents)?;
}
builder
.finish()
.map_err(|error| BundleError::Archive(format!("tar encoding failed: {error}")))?;
let encoder = builder
.into_inner()
.map_err(|error| BundleError::Archive(format!("tar encoding failed: {error}")))?;
encoder
.finish()
.map_err(|error| BundleError::Archive(format!("gzip encoding failed: {error}")))
}
fn append_tar_file<W: Write>(builder: &mut Builder<W>, path: &str, contents: &[u8]) -> Result<()> {
let mut header = Header::new_gnu();
header.set_entry_type(EntryType::Regular);
header.set_mode(0o644);
header.set_uid(0);
header.set_gid(0);
header.set_mtime(0);
header.set_size(contents.len() as u64);
header.set_cksum();
builder
.append_data(&mut header, path, Cursor::new(contents))
.map_err(|error| BundleError::Archive(format!("tar encoding failed: {error}")))
}
fn compute_bundle_id(manifest: &ArchiveManifest) -> Result<String> {
let mut payload = serde_json::to_value(manifest)
.map_err(|error| BundleError::Serialization(error.to_string()))?;
payload
.as_object_mut()
.expect("ArchiveManifest always serializes as an object")
.remove("bundle_id");
Ok(sha256_hex(&canonical_json_bytes(&payload)?))
}
pub(crate) fn canonical_json_bytes(value: &impl Serialize) -> Result<Vec<u8>> {
let value = serde_json::to_value(value)
.map_err(|error| BundleError::Serialization(error.to_string()))?;
let value = sort_json(value);
let mut bytes = serde_json::to_vec(&value)
.map_err(|error| BundleError::Serialization(error.to_string()))?;
bytes.push(b'\n');
Ok(bytes)
}
fn sort_json(value: Value) -> Value {
match value {
Value::Object(object) => {
let sorted = object
.into_iter()
.map(|(key, value)| (key, sort_json(value)))
.collect::<std::collections::BTreeMap<_, _>>();
Value::Object(sorted.into_iter().collect::<Map<_, _>>())
}
Value::Array(values) => Value::Array(values.into_iter().map(sort_json).collect()),
other => other,
}
}
fn parse_json<T: for<'de> Deserialize<'de>>(path: &str, bytes: &[u8]) -> Result<T> {
let source = std::str::from_utf8(bytes)
.map_err(|error| BundleError::Archive(format!("{path} is not UTF-8: {error}")))?;
serde_json::from_str(source)
.map_err(|error| BundleError::Archive(format!("{path} is invalid JSON: {error}")))
}
fn utf8(path: &str, bytes: &[u8]) -> Result<String> {
String::from_utf8(bytes.to_vec())
.map_err(|error| BundleError::Archive(format!("{path} is not UTF-8: {error}")))
}
fn sha256_hex(bytes: &[u8]) -> String {
Sha256::digest(bytes)
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
fn limit_plus_one(limit: usize, kind: &'static str) -> Result<u64> {
u64::try_from(limit)
.ok()
.and_then(|limit| limit.checked_add(1))
.ok_or_else(|| BundleError::Archive(format!("{kind} size limit is too large to enforce")))
}