pyOpenVBA 2.0.0__tar.gz → 3.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/.gitignore +5 -0
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/PKG-INFO +50 -18
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/README.md +49 -17
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/docs/architecture.md +108 -1
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/pyproject.toml +35 -1
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/__init__.py +52 -28
- pyopenvba-3.0.0/src/pyopenvba/__main__.py +216 -0
- pyopenvba-3.0.0/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
- pyopenvba-3.0.0/src/pyopenvba/access_read.py +1728 -0
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/cfb.py +11 -10
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/excel.py +10 -9
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/powerpoint.py +10 -9
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/vba.py +92 -26
- pyopenvba-3.0.0/src/pyopenvba/vba_pcode.py +736 -0
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/word.py +10 -9
- pyopenvba-2.0.0/src/pyopenvba/__main__.py +0 -68
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/LICENSE.md +0 -0
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/docs/ms-ovba-implementation-guide_v2.md +0 -0
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/docs/roadmap.md +0 -0
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/_templates/__init__.py +0 -0
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/src/pyopenvba/exceptions.py +0 -0
- {pyopenvba-2.0.0 → pyopenvba-3.0.0}/tests/fuzz_corpus/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyOpenVBA
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Read and write VBA macros inside Excel, Word, and PowerPoint files in pure Python, no dependencies.
|
|
5
5
|
Project-URL: Homepage, https://github.com/WilliamSmithEdward/pyOpenVBA
|
|
6
6
|
Project-URL: Repository, https://github.com/WilliamSmithEdward/pyOpenVBA
|
|
@@ -39,7 +39,7 @@ Description-Content-Type: text/markdown
|
|
|
39
39
|
[](https://pypi.org/project/pyOpenVBA/)
|
|
40
40
|
[](https://pypi.org/project/pyOpenVBA/)
|
|
41
41
|
[](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml)
|
|
42
|
-
[](LICENSE.md)
|
|
42
|
+
[](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
|
|
43
43
|
[](https://pypi.org/project/pyOpenVBA/)
|
|
44
44
|
|
|
45
45
|
**Read and write VBA macros inside Excel, Word, and PowerPoint files, in pure Python.**
|
|
@@ -52,6 +52,8 @@ Supports:
|
|
|
52
52
|
* Excel (`.xlsm`, `.xlsb`, `.xlam`, `.xls`)
|
|
53
53
|
* PowerPoint (`.pptm`, `.potm`, `.ppt`)
|
|
54
54
|
* Word (`.docm`, `.dotm`, `.doc`)
|
|
55
|
+
* Access (`.accdb`) - **read-only**, see
|
|
56
|
+
[docs/msaccess_lessons_learned.md](docs/msaccess_lessons_learned.md)
|
|
55
57
|
|
|
56
58
|
<a href="https://github.com/sponsors/WilliamSmithEdward"><img src="https://img.shields.io/badge/Sponsor-%E2%9D%A4-pink?style=for-the-badge" alt="Sponsor WilliamSmithEdward"></a>
|
|
57
59
|
|
|
@@ -226,18 +228,9 @@ with ExcelFile("workbook.xlsm") as wb:
|
|
|
226
228
|
kind=VBAModuleKind.standard,
|
|
227
229
|
)
|
|
228
230
|
|
|
229
|
-
# Add a class module (
|
|
230
|
-
_CLASS_VB_BASE = "0{FCFB3D2A-A0FA-1068-A738-08002B3371B5}"
|
|
231
|
+
# Add a class module (header is synthesized automatically)
|
|
231
232
|
project.add_module(
|
|
232
233
|
"MyClass",
|
|
233
|
-
f'Attribute VB_Name = "MyClass"\r\n'
|
|
234
|
-
f'Attribute VB_Base = "{_CLASS_VB_BASE}"\r\n'
|
|
235
|
-
"Attribute VB_GlobalNameSpace = False\r\n"
|
|
236
|
-
"Attribute VB_Creatable = False\r\n"
|
|
237
|
-
"Attribute VB_PredeclaredId = False\r\n"
|
|
238
|
-
"Attribute VB_Exposed = False\r\n"
|
|
239
|
-
"Attribute VB_TemplateDerived = False\r\n"
|
|
240
|
-
"Attribute VB_Customizable = False\r\n"
|
|
241
234
|
"Option Explicit\r\n",
|
|
242
235
|
kind=VBAModuleKind.other,
|
|
243
236
|
)
|
|
@@ -335,6 +328,45 @@ modules, `.cls` for class modules and code-behind.
|
|
|
335
328
|
| `.potm` | Macro-enabled template | yes | yes | no |
|
|
336
329
|
| `.ppt` | Legacy (PowerPoint 97-2003) | yes | yes | no |
|
|
337
330
|
|
|
331
|
+
### Access (read-only)
|
|
332
|
+
|
|
333
|
+
| Extension | What it is | Read | Write | create_new |
|
|
334
|
+
|-----------|------------------------------|:----:|:-----:|:----------:|
|
|
335
|
+
| `.accdb` | Access database (ACE engine) | yes | no | no |
|
|
336
|
+
|
|
337
|
+
Access stores compiled VBA p-code (the `rU@` + `CAFE` rows in the LVAL
|
|
338
|
+
catalog) separately from the OVBA source cache. The compiled p-code is
|
|
339
|
+
authoritative for the Access GUI; mutations to the source cache do not
|
|
340
|
+
survive reload because Access never recompiles from the cache. After
|
|
341
|
+
extensive reverse-engineering experiments we concluded that a
|
|
342
|
+
production-quality writer would require a complete VBA7 p-code
|
|
343
|
+
assembler, which is out of scope. See
|
|
344
|
+
[docs/msaccess_lessons_learned.md](docs/msaccess_lessons_learned.md)
|
|
345
|
+
for the full chronicle.
|
|
346
|
+
|
|
347
|
+
What `AccessReader` does support:
|
|
348
|
+
|
|
349
|
+
- `AccessReader(path)` / `vba_module_names()` / `read_vba_module(name)`
|
|
350
|
+
- `read_vba_module_with_attributes(name)`
|
|
351
|
+
- `vba_modules()` (dict of name -> source)
|
|
352
|
+
- `iter_vba_modules()` (rich `VBAModule` records)
|
|
353
|
+
- `export_module()` / `export_modules()` / `pull_modules()` (write `.bas` / `.cls` to disk)
|
|
354
|
+
- `read_project_info()`, `identifiers()`, `find_interned_strings()`,
|
|
355
|
+
`find_module_streams()`, `iter_pcode_streams()`, `disassemble_module()`
|
|
356
|
+
- `iter_msys_objects()` / `msys_objects()` / `iter_msys_modules()` /
|
|
357
|
+
`find_msys_module()` (MSysObjects catalog inspection)
|
|
358
|
+
- Top-level helper: `pyopenvba.pull_access(database, dest_dir)`
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
from pyopenvba import AccessReader, pull_access
|
|
362
|
+
|
|
363
|
+
with AccessReader("database.accdb") as db:
|
|
364
|
+
for name, source in db.vba_modules().items():
|
|
365
|
+
print(name, len(source))
|
|
366
|
+
|
|
367
|
+
pull_access("database.accdb", "./vba_src") # export every module to .bas / .cls
|
|
368
|
+
```
|
|
369
|
+
|
|
338
370
|
Every save is verified to reopen in the host application **without** the
|
|
339
371
|
"we found a problem with some content" repair dialog.
|
|
340
372
|
|
|
@@ -385,7 +417,7 @@ following are preserved byte-for-byte but not interpreted:
|
|
|
385
417
|
- Re-signing digitally signed projects.
|
|
386
418
|
- ActiveX license editing.
|
|
387
419
|
|
|
388
|
-
See [docs/roadmap.md](docs/roadmap.md) for the full feature matrix.
|
|
420
|
+
See [docs/roadmap.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/roadmap.md) for the full feature matrix.
|
|
389
421
|
|
|
390
422
|
---
|
|
391
423
|
|
|
@@ -395,7 +427,7 @@ See [docs/roadmap.md](docs/roadmap.md) for the full feature matrix.
|
|
|
395
427
|
src/pyopenvba/
|
|
396
428
|
__init__.py public API (ExcelFile, WordFile, PowerPointFile,
|
|
397
429
|
pull/push, pull_word/push_word, pull_ppt/push_ppt,
|
|
398
|
-
VBAModuleKind, exceptions)
|
|
430
|
+
VBAModuleKind, synthesize_class_header, exceptions)
|
|
399
431
|
excel.py ExcelFile facade (ZIP / CFB dispatch, pull/push helpers)
|
|
400
432
|
word.py WordFile facade
|
|
401
433
|
powerpoint.py PowerPointFile facade
|
|
@@ -408,10 +440,10 @@ src/pyopenvba/
|
|
|
408
440
|
|
|
409
441
|
For deeper documentation:
|
|
410
442
|
|
|
411
|
-
- [docs/architecture.md](docs/architecture.md) - internal module layout.
|
|
412
|
-
- [docs/ms-ovba-implementation-guide_v2.md](docs/ms-ovba-implementation-guide_v2.md) -
|
|
443
|
+
- [docs/architecture.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/architecture.md) - internal module layout.
|
|
444
|
+
- [docs/ms-ovba-implementation-guide_v2.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/ms-ovba-implementation-guide_v2.md) -
|
|
413
445
|
language-agnostic guide for re-implementing MS-OVBA in another language.
|
|
414
|
-
- [docs/roadmap.md](docs/roadmap.md) - per-feature implementation status.
|
|
446
|
+
- [docs/roadmap.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/roadmap.md) - per-feature implementation status.
|
|
415
447
|
|
|
416
448
|
---
|
|
417
449
|
|
|
@@ -438,4 +470,4 @@ pushed.
|
|
|
438
470
|
|
|
439
471
|
## License
|
|
440
472
|
|
|
441
|
-
[MIT](LICENSE.md).
|
|
473
|
+
[MIT](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md).
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://pypi.org/project/pyOpenVBA/)
|
|
4
4
|
[](https://pypi.org/project/pyOpenVBA/)
|
|
5
5
|
[](https://github.com/WilliamSmithEdward/pyOpenVBA/actions/workflows/ci.yml)
|
|
6
|
-
[](LICENSE.md)
|
|
6
|
+
[](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md)
|
|
7
7
|
[](https://pypi.org/project/pyOpenVBA/)
|
|
8
8
|
|
|
9
9
|
**Read and write VBA macros inside Excel, Word, and PowerPoint files, in pure Python.**
|
|
@@ -16,6 +16,8 @@ Supports:
|
|
|
16
16
|
* Excel (`.xlsm`, `.xlsb`, `.xlam`, `.xls`)
|
|
17
17
|
* PowerPoint (`.pptm`, `.potm`, `.ppt`)
|
|
18
18
|
* Word (`.docm`, `.dotm`, `.doc`)
|
|
19
|
+
* Access (`.accdb`) - **read-only**, see
|
|
20
|
+
[docs/msaccess_lessons_learned.md](docs/msaccess_lessons_learned.md)
|
|
19
21
|
|
|
20
22
|
<a href="https://github.com/sponsors/WilliamSmithEdward"><img src="https://img.shields.io/badge/Sponsor-%E2%9D%A4-pink?style=for-the-badge" alt="Sponsor WilliamSmithEdward"></a>
|
|
21
23
|
|
|
@@ -190,18 +192,9 @@ with ExcelFile("workbook.xlsm") as wb:
|
|
|
190
192
|
kind=VBAModuleKind.standard,
|
|
191
193
|
)
|
|
192
194
|
|
|
193
|
-
# Add a class module (
|
|
194
|
-
_CLASS_VB_BASE = "0{FCFB3D2A-A0FA-1068-A738-08002B3371B5}"
|
|
195
|
+
# Add a class module (header is synthesized automatically)
|
|
195
196
|
project.add_module(
|
|
196
197
|
"MyClass",
|
|
197
|
-
f'Attribute VB_Name = "MyClass"\r\n'
|
|
198
|
-
f'Attribute VB_Base = "{_CLASS_VB_BASE}"\r\n'
|
|
199
|
-
"Attribute VB_GlobalNameSpace = False\r\n"
|
|
200
|
-
"Attribute VB_Creatable = False\r\n"
|
|
201
|
-
"Attribute VB_PredeclaredId = False\r\n"
|
|
202
|
-
"Attribute VB_Exposed = False\r\n"
|
|
203
|
-
"Attribute VB_TemplateDerived = False\r\n"
|
|
204
|
-
"Attribute VB_Customizable = False\r\n"
|
|
205
198
|
"Option Explicit\r\n",
|
|
206
199
|
kind=VBAModuleKind.other,
|
|
207
200
|
)
|
|
@@ -299,6 +292,45 @@ modules, `.cls` for class modules and code-behind.
|
|
|
299
292
|
| `.potm` | Macro-enabled template | yes | yes | no |
|
|
300
293
|
| `.ppt` | Legacy (PowerPoint 97-2003) | yes | yes | no |
|
|
301
294
|
|
|
295
|
+
### Access (read-only)
|
|
296
|
+
|
|
297
|
+
| Extension | What it is | Read | Write | create_new |
|
|
298
|
+
|-----------|------------------------------|:----:|:-----:|:----------:|
|
|
299
|
+
| `.accdb` | Access database (ACE engine) | yes | no | no |
|
|
300
|
+
|
|
301
|
+
Access stores compiled VBA p-code (the `rU@` + `CAFE` rows in the LVAL
|
|
302
|
+
catalog) separately from the OVBA source cache. The compiled p-code is
|
|
303
|
+
authoritative for the Access GUI; mutations to the source cache do not
|
|
304
|
+
survive reload because Access never recompiles from the cache. After
|
|
305
|
+
extensive reverse-engineering experiments we concluded that a
|
|
306
|
+
production-quality writer would require a complete VBA7 p-code
|
|
307
|
+
assembler, which is out of scope. See
|
|
308
|
+
[docs/msaccess_lessons_learned.md](docs/msaccess_lessons_learned.md)
|
|
309
|
+
for the full chronicle.
|
|
310
|
+
|
|
311
|
+
What `AccessReader` does support:
|
|
312
|
+
|
|
313
|
+
- `AccessReader(path)` / `vba_module_names()` / `read_vba_module(name)`
|
|
314
|
+
- `read_vba_module_with_attributes(name)`
|
|
315
|
+
- `vba_modules()` (dict of name -> source)
|
|
316
|
+
- `iter_vba_modules()` (rich `VBAModule` records)
|
|
317
|
+
- `export_module()` / `export_modules()` / `pull_modules()` (write `.bas` / `.cls` to disk)
|
|
318
|
+
- `read_project_info()`, `identifiers()`, `find_interned_strings()`,
|
|
319
|
+
`find_module_streams()`, `iter_pcode_streams()`, `disassemble_module()`
|
|
320
|
+
- `iter_msys_objects()` / `msys_objects()` / `iter_msys_modules()` /
|
|
321
|
+
`find_msys_module()` (MSysObjects catalog inspection)
|
|
322
|
+
- Top-level helper: `pyopenvba.pull_access(database, dest_dir)`
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
from pyopenvba import AccessReader, pull_access
|
|
326
|
+
|
|
327
|
+
with AccessReader("database.accdb") as db:
|
|
328
|
+
for name, source in db.vba_modules().items():
|
|
329
|
+
print(name, len(source))
|
|
330
|
+
|
|
331
|
+
pull_access("database.accdb", "./vba_src") # export every module to .bas / .cls
|
|
332
|
+
```
|
|
333
|
+
|
|
302
334
|
Every save is verified to reopen in the host application **without** the
|
|
303
335
|
"we found a problem with some content" repair dialog.
|
|
304
336
|
|
|
@@ -349,7 +381,7 @@ following are preserved byte-for-byte but not interpreted:
|
|
|
349
381
|
- Re-signing digitally signed projects.
|
|
350
382
|
- ActiveX license editing.
|
|
351
383
|
|
|
352
|
-
See [docs/roadmap.md](docs/roadmap.md) for the full feature matrix.
|
|
384
|
+
See [docs/roadmap.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/roadmap.md) for the full feature matrix.
|
|
353
385
|
|
|
354
386
|
---
|
|
355
387
|
|
|
@@ -359,7 +391,7 @@ See [docs/roadmap.md](docs/roadmap.md) for the full feature matrix.
|
|
|
359
391
|
src/pyopenvba/
|
|
360
392
|
__init__.py public API (ExcelFile, WordFile, PowerPointFile,
|
|
361
393
|
pull/push, pull_word/push_word, pull_ppt/push_ppt,
|
|
362
|
-
VBAModuleKind, exceptions)
|
|
394
|
+
VBAModuleKind, synthesize_class_header, exceptions)
|
|
363
395
|
excel.py ExcelFile facade (ZIP / CFB dispatch, pull/push helpers)
|
|
364
396
|
word.py WordFile facade
|
|
365
397
|
powerpoint.py PowerPointFile facade
|
|
@@ -372,10 +404,10 @@ src/pyopenvba/
|
|
|
372
404
|
|
|
373
405
|
For deeper documentation:
|
|
374
406
|
|
|
375
|
-
- [docs/architecture.md](docs/architecture.md) - internal module layout.
|
|
376
|
-
- [docs/ms-ovba-implementation-guide_v2.md](docs/ms-ovba-implementation-guide_v2.md) -
|
|
407
|
+
- [docs/architecture.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/architecture.md) - internal module layout.
|
|
408
|
+
- [docs/ms-ovba-implementation-guide_v2.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/ms-ovba-implementation-guide_v2.md) -
|
|
377
409
|
language-agnostic guide for re-implementing MS-OVBA in another language.
|
|
378
|
-
- [docs/roadmap.md](docs/roadmap.md) - per-feature implementation status.
|
|
410
|
+
- [docs/roadmap.md](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/docs/roadmap.md) - per-feature implementation status.
|
|
379
411
|
|
|
380
412
|
---
|
|
381
413
|
|
|
@@ -402,4 +434,4 @@ pushed.
|
|
|
402
434
|
|
|
403
435
|
## License
|
|
404
436
|
|
|
405
|
-
[MIT](LICENSE.md).
|
|
437
|
+
[MIT](https://github.com/WilliamSmithEdward/pyOpenVBA/blob/main/LICENSE.md).
|
|
@@ -33,12 +33,34 @@ knows about the layer below it but not the layer above.
|
|
|
33
33
|
| - identical save() pipeline as ExcelFile |
|
|
34
34
|
| - pull / push disk workflow |
|
|
35
35
|
+--------------------------------------------------------+
|
|
36
|
+
| access_read.py AccessReader facade (READ-ONLY) |
|
|
37
|
+
| - ACE / Jet 4 page reader (.accdb, .mdb) |
|
|
38
|
+
| - MS-OVBA blob discovery via signature scan |
|
|
39
|
+
| - LVAL page-chain walker |
|
|
40
|
+
| - read_vba_module(name) -> str (pure Python) |
|
|
41
|
+
| - get_module / vba_modules / iter_vba_modules |
|
|
42
|
+
| - pull_modules / export_modules / export_module |
|
|
43
|
+
| - read_vba_module_with_attributes (full preamble) |
|
|
44
|
+
| - read_project_info / identifiers |
|
|
45
|
+
| - find_interned_strings / find_module_streams |
|
|
46
|
+
| - iter_pcode_streams / read_module_pcode_stream |
|
|
47
|
+
| - disassemble_module (via vba_pcode) |
|
|
48
|
+
| - iter_msys_objects / find_msys_module |
|
|
49
|
+
| (MSysObjects system catalog reader) |
|
|
50
|
+
| - NO write APIs; see msaccess_lessons_learned.md |
|
|
51
|
+
+--------------------------------------------------------+
|
|
36
52
|
| vba.py VBA project layer |
|
|
37
53
|
| - MS-OVBA compression / decompression |
|
|
38
54
|
| - dir / PROJECT / PROJECTwm / PROJECTlk parsers |
|
|
39
55
|
| - VBAProject + VBAModule data model |
|
|
40
56
|
| - cache invalidation, signature detection |
|
|
41
57
|
+--------------------------------------------------------+
|
|
58
|
+
| vba_pcode.py VBA7 p-code disassembler (EXPERIMENTAL)|
|
|
59
|
+
| - 264-entry VBA7 opcode table (factual data) |
|
|
60
|
+
| - per-line p-code streaming parser |
|
|
61
|
+
| - source-line correlation (to_annotated_listing) |
|
|
62
|
+
| - dependency-free (no oletools, no pcodedmp) |
|
|
63
|
+
+--------------------------------------------------------+
|
|
42
64
|
| cfb.py MS-CFB layer |
|
|
43
65
|
| - Compound File Binary parser/writer |
|
|
44
66
|
| - stream + storage CRUD (case-insensitive lookup) |
|
|
@@ -50,7 +72,10 @@ Other files:
|
|
|
50
72
|
- `exceptions.py` — exception hierarchy shared by all layers.
|
|
51
73
|
- `__init__.py` — public re-exports (`ExcelFile`, `pull`, `push`,
|
|
52
74
|
`VBAModuleKind`, exceptions).
|
|
53
|
-
- `__main__.py` — `python -m pyopenvba
|
|
75
|
+
- `__main__.py` — `python -m pyopenvba
|
|
76
|
+
{pull,push,ls,access-ls,access-pull,access-disasm,disasm}` CLI.
|
|
77
|
+
`disasm` / `access-disasm` accept `--with-source` to interleave
|
|
78
|
+
the original VBA source with the decoded p-code.
|
|
54
79
|
- `_templates/__init__.py` — generated module embedding a
|
|
55
80
|
zlib-compressed base85 blob of a freshly Excel-authored empty `.xlsm`,
|
|
56
81
|
consumed by `ExcelFile.create_new()`. Regenerated from
|
|
@@ -154,6 +179,84 @@ If any of steps 3-10 fail, the on-disk file is **never modified** —
|
|
|
154
179
|
all work is done on the in-memory CFB and the final bytes are written
|
|
155
180
|
in one atomic-ish operation.
|
|
156
181
|
|
|
182
|
+
### 3.3 Access read-only model (`AccessReader`)
|
|
183
|
+
|
|
184
|
+
`.accdb`/`.mdb` files are exposed through a **read-only** facade.
|
|
185
|
+
After an extensive reverse-engineering effort (chronicled in
|
|
186
|
+
[docs/msaccess_lessons_learned.md](msaccess_lessons_learned.md))
|
|
187
|
+
pyOpenVBA does not support writing back to Access databases. Use
|
|
188
|
+
Access COM (`win32com.client.Dispatch("Access.Application")`) if you
|
|
189
|
+
need to programmatically modify VBA inside a `.accdb`.
|
|
190
|
+
|
|
191
|
+
The Access on-disk layout differs from Excel/Word/PowerPoint:
|
|
192
|
+
|
|
193
|
+
* The MS-OVBA blob on the LVAL chain is a **passive cache**. Zero-filling
|
|
194
|
+
the entire blob has no effect on what Access (or the VBA editor)
|
|
195
|
+
displays — verified end-to-end on a live fixture. This is what
|
|
196
|
+
pyOpenVBA reads.
|
|
197
|
+
* The **authoritative** sources Access reads from are:
|
|
198
|
+
* **Project metadata** — the MS-OVBA `dir` stream (Section 2.3.4.2)
|
|
199
|
+
OVBA-compressed in a single LVAL row; located by content
|
|
200
|
+
(decompressed prefix = `01 00 04 00 00 00`). Parsed by
|
|
201
|
+
`AccessReader.read_project_info()` -> `AccessVBAProject` (system kind,
|
|
202
|
+
LCID, code page, project name, references, modules with class flag,
|
|
203
|
+
private/read-only flags).
|
|
204
|
+
* **Module bytecode** — the compiled VBA p-code. Lives in LVAL rows
|
|
205
|
+
whose payload starts with magic `72 55 40 00` ('rU@\0'). The
|
|
206
|
+
**module-active** one is identified deterministically by the
|
|
207
|
+
12-byte prefix `72 55 40 00 00 00 00 00 00 00 40 00`
|
|
208
|
+
(byte 10 = 0x40). Exposed via
|
|
209
|
+
`AccessReader.read_module_pcode_stream()` and `iter_pcode_streams()`.
|
|
210
|
+
Compiled p-code is **fully anonymised**: opcodes and slot
|
|
211
|
+
references with no user-authored text. The standard MS-OVBA
|
|
212
|
+
`0xCAFE` module stream coexists alongside `rU@` in a separate
|
|
213
|
+
LVAL row and is what `AccessReader.disassemble_module()` consumes
|
|
214
|
+
via the pure-Python `pyopenvba.vba_pcode` decoder.
|
|
215
|
+
* **Identifier inventory** — every project-level identifier name
|
|
216
|
+
(typelib refs, project name, module/proc/variable names, intrinsic
|
|
217
|
+
call targets such as `MsgBox`) is enumerated in the
|
|
218
|
+
`_VBA_PROJECT`-equivalent stream stored UNCOMPRESSED in the LVAL
|
|
219
|
+
row whose first 2 bytes are the `CC 61` Office magic. Exposed via
|
|
220
|
+
`AccessReader.identifiers() -> tuple[AccessVBAIdentifier, ...]`.
|
|
221
|
+
* **Comment text** — stored verbatim in plaintext rows tagged
|
|
222
|
+
`E3 00 00 00 <u16-LE length> <ASCII payload>` (apostrophe stripped).
|
|
223
|
+
* **String literal text** — stored verbatim in rows tagged
|
|
224
|
+
`B9 00 <u16-LE length> <ASCII payload> <12-byte trailer>`,
|
|
225
|
+
exposed via `AccessReader.find_interned_strings()`.
|
|
226
|
+
|
|
227
|
+
The **MSysObjects** system catalog is also surfaced read-only:
|
|
228
|
+
`AccessReader.iter_msys_objects()`, `msys_objects()`,
|
|
229
|
+
`iter_msys_modules()`, `find_msys_object(name, *, type_=None)`,
|
|
230
|
+
`find_msys_module(name)`. Each row yields an `AccessSysObject`
|
|
231
|
+
dataclass (id_, parent_id, type_, flags, name, page, slot). VBA code
|
|
232
|
+
modules carry `type_ == MSYS_TYPE_MODULE` (-32761, 0x8007) and are
|
|
233
|
+
parented to the `Modules` container row.
|
|
234
|
+
|
|
235
|
+
### Excel-symmetric VBA module read API
|
|
236
|
+
|
|
237
|
+
`AccessReader` exposes the same ergonomic read surface as `ExcelFile` /
|
|
238
|
+
`WordFile` / `PowerPointFile`: `get_module(name)`,
|
|
239
|
+
`vba_modules() -> dict[str, str]`, `read_vba_module(name)`,
|
|
240
|
+
`read_vba_module_with_attributes(name)`, `iter_vba_modules()`,
|
|
241
|
+
`pull_modules(dest_dir)`, `export_module(name)`,
|
|
242
|
+
`export_modules(dest_dir, *, include_attributes=False)`. The
|
|
243
|
+
top-level helper `pyopenvba.pull_access(database, dest_dir)` mirrors
|
|
244
|
+
`pull` / `pull_word` / `pull_ppt`. There is intentionally no
|
|
245
|
+
`push_access` symbol.
|
|
246
|
+
|
|
247
|
+
### Why no write path
|
|
248
|
+
|
|
249
|
+
Access stores compiled VBA p-code (the `rU@` + `CAFE` rows) separately
|
|
250
|
+
from the OVBA source cache. The compiled p-code is authoritative for
|
|
251
|
+
the Access GUI; mutations to the source cache do not survive reload
|
|
252
|
+
because Access never recompiles from the cache. We could not locate a
|
|
253
|
+
recompile trigger. A production-quality writer would require a full
|
|
254
|
+
VBA7 p-code assembler (instruction emission, identifier table
|
|
255
|
+
re-indexing, jump fixup, MSysObjects long-value chunk reflow, ACE
|
|
256
|
+
page allocator parity). That is out of scope. See
|
|
257
|
+
[docs/msaccess_lessons_learned.md](msaccess_lessons_learned.md) for
|
|
258
|
+
the empirical results matrix and the reasoning in full.
|
|
259
|
+
|
|
157
260
|
---
|
|
158
261
|
|
|
159
262
|
## 4. Mutation safety gates
|
|
@@ -256,10 +359,14 @@ tests/
|
|
|
256
359
|
test_powerpoint.py PowerPointFile facade, end-to-end
|
|
257
360
|
test_pull_push.py Disk workflow
|
|
258
361
|
test_gates.py Per-roadmap-gate regression tests
|
|
362
|
+
test_access.py AccessReader read path: page walk,
|
|
363
|
+
LVAL chain walk, MS-OVBA blob decode,
|
|
364
|
+
byte-for-byte oracle parity (EXPERIMENTAL)
|
|
259
365
|
fuzz_corpus/ Persistent fuzz seeds (see test_gates.py Gate 23)
|
|
260
366
|
live_excel_testing/ Real fixture workbooks (Excel)
|
|
261
367
|
live_word_testing/ Real fixture documents (Word)
|
|
262
368
|
live_powerpoint_testing/ Real fixture presentations (PowerPoint)
|
|
369
|
+
live_access_test/ Real fixture databases (Access) + COM oracle
|
|
263
370
|
```
|
|
264
371
|
|
|
265
372
|
- **Always** run pytest with `-p no:randomly` to keep ordering
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyOpenVBA"
|
|
7
|
-
version = "
|
|
7
|
+
version = "3.0.0"
|
|
8
8
|
description = "Read and write VBA macros inside Excel, Word, and PowerPoint files in pure Python, no dependencies."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -86,3 +86,37 @@ testpaths = ["tests"]
|
|
|
86
86
|
include = ["src", "tests"]
|
|
87
87
|
pythonVersion = "3.10"
|
|
88
88
|
typeCheckingMode = "strict"
|
|
89
|
+
|
|
90
|
+
[tool.ruff]
|
|
91
|
+
line-length = 120
|
|
92
|
+
target-version = "py310"
|
|
93
|
+
|
|
94
|
+
[tool.ruff.lint]
|
|
95
|
+
select = ["E", "F", "W", "B", "UP", "SIM", "I", "RUF", "PIE", "C4", "PERF", "N", "TC", "RET", "TRY"]
|
|
96
|
+
# Ignore rules that conflict with the project's strict-pyright typing posture,
|
|
97
|
+
# intentional MS-spec naming, or stylistic preferences:
|
|
98
|
+
# B009 getattr(obj, "_private") -- deliberate to bypass reportPrivateUsage in tests
|
|
99
|
+
# PIE807 default_factory=lambda: [] -- pyright strict cannot infer element type from bare `list`
|
|
100
|
+
# TRY003 raising vanilla exception messages is fine in this project
|
|
101
|
+
# E501 long lines are tolerated where they aid readability of binary specs
|
|
102
|
+
# N801/N802/N806 mixed-case names mirror MS-OVBA spec record/field names
|
|
103
|
+
# PERF203 try/except inside loops is required for per-row recovery in parsers
|
|
104
|
+
# PERF401 manual list builds are clearer than comprehensions in spec-driven code
|
|
105
|
+
# SIM105 contextlib.suppress is slower; explicit try/except is preferred
|
|
106
|
+
# TRY300 try-else is stylistic
|
|
107
|
+
# TC001/TC003 moving runtime-required imports into TYPE_CHECKING risks breakage
|
|
108
|
+
ignore = [
|
|
109
|
+
"B009",
|
|
110
|
+
"PIE807",
|
|
111
|
+
"TRY003",
|
|
112
|
+
"E501",
|
|
113
|
+
"N801",
|
|
114
|
+
"N802",
|
|
115
|
+
"N806",
|
|
116
|
+
"PERF203",
|
|
117
|
+
"PERF401",
|
|
118
|
+
"SIM105",
|
|
119
|
+
"TRY300",
|
|
120
|
+
"TC001",
|
|
121
|
+
"TC003",
|
|
122
|
+
]
|
|
@@ -13,10 +13,13 @@ Supported formats
|
|
|
13
13
|
Public API
|
|
14
14
|
----------
|
|
15
15
|
from pyopenvba import (
|
|
16
|
-
ExcelFile, WordFile, PowerPointFile,
|
|
16
|
+
ExcelFile, WordFile, PowerPointFile, AccessReader,
|
|
17
17
|
pull, push, pull_word, push_word, pull_ppt, push_ppt,
|
|
18
|
+
pull_access,
|
|
18
19
|
)
|
|
19
20
|
|
|
21
|
+
# Access (.accdb) is READ-ONLY. See docs/msaccess_lessons_learned.md.
|
|
22
|
+
|
|
20
23
|
# In-process module edit (Excel)
|
|
21
24
|
with ExcelFile("workbook.xlsm") as wb:
|
|
22
25
|
modules = wb.vba_modules() # dict[name -> source]
|
|
@@ -44,26 +47,29 @@ Public API
|
|
|
44
47
|
|
|
45
48
|
pull_ppt("presentation.pptm", "./vba_src") # extract modules (PowerPoint)
|
|
46
49
|
push_ppt("./vba_src", "presentation.pptm")
|
|
50
|
+
|
|
51
|
+
pull_access("database.accdb", "./vba_src") # extract modules (Access, read-only)
|
|
47
52
|
"""
|
|
48
53
|
|
|
49
54
|
from pathlib import Path
|
|
50
|
-
from typing import Union
|
|
51
55
|
|
|
56
|
+
from pyopenvba.access_read import AccessReader
|
|
52
57
|
from pyopenvba.excel import ExcelFile
|
|
53
|
-
from pyopenvba.word import WordFile
|
|
54
|
-
from pyopenvba.powerpoint import PowerPointFile
|
|
55
58
|
from pyopenvba.exceptions import (
|
|
56
|
-
PyOpenVBAError,
|
|
57
59
|
CFBError,
|
|
58
|
-
|
|
60
|
+
PyOpenVBAError,
|
|
59
61
|
UnsupportedFormatError,
|
|
62
|
+
VBAProjectError,
|
|
60
63
|
)
|
|
64
|
+
from pyopenvba.powerpoint import PowerPointFile
|
|
61
65
|
from pyopenvba.vba import VBAModuleKind
|
|
66
|
+
from pyopenvba.vba import synthesize_class_header as synthesize_class_header
|
|
67
|
+
from pyopenvba.word import WordFile
|
|
62
68
|
|
|
63
69
|
|
|
64
70
|
def pull(
|
|
65
|
-
workbook:
|
|
66
|
-
dest_dir:
|
|
71
|
+
workbook: str | Path,
|
|
72
|
+
dest_dir: str | Path,
|
|
67
73
|
*,
|
|
68
74
|
encoding: str = "utf-8",
|
|
69
75
|
overwrite: bool = True,
|
|
@@ -77,10 +83,10 @@ def pull(
|
|
|
77
83
|
|
|
78
84
|
|
|
79
85
|
def push(
|
|
80
|
-
src_dir:
|
|
81
|
-
workbook:
|
|
86
|
+
src_dir: str | Path,
|
|
87
|
+
workbook: str | Path,
|
|
82
88
|
*,
|
|
83
|
-
out:
|
|
89
|
+
out: str | Path | None = None,
|
|
84
90
|
encoding: str = "utf-8",
|
|
85
91
|
strict: bool = False,
|
|
86
92
|
) -> list[str]:
|
|
@@ -96,8 +102,8 @@ def push(
|
|
|
96
102
|
|
|
97
103
|
|
|
98
104
|
def pull_word(
|
|
99
|
-
document:
|
|
100
|
-
dest_dir:
|
|
105
|
+
document: str | Path,
|
|
106
|
+
dest_dir: str | Path,
|
|
101
107
|
*,
|
|
102
108
|
encoding: str = "utf-8",
|
|
103
109
|
overwrite: bool = True,
|
|
@@ -111,10 +117,10 @@ def pull_word(
|
|
|
111
117
|
|
|
112
118
|
|
|
113
119
|
def push_word(
|
|
114
|
-
src_dir:
|
|
115
|
-
document:
|
|
120
|
+
src_dir: str | Path,
|
|
121
|
+
document: str | Path,
|
|
116
122
|
*,
|
|
117
|
-
out:
|
|
123
|
+
out: str | Path | None = None,
|
|
118
124
|
encoding: str = "utf-8",
|
|
119
125
|
strict: bool = False,
|
|
120
126
|
) -> list[str]:
|
|
@@ -130,8 +136,8 @@ def push_word(
|
|
|
130
136
|
|
|
131
137
|
|
|
132
138
|
def pull_ppt(
|
|
133
|
-
presentation:
|
|
134
|
-
dest_dir:
|
|
139
|
+
presentation: str | Path,
|
|
140
|
+
dest_dir: str | Path,
|
|
135
141
|
*,
|
|
136
142
|
encoding: str = "utf-8",
|
|
137
143
|
overwrite: bool = True,
|
|
@@ -145,10 +151,10 @@ def pull_ppt(
|
|
|
145
151
|
|
|
146
152
|
|
|
147
153
|
def push_ppt(
|
|
148
|
-
src_dir:
|
|
149
|
-
presentation:
|
|
154
|
+
src_dir: str | Path,
|
|
155
|
+
presentation: str | Path,
|
|
150
156
|
*,
|
|
151
|
-
out:
|
|
157
|
+
out: str | Path | None = None,
|
|
152
158
|
encoding: str = "utf-8",
|
|
153
159
|
strict: bool = False,
|
|
154
160
|
) -> list[str]:
|
|
@@ -163,21 +169,39 @@ def push_ppt(
|
|
|
163
169
|
return updated
|
|
164
170
|
|
|
165
171
|
|
|
172
|
+
def pull_access(
|
|
173
|
+
database: str | Path,
|
|
174
|
+
dest_dir: str | Path,
|
|
175
|
+
*,
|
|
176
|
+
encoding: str = "utf-8",
|
|
177
|
+
overwrite: bool = True,
|
|
178
|
+
) -> list[Path]:
|
|
179
|
+
"""
|
|
180
|
+
Export every VBA module from an Access ``database`` (.accdb) into
|
|
181
|
+
``dest_dir`` as ``.bas`` / ``.cls`` files. Returns the paths
|
|
182
|
+
written. Mirrors :func:`pull` / :func:`pull_word` / :func:`pull_ppt`.
|
|
183
|
+
"""
|
|
184
|
+
db = AccessReader(database)
|
|
185
|
+
return db.pull_modules(dest_dir, encoding=encoding, overwrite=overwrite)
|
|
186
|
+
|
|
187
|
+
|
|
166
188
|
__all__ = [
|
|
189
|
+
"AccessReader",
|
|
190
|
+
"CFBError",
|
|
167
191
|
"ExcelFile",
|
|
168
|
-
"WordFile",
|
|
169
192
|
"PowerPointFile",
|
|
170
|
-
"VBAModuleKind",
|
|
171
193
|
"PyOpenVBAError",
|
|
172
|
-
"CFBError",
|
|
173
|
-
"VBAProjectError",
|
|
174
194
|
"UnsupportedFormatError",
|
|
195
|
+
"VBAModuleKind",
|
|
196
|
+
"VBAProjectError",
|
|
197
|
+
"WordFile",
|
|
175
198
|
"pull",
|
|
176
|
-
"
|
|
177
|
-
"pull_word",
|
|
178
|
-
"push_word",
|
|
199
|
+
"pull_access",
|
|
179
200
|
"pull_ppt",
|
|
201
|
+
"pull_word",
|
|
202
|
+
"push",
|
|
180
203
|
"push_ppt",
|
|
204
|
+
"push_word",
|
|
181
205
|
]
|
|
182
206
|
|
|
183
207
|
__version__ = "2.0.0"
|