Skip to content

Commit 9fd9306

Browse files
authored
Merge pull request #7500 from hi-rustin/rustin-patch-feature-name
Use the same feature name validation rule from Cargo
2 parents e742b01 + cc5ed5e commit 9fd9306

14 files changed

+231
-66
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ tracing = "=0.1.40"
100100
tracing-subscriber = { version = "=0.3.18", features = ["env-filter"] }
101101
typomania = { version = "=0.1.2", default-features = false }
102102
url = "=2.4.1"
103+
unicode-xid = "0.2.4"
103104

104105
[dev-dependencies]
105106
bytes = "=1.5.0"

src/controllers/krate/publish.rs

Lines changed: 4 additions & 19 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

@@ -602,11 +596,7 @@ pub fn validate_dependency(dep: &EncodableCrateDependency) -> AppResult<()> {
602596
}
603597

604598
for feature in &dep.features {
605-
if !Crate::valid_feature(feature) {
606-
return Err(cargo_err(&format_args!(
607-
"\"{feature}\" is an invalid feature name",
608-
)));
609-
}
599+
Crate::valid_feature(feature)?;
610600
}
611601

612602
if let Some(registry) = &dep.registry {
@@ -633,12 +623,7 @@ pub fn validate_dependency(dep: &EncodableCrateDependency) -> AppResult<()> {
633623

634624
if let Some(toml_name) = &dep.explicit_name_in_toml {
635625
if !Crate::valid_dependency_name(toml_name) {
636-
return Err(cargo_err(&format_args!(
637-
"\"{toml_name}\" is an invalid dependency name (dependency \
638-
names must start with a letter or underscore, contain only \
639-
letters, numbers, hyphens, or underscores and have at most \
640-
{MAX_NAME_LENGTH} characters)"
641-
)));
626+
return Err(cargo_err(&Crate::invalid_dependency_name_msg(toml_name)));
642627
}
643628
}
644629

src/models/krate.rs

Lines changed: 84 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -220,12 +220,68 @@ 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 == '+')
223+
pub fn invalid_dependency_name_msg(dep: &str) -> String {
224+
format!(
225+
"\"{dep}\" is an invalid dependency name (dependency \
226+
names must start with a letter or underscore, contain only \
227+
letters, numbers, hyphens, or underscores and have at most \
228+
{MAX_NAME_LENGTH} characters)"
229+
)
230+
}
231+
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(&Crate::invalid_dependency_name_msg(dep)));
275+
}
276+
Crate::valid_feature_name(dep_feat)
277+
} else if let Some((_, dep)) = name.split_once("dep:") {
278+
if !Crate::valid_dependency_name(dep) {
279+
return Err(cargo_err(&Crate::invalid_dependency_name_msg(dep)));
280+
}
281+
return Ok(());
282+
} else {
283+
Crate::valid_feature_name(name)
284+
}
229285
}
230286

231287
/// Validates the prefix in front of the slash: `features = ["THIS/feature"]`.
@@ -237,17 +293,6 @@ impl Crate {
237293
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
238294
}
239295

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)
246-
}
247-
None => Crate::valid_feature_name(name.strip_prefix("dep:").unwrap_or(name)),
248-
}
249-
}
250-
251296
/// Return both the newest (most recently updated) and
252297
/// highest version (in semver order) for the current crate.
253298
pub fn top_versions(&self, conn: &mut PgConnection) -> QueryResult<TopVersions> {
@@ -514,22 +559,29 @@ mod tests {
514559
assert!(Crate::valid_dependency_name("_foo"));
515560
assert!(!Crate::valid_dependency_name("-foo"));
516561
}
517-
518562
#[test]
519563
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"));
564+
assert!(Crate::valid_feature("foo").is_ok());
565+
assert!(Crate::valid_feature("1foo").is_ok());
566+
assert!(Crate::valid_feature("_foo").is_ok());
567+
assert!(Crate::valid_feature("_foo-_+.1").is_ok());
568+
assert!(Crate::valid_feature("_foo-_+.1").is_ok());
569+
assert!(Crate::valid_feature("").is_err());
570+
assert!(Crate::valid_feature("/").is_err());
571+
assert!(Crate::valid_feature("%/%").is_err());
572+
assert!(Crate::valid_feature("a/a").is_ok());
573+
assert!(Crate::valid_feature("32-column-tables").is_ok());
574+
assert!(Crate::valid_feature("c++20").is_ok());
575+
assert!(Crate::valid_feature("krate/c++20").is_ok());
576+
assert!(Crate::valid_feature("c++20/wow").is_err());
577+
assert!(Crate::valid_feature("foo?/bar").is_ok());
578+
assert!(Crate::valid_feature("dep:foo").is_ok());
579+
assert!(Crate::valid_feature("dep:foo?/bar").is_err());
580+
assert!(Crate::valid_feature("foo/?bar").is_err());
581+
assert!(Crate::valid_feature("foo?bar").is_err());
582+
assert!(Crate::valid_feature("bar.web").is_ok());
583+
assert!(Crate::valid_feature("foo/bar.web").is_ok());
584+
assert!(Crate::valid_feature("dep:0foo").is_err());
585+
assert!(Crate::valid_feature("0foo?/bar.web").is_err());
534586
}
535587
}

