Skip to content

Commit 2c9df3b

Browse files
committed
Fix parse error of system default /usr/share/nano/*.nanorc
(Recent) `nano` packages in Ubuntu come with some `.nanorc` files preinstalled. jline's `NanorcParser` sadly fails parsing a couple of the regular expressions. This change translates the regular expressions to Java regular expressions. The differences are described in `org.jline.builtins.SyntaxHighlighter#posixToJavaRegex`: * The first `]` in a bracket expression does not need to be escaped in Posix,translate to `\]`. * Same as above for a negating bracket expression like `[^][]`, translate to `[^\]\[]`. * Any `[` in a bracket expression does not need to be escaped in Posix, translate to `\[`. * Any `]` not in a bracket expression is valid in both Posix and Java, no translation. * A backslash before the closing bracket like `[.f\]` is not an escape of the closing bracket, the backslash needs to be escaped for Java, translate to `[.f\\]`. * Do not perform the above translations within an escape via `\`. * Do not perform the above translations for Posix "classes" like `[[:word:]]` or `[[:digit:]]` and their negation `[-[:word]]`. * Do not perform the above translations for single-bracket Posix classes like `[:digit:]`, and handle the case of single-bracket Posix classes inside bracket expressions, like `[[:digit:]-.]`. Test cases have been added. There are however two regexes that still don't work, but those look invalid. To let jnano not trip over these, any `PatternSyntaxException` lets jnano just ignore the particular rule. A warning is logged in such cases. Fixes #1156
1 parent 4504b46 commit 2c9df3b

3 files changed

Lines changed: 773 additions & 31 deletions

File tree

‎builtins/pom.xml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@
4747
<artifactId>junit-jupiter-api</artifactId>
4848
<scope>test</scope>
4949
</dependency>
50+
<dependency>
51+
<groupId>org.junit.jupiter</groupId>
52+
<artifactId>junit-jupiter-params</artifactId>
53+
<scope>test</scope>
54+
</dependency>
5055
</dependencies>
5156

5257
<build>

‎builtins/src/main/java/org/jline/builtins/SyntaxHighlighter.java‎

Lines changed: 171 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -656,47 +656,187 @@ private void addHighlightRule(String reference, List<String> parts, boolean case
656656
Styles.StyleCompiler sh = new Styles.StyleCompiler(spec, true);
657657
AttributedStyle style = new StyleResolver(sh::getStyle).resolve("." + reference);
658658

659-
if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.PATTERN) {
660-
if (parts.size() == 2) {
661-
highlightRules.get(tokenName).add(new HighlightRule(style, doPattern(".*", caseInsensitive)));
662-
} else {
663-
for (int i = 2; i < parts.size(); i++) {
664-
highlightRules
665-
.get(tokenName)
666-
.add(new HighlightRule(style, doPattern(parts.get(i), caseInsensitive)));
659+
try {
660+
if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.PATTERN) {
661+
if (parts.size() == 2) {
662+
highlightRules.get(tokenName).add(new HighlightRule(style, doPattern(".*", caseInsensitive)));
663+
} else {
664+
for (int i = 2; i < parts.size(); i++) {
665+
highlightRules
666+
.get(tokenName)
667+
.add(new HighlightRule(style, doPattern(parts.get(i), caseInsensitive)));
668+
}
667669
}
670+
} else if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.START_END) {
671+
String s = parts.get(2);
672+
String e = parts.get(3);
673+
highlightRules
674+
.get(tokenName)
675+
.add(new HighlightRule(
676+
style,
677+
doPattern(s.substring(7, s.length() - 1), caseInsensitive),
678+
doPattern(e.substring(5, e.length() - 1), caseInsensitive)));
679+
} else if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.PARSER_START_WITH) {
680+
highlightRules
681+
.get(tokenName)
682+
.add(new HighlightRule(
683+
HighlightRule.RuleType.PARSER_START_WITH,
684+
style,
685+
parts.get(2).substring(10)));
686+
} else if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.PARSER_CONTINUE_AS) {
687+
highlightRules
688+
.get(tokenName)
689+
.add(new HighlightRule(
690+
HighlightRule.RuleType.PARSER_CONTINUE_AS,
691+
style,
692+
parts.get(2).substring(11)));
668693
}
669-
} else if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.START_END) {
670-
String s = parts.get(2);
671-
String e = parts.get(3);
672-
highlightRules
673-
.get(tokenName)
674-
.add(new HighlightRule(
675-
style,
676-
doPattern(s.substring(7, s.length() - 1), caseInsensitive),
677-
doPattern(e.substring(5, e.length() - 1), caseInsensitive)));
678-
} else if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.PARSER_START_WITH) {
679-
highlightRules
680-
.get(tokenName)
681-
.add(new HighlightRule(
682-
HighlightRule.RuleType.PARSER_START_WITH,
683-
style,
684-
parts.get(2).substring(10)));
685-
} else if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.PARSER_CONTINUE_AS) {
686-
highlightRules
687-
.get(tokenName)
688-
.add(new HighlightRule(
689-
HighlightRule.RuleType.PARSER_CONTINUE_AS,
690-
style,
691-
parts.get(2).substring(11)));
694+
} catch (PatternSyntaxException e) {
695+
Log.warn("Invalid highlight regex", e);
692696
}
693697
}
694698

695699
private Pattern doPattern(String regex, boolean caseInsensitive) {
700+
regex = posixToJavaRegex(regex);
696701
return caseInsensitive ? Pattern.compile(regex, Pattern.CASE_INSENSITIVE) : Pattern.compile(regex);
697702
}
698703
}
699704

705+
/**
706+
* Posix regex is different from Java regex. This function parses the given Posix regex and escapes according to these rules:
707+
*
708+
* <p>The first {@code ]} in a bracket expression does not need to be escaped in Posix,translate to {@code \]}.
709+
*
710+
* <p>Same as above for a negating bracket expression like {@code [^][]}, translate to {@code [^\]\[]}.
711+
*
712+
* <p>Any {@code [} in a bracket expression does not need to be escaped in Posix, translate to {@code \[}.
713+
*
714+
* <p>Any {@code ]} not in a bracket expression is valid in both Posix and Java, no translation.
715+
*
716+
* <p>A backslash before the closing bracket like {@code [.f\]} is not an escape of the closing bracket,
717+
* the backslash needs to be escaped for Java, translate to {@code [.f\\]}.
718+
*
719+
* <p>Do not perform the above translations within an escape via {@code \}.
720+
*
721+
* <p>Do not perform the above translations for Posix "classes" like {@code [[:word:]]} or {@code [[:digit:]]}
722+
* and their negation {@code [-[:word]]}.
723+
*
724+
* <p>Do not perform the above translations for single-bracket Posix classes like {@code [:digit:]},
725+
* and handle the case of single-bracket Posix classes inside bracket expressions, like
726+
* @code {[[:digit:]-.]}.
727+
*
728+
* @param posix Posix regex
729+
* @return Java regex
730+
*/
731+
static String posixToJavaRegex(String posix) {
732+
int len = posix.length();
733+
StringBuilder java = new StringBuilder();
734+
735+
boolean inBracketExpression = false;
736+
737+
int i = 0;
738+
char next;
739+
try {
740+
for (; i < len; i++) {
741+
char c = posix.charAt(i);
742+
743+
switch (c) {
744+
case '\\':
745+
next = posix.charAt(++i);
746+
// Don't translate anything after the \ character escape
747+
if (inBracketExpression && next == ']') {
748+
inBracketExpression = false;
749+
java.append("\\\\").append(next);
750+
} else {
751+
java.append(c).append(next);
752+
}
753+
break;
754+
case '[':
755+
if (i == len - 1) {
756+
throw new IllegalArgumentException("Lone [ at the end of (index " + i + "): " + posix);
757+
}
758+
// Handle "double bracket" Posix "classes" like [[:word:]] or [[:digit:]] and their negations
759+
// starting with [-[:
760+
if (posix.regionMatches(i, "[[:", 0, 3) || posix.regionMatches(i, "[-[:", 0, 4)) {
761+
int afterClass = nextAfterClass(posix, i + 3);
762+
if (posix.regionMatches(afterClass, ":]]", 0, 3)) {
763+
java.append(posix, i, afterClass + 3);
764+
i = afterClass + 2;
765+
break;
766+
} else if (posix.regionMatches(afterClass, ":]", 0, 2)) {
767+
if (inBracketExpression) {
768+
throw new IllegalArgumentException("Unclear bracket expression");
769+
}
770+
// Handles character patterns like [[:alpha:]_-]
771+
java.append(posix, i, afterClass + 2);
772+
i = afterClass + 1;
773+
inBracketExpression = true;
774+
break;
775+
} else {
776+
throw new IllegalArgumentException("Invalid character class");
777+
}
778+
}
779+
// Handle "single bracket" Posix "classes" like [:word:]
780+
else if (posix.charAt(i + 1) == ':') {
781+
int afterClass = nextAfterClass(posix, i + 2);
782+
if (!posix.regionMatches(afterClass, ":]", 0, 2)) {
783+
java.append("[:");
784+
i++;
785+
inBracketExpression = true;
786+
} else {
787+
java.append(posix, i, afterClass + 2);
788+
i = afterClass + 1;
789+
}
790+
break;
791+
}
792+
if (inBracketExpression) {
793+
// Translate lone [ to \[
794+
java.append('\\').append(c);
795+
} else {
796+
inBracketExpression = true;
797+
java.append(c);
798+
next = posix.charAt(i + 1);
799+
if (next == ']') {
800+
i++;
801+
java.append("\\]");
802+
} else if (next == '^' && posix.charAt(i + 2) == ']') {
803+
i += 2;
804+
java.append("^\\]");
805+
}
806+
}
807+
break;
808+
case ']':
809+
if (inBracketExpression) {
810+
inBracketExpression = false;
811+
}
812+
java.append(c);
813+
break;
814+
default:
815+
java.append(c);
816+
break;
817+
}
818+
}
819+
} catch (Exception e) {
820+
throw new IllegalArgumentException(
821+
"Posix-to-Java regex translation failed around index " + i + " of: " + posix, e);
822+
}
823+
return java.toString();
824+
}
825+
826+
private static int nextAfterClass(String s, int idx) {
827+
if (s.charAt(idx) == ':') {
828+
idx++;
829+
}
830+
while (true) {
831+
char c = s.charAt(idx);
832+
if (!Character.isLetterOrDigit(c)) {
833+
break;
834+
}
835+
idx++;
836+
}
837+
return idx;
838+
}
839+
700840
protected static class RuleSplitter {
701841
protected static List<String> split(String s) {
702842
List<String> out = new ArrayList<>();

0 commit comments

Comments
 (0)