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 pub(crate) fn as_str(&self) -> &str {
35 &self.0
36 }
37}
38
39impl Display for DistInfoStem<'_> {
40 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
41 self.0.fmt(f)
42 }
43}
44
45fn normalize(stem: &str) -> Cow<'_, str> {
50 let mut last_was_separator = false;
51 let normalized = stem.bytes().filter_map(move |byte| {
52 let byte = match byte {
53 b'-' | b'_' | b'.' => b'-',
54 byte => byte.to_ascii_lowercase(),
55 };
56 let is_separator = byte == b'-';
57 let repeated_separator = last_was_separator && is_separator;
58 last_was_separator = is_separator;
59 (!repeated_separator).then_some(byte)
60 });
61
62 if normalized.clone().eq(stem.bytes()) {
63 return Cow::Borrowed(stem);
64 }
65
66 let mut output = String::with_capacity(stem.len());
67 output.extend(normalized.map(char::from));
68 Cow::Owned(output)
69}
70
71#[cfg(test)]
72mod tests {
73 use std::borrow::Cow;
74
75 #[test]
76 fn normalize() {
77 let inputs = [
78 "friendly-bard",
79 "Friendly-Bard",
80 "FRIENDLY-BARD",
81 "friendly.bard",
82 "friendly_bard",
83 "friendly--bard",
84 "friendly-.bard",
85 "FrIeNdLy-._.-bArD",
86 ];
87 for input in inputs {
88 assert_eq!(super::normalize(input), "friendly-bard");
89 }
90 }
91
92 #[test]
93 fn normalize_borrowed() {
94 for input in ["", "-", "friendly-bard", "friendly+local!", "café"] {
95 assert_eq!(
96 match super::normalize(input) {
97 Cow::Borrowed(normalized) => Some(normalized),
98 Cow::Owned(_) => None,
99 },
100 Some(input)
101 );
102 }
103 }
104}