Skip to content

Commit 0fdf754

Browse files
authored
Add better error message for invalid crate names (#7603)
1 parent 0297b78 commit 0fdf754

9 files changed

+157
-54
lines changed

Diff for: src/controllers/krate/publish.rs

+2-17
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ use tokio::runtime::Handle;
1616
use url::Url;
1717

1818
use crate::controllers::cargo_prelude::*;
19-
use crate::models::krate::MAX_NAME_LENGTH;
2019
use crate::models::{
2120
insert_version_owner_action, Category, Crate, DependencyKind, Keyword, NewCrate, NewVersion,
2221
Rights, VersionAction,
@@ -53,14 +52,7 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
5352
let metadata: PublishMetadata = serde_json::from_slice(&json_bytes)
5453
.map_err(|e| cargo_err(format_args!("invalid upload request: {e}")))?;
5554

56-
if !Crate::valid_name(&metadata.name) {
57-
return Err(cargo_err(format_args!(
58-
"\"{}\" is an invalid crate name (crate names must start with a \
59-
letter, contain only letters, numbers, hyphens, or underscores and \
60-
have at most {MAX_NAME_LENGTH} characters)",
61-
metadata.name
62-
)));
63-
}
55+
Crate::validate_crate_name("crate", &metadata.name).map_err(cargo_err)?;
6456

6557
let version = match semver::Version::parse(&metadata.vers) {
6658
Ok(parsed) => parsed,
@@ -594,14 +586,7 @@ fn convert_dependency(
594586
}
595587

596588
pub fn validate_dependency(dep: &EncodableCrateDependency) -> AppResult<()> {
597-
if !Crate::valid_name(&dep.name) {
598-
return Err(cargo_err(format_args!(
599-
"\"{}\" is an invalid dependency name (dependency names must \
600-
start with a letter, contain only letters, numbers, hyphens, \
601-
or underscores and have at most {MAX_NAME_LENGTH} characters)",
602-
dep.name
603-
)));
604-
}
589+
Crate::validate_crate_name("dependency", &dep.name).map_err(cargo_err)?;
605590

606591
for feature in &dep.features {
607592
Crate::validate_feature(feature).map_err(cargo_err)?;

Diff for: src/models/krate.rs

+148-30
Original file line numberDiff line numberDiff line change
@@ -196,18 +196,59 @@ impl Crate {
196196
})
197197
}
198198

199-
pub fn valid_name(name: &str) -> bool {
200-
let under_max_length = name.chars().take(MAX_NAME_LENGTH + 1).count() <= MAX_NAME_LENGTH;
201-
Crate::valid_ident(name) && under_max_length
199+
// Validates the name is a valid crate name.
200+
// This is also used for validating the name of dependencies.
201+
// So the `for_what` parameter is used to indicate what the name is used for.
202+
// It can be "crate" or "dependency".
203+
pub fn validate_crate_name(for_what: &str, name: &str) -> Result<(), InvalidCrateName> {
204+
if name.chars().count() > MAX_NAME_LENGTH {
205+
return Err(InvalidCrateName::TooLong {
206+
what: for_what.into(),
207+
name: name.into(),
208+
});
209+
}
210+
Crate::validate_create_ident(for_what, name)
202211
}
203212

204-
fn valid_ident(name: &str) -> bool {
205-
Self::valid_feature_prefix(name)
206-
&& name
207-
.chars()
208-
.next()
209-
.map(char::is_alphabetic)
210-
.unwrap_or(false)
213+
// Checks that the name is a valid crate name.
214+
// 1. The name must be non-empty.
215+
// 2. The first character must be an ASCII character.
216+
// 3. The remaining characters must be ASCII alphanumerics or `-` or `_`.
217+
// Note: This differs from `valid_dependency_name`, which allows `_` as the first character.
218+
fn validate_create_ident(for_what: &str, name: &str) -> Result<(), InvalidCrateName> {
219+
if name.is_empty() {
220+
return Err(InvalidCrateName::Empty {
221+
what: for_what.into(),
222+
});
223+
}
224+
let mut chars = name.chars();
225+
if let Some(ch) = chars.next() {
226+
if ch.is_ascii_digit() {
227+
return Err(InvalidCrateName::StartWithDigit {
228+
what: for_what.into(),
229+
name: name.into(),
230+
});
231+
}
232+
if !ch.is_ascii_alphabetic() {
233+
return Err(InvalidCrateName::Start {
234+
first_char: ch,
235+
what: for_what.into(),
236+
name: name.into(),
237+
});
238+
}
239+
}
240+
241+
for ch in chars {
242+
if !(ch.is_ascii_alphanumeric() || ch == '-' || ch == '_') {
243+
return Err(InvalidCrateName::Char {
244+
ch,
245+
what: for_what.into(),
246+
name: name.into(),
247+
});
248+
}
249+
}
250+
251+
Ok(())
211252
}
212253

213254
pub fn validate_dependency_name(name: &str) -> Result<(), InvalidDependencyName> {
@@ -285,15 +326,6 @@ impl Crate {
285326
}
286327
}
287328

288-
/// Validates the prefix in front of the slash: `features = ["THIS/feature"]`.
289-
/// Normally this corresponds to the crate name of a dependency.
290-
fn valid_feature_prefix(name: &str) -> bool {
291-
!name.is_empty()
292-
&& name
293-
.chars()
294-
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
295-
}
296-
297329
/// Return both the newest (most recently updated) and
298330
/// highest version (in semver order) for the current crate.
299331
pub fn top_versions(&self, conn: &mut PgConnection) -> QueryResult<TopVersions> {
@@ -549,6 +581,37 @@ pub enum InvalidFeature {
549581
DependencyName(#[from] InvalidDependencyName),
550582
}
551583

584+
#[derive(Debug, Eq, PartialEq, thiserror::Error)]
585+
pub enum InvalidCrateName {
586+
#[error("the {what} name `{name}` is too long (max {MAX_NAME_LENGTH} characters)")]
587+
TooLong { what: String, name: String },
588+
#[error("{what} name cannot be empty")]
589+
Empty { what: String },
590+
#[error(
591+
"the name `{name}` cannot be used as a {what} name, \
592+
the name cannot start with a digit"
593+
)]
594+
StartWithDigit { what: String, name: String },
595+
#[error(
596+
"invalid character `{first_char}` in {what} name: `{name}`, \
597+
the first character must be an ASCII character"
598+
)]
599+
Start {
600+
first_char: char,
601+
what: String,
602+
name: String,
603+
},
604+
#[error(
605+
"invalid character `{ch}` in {what} name: `{name}`, \
606+
characters must be an ASCII alphanumeric characters, `-`, or `_`"
607+
)]
608+
Char {
609+
ch: char,
610+
what: String,
611+
name: String,
612+
},
613+
}
614+
552615
#[derive(Debug, Eq, PartialEq, thiserror::Error)]
553616
pub enum InvalidDependencyName {
554617
#[error("the dependency name `{0}` is too long (max {MAX_NAME_LENGTH} characters)")]
@@ -577,17 +640,72 @@ mod tests {
577640
use crate::models::Crate;
578641

579642
#[test]
580-
fn valid_name() {
581-
assert!(Crate::valid_name("foo"));
582-
assert!(!Crate::valid_name("京"));
583-
assert!(!Crate::valid_name(""));
584-
assert!(!Crate::valid_name("💝"));
585-
assert!(Crate::valid_name("foo_underscore"));
586-
assert!(Crate::valid_name("foo-dash"));
587-
assert!(!Crate::valid_name("foo+plus"));
588-
// Starting with an underscore is an invalid crate name.
589-
assert!(!Crate::valid_name("_foo"));
590-
assert!(!Crate::valid_name("-foo"));
643+
fn validate_crate_name() {
644+
use super::{InvalidCrateName, MAX_NAME_LENGTH};
645+
646+
assert_ok!(Crate::validate_crate_name("crate", "foo"));
647+
assert_err_eq!(
648+
Crate::validate_crate_name("crate", "京"),
649+
InvalidCrateName::Start {
650+
first_char: '京',
651+
what: "crate".into(),
652+
name: "京".into()
653+
}
654+
);
655+
assert_err_eq!(
656+
Crate::validate_crate_name("crate", ""),
657+
InvalidCrateName::Empty {
658+
what: "crate".into()
659+
}
660+
);
661+
assert_err_eq!(
662+
Crate::validate_crate_name("crate", "💝"),
663+
InvalidCrateName::Start {
664+
first_char: '💝',
665+
what: "crate".into(),
666+
name: "💝".into()
667+
}
668+
);
669+
assert_ok!(Crate::validate_crate_name("crate", "foo_underscore"));
670+
assert_ok!(Crate::validate_crate_name("crate", "foo-dash"));
671+
assert_err_eq!(
672+
Crate::validate_crate_name("crate", "foo+plus"),
673+
InvalidCrateName::Char {
674+
ch: '+',
675+
what: "crate".into(),
676+
name: "foo+plus".into()
677+
}
678+
);
679+
assert_err_eq!(
680+
Crate::validate_crate_name("crate", "_foo"),
681+
InvalidCrateName::Start {
682+
first_char: '_',
683+
what: "crate".into(),
684+
name: "_foo".into()
685+
}
686+
);
687+
assert_err_eq!(
688+
Crate::validate_crate_name("crate", "-foo"),
689+
InvalidCrateName::Start {
690+
first_char: '-',
691+
what: "crate".into(),
692+
name: "-foo".into()
693+
}
694+
);
695+
assert_err_eq!(
696+
Crate::validate_crate_name("crate", "123"),
697+
InvalidCrateName::StartWithDigit {
698+
what: "crate".into(),
699+
name: "123".into()
700+
}
701+
);
702+
assert_err_eq!(
703+
Crate::validate_crate_name("crate", "o".repeat(MAX_NAME_LENGTH + 1).as_str()),
704+
InvalidCrateName::TooLong {
705+
what: "crate".into(),
706+
name: "o".repeat(MAX_NAME_LENGTH + 1).as_str().into()
707+
}
708+
);
591709
}
592710

593711
#[test]

Diff for: src/models/token/scopes.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ impl CrateScope {
108108
}
109109

110110
let name_without_wildcard = pattern.strip_suffix('*').unwrap_or(pattern);
111-
Crate::valid_name(name_without_wildcard)
111+
Crate::validate_crate_name("crate", name_without_wildcard).is_ok()
112112
}
113113

114114
pub fn matches(&self, crate_name: &str) -> bool {

Diff for: src/tests/krate/publish/snapshots/all__krate__publish__dependencies__invalid_dependency_name.snap

+1-1
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 dependency name (dependency names must start with a letter, contain only letters, numbers, hyphens, or underscores and have at most 64 characters)"
8+
"detail": "invalid character `🦀` in dependency name: `🦀`, the first character must be an ASCII character"
99
}
1010
]
1111
}

