Skip to main content

chaste_types/
name.rs

1// SPDX-FileCopyrightText: 2024 The Chaste Authors
2// SPDX-License-Identifier: Apache-2.0 OR BSD-2-Clause
3
4use std::{cmp, fmt};
5
6use nom::bytes::complete::tag;
7use nom::combinator::{eof, opt, recognize, verify};
8use nom::sequence::{preceded, terminated};
9use nom::{IResult, Parser};
10
11use crate::error::{Error, Result};
12use crate::misc::partial_eq_field;
13
14#[derive(Debug, PartialEq, Eq, Clone)]
15pub(crate) struct PackageNamePositions {
16    scope_end: Option<usize>,
17    pub(crate) total_length: usize,
18}
19
20/// Helper nom parser, public for reuse in implementations.
21pub fn package_name_part(input: &str) -> IResult<&str, &str> {
22    let input_bytes = input.as_bytes();
23    let mut ind = 0usize;
24    for byte in input_bytes {
25        // The special characters are not permitted by registry.npmjs.org for new packages,
26        // but used to be permitted as the check relied on ECMA-262 encodeURIComponent.
27        if !matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'.' | b'-' | b'_' | b'(' | b')' | b'~' | b'\'' | b'!' | b'*')
28        {
29            break;
30        }
31        ind += 1;
32    }
33    match ind {
34        0 => Err(nom::Err::Error(nom::error::Error::new(
35            input,
36            nom::error::ErrorKind::Many1,
37        ))),
38        i => {
39            let output = &input[..i];
40            if output.starts_with("_") || output.starts_with(".") {
41                Err(nom::Err::Error(nom::error::Error::new(
42                    input,
43                    nom::error::ErrorKind::Verify,
44                )))
45            } else {
46                Ok((&input[i..], output))
47            }
48        }
49    }
50}
51
52fn package_name_str_internal(
53    input: &str,
54) -> IResult<&str, (Option<&str>, &str), nom::error::Error<&str>> {
55    (
56        opt(preceded(tag("@"), terminated(package_name_part, tag("/")))),
57        verify(package_name_part, |part: &str| {
58            part != "node_modules" && part != "favicon.ico"
59        }),
60    )
61        .parse(input)
62}
63
64pub(crate) fn package_name(
65    input: &str,
66) -> IResult<&str, PackageNamePositions, nom::error::Error<&str>> {
67    package_name_str_internal
68        .parse(input)
69        .map(|(inp, (scope, rest))| {
70            let scope_end = scope.map(|s| s.len() + 1);
71            (
72                inp,
73                PackageNamePositions {
74                    scope_end,
75                    total_length: scope_end.map(|e| e + 1).unwrap_or(0) + rest.len(),
76                },
77            )
78        })
79}
80
81/// [nom] parser that recognizes a package name, but does not parse it.
82pub fn package_name_str(input: &str) -> IResult<&str, &str, nom::error::Error<&str>> {
83    recognize(package_name_str_internal).parse(input)
84}
85
86impl PackageNamePositions {
87    fn parse(input: &str) -> Result<Self> {
88        terminated(package_name, eof)
89            .parse(input)
90            .map(|(_, pos)| pos)
91            .map_err(|_| crate::Error::InvalidPackageName(input.to_string()))
92    }
93
94    /// "@scope" in "@scope/name"
95    fn scope(&self) -> Option<(usize, usize)> {
96        self.scope_end.map(|end| (0, end))
97    }
98    /// "@scope/" in "@scope/name"
99    fn scope_prefix(&self) -> Option<(usize, usize)> {
100        self.scope_end.map(|end| (0, end + 1))
101    }
102    /// "scope" in "@scope/name"
103    fn scope_name(&self) -> Option<(usize, usize)> {
104        self.scope_end.map(|end| (1, end))
105    }
106    /// "name" in "@scope/name"
107    fn name_rest(&self) -> (usize, usize) {
108        match self.scope_end {
109            Some(scope_end) => (scope_end + 1, self.total_length),
110            None => (0, self.total_length),
111        }
112    }
113}
114
115#[derive(Debug, PartialEq, Eq, Clone)]
116pub struct PackageName {
117    inner: String,
118    positions: PackageNamePositions,
119}
120
121#[derive(Debug, PartialEq, Eq, Clone)]
122pub struct PackageNameBorrowed<'a> {
123    pub(crate) inner: &'a str,
124    pub(crate) positions: &'a PackageNamePositions,
125}
126
127partial_eq_field!(PackageName, inner, String);
128partial_eq_field!(PackageName, inner, &str);
129partial_eq_field!(PackageNameBorrowed<'_>, inner, String);
130
131impl PartialEq<&str> for PackageNameBorrowed<'_> {
132    fn eq(&self, other: &&str) -> bool {
133        self.inner.eq(*other)
134    }
135}
136
137impl PackageNameBorrowed<'_> {
138    pub fn to_owned(&self) -> PackageName {
139        PackageName {
140            inner: self.inner.to_string(),
141            positions: self.positions.clone(),
142        }
143    }
144}
145
146macro_rules! option_segment {
147    ($name:ident) => {
148        pub fn $name(&self) -> Option<&str> {
149            self.positions
150                .$name()
151                .map(|(start, end)| &self.inner[start..end])
152        }
153    };
154}
155
156macro_rules! required_segment {
157    ($name:ident) => {
158        pub fn $name(&self) -> &str {
159            let (start, end) = self.positions.$name();
160            &self.inner[start..end]
161        }
162    };
163}
164
165impl PackageName {
166    pub fn new(name: String) -> Result<Self> {
167        Ok(Self {
168            positions: PackageNamePositions::parse(&name)?,
169            inner: name,
170        })
171    }
172
173    pub fn as_borrowed(&self) -> PackageNameBorrowed<'_> {
174        PackageNameBorrowed {
175            inner: &self.inner,
176            positions: &self.positions,
177        }
178    }
179
180    option_segment!(scope);
181    option_segment!(scope_prefix);
182    option_segment!(scope_name);
183    required_segment!(name_rest);
184}
185
186impl PackageNameBorrowed<'_> {
187    option_segment!(scope);
188    option_segment!(scope_prefix);
189    option_segment!(scope_name);
190    required_segment!(name_rest);
191}
192
193impl TryFrom<String> for PackageName {
194    type Error = Error;
195
196    fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
197        PackageName::new(value)
198    }
199}
200
201impl From<PackageName> for String {
202    fn from(value: PackageName) -> Self {
203        value.inner
204    }
205}
206impl fmt::Display for PackageName {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        self.inner.fmt(f)
209    }
210}
211impl fmt::Display for PackageNameBorrowed<'_> {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        self.inner.fmt(f)
214    }
215}
216impl AsRef<str> for PackageName {
217    fn as_ref(&self) -> &str {
218        &self.inner
219    }
220}
221impl AsRef<str> for PackageNameBorrowed<'_> {
222    fn as_ref(&self) -> &str {
223        self.inner
224    }
225}
226impl PartialOrd for PackageName {
227    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
228        Some(self.inner.cmp(&other.inner))
229    }
230}
231impl PartialOrd for PackageNameBorrowed<'_> {
232    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
233        Some(self.inner.cmp(other.inner))
234    }
235}
236impl Ord for PackageName {
237    fn cmp(&self, other: &Self) -> cmp::Ordering {
238        self.inner.cmp(&other.inner)
239    }
240}
241impl Ord for PackageNameBorrowed<'_> {
242    fn cmp(&self, other: &Self) -> cmp::Ordering {
243        self.inner.cmp(other.inner)
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use crate::error::{Error, Result};
250
251    use super::PackageName;
252
253    #[test]
254    fn test_positions_scoped() -> Result<()> {
255        let name = PackageName::new("@scope/name".to_string())?;
256        assert_eq!(name.scope(), Some("@scope"));
257        assert_eq!(name.scope_name(), Some("scope"));
258        assert_eq!(name.scope_prefix(), Some("@scope/"));
259        assert_eq!(name.name_rest(), "name");
260        Ok(())
261    }
262
263    #[test]
264    fn test_positions_unscoped() -> Result<()> {
265        let name = PackageName::new("name__1".to_string())?;
266        assert_eq!(name.scope(), None);
267        assert_eq!(name.scope_name(), None);
268        assert_eq!(name.scope_prefix(), None);
269        assert_eq!(name.name_rest(), "name__1");
270        Ok(())
271    }
272
273    #[test]
274    fn test_cursed_chars() -> Result<()> {
275        let name = PackageName::new("@a/verboden(name~'!*)".to_string())?;
276        assert_eq!(name.scope(), Some("@a"));
277        assert_eq!(name.scope_name(), Some("a"));
278        assert_eq!(name.scope_prefix(), Some("@a/"));
279        assert_eq!(name.name_rest(), "verboden(name~'!*)");
280        Ok(())
281    }
282
283    #[test]
284    fn test_invalid_names() {
285        macro_rules! assert_name_error {
286            ($name:expr) => {
287                assert_eq!(
288                    PackageName::new($name.to_string()),
289                    Err(Error::InvalidPackageName($name.to_string()))
290                );
291            };
292        }
293        assert_name_error!("");
294        assert_name_error!("ą");
295        assert_name_error!(".bin");
296        assert_name_error!("a/");
297        assert_name_error!("a@a/a");
298        assert_name_error!("@");
299        assert_name_error!("@a");
300        assert_name_error!("@a/");
301        assert_name_error!("/");
302        assert_name_error!("@/a");
303        assert_name_error!("@/");
304        assert_name_error!("@chastelock/node_modules");
305        assert_name_error!("node_modules");
306    }
307}