Skip to content

Commit ef8b764

Browse files
lachiehclaude
andcommitted
fix: Handle semver range prefixes in devEngines version strings
Strip common range prefixes (^, >=, ~, etc.) before extracting the major version from devEngines.packageManager.version. Previously, versions like "^9.0.0" or ">=4.0.0" would silently fall through to lockfile detection instead of correctly identifying the major version. Also adds a length cap (10 chars) on digit extraction to prevent theoretical unbounded allocation from adversarial input. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3368a1b commit ef8b764

File tree

1 file changed

+52
-7
lines changed
  • crates/turborepo-repository/src/package_manager

1 file changed

+52
-7
lines changed

crates/turborepo-repository/src/package_manager/mod.rs

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -396,14 +396,15 @@ impl PackageManager {
396396
name: &str,
397397
version: Option<&str>,
398398
) -> Result<Self, Error> {
399-
// Extract leading digits as major version (handles "9.x", "9.0.0", etc.)
399+
// Extract major version digits, stripping common range prefixes like
400+
// ">=", "^", "~" (e.g. "^9.0.0" → "9", ">=9" → "9", "9.x" → "9").
400401
let synthetic_version: Option<Version> = version.and_then(|v| {
401-
let digits: String = v
402-
.chars()
403-
.take_while(|c| c.is_ascii_digit())
404-
.collect();
405-
// Construct a synthetic semver so we can reuse the existing detector methods
406-
format!("{}.0.0", digits).parse().ok()
402+
let v = v.trim_start_matches(|c: char| !c.is_ascii_digit());
403+
let digits: String = v.chars().take(10).take_while(|c| c.is_ascii_digit()).collect();
404+
if digits.is_empty() {
405+
return None;
406+
}
407+
format!("{digits}.0.0").parse().ok()
407408
});
408409

409410
match name {
@@ -1134,6 +1135,50 @@ mod tests {
11341135
Ok(())
11351136
}
11361137

1138+
#[test]
1139+
fn test_read_package_manager_dev_engines_caret_range() -> Result<(), Error> {
1140+
// Version with caret prefix like "^9.0.0" should extract major version 9
1141+
let dir = TempDir::new()?;
1142+
let repo_root = AbsoluteSystemPath::from_std_path(dir.path())?;
1143+
let package_json = PackageJson {
1144+
other: serde_json::from_value(serde_json::json!({
1145+
"devEngines": {
1146+
"packageManager": {
1147+
"name": "pnpm",
1148+
"version": "^9.0.0"
1149+
}
1150+
}
1151+
}))
1152+
.unwrap(),
1153+
..Default::default()
1154+
};
1155+
let pm = PackageManager::read_package_manager(repo_root, &package_json)?;
1156+
assert_eq!(pm, PackageManager::Pnpm9);
1157+
Ok(())
1158+
}
1159+
1160+
#[test]
1161+
fn test_read_package_manager_dev_engines_gte_range() -> Result<(), Error> {
1162+
// Version with ">=" prefix should extract major version
1163+
let dir = TempDir::new()?;
1164+
let repo_root = AbsoluteSystemPath::from_std_path(dir.path())?;
1165+
let package_json = PackageJson {
1166+
other: serde_json::from_value(serde_json::json!({
1167+
"devEngines": {
1168+
"packageManager": {
1169+
"name": "yarn",
1170+
"version": ">=4.0.0"
1171+
}
1172+
}
1173+
}))
1174+
.unwrap(),
1175+
..Default::default()
1176+
};
1177+
let pm = PackageManager::read_package_manager(repo_root, &package_json)?;
1178+
assert_eq!(pm, PackageManager::Berry);
1179+
Ok(())
1180+
}
1181+
11371182
#[test]
11381183
fn test_read_package_manager_dev_engines_unparseable_version_no_lockfile() {
11391184
// Version string with no leading digits ("latest") can't produce a

0 commit comments

Comments
 (0)