Diff for: src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-2.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ expression: response.into_json()
55
{
66
"errors": [
77
{
8-
"detail": "\"foo bar\" is an invalid crate name (crate names must start with a letter, contain only letters, numbers, hyphens, or underscores and have at most 64 characters)"
8+
"detail": "invalid character ` ` in crate name: `foo bar`, characters must be an ASCII alphanumeric characters, `-`, or `_`"
99
}
1010
]
1111
}

Diff for: src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-3.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ expression: response.into_json()
55
{
66
"errors": [
77
{
8-
"detail": "\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\" is an invalid crate name (crate names must start with a letter, contain only letters, numbers, hyphens, or underscores and have at most 64 characters)"
8+
"detail": "the crate name `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` is too long (max 64 characters)"
99
}
1010
]
1111
}

Diff for: src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-4.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ expression: response.into_json()
55
{
66
"errors": [
77
{
8-
"detail": "\"snow☃\" is an invalid crate name (crate names must start with a letter, contain only letters, numbers, hyphens, or underscores and have at most 64 characters)"
8+
"detail": "invalid character `☃` in crate name: `snow☃`, characters must be an ASCII alphanumeric characters, `-`, or `_`"
99
}
1010
]
1111
}

Diff for: src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-5.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ expression: response.into_json()
55
{
66
"errors": [
77
{
8-
"detail": "\"áccênts\" is an invalid crate name (crate names must start with a letter, contain only letters, numbers, hyphens, or underscores and have at most 64 characters)"
8+
"detail": "invalid character `á` in crate name: `áccênts`, the first character must be an ASCII character"
99
}
1010
]
1111
}

Diff for: src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names.snap

+1-1
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 crate name (crate names must start with a letter, contain only letters, numbers, hyphens, or underscores and have at most 64 characters)"
8+
"detail": "crate name cannot be empty"
99
}
1010
]
1111
}

0 commit comments

Comments
 (0)