src/tests/krate/publish/features.rs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,46 @@ fn features_version_2() {
2626
}
2727

2828
#[test]
29-
fn invalid_feature_name() {
29+
fn feature_name_with_dot() {
30+
let (app, _, _, token) = TestApp::full().with_token();
31+
let crate_to_publish = PublishBuilder::new("foo", "1.0.0").feature("foo.bar", &[]);
32+
token.publish_crate(crate_to_publish).good();
33+
let crates = app.crates_from_index_head("foo");
34+
assert_json_snapshot!(crates);
35+
}
36+
37+
#[test]
38+
fn feature_name_start_with_number_and_underscore() {
39+
let (app, _, _, token) = TestApp::full().with_token();
40+
let crate_to_publish = PublishBuilder::new("foo", "1.0.0")
41+
.feature("0foo1.bar", &[])
42+
.feature("_foo2.bar", &[]);
43+
token.publish_crate(crate_to_publish).good();
44+
let crates = app.crates_from_index_head("foo");
45+
assert_json_snapshot!(crates);
46+
}
47+
48+
#[test]
49+
fn feature_name_with_unicode_chars() {
50+
let (app, _, _, token) = TestApp::full().with_token();
51+
let crate_to_publish = PublishBuilder::new("foo", "1.0.0").feature("foo.你好世界", &[]);
52+
token.publish_crate(crate_to_publish).good();
53+
let crates = app.crates_from_index_head("foo");
54+
assert_json_snapshot!(crates);
55+
}
56+
57+
#[test]
58+
fn empty_feature_name() {
59+
let (app, _, _, token) = TestApp::full().with_token();
60+
let crate_to_publish = PublishBuilder::new("foo", "1.0.0").feature("", &[]);
61+
let response = token.publish_crate(crate_to_publish);
62+
assert_eq!(response.status(), StatusCode::OK);
63+
assert_json_snapshot!(response.into_json());
64+
assert!(app.stored_files().is_empty());
65+
}
66+
67+
#[test]
68+
fn invalid_feature_name1() {
3069
let (app, _, _, token) = TestApp::full().with_token();
3170

3271
let crate_to_publish = PublishBuilder::new("foo", "1.0.0").feature("~foo", &[]);
@@ -37,7 +76,7 @@ fn invalid_feature_name() {
3776
}
3877

3978
#[test]
40-
fn invalid_feature() {
79+
fn invalid_feature_name2() {
4180
let (app, _, _, token) = TestApp::full().with_token();
4281

4382
let crate_to_publish = PublishBuilder::new("foo", "1.0.0").feature("foo", &["!bar"]);
@@ -47,6 +86,16 @@ fn invalid_feature() {
4786
assert_that!(app.stored_files(), empty());
4887
}
4988

89+
#[test]
90+
fn invalid_feature_name_start_with_hyphen() {
91+
let (app, _, _, token) = TestApp::full().with_token();
92+
let crate_to_publish = PublishBuilder::new("foo", "1.0.0").feature("-foo1.bar", &[]);
93+
let response = token.publish_crate(crate_to_publish);
94+
assert_eq!(response.status(), StatusCode::OK);
95+
assert_json_snapshot!(response.into_json());
96+
assert!(app.stored_files().is_empty());
97+
}
98+
5099
#[test]
51100
fn too_many_features() {
52101
let (app, _, _, token) = TestApp::full()

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 renamed to src/tests/krate/publish/snapshots/all__krate__publish__features__empty_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": "\"!bar\" is an invalid feature name"
8+
"detail": "feature cannot be an empty"
99
}
1010
]
1111
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
source: src/tests/krate/publish/features.rs
3+
expression: crates
4+
---
5+
[
6+
{
7+
"name": "foo",
8+
"vers": "1.0.0",
9+
"deps": [],
10+
"cksum": "6f73fad556c46cdb740173ccc7a5f5bf64b8d954966be16963a08eb138e3c69c",
11+
"features": {
12+
"0foo1.bar": [],
13+
"_foo2.bar": []
14+
},
15+
"yanked": false
16+
}
17+
]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
source: src/tests/krate/publish/features.rs
3+
expression: crates
4+
---
5+
[
6+
{
7+
"name": "foo",
8+
"vers": "1.0.0",
9+
"deps": [],
10+
"cksum": "d0bfdbcd4905a15b3dc6db5ce23e206ac413b4d780053fd38e145a75197fb1e1",
11+
"features": {
12+
"foo.bar": []
13+
},
14+
"yanked": false
15+
}
16+
]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
source: src/tests/krate/publish/features.rs
3+
expression: crates
4+
---
5+
[
6+
{
7+
"name": "foo",
8+
"vers": "1.0.0",
9+
"deps": [],
10+
"cksum": "493720846371607438c1a4eb90c9cc7d7286600ca9c4e2ca04151aad9563b47a",
11+
"features": {
12+
"foo.你好世界": []
13+
},
14+
"yanked": false
15+
}
16+
]

0 commit comments

Comments
 (0)