Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use the same feature and dep name validation rules from Cargo #7379

Closed
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
45 changes: 6 additions & 39 deletions src/controllers/krate/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -53,14 +52,7 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
let metadata: PublishMetadata = serde_json::from_slice(&json_bytes)
.map_err(|e| cargo_err(&format_args!("invalid upload request: {e}")))?;

if !Crate::valid_name(&metadata.name) {
return Err(cargo_err(&format_args!(
"\"{}\" is an invalid crate name (crate names must start with a \
letter, contain only letters, numbers, hyphens, or underscores and \
have at most {MAX_NAME_LENGTH} characters)",
metadata.name
)));
}
Crate::valid_name(&metadata.name)?;

let version = match semver::Version::parse(&metadata.vers) {
Ok(parsed) => parsed,
Expand Down Expand Up @@ -238,12 +230,7 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
}

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

Crate::valid_feature_name(key)?;
let num_features = values.len();
if num_features > max_features {
return Err(cargo_err(&format!(
Expand All @@ -257,9 +244,7 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
}

for value in values.iter() {
if !Crate::valid_feature(value) {
return Err(cargo_err(&format!("\"{value}\" is an invalid feature name")));
}
Crate::valid_feature(value)?;
}
}

Expand Down Expand Up @@ -587,21 +572,10 @@ fn convert_dependency(
}

pub fn validate_dependency(dep: &EncodableCrateDependency) -> 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 {
Expand All @@ -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(())
Expand Down
213 changes: 136 additions & 77 deletions src/models/krate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,59 +192,112 @@ 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_ident(name, "crate name")
}

fn valid_ident(name: &str) -> bool {
Self::valid_feature_prefix(name)
&& name
.chars()
.next()
.map(char::is_alphabetic)
.unwrap_or(false)
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_ident(name, "dependency 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
}
// Checks that the name is a valid identifier.
// 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 `_`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand why you changed all of these implementations too? I thought the goal of this PR was to adjust the validation rules for feature names, but not for crate names 🤔

Copy link
Member Author

@Rustin170506 Rustin170506 Nov 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember the last meeting, we decided not to allow unicode for crate names, but we can follow the same rules for the validations.

But if you think we should only change the feature name first. I can revert my changes for crate names.

Copy link
Member Author

@Rustin170506 Rustin170506 Nov 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't change the create name validation as well, _a?/feature_name still is invalid in crates.io but it works in Cargo.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think changing the rules on crate names should be out of scope for this PR. That would need a bit more investigation since these are relevant for URLs, S3 paths, and other things where allowing additional characters could have unintended consequences. Let's focus this PR on just the feature names.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense. I will revert it. Thanks!

Copy link
Member Author

@Rustin170506 Rustin170506 Nov 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reverted it in f47425f

I retained the modification of the message as it provides a clearer error message to users.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I retained the modification of the message as it provides a clearer error message to users.

the problem is that this makes the diff a lot harder to review because the PR is doing multiple things at once. (see https://mtlynch.io/code-review-love/#5-narrowly-scope-changes 😉)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the problem is that this makes the diff a lot harder to review because the PR is doing multiple things at once.

Sorry for the big PR. I tried to make sure one commit only had one main change. Could you please review it by commit? The reason it has a really big change is the old validations for create name and dep name also use some feature validation rules(functions). We mixed them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you think it is still really difficult to review. I can split it into three different PRs. Sorry again for the confusing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first one: #7500

fn valid_ident(name: &str, what: &str) -> AppResult<()> {
if name.is_empty() {
return Err(cargo_err(&format!("the {} cannot be an empty", what)));
}
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 {}, \
the name cannot start with a digit",
name, what,
)));
}
if !(ch.is_ascii_alphabetic() || ch == '_') {
return Err(cargo_err(&format!(
"invalid character `{}` in {}: `{}`, \
the first character must be an ASCII character, or `_`",
ch, what, 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)
}
for ch in chars {
if !(ch.is_ascii_alphanumeric() || ch == '-' || ch == '_') {
return Err(cargo_err(&format!(
"invalid character `{}` in {}: `{}`, \
characters must be an ASCII alphanumeric characters, `-`, or `_`",
ch, what, name
)));
}
}

/// 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 == '+')
Ok(())
}

/// 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 == '-')
/// 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,
)));
}
}

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)
}
None => Crate::valid_feature_name(name.strip_prefix("dep:").unwrap_or(name)),
/// 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)
}
}

Expand Down Expand Up @@ -489,47 +542,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_ok());
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());
}
}
2 changes: 1 addition & 1 deletion src/models/token/scopes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions src/tests/krate/publish/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, or `_`"
}
]
}
Loading