@@ -344,19 +344,32 @@ fn parse_version_from_uv_dir_name(dir_name: &str) -> Option<String> {
344344 let mut parts = dir_name. splitn ( 3 , '-' ) ;
345345 let _impl = parts. next ( ) ?;
346346 let version = parts. next ( ) ?;
347- // Verify at minimum X.Y format (e.g., "3.12" or "3.12.3")
348- // and that each component starts with a digit. Trailing alpha chars are
349- // allowed on the last component to support pre-release versions like "3.14.0a4".
347+ // Require the platform segment to exist (rejects bare "<impl>-<version>").
348+ let platform = parts. next ( ) ?;
349+ if platform. is_empty ( ) {
350+ return None ;
351+ }
352+ // Verify at minimum X.Y format (e.g., "3.12" or "3.12.3").
353+ // Components before the last must be purely numeric.
354+ // The last component may have a pre-release suffix (e.g., "0a4", "0rc1").
350355 let components: Vec < & str > = version. split ( '.' ) . collect ( ) ;
351- if components. len ( ) >= 2
352- && components
353- . iter ( )
354- . all ( |c| !c. is_empty ( ) && c. starts_with ( |ch : char | ch. is_ascii_digit ( ) ) )
356+ if components. len ( ) < 2 {
357+ return None ;
358+ }
359+ // All components except the last must be purely numeric.
360+ let all_but_last = & components[ ..components. len ( ) - 1 ] ;
361+ if !all_but_last
362+ . iter ( )
363+ . all ( |c| !c. is_empty ( ) && c. chars ( ) . all ( |ch| ch. is_ascii_digit ( ) ) )
355364 {
356- Some ( version. to_string ( ) )
357- } else {
358- None
365+ return None ;
359366 }
367+ // The last component must start with a digit (allows pre-release suffix like "0a4").
368+ let last = components. last ( ) ?;
369+ if last. is_empty ( ) || !last. starts_with ( |ch : char | ch. is_ascii_digit ( ) ) {
370+ return None ;
371+ }
372+ Some ( version. to_string ( ) )
360373}
361374
362375/// Walks up from `project_path` looking for a workspace that this project belongs to.
@@ -1123,6 +1136,17 @@ exclude = ["packages/legacy"]"#;
11231136 ) ;
11241137 // Empty component after dot.
11251138 assert_eq ! ( parse_version_from_uv_dir_name( "cpython-3.12.-linux" ) , None ) ;
1139+ // Non-numeric middle component must be rejected even if it starts with a digit.
1140+ assert_eq ! (
1141+ parse_version_from_uv_dir_name( "cpython-3.12abc.1-linux" ) ,
1142+ None
1143+ ) ;
1144+ }
1145+
1146+ #[ test]
1147+ fn test_parse_version_from_uv_dir_name_rejects_missing_platform ( ) {
1148+ // Bare "<impl>-<version>" without platform segment should be rejected.
1149+ assert_eq ! ( parse_version_from_uv_dir_name( "cpython-3.12" ) , None ) ;
11261150 }
11271151
11281152 #[ test]
0 commit comments