Skip to main content

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_ident::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_ident::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_ident::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_ident::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 == "build-override" {
141        return Err(ErrorKind::ProfileNameReservedKeyword {
142            name: name.into(),
143            help: "To configure build dependency settings, use [profile.dev.build-override] \
144                 and [profile.release.build-override]",
145        }
146        .into());
147    }
148
149    // These are some arbitrary reservations. We have no plans to use
150    // these, but it seems safer to reserve a few just in case we want to
151    // add more built-in profiles in the future. We can also uses special
152    // syntax like cargo:foo if needed. But it is unlikely these will ever
153    // be used.
154    if matches!(
155        lower_name.as_str(),
156        "build"
157            | "check"
158            | "clean"
159            | "config"
160            | "fetch"
161            | "fix"
162            | "install"
163            | "metadata"
164            | "package"
165            | "publish"
166            | "report"
167            | "root"
168            | "run"
169            | "rust"
170            | "rustc"
171            | "rustdoc"
172            | "target"
173            | "tmp"
174            | "uninstall"
175    ) || lower_name.starts_with("cargo")
176    {
177        return Err(ErrorKind::ProfileNameReservedKeyword {
178            name: name.into(),
179            help: "Please choose a different name.",
180        }
181        .into());
182    }
183
184    Ok(())
185}
186
187pub(crate) fn validate_feature_name(name: &str) -> Result<()> {
188    let what = "feature name";
189    if name.is_empty() {
190        return Err(ErrorKind::Empty(what).into());
191    }
192
193    if name.starts_with("dep:") {
194        return Err(ErrorKind::FeatureNameStartsWithDepColon(name.into()).into());
195    }
196    if name.contains('/') {
197        return Err(ErrorKind::InvalidCharacter {
198            ch: '/',
199            what,
200            name: name.into(),
201            reason: "feature name is not allowed to contain slashes",
202        }
203        .into());
204    }
205    let mut chars = name.chars();
206    if let Some(ch) = chars.next() {
207        if !(unicode_ident::is_xid_start(ch) || ch == '_' || ch.is_digit(10)) {
208            return Err(ErrorKind::InvalidCharacter {
209                ch,
210                what,
211                name: name.into(),
212                reason: "the first character must be a Unicode XID start character or digit \
213                 (most letters or `_` or `0` to `9`)",
214            }
215            .into());
216        }
217    }
218    for ch in chars {
219        if !(unicode_ident::is_xid_continue(ch) || ch == '-' || ch == '+' || ch == '.') {
220            return Err(ErrorKind::InvalidCharacter {
221                ch,
222                what,
223                name: name.into(),
224                reason: "characters must be Unicode XID characters, '-', `+`, or `.` \
225                 (numbers, `+`, `-`, `_`, `.`, or most letters)",
226            }
227            .into());
228        }
229    }
230    Ok(())
231}
232
233pub(crate) fn validate_path_base_name(name: &str) -> Result<()> {
234    validate_name(name, "path base name")
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn valid_feature_names() {
243        assert!(validate_feature_name("c++17").is_ok());
244        assert!(validate_feature_name("128bit").is_ok());
245        assert!(validate_feature_name("_foo").is_ok());
246        assert!(validate_feature_name("feat-name").is_ok());
247        assert!(validate_feature_name("feat_name").is_ok());
248        assert!(validate_feature_name("foo.bar").is_ok());
249
250        assert!(validate_feature_name("").is_err());
251        assert!(validate_feature_name("+foo").is_err());
252        assert!(validate_feature_name("-foo").is_err());
253        assert!(validate_feature_name(".foo").is_err());
254        assert!(validate_feature_name("dep:bar").is_err());
255        assert!(validate_feature_name("foo/bar").is_err());
256        assert!(validate_feature_name("foo:bar").is_err());
257        assert!(validate_feature_name("foo?").is_err());
258        assert!(validate_feature_name("?foo").is_err());
259        assert!(validate_feature_name("ⒶⒷⒸ").is_err());
260        assert!(validate_feature_name("a¼").is_err());
261        assert!(validate_feature_name("").is_err());
262    }
263}