diff --git a/Cargo.lock b/Cargo.lock index 5bdaed133..4e07002b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,12 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[package]] +name = "arcstr" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d" + [[package]] name = "arrayref" version = "0.3.9" @@ -5227,6 +5233,7 @@ dependencies = [ name = "uv-normalize" version = "0.0.1" dependencies = [ + "arcstr", "rkyv", "schemars", "serde", diff --git a/Cargo.toml b/Cargo.toml index 6bba2c93e..ae7315f42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ uv-workspace = { path = "crates/uv-workspace" } anstream = { version = "0.6.15" } anyhow = { version = "1.0.89" } +arcstr = { version = "1.2.0" } async-channel = { version = "2.3.1" } async-compression = { version = "0.4.12", features = ["bzip2", "gzip", "xz", "zstd"] } async-trait = { version = "0.1.82" } diff --git a/crates/uv-normalize/Cargo.toml b/crates/uv-normalize/Cargo.toml index 8f4db6a15..8d9c1c23e 100644 --- a/crates/uv-normalize/Cargo.toml +++ b/crates/uv-normalize/Cargo.toml @@ -11,6 +11,7 @@ doctest = false workspace = true [dependencies] +arcstr = { workspace = true } rkyv = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/uv-normalize/src/extra_name.rs b/crates/uv-normalize/src/extra_name.rs index f23431178..bc3c5f737 100644 --- a/crates/uv-normalize/src/extra_name.rs +++ b/crates/uv-normalize/src/extra_name.rs @@ -4,6 +4,7 @@ use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize}; +use crate::small_string::SmallString; use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNameError}; /// The normalized name of an extra dependency. @@ -14,9 +15,9 @@ use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNam /// See: /// - /// - -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ExtraName(String); +pub struct ExtraName(SmallString); impl ExtraName { /// Create a validated, normalized extra name. diff --git a/crates/uv-normalize/src/group_name.rs b/crates/uv-normalize/src/group_name.rs index 72aa898ec..a3ecb74c8 100644 --- a/crates/uv-normalize/src/group_name.rs +++ b/crates/uv-normalize/src/group_name.rs @@ -5,6 +5,7 @@ use std::sync::LazyLock; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use crate::small_string::SmallString; use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNameError}; /// The normalized name of a dependency group. @@ -12,9 +13,9 @@ use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNam /// See: /// - /// - -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct GroupName(String); +pub struct GroupName(SmallString); impl GroupName { /// Create a validated, normalized group name. diff --git a/crates/uv-normalize/src/lib.rs b/crates/uv-normalize/src/lib.rs index 583b17bcc..17beee6b9 100644 --- a/crates/uv-normalize/src/lib.rs +++ b/crates/uv-normalize/src/lib.rs @@ -5,26 +5,37 @@ pub use dist_info_name::DistInfoName; pub use extra_name::ExtraName; pub use group_name::{GroupName, DEV_DEPENDENCIES}; pub use package_name::PackageName; +use small_string::SmallString; mod dist_info_name; mod extra_name; mod group_name; mod package_name; +mod small_string; /// Validate and normalize an owned package or extra name. -pub(crate) fn validate_and_normalize_owned(name: String) -> Result { +pub(crate) fn validate_and_normalize_owned(name: String) -> Result { if is_normalized(&name)? { - Ok(name) + Ok(SmallString::from(name)) } else { - validate_and_normalize_ref(name) + Ok(SmallString::from(normalize(&name)?)) } } /// Validate and normalize an unowned package or extra name. pub(crate) fn validate_and_normalize_ref( name: impl AsRef, -) -> Result { +) -> Result { let name = name.as_ref(); + if is_normalized(name)? { + Ok(SmallString::from(name)) + } else { + Ok(SmallString::from(normalize(name)?)) + } +} + +/// Normalize an unowned package or extra name. +fn normalize(name: &str) -> Result { let mut normalized = String::with_capacity(name.len()); let mut last = None; @@ -136,9 +147,14 @@ mod tests { "FrIeNdLy-._.-bArD", ]; for input in inputs { - assert_eq!(validate_and_normalize_ref(input).unwrap(), "friendly-bard"); assert_eq!( - validate_and_normalize_owned(input.to_string()).unwrap(), + validate_and_normalize_ref(input).unwrap().as_ref(), + "friendly-bard" + ); + assert_eq!( + validate_and_normalize_owned(input.to_string()) + .unwrap() + .as_ref(), "friendly-bard" ); } @@ -169,9 +185,11 @@ mod tests { // Unchanged let unchanged = ["friendly-bard", "1okay", "okay2"]; for input in unchanged { - assert_eq!(validate_and_normalize_ref(input).unwrap(), input); + assert_eq!(validate_and_normalize_ref(input).unwrap().as_ref(), input); assert_eq!( - validate_and_normalize_owned(input.to_string()).unwrap(), + validate_and_normalize_owned(input.to_string()) + .unwrap() + .as_ref(), input ); assert!(is_normalized(input).unwrap()); diff --git a/crates/uv-normalize/src/package_name.rs b/crates/uv-normalize/src/package_name.rs index d3067f17a..742867e7f 100644 --- a/crates/uv-normalize/src/package_name.rs +++ b/crates/uv-normalize/src/package_name.rs @@ -1,8 +1,10 @@ use std::borrow::Cow; +use std::cmp::PartialEq; use std::str::FromStr; use serde::{Deserialize, Deserializer, Serialize}; +use crate::small_string::SmallString; use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNameError}; /// The normalized name of a package. @@ -13,7 +15,6 @@ use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNam /// See: #[derive( Debug, - Default, Clone, PartialEq, Eq, @@ -27,7 +28,7 @@ use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNam )] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[rkyv(derive(Debug))] -pub struct PackageName(String); +pub struct PackageName(SmallString); impl PackageName { /// Create a validated, normalized package name. @@ -56,7 +57,7 @@ impl PackageName { Cow::Owned(owned_string) } else { - Cow::Borrowed(self.0.as_str()) + Cow::Borrowed(self.0.as_ref()) } } diff --git a/crates/uv-normalize/src/small_string.rs b/crates/uv-normalize/src/small_string.rs new file mode 100644 index 000000000..ca6d3ea6d --- /dev/null +++ b/crates/uv-normalize/src/small_string.rs @@ -0,0 +1,119 @@ +use std::cmp::PartialEq; +use std::ops::Deref; + +/// An optimized small string type for short identifiers, like package names. +/// +/// Represented as an [`arcstr::ArcStr`] internally. +#[derive(Default, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct SmallString(arcstr::ArcStr); + +impl From<&str> for SmallString { + #[inline] + fn from(s: &str) -> Self { + Self(s.into()) + } +} + +impl From for SmallString { + #[inline] + fn from(s: String) -> Self { + Self(s.into()) + } +} + +impl AsRef for SmallString { + #[inline] + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Deref for SmallString { + type Target = str; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl core::fmt::Debug for SmallString { + #[inline] + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Debug::fmt(&self.0, f) + } +} + +impl core::fmt::Display for SmallString { + #[inline] + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(&self.0, f) + } +} + +/// A [`serde::Serialize`] implementation for [`SmallString`]. +impl serde::Serialize for SmallString { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + +/// An [`rkyv`] implementation for [`SmallString`]. +impl rkyv::Archive for SmallString { + type Archived = rkyv::string::ArchivedString; + type Resolver = rkyv::string::StringResolver; + + #[inline] + fn resolve(&self, resolver: Self::Resolver, out: rkyv::Place) { + rkyv::string::ArchivedString::resolve_from_str(&self.0, resolver, out); + } +} + +impl rkyv::Serialize for SmallString +where + S: rkyv::rancor::Fallible + rkyv::ser::Allocator + rkyv::ser::Writer + ?Sized, + S::Error: rkyv::rancor::Source, +{ + fn serialize(&self, serializer: &mut S) -> Result { + rkyv::string::ArchivedString::serialize_from_str(&self.0, serializer) + } +} + +impl rkyv::Deserialize + for rkyv::string::ArchivedString +{ + fn deserialize(&self, _deserializer: &mut D) -> Result { + Ok(SmallString::from(self.as_str())) + } +} + +impl PartialEq for rkyv::string::ArchivedString { + fn eq(&self, other: &SmallString) -> bool { + **other == **self + } +} + +impl PartialOrd for rkyv::string::ArchivedString { + fn partial_cmp(&self, other: &SmallString) -> Option<::core::cmp::Ordering> { + Some(self.as_str().cmp(other)) + } +} + +/// An [`schemars::JsonSchema`] implementation for [`SmallString`]. +#[cfg(feature = "schemars")] +impl schemars::JsonSchema for SmallString { + fn is_referenceable() -> bool { + String::is_referenceable() + } + + fn schema_name() -> String { + String::schema_name() + } + + fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + String::json_schema(_gen) + } +} diff --git a/crates/uv-resolver/src/universal_marker.rs b/crates/uv-resolver/src/universal_marker.rs index 5724f19ab..0789ad264 100644 --- a/crates/uv-resolver/src/universal_marker.rs +++ b/crates/uv-resolver/src/universal_marker.rs @@ -488,6 +488,7 @@ fn encode_package_group(package: &PackageName, group: &GroupName) -> ExtraName { #[cfg(test)] mod tests { use super::*; + use std::str::FromStr; use uv_pypi_types::ConflictSet; @@ -516,7 +517,7 @@ mod tests { /// Shortcut for creating a package name. fn create_package(name: &str) -> PackageName { - PackageName::new(name.to_string()).unwrap() + PackageName::from_str(name).unwrap() } /// Shortcut for creating an extra name.