Skip to content

Commit 473d074

Browse files
authored
fix(core): check used extension versions against resolved extensions (#2588)
Extension resolution computes the actually used extensions in the HUGR and places them inside the HUGR, this can be used to check for version mismatch rather than the registry used for loading Reverts unnecessary registry returns from 91bff6e
1 parent 91bff6e commit 473d074

File tree

2 files changed

+98
-49
lines changed

2 files changed

+98
-49
lines changed

hugr-core/src/envelope.rs

Lines changed: 93 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ fn read_impl(
361361
header: EnvelopeHeader,
362362
registry: &ExtensionRegistry,
363363
) -> Result<Package, EnvelopeError> {
364-
let (package, combined_registry) = match header.format {
364+
let package = match header.format {
365365
#[allow(deprecated)]
366366
EnvelopeFormat::PackageJson => Ok(package_json::from_json_reader(payload, registry)?),
367367
EnvelopeFormat::Model | EnvelopeFormat::ModelWithExtensions => {
@@ -373,8 +373,7 @@ fn read_impl(
373373
}?;
374374

375375
package.modules.iter().try_for_each(|module| {
376-
check_breaking_extensions(module, &combined_registry)
377-
.map_err(|err| WithGenerator::new(err, &package.modules))
376+
check_breaking_extensions(module).map_err(|err| WithGenerator::new(err, &package.modules))
378377
})?;
379378
Ok(package)
380379
}
@@ -393,7 +392,7 @@ fn decode_model(
393392
mut stream: impl BufRead,
394393
extension_registry: &ExtensionRegistry,
395394
format: EnvelopeFormat,
396-
) -> Result<(Package, ExtensionRegistry), EnvelopeError> {
395+
) -> Result<Package, EnvelopeError> {
397396
check_model_version(format)?;
398397
let bump = Bump::default();
399398
let model_package = hugr_model::v0::binary::read_from_reader(&mut stream, &bump)?;
@@ -406,7 +405,7 @@ fn decode_model(
406405

407406
let package = import_package(&model_package, &extension_registry)?;
408407

409-
Ok((package, extension_registry))
408+
Ok(package)
410409
}
411410

412411
fn check_model_version(format: EnvelopeFormat) -> Result<(), EnvelopeError> {
@@ -430,7 +429,7 @@ fn decode_model_ast(
430429
mut stream: impl BufRead,
431430
extension_registry: &ExtensionRegistry,
432431
format: EnvelopeFormat,
433-
) -> Result<(Package, ExtensionRegistry), EnvelopeError> {
432+
) -> Result<Package, EnvelopeError> {
434433
check_model_version(format)?;
435434

436435
let mut extension_registry = extension_registry.clone();
@@ -458,7 +457,7 @@ fn decode_model_ast(
458457

459458
let package = import_package(&model_package, &extension_registry)?;
460459

461-
Ok((package, extension_registry))
460+
Ok(package)
462461
}
463462

464463
/// Internal implementation of [`write_envelope`] to call with/without the zstd compression wrapper.
@@ -558,12 +557,20 @@ pub enum ExtensionBreakingError {
558557
#[error("Failed to deserialize used extensions metadata")]
559558
Deserialization(#[from] serde_json::Error),
560559
}
560+
/// If HUGR metadata contains a list of used extensions, under the key [`USED_EXTENSIONS_KEY`],
561+
/// and extension is resolved in the HUGR, check that the
562+
/// version of the extension in the metadata matches the resolved version.
563+
/// Version compatibility is defined by [`compatible_versions`].
564+
fn check_breaking_extensions(hugr: impl crate::HugrView) -> Result<(), ExtensionBreakingError> {
565+
check_breaking_extensions_against_registry(&hugr, hugr.extensions())
566+
}
567+
561568
/// If HUGR metadata contains a list of used extensions, under the key [`USED_EXTENSIONS_KEY`],
562569
/// and extension is registered in the given registry, check that the
563-
/// version of the extension in the metadata matches the registered version (up to
564-
/// MAJOR.MINOR).
565-
fn check_breaking_extensions(
566-
hugr: impl crate::HugrView,
570+
/// version of the extension in the metadata matches the registered version.
571+
/// Version compatibility is defined by [`compatible_versions`].
572+
fn check_breaking_extensions_against_registry(
573+
hugr: &impl crate::HugrView,
567574
registry: &ExtensionRegistry,
568575
) -> Result<(), ExtensionBreakingError> {
569576
let Some(exts) = hugr.get_metadata(hugr.module_root(), USED_EXTENSIONS_KEY) else {
@@ -594,17 +601,16 @@ fn check_breaking_extensions(
594601
/// Check if two versions are compatible according to:
595602
/// - Major version must match.
596603
/// - If major version is 0, minor version must match.
597-
fn compatible_versions(v1: &Version, v2: &Version) -> bool {
598-
if v1.major != v2.major {
599-
return false; // Major version mismatch
604+
/// - The registered version must be greater than or equal to the used version.
605+
fn compatible_versions(registered: &Version, used: &Version) -> bool {
606+
if used.major != registered.major {
607+
return false;
600608
}
601-
602-
if v1.major == 0 {
603-
// For major version 0, we only allow minor version matches
604-
return v1.minor == v2.minor;
609+
if used.major == 0 && used.minor != registered.minor {
610+
return false;
605611
}
606612

607-
true
613+
registered >= used
608614
}
609615

610616
#[cfg(test)]
@@ -740,6 +746,11 @@ pub(crate) mod test {
740746
assert_eq!(package, new_package);
741747
}
742748

749+
/// Test helper to call `check_breaking_extensions_against_registry`
750+
fn check(hugr: &Hugr, registry: &ExtensionRegistry) -> Result<(), ExtensionBreakingError> {
751+
check_breaking_extensions_against_registry(hugr, registry)
752+
}
753+
743754
#[rstest]
744755
#[case::simple(simple_package())]
745756
fn test_check_breaking_extensions(#[case] mut package: Package) {
@@ -756,73 +767,114 @@ pub(crate) mod test {
756767
let mut hugr = package.modules.remove(0);
757768

758769
// No metadata - should pass
759-
assert_matches!(check_breaking_extensions(&hugr, &registry), Ok(()));
770+
assert_matches!(check(&hugr, &registry), Ok(()));
760771

761772
// Matching version for v0 - should pass
762773
let used_exts = json!([{ "name": "test-v0", "version": "0.2.3" }]);
763774
hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
764-
assert_matches!(check_breaking_extensions(&hugr, &registry), Ok(()));
775+
assert_matches!(check(&hugr, &registry), Ok(()));
765776

766-
// Matching major/minor but different patch for v0 - should pass
767-
let used_exts = json!([{ "name": "test-v0", "version": "0.2.4" }]);
777+
// Matching major/minor but lower patch for v0 - should pass
778+
let used_exts = json!([{ "name": "test-v0", "version": "0.2.2" }]);
768779
hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
769-
assert_matches!(check_breaking_extensions(&hugr, &registry), Ok(()));
780+
assert_matches!(check(&hugr, &registry), Ok(()));
770781

771782
//Different minor version for v0 - should fail
772783
let used_exts = json!([{ "name": "test-v0", "version": "0.3.3" }]);
773784
hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
774785
assert_matches!(
775-
check_breaking_extensions(&hugr, &registry),
786+
check(&hugr, &registry),
776787
Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
777788
name,
778789
registered,
779790
used
780791
})) if name == "test-v0" && registered == Version::new(0, 2, 3) && used == Version::new(0, 3, 3)
781792
);
782793

794+
assert!(
795+
check_breaking_extensions(&hugr).is_ok(),
796+
"Extension is not actually used in the HUGR, should be ignored by full check"
797+
);
798+
783799
// Different major version for v0 - should fail
784800
let used_exts = json!([{ "name": "test-v0", "version": "1.2.3" }]);
785801
hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
786802
assert_matches!(
787-
check_breaking_extensions(&hugr, &registry),
803+
check(&hugr, &registry),
788804
Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
789805
name,
790806
registered,
791807
used
792808
})) if name == "test-v0" && registered == Version::new(0, 2, 3) && used == Version::new(1, 2, 3)
793809
);
794810

811+
// Higher patch version for v0 - should fail
812+
let used_exts = json!([{ "name": "test-v0", "version": "0.2.4" }]);
813+
hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
814+
assert_matches!(
815+
check(&hugr, &registry),
816+
Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
817+
name,
818+
registered,
819+
used
820+
})) if name == "test-v0" && registered == Version::new(0, 2, 3) && used == Version::new(0, 2, 4)
821+
);
822+
795823
// Matching version for v1 - should pass
796824
let used_exts = json!([{ "name": "test-v1", "version": "1.2.3" }]);
797825
hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
798-
assert_matches!(check_breaking_extensions(&hugr, &registry), Ok(()));
826+
assert_matches!(check(&hugr, &registry), Ok(()));
799827

800-
// Different minor version for v1 - should pass
801-
let used_exts = json!([{ "name": "test-v1", "version": "1.3.0" }]);
828+
// Lower minor version for v1 - should pass
829+
let used_exts = json!([{ "name": "test-v1", "version": "1.1.0" }]);
802830
hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
803-
assert_matches!(check_breaking_extensions(&hugr, &registry), Ok(()));
831+
assert_matches!(check(&hugr, &registry), Ok(()));
804832

805-
// Different patch for v1 - should pass
806-
let used_exts = json!([{ "name": "test-v1", "version": "1.2.4" }]);
833+
// Lower patch for v1 - should pass
834+
let used_exts = json!([{ "name": "test-v1", "version": "1.2.2" }]);
807835
hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
808-
assert_matches!(check_breaking_extensions(&hugr, &registry), Ok(()));
836+
assert_matches!(check(&hugr, &registry), Ok(()));
809837

810838
// Different major version for v1 - should fail
811839
let used_exts = json!([{ "name": "test-v1", "version": "2.2.3" }]);
812840
hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
813841
assert_matches!(
814-
check_breaking_extensions(&hugr, &registry),
842+
check(&hugr, &registry),
815843
Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
816844
name,
817845
registered,
818846
used
819847
})) if name == "test-v1" && registered == Version::new(1, 2, 3) && used == Version::new(2, 2, 3)
820848
);
821849

850+
// Higher minor version for v1 - should fail
851+
let used_exts = json!([{ "name": "test-v1", "version": "1.3.0" }]);
852+
hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
853+
assert_matches!(
854+
check(&hugr, &registry),
855+
Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
856+
name,
857+
registered,
858+
used
859+
})) if name == "test-v1" && registered == Version::new(1, 2, 3) && used == Version::new(1, 3, 0)
860+
);
861+
862+
// Higher patch version for v1 - should fail
863+
let used_exts = json!([{ "name": "test-v1", "version": "1.2.4" }]);
864+
hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
865+
assert_matches!(
866+
check(&hugr, &registry),
867+
Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
868+
name,
869+
registered,
870+
used
871+
})) if name == "test-v1" && registered == Version::new(1, 2, 3) && used == Version::new(1, 2, 4)
872+
);
873+
822874
// Non-registered extension - should pass
823875
let used_exts = json!([{ "name": "unknown", "version": "1.0.0" }]);
824876
hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
825-
assert_matches!(check_breaking_extensions(&hugr, &registry), Ok(()));
877+
assert_matches!(check(&hugr, &registry), Ok(()));
826878

