Skip to content

Commit 01b8537

Browse files
committed
Extract cargo vcs info from tarball
Supported in rust-lang/cargo#9866
1 parent c4f1054 commit 01b8537

File tree

3 files changed

+130
-10
lines changed

3 files changed

+130
-10
lines changed

src/admin/render_readmes.rs

+69-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,62 @@ 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!(result.contains("\"https://github.com/foo/foo/blob/HEAD/path/in/vcs/docs/./Other.md\""))
481+
}
482+
483+
#[test]
484+
fn test_render_pkg_blank_vcs_info() {
485+
let mut pkg = tar::Builder::new(vec![]);
486+
add_file(
487+
&mut pkg,
488+
"foo-0.0.1/Cargo.toml",
489+
br#"
490+
[package]
491+
readme = "docs/README.md"
492+
repository = "https://github.com/foo/foo"
493+
"#,
494+
);
495+
add_file(
496+
&mut pkg,
497+
"foo-0.0.1/docs/README.md",
498+
b"docs/readme [link](./Other.md)",
499+
);
500+
add_file(
501+
&mut pkg,
502+
"foo-0.0.1/.cargo_vcs_info.json",
503+
br#"{}"#,
504+
);
505+
let serialized_archive = pkg.into_inner().unwrap();
506+
let result =
507+
render_pkg_readme(tar::Archive::new(&*serialized_archive), "foo-0.0.1").unwrap();
508+
assert!(result.contains("docs/readme"));
509+
assert!(result.contains("\"https://github.com/foo/foo/blob/HEAD/docs/./Other.md\""))
510+
}
444511
}

src/controllers/krate/publish.rs

+18-8
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
};
@@ -192,9 +193,8 @@ pub fn publish(req: &mut dyn RequestExt) -> EndpointResult {
192193
let mut tarball = Vec::new();
193194
LimitErrorReader::new(req.body(), maximums.max_upload_size).read_to_end(&mut tarball)?;
194195
let hex_cksum: String = Sha256::digest(&tarball).encode_hex();
195-
verify_tarball(&krate, vers, &tarball, maximums.max_unpack_size)?;
196-
197-
let pkg_path_in_vcs = None;
196+
let cargo_vcs_info = verify_tarball(&krate, vers, &tarball, maximums.max_unpack_size)?;
197+
let pkg_path_in_vcs = cargo_vcs_info.map(|info| info.path_in_vcs);
198198

199199
if let Some(readme) = new_crate.readme {
200200
render::render_and_upload_readme(
@@ -370,7 +370,7 @@ fn verify_tarball(
370370
vers: &semver::Version,
371371
tarball: &[u8],
372372
max_unpack: u64,
373-
) -> AppResult<()> {
373+
) -> AppResult<Option<CargoVcsInfo>> {
374374
// All our data is currently encoded with gzip
375375
let decoder = GzDecoder::new(tarball);
376376

@@ -380,9 +380,13 @@ fn verify_tarball(
380380

381381
// Use this I/O object now to take a peek inside
382382
let mut archive = tar::Archive::new(decoder);
383+
383384
let prefix = format!("{}-{}", krate.name, vers);
385+
let vcs_info_path = Path::new(&prefix).join(".cargo_vcs_info.json");
386+
let mut vcs_info = None;
387+
384388
for entry in archive.entries()? {
385-
let entry = entry.map_err(|err| {
389+
let mut entry = entry.map_err(|err| {
386390
err.chain(cargo_err(
387391
"uploaded tarball is malformed or too large when decompressed",
388392
))
@@ -393,9 +397,15 @@ fn verify_tarball(
393397
// upload a tarball that contains both `foo-0.1.0/` source code as well
394398
// as `bar-0.1.0/` source code, and this could overwrite other crates in
395399
// the registry!
396-
if !entry.path()?.starts_with(&prefix) {
400+
let entry_path = entry.path()?;
401+
if !entry_path.starts_with(&prefix) {
397402
return Err(cargo_err("invalid tarball uploaded"));
398403
}
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+
}
399409

400410
// Historical versions of the `tar` crate which Cargo uses internally
401411
// don't properly prevent hard links and symlinks from overwriting
@@ -407,7 +417,7 @@ fn verify_tarball(
407417
return Err(cargo_err("invalid tarball uploaded"));
408418
}
409419
}
410-
Ok(())
420+
Ok(vcs_info)
411421
}
412422

413423
#[cfg(test)]

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)