Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package jline3 for openSUSE:Factory checked in at 2026-07-01 17:12:40 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/jline3 (Old) and /work/SRC/openSUSE:Factory/.jline3.new.11887 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "jline3" Wed Jul 1 17:12:40 2026 rev:15 rq:1362999 version:3.30.15 Changes: -------- --- /work/SRC/openSUSE:Factory/jline3/jline3.changes 2026-06-30 15:14:41.438596643 +0200 +++ /work/SRC/openSUSE:Factory/.jline3.new.11887/jline3.changes 2026-07-01 17:12:49.256093305 +0200 @@ -1,0 +2,30 @@ +Wed Jul 1 13:32:12 UTC 2026 - Fridrich Strba <[email protected]> + +- Update to upstream version 3.30.15 + * Security Fixes + + fix: guard regex matching against catastrophic backtracking + (ReDoS) (#2018, backport of #2012) + ° Adds SafeRegex utility with TimeoutCharSequence to enforce + wall-clock deadlines during regex matching + ° Fixes 8 locations across terminal, reader, and builtins + where user-controlled input could trigger catastrophic + backtracking + ° Addresses GHSA-r2xf-8xr9-62gw, GHSA-2v9w-34q6-wpqx, + GHSA-ph9c-7hw9-vhhw, GHSA-5q95-hrpc-m3w3 + + fix: backport security hardening (#1986, #1995) + ° Create persisted history file with owner-only permissions + ° Use exclusive create for extracted native library temp files + * Bug Fixes + + fix: warn on insecure permissions when history file created + concurrently + * Dependency Updates + + chore: bump eu.maveniverse.maven.njord:extension3 from 0.9.8 + to 0.9.9 (#1999) + + chore: bump com.palantir.javaformat:palantir-java-format + (#1991) + + chore: bump actions/cache from 5 to 6 (#1989) +- Modified patch: + * 0001-Remove-optional-dependency-on-universalchardet.patch + + rebase + +------------------------------------------------------------------- Old: ---- jline-3.30.14.tar.gz New: ---- jline-3.30.15.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ jline3.spec ++++++ --- /var/tmp/diff_new_pack.OzWhHC/_old 2026-07-01 17:12:51.728178504 +0200 +++ /var/tmp/diff_new_pack.OzWhHC/_new 2026-07-01 17:12:51.728178504 +0200 @@ -31,7 +31,7 @@ %endif %bcond_with ssh Name: jline3 -Version: 3.30.14 +Version: 3.30.15 Release: 0 Summary: Java library for handling console input License: BSD-3-Clause ++++++ 0001-Remove-optional-dependency-on-universalchardet.patch ++++++ --- /var/tmp/diff_new_pack.OzWhHC/_old 2026-07-01 17:12:51.756179469 +0200 +++ /var/tmp/diff_new_pack.OzWhHC/_new 2026-07-01 17:12:51.760179607 +0200 @@ -1,4 +1,4 @@ -From f3eda0bf4fa1b9efb0562224fa3c090ba59f0253 Mon Sep 17 00:00:00 2001 +From 122ad3693ebf643e8f3d0eafd9ec4ac5b13130d7 Mon Sep 17 00:00:00 2001 From: Mikolaj Izdebski <[email protected]> Date: Wed, 26 Feb 2025 16:26:49 +0100 Subject: [PATCH] Remove optional dependency on universalchardet @@ -8,18 +8,18 @@ 1 file changed, 12 deletions(-) diff --git a/builtins/src/main/java/org/jline/builtins/Nano.java b/builtins/src/main/java/org/jline/builtins/Nano.java -index 491d0bc3..be5748c1 100644 +index b5112c2e..7d3bb3a7 100644 --- a/builtins/src/main/java/org/jline/builtins/Nano.java +++ b/builtins/src/main/java/org/jline/builtins/Nano.java -@@ -51,7 +51,6 @@ import org.jline.terminal.impl.MouseSupport; - import org.jline.utils.*; - import org.jline.utils.InfoCmp.Capability; +@@ -53,7 +53,6 @@ import org.jline.utils.InfoCmp.Capability; + import org.jline.utils.RegexTimeoutException; + import org.jline.utils.SafeRegex; import org.jline.utils.Status; -import org.mozilla.universalchardet.UniversalDetector; import static org.jline.builtins.SyntaxHighlighter.*; import static org.jline.keymap.KeyMap.KEYMAP_LENGTH; -@@ -380,17 +379,6 @@ public class Nano implements Editor { +@@ -382,17 +381,6 @@ public class Nano implements Editor { } byte[] bytes = bos.toByteArray(); @@ -38,6 +38,6 @@ try (BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes), charset))) { -- -2.49.0 +2.54.0 ++++++ _scmsync.obsinfo ++++++ --- /var/tmp/diff_new_pack.OzWhHC/_old 2026-07-01 17:12:51.804181124 +0200 +++ /var/tmp/diff_new_pack.OzWhHC/_new 2026-07-01 17:12:51.816181537 +0200 @@ -1,6 +1,6 @@ -mtime: 1782808776 -commit: ed2991ad1cda5cd9fc917625410e7471557791f8e6156aece1d4759ea7a32b27 +mtime: 1782913240 +commit: c3113bd96cf7b714c5019d392c92a1b96b2ec4ac6b6e2504d14dac0677a69ee9 url: https://src.opensuse.org/java-packages/jline3 -revision: ed2991ad1cda5cd9fc917625410e7471557791f8e6156aece1d4759ea7a32b27 +revision: c3113bd96cf7b714c5019d392c92a1b96b2ec4ac6b6e2504d14dac0677a69ee9 projectscmsync: https://src.opensuse.org/java-packages/_ObsPrj ++++++ build.specials.obscpio ++++++ ++++++ build.specials.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/.gitignore new/.gitignore --- old/.gitignore 1970-01-01 01:00:00.000000000 +0100 +++ new/.gitignore 2026-07-01 15:40:40.000000000 +0200 @@ -0,0 +1 @@ +.osc ++++++ jline-3.30.14.tar.gz -> jline-3.30.15.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/.github/workflows/master-build.yml new/jline3-jline-3.30.15/.github/workflows/master-build.yml --- old/jline3-jline-3.30.14/.github/workflows/master-build.yml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/.github/workflows/master-build.yml 2026-06-30 21:19:48.000000000 +0200 @@ -40,7 +40,7 @@ distribution: temurin - name: Cache Maven dependencies - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ matrix.java }}-${{ hashFiles('**/pom.xml') }} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/.mvn/extensions.xml new/jline3-jline-3.30.15/.mvn/extensions.xml --- old/jline3-jline-3.30.14/.mvn/extensions.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/.mvn/extensions.xml 2026-06-30 21:19:48.000000000 +0200 @@ -3,6 +3,6 @@ <extension> <groupId>eu.maveniverse.maven.njord</groupId> <artifactId>extension3</artifactId> - <version>0.9.8</version> + <version>0.9.9</version> </extension> </extensions> \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/builtins/pom.xml new/jline3-jline-3.30.15/builtins/pom.xml --- old/jline3-jline-3.30.14/builtins/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/builtins/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -16,7 +16,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline-builtins</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/builtins/src/main/java/org/jline/builtins/Commands.java new/jline3-jline-3.30.15/builtins/src/main/java/org/jline/builtins/Commands.java --- old/jline3-jline-3.30.14/builtins/src/main/java/org/jline/builtins/Commands.java 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/builtins/src/main/java/org/jline/builtins/Commands.java 2026-06-30 21:19:48.000000000 +0200 @@ -45,6 +45,7 @@ import org.jline.terminal.Terminal; import org.jline.utils.AttributedStringBuilder; import org.jline.utils.AttributedStyle; +import org.jline.utils.SafeRegex; import org.jline.utils.StyleResolver; import static org.jline.builtins.SyntaxHighlighter.*; @@ -360,16 +361,7 @@ Pattern pattern = null; if (opt.isSet("m") && opt.args().size() > argId) { - StringBuilder sb = new StringBuilder(); - char prev = '0'; - for (char c : opt.args().get(argId++).toCharArray()) { - if (c == '*' && prev != '\\' && prev != '.') { - sb.append('.'); - } - sb.append(c); - prev = c; - } - pattern = Pattern.compile(sb.toString(), Pattern.DOTALL); + pattern = SafeRegex.compileGlob(opt.args().get(argId++), Pattern.DOTALL); } boolean reverse = opt.isSet("r") || (opt.isSet("s") && opt.args().size() <= argId); int firstId = opt.args().size() > argId @@ -399,7 +391,7 @@ while (iter.hasNext() && listed < tot) { History.Entry entry = iter.next(); listed++; - if (pattern != null && !pattern.matcher(entry.line()).matches()) { + if (pattern != null && !SafeRegex.matches(pattern, entry.line())) { continue; } if (execute.isExecute()) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/builtins/src/main/java/org/jline/builtins/Less.java new/jline3-jline-3.30.15/builtins/src/main/java/org/jline/builtins/Less.java --- old/jline3-jline-3.30.14/builtins/src/main/java/org/jline/builtins/Less.java 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/builtins/src/main/java/org/jline/builtins/Less.java 2026-06-30 21:19:48.000000000 +0200 @@ -42,6 +42,7 @@ import org.jline.utils.Display; import org.jline.utils.InfoCmp.Capability; import org.jline.utils.NonBlockingReader; +import org.jline.utils.SafeRegex; import org.jline.utils.Status; import static org.jline.builtins.SyntaxHighlighter.*; @@ -1095,7 +1096,7 @@ break; } else if (!toBeDisplayed(line, dpCompiled)) { continue; - } else if (compiled.matcher(line).find()) { + } else if (SafeRegex.find(compiled, line)) { display.clear(); firstLineToDisplay = lineNumber; offsetInLine = 0; @@ -1135,7 +1136,7 @@ break; } else if (!toBeDisplayed(line, dpCompiled)) { continue; - } else if (compiled.matcher(line).find()) { + } else if (SafeRegex.find(compiled, line)) { display.clear(); firstLineToDisplay = lineNumber; offsetInLine = 0; @@ -1309,10 +1310,7 @@ } private boolean toBeDisplayed(AttributedString curLine, Pattern dpCompiled) { - return curLine == null - || dpCompiled == null - || sourceIdx == 0 - || dpCompiled.matcher(curLine).find(); + return curLine == null || dpCompiled == null || sourceIdx == 0 || SafeRegex.find(dpCompiled, curLine); } synchronized boolean display(boolean oneScreen) throws IOException { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/builtins/src/main/java/org/jline/builtins/Nano.java new/jline3-jline-3.30.15/builtins/src/main/java/org/jline/builtins/Nano.java --- old/jline3-jline-3.30.14/builtins/src/main/java/org/jline/builtins/Nano.java 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/builtins/src/main/java/org/jline/builtins/Nano.java 2026-06-30 21:19:48.000000000 +0200 @@ -50,6 +50,8 @@ import org.jline.terminal.impl.MouseSupport; import org.jline.utils.*; import org.jline.utils.InfoCmp.Capability; +import org.jline.utils.RegexTimeoutException; +import org.jline.utils.SafeRegex; import org.jline.utils.Status; import org.mozilla.universalchardet.UniversalDetector; @@ -1321,15 +1323,20 @@ } private List<Integer> doSearch(String text) { + matchedLength = 0; Pattern pat = Pattern.compile( searchTerm, (searchCaseSensitive ? 0 : Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE) | (searchRegexp ? 0 : Pattern.LITERAL)); - Matcher m = pat.matcher(text); + Matcher m = SafeRegex.matcher(pat, text); List<Integer> res = new ArrayList<>(); - while (m.find()) { - res.add(m.start()); - matchedLength = m.group(0).length(); + try { + while (m.find()) { + res.add(m.start()); + matchedLength = m.end() - m.start(); + } + } catch (RegexTimeoutException e) { + // Return whatever matches were found before timeout } return res; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/builtins/src/main/java/org/jline/builtins/PosixCommands.java new/jline3-jline-3.30.15/builtins/src/main/java/org/jline/builtins/PosixCommands.java --- old/jline3-jline-3.30.14/builtins/src/main/java/org/jline/builtins/PosixCommands.java 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/builtins/src/main/java/org/jline/builtins/PosixCommands.java 2026-06-30 21:19:48.000000000 +0200 @@ -41,6 +41,8 @@ import org.jline.utils.AttributedStringBuilder; import org.jline.utils.InfoCmp.Capability; import org.jline.utils.OSUtils; +import org.jline.utils.RegexTimeoutException; +import org.jline.utils.SafeRegex; import org.jline.utils.StyleResolver; /** @@ -1319,10 +1321,9 @@ if (opt.isSet("word-regexp")) { regexp = "\\b" + regexp + "\\b"; } - if (opt.isSet("line-regexp")) { + boolean lineRegexp = opt.isSet("line-regexp"); + if (lineRegexp) { regexp = "^" + regexp + "$"; - } else { - regexp = ".*" + regexp + ".*"; } Pattern p; Pattern p2; @@ -1394,7 +1395,7 @@ int lineno = 1; int lineMatch = 0; while ((line = r.readLine()) != null) { - boolean matches = p.matcher(line).matches(); + boolean matches = lineRegexp ? SafeRegex.matches(p, line) : SafeRegex.find(p, line); if (invert) { matches = !matches; } @@ -1423,14 +1424,18 @@ sbl.append(":"); } if (colored) { - Matcher matcher2 = p2.matcher(line); + Matcher matcher2 = SafeRegex.matcher(p2, line); int cur = 0; - while (matcher2.find()) { - sbl.append(line, cur, matcher2.start()); - applyStyle(sbl, colors, "ms"); - sbl.append(line, matcher2.start(), matcher2.end()); - applyStyle(sbl, colors, "se"); - cur = matcher2.end(); + try { + while (matcher2.find()) { + sbl.append(line, cur, matcher2.start()); + applyStyle(sbl, colors, "ms"); + sbl.append(line, matcher2.start(), matcher2.end()); + applyStyle(sbl, colors, "se"); + cur = matcher2.end(); + } + } catch (RegexTimeoutException e) { + // Append remaining text without highlighting } sbl.append(line, cur, line.length()); } else { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/console/pom.xml new/jline3-jline-3.30.15/console/pom.xml --- old/jline3-jline-3.30.14/console/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/console/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -16,7 +16,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline-console</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/console-ui/pom.xml new/jline3-jline-3.30.15/console-ui/pom.xml --- old/jline3-jline-3.30.14/console-ui/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/console-ui/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -5,7 +5,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline-console-ui</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/curses/pom.xml new/jline3-jline-3.30.15/curses/pom.xml --- old/jline3-jline-3.30.14/curses/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/curses/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -16,7 +16,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline-curses</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/demo/pom.xml new/jline3-jline-3.30.15/demo/pom.xml --- old/jline3-jline-3.30.14/demo/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/demo/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -16,7 +16,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline-demo</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/graal/pom.xml new/jline3-jline-3.30.15/graal/pom.xml --- old/jline3-jline-3.30.14/graal/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/graal/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -16,7 +16,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline-graal</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/groovy/pom.xml new/jline3-jline-3.30.15/groovy/pom.xml --- old/jline3-jline-3.30.14/groovy/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/groovy/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -14,7 +14,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline-groovy</artifactId> <name>JLine Groovy</name> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/jansi/pom.xml new/jline3-jline-3.30.15/jansi/pom.xml --- old/jline3-jline-3.30.14/jansi/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/jansi/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -16,7 +16,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jansi</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/jansi-core/pom.xml new/jline3-jline-3.30.15/jansi-core/pom.xml --- old/jline3-jline-3.30.14/jansi-core/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/jansi-core/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -16,7 +16,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jansi-core</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/jline/pom.xml new/jline3-jline-3.30.15/jline/pom.xml --- old/jline3-jline-3.30.14/jline/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/jline/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -16,7 +16,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/native/pom.xml new/jline3-jline-3.30.15/native/pom.xml --- old/jline3-jline-3.30.14/native/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/native/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -15,7 +15,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline-native</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/native/src/main/java/org/jline/nativ/JLineNativeLoader.java new/jline3-jline-3.30.15/native/src/main/java/org/jline/nativ/JLineNativeLoader.java --- old/jline3-jline-3.30.14/native/src/main/java/org/jline/nativ/JLineNativeLoader.java 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/native/src/main/java/org/jline/nativ/JLineNativeLoader.java 2026-06-30 21:19:48.000000000 +0200 @@ -29,17 +29,18 @@ import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Properties; -import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; import java.util.logging.Level; import java.util.logging.Logger; @@ -327,12 +328,13 @@ File extractedLckFile = new File(targetFolder, extractedLckFileName); try { - // Extract a native library file into the target directory + // Extract a native library file into the target directory. + // The target sits in the shared system temp directory, so open both + // files with CREATE_NEW: an existing path or a symlink planted there + // by another local user is refused instead of being followed. try (InputStream in = JLineNativeLoader.class.getResourceAsStream(nativeLibraryFilePath)) { - if (!extractedLckFile.exists()) { - new FileOutputStream(extractedLckFile).close(); - } - try (OutputStream out = new FileOutputStream(extractedLibFile)) { + newExclusiveStream(extractedLckFile).close(); + try (OutputStream out = newExclusiveStream(extractedLibFile)) { copy(in, out); } } finally { @@ -371,7 +373,17 @@ } private static String randomUUID() { - return Long.toHexString(new Random().nextLong()); + return Long.toHexString(ThreadLocalRandom.current().nextLong()); + } + + /** + * Opens an output stream that creates a brand new file, failing if the path already + * exists. {@code CREATE_NEW} maps to an exclusive create at the OS level, so a symlink + * or a regular file planted at the target path in the shared temp directory is rejected + * rather than followed. + */ + static OutputStream newExclusiveStream(File file) throws IOException { + return Files.newOutputStream(file.toPath(), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); } private static void copy(InputStream in, OutputStream out) throws IOException { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/native/src/test/java/org/jline/nativ/JLineNativeLoaderTest.java new/jline3-jline-3.30.15/native/src/test/java/org/jline/nativ/JLineNativeLoaderTest.java --- old/jline3-jline-3.30.14/native/src/test/java/org/jline/nativ/JLineNativeLoaderTest.java 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/native/src/test/java/org/jline/nativ/JLineNativeLoaderTest.java 2026-06-30 21:19:48.000000000 +0200 @@ -8,7 +8,17 @@ */ package org.jline.nativ; +import java.io.File; +import java.io.OutputStream; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; public class JLineNativeLoaderTest { @@ -16,4 +26,22 @@ public void testLoadLibrary() { JLineNativeLoader.initialize(); } + + @Test + void exclusiveStreamRefusesExistingTarget(@TempDir Path dir) throws Exception { + // A file already sitting at the target path stands in for a symlink an attacker + // planted in the shared temp directory: the open must fail instead of writing through it. + File planted = dir.resolve("jlinenative-planted").toFile(); + assertTrue(planted.createNewFile()); + assertThrows( + FileAlreadyExistsException.class, + () -> JLineNativeLoader.newExclusiveStream(planted).close()); + + // A fresh path is created normally. + File fresh = dir.resolve("jlinenative-fresh").toFile(); + try (OutputStream out = JLineNativeLoader.newExclusiveStream(fresh)) { + out.write(new byte[] {1, 2, 3}); + } + assertTrue(Files.isRegularFile(fresh.toPath())); + } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/pom.xml new/jline3-jline-3.30.15/pom.xml --- old/jline3-jline-3.30.14/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -15,7 +15,7 @@ <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> <packaging>pom</packaging> <name>JLine</name> <description>JLine</description> @@ -74,7 +74,7 @@ <scm child.scm.connection.inherit.append.path="false" child.scm.developerConnection.inherit.append.path="false" child.scm.url.inherit.append.path="false"> <connection>scm:git:https://github.com/jline/jline3.git</connection> <developerConnection>scm:git:https://github.com/jline/jline3.git</developerConnection> - <tag>jline-3.30.14</tag> + <tag>jline-3.30.15</tag> <url>https://github.com/jline/jline3</url> </scm> @@ -98,7 +98,7 @@ <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> - <project.build.outputTimestamp>2026-06-26T18:08:31Z</project.build.outputTimestamp> + <project.build.outputTimestamp>2026-06-30T19:17:34Z</project.build.outputTimestamp> <java.build.version>22</java.build.version> <java.release.version>8</java.release.version> @@ -121,7 +121,7 @@ <ivy.version>2.5.3</ivy.version> <graal.version>25.0.3</graal.version> <graal.plugin.version>21.2.0</graal.plugin.version> - <palantir.version>2.93.0</palantir.version> + <palantir.version>2.94.0</palantir.version> <surefire.argLine>--add-opens java.base/java.io=ALL-UNNAMED</surefire.argLine> </properties> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/reader/pom.xml new/jline3-jline-3.30.15/reader/pom.xml --- old/jline3-jline-3.30.14/reader/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/reader/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -16,7 +16,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline-reader</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java new/jline3-jline-3.30.15/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java --- old/jline3-jline-3.30.14/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java 2026-06-30 21:19:48.000000000 +0200 @@ -49,6 +49,8 @@ import org.jline.utils.Display; import org.jline.utils.InfoCmp.Capability; import org.jline.utils.Log; +import org.jline.utils.RegexTimeoutException; +import org.jline.utils.SafeRegex; import org.jline.utils.Status; import org.jline.utils.StyleResolver; import org.jline.utils.WCWidth; @@ -2856,9 +2858,13 @@ private List<Pair<Integer, Integer>> matches(Pattern p, String line, int index) { List<Pair<Integer, Integer>> starts = new ArrayList<>(); - Matcher m = p.matcher(line); - while (m.find()) { - starts.add(new Pair<>(index, m.start())); + Matcher m = SafeRegex.matcher(p, line); + try { + while (m.find()) { + starts.add(new Pair<>(index, m.start())); + } + } catch (RegexTimeoutException e) { + // Return whatever matches were found before timeout } return starts; } @@ -4118,25 +4124,22 @@ return ""; } History history = getHistory(); - StringBuilder sb = new StringBuilder(); - for (char c : buffer.replace("\\", "\\\\").toCharArray()) { - if (c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}' || c == '^' || c == '*' || c == '$' - || c == '.' || c == '?' || c == '+' || c == '|' || c == '<' || c == '>' || c == '!' || c == '-') { - sb.append('\\'); - } - sb.append(c); - } - Pattern pattern = Pattern.compile(sb.toString() + ".*", Pattern.DOTALL); + Pattern pattern = Pattern.compile(Pattern.quote(buffer) + ".*", Pattern.DOTALL); Iterator<History.Entry> iter = history.reverseIterator(history.last()); String suggestion = ""; int tot = 0; while (iter.hasNext()) { History.Entry entry = iter.next(); - Matcher matcher = pattern.matcher(entry.line()); - if (matcher.matches()) { - suggestion = entry.line().substring(buffer.length()); - break; - } else if (tot > 200) { + try { + Matcher matcher = SafeRegex.matcher(pattern, entry.line()); + if (matcher.matches()) { + suggestion = entry.line().substring(buffer.length()); + break; + } + } catch (RegexTimeoutException e) { + // Treat timeout as non-match, continue searching + } + if (tot > 200) { break; } tot++; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/reader/src/main/java/org/jline/reader/impl/history/DefaultHistory.java new/jline3-jline-3.30.15/reader/src/main/java/org/jline/reader/impl/history/DefaultHistory.java --- old/jline3-jline-3.30.14/reader/src/main/java/org/jline/reader/impl/history/DefaultHistory.java 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/reader/src/main/java/org/jline/reader/impl/history/DefaultHistory.java 2026-06-30 21:19:48.000000000 +0200 @@ -10,13 +10,17 @@ import java.io.*; import java.nio.file.*; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; import java.time.DateTimeException; import java.time.Instant; import java.util.*; +import java.util.regex.Pattern; import org.jline.reader.History; import org.jline.reader.LineReader; import org.jline.utils.Log; +import org.jline.utils.SafeRegex; import static org.jline.reader.LineReader.HISTORY_IGNORE; import static org.jline.reader.impl.ReaderUtils.*; @@ -388,12 +392,13 @@ if (!Files.exists(parent)) { Files.createDirectories(parent); } - // Append new items to the history file + createWithOwnerOnlyPermissions(path.toAbsolutePath()); + // Append new items to the history file. createWithOwnerOnlyPermissions guarantees the + // file exists, so CREATE is intentionally omitted: should the file be removed in the + // narrow window before this open, fail with NoSuchFileException rather than silently + // re-creating it with umask-default (group/world readable) permissions. try (BufferedWriter writer = Files.newBufferedWriter( - path.toAbsolutePath(), - StandardOpenOption.WRITE, - StandardOpenOption.APPEND, - StandardOpenOption.CREATE)) { + path.toAbsolutePath(), StandardOpenOption.WRITE, StandardOpenOption.APPEND)) { for (Entry entry : items.subList(from, items.size())) { if (isPersistable(entry)) { writer.append(format(entry)); @@ -410,6 +415,63 @@ } /** + * Creates the history file restricted to the owner when it does not exist yet. + * History lines can contain secrets typed on the command line, so the file + * must not be left group/world readable as the umask default (0644) would. + * On non-POSIX filesystems the file is created with the platform default. + */ + private static void createWithOwnerOnlyPermissions(Path path) throws IOException { + if (Files.exists(path)) { + // A pre-existing file (e.g. created by an older JLine without this fix) keeps its + // current permissions; we don't silently tighten it, but warn so the user can. + warnIfAccessibleByOthers(path); + return; + } + try { + Files.createFile( + path, + PosixFilePermissions.asFileAttribute( + EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE))); + } catch (FileAlreadyExistsException e) { + // created concurrently between the check and the call — warn if the + // other creator left the file group/world readable + warnIfAccessibleByOthers(path); + } catch (UnsupportedOperationException e) { + // non-POSIX filesystem (e.g. Windows): fall back to default creation + try { + Files.createFile(path); + } catch (FileAlreadyExistsException ignore) { + // created concurrently (no POSIX perms to warn about) + } + } + } + + private static final Set<Path> WARNED_INSECURE_PERMS = Collections.synchronizedSet(new HashSet<>()); + + private static final Set<PosixFilePermission> NON_OWNER_PERMS = EnumSet.of( + PosixFilePermission.GROUP_READ, + PosixFilePermission.GROUP_WRITE, + PosixFilePermission.GROUP_EXECUTE, + PosixFilePermission.OTHERS_READ, + PosixFilePermission.OTHERS_WRITE, + PosixFilePermission.OTHERS_EXECUTE); + + /** + * Warns, at most once per path, when an existing history file is accessible by users other + * than the owner. The file's permissions are left untouched so as not to surprise the user. + */ + private static void warnIfAccessibleByOthers(Path path) { + try { + Set<PosixFilePermission> perms = Files.getPosixFilePermissions(path); + if (perms.stream().anyMatch(NON_OWNER_PERMS::contains) && WARNED_INSECURE_PERMS.add(path)) { + Log.warn("History file ", path, " is accessible by other users; consider 'chmod 600'"); + } + } catch (UnsupportedOperationException | IOException e) { + // non-POSIX filesystem or attributes unavailable: nothing to warn about + } + } + + /** * Trims the history file to the specified maximum number of entries. * * @param path the path to the history file @@ -608,21 +670,25 @@ if (patterns == null || patterns.isEmpty()) { return false; } + // HISTORY_IGNORE uses a glob-like syntax where '*' matches any string, + // ':' separates alternatives, and '\' escapes the next character. + // All other characters must match literally — regex metacharacters + // are quoted to prevent ReDoS via catastrophic backtracking. StringBuilder sb = new StringBuilder(); for (int i = 0; i < patterns.length(); i++) { char ch = patterns.charAt(i); - if (ch == '\\') { + if (ch == '\\' && i + 1 < patterns.length()) { ch = patterns.charAt(++i); - sb.append(ch); + sb.append(Pattern.quote(String.valueOf(ch))); } else if (ch == ':') { sb.append('|'); } else if (ch == '*') { - sb.append('.').append('*'); + sb.append(".*"); } else { - sb.append(ch); + sb.append(Pattern.quote(String.valueOf(ch))); } } - return line.matches(sb.toString()); + return SafeRegex.matches(Pattern.compile(sb.toString()), line); } /** diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/reader/src/test/java/org/jline/reader/impl/history/HistoryPersistenceTest.java new/jline3-jline-3.30.15/reader/src/test/java/org/jline/reader/impl/history/HistoryPersistenceTest.java --- old/jline3-jline-3.30.14/reader/src/test/java/org/jline/reader/impl/history/HistoryPersistenceTest.java 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/reader/src/test/java/org/jline/reader/impl/history/HistoryPersistenceTest.java 2026-06-30 21:19:48.000000000 +0200 @@ -9,10 +9,16 @@ package org.jline.reader.impl.history; import java.io.IOException; +import java.nio.file.FileStore; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermission; import java.time.Instant; +import java.util.EnumSet; import java.util.List; +import java.util.Set; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; import java.util.stream.IntStream; @@ -21,6 +27,7 @@ import org.jline.reader.impl.ReaderTestSupport; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -119,6 +126,30 @@ } @Test + public void testHistoryFilePermissions() throws Exception { + Path dir = Files.createTempDirectory("jline-hist-perm"); + Path testPath = dir.resolve("history"); + try { + FileStore store = Files.getFileStore(dir); + Assumptions.assumeTrue(store.supportsFileAttributeView(PosixFileAttributeView.class)); + + reader.setVariable(LineReader.HISTORY_FILE, testPath); + DefaultHistory history = new DefaultHistory(reader); + history.add("password=hunter2"); + history.save(); + + Set<PosixFilePermission> perms = Files.getPosixFilePermissions(testPath); + assertEquals( + EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE), + perms, + "newly created history file must not be group/world readable"); + } finally { + Files.deleteIfExists(testPath); + Files.deleteIfExists(dir); + } + } + + @Test public void testHistoryTrimNonTimestamped() { testHistoryTrim(false); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/remote-ssh/pom.xml new/jline3-jline-3.30.15/remote-ssh/pom.xml --- old/jline3-jline-3.30.14/remote-ssh/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/remote-ssh/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -16,7 +16,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline-remote-ssh</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/remote-telnet/pom.xml new/jline3-jline-3.30.15/remote-telnet/pom.xml --- old/jline3-jline-3.30.14/remote-telnet/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/remote-telnet/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -16,7 +16,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline-remote-telnet</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/style/pom.xml new/jline3-jline-3.30.15/style/pom.xml --- old/jline3-jline-3.30.14/style/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/style/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -16,7 +16,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline-style</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/terminal/pom.xml new/jline3-jline-3.30.15/terminal/pom.xml --- old/jline3-jline-3.30.14/terminal/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/terminal/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -16,7 +16,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline-terminal</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/terminal/src/main/java/org/jline/utils/AttributedString.java new/jline3-jline-3.30.15/terminal/src/main/java/org/jline/utils/AttributedString.java --- old/jline3-jline-3.30.14/terminal/src/main/java/org/jline/utils/AttributedString.java 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/terminal/src/main/java/org/jline/utils/AttributedString.java 2026-06-30 21:19:48.000000000 +0200 @@ -440,16 +440,25 @@ * @return a new AttributedString with the specified style applied to matching regions */ public AttributedString styleMatches(Pattern pattern, AttributedStyle style) { - Matcher matcher = pattern.matcher(this); - boolean result = matcher.find(); + Matcher matcher = SafeRegex.matcher(pattern, this); + boolean result; + try { + result = matcher.find(); + } catch (RegexTimeoutException e) { + return this; + } if (result) { long[] newstyle = this.style.clone(); - do { - for (int i = matcher.start(); i < matcher.end(); i++) { - newstyle[this.start + i] = (newstyle[this.start + i] & ~style.getMask()) | style.getStyle(); - } - result = matcher.find(); - } while (result); + try { + do { + for (int i = matcher.start(); i < matcher.end(); i++) { + newstyle[this.start + i] = (newstyle[this.start + i] & ~style.getMask()) | style.getStyle(); + } + result = matcher.find(); + } while (result); + } catch (RegexTimeoutException e) { + // Apply whatever matches we found so far + } return new AttributedString(buffer, newstyle, start, end); } return this; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/terminal/src/main/java/org/jline/utils/RegexTimeoutException.java new/jline3-jline-3.30.15/terminal/src/main/java/org/jline/utils/RegexTimeoutException.java --- old/jline3-jline-3.30.14/terminal/src/main/java/org/jline/utils/RegexTimeoutException.java 1970-01-01 01:00:00.000000000 +0100 +++ new/jline3-jline-3.30.15/terminal/src/main/java/org/jline/utils/RegexTimeoutException.java 2026-06-30 21:19:48.000000000 +0200 @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +/** + * Thrown when a regular expression match exceeds its time budget. + * + * <p>This is an unchecked exception because it is thrown from within + * {@link CharSequence#charAt(int)}, which the {@code java.util.regex} + * engine calls during matching. Callers that use + * {@link SafeRegex#matcher(java.util.regex.Pattern, CharSequence)} + * directly should catch this exception; the convenience methods + * {@link SafeRegex#matches} and {@link SafeRegex#find} catch it + * internally and return {@code false}.</p> + */ +public class RegexTimeoutException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public RegexTimeoutException() { + super("Regular expression matching timed out"); + } + + public RegexTimeoutException(String message) { + super(message); + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/terminal/src/main/java/org/jline/utils/SafeRegex.java new/jline3-jline-3.30.15/terminal/src/main/java/org/jline/utils/SafeRegex.java --- old/jline3-jline-3.30.14/terminal/src/main/java/org/jline/utils/SafeRegex.java 1970-01-01 01:00:00.000000000 +0100 +++ new/jline3-jline-3.30.15/terminal/src/main/java/org/jline/utils/SafeRegex.java 2026-06-30 21:19:48.000000000 +0200 @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2026, the original author(s). + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.utils; + +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Regex utilities that guard against catastrophic backtracking (ReDoS). + * + * <p>Java's {@code java.util.regex} engine uses backtracking, so patterns with + * nested quantifiers (e.g. {@code (a+)+b}) can take exponential time on + * non-matching input. This class wraps input in a {@link CharSequence} that + * enforces a wall-clock deadline, throwing {@link RegexTimeoutException} if + * matching takes too long.</p> + * + * <p>Usage:</p> + * <ul> + * <li>For simple boolean checks, use {@link #matches} or {@link #find} — + * they catch timeouts and return {@code false}.</li> + * <li>When you need the {@link Matcher} (e.g. for a find loop or to read + * match groups), use {@link #matcher} and catch + * {@link RegexTimeoutException} yourself.</li> + * <li>For glob-style patterns (only {@code *} is special), use + * {@link #compileGlob} to compile a {@link Pattern} that properly + * escapes literal characters. Note: {@code compileGlob} only builds + * the pattern; to get timeout protection, pass the result through + * {@link #matcher}, {@link #matches}, or {@link #find}.</li> + * </ul> + */ +public final class SafeRegex { + + /** Default timeout for regex matching operations. */ + private static final long DEFAULT_TIMEOUT_MS = 1500; + + /** + * How often (in {@code charAt} calls) the deadline is checked. + * Checking every call would add measurable overhead; checking every + * 1024 calls keeps the cost negligible while still catching runaway + * backtracking within milliseconds on typical input. + */ + private static final int CHECK_INTERVAL = 1024; + + private SafeRegex() {} + + // ---- matcher factory --------------------------------------------------- + + /** + * Create a {@link Matcher} that will throw {@link RegexTimeoutException} + * if matching exceeds the default timeout. + */ + public static Matcher matcher(Pattern pattern, CharSequence input) { + return matcher(pattern, input, DEFAULT_TIMEOUT_MS); + } + + /** + * Create a {@link Matcher} that will throw {@link RegexTimeoutException} + * if matching exceeds the given timeout. The timeout starts lazily on + * the first deadline check during matching (not when the {@link Matcher} + * is created), so it measures actual matching time. + */ + public static Matcher matcher(Pattern pattern, CharSequence input, long timeoutMs) { + long timeoutNanos = TimeUnit.MILLISECONDS.toNanos(timeoutMs); + return pattern.matcher(new TimeoutCharSequence(input, timeoutNanos)); + } + + // ---- convenience boolean methods --------------------------------------- + + /** + * Test whether the pattern matches the entire input, with timeout + * protection. Returns {@code false} on timeout. + */ + public static boolean matches(Pattern pattern, CharSequence input) { + try { + return matcher(pattern, input).matches(); + } catch (RegexTimeoutException e) { + return false; + } + } + + /** + * Test whether the pattern is found anywhere in the input, with timeout + * protection. Returns {@code false} on timeout. + */ + public static boolean find(Pattern pattern, CharSequence input) { + try { + return matcher(pattern, input).find(); + } catch (RegexTimeoutException e) { + return false; + } + } + + // ---- glob compilation -------------------------------------------------- + + /** + * Compile a glob-style pattern into a {@link Pattern}. + * + * <p>Only {@code *} (match any string) and {@code \} (escape) are + * special; every other character is regex-quoted so it matches + * literally. This is suitable for user-facing wildcard syntax where + * full regex power is not intended.</p> + * + * @param globPattern the glob pattern (e.g. {@code "foo*bar"}) + * @return a compiled regex pattern + */ + public static Pattern compileGlob(String globPattern) { + return compileGlob(globPattern, 0); + } + + /** + * Compile a glob-style pattern into a {@link Pattern} with the given + * regex flags. + * + * @param globPattern the glob pattern + * @param flags regex flags (e.g. {@link Pattern#DOTALL}) + * @return a compiled regex pattern + */ + public static Pattern compileGlob(String globPattern, int flags) { + StringBuilder sb = new StringBuilder(); + int i = 0; + while (i < globPattern.length()) { + char ch = globPattern.charAt(i); + if (ch == '\\' && i + 1 < globPattern.length()) { + appendQuoted(sb, globPattern.charAt(i + 1)); + i += 2; + } else if (ch == '*') { + sb.append(".*"); + i++; + } else { + appendQuoted(sb, ch); + i++; + } + } + return Pattern.compile(sb.toString(), flags); + } + + // ---- internal ---------------------------------------------------------- + + private static void appendQuoted(StringBuilder sb, char ch) { + // Quote individual regex metacharacters inline rather than using + // Pattern.quote() (\Q...\E) to keep the generated regex readable. + if ("\\^$.|?*+()[]{}".indexOf(ch) >= 0) { + sb.append('\\'); + } + sb.append(ch); + } + + /** + * A {@link CharSequence} wrapper that enforces a wall-clock deadline. + * + * <p>The Java regex engine calls {@link #charAt(int)} for every position + * it considers during matching. During catastrophic backtracking the + * call count explodes; we check {@link System#nanoTime()} every + * {@value #CHECK_INTERVAL} calls and throw if the deadline has passed.</p> + * + * <p>The deadline is lazily initialised on the first {@code charAt} + * check so the timeout measures actual matching time, not the gap + * between {@link Matcher} creation and first use. Instances created + * via {@link #subSequence} share the same deadline array, so the + * timeout covers the entire matching operation.</p> + */ + private static final class TimeoutCharSequence implements CharSequence { + + private final CharSequence inner; + private final long timeoutNanos; + /** Shared across {@link #subSequence} calls; element 0 holds the deadline (0 = not yet started). */ + private final long[] sharedDeadline; + + private int calls; + + TimeoutCharSequence(CharSequence inner, long timeoutNanos) { + this.inner = inner; + this.timeoutNanos = timeoutNanos; + this.sharedDeadline = new long[1]; + } + + private TimeoutCharSequence(CharSequence inner, long timeoutNanos, long[] sharedDeadline) { + this.inner = inner; + this.timeoutNanos = timeoutNanos; + this.sharedDeadline = sharedDeadline; + } + + @Override + public char charAt(int index) { + if (++calls % CHECK_INTERVAL == 0) { + long now = System.nanoTime(); + if (sharedDeadline[0] == 0) { + sharedDeadline[0] = now + timeoutNanos; + } else if (now > sharedDeadline[0]) { + throw new RegexTimeoutException(); + } + } + return inner.charAt(index); + } + + @Override + public int length() { + return inner.length(); + } + + @Override + public CharSequence subSequence(int start, int end) { + return new TimeoutCharSequence(inner.subSequence(start, end), timeoutNanos, sharedDeadline); + } + + @Override + public String toString() { + return inner.toString(); + } + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/terminal-ffm/pom.xml new/jline3-jline-3.30.15/terminal-ffm/pom.xml --- old/jline3-jline-3.30.14/terminal-ffm/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/terminal-ffm/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -16,7 +16,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline-terminal-ffm</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/terminal-jansi/pom.xml new/jline3-jline-3.30.15/terminal-jansi/pom.xml --- old/jline3-jline-3.30.14/terminal-jansi/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/terminal-jansi/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -16,7 +16,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline-terminal-jansi</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/terminal-jna/pom.xml new/jline3-jline-3.30.15/terminal-jna/pom.xml --- old/jline3-jline-3.30.14/terminal-jna/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/terminal-jna/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -16,7 +16,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline-terminal-jna</artifactId> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/jline3-jline-3.30.14/terminal-jni/pom.xml new/jline3-jline-3.30.15/terminal-jni/pom.xml --- old/jline3-jline-3.30.14/terminal-jni/pom.xml 2026-06-26 20:10:56.000000000 +0200 +++ new/jline3-jline-3.30.15/terminal-jni/pom.xml 2026-06-30 21:19:48.000000000 +0200 @@ -16,7 +16,7 @@ <parent> <groupId>org.jline</groupId> <artifactId>jline-parent</artifactId> - <version>3.30.14</version> + <version>3.30.15</version> </parent> <artifactId>jline-terminal-jni</artifactId> ++++++ jline3-build.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/common.xml new/common.xml --- old/common.xml 2026-06-30 10:26:47.683438582 +0200 +++ new/common.xml 2026-07-01 15:23:56.001337071 +0200 @@ -3,7 +3,7 @@ <project name="common" basedir="."> <property file="build.properties"/> - <property name="project.version" value="3.30.14"/> + <property name="project.version" value="3.30.15"/> <property name="project.groupId" value="org.jline"/> <property name="project.url" value="https://github.com/jline/jline3"/>
