From b2c8f5ef68c1ef815b5b40a809e260353797949b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 26 Aug 2025 10:22:50 -0400 Subject: [PATCH] Add `rkyv` implementation for Core Metadata (#15532) ## Summary Enables us to store Core Metadata in zero-copy format. --- Cargo.lock | 1 + crates/uv-normalize/src/extra_name.rs | 15 +++++++- crates/uv-normalize/src/group_name.rs | 14 +++++++- crates/uv-pep508/Cargo.toml | 2 ++ crates/uv-pep508/src/lib.rs | 35 +++++++++++++++++++ crates/uv-pypi-types/Cargo.toml | 2 +- .../src/metadata/metadata_resolver.rs | 5 ++- crates/uv-pypi-types/src/simple_json.rs | 24 +++++++++++++ 8 files changed, 94 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 89ecf256c..d311fba8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5915,6 +5915,7 @@ dependencies = [ "insta", "itertools 0.14.0", "regex", + "rkyv", "rustc-hash", "schemars", "serde", diff --git a/crates/uv-normalize/src/extra_name.rs b/crates/uv-normalize/src/extra_name.rs index 818aa8051..7b9000ac6 100644 --- a/crates/uv-normalize/src/extra_name.rs +++ b/crates/uv-normalize/src/extra_name.rs @@ -97,8 +97,21 @@ impl Default for DefaultExtras { /// See: /// - /// - -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + rkyv::Archive, + rkyv::Deserialize, + rkyv::Serialize, +)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[rkyv(derive(Debug))] pub struct ExtraName(SmallString); impl ExtraName { diff --git a/crates/uv-normalize/src/group_name.rs b/crates/uv-normalize/src/group_name.rs index ce4c78bf8..124079c08 100644 --- a/crates/uv-normalize/src/group_name.rs +++ b/crates/uv-normalize/src/group_name.rs @@ -17,8 +17,20 @@ use crate::{ /// See: /// - /// - -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + rkyv::Archive, + rkyv::Deserialize, + rkyv::Serialize, +)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[rkyv(derive(Debug))] pub struct GroupName(SmallString); impl GroupName { diff --git a/crates/uv-pep508/Cargo.toml b/crates/uv-pep508/Cargo.toml index c4830043b..aaa9698b4 100644 --- a/crates/uv-pep508/Cargo.toml +++ b/crates/uv-pep508/Cargo.toml @@ -30,6 +30,7 @@ boxcar = { workspace = true } indexmap = { workspace = true } itertools = { workspace = true } regex = { workspace = true } +rkyv = { workspace = true, optional = true } rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive", "rc"] } @@ -56,5 +57,6 @@ schemars = ["dep:schemars"] # be supported. non-pep508-extensions = [] default = [] +rkyv = ["dep:rkyv"] # Match the API of the published crate, for compatibility. serde = [] diff --git a/crates/uv-pep508/src/lib.rs b/crates/uv-pep508/src/lib.rs index 85da84938..30c7b9362 100644 --- a/crates/uv-pep508/src/lib.rs +++ b/crates/uv-pep508/src/lib.rs @@ -1045,6 +1045,41 @@ fn parse_pep508_requirement( }) } +#[cfg(feature = "rkyv")] +/// An [`rkyv`] implementation for [`Requirement`]. +impl rkyv::Archive for Requirement { + type Archived = rkyv::string::ArchivedString; + type Resolver = rkyv::string::StringResolver; + + #[inline] + fn resolve(&self, resolver: Self::Resolver, out: rkyv::Place) { + let as_str = self.to_string(); + rkyv::string::ArchivedString::resolve_from_str(&as_str, resolver, out); + } +} + +#[cfg(feature = "rkyv")] +impl rkyv::Serialize for Requirement +where + S: rkyv::rancor::Fallible + rkyv::ser::Allocator + rkyv::ser::Writer + ?Sized, + S::Error: rkyv::rancor::Source, +{ + fn serialize(&self, serializer: &mut S) -> Result { + let as_str = self.to_string(); + rkyv::string::ArchivedString::serialize_from_str(&as_str, serializer) + } +} + +#[cfg(feature = "rkyv")] +impl + rkyv::Deserialize, D> for rkyv::string::ArchivedString +{ + fn deserialize(&self, _deserializer: &mut D) -> Result, D::Error> { + // SAFETY: We only serialize valid requirements. + Ok(Requirement::::from_str(self.as_str()).unwrap()) + } +} + #[cfg(test)] mod tests { //! Half of these tests are copied from diff --git a/crates/uv-pypi-types/Cargo.toml b/crates/uv-pypi-types/Cargo.toml index e5ceef631..e75a6c057 100644 --- a/crates/uv-pypi-types/Cargo.toml +++ b/crates/uv-pypi-types/Cargo.toml @@ -21,7 +21,7 @@ uv-distribution-filename = { workspace = true } uv-git-types = { workspace = true } uv-normalize = { workspace = true } uv-pep440 = { workspace = true } -uv-pep508 = { workspace = true } +uv-pep508 = { workspace = true, features = ["rkyv"] } uv-redacted = { workspace = true } uv-small-str = { workspace = true } diff --git a/crates/uv-pypi-types/src/metadata/metadata_resolver.rs b/crates/uv-pypi-types/src/metadata/metadata_resolver.rs index 8f90b3614..506b7651d 100644 --- a/crates/uv-pypi-types/src/metadata/metadata_resolver.rs +++ b/crates/uv-pypi-types/src/metadata/metadata_resolver.rs @@ -19,8 +19,11 @@ use crate::{LenientVersionSpecifiers, MetadataError, VerbatimParsedUrl, metadata /// fields that are relevant to dependency resolution. /// /// Core Metadata 2.3 is specified in . -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive( + Serialize, Deserialize, Debug, Clone, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, +)] #[serde(rename_all = "kebab-case")] +#[rkyv(derive(Debug))] pub struct ResolutionMetadata { // Mandatory fields pub name: PackageName, diff --git a/crates/uv-pypi-types/src/simple_json.rs b/crates/uv-pypi-types/src/simple_json.rs index f92516f81..b26837cff 100644 --- a/crates/uv-pypi-types/src/simple_json.rs +++ b/crates/uv-pypi-types/src/simple_json.rs @@ -141,6 +141,18 @@ impl<'de> Deserialize<'de> for CoreMetadata { } } +impl Serialize for CoreMetadata { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Bool(is_available) => serializer.serialize_bool(*is_available), + Self::Hashes(hashes) => hashes.serialize(serializer), + } + } +} + impl CoreMetadata { pub fn is_available(&self) -> bool { match self { @@ -169,6 +181,18 @@ impl<'de> Deserialize<'de> for Yanked { } } +impl Serialize for Yanked { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Bool(is_yanked) => serializer.serialize_bool(*is_yanked), + Self::Reason(reason) => serializer.serialize_str(reason.as_ref()), + } + } +} + impl Yanked { pub fn is_yanked(&self) -> bool { match self {