This is an automated email from the ASF dual-hosted git repository.

tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git


The following commit(s) were added to refs/heads/main by this push:
     new 6034506703 TIKA-4783 -- improve chm parsing (#2942)
6034506703 is described below

commit 6034506703c31b5c2746f41ccd96af14cd1d7e2d
Author: Tim Allison <[email protected]>
AuthorDate: Mon Jul 13 10:08:31 2026 -0400

    TIKA-4783 -- improve chm parsing (#2942)
---
 .../tika/parser/microsoft/chm/ChmCommons.java      |  13 +-
 .../microsoft/chm/ChmDirectoryListingSet.java      |  55 +++---
 .../tika/parser/microsoft/chm/ChmExtractor.java    |   3 +
 .../tika/parser/microsoft/chm/ChmItsfHeader.java   |   3 +-
 .../tika/parser/microsoft/chm/ChmItspHeader.java   |   3 +-
 .../tika/parser/microsoft/chm/ChmLzxBlock.java     |   8 +-
 .../parser/microsoft/chm/ChmLzxcControlData.java   |   3 +-
 .../parser/microsoft/chm/ChmLzxcResetTable.java    |  14 +-
 .../tika/parser/microsoft/chm/ChmPmgiHeader.java   |   3 +-
 .../tika/parser/microsoft/chm/ChmPmglHeader.java   |   3 +-
 .../tika/parser/microsoft/chm/ChmSection.java      |  15 +-
 .../microsoft/chm/TestChmLzxcControlData.java      |  18 ++
 .../parser/microsoft/chm/TestChmMalformed.java     | 194 +++++++++++++++++++++
 .../tika/parser/microsoft/chm/TestParameters.java  |   3 +-
 14 files changed, 289 insertions(+), 49 deletions(-)

diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmCommons.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmCommons.java
index 102a3691c2..25915c97b7 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmCommons.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmCommons.java
@@ -307,7 +307,7 @@ public class ChmCommons {
         checkCopyOfRangeParams(original, from, to);
         int newLength = to - from;
         if (newLength < 0) {
-            throw new IllegalArgumentException(from + " > " + to);
+            throw new ChmParsingException(from + " > " + to);
         }
 
         byte[] copy = new byte[newLength];
@@ -315,18 +315,19 @@ public class ChmCommons {
         return copy;
     }
 
-    private static void checkCopyOfRangeParams(byte[] original, int from, int 
to) {
+    private static void checkCopyOfRangeParams(byte[] original, int from, int 
to)
+            throws ChmParsingException {
         if (original == null) {
-            throw new NullPointerException("array is null");
+            throw new ChmParsingException("array is null");
         }
         if (from < 0) {
-            throw new IllegalArgumentException(from + " should be > 0");
+            throw new ChmParsingException(from + " should be > 0");
         }
         if (to < 0) {
-            throw new IllegalArgumentException(to + " should be > 0");
+            throw new ChmParsingException(to + " should be > 0");
         }
         if (to > original.length) {
-            throw new IllegalArgumentException("can't copy beyond array 
length");
+            throw new ChmParsingException("can't copy beyond array length");
         }
     }
 
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmDirectoryListingSet.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmDirectoryListingSet.java
index ddca6a9aef..e852f3a8f8 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmDirectoryListingSet.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmDirectoryListingSet.java
@@ -18,7 +18,6 @@ package org.apache.tika.parser.microsoft.chm;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-import java.math.BigInteger;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -220,17 +219,12 @@ public class ChmDirectoryListingSet {
             //setPlaceHolder(header_len);
             while (placeHolder > 0 && placeHolder < dir_chunk.length - 
PMGLheader.getFreeSpace()
                 /*&& dir_chunk[placeHolder - 1] != 115*/) {
-                //get entry name length
-                int strlen = 0;// = getEncint(data);
-                byte temp;
-                while ((temp = dir_chunk[placeHolder++]) >= 0x80) {
-                    strlen <<= 7;
-                    strlen += temp & 0x7f;
-                }
-
-                strlen = (strlen << 7) + temp & 0x7f;
+                //get entry name length (variable-length ENCINT). The old 
inline
+                //decoder compared a signed byte to 0x80 (never true), so it 
read a
+                //single byte and capped the length at 127, corrupting longer 
names.
+                int strlen = getEncint(dir_chunk);
 
-                if (strlen > dir_chunk.length) {
+                if (strlen < 0 || strlen > dir_chunk.length) {
                     throw new ChmParsingException("Bad data of a string 
length.");
                 }
 
@@ -324,22 +318,33 @@ public class ChmDirectoryListingSet {
      * @param data_chunk
      * @return
      */
-    private int getEncint(byte[] data_chunk) {
-        byte ob;
-        BigInteger bi = BigInteger.ZERO;
-        byte[] nb = new byte[1];
-
-        if (placeHolder < data_chunk.length) {
-            while ((ob = data_chunk[placeHolder]) < 0) {
-                nb[0] = (byte) ((ob & 0x7f));
-                bi = bi.shiftLeft(7).add(new BigInteger(nb));
-                setPlaceHolder(placeHolder + 1);
-            }
-            nb[0] = (byte) ((ob & 0x7f));
-            bi = bi.shiftLeft(7).add(new BigInteger(nb));
+    private int getEncint(byte[] dataChunk) {
+        int start = placeHolder;
+        //advance past the continuation bytes (high bit set) and the 
terminating byte
+        while (placeHolder < dataChunk.length && dataChunk[placeHolder] < 0) {
+            setPlaceHolder(placeHolder + 1);
+        }
+        if (placeHolder < dataChunk.length) {
             setPlaceHolder(placeHolder + 1);
         }
-        return bi.intValue();
+        return (int) decodeEncint(dataChunk, start);
+    }
+
+    /**
+     * Decodes a variable-length ENCINT beginning at {@code offset}: each byte
+     * contributes its low 7 bits (most-significant group first) and the high 
bit
+     * signals that another byte follows. Package-private for testing.
+     */
+    static long decodeEncint(byte[] data, int offset) {
+        long value = 0;
+        while (offset < data.length && data[offset] < 0) {
+            value = (value << 7) | (data[offset] & 0x7f);
+            offset++;
+        }
+        if (offset < data.length) {
+            value = (value << 7) | (data[offset] & 0x7f);
+        }
+        return value;
     }
 
     /**
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmExtractor.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmExtractor.java
index f63e0f30f2..c40b5687cf 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmExtractor.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmExtractor.java
@@ -131,7 +131,10 @@ public class ChmExtractor {
             setLzxBlocksCache(new ArrayList<>());
 
         } catch (IOException e) {
+            //don't swallow: propagate so a broken stream surfaces instead of 
leaving
+            //a half-built extractor that NPEs later in ChmParser
             LOG.warn("IOException parsing chm file", e);
+            throw e;
         }
     }
 
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmItsfHeader.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmItsfHeader.java
index 59d5458424..9e195d81d3 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmItsfHeader.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmItsfHeader.java
@@ -395,10 +395,11 @@ public class ChmItsfHeader implements 
ChmAccessor<ChmItsfHeader> {
         if (4 > getDataRemained()) {
             throw new TikaException("4 > dataLenght");
         }
+        //shift the top byte in long arithmetic so a value >= 2^31 stays 
unsigned
         dest = (data[this.getCurrentPlace()] & 0xff) |
                 (data[this.getCurrentPlace() + 1] & 0xff) << 8 |
                 (data[this.getCurrentPlace() + 2] & 0xff) << 16 |
-                (data[this.getCurrentPlace() + 3] & 0xff) << 24;
+                ((long) (data[this.getCurrentPlace() + 3] & 0xff)) << 24;
 
         setDataRemained(this.getDataRemained() - 4);
         this.setCurrentPlace(this.getCurrentPlace() + 4);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmItspHeader.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmItspHeader.java
index c2f8eb9a53..6ede88b9d1 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmItspHeader.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmItspHeader.java
@@ -166,10 +166,11 @@ public class ChmItspHeader implements 
ChmAccessor<ChmItspHeader> {
         if (4 > dataLenght) {
             throw new TikaException("4 > dataLenght");
         }
+        //shift the top byte in long arithmetic so a value >= 2^31 stays 
unsigned
         dest = (data[this.getCurrentPlace()] & 0xff) |
                 (data[this.getCurrentPlace() + 1] & 0xff) << 8 |
                 (data[this.getCurrentPlace() + 2] & 0xff) << 16 |
-                (data[this.getCurrentPlace() + 3] & 0xff) << 24;
+                ((long) (data[this.getCurrentPlace() + 3] & 0xff)) << 24;
 
         setDataRemained(this.getDataRemained() - 4);
         this.setCurrentPlace(this.getCurrentPlace() + 4);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmLzxBlock.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmLzxBlock.java
index d1cd17e758..e505c5ed63 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmLzxBlock.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmLzxBlock.java
@@ -876,10 +876,14 @@ public class ChmLzxBlock {
         } else {
             throw new ChmParsingException("data segment should not be null");
         }
-        if (blockLength > 0) {
+        //cap at MAX_CONTENT_SIZE: the content buffer is (int) blockLength, so 
a
+        //blockLength past that either overflows the int (negative allocation) 
or
+        //trips the Integer.MAX_VALUE branch in checkLzxBlock. Real blocks are 
~32KB.
+        if (blockLength > 0 && blockLength <= MAX_CONTENT_SIZE) {
             ++goodParameter;
         } else {
-            throw new ChmParsingException("block length should be more than 
zero");
+            throw new ChmParsingException(
+                    "block length should be between 1 and " + MAX_CONTENT_SIZE 
+ ": " + blockLength);
         }
         return (goodParameter == 3);
     }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmLzxcControlData.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmLzxcControlData.java
index 1d63951ef2..9e6299ee11 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmLzxcControlData.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmLzxcControlData.java
@@ -225,10 +225,11 @@ public class ChmLzxcControlData implements 
ChmAccessor<ChmLzxcControlData> {
         if (4 > getDataRemained()) {
             throw new ChmParsingException("4 > dataLenght");
         }
+        //shift the top byte in long arithmetic so a value >= 2^31 stays 
unsigned
         dest = (data[this.getCurrentPlace()] & 0xff) |
                 (data[this.getCurrentPlace() + 1] & 0xff) << 8 |
                 (data[this.getCurrentPlace() + 2] & 0xff) << 16 |
-                (data[this.getCurrentPlace() + 3] & 0xff) << 24;
+                ((long) (data[this.getCurrentPlace() + 3] & 0xff)) << 24;
 
         setDataRemained(this.getDataRemained() - 4);
         this.setCurrentPlace(this.getCurrentPlace() + 4);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmLzxcResetTable.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmLzxcResetTable.java
index 91543aa3bb..adff2e6c5b 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmLzxcResetTable.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmLzxcResetTable.java
@@ -110,9 +110,16 @@ public class ChmLzxcResetTable implements 
ChmAccessor<ChmLzxcResetTable> {
             setBlockCount(getDataRemained() / 8);
         }
 
-        long[] addresses = new long[(int) getBlockCount()];
+        int count = (int) getBlockCount();
+        if (count < 0) {
+            throw new ChmParsingException("negative chm reset table block 
count: " + count);
+        }
+        long[] addresses = new long[count];
         int rem = getDataRemained() / 8;
-        for (int i = 0; i < rem; i++) {
+        //only read as many addresses as both the block count and the trailing 
data
+        //allow, so a block count smaller than the data can't write past the 
array
+        int toRead = Math.min(count, rem);
+        for (int i = 0; i < toRead; i++) {
             long num = -1;
 
             try {
@@ -144,10 +151,11 @@ public class ChmLzxcResetTable implements 
ChmAccessor<ChmLzxcResetTable> {
 
     private long unmarshalUInt32(byte[] data, long dest) throws TikaException {
         ChmAssert.assertByteArrayNotNull(data);
+        //shift the top byte in long arithmetic so a value >= 2^31 stays 
unsigned
         dest = (data[this.getCurrentPlace()] & 0xff) |
                 (data[this.getCurrentPlace() + 1] & 0xff) << 8 |
                 (data[this.getCurrentPlace() + 2] & 0xff) << 16 |
-                (data[this.getCurrentPlace() + 3] & 0xff) << 24;
+                ((long) (data[this.getCurrentPlace() + 3] & 0xff)) << 24;
 
         setDataRemained(this.getDataRemained() - 4);
         this.setCurrentPlace(this.getCurrentPlace() + 4);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmPmgiHeader.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmPmgiHeader.java
index a57e03d5bb..244b6cef10 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmPmgiHeader.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmPmgiHeader.java
@@ -91,10 +91,11 @@ public class ChmPmgiHeader implements 
ChmAccessor<ChmPmgiHeader> {
         if (4 > getDataRemained()) {
             throw new ChmParsingException("4 > dataLenght");
         }
+        //shift the top byte in long arithmetic so a value >= 2^31 stays 
unsigned
         dest = (data[this.getCurrentPlace()] & 0xff) |
                 (data[this.getCurrentPlace() + 1] & 0xff) << 8 |
                 (data[this.getCurrentPlace() + 2] & 0xff) << 16 |
-                (data[this.getCurrentPlace() + 3] & 0xff) << 24;
+                ((long) (data[this.getCurrentPlace() + 3] & 0xff)) << 24;
 
         setDataRemained(this.getDataRemained() - 4);
         this.setCurrentPlace(this.getCurrentPlace() + 4);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmPmglHeader.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmPmglHeader.java
index e1e74fe175..59f3ba3130 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmPmglHeader.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmPmglHeader.java
@@ -133,10 +133,11 @@ public class ChmPmglHeader implements 
ChmAccessor<ChmPmglHeader> {
         if (4 > getDataRemained()) {
             throw new ChmParsingException("4 > dataLenght");
         }
+        //shift the top byte in long arithmetic so a value >= 2^31 stays 
unsigned
         dest = (data[this.getCurrentPlace()] & 0xff) |
                 (data[this.getCurrentPlace() + 1] & 0xff) << 8 |
                 (data[this.getCurrentPlace() + 2] & 0xff) << 16 |
-                (data[this.getCurrentPlace() + 3] & 0xff) << 24;
+                ((long) (data[this.getCurrentPlace() + 3] & 0xff)) << 24;
 
         setDataRemained(this.getDataRemained() - 4);
         this.setCurrentPlace(this.getCurrentPlace() + 4);
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmSection.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmSection.java
index 6c2961c170..e0cf2e86f5 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmSection.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/chm/ChmSection.java
@@ -161,15 +161,16 @@ public class ChmSection {
 
     public BigInteger getEncint() {
         byte ob;
-        BigInteger bi = BigInteger.ZERO;
-        byte[] nb = new byte[1];
+        //accumulate in a fixed-width long rather than a BigInteger: a run of
+        //continuation bytes from a hostile section would otherwise grow the
+        //BigInteger without bound, an O(n^2) memory/CPU DoS. getByte() 
returns 0
+        //past the end of data, which terminates the loop.
+        long value = 0;
         while ((ob = this.getByte()) < 0) {
-            nb[0] = (byte) ((ob & 0x7f));
-            bi = bi.shiftLeft(7).add(new BigInteger(nb));
+            value = (value << 7) | (ob & 0x7f);
         }
-        nb[0] = (byte) ((ob & 0x7f));
-        bi = bi.shiftLeft(7).add(new BigInteger(nb));
-        return bi;
+        value = (value << 7) | (ob & 0x7f);
+        return BigInteger.valueOf(value);
     }
 
 //    private void setData(byte[] data) {
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/chm/TestChmLzxcControlData.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/chm/TestChmLzxcControlData.java
index 277cb695ee..964f14232a 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/chm/TestChmLzxcControlData.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/chm/TestChmLzxcControlData.java
@@ -142,4 +142,22 @@ public class TestChmLzxcControlData {
         assertEquals(0x8000L, control.getSize());
     }
 
+    @Test
+    public void testUInt32TopBitNotSignExtended() throws Exception {
+        // size = 0x80000000 little-endian; the top byte's shift must be done 
in long
+        // arithmetic, else the whole uint32 reads as a negative int 
(-2147483648)
+        byte[] data = new byte[ChmConstants.CHM_LZXC_MIN_LEN];
+        data[3] = (byte) 0x80; // size = 0x80000000
+        byte[] sig = ChmConstants.LZXC.getBytes(UTF_8);
+        System.arraycopy(sig, 0, data, 4, sig.length);
+        data[8] = 0x01;  // version
+        data[12] = 0x02; // resetInterval
+        data[16] = 0x02; // windowSize
+        data[20] = 0x02; // windowsPerReset
+
+        ChmLzxcControlData control = new ChmLzxcControlData();
+        control.parse(data, control);
+        assertEquals(0x80000000L, control.getSize());
+    }
+
 }
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/chm/TestChmMalformed.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/chm/TestChmMalformed.java
new file mode 100644
index 0000000000..ad8d10585e
--- /dev/null
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/chm/TestChmMalformed.java
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.microsoft.chm;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import org.apache.tika.exception.TikaException;
+
+/**
+ * Characterization harness for CHM parsing on malformed / hostile input.
+ * <p>
+ * CHM is a binary format read from untrusted bytes. The contract for any Tika 
parser is that
+ * bad input surfaces as a {@link TikaException} (here {@link 
ChmParsingException}), never as a
+ * raw {@link RuntimeException} ({@code IllegalArgumentException}, {@code 
NegativeArraySizeException},
+ * {@code ArrayIndexOutOfBoundsException}, {@code NullPointerException}) and 
never as an
+ * {@code OutOfMemoryError}. These tests assert that target. A failing test 
here documents a spot
+ * where malformed input currently escapes as the wrong throwable; the 
hardening turns it green,
+ * while {@code TestChmExtraction} guards that real CHM files still parse.
+ */
+public class TestChmMalformed {
+
+    // ------------------------------------------------------------------
+    // ChmCommons.copyOfRange: the shared offset chokepoint. Malformed CHM
+    // offsets/lengths reach it as out-of-range (from, to) after being cast
+    // from parsed uint32/uint64 fields. It already declares throws 
TikaException.
+    // ------------------------------------------------------------------
+
+    @Test
+    public void testCopyOfRangeToBeyondLength() {
+        byte[] data = new byte[16];
+        assertThrows(TikaException.class, () -> ChmCommons.copyOfRange(data, 
0, 32));
+    }
+
+    @Test
+    public void testCopyOfRangeFromGreaterThanTo() {
+        byte[] data = new byte[16];
+        assertThrows(TikaException.class, () -> ChmCommons.copyOfRange(data, 
12, 4));
+    }
+
+    @Test
+    public void testCopyOfRangeNegativeFrom() {
+        // a wrapped long->int offset can land negative
+        byte[] data = new byte[16];
+        assertThrows(TikaException.class, () -> ChmCommons.copyOfRange(data, 
-1, 4));
+    }
+
+    @Test
+    public void testCopyOfRangeFromBeyondLength() {
+        byte[] data = new byte[16];
+        assertThrows(TikaException.class, () -> ChmCommons.copyOfRange(data, 
32, 40));
+    }
+
+    // ------------------------------------------------------------------
+    // ChmExtractor end-to-end: the ctor parses ITSF/ITSP/directory eagerly.
+    // ------------------------------------------------------------------
+
+    @Test
+    public void testTruncatedShorterThanItsfHeader() {
+        // shorter than CHM_ITSF_V3_LEN, which the ctor immediately 
copyOfRange's out
+        byte[] tiny = new byte[10];
+        assertThrows(TikaException.class,
+                () -> new ChmExtractor(new ByteArrayInputStream(tiny)));
+    }
+
+    @Test
+    public void testEmptyInput() {
+        assertThrows(TikaException.class,
+                () -> new ChmExtractor(new ByteArrayInputStream(new byte[0])));
+    }
+
+    @Test
+    public void testItsfLengthButAllZero() {
+        // long enough for the first header copy, but not a valid ITSF 
signature
+        byte[] zeros = new byte[512];
+        assertThrows(TikaException.class,
+                () -> new ChmExtractor(new ByteArrayInputStream(zeros)));
+    }
+
+    @Test
+    public void testIOExceptionNotSwallowed() {
+        // a stream that fails mid-read must surface, not leave a half-built 
extractor
+        // that NPEs later in ChmParser
+        InputStream broken = new InputStream() {
+            @Override
+            public int read() throws IOException {
+                throw new IOException("boom");
+            }
+        };
+        assertThrows(IOException.class, () -> new ChmExtractor(broken));
+    }
+
+    // ------------------------------------------------------------------
+    // ChmLzxcResetTable.enumerateBlockAddresses allocates new 
long[blockCount] and
+    // reads dataRemained/8 addresses. A block count that is huge (uint32 high 
bit)
+    // or smaller than the trailing data must not become a huge/negative 
allocation
+    // or write past the array.
+    // ------------------------------------------------------------------
+
+    @Test
+    public void testResetTableHugeBlockCountIsCapped() {
+        // 40-byte header, no address data; blockCount 0x80000000 is a large 
unsigned
+        // value that must be capped rather than allocated in full or read as 
negative
+        byte[] data = new byte[40];
+        data[0] = 0x02;             // version = 2 (little-endian)
+        data[7] = (byte) 0x80;      // blockCount = 0x80000000 = 2147483648 
unsigned
+        ChmLzxcResetTable table = new ChmLzxcResetTable();
+        assertDoesNotThrow(() -> table.parse(data, table));
+        assertTrue(table.getBlockAddress().length <= 5000);
+    }
+
+    @Test
+    public void testResetTableBlockCountSmallerThanData() {
+        // block count smaller than the trailing address data must not write 
past
+        // the addresses array (previously an AIOOBE)
+        byte[] data = new byte[56];  // 40-byte header + room for 2 addresses
+        data[0] = 0x02;             // version = 2
+        data[4] = 0x01;             // blockCount = 1
+        ChmLzxcResetTable table = new ChmLzxcResetTable();
+        assertDoesNotThrow(() -> table.parse(data, table));
+        assertEquals(1, table.getBlockAddress().length);
+    }
+
+    // ------------------------------------------------------------------
+    // ChmLzxBlock allocates content of (int) blockLength. A blockLength coming
+    // from a crafted reset table that is >= Integer.MAX_VALUE overflows the 
int
+    // (negative content length) and, before that, null-derefs in 
checkLzxBlock.
+    // ------------------------------------------------------------------
+
+    @Test
+    public void testLzxBlockOversizeBlockLength() {
+        // blockLength >= Integer.MAX_VALUE (also > MAX_CONTENT_SIZE)
+        assertThrows(TikaException.class,
+                () -> new ChmLzxBlock(0, new byte[16], 0x80000000L, null));
+    }
+
+    // ------------------------------------------------------------------
+    // Entry name lengths are variable-length ENCINTs. A signed-byte 
comparison used
+    // to cap the decoded value at 127 (corrupting longer names), and 
BigInteger-based
+    // decoders grew without bound on a run of continuation bytes (an O(n^2) 
DoS).
+    // ------------------------------------------------------------------
+
+    @Test
+    public void testDecodeEncintSingleByte() {
+        assertEquals(0, ChmDirectoryListingSet.decodeEncint(new byte[] {0x00}, 
0));
+        assertEquals(5, ChmDirectoryListingSet.decodeEncint(new byte[] {0x05}, 
0));
+        assertEquals(127, ChmDirectoryListingSet.decodeEncint(new byte[] 
{0x7f}, 0));
+    }
+
+    @Test
+    public void testDecodeEncintMultiByte() {
+        // the high bit signals a continuation byte: 0x81 0x00 = 128, 0x81 
0x48 = 200
+        assertEquals(128, ChmDirectoryListingSet.decodeEncint(new byte[] 
{(byte) 0x81, 0x00}, 0));
+        assertEquals(200, ChmDirectoryListingSet.decodeEncint(new byte[] 
{(byte) 0x81, 0x48}, 0));
+        assertEquals(16384,
+                ChmDirectoryListingSet.decodeEncint(new byte[] {(byte) 0x81, 
(byte) 0x80, 0x00}, 0));
+    }
+
+    @Test
+    @Timeout(value = 30, unit = TimeUnit.SECONDS)
+    public void testChmSectionGetEncintNotUnboundedBigInteger() throws 
Exception {
+        // a section that is all continuation bytes must not accumulate an 
unbounded
+        // BigInteger (previously O(n^2)); the bounded long version returns 
promptly
+        byte[] hostile = new byte[1_000_000];
+        Arrays.fill(hostile, (byte) 0xff);
+        ChmSection section = new ChmSection(hostile);
+        assertDoesNotThrow(section::getEncint);
+    }
+}
diff --git 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/chm/TestParameters.java
 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/chm/TestParameters.java
index 2774ce56f7..da742fd778 100644
--- 
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/chm/TestParameters.java
+++ 
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/test/java/org/apache/tika/parser/microsoft/chm/TestParameters.java
@@ -57,7 +57,8 @@ public class TestParameters {
     static final int VP_INDEX_DEPTH = 1;
     static final int VP_INDEX_HEAD = 0;
     static final int VP_INDEX_ROOT = -1;
-    static final int VP_UNKNOWN_NUM_BLOCKS = -1;
+    //0xFFFFFFFF read as an unsigned uint32 (was -1 before the sign-extension 
fix)
+    static final long VP_UNKNOWN_NUM_BLOCKS = 0xFFFFFFFFL;
     static final int VP_ITSP_UNKNOWN_000C = 10;
     static final int VP_ITSP_UNKNOWN_0024 = 0;
     static final int VP_ITSP_UNKNOWN_002C = 1;

Reply via email to