Fix "pushback value was too large" crash on gender forms with non-ASCII slash
The SLASH macro matches four characters (/ ⁄ ∕ /), but the gender-slash handlers
genderSlashSuffixToken() and genderNounSlashToken() located the separator with
matched.lastIndexOf('/'), which only finds the ASCII solidus. For the non-ASCII
variants (U+2044, U+2215, U+FF0F) it returned -1, so yypushback(length - (-1))
exceeded the match length and JFlex threw "pushback value was too large" — crashing
the tokenizer on real EPUB input (e.g. DNB25).
Add a lastIndexOfSlash() helper that matches all four SLASH characters and use it in
both handlers, so a non-ASCII slash now tokenizes identically to the ASCII one. Adds
a regression test (testGenderSensitiveSlashFormsWithUnicodeSlash); note tokenizePos
catches the Error and recovers lossily, so the test asserts the exact tokens rather
than merely "does not throw".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Change-Id: I0b3ab80813b002b9714fb040f4c292318b70f445
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a193dce..d767074 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,12 @@
# Changelog
+## 2.4.2 [2026-06-16]
+
+* Fixed a crash (`pushback value was too large`) on German gender-sensitive forms
+ separated by a non-ASCII slash — division slash (U+2215 `∕`), fraction slash
+ (U+2044 `⁄`) or fullwidth solidus (U+FF0F `/`) — which could occur on EPUB input
+
## 2.4.1 [2026-04-03]
* Fixed Unicode surrogate pair handling bugs in German gender sensitive forms
diff --git a/src/main/jpc/jflex/de/ids_mannheim/korap/tokenizer/DerekoDfaTokenizer.jflex b/src/main/jpc/jflex/de/ids_mannheim/korap/tokenizer/DerekoDfaTokenizer.jflex
index 58bbf28..afad9ee 100644
--- a/src/main/jpc/jflex/de/ids_mannheim/korap/tokenizer/DerekoDfaTokenizer.jflex
+++ b/src/main/jpc/jflex/de/ids_mannheim/korap/tokenizer/DerekoDfaTokenizer.jflex
@@ -405,6 +405,23 @@
}
/**
+ * Find the last index of a SLASH-class separator. MUST stay in sync with the
+ * SLASH macro ([/⁄∕/]): matched.lastIndexOf('/') alone misses the non-ASCII
+ * variants (U+2044, U+2215, U+FF0F); for those it returns -1, which made
+ * yypushback(length - (-1)) overflow and throw "pushback value was too large"
+ * on gender-slash forms (e.g. "Lehrer∕innenx").
+ */
+ private static int lastIndexOfSlash(String s) {
+ for (int i = s.length() - 1; i >= 0; i--) {
+ char c = s.charAt(i);
+ if (c == '/' || c == '⁄' || c == '∕' || c == '/') {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
* Handle gender short suffix with colon separator.
* Pattern: {WORD}:{suffix}{lookahead}
* If lookahead is a letter, return just WORD, pushing back the rest.
@@ -439,7 +456,7 @@
int lastChar = matched.codePointBefore(matched.length());
// Find the slash position
- int slashPos = matched.lastIndexOf('/');
+ int slashPos = lastIndexOfSlash(matched);
if (isLetter(lastChar)) {
// Followed by a letter - not a valid gender form
@@ -521,7 +538,7 @@
int lastChar = matched.codePointBefore(matched.length());
// Find the slash position
- int slashPos = matched.lastIndexOf('/');
+ int slashPos = lastIndexOfSlash(matched);
if (isLetter(lastChar)) {
// Followed by a letter - not a valid gender form
diff --git a/src/test/java/de/ids_mannheim/korap/tokenizer/TokenizerTest.java b/src/test/java/de/ids_mannheim/korap/tokenizer/TokenizerTest.java
index 8ad27c2..1499f76 100644
--- a/src/test/java/de/ids_mannheim/korap/tokenizer/TokenizerTest.java
+++ b/src/test/java/de/ids_mannheim/korap/tokenizer/TokenizerTest.java
@@ -926,6 +926,31 @@
}
@Test
+ public void testGenderSensitiveSlashFormsWithUnicodeSlash() {
+ // Regression: the SLASH macro matches four characters (/ ⁄ ∕ /), but the
+ // gender-slash handlers located the separator with matched.lastIndexOf('/'),
+ // which returns -1 for the non-ASCII variants. That made yypushback compute
+ // length-(-1) and throw "pushback value was too large" (crash on real epub
+ // input, e.g. DNB25). See genderSlashSuffixToken/genderNounSlashToken.
+ DerekoDfaTokenizer_de tok = new DerekoDfaTokenizer_de();
+
+ // Valid Unicode-slash gender form (non-letter lookahead) is kept as one
+ // token, exactly like its ASCII counterpart:
+ String[] tokens = tok.tokenize("Lehrer∕innen kamen."); // U+2215 DIVISION SLASH
+ assertEquals("Lehrer∕innen", tokens[0]);
+ assertEquals("kamen", tokens[1]);
+ assertEquals(".", tokens[2]);
+ assertEquals(3, tokens.length);
+
+ // Crash cases (genderSlashSuffixToken): a non-ASCII slash + short gender
+ // suffix + letter lookahead must not overflow the pushback. The stem, the
+ // slash and the remainder are all emitted (no character is lost):
+ assertArrayEquals(new String[]{"Auto", "∕", "nn"}, tok.tokenize("Auto∕nn")); // U+2215
+ assertArrayEquals(new String[]{"Gerd", "⁄", "ss"}, tok.tokenize("Gerd⁄ss")); // U+2044
+ assertArrayEquals(new String[]{"Tag", "/", "rr"}, tok.tokenize("Tag/rr")); // U+FF0F
+ }
+
+ @Test
public void testGenderSensitiveParentheticalForms() {
DerekoDfaTokenizer_de tok = new DerekoDfaTokenizer_de();