pjfanning opened a new pull request, #1440:
URL: https://github.com/apache/poi/pull/1440

   ## Repro
   
   Each of these takes seconds with a crafted format string from styles.xml / 
an HSSF FORMAT record, or an embedded object name (times for 20k-char inputs 
below):
   
   - `DataFormatter`, colours: `"a"` x 10k + `"[RED]"` x 2k. Every colour was 
removed by `indexOf` + `substring`, then the search started again from the 
beginning.
   - `DataFormatter`, `localePatternGroup` `\[\$[^-\]]*(?:-[0-9A-Za-z]+)?]`: 
`"[$"` x 10k with no `]` or `-`. Each `[$` scans to the end of the string and 
fails.
   - `DataFormatter.InternalDecimalFormatWithScale`, `endsWithCommas` `(,+)$`: 
a long run of commas that is not at the end, e.g. `0"x,,,,...,y"`. find() is 
tried from every comma in the run.
   - `DateUtil.date_ptrn3b`: `y` + `/` x n + `x`. `/`, `m`, `M` and space are 
in both the first class and the `0* ?[ampAMP/]*` tail, so `+` gives them back 
one at a time.
   - `EmbeddedData.setFilename`, `[^/\\]*[/\\]` replaceAll: a long name with no 
separator. Each start position scans to the end.
   
   ## Fix
   
   - Colours: one pass that appends to a StringBuilder. A colour has no 
brackets inside it, so a colour can only end at a `]` and start at the last `[` 
before it. On a `]`, the text from that `[` is checked with 
`colorPattern.matches()` and removed if it is a colour. That also handles 
colours made by an earlier removal, e.g. `[Re[Red]d]`, as the old loop did.
   - Locale: a scanner gives the same matches as the regex. From `[$` it scans 
to the first `-` or `]`. `]` is a match. `-` needs one or more ASCII 
letters/digits and then `]`. If that fails, the search resumes after the `-`, 
because any `[$` before it stops at the same `-` and fails the same way. The 
replacement text is unchanged.
   - `endsWithCommas` becomes `(?<!,)(,++)$`. A match can only start at the 
first comma of a run, and a shorter run never reaches `$`.
   - `normalizeSectionGrouping` counted the scaling commas by repeatedly 
calling `substring` (also quadratic). It now counts them with an index.
   - `date_ptrn3b`: the first class is possessive (`++`). If the old regex 
matched by giving chars back, those chars are `m`/`M`/`/` and maybe a space. 
The rest of the string after the longest first-class prefix starts with a char 
outside that class, so it can only be `[ampAMP/]*`. The possessive form 
therefore matches with an empty `0* ?` and the tail.
   - `EmbeddedData`: `substring(max(lastIndexOf('/'), lastIndexOf('\\')) + 
1).trim()`.
   
   ## Equivalence check
   
   I compared the old code against the new code on every string up to a length 
bound and looked for identical outputs:
   
   | What | Alphabet | Max length | Strings |
   |---|---|---|---|
   | colour removal (output) | `[ ] R e D x` | 9 | 12,093,235 |
   | colour removal (output) | tokens `[ ] RED Re d COLOR color ␠ \t 1 5 6 x` | 
6 tokens | 5,229,043 |
   | locale replacement (output of the whole loop) | `[ $ - ] a Z 1 . \ _` | 7 
| 11,111,111 |
   | endsWithCommas (replaceAll result, find() group and start) | `, 0 x \n \r 
\u0085 
` | 8 | 6,725,601 |
   | date_ptrn3b matches() | `y / ␠ 0 a m M P x \n` | 7 | 11,111,111 |
   | EmbeddedData file name | `/ \ a ␠ \n \t` | 9 | 12,093,235 |
   
   I also compared 2M random strings of length < 30 for colours and for 
locales. Every output was identical.
   
   ## Timings (20k-char adversarial input, old vs new)
   
   | Case | Old | New |
   |---|---|---|
   | colours | 20,028 ms | 2 ms |
   | locale | 21,837 ms | 0 ms |
   | endsWithCommas | 91,688 ms (replaceAll + find) | 4 ms |
   | date_ptrn3b | 17,237 ms | 11 ms |
   | EmbeddedData | 18,716 ms | 0 ms |
   
   ## Tests
   
   New tests use 50k-char inputs with 
`assertTimeoutPreemptively(Duration.ofSeconds(10), ...)`. All six time out on 
trunk and pass with this change:
   TestDataFormatter `testManyColours`, `testLocaleWithoutEnd`, 
`testManyCommasNotAtEnd`, `testLongDateLikeFormat`; TestDateUtil 
`isADateFormatWithLongRun`; TestEmbeddedExtractor `filenameWithoutPath`.
   
   I ran these test classes, with no failures:
   - poi: TestDataFormatter (70, 2 skipped), TestDateUtil (29), 
TestHSSFDataFormatter (5), TestCellFormat (50), TestCellFormatCondition (1), 
TestCellFormatResult (1)
   - poi-ooxml: TestDataFormatterAgainstExcel (569), TestXSSFDataFormat (12), 
TestCellFormatPart (9), TestEmbeddedExtractor (4), TestEmbedOLEPackage (3)
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to