Skip to content

Commit 5525fc1

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 6342381 commit 5525fc1

File tree

20 files changed

+184
-49
lines changed

20 files changed

+184
-49
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: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
//! Functionality related to publishing a new crate or version of a crate.
22
3-
use crate::auth::AuthCheck;
43
use axum::body::Bytes;
54
use flate2::read::GzDecoder;
65
use hex::ToHex;
76
use hyper::body::Buf;
87
use sha2::{Digest, Sha256};
98
use std::collections::BTreeMap;
109
use std::io::Read;
11-
use std::path::Path;
10+
use std::path::{Path, PathBuf};
1211

1312
use crate::controllers::cargo_prelude::*;
1413
use crate::controllers::util::RequestPartsExt;
@@ -18,10 +17,12 @@ use crate::models::{
1817
};
1918
use crate::worker;
2019

20+
use crate::auth::AuthCheck;
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
worker::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
worker::add_crate(git_crate).enqueue(conn)?;
@@ -419,11 +426,13 @@ pub fn add_dependencies(
419426
Ok(git_deps)
420427
}
421428

422-
fn verify_tarball(
423-
pkg_name: &str,
424-
tarball: &[u8],
425-
max_unpack: u64,
426-
) -> AppResult<Option<CargoVcsInfo>> {
429+
#[derive(Debug)]
430+
struct TarballInfo {
431+
manifest: Option<Manifest>,
432+
vcs_info: Option<CargoVcsInfo>,
433+
}
434+
435+
fn verify_tarball(pkg_name: &str, tarball: &[u8], max_unpack: u64) -> AppResult<TarballInfo> {
427436
// All our data is currently encoded with gzip
428437
let decoder = GzDecoder::new(tarball);
429438

@@ -434,10 +443,15 @@ fn verify_tarball(
434443
// Use this I/O object now to take a peek inside
435444
let mut archive = tar::Archive::new(decoder);
436445

446+
let entries = archive.entries()?;
447+
437448
let vcs_info_path = Path::new(&pkg_name).join(".cargo_vcs_info.json");
438449
let mut vcs_info = None;
439450

440-
for entry in archive.entries()? {
451+
let manifest_path = PathBuf::from(format!("{pkg_name}/Cargo.toml"));
452+
let mut manifest: Option<Manifest> = None;
453+
454+
for entry in entries {
441455
let mut entry = entry.map_err(|err| {
442456
err.chain(cargo_err(
443457
"uploaded tarball is malformed or too large when decompressed",
@@ -459,6 +473,15 @@ fn verify_tarball(
459473
vcs_info = CargoVcsInfo::from_contents(&contents).ok();
460474
}
461475

476+
// Try to extract and read the Cargo.toml from the tarball, silently
477+
// erroring if it cannot be read.
478+
let entry_path = entry.path()?;
479+
if entry_path == manifest_path {
480+
let mut contents = String::new();
481+
entry.read_to_string(&mut contents)?;
482+
manifest = toml::from_str(&contents).ok();
483+
}
484+
462485
// Historical versions of the `tar` crate which Cargo uses internally
463486
// don't properly prevent hard links and symlinks from overwriting
464487
// arbitrary files on the filesystem. As a bit of a hammer we reject any
@@ -469,7 +492,8 @@ fn verify_tarball(
469492
return Err(cargo_err("invalid tarball uploaded"));
470493
}
471494
}
472-
Ok(vcs_info)
495+
496+
Ok(TarballInfo { manifest, vcs_info })
473497
}
474498

475499
#[cfg(test)]
@@ -497,7 +521,9 @@ mod tests {
497521

498522
let limit = 512 * 1024 * 1024;
499523
assert_eq!(
500-
verify_tarball("foo-0.0.1", &serialized_archive, limit).unwrap(),
524+
verify_tarball("foo-0.0.1", &serialized_archive, limit)
525+
.unwrap()
526+
.vcs_info,
501527
None
502528
);
503529
assert_err!(verify_tarball("bar-0.0.1", &serialized_archive, limit));
@@ -519,6 +545,7 @@ mod tests {
519545
let limit = 512 * 1024 * 1024;
520546
let vcs_info = verify_tarball("foo-0.0.1", &serialized_archive, limit)
521547
.unwrap()
548+
.vcs_info
522549
.unwrap();
523550
assert_eq!(vcs_info.path_in_vcs, "");
524551
}
@@ -539,7 +566,51 @@ mod tests {
539566
let limit = 512 * 1024 * 1024;
540567
let vcs_info = verify_tarball("foo-0.0.1", &serialized_archive, limit)
541568
.unwrap()
569+
.vcs_info
542570
.unwrap();
543571
assert_eq!(vcs_info.path_in_vcs, "path/in/vcs");
544572
}
573+
574+
#[test]
575+
fn verify_tarball_test_manifest() {
576+
let mut pkg = tar::Builder::new(vec![]);
577+
add_file(
578+
&mut pkg,
579+
"foo-0.0.1/Cargo.toml",
580+
br#"
581+
[package]
582+
rust_version = "1.59"
583+
readme = "README.md"
584+
repository = "https://github.com/foo/bar"
585+
"#,
586+
);
587+
let mut serialized_archive = vec![];
588+
GzEncoder::new(pkg.into_inner().unwrap().as_slice(), Default::default())
589+
.read_to_end(&mut serialized_archive)
590+
.unwrap();
591+
592+
let limit = 512 * 1024 * 1024;
593+
let manifest = verify_tarball("foo-0.0.1", &serialized_archive, limit)
594+
.unwrap()
595+
.manifest;
596+
assert_eq!(
597+
manifest
598+
.as_ref()
599+
.unwrap()
600+
.package
601+
.rust_version
602+
.as_ref()
603+
.unwrap()
604+
.0,
605+
"1.59".to_owned()
606+
);
607+
assert_eq!(
608+
manifest.as_ref().unwrap().package.readme,
609+
Some("README.md".to_owned())
610+
);
611+
assert_eq!(
612+
manifest.as_ref().unwrap().package.repository,
613+
Some("https://github.com/foo/bar".to_owned())
614+
);
615+
}
545616
}

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/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);

0 commit comments

Comments
 (0)