tame_index/
krate_name.rs

1use crate::error::{Error, InvalidKrateName};
2
3#[cfg(test)]
4/// Create a `KrateName` from a string literal.
5#[macro_export]
6macro_rules! kn {
7    ($kn:literal) => {
8        $crate::KrateName($kn)
9    };
10}
11
12/// Used to wrap user-provided strings so that bad crate names are required to
13/// be handled separately from things more outside the user control such as I/O
14/// errors
15#[derive(Copy, Clone)]
16pub struct KrateName<'name>(pub(crate) &'name str);
17
18impl<'name> KrateName<'name> {
19    /// Ensures the specified string is a valid crates.io crate name, according
20    /// to the (current) crates.io name restrictions
21    ///
22    /// 1. Non-empty
23    /// 2. May not start with a digit
24    /// 3. Maximum of 64 characters in length
25    /// 4. Must be ASCII alphanumeric, `-`, or `_`
26    /// 5. May not be a reserved name
27    ///     * A Rust keyword
28    ///     * Name of a Cargo output artifact
29    ///     * Name of a std library crate (or `test`)
30    ///     * A reserved Windows name (such as `nul`)
31    #[inline]
32    pub fn crates_io(name: &'name str) -> Result<Self, Error> {
33        Self::validated(name, Some(64))
34    }
35
36    /// Ensures the specified string is a valid crate name according to [cargo](https://github.com/rust-lang/cargo/blob/00b8da63269420610758464c02fc46584e373dd3/src/cargo/ops/cargo_new.rs#L167-L264)
37    ///
38    /// 1. Non-empty
39    /// 2. May not start with a digit
40    /// 3. Must be ASCII alphanumeric, `-`, or `_`
41    /// 4. May not be a reserved name
42    ///     * A Rust keyword
43    ///     * Name of a Cargo output artifact
44    ///     * Name of a std library crate (or `test`)
45    ///     * A reserved Windows name (such as `nul`)
46    #[inline]
47    pub fn cargo(name: &'name str) -> Result<Self, Error> {
48        Self::validated(name, None)
49    }
50
51    fn validated(name: &'name str, max_len: Option<usize>) -> Result<Self, Error> {
52        if name.is_empty() {
53            return Err(InvalidKrateName::InvalidLength(0).into());
54        }
55
56        let mut chars = name.chars().enumerate();
57
58        while let Some((i, c)) = chars.next() {
59            if i == 0 && c != '_' && !c.is_ascii_alphabetic() {
60                return Err(InvalidKrateName::InvalidCharacter {
61                    invalid: c,
62                    index: i,
63                }
64                .into());
65            }
66
67            if max_len == Some(i) {
68                return Err(InvalidKrateName::InvalidLength(i + 1 + chars.count()).into());
69            }
70
71            if c != '-' && c != '_' && !c.is_ascii_alphanumeric() {
72                return Err(InvalidKrateName::InvalidCharacter {
73                    invalid: c,
74                    index: i,
75                }
76                .into());
77            }
78        }
79
80        // This is a single table, binary sorted so that we can more easily just
81        // check matches and move on
82        //
83        // 1. Rustlang keywords, see https://doc.rust-lang.org/reference/keywords.html
84        // 2. Windows reserved, see https://github.com/rust-lang/cargo/blob/b40be8bdcf2eff9ed81702594d44bf96c27973a6/src/cargo/util/restricted_names.rs#L26-L32
85        // 3. Cargo artifacts, see https://github.com/rust-lang/cargo/blob/b40be8bdcf2eff9ed81702594d44bf96c27973a6/src/cargo/util/restricted_names.rs#L35-L37
86        // 4. Rustlang std, see https://github.com/rust-lang/cargo/blob/b40be8bdcf2eff9ed81702594d44bf96c27973a6/src/cargo/ops/cargo_new.rs#L225-L239
87        use crate::error::ReservedNameKind::{Artifact, Keyword, Standard, Windows};
88        const DISALLOWED: &[(&str, crate::error::ReservedNameKind)] = &[
89            ("Self", Keyword),
90            ("abstract", Keyword),
91            ("alloc", Standard),
92            ("as", Keyword),
93            ("async", Keyword),
94            ("aux", Windows),
95            ("await", Keyword),
96            ("become", Keyword),
97            ("box", Keyword),
98            ("break", Keyword),
99            ("build", Artifact),
100            ("com1", Windows),
101            ("com2", Windows),
102            ("com3", Windows),
103            ("com4", Windows),
104            ("com5", Windows),
105            ("com6", Windows),
106            ("com7", Windows),
107            ("com8", Windows),
108            ("com9", Windows),
109            ("con", Windows),
110            ("const", Keyword),
111            ("continue", Keyword),
112            ("core", Standard),
113            ("crate", Keyword),
114            ("deps", Artifact),
115            ("do", Keyword),
116            ("dyn", Keyword),
117            ("else", Keyword),
118            ("enum", Keyword),
119            ("examples", Artifact),
120            ("extern", Keyword),
121            ("false", Keyword),
122            ("final", Keyword),
123            ("fn", Keyword),
124            ("for", Keyword),
125            ("if", Keyword),
126            ("impl", Keyword),
127            ("in", Keyword),
128            ("incremental", Artifact),
129            ("let", Keyword),
130            ("loop", Keyword),
131            ("lpt1", Windows),
132            ("lpt2", Windows),
133            ("lpt3", Windows),
134            ("lpt4", Windows),
135            ("lpt5", Windows),
136            ("lpt6", Windows),
137            ("lpt7", Windows),
138            ("lpt8", Windows),
139            ("lpt9", Windows),
140            ("macro", Keyword),
141            ("match", Keyword),
142            ("mod", Keyword),
143            ("move", Keyword),
144            ("mut", Keyword),
145            ("nul", Windows),
146            ("override", Keyword),
147            ("priv", Keyword),
148            ("prn", Windows),
149            ("proc-macro", Standard),
150            ("proc_macro", Standard),
151            ("pub", Keyword),
152            ("ref", Keyword),
153            ("return", Keyword),
154            ("self", Keyword),
155            ("static", Keyword),
156            ("std", Standard),
157            ("struct", Keyword),
158            ("super", Keyword),
159            ("test", Standard),
160            ("trait", Keyword),
161            ("true", Keyword),
162            ("try", Keyword),
163            ("type", Keyword),
164            ("typeof", Keyword),
165            ("unsafe", Keyword),
166            ("unsized", Keyword),
167            ("use", Keyword),
168            ("virtual", Keyword),
169            ("where", Keyword),
170            ("while", Keyword),
171            ("yield", Keyword),
172        ];
173
174        if let Ok(i) = DISALLOWED.binary_search_by_key(&name, |(k, _v)| k) {
175            let (reserved, kind) = DISALLOWED[i];
176            Err(InvalidKrateName::ReservedName { reserved, kind }.into())
177        } else {
178            Ok(Self(name))
179        }
180    }
181}
182
183/// The simplest way to create a crate name, this just ensures that the crate name
184/// is non-empty, and ASCII alphanumeric, `-`, or, `-`, the minimum requirements
185/// for this crate
186impl<'name> TryFrom<&'name str> for KrateName<'name> {
187    type Error = Error;
188    #[inline]
189    fn try_from(s: &'name str) -> Result<Self, Self::Error> {
190        if s.is_empty() {
191            Err(InvalidKrateName::InvalidLength(0).into())
192        } else if let Some((index, invalid)) = s
193            .chars()
194            .enumerate()
195            .find(|(_i, c)| *c != '-' && *c != '_' && !c.is_ascii_alphanumeric())
196        {
197            Err(InvalidKrateName::InvalidCharacter { invalid, index }.into())
198        } else {
199            Ok(Self(s))
200        }
201    }
202}
203
204impl KrateName<'_> {
205    /// Writes the crate's prefix to the specified string
206    ///
207    /// Cargo uses a simple prefix in the registry index so that crate's can be
208    /// partitioned, particularly on disk without running up against potential OS
209    /// specific issues when hundreds of thousands of files are located with a single
210    /// directory
211    ///
212    /// The separator should be [`std::path::MAIN_SEPARATOR`] in disk cases and
213    /// '/' when used for urls
214    pub fn prefix(&self, acc: &mut String, sep: char) {
215        let name = self.0;
216        match name.len() {
217            0 => unreachable!(),
218            1 => acc.push('1'),
219            2 => acc.push('2'),
220            3 => {
221                acc.push('3');
222                acc.push(sep);
223                acc.push_str(&name[..1]);
224            }
225            _ => {
226                acc.push_str(&name[..2]);
227                acc.push(sep);
228                acc.push_str(&name[2..4]);
229            }
230        }
231    }
232
233    /// Gets the relative path to a crate
234    ///
235    /// This will be of the form [`Self::prefix`] + `<sep>` + `<name>`
236    ///
237    /// If not specified, the separator is [`std::path::MAIN_SEPARATOR`]
238    ///
239    /// ```
240    /// let crate_name: tame_index::KrateName = "tame-index".try_into().unwrap();
241    /// assert_eq!(crate_name.relative_path(Some('/')), "ta/me/tame-index");
242    /// ```
243    pub fn relative_path(&self, sep: Option<char>) -> String {
244        let name = self.0;
245        // Preallocate with the maximum possible width of a crate prefix `aa/bb/`
246        let mut rel_path = String::with_capacity(name.len() + 6);
247        let sep = sep.unwrap_or(std::path::MAIN_SEPARATOR);
248
249        self.prefix(&mut rel_path, sep);
250        rel_path.push(sep);
251        rel_path.push_str(name);
252
253        // A valid krate name is ASCII only, we don't need to worry about
254        // lowercasing utf-8
255        rel_path.make_ascii_lowercase();
256
257        rel_path
258    }
259}
260
261use std::fmt;
262
263impl fmt::Display for KrateName<'_> {
264    #[inline]
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        f.write_str(self.0)
267    }
268}
269
270impl fmt::Debug for KrateName<'_> {
271    #[inline]
272    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273        f.write_str(self.0)
274    }
275}
276
277#[cfg(test)]
278mod test {
279    use super::KrateName;
280    use crate::error::{Error, InvalidKrateName, ReservedNameKind};
281
282    /// Validates that all ways to create a krate name validate the basics of
283    /// not empty and allowed characters
284    #[test]
285    fn rejects_simple() {
286        assert!(matches!(
287            TryInto::<KrateName<'_>>::try_into("").unwrap_err(),
288            Error::InvalidKrateName(InvalidKrateName::InvalidLength(0))
289        ));
290        assert!(matches!(
291            KrateName::crates_io("").unwrap_err(),
292            Error::InvalidKrateName(InvalidKrateName::InvalidLength(0))
293        ));
294        assert!(matches!(
295            TryInto::<KrateName<'_>>::try_into("no.pe").unwrap_err(),
296            Error::InvalidKrateName(InvalidKrateName::InvalidCharacter {
297                index: 2,
298                invalid: '.',
299            })
300        ));
301        assert!(matches!(
302            KrateName::crates_io("no.pe").unwrap_err(),
303            Error::InvalidKrateName(InvalidKrateName::InvalidCharacter {
304                index: 2,
305                invalid: '.',
306            })
307        ));
308    }
309
310    /// Validates that crate names can't start with digit
311    #[test]
312    fn rejects_leading_digit() {
313        assert!(matches!(
314            KrateName::crates_io("3nop").unwrap_err(),
315            Error::InvalidKrateName(InvalidKrateName::InvalidCharacter {
316                index: 0,
317                invalid: '3',
318            })
319        ));
320    }
321
322    /// Validates the crate name doesn't exceed the crates.io limit
323    #[test]
324    fn rejects_too_long() {
325        assert!(matches!(
326            KrateName::crates_io(
327                "aaaaaaaabbbbbbbbccccccccddddddddaaaaaaaabbbbbbbbccccccccddddddddxxxxxxx"
328            )
329            .unwrap_err(),
330            Error::InvalidKrateName(InvalidKrateName::InvalidLength(71))
331        ));
332
333        assert!(KrateName::cargo(
334            "aaaaaaaabbbbbbbbccccccccddddddddaaaaaaaabbbbbbbbccccccccddddddddxxxxxxx"
335        )
336        .is_ok());
337    }
338
339    /// Validates the crate name can't be a reserved name
340    #[test]
341    fn rejects_reserved() {
342        assert!(matches!(
343            KrateName::cargo("nul").unwrap_err(),
344            Error::InvalidKrateName(InvalidKrateName::ReservedName {
345                reserved: "nul",
346                kind: ReservedNameKind::Windows
347            })
348        ));
349        assert!(matches!(
350            KrateName::cargo("deps").unwrap_err(),
351            Error::InvalidKrateName(InvalidKrateName::ReservedName {
352                reserved: "deps",
353                kind: ReservedNameKind::Artifact
354            })
355        ));
356        assert!(matches!(
357            KrateName::cargo("Self").unwrap_err(),
358            Error::InvalidKrateName(InvalidKrateName::ReservedName {
359                reserved: "Self",
360                kind: ReservedNameKind::Keyword
361            })
362        ));
363        assert!(matches!(
364            KrateName::cargo("yield").unwrap_err(),
365            Error::InvalidKrateName(InvalidKrateName::ReservedName {
366                reserved: "yield",
367                kind: ReservedNameKind::Keyword
368            })
369        ));
370        assert!(matches!(
371            KrateName::cargo("proc-macro").unwrap_err(),
372            Error::InvalidKrateName(InvalidKrateName::ReservedName {
373                reserved: "proc-macro",
374                kind: ReservedNameKind::Standard
375            })
376        ));
377        assert!(matches!(
378            KrateName::cargo("proc_macro").unwrap_err(),
379            Error::InvalidKrateName(InvalidKrateName::ReservedName {
380                reserved: "proc_macro",
381                kind: ReservedNameKind::Standard
382            })
383        ));
384    }
385
386    #[inline]
387    fn rp(n: &str) -> String {
388        KrateName(n).relative_path(Some('/'))
389    }
390
391    /// Validates we get the correct relative path to crate
392    #[test]
393    fn relative_path() {
394        assert_eq!(rp("a"), "1/a");
395        assert_eq!(rp("ab"), "2/ab");
396        assert_eq!(rp("abc"), "3/a/abc");
397        assert_eq!(rp("AbCd"), "ab/cd/abcd");
398        assert_eq!(rp("normal"), "no/rm/normal");
399        assert_eq!(rp("_boop-"), "_b/oo/_boop-");
400        assert_eq!(rp("Inflector"), "in/fl/inflector");
401    }
402}