Skip to content

Commit 1318f92

Browse files
committed
fix(toolchain): restrict named toolchain characters
1 parent 4764169 commit 1318f92

3 files changed

Lines changed: 104 additions & 32 deletions

File tree

src/toolchain/names.rs

Lines changed: 85 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -117,19 +117,6 @@ macro_rules! try_from_str {
117117
};
118118
}
119119

120-
/// Common validate rules for all sorts of toolchain names
121-
fn validate(candidate: &str) -> Result<&str, InvalidName> {
122-
if let Some(without_plus) = candidate.strip_prefix('+') {
123-
return Err(InvalidName::PlusPrefix(without_plus.to_string()));
124-
}
125-
let normalized_name = candidate.trim_end_matches('/');
126-
if normalized_name.is_empty() {
127-
Err(InvalidName::ToolchainName(candidate.into()))
128-
} else {
129-
Ok(normalized_name)
130-
}
131-
}
132-
133120
/// A toolchain name from user input.
134121
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
135122
pub(crate) enum ResolvableToolchainName {
@@ -152,7 +139,7 @@ impl ResolvableToolchainName {
152139
// If candidate could be resolved, return a ready to resolve version of it.
153140
// Otherwise error.
154141
fn validate(candidate: &str) -> Result<ResolvableToolchainName, InvalidName> {
155-
let candidate = validate(candidate)?;
142+
let candidate = validate_named_toolchain(candidate)?;
156143
candidate
157144
.parse::<PartialToolchainDesc>()
158145
.map(ResolvableToolchainName::Official)
@@ -226,7 +213,7 @@ pub(crate) enum MaybeOfficialToolchainName {
226213

227214
impl MaybeOfficialToolchainName {
228215
fn validate(candidate: &str) -> Result<MaybeOfficialToolchainName, InvalidName> {
229-
let candidate = validate(candidate)?;
216+
let candidate = validate_named_toolchain(candidate)?;
230217
if candidate == "none" {
231218
Ok(MaybeOfficialToolchainName::None)
232219
} else {
@@ -262,7 +249,7 @@ pub enum ToolchainName {
262249
impl ToolchainName {
263250
/// If the string is already resolved, allow direct conversion
264251
fn validate(candidate: &str) -> Result<Self, InvalidName> {
265-
let candidate = validate(candidate)?;
252+
let candidate = validate_named_toolchain(candidate)?;
266253
candidate
267254
.parse::<ToolchainDesc>()
268255
.map(ToolchainName::Official)
@@ -311,8 +298,13 @@ impl ResolvableLocalToolchainName {
311298
ResolvableToolchainName::try_from(candidate)
312299
.map(ResolvableLocalToolchainName::Named)
313300
.or_else(|_| {
314-
PathBasedToolchainName::try_from(&PathBuf::from(candidate) as &Path)
315-
.map(ResolvableLocalToolchainName::Path)
301+
let path = PathBuf::from(candidate);
302+
if candidate.contains('/') || candidate.contains('\\') {
303+
PathBasedToolchainName::try_from(&path as &Path)
304+
.map(ResolvableLocalToolchainName::Path)
305+
} else {
306+
Err(InvalidName::ToolchainName(candidate.into()))
307+
}
316308
})
317309
}
318310
}
@@ -394,6 +386,7 @@ impl CustomToolchainName {
394386
|| candidate == "none"
395387
|| candidate.contains('/')
396388
|| candidate.contains('\\')
389+
|| !is_legal_named_toolchain(candidate)
397390
{
398391
Err(InvalidName::CustomName(candidate.into()))
399392
} else {
@@ -473,6 +466,36 @@ impl Deref for PathBasedToolchainName {
473466
}
474467
}
475468

469+
fn validate_named_toolchain(candidate: &str) -> Result<&str, InvalidName> {
470+
let candidate = validate(candidate)?;
471+
if is_legal_named_toolchain(candidate) {
472+
Ok(candidate)
473+
} else {
474+
Err(InvalidName::ToolchainName(candidate.into()))
475+
}
476+
}
477+
478+
/// Common validate rules for all sorts of toolchain names
479+
fn validate(candidate: &str) -> Result<&str, InvalidName> {
480+
if let Some(without_plus) = candidate.strip_prefix('+') {
481+
return Err(InvalidName::PlusPrefix(without_plus.to_string()));
482+
}
483+
let normalized_name = candidate.trim_end_matches('/');
484+
if normalized_name.is_empty() {
485+
Err(InvalidName::ToolchainName(candidate.into()))
486+
} else {
487+
Ok(normalized_name)
488+
}
489+
}
490+
491+
fn is_legal_named_toolchain(candidate: &str) -> bool {
492+
!matches!(candidate, "." | "..") && candidate.chars().all(is_legal_named_toolchain_char)
493+
}
494+
495+
fn is_legal_named_toolchain_char(c: char) -> bool {
496+
c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')
497+
}
498+
476499
#[cfg(test)]
477500
mod tests {
478501
use std::str::FromStr;
@@ -490,13 +513,21 @@ mod tests {
490513
fn partial_toolchain_desc_regex() -> String {
491514
let tuple_regex = format!(
492515
r"(-({}))?(?:-({}))?(?:-({}))?",
493-
LIST_ARCHS.join("|"),
494-
LIST_OSES.join("|"),
495-
LIST_ENVS.join("|")
516+
regex_alternates(LIST_ARCHS),
517+
regex_alternates(LIST_OSES),
518+
regex_alternates(LIST_ENVS)
496519
);
497520
r"(nightly|beta|stable|[0-9]{1}(\.(0|[1-9][0-9]{0,2}))(\.(0|[1-9][0-9]{0,1}))?(-beta(\.(0|[1-9][1-9]{0,1}))?)?)(-([0-9]{4}-[0-9]{2}-[0-9]{2}))?".to_owned() + &tuple_regex
498521
}
499522

523+
fn regex_alternates(values: &[&str]) -> String {
524+
values
525+
.iter()
526+
.map(|value| regex::escape(value))
527+
.collect::<Vec<_>>()
528+
.join("|")
529+
}
530+
500531
prop_compose! {
501532
fn arb_partial_toolchain_desc()
502533
(s in string_regex(&partial_toolchain_desc_regex()).unwrap()) -> String {
@@ -506,9 +537,8 @@ mod tests {
506537

507538
prop_compose! {
508539
fn arb_custom_name()
509-
(s in r"[^\\/+][^\\/]*") -> String {
540+
(s in r"[A-Za-z0-9._-]+") -> String {
510541
// perhaps need to filter 'none' and partial toolchains - but they won't typically be generated anyway.
511-
// Also filter '+' prefix as that's reserved for +toolchain syntax.
512542
s
513543
}
514544
}
@@ -541,11 +571,18 @@ mod tests {
541571

542572
#[test]
543573
fn test_parse_custom(name in arb_custom_name()) {
574+
prop_assume!(name != "none");
575+
prop_assume!(name != ".");
576+
prop_assume!(name != "..");
577+
prop_assume!(PartialToolchainDesc::from_str(&name).is_err());
544578
CustomToolchainName::try_from(name).unwrap();
545579
}
546580

547581
#[test]
548582
fn test_parse_resolvable_name(name in arb_resolvable_name()) {
583+
prop_assume!(name != "none");
584+
prop_assume!(name != ".");
585+
prop_assume!(name != "..");
549586
ResolvableToolchainName::try_from(name).unwrap();
550587
}
551588

@@ -577,10 +614,10 @@ mod tests {
577614
"1.8.0-x86_64-apple-darwin",
578615
"1.8.0-x86_64-unknown-linux-gnu",
579616
"1.10.0-x86_64-unknown-linux-gnu",
580-
"bar(baz)",
581-
"foo#bar",
582-
"the cake is a lie",
583-
"this.is.not-a+semver",
617+
"bar.baz",
618+
"foo_bar",
619+
"stage1-local",
620+
"this.is.not-a_semver",
584621
]
585622
.into_iter()
586623
.map(|s| ToolchainName::try_from(s).unwrap())
@@ -601,11 +638,11 @@ mod tests {
601638
"1.8.0-beta-x86_64-apple-darwin",
602639
"1.8.0-beta.2-x86_64-apple-darwin",
603640
// https://github.com/rust-lang/rustup/issues/3517
604-
"foo#bar",
605-
"bar(baz)",
606-
"this.is.not-a+semver",
641+
"foo_bar",
642+
"bar.baz",
643+
"this.is.not-a_semver",
607644
// https://github.com/rust-lang/rustup/issues/3168
608-
"the cake is a lie",
645+
"stage1-local",
609646
]
610647
.into_iter()
611648
.map(|s| ToolchainName::try_from(s).unwrap())
@@ -615,4 +652,21 @@ mod tests {
615652

616653
assert_eq!(expected, v);
617654
}
655+
656+
#[test]
657+
fn custom_names_reject_special_characters() {
658+
for name in [
659+
"bar(baz)",
660+
"foo#bar",
661+
"the cake is a lie",
662+
"this.is.not-a+semver",
663+
"quote'toolchain",
664+
".",
665+
"..",
666+
] {
667+
CustomToolchainName::try_from(name).unwrap_err();
668+
ResolvableToolchainName::try_from(name).unwrap_err();
669+
ToolchainName::try_from(name).unwrap_err();
670+
}
671+
}
618672
}

tests/suite/cli_misc.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,24 @@ error:[..] invalid custom toolchain name 'beta'
9898
...
9999
error:[..] invalid custom toolchain name 'stable'
100100
...
101+
"#]])
102+
.is_err();
103+
cx.config
104+
.expect(["rustup", "toolchain", "link", "bad name", "foo"])
105+
.await
106+
.with_stderr(snapbox::str![[r#"
107+
...
108+
error:[..] invalid custom toolchain name 'bad name'
109+
...
110+
"#]])
111+
.is_err();
112+
cx.config
113+
.expect(["rustup", "toolchain", "link", "foo#bar", "foo"])
114+
.await
115+
.with_stderr(snapbox::str![[r#"
116+
...
117+
error:[..] invalid custom toolchain name 'foo#bar'
118+
...
101119
"#]])
102120
.is_err();
103121
}

tests/suite/cli_rustup.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3747,7 +3747,7 @@ async fn non_utf8_toolchain() {
37473747
.await
37483748
.with_stderr(snapbox::str![[r#"
37493749
...
3750-
error: toolchain '�(' is not installed
3750+
error: invalid toolchain name '�('
37513751
...
37523752
"#]]);
37533753
}

0 commit comments

Comments
 (0)