diff --git a/Cargo.lock b/Cargo.lock index cac630522ae..8cd46c08fb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -965,6 +965,7 @@ dependencies = [ "tower-service", "tracing", "tracing-subscriber", + "unicode-xid", "url", ] @@ -4261,6 +4262,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "unicode_categories" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 34e700b3c9c..ffc756b7e2c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,7 @@ tower-http = { version = "=0.4.4", features = ["add-extension", "fs", "catch-pan tracing = "=0.1.40" tracing-subscriber = { version = "=0.3.17", features = ["env-filter"] } url = "=2.4.1" +unicode-xid = "0.2.4" [dev-dependencies] bytes = "=1.5.0" diff --git a/src/controllers/krate/publish.rs b/src/controllers/krate/publish.rs index 9e520793de9..4a9f9cea38f 100644 --- a/src/controllers/krate/publish.rs +++ b/src/controllers/krate/publish.rs @@ -16,7 +16,6 @@ use tokio::runtime::Handle; use url::Url; use crate::controllers::cargo_prelude::*; -use crate::models::krate::MAX_NAME_LENGTH; use crate::models::{ insert_version_owner_action, Category, Crate, DependencyKind, Keyword, NewCrate, NewVersion, Rights, VersionAction, @@ -53,14 +52,7 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult parsed, @@ -238,12 +230,7 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult max_features { return Err(cargo_err(&format!( @@ -257,9 +244,7 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult AppResult<()> { - if !Crate::valid_name(&dep.name) { - return Err(cargo_err(&format_args!( - "\"{}\" is an invalid dependency name (dependency names must \ - start with a letter, contain only letters, numbers, hyphens, \ - or underscores and have at most {MAX_NAME_LENGTH} characters)", - dep.name - ))); - } + Crate::valid_name(&dep.name)?; for feature in &dep.features { - if !Crate::valid_feature(feature) { - return Err(cargo_err(&format_args!( - "\"{feature}\" is an invalid feature name", - ))); - } + Crate::valid_feature(feature)?; } if let Some(registry) = &dep.registry { @@ -627,14 +601,7 @@ pub fn validate_dependency(dep: &EncodableCrateDependency) -> AppResult<()> { } if let Some(toml_name) = &dep.explicit_name_in_toml { - if !Crate::valid_dependency_name(toml_name) { - return Err(cargo_err(&format_args!( - "\"{toml_name}\" is an invalid dependency name (dependency \ - names must start with a letter or underscore, contain only \ - letters, numbers, hyphens, or underscores and have at most \ - {MAX_NAME_LENGTH} characters)" - ))); - } + Crate::valid_dependency_name(toml_name)?; } Ok(()) diff --git a/src/models/krate.rs b/src/models/krate.rs index be034d36b7c..37082856fd2 100644 --- a/src/models/krate.rs +++ b/src/models/krate.rs @@ -192,59 +192,152 @@ impl Crate { }) } - pub fn valid_name(name: &str) -> bool { - let under_max_length = name.chars().take(MAX_NAME_LENGTH + 1).count() <= MAX_NAME_LENGTH; - Crate::valid_ident(name) && under_max_length + pub fn valid_name(name: &str) -> AppResult<()> { + if name.chars().count() > MAX_NAME_LENGTH { + return Err(cargo_err(&format!( + "the name `{}` is too long (max {} characters)", + name, MAX_NAME_LENGTH + ))); + } + Crate::valid_create_ident(name) } - fn valid_ident(name: &str) -> bool { - Self::valid_feature_prefix(name) - && name - .chars() - .next() - .map(char::is_alphabetic) - .unwrap_or(false) - } + // Checks that the name is a valid crate name. + // 1. The name must be non-empty. + // 2. The first character must be an ASCII character. + // 3. The remaining characters must be ASCII alphanumerics or `-` or `_`. + // Note: This differs from `valid_dependency_name`, which allows `_` as the first character. + fn valid_create_ident(name: &str) -> AppResult<()> { + if name.is_empty() { + return Err(cargo_err("the crate name cannot be an empty")); + } + let mut chars = name.chars(); + if let Some(ch) = chars.next() { + if ch.is_ascii_digit() { + return Err(cargo_err(&format!( + "the name `{}` cannot be used as a crate name, \ + the name cannot start with a digit", + name, + ))); + } + if !ch.is_ascii_alphabetic() { + return Err(cargo_err(&format!( + "invalid character `{}` in crate name: `{}`, \ + the first character must be an ASCII character", + ch, name + ))); + } + } - pub fn valid_dependency_name(name: &str) -> bool { - let under_max_length = name.chars().take(MAX_NAME_LENGTH + 1).count() <= MAX_NAME_LENGTH; - Crate::valid_dependency_ident(name) && under_max_length - } + for ch in chars { + if !(ch.is_ascii_alphanumeric() || ch == '-' || ch == '_') { + return Err(cargo_err(&format!( + "invalid character `{}` in crate name: `{}`, \ + characters must be an ASCII alphanumeric characters, `-`, or `_`", + ch, name + ))); + } + } - fn valid_dependency_ident(name: &str) -> bool { - Self::valid_feature_prefix(name) - && name - .chars() - .next() - .map(|n| n.is_alphabetic() || n == '_') - .unwrap_or(false) + Ok(()) } - /// Validates the THIS parts of `features = ["THIS", "and/THIS"]`. - pub fn valid_feature_name(name: &str) -> bool { - !name.is_empty() - && name - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '+') + pub fn valid_dependency_name(name: &str) -> AppResult<()> { + if name.chars().count() > MAX_NAME_LENGTH { + return Err(cargo_err(&format!( + "the name `{}` is too long (max {} characters)", + name, MAX_NAME_LENGTH + ))); + } + Crate::valid_dependency_ident(name) } - /// Validates the prefix in front of the slash: `features = ["THIS/feature"]`. - /// Normally this corresponds to the crate name of a dependency. - fn valid_feature_prefix(name: &str) -> bool { - !name.is_empty() - && name - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + // Checks that the name is a valid dependency name. + // 1. The name must be non-empty. + // 2. The first character must be an ASCII character or `_`. + // 3. The remaining characters must be ASCII alphanumerics or `-` or `_`. + fn valid_dependency_ident(name: &str) -> AppResult<()> { + if name.is_empty() { + return Err(cargo_err("the dependency name cannot be an empty")); + } + let mut chars = name.chars(); + if let Some(ch) = chars.next() { + if ch.is_ascii_digit() { + return Err(cargo_err(&format!( + "the name `{}` cannot be used as a dependency name, \ + the name cannot start with a digit", + name, + ))); + } + if !(ch.is_ascii_alphabetic() || ch == '_') { + return Err(cargo_err(&format!( + "invalid character `{}` in dependency name: `{}`, \ + the first character must be an ASCII character, or `_`", + ch, name + ))); + } + } + + for ch in chars { + if !(ch.is_ascii_alphanumeric() || ch == '-' || ch == '_') { + return Err(cargo_err(&format!( + "invalid character `{}` in dependency name: `{}`, \ + characters must be an ASCII alphanumeric characters, `-`, or `_`", + ch, name + ))); + } + } + + Ok(()) } - /// Validates a whole feature string, `features = ["THIS", "ALL/THIS"]`. - pub fn valid_feature(name: &str) -> bool { - match name.split_once('/') { - Some((dep, dep_feat)) => { - let dep = dep.strip_suffix('?').unwrap_or(dep); - Crate::valid_feature_prefix(dep) && Crate::valid_feature_name(dep_feat) + /// Validates the THIS parts of `features = ["THIS", "and/THIS", "dep:THIS", "dep?/THIS"]`. + /// 1. The name must be non-empty. + /// 2. The first character must be a Unicode XID start character, `_`, or a digit. + /// 3. The remaining characters must be Unicode XID characters, `_`, `+`, `-`, or `.`. + pub fn valid_feature_name(name: &str) -> AppResult<()> { + if name.is_empty() { + return Err(cargo_err("feature cannot be an empty")); + } + let mut chars = name.chars(); + if let Some(ch) = chars.next() { + if !(unicode_xid::UnicodeXID::is_xid_start(ch) || ch == '_' || ch.is_ascii_digit()) { + return Err(cargo_err(&format!( + "invalid character `{}` in feature `{}`, \ + the first character must be a Unicode XID start character or digit \ + (most letters or `_` or `0` to `9`)", + ch, name, + ))); + } + } + for ch in chars { + if !(unicode_xid::UnicodeXID::is_xid_continue(ch) + || ch == '+' + || ch == '-' + || ch == '.') + { + return Err(cargo_err(&format!( + "invalid character `{}` in feature `{}`, \ + characters must be Unicode XID characters, `+`, `-`, or `.` \ + (numbers, `+`, `-`, `_`, `.`, or most letters)", + ch, name, + ))); } - None => Crate::valid_feature_name(name.strip_prefix("dep:").unwrap_or(name)), + } + + Ok(()) + } + + /// Validates a whole feature string, `features = ["THIS", "and/THIS", "dep:THIS", "dep?/THIS"]`. + pub fn valid_feature(name: &str) -> AppResult<()> { + if let Some((dep, dep_feat)) = name.split_once('/') { + let dep = dep.strip_suffix('?').unwrap_or(dep); + Crate::valid_dependency_name(dep)?; + Crate::valid_feature_name(dep_feat) + } else if let Some((_, dep)) = name.split_once("dep:") { + Crate::valid_dependency_name(dep) + } else { + Crate::valid_feature_name(name) } } @@ -489,47 +582,53 @@ mod tests { #[test] fn valid_name() { - assert!(Crate::valid_name("foo")); - assert!(!Crate::valid_name("京")); - assert!(!Crate::valid_name("")); - assert!(!Crate::valid_name("💝")); - assert!(Crate::valid_name("foo_underscore")); - assert!(Crate::valid_name("foo-dash")); - assert!(!Crate::valid_name("foo+plus")); - // Starting with an underscore is an invalid crate name. - assert!(!Crate::valid_name("_foo")); - assert!(!Crate::valid_name("-foo")); + assert!(Crate::valid_name("foo").is_ok()); + assert!(Crate::valid_name("京").is_err()); + assert!(Crate::valid_name("").is_err()); + assert!(Crate::valid_name("💝").is_err()); + assert!(Crate::valid_name("foo_underscore").is_ok()); + assert!(Crate::valid_name("foo-dash").is_ok()); + assert!(Crate::valid_name("foo+plus").is_err()); + assert!(Crate::valid_name("_foo").is_err()); + assert!(Crate::valid_name("-foo").is_err()); } #[test] fn valid_dependency_name() { - assert!(Crate::valid_dependency_name("foo")); - assert!(!Crate::valid_dependency_name("京")); - assert!(!Crate::valid_dependency_name("")); - assert!(!Crate::valid_dependency_name("💝")); - assert!(Crate::valid_dependency_name("foo_underscore")); - assert!(Crate::valid_dependency_name("foo-dash")); - assert!(!Crate::valid_dependency_name("foo+plus")); - // Starting with an underscore is a valid dependency name. - assert!(Crate::valid_dependency_name("_foo")); - assert!(!Crate::valid_dependency_name("-foo")); + assert!(Crate::valid_dependency_name("foo").is_ok()); + assert!(Crate::valid_dependency_name("京").is_err()); + assert!(Crate::valid_dependency_name("").is_err()); + assert!(Crate::valid_dependency_name("💝").is_err()); + assert!(Crate::valid_dependency_name("foo_underscore").is_ok()); + assert!(Crate::valid_dependency_name("foo-dash").is_ok()); + assert!(Crate::valid_dependency_name("foo+plus").is_err()); + assert!(Crate::valid_dependency_name("_foo").is_ok()); + assert!(Crate::valid_dependency_name("-foo").is_err()); } #[test] fn valid_feature_names() { - assert!(Crate::valid_feature("foo")); - assert!(!Crate::valid_feature("")); - assert!(!Crate::valid_feature("/")); - assert!(!Crate::valid_feature("%/%")); - assert!(Crate::valid_feature("a/a")); - assert!(Crate::valid_feature("32-column-tables")); - assert!(Crate::valid_feature("c++20")); - assert!(Crate::valid_feature("krate/c++20")); - assert!(!Crate::valid_feature("c++20/wow")); - assert!(Crate::valid_feature("foo?/bar")); - assert!(Crate::valid_feature("dep:foo")); - assert!(!Crate::valid_feature("dep:foo?/bar")); - assert!(!Crate::valid_feature("foo/?bar")); - assert!(!Crate::valid_feature("foo?bar")); + assert!(Crate::valid_feature("foo").is_ok()); + assert!(Crate::valid_feature("1foo").is_ok()); + assert!(Crate::valid_feature("_foo").is_ok()); + assert!(Crate::valid_feature("_foo-_+.1").is_ok()); + assert!(Crate::valid_feature("_foo-_+.1").is_ok()); + assert!(Crate::valid_feature("").is_err()); + assert!(Crate::valid_feature("/").is_err()); + assert!(Crate::valid_feature("%/%").is_err()); + assert!(Crate::valid_feature("a/a").is_ok()); + assert!(Crate::valid_feature("32-column-tables").is_ok()); + assert!(Crate::valid_feature("c++20").is_ok()); + assert!(Crate::valid_feature("krate/c++20").is_ok()); + assert!(Crate::valid_feature("c++20/wow").is_err()); + assert!(Crate::valid_feature("foo?/bar").is_ok()); + assert!(Crate::valid_feature("dep:foo").is_ok()); + assert!(Crate::valid_feature("dep:foo?/bar").is_err()); + assert!(Crate::valid_feature("foo/?bar").is_err()); + assert!(Crate::valid_feature("foo?bar").is_err()); + assert!(Crate::valid_feature("bar.web").is_ok()); + assert!(Crate::valid_feature("foo/bar.web").is_ok()); + assert!(Crate::valid_feature("dep:0foo").is_err()); + assert!(Crate::valid_feature("0foo?/bar.web").is_err()); } } diff --git a/src/models/token/scopes.rs b/src/models/token/scopes.rs index 2a4127c39ca..67c4b94ccb0 100644 --- a/src/models/token/scopes.rs +++ b/src/models/token/scopes.rs @@ -108,7 +108,7 @@ impl CrateScope { } let name_without_wildcard = pattern.strip_suffix('*').unwrap_or(pattern); - Crate::valid_name(name_without_wildcard) + Crate::valid_name(name_without_wildcard).is_ok() } pub fn matches(&self, crate_name: &str) -> bool { diff --git a/src/tests/krate/publish/features.rs b/src/tests/krate/publish/features.rs index 5ba029f9c29..268ccee7c4c 100644 --- a/src/tests/krate/publish/features.rs +++ b/src/tests/krate/publish/features.rs @@ -24,6 +24,15 @@ fn features_version_2() { assert_json_snapshot!(crates); } +#[test] +fn feature_name_with_dot() { + let (app, _, _, token) = TestApp::full().with_token(); + let crate_to_publish = PublishBuilder::new("foo", "1.0.0").feature("foo.bar", &[]); + token.publish_crate(crate_to_publish).good(); + let crates = app.crates_from_index_head("foo"); + assert_json_snapshot!(crates); +} + #[test] fn invalid_feature_name() { let (app, _, _, token) = TestApp::full().with_token(); diff --git a/src/tests/krate/publish/snapshots/all__krate__publish__dependencies__invalid_dependency_name.snap b/src/tests/krate/publish/snapshots/all__krate__publish__dependencies__invalid_dependency_name.snap index 0e8461c375f..a49f9a50b7e 100644 --- a/src/tests/krate/publish/snapshots/all__krate__publish__dependencies__invalid_dependency_name.snap +++ b/src/tests/krate/publish/snapshots/all__krate__publish__dependencies__invalid_dependency_name.snap @@ -5,7 +5,7 @@ expression: response.into_json() { "errors": [ { - "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)" + "detail": "invalid character `🦀` in crate name: `🦀`, the first character must be an ASCII character" } ] } diff --git a/src/tests/krate/publish/snapshots/all__krate__publish__dependencies__invalid_dependency_rename.snap b/src/tests/krate/publish/snapshots/all__krate__publish__dependencies__invalid_dependency_rename.snap index 0fcc4196e97..d4d08413d9e 100644 --- a/src/tests/krate/publish/snapshots/all__krate__publish__dependencies__invalid_dependency_rename.snap +++ b/src/tests/krate/publish/snapshots/all__krate__publish__dependencies__invalid_dependency_rename.snap @@ -5,7 +5,7 @@ expression: response.into_json() { "errors": [ { - "detail": "\"💩\" is an invalid dependency name (dependency names must start with a letter or underscore, contain only letters, numbers, hyphens, or underscores and have at most 64 characters)" + "detail": "invalid character `💩` in dependency name: `💩`, the first character must be an ASCII character, or `_`" } ] } diff --git a/src/tests/krate/publish/snapshots/all__krate__publish__dependencies__invalid_feature_name.snap b/src/tests/krate/publish/snapshots/all__krate__publish__dependencies__invalid_feature_name.snap index dda7e3da25f..f9566fa6ba2 100644 --- a/src/tests/krate/publish/snapshots/all__krate__publish__dependencies__invalid_feature_name.snap +++ b/src/tests/krate/publish/snapshots/all__krate__publish__dependencies__invalid_feature_name.snap @@ -5,7 +5,7 @@ expression: response.into_json() { "errors": [ { - "detail": "\"🍺\" is an invalid feature name" + "detail": "invalid character `🍺` in feature `🍺`, the first character must be a Unicode XID start character or digit (most letters or `_` or `0` to `9`)" } ] } diff --git a/src/tests/krate/publish/snapshots/all__krate__publish__features__feature_name_with_dot.snap b/src/tests/krate/publish/snapshots/all__krate__publish__features__feature_name_with_dot.snap new file mode 100644 index 00000000000..7f63ceefb5e --- /dev/null +++ b/src/tests/krate/publish/snapshots/all__krate__publish__features__feature_name_with_dot.snap @@ -0,0 +1,16 @@ +--- +source: src/tests/krate/publish/features.rs +expression: crates +--- +[ + { + "name": "foo", + "vers": "1.0.0", + "deps": [], + "cksum": "d0bfdbcd4905a15b3dc6db5ce23e206ac413b4d780053fd38e145a75197fb1e1", + "features": { + "foo.bar": [] + }, + "yanked": false + } +] diff --git a/src/tests/krate/publish/snapshots/all__krate__publish__features__invalid_feature.snap b/src/tests/krate/publish/snapshots/all__krate__publish__features__invalid_feature.snap index 8abeeb76659..8d5ba6c9916 100644 --- a/src/tests/krate/publish/snapshots/all__krate__publish__features__invalid_feature.snap +++ b/src/tests/krate/publish/snapshots/all__krate__publish__features__invalid_feature.snap @@ -5,7 +5,7 @@ expression: response.into_json() { "errors": [ { - "detail": "\"!bar\" is an invalid feature name" + "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`)" } ] } diff --git a/src/tests/krate/publish/snapshots/all__krate__publish__features__invalid_feature_name.snap b/src/tests/krate/publish/snapshots/all__krate__publish__features__invalid_feature_name.snap index 1f5b30cc585..3df54b3eb26 100644 --- a/src/tests/krate/publish/snapshots/all__krate__publish__features__invalid_feature_name.snap +++ b/src/tests/krate/publish/snapshots/all__krate__publish__features__invalid_feature_name.snap @@ -5,7 +5,7 @@ expression: response.into_json() { "errors": [ { - "detail": "\"~foo\" is an invalid feature name (feature names must contain only letters, numbers, '-', '+', or '_')" + "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`)" } ] } diff --git a/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-2.snap b/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-2.snap index a550286d9af..1ebd0bfd064 100644 --- a/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-2.snap +++ b/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-2.snap @@ -5,7 +5,7 @@ expression: response.into_json() { "errors": [ { - "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)" + "detail": "invalid character ` ` in crate name: `foo bar`, characters must be an ASCII alphanumeric characters, `-`, or `_`" } ] } diff --git a/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-3.snap b/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-3.snap index 4c5037fb9e7..75fc2ec30a2 100644 --- a/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-3.snap +++ b/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-3.snap @@ -5,7 +5,7 @@ expression: response.into_json() { "errors": [ { - "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)" + "detail": "the name `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa` is too long (max 64 characters)" } ] } diff --git a/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-4.snap b/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-4.snap index e089171f337..9d8c103d692 100644 --- a/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-4.snap +++ b/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-4.snap @@ -5,7 +5,7 @@ expression: response.into_json() { "errors": [ { - "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)" + "detail": "invalid character `☃` in crate name: `snow☃`, characters must be an ASCII alphanumeric characters, `-`, or `_`" } ] } diff --git a/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-5.snap b/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-5.snap index 0975911a29f..f0e3553b45c 100644 --- a/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-5.snap +++ b/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names-5.snap @@ -5,7 +5,7 @@ expression: response.into_json() { "errors": [ { - "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)" + "detail": "invalid character `á` in crate name: `áccênts`, the first character must be an ASCII character" } ] } diff --git a/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names.snap b/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names.snap index 895e92e797f..89555ae1a8e 100644 --- a/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names.snap +++ b/src/tests/krate/publish/snapshots/all__krate__publish__validation__invalid_names.snap @@ -5,7 +5,7 @@ expression: response.into_json() { "errors": [ { - "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)" + "detail": "the crate name cannot be an empty" } ] }