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!(
334            KrateName::cargo(
335                "aaaaaaaabbbbbbbbccccccccddddddddaaaaaaaabbbbbbbbccccccccddddddddxxxxxxx"
336            )
337            .is_ok()
338        );
339    }
340
341    /// Validates the crate name can't be a reserved name
342    #[test]
343    fn rejects_reserved() {
344        assert!(matches!(
345            KrateName::cargo("nul").unwrap_err(),
346            Error::InvalidKrateName(InvalidKrateName::ReservedName {
347                reserved: "nul",
348                kind: ReservedNameKind::Windows
349            })
350        ));
351        assert!(matches!(
352            KrateName::cargo("deps").unwrap_err(),
353            Error::InvalidKrateName(InvalidKrateName::ReservedName {
354                reserved: "deps",
355                kind: ReservedNameKind::Artifact
356            })
357        ));
358        assert!(matches!(
359            KrateName::cargo("Self").unwrap_err(),
360            Error::InvalidKrateName(InvalidKrateName::ReservedName {
361                reserved: "Self",
362                kind: ReservedNameKind::Keyword
363            })
364        ));
365        assert!(matches!(
366            KrateName::cargo("yield").unwrap_err(),
367            Error::InvalidKrateName(InvalidKrateName::ReservedName {
368                reserved: "yield",
369                kind: ReservedNameKind::Keyword
370            })
371        ));
372        assert!(matches!(
373            KrateName::cargo("proc-macro").unwrap_err(),
374            Error::InvalidKrateName(InvalidKrateName::ReservedName {
375                reserved: "proc-macro",
376                kind: ReservedNameKind::Standard
377            })
378        ));
379        assert!(matches!(
380            KrateName::cargo("proc_macro").unwrap_err(),
381            Error::InvalidKrateName(InvalidKrateName::ReservedName {
382                reserved: "proc_macro",
383                kind: ReservedNameKind::Standard
384            })
385        ));
386    }
387
388    #[inline]
389    fn rp(n: &str) -> String {
390        KrateName(n).relative_path(Some('/'))
391    }
392
393    /// Validates we get the correct relative path to crate
394    #[test]
395    fn relative_path() {
396        assert_eq!(rp("a"), "1/a");
397        assert_eq!(rp("ab"), "2/ab");
398        assert_eq!(rp("abc"), "3/a/abc");
399        assert_eq!(rp("AbCd"), "ab/cd/abcd");
400        assert_eq!(rp("normal"), "no/rm/normal");
401        assert_eq!(rp("_boop-"), "_b/oo/_boop-");
402        assert_eq!(rp("Inflector"), "in/fl/inflector");
403    }
404}