@@ -535,36 +535,53 @@ fn segments_starts_with<
535535 a : A ,
536536 b : B , // the prefix we're looking for in 'a'
537537) -> bool {
538+ let mut had_extra_left = false ;
538539 for ranges in a. zip_longest ( b) {
539540 let ( left, right) = match ranges {
540- EitherOrBoth :: Both ( left, right) => ( left, right) ,
541+ EitherOrBoth :: Both ( left, right) => {
542+ // Previous segment had extra left components, but there are more
543+ // prefix segments - this is a structural mismatch.
544+ // E.g., "1.1c.1" does NOT start with "1.1.1"
545+ if had_extra_left {
546+ return false ;
547+ }
548+ ( left, right)
549+ }
550+ // Extra segments in version after prefix is exhausted - OK
551+ // E.g., "1.0.1.2" starts with "1.0.1"
541552 EitherOrBoth :: Left ( _) => return true ,
542553 EitherOrBoth :: Right ( segment) => {
543- // If the segment is zero we can skip it. As long as there are
544- // only zeros, the version is still considered to start with
545- // the other version.
554+ // Prefix has more segments. Zero segments are OK (implicit zeros).
555+ // E.g., "1.0" starts with "1.0.0"
546556 if segment. is_zero ( ) {
547557 continue ;
548558 }
549559 return false ;
550560 }
551561 } ;
562+ had_extra_left = false ;
552563 for values in left. components ( ) . zip_longest ( right. components ( ) ) {
553- if !match values {
554- EitherOrBoth :: Both ( a, b) => a == b,
555-
556- EitherOrBoth :: Left ( component) => {
557- // If the component is zero we can skip it. If there are
558- // no more right components and there are still left components,
559- // we should consider the right component to be 0.
564+ match values {
565+ EitherOrBoth :: Both ( a, b) => {
566+ if a != b {
567+ return false ;
568+ }
569+ }
570+ // Extra components in version segment. Only OK if this is the last
571+ // prefix segment (checked on next outer iteration).
572+ // E.g., "1.0.1c" starts with "1.0.1"
573+ EitherOrBoth :: Left ( _) => {
574+ had_extra_left = true ;
575+ break ;
576+ }
577+ // Missing component in version. Zero components are OK (implicit zeros).
578+ // E.g., "1.1c.1" starts with "1.1c0.1"
579+ EitherOrBoth :: Right ( component) => {
560580 if component. is_zero ( ) {
561581 continue ;
562582 }
563583 return false ;
564584 }
565- EitherOrBoth :: Right ( _) => return false ,
566- } {
567- return false ;
568585 }
569586 }
570587 }
@@ -1289,17 +1306,90 @@ mod test {
12891306 }
12901307
12911308 #[ test]
1292- fn starts_with_matches_extra_component_against_zero ( ) {
1309+ fn starts_with_extra_components ( ) {
1310+ // For glob matching (e.g., "1.0.0_version*"), versions with extra
1311+ // components should match the prefix.
12931312 let version = Version :: from_str ( "1.0.0_version" ) . unwrap ( ) ;
1294- assert ! ( !Version :: from_str( "1.0.0_version1" )
1313+ // "1.0.0_version1" starts with "1.0.0_version" (extra component "1")
1314+ assert ! ( Version :: from_str( "1.0.0_version1" )
12951315 . unwrap( )
12961316 . starts_with( & version) ) ;
1317+ // "1.0.0_version0" starts with "1.0.0_version" (extra component "0")
12971318 assert ! ( Version :: from_str( "1.0.0_version0" )
12981319 . unwrap( )
12991320 . starts_with( & version) ) ;
1300- assert ! ( !Version :: from_str( "1.0.0_version0foo" )
1321+ // "1.0.0_version0foo" starts with "1.0.0_version" (extra components "0", "foo")
1322+ assert ! ( Version :: from_str( "1.0.0_version0foo" )
1323+ . unwrap( )
1324+ . starts_with( & version) ) ;
1325+
1326+ // But different base components should NOT match
1327+ assert ! ( !Version :: from_str( "1.0.0_other" )
1328+ . unwrap( )
1329+ . starts_with( & version) ) ;
1330+ assert ! ( !Version :: from_str( "1.0.0_ver" )
1331+ . unwrap( )
1332+ . starts_with( & version) ) ;
1333+
1334+ // Different segment structure should NOT match (PR #1791)
1335+ // "1.0.0_version1" has segment [version, 1]
1336+ // "1.0.0_version_2" has segments [version], [2]
1337+ // This ensures "1.0.0_version_2*" does NOT match "1.0.0_version1"
1338+ let version_with_extra_segment = Version :: from_str ( "1.0.0_version_2" ) . unwrap ( ) ;
1339+ assert ! ( !Version :: from_str( "1.0.0_version1" )
1340+ . unwrap( )
1341+ . starts_with( & version_with_extra_segment) ) ;
1342+
1343+ // Different component values should NOT match
1344+ let version1 = Version :: from_str ( "1.0.0_version1" ) . unwrap ( ) ;
1345+ assert ! ( !Version :: from_str( "1.0.0_version0" )
1346+ . unwrap( )
1347+ . starts_with( & version1) ) ;
1348+
1349+ // Extra components after matching prefix should match
1350+ assert ! ( Version :: from_str( "1.0.0_version1a" )
1351+ . unwrap( )
1352+ . starts_with( & version1) ) ;
1353+ assert ! ( Version :: from_str( "1.0.0_version0a" )
13011354 . unwrap( )
13021355 . starts_with( & version) ) ;
1356+
1357+ // Extra components in INTERMEDIATE segments should NOT match
1358+ // "1.1c.1" has segment [1, c] where "1.1.1" has segment [1]
1359+ // This is a structure mismatch, not just extra components at the end
1360+ assert ! ( !Version :: from_str( "1.1c.1" )
1361+ . unwrap( )
1362+ . starts_with( & Version :: from_str( "1.1.1" ) . unwrap( ) ) ) ;
1363+ assert ! ( !Version :: from_str( "1.1c1.1" )
1364+ . unwrap( )
1365+ . starts_with( & Version :: from_str( "1.1c.1" ) . unwrap( ) ) ) ;
1366+
1367+ // BUT zero components in prefix are treated as "no component" (implicit zeros)
1368+ // So "1.1c.1" starts with "1.1c0.1" because c0 == c (trailing zero)
1369+ assert ! ( Version :: from_str( "1.1c.1" )
1370+ . unwrap( )
1371+ . starts_with( & Version :: from_str( "1.1c0.1" ) . unwrap( ) ) ) ;
1372+ }
1373+
1374+ /// Test for <https://github.com/conda/rattler/issues/1914>
1375+ /// Versions with letter suffixes (like openssl 1.0.1c) should match
1376+ /// prefix patterns (like 1.0.1*).
1377+ #[ test]
1378+ fn starts_with_letter_suffix ( ) {
1379+ // openssl versions like 1.0.1c, 1.0.1g, etc. should start with 1.0.1
1380+ let prefix = Version :: from_str ( "1.0.1" ) . unwrap ( ) ;
1381+ assert ! ( Version :: from_str( "1.0.1c" ) . unwrap( ) . starts_with( & prefix) ) ;
1382+ assert ! ( Version :: from_str( "1.0.1g" ) . unwrap( ) . starts_with( & prefix) ) ;
1383+ assert ! ( Version :: from_str( "1.0.1k" ) . unwrap( ) . starts_with( & prefix) ) ;
1384+
1385+ // Also test 1.0.2 series
1386+ let prefix_2 = Version :: from_str ( "1.0.2" ) . unwrap ( ) ;
1387+ assert ! ( Version :: from_str( "1.0.2a" ) . unwrap( ) . starts_with( & prefix_2) ) ;
1388+ assert ! ( Version :: from_str( "1.0.2l" ) . unwrap( ) . starts_with( & prefix_2) ) ;
1389+
1390+ // Negative cases - these should NOT match
1391+ assert ! ( !Version :: from_str( "1.0.2a" ) . unwrap( ) . starts_with( & prefix) ) ;
1392+ assert ! ( !Version :: from_str( "1.0.1c" ) . unwrap( ) . starts_with( & prefix_2) ) ;
13031393 }
13041394
13051395 fn get_hash ( spec : & impl Hash ) -> u64 {
0 commit comments