cargo_util_schemas/
restricted_names.rs
1type 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_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
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_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
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 == "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 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}