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.