Skip to main content

uv_metadata/
dist_info_stem.rs

1use std::borrow::Cow;
2use std::fmt::{Display, Formatter};
3
4use uv_normalize::PackageName;
5
6use crate::Error;
7
8/// The stem of a `.dist-info` directory whose normalized form starts with the expected package name.
9///
10/// Stores the original spelling, without the `.dist-info` suffix, so it can be used to locate files.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct DistInfoStem<'a>(Cow<'a, str>);
13
14impl<'a> DistInfoStem<'a> {
15    /// Validate a `.dist-info` directory stem against its package name.
16    ///
17    /// Like `pip`, only require the normalized stem to start with the canonical package name. Some
18    /// wheels use names that do not follow the current name and version rules.
19    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    /// Return the original directory stem, without the `.dist-info` suffix.
34    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
45/// Normalize a `.dist-info` stem, converting it to lowercase and collapsing runs of `-`, `_`,
46/// and `.` down to a single `-`.
47///
48/// Unlike [`PackageName`], this does not restrict the allowed characters.
49fn 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}