From d95a3590238b51eb83f73e2c1743bf4891c698cb Mon Sep 17 00:00:00 2001 From: Malte Brunnlieb Date: Sun, 28 Jun 2026 16:49:54 +0200 Subject: [PATCH 1/3] #2088: skip malformed PATH entries instead of failing with InvalidPathException SystemPath called Path.of(segment) for every PATH entry without guarding against InvalidPathException. A single malformed entry (e.g. one containing an illegal character such as a newline on Windows or a NUL byte on any OS) aborted the whole SystemPath construction and therefore all of IDEasy. Invalid PATH entries are now skipped with a warning while all valid entries continue to be processed. --- CHANGELOG.adoc | 1 + .../devonfw/tools/ide/common/SystemPath.java | 14 ++++++++++++- .../tools/ide/common/SystemPathTest.java | 20 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 2132e67ad4..8fc6c49e86 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -8,6 +8,7 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/2052[#2052]: Split issue statistics in 2 pie-charts * https://github.com/devonfw/IDEasy/issues/822[#822] : Added Cwd path limit in shell +* https://github.com/devonfw/IDEasy/issues/2088[#2088]: Skip malformed PATH entries instead of failing with InvalidPathException The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/46?closed=1[milestone 2026.07.001]. diff --git a/cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java b/cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java index 87ff6b0352..05ed203046 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java +++ b/cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java @@ -3,6 +3,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.LinkOption; import java.nio.file.Path; import java.util.ArrayList; @@ -16,6 +17,9 @@ import java.util.regex.Pattern; import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.os.SystemInfoImpl; import com.devonfw.tools.ide.os.WindowsPathSyntax; @@ -33,6 +37,8 @@ */ public class SystemPath { + private static final Logger LOG = LoggerFactory.getLogger(SystemPath.class); + private static final Pattern REGEX_WINDOWS_PATH = Pattern.compile("([a-zA-Z]:)?(\\\\[a-zA-Z0-9\\s_.-]+)+\\\\?"); private final char pathSeparator; @@ -96,7 +102,13 @@ public SystemPath(IdeContext context, String envPath, Path ideRoot, Path softwar this(context, pathSeparator, extraPathEntries, new HashMap<>(), new ArrayList<>()); String[] envPaths = envPath.split(Character.toString(pathSeparator)); for (String segment : envPaths) { - Path path = Path.of(segment); + Path path; + try { + path = Path.of(segment); + } catch (InvalidPathException e) { + LOG.warn("Ignoring invalid PATH entry '{}' - {}", segment, e.getMessage()); + continue; + } String tool = getTool(path, ideRoot); if (tool == null) { this.paths.add(path); diff --git a/cli/src/test/java/com/devonfw/tools/ide/common/SystemPathTest.java b/cli/src/test/java/com/devonfw/tools/ide/common/SystemPathTest.java index 3b7be3335a..1c05bf87a7 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/common/SystemPathTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/common/SystemPathTest.java @@ -12,6 +12,7 @@ import com.devonfw.tools.ide.context.AbstractIdeContextTest; import com.devonfw.tools.ide.context.IdeTestContext; +import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.os.SystemInfo; import com.devonfw.tools.ide.os.SystemInfoMock; @@ -142,6 +143,25 @@ void testFindBinaryFindsNothingWithoutFilter() { assertThat(result).isEqualTo(test); } + @Test + void testConstructorSkipsMalformedPathEntryAndKeepsValidOnes() { + // arrange + IdeTestContext context = newContext("find-binary", "project/workspaces", false); + String validEntryBefore = "C:\\Tools\\bin"; + // (char) 0 is an illegal NUL character, making Path.of throw InvalidPathException on every OS + String malformedEntry = "broken" + (char) 0 + "entry"; + String validEntryAfter = "C:\\Other\\bin"; + String envPath = validEntryBefore + ';' + malformedEntry + ';' + validEntryAfter; + + // act + SystemPath systemPath = new SystemPath(context, envPath, context.getIdeRoot(), context.getSoftwarePath(), ';', new ArrayList<>()); + + // assert + String result = systemPath.toString(); + assertThat(result).contains(validEntryBefore).contains(validEntryAfter); + assertThat(context).log(IdeLogLevel.WARNING).hasMessageContaining("Ignoring invalid PATH entry"); + } + private static boolean checkPathToIgnoreLowercase(Path p, String toIgnore) { String s = p.toAbsolutePath().toString().replace('\\', '/').toLowerCase(Locale.ROOT); return !s.contains(toIgnore); From d26af5a7047fc6bc1ce425abbc7e445fe8b4e116 Mon Sep 17 00:00:00 2001 From: Malte Brunnlieb Date: Mon, 6 Jul 2026 20:30:43 +0200 Subject: [PATCH 2/3] #2088: normalize PATH entries by removing control characters Following review feedback: besides ignoring broken entries via try/catch, PATH segments are now normalized by stripping illegal ISO control characters (CR, LF, NUL, TAB, etc.). This keeps otherwise usable entries (e.g. a valid path with a trailing newline from a broken tool config) instead of dropping them, while entries that only consist of control characters or remain invalid after normalization are still skipped without aborting SystemPath construction. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014bvJgB42fjjeN7qDboXQyg --- CHANGELOG.adoc | 2 +- .../devonfw/tools/ide/common/SystemPath.java | 52 +++++++++++++++++-- .../tools/ide/common/SystemPathTest.java | 28 ++++++++-- 3 files changed, 72 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 3559492fd0..c49c9f36e9 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -10,7 +10,7 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/1056[#1056]: Improve MSI license display by generating formatted RTF from AsciiDoc * https://github.com/devonfw/IDEasy/issues/2052[#2052]: Split issue statistics in 2 pie-charts * https://github.com/devonfw/IDEasy/issues/822[#822] : Added Cwd path limit in shell -* https://github.com/devonfw/IDEasy/issues/2088[#2088]: Skip malformed PATH entries instead of failing with InvalidPathException +* https://github.com/devonfw/IDEasy/issues/2088[#2088]: Normalize and skip malformed PATH entries instead of failing with InvalidPathException * https://github.com/devonfw/IDEasy/issues/1517[#1517]: Fix IDEasy MSI does not configure Windows Terminal for git-bash * https://github.com/devonfw/IDEasy/issues/1227[#1227]: Added Setup instrcutions to the Windows installer diff --git a/cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java b/cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java index 05ed203046..72d7f3ccd4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java +++ b/cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java @@ -102,11 +102,8 @@ public SystemPath(IdeContext context, String envPath, Path ideRoot, Path softwar this(context, pathSeparator, extraPathEntries, new HashMap<>(), new ArrayList<>()); String[] envPaths = envPath.split(Character.toString(pathSeparator)); for (String segment : envPaths) { - Path path; - try { - path = Path.of(segment); - } catch (InvalidPathException e) { - LOG.warn("Ignoring invalid PATH entry '{}' - {}", segment, e.getMessage()); + Path path = parsePathSegment(segment); + if (path == null) { continue; } String tool = getTool(path, ideRoot); @@ -117,6 +114,51 @@ public SystemPath(IdeContext context, String envPath, Path ideRoot, Path softwar collectToolPath(softwarePath); } + /** + * Parses a single {@code PATH} segment into a {@link Path}. The segment is first {@link #normalizePathSegment(String) normalized} by removing illegal control + * characters (e.g. CR, LF, NUL, TAB) that may sneak into the {@code PATH} variable (e.g. via broken tool configurations) and would otherwise render the entry + * invalid. This way a still usable entry is kept instead of being dropped. If nothing usable remains after normalization or the segment cannot be parsed as a + * {@link Path} even after normalization, {@code null} is returned so the entry is skipped without aborting the entire {@link SystemPath} construction. + * + * @param segment the raw {@code PATH} segment. + * @return the parsed {@link Path} or {@code null} if the segment is invalid and should be ignored. + */ + private static Path parsePathSegment(String segment) { + + String normalized = normalizePathSegment(segment); + if (normalized.isEmpty()) { + if (!segment.isEmpty()) { + LOG.warn("Ignoring invalid PATH entry '{}' as it only contains illegal control characters.", segment); + } + return null; + } + if (!normalized.equals(segment)) { + LOG.warn("Normalized PATH entry '{}' by removing illegal control characters.", segment); + } + try { + return Path.of(normalized); + } catch (InvalidPathException e) { + LOG.warn("Ignoring invalid PATH entry '{}' - {}", segment, e.getMessage()); + return null; + } + } + + /** + * @param segment the raw {@code PATH} segment. + * @return the given {@code segment} with all {@link Character#isISOControl(char) ISO control characters} (e.g. CR, LF, NUL, TAB) removed. + */ + private static String normalizePathSegment(String segment) { + + StringBuilder sb = new StringBuilder(segment.length()); + for (int i = 0; i < segment.length(); i++) { + char c = segment.charAt(i); + if (!Character.isISOControl(c)) { + sb.append(c); + } + } + return sb.toString(); + } + /** * @param context {@link IdeContext} for the output of information. * @param softwarePath the {@link IdeContext#getSoftwarePath() software path}. diff --git a/cli/src/test/java/com/devonfw/tools/ide/common/SystemPathTest.java b/cli/src/test/java/com/devonfw/tools/ide/common/SystemPathTest.java index 1c05bf87a7..a2e9c3853c 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/common/SystemPathTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/common/SystemPathTest.java @@ -144,14 +144,34 @@ void testFindBinaryFindsNothingWithoutFilter() { } @Test - void testConstructorSkipsMalformedPathEntryAndKeepsValidOnes() { + void testConstructorNormalizesPathEntryWithControlCharactersAndKeepsIt() { + // arrange + IdeTestContext context = newContext("find-binary", "project/workspaces", false); + String validEntry = "C:\\Tools\\bin"; + // an entry polluted with control characters (CR, LF, TAB, NUL) that must be stripped instead of dropping the whole entry + String cleanedEntry = "C:\\Other\\bin"; + String pollutedEntry = "C:\\Other" + "\r\n\t" + (char) 0 + "\\bin"; + String envPath = validEntry + ';' + pollutedEntry; + + // act + SystemPath systemPath = new SystemPath(context, envPath, context.getIdeRoot(), context.getSoftwarePath(), ';', new ArrayList<>()); + + // assert + String result = systemPath.toString(); + assertThat(result).contains(validEntry).contains(cleanedEntry); + assertThat(result).doesNotContain("\r").doesNotContain("\n").doesNotContain("\t").doesNotContain(Character.toString((char) 0)); + assertThat(context).log(IdeLogLevel.WARNING).hasMessageContaining("Normalized PATH entry"); + } + + @Test + void testConstructorSkipsPathEntryConsistingOnlyOfControlCharactersAndKeepsValidOnes() { // arrange IdeTestContext context = newContext("find-binary", "project/workspaces", false); String validEntryBefore = "C:\\Tools\\bin"; - // (char) 0 is an illegal NUL character, making Path.of throw InvalidPathException on every OS - String malformedEntry = "broken" + (char) 0 + "entry"; + // an entry that has nothing usable left after normalization must be skipped without aborting the whole construction + String controlOnlyEntry = "\r\n\t" + (char) 0; String validEntryAfter = "C:\\Other\\bin"; - String envPath = validEntryBefore + ';' + malformedEntry + ';' + validEntryAfter; + String envPath = validEntryBefore + ';' + controlOnlyEntry + ';' + validEntryAfter; // act SystemPath systemPath = new SystemPath(context, envPath, context.getIdeRoot(), context.getSoftwarePath(), ';', new ArrayList<>()); From 67da17851e3fbf65228a31953b8d857d1e6f253b Mon Sep 17 00:00:00 2001 From: Malte Brunnlieb Date: Mon, 6 Jul 2026 20:37:06 +0200 Subject: [PATCH 3/3] #2088: fix checkstyle violation by avoiding illegal java.io.File import The changed-files checkstyle run flagged the pre-existing java.io.File import (forbidden by the project ruleset). Its only use was File.pathSeparatorChar, which the JDK derives from the "path.separator" system property, so replace it with System.getProperty("path.separator").charAt(0) and drop the import. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_014bvJgB42fjjeN7qDboXQyg --- cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java b/cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java index 72d7f3ccd4..310acc00ba 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java +++ b/cli/src/main/java/com/devonfw/tools/ide/common/SystemPath.java @@ -1,6 +1,5 @@ package com.devonfw.tools.ide.common; -import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.InvalidPathException; @@ -84,7 +83,7 @@ public SystemPath(IdeContext context, String envPath) { */ public SystemPath(IdeContext context, String envPath, Path ideRoot, Path softwarePath) { - this(context, envPath, ideRoot, softwarePath, File.pathSeparatorChar, Collections.emptyList()); + this(context, envPath, ideRoot, softwarePath, System.getProperty("path.separator").charAt(0), Collections.emptyList()); } /**