Skip to content

Commit 251e6e6

Browse files
committed
controllers::krate::publish: Extract cargo vcs info from tarball
Supported in rust-lang/cargo#9866
1 parent 7567953 commit 251e6e6

File tree

2 files changed

+110
-10
lines changed

2 files changed

+110
-10
lines changed

src/controllers/krate/publish.rs

+67-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use flate2::read::GzDecoder;
44
use hex::ToHex;
55
use sha2::{Digest, Sha256};
66
use std::io::Read;
7+
use std::path::Path;
78
use std::sync::Arc;
89
use swirl::Job;
910

@@ -17,7 +18,7 @@ use crate::worker;
1718

1819
use crate::schema::*;
1920
use crate::util::errors::{cargo_err, AppResult};
20-
use crate::util::{read_fill, read_le_u32, LimitErrorReader, Maximums};
21+
use crate::util::{read_fill, read_le_u32, CargoVcsInfo, LimitErrorReader, Maximums};
2122
use crate::views::{
2223
EncodableCrate, EncodableCrateDependency, EncodableCrateUpload, GoodCrate, PublishWarnings,
2324
};
@@ -194,9 +195,8 @@ pub fn publish(req: &mut dyn RequestExt) -> EndpointResult {
194195
LimitErrorReader::new(req.body(), maximums.max_upload_size).read_to_end(&mut tarball)?;
195196
let hex_cksum: String = Sha256::digest(&tarball).encode_hex();
196197
let pkg_name = format!("{}-{}", krate.name, vers);
197-
verify_tarball(&pkg_name, &tarball, maximums.max_unpack_size)?;
198-
199-
let pkg_path_in_vcs = None;
198+
let cargo_vcs_info = verify_tarball(&pkg_name, &tarball, maximums.max_unpack_size)?;
199+
let pkg_path_in_vcs = cargo_vcs_info.map(|info| info.path_in_vcs);
200200

201201
if let Some(readme) = new_crate.readme {
202202
worker::render_and_upload_readme(
@@ -367,7 +367,11 @@ pub fn add_dependencies(
367367
Ok(git_deps)
368368
}
369369

370-
fn verify_tarball(pkg_name: &str, tarball: &[u8], max_unpack: u64) -> AppResult<()> {
370+
fn verify_tarball(
371+
pkg_name: &str,
372+
tarball: &[u8],
373+
max_unpack: u64,
374+
) -> AppResult<Option<CargoVcsInfo>> {
371375
// All our data is currently encoded with gzip
372376
let decoder = GzDecoder::new(tarball);
373377

@@ -377,8 +381,12 @@ fn verify_tarball(pkg_name: &str, tarball: &[u8], max_unpack: u64) -> AppResult<
377381

378382
// Use this I/O object now to take a peek inside
379383
let mut archive = tar::Archive::new(decoder);
384+
385+
let vcs_info_path = Path::new(&pkg_name).join(".cargo_vcs_info.json");
386+
let mut vcs_info = None;
387+
380388
for entry in archive.entries()? {
381-
let entry = entry.map_err(|err| {
389+
let mut entry = entry.map_err(|err| {
382390
err.chain(cargo_err(
383391
"uploaded tarball is malformed or too large when decompressed",
384392
))
@@ -389,9 +397,15 @@ fn verify_tarball(pkg_name: &str, tarball: &[u8], max_unpack: u64) -> AppResult<
389397
// upload a tarball that contains both `foo-0.1.0/` source code as well
390398
// as `bar-0.1.0/` source code, and this could overwrite other crates in
391399
// the registry!
392-
if !entry.path()?.starts_with(&pkg_name) {
400+
let entry_path = entry.path()?;
401+
if !entry_path.starts_with(&pkg_name) {
393402
return Err(cargo_err("invalid tarball uploaded"));
394403
}
404+
if entry_path == vcs_info_path {
405+
let mut contents = String::new();
406+
entry.read_to_string(&mut contents)?;
407+
vcs_info = CargoVcsInfo::from_contents(&contents).ok();
408+
}
395409

396410
// Historical versions of the `tar` crate which Cargo uses internally
397411
// don't properly prevent hard links and symlinks from overwriting
@@ -403,7 +417,7 @@ fn verify_tarball(pkg_name: &str, tarball: &[u8], max_unpack: u64) -> AppResult<
403417
return Err(cargo_err("invalid tarball uploaded"));
404418
}
405419
}
406-
Ok(())
420+
Ok(vcs_info)
407421
}
408422

409423
#[cfg(test)]
@@ -423,14 +437,57 @@ mod tests {
423437
#[test]
424438
fn verify_tarball_test() {
425439
let mut pkg = tar::Builder::new(vec![]);
426-
add_file(&mut pkg, "foo-0.0.1/.cargo_vcs_info.json", br#"{}"#);
440+
add_file(&mut pkg, "foo-0.0.1/Cargo.toml", b"");
427441
let mut serialized_archive = vec![];
428442
GzEncoder::new(pkg.into_inner().unwrap().as_slice(), Default::default())
429443
.read_to_end(&mut serialized_archive)
430444
.unwrap();
431445

432446
let limit = 512 * 1024 * 1024;
433-
assert_ok!(verify_tarball("foo-0.0.1", &serialized_archive, limit));
447+
assert_eq!(
448+
verify_tarball("foo-0.0.1", &serialized_archive, limit).unwrap(),
449+
None
450+
);
434451
assert_err!(verify_tarball("bar-0.0.1", &serialized_archive, limit));
435452
}
453+
454+
#[test]
455+
fn verify_tarball_test_incomplete_vcs_info() {
456+
let mut pkg = tar::Builder::new(vec![]);
457+
add_file(&mut pkg, "foo-0.0.1/Cargo.toml", b"");
458+
add_file(
459+
&mut pkg,
460+
"foo-0.0.1/.cargo_vcs_info.json",
461+
br#"{"unknown": "field"}"#,
462+
);
463+
let mut serialized_archive = vec![];
464+
GzEncoder::new(pkg.into_inner().unwrap().as_slice(), Default::default())
465+
.read_to_end(&mut serialized_archive)
466+
.unwrap();
467+
let limit = 512 * 1024 * 1024;
468+
let vcs_info = verify_tarball("foo-0.0.1", &serialized_archive, limit)
469+
.unwrap()
470+
.unwrap();
471+
assert_eq!(vcs_info.path_in_vcs, "");
472+
}
473+
474+
#[test]
475+
fn verify_tarball_test_vcs_info() {
476+
let mut pkg = tar::Builder::new(vec![]);
477+
add_file(&mut pkg, "foo-0.0.1/Cargo.toml", b"");
478+
add_file(
479+
&mut pkg,
480+
"foo-0.0.1/.cargo_vcs_info.json",
481+
br#"{"path_in_vcs": "path/in/vcs"}"#,
482+
);
483+
let mut serialized_archive = vec![];
484+
GzEncoder::new(pkg.into_inner().unwrap().as_slice(), Default::default())
485+
.read_to_end(&mut serialized_archive)
486+
.unwrap();
487+
let limit = 512 * 1024 * 1024;
488+
let vcs_info = verify_tarball("foo-0.0.1", &serialized_archive, limit)
489+
.unwrap()
490+
.unwrap();
491+
assert_eq!(vcs_info.path_in_vcs, "path/in/vcs");
492+
}
436493
}

src/util.rs

+43
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,46 @@ impl Maximums {
5353
}
5454
}
5555
}
56+
57+
/// Represents relevant contents of .cargo_vcs_info.json file when uploaded from cargo
58+
/// or downloaded from crates.io
59+
#[derive(Debug, Deserialize, Eq, PartialEq)]
60+
pub struct CargoVcsInfo {
61+
/// Path to the package within repo (empty string if root). / not \
62+
#[serde(default)]
63+
pub path_in_vcs: String,
64+
}
65+
66+
impl CargoVcsInfo {
67+
pub fn from_contents(contents: &str) -> serde_json::Result<Self> {
68+
serde_json::from_str(contents)
69+
}
70+
}
71+
72+
#[cfg(test)]
73+
mod tests {
74+
use super::CargoVcsInfo;
75+
76+
#[test]
77+
fn test_cargo_vcs_info() {
78+
assert_eq!(CargoVcsInfo::from_contents("").ok(), None);
79+
assert_eq!(
80+
CargoVcsInfo::from_contents("{}").unwrap(),
81+
CargoVcsInfo {
82+
path_in_vcs: "".into()
83+
}
84+
);
85+
assert_eq!(
86+
CargoVcsInfo::from_contents(r#"{"path_in_vcs": "hi"}"#).unwrap(),
87+
CargoVcsInfo {
88+
path_in_vcs: "hi".into()
89+
}
90+
);
91+
assert_eq!(
92+
CargoVcsInfo::from_contents(r#"{"path_in_vcs": "hi", "future": "field"}"#).unwrap(),
93+
CargoVcsInfo {
94+
path_in_vcs: "hi".into()
95+
}
96+
);
97+
}
98+
}

0 commit comments

Comments
 (0)