@@ -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 ) ]
135122pub ( 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
227214impl 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 {
262249impl 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) ]
477500mod 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}
0 commit comments