827879
// Multiple extensions - one mismatch should fail
828880
let used_exts = json!([
@@ -831,7 +883,7 @@ pub(crate) mod test {
831883
]);
832884
hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
833885
assert_matches!(
834-
check_breaking_extensions(&hugr, &registry),
886+
check(&hugr, &registry),
835887
Err(ExtensionBreakingError::ExtensionVersionMismatch(ExtensionVersionMismatch {
836888
name,
837889
registered,
@@ -846,17 +898,17 @@ pub(crate) mod test {
846898
json!("not an array"),
847899
);
848900
assert_matches!(
849-
check_breaking_extensions(&hugr, &registry),
901+
check(&hugr, &registry),
850902
Err(ExtensionBreakingError::Deserialization(_))
851903
);
852904

853905
// Multiple extensions with all compatible versions - should pass
854906
let used_exts = json!([
855-
{ "name": "test-v0", "version": "0.2.5" },
856-
{ "name": "test-v1", "version": "1.9.9" }
907+
{ "name": "test-v0", "version": "0.2.2" },
908+
{ "name": "test-v1", "version": "1.1.9" }
857909
]);
858910
hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
859-
assert_matches!(check_breaking_extensions(&hugr, &registry), Ok(()));
911+
assert_matches!(check(&hugr, &registry), Ok(()));
860912
}
861913

862914
#[test]
@@ -875,7 +927,7 @@ pub(crate) mod test {
875927
hugr.set_metadata(hugr.module_root(), USED_EXTENSIONS_KEY, used_exts);
876928

877929
// Create the error and wrap it with WithGenerator
878-
let err = check_breaking_extensions(&hugr, &registry).unwrap_err();
930+
let err = check_breaking_extensions_against_registry(&hugr, &registry).unwrap_err();
879931
let with_gen = WithGenerator::new(err, &[&hugr]);
880932

881933
let err_msg = with_gen.to_string();

hugr-core/src/envelope/package_json.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::{Extension, Hugr};
1515
pub(super) fn from_json_reader(
1616
reader: impl io::Read,
1717
extension_registry: &ExtensionRegistry,
18-
) -> Result<(Package, ExtensionRegistry), PackageEncodingError> {
18+
) -> Result<Package, PackageEncodingError> {
1919
let val: serde_json::Value = serde_json::from_reader(reader)?;
2020

2121
let PackageDeser {
@@ -38,13 +38,10 @@ pub(super) fn from_json_reader(
3838
.try_for_each(|module| module.resolve_extension_defs(&combined_registry))
3939
.map_err(|err| WithGenerator::new(err, &modules))?;
4040

41-
Ok((
42-
Package {
43-
modules,
44-
extensions: pkg_extensions,
45-
},
46-
combined_registry,
47-
))
41+
Ok(Package {
42+
modules,
43+
extensions: pkg_extensions,
44+
})
4845
}
4946

5047
/// Write the Package in json format into an io writer.

0 commit comments

Comments
 (0)