Skip to content

Commit fc5dbcf

Browse files
authored
Merge pull request #7537 from Turbo87/feature-validation
models/krate: Improve feature validation
2 parents d05d209 + 0e0feda commit fc5dbcf

File tree

3 files changed

+95
-56
lines changed

3 files changed

+95
-56
lines changed

src/controllers/krate/publish.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use tokio::runtime::Handle;
1616
use url::Url;
1717

1818
use crate::controllers::cargo_prelude::*;
19-
use crate::models::krate::MAX_NAME_LENGTH;
19+
use crate::models::krate::{InvalidDependencyName, MAX_NAME_LENGTH};
2020
use crate::models::{
2121
insert_version_owner_action, Category, Crate, DependencyKind, Keyword, NewCrate, NewVersion,
2222
Rights, VersionAction,
@@ -238,7 +238,7 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
238238
}
239239

240240
for (key, values) in features.iter() {
241-
Crate::valid_feature_name(key)?;
241+
Crate::validate_feature_name(key).map_err(|error| cargo_err(&error))?;
242242

243243
let num_features = values.len();
244244
if num_features > max_features {
@@ -253,7 +253,7 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
253253
}
254254

255255
for value in values.iter() {
256-
Crate::valid_feature(value)?;
256+
Crate::validate_feature(value).map_err(|error| cargo_err(&error))?;
257257
}
258258
}
259259

@@ -596,7 +596,7 @@ pub fn validate_dependency(dep: &EncodableCrateDependency) -> AppResult<()> {
596596
}
597597

598598
for feature in &dep.features {
599-
Crate::valid_feature(feature)?;
599+
Crate::validate_feature(feature).map_err(|error| cargo_err(&error))?;
600600
}
601601

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

624624
if let Some(toml_name) = &dep.explicit_name_in_toml {
625625
if !Crate::valid_dependency_name(toml_name) {
626-
return Err(cargo_err(&Crate::invalid_dependency_name_msg(toml_name)));
626+
return Err(cargo_err(&InvalidDependencyName(toml_name.into())));
627627
}
628628
}
629629

src/models/krate.rs

Lines changed: 89 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -220,32 +220,18 @@ impl Crate {
220220
.unwrap_or(false)
221221
}
222222

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-
232223
/// Validates the THIS parts of `features = ["THIS", "and/THIS", "dep:THIS", "dep?/THIS"]`.
233224
/// 1. The name must be non-empty.
234225
/// 2. The first character must be a Unicode XID start character, `_`, or a digit.
235226
/// 3. The remaining characters must be Unicode XID characters, `_`, `+`, `-`, or `.`.
236-
pub fn valid_feature_name(name: &str) -> AppResult<()> {
227+
pub fn validate_feature_name(name: &str) -> Result<(), InvalidFeature> {
237228
if name.is_empty() {
238-
return Err(cargo_err("feature cannot be an empty"));
229+
return Err(InvalidFeature::Empty);
239230
}
240231
let mut chars = name.chars();
241232
if let Some(ch) = chars.next() {
242233
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-
)));
234+
return Err(InvalidFeature::Start(ch, name.into()));
249235
}
250236
}
251237
for ch in chars {
@@ -254,33 +240,30 @@ impl Crate {
254240
|| ch == '-'
255241
|| ch == '.')
256242
{
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-
)));
243+
return Err(InvalidFeature::Char(ch, name.into()));
263244
}
264245
}
265246

266247
Ok(())
267248
}
268249

269250
/// Validates a whole feature string, `features = ["THIS", "and/THIS", "dep:THIS", "dep?/THIS"]`.
270-
pub fn valid_feature(name: &str) -> AppResult<()> {
251+
pub fn validate_feature(name: &str) -> Result<(), InvalidFeature> {
271252
if let Some((dep, dep_feat)) = name.split_once('/') {
272253
let dep = dep.strip_suffix('?').unwrap_or(dep);
273254
if !Crate::valid_dependency_name(dep) {
274-
return Err(cargo_err(&Crate::invalid_dependency_name_msg(dep)));
255+
let err = InvalidDependencyName(dep.into());
256+
return Err(InvalidFeature::DependencyName(err));
275257
}
276-
Crate::valid_feature_name(dep_feat)
258+
Crate::validate_feature_name(dep_feat)
277259
} else if let Some((_, dep)) = name.split_once("dep:") {
278260
if !Crate::valid_dependency_name(dep) {
279-
return Err(cargo_err(&Crate::invalid_dependency_name_msg(dep)));
261+
let err = InvalidDependencyName(dep.into());
262+
return Err(InvalidFeature::DependencyName(err));
280263
}
281264
return Ok(());
282265
} else {
283-
Crate::valid_feature_name(name)
266+
Crate::validate_feature_name(name)
284267
}
285268
}
286269

@@ -528,6 +511,34 @@ impl CrateVersions for [Crate] {
528511
}
529512
}
530513

