Skip to content

Commit 523c14d

Browse files
committed
Add rust_version field to the index
The rust_version field is read from the uploaded tarball, breaking from convention.
1 parent 07c0163 commit 523c14d

File tree

21 files changed

+186
-50
lines changed

21 files changed

+186
-50
lines changed

cargo-registry-index/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ pub struct Crate {
125125
pub yanked: Option<bool>,
126126
#[serde(skip_serializing_if = "Option::is_none")]
127127
pub links: Option<String>,
128+
#[serde(skip_serializing_if = "Option::is_none")]
129+
pub rust_version: Option<String>,
128130
/// The schema version for this entry.
129131
///
130132
/// If this is None, it defaults to version 1. Entries with unknown
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE versions DROP COLUMN rust_version;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE versions ADD COLUMN rust_version VARCHAR;

src/admin/render_readmes.rs

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::{
33
models::Version,
44
schema::{crates, readme_renderings, versions},
55
uploaders::Uploader,
6+
util::{find_file_by_path, manifest::Manifest},
67
};
78
use anyhow::{anyhow, Context};
89
use std::{io::Read, path::Path, sync::Arc, thread};
@@ -200,38 +201,7 @@ fn render_pkg_readme<R: Read>(mut archive: Archive<R>, pkg_name: &str) -> anyhow
200201
pkg_path_in_vcs,
201202
)
202203
};
203-
return Ok(rendered);
204-
205-
#[derive(Debug, Deserialize)]
206-
struct Package {
207-
readme: Option<String>,
208-
repository: Option<String>,
209-
}
210-
211-
#[derive(Debug, Deserialize)]
212-
struct Manifest {
213-
package: Package,
214-
}
215-
}
216-
217-
/// Search an entry by its path in a Tar archive.
218-
fn find_file_by_path<R: Read>(
219-
entries: &mut tar::Entries<'_, R>,
220-
path: &Path,
221-
) -> anyhow::Result<String> {
222-
let mut file = entries
223-
.filter_map(|entry| entry.ok())
224-
.find(|file| match file.path() {
225-
Ok(p) => p == path,
226-
Err(_) => false,
227-
})
228-
.ok_or_else(|| anyhow!("Failed to find tarball entry: {}", path.display()))?;
229-
230-
let mut contents = String::new();
231-
file.read_to_string(&mut contents)
232-
.context("Failed to read file contents")?;
233-
234-
Ok(contents)
204+
Ok(rendered)
235205
}
236206

237207
#[cfg(test)]

src/controllers/krate/publish.rs

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
//! Functionality related to publishing a new crate or version of a crate.
22
3-
use crate::auth::AuthCheck;
4-
use crate::background_jobs::Job;
53
use axum::body::Bytes;
64
use flate2::read::GzDecoder;
75
use hex::ToHex;
86
use hyper::body::Buf;
97
use sha2::{Digest, Sha256};
108
use std::collections::BTreeMap;
119
use std::io::Read;
12-
use std::path::Path;
10+
use std::path::{Path, PathBuf};
1311

1412
use crate::controllers::cargo_prelude::*;
1513
use crate::controllers::util::RequestPartsExt;
@@ -18,10 +16,13 @@ use crate::models::{
1816
Rights, VersionAction,
1917
};
2018

