Skip to content

Commit b8978f4

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

File tree

3 files changed

+173
-12
lines changed

3 files changed

+173
-12
lines changed

src/admin/render_readmes.rs

+67-2
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::CargoVcsInfo,
67
};
78
use std::{collections::HashMap, ffi::OsString, io::Read, sync::Arc, thread};
89

@@ -212,6 +213,15 @@ fn render_pkg_readme<R: Read>(mut pkg: Archive<R>, pkg_name: &str) -> Option<Str
212213
})
213214
.collect();
214215

216+
let pkg_path_in_vcs = {
217+
let path = format!("{}/.cargo_vcs_info.json", pkg_name);
218+
entries.get(&OsString::from(path)).map(|contents| {
219+
CargoVcsInfo::from_contents(contents)
220+
.map(|info| info.path_in_vcs)
221+
.unwrap_or_else(|e| panic!("[{}] invalid .cargo_vcs_info.json: {}", pkg_name, e))
222+
})
223+
};
224+
215225
let manifest: Manifest = {
216226
let path = format!("{}/Cargo.toml", pkg_name);
217227
let contents = entries
@@ -242,12 +252,11 @@ fn render_pkg_readme<R: Read>(mut pkg: Archive<R>, pkg_name: &str) -> Option<Str
242252

243253
let path = format!("{}/{}", pkg_name, readme_path);
244254
let contents = entries.get(&OsString::from(path))?;
245-
let pkg_path_in_vcs = None;
246255
text_to_html(
247256
contents,
248257
readme_path,
249258
manifest.package.repository.as_deref(),
250-
pkg_path_in_vcs,
259+
pkg_path_in_vcs.as_deref(),
251260
)
252261
};
253262
return Some(rendered);
@@ -441,4 +450,60 @@ repository = "https://github.com/foo/foo"
441450
assert!(result.contains("docs/readme"));
442451
assert!(result.contains("\"https://github.com/foo/foo/blob/HEAD/docs/./Other.md\""))
443452
}
453+
454+
#[test]
455+
fn test_render_pkg_pkg_not_at_root_of_repo() {
456+
let mut pkg = tar::Builder::new(vec![]);
457+
add_file(
458+
&mut pkg,
459+
"foo-0.0.1/Cargo.toml",
460+
br#"
461+
[package]
462+
readme = "docs/README.md"
463+
repository = "https://github.com/foo/foo"
464+
"#,
465+
);
466+
add_file(
467+
&mut pkg,
468+
"foo-0.0.1/docs/README.md",
469+
b"docs/readme [link](./Other.md)",
470+
);
471+
add_file(
472+
&mut pkg,
473+
"foo-0.0.1/.cargo_vcs_info.json",
474+
br#"{"path_in_vcs": "path/in/vcs"}"#,
475+
);
476+
let serialized_archive = pkg.into_inner().unwrap();
477+
let result =
478+
render_pkg_readme(tar::Archive::new(&*serialized_archive), "foo-0.0.1").unwrap();
479+
assert!(result.contains("docs/readme"));
480+
assert!(
481+
result.contains("\"https://github.com/foo/foo/blob/HEAD/path/in/vcs/docs/./Other.md\"")
482+
)
483+
}
484+
485+
#[test]
486+
fn test_render_pkg_blank_vcs_info() {
487+
let mut pkg = tar::Builder::new(vec![]);
488+
add_file(
489+
&mut pkg,
490+
"foo-0.0.1/Cargo.toml",
491+
br#"
492+
[package]
493+
readme = "docs/README.md"
494+
repository = "https://github.com/foo/foo"
495+
"#,
496+
);
497+
add_file(
498+
&mut pkg,
499+
"foo-0.0.1/docs/README.md",
500+
b"docs/readme [link](./Other.md)",
501+
);
502+
add_file(&mut pkg, "foo-0.0.1/.cargo_vcs_info.json", br#"{}"#);
503+
let serialized_archive = pkg.into_inner().unwrap();
504+
let result =
505+
render_pkg_readme(tar::Archive::new(&*serialized_archive), "foo-0.0.1").unwrap();
506+
assert!(result.contains("docs/readme"));
507+
assert!(result.contains("\"https://github.com/foo/foo/blob/HEAD/docs/./Other.md\""))
508+
}
444509
}

