Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion mammoth/docx/numbering_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@ def __init__(self, abstract_nums, nums, styles):
self._styles = styles

def find_level(self, num_id, level):
return self._find_level(num_id, level, visited=set())

def _find_level(self, num_id, level, visited):
# A w:numStyleLink can point (directly or transitively) back to a
# numbering definition that has already been visited, forming a cycle.
# Track the num IDs seen while resolving this chain so a malformed
# document cannot cause unbounded recursion.
if num_id in visited:
return None
visited.add(num_id)

num = self._nums.get(num_id)
if num is None:
return None
Expand All @@ -112,7 +123,7 @@ def find_level(self, num_id, level):
return self._to_numbering_level(abstract_num.levels.get(level))
else:
style = self._styles.find_numbering_style_by_id(abstract_num.num_style_link)
return self.find_level(style.num_id, level)
return self._find_level(style.num_id, level, visited)

def find_level_by_paragraph_style_id(self, style_id):
return self._levels_by_paragraph_style_id.get(style_id)
Expand Down
20 changes: 20 additions & 0 deletions tests/docx/numbering_xml_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,26 @@ def test_numbering_level_can_be_found_by_paragraph_style_id():
assert_equal(None, numbering.find_level_by_paragraph_style_id("Paragraph"))



def test_find_level_returns_none_when_num_style_link_forms_a_cycle():
# num 201 -> abstractNum 101 -> numStyleLink "List1" -> num 201 -> ...
# Without cycle detection this recurses until the interpreter stack is
# exhausted; find_level should instead return None.
numbering = _read_numbering_xml_element(
xml_element("w:numbering", {}, [
xml_element("w:abstractNum", {"w:abstractNumId": "101"}, [
xml_element("w:numStyleLink", {"w:val": "List1"}),
]),
xml_element("w:num", {"w:numId": "201"}, [
xml_element("w:abstractNumId", {"w:val": "101"}),
]),
]),
styles=Styles.create(numbering_styles={
"List1": NumberingStyle(style_id="List1", num_id="201"),
}),
)
assert_equal(None, numbering.find_level("201", "0"))

def _read_numbering_xml_element(element, styles=None):
if styles is None:
styles = Styles.EMPTY
Expand Down