1"""VM-free gate for corpus/notebook-package: a package Snowbound packs is a cabinet OneNote 2010
2unpacks and opens, the notebook Snowbound unpacks cold-opens in OneNote as written, and
3OneNote's own LZX package lists the files Snowbound unpacks."""
4
5import pathlib
6import struct
7import unittest
8import xml.etree.ElementTree as ElementTree
9import zlib
10
11ROOT = pathlib.Path(__file__).resolve().parents[1] / "corpus" / "notebook-package"
12ONE = "{http://schemas.microsoft.com/office/onenote/2010/onenote}"
13
14
15def cabinet(data):
16 """The compression of each folder and each file's name and bytes, read independently of
17 Snowbound's reader: MS-CAB's header, folders, files and MSZIP data blocks."""
18 signature, _, size, _, files_at, _, minor, major, folders, files, flags = struct.unpack_from(
19 "<4sIIIIIBBHHH", data
20 )
21 assert signature == b"MSCF" and size == len(data) and (major, minor) == (1, 3) and flags == 0
22 folder_list = []
23 for index in range(folders):
24 start, blocks, compression = struct.unpack_from("<IHH", data, 36 + 8 * index)
25 folder_list.append((start, blocks, compression))
26 entries, at = [], files_at
27 for _ in range(files):
28 length, offset, folder, _, _, _ = struct.unpack_from("<IIHHHH", data, at)
29 end = data.index(b"\0", at + 16)
30 entries.append((data[at + 16 : end].decode(), offset, length, folder))
31 at = end + 1
32 unpacked = []
33 for start, blocks, compression in folder_list:
34 stream, at, history = b"", start, b""
35 for _ in range(blocks):
36 _, compressed, _ = struct.unpack_from("<IHH", data, at)
37 block = data[at + 8 : at + 8 + compressed]
38 at += 8 + compressed
39 if compression == 1:
40 assert block[:2] == b"CK"
41 inflate = zlib.decompressobj(-15, zdict=history) if history else zlib.decompressobj(-15)
42 out = inflate.decompress(block[2:])
43 history = (history + out)[-32768:]
44 else:
45 out = block
46 stream += out
47 unpacked.append((compression, stream))
48 return [
49 (name, unpacked[folder][0], unpacked[folder][1][offset : offset + length])
50 for name, offset, length, folder in entries
51 ]
52
53
54def names(hierarchy):
55 notebook = next(ElementTree.parse(hierarchy).getroot().iter(ONE + "Notebook"))
56
57 def walk(node):
58 for child in node:
59 tag = child.tag.replace(ONE, "")
60 if tag in ("Section", "SectionGroup", "Page"):
61 yield (tag, child.get("name"), list(walk(child)))
62
63 return list(walk(notebook))
64
65
66EXPECTED = [
67 ("Section", "New Section 1", [("Page", "First", [])]),
68 ("SectionGroup", "Group", [("Section", "Inner", [("Page", "Inside", [])])]),
69]
70
71
72class NotebookPackage(unittest.TestCase):
73 def test_snowbound_packs_mszip_files_outside_any_notebook(self):
74 files = cabinet((ROOT / "candidate" / "Packed.onepkg").read_bytes())
75 self.assertEqual(
76 [name for name, *_ in files],
77 ["Open Notebook.onetoc2", "New Section 1.one", "Group\\Open Notebook.onetoc2", "Group\\Inner.one"],
78 )
79 for name, compression, data in files:
80 self.assertEqual(compression, 1, name)
81 self.assertEqual(data[128:148], bytes(20), f"{name} names no ancestor")
82
83 def test_onenote_unpacks_snowbounds_package(self):
84 self.assertEqual(names(ROOT / "onenote-unpacked" / "hierarchy.xml"), EXPECTED)
85
86 def test_snowbounds_unpacked_notebook_cold_opens_as_written(self):
87 self.assertEqual(names(ROOT / "cold" / "read" / "hierarchy.xml"), EXPECTED)
88 for written in (ROOT / "candidate" / "Unpacked").rglob("*.one*"):
89 relative = written.relative_to(ROOT / "candidate" / "Unpacked")
90 self.assertEqual(written.read_bytes(), (ROOT / "cold" / "notebook" / relative).read_bytes())
91
92 def test_save_as_copies_open_in_onenote(self):
93 root = ROOT / "save-as"
94 notebook = ElementTree.parse(root / "cold" / "read" / "hierarchy.xml").getroot()
95 sections = {
96 section.get("name"): [page.get("name") for page in section.iter(ONE + "Page")]
97 for section in notebook.iter(ONE + "Section")
98 }
99 self.assertEqual(sections, {"Inner copy": ["Inside"], "Inside": ["Inside"]})
100 for written in (root / "candidate").glob("*.one"):
101 data = written.read_bytes()
102 self.assertEqual(data[128:148], bytes(20), written.name)
103 # OneNote adopted them into the folder it opened, rewriting only their headers.
104 self.assertEqual(data[1024:], (root / "cold" / "notebook" / written.name).read_bytes()[1024:])
105
106 def test_onenotes_package_is_lzx(self):
107 data = (ROOT / "native" / "Lab.onepkg").read_bytes()
108 _, compression = struct.unpack_from("<HH", data, 36 + 4)
109 self.assertEqual(compression & 0xF, 3, "LZX")
110
111
112if __name__ == "__main__":
113 unittest.main()