uv_metadata/
dist_info_stem.rs1use std::borrow::Cow;
2use std::fmt::{Display, Formatter};
3
4use uv_normalize::PackageName;
5
6use crate::Error;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct DistInfoStem<'a>(Cow<'a, str>);
13
14impl<'a> DistInfoStem<'a> {
15 pub(crate) fn new(
20 stem: impl Into<Cow<'a, str>>,
21 package_name: &PackageName,
22 ) -> Result<Self, Error> {
23 let stem = stem.into();
24 if !normalize(&stem).starts_with(package_name.as_str()) {
25 return Err(Error::MissingDistInfoPackageName(
26 stem.into_owned(),
27 package_name.to_string(),
28 ));
29 }
30 Ok(Self(stem))
31 }
32
33 #[cfg(test)]
35 pub(crate) fn as_str(&self) -> &str {
36 &self.0
37 }
38}
39
40impl Display for DistInfoStem<'_> {
41 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
42 self.0.fmt(f)
43 }
44}
45
46fn normalize(stem: &str) -> Cow<'_, str> {
51 let mut last_was_separator = false;
52 let normalized = stem.bytes().filter_map(move |byte| {
53 let byte = match byte {
54 b'-' | b'_' | b'.' => b'-',
55 byte => byte.to_ascii_lowercase(),
56 };
57 let is_separator = byte == b'-';
58 let repeated_separator = last_was_separator && is_separator;
59 last_was_separator = is_separator;
60 (!repeated_separator).then_some(byte)
61 });
62
63 if normalized.clone().eq(stem.bytes()) {
64 return Cow::Borrowed(stem);
65 }
66
67 let mut output = String::with_capacity(stem.len());
68 output.extend(normalized.map(char::from));
69 Cow::Owned(output)
70}
71
72#[cfg(test)]
73mod tests {
74 use std::borrow::Cow;
75
76 #[test]
77 fn normalize() {
78 let inputs = [
79 "friendly-bard",
80 "Friendly-Bard",
81 "FRIENDLY-BARD",
82 "friendly.bard",
83 "friendly_bard",
84 "friendly--bard",
85 "friendly-.bard",
86 "FrIeNdLy-._.-bArD",
87 ];
88 for input in inputs {
89 assert_eq!(super::normalize(input), "friendly-bard");
90 }
91 }
92
93 #[test]
94 fn normalize_borrowed() {
95 for input in ["", "-", "friendly-bard", "friendly+local!", "café"] {
96 assert_eq!(
97 match super::normalize(input) {
98 Cow::Borrowed(normalized) => Some(normalized),
99 Cow::Owned(_) => None,
100 },
101 Some(input)
102 );
103 }
104 }
105}