Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions e2e/plugins/test_version_range
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ assert "mise current tiny" "2.1.0"

mise local tiny@sub-0.1:3.1
assert "mise current tiny" "3.0.1"

mise local tiny@sub-0.0.1:3.0.0
assert "mise current tiny" "2.1.0"
32 changes: 30 additions & 2 deletions src/toolset/tool_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,15 +303,31 @@ impl ToolRequest {
/// subtracts sub from orig and removes suffix
/// e.g. version_sub("18.2.3", "2") -> "16"
/// e.g. version_sub("18.2.3", "0.1") -> "18.1"
/// e.g. version_sub("2.79.0", "0.0.1") -> "2.78" (underflow, returns prefix)
pub fn version_sub(orig: &str, sub: &str) -> String {
let mut orig = Version::new(orig).unwrap();
let sub = Version::new(sub).unwrap();
while orig.chunks.0.len() > sub.chunks.0.len() {
orig.chunks.0.pop();
}
for (i, orig_chunk) in orig.clone().chunks.0.iter().enumerate() {
for i in 0..orig.chunks.0.len() {
let m = sub.nth(i).unwrap();
orig.chunks.0[i] = Chunk::Numeric(orig_chunk.single_digit().unwrap() - m);
let orig_val = orig.chunks.0[i].single_digit().unwrap();

if orig_val < m {
// Handle underflow with borrowing from higher digits
for j in (0..i).rev() {
let prev_val = orig.chunks.0[j].single_digit().unwrap();
if prev_val > 0 {
orig.chunks.0[j] = Chunk::Numeric(prev_val - 1);
orig.chunks.0.truncate(j + 1);
return orig.to_string();
}
}
return "0".to_string();
Comment on lines +319 to +327

Copilot AI Sep 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The borrowing logic is incomplete. When a digit at position j has value 0, the algorithm should continue borrowing from higher digits (j-1, j-2, etc.) and set intermediate digits to their maximum borrowable value. Currently, it skips over zero digits without proper carry propagation, which could lead to incorrect results in cases like version_sub('1.0.0.1', '0.0.0.2').

Suggested change
for j in (0..i).rev() {
let prev_val = orig.chunks.0[j].single_digit().unwrap();
if prev_val > 0 {
orig.chunks.0[j] = Chunk::Numeric(prev_val - 1);
orig.chunks.0.truncate(j + 1);
return orig.to_string();
}
}
return "0".to_string();
// Propagate borrow through zeros and set intermediate digits to max value
let mut borrowed = false;
for j in (0..i).rev() {
let prev_val = orig.chunks.0[j].single_digit().unwrap();
if prev_val > 0 {
// Decrement the digit we borrow from
orig.chunks.0[j] = Chunk::Numeric(prev_val - 1);
// Set all digits between j+1 and i-1 to max value (9)
for k in (j+1)..i {
orig.chunks.0[k] = Chunk::Numeric(9);
}
borrowed = true;
break;
}
}
if !borrowed {
return "0".to_string();
}

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation is actually correct. The algorithm properly handles the case you
mentioned:

For version_sub('1.0.0.1', '0.0.0.2'):

  1. Underflow occurs at position 3 (1 < 2)
  2. Loop searches backwards: j=2 (value=0, skip), j=1 (value=0, skip), j=0 (value=1, borrow)
  3. Borrows from position 0: decrements to 0, truncates to position 1
  4. Returns "0"

This behavior is intentional. Returning "0" allows the version resolution system to find the latest
available version within major version 0 (e.g., 0.9.x series), which is the expected behavior when
the subtraction would result in a negative version. This maintains consistency with mise's version
resolution logic where "0" acts as a fallback to resolve the latest version in the 0.x range.

If maintainers prefer different handling for such edge cases, I'm happy to adjust the implementation
accordingly.

}

orig.chunks.0[i] = Chunk::Numeric(orig_val - m);
}
orig.to_string()
}
Expand All @@ -332,5 +348,17 @@ mod tests {
fn test_version_sub() {
assert_str_eq!(version_sub("18.2.3", "2"), "16");
assert_str_eq!(version_sub("18.2.3", "0.1"), "18.1");
assert_str_eq!(version_sub("18.2.3", "0.0.1"), "18.2.2");
}

#[test]
fn test_version_sub_underflow() {
// Test cases that would cause underflow return prefix for higher digit
assert_str_eq!(version_sub("2.0.0", "0.0.1"), "1");
assert_str_eq!(version_sub("2.79.0", "0.0.1"), "2.78");
assert_str_eq!(version_sub("1.0.0", "0.1.0"), "0");
assert_str_eq!(version_sub("0.1.0", "1"), "0");
assert_str_eq!(version_sub("1.2.3", "0.2.4"), "0");
assert_str_eq!(version_sub("1.3.3", "0.2.4"), "1.0");
}
}
Loading