514+
#[derive(Debug, Eq, PartialEq, thiserror::Error)]
515+
pub enum InvalidFeature {
516+
#[error("feature cannot be empty")]
517+
Empty,
518+
#[error(
519+
"invalid character `{0}` in feature `{1}`, the first character must be \
520+
a Unicode XID start character or digit (most letters or `_` or `0` to \
521+
`9`)"
522+
)]
523+
Start(char, String),
524+
#[error(
525+
"invalid character `{0}` in feature `{1}`, characters must be Unicode \
526+
XID characters, `+`, `-`, or `.` (numbers, `+`, `-`, `_`, `.`, or most \
527+
letters)"
528+
)]
529+
Char(char, String),
530+
#[error(transparent)]
531+
DependencyName(#[from] InvalidDependencyName),
532+
}
533+
534+
#[derive(Debug, Eq, PartialEq, thiserror::Error)]
535+
#[error(
536+
"\"{0}\" is an invalid dependency name (dependency names must start with a \
537+
letter or underscore, contain only letters, numbers, hyphens, or \
538+
underscores and have at most {MAX_NAME_LENGTH} characters)"
539+
)]
540+
pub struct InvalidDependencyName(pub String);
541+
531542
#[cfg(test)]
532543
mod tests {
533544
use crate::models::Crate;
@@ -559,29 +570,57 @@ mod tests {
559570
assert!(Crate::valid_dependency_name("_foo"));
560571
assert!(!Crate::valid_dependency_name("-foo"));
561572
}
573+
562574
#[test]
563575
fn valid_feature_names() {
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());
576+
use super::InvalidDependencyName;
577+
use super::InvalidFeature::*;
578+
579+
assert_ok!(Crate::validate_feature("foo"));
580+
assert_ok!(Crate::validate_feature("1foo"));
581+
assert_ok!(Crate::validate_feature("_foo"));
582+
assert_ok!(Crate::validate_feature("_foo-_+.1"));
583+
assert_ok!(Crate::validate_feature("_foo-_+.1"));
584+
assert_err_eq!(Crate::validate_feature(""), Empty);
585+
assert_err_eq!(
586+
Crate::validate_feature("/"),
587+
InvalidDependencyName("".into()).into()
588+
);
589+
assert_err_eq!(
590+
Crate::validate_feature("%/%"),
591+
InvalidDependencyName("%".into()).into()
592+
);
593+
assert_ok!(Crate::validate_feature("a/a"));
594+
assert_ok!(Crate::validate_feature("32-column-tables"));
595+
assert_ok!(Crate::validate_feature("c++20"));
596+
assert_ok!(Crate::validate_feature("krate/c++20"));
597+
assert_err_eq!(
598+
Crate::validate_feature("c++20/wow"),
599+
InvalidDependencyName("c++20".into()).into()
600+
);
601+
assert_ok!(Crate::validate_feature("foo?/bar"));
602+
assert_ok!(Crate::validate_feature("dep:foo"));
603+
assert_err_eq!(
604+
Crate::validate_feature("dep:foo?/bar"),
605+
InvalidDependencyName("dep:foo".into()).into()
606+
);
607+
assert_err_eq!(
608+
Crate::validate_feature("foo/?bar"),
609+
Start('?', "?bar".into())
610+
);
611+
assert_err_eq!(
612+
Crate::validate_feature("foo?bar"),
613+
Char('?', "foo?bar".into())
614+
);
615+
assert_ok!(Crate::validate_feature("bar.web"));
616+
assert_ok!(Crate::validate_feature("foo/bar.web"));
617+
assert_err_eq!(
618+
Crate::validate_feature("dep:0foo"),
619+
InvalidDependencyName("0foo".into()).into()
620+
);
621+
assert_err_eq!(
622+
Crate::validate_feature("0foo?/bar.web"),
623+
InvalidDependencyName("0foo".into()).into()
624+
);
586625
}
587626
}

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": "feature cannot be an empty"
8+
"detail": "feature cannot be empty"
99
}
1010
]
1111
}

0 commit comments

Comments
 (0)