From d897b3c5ffb507d4f5fa2951f150157bdc2cf23c Mon Sep 17 00:00:00 2001 From: Adva Oren Date: Wed, 29 Apr 2026 10:58:40 +0300 Subject: [PATCH] fix: handle ~= and != version operators in Python dependency name parsing getDependencyName() only recognized >, <, and = as version operator characters, causing ~ from ~= (compatibility) and ! from != (exclusion) to be included in the package name (e.g. "urllib3~", "click!"). Replaced the three-index approach with a loop that recognizes all PEP 508 version operator characters (>, <, =, ~, !). Fixes TC-4041 Assisted-by: Claude Code --- .../utils/PythonControllerBase.java | 29 +++++++------------ .../utils/PythonControllerRealEnvTest.java | 21 ++++++++++++++ 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/main/java/io/github/guacsec/trustifyda/utils/PythonControllerBase.java b/src/main/java/io/github/guacsec/trustifyda/utils/PythonControllerBase.java index cc367bc1..67d92569 100644 --- a/src/main/java/io/github/guacsec/trustifyda/utils/PythonControllerBase.java +++ b/src/main/java/io/github/guacsec/trustifyda/utils/PythonControllerBase.java @@ -388,31 +388,24 @@ public static String getDependencyName(String dep) { requirement = requirement.substring(0, extrasStart) + requirement.substring(extrasEnd + 1); } } - int rightTriangleBracket = requirement.indexOf(">"); - int leftTriangleBracket = requirement.indexOf("<"); - int equalsSign = requirement.indexOf("="); - int minimumIndex = getFirstSign(rightTriangleBracket, leftTriangleBracket, equalsSign); + // Find the first PEP 508 version operator character (~=, !=, ==, >=, <=, >, <, ===) + int firstOperatorChar = -1; + for (int i = 0; i < requirement.length(); i++) { + char c = requirement.charAt(i); + if (c == '>' || c == '<' || c == '=' || c == '~' || c == '!') { + firstOperatorChar = i; + break; + } + } String depName; - if (rightTriangleBracket == -1 && leftTriangleBracket == -1 && equalsSign == -1) { + if (firstOperatorChar == -1) { depName = requirement; } else { - depName = requirement.substring(0, minimumIndex); + depName = requirement.substring(0, firstOperatorChar); } return depName.trim(); } - private static int getFirstSign( - int rightTriangleBracket, int leftTriangleBracket, int equalsSign) { - rightTriangleBracket = rightTriangleBracket == -1 ? 999 : rightTriangleBracket; - leftTriangleBracket = leftTriangleBracket == -1 ? 999 : leftTriangleBracket; - equalsSign = equalsSign == -1 ? 999 : equalsSign; - return equalsSign < leftTriangleBracket && equalsSign < rightTriangleBracket - ? equalsSign - : (leftTriangleBracket < equalsSign && leftTriangleBracket < rightTriangleBracket - ? leftTriangleBracket - : rightTriangleBracket); - } - static List splitPipShowLines(String pipShowOutput) { return Arrays.stream( pipShowOutput.split(System.lineSeparator() + "---" + System.lineSeparator())) diff --git a/src/test/java/io/github/guacsec/trustifyda/utils/PythonControllerRealEnvTest.java b/src/test/java/io/github/guacsec/trustifyda/utils/PythonControllerRealEnvTest.java index ee63d5d1..4aa98d2c 100644 --- a/src/test/java/io/github/guacsec/trustifyda/utils/PythonControllerRealEnvTest.java +++ b/src/test/java/io/github/guacsec/trustifyda/utils/PythonControllerRealEnvTest.java @@ -301,6 +301,16 @@ void get_Dependency_Name_with_markers() { PythonControllerRealEnv.getDependencyName("certifi==2023.7.22 ; python_version >= \"3\"")); } + /** Verifies getDependencyName handles compatibility (~=) and exclusion (!=) operators. */ + @Test + void get_Dependency_Name_with_compatibility_and_exclusion_operators() { + assertEquals("urllib3", PythonControllerRealEnv.getDependencyName("urllib3~=1.26.0")); + assertEquals("click", PythonControllerRealEnv.getDependencyName("click!=7.1.1")); + assertEquals("certifi", PythonControllerRealEnv.getDependencyName("certifi>=2021.0.0")); + assertEquals("package", PythonControllerRealEnv.getDependencyName("package~=2.0")); + assertEquals("package", PythonControllerRealEnv.getDependencyName("package!=1.0.0")); + } + /** Verifies getDependencyName strips PEP 508 extras from requirements. */ @Test void get_Dependency_Name_with_extras() { @@ -313,6 +323,17 @@ void get_Dependency_Name_with_extras() { "package[extra1]>=1.0 ; python_version >= \"3.8\"")); } + /** Verifies getDependencyName handles extras combined with special version operators. */ + @Test + void get_Dependency_Name_with_extras_and_special_operators() { + assertEquals( + "requests", PythonControllerRealEnv.getDependencyName("requests[security,socks]==2.25.1")); + assertEquals("httpx", PythonControllerRealEnv.getDependencyName("httpx [http2] >=0.23.0")); + assertEquals( + "package", + PythonControllerRealEnv.getDependencyName("package[extra]~=1.0 ; python_version >= \"3\"")); + } + @Test void automaticallyInstallPackageOnEnvironment() { assertFalse(pythonControllerRealEnv.automaticallyInstallPackageOnEnvironment());