Skip to content

Commit 2765157

Browse files
committed
Use the same feature name validation rule from Cargo
Signed-off-by: hi-rustin <rustin.liu@gmail.com>
1 parent 57b2616 commit 2765157

File tree

5 files changed

+89
-45
lines changed

5 files changed

+89
-45
lines changed

src/controllers/krate/publish.rs

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -238,11 +238,7 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
238238
}
239239

240240
for (key, values) in features.iter() {
241-
if !Crate::valid_feature_name(key) {
242-
return Err(cargo_err(&format!(
243-
"\"{key}\" is an invalid feature name (feature names must contain only letters, numbers, '-', '+', or '_')"
244-
)));
245-
}
241+
Crate::valid_feature_name(key)?;
246242

247243
let num_features = values.len();
248244
if num_features > max_features {
@@ -257,9 +253,7 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
257253
}
258254

259255
for value in values.iter() {
260-
if !Crate::valid_feature(value) {
261-
return Err(cargo_err(&format!("\"{value}\" is an invalid feature name")));
262-
}
256+
Crate::valid_feature(value)?;
263257
}
264258
}
265259

@@ -597,11 +591,7 @@ pub fn validate_dependency(dep: &EncodableCrateDependency) -> AppResult<()> {
597591
}
598592

599593
for feature in &dep.features {
600-
if !Crate::valid_feature(feature) {
601-
return Err(cargo_err(&format_args!(
602-
"\"{feature}\" is an invalid feature name",
603-
)));
604-
}
594+
Crate::valid_feature(feature)?;
605595
}
606596

