pyOpenVBA 3.1.0__tar.gz → 3.2.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-3.1.0 → pyopenvba-3.2.0}/PKG-INFO +1 -1
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/pyproject.toml +1 -1
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/__init__.py +1 -1
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/vba.py +237 -62
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/.gitignore +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/LICENSE.md +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/README.md +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/docs/architecture.md +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/docs/ms-ovba-implementation-guide_v2.md +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/docs/roadmap.md +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/__main__.py +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/_host.py +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/_templates/__init__.py +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/_templates/blank_files/blank_database.accdb +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/_templates/blank_files/blank_document.docm +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/access_read.py +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/cfb.py +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/excel.py +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/exceptions.py +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/powerpoint.py +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/vba_pcode.py +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/word.py +0 -0
- {pyopenvba-3.1.0 → pyopenvba-3.2.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.
|
|
3
|
+
Version: 3.2.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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pyOpenVBA"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.2.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" }
|
|
@@ -25,7 +25,7 @@ Critical implementation traps (guide section 31)
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
27
|
import struct
|
|
28
|
-
from collections.abc import Iterable
|
|
28
|
+
from collections.abc import Callable, Iterable
|
|
29
29
|
from dataclasses import dataclass, field
|
|
30
30
|
from enum import Enum
|
|
31
31
|
from typing import TYPE_CHECKING
|
|
@@ -58,7 +58,9 @@ def copy_token_help(decompressed_current: int, decompressed_chunk_start: int) ->
|
|
|
58
58
|
return length_mask, offset_mask, bit_count
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
def decompress(
|
|
61
|
+
def decompress(
|
|
62
|
+
data: bytes, *, stream_name: str = "<unknown>", max_bytes: int | None = None
|
|
63
|
+
) -> bytes:
|
|
62
64
|
"""
|
|
63
65
|
Decompress a VBA stream using the MS-OVBA compression algorithm.
|
|
64
66
|
|
|
@@ -81,6 +83,36 @@ def decompress(data: bytes, *, stream_name: str = "<unknown>") -> bytes:
|
|
|
81
83
|
``stream_name`` is purely contextual: it is embedded in any
|
|
82
84
|
:class:`VBAProjectError` raised here so callers can identify which
|
|
83
85
|
CFB stream a malformed chunk came from.
|
|
86
|
+
|
|
87
|
+
``max_bytes``, when given, stops decompression at the first chunk
|
|
88
|
+
boundary at or beyond that many output bytes and returns the
|
|
89
|
+
chunk-aligned prefix. Copy tokens never reference across chunk
|
|
90
|
+
boundaries (this decoder enforces it), so the prefix is
|
|
91
|
+
byte-identical to the same range of a full decompression. Useful
|
|
92
|
+
for reading a module's ``Attribute VB_*`` header without paying for
|
|
93
|
+
the whole stream.
|
|
94
|
+
"""
|
|
95
|
+
out, _ = _decompress(data, stream_name=stream_name, max_bytes=max_bytes)
|
|
96
|
+
return out
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _decompress(
|
|
100
|
+
data: bytes, *, stream_name: str, max_bytes: int | None
|
|
101
|
+
) -> tuple[bytes, bool]:
|
|
102
|
+
"""Core decoder behind :func:`decompress`.
|
|
103
|
+
|
|
104
|
+
Returns ``(decompressed, consumed_all)`` where ``consumed_all`` is
|
|
105
|
+
True when the entire compressed input was processed — i.e. the
|
|
106
|
+
returned bytes are the complete decompression, not a prefix.
|
|
107
|
+
|
|
108
|
+
Performance shape (measured; see issue #5): output is produced with
|
|
109
|
+
slice operations wherever the spec allows — non-overlapping copy
|
|
110
|
+
tokens move as one slice, runs of literal tokens within a flag byte
|
|
111
|
+
extend once — and the copy-token masks are recomputed only when the
|
|
112
|
+
chunk-local output size crosses a power of two, since
|
|
113
|
+
:func:`copy_token_help` depends on nothing else. Overlapping
|
|
114
|
+
copies (offset < length) keep the spec's byte-at-a-time semantics,
|
|
115
|
+
which are load-bearing there.
|
|
84
116
|
"""
|
|
85
117
|
|
|
86
118
|
def _err(msg: str, offset: int) -> VBAProjectError:
|
|
@@ -92,13 +124,17 @@ def decompress(data: bytes, *, stream_name: str = "<unknown>") -> bytes:
|
|
|
92
124
|
raise _err("Invalid compressed stream: missing 0x01 signature byte.", 0)
|
|
93
125
|
|
|
94
126
|
pos = 1
|
|
127
|
+
n = len(data)
|
|
95
128
|
out = bytearray()
|
|
96
129
|
|
|
97
|
-
while pos <
|
|
98
|
-
if
|
|
130
|
+
while pos < n:
|
|
131
|
+
if max_bytes is not None and len(out) >= max_bytes:
|
|
132
|
+
return bytes(out), False
|
|
133
|
+
|
|
134
|
+
if pos + 2 > n:
|
|
99
135
|
raise _err("Truncated compressed stream: missing chunk header.", pos)
|
|
100
136
|
|
|
101
|
-
header =
|
|
137
|
+
header = data[pos] | data[pos + 1] << 8
|
|
102
138
|
chunk_data_size = (header & 0x0FFF) + 1 # data bytes after the header
|
|
103
139
|
chunk_signature = (header >> 12) & 0x7
|
|
104
140
|
chunk_flag = (header >> 15) & 0x1
|
|
@@ -113,10 +149,10 @@ def decompress(data: bytes, *, stream_name: str = "<unknown>") -> bytes:
|
|
|
113
149
|
)
|
|
114
150
|
|
|
115
151
|
chunk_end = pos + chunk_data_size
|
|
116
|
-
if chunk_end >
|
|
152
|
+
if chunk_end > n:
|
|
117
153
|
raise _err(
|
|
118
154
|
f"Truncated chunk: header announces {chunk_data_size} bytes "
|
|
119
|
-
f"but only {
|
|
155
|
+
f"but only {n - pos} remain.",
|
|
120
156
|
header_offset,
|
|
121
157
|
)
|
|
122
158
|
decompressed_chunk_start = len(out)
|
|
@@ -129,32 +165,42 @@ def decompress(data: bytes, *, stream_name: str = "<unknown>") -> bytes:
|
|
|
129
165
|
f"got {chunk_data_size}.",
|
|
130
166
|
header_offset,
|
|
131
167
|
)
|
|
132
|
-
if pos + 4096 >
|
|
168
|
+
if pos + 4096 > n:
|
|
133
169
|
raise _err("Truncated raw chunk.", pos)
|
|
134
170
|
out.extend(data[pos: pos + 4096])
|
|
135
171
|
pos += 4096
|
|
136
172
|
else:
|
|
137
|
-
# Token-compressed chunk.
|
|
173
|
+
# Token-compressed chunk. Masks depend only on the
|
|
174
|
+
# chunk-local output offset; recompute at power-of-two
|
|
175
|
+
# crossings instead of per token.
|
|
176
|
+
length_mask, offset_mask, bit_count = copy_token_help(
|
|
177
|
+
len(out), decompressed_chunk_start
|
|
178
|
+
)
|
|
179
|
+
next_threshold = (1 << bit_count) + 1
|
|
180
|
+
|
|
138
181
|
while pos < chunk_end:
|
|
139
|
-
if pos >=
|
|
182
|
+
if pos >= n:
|
|
140
183
|
break
|
|
141
|
-
flag_byte =
|
|
184
|
+
flag_byte = data[pos]
|
|
142
185
|
pos += 1
|
|
143
186
|
|
|
144
|
-
|
|
145
|
-
|
|
187
|
+
bit = 0
|
|
188
|
+
while bit < 8:
|
|
189
|
+
if pos >= chunk_end or pos >= n:
|
|
146
190
|
break
|
|
147
191
|
|
|
148
192
|
if (flag_byte >> bit) & 1:
|
|
149
193
|
# Copy token — back-reference into already-decompressed output.
|
|
150
|
-
if pos + 2 >
|
|
194
|
+
if pos + 2 > n:
|
|
151
195
|
raise _err("Truncated copy token.", pos)
|
|
152
|
-
token =
|
|
196
|
+
token = data[pos] | data[pos + 1] << 8
|
|
153
197
|
pos += 2
|
|
154
198
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
199
|
+
if len(out) - decompressed_chunk_start >= next_threshold:
|
|
200
|
+
length_mask, offset_mask, bit_count = copy_token_help(
|
|
201
|
+
len(out), decompressed_chunk_start
|
|
202
|
+
)
|
|
203
|
+
next_threshold = (1 << bit_count) + 1
|
|
158
204
|
length = (token & length_mask) + 3
|
|
159
205
|
offset = ((token & offset_mask) >> (16 - bit_count)) + 1
|
|
160
206
|
|
|
@@ -171,16 +217,29 @@ def decompress(data: bytes, *, stream_name: str = "<unknown>") -> bytes:
|
|
|
171
217
|
pos - 2,
|
|
172
218
|
)
|
|
173
219
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
out
|
|
177
|
-
|
|
220
|
+
if offset >= length:
|
|
221
|
+
# Source range is fully materialized: one slice.
|
|
222
|
+
out += out[copy_src: copy_src + length]
|
|
223
|
+
else:
|
|
224
|
+
# Overlapping copy; byte-at-a-time is required
|
|
225
|
+
# by spec semantics (the pattern repeats).
|
|
226
|
+
for _ in range(length):
|
|
227
|
+
out.append(out[copy_src])
|
|
228
|
+
copy_src += 1
|
|
229
|
+
bit += 1
|
|
178
230
|
else:
|
|
179
|
-
#
|
|
180
|
-
|
|
181
|
-
|
|
231
|
+
# Run of literal tokens: consecutive clear bits in
|
|
232
|
+
# this flag byte copy verbatim as one slice.
|
|
233
|
+
run_start = pos
|
|
234
|
+
while (bit < 8
|
|
235
|
+
and not ((flag_byte >> bit) & 1)
|
|
236
|
+
and pos < chunk_end
|
|
237
|
+
and pos < n):
|
|
238
|
+
pos += 1
|
|
239
|
+
bit += 1
|
|
240
|
+
out += data[run_start:pos]
|
|
182
241
|
|
|
183
|
-
return bytes(out)
|
|
242
|
+
return bytes(out), True
|
|
184
243
|
|
|
185
244
|
|
|
186
245
|
def compress(data: bytes) -> bytes:
|
|
@@ -678,35 +737,101 @@ class _DirInfo:
|
|
|
678
737
|
# Public data model
|
|
679
738
|
# ---------------------------------------------------------------------------
|
|
680
739
|
|
|
681
|
-
@dataclass
|
|
682
740
|
class VBAModule:
|
|
683
|
-
"""A parsed VBA module with its source code.
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
741
|
+
"""A parsed VBA module with its source code.
|
|
742
|
+
|
|
743
|
+
``source`` may be materialized lazily: modules produced by
|
|
744
|
+
:func:`parse_vba_project` defer the MS-OVBA decompression of their
|
|
745
|
+
stream until the first ``.source`` access (issue #5 — decompression
|
|
746
|
+
is 88-96% of the cost of opening a project, and callers that only
|
|
747
|
+
want names, kinds, or one module out of many should not pay for all
|
|
748
|
+
of it). Constructing a module with an explicit ``source`` string is
|
|
749
|
+
fully eager and behaves exactly as before. One consequence of
|
|
750
|
+
laziness: a corrupt chunk past the first one raises
|
|
751
|
+
:class:`VBAProjectError` at first access rather than at parse time.
|
|
752
|
+
"""
|
|
753
|
+
|
|
754
|
+
def __init__(
|
|
755
|
+
self,
|
|
756
|
+
name: str,
|
|
757
|
+
stream_name: str,
|
|
758
|
+
source: str = "",
|
|
759
|
+
kind: VBAModuleKind = VBAModuleKind.standard,
|
|
760
|
+
text_offset: int = 0,
|
|
761
|
+
is_read_only: bool = False,
|
|
762
|
+
is_private: bool = False,
|
|
763
|
+
# Dir-stream sub-records preserved for round-trip serialization.
|
|
764
|
+
name_unicode: str = "",
|
|
765
|
+
stream_name_unicode: str = "",
|
|
766
|
+
doc_string: str = "",
|
|
767
|
+
doc_string_unicode: str = "",
|
|
768
|
+
help_context: int = 0,
|
|
769
|
+
cookie: int = 0,
|
|
770
|
+
# Original bytes 0..text_offset of the module stream (performance
|
|
771
|
+
# cache / version-dependent prefix). Preserved across write-back
|
|
772
|
+
# so that Office's cache invalidation logic operates the same way
|
|
773
|
+
# as it would for an untouched stream.
|
|
774
|
+
prefix_bytes: bytes = b"",
|
|
775
|
+
# Whether the source has been edited and needs recompression on save.
|
|
776
|
+
dirty: bool = False,
|
|
777
|
+
# Cached attribute header captured at parse time so a body-only
|
|
778
|
+
# source replacement (the VBE-style edit surface) can re-prepend
|
|
779
|
+
# the required ``Attribute VB_*`` block. Re-derived on demand when
|
|
780
|
+
# missing via ``split_attribute_header(self.source)``.
|
|
781
|
+
attribute_header: str = "",
|
|
782
|
+
source_loader: Callable[[], str] | None = None,
|
|
783
|
+
) -> None:
|
|
784
|
+
self.name = name
|
|
785
|
+
self.stream_name = stream_name
|
|
786
|
+
self.kind = kind
|
|
787
|
+
self.text_offset = text_offset
|
|
788
|
+
self.is_read_only = is_read_only
|
|
789
|
+
self.is_private = is_private
|
|
790
|
+
self.name_unicode = name_unicode
|
|
791
|
+
self.stream_name_unicode = stream_name_unicode
|
|
792
|
+
self.doc_string = doc_string
|
|
793
|
+
self.doc_string_unicode = doc_string_unicode
|
|
794
|
+
self.help_context = help_context
|
|
795
|
+
self.cookie = cookie
|
|
796
|
+
self.prefix_bytes = prefix_bytes
|
|
797
|
+
self.dirty = dirty
|
|
798
|
+
self.attribute_header = attribute_header
|
|
799
|
+
if source_loader is not None:
|
|
800
|
+
self._source: str | None = None
|
|
801
|
+
self._source_loader: Callable[[], str] | None = source_loader
|
|
802
|
+
else:
|
|
803
|
+
self._source = source
|
|
804
|
+
self._source_loader = None
|
|
805
|
+
|
|
806
|
+
def __repr__(self) -> str:
|
|
807
|
+
return (
|
|
808
|
+
f"VBAModule(name={self.name!r}, stream_name={self.stream_name!r}, "
|
|
809
|
+
f"kind={self.kind!r}, text_offset={self.text_offset}, "
|
|
810
|
+
f"source_loaded={self._source is not None})"
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
@property
|
|
814
|
+
def source_loaded(self) -> bool:
|
|
815
|
+
"""True once the source text is materialized in memory."""
|
|
816
|
+
return self._source is not None
|
|
817
|
+
|
|
818
|
+
@property
|
|
819
|
+
def source(self) -> str:
|
|
820
|
+
"""The module's full source text (header plus body).
|
|
821
|
+
|
|
822
|
+
Materializes lazily on first access for parsed modules.
|
|
823
|
+
"""
|
|
824
|
+
if self._source is None:
|
|
825
|
+
loader = self._source_loader
|
|
826
|
+
assert loader is not None, "lazy module lost its loader"
|
|
827
|
+
self._source = loader()
|
|
828
|
+
self._source_loader = None
|
|
829
|
+
return self._source
|
|
830
|
+
|
|
831
|
+
@source.setter
|
|
832
|
+
def source(self, value: str) -> None:
|
|
833
|
+
self._source = value
|
|
834
|
+
self._source_loader = None
|
|
710
835
|
|
|
711
836
|
@property
|
|
712
837
|
def body(self) -> str:
|
|
@@ -1470,12 +1595,31 @@ def _project_section_end(lines: list[str]) -> int:
|
|
|
1470
1595
|
# Public factory
|
|
1471
1596
|
# ---------------------------------------------------------------------------
|
|
1472
1597
|
|
|
1598
|
+
def _make_source_loader(
|
|
1599
|
+
compressed: bytes, encoding: str, stream_name: str
|
|
1600
|
+
) -> Callable[[], str]:
|
|
1601
|
+
"""Build the deferred decompress-and-decode thunk for a lazy module."""
|
|
1602
|
+
def _load() -> str:
|
|
1603
|
+
return decompress(compressed, stream_name=stream_name).decode(
|
|
1604
|
+
encoding, errors="replace"
|
|
1605
|
+
)
|
|
1606
|
+
return _load
|
|
1607
|
+
|
|
1608
|
+
|
|
1473
1609
|
def parse_vba_project(cfb: CFB) -> VBAProject:
|
|
1474
1610
|
"""
|
|
1475
|
-
Extract
|
|
1611
|
+
Extract all VBA module metadata from a parsed CFB.
|
|
1476
1612
|
|
|
1477
1613
|
The CFB must be the vbaProject.bin from an xlsm/xlsb, or the whole
|
|
1478
1614
|
file for an xls workbook.
|
|
1615
|
+
|
|
1616
|
+
Module source text is loaded lazily: only the first compressed
|
|
1617
|
+
chunk of each module stream is decompressed here (enough for the
|
|
1618
|
+
``Attribute VB_*`` header, and for single-chunk modules it is
|
|
1619
|
+
already the whole source). The remaining chunks decompress on the
|
|
1620
|
+
first ``VBAModule.source`` access. Stream lookup and MODULEOFFSET
|
|
1621
|
+
bounds checks stay eager so structural corruption still surfaces at
|
|
1622
|
+
parse time.
|
|
1479
1623
|
"""
|
|
1480
1624
|
try:
|
|
1481
1625
|
dir_compressed = cfb.get_stream_in_storage("VBA", "dir")
|
|
@@ -1510,17 +1654,47 @@ def parse_vba_project(cfb: CFB) -> VBAProject:
|
|
|
1510
1654
|
f"{len(stream_compressed)} for module {info.name!r}."
|
|
1511
1655
|
)
|
|
1512
1656
|
|
|
1513
|
-
compressed_source = stream_compressed[info.text_offset:]
|
|
1514
|
-
|
|
1515
|
-
|
|
1657
|
+
compressed_source = bytes(stream_compressed[info.text_offset:])
|
|
1658
|
+
ovba_name = f"VBA/{stream_name}"
|
|
1659
|
+
|
|
1660
|
+
# Decompress just the first chunk. For single-chunk modules
|
|
1661
|
+
# (decompressed size <= 4096) this is the complete source and
|
|
1662
|
+
# the module is materialized eagerly with zero extra work.
|
|
1663
|
+
prefix_raw, consumed_all = _decompress(
|
|
1664
|
+
compressed_source, stream_name=ovba_name, max_bytes=1
|
|
1516
1665
|
)
|
|
1517
|
-
|
|
1518
|
-
|
|
1666
|
+
prefix_text = prefix_raw.decode(encoding, errors="replace")
|
|
1667
|
+
|
|
1668
|
+
source: str | None
|
|
1669
|
+
loader: Callable[[], str] | None
|
|
1670
|
+
if consumed_all:
|
|
1671
|
+
source = prefix_text
|
|
1672
|
+
header, _ = split_attribute_header(prefix_text)
|
|
1673
|
+
loader = None
|
|
1674
|
+
else:
|
|
1675
|
+
header, body_prefix = split_attribute_header(prefix_text)
|
|
1676
|
+
if header and body_prefix:
|
|
1677
|
+
# Header fits inside the first chunk (the normal case);
|
|
1678
|
+
# defer the rest of the stream.
|
|
1679
|
+
source = None
|
|
1680
|
+
loader = _make_source_loader(
|
|
1681
|
+
compressed_source, encoding, ovba_name
|
|
1682
|
+
)
|
|
1683
|
+
else:
|
|
1684
|
+
# Either no attribute header at all, or the header runs
|
|
1685
|
+
# to the chunk boundary and may be truncated. Both are
|
|
1686
|
+
# abnormal; fall back to eager full decompression so
|
|
1687
|
+
# attribute_header is always derived from complete text.
|
|
1688
|
+
source = decompress(
|
|
1689
|
+
compressed_source, stream_name=ovba_name
|
|
1690
|
+
).decode(encoding, errors="replace")
|
|
1691
|
+
header, _ = split_attribute_header(source)
|
|
1692
|
+
loader = None
|
|
1519
1693
|
|
|
1520
1694
|
modules.append(VBAModule(
|
|
1521
1695
|
name=info.name,
|
|
1522
1696
|
stream_name=stream_name,
|
|
1523
|
-
source=source,
|
|
1697
|
+
source=source if source is not None else "",
|
|
1524
1698
|
kind=info.module_kind,
|
|
1525
1699
|
text_offset=info.text_offset,
|
|
1526
1700
|
is_read_only=info.is_read_only,
|
|
@@ -1531,8 +1705,9 @@ def parse_vba_project(cfb: CFB) -> VBAProject:
|
|
|
1531
1705
|
doc_string_unicode=info.doc_string_unicode,
|
|
1532
1706
|
help_context=info.help_context,
|
|
1533
1707
|
cookie=info.cookie,
|
|
1534
|
-
prefix_bytes=stream_compressed[: info.text_offset],
|
|
1708
|
+
prefix_bytes=bytes(stream_compressed[: info.text_offset]),
|
|
1535
1709
|
attribute_header=header,
|
|
1710
|
+
source_loader=loader,
|
|
1536
1711
|
))
|
|
1537
1712
|
|
|
1538
1713
|
project = VBAProject(modules=modules, code_page=code_page)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/_templates/blank_files/blank_database.accdb
RENAMED
|
File without changes
|
{pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/_templates/blank_files/blank_document.docm
RENAMED
|
File without changes
|
{pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/_templates/blank_files/blank_presentation.pptm
RENAMED
|
File without changes
|
{pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsb
RENAMED
|
File without changes
|
{pyopenvba-3.1.0 → pyopenvba-3.2.0}/src/pyopenvba/_templates/blank_files/blank_workbook.xlsm
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|