Skip to content

Commit bc3dbf6

Browse files
authored
tarball: Use cargo_toml to parse Cargo.toml file (#6914)
1 parent 18599be commit bc3dbf6

File tree

5 files changed

+152
-65
lines changed

5 files changed

+152
-65
lines changed

crates_io_tarball/src/lib.rs

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ extern crate claims;
55
#[cfg(any(feature = "builder", test))]
66
pub use crate::builder::TarballBuilder;
77
use crate::limit_reader::LimitErrorReader;
8-
pub use crate::manifest::Manifest;
8+
use crate::manifest::validate_manifest;
99
pub use crate::vcs_info::CargoVcsInfo;
10+
pub use cargo_toml::Manifest;
1011
use flate2::read::GzDecoder;
1112
use std::io::Read;
1213
use std::path::Path;
@@ -35,7 +36,7 @@ pub enum TarballError {
3536
#[error("Cargo.toml manifest is missing")]
3637
MissingManifest,
3738
#[error("Cargo.toml manifest is invalid: {0}")]
38-
InvalidManifest(#[source] toml::de::Error),
39+
InvalidManifest(#[from] cargo_toml::Error),
3940
#[error(transparent)]
4041
IO(#[from] std::io::Error),
4142
}
@@ -97,7 +98,11 @@ pub fn process_tarball<R: Read>(
9798
// erroring if it cannot be read.
9899
let mut contents = String::new();
99100
entry.read_to_string(&mut contents)?;
100-
manifest = Some(toml::from_str(&contents).map_err(TarballError::InvalidManifest)?);
101+
manifest = Some({
102+
let manifest = Manifest::from_str(&contents)?;
103+
validate_manifest(&manifest)?;
104+
manifest
105+
});
101106
}
102107
}
103108

@@ -178,10 +183,10 @@ repository = "https://github.com/foo/bar"
178183
let limit = 512 * 1024 * 1024;
179184

180185
let tarball_info = assert_ok!(process_tarball("foo-0.0.1", &*tarball, limit));
181-
let package = tarball_info.manifest.package;
182-
assert_some_eq!(package.readme.as_path(), Path::new("README.md"));
183-
assert_some_eq!(package.repository, "https://github.com/foo/bar");
184-
assert_some_eq!(package.rust_version, "1.59");
186+
let package = assert_some!(tarball_info.manifest.package);
187+
assert_some_eq!(package.readme().as_path(), Path::new("README.md"));
188+
assert_some_eq!(package.repository(), "https://github.com/foo/bar");
189+
assert_some_eq!(package.rust_version(), "1.59");
185190
}
186191

187192
#[test]
@@ -200,8 +205,8 @@ repository = "https://github.com/foo/bar"
200205
let limit = 512 * 1024 * 1024;
201206

202207
let tarball_info = assert_ok!(process_tarball("foo-0.0.1", &*tarball, limit));
203-
let package = tarball_info.manifest.package;
204-
assert_some_eq!(package.rust_version, "1.23");
208+
let package = assert_some!(tarball_info.manifest.package);
209+
assert_some_eq!(package.rust_version(), "1.23");
205210
}
206211

207212
#[test]
@@ -219,8 +224,8 @@ repository = "https://github.com/foo/bar"
219224
let limit = 512 * 1024 * 1024;
220225

221226
let tarball_info = assert_ok!(process_tarball("foo-0.0.1", &*tarball, limit));
222-
let package = tarball_info.manifest.package;
223-
assert_matches!(package.readme, OptionalFile::Flag(true));
227+
let package = assert_some!(tarball_info.manifest.package);
228+
assert_matches!(package.readme(), OptionalFile::Flag(true));
224229
}
225230

226231
#[test]
@@ -239,8 +244,8 @@ repository = "https://github.com/foo/bar"
239244
let limit = 512 * 1024 * 1024;
240245

241246
let tarball_info = assert_ok!(process_tarball("foo-0.0.1", &*tarball, limit));
242-
let package = tarball_info.manifest.package;
243-
assert_matches!(package.readme, OptionalFile::Flag(false));
247+
let package = assert_some!(tarball_info.manifest.package);
248+
assert_matches!(package.readme(), OptionalFile::Flag(false));
244249
}
245250

246251
#[test]
@@ -260,7 +265,7 @@ repository = "https://github.com/foo/bar"
260265
let limit = 512 * 1024 * 1024;
261266

262267
let tarball_info = assert_ok!(process_tarball("foo-0.0.1", &*tarball, limit));
263-
let package = tarball_info.manifest.package;
264-
assert_some_eq!(package.repository, "https://github.com/foo/bar");
268+
let package = assert_some!(tarball_info.manifest.package);
269+
assert_some_eq!(package.repository(), "https://github.com/foo/bar");
265270
}
266271
}

crates_io_tarball/src/manifest.rs

