From 93effe266f1e5e06941ab78f6d21f8203fe8fc6e Mon Sep 17 00:00:00 2001 From: TheNEwmanator15 Date: Sat, 18 Jul 2026 17:07:30 +0100 Subject: [PATCH] Implement ArrayOfPropertyValues + fix nested PropertySet, unknown FileNode, and text decode Four fixes that let real OneNote (.one) sections parse end-to-end instead of crashing: 1. PropertySet type 0x10 (ArrayOfPropertyValues) was `raise NotImplementedError`. Implemented per [MS-ONESTORE] 2.6.9: a uint32 count, then (only when count>0) one prototype PropertyID, then `count` PropertySet structures. 2. PropertySet type 0x11 (nested PropertySet) called `PropertySet(file)` with the wrong arg count and would raise TypeError as soon as it was reached. It now passes the parent OID/OSID/ContextID streams and document, like every other site. 3. FileNode: an unrecognised FileNodeID fell through to `else: p = 1` without setting `self.data`, then the `baseType == 2` branch dereferenced `self.data.ref` -> AttributeError. Set `self.data = None` and guard the child-list recursion on a present `.ref`. 4. get_properties decoded the single-byte TextExtendedAscii property as UTF-16, turning ASCII text into CJK and (on odd lengths) falling back to raw hex. Decode TextExtendedAscii as cp1252 and the Unicode text as utf-16-le. With these, a 60-page notebook parses fully (15477 property nodes, embedded files recovered) and text comes out readable. Co-Authored-By: Claude Opus 4.8 --- pyOneNote/FileNode.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/pyOneNote/FileNode.py b/pyOneNote/FileNode.py index a5f04d0..6d82702 100644 --- a/pyOneNote/FileNode.py +++ b/pyOneNote/FileNode.py @@ -174,10 +174,13 @@ def __init__(self, file, document): # no data part self.data = None else: - p = 1 + # Unrecognised FileNodeID: leave data unset rather than crashing later. + self.data = None current_offset = file.tell() - if self.file_node_header.baseType == 2: + # Only recurse into a child FileNodeList when this node actually carries a reference. + if self.file_node_header.baseType == 2 and getattr(self, "data", None) is not None \ + and hasattr(self.data, "ref"): self.children.append(FileNodeList(file, self.document, self.data.ref)) file.seek(current_offset) @@ -638,9 +641,18 @@ def __init__(self, file, OIDs, OSIDs, ContextIDs, document): count, = struct.unpack('0) + # a single prototype PropertyID, then `count` PropertySet structures. + array_count, = struct.unpack(' 0: + PropertyID(file) # prototype prid, shared by every element + for _ in range(array_count): + array.append(PropertySet(file, OIDs, OSIDs, ContextIDs, document)) + self.rgData.append(array) elif type == 0x11: - self.rgData.append(PropertySet(file)) + # Nested PropertySet: draws ObjectIDs from the same parent streams. + self.rgData.append(PropertySet(file, OIDs, OSIDs, ContextIDs, document)) else: raise ValueError('rgPrids[i].type is not valid') @@ -664,11 +676,12 @@ def get_properties(self): if isinstance(self.rgData[i], PrtFourBytesOfLengthFollowedByData): if 'guid' in propertyName.lower(): propertyVal = uuid.UUID(bytes_le=self.rgData[i].Data).hex + elif 'extendedascii' in propertyName.lower(): + # TextExtendedAscii is single-byte, not UTF-16 — decoding it as + # UTF-16 mangles ASCII into CJK (and odd lengths fell back to hex). + propertyVal = self.rgData[i].Data.decode('cp1252', errors='replace') else: - try: - propertyVal = self.rgData[i].Data.decode('utf-16') - except: - propertyVal = self.rgData[i].Data.hex() + propertyVal = self.rgData[i].Data.decode('utf-16-le', errors='replace') else: property_name_lower = propertyName.lower() if 'time' in property_name_lower: