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    #[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
46/// Normalize a `.dist-info` stem, converting it to lowercase and collapsing runs of `-`, `_`,
47/// and `.` down to a single `-`.
48///
49/// Unlike [`PackageName`], this does not restrict the allowed characters.
50fn 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}