Lines changed: 87 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,89 @@
1-
use cargo_toml::OptionalFile;
2-
use derive_deref::Deref;
3-
use serde::{de, Deserialize, Deserializer};
4-
5-
#[derive(Debug, Deserialize)]
6-
pub struct Manifest {
7-
#[serde(alias = "project")]
8-
pub package: Package,
9-
}
10-
11-
#[derive(Debug, Deserialize)]
12-
#[serde(rename_all = "kebab-case")]
13-
pub struct Package {
14-
pub name: String,
15-
pub version: String,
16-
#[serde(default)]
17-
pub readme: OptionalFile,
18-
pub repository: Option<String>,
19-
pub rust_version: Option<RustVersion>,
20-
}
21-
22-
#[derive(Debug, Deref)]
23-
pub struct RustVersion(String);
24-
25-
impl PartialEq<&str> for RustVersion {
26-
fn eq(&self, other: &&str) -> bool {
27-
self.0.eq(other)
28-
}
29-
}
30-
31-
impl<'de> Deserialize<'de> for RustVersion {
32-
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<RustVersion, D::Error> {
33-
let s = String::deserialize(d)?;
34-
match semver::VersionReq::parse(&s) {
35-
// Exclude semver operators like `^` and pre-release identifiers
36-
Ok(_) if s.chars().all(|c| c.is_ascii_digit() || c == '.') => Ok(RustVersion(s)),
37-
Ok(_) | Err(..) => {
38-
let value = de::Unexpected::Str(&s);
39-
let expected = "a valid rust_version";
40-
Err(de::Error::invalid_value(value, &expected))
41-
}
42-
}
1+
use cargo_toml::{Dependency, DepsSet, Error, Inheritable, Manifest, Package};
2+
3+
pub fn validate_manifest(manifest: &Manifest) -> Result<(), Error> {
4+
let package = manifest.package.as_ref();
5+
6+
// Check that a `[package]` table exists in the manifest, since crates.io
7+
// does not accept workspace manifests.
8+
let package = package.ok_or(Error::Other("missing field `package`"))?;
9+
10+
validate_package(package)?;
11+
12+
// These checks ensure that dependency workspace inheritance has been
13+
// normalized by cargo before publishing.
14+
if manifest.dependencies.is_inherited()
15+
|| manifest.dev_dependencies.is_inherited()
16+
|| manifest.build_dependencies.is_inherited()
17+
{
18+
return Err(Error::InheritedUnknownValue);
19+
}
20+
21+
Ok(())
22+
}
23+
24+
pub fn validate_package(package: &Package) -> Result<(), Error> {
25+
// These checks ensure that package field workspace inheritance has been
26+
// normalized by cargo before publishing.
27+
if package.edition.is_inherited()
28+
|| package.rust_version.is_inherited()
29+
|| package.version.is_inherited()
30+
|| package.authors.is_inherited()
31+
|| package.description.is_inherited()
32+
|| package.homepage.is_inherited()
33+
|| package.documentation.is_inherited()
34+
|| package.readme.is_inherited()
35+
|| package.keywords.is_inherited()
36+
|| package.categories.is_inherited()
37+
|| package.exclude.is_inherited()
38+
|| package.include.is_inherited()
39+
|| package.license.is_inherited()
40+
|| package.license_file.is_inherited()
41+
|| package.repository.is_inherited()
42+
|| package.publish.is_inherited()
43+
{
44+
return Err(Error::InheritedUnknownValue);
45+
}
46+
47+
// Check that the `rust-version` field has a valid value, if it exists.
48+
if let Some(rust_version) = package.rust_version() {
49+
validate_rust_version(rust_version)?;
50+
}
51+
52+
Ok(())
53+
}
54+
55+
trait IsInherited {
56+
fn is_inherited(&self) -> bool;
57+
}
58+
59+
impl<T> IsInherited for Inheritable<T> {
60+
fn is_inherited(&self) -> bool {
61+
!self.is_set()
62+
}
63+
}
64+
65+
impl<T: IsInherited> IsInherited for Option<T> {
66+
fn is_inherited(&self) -> bool {
67+
self.as_ref().map(|it| it.is_inherited()).unwrap_or(false)
68+
}
69+
}
70+
71+
impl IsInherited for Dependency {
72+
fn is_inherited(&self) -> bool {
73+
matches!(self, Dependency::Inherited(_))
74+
}
75+
}
76+
77+
impl IsInherited for DepsSet {
78+
fn is_inherited(&self) -> bool {
79+
self.iter().any(|(_key, dep)| dep.is_inherited())
80+
}
81+
}
82+
83+
pub fn validate_rust_version(value: &str) -> Result<(), Error> {
84+
match semver::VersionReq::parse(value) {
85+
// Exclude semver operators like `^` and pre-release identifiers
86+
Ok(_) if value.chars().all(|c| c.is_ascii_digit() || c == '.') => Ok(()),
87+
Ok(_) | Err(..) => Err(Error::Other("invalid `rust-version` value")),
4388
}
4489
}

src/admin/render_readmes.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,14 @@ fn render_pkg_readme<R: Read>(mut archive: Archive<R>, pkg_name: &str) -> anyhow
177177
let contents = find_file_by_path(&mut entries, Path::new(&path))
178178
.context("Failed to read Cargo.toml file")?;
179179

180-
toml::from_str(&contents).context("Failed to parse manifest file")?
180+
Manifest::from_str(&contents).context("Failed to parse manifest file")?
181+
182+
// We don't call `validate_manifest()` here since the additional validation is not needed
183+
// and it would prevent us from reading a couple of legacy crate files.
181184
};
182185

183186
let rendered = {
184-
let readme = manifest.package.readme;
187+
let readme = manifest.package().readme();
185188
if !readme.is_some() {
186189
return Ok("".to_string());
187190
}
@@ -198,7 +201,7 @@ fn render_pkg_readme<R: Read>(mut archive: Archive<R>, pkg_name: &str) -> anyhow
198201
text_to_html(
199202
&contents,
200203
&readme_path,
201-
manifest.package.repository.as_deref(),
204+
manifest.package().repository(),
202205
pkg_path_in_vcs,
203206
)
204207
};

src/controllers/krate/publish.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,8 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
197197

198198
let rust_version = tarball_info
199199
.manifest
200-
.package
201-
.rust_version
200+
.package()
201+
.rust_version()
202202
.map(|rv| rv.deref().to_string());
203203

204204
// Persist the new version of this crate

src/tests/krate/publish.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,7 +1291,7 @@ fn invalid_manifest() {
12911291
assert_eq!(response.status(), StatusCode::OK);
12921292
assert_eq!(
12931293
response.into_json(),
1294-
json!({ "errors": [{ "detail": "failed to parse `Cargo.toml` manifest file\n\nTOML parse error at line 1, column 1\n |\n1 | \n | ^\nmissing field `package`\n" }] })
1294+
json!({ "errors": [{ "detail": "failed to parse `Cargo.toml` manifest file\n\nmissing field `name`\n" }] })
12951295
);
12961296
}
12971297

@@ -1339,7 +1339,7 @@ fn invalid_rust_version() {
13391339
assert_eq!(response.status(), StatusCode::OK);
13401340
assert_eq!(
13411341
response.into_json(),
1342-
json!({ "errors": [{ "detail": "failed to parse `Cargo.toml` manifest file\n\nTOML parse error at line 4, column 16\n |\n4 | rust-version = \"\"\n | ^^\ninvalid value: string \"\", expected a valid rust_version\n" }] })
1342+
json!({ "errors": [{ "detail": "failed to parse `Cargo.toml` manifest file\n\ninvalid `rust-version` value" }] })
13431343
);
13441344

13451345
let tarball = TarballBuilder::new("foo", "1.0.0")
@@ -1352,6 +1352,40 @@ fn invalid_rust_version() {
13521352
assert_eq!(response.status(), StatusCode::OK);
13531353
assert_eq!(
13541354
response.into_json(),
1355-
json!({ "errors": [{ "detail": "failed to parse `Cargo.toml` manifest file\n\nTOML parse error at line 4, column 16\n |\n4 | rust-version = \"1.0.0-beta.2\"\n | ^^^^^^^^^^^^^^\ninvalid value: string \"1.0.0-beta.2\", expected a valid rust_version\n" }] })
1355+
json!({ "errors": [{ "detail": "failed to parse `Cargo.toml` manifest file\n\ninvalid `rust-version` value" }] })
1356+
);
1357+
}
1358+
1359+
#[test]
1360+
fn workspace_inheritance() {
1361+
let (_app, _anon, _cookie, token) = TestApp::full().with_token();
1362+
1363+
let tarball = TarballBuilder::new("foo", "1.0.0")
1364+
.add_raw_manifest(b"[package]\nname = \"foo\"\nversion.workspace = true\n")
1365+
.build();
1366+
1367+
let response = token.publish_crate(PublishBuilder::new("foo", "1.0.0").tarball(tarball));
1368+
assert_eq!(response.status(), StatusCode::OK);
1369+
assert_eq!(
1370+
response.into_json(),
1371+
json!({ "errors": [{ "detail": "failed to parse `Cargo.toml` manifest file\n\nvalue from workspace hasn't been set" }] })
1372+
);
1373+
}
1374+
1375+
#[test]
1376+
fn workspace_inheritance_with_dep() {
1377+
let (_app, _anon, _cookie, token) = TestApp::full().with_token();
1378+
1379+
let tarball = TarballBuilder::new("foo", "1.0.0")
1380+
.add_raw_manifest(
1381+
b"[package]\nname = \"foo\"\nversion = \"1.0.0\"\n\n[dependencies]\nserde.workspace = true\n",
1382+
)
1383+
.build();
1384+
1385+
let response = token.publish_crate(PublishBuilder::new("foo", "1.0.0").tarball(tarball));
1386+
assert_eq!(response.status(), StatusCode::OK);
1387+
assert_eq!(
1388+
response.into_json(),
1389+
json!({ "errors": [{ "detail": "failed to parse `Cargo.toml` manifest file\n\nvalue from workspace hasn't been set" }] })
13561390
);
13571391
}

0 commit comments

Comments
 (0)