pub mod description;
mod header;
mod package_json;
mod reader;
mod writer;
use reader::EnvelopeReader;
pub use reader::PayloadError;
pub use writer::WriteError;
pub mod serde_with;
pub use header::{EnvelopeConfig, EnvelopeFormat, EnvelopeHeader, MAGIC_NUMBERS, ZstdConfig};
pub use package_json::PackageEncodingError;
pub use crate::metadata::{HugrGenerator, HugrUsedExtensions};
use crate::Hugr;
use crate::envelope::description::PackageDesc;
use crate::envelope::header::HeaderError;
use crate::{
extension::{ExtensionRegistry, Version},
package::Package,
};
use std::io::BufRead;
use std::io::Write;
use thiserror::Error;
#[allow(unused_imports)]
use itertools::Itertools as _;
pub fn read_envelope(
reader: impl BufRead,
registry: &ExtensionRegistry,
) -> Result<(PackageDesc, Package), ReadError> {
let reader = EnvelopeReader::new(reader, registry).map_err(Box::new)?;
let (desc, res) = reader.read();
match res {
Ok(pkg) => Ok((desc, pkg)),
Err(e) => Err(ReadError::Payload {
source: Box::new(e),
partial_description: desc,
}),
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ReadError {
#[error(transparent)]
EnvelopeHeader(#[from] Box<HeaderError>),
#[error("Error reading package payload in envelope.")]
Payload {
source: Box<PayloadError>,
partial_description: PackageDesc,
},
#[error("Expected an envelope containing a single hugr, but it contained {}.", if *count == 0 {
"none".to_string()
} else {
count.to_string()
})]
ExpectedSingleHugr {
count: usize,
},
}
pub fn write_envelope(
writer: impl Write,
package: &Package,
config: EnvelopeConfig,
) -> Result<(), WriteError> {
write_envelope_impl(writer, &package.modules, &package.extensions, config)
}
pub(crate) fn write_envelope_impl<'h>(
writer: impl Write,
hugrs: impl IntoIterator<Item = &'h Hugr>,
extensions: &ExtensionRegistry,
config: EnvelopeConfig,
) -> Result<(), WriteError> {
writer::write_envelope(writer, hugrs, extensions, config)
}
#[derive(Debug, Error)]
#[error(
"The envelope format {format} is not supported.{}",
match feature {
Some(f) => format!(" This requires the '{f}' feature for `hugr`."),
None => String::new()
},
)]
pub(crate) struct FormatUnsupportedError {
format: EnvelopeFormat,
feature: Option<&'static str>,
}
fn check_model_version(format: EnvelopeFormat) -> Result<(), FormatUnsupportedError> {
if format.model_version() != Some(0) {
return Err(FormatUnsupportedError {
format,
feature: None,
});
}
Ok(())
}
#[derive(Debug, Error)]
#[error(
"Extension '{name}' version mismatch: registered version is {registered}, but used version is {used}"
)]
pub struct ExtensionVersionMismatch {
pub name: String,
pub registered: Version,
pub used: Version,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ExtensionBreakingError {
#[error("{0}")]
ExtensionVersionMismatch(ExtensionVersionMismatch),
#[error("Failed to deserialize used extensions metadata")]
Deserialization(#[from] serde_json::Error),
}
fn check_breaking_extensions<'e>(
registry: &ExtensionRegistry,
used_exts: impl IntoIterator<Item = &'e description::ExtensionDesc>,
) -> Result<(), ExtensionBreakingError> {
for ext in used_exts {
let Some(registered) = registry.get(ext.name.as_str()) else {
continue; };
if !compatible_versions(registered.version(), &ext.version) {
return Err(ExtensionBreakingError::ExtensionVersionMismatch(
ExtensionVersionMismatch {
name: ext.name.clone(),
registered: registered.version().clone(),
used: ext.version.clone(),
},
));
}
}
Ok(())
}
fn compatible_versions(registered: &Version, used: &Version) -> bool {
if used.major != registered.major {
return false;
}
if used.major == 0 && used.minor != registered.minor {
return false;
}
registered >= used
}
#[cfg(test)]
pub(crate) mod test {
use super::*;
use cool_asserts::assert_matches;
use rstest::rstest;
use std::borrow::Cow;
use std::io::BufReader;
use crate::HugrView;
use crate::builder::test::{multi_module_package, simple_package};
use crate::envelope::writer::WriteErrorInner;
use crate::extension::{Extension, ExtensionRegistry, Version};
use crate::extension::{ExtensionId, PRELUDE_REGISTRY};
use crate::hugr::HugrMut;
use crate::hugr::test::check_hugr_equality;
use crate::std_extensions::STD_REG;
use std::sync::Arc;
fn join_extensions<'a>(
extensions: &'a ExtensionRegistry,
other: &ExtensionRegistry,
) -> Cow<'a, ExtensionRegistry> {
if other.iter().all(|e| extensions.contains(e.name())) {
Cow::Borrowed(extensions)
} else {
let mut extensions = extensions.clone();
extensions.extend(other);
Cow::Owned(extensions)
}
}
pub(crate) fn check_hugr_roundtrip(hugr: &Hugr, config: EnvelopeConfig) -> Hugr {
let mut buffer = Vec::new();
hugr.store(&mut buffer, config).unwrap();
let extensions = join_extensions(&STD_REG, hugr.extensions());
let reader = BufReader::new(buffer.as_slice());
let extracted = Hugr::load(reader, Some(&extensions)).unwrap();
check_hugr_equality(&extracted, hugr);
extracted
}
#[rstest]
fn errors() {
let package = simple_package();
assert_matches!(
package.store_str(EnvelopeConfig::binary()),
Err(WriteError(WriteErrorInner::NonASCIIFormat { .. }))
);
}
#[rstest]
#[case::empty(Package::default())]
#[case::simple(simple_package())]
#[case::multi(multi_module_package())]
fn text_roundtrip(#[case] package: Package) {
let envelope = package.store_str(EnvelopeConfig::text()).unwrap();
let new_package = Package::load_str(&envelope, None).unwrap();
assert_eq!(package, new_package);
}
#[rstest]
#[case::empty(Package::default())]
#[case::simple(simple_package())]
#[case::multi(multi_module_package())]
#[cfg_attr(all(miri, feature = "zstd"), ignore)] fn compressed_roundtrip(#[case] package: Package) {
let mut buffer = Vec::new();
let config = EnvelopeConfig {
#[expect(deprecated)]
format: EnvelopeFormat::PackageJson,
zstd: Some(ZstdConfig::default()),
};
let res = package.store(&mut buffer, config);
match cfg!(feature = "zstd") {
true => res.unwrap(),
false => {
assert_matches!(res, Err(WriteError(WriteErrorInner::ZstdUnsupported)));
return;
}
}
let (desc, new_package) =
read_envelope(BufReader::new(buffer.as_slice()), &PRELUDE_REGISTRY).unwrap();
let decoded_config = desc.header.config();
assert_eq!(config.format, decoded_config.format);
assert_eq!(config.zstd.is_some(), decoded_config.zstd.is_some());
assert_eq!(package, new_package);
}
#[rstest]
#[case::empty_model(Package::default(), EnvelopeFormat::Model)]
#[case::empty_model_exts(Package::default(), EnvelopeFormat::ModelWithExtensions)]
#[case::empty_text(Package::default(), EnvelopeFormat::SExpression)]
#[case::empty_text_exts(Package::default(), EnvelopeFormat::SExpressionWithExtensions)]
#[case::simple_bin(simple_package(), EnvelopeFormat::Model)]
#[case::simple_bin_exts(simple_package(), EnvelopeFormat::ModelWithExtensions)]
#[case::simple_text(simple_package(), EnvelopeFormat::SExpression)]
#[case::simple_text_exts(simple_package(), EnvelopeFormat::SExpressionWithExtensions)]
#[case::multi_bin(multi_module_package(), EnvelopeFormat::Model)]
#[case::multi_bin_exts(multi_module_package(), EnvelopeFormat::ModelWithExtensions)]
#[case::multi_text(multi_module_package(), EnvelopeFormat::SExpression)]
#[case::multi_text_exts(multi_module_package(), EnvelopeFormat::SExpressionWithExtensions)]
fn model_roundtrip(#[case] package: Package, #[case] format: EnvelopeFormat) {
let mut buffer = Vec::new();
let config = EnvelopeConfig { format, zstd: None };
package.store(&mut buffer, config).unwrap();
let (desc, new_package) =
read_envelope(BufReader::new(buffer.as_slice()), &PRELUDE_REGISTRY).unwrap();
let decoded_config = desc.header.config();
assert_eq!(config.format, decoded_config.format);
assert_eq!(config.zstd.is_some(), decoded_config.zstd.is_some());
assert_eq!(package, new_package);
}
fn check(hugr: &Hugr, registry: &ExtensionRegistry) -> Result<(), ExtensionBreakingError> {
let mut desc = description::ModuleDesc::default();
desc.load_used_extensions_generator(&hugr)?;
let Some(used_exts) = desc.used_extensions_generator else {
return Ok(());
};
check_breaking_extensions(registry, &used_exts)
}
#[rstest]
#[case::simple(simple_package())]
fn test_check_breaking_extensions(#[case] mut package: Package) {
use crate::envelope::description::ExtensionDesc;
let test_ext_v0 =
Extension::new(ExtensionId::new_unchecked("test-v0"), Version::new(0, 2, 3));
let test_ext_v1 =
Extension::new(ExtensionId::new_unchecked("test-v1"), Version::new(1, 2, 3));
let registry =
ExtensionRegistry::new([Arc::new(test_ext_v0.clone()), Arc::new(test_ext_v1.clone())]);
let mut hugr = package.modules.remove(0);
assert_matches!(check(&hugr, ®istry), Ok(()));
let used_exts = vec![ExtensionDesc::new("test-v0", Version::new(0, 2, 3))];
hugr.set_metadata::<HugrUsedExtensions>(hugr.module_root(), used_exts);
assert_matches!(check(&hugr, ®istry), Ok(()));
let used_exts = vec![ExtensionDesc::new("test-v0", Version::new(0, 2, 2))];
hugr.set_metadata::<HugrUsedExtensions>(hugr.module_root(), used_exts);
assert_matches!(check(&hugr, ®istry), Ok(()));
let used_exts = vec![ExtensionDesc::new("test-v0", Version::new(0, 3, 3))];
hugr.set_metadata::<HugrUsedExtensions>(hugr.module_root(), used_exts);
assert_matches!(
check(&hugr, ®istry),
Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
name,
registered,
used
})) if name == "test-v0" && registered == Version::new(0, 2, 3) && used == Version::new(0, 3, 3)
);
assert!(
check(&hugr, hugr.extensions()).is_ok(),
"Extension is not actually used in the HUGR, should be ignored by full check"
);
let used_exts = vec![ExtensionDesc::new("test-v0", Version::new(1, 2, 3))];
hugr.set_metadata::<HugrUsedExtensions>(hugr.module_root(), used_exts);
assert_matches!(
check(&hugr, ®istry),
Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
name,
registered,
used
})) if name == "test-v0" && registered == Version::new(0, 2, 3) && used == Version::new(1, 2, 3)
);
let used_exts = vec![ExtensionDesc::new("test-v0", Version::new(0, 2, 4))];
hugr.set_metadata::<HugrUsedExtensions>(hugr.module_root(), used_exts);
assert_matches!(
check(&hugr, ®istry),
Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
name,
registered,
used
})) if name == "test-v0" && registered == Version::new(0, 2, 3) && used == Version::new(0, 2, 4)
);
let used_exts = vec![ExtensionDesc::new("test-v1", Version::new(1, 2, 3))];
hugr.set_metadata::<HugrUsedExtensions>(hugr.module_root(), used_exts);
assert_matches!(check(&hugr, ®istry), Ok(()));
let used_exts = vec![ExtensionDesc::new("test-v1", Version::new(1, 1, 0))];
hugr.set_metadata::<HugrUsedExtensions>(hugr.module_root(), used_exts);
assert_matches!(check(&hugr, ®istry), Ok(()));
let used_exts = vec![ExtensionDesc::new("test-v1", Version::new(1, 2, 2))];
hugr.set_metadata::<HugrUsedExtensions>(hugr.module_root(), used_exts);
assert_matches!(check(&hugr, ®istry), Ok(()));
let used_exts = vec![ExtensionDesc::new("test-v1", Version::new(2, 2, 3))];
hugr.set_metadata::<HugrUsedExtensions>(hugr.module_root(), used_exts);
assert_matches!(
check(&hugr, ®istry),
Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
name,
registered,
used
})) if name == "test-v1" && registered == Version::new(1, 2, 3) && used == Version::new(2, 2, 3)
);
let used_exts = vec![ExtensionDesc::new("test-v1", Version::new(1, 3, 0))];
hugr.set_metadata::<HugrUsedExtensions>(hugr.module_root(), used_exts);
assert_matches!(
check(&hugr, ®istry),
Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
name,
registered,
used
})) if name == "test-v1" && registered == Version::new(1, 2, 3) && used == Version::new(1, 3, 0)
);
let used_exts = vec![ExtensionDesc::new("test-v1", Version::new(1, 2, 4))];
hugr.set_metadata::<HugrUsedExtensions>(hugr.module_root(), used_exts);
assert_matches!(
check(&hugr, ®istry),
Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
name,
registered,
used
})) if name == "test-v1" && registered == Version::new(1, 2, 3) && used == Version::new(1, 2, 4)
);
let used_exts = vec![ExtensionDesc::new("unknown", Version::new(1, 0, 0))];
hugr.set_metadata::<HugrUsedExtensions>(hugr.module_root(), used_exts);
assert_matches!(check(&hugr, ®istry), Ok(()));
let used_exts = vec![
ExtensionDesc::new("unknown", Version::new(1, 0, 0)),
ExtensionDesc::new("test-v1", Version::new(2, 0, 0)),
];
hugr.set_metadata::<HugrUsedExtensions>(hugr.module_root(), used_exts);
assert_matches!(
check(&hugr, ®istry),
Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
name,
registered,
used
})) if name == "test-v1" && registered == Version::new(1, 2, 3) && used == Version::new(2, 0, 0)
);
let used_exts = vec![
ExtensionDesc::new("test-v0", Version::new(0, 2, 2)),
ExtensionDesc::new("test-v1", Version::new(1, 1, 9)),
];
hugr.set_metadata::<HugrUsedExtensions>(hugr.module_root(), used_exts);
assert_matches!(check(&hugr, ®istry), Ok(()));
let used_exts = vec![
ExtensionDesc::new("test-v0", Version::new(0, 2, 2)),
ExtensionDesc::new_unversioned("test-v1"),
];
hugr.set_metadata::<HugrUsedExtensions>(hugr.module_root(), used_exts);
assert_matches!(check(&hugr, ®istry), Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
name,
registered,
used
})) if name == "test-v1" && registered == Version::new(1, 2, 3) && used == Version::new(0, 0, 0)
);
}
}