Skip to content

Commit 0ad1d9d

Browse files
authored
Merge pull request #7234 from Turbo87/validate-dependencies
publish: Move dependency validation to `validate_dependency()` fn
2 parents 79f2ff6 + c2001f9 commit 0ad1d9d

9 files changed

+134
-113
lines changed

src/controllers/krate/publish.rs

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use tokio::runtime::Handle;
1414
use url::Url;
1515

1616
use crate::controllers::cargo_prelude::*;
17+
use crate::models::krate::MAX_NAME_LENGTH;
1718
use crate::models::{
1819
insert_version_owner_action, Category, Crate, DependencyKind, Keyword, NewCrate, NewVersion,
1920
Rights, VersionAction,
@@ -283,6 +284,10 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
283284
VersionAction::Publish,
284285
)?;
285286

287+
for dep in &metadata.deps {
288+
validate_dependency(dep)?;
289+
}
290+
286291
// Link this new version to all dependencies
287292
add_dependencies(conn, &metadata.deps, version.id)?;
288293

@@ -429,6 +434,60 @@ fn missing_metadata_error_message(missing: &[&str]) -> String {
429434
)
430435
}
431436

437+
pub fn validate_dependency(dep: &EncodableCrateDependency) -> AppResult<()> {
438+
if !Crate::valid_name(&dep.name) {
439+
return Err(cargo_err(&format_args!(
440+
"\"{}\" is an invalid dependency name (dependency names must \
441+
start with a letter, contain only letters, numbers, hyphens, \
442+
or underscores and have at most {MAX_NAME_LENGTH} characters)",
443+
dep.name
444+
)));
445+
}
446+
447+
for feature in &dep.features {
448+
if !Crate::valid_feature(feature) {
449+
return Err(cargo_err(&format_args!(
450+
"\"{feature}\" is an invalid feature name",
451+
)));
452+
}
453+
}
454+
455+
if let Some(registry) = &dep.registry {
456+
if !registry.is_empty() {
457+
return Err(cargo_err(&format_args!("Dependency `{}` is hosted on another registry. Cross-registry dependencies are not permitted on crates.io.", dep.name)));
458+
}
459+
}
460+
461+
match semver::VersionReq::parse(&dep.version_req) {
462+
Err(_) => {
463+
return Err(cargo_err(&format_args!(
464+
"\"{}\" is an invalid version requirement",
465+
dep.version_req
466+
)));
467+
}
468+
Ok(req) if req == semver::VersionReq::STAR => {
469+
return Err(cargo_err(&format_args!("wildcard (`*`) dependency constraints are not allowed \
470+
on crates.io. Crate with this problem: `{}` See https://doc.rust-lang.org/cargo/faq.html#can-\
471+
libraries-use--as-a-version-for-their-dependencies for more \
472+
information", dep.name)));
473+
}
474+
_ => {}
475+
}
476+
477+
if let Some(toml_name) = &dep.explicit_name_in_toml {
478+
if !Crate::valid_dependency_name(toml_name) {
479+
return Err(cargo_err(&format_args!(
480+
"\"{toml_name}\" is an invalid dependency name (dependency \
481+
names must start with a letter or underscore, contain only \
482+
letters, numbers, hyphens, or underscores and have at most \
483+
{MAX_NAME_LENGTH} characters)"
484+
)));
485+
}
486+
}
487+
488+
Ok(())
489+
}
490+
432491
#[instrument(skip_all)]
433492
pub fn add_dependencies(
434493
conn: &mut PgConnection,
@@ -439,33 +498,21 @@ pub fn add_dependencies(
439498

440499
let crate_ids = crates::table
441500
.select((crates::name, crates::id))
442-
.filter(crates::name.eq_any(deps.iter().map(|d| &d.name.0)))
501+
.filter(crates::name.eq_any(deps.iter().map(|d| &d.name)))
443502
.load_iter::<(String, i32), DefaultLoadingMode>(conn)?
444503
.collect::<QueryResult<HashMap<_, _>>>()?;
445504

446505
let new_dependencies = deps
447506
.iter()
448507
.map(|dep| {
449-
if let Some(registry) = &dep.registry {
450-
if !registry.is_empty() {
451-
return Err(cargo_err(&format_args!("Dependency `{}` is hosted on another registry. Cross-registry dependencies are not permitted on crates.io.", &*dep.name)));
452-
}
453-
}
454-
455508
// Match only identical names to ensure the index always references the original crate name
456-
let Some(&crate_id) = crate_ids.get(&dep.name.0) else {
457-
return Err(cargo_err(&format_args!("no known crate named `{}`", &*dep.name)));
509+
let Some(&crate_id) = crate_ids.get(&dep.name) else {
510+
return Err(cargo_err(&format_args!(
511+
"no known crate named `{}`",
512+
dep.name
513+
)));
458514
};
459515

460-
if let Ok(version_req) = semver::VersionReq::parse(&dep.version_req.0) {
461-
if version_req == semver::VersionReq::STAR {
462-
return Err(cargo_err(&format_args!("wildcard (`*`) dependency constraints are not allowed \
463-
on crates.io. Crate with this problem: `{}` See https://doc.rust-lang.org/cargo/faq.html#can-\
464-
libraries-use--as-a-version-for-their-dependencies for more \
465-
information", &*dep.name)));
466-
}
467-
}
468-
469516
Ok((
470517
dependencies::version_id.eq(version_id),
471518
dependencies::crate_id.eq(crate_id),
@@ -475,7 +522,7 @@ pub fn add_dependencies(
475522
dependencies::default_features.eq(dep.default_features),
476523
dependencies::features.eq(&dep.features),
477524
dependencies::target.eq(dep.target.as_deref()),
478-
dependencies::explicit_name.eq(dep.explicit_name_in_toml.as_deref())
525+
dependencies::explicit_name.eq(dep.explicit_name_in_toml.as_deref()),
479526
))
480527
})
481528
.collect::<Result<Vec<_>, _>>()?;

src/tests/builders/dependency.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ use crates_io::views::krate_publish as u;
22

33
/// A builder for constructing a dependency of another crate.
44
pub struct DependencyBuilder {
5-
explicit_name_in_toml: Option<u::EncodableDependencyName>,
5+
explicit_name_in_toml: Option<String>,
66
name: String,
7+
features: Vec<String>,
78
registry: Option<String>,
8-
version_req: u::EncodableCrateVersionReq,
9+
version_req: String,
910
}
1011

1112
impl DependencyBuilder {
@@ -14,14 +15,15 @@ impl DependencyBuilder {
1415
DependencyBuilder {
1516
explicit_name_in_toml: None,
1617
name: name.to_string(),
18+
features: vec![],
1719
registry: None,
18-
version_req: u::EncodableCrateVersionReq("> 0".to_string()),
20+
version_req: "> 0".to_string(),
1921
}
2022
}
2123

2224
/// Rename this dependency.
2325
pub fn rename(mut self, new_name: &str) -> Self {
24-
self.explicit_name_in_toml = Some(u::EncodableDependencyName(new_name.to_string()));
26+
self.explicit_name_in_toml = Some(new_name.to_string());
2527
self
2628
}
2729

@@ -38,18 +40,23 @@ impl DependencyBuilder {
3840
/// Panics if the `version_req` string specified isn't a valid `semver::VersionReq`.
3941
#[track_caller]
4042
pub fn version_req(mut self, version_req: &str) -> Self {
41-
self.version_req = u::EncodableCrateVersionReq(version_req.to_string());
43+
self.version_req = version_req.to_string();
44+
self
45+
}
46+
47+
pub fn add_feature<T: Into<String>>(mut self, feature: T) -> Self {
48+
self.features.push(feature.into());
4249
self
4350
}
4451

4552
/// Consume this builder to create a `u::CrateDependency`. If the dependent crate doesn't
4653
/// already exist, publishing a crate with this dependency will fail.
4754
pub fn build(self) -> u::EncodableCrateDependency {
4855
u::EncodableCrateDependency {
49-
name: u::EncodableCrateName(self.name),
56+
name: self.name,
5057
optional: false,
5158
default_features: true,
52-
features: Vec::new(),
59+
features: self.features,
5360
version_req: self.version_req,
5461
target: None,
5562
kind: None,

src/tests/builders/publish.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -236,17 +236,10 @@ fn convert_dependency(
236236
),
237237
};
238238

239-
let features = encoded
240-
.features
241-
.iter()
242-
.map(|f| f.to_string())
243-
.collect::<Vec<_>>()
244-
.none_or_filled();
245-
246239
let dependency = DependencyDetail {
247240
version: Some(encoded.version_req.to_string()),
248241
registry: encoded.registry.clone(),
249-
features,
242+
features: encoded.features.clone().none_or_filled(),
250243
optional: match encoded.optional {
251244
true => Some(true),
252245
false => None,

src/tests/krate/publish/dependencies.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ use crate::util::{RequestHelper, TestApp};
33
use http::StatusCode;
44
use insta::assert_json_snapshot;
55

6+
#[test]
7+
fn invalid_dependency_name() {
8+
let (app, _, _, token) = TestApp::full().with_token();
9+
10+
let response = token.publish_crate(
11+
PublishBuilder::new("foo", "1.0.0").dependency(DependencyBuilder::new("🦀")),
12+
);
13+
assert_eq!(response.status(), StatusCode::OK);
14+
assert_json_snapshot!(response.into_json());
15+
assert!(app.stored_files().is_empty());
16+
}
17+
618
#[test]
719
fn new_with_renamed_dependency() {
820
let (app, _, user, token) = TestApp::full().with_token();
@@ -211,3 +223,16 @@ fn new_krate_sorts_deps() {
211223
let crates = app.crates_from_index_head("two-deps");
212224
assert_json_snapshot!(crates);
213225
}
226+
227+
#[test]
228+
fn invalid_feature_name() {
229+
let (app, _, _, token) = TestApp::full().with_token();
230+
231+
let response = token.publish_crate(
232+
PublishBuilder::new("foo", "1.0.0")
233+
.dependency(DependencyBuilder::new("bar").add_feature("🍺")),
234+
);
235+
assert_eq!(response.status(), StatusCode::OK);
236+
assert_json_snapshot!(response.into_json());
237+
assert!(app.stored_files().is_empty());
238+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
source: src/tests/krate/publish/dependencies.rs
3+
expression: response.into_json()
4+
---
5+
{
6+
"errors": [
7+
{
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)"
9+
}
10+
]
11+
}

src/tests/krate/publish/snapshots/all__krate__publish__dependencies__invalid_dependency_rename.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": "invalid upload request: invalid value: string \"💩\", expected a valid dependency name to start with a letter or underscore, contain only letters, numbers, hyphens, or underscores and have at most 64 characters at line 1 column 197"
8+
"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)"
99
}
1010
]
1111
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
source: src/tests/krate/publish/dependencies.rs
3+
expression: response.into_json()
4+
---
5+
{
6+
"errors": [
7+
{
8+
"detail": "\"🍺\" is an invalid feature name"
9+
}
10+
]
11+
}

src/tests/krate/publish/snapshots/all__krate__publish__dependencies__new_krate_with_broken_dependency_requirement.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": "invalid upload request: invalid value: string \"broken\", expected a valid version req at line 1 column 136"
8+
"detail": "\"broken\" is an invalid version requirement"
99
}
1010
]
1111
}

src/views/krate_publish.rs

Lines changed: 4 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
//! to and from structs. The serializing is only utilised in
44
//! integration tests.
55
6-
use diesel::pg::Pg;
7-
use diesel::serialize::{self, Output, ToSql};
8-
use diesel::sql_types::Text;
96
use serde::{de, Deserialize, Deserializer, Serialize};
107

118
use crate::models::krate::MAX_NAME_LENGTH;
@@ -26,12 +23,12 @@ pub struct PublishMetadata {
2623
pub struct EncodableCrateDependency {
2724
pub optional: bool,
2825
pub default_features: bool,
29-
pub name: EncodableCrateName,
30-
pub features: Vec<EncodableFeature>,
31-
pub version_req: EncodableCrateVersionReq,
26+
pub name: String,
27+
pub features: Vec<String>,
28+
pub version_req: String,
3229
pub target: Option<String>,
3330
pub kind: Option<DependencyKind>,
34-
pub explicit_name_in_toml: Option<EncodableDependencyName>,
31+
pub explicit_name_in_toml: Option<String>,
3532
pub registry: Option<String>,
3633
}
3734

@@ -54,47 +51,6 @@ impl<'de> Deserialize<'de> for EncodableCrateName {
5451
}
5552
}
5653

57-
#[derive(Serialize, Clone, Debug, Deref)]
58-
pub struct EncodableDependencyName(pub String);
59-
60-
impl<'de> Deserialize<'de> for EncodableDependencyName {
61-
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<EncodableDependencyName, D::Error> {
62-
let s = String::deserialize(d)?;
63-
if !Crate::valid_dependency_name(&s) {
64-
let value = de::Unexpected::Str(&s);
65-
let expected = format!(
66-
"a valid dependency name to start with a letter or underscore, contain only letters, \
67-
numbers, hyphens, or underscores and have at most {MAX_NAME_LENGTH} characters"
68-
);
69-
Err(de::Error::invalid_value(value, &expected.as_ref()))
70-
} else {
71-
Ok(EncodableDependencyName(s))
72-
}
73-
}
74-
}
75-
76-
#[derive(Serialize, Clone, Debug, Deref)]
77-
pub struct EncodableFeature(pub String);
78-
79-
impl<'de> Deserialize<'de> for EncodableFeature {
80-
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<EncodableFeature, D::Error> {
81-
let s = String::deserialize(d)?;
82-
if !Crate::valid_feature(&s) {
83-
let value = de::Unexpected::Str(&s);
84-
let expected = "a valid feature name";
85-
Err(de::Error::invalid_value(value, &expected))
86-
} else {
87-
Ok(EncodableFeature(s))
88-
}
89-
}
90-
}
91-
92-
impl ToSql<Text, Pg> for EncodableFeature {
93-
fn to_sql(&self, out: &mut Output<'_, '_, Pg>) -> serialize::Result {
94-
ToSql::<Text, Pg>::to_sql(&**self, &mut out.reborrow())
95-
}
96-
}
97-
9854
#[derive(Serialize, Debug, Deref)]
9955
pub struct EncodableCrateVersion(pub semver::Version);
10056

@@ -111,32 +67,3 @@ impl<'de> Deserialize<'de> for EncodableCrateVersion {
11167
}
11268
}
11369
}
114-
115-
#[derive(Serialize, Clone, Debug, Deref)]
116-
pub struct EncodableCrateVersionReq(pub String);
117-
118-
impl<'de> Deserialize<'de> for EncodableCrateVersionReq {
119-
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<EncodableCrateVersionReq, D::Error> {
120-
let s = String::deserialize(d)?;
121-
match semver::VersionReq::parse(&s) {
122-
Ok(_) => Ok(EncodableCrateVersionReq(s)),
123-
Err(..) => {
124-
let value = de::Unexpected::Str(&s);
125-
let expected = "a valid version req";
126-
Err(de::Error::invalid_value(value, &expected))
127-
}
128-
}
129-
}
130-
}
131-
132-
#[test]
133-
fn feature_deserializes_for_valid_features() {
134-
use serde_json as json;
135-
136-
assert_ok!(json::from_str::<EncodableFeature>("\"foo\""));
137-
assert_err!(json::from_str::<EncodableFeature>("\"\""));
138-
assert_err!(json::from_str::<EncodableFeature>("\"/\""));
139-
assert_err!(json::from_str::<EncodableFeature>("\"%/%\""));
140-
assert_ok!(json::from_str::<EncodableFeature>("\"a/a\""));
141-
assert_ok!(json::from_str::<EncodableFeature>("\"32-column-tables\""));
142-
}

0 commit comments

Comments
 (0)