| 1 | """VM-free gate for corpus/notebook-package: a package Snowbound packs is a cabinet OneNote 2010 |
| 2 | unpacks and opens, the notebook Snowbound unpacks cold-opens in OneNote as written, and |
| 3 | OneNote's own LZX package lists the files Snowbound unpacks.""" |
| 4 | |
| 5 | import pathlib |
| 6 | import struct |
| 7 | import unittest |
| 8 | import xml.etree.ElementTree as ElementTree |
| 9 | import zlib |
| 10 | |
| 11 | ROOT = pathlib.Path(__file__).resolve().parents[1] / "corpus" / "notebook-package" |
| 12 | ONE = "{http://schemas.microsoft.com/office/onenote/2010/onenote}" |
| 13 | |
| 14 | |
| 15 | def 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 | |
| 54 | def 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 | |
| 66 | EXPECTED = [ |
| 67 | ("Section", "New Section 1", [("Page", "First", [])]), |
| 68 | ("SectionGroup", "Group", [("Section", "Inner", [("Page", "Inside", [])])]), |
| 69 | ] |
| 70 | |
| 71 | |
| 72 | class 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 | |
| 112 | if __name__ == "__main__": |
| 113 | unittest.main() |