Commit 289f7266 for tesseract

commit 289f72664cb51a1476a0869cb027fe53be1f3e01
Author: Stefan Weil <sw@weilnetz.de>
Date:   Sun Jul 12 21:28:34 2026 +0200

    Fix O(n²) forward-edge validation and TFile::Skip overflow

    - dawg.cpp: restructure forward-edge run validation to validate each
      edge in the run in a single pass and advance the outer loop to the
      terminator, reducing worst-case from O(n²) to O(n).
    - serialis.cpp: replace addition-based bounds check in Skip with a
      subtraction-based check to avoid overflow when offset_ + count
      wraps. Clamp offset_ to UINT_MAX when data exceeds 32-bit range.

    Assisted-by: OpenCode / big-pickle (opencode)
    Reported-by: GitHub Copilot
    Signed-off-by: Stefan Weil <stweil@tessus.org>

diff --git a/src/ccutil/serialis.cpp b/src/ccutil/serialis.cpp
index f750823a..b8914c0b 100644
--- a/src/ccutil/serialis.cpp
+++ b/src/ccutil/serialis.cpp
@@ -143,9 +143,13 @@ bool TFile::Serialize(const std::vector<char> &data) {
 }

 bool TFile::Skip(size_t count) {
-  if (data_ != nullptr && offset_ + count > data_->size()) {
-    offset_ = data_->size();
-    return false;
+  if (data_ != nullptr) {
+    size_t data_size = data_->size();
+    // Subtraction-based check to avoid overflow in offset_ + count.
+    if (offset_ >= data_size || count > data_size - offset_) {
+      offset_ = data_size > UINT_MAX ? UINT_MAX : static_cast<unsigned>(data_size);
+      return false;
+    }
   }
   offset_ += count;
   return true;
diff --git a/src/dict/dawg.cpp b/src/dict/dawg.cpp
index f386dd9d..93a18f6e 100644
--- a/src/dict/dawg.cpp
+++ b/src/dict/dawg.cpp
@@ -391,17 +391,19 @@ bool SquishedDawg::read_squished_dawg(TFile *file) {
     if (edges_[i] == next_node_mask_) {
       continue; // Empty slot.
     }
-    NODE_REF next = next_node_from_edge_rec(edges_[i]);
-    if (next != 0 && static_cast<uint32_t>(next) >= num_edges_) {
-      tprintf("Dawg edge %u has out-of-bounds next_node\n", i);
-      return false;
-    }
     if (forward_edge(i)) {
-      uint32_t j = i;
+      // Validate the entire forward-edge run and skip to its terminator.
       bool terminated = false;
+      uint32_t j = i;
       do {
+        NODE_REF nj = next_node_from_edge_rec(edges_[j]);
+        if (nj != 0 && static_cast<uint32_t>(nj) >= num_edges_) {
+          tprintf("Dawg edge %u has out-of-bounds next_node\n", j);
+          return false;
+        }
         if (last_edge(j)) {
           terminated = true;
+          i = j; // Advance outer loop past the terminator.
           break;
         }
         ++j;
@@ -410,6 +412,12 @@ bool SquishedDawg::read_squished_dawg(TFile *file) {
         tprintf("Dawg forward edge run starting at %u is not terminated\n", i);
         return false;
       }
+    } else {
+      NODE_REF next = next_node_from_edge_rec(edges_[i]);
+      if (next != 0 && static_cast<uint32_t>(next) >= num_edges_) {
+        tprintf("Dawg edge %u has out-of-bounds next_node\n", i);
+        return false;
+      }
     }
   }
   if (debug_level_ > 2) {