cargo_util_schemas/
restricted_names.rs

1//! Helpers for validating and checking names like package and crate names.
2
3type Result<T> = std::result::Result<T, NameValidationError>;
4
5/// Error validating names in Cargo.
6#[derive(Debug, thiserror::Error)]
7#[error(transparent)]
8pub struct NameValidationError(#[from] ErrorKind);
9
10/// Non-public error kind for [`NameValidationError`].
11#[non_exhaustive]
12#[derive(Debug, thiserror::Error)]
13enum ErrorKind {
14    #[error("{0} cannot be empty")]
15    Empty(&'static str),
16
17    #[error("invalid character `{ch}` in {what}: `{name}`, {reason}")]
18    InvalidCharacter {
19        ch: char,
20        what: &'static str,
21        name: String,
22        reason: &'static str,
23    },
24
25    #[error(
26        "profile name `{name}` is reserved\n{help}\n\
27         See https://doc.rust-lang.org/cargo/reference/profiles.html \
28         for more on configuring profiles."
29    )]
30    ProfileNameReservedKeyword { name: String, help: &'static str },
31
32    #[error("feature named `{0}` is not allowed to start with `dep:`")]
33    FeatureNameStartsWithDepColon(String),
34}
35
36pub(crate) fn validate_package_name(name: &str) -> Result<()> {
37    for part in name.split("::") {
38        validate_name(part, "package name")?;
39    }
40    Ok(())
41}
42
43pub(crate) fn validate_registry_name(name: &str) -> Result<()> {
44    validate_name(name, "registry name")
45}
46
47pub(crate) fn validate_name(name: &str, what: &'static str) -> Result<()> {
48    if name.is_empty() {
49        return Err(ErrorKind::Empty(what).into());
50    }
51
52    let mut chars = name.chars();
53    if let Some(ch) = chars.next() {
54        if ch.is_digit(10) {
55            // A specific error for a potentially common case.
56            return Err(ErrorKind::InvalidCharacter {
57                ch,
58                what,
59                name: name.into(),
60                reason: "the name cannot start with a digit",
61            }
62            .into());
63        }
64        if !(unicode_xid::UnicodeXID::is_xid_start(ch) || ch == '_') {
65            return Err(ErrorKind::InvalidCharacter {
66                ch,
67                what,
68                name: name.into(),
69                reason: "the first character must be a Unicode XID start character \
70                 (most letters or `_`)",
71            }
72            .into());
73        }
74    }
75    for ch in chars {
76        if !(unicode_xid::UnicodeXID::is_xid_continue(ch) || ch == '-') {
77            return Err(ErrorKind::InvalidCharacter {
78                ch,
79                what,
80                name: name.into(),
81                reason: "characters must be Unicode XID characters \
82                 (numbers, `-`, `_`, or most letters)",
83            }
84            .into());
85        }
86    }
87    Ok(())
88}
89
90/// Ensure a package name is [valid][validate_package_name]
91pub(crate) fn sanitize_package_name(name: &str, placeholder: char) -> String {
92    let mut slug = String::new();
93    for part in name.split("::") {
94        if !slug.is_empty() {
95            slug.push_str("::");
96        }
97        slug.push_str(&sanitize_name(part, placeholder));
98    }
99    slug
100}
101
102pub(crate) fn sanitize_name(name: &str, placeholder: char) -> String {
103    let mut slug = String::new();
104    let mut chars = name.chars();
105    while let Some(ch) = chars.next() {
106        if (unicode_xid::UnicodeXID::is_xid_start(ch) || ch == '_') && !ch.is_digit(10) {
107            slug.push(ch);
108            break;
109        }
110    }
111    while let Some(ch) = chars.next() {
112        if unicode_xid::UnicodeXID::is_xid_continue(ch) || ch == '-' {
113            slug.push(ch);
114        } else {
115            slug.push(placeholder);
116        }
117    }
118    if slug.is_empty() {
119        slug.push_str("package");
120    }
121    slug
122}
123
124/// Validate dir-names and profile names according to RFC 2678.
125pub(crate) fn validate_profile_name(name: &str) -> Result<()> {
126    if let Some(ch) = name
127        .chars()
128        .find(|ch| !ch.is_alphanumeric() && *ch != '_' && *ch != '-')
129    {
130        return Err(ErrorKind::InvalidCharacter {
131            ch,
132            what: "profile name",
133            name: name.into(),
134            reason: "allowed characters are letters, numbers, underscore, and hyphen",
135        }
136        .into());
137    }
138
139    let lower_name = name.to_lowercase();
140    if lower_name == "debug" {
141        return Err(ErrorKind::ProfileNameReservedKeyword {
142            name: name.into(),
143            help: "To configure the default development profile, \
144                use the name `dev` as in [profile.dev]",
145        }
146        .into());
147    }
148    if lower_name == "build-override" {
149        return Err(ErrorKind::ProfileNameReservedKeyword {
150            name: name.into(),
151            help: "To configure build dependency settings, use [profile.dev.build-override] \
152                 and [profile.release.build-override]",
153        }
154        .into());
155    }
156
157    // These are some arbitrary reservations. We have no plans to use
158    // these, but it seems safer to reserve a few just in case we want to
159    // add more built-in profiles in the future. We can also uses special
160    // syntax like cargo:foo if needed. But it is unlikely these will ever
161    // be used.
162    if matches!(
163        lower_name.as_str(),
164        "build"
165            | "check"
166            | "clean"
167            | "config"
168            | "fetch"
169            | "fix"
170            | "install"
171            | "metadata"
172            | "package"
173            | "publish"
174            | "report"
175            | "root"
176            | "run"
177            | "rust"
178            | "rustc"
179            | "rustdoc"
180            | "target"
181            | "tmp"
182            | "uninstall"
183    ) || lower_name.starts_with("cargo")
184    {
185        return Err(ErrorKind::ProfileNameReservedKeyword {
186            name: name.into(),
187            help: "Please choose a different name.",
188        }
189        .into());
190    }
191
192    Ok(())
193}
194
195pub(crate) fn validate_feature_name(name: &str) -> Result<()> {
196    let what = "feature name";
197    if name.is_empty() {
198        return Err(ErrorKind::Empty(what).into());
199    }
200
201    if name.starts_with("dep:") {
202        return Err(ErrorKind::FeatureNameStartsWithDepColon(name.into()).into());
203    }
204    if name.contains('/') {
205        return Err(ErrorKind::InvalidCharacter {
206            ch: '/',
207            what,
208            name: name.into(),
209            reason: "feature name is not allowed to contain slashes",
210        }
211        .into());
212    }
213    let mut chars = name.chars();
214    if let Some(ch) = chars.next() {
215        if !(unicode_xid::UnicodeXID::is_xid_start(ch) || ch == '_' || ch.is_digit(10)) {
216            return Err(ErrorKind::InvalidCharacter {
217                ch,
218                what,
219                name: name.into(),
220                reason: "the first character must be a Unicode XID start character or digit \
221                 (most letters or `_` or `0` to `9`)",
222            }
223            .into());
224        }
225    }
226    for ch in chars {
227        if !(unicode_xid::UnicodeXID::is_xid_continue(ch) || ch == '-' || ch == '+' || ch == '.') {
228            return Err(ErrorKind::InvalidCharacter {
229                ch,
230                what,
231                name: name.into(),
232                reason: "characters must be Unicode XID characters, '-', `+`, or `.` \
233                 (numbers, `+`, `-`, `_`, `.`, or most letters)",
234            }
235            .into());
236        }
237    }
238    Ok(())
239}
240
241pub(crate) fn validate_path_base_name(name: &str) -> Result<()> {
242    validate_name(name, "path base name")
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn valid_feature_names() {
251        assert!(validate_feature_name("c++17").is_ok());
252        assert!(validate_feature_name("128bit").is_ok());
253        assert!(validate_feature_name("_foo").is_ok());
254        assert!(validate_feature_name("feat-name").is_ok());
255        assert!(validate_feature_name("feat_name").is_ok());
256        assert!(validate_feature_name("foo.bar").is_ok());
257
258        assert!(validate_feature_name("").is_err());
259        assert!(validate_feature_name("+foo").is_err());
260        assert!(validate_feature_name("-foo").is_err());
261        assert!(validate_feature_name(".foo").is_err());
262        assert!(validate_feature_name("dep:bar").is_err());
263        assert!(validate_feature_name("foo/bar").is_err());
264        assert!(validate_feature_name("foo:bar").is_err());
265        assert!(validate_feature_name("foo?").is_err());
266        assert!(validate_feature_name("?foo").is_err());
267        assert!(validate_feature_name("ⒶⒷⒸ").is_err());
268        assert!(validate_feature_name("a¼").is_err());
269        assert!(validate_feature_name("").is_err());
270    }
271}