607597
if let Some(registry) = &dep.registry {

src/models/krate.rs

Lines changed: 83 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -220,14 +220,6 @@ impl Crate {
220220
.unwrap_or(false)
221221
}
222222

223-
/// Validates the THIS parts of `features = ["THIS", "and/THIS"]`.
224-
pub fn valid_feature_name(name: &str) -> bool {
225-
!name.is_empty()
226-
&& name
227-
.chars()
228-
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '+')
229-
}
230-
231223
/// Validates the prefix in front of the slash: `features = ["THIS/feature"]`.
232224
/// Normally this corresponds to the crate name of a dependency.
233225
fn valid_feature_prefix(name: &str) -> bool {
@@ -237,14 +229,68 @@ impl Crate {
237229
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
238230
}
239231

240-
/// Validates a whole feature string, `features = ["THIS", "ALL/THIS"]`.
241-
pub fn valid_feature(name: &str) -> bool {
242-
match name.split_once('/') {
243-
Some((dep, dep_feat)) => {
244-
let dep = dep.strip_suffix('?').unwrap_or(dep);
245-
Crate::valid_feature_prefix(dep) && Crate::valid_feature_name(dep_feat)
232+
/// Validates the THIS parts of `features = ["THIS", "and/THIS", "dep:THIS", "dep?/THIS"]`.
233+
/// 1. The name must be non-empty.
234+
/// 2. The first character must be a Unicode XID start character, `_`, or a digit.
235+
/// 3. The remaining characters must be Unicode XID characters, `_`, `+`, `-`, or `.`.
236+
pub fn valid_feature_name(name: &str) -> AppResult<()> {
237+
if name.is_empty() {
238+
return Err(cargo_err("feature cannot be an empty"));
239+
}
240+
let mut chars = name.chars();
241+
if let Some(ch) = chars.next() {
242+
if !(unicode_xid::UnicodeXID::is_xid_start(ch) || ch == '_' || ch.is_ascii_digit()) {
243+
return Err(cargo_err(&format!(
244+
"invalid character `{}` in feature `{}`, \
245+
the first character must be a Unicode XID start character or digit \
246+
(most letters or `_` or `0` to `9`)",
247+
ch, name,
248+
)));
249+
}
250+
}
251+
for ch in chars {
252+
if !(unicode_xid::UnicodeXID::is_xid_continue(ch)
253+
|| ch == '+'
254+
|| ch == '-'
255+
|| ch == '.')
256+
{
257+
return Err(cargo_err(&format!(
258+
"invalid character `{}` in feature `{}`, \
259+
characters must be Unicode XID characters, `+`, `-`, or `.` \
260+
(numbers, `+`, `-`, `_`, `.`, or most letters)",
261+
ch, name,
262+
)));
263+
}
264+
}
265+
266+
Ok(())
267+
}
268+
269+
/// Validates a whole feature string, `features = ["THIS", "and/THIS", "dep:THIS", "dep?/THIS"]`.
270+
pub fn valid_feature(name: &str) -> AppResult<()> {
271+
if let Some((dep, dep_feat)) = name.split_once('/') {
272+
let dep = dep.strip_suffix('?').unwrap_or(dep);
273+
if !Crate::valid_dependency_name(dep) {
274+
return Err(cargo_err(&format_args!(
275+
"\"{dep}\" is an invalid dependency name (dependency \
276+
names must start with a letter or underscore, contain only \
277+
letters, numbers, hyphens, or underscores and have at most \
278+
{MAX_NAME_LENGTH} characters)"
279+
)));
246280
}
247-
None => Crate::valid_feature_name(name.strip_prefix("dep:").unwrap_or(name)),
281+
Crate::valid_feature_name(dep_feat)
282+
} else if let Some((_, dep)) = name.split_once("dep:") {
283+
if !Crate::valid_dependency_name(dep) {
284+
return Err(cargo_err(&format_args!(
285+
"\"{dep}\" is an invalid dependency name (dependency \
286+
names must start with a letter or underscore, contain only \
287+
letters, numbers, hyphens, or underscores and have at most \
288+
{MAX_NAME_LENGTH} characters)"
289+
)));
290+
}
291+
return Ok(());
292+
} else {
293+
Crate::valid_feature_name(name)
248294
}
249295
}
250296

@@ -517,19 +563,27 @@ mod tests {
517563

518564
#[test]
519565
fn valid_feature_names() {
520-
assert!(Crate::valid_feature("foo"));
521-
assert!(!Crate::valid_feature(""));
522-
assert!(!Crate::valid_feature("/"));
523-
assert!(!Crate::valid_feature("%/%"));
524-
assert!(Crate::valid_feature("a/a"));
525-
assert!(Crate::valid_feature("32-column-tables"));
526-
assert!(Crate::valid_feature("c++20"));
527-
assert!(Crate::valid_feature("krate/c++20"));
528-
assert!(!Crate::valid_feature("c++20/wow"));
529-
assert!(Crate::valid_feature("foo?/bar"));
530-
assert!(Crate::valid_feature("dep:foo"));
531-
assert!(!Crate::valid_feature("dep:foo?/bar"));
532-
assert!(!Crate::valid_feature("foo/?bar"));
533-
assert!(!Crate::valid_feature("foo?bar"));
566+
assert!(Crate::valid_feature("foo").is_ok());
567+
assert!(Crate::valid_feature("1foo").is_ok());
568+
assert!(Crate::valid_feature("_foo").is_ok());
569+
assert!(Crate::valid_feature("_foo-_+.1").is_ok());
570+
assert!(Crate::valid_feature("_foo-_+.1").is_ok());
571+
assert!(Crate::valid_feature("").is_err());
572+
assert!(Crate::valid_feature("/").is_err());
573+
assert!(Crate::valid_feature("%/%").is_err());
574+
assert!(Crate::valid_feature("a/a").is_ok());
575+
assert!(Crate::valid_feature("32-column-tables").is_ok());
576+
assert!(Crate::valid_feature("c++20").is_ok());
577+
assert!(Crate::valid_feature("krate/c++20").is_ok());
578+
assert!(Crate::valid_feature("c++20/wow").is_err());
579+
assert!(Crate::valid_feature("foo?/bar").is_ok());
580+
assert!(Crate::valid_feature("dep:foo").is_ok());
581+
assert!(Crate::valid_feature("dep:foo?/bar").is_err());
582+
assert!(Crate::valid_feature("foo/?bar").is_err());
583+
assert!(Crate::valid_feature("foo?bar").is_err());
584+
assert!(Crate::valid_feature("bar.web").is_ok());
585+
assert!(Crate::valid_feature("foo/bar.web").is_ok());
586+
assert!(Crate::valid_feature("dep:0foo").is_err());
587+
assert!(Crate::valid_feature("0foo?/bar.web").is_err());
534588
}
535589
}

src/tests/krate/publish/snapshots/all__krate__publish__dependencies__invalid_feature_name.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ expression: response.into_json()
55
{
66
"errors": [
77
{
8-
"detail": "\"🍺\" is an invalid feature name"
8+
"detail": "invalid character `🍺` in feature `🍺`, the first character must be a Unicode XID start character or digit (most letters or `_` or `0` to `9`)"
99
}
1010
]
1111
}

src/tests/krate/publish/snapshots/all__krate__publish__features__invalid_feature.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ expression: response.into_json()
55
{
66
"errors": [
77
{
8-
"detail": "\"!bar\" is an invalid feature name"
8+
"detail": "invalid character `!` in feature `!bar`, the first character must be a Unicode XID start character or digit (most letters or `_` or `0` to `9`)"
99
}
1010
]
1111
}

src/tests/krate/publish/snapshots/all__krate__publish__features__invalid_feature_name.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ expression: response.into_json()
55
{
66
"errors": [
77
{
8-
"detail": "\"~foo\" is an invalid feature name (feature names must contain only letters, numbers, '-', '+', or '_')"
8+
"detail": "invalid character `~` in feature `~foo`, the first character must be a Unicode XID start character or digit (most letters or `_` or `0` to `9`)"
99
}
1010
]
1111
}

0 commit comments

Comments
 (0)