Skip to content

Commit c603b63

Browse files
committed
fix: close latent crash paths for malformed physical_index values
Review follow-ups. Removing the delete-based validators re-exposed two crash paths that predate the sanitization PR and were masked by it (at the cost of silently deleted sections): - convert_physical_index_to_int crashed on malformed markers ("<physical_index_x>" -> int('x>') ValueError) and passed bare numeric strings through, which later raised TypeError at the validate_and_truncate comparison. Marker tails and bare numeric strings now parse to int; anything unparseable is left as-is for validation to nullify. Single-value mode now accepts bare "7" (previously None). - validate_and_truncate_physical_indices only checked the upper bound. Below-start_index values (possible when process_large_node_recursively runs with start_index > 1) reached page_list[negative] in verify_toc and could raise IndexError. Non-int and out-of-range values are now nullified — this runs after the None-entry filter, so nullified entries survive as placeholders on the existing None-tolerant paths. - _parse_physical_index accepted bools (True -> page 1), truncated non-integral floats (1.9 -> page 1), and raised uncaught OverflowError on infinite floats. It now rejects all three.
1 parent 78b5bc4 commit c603b63

3 files changed

Lines changed: 71 additions & 17 deletions

File tree

pageindex/page_index.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@
1212
_PHYSICAL_INDEX_MARKER_RE = re.compile(r"^<physical_index_(\d+)>$")
1313

1414
def _parse_physical_index(raw):
15-
if raw is None:
15+
if raw is None or isinstance(raw, bool):
1616
return None
1717
marker_match = _PHYSICAL_INDEX_MARKER_RE.match(str(raw).strip())
1818
if marker_match:
1919
return int(marker_match.group(1))
20+
if isinstance(raw, float) and not raw.is_integer():
21+
return None
2022
try:
2123
return int(raw)
22-
except (TypeError, ValueError):
24+
except (TypeError, ValueError, OverflowError):
2325
return None
2426

2527

@@ -1188,14 +1190,15 @@ def validate_and_truncate_physical_indices(toc_with_page_number, page_list_lengt
11881190
for i, item in enumerate(toc_with_page_number):
11891191
if item.get('physical_index') is not None:
11901192
original_index = item['physical_index']
1191-
if original_index > max_allowed_page:
1193+
if (not isinstance(original_index, int) or isinstance(original_index, bool)
1194+
or not (start_index <= original_index <= max_allowed_page)):
11921195
item['physical_index'] = None
11931196
truncated_items.append({
11941197
'title': item.get('title', 'Unknown'),
11951198
'original_index': original_index
11961199
})
11971200
if logger:
1198-
logger.info(f"Removed physical_index for '{item.get('title', 'Unknown')}' (was {original_index}, too far beyond document)")
1201+
logger.info(f"Removed physical_index for '{item.get('title', 'Unknown')}' (was {original_index}, outside the document range)")
11991202

12001203
if truncated_items and logger:
12011204
logger.info(f"Total removed items: {len(truncated_items)}")

pageindex/utils.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -580,19 +580,24 @@ def convert_physical_index_to_int(data):
580580
# Check if item is a dictionary and has 'physical_index' key
581581
if isinstance(data[i], dict) and 'physical_index' in data[i]:
582582
if isinstance(data[i]['physical_index'], str):
583-
if data[i]['physical_index'].startswith('<physical_index_'):
584-
data[i]['physical_index'] = int(data[i]['physical_index'].split('_')[-1].rstrip('>').strip())
585-
elif data[i]['physical_index'].startswith('physical_index_'):
586-
data[i]['physical_index'] = int(data[i]['physical_index'].split('_')[-1].strip())
583+
value = data[i]['physical_index']
584+
if value.startswith('<physical_index_'):
585+
value = value.split('_')[-1].rstrip('>').strip()
586+
elif value.startswith('physical_index_'):
587+
value = value.split('_')[-1].strip()
588+
try:
589+
data[i]['physical_index'] = int(value)
590+
except ValueError:
591+
pass
587592
elif isinstance(data, str):
588-
if data.startswith('<physical_index_'):
589-
data = int(data.split('_')[-1].rstrip('>').strip())
590-
elif data.startswith('physical_index_'):
591-
data = int(data.split('_')[-1].strip())
592-
# Check data is int
593-
if isinstance(data, int):
594-
return data
595-
else:
593+
value = data
594+
if value.startswith('<physical_index_'):
595+
value = value.split('_')[-1].rstrip('>').strip()
596+
elif value.startswith('physical_index_'):
597+
value = value.split('_')[-1].strip()
598+
try:
599+
return int(value)
600+
except ValueError:
596601
return None
597602
return data
598603

tests/test_page_index.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,53 @@
11
import unittest
22
from unittest.mock import Mock, patch
33

4-
from pageindex.page_index import process_toc_no_page_numbers
4+
from pageindex.page_index import (
5+
_parse_physical_index,
6+
process_toc_no_page_numbers,
7+
validate_and_truncate_physical_indices,
8+
)
9+
from pageindex.utils import convert_physical_index_to_int
10+
11+
12+
class PhysicalIndexGuardsTest(unittest.TestCase):
13+
def test_parse_rejects_non_integral_values(self):
14+
self.assertEqual(_parse_physical_index("<physical_index_7>"), 7)
15+
self.assertEqual(_parse_physical_index("7"), 7)
16+
self.assertEqual(_parse_physical_index(7.0), 7)
17+
self.assertIsNone(_parse_physical_index(True))
18+
self.assertIsNone(_parse_physical_index(1.9))
19+
self.assertIsNone(_parse_physical_index(float("inf")))
20+
self.assertIsNone(_parse_physical_index(float("nan")))
21+
22+
def test_convert_handles_bare_and_malformed_strings(self):
23+
data = [
24+
{"physical_index": "1"},
25+
{"physical_index": "<physical_index_2>"},
26+
{"physical_index": "<physical_index_x>"},
27+
{"physical_index": "abc"},
28+
]
29+
convert_physical_index_to_int(data)
30+
self.assertEqual(data[0]["physical_index"], 1)
31+
self.assertEqual(data[1]["physical_index"], 2)
32+
self.assertEqual(data[2]["physical_index"], "<physical_index_x>")
33+
self.assertEqual(data[3]["physical_index"], "abc")
34+
self.assertEqual(convert_physical_index_to_int("7"), 7)
35+
self.assertIsNone(convert_physical_index_to_int("<physical_index_x>"))
36+
37+
def test_truncate_nullifies_out_of_range_and_non_int(self):
38+
toc = [
39+
{"physical_index": 1},
40+
{"physical_index": 5},
41+
{"physical_index": 99},
42+
{"physical_index": "abc"},
43+
{"physical_index": 1.9},
44+
]
45+
validate_and_truncate_physical_indices(toc, 10, start_index=5)
46+
self.assertIsNone(toc[0]["physical_index"])
47+
self.assertEqual(toc[1]["physical_index"], 5)
48+
self.assertIsNone(toc[2]["physical_index"])
49+
self.assertIsNone(toc[3]["physical_index"])
50+
self.assertIsNone(toc[4]["physical_index"])
551

652

753
class ProcessTocNoPageNumbersTest(unittest.TestCase):

0 commit comments

Comments
 (0)