19+
use crate::auth::AuthCheck;
20+
use crate::background_jobs::Job;
2121
use crate::middleware::log_request::RequestLogExt;
2222
use crate::models::token::EndpointScope;
2323
use crate::schema::*;
2424
use crate::util::errors::{cargo_err, AppResult};
25+
use crate::util::manifest::Manifest;
2526
use crate::util::{CargoVcsInfo, LimitErrorReader, Maximums};
2627
use crate::views::{
2728
EncodableCrate, EncodableCrateDependency, EncodableCrateUpload, GoodCrate, PublishWarnings,
@@ -188,6 +189,13 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
188189
// Read tarball from request
189190
let hex_cksum: String = Sha256::digest(&tarball_bytes).encode_hex();
190191

192+
let pkg_name = format!("{}-{}", krate.name, vers);
193+
let tarball_info = verify_tarball(&pkg_name, &tarball_bytes, maximums.max_unpack_size)?;
194+
195+
let rust_version = tarball_info
196+
.manifest
197+
.and_then(|m| m.package.rust_version.map(|rv| rv.0));
198+
191199
// Persist the new version of this crate
192200
let version = NewVersion::new(
193201
krate.id,
@@ -201,6 +209,7 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
201209
user.id,
202210
hex_cksum.clone(),
203211
links.clone(),
212+
rust_version.as_deref(),
204213
)?
205214
.save(conn, &verified_email_address)?;
206215

@@ -224,10 +233,7 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
224233

225234
let top_versions = krate.top_versions(conn)?;
226235

227-
let pkg_name = format!("{}-{}", krate.name, vers);
228-
let cargo_vcs_info =
229-
verify_tarball(&pkg_name, &tarball_bytes, maximums.max_unpack_size)?;
230-
let pkg_path_in_vcs = cargo_vcs_info.map(|info| info.path_in_vcs);
236+
let pkg_path_in_vcs = tarball_info.vcs_info.map(|info| info.path_in_vcs);
231237

232238
if let Some(readme) = new_crate.readme {
233239
Job::render_and_upload_readme(
@@ -269,6 +275,7 @@ pub async fn publish(app: AppState, req: BytesRequest) -> AppResult<Json<GoodCra
269275
deps: git_deps,
270276
yanked: Some(false),
271277
links,
278+
rust_version,
272279
v,
273280
};
274281

@@ -426,12 +433,14 @@ pub fn add_dependencies(
426433
Ok(git_deps)
427434
}
428435

436+
#[derive(Debug)]
437+
struct TarballInfo {
438+
manifest: Option<Manifest>,
439+
vcs_info: Option<CargoVcsInfo>,
440+
}
441+
429442
#[instrument(skip_all, fields(%pkg_name))]
430-
fn verify_tarball(
431-
pkg_name: &str,
432-
tarball: &[u8],
433-
max_unpack: u64,
434-
) -> AppResult<Option<CargoVcsInfo>> {
443+
fn verify_tarball(pkg_name: &str, tarball: &[u8], max_unpack: u64) -> AppResult<TarballInfo> {
435444
// All our data is currently encoded with gzip
436445
let decoder = GzDecoder::new(tarball);
437446

@@ -442,10 +451,15 @@ fn verify_tarball(
442451
// Use this I/O object now to take a peek inside
443452
let mut archive = tar::Archive::new(decoder);
444453

454+
let entries = archive.entries()?;
455+
445456
let vcs_info_path = Path::new(&pkg_name).join(".cargo_vcs_info.json");
446457
let mut vcs_info = None;
447458

448-
for entry in archive.entries()? {
459+
let manifest_path = PathBuf::from(format!("{pkg_name}/Cargo.toml"));
460+
let mut manifest: Option<Manifest> = None;
461+
462+
for entry in entries {
449463
let mut entry = entry.map_err(|err| {
450464
err.chain(cargo_err(
451465
"uploaded tarball is malformed or too large when decompressed",
@@ -467,6 +481,15 @@ fn verify_tarball(
467481
vcs_info = CargoVcsInfo::from_contents(&contents).ok();
468482
}
469483

484+
// Try to extract and read the Cargo.toml from the tarball, silently
485+
// erroring if it cannot be read.
486+
let entry_path = entry.path()?;
487+
if entry_path == manifest_path {
488+
let mut contents = String::new();
489+
entry.read_to_string(&mut contents)?;
490+
manifest = toml::from_str(&contents).ok();
491+
}
492+
470493
// Historical versions of the `tar` crate which Cargo uses internally
471494
// don't properly prevent hard links and symlinks from overwriting
472495
// arbitrary files on the filesystem. As a bit of a hammer we reject any
@@ -477,7 +500,8 @@ fn verify_tarball(
477500
return Err(cargo_err("invalid tarball uploaded"));
478501
}
479502
}
480-
Ok(vcs_info)
503+
504+
Ok(TarballInfo { manifest, vcs_info })
481505
}
482506

483507
#[cfg(test)]
@@ -505,7 +529,9 @@ mod tests {
505529

506530
let limit = 512 * 1024 * 1024;
507531
assert_eq!(
508-
verify_tarball("foo-0.0.1", &serialized_archive, limit).unwrap(),
532+
verify_tarball("foo-0.0.1", &serialized_archive, limit)
533+
.unwrap()
534+
.vcs_info,
509535
None
510536
);
511537
assert_err!(verify_tarball("bar-0.0.1", &serialized_archive, limit));
@@ -527,6 +553,7 @@ mod tests {
527553
let limit = 512 * 1024 * 1024;
528554
let vcs_info = verify_tarball("foo-0.0.1", &serialized_archive, limit)
529555
.unwrap()
556+
.vcs_info
530557
.unwrap();
531558
assert_eq!(vcs_info.path_in_vcs, "");
532559
}
@@ -547,7 +574,51 @@ mod tests {
547574
let limit = 512 * 1024 * 1024;
548575
let vcs_info = verify_tarball("foo-0.0.1", &serialized_archive, limit)
549576
.unwrap()
577+
.vcs_info
550578
.unwrap();
551579
assert_eq!(vcs_info.path_in_vcs, "path/in/vcs");
552580
}
581+
582+
#[test]
583+
fn verify_tarball_test_manifest() {
584+
let mut pkg = tar::Builder::new(vec![]);
585+
add_file(
586+
&mut pkg,
587+
"foo-0.0.1/Cargo.toml",
588+
br#"
589+
[package]
590+
rust_version = "1.59"
591+
readme = "README.md"
592+
repository = "https://github.com/foo/bar"
593+
"#,
594+
);
595+
let mut serialized_archive = vec![];
596+
GzEncoder::new(pkg.into_inner().unwrap().as_slice(), Default::default())
597+
.read_to_end(&mut serialized_archive)
598+
.unwrap();
599+
600+
let limit = 512 * 1024 * 1024;
601+
let manifest = verify_tarball("foo-0.0.1", &serialized_archive, limit)
602+
.unwrap()
603+
.manifest;
604+
assert_eq!(
605+
manifest
606+
.as_ref()
607+
.unwrap()
608+
.package
609+
.rust_version
610+
.as_ref()
611+
.unwrap()
612+
.0,
613+
"1.59".to_owned()
614+
);
615+
assert_eq!(
616+
manifest.as_ref().unwrap().package.readme,
617+
Some("README.md".to_owned())
618+
);
619+
assert_eq!(
620+
manifest.as_ref().unwrap().package.repository,
621+
Some("https://github.com/foo/bar".to_owned())
622+
);
623+
}
553624
}

src/downloads_counter.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@ mod tests {
459459
self.user.id,
460460
"0000000000000000000000000000000000000000000000000000000000000000".to_string(),
461461
None,
462+
None,
462463
)
463464
.expect("failed to create version")
464465
.save(conn, "[email protected]")

src/models/krate.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ impl Crate {
510510
deps,
511511
features,
512512
links: version.links,
513+
rust_version: version.rust_version,
513514
features2,
514515
v,
515516
};

src/models/version.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub struct Version {
2525
pub published_by: Option<i32>,
2626
pub checksum: String,
2727
pub links: Option<String>,
28+
pub rust_version: Option<String>,
2829
}
2930

3031
#[derive(Insertable, Debug)]
@@ -38,6 +39,7 @@ pub struct NewVersion {
3839
published_by: i32,
3940
checksum: String,
4041
links: Option<String>,
42+
rust_version: Option<String>,
4143
}
4244

4345
/// The highest version (semver order) and the most recently updated version.
@@ -139,6 +141,7 @@ impl NewVersion {
139141
published_by: i32,
140142
checksum: String,
141143
links: Option<String>,
144+
rust_version: Option<&str>,
142145
) -> AppResult<Self> {
143146
let features = serde_json::to_value(features)?;
144147

@@ -151,6 +154,7 @@ impl NewVersion {
151154
published_by,
152155
checksum,
153156
links,
157+
rust_version: rust_version.map(ToOwned::to_owned),
154158
};
155159

156160
new_version.validate_license(license_file)?;

src/schema.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,12 @@ diesel::table! {
966966
///
967967
/// (Automatically generated by Diesel.)
968968
links -> Nullable<Varchar>,
969+
/// The `rust_version` column of the `versions` table.
970+
///
971+
/// Its SQL type is `Nullable<Varchar>`.
972+
///
973+
/// (Automatically generated by Diesel.)
974+
rust_version -> Nullable<Varchar>,
969975
}
970976
}
971977

src/tests/builders/version.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub struct VersionBuilder<'a> {
2020
yanked: bool,
2121
checksum: String,
2222
links: Option<String>,
23+
rust_version: Option<String>,
2324
}
2425

2526
impl<'a> VersionBuilder<'a> {
@@ -45,6 +46,7 @@ impl<'a> VersionBuilder<'a> {
4546
yanked: false,
4647
checksum: String::new(),
4748
links: None,
49+
rust_version: None,
4850
}
4951
}
5052

@@ -83,6 +85,12 @@ impl<'a> VersionBuilder<'a> {
8385
self
8486
}
8587

88+
/// Sets the version's `rust_version` value.
89+
pub fn rust_version(mut self, rust_version: &str) -> Self {
90+
self.rust_version = Some(rust_version.to_owned());
91+
self
92+
}
93+
8694
pub fn build(
8795
self,
8896
crate_id: i32,
@@ -103,6 +111,7 @@ impl<'a> VersionBuilder<'a> {
103111
published_by,
104112
self.checksum,
105113
self.links,
114+
self.rust_version.as_deref(),
106115
)?
107116
.save(connection, "[email protected]")?;
108117

src/tests/krate/versions.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::builders::CrateBuilder;
1+
use crate::builders::{CrateBuilder, VersionBuilder};
22
use crate::util::{RequestHelper, TestApp};
33
use cargo_registry::schema::versions;
44
use cargo_registry::views::EncodableVersion;
@@ -16,7 +16,7 @@ fn versions() {
1616
app.db(|conn| {
1717
CrateBuilder::new("foo_versions", user.id)
1818
.version("0.5.1")
19-
.version("1.0.0")
19+
.version(VersionBuilder::new("1.0.0").rust_version("1.64"))
2020
.version("0.5.0")
2121
.expect_build(conn);
2222
// Make version 1.0.0 mimic a version published before we started recording who published
@@ -33,6 +33,7 @@ fn versions() {
3333

3434
assert_eq!(json.versions.len(), 3);
3535
assert_eq!(json.versions[0].num, "1.0.0");
36+
assert_eq!(json.versions[0].rust_version, Some("1.64".to_owned()));
3637
assert_eq!(json.versions[1].num, "0.5.1");
3738
assert_eq!(json.versions[2].num, "0.5.0");
3839
assert_none!(&json.versions[0].published_by);

src/tests/routes/crates/versions/read.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ fn show_by_crate_name_and_version() {
1414
VersionBuilder::new("2.0.0")
1515
.size(1234)
1616
.checksum("c241cd77c3723ccf1aa453f169ee60c0a888344da504bee0142adb859092acb4")
17+
.rust_version("1.64")
1718
.expect_build(krate.id, user.id, conn)
1819
});
1920

0 commit comments

Comments
 (0)