src/controllers/krate/publish.rs

+63-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::models::{
1718
use crate::render;
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
};
@@ -193,9 +194,8 @@ pub fn publish(req: &mut dyn RequestExt) -> EndpointResult {
193194
LimitErrorReader::new(req.body(), maximums.max_upload_size).read_to_end(&mut tarball)?;
194195
let hex_cksum: String = Sha256::digest(&tarball).encode_hex();
195196
let pkg_name = format!("{}-{}", krate.name, vers);
196-
verify_tarball(&pkg_name, &tarball, maximums.max_unpack_size)?;
197-
198-
let pkg_path_in_vcs = None;
197+
let cargo_vcs_info = verify_tarball(&pkg_name, &tarball, maximums.max_unpack_size)?;
198+
let pkg_path_in_vcs = cargo_vcs_info.map(|info| info.path_in_vcs);
199199

200200
if let Some(readme) = new_crate.readme {
201201
render::render_and_upload_readme(
@@ -366,7 +366,11 @@ pub fn add_dependencies(
366366
Ok(git_deps)
367367
}
368368

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

@@ -376,8 +380,12 @@ fn verify_tarball(pkg_name: &str, tarball: &[u8], max_unpack: u64) -> AppResult<
376380

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

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

408422
#[cfg(test)]
@@ -422,12 +436,51 @@ mod tests {
422436
#[test]
423437
fn verify_tarball_test() {
424438
let mut pkg = tar::Builder::new(vec![]);
425-
add_file(&mut pkg, "foo-0.0.1/.cargo_vcs_info.json", br#"{}"#);
439+
add_file(&mut pkg, "foo-0.0.1/Cargo.toml", b"");
426440
let mut serialized_archive = vec![];
427441
GzEncoder::new(pkg.into_inner().unwrap().as_slice(), Default::default())
428442
.read_to_end(&mut serialized_archive)
429443
.unwrap();
430-
verify_tarball("foo-0.0.1", &serialized_archive, 512 * 1024 * 1024).unwrap();
444+
let vcs_info = verify_tarball("foo-0.0.1", &serialized_archive, 512 * 1024 * 1024).unwrap();
445+
assert!(vcs_info.is_none());
431446
verify_tarball("bar-0.0.1", &serialized_archive, 512 * 1024 * 1024).unwrap_err();
432447
}
448+
449+
#[test]
450+
fn verify_tarball_test_incomplete_vcs_info() {
451+
let mut pkg = tar::Builder::new(vec![]);
452+
add_file(&mut pkg, "foo-0.0.1/Cargo.toml", b"");
453+
add_file(
454+
&mut pkg,
455+
"foo-0.0.1/.cargo_vcs_info.json",
456+
br#"{"unknown": "field"}"#,
457+
);
458+
let mut serialized_archive = vec![];
459+
GzEncoder::new(pkg.into_inner().unwrap().as_slice(), Default::default())
460+
.read_to_end(&mut serialized_archive)
461+
.unwrap();
462+
let vcs_info = verify_tarball("foo-0.0.1", &serialized_archive, 512 * 1024 * 1024)
463+
.unwrap()
464+
.unwrap();
465+
assert_eq!(vcs_info.path_in_vcs, "");
466+
}
467+
468+
#[test]
469+
fn verify_tarball_test_vcs_info() {
470+
let mut pkg = tar::Builder::new(vec![]);
471+
add_file(&mut pkg, "foo-0.0.1/Cargo.toml", b"");
472+
add_file(
473+
&mut pkg,
474+
"foo-0.0.1/.cargo_vcs_info.json",
475+
br#"{"path_in_vcs": "path/in/vcs"}"#,
476+
);
477+
let mut serialized_archive = vec![];
478+
GzEncoder::new(pkg.into_inner().unwrap().as_slice(), Default::default())
479+
.read_to_end(&mut serialized_archive)
480+
.unwrap();
481+
let vcs_info = verify_tarball("foo-0.0.1", &serialized_archive, 512 * 1024 * 1024)
482+
.unwrap()
483+
.unwrap();
484+
assert_eq!(vcs_info.path_in_vcs, "path/in/vcs");
485+
}
433486
}

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)