cargo_util_schemas/
restricted_names.rs1type Result<T> = std::result::Result<T, NameValidationError>;
4
5#[derive(Debug, thiserror::Error)]
7#[error(transparent)]
8pub struct NameValidationError(#[from] ErrorKind);
9
10#[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 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
90pub(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
124pub(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 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}