Skip to content

Commit 8369c35

Browse files
committed
Fix registry download URL template expansion
1 parent 3862afd commit 8369c35

4 files changed

Lines changed: 191 additions & 17 deletions

File tree

src/config.rs

Lines changed: 125 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
11
use crate::dirs::crate_prefix;
22
use serde_derive::Deserialize;
33

4+
/// marker / template vars for generating download urls.
5+
const CRATE_MARKER: &str = "{crate}";
6+
const VERSION_MARKER: &str = "{version}";
7+
const PREFIX_MARKER: &str = "{prefix}";
8+
const LOWERPREFIX_MARKER: &str = "{lowerprefix}";
9+
const SHA256_CHECKSUM_MARKER: &str = "{sha256-checksum}";
10+
const DOWNLOAD_URL_MARKERS: [&str; 5] = [
11+
CRATE_MARKER,
12+
VERSION_MARKER,
13+
PREFIX_MARKER,
14+
LOWERPREFIX_MARKER,
15+
SHA256_CHECKSUM_MARKER,
16+
];
17+
418
/// Global configuration of an index, reflecting the [contents of config.json](https://doc.rust-lang.org/cargo/reference/registries.html#index-format).
519
#[derive(Clone, Debug, Deserialize)]
620
pub struct IndexConfig {
7-
/// Pattern for creating download URLs. Use [`IndexConfig::download_url`] instead.
21+
/// Pattern for creating download URLs. Use [`IndexConfig::download_url`] or
22+
/// [`IndexConfig::download_url_with_checksum`] instead.
823
pub dl: String,
924
/// Base URL for publishing, etc.
1025
pub api: Option<String>,
@@ -14,13 +29,27 @@ impl IndexConfig {
1429
/// Get the URL from where the specified package can be downloaded.
1530
/// This method assumes the particular version is present in the registry,
1631
/// and does not verify that it is.
32+
///
33+
/// Returns `None` when the configured URL requires the
34+
/// `{sha256-checksum}` placeholder. Use [`Self::download_url_with_checksum`]
35+
/// when the checksum is available.
1736
#[must_use]
1837
pub fn download_url(&self, name: &str, version: &str) -> Option<String> {
19-
if !self.dl.contains("{crate}")
20-
&& !self.dl.contains("{version}")
21-
&& !self.dl.contains("{prefix}")
22-
&& !self.dl.contains("{lowerprefix}")
23-
{
38+
self.download_url_inner(name, version, None)
39+
}
40+
41+
/// Get the URL from where the specified package can be downloaded, including
42+
/// the package checksum when required by the registry's URL template.
43+
///
44+
/// This method assumes the particular version is present in the registry,
45+
/// and does not verify that it is.
46+
#[must_use]
47+
pub fn download_url_with_checksum(&self, name: &str, version: &str, checksum: &[u8; 32]) -> Option<String> {
48+
self.download_url_inner(name, version, Some(checksum))
49+
}
50+
51+
fn download_url_inner(&self, name: &str, version: &str, checksum: Option<&[u8; 32]>) -> Option<String> {
52+
if !DOWNLOAD_URL_MARKERS.iter().any(|marker| self.dl.contains(marker)) {
2453
let mut new = String::with_capacity(self.dl.len() + name.len() + version.len() + 10);
2554
new.push_str(&self.dl);
2655
new.push('/');
@@ -30,15 +59,99 @@ impl IndexConfig {
3059
new.push_str("/download");
3160
Some(new)
3261
} else {
33-
let mut prefix = String::with_capacity(5);
34-
crate_prefix(&mut prefix, name, '/')?;
62+
let prefix = if self.dl.contains(PREFIX_MARKER) || self.dl.contains(LOWERPREFIX_MARKER) {
63+
let mut prefix = String::with_capacity(5);
64+
crate_prefix(&mut prefix, name, '/')?;
65+
Some(prefix)
66+
} else {
67+
None
68+
};
69+
let lowerprefix = prefix.as_ref().map(|prefix| prefix.to_ascii_lowercase());
70+
let checksum = checksum.map(hex::encode);
71+
if self.dl.contains(SHA256_CHECKSUM_MARKER) && checksum.is_none() {
72+
return None;
73+
}
74+
3575
Some(
3676
self.dl
37-
.replace("{crate}", name)
38-
.replace("{version}", version)
39-
.replace("{prefix}", &prefix)
40-
.replace("{lowerprefix}", &prefix.to_ascii_lowercase()),
77+
.replace(CRATE_MARKER, name)
78+
.replace(VERSION_MARKER, version)
79+
.replace(PREFIX_MARKER, prefix.as_deref().unwrap_or_default())
80+
.replace(LOWERPREFIX_MARKER, lowerprefix.as_deref().unwrap_or_default())
81+
.replace(SHA256_CHECKSUM_MARKER, checksum.as_deref().unwrap_or_default()),
4182
)
4283
}
4384
}
4485
}
86+
87+
#[cfg(test)]
88+
mod tests {
89+
use super::IndexConfig;
90+
use crate::Crate;
91+
92+
fn config(dl: &str) -> IndexConfig {
93+
IndexConfig {
94+
dl: dl.to_owned(),
95+
api: None,
96+
}
97+
}
98+
99+
#[test]
100+
fn appends_the_default_download_path() {
101+
assert_eq!(
102+
config("https://example.invalid/crates")
103+
.download_url("demo", "1.2.3")
104+
.as_deref(),
105+
Some("https://example.invalid/crates/demo/1.2.3/download"),
106+
);
107+
}
108+
109+
#[test]
110+
fn substitutes_all_documented_markers() {
111+
let checksum = [0xab; 32];
112+
let config = config("https://example.invalid/{crate}/{version}/{prefix}/{lowerprefix}/{sha256-checksum}");
113+
114+
assert_eq!(
115+
config
116+
.download_url_with_checksum("MyCrate", "1.2.3", &checksum)
117+
.as_deref(),
118+
Some(
119+
"https://example.invalid/MyCrate/1.2.3/My/Cr/my/cr/\
120+
abababababababababababababababababababababababababababababababab"
121+
),
122+
);
123+
}
124+
125+
#[test]
126+
fn checksum_marker_requires_a_checksum() {
127+
assert_eq!(
128+
config("https://example.invalid/{sha256-checksum}").download_url("demo", "1.2.3"),
129+
None,
130+
);
131+
}
132+
133+
#[test]
134+
fn templates_without_prefix_markers_do_not_build_one() {
135+
assert_eq!(
136+
config("https://example.invalid/{crate}/{version}")
137+
.download_url("", "1.2.3")
138+
.as_deref(),
139+
Some("https://example.invalid//1.2.3"),
140+
);
141+
}
142+
143+
#[test]
144+
fn version_download_url_supplies_its_checksum() {
145+
let krate = Crate::from_slice(
146+
br#"{"name":"demo","vers":"1.2.3","deps":[],"cksum":"abababababababababababababababababababababababababababababababab","features":{}}"#,
147+
)
148+
.unwrap();
149+
150+
assert_eq!(
151+
krate.versions()[0]
152+
.download_url(&config("https://example.invalid/{sha256-checksum}"))
153+
.as_deref(),
154+
Some("https://example.invalid/abababababababababababababababababababababababababababababababab"),
155+
);
156+
}
157+
}

src/dirs.rs

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,17 @@ pub fn local_path_and_canonical_url_with_hash_kind(
3232
Ok((path, canonical_url))
3333
}
3434

35+
/// Appends Cargo's case-preserving `{prefix}` value.
3536
pub(crate) fn crate_prefix(accumulator: &mut String, crate_name: &str, separator: char) -> Option<()> {
37+
crate_prefix_inner(accumulator, crate_name, separator, false)
38+
}
39+
40+
/// Appends Cargo's lowercased `{lowerprefix}` value.
41+
pub(crate) fn crate_lowerprefix(accumulator: &mut String, crate_name: &str, separator: char) -> Option<()> {
42+
crate_prefix_inner(accumulator, crate_name, separator, true)
43+
}
44+
45+
fn crate_prefix_inner(accumulator: &mut String, crate_name: &str, separator: char, lowercase: bool) -> Option<()> {
3646
match crate_name.len() {
3747
0 => return None,
3848
1 => accumulator.push('1'),
@@ -45,7 +55,7 @@ pub(crate) fn crate_prefix(accumulator: &mut String, crate_name: &str, separator
4555
.as_bytes()
4656
.get(0..1)?
4757
.iter()
48-
.map(|c| c.to_ascii_lowercase() as char),
58+
.map(|c| (if lowercase { c.to_ascii_lowercase() } else { *c }) as char),
4959
);
5060
}
5161
_ => {
@@ -54,15 +64,15 @@ pub(crate) fn crate_prefix(accumulator: &mut String, crate_name: &str, separator
5464
.as_bytes()
5565
.get(0..2)?
5666
.iter()
57-
.map(|c| c.to_ascii_lowercase() as char),
67+
.map(|c| (if lowercase { c.to_ascii_lowercase() } else { *c }) as char),
5868
);
5969
accumulator.push(separator);
6070
accumulator.extend(
6171
crate_name
6272
.as_bytes()
6373
.get(2..4)?
6474
.iter()
65-
.map(|c| c.to_ascii_lowercase() as char),
75+
.map(|c| (if lowercase { c.to_ascii_lowercase() } else { *c }) as char),
6676
);
6777
}
6878
};
@@ -72,13 +82,54 @@ pub(crate) fn crate_prefix(accumulator: &mut String, crate_name: &str, separator
7282
pub(crate) fn crate_name_to_relative_path(crate_name: &str, separator: Option<char>) -> Option<String> {
7383
let separator = separator.unwrap_or(std::path::MAIN_SEPARATOR);
7484
let mut rel_path = String::with_capacity(crate_name.len() + 6);
75-
crate_prefix(&mut rel_path, crate_name, separator)?;
85+
crate_lowerprefix(&mut rel_path, crate_name, separator)?;
7686
rel_path.push(separator);
7787
rel_path.extend(crate_name.as_bytes().iter().map(|c| c.to_ascii_lowercase() as char));
7888

7989
Some(rel_path)
8090
}
8191

92+
#[cfg(test)]
93+
mod tests {
94+
use super::{crate_lowerprefix, crate_name_to_relative_path, crate_prefix};
95+
96+
#[test]
97+
fn crate_prefixes_follow_cargo_layout_rules() {
98+
for (name, prefix, lowerprefix) in [
99+
("a", "1", "1"),
100+
("ab", "2", "2"),
101+
("AbC", "3/A", "3/a"),
102+
("MyCrate", "My/Cr", "my/cr"),
103+
] {
104+
let mut actual_prefix = String::new();
105+
crate_prefix(&mut actual_prefix, name, '/').unwrap();
106+
assert_eq!(actual_prefix, prefix);
107+
108+
let mut actual_lowerprefix = String::new();
109+
crate_lowerprefix(&mut actual_lowerprefix, name, '/').unwrap();
110+
assert_eq!(actual_lowerprefix, lowerprefix);
111+
}
112+
}
113+
114+
#[test]
115+
fn empty_crate_names_have_no_prefix() {
116+
let mut prefix = String::new();
117+
assert!(crate_prefix(&mut prefix, "", '/').is_none());
118+
assert!(prefix.is_empty());
119+
120+
assert!(crate_lowerprefix(&mut prefix, "", '/').is_none());
121+
assert!(prefix.is_empty());
122+
}
123+
124+
#[test]
125+
fn local_index_paths_use_lowerprefix_and_lowercase_filenames() {
126+
assert_eq!(
127+
crate_name_to_relative_path("MyCrate", Some('/')).as_deref(),
128+
Some("my/cr/mycrate"),
129+
);
130+
}
131+
}
132+
82133
/// Matches https://github.com/rust-lang/cargo/blob/2928e32734b04925ee51e1ae88bea9a83d2fd451/crates/cargo-util-schemas/src/core/source_kind.rs#L5
83134
type SourceKind = u64;
84135
const SOURCE_KIND_REGISTRY: SourceKind = 2;

src/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ impl Version {
135135
/// Where to find crate tarball
136136
#[must_use]
137137
pub fn download_url(&self, index: &IndexConfig) -> Option<String> {
138-
index.download_url(&self.name, &self.vers)
138+
index.download_url_with_checksum(&self.name, &self.vers, &self.cksum)
139139
}
140140
}
141141

tests/sparse_index/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ mod with_sparse_http_feature {
7171
}
7272
}
7373

74+
#[test]
75+
fn crate_url_uses_a_lowercase_index_path() {
76+
let index = crates_io();
77+
78+
assert_eq!(
79+
index.crate_url("MyCrate").as_deref(),
80+
Some("https://index.crates.io/my/cr/mycrate"),
81+
);
82+
}
83+
7484
mod parse_cache_response {
7585
use crate::sparse_index::with_sparse_http_feature::crates_io;
7686
use http::header;

0 commit comments

Comments
 (0)