use std::path::Path;
use thiserror::Error;
use zip::ZipArchive;
use crate::{
build::PACKED_MANIFEST_ENTRY,
manifest::{Manifest, ManifestError},
registry::RuntimeType,
zip_guard::{self, ZipGuardError},
};
pub fn load_manifest_yaml_from_artifact(path: &Path) -> Result<String, ArtifactError> {
let bytes = std::fs::read(path).map_err(|source| ArtifactError::Io {
path: path.display().to_string(),
source,
})?;
load_manifest_yaml_from_artifact_bytes(&bytes)
}
pub fn load_manifest_yaml_from_artifact_bytes(bytes: &[u8]) -> Result<String, ArtifactError> {
load_manifest_yaml_from_artifact_bytes_capped(
bytes,
zip_guard::max_artifact_decompressed_bytes(),
)
}
fn load_manifest_yaml_from_artifact_bytes_capped(
bytes: &[u8],
max_bytes: u64,
) -> Result<String, ArtifactError> {
let cursor = std::io::Cursor::new(bytes);
let mut archive = ZipArchive::new(cursor).map_err(|source| ArtifactError::Zip {
path: "<bytes>".to_string(),
source,
})?;
match zip_guard::read_zip_entry_to_string_capped(&mut archive, PACKED_MANIFEST_ENTRY, max_bytes)
{
Ok(content) => Ok(content),
Err(ZipGuardError::Zip(_)) => Err(ArtifactError::MissingManifest),
Err(other) => Err(ArtifactError::ZipGuard(other)),
}
}
#[derive(Debug, Error)]
pub enum ArtifactError {
#[error("failed to read artifact at {path}: {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("invalid artifact archive at {path}: {source}")]
Zip {
path: String,
#[source]
source: zip::result::ZipError,
},
#[error("artifact missing required file murmur.yaml")]
MissingManifest,
#[error(transparent)]
Manifest(#[from] ManifestError),
#[error(transparent)]
ZipGuard(#[from] ZipGuardError),
}
pub fn load_manifest_from_artifact(path: &Path) -> Result<Manifest, ArtifactError> {
let bytes = std::fs::read(path).map_err(|source| ArtifactError::Io {
path: path.display().to_string(),
source,
})?;
load_manifest_from_artifact_bytes(&bytes)
}
pub fn load_manifest_from_artifact_bytes(bytes: &[u8]) -> Result<Manifest, ArtifactError> {
let cursor = std::io::Cursor::new(bytes);
let mut archive = ZipArchive::new(cursor).map_err(|source| ArtifactError::Zip {
path: "<bytes>".to_string(),
source,
})?;
let max_bytes = zip_guard::max_artifact_decompressed_bytes();
let manifest_content = match zip_guard::read_zip_entry_to_string_capped(
&mut archive,
PACKED_MANIFEST_ENTRY,
max_bytes,
) {
Ok(content) => content,
Err(ZipGuardError::Zip(_)) => return Err(ArtifactError::MissingManifest),
Err(other) => return Err(ArtifactError::ZipGuard(other)),
};
Ok(Manifest::from_yaml_str(&manifest_content)?)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeclaredRuntime {
pub runtime: RuntimeType,
pub artifact_runtime: String,
}
#[must_use]
pub fn declared_runtime_from_artifact_bytes(bytes: &[u8]) -> Option<DeclaredRuntime> {
let manifest = load_manifest_from_artifact_bytes(bytes).ok()?;
Some(DeclaredRuntime {
runtime: manifest.registry_runtime(),
artifact_runtime: manifest.runtime,
})
}
#[cfg(test)]
mod tests {
use std::io::Write;
use zip::write::SimpleFileOptions;
use super::*;
fn archive_with_files(files: &[(&str, &[u8])]) -> Vec<u8> {
let mut cursor = std::io::Cursor::new(Vec::<u8>::new());
{
let mut zip = zip::ZipWriter::new(&mut cursor);
let options = SimpleFileOptions::default();
for (name, bytes) in files {
zip.start_file(*name, options).unwrap();
zip.write_all(bytes).unwrap();
}
zip.finish().unwrap();
}
cursor.into_inner()
}
#[test]
fn load_manifest_yaml_reads_small_manifest() {
let bytes = archive_with_files(&[("murmur.yaml", b"name: demo\nversion: 0.1.0\n")]);
let content = load_manifest_yaml_from_artifact_bytes(&bytes).unwrap();
assert!(content.contains("name: demo"));
}
#[test]
fn load_manifest_yaml_missing_returns_missing_manifest_error() {
let bytes = archive_with_files(&[("other.txt", b"hello")]);
let err = load_manifest_yaml_from_artifact_bytes(&bytes).unwrap_err();
assert!(matches!(err, ArtifactError::MissingManifest));
}
#[test]
fn packed_entry_is_addressed_as_murmur_yaml() {
assert_eq!(PACKED_MANIFEST_ENTRY, "murmur.yaml");
}
#[test]
fn load_manifest_reads_packed_entry() {
let bytes = archive_with_files(&[(
"murmur.yaml",
b"name: demo\nversion: 0.1.0\nruntime: wasm\n",
)]);
let manifest = load_manifest_from_artifact_bytes(&bytes).unwrap();
assert_eq!(manifest.name, "demo");
}
#[test]
fn load_manifest_yaml_rejects_entry_past_ceiling() {
let big = vec![b'a'; 1024];
let bytes = archive_with_files(&[("murmur.yaml", &big)]);
let err = load_manifest_yaml_from_artifact_bytes_capped(&bytes, 16).unwrap_err();
assert!(matches!(
err,
ArtifactError::ZipGuard(ZipGuardError::DecompressionCeilingExceeded { .. })
));
}
#[test]
fn load_manifest_yaml_default_ceiling_does_not_reject_small_manifest() {
let bytes = archive_with_files(&[("murmur.yaml", b"name: demo\nversion: 0.1.0\n")]);
let content = load_manifest_yaml_from_artifact_bytes(&bytes).unwrap();
assert!(content.contains("name: demo"));
}
}