diff --git a/crates/typst-syntax/src/package.rs b/crates/typst-syntax/src/package.rs index 138b39ca1..911af6d4c 100644 --- a/crates/typst-syntax/src/package.rs +++ b/crates/typst-syntax/src/package.rs @@ -40,7 +40,7 @@ pub struct PackageInfo { /// The path of the entrypoint into the package. pub entrypoint: EcoString, /// The minimum required compiler version for the package. - pub compiler: Option, + pub compiler: Option, } impl PackageManifest { @@ -62,7 +62,7 @@ impl PackageManifest { if let Some(required) = self.package.compiler { let current = PackageVersion::compiler(); - if current < required { + if !current.matches_ge(&required) { return Err(eco_format!( "package requires typst {required} or newer \ (current version is {current})" @@ -214,6 +214,62 @@ impl PackageVersion { patch: env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(), } } + + /// Performs an `==` match with the given version bound. Version elements + /// missing in the bound are ignored. + pub fn matches_eq(&self, bound: &VersionBound) -> bool { + self.major == bound.major + && bound.minor.map_or(true, |minor| self.minor == minor) + && bound.patch.map_or(true, |patch| self.patch == patch) + } + + /// Performs a `>` match with the given version bound. The match only + /// succeeds if some version element in the bound is actually greater than + /// that of the version. + pub fn matches_gt(&self, bound: &VersionBound) -> bool { + if self.major != bound.major { + return self.major > bound.major; + } + let Some(minor) = bound.minor else { return false }; + if self.minor != minor { + return self.minor > minor; + } + let Some(patch) = bound.patch else { return false }; + if self.patch != patch { + return self.patch > patch; + } + false + } + + /// Performs a `<` match with the given version bound. The match only + /// succeeds if some version element in the bound is actually less than that + /// of the version. + pub fn matches_lt(&self, bound: &VersionBound) -> bool { + if self.major != bound.major { + return self.major < bound.major; + } + let Some(minor) = bound.minor else { return false }; + if self.minor != minor { + return self.minor < minor; + } + let Some(patch) = bound.patch else { return false }; + if self.patch != patch { + return self.patch < patch; + } + false + } + + /// Performs a `>=` match with the given versions. The match succeeds when + /// either a `==` or `>` match does. + pub fn matches_ge(&self, bound: &VersionBound) -> bool { + self.matches_eq(bound) || self.matches_gt(bound) + } + + /// Performs a `<=` match with the given versions. The match succeeds when + /// either a `==` or `<` match does. + pub fn matches_le(&self, bound: &VersionBound) -> bool { + self.matches_eq(bound) || self.matches_lt(bound) + } } impl FromStr for PackageVersion { @@ -265,3 +321,97 @@ impl<'de> Deserialize<'de> for PackageVersion { string.parse().map_err(serde::de::Error::custom) } } + +/// A version bound for compatibility specification. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct VersionBound { + /// The bounds's major version. + pub major: u32, + /// The bounds's minor version. + pub minor: Option, + /// The bounds's patch version. Can only be present if minor is too. + pub patch: Option, +} + +impl FromStr for VersionBound { + type Err = EcoString; + + fn from_str(s: &str) -> Result { + let mut parts = s.split('.'); + let mut next = |kind| { + if let Some(part) = parts.next() { + part.parse::().map(Some).map_err(|_| { + eco_format!("`{part}` is not a valid {kind} version bound") + }) + } else { + Ok(None) + } + }; + + let major = next("major")? + .ok_or_else(|| eco_format!("version bound is missing major version"))?; + let minor = next("minor")?; + let patch = next("patch")?; + if let Some(rest) = parts.next() { + Err(eco_format!("version bound has unexpected fourth component: `{rest}`"))?; + } + + Ok(Self { major, minor, patch }) + } +} + +impl Debug for VersionBound { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl Display for VersionBound { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}", self.major)?; + if let Some(minor) = self.minor { + write!(f, ".{minor}")?; + } + if let Some(patch) = self.patch { + write!(f, ".{patch}")?; + } + Ok(()) + } +} + +impl Serialize for VersionBound { + fn serialize(&self, s: S) -> Result { + s.collect_str(self) + } +} + +impl<'de> Deserialize<'de> for VersionBound { + fn deserialize>(d: D) -> Result { + let string = EcoString::deserialize(d)?; + string.parse().map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + + #[test] + fn version_version_match() { + let v1_1_1 = PackageVersion::from_str("1.1.1").unwrap(); + + assert!(v1_1_1.matches_eq(&VersionBound::from_str("1").unwrap())); + assert!(v1_1_1.matches_eq(&VersionBound::from_str("1.1").unwrap())); + assert!(!v1_1_1.matches_eq(&VersionBound::from_str("1.2").unwrap())); + + assert!(!v1_1_1.matches_gt(&VersionBound::from_str("1").unwrap())); + assert!(v1_1_1.matches_gt(&VersionBound::from_str("1.0").unwrap())); + assert!(!v1_1_1.matches_gt(&VersionBound::from_str("1.1").unwrap())); + + assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1").unwrap())); + assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1.1").unwrap())); + assert!(v1_1_1.matches_lt(&VersionBound::from_str("1.2").unwrap())); + } +}