Skip to content

Read and decompress VBA module source, behind an optional [vba] extra #128

Description

@toxicphreAK

Split out of #27, tier 2 of three. Depends on #127 for reaching the bytes.

Reading what the macros in a document actually are — module names, their source, whether the project is locked or signed. The auditing case ("what does this document run") and the review case ("diff the macros between two versions of our template") both need it, and neither is served by opaque bytes.

Dependency: olefile, not oletools

The issue body proposes oletools/olevba. That brings pyparsing, colorclass, pcodedmp, msoffcrypto-tool and easygui — a Tkinter GUI toolkit — into the dependency tree of a document library whose runtime deps are lxml and typing_extensions. Not acceptable as a hard requirement, and olevba is oriented at malware triage rather than at being a library.

olefile is the right piece: BSD, pure Python, zero dependencies, actively maintained, and it is exactly the OLE compound-file reader this needs. It is also what oletools itself uses underneath.

Behind an optional extra:

[project.optional-dependencies]
vba = ["olefile>=0.47"]

so the default install is unchanged and document.vba_project.modules raises a clear "install python-docx-ng[vba]" message when the extra is absent.

Shape

>>> project = document.vba_project          # richer object than tier 1's bytes
>>> [m.name for m in project.modules]
['ThisDocument', 'Module1', 'UserForm1']
>>> project.modules['Module1'].code
'Sub Hello()\n    MsgBox "hi"\nEnd Sub\n'
>>> project.is_locked, project.is_signed
(False, True)

Keep Document.vba_project returning the tier-1 bytes accessor's object and hang the parsed view off it, or the two tiers fight over the same name. Deciding that boundary is part of this issue.

The work beyond olefile

olefile gets you the storage/stream tree. The module source is compressed with the VBA run-length scheme, which olefile does not implement. That algorithm is [MS-OVBA] §2.4.1 "Compression and Decompression" — a public Microsoft specification, roughly 100 lines to implement, and the single trickiest part of this tier. It is a chunk-based scheme with a 4096-byte window and a variable-width copy token; the bit-width of the token changes with the current decompressed position, which is where naive implementations go wrong.

Locating the module streams needs the dir stream (itself compressed) and the PROJECT stream, which names the modules and their types.

Blocker: no usable fixture

tests/test_files/macro-enabled.docm has a 27-byte stub vbaProject.bin, not a real OLE file. This tier cannot be tested without a genuine macro project, which means authoring one in Word or sourcing one — the same blocker as #23. Worth resolving before starting rather than discovering halfway.

A real fixture should be deliberately trivial (one module, one Sub, no forms, no signature) so it is reviewable, and its provenance recorded in the test file.

Scope note

Read-only. Editing a module's source means rewriting the compound file, which is #129.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions