From 6621bc9245fedaef8be469306f77d44336198da9 Mon Sep 17 00:00:00 2001 From: Tim Wortley <5639920+twortley@users.noreply.github.com> Date: Sun, 5 Jul 2026 17:24:38 -0400 Subject: [PATCH] Fix FileNode crash on terminator / unknown node types (baseType == 2) FileNode.__init__ dereferenced self.data.ref for any node whose header baseType == 2 (a reference node), but the dispatch chain's final `else` branch left self.data unset for unrecognised / terminator node IDs, so those nodes raised `AttributeError: 'FileNode' object has no attribute 'data'`. The real-world trigger is a FileNodeListFragment terminator (file_node_id 0 with non-zero upper bits, seen in large / heavily-revised .one files): the fragment loop breaks on id 0/255, but only after the node is constructed, so the crash beats the guard. Fix: default self.data to None for unknown node types, and only recurse into a child FileNodeList when self.data is set. Adds a self-contained regression test (4-byte header, no sample file needed). Co-Authored-By: Claude Opus 4.8 --- pyOneNote/FileNode.py | 5 ++-- tests/test_filenode_terminator.py | 45 +++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 tests/test_filenode_terminator.py diff --git a/pyOneNote/FileNode.py b/pyOneNote/FileNode.py index a5f04d0..509ed5e 100644 --- a/pyOneNote/FileNode.py +++ b/pyOneNote/FileNode.py @@ -174,10 +174,11 @@ def __init__(self, file, document): # no data part self.data = None else: - p = 1 + # Unknown / terminator node types have no data part either. + self.data = None current_offset = file.tell() - if self.file_node_header.baseType == 2: + if self.file_node_header.baseType == 2 and self.data is not None: self.children.append(FileNodeList(file, self.document, self.data.ref)) file.seek(current_offset) diff --git a/tests/test_filenode_terminator.py b/tests/test_filenode_terminator.py new file mode 100644 index 0000000..5bbeecb --- /dev/null +++ b/tests/test_filenode_terminator.py @@ -0,0 +1,45 @@ +"""Regression test for the FileNode terminator / unknown-node crash. + +Before the fix, ``FileNode.__init__`` dereferenced ``self.data.ref`` for any +node whose header ``baseType == 2`` (a reference node), but the dispatch +chain's final ``else`` branch left ``self.data`` unset for unrecognised or +terminator node IDs. A single 4-byte FileNode header with an unknown id and +``baseType == 2`` triggered: + + AttributeError: 'FileNode' object has no attribute 'data' + +The real-world trigger is a FileNodeListFragment terminator (file_node_id 0 +with non-zero upper bits, seen in large / heavily-revised .one sections): the +fragment loop breaks on id 0/255, but only *after* the node is constructed, +so the crash beats the guard. No sample .one file is needed to reproduce it. +""" + +import struct +from io import BytesIO + +from pyOneNote.FileNode import FileNode + + +class _Doc: + """Minimal document stand-in; unused on this code path.""" + + cur_revision = None + _global_identification_table: dict = {} + + +def _reference_node_with_unknown_id() -> bytes: + """4-byte FileNode header: file_node_id=0 (Invalid), baseType=2. + + bits 0-9 file_node_id = 0 (not in _FileNodeIDs -> "Invalid") + bits 10-22 size = 4 + bits 27-30 baseType = 2 (reference node -> would recurse) + """ + header = (0) | (4 << 10) | (2 << 27) # 0x10001000 + return struct.pack("