Module refinery.lib.ole.vba
Parser for VBA macro extraction from OLE and OOXML documents.
Implements MS-OVBA decompression and the parsing of the dir stream of a VBA project. VBA projects
are stored in compound files: the input itself, a part of an OpenXML (ZIP) document, a Word 2003
XML or Flat OPC document, an MHTML file, or the VBA storage of a Microsoft Access database. The
function vba_containers() lists the compound files of an input, vba_compound_files() opens them and
lists the VBA projects inside each, and every VBAProject lists its modules as VBAModule objects.
The forms of a compound file are listed by vba_form_storages().
Expand source code Browse git
"""
Parser for VBA macro extraction from OLE and OOXML documents.
Implements MS-OVBA decompression and the parsing of the dir stream of a VBA project. VBA projects
are stored in compound files: the input itself, a part of an OpenXML (ZIP) document, a Word 2003
XML or Flat OPC document, an MHTML file, or the VBA storage of a Microsoft Access database. The
function `vba_containers` lists the compound files of an input, `vba_compound_files` opens them and
lists the VBA projects inside each, and every `VBAProject` lists its modules as `VBAModule` objects.
The forms of a compound file are listed by `vba_form_storages`.
"""
from __future__ import annotations
import base64
import codecs
import email
import email.feedparser
import enum
import math
import re
import struct
import zipfile
import zlib
from typing import Callable, Iterator, NamedTuple
from defusedxml.ElementTree import fromstring
from refinery.lib.access import AccessDatabase, is_access_database
from refinery.lib.id import buffer_contains
from refinery.lib.ole.file import MAGIC as OLE_MAGIC
from refinery.lib.ole.file import STGTY, OleFile, OleFileLike
from refinery.lib.structures import MemoryFile, Struct, StructReader
from refinery.lib.types import buf
_NAMESPACE_WORD2003 = 'http://schemas.microsoft.com/office/word/2003/wordml'
_NAMESPACE_FLATOPC = 'http://schemas.microsoft.com/office/2006/xmlPackage'
NS_W = F'{{{_NAMESPACE_WORD2003}}}'
TAG_BINDATA = F'{NS_W}binData'
ATTR_NAME = F'{NS_W}name'
NS_XMLPACKAGE = F'{{{_NAMESPACE_FLATOPC}}}'
TAG_PKGPART = F'{NS_XMLPACKAGE}part'
ATTR_PKG_NAME = F'{NS_XMLPACKAGE}name'
ATTR_PKG_CONTENTTYPE = F'{NS_XMLPACKAGE}contentType'
TAG_PKGBINDATA = F'{NS_XMLPACKAGE}binaryData'
CTYPE_VBAPROJECT = 'application/vnd.ms-office.vbaProject'
MSO_ACTIVEMIME_HEADER = b'ActiveMime'
_RE_PRINTABLE_STRING = re.compile(b'[\\t\\r\\n\\x20-\\xFF]{5,}')
Reporter = Callable[[str, Exception], None]
"""
A function that receives the location of a part of the input that cannot be read, and the error
that occurred while reading it.
"""
_CODEPAGE_OVERRIDES = {
10000: 'mac-roman',
10006: 'mac-greek',
10007: 'mac-cyrillic',
10029: 'mac-latin2',
10079: 'mac-iceland',
10081: 'mac-turkish',
20127: 'ascii',
20866: 'koi8-r',
20932: 'euc-jp',
21866: 'koi8-u',
28591: 'iso-8859-1',
28592: 'iso-8859-2',
28593: 'iso-8859-3',
28594: 'iso-8859-4',
28595: 'iso-8859-5',
28596: 'iso-8859-6',
28597: 'iso-8859-7',
28598: 'iso-8859-8',
28599: 'iso-8859-9',
28603: 'iso-8859-13',
28605: 'iso-8859-15',
50220: 'iso-2022-jp',
50225: 'iso-2022-kr',
51932: 'euc-jp',
51949: 'euc-kr',
65000: 'utf-7',
65001: 'utf-8',
}
def _codepage_to_codec(cp: int) -> str:
if name := _CODEPAGE_OVERRIDES.get(cp):
return name
try:
return codecs.lookup(F'cp{cp}').name
except LookupError:
return 'cp1252'
def _find_vba_projects(ole: OleFileLike) -> list[str]:
"""
The storages of all VBA projects in the given compound file. Each storage is given as a path
prefix, which is empty for the root storage and otherwise ends with a forward slash.
"""
storages: list[str] = []
for storage, entry in ole.walk(streams=False, storages=True):
if storage[-1].upper() != 'VBA' or ole.find('/'.join(storage)) is not entry:
continue
vba_root = '/'.join(storage[:-1])
if vba_root:
vba_root += '/'
if ole.get_type(F'{vba_root}PROJECT') != STGTY.STREAM:
continue
if ole.get_type(F'{vba_root}VBA/_VBA_PROJECT') != STGTY.STREAM:
continue
if ole.get_type(_dir_path(vba_root)) != STGTY.STREAM:
continue
storages.append(vba_root)
return storages
def _dir_path(storage: str) -> str:
return F'{storage}VBA/dir'
def vba_form_storages(ole: OleFileLike) -> list[str]:
"""
The paths of all form storages in the given compound file, i.e. storages that hold a stream
named `f` and a stream named `o`. The storage of a frame inside a form is a form storage below
the storage of the form.
"""
forms: list[str] = []
for storage, entry in ole.walk(streams=False, storages=True):
prefix = '/'.join(storage)
if ole.find(prefix) is not entry:
continue
if (
ole.get_type(F'{prefix}/o') == STGTY.STREAM
and ole.get_type(F'{prefix}/f') == STGTY.STREAM
):
forms.append(prefix)
return forms
class FileOpenError(Exception):
"""
Raised when data cannot be recognized as a supported file format.
"""
def __init__(self, message: str = 'failed to open file'):
super().__init__(message)
def copytoken_help(
decompressed_current: int,
decompressed_chunk_start: int,
) -> tuple[int, int, int, int]:
"""
Compute bit masks to decode a CopyToken per MS-OVBA 2.4.1.3.19.1. Returns (length_mask,
offset_mask, bit_count, maximum_length).
"""
difference = decompressed_current - decompressed_chunk_start
if difference < 2:
bit_count = 4
else:
bit_count = max(int(math.ceil(math.log(difference, 2))), 4)
length_mask = 0xFFFF >> bit_count
offset_mask = ~length_mask & 0xFFFF
maximum_length = (0xFFFF >> bit_count) + 3
return length_mask, offset_mask, bit_count, maximum_length
def decompress_stream(data: bytes | bytearray | memoryview) -> bytearray:
"""
Decompress a VBA compressed stream per MS-OVBA section 2.4.1.
The compressed container starts with a signature byte (0x01), followed by compressed chunks.
Each chunk has a 2-byte header encoding size, signature (0b011), and a flag indicating whether
the chunk is compressed or raw.
"""
view = memoryview(data)
if len(view) < 1:
raise ValueError('empty compressed container')
if view[0] != 0x01:
raise ValueError(F'invalid signature byte 0x{view[0]:02X}, expected 1')
decompressed = bytearray()
pos = 1
while pos < len(view):
if pos + 2 > len(view):
break
chunk_header = struct.unpack_from('<H', view, pos)[0]
chunk_size = (chunk_header & 0x0FFF) + 3
chunk_signature = (chunk_header >> 12) & 0x07
chunk_flag = (chunk_header >> 15) & 0x01
if chunk_signature != 0b011:
raise ValueError('Invalid CompressedChunkSignature in VBA stream')
if chunk_flag == 1 and chunk_size > 4098:
raise ValueError(F'CompressedChunkSize {chunk_size} > 4098 but CompressedChunkFlag == 1')
if chunk_flag == 0 and chunk_size != 4098:
raise ValueError(F'CompressedChunkSize {chunk_size} != 4098 but CompressedChunkFlag == 0')
compressed_end = min(len(view), pos + chunk_size)
compressed_current = pos + 2
if chunk_flag == 0:
decompressed.extend(view[compressed_current:compressed_current + 4096])
compressed_current += 4096
else:
decompressed_chunk_start = len(decompressed)
while compressed_current < compressed_end:
if compressed_current >= len(view):
break
flag_byte = view[compressed_current]
compressed_current += 1
for bit_index in range(8):
if compressed_current >= compressed_end:
break
flag_bit = (flag_byte >> bit_index) & 1
if flag_bit == 0:
decompressed.append(view[compressed_current])
compressed_current += 1
else:
if compressed_current + 2 > len(view):
break
copy_token = struct.unpack_from(
'<H', view, compressed_current)[0]
length_mask, offset_mask, bit_count, _ = (
copytoken_help(
len(decompressed),
decompressed_chunk_start))
length = (copy_token & length_mask) + 3
temp1 = copy_token & offset_mask
temp2 = 16 - bit_count
offset = (temp1 >> temp2) + 1
copy_source = len(decompressed) - offset
for index in range(
copy_source, copy_source + length
):
decompressed.append(decompressed[index])
compressed_current += 2
pos = compressed_end
return decompressed
def _is_mso_file(data: buf) -> bool:
return data[:len(MSO_ACTIVEMIME_HEADER)] == MSO_ACTIVEMIME_HEADER
def _mso_file_extract(data: buf) -> bytes:
"""
Extract OLE data from an ActiveMime/MSO container by decompressing the embedded zlib stream.
"""
offsets: list[int] = []
try:
offset = struct.unpack_from('<H', data, 0x1E)[0] + 46
offsets.append(offset)
except struct.error:
pass
offsets.extend([0x32, 0x22A])
for start in offsets:
try:
return zlib.decompress(data[start:])
except zlib.error:
pass
for start in range(len(data)):
if data[start:start + 1] == b'\x78':
try:
return zlib.decompress(data[start:])
except zlib.error:
pass
raise ValueError('unable to decompress data from MSO/ActiveMime file')
def _is_mhtml(data: buf) -> bool:
if (mime := re.search(B'(?i)mime', data)) is None:
return False
if (version := re.search(B'(?i)version', data)) is None:
return False
if re.search(B'(?i)multipart', data) is None:
return False
return abs(version.start() - mime.start()) < 20
def is_vba_container(data: buf) -> bool:
"""
A quick check whether the given data looks like one of the containers that `vba_containers`
reads: a compound file, a Microsoft Access database, an OpenXML document, a Word 2003 XML or
Flat OPC document, or an MHTML file.
"""
view = memoryview(data)
if view[:8] == OLE_MAGIC or is_access_database(view):
return True
if view[:2] == B'PK':
return buffer_contains(view, B'[Content_Types].xml')
if buffer_contains(view, _NAMESPACE_WORD2003.encode()):
return True
if buffer_contains(view, _NAMESPACE_FLATOPC.encode()):
return True
return _is_mhtml(view)
class VBAContainer(NamedTuple):
"""
A compound file of an input, which may hold VBA projects. The part is the name under which the
input stores the compound file, or `None` when the compound file is the input itself. The part
of a zip member has forward slashes in place of backslashes on every platform. The
compound file is read only when `open` is called, which raises the error of the read when the
part cannot be read or is no compound file.
"""
part: str | None
open: Callable[[], OleFileLike]
def _ole_containers(data: memoryview) -> list[VBAContainer]:
if data[:8] != OLE_MAGIC:
return []
return [VBAContainer(None, lambda: OleFile(data))]
def _zip_containers(data: memoryview) -> list[VBAContainer]:
if data[:2] != B'PK':
return []
try:
archive = zipfile.ZipFile(MemoryFile(data, bytes))
except Exception:
return []
containers: list[VBAContainer] = []
for info in archive.infolist():
try:
with archive.open(info) as part:
is_ole = part.read(8) == OLE_MAGIC
except Exception:
is_ole = True
if is_ole:
name = info.filename.replace('\\', '/')
containers.append(VBAContainer(name, lambda info=info: OleFile(archive.read(info))))
return containers
def _word2003_containers(data: memoryview) -> list[VBAContainer]:
if not buffer_contains(data, _NAMESPACE_WORD2003.encode()):
return []
try:
document = fromstring(bytes(data))
except Exception:
return []
def extract(text: str, decoded: bytes | None):
if decoded is None:
decoded = base64.b64decode(text)
return OleFile(_mso_file_extract(decoded))
containers: list[VBAContainer] = []
for bindata in document.iter(TAG_BINDATA):
text = bindata.text or ''
try:
decoded = base64.b64decode(text)
except Exception:
decoded = None
else:
if not _is_mso_file(decoded):
continue
name = bindata.get(ATTR_NAME, 'noname.mso')
containers.append(VBAContainer(name, lambda text=text, decoded=decoded: extract(text, decoded)))
return containers
def _flatopc_containers(data: memoryview) -> list[VBAContainer]:
if not buffer_contains(data, _NAMESPACE_FLATOPC.encode()):
return []
try:
document = fromstring(bytes(data))
except Exception:
return []
containers: list[VBAContainer] = []
for package_part in document.iter(TAG_PKGPART):
if package_part.get(ATTR_PKG_CONTENTTYPE, 'unknown') != CTYPE_VBAPROJECT:
continue
name = package_part.get(ATTR_PKG_NAME, 'unknown')
for bindata in package_part.iterfind(TAG_PKGBINDATA):
text = bindata.text or ''
containers.append(VBAContainer(name, lambda text=text: OleFile(base64.b64decode(text))))
return containers
def _mhtml_containers(data: memoryview) -> list[VBAContainer]:
if not _is_mhtml(data):
return []
try:
stripped = bytes(data).lstrip(b'\r\n\t ')
mime_offset = stripped.find(b'MIME')
content_offset = stripped.find(b'Content')
if -1 < mime_offset <= content_offset:
stripped = stripped[mime_offset:]
elif content_offset > -1:
stripped = stripped[content_offset:]
old_header_re = getattr(email.feedparser, 'headerRE')
loose_re = re.compile(r'^(From |[\041-\071\073-\176]{1,}:?|[\t ])')
setattr(email.feedparser, 'headerRE', loose_re)
try:
mhtml = email.message_from_bytes(stripped)
finally:
setattr(email.feedparser, 'headerRE', old_header_re)
containers: list[VBAContainer] = []
for part in mhtml.walk():
payload = part.get_payload(decode=True)
if not isinstance(payload, bytes):
continue
if _is_mso_file(payload):
name = part.get_filename('editdata.mso')
containers.append(VBAContainer(name, lambda payload=payload: OleFile(_mso_file_extract(payload))))
elif payload[:8] == OLE_MAGIC:
name = part.get_filename('embedded.ole')
containers.append(VBAContainer(name, lambda payload=payload: OleFile(payload)))
except Exception:
return []
return containers
def _access_containers(data: memoryview) -> list[VBAContainer]:
if not is_access_database(data):
return []
if (ole := AccessDatabase(bytes(data)).open_vba()) is None:
return []
return [VBAContainer(None, lambda: ole)]
def vba_containers(data: buf) -> list[VBAContainer]:
"""
The compound files of the given input that may hold VBA projects, in the order in which the
input stores them. The input is a compound file, an OpenXML document or any other zip archive,
a Word 2003 XML or Flat OPC document, an MHTML file, or a Microsoft Access database. The parts
of these containers are found without reading them in full; a part that cannot be read is
listed, so that the error of opening it can be reported. Raises `FileOpenError` when the input
is none of these containers or has no part that may be a compound file. The error of reading
the VBA storage of an Access database is raised.
"""
view = memoryview(data)
for find_containers in (
_ole_containers,
_access_containers,
_zip_containers,
_word2003_containers,
_flatopc_containers,
_mhtml_containers,
):
if containers := find_containers(view):
return containers
raise FileOpenError('data is not a supported file type for VBA extraction')
def _open_containers(
containers: list[VBAContainer],
report: Reporter,
) -> Iterator[tuple[str | None, OleFileLike]]:
"""
Opens the given containers one at a time and lists each part name with its compound file. A
part that cannot be opened is passed to the given function with the error and skipped. When the
input itself cannot be opened, the error is raised.
"""
for container in containers:
try:
ole = container.open()
except Exception as error:
if container.part is None:
raise
report(container.part, error)
continue
yield container.part, ole
def _location(part: str | None, path: str) -> str:
if part is None:
return path
return F'{part}/{path}'
class DirRecord(enum.IntEnum):
"""
The ids of the records of a dir stream that are read for their content or that have a body of
fixed size (MS-OVBA 2.3.4.2).
"""
PROJECTSYSKIND = 0x0001
PROJECTLCID = 0x0002
PROJECTCODEPAGE = 0x0003
PROJECTNAME = 0x0004
PROJECTHELPCONTEXT = 0x0007
PROJECTLIBFLAGS = 0x0008
PROJECTVERSION = 0x0009
PROJECTMODULES = 0x000F
PROJECTCOOKIE = 0x0013
PROJECTLCIDINVOKE = 0x0014
MODULENAME = 0x0019
MODULESTREAMNAME = 0x001A
MODULEHELPCONTEXT = 0x001E
MODULETYPEPROCEDURAL = 0x0021
MODULETYPEDOCUMENT = 0x0022
MODULEREADONLY = 0x0025
MODULEPRIVATE = 0x0028
MODULETERMINATOR = 0x002B
MODULECOOKIE = 0x002C
MODULEOFFSET = 0x0031
MODULESTREAMNAMEUNICODE = 0x0032
MODULENAMEUNICODE = 0x0047
PROJECTCOMPATVERSION = 0x004A
_FIXED_BODY_SIZES: dict[int, int] = {
DirRecord.PROJECTSYSKIND : 4,
DirRecord.PROJECTLCID : 4,
DirRecord.PROJECTCODEPAGE : 2,
DirRecord.PROJECTHELPCONTEXT : 4,
DirRecord.PROJECTLIBFLAGS : 4,
DirRecord.PROJECTVERSION : 6,
DirRecord.PROJECTMODULES : 2,
DirRecord.PROJECTCOOKIE : 2,
DirRecord.PROJECTLCIDINVOKE : 4,
DirRecord.MODULEHELPCONTEXT : 4,
DirRecord.MODULETYPEPROCEDURAL : 0,
DirRecord.MODULETYPEDOCUMENT : 0,
DirRecord.MODULEREADONLY : 0,
DirRecord.MODULEPRIVATE : 0,
DirRecord.MODULETERMINATOR : 0,
DirRecord.MODULECOOKIE : 2,
DirRecord.MODULEOFFSET : 4,
DirRecord.PROJECTCOMPATVERSION : 4,
}
class _DirStreamRecord(Struct[memoryview]):
"""
A record of a dir stream: a 16-bit id, a 32-bit size, and a body. The body has the given size,
unless the record is listed in `_FIXED_BODY_SIZES`; for these, the 32-bit field is a reserved
value or the size of a body whose size is fixed anyway.
"""
def __init__(self, reader: StructReader[memoryview]):
self.id = reader.u16()
size = reader.u32()
self.body = reader.read_exactly(_FIXED_BODY_SIZES.get(self.id, size))
@property
def value(self) -> int:
return int.from_bytes(self.body, 'little')
class VBAModuleRecord(Struct[memoryview]):
"""
The MODULE record of a dir stream (MS-OVBA 2.3.4.2.3.2): the name of a module, the name of its
stream, both also in UTF-16 if present, and the offset of the compressed source code in the
stream. The record begins with the name of the module and ends with a terminator; records
between them that are not listed here are skipped.
"""
def __init__(self, reader: StructReader[memoryview], codec: str):
if (record := _DirStreamRecord(reader)).id != DirRecord.MODULENAME:
raise ValueError(F'Expected a module record, found a record with id 0x{record.id:04X}.')
self.name = codecs.decode(record.body, codec, 'replace')
self.name_unicode: str | None = None
self.stream_name: str | None = None
self.stream_name_unicode: str | None = None
self.text_offset: int | None = None
while (record := _DirStreamRecord(reader)).id != DirRecord.MODULETERMINATOR:
if record.id == DirRecord.MODULENAMEUNICODE:
self.name_unicode = codecs.decode(record.body, 'utf-16le', 'replace')
elif record.id == DirRecord.MODULESTREAMNAME:
self.stream_name = codecs.decode(record.body, codec, 'replace')
elif record.id == DirRecord.MODULESTREAMNAMEUNICODE:
self.stream_name_unicode = codecs.decode(record.body, 'utf-16le', 'replace')
elif record.id == DirRecord.MODULEOFFSET:
self.text_offset = record.value
@property
def stream_names(self) -> list[str]:
"""
The names under which the stream of the module may be stored, in the order in which they
are tried: the names of the stream, then the names of the module.
"""
names = (self.stream_name, self.stream_name_unicode, self.name, self.name_unicode)
return list(dict.fromkeys(name for name in names if name is not None))
class VBADirectory(Struct[memoryview]):
"""
The decompressed dir stream of a VBA project (MS-OVBA 2.3.4.2): the system kind, the code page
and the name of the project as stored, and the number of its modules. The stream is a sequence
of records; the records in front of the list of modules, which include the references, are read
in any order. Every record is read with its exact length, so that a record which runs past the
end of the stream raises `refinery.lib.structures.EOF`. The module records that follow are read
by `module_records`, one at a time.
"""
def __init__(self, reader: StructReader[memoryview]):
information: dict[int, memoryview] = {}
while (record := _DirStreamRecord(reader)).id != DirRecord.PROJECTMODULES:
information[record.id] = record.body
self.module_count = record.value
self._module_data = reader.read()
def field(record: DirRecord) -> memoryview:
try:
return information[record]
except KeyError:
raise ValueError(F'The dir stream has no {record.name} record.') from None
self.syskind = int.from_bytes(field(DirRecord.PROJECTSYSKIND), 'little')
self.codepage = int.from_bytes(field(DirRecord.PROJECTCODEPAGE), 'little')
self.codec = _codepage_to_codec(self.codepage)
self.name = codecs.decode(field(DirRecord.PROJECTNAME), self.codec, 'replace')
@property
def is_64bit(self) -> bool:
return self.syskind == 3
def module_records(self) -> Iterator[VBAModuleRecord]:
"""
The module records of the stream, in their order, preceded by the project cookie record if
there is one. A record that cannot be read raises its error after the records in front of
it have been listed.
"""
reader = StructReader(self._module_data)
if reader.u16(peek=True) == DirRecord.PROJECTCOOKIE:
_DirStreamRecord(reader)
for _ in range(self.module_count):
yield VBAModuleRecord(reader, self.codec)
class VBAProject:
"""
A VBA project in a compound file of the input. The part is the name under which the input
stores the compound file, or `None` when the compound file is the input itself. The storage is
the path prefix of the project inside the compound file, which is empty for the root storage
and otherwise ends with a forward slash.
"""
def __init__(
self,
part: str | None,
ole: OleFileLike,
storage: str,
directory: VBADirectory,
):
self.part = part
self.ole = ole
self.storage = storage
self.directory = directory
@property
def dir_path(self) -> str:
return _dir_path(self.storage)
def modules(self) -> Iterator[VBAModule]:
"""
The modules of the project, in the order of the dir stream. A module record that cannot be
read raises its error after the modules in front of it have been listed.
"""
for record in self.directory.module_records():
yield VBAModule(self, record)
@property
def name(self) -> str:
return self.directory.name
@property
def codec(self) -> str:
return self.directory.codec
@property
def is_64bit(self) -> bool:
return self.directory.is_64bit
def location(self, path: str) -> str:
"""
The location of the stream with the given path in the compound file of this project: the
path, preceded by the part when there is one.
"""
return _location(self.part, path)
class VBAModule:
"""
A module of a `VBAProject`. The name of the module is the first name in the `stream_names` of
its record under which the project holds a stream, or the first of these names when there is
no such stream. The path and the location refer to the stream with that name.
"""
def __init__(self, project: VBAProject, record: VBAModuleRecord):
self.project = project
self.record = record
code = F'{project.storage}VBA/'
names = record.stream_names
stored = [name for name in names if project.ole.get_type(F'{code}{name}') == STGTY.STREAM]
self.name = (stored or names)[0]
self.path = F'{code}{self.name}'
self.location = project.location(self.path)
def read(self) -> memoryview:
"""
The content of the module stream.
"""
return self.project.ole.openstream(self.path).read()
def source(self) -> str:
"""
The decompressed source code of the module.
"""
if (offset := self.record.text_offset) is None:
raise ValueError('The module record has no offset of the source code.')
if offset >= len(data := self.read()):
raise ValueError(F'The source code offset {offset} lies outside the stream of {len(data)} bytes.')
return decompress_stream(data[offset:]).decode(self.project.codec, 'replace')
class VBACompoundFile:
"""
A compound file of the input and the VBA projects inside it whose dir stream can be read. The
part is the name under which the input stores the compound file, or `None` when the compound
file is the input itself.
"""
def __init__(self, part: str | None, ole: OleFileLike, projects: list[VBAProject]):
self.part = part
self.ole = ole
self.projects = projects
def location(self, path: str) -> str:
"""
The location of the stream or storage with the given path in this compound file: the path,
preceded by the part when there is one.
"""
return _location(self.part, path)
def vba_compound_files(data: buf, report: Reporter) -> Iterator[VBACompoundFile]:
"""
The compound files of the given input that `vba_containers` lists, read one at a time, each with
its VBA projects. An input that is no supported container has no compound files. A part that
cannot be opened as a compound file, and a project whose dir stream cannot be read or parsed,
are passed to the given function with their location and the error; the others are listed.
When the input itself is a compound file or an Access database that cannot be read, the error
is raised.
"""
try:
containers = vba_containers(data)
except FileOpenError:
return
for part, ole in _open_containers(containers, report):
projects: list[VBAProject] = []
for storage in _find_vba_projects(ole):
path = _dir_path(storage)
try:
directory = VBADirectory.Parse(memoryview(decompress_stream(ole.openstream(path).read())))
except Exception as error:
report(_location(part, path), error)
continue
projects.append(VBAProject(part, ole, storage, directory))
yield VBACompoundFile(part, ole, projects)
def vba_projects(data: buf, report: Reporter) -> Iterator[VBAProject]:
"""
The VBA projects of the compound files that `vba_compound_files` lists, in their order.
"""
for compound_file in vba_compound_files(data, report):
yield from compound_file.projects
def vba_form_strings(ole: OleFileLike, storage: str) -> list[bytes]:
"""
The runs of at least five printable bytes in the `o` stream of the form storage with the given
path, as stored, except the name of the default font Tahoma.
"""
data = ole.openstream(F'{storage}/o').read()
strings = (match.group() for match in _RE_PRINTABLE_STRING.finditer(data))
return [string for string in strings if string != B'Tahoma']
Global variables
var Reporter-
A function that receives the location of a part of the input that cannot be read, and the error that occurred while reading it.
Functions
def vba_form_storages(ole)-
The paths of all form storages in the given compound file, i.e. storages that hold a stream named
fand a stream namedo. The storage of a frame inside a form is a form storage below the storage of the form.Expand source code Browse git
def vba_form_storages(ole: OleFileLike) -> list[str]: """ The paths of all form storages in the given compound file, i.e. storages that hold a stream named `f` and a stream named `o`. The storage of a frame inside a form is a form storage below the storage of the form. """ forms: list[str] = [] for storage, entry in ole.walk(streams=False, storages=True): prefix = '/'.join(storage) if ole.find(prefix) is not entry: continue if ( ole.get_type(F'{prefix}/o') == STGTY.STREAM and ole.get_type(F'{prefix}/f') == STGTY.STREAM ): forms.append(prefix) return forms def copytoken_help(decompressed_current, decompressed_chunk_start)-
Compute bit masks to decode a CopyToken per MS-OVBA 2.4.1.3.19.1. Returns (length_mask, offset_mask, bit_count, maximum_length).
Expand source code Browse git
def copytoken_help( decompressed_current: int, decompressed_chunk_start: int, ) -> tuple[int, int, int, int]: """ Compute bit masks to decode a CopyToken per MS-OVBA 2.4.1.3.19.1. Returns (length_mask, offset_mask, bit_count, maximum_length). """ difference = decompressed_current - decompressed_chunk_start if difference < 2: bit_count = 4 else: bit_count = max(int(math.ceil(math.log(difference, 2))), 4) length_mask = 0xFFFF >> bit_count offset_mask = ~length_mask & 0xFFFF maximum_length = (0xFFFF >> bit_count) + 3 return length_mask, offset_mask, bit_count, maximum_length def decompress_stream(data)-
Decompress a VBA compressed stream per MS-OVBA section 2.4.1.
The compressed container starts with a signature byte (0x01), followed by compressed chunks. Each chunk has a 2-byte header encoding size, signature (0b011), and a flag indicating whether the chunk is compressed or raw.
Expand source code Browse git
def decompress_stream(data: bytes | bytearray | memoryview) -> bytearray: """ Decompress a VBA compressed stream per MS-OVBA section 2.4.1. The compressed container starts with a signature byte (0x01), followed by compressed chunks. Each chunk has a 2-byte header encoding size, signature (0b011), and a flag indicating whether the chunk is compressed or raw. """ view = memoryview(data) if len(view) < 1: raise ValueError('empty compressed container') if view[0] != 0x01: raise ValueError(F'invalid signature byte 0x{view[0]:02X}, expected 1') decompressed = bytearray() pos = 1 while pos < len(view): if pos + 2 > len(view): break chunk_header = struct.unpack_from('<H', view, pos)[0] chunk_size = (chunk_header & 0x0FFF) + 3 chunk_signature = (chunk_header >> 12) & 0x07 chunk_flag = (chunk_header >> 15) & 0x01 if chunk_signature != 0b011: raise ValueError('Invalid CompressedChunkSignature in VBA stream') if chunk_flag == 1 and chunk_size > 4098: raise ValueError(F'CompressedChunkSize {chunk_size} > 4098 but CompressedChunkFlag == 1') if chunk_flag == 0 and chunk_size != 4098: raise ValueError(F'CompressedChunkSize {chunk_size} != 4098 but CompressedChunkFlag == 0') compressed_end = min(len(view), pos + chunk_size) compressed_current = pos + 2 if chunk_flag == 0: decompressed.extend(view[compressed_current:compressed_current + 4096]) compressed_current += 4096 else: decompressed_chunk_start = len(decompressed) while compressed_current < compressed_end: if compressed_current >= len(view): break flag_byte = view[compressed_current] compressed_current += 1 for bit_index in range(8): if compressed_current >= compressed_end: break flag_bit = (flag_byte >> bit_index) & 1 if flag_bit == 0: decompressed.append(view[compressed_current]) compressed_current += 1 else: if compressed_current + 2 > len(view): break copy_token = struct.unpack_from( '<H', view, compressed_current)[0] length_mask, offset_mask, bit_count, _ = ( copytoken_help( len(decompressed), decompressed_chunk_start)) length = (copy_token & length_mask) + 3 temp1 = copy_token & offset_mask temp2 = 16 - bit_count offset = (temp1 >> temp2) + 1 copy_source = len(decompressed) - offset for index in range( copy_source, copy_source + length ): decompressed.append(decompressed[index]) compressed_current += 2 pos = compressed_end return decompressed def is_vba_container(data)-
A quick check whether the given data looks like one of the containers that
vba_containers()reads: a compound file, a Microsoft Access database, an OpenXML document, a Word 2003 XML or Flat OPC document, or an MHTML file.Expand source code Browse git
def is_vba_container(data: buf) -> bool: """ A quick check whether the given data looks like one of the containers that `vba_containers` reads: a compound file, a Microsoft Access database, an OpenXML document, a Word 2003 XML or Flat OPC document, or an MHTML file. """ view = memoryview(data) if view[:8] == OLE_MAGIC or is_access_database(view): return True if view[:2] == B'PK': return buffer_contains(view, B'[Content_Types].xml') if buffer_contains(view, _NAMESPACE_WORD2003.encode()): return True if buffer_contains(view, _NAMESPACE_FLATOPC.encode()): return True return _is_mhtml(view) def vba_containers(data)-
The compound files of the given input that may hold VBA projects, in the order in which the input stores them. The input is a compound file, an OpenXML document or any other zip archive, a Word 2003 XML or Flat OPC document, an MHTML file, or a Microsoft Access database. The parts of these containers are found without reading them in full; a part that cannot be read is listed, so that the error of opening it can be reported. Raises
FileOpenErrorwhen the input is none of these containers or has no part that may be a compound file. The error of reading the VBA storage of an Access database is raised.Expand source code Browse git
def vba_containers(data: buf) -> list[VBAContainer]: """ The compound files of the given input that may hold VBA projects, in the order in which the input stores them. The input is a compound file, an OpenXML document or any other zip archive, a Word 2003 XML or Flat OPC document, an MHTML file, or a Microsoft Access database. The parts of these containers are found without reading them in full; a part that cannot be read is listed, so that the error of opening it can be reported. Raises `FileOpenError` when the input is none of these containers or has no part that may be a compound file. The error of reading the VBA storage of an Access database is raised. """ view = memoryview(data) for find_containers in ( _ole_containers, _access_containers, _zip_containers, _word2003_containers, _flatopc_containers, _mhtml_containers, ): if containers := find_containers(view): return containers raise FileOpenError('data is not a supported file type for VBA extraction') def vba_compound_files(data, report)-
The compound files of the given input that
vba_containers()lists, read one at a time, each with its VBA projects. An input that is no supported container has no compound files. A part that cannot be opened as a compound file, and a project whose dir stream cannot be read or parsed, are passed to the given function with their location and the error; the others are listed. When the input itself is a compound file or an Access database that cannot be read, the error is raised.Expand source code Browse git
def vba_compound_files(data: buf, report: Reporter) -> Iterator[VBACompoundFile]: """ The compound files of the given input that `vba_containers` lists, read one at a time, each with its VBA projects. An input that is no supported container has no compound files. A part that cannot be opened as a compound file, and a project whose dir stream cannot be read or parsed, are passed to the given function with their location and the error; the others are listed. When the input itself is a compound file or an Access database that cannot be read, the error is raised. """ try: containers = vba_containers(data) except FileOpenError: return for part, ole in _open_containers(containers, report): projects: list[VBAProject] = [] for storage in _find_vba_projects(ole): path = _dir_path(storage) try: directory = VBADirectory.Parse(memoryview(decompress_stream(ole.openstream(path).read()))) except Exception as error: report(_location(part, path), error) continue projects.append(VBAProject(part, ole, storage, directory)) yield VBACompoundFile(part, ole, projects) def vba_projects(data, report)-
The VBA projects of the compound files that
vba_compound_files()lists, in their order.Expand source code Browse git
def vba_projects(data: buf, report: Reporter) -> Iterator[VBAProject]: """ The VBA projects of the compound files that `vba_compound_files` lists, in their order. """ for compound_file in vba_compound_files(data, report): yield from compound_file.projects def vba_form_strings(ole, storage)-
The runs of at least five printable bytes in the
ostream of the form storage with the given path, as stored, except the name of the default font Tahoma.Expand source code Browse git
def vba_form_strings(ole: OleFileLike, storage: str) -> list[bytes]: """ The runs of at least five printable bytes in the `o` stream of the form storage with the given path, as stored, except the name of the default font Tahoma. """ data = ole.openstream(F'{storage}/o').read() strings = (match.group() for match in _RE_PRINTABLE_STRING.finditer(data)) return [string for string in strings if string != B'Tahoma']
Classes
class FileOpenError (message='failed to open file')-
Raised when data cannot be recognized as a supported file format.
Expand source code Browse git
class FileOpenError(Exception): """ Raised when data cannot be recognized as a supported file format. """ def __init__(self, message: str = 'failed to open file'): super().__init__(message)Ancestors
- builtins.Exception
- builtins.BaseException
class VBAContainer (part, open)-
A compound file of an input, which may hold VBA projects. The part is the name under which the input stores the compound file, or
Nonewhen the compound file is the input itself. The part of a zip member has forward slashes in place of backslashes on every platform. The compound file is read only whenopenis called, which raises the error of the read when the part cannot be read or is no compound file.Expand source code Browse git
class VBAContainer(NamedTuple): """ A compound file of an input, which may hold VBA projects. The part is the name under which the input stores the compound file, or `None` when the compound file is the input itself. The part of a zip member has forward slashes in place of backslashes on every platform. The compound file is read only when `open` is called, which raises the error of the read when the part cannot be read or is no compound file. """ part: str | None open: Callable[[], OleFileLike]Ancestors
- builtins.tuple
Instance variables
var part-
Alias for field number 0
Expand source code Browse git
class VBAContainer(NamedTuple): """ A compound file of an input, which may hold VBA projects. The part is the name under which the input stores the compound file, or `None` when the compound file is the input itself. The part of a zip member has forward slashes in place of backslashes on every platform. The compound file is read only when `open` is called, which raises the error of the read when the part cannot be read or is no compound file. """ part: str | None open: Callable[[], OleFileLike] var open-
Alias for field number 1
Expand source code Browse git
class VBAContainer(NamedTuple): """ A compound file of an input, which may hold VBA projects. The part is the name under which the input stores the compound file, or `None` when the compound file is the input itself. The part of a zip member has forward slashes in place of backslashes on every platform. The compound file is read only when `open` is called, which raises the error of the read when the part cannot be read or is no compound file. """ part: str | None open: Callable[[], OleFileLike]
class DirRecord (*args, **kwds)-
The ids of the records of a dir stream that are read for their content or that have a body of fixed size (MS-OVBA 2.3.4.2).
Expand source code Browse git
class DirRecord(enum.IntEnum): """ The ids of the records of a dir stream that are read for their content or that have a body of fixed size (MS-OVBA 2.3.4.2). """ PROJECTSYSKIND = 0x0001 PROJECTLCID = 0x0002 PROJECTCODEPAGE = 0x0003 PROJECTNAME = 0x0004 PROJECTHELPCONTEXT = 0x0007 PROJECTLIBFLAGS = 0x0008 PROJECTVERSION = 0x0009 PROJECTMODULES = 0x000F PROJECTCOOKIE = 0x0013 PROJECTLCIDINVOKE = 0x0014 MODULENAME = 0x0019 MODULESTREAMNAME = 0x001A MODULEHELPCONTEXT = 0x001E MODULETYPEPROCEDURAL = 0x0021 MODULETYPEDOCUMENT = 0x0022 MODULEREADONLY = 0x0025 MODULEPRIVATE = 0x0028 MODULETERMINATOR = 0x002B MODULECOOKIE = 0x002C MODULEOFFSET = 0x0031 MODULESTREAMNAMEUNICODE = 0x0032 MODULENAMEUNICODE = 0x0047 PROJECTCOMPATVERSION = 0x004AAncestors
- enum.IntEnum
- builtins.int
- enum.ReprEnum
- enum.Enum
Class variables
var PROJECTSYSKIND-
The type of the None singleton.
var PROJECTLCID-
The type of the None singleton.
var PROJECTCODEPAGE-
The type of the None singleton.
var PROJECTNAME-
The type of the None singleton.
var PROJECTHELPCONTEXT-
The type of the None singleton.
var PROJECTLIBFLAGS-
The type of the None singleton.
var PROJECTVERSION-
The type of the None singleton.
var PROJECTMODULES-
The type of the None singleton.
var PROJECTCOOKIE-
The type of the None singleton.
var PROJECTLCIDINVOKE-
The type of the None singleton.
var MODULENAME-
The type of the None singleton.
var MODULESTREAMNAME-
The type of the None singleton.
var MODULEHELPCONTEXT-
The type of the None singleton.
var MODULETYPEPROCEDURAL-
The type of the None singleton.
var MODULETYPEDOCUMENT-
The type of the None singleton.
var MODULEREADONLY-
The type of the None singleton.
var MODULEPRIVATE-
The type of the None singleton.
var MODULETERMINATOR-
The type of the None singleton.
var MODULECOOKIE-
The type of the None singleton.
var MODULEOFFSET-
The type of the None singleton.
var MODULESTREAMNAMEUNICODE-
The type of the None singleton.
var MODULENAMEUNICODE-
The type of the None singleton.
var PROJECTCOMPATVERSION-
The type of the None singleton.
class VBAModuleRecord (reader, codec)-
The MODULE record of a dir stream (MS-OVBA 2.3.4.2.3.2): the name of a module, the name of its stream, both also in UTF-16 if present, and the offset of the compressed source code in the stream. The record begins with the name of the module and ends with a terminator; records between them that are not listed here are skipped.
Expand source code Browse git
class VBAModuleRecord(Struct[memoryview]): """ The MODULE record of a dir stream (MS-OVBA 2.3.4.2.3.2): the name of a module, the name of its stream, both also in UTF-16 if present, and the offset of the compressed source code in the stream. The record begins with the name of the module and ends with a terminator; records between them that are not listed here are skipped. """ def __init__(self, reader: StructReader[memoryview], codec: str): if (record := _DirStreamRecord(reader)).id != DirRecord.MODULENAME: raise ValueError(F'Expected a module record, found a record with id 0x{record.id:04X}.') self.name = codecs.decode(record.body, codec, 'replace') self.name_unicode: str | None = None self.stream_name: str | None = None self.stream_name_unicode: str | None = None self.text_offset: int | None = None while (record := _DirStreamRecord(reader)).id != DirRecord.MODULETERMINATOR: if record.id == DirRecord.MODULENAMEUNICODE: self.name_unicode = codecs.decode(record.body, 'utf-16le', 'replace') elif record.id == DirRecord.MODULESTREAMNAME: self.stream_name = codecs.decode(record.body, codec, 'replace') elif record.id == DirRecord.MODULESTREAMNAMEUNICODE: self.stream_name_unicode = codecs.decode(record.body, 'utf-16le', 'replace') elif record.id == DirRecord.MODULEOFFSET: self.text_offset = record.value @property def stream_names(self) -> list[str]: """ The names under which the stream of the module may be stored, in the order in which they are tried: the names of the stream, then the names of the module. """ names = (self.stream_name, self.stream_name_unicode, self.name, self.name_unicode) return list(dict.fromkeys(name for name in names if name is not None))Ancestors
- Struct
- typing.Generic
- collections.abc.Buffer
Static methods
def Parse(reader, *args, **kwargs)
Instance variables
var stream_names-
The names under which the stream of the module may be stored, in the order in which they are tried: the names of the stream, then the names of the module.
Expand source code Browse git
@property def stream_names(self) -> list[str]: """ The names under which the stream of the module may be stored, in the order in which they are tried: the names of the stream, then the names of the module. """ names = (self.stream_name, self.stream_name_unicode, self.name, self.name_unicode) return list(dict.fromkeys(name for name in names if name is not None))
class VBADirectory (reader)-
The decompressed dir stream of a VBA project (MS-OVBA 2.3.4.2): the system kind, the code page and the name of the project as stored, and the number of its modules. The stream is a sequence of records; the records in front of the list of modules, which include the references, are read in any order. Every record is read with its exact length, so that a record which runs past the end of the stream raises
EOF. The module records that follow are read bymodule_records, one at a time.Expand source code Browse git
class VBADirectory(Struct[memoryview]): """ The decompressed dir stream of a VBA project (MS-OVBA 2.3.4.2): the system kind, the code page and the name of the project as stored, and the number of its modules. The stream is a sequence of records; the records in front of the list of modules, which include the references, are read in any order. Every record is read with its exact length, so that a record which runs past the end of the stream raises `refinery.lib.structures.EOF`. The module records that follow are read by `module_records`, one at a time. """ def __init__(self, reader: StructReader[memoryview]): information: dict[int, memoryview] = {} while (record := _DirStreamRecord(reader)).id != DirRecord.PROJECTMODULES: information[record.id] = record.body self.module_count = record.value self._module_data = reader.read() def field(record: DirRecord) -> memoryview: try: return information[record] except KeyError: raise ValueError(F'The dir stream has no {record.name} record.') from None self.syskind = int.from_bytes(field(DirRecord.PROJECTSYSKIND), 'little') self.codepage = int.from_bytes(field(DirRecord.PROJECTCODEPAGE), 'little') self.codec = _codepage_to_codec(self.codepage) self.name = codecs.decode(field(DirRecord.PROJECTNAME), self.codec, 'replace') @property def is_64bit(self) -> bool: return self.syskind == 3 def module_records(self) -> Iterator[VBAModuleRecord]: """ The module records of the stream, in their order, preceded by the project cookie record if there is one. A record that cannot be read raises its error after the records in front of it have been listed. """ reader = StructReader(self._module_data) if reader.u16(peek=True) == DirRecord.PROJECTCOOKIE: _DirStreamRecord(reader) for _ in range(self.module_count): yield VBAModuleRecord(reader, self.codec)Ancestors
- Struct
- typing.Generic
- collections.abc.Buffer
Static methods
def Parse(reader, *args, **kwargs)
Instance variables
var is_64bit-
Expand source code Browse git
@property def is_64bit(self) -> bool: return self.syskind == 3
Methods
def module_records(self)-
The module records of the stream, in their order, preceded by the project cookie record if there is one. A record that cannot be read raises its error after the records in front of it have been listed.
Expand source code Browse git
def module_records(self) -> Iterator[VBAModuleRecord]: """ The module records of the stream, in their order, preceded by the project cookie record if there is one. A record that cannot be read raises its error after the records in front of it have been listed. """ reader = StructReader(self._module_data) if reader.u16(peek=True) == DirRecord.PROJECTCOOKIE: _DirStreamRecord(reader) for _ in range(self.module_count): yield VBAModuleRecord(reader, self.codec)
class VBAProject (part, ole, storage, directory)-
A VBA project in a compound file of the input. The part is the name under which the input stores the compound file, or
Nonewhen the compound file is the input itself. The storage is the path prefix of the project inside the compound file, which is empty for the root storage and otherwise ends with a forward slash.Expand source code Browse git
class VBAProject: """ A VBA project in a compound file of the input. The part is the name under which the input stores the compound file, or `None` when the compound file is the input itself. The storage is the path prefix of the project inside the compound file, which is empty for the root storage and otherwise ends with a forward slash. """ def __init__( self, part: str | None, ole: OleFileLike, storage: str, directory: VBADirectory, ): self.part = part self.ole = ole self.storage = storage self.directory = directory @property def dir_path(self) -> str: return _dir_path(self.storage) def modules(self) -> Iterator[VBAModule]: """ The modules of the project, in the order of the dir stream. A module record that cannot be read raises its error after the modules in front of it have been listed. """ for record in self.directory.module_records(): yield VBAModule(self, record) @property def name(self) -> str: return self.directory.name @property def codec(self) -> str: return self.directory.codec @property def is_64bit(self) -> bool: return self.directory.is_64bit def location(self, path: str) -> str: """ The location of the stream with the given path in the compound file of this project: the path, preceded by the part when there is one. """ return _location(self.part, path)Instance variables
var dir_path-
Expand source code Browse git
@property def dir_path(self) -> str: return _dir_path(self.storage) var name-
Expand source code Browse git
@property def name(self) -> str: return self.directory.name var codec-
Expand source code Browse git
@property def codec(self) -> str: return self.directory.codec var is_64bit-
Expand source code Browse git
@property def is_64bit(self) -> bool: return self.directory.is_64bit
Methods
def modules(self)-
The modules of the project, in the order of the dir stream. A module record that cannot be read raises its error after the modules in front of it have been listed.
Expand source code Browse git
def modules(self) -> Iterator[VBAModule]: """ The modules of the project, in the order of the dir stream. A module record that cannot be read raises its error after the modules in front of it have been listed. """ for record in self.directory.module_records(): yield VBAModule(self, record) def location(self, path)-
The location of the stream with the given path in the compound file of this project: the path, preceded by the part when there is one.
Expand source code Browse git
def location(self, path: str) -> str: """ The location of the stream with the given path in the compound file of this project: the path, preceded by the part when there is one. """ return _location(self.part, path)
class VBAModule (project, record)-
A module of a
VBAProject. The name of the module is the first name in thestream_namesof its record under which the project holds a stream, or the first of these names when there is no such stream. The path and the location refer to the stream with that name.Expand source code Browse git
class VBAModule: """ A module of a `VBAProject`. The name of the module is the first name in the `stream_names` of its record under which the project holds a stream, or the first of these names when there is no such stream. The path and the location refer to the stream with that name. """ def __init__(self, project: VBAProject, record: VBAModuleRecord): self.project = project self.record = record code = F'{project.storage}VBA/' names = record.stream_names stored = [name for name in names if project.ole.get_type(F'{code}{name}') == STGTY.STREAM] self.name = (stored or names)[0] self.path = F'{code}{self.name}' self.location = project.location(self.path) def read(self) -> memoryview: """ The content of the module stream. """ return self.project.ole.openstream(self.path).read() def source(self) -> str: """ The decompressed source code of the module. """ if (offset := self.record.text_offset) is None: raise ValueError('The module record has no offset of the source code.') if offset >= len(data := self.read()): raise ValueError(F'The source code offset {offset} lies outside the stream of {len(data)} bytes.') return decompress_stream(data[offset:]).decode(self.project.codec, 'replace')Methods
def read(self)-
The content of the module stream.
Expand source code Browse git
def read(self) -> memoryview: """ The content of the module stream. """ return self.project.ole.openstream(self.path).read() def source(self)-
The decompressed source code of the module.
Expand source code Browse git
def source(self) -> str: """ The decompressed source code of the module. """ if (offset := self.record.text_offset) is None: raise ValueError('The module record has no offset of the source code.') if offset >= len(data := self.read()): raise ValueError(F'The source code offset {offset} lies outside the stream of {len(data)} bytes.') return decompress_stream(data[offset:]).decode(self.project.codec, 'replace')
class VBACompoundFile (part, ole, projects)-
A compound file of the input and the VBA projects inside it whose dir stream can be read. The part is the name under which the input stores the compound file, or
Nonewhen the compound file is the input itself.Expand source code Browse git
class VBACompoundFile: """ A compound file of the input and the VBA projects inside it whose dir stream can be read. The part is the name under which the input stores the compound file, or `None` when the compound file is the input itself. """ def __init__(self, part: str | None, ole: OleFileLike, projects: list[VBAProject]): self.part = part self.ole = ole self.projects = projects def location(self, path: str) -> str: """ The location of the stream or storage with the given path in this compound file: the path, preceded by the part when there is one. """ return _location(self.part, path)Methods
def location(self, path)-
The location of the stream or storage with the given path in this compound file: the path, preceded by the part when there is one.
Expand source code Browse git
def location(self, path: str) -> str: """ The location of the stream or storage with the given path in this compound file: the path, preceded by the part when there is one. """ return _location(self.part, path)