diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index f68e4d390..d31e50507 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v3 @@ -26,12 +26,9 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - if [ -f requirements_dev.txt ]; then pip install -r requirements_dev.txt; fi - pip install git+https://github.com/Beakerboy/MS-CFB@dev - pip install git+https://github.com/Beakerboy/MS-Pcode-Assembler@dev - pip install -e . + pip install git+https://github.com/Beakerboy/MS-CFB + pip install git+https://github.com/Beakerboy/MS-Pcode-Assembler + pip install -e .[tests] - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names @@ -42,15 +39,16 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - pytest --cov=vbaProjectCompiler + pytest --cov=src coveralls --service=github + ls -al flake8: runs-on: ubuntu-latest strategy: fail-fast: false matrix: - python-version: ["3.10"] + python-version: ["3.14"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} diff --git a/README.md b/README.md index 9d6080ed1..33b32249e 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@ -[![Coverage Status](https://coveralls.io/repos/github/Beakerboy/vbaProject-Compiler/badge.svg?branch=main)](https://coveralls.io/github/Beakerboy/vbaProject-Compiler?branch=main) +[![Coverage Status](https://coveralls.io/repos/github/Beakerboy/MS-OVBA/badge.svg?branch=main)](https://coveralls.io/github/Beakerboy/MS-OVBA?branch=main) # vbaProject-Compiler Create a vbaProject.bin file from VBA source files. -## VBAProject Class +## Command Line Interface +Eventually it will be possible to use the CLI to create a vbaProject.bin file from source files and an optional configuration file. -The vbaProject class contains all the data and metadata that will be used to create the OLE container. +## VBAProject Class +The vbaProject class contains all the data and metadata that is used to create the OLE container. It can use this data to create several files, then compress and combine them into an OLE container ```python -from vbaProjectCompiler.vbaProject import VbaProject -from vbaProjectCompiler.ole_file import OleFile +from ms_ovba.vbaProject import VbaProject +from ms_cfb.ole_file import OleFile project = VbaProject() @@ -17,40 +19,21 @@ thisWorkbook = DocModule("ThisWorkbook") thisWorkbook.addFile(path) project.addModule(thisWorkbook) -ole_file = OleFile(project) -ole_file.writeFile(".") +ProjectOleFile.write_file(project) ``` -The VbaProject class has many layers of customization available. Forexample a librry referenece can be added to the project. +The VbaProject class has many layers of customization available. For example a library reference can be added to the project. ```python codePage = 0x04E4 codePageName = "cp" + str(codePage) -libidRef = LibidReference( - "windows", +libidRef = ReferenceRecord(codePageName, LibidReference( "{00020430-0000-0000-C000-000000000046}", "2.0", "0", "C:\\Windows\\System32\\stdole2.tlb", "OLE Automation" -) -oleReference = ReferenceRecord(codePageName, "stdole", libidRef) +)) +oleReference = Reference(codePageName, libidRef, "stdole") project.addReference(oleReference) ``` - -## oleFile Class - -Users should not have to interact with the oleFile class. It's job is to extract the data from the vbaProject and turn it into a valid file. This includes deciding which data stream appears where, and applying different views to the models to save the data in the correct formats. - -The oleFIle has two parts, a header and a FAT Sector Chain. This FAT chain stores multiple streams of data: -* Fat Chain Stream -* Directory Stream -* Minifat Chain Stream -* Minifat Data Stream -* Fat Data Stream - -These are all different views of data from the following Models - -* fatChain -* minifatChain -* directoryStream diff --git a/pyproject.toml b/pyproject.toml index 8dd59b357..ebefd1570 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "vba_project_compiler" +name = "vbaproject_compiler" version = "0.0.1" authors = [ { name="Kevin Nowaczyk", email="beakerboy99@yahoo.com" }, @@ -16,7 +16,19 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] - +dependencies = [ + 'ms_ovba_compression', + 'ms_ovba_crypto' +] +[project.optional-dependencies] +tests = [ + 'pytest', + 'pytest-cov', + 'pytest-mock', + 'coveralls', + 'pep8-naming', + 'flake8-annotations' +] [project.urls] "Homepage" = "https://github.com/Beakerboy/vbaProject-Compiler" "Bug Tracker" = "https://github.com/Beakerboy/vbaProject-Compiler/issues" @@ -26,3 +38,5 @@ pythonpath = "src" testpaths = [ "tests", ] +[tool.setuptools.package-data] +"*" = ["*.cls"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3d80e0cd7..000000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -ms_ovba_compression -ms_ovba_crypto diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index 30c969c3a..000000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,4 +0,0 @@ -pytest -pytest-cov -pytest-mock -coveralls diff --git a/setup.py b/setup.py deleted file mode 100644 index dd30318df..000000000 --- a/setup.py +++ /dev/null @@ -1,7 +0,0 @@ -from setuptools import setup - -setup( - name="vbaProjectCompiler", - packages=['vbaProjectCompiler'], - tests_require=['pytest'], -) diff --git a/vbaProjectCompiler/Models/Entities/doc_module.py b/src/ms_ovba/Models/Entities/doc_module.py similarity index 73% rename from vbaProjectCompiler/Models/Entities/doc_module.py rename to src/ms_ovba/Models/Entities/doc_module.py index 9538e5461..29f90073a 100644 --- a/vbaProjectCompiler/Models/Entities/doc_module.py +++ b/src/ms_ovba/Models/Entities/doc_module.py @@ -1,5 +1,9 @@ from ms_ovba_compression.ms_ovba import MsOvba -from vbaProjectCompiler.Models.Entities.module_base import ModuleBase +from ms_ovba.Models.Entities.module_base import ModuleBase +from typing import TypeVar + + +T = TypeVar('T', bound='DocModule') class DocModule(ModuleBase): @@ -7,28 +11,19 @@ class DocModule(ModuleBase): A Document Module is a module record that is associated with a worksheet or workbook. """ - def __init__(self, name): - self.docTlibVer = 0 + def __init__(self: T, name: str) -> None: + self.doc_tlib_ver = 0 super(DocModule, self).__init__(name) self.type = "Document" # GUIDs self._guid = [] - def toProjectModuleString(self): + def to_project_module_string(self: T) -> str: return ("Document=" + self.modName.value + "/&H" - + self.docTlibVer.to_bytes(4, "big").hex()) - - def set_guid(self, guid): - if isinstance(guid, list): - self._guid = guid - else: - self._guid = [guid] - - def add_guid(self, guid): - self._guid += guid + + self.doc_tlib_ver.to_bytes(4, "big").hex()) - def normalize_file(self): + def normalize_file(self: T) -> None: f = open(self._file_path, "r") new_f = open(self._file_path + ".new", "a+", newline='\r\n') for i in range(5): @@ -46,7 +41,7 @@ def normalize_file(self): new_f.writelines([self._attr("Customizable", "True")]) new_f.close() - def write_file(self): + def write_file(self: T) -> None: bin_f = open(self._file_path + ".bin", "wb") bin_f.write(self._cache) with open(self._file_path + ".new", mode="rb") as new_f: diff --git a/src/ms_ovba/Models/Entities/license_info.py b/src/ms_ovba/Models/Entities/license_info.py new file mode 100644 index 000000000..6208eaef2 --- /dev/null +++ b/src/ms_ovba/Models/Entities/license_info.py @@ -0,0 +1,24 @@ +import struct +import uuid +from typing import TypeVar + + +T = TypeVar('T', bound='LicenseInfo') + + +class LicenseInfo: + def __init__(self: T, guid: uuid.UUID, key: bytes) -> None: + self._guid = guid + self._key = key + self._required = len(key) > 0 + + def not_required(self: T) -> None: + self._required = False + + def to_bytes(self: T) -> bytes: + return ( + self._guid.bytes + + struct.pack(" None: + """ + Initialize the module record + """ + self.modName = DoubleEncodedString([0x0019, 0x0047], name) + self.streamName = DoubleEncodedString([0x001A, 0x0032], name) + self.docString = DoubleEncodedString([0x001C, 0x0048], "") + self.helpContext = IdSizeField(0x001E, 4, 0) + self._cookie = IdSizeField(0x002C, 2, 0xFFFF) + + # self.readonly = SimpleRecord(0x001E, 4, helpContext) + # self.private = SimpleRecord(0x001E, 4, helpContext) + self._cache = b'' + self.workspace = [0, 0, 0, 0, 'C'] + self.type = '' + self.created = 0 + self.modified = 0 + self._fileSize = 0 + self._size = 0 + + # GUIDs + self._guids = [] + + @property + def guids(self: T) -> str: + return self._guids + + @guids.setter + def guids(self: T, guid: str) -> None: + if isinstance(guid, list): + self._guids = guid + else: + self._guids = [guid] + + def add_guid(self: T, guid: str) -> None: + """ + Append a guid to the list + """ + self._guid += [guid] + + @property + def cache(self: T) -> bytes: + return self._cache + + @cache.setter + def cache(self: T, cache: bytes) -> None: + self._cache = cache + + @property + def cookie(self: T) -> int: + return self._cookie.value + + @cookie.setter + def cookie(self: T, value: int) -> None: + self._cookie = IdSizeField(0x002C, 2, value) + + @property + def name(self: T) -> str: + return self.modName.value + + @property + def bin_path(self: T) -> str: + return self._file_path + ".bin" + + def add_workspace(self: T, val1: int, val2: int, + val3: int, val4: int, val5: int) -> None: + self.workspace = [val1, val2, val3, val4, val5] + + def pack(self: T, endien: str, cp_name: str) -> bytes: + """ + Pack the metadata for use in the dir stream. + """ + typeid_value = 0x0022 if self.type == 'Document' else 0x0021 + type_id = PackedData("HI", typeid_value, 0) + self.offsetRec = IdSizeField(0x0031, 4, len(self._cache)) + output = (self.modName.pack(endien, cp_name) + + self.streamName.pack(endien, cp_name) + + self.docString.pack(endien, cp_name) + + self.offsetRec.pack(endien) + + self.helpContext.pack(endien, cp_name) + + self._cookie.pack(endien) + + type_id.pack(endien)) + footer = PackedData("HI", 0x002B, 0) + output += footer.pack(endien) + return output + + def to_project_module_string(self: T) -> str: + return self.type + "=" + self.modName.value + + def add_file(self: T, file_path: str) -> None: + self._file_path = file_path + + def write_file(self: T) -> None: + bin_f = open(self._file_path + ".bin", "wb") + bin_f.write(self._cache) + with open(self._file_path + ".new", mode="rb") as new_f: + contents = new_f.read() + ms_ovba = MsOvba() + compressed = ms_ovba.compress(contents) + bin_f.write(compressed) + bin_f.close() + + def _attr(self: T, name: str, value: str) -> str: + return 'Attribute VB_' + name + ' = ' + value + '\n' diff --git a/src/ms_ovba/Models/Entities/reference.py b/src/ms_ovba/Models/Entities/reference.py new file mode 100644 index 000000000..a9b9d8368 --- /dev/null +++ b/src/ms_ovba/Models/Entities/reference.py @@ -0,0 +1,55 @@ +import struct +from ms_ovba.Models.Entities.reference_record import ReferenceRecord +from ms_ovba.Models.Fields.doubleEncodedString import ( + DoubleEncodedString +) +from typing import TypeVar + + +T = TypeVar('T', bound='Reference') + + +class Reference(): + """ + 2.3.4.2.2.1 REFERENCE Record + """ + def __init__(self: T, ref: ReferenceRecord, + name: str = None) -> None: + self._ref = ref + self._refname = name + + def pack(self: T, endien: str, cp_name: str) -> bytes: + name_pack = b'' + if self._refname is not None: + name_de = DoubleEncodedString([0x0016, 0x003E], self._refname) + name_pack = name_de.pack(endien, cp_name) + + return name_pack + self._ref.pack(endien, cp_name) + + @staticmethod + def unpack(data: bytes, endien: str) -> T: + endien_symbol = '<' if endien == 'little' else '>' + name = None + offset = 0 + id = struct.unpack_from(endien_symbol + "H", data, offset) + if id == 0x0016: + offset += 2 + size1 = struct.unpack_from(endien_symbol + "I", data, offset) + offset += 4 + format = endien_symbol + size1 + "s" + name = struct.unpack_from(format, data, offset) + offset += size1 + size2 = struct.unpack_from(endien_symbol + "I", data, offset) + offset += 4 + if size2 != size1 * 2: + # raise warning + pass + # format = endien_symbol + size2 + "s" + # name2 = struct.unpack_from(format, data, offset) + offset += size2 + # if name2 != unicode version on name1: + # raise warning + + bytestring = data[offset:] + ref = ReferenceRecord.unpack(bytestring, endien) + return Reference("cp", ref, name) diff --git a/src/ms_ovba/Models/Entities/reference_project.py b/src/ms_ovba/Models/Entities/reference_project.py new file mode 100644 index 000000000..202656fc8 --- /dev/null +++ b/src/ms_ovba/Models/Entities/reference_project.py @@ -0,0 +1,27 @@ +import struct +from ms_ovba.Models.Entities.reference_record import ReferenceRecord +from ms_ovba.Models.Fields.project_reference import ProjectReference +from typing import TypeVar + + +T = TypeVar('T', bound='ReferenceProject') + + +class ReferenceProject(ReferenceRecord): + + def __init__(self: T, ref: ProjectReference) -> None: + self._ref = ref + + def pack(self: T, endien: str, cp_name: str) -> bytes: + endien_symbol = '<' if endien == 'little' else '>' + lib_rel = self._ref.relative() + libid_abs_size = len(self._ref) + libid_rel_size = len(lib_rel) + format = (endien_symbol + "HII" + str(libid_abs_size) + "sI" + + str(libid_rel_size) + "sIH") + ref_str = str(self._ref).encode(cp_name) + ref_str_rel = str(lib_rel).encode(cp_name) + return struct.pack(format, 0x000E, + libid_abs_size + libid_rel_size + 14, + libid_abs_size, ref_str, libid_rel_size, + ref_str_rel, 0x65BE0257, 0x0017) diff --git a/src/ms_ovba/Models/Entities/reference_record.py b/src/ms_ovba/Models/Entities/reference_record.py new file mode 100644 index 000000000..6cb14819b --- /dev/null +++ b/src/ms_ovba/Models/Entities/reference_record.py @@ -0,0 +1,31 @@ +import struct +from typing import TypeVar + + +T = TypeVar('T', bound='ReferenceRecord') + + +class ReferenceRecord: + + @staticmethod + def unpack(bytestring: bytes, endien: str) -> T: + from ms_ovba.Models.Entities.reference_project import ReferenceProject + from ms_ovba.Models.Entities.reference_registered import ( + ReferenceRegistered + ) + endien_symbol = '<' if endien == 'little' else '>' + id = struct.unpack(endien_symbol + "H", bytestring) + if id == 0x000D: + ref = ReferenceRegistered.unpack(bytestring, endien) + elif id == 0x000E: + ref = ReferenceProject.unpack(bytestring, endien) + elif id == 0x002F: + # ref = ReferenceControl.unpack(bytestring, endien) + pass + elif id == 0x0033: + # ref = ReferenceOriginal.unpack(bytestring, endien) + pass + else: + # raise warning + return None + return ref diff --git a/src/ms_ovba/Models/Entities/reference_registered.py b/src/ms_ovba/Models/Entities/reference_registered.py new file mode 100644 index 000000000..b72508303 --- /dev/null +++ b/src/ms_ovba/Models/Entities/reference_registered.py @@ -0,0 +1,64 @@ +import struct +from ms_ovba.Models.Entities.reference_record import ReferenceRecord +from ms_ovba.Models.Fields.libid_reference import LibidReference +from typing import TypeVar + + +T = TypeVar('T', bound='ReferenceRegistered') + + +class ReferenceRegistered(ReferenceRecord): + """ + 2.3.4.2.2.5 + Specifies a reference to an Automation type library. + """ + def __init__(self: T, libid_ref: LibidReference) -> None: + self._libid_ref = libid_ref + + @property + def libid(self: T) -> LibidReference: + return self._libid_ref + + def pack(self: T, endien: str, cp_name: str) -> bytes: + endien_symbol = '<' if endien == 'little' else '>' + strlen = len(self._libid_ref) + format = endien_symbol + "HII" + str(strlen) + "sIH" + lib_str = str(self._libid_ref).encode(cp_name) + return struct.pack(format, 0x000D, strlen + 10, + strlen, lib_str, 0, 0) + + @staticmethod + def unpack(data: bytes, endien: str) -> T: + endien_symbol = '<' if endien == 'little' else '>' + offset = 0 + id, = struct.unpack_from(endien_symbol + "H", data, offset) + offset += 2 + if id != 0x000D: + msg = "Incorrect id in data. Received " + id + ", expected 0x000D" + raise ValueError(msg) + + format = endien_symbol + "II" + recordsize, libidsize = struct.unpack_from(format, data, offset) + if len(data) != recordsize + 6: + # raise a warning + pass + + offset += 8 + + libid_ref_bytes = data[offset:offset + libidsize] + offset += libidsize + + format = endien_symbol + "IH" + reserved1, reserved2 = struct.unpack_from(format, data, offset) + offset += 6 + + if reserved1 != 0: + # raise a warning + pass + + if reserved2 != 0: + # raise a warning + pass + + libid_ref = LibidReference.unpack(libid_ref_bytes) + return ReferenceRegistered(libid_ref) diff --git a/vbaProjectCompiler/Models/Entities/std_module.py b/src/ms_ovba/Models/Entities/std_module.py similarity index 59% rename from vbaProjectCompiler/Models/Entities/std_module.py rename to src/ms_ovba/Models/Entities/std_module.py index 2a5cc8fcc..3763cedd5 100644 --- a/vbaProjectCompiler/Models/Entities/std_module.py +++ b/src/ms_ovba/Models/Entities/std_module.py @@ -1,13 +1,17 @@ -from vbaProjectCompiler.Models.Entities.module_base import ModuleBase +from ms_ovba.Models.Entities.module_base import ModuleBase +from typing import TypeVar + + +T = TypeVar('T', bound='StdModule') class StdModule(ModuleBase): - def __init__(self, name): + def __init__(self: T, name: str) -> None: super(StdModule, self).__init__(name) self.type = "Module" - def normalize_file(self): + def normalize_file(self: T) -> None: f = open(self._file_path, "r") new_f = open(self._file_path + ".new", "a+", newline='\r\n') while line := f.readline(): diff --git a/src/ms_ovba/Models/Fields/doubleEncodedString.py b/src/ms_ovba/Models/Fields/doubleEncodedString.py new file mode 100644 index 000000000..52332a4b1 --- /dev/null +++ b/src/ms_ovba/Models/Fields/doubleEncodedString.py @@ -0,0 +1,28 @@ +from ms_ovba.Models.Fields.idSizeField import IdSizeField +from typing import TypeVar + + +T = TypeVar('T', bound='DoubleEncodedString') + + +class DoubleEncodedString(): + """ + A union of two IdSizeFields + The strings are encoded differently in each. + """ + def __init__(self: T, ids: list, text: str) -> None: + self.ids = ids + self._value = text + + @property + def value(self) -> str: + return self._value + + def pack(self: T, endien: str, cp_name: str) -> bytes: + encoded = self._value.encode(cp_name) + self.mod_name1 = IdSizeField(self.ids[0], len(encoded), encoded) + format = "utf_16_le" if endien == 'little' else "utf_16_be" + encoded = self._value.encode(format) + self.mod_name2 = IdSizeField(self.ids[1], len(encoded), encoded) + return (self.mod_name1.pack(endien) + + self.mod_name2.pack(endien)) diff --git a/src/ms_ovba/Models/Fields/idSizeField.py b/src/ms_ovba/Models/Fields/idSizeField.py new file mode 100644 index 000000000..6a1b5cae8 --- /dev/null +++ b/src/ms_ovba/Models/Fields/idSizeField.py @@ -0,0 +1,38 @@ +import struct +from typing import Any, TypeVar + + +T = TypeVar('T', bound='IdSizeField') + + +class IdSizeField(): + """ + Many Records have the same format, a two bye ID, a four byte size and an + int value formatted to the defined size. + """ + + def __init__(self: T, id: int, size: int, value: Any) -> None: + self._id = id + self._size = size + self._value = value + + @property + def value(self: T) -> Any: + return self._value + + def pack(self: T, endien: str, cp_name: str = None) -> bytes: + endien_symbol = '<' if endien == 'little' else '>' + format = endien_symbol + "HI" + if isinstance(self._value, str): + self.stringValue = self._value + self._value = bytes(self._value, encoding="ascii") + if isinstance(self._value, bytes): + format += str(self._size) + "s" + elif self._size == 2: + format += "H" + elif self._size == 4: + format += "I" + else: + msg = "Received data of type " + type(self._value).__name__ + raise Exception(msg) + return struct.pack(format, self._id, self._size, self._value) diff --git a/src/ms_ovba/Models/Fields/libid_reference.py b/src/ms_ovba/Models/Fields/libid_reference.py new file mode 100644 index 000000000..ff492ee27 --- /dev/null +++ b/src/ms_ovba/Models/Fields/libid_reference.py @@ -0,0 +1,64 @@ +import uuid +from typing import TypeVar + + +T = TypeVar('T', bound='LibidReference') + + +class LibidReference(): + """ + 2.1.1.8 + + LibidReference = "*\" LibidReferenceKind LibidGuid + "#" LibidMajorVersion "." LibidMinorVersion + "#" LibidLcid + "#" LibidPath + "#" LibidRegName + LibidReferenceKind = %x47 / %x48 + LibidGuid = GUID + LibidMajorVersion = 1*4HEXDIG + LibidMinorVersion = 1*4HEXDIG + LibidLcid = 1*8HEXDIG + LibidPath = *(%x01-22 / %x24-FF) + LibidRegName = *255(%x01-FF) + """ + def __init__(self: T, libid_guid: uuid.UUID, version: str, + libid_lcid: str, libid_path: str, + libid_reg_name: str) -> None: + self._libid_guid = libid_guid + self._version = version + self._libid_lcid = libid_lcid + self._libid_path = libid_path + self._libid_reg_name = libid_reg_name + if self._is_windows_path(libid_path): + self._libid_reference_kind = "G" + else: + self._libid_reference_kind = "H" + + def __str__(self: T) -> str: + return "*\\" + \ + self._libid_reference_kind + \ + "{" + str(self._libid_guid).upper() + "}#" + \ + self._version + "#" + \ + self._libid_lcid + "#" + \ + str(self._libid_path) + "#" + \ + self._libid_reg_name + + def __len__(self: T) -> int: + return len(str(self)) + + @staticmethod + def unpack(data: bytes): + guid, version, lcid, path, name = data.decode('ascii').split("#") + prefix = guid[:3] + guid = uuid.UUID(guid[3:]) + if prefix[:2] != "*\\": + raise Exception("Improper prefix") + kind = prefix[2:] + if kind != "G" and kind != "H": + raise Exception("Unknown Reference Kind") + return LibidReference(guid, version, lcid, + path, name) + + def _is_windows_path(self: T, path: str) -> bool: + return path[0] != '/' diff --git a/src/ms_ovba/Models/Fields/packed_data.py b/src/ms_ovba/Models/Fields/packed_data.py new file mode 100644 index 000000000..70e8430f8 --- /dev/null +++ b/src/ms_ovba/Models/Fields/packed_data.py @@ -0,0 +1,20 @@ +import struct +from typing import Any, TypeVar + + +T = TypeVar('T', bound='PackedData') + + +class PackedData(): + """ + Multivalue field with a packing format. + This class allows a user to define a data format, + and render it at a later time. + """ + def __init__(self: T, format: str, *values: Any) -> None: + self.values = values + self.format = format + + def pack(self: T, endien: str, cp_name: str = None) -> bytes: + endien_symbol = '<' if endien == 'little' else '>' + return struct.pack(endien_symbol + self.format, *self.values) diff --git a/src/ms_ovba/Models/Fields/project_reference.py b/src/ms_ovba/Models/Fields/project_reference.py new file mode 100644 index 000000000..436677afb --- /dev/null +++ b/src/ms_ovba/Models/Fields/project_reference.py @@ -0,0 +1,48 @@ +from typing import TypeVar + + +T = TypeVar('T', bound='ProjectReference') + + +class ProjectReference(): + """ + 2.1.1.12 + Specifies the identifier of a VBA project. + + ProjectReference = "*\" ProjectKind ProjectPath + ProjectKind = %x41-44 + ProjectPath = *(%x01-FF} + """ + def __init__(self: T, project_path: str, embedded: bool = True) -> None: + self._project_path = project_path + self._embedded = embedded + + # Dunder Methods + def __str__(self) -> str: + return self._header() + \ + str(self._project_path) + + def __len__(self) -> int: + return len(str(self)) + + def relative(self: T) -> T: + """ + Strip off the path and just return the file. + """ + # Find last '\' + pos = self._project_path.rfind('\\') + + rel_path = self._project_path[pos + 1:] + return ProjectReference(rel_path, self._embedded) + + def _header(self: T) -> str: + project_kind = 0x41 + if not self._is_windows_path(self._project_path): + project_kind += 1 + if self._embedded: + project_kind += 2 + return "*\\" + \ + chr(project_kind) + + def _is_windows_path(self: T, path: str) -> bool: + return path[0] != '/' diff --git a/src/ms_ovba/Views/dirStream.py b/src/ms_ovba/Views/dirStream.py new file mode 100644 index 000000000..f0cf29191 --- /dev/null +++ b/src/ms_ovba/Views/dirStream.py @@ -0,0 +1,92 @@ +import struct +from ms_ovba_compression.ms_ovba import MsOvba +from ms_ovba.vbaProject import VbaProject +from ms_ovba.Models.Fields.idSizeField import IdSizeField +from ms_ovba.Models.Fields.doubleEncodedString import ( + DoubleEncodedString +) +from ms_ovba.Models.Fields.packed_data import PackedData +from typing import List, TypeVar + + +T = TypeVar('T', bound='DirStream') + + +class DirStream(): + """ + The dir stream is compressed on write + """ + + def __init__(self: T, project: VbaProject) -> None: + self.project = project + self._include_compat = project.compat + + def to_bytes(self: T) -> bytes: + information = self._load_information() + endien = self.project.endien + cp_name = self.project.codepage_name + pack_symbol = '<' if endien == 'little' else '>' + # should be 0xFFFF + cookie_value = self.project.project_cookie + self.project_cookie = IdSizeField(19, 2, cookie_value) + references = self.project.references + modules = self.project.modules + output = b'' + for record in information: + output += record.pack(endien, cp_name) + for record in references: + output += record.pack(endien, cp_name) + + modules_header = IdSizeField(0x000F, 2, len(modules)) + + output += (modules_header.pack(endien) + + self.project_cookie.pack(endien)) + for record in modules: + output += record.pack(endien, cp_name) + output += struct.pack(pack_symbol + "HI", 16, 0) + return output + + def include_compat(self: T) -> None: + self._include_compat = True + + def write_file(self: T) -> None: + bin_f = open("dir.bin", "wb") + ms_ovba = MsOvba() + compressed = ms_ovba.compress(self.to_bytes()) + bin_f.write(compressed) + bin_f.close() + + def _load_information(self: T) -> List: + codepage = 0x04E4 + # 0=16bit, 1=32bit, 2=mac, 3=64bit + syskind = IdSizeField(1, 4, 3) + compat_version = IdSizeField(74, 4, 3) + lcid = IdSizeField(2, 4, 0x0409) + lcid_invoke = IdSizeField(20, 4, 0x0409) + codepage_record = IdSizeField(3, 2, codepage) + project_name = IdSizeField(4, 10, "VBAProject") + docstring = DoubleEncodedString([5, 0x0040], "") + helpfile = DoubleEncodedString([6, 0x003D], "") + help_context = IdSizeField(7, 4, 0) + lib_flags = IdSizeField(8, 4, 0) + version = IdSizeField(9, 4, 0x65BE0257) + minor_version = PackedData("H", 17) + constants = DoubleEncodedString([12, 0x003C], "") + + information = [syskind] + if self._include_compat: + information.append(compat_version) + information.extend([ + lcid, + lcid_invoke, + codepage_record, + project_name, + docstring, + helpfile, + help_context, + lib_flags, + version, + minor_version, + constants + ]) + return information diff --git a/src/ms_ovba/Views/project.py b/src/ms_ovba/Views/project.py new file mode 100644 index 000000000..6c03fe844 --- /dev/null +++ b/src/ms_ovba/Views/project.py @@ -0,0 +1,81 @@ +import binascii +import ms_ovba_crypto +from ms_ovba.vbaProject import VbaProject +from typing import TypeVar + + +T = TypeVar('T', bound='Project') + + +class Project: + """ + The Project data view for the vbaProject + """ + def __init__(self: T, project: VbaProject) -> None: + self.project = project + # Attributes + + # A list of attributes and values + self.attributes = project.attributes + + # The HostExtenderInfo string + guid = "{3832D640-CF90-11CF-8E43-00A0C911005A}" + self.hostExtenderInfo = "&H00000001=" + guid + ";VBE;&H00000000" + + def add_attribute(self: T, name: str, value: str) -> None: + self.attributes[name] = value + + def to_bytes(self: T) -> bytes: + project = self.project + codepage_name = project.codepage_name + # Use \x0D0A line endings...however python encodes that. + eol = b'\x0D\x0A' + project_id = project.project_id + id = bytearray(project_id, codepage_name) + result = b'ID="' + id + b'"' + eol + modules = project.modules + for module in modules: + result += bytes(module.to_project_module_string(), codepage_name) + result += eol + result += b'Name="VBAProject"' + eol + for key in self.attributes: + result += self._attr(key, self.attributes[key]) + cmg = ms_ovba_crypto.encrypt( + project_id, + project.protection_state + ) + dpb = ms_ovba_crypto.encrypt(project_id, project.password) + gc = ms_ovba_crypto.encrypt(project_id, project.visibility_state) + result += (bytes('CMG="', codepage_name) + + binascii.hexlify(cmg).upper() + + b'\x22\x0D\x0A') + result += (bytes('DPB="', codepage_name) + + binascii.hexlify(dpb).upper() + + b'\x22\x0D\x0A') + result += (bytes('GC="', codepage_name) + + binascii.hexlify(gc).upper() + + b'\x22\x0D\x0A') + result += eol + result += b'[Host Extender Info]' + eol + result += bytes(self.hostExtenderInfo, codepage_name) + result += eol + eol + result += b'[Workspace]' + eol + for module in modules: + separator = ", " + result += bytes(module.modName.value, codepage_name) + b'=' + joined = separator.join(map(str, module.workspace)) + result += bytes(joined, codepage_name) + result += eol + return result + + def write_file(self: T) -> None: + bin_f = open("project.bin", "wb") + bin_f.write(self.to_bytes()) + bin_f.close() + + def _attr(self: T, name: str, value: str) -> str: + codepage_name = self.project.codepage_name + eol = b'\x0D\x0A' + b_name = bytes(name, codepage_name) + b_value = bytes(value, codepage_name) + return b_name + b'="' + b_value + b'"' + eol diff --git a/src/ms_ovba/Views/projectLk.py b/src/ms_ovba/Views/projectLk.py new file mode 100644 index 000000000..7806cc046 --- /dev/null +++ b/src/ms_ovba/Views/projectLk.py @@ -0,0 +1,26 @@ +import struct +from ms_ovba.vbaProject import VbaProject +from typing import TypeVar + + +T = TypeVar('T', bound='ProjectLk') + + +class ProjectLk: + """ + The ProjectLK data view for the vbaProject + """ + def __init__(self: T, project: VbaProject) -> None: + self.project = project + + def to_bytes(self: T) -> bytes: + size = len(self.project._license_records) + output = struct.pack(" None: + bin_f = open("projectlk.bin", "wb") + bin_f.write(self.to_bytes()) + bin_f.close() diff --git a/vbaProjectCompiler/Views/projectWm.py b/src/ms_ovba/Views/projectWm.py similarity index 55% rename from vbaProjectCompiler/Views/projectWm.py rename to src/ms_ovba/Views/projectWm.py index 76f60a57c..cc3f86217 100644 --- a/vbaProjectCompiler/Views/projectWm.py +++ b/src/ms_ovba/Views/projectWm.py @@ -1,11 +1,18 @@ +from ms_ovba.vbaProject import VbaProject +from typing import TypeVar + + +T = TypeVar('T', bound='ProjectWm') + + class ProjectWm: """ The ProjectWM data view for the vbaProject """ - def __init__(self, project): + def __init__(self: T, project: VbaProject) -> None: self.project = project - def toBytes(self): + def to_bytes(self: T) -> bytes: output = b'' for module in self.project.modules: output += (bytes(module.modName.value, 'ascii') @@ -14,3 +21,8 @@ def toBytes(self): + b'\x00\x00') output += b'\x00\x00' return output + + def write_file(self: T) -> None: + bin_f = open("projectwm.bin", "wb") + bin_f.write(self.to_bytes()) + bin_f.close() diff --git a/src/ms_ovba/Views/project_ole_file.py b/src/ms_ovba/Views/project_ole_file.py new file mode 100644 index 000000000..86c07fc5b --- /dev/null +++ b/src/ms_ovba/Views/project_ole_file.py @@ -0,0 +1,72 @@ +from ms_cfb.ole_file import OleFile +from ms_cfb.Models.Directories.root_directory import RootDirectory +from ms_cfb.Models.Directories.storage_directory import StorageDirectory +from ms_cfb.Models.Directories.stream_directory import StreamDirectory +from ms_ovba.vbaProject import VbaProject +from ms_ovba.Views.dirStream import DirStream +from ms_ovba.Views.project_view import ProjectView +from ms_ovba.Views.project import Project +from ms_ovba.Views.projectWm import ProjectWm +from typing import TypeVar + + +T = TypeVar('T', bound='ProjectOleFile') + + +class ProjectOleFile: + + @staticmethod + def _build_ole_directory(project: VbaProject) -> RootDirectory: + """ + Create all the custom views for the OLE file: + dir + project + projectWm + vbs_project + + Organize the modules and views into the correct storage directories + """ + directory = RootDirectory() + directory.set_modified(project.default_date) + storage = StorageDirectory("VBA") + storage.set_created(project.default_date) + storage.set_modified(project.default_date) + for module in project.get_modules(): + module.write_file() + dir = StreamDirectory(module.name, module.bin_path) + storage.add_directory(dir) + + module = DirStream(project) + module.write_file() + dir = StreamDirectory("dir", "dir.bin") + storage.add_directory(dir) + + module = ProjectView(project) + module.write_file() + dir = StreamDirectory("_VBA_PROJECT", "vba_project.bin") + storage.add_directory(dir) + + directory.add_directory(storage) + + if project.projectwm: + module = ProjectWm(project) + module.write_file() + stream = StreamDirectory("PROJECTwm", "projectwm.bin") + directory.add_directory(stream) + + module = Project(project) + module.write_file() + stream = StreamDirectory("PROJECT", "project.bin") + directory.add_directory(stream) + return directory + + @staticmethod + def _write_ole_file(root: RootDirectory) -> None: + ole_file = OleFile() + ole_file.root_directory = root + ole_file.create_file("vbaProject.bin") + + @staticmethod + def write_file(project: VbaProject) -> None: + directory = ProjectOleFile._build_ole_directory(project) + ProjectOleFile._write_ole_file(directory) diff --git a/src/ms_ovba/Views/project_view.py b/src/ms_ovba/Views/project_view.py new file mode 100644 index 000000000..b030a1287 --- /dev/null +++ b/src/ms_ovba/Views/project_view.py @@ -0,0 +1,32 @@ +import struct +from ms_ovba.vbaProject import VbaProject +from typing import TypeVar + + +T = TypeVar('T', bound='ProjectView') + + +class ProjectView: + """ + The _VBA_PROJECT data view for the vbaProject + """ + def __init__(self: T, project: VbaProject, reserved: int = 3) -> None: + self.project = project + self._reserved3 = reserved + + def to_bytes(self: T) -> bytes: + endien_symbol = '<' if self.project.endien == 'little' else '>' + format = endien_symbol + "HHBH" + output = b'' + reserved1 = 0x61CC + reserved2 = 0x00 + cache_version = self.project.performance_cache_version + + output += struct.pack(format, reserved1, cache_version, + reserved2, self._reserved3) + return output + self.project.performance_cache + + def write_file(self: T) -> None: + bin_f = open("vba_project.bin", "wb") + bin_f.write(self.to_bytes()) + bin_f.close() diff --git a/src/ms_ovba/__init__.py b/src/ms_ovba/__init__.py new file mode 100644 index 000000000..308ec1db4 --- /dev/null +++ b/src/ms_ovba/__init__.py @@ -0,0 +1 @@ +# comment diff --git a/src/ms_ovba/__main__.py b/src/ms_ovba/__main__.py new file mode 100644 index 000000000..f2be899c7 --- /dev/null +++ b/src/ms_ovba/__main__.py @@ -0,0 +1,79 @@ +import argparse +import glob +import os +import uuid +from ms_ovba.vbaProject import VbaProject +from ms_ovba.Models.Entities.doc_module import DocModule +from ms_ovba.Models.Entities.std_module import StdModule +from ms_ovba.Views.project_ole_file import ProjectOleFile +from ms_ovba.Models.Entities.reference_record import ( + ReferenceRecord +) +from ms_ovba.Models.Fields.libid_reference import LibidReference + + +def main() -> None: + + parser = argparse.ArgumentParser() + parser.add_argument("directory", + help="The directory that contains your files.") + args = parser.parse_args() + # cd args.output + # build a list of all bas, cls, frm, and frx files + bas_files = glob.glob(args.directory + '/**/*.bas', recursive=True) + # cls_files = glob.glob('*.cls') + # frm_files = glob.glob('*.frm') + # frx_files = glob.glob('*.frx') + + # create a new project object + project = VbaProject() + + # add default modules + module = DocModule('Sheet1') + base_path = os.path.dirname(__file__) + module.add_file(base_path + '/blank_files/Sheet1.cls') + module.normalize_file() + guid = uuid.UUID("0002082000000000C000000000000046") + module.set_guid(guid) + project.add_module(module) + module = DocModule('ThisWorkbook') + module.add_file(base_path + '/blank_files/ThisWorkbook.cls') + module.normalize_file() + guid = uuid.UUID("0002081900000000C000000000000046") + module.set_guid(guid) + project.add_module(module) + project.set_project_id('{9E394C0B-697E-4AEE-9FA6-446F51FB30DC}') + # add the files + for file_path in bas_files: + file_name = os.path.basename(file_path) + file = os.path.splitext(file_name) + code = StdModule(file[0]) + code.add_file(file_path) + code.normalize_file() + project.add_module(code) + codepage = 0x04E4 + codepage_name = "cp" + str(codepage) + libid_ref = LibidReference( + uuid.UUID("0002043000000000C000000000000046"), + "2.0", + "0", + "C:\\Windows\\System32\\stdole2.tlb", + "OLE Automation" + ) + ole_reference = ReferenceRecord(codepage_name, "stdole", libid_ref) + libid_ref2 = LibidReference( + uuid.UUID("2DF8D04C5BFA101BBDE500AA0044DE52"), + "2.0", + "0", + "C:\\Program Files\\Common Files\\Microsoft Shared\\OFFICE16\\MSO.DLL", + "Microsoft Office 16.0 Object Library" + ) + office_reference = ReferenceRecord(codepage_name, "Office", libid_ref2) + project.add_reference(ole_reference) + project.add_reference(office_reference) + ProjectOleFile.write_file(project) + file = glob.glob('vbaProject.bin') + + +if __name__ == '__main__': + main() diff --git a/vbaProjectCompiler/blank_files/Sheet1.cls b/src/ms_ovba/blank_files/Sheet1.cls similarity index 100% rename from vbaProjectCompiler/blank_files/Sheet1.cls rename to src/ms_ovba/blank_files/Sheet1.cls diff --git a/vbaProjectCompiler/blank_files/ThisWorkbook.cls b/src/ms_ovba/blank_files/ThisWorkbook.cls similarity index 100% rename from vbaProjectCompiler/blank_files/ThisWorkbook.cls rename to src/ms_ovba/blank_files/ThisWorkbook.cls diff --git a/src/ms_ovba/vbaProject.py b/src/ms_ovba/vbaProject.py new file mode 100644 index 000000000..6feb76806 --- /dev/null +++ b/src/ms_ovba/vbaProject.py @@ -0,0 +1,152 @@ +from ms_ovba.Models.Entities.module_base import ModuleBase +from ms_ovba.Models.Entities.reference_registered import ( + ReferenceRegistered +) +from ms_dtyp.filetime import Filetime +from typing import TypeVar + + +T = TypeVar('T', bound='VbaProject') + + +class VbaProject: + + def __init__(self: T) -> None: + + self.endien = 'little' + + # Protected Instance Attributes + self._codepage_name = 'cp1252' + self._project_id = '{}' + self._protection_state = b'\x00\x00\x00\x00' + self._password = b'\x00' + self._visibility_state = b'\xFF' + self._performance_cache = b'' + self._performance_cache_version = 0xFFFF + + # Lists + self.directories = [] + self.references = [] + self.modules = [] + self._license_records = [] + + # Attributes and values + self.attributes = {} + + self._project_cookie = 0xFFFF + + self._project_wm = False + self._compat = False + self._default_date = Filetime.from_msfiletime(0x0000000000000000) + # Getters and Setters + + @property + def default_date(self: T) -> Filetime: + return self._default_date + + @default_date.setter + def default_date(self: T, date: Filetime) -> None: + self._default_date = date + + @property + def project_id(self: T) -> str: + return self._project_id + + @project_id.setter + def project_id(self: T, id: str) -> None: + self._project_id = id + + @property + def protection_state(self: T) -> int: + return self._protection_state + + @protection_state.setter + def protection_state(self: T, state: int) -> None: + self._protection_state = state + + @property + def visibility_state(self: T) -> bytes: + return self._visibility_state + + @visibility_state.setter + def visibility_state(self: T, state: int) -> None: + """ + 0 = not visible + 255 = visible + """ + if state != 0 and state != 255: + raise Exception("Bad visibility value.") + self._visibility_state = state + + @property + def password(self: T) -> bytes: + return self._password + + @password.setter + def password(self: T, value: bytes) -> None: + self._password = value + + @property + def performance_cache(self: T) -> bytes: + return self._performance_cache + + @performance_cache.setter + def performance_cache(self: T, cache: bytes) -> None: + self._performance_cache = cache + + @property + def performance_cache_version(self: T) -> int: + return self._performance_cache_version + + @performance_cache_version.setter + def performance_cache_version(self: T, version: int) -> None: + self._performance_cache_version = version + + @property + def codepage_name(self: T) -> str: + return self._codepage_name + + @codepage_name.setter + def codepage_name(self: T, name: str) -> None: + self._codepage_name = name + + @property + def project_cookie(self: T) -> int: + return self._project_cookie + + @project_cookie.setter + def project_cookie(self: T, value: int) -> None: + self._project_cookie = value + + def get_modules(self: T) -> list: + return self.modules + + @property + def projectwm(self: T) -> bool: + return self._project_wm + + def include_projectwm(self: T) -> bool: + self._project_wm = True + + def exclude_projectwm(self: T) -> bool: + self._project_wm = False + + @property + def compat(self: T) -> bool: + return self._compat + + def include_compat(self: T) -> None: + self._compat = True + + def exclude_compat(self: T) -> None: + self._compat = False + + # Appenders + def add_module(self: T, mod: ModuleBase) -> None: + self.modules.append(mod) + + def add_reference(self: T, ref: ReferenceRegistered) -> None: + self.references.append(ref) + + def add_attribute(self: T, name: str, value: str) -> None: + self.attributes[name] = value diff --git a/tests/Unit/Models/Entities/test_doc_module.py b/tests/Functional/Models/Entities/test_doc_module.pyt similarity index 83% rename from tests/Unit/Models/Entities/test_doc_module.py rename to tests/Functional/Models/Entities/test_doc_module.pyt index 133087dff..f72a3f796 100644 --- a/tests/Unit/Models/Entities/test_doc_module.py +++ b/tests/Functional/Models/Entities/test_doc_module.pyt @@ -1,17 +1,18 @@ -import os import uuid from ms_ovba_compression.ms_ovba import MsOvba from ms_pcode_assembler.module_cache import ModuleCache -from vbaProjectCompiler.Models.Entities.doc_module import DocModule +from ms_ovba.Models.Entities.doc_module import DocModule -def test_normalize(): +def test_normalize() -> None: - cache = ModuleCache(0xB5, 0x08F3) + cache = ModuleCache(0xB5, 0x08F3, signature=3) cache.module_cookie = 0xB81C - cache.misc = [0x0316, 0x0123, 0x88, 8, 0x18, "00000000", 1] + cache.header.data3 = 0x0123 + cache.header.data4 = 0x88 + cache.misc = [[-1, 8], 24, 0, [1, "00000000"]] guid = uuid.UUID('0002081900000000C000000000000046') - cache.guid = bytes(("0{" + str(guid) + "}").upper(), "utf_16_le") + cache.guid = [guid] indirect_table = ("02 80 FE FF FF FF FF FF 20 00 00 00 FF FF FF FF", "30 00 00 00 02 01 FF FF 00 00 00 00 00 00 00 00", @@ -24,8 +25,7 @@ def test_normalize(): cache.object_table = bytes.fromhex(" ".join(object_table)) module = DocModule("foo") - path1 = "vbaProjectCompiler/blank_files/ThisWorkbook.cls" - os.remove(path1 + ".new") + path1 = "src/ms_ovba/blank_files/ThisWorkbook.cls" module.add_file(path1) module.cookie.value = 0xB81C guid = uuid.UUID('0002081900000000C000000000000046') diff --git a/tests/Functional/test_dirObject.py b/tests/Functional/test_dirObject.py deleted file mode 100644 index 66eee039f..000000000 --- a/tests/Functional/test_dirObject.py +++ /dev/null @@ -1,96 +0,0 @@ -import struct -import uuid -from ms_ovba_compression.ms_ovba import MsOvba -from ms_pcode_assembler.module_cache import ModuleCache -from vbaProjectCompiler.vbaProject import VbaProject -from vbaProjectCompiler.Views.dirStream import DirStream -from vbaProjectCompiler.Models.Fields.libidReference import LibidReference -from vbaProjectCompiler.Models.Entities.doc_module import DocModule -from vbaProjectCompiler.Models.Entities.std_module import StdModule -from vbaProjectCompiler.Models.Entities.referenceRecord import ReferenceRecord - - -def test_dirStream(): - module_cache = ModuleCache(0xB5, 0x08F3) - # Read the data from the demo file and decompress it. - f = open('tests/blank/vbaProject.bin', 'rb') - offset = 0x1EC0 - length = 0x0232 - f.seek(offset) - container = f.read(length) - ms_ovba = MsOvba() - decompressedStream = ms_ovba.decompress(container) - - # Create a project with the same attributes - project = VbaProject() - stream = DirStream(project) - codePage = 0x04E4 - codePageName = "cp" + str(codePage) - guid = uuid.UUID('0002043000000000C000000000000046') - libidRef = LibidReference( - guid, - "2.0", - "0", - "C:\\Windows\\System32\\stdole2.tlb", - "OLE Automation" - ) - oleReference = ReferenceRecord(codePageName, "stdole", libidRef) - guid = uuid.UUID('2DF8D04C5BFA101BBDE500AA0044DE52') - libidRef2 = LibidReference( - guid, - "2.0", - "0", - "C:\\Program Files\\Common Files\\Microsoft Shared\\OFFICE16\\MSO.DLL", - "Microsoft Office 16.0 Object Library" - ) - officeReference = ReferenceRecord(codePageName, "Office", libidRef2) - project.addReference(oleReference) - project.addReference(officeReference) - project.setProjectCookie(0x08F3) - - indirect_table = ("02 80 FE FF FF FF FF FF 20 00 00 00 FF FF FF FF", - "30 00 00 00 02 01 FF FF 00 00 00 00 00 00 00 00", - "FF FF FF FF FF FF FF FF 00 00 00 00 2E 00 43 00", - "1D 00 00 00 25 00 00 00 FF FF FF FF 40 00 00 00") - module_cache.indirect_table = bytes.fromhex(" ".join(indirect_table)) - object_table = ("02 00 53 4C FF FF FF FF 00 00 01 00 53 10 FF FF", - "FF FF 00 00 01 00 53 94 FF FF FF FF 00 00 00 00", - "02 3C FF FF FF FF 00 00") - module_cache.object_table = bytes.fromhex(" ".join(object_table)) - module_cache.misc = [0x0316, 0x0123, 0x88, 8, - 0x18, "00000000", 1] - - this_workbook = DocModule("ThisWorkbook") - this_workbook.cookie.value = 0xB81C - module_cache.cookie = this_workbook.cookie.value - guid = uuid.UUID('0002081900000000C000000000000046') - this_workbook.set_guid(guid) - module_cache.guid = bytes(("0{" + str(guid) + "}").upper(), "utf_16_le") - this_workbook.set_cache(module_cache.to_bytes()) - - sheet1 = DocModule("Sheet1") - sheet1.cookie.value = 0x9B9A - module_cache.cookie = sheet1.cookie.value - guid = uuid.UUID('0002082000000000C000000000000046') - module_cache.guid = bytes(("0{" + str(guid) + "}").upper(), "utf_16_le") - sheet1.set_guid(guid) - sheet1.set_cache(module_cache.to_bytes()) - - module1 = StdModule("Module1") - module1.cookie.value = 0xB241 - module_cache.clear_variables() - module_cache.cookie = module1.cookie.value - module_cache.misc = [0x0316, 3, 0, 2, - 0xFFFF, "FFFFFFFF", 0] - module_cache.indirect_table = struct.pack(" None: cls._rand = seeds @classmethod - def randint(cls, param1, param2): + def randint(cls: Type[T], param1: int, param2: int) -> int: return cls._rand.pop(0) -def module_matches_bin(module_path, - cache_size, - bin_path, - bin_offset, - bin_length): +@pytest.fixture(autouse=True) +def run_around_tests() -> None: + # Code that will run before your test, for example: + + # A test function will be run at this point + yield + # Code that will run after your test + root = "src/ms_ovba/blank_files/" + root2 = "tests/blank/" + names = [root + "ThisWorkbook.cls", root + "Sheet1.cls", + root2 + "Module1.bas"] + remove_module(names) + names = ["dir.bin", "projectwm.bin", "project.bin", "vba_project.bin"] + map(os.remove, names) + + +def remove_module(names: str) -> None: + for name in names: + os.remove(name + ".new") + os.remove(name + ".bin") + + +def assert_module_matches_bin(module_path: str, + cache_size: int, + bin_path: str, + bin_offset: int, + bin_length: int) -> bool: m = open(module_path, "rb") b = open(bin_path, "rb") b.seek(bin_offset) - if m.read(cache_size) != b.read(cache_size): - return False + assert m.read(cache_size) == b.read(cache_size) ms_ovba = MsOvba() m_uncompressed = ms_ovba.decompress(m.read()) - b_uncompressed = ms_ovba.decompress(b.read(bin_length)) - return m_uncompressed == b_uncompressed + b_uncompressed = ms_ovba.decompress(b.read(bin_length - cache_size)) + assert m_uncompressed == b_uncompressed -@unittest.mock.patch('random.randint', NotSoRandom.randint) -def test_fullFile(): - rand = [0x41, 0xBC, 0x7B, 0x7B, 0x37, 0x7B, 0x7B, 0x7B] - NotSoRandom.set_seed(rand) - project = VbaProject() - codePage = 0x04E4 - codePageName = "cp" + str(codePage) - libidRef = LibidReference( - uuid.UUID("0002043000000000C000000000000046"), +stdole_lib = LibidReference( + uuid.UUID("00020430-0000-0000-C000-000000000046"), "2.0", "0", "C:\\Windows\\System32\\stdole2.tlb", "OLE Automation" ) - oleReference = ReferenceRecord(codePageName, "stdole", libidRef) - libidRef2 = LibidReference( + + +@unittest.mock.patch('random.randint', NotSoRandom.randint) +def test_full_file() -> None: + """ + Create an exact reproduction of a complete "empty" vba excel addin. + """ + rand = [0x41, 0xBC, 0x7B, 0x7B, 0x37, 0x7B, 0x7B, 0x7B] + NotSoRandom.set_seed(rand) + project = VbaProject() + project.default_date = Filetime.from_msfiletime(0x01D92433C2B823C0) + project.include_projectwm() + libid_ref = ReferenceRegistered(stdole_lib) + ole_reference = Reference(libid_ref, "stdole") + libid_ref2 = ReferenceRegistered(LibidReference( uuid.UUID("2DF8D04C5BFA101BBDE500AA0044DE52"), "2.0", "0", "C:\\Program Files\\Common Files\\Microsoft Shared\\OFFICE16\\MSO.DLL", "Microsoft Office 16.0 Object Library" - ) - officeReference = ReferenceRecord(codePageName, "Office", libidRef2) - project.addReference(oleReference) - project.addReference(officeReference) - project.setProjectCookie(0x08F3) - project.setProjectId('{9E394C0B-697E-4AEE-9FA6-446F51FB30DC}') - project.setPerformanceCache(createCache()) - project.setPerformanceCacheVersion(0x00B5) - - module_cache = ModuleCache(0xB5, 0x08F3) - module_cache.misc = [0x0316, 0x0123, 0x88, 8, 0x18, "00000000", 1] - indirect_table = ("02 80 FE FF FF FF FF FF 20 00 00 00 FF FF FF FF", - "30 00 00 00 02 01 FF FF 00 00 00 00 00 00 00 00", - "FF FF FF FF FF FF FF FF 00 00 00 00 2E 00 43 00", - "1D 00 00 00 25 00 00 00 FF FF FF FF 40 00 00 00") - module_cache.indirect_table = bytes.fromhex(" ".join(indirect_table)) - object_table = ("02 00 53 4C FF FF FF FF 00 00 01 00 53 10 FF FF", - "FF FF 00 00 01 00 53 94 FF FF FF FF 00 00 00 00", - "02 3C FF FF FF FF 00 00") - module_cache.object_table = bytes.fromhex(" ".join(object_table)) + )) + office_reference = Reference(libid_ref2, "Office") + project.add_reference(ole_reference) + project.add_reference(office_reference) + proj_cookie = 0x08F3 + project.project_cookie = proj_cookie + project.project_id = '{9E394C0B-697E-4AEE-9FA6-446F51FB30DC}' + project.performance_cache_version = 0x00B5 + base_path = "src/ms_ovba/blank_files/" # Add Modules - this_workbook = DocModule("ThisWorkbook") - this_workbook.cookie.value = 0xB81C - module_cache.module_cookie = 0xB81C - guid = uuid.UUID("0002081900000000C000000000000046") - module_cache.guid = bytes(("0{" + str(guid) + "}").upper(), "utf_16_le") - this_workbook.set_guid(guid) - module_path = "vbaProjectCompiler/blank_files/ThisWorkbook.cls" - this_workbook.add_file(module_path) - this_workbook.normalize_file() - this_workbook.set_cache(module_cache.to_bytes()) - - sheet1 = DocModule("Sheet1") - sheet1.cookie.value = 0x9B9A - module_cache.module_cookie = 0x9B9A - guid = uuid.UUID("0002082000000000C000000000000046") - module_cache.guid = bytes(("0{" + str(guid) + "}").upper(), "utf_16_le") - sheet1.set_guid(guid) - module_path = "vbaProjectCompiler/blank_files/Sheet1.cls" - sheet1.add_file(module_path) - sheet1.normalize_file() - sheet1.set_cache(module_cache.to_bytes()) + this_workbook = create_doc_module(project, "ThisWorkbook", 0xB81C, + "0002081900000000C000000000000046", + base_path + "ThisWorkbook.cls") + + sheet1 = create_doc_module(project, "Sheet1", 0x9B9A, + "0002082000000000C000000000000046", + base_path + "Sheet1.cls") module1 = StdModule("Module1") - module1.cookie.value = 0xB241 - module_cache.clear_variables() - module_cache.misc = [0x0316, 3, 0, 2, 0xFFFF, "FFFFFFFF", 0] + cookie = 0xB241 + module1.cookie = cookie + module_cache = ModuleCache(0xB5, proj_cookie, signature=3) + module_cache.misc = [[-1, 2], 0xFFFF, 0, [0, "FFFFFFFF"]] + module_cache.header.data2 = 0 + module_cache.header.data3 = 3 + module_cache.header.data4 = 0 module_cache.indirect_table = struct.pack(" bytes: + cache = ProjectCache(0x04E4, proj_cookie, 0x65BE0257) + cache._hex = 0x65BE0257 + module_array = [] + i = 0 + id = [0x227, 0x22B, 0x22C] + for module in modules: + hex = 0x65BE0263 if i == 2 else cache._hex + module_array.append( + (module.name, 50, 70 + i, hex, id[i], + module.cookie, len(module.cache), [], -1) + ) + i += 1 + + cache._modules = module_array + + cache.add_library(str(LibidReference( + uuid.UUID("000204EF-0000-0000-C000-000000000046"), "4.2", "9", "C:\\Program Files\\Common Files\\Microsoft Shared\\VBA" "\\VBA7.1\\VBE7.DLL", "Visual Basic For Applications" - )) - delim.append(0x011A) - libraries.append(LibidReference( - "{00020813-0000-0000-C000-000000000046}", + ))) + cache.add_library(str(LibidReference( + uuid.UUID("00020813-0000-0000-C000-000000000046"), "1.9", "0", "C:\\Program Files\\Microsoft Office\\root\\Office16\\EXCEL.EXE", "Microsoft Excel 16.0 Object Library" - )) - delim.append(0x00BC) - libraries.append(LibidReference( - "{00020430-0000-0000-C000-000000000046}", - "2.0", - "0", - "C:\\Windows\\System32\\stdole2.tlb", - "OLE Automation" - )) - delim.append(0x0128) - libraries.append(LibidReference( - "{2DF8D04C-5BFA-101B-BDE5-00AA0044DE52}", + ))) + cache.add_library(str(stdole_lib)) + cache.add_library(str(LibidReference( + uuid.UUID("2DF8D04C-5BFA-101B-BDE5-00AA0044DE52"), "2.8", "0", "C:\\Program Files\\Common Files\\Microsoft Shared\\OFFICE16\\MSO.DLL", "Microsoft Office 16.0 Object Library" - )) - delim.append(0x0003) - ca = (b'' - + b'\xFF\x09\x04\x00\x00\x09\x04\x00\x00\xE4\x04\x03\x00\x00\x00\x00' - + b'\x00\x00\x00\x00\x00\x01\x00\x04\x00\x02\x00\x20\x01') - i = 0 - for lib in libraries: - ca += bytearray(str(lib), "utf_16_le") - ca += struct.pack("\xc3\x82\xfcD\x88\xcav\x96\xe5\x061"' + ] + cache._post_footer = 0x30 + cache._w0 = 0x117 + cache._w2 = 0x2ba0 + cache._identifiers = [ + (b"Excel", 4, 0x2b80), (b"VBA", 4, 0xe2f7), (b"Win16", 4, 0x7ec1), + (b"Win32", 4, 0x7f07), (b"Win64", 4, 0x7f78), (b"Mac", 4, 0xb2b3), + (b"VBA6", 4, 0x23ad), (b"VBA7", 4, 0x23ae), + (b"Project1", 4, 0x170a), + (b"stdole", 4, 0x6093), (b"VBAProject", 4, 0xbfbe), + (b"Office", 4, 0x7515), (b"ThisWorkbook", 4, 0xe37c), + (b"_Evaluate", 128, 0xd918, 0, 0x103, -1), + (b"Sheet1", 4, 0x1ae8), (b"Module1", 4, 0x1162), + (b"Workbook", 4, 0x186b) + ] + + hex = ("20 02 02 00 FF FF 22 02 FF FF FF FF 24 02 03 00", + "FF FF 27 02 00 00 03 00 FF FF FF FF FF FF 2B 02", + "01 00 03 00 2D 02 02 00 05 00 0E 02 01 00 FF FF", + "10 02 00 00 FF FF FF FF FF FF FF FF FF FF FF FF", + "FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF", + "FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF") + + hex2 = ("06 00 10 00 00 00 01 00 36 00 00 00 00 00 00 00", + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00", + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00", + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00", + "00 00") + + cache._footer = [ + bytes.fromhex(" ".join(hex)), + bytes.fromhex(" ".join(hex2)) + ] + return cache.to_bytes() + + +def create_doc_module(project: VbaProject, name: str, + cookie: int, guid_s: str, path: str) -> DocModule: + mod = DocModule(name) + mod.cookie = cookie + guid = uuid.UUID(guid_s) + mod.add_guid(guid) + module_path = path + mod.add_file(module_path) + mod.normalize_file() + + cache_ver = project.performance_cache_version + proj_cookie = project.project_cookie + module_cache = ModuleCache(cache_ver, proj_cookie, signature=3) + module_cache.header.data2 = 0 + module_cache.header.data3 = 0x123 + module_cache.header.data4 = 0x88 + module_cache.misc = [[-1, 8], 0x18, 0, [1, "00000000"]] + indirect_table = ("02 80 FE FF FF FF FF FF 20 00 00 00 FF FF FF FF", + "30 00 00 00 02 01 FF FF 00 00 00 00 00 00 00 00", + "FF FF FF FF FF FF FF FF 00 00 00 00 2E 00 43 00", + "1D 00 00 00 25 00 00 00 FF FF FF FF 40 00 00 00") + module_cache.indirect_table = bytes.fromhex(" ".join(indirect_table)) + + object_table = [[2, 0x4C53], [1, 0x1053], [1, 0x9453], [0, 0x3C02]] + object_table_bytes = b'' + for entry in object_table: + object_table_bytes += struct.pack(" None: + expected = (b'-\xF8\xD0L[\xFA\x10\x1B\xBD\xE5\x00\xAA\x00D\xDER' + + b'\x03\x00\x00\x00Key\x01\x00\x00\x00') + assert out.to_bytes() == expected + + +def test_not_required() -> None: + expected = (b'-\xF8\xD0L[\xFA\x10\x1B\xBD\xE5\x00\xAA\x00D\xDER' + + b'\x03\x00\x00\x00Key\x00\x00\x00\x00') + out.not_required() + assert out.to_bytes() == expected diff --git a/tests/Unit/Models/Entities/test_reference.py b/tests/Unit/Models/Entities/test_reference.py new file mode 100644 index 000000000..37b4bbc09 --- /dev/null +++ b/tests/Unit/Models/Entities/test_reference.py @@ -0,0 +1,54 @@ +from ms_ovba.Models.Entities.reference import Reference +from unittest import mock + + +class MockDEString: + def __init__(self, foo, bar) -> None: + pass + + def pack(self, one, two) -> bytes: + return (b'\x16\x00\x0B\x00\x00\x00VBAProject1' + + b'\x3E\x00\x16\x00\x00\x00V\x00B\x00A\x00P\x00r' + + b'\x00o\x00j\x00e\x00c\x00t\x001\x00') + + +class MockRefProj: + def pack(self, foo, bar) -> bytes: + return (b'\x0e\x00^\x00\x00\x000\x00\x00\x00' + + b'*\\CC:\\Example Path\\Example-ReferencedProject.xls ' + + b'\x00\x00\x00*\\CExample-ReferencedProject.xls' + + b'W\x02\xbee\x17\x00') + + +def test_constructor1() -> None: + ref_proj = "" + ref = Reference(ref_proj) + assert isinstance(ref, Reference) + + +def test_constructor2() -> None: + ref = Reference("", "VBAProject1") + assert isinstance(ref, Reference) + + +def test_pack() -> None: + ref_proj = MockRefProj() + ref = Reference(ref_proj, "VBAProject1") + + expected_hex = ("16 00 0B 00 00 00 56 42 41 50 72 6F 6A 65 63 74", + "31 3E 00 16 00 00 00 56 00 42 00 41 00 50 00 72", + "00 6F 00 6A 00 65 00 63 00 74 00 31 00 0E 00 5E", + "00 00 00 30 00 00 00 2A 5C 43 43 3A 5C 45 78 61", + "6D 70 6C 65 20 50 61 74 68 5C 45 78 61 6D 70 6C", + "65 2D 52 65 66 65 72 65 6E 63 65 64 50 72 6F 6A", + "65 63 74 2E 78 6C 73 20 00 00 00 2A 5C 43 45 78", + "61 6D 70 6C 65 2D 52 65 66 65 72 65 6E 63 65 64", + "50 72 6F 6A 65 63 74 2E 78 6C 73 57 02 BE 65 17", + "00") + expected = bytes.fromhex(" ".join(expected_hex)) + codepage = 0x04E4 + cp_name = "cp" + str(codepage) + path = 'ms_ovba.Models.Entities.reference.DoubleEncodedString' + with mock.patch(path, MockDEString): + results = ref.pack('little', cp_name) + assert results == expected diff --git a/tests/Unit/Models/Entities/test_reference_project.py b/tests/Unit/Models/Entities/test_reference_project.py new file mode 100644 index 000000000..1026fa175 --- /dev/null +++ b/tests/Unit/Models/Entities/test_reference_project.py @@ -0,0 +1,44 @@ +from ms_ovba.Models.Entities.reference_project import ReferenceProject + + +def test_constructor() -> None: + ref = MockProjectReference() + module = ReferenceProject(ref) + assert isinstance(module, ReferenceProject) + + +class MockProjectReference2: + def __len__(self): + return 0x0020 + + def __str__(self): + return "*\\CExample-ReferencedProject.xls" + + +class MockProjectReference: + def __len__(self): + return 0x0030 + + def __str__(self): + return "*\\CC:\\Example Path\\Example-ReferencedProject.xls" + + def relative(self): + return MockProjectReference2() + + +def test_pack() -> None: + ref = MockProjectReference() + + expected_hex = ("0E 00 5E 00 00 00 30 00 00 00 2A 5C 43 43 3A 5C", + "45 78 61 6D 70 6C 65 20 50 61 74 68 5C 45 78 61", + "6D 70 6C 65 2D 52 65 66 65 72 65 6E 63 65 64 50", + "72 6F 6A 65 63 74 2E 78 6C 73 20 00 00 00 2A 5C", + "43 45 78 61 6D 70 6C 65 2D 52 65 66 65 72 65 6E", + "63 65 64 50 72 6F 6A 65 63 74 2E 78 6C 73 57 02", + "BE 65 17 00") + expected = bytes.fromhex(" ".join(expected_hex)) + codepage = 0x04E4 + cp_name = "cp" + str(codepage) + ref_proj = ReferenceProject(ref) + results = ref_proj.pack('little', cp_name) + assert results == expected diff --git a/tests/Unit/Models/Entities/test_reference_registered.py b/tests/Unit/Models/Entities/test_reference_registered.py new file mode 100644 index 000000000..92fd7f370 --- /dev/null +++ b/tests/Unit/Models/Entities/test_reference_registered.py @@ -0,0 +1,75 @@ +import pytest +from unittest import mock +from ms_ovba.Models.Entities.reference_registered import ReferenceRegistered + + +class MockLibid1: + def __len__(self): + return 0x005E + + def __str__(self): + return ("*\\G{00020430-0000-0000-C000-000000000046}#2.0#0#" + + "C:\\Windows\\system32\\stdole2.tlb#OLE Automation") + + +class MockLibid2: + @staticmethod + def unpack(data): + lib = MockLibid2() + lib.data = data + return lib + + +def test_constructor() -> None: + ref = MockLibid1() + module = ReferenceRegistered(ref) + assert isinstance(module, ReferenceRegistered) + + +def test_pack() -> None: + expected_hex = ("0D 00 68 00 00 00 5E 00 00 00 2A 5C 47 7B 30 30", + "30 32 30 34 33 30 2D 30 30 30 30 2D 30 30 30 30", + "2D 43 30 30 30 2D 30 30 30 30 30 30 30 30 30 30", + "34 36 7D 23 32 2E 30 23 30 23 43 3A 5C 57 69 6E", + "64 6F 77 73 5C 73 79 73 74 65 6D 33 32 5C 73 74", + "64 6F 6C 65 32 2E 74 6C 62 23 4F 4C 45 20 41 75", + "74 6F 6D 61 74 69 6F 6E 00 00 00 00 00 00") + expected = bytes.fromhex(" ".join(expected_hex)) + codepage = 0x04E4 + cp_name = "cp" + str(codepage) + ref_reg = ReferenceRegistered(MockLibid1()) + results = ref_reg.pack('little', cp_name) + assert results == expected + + +def test_unpack(): + hex = ("0D 00 68 00 00 00 5E 00 00 00 2A 5C 47 7B 30 30", + "30 32 30 34 33 30 2D 30 30 30 30 2D 30 30 30 30", + "2D 43 30 30 30 2D 30 30 30 30 30 30 30 30 30 30", + "34 36 7D 23 32 2E 30 23 30 23 43 3A 5C 57 69 6E", + "64 6F 77 73 5C 73 79 73 74 65 6D 33 32 5C 73 74", + "64 6F 6C 65 32 2E 74 6C 62 23 4F 4C 45 20 41 75", + "74 6F 6D 61 74 69 6F 6E 00 00 00 00 00 00") + data = bytes.fromhex(" ".join(hex)) + path = 'ms_ovba.Models.Entities.reference_registered.LibidReference' + with mock.patch(path, MockLibid2): + ref = ReferenceRegistered.unpack(data, "little") + assert ref.libid.data == ( + b'*\\G{00020430-0000-0000-C000-000000000046}#2.0#0#' + + b'C:\\Windows\\system32\\stdole2.tlb#OLE Automation' + ) + + +def test_bad_id(): + hex = ("0C 00 68 00 00 00 5E 00 00 00 2A 5C 47 7B 30 30", + "30 32 30 34 33 30 2D 30 30 30 30 2D 30 30 30 30", + "2D 43 30 30 30 2D 30 30 30 30 30 30 30 30 30 30", + "34 36 7D 23 32 2E 30 23 30 23 43 3A 5C 57 69 6E", + "64 6F 77 73 5C 73 79 73 74 65 6D 33 32 5C 73 74", + "64 6F 6C 65 32 2E 74 6C 62 23 4F 4C 45 20 41 75", + "74 6F 6D 61 74 69 6F 6E 00 00 00 00 00 00") + data = bytes.fromhex(" ".join(hex)) + path = 'ms_ovba.Models.Entities.reference_registered.LibidReference' + with mock.patch(path, MockLibid2): + with pytest.raises(Exception): + ReferenceRegistered.unpack(data, "little") diff --git a/tests/Unit/Models/Entities/test_std_module.py b/tests/Unit/Models/Entities/test_std_module.py index f1ce91845..184031cc3 100644 --- a/tests/Unit/Models/Entities/test_std_module.py +++ b/tests/Unit/Models/Entities/test_std_module.py @@ -1,13 +1,13 @@ -from vbaProjectCompiler.Models.Entities.std_module import StdModule +from unittest import mock +from ms_ovba.Models.Entities.std_module import StdModule -def test_set_get_cache(): - module = StdModule("Module1") - cache = b'foo' - module.set_cache(cache) - assert module.get_cache() == cache +path = "ms_ovba.Models.Entities.std_module.ModuleBase.__init__" -def test_get_name(): +@mock.patch(path, return_value=None) +def test_construct(mock_base_init) -> None: module = StdModule("Module1") - assert module.get_name() == "Module1" + mock_base_init.assert_called_once_with("Module1") + assert isinstance(module, StdModule) + assert module.type == "Module" diff --git a/tests/Unit/Models/Fields/test_double_encoded_string.py b/tests/Unit/Models/Fields/test_double_encoded_string.py new file mode 100644 index 000000000..94377392d --- /dev/null +++ b/tests/Unit/Models/Fields/test_double_encoded_string.py @@ -0,0 +1,23 @@ +from ms_ovba.Models.Fields.doubleEncodedString import ( + DoubleEncodedString +) + + +def test_constructor() -> None: + de_string = DoubleEncodedString([0x01, 0x02], "Foo") + assert isinstance(de_string, DoubleEncodedString) + + +def test_value_property() -> None: + expected = "Foo" + de_string = DoubleEncodedString([0x01, 0x02], expected) + assert de_string.value == expected + + +def test_pack() -> None: + codepage = 0x04E4 + cp_name = "cp" + str(codepage) + de_string = DoubleEncodedString([0x01, 0x02], "Foo") + expected = (b'\x01\x00\x03\x00\x00\x00Foo' + + b'\x02\x00\x06\x00\x00\x00F\x00o\x00o\x00') + assert de_string.pack("little", cp_name) == expected diff --git a/tests/Unit/Models/Fields/test_idSizeField.py b/tests/Unit/Models/Fields/test_idSizeField.py index a63fea652..d345bb00a 100644 --- a/tests/Unit/Models/Fields/test_idSizeField.py +++ b/tests/Unit/Models/Fields/test_idSizeField.py @@ -1,8 +1,26 @@ import pytest -from vbaProjectCompiler.Models.Fields.idSizeField import IdSizeField +from ms_ovba.Models.Fields.idSizeField import IdSizeField -def test_bad_value(): +def test_string() -> None: + field = IdSizeField(1, 2, "Hi") + expected = b'\x01\x00\x02\x00\x00\x00Hi' + assert field.pack("little") == expected + + +def test_H(): + field = IdSizeField(1, 2, 3) + expected = b'\x01\x00\x02\x00\x00\x00\x03\x00' + assert field.pack("little") == expected + + +def test_I(): + field = IdSizeField(1, 4, 3) + expected = b'\x01\x00\x04\x00\x00\x00\x03\x00\x00\x00' + assert field.pack("little") == expected + + +def test_bad_value() -> None: field = IdSizeField(2, 3, 6) with pytest.raises(Exception): - field.pack(1234, "little") + field.pack("little") diff --git a/tests/Unit/Models/Fields/test_libidReference.py b/tests/Unit/Models/Fields/test_libidReference.py index 26a323869..c812cac0f 100644 --- a/tests/Unit/Models/Fields/test_libidReference.py +++ b/tests/Unit/Models/Fields/test_libidReference.py @@ -1,10 +1,11 @@ import uuid -from vbaProjectCompiler.Models.Fields.libidReference import LibidReference +import pytest +from ms_ovba.Models.Fields.libid_reference import LibidReference -def test_str(): +def test_str() -> None: guid = uuid.UUID('0002043000000000C000000000000046') - libidRef = LibidReference( + libid_ref = LibidReference( guid, "2.0", "0", @@ -13,13 +14,13 @@ def test_str(): ) expected = ("*\\G{00020430-0000-0000-C000-000000000046}" "#2.0#0#C:\\Windows\\System32\\stdole2.tlb#OLE Automation") - assert str(libidRef) == expected - assert len(libidRef) == 94 + assert str(libid_ref) == expected + assert len(libid_ref) == 94 -def test_posix(): +def test_posix() -> None: guid = uuid.UUID('0002043000000000C000000000000046') - libidRef = LibidReference( + libid_ref = LibidReference( guid, "2.0", "0", @@ -28,4 +29,22 @@ def test_posix(): ) expected = ("*\\H{00020430-0000-0000-C000-000000000046}" "#2.0#0#//usr/bin/stdole2.tlb#OLE Automation") - assert str(libidRef) == expected + assert str(libid_ref) == expected + + +def test_unpack() -> None: + data = (b'*\\G{00020430-0000-0000-C000-000000000046}' + b'#2.0#0#C:\\Windows\\System32\\stdole2.tlb#OLE Automation') + lib = LibidReference.unpack(data) + assert lib._version == "2.0" + + +@pytest.mark.parametrize("data", [ + (b'*\\A{00020430-0000-0000-C000-000000000046}' + b'#2.0#0#C:\\Windows\\System32\\stdole2.tlb#OLE Automation'), + (b'+\\G{00020430-0000-0000-C000-000000000046}' + b'#2.0#0#C:\\Windows\\System32\\stdole2.tlb#OLE Automation'), +]) +def test_unpack_exception(data) -> None: + with pytest.raises(Exception): + LibidReference.unpack(data) diff --git a/tests/Unit/Models/Fields/test_project_reference.py b/tests/Unit/Models/Fields/test_project_reference.py new file mode 100644 index 000000000..9c8b12dc7 --- /dev/null +++ b/tests/Unit/Models/Fields/test_project_reference.py @@ -0,0 +1,41 @@ +import pytest +from ms_ovba.Models.Fields.project_reference import ProjectReference + + +def test_constructor1() -> None: + ref = ProjectReference("C:\\Example Path\\Example-ReferencedProject.xls") + assert isinstance(ref, ProjectReference) + + +def test_constructor2() -> None: + path = "C:\\Example Path\\Example-ReferencedProject.xls" + ref = ProjectReference(path, False) + assert isinstance(ref, ProjectReference) + + +def test_len() -> None: + ref = ProjectReference("C:\\Example Path\\Example-ReferencedProject.xls") + assert len(ref) == 48 + + +@pytest.mark.parametrize("data, embedded, expected", [ + ( + "C:\\Example Path\\Example-ReferencedProject.xls", + True, + "*\\CC:\\Example Path\\Example-ReferencedProject.xls" + ), + ( + "/Example Path/Example-ReferencedProject.xls", + False, + "*\\B/Example Path/Example-ReferencedProject.xls" + ) +]) +def test_str(data, embedded, expected) -> None: + ref = ProjectReference(data, embedded) + assert str(ref) == expected + + +def test_relative() -> None: + ref = ProjectReference("C:\\Example Path\\Example-ReferencedProject.xls") + rel = ref.relative() + assert str(rel) == "*\\CExample-ReferencedProject.xls" diff --git a/tests/Unit/Models/test_vbaProject.py b/tests/Unit/Models/test_vbaProject.py deleted file mode 100644 index 769ce9225..000000000 --- a/tests/Unit/Models/test_vbaProject.py +++ /dev/null @@ -1,29 +0,0 @@ -import pytest -from vbaProjectCompiler.vbaProject import VbaProject - - -def test_set_get_visibility(): - project = VbaProject() - project.set_visibility_state(0) - assert project.get_visibility_state() == 0 - - -def test_set_get_protection(): - project = VbaProject() - project.set_protection_state(0) - assert project.get_protection_state() == 0 - - -def test_set_get_password(): - project = VbaProject() - project.set_password(0) - assert project.get_password() == 0 - - -def test_bad_visibility(): - """ - Visibility must be zero or 0xFF - """ - project = VbaProject() - with pytest.raises(Exception): - project.set_visibility_state(1) diff --git a/tests/Unit/Views/test_project.py b/tests/Unit/Views/test_project.py new file mode 100644 index 000000000..659cc1c94 --- /dev/null +++ b/tests/Unit/Views/test_project.py @@ -0,0 +1,75 @@ +import unittest.mock +from ms_ovba.Views.project import Project +from typing import Type, TypeVar + + +T = TypeVar('T', bound='NotSoRandom') + + +class NotSoRandom(): + _rand = [] + + @classmethod + def set_seed(cls: Type[T], seeds: list) -> None: + cls._rand = seeds + + @classmethod + def randint(cls: Type[T], param1: int, param2: int) -> int: + return cls._rand.pop(0) + + +class Obj: + def __init__(self, name) -> None: + self.value = name + + +class Mod: + def __init__(self, name) -> None: + self.modName = Obj(name) + self.workspace = [0, 0, 0, 0, 'C'] + + def to_project_module_string(self): + return "Document" + "=" + self.modName.value + "/&H00000000" + + +class Mod1: + def __init__(self, name) -> None: + self.modName = Obj(name) + self.workspace = [26, 26, 1349, 522, 'Z'] + + def to_project_module_string(self): + return "Module=Module1" + + +class MockVbaProject: + def __init__(self) -> None: + mod1 = Mod1("Module1") + self.modules = [Mod("ThisWorkbook"), + Mod("Sheet1"), mod1] + self.project_id = '{9E394C0B-697E-4AEE-9FA6-446F51FB30DC}' + self.codepage_name = 'cp1252' + self.protection_state = b'\x00\x00\x00\x00' + self.password = b'\x00' + self.visibility_state = b'\xFF' + self.attributes = {} + + +@unittest.mock.patch('random.randint', NotSoRandom.randint) +def test_blank() -> None: + rand = [0x41, 0xBC, 0x7B, 0x7B, 0x37, 0x7B, 0x7B, 0x7B] + NotSoRandom.set_seed(rand) + vba_project = MockVbaProject() + project = Project(vba_project) + project.add_attribute("HelpContextID", "0") + project.add_attribute("VersionCompatible32", "393222000") + + project.hostExtenderInfo = ("&H00000001=" + + "{3832D640-CF90-11CF-8E43-00A0C911005A};VBE;" + + "&H00000000") + file = open("tests/blank/vbaProject.bin", "rb") + file.seek(0x2180) + expected = file.read(0x0080) + file.seek(0x2400) + expected += file.read(0x0152) + + assert project.to_bytes() == expected diff --git a/tests/Unit/Views/test_projectLk.py b/tests/Unit/Views/test_projectLk.py new file mode 100644 index 000000000..cfed0a7e8 --- /dev/null +++ b/tests/Unit/Views/test_projectLk.py @@ -0,0 +1,31 @@ +from ms_ovba.Views.projectLk import ProjectLk + + +class MockVbaProject: + def __init__(self) -> None: + self._license_records = [] + + +class MockLicense: + def to_bytes(self) -> bytes: + return b'Test' + + +def test_project_lk_empty() -> None: + vba_project = MockVbaProject() + project_lk = ProjectLk(vba_project) + + expected = (b'\x01\x00\x00\x00\x00\x00') + result = project_lk.to_bytes() + assert result == expected + + +def test_project_lk() -> None: + vba_project = MockVbaProject() + mock_license = MockLicense() + vba_project._license_records = [mock_license] + project_lk = ProjectLk(vba_project) + + expected = (b'\x01\x00\x01\x00\x00\x00Test') + result = project_lk.to_bytes() + assert result == expected diff --git a/tests/Unit/Views/test_projectWm.py b/tests/Unit/Views/test_projectWm.py new file mode 100644 index 000000000..65b359258 --- /dev/null +++ b/tests/Unit/Views/test_projectWm.py @@ -0,0 +1,29 @@ +from ms_ovba.Views.projectWm import ProjectWm + + +class Obj: + def __init__(self, name) -> None: + self.value = name + + +class Mod: + def __init__(self, name) -> None: + self.modName = Obj(name) + + +class MockVbaProject: + def __init__(self) -> None: + self.modules = [Mod("ThisWorkbook"), Mod("Sheet1"), Mod("Module1")] + + +def test_project_wm() -> None: + vba_project = MockVbaProject() + project_wm = ProjectWm(vba_project) + + expected = (b'ThisWorkbook\x00T\x00h\x00i\x00s\x00W\x00o\x00r\x00k\x00b' + + b'\x00o\x00o\x00k\x00\x00\x00Sheet1\x00S\x00h\x00e\x00e\x00' + + b't\x001\x00\x00\x00Module1\x00M\x00o\x00d\x00u\x00l\x00e' + + b'\x001\x00\x00\x00\x00\x00') + result = project_wm.to_bytes() + assert len(result) == 86 + assert result == expected diff --git a/tests/Unit/Views/test_vba_project.py b/tests/Unit/Views/test_vba_project.py new file mode 100644 index 000000000..3a2e6d35c --- /dev/null +++ b/tests/Unit/Views/test_vba_project.py @@ -0,0 +1,35 @@ +from ms_ovba.Views.project_view import ProjectView + + +class MockVbaProject(): + + def __init__(self) -> None: + self.endien = 'little' + self.performance_cache = b'' + self.performance_cache_version = 0xFFFF + + def get_performance_cache(self) -> bytes: + return self.performance_cache + + def get_performance_cache_version(self): + return self.performance_cache_version + + +def test_vba_project_default() -> None: + vba_project = MockVbaProject() + vba_project_view = ProjectView(vba_project) + expected = b'\xCC\x61\xFF\xFF\x00\x03\x00' + assert vba_project_view.to_bytes() == expected + + +def test_vba_project() -> None: + """ + Demonstrate the effect of performance cache on the project + """ + vba_project = MockVbaProject() + vba_project.performance_cache = b'\x00\x01\x02\x03' + vba_project.performance_cache_version = 0x00B5 + + vba_project_view = ProjectView(vba_project) + expected = b'\xCC\x61\xB5\x00\x00\x03\x00\x00\x01\x02\x03' + assert vba_project_view.to_bytes() == expected diff --git a/tests/Unit/test_vbaProject.py b/tests/Unit/test_vbaProject.py new file mode 100644 index 000000000..1ca43dcc6 --- /dev/null +++ b/tests/Unit/test_vbaProject.py @@ -0,0 +1,78 @@ +import pytest +from ms_ovba.vbaProject import VbaProject + + +def test_set_get_default_date() -> None: + project = VbaProject() + date = project.default_date + project.default_date = date + assert project.default_date == date + + +def test_set_get_project_id() -> None: + project = VbaProject() + project.project_id = '{test}' + assert project.project_id == '{test}' + + +def test_set_get_visibility() -> None: + project = VbaProject() + project.visibility_state = 0 + assert project.visibility_state == 0 + + +def test_set_get_protection() -> None: + project = VbaProject() + project.protection_state = 0 + assert project.protection_state == 0 + + +def test_set_get_password() -> None: + project = VbaProject() + project.password = 0 + assert project.password == 0 + + +def test_set_get_cache() -> None: + project = VbaProject() + project.performance_cache = b'0' + assert project.performance_cache == b'0' + + +def test_set_get_cache_version() -> None: + project = VbaProject() + project.performance_cache_version = 0x01 + assert project.performance_cache_version == 1 + + +def test_set_get_codepage() -> None: + project = VbaProject() + project.codepage_name = 'cp1253' + assert project.codepage_name == 'cp1253' + + +def test_set_get_cookie() -> None: + project = VbaProject() + project.project_cookie = 0x02 + assert project.project_cookie == 2 + + +def test_include_projectwm() -> None: + project = VbaProject() + project.include_projectwm() + assert project.projectwm + + +def test_exclude_projectwm() -> None: + project = VbaProject() + project.exclude_projectwm() + assert not project.projectwm + + +def test_bad_visibility() -> None: + """ + Visibility must be zero or 0xFF + """ + project = VbaProject() + with pytest.raises(Exception): + project.visibility_state = 1 diff --git a/tests/Views/test_project.py b/tests/Views/test_project.py deleted file mode 100644 index 7a2c06434..000000000 --- a/tests/Views/test_project.py +++ /dev/null @@ -1,51 +0,0 @@ -import unittest.mock - -from vbaProjectCompiler.vbaProject import VbaProject -from vbaProjectCompiler.Models.Entities.doc_module import DocModule -from vbaProjectCompiler.Models.Entities.std_module import StdModule -from vbaProjectCompiler.Views.project import Project - - -class NotSoRandom(): - _rand = [] - - @classmethod - def set_seed(cls, seeds): - cls._rand = seeds - - @classmethod - def randint(cls, param1, param2): - return cls._rand.pop(0) - - -@unittest.mock.patch('random.randint', NotSoRandom.randint) -def test_blank(): - rand = [0x41, 0xBC, 0x7B, 0x7B, 0x37, 0x7B, 0x7B, 0x7B] - NotSoRandom.set_seed(rand) - vbaProject = VbaProject() - vbaProject.setProjectId('{9E394C0B-697E-4AEE-9FA6-446F51FB30DC}') - project = Project(vbaProject) - project.addAttribute("HelpContextID", "0") - project.addAttribute("VersionCompatible32", "393222000") - - project.hostExtenderInfo = ("&H00000001=" - + "{3832D640-CF90-11CF-8E43-00A0C911005A};VBE;" - + "&H00000000") - - thisWorkbook = DocModule("ThisWorkbook") - sheet1 = DocModule("Sheet1") - module1 = StdModule("Module1") - module1.addWorkspace(26, 26, 1349, 522, 'Z') - - vbaProject.addModule(thisWorkbook) - vbaProject.addModule(sheet1) - vbaProject.addModule(module1) - - # expected = Path("tests/blank/vbaProject.bin").read_text() - file = open("tests/blank/vbaProject.bin", "rb") - file.seek(0x2180) - expected = file.read(0x0080) - file.seek(0x2400) - expected += file.read(0x0152) - - assert project.to_bytes() == expected diff --git a/tests/Views/test_projectWm.py b/tests/Views/test_projectWm.py deleted file mode 100644 index d0e92f036..000000000 --- a/tests/Views/test_projectWm.py +++ /dev/null @@ -1,22 +0,0 @@ -from vbaProjectCompiler.vbaProject import VbaProject -from vbaProjectCompiler.Models.Entities.doc_module import DocModule -from vbaProjectCompiler.Models.Entities.std_module import StdModule -from vbaProjectCompiler.Views.projectWm import ProjectWm - - -def test_projectWm(): - vbaProject = VbaProject() - projectWm = ProjectWm(vbaProject) - thisWorkbook = DocModule("ThisWorkbook") - sheet1 = DocModule("Sheet1") - module1 = StdModule("Module1") - vbaProject.addModule(thisWorkbook) - vbaProject.addModule(sheet1) - vbaProject.addModule(module1) - expected = (b'ThisWorkbook\x00T\x00h\x00i\x00s\x00W\x00o\x00r\x00k\x00b' - + b'\x00o\x00o\x00k\x00\x00\x00Sheet1\x00S\x00h\x00e\x00e\x00' - + b't\x001\x00\x00\x00Module1\x00M\x00o\x00d\x00u\x00l\x00e' - + b'\x001\x00\x00\x00\x00\x00') - result = projectWm.toBytes() - assert len(result) == 86 - assert projectWm.toBytes() == expected diff --git a/tests/Views/test_vba_project.py b/tests/Views/test_vba_project.py deleted file mode 100644 index 0f18a0e37..000000000 --- a/tests/Views/test_vba_project.py +++ /dev/null @@ -1,13 +0,0 @@ -from vbaProjectCompiler.vbaProject import VbaProject -from vbaProjectCompiler.Views.vba_Project import Vba_Project - - -def test_projectWm(): - vbaProject = VbaProject() - vba_Project = Vba_Project(vbaProject) - expected = b'\xCC\x61\xFF\xFF\x00\x03\x00' - assert vba_Project.toBytes() == expected - vbaProject.setPerformanceCache(b'\x00\x01\x02\x03') - vbaProject.setPerformanceCacheVersion(0x00B5) - expected = b'\xCC\x61\xB5\x00\x00\x03\x00\x00\x01\x02\x03' - assert vba_Project.toBytes() == expected diff --git a/tests/blank/project.uml b/tests/blank/project.uml new file mode 100644 index 000000000..8d84cafe4 --- /dev/null +++ b/tests/blank/project.uml @@ -0,0 +1,19 @@ +@startuml +digraph G { + "Root Entry" [fillcolor="red" style="filled" fontcolor="white"]; + VBA [fillcolor="red" style="filled" fontcolor="white"]; + PROJECTwm [fillcolor="red" style="filled" fontcolor="white"] + dir [fillcolor="red" style="filled" fontcolor="white"] + _VBA_PROJECT [fillcolor="red" style="filled" fontcolor="white"] + "Root Entry" -> PROJECT + PROJECT -> VBA + PROJECT -> PROJECTwm + VBA -> Module1 + Module1 -> Sheet1 + Module1 -> ThisWorkbook + Sheet1 -> dir + Sheet1 -> nil + ThisWorkbook -> _VBA_PROJECT + ThisWorkbook -> nil +} +@enduml diff --git a/tests/test_datetime.py b/tests/test_datetime.py deleted file mode 100644 index ac9b8c6b6..000000000 --- a/tests/test_datetime.py +++ /dev/null @@ -1,16 +0,0 @@ -import datetime - - -def test_datetime(): - # use a library - input = 0x01D92433C2B823C0 - date = filetime2datetime(input) - assert date.ctime() == "Mon Jan 9 14:07:51 2023" - - -def filetime2datetime(filetime): - """ - convert FILETIME (64 bits int) to Python datetime.datetime - """ - _FILETIME_null_date = datetime.datetime(1601, 1, 1, 0, 0, 0) - return _FILETIME_null_date + datetime.timedelta(microseconds=filetime//10) diff --git a/vbaProjectCompiler/Models/Entities/__init__.py b/vbaProjectCompiler/Models/Entities/__init__.py deleted file mode 100644 index 33123adf7..000000000 --- a/vbaProjectCompiler/Models/Entities/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Does a comment help this pass? diff --git a/vbaProjectCompiler/Models/Entities/module_base.py b/vbaProjectCompiler/Models/Entities/module_base.py deleted file mode 100644 index e6676e25d..000000000 --- a/vbaProjectCompiler/Models/Entities/module_base.py +++ /dev/null @@ -1,77 +0,0 @@ -from ms_ovba_compression.ms_ovba import MsOvba -from vbaProjectCompiler.Models.Fields.doubleEncodedString import ( - DoubleEncodedString -) -from vbaProjectCompiler.Models.Fields.packedData import PackedData -from vbaProjectCompiler.Models.Fields.idSizeField import IdSizeField - - -class ModuleBase(): - def __init__(self, name): - """ - Initialize the module record - """ - self.modName = DoubleEncodedString([0x0019, 0x0047], name) - self.streamName = DoubleEncodedString([0x001A, 0x0032], name) - self.docString = DoubleEncodedString([0x001C, 0x0048], "") - self.helpContext = IdSizeField(0x001E, 4, 0) - self.cookie = IdSizeField(0x002C, 2, 0xFFFF) - - # self.readonly = SimpleRecord(0x001E, 4, helpContext) - # self.private = SimpleRecord(0x001E, 4, helpContext) - self._cache = b'' - self.workspace = [0, 0, 0, 0, 'C'] - self.type = '' - self.created = 0 - self.modified = 0 - self._fileSize = 0 - self._size = 0 - - def set_cache(self, cache): - self._cache = cache - - def get_cache(self): - return self._cache - - def get_name(self): - return self.modName.value - - def addWorkspace(self, val1, val2, val3, val4, val5): - self.workspace = [val1, val2, val3, val4, val5] - - def pack(self, codePageName, endien): - """ - Pack the metadata for use in the dir stream. - """ - typeIdValue = 0x0022 if self.type == 'Document' else 0x0021 - typeId = PackedData("HI", typeIdValue, 0) - self.offsetRec = IdSizeField(0x0031, 4, len(self._cache)) - output = (self.modName.pack(codePageName, endien) - + self.streamName.pack(codePageName, endien) - + self.docString.pack(codePageName, endien) - + self.offsetRec.pack(codePageName, endien) - + self.helpContext.pack(codePageName, endien) - + self.cookie.pack(codePageName, endien) - + typeId.pack(codePageName, endien)) - footer = PackedData("HI", 0x002B, 0) - output += footer.pack(codePageName, endien) - return output - - def toProjectModuleString(self): - return self.type + "=" + self.modName.value - - def add_file(self, file_path): - self._file_path = file_path - - def write_file(self): - bin_f = open(self._file_path + ".bin", "wb") - bin_f.write(self._cache) - with open(self._file_path + ".new", mode="rb") as new_f: - contents = new_f.read() - ms_ovba = MsOvba() - compressed = ms_ovba.compress(contents) - bin_f.write(compressed) - bin_f.close() - - def _attr(self, name, value): - return 'Attribute VB_' + name + ' = ' + value + '\n' diff --git a/vbaProjectCompiler/Models/Entities/referenceRecord.py b/vbaProjectCompiler/Models/Entities/referenceRecord.py deleted file mode 100644 index 2d1ce917c..000000000 --- a/vbaProjectCompiler/Models/Entities/referenceRecord.py +++ /dev/null @@ -1,22 +0,0 @@ -from vbaProjectCompiler.Models.Fields.doubleEncodedString import ( - DoubleEncodedString -) -from vbaProjectCompiler.Models.Fields.packedData import PackedData - - -class ReferenceRecord(): - - def __init__(self, codePageName, name, libidRef): - self.codePageName = codePageName - self.RefName = DoubleEncodedString([0x0016, 0x003E], name) - self.libidRef = libidRef - - def pack(self, codePageName, endien): - strlen = len(self.libidRef) - format = "HII" + str(strlen) + "sIH" - lib_str = str(self.libidRef).encode(self.codePageName) - refRegistered = PackedData(format, 0x000D, strlen + 10, - strlen, lib_str, 0, 0) - - return (self.RefName.pack(codePageName, endien) - + refRegistered.pack(codePageName, endien)) diff --git a/vbaProjectCompiler/Models/Fields/__init__.py b/vbaProjectCompiler/Models/Fields/__init__.py deleted file mode 100644 index 33123adf7..000000000 --- a/vbaProjectCompiler/Models/Fields/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Does a comment help this pass? diff --git a/vbaProjectCompiler/Models/Fields/doubleEncodedString.py b/vbaProjectCompiler/Models/Fields/doubleEncodedString.py deleted file mode 100644 index 64f517d94..000000000 --- a/vbaProjectCompiler/Models/Fields/doubleEncodedString.py +++ /dev/null @@ -1,19 +0,0 @@ -from vbaProjectCompiler.Models.Fields.idSizeField import IdSizeField - - -class DoubleEncodedString(): - """ - Encode text data twice with different ids and lengths - """ - def __init__(self, ids, text): - self.ids = ids - self.value = text - - def pack(self, codePageName, endien): - encoded = self.value.encode(codePageName) - self.modName1 = IdSizeField(self.ids[0], len(encoded), encoded) - format = "utf_16_le" if endien == 'little' else "utf_16_be" - encoded = self.value.encode(format) - self.modName2 = IdSizeField(self.ids[1], len(encoded), encoded) - return (self.modName1.pack(codePageName, endien) - + self.modName2.pack(codePageName, endien)) diff --git a/vbaProjectCompiler/Models/Fields/idSizeField.py b/vbaProjectCompiler/Models/Fields/idSizeField.py deleted file mode 100644 index 363bc3092..000000000 --- a/vbaProjectCompiler/Models/Fields/idSizeField.py +++ /dev/null @@ -1,31 +0,0 @@ -import struct - - -class IdSizeField(): - """ - Many Records have the same format, a two bye ID, a four byte size and an - int value formatted to the defined size. - """ - - def __init__(self, id, size, value): - self.id = id - self.size = size - self.value = value - - def pack(self, codePageName, endien): - endienSymbol = '<' if endien == 'little' else '>' - format = endienSymbol + "HI" - if isinstance(self.value, str): - self.stringValue = self.value - self.value = bytes(self.value, encoding="ascii") - format += str(self.size) + "s" - elif isinstance(self.value, bytes): - format += str(self.size) + "s" - elif self.size == 2: - format += "H" - elif self.size == 4: - format += "I" - else: - msg = "Received data of type " + type(self.value).__name__ - raise Exception(msg) - return struct.pack(format, self.id, self.size, self.value) diff --git a/vbaProjectCompiler/Models/Fields/libidReference.py b/vbaProjectCompiler/Models/Fields/libidReference.py deleted file mode 100644 index 7f49f9110..000000000 --- a/vbaProjectCompiler/Models/Fields/libidReference.py +++ /dev/null @@ -1,27 +0,0 @@ -class LibidReference(): - def __init__(self, libidGuid, version, - libidLcid, libidPath, libidRegName): - self.libidGuid = libidGuid - self.version = version - self.libidLcid = libidLcid - self.libidPath = libidPath - self.libidRegName = libidRegName - if self._is_windows_path(libidPath): - self.libidReferenceKind = "G" - else: - self.libidReferenceKind = "H" - - def __str__(self): - return "*\\" + \ - self.libidReferenceKind + \ - "{" + str(self.libidGuid).upper() + "}#" + \ - self.version + "#" + \ - self.libidLcid + "#" + \ - str(self.libidPath) + "#" + \ - self.libidRegName - - def __len__(self): - return len(str(self)) - - def _is_windows_path(self, path): - return path[0] != '/' diff --git a/vbaProjectCompiler/Models/Fields/packedData.py b/vbaProjectCompiler/Models/Fields/packedData.py deleted file mode 100644 index c1e901c6e..000000000 --- a/vbaProjectCompiler/Models/Fields/packedData.py +++ /dev/null @@ -1,14 +0,0 @@ -import struct - - -class PackedData(): - """ - Mutivalue field with a packing format - """ - def __init__(self, format, *values): - self.values = values - self.format = format - - def pack(self, codePageName, endien): - endienSymbol = '<' if endien == 'little' else '>' - return struct.pack(endienSymbol + self.format, *self.values) diff --git a/vbaProjectCompiler/Views/dirStream.py b/vbaProjectCompiler/Views/dirStream.py deleted file mode 100644 index 7b9316aa4..000000000 --- a/vbaProjectCompiler/Views/dirStream.py +++ /dev/null @@ -1,71 +0,0 @@ -import struct -from vbaProjectCompiler.Models.Fields.idSizeField import IdSizeField -from vbaProjectCompiler.Models.Fields.doubleEncodedString import ( - DoubleEncodedString -) -from vbaProjectCompiler.Models.Fields.packedData import PackedData - - -class DirStream(): - """ - The dir stream is compressed on write - """ - - def __init__(self, project): - self.project = project - self.codePage = 0x04E4 - # 0=16bit, 1=32bit, 2=mac, 3=64bit - syskind = IdSizeField(1, 4, 3) - compatVersion = IdSizeField(74, 4, 3) - lcid = IdSizeField(2, 4, 0x0409) - lcidInvoke = IdSizeField(20, 4, 0x0409) - codePageRecord = IdSizeField(3, 2, self.codePage) - projectName = IdSizeField(4, 10, "VBAProject") - docString = DoubleEncodedString([5, 0x0040], "") - helpfile = DoubleEncodedString([6, 0x003D], "") - helpContext = IdSizeField(7, 4, 0) - libFlags = IdSizeField(8, 4, 0) - version = IdSizeField(9, 4, 0x65BE0257) - minorVersion = PackedData("H", 17) - constants = DoubleEncodedString([12, 0x003C], "") - self.information = [ - syskind, - compatVersion, - lcid, - lcidInvoke, - codePageRecord, - projectName, - docString, - helpfile, - helpContext, - libFlags, - version, - minorVersion, - constants - ] - self.references = [] - self.modules = [] - - def to_bytes(self): - endien = self.project.endien - codePageName = self.project.getCodePageName() - packSymbol = '<' if endien == 'little' else '>' - # should be 0xFFFF - cookie_value = self.project.projectCookie - self.projectCookie = IdSizeField(19, 2, cookie_value) - self.references = self.project.references - self.modules = self.project.modules - output = b'' - for record in self.information: - output += record.pack(codePageName, endien) - for record in self.references: - output += record.pack(codePageName, endien) - - modulesHeader = IdSizeField(0x000F, 2, len(self.modules)) - - output += (modulesHeader.pack(codePageName, endien) - + self.projectCookie.pack(codePageName, endien)) - for record in self.modules: - output += record.pack(codePageName, endien) - output += struct.pack(packSymbol + "HI", 16, 0) - return output diff --git a/vbaProjectCompiler/Views/project.py b/vbaProjectCompiler/Views/project.py deleted file mode 100644 index 082d9c3f9..000000000 --- a/vbaProjectCompiler/Views/project.py +++ /dev/null @@ -1,69 +0,0 @@ -import binascii -import ms_ovba_crypto - - -class Project: - """ - The Project data view for the vbaProject - """ - def __init__(self, project): - self.project = project - # Attributes - - # A list of attributes and values - self.attributes = {} - - # The HostExtenderInfo string - self.hostExtenderInfo = "" - - def addAttribute(self, name, value): - self.attributes[name] = value - - def to_bytes(self): - project = self.project - codePageName = project.getCodePageName() - # Use \x0D0A line endings...however python encodes that. - eol = b'\x0D\x0A' - project_id = project.getProjectId() - id = bytearray(project_id, codePageName) - result = b'ID="' + id + b'"' + eol - modules = project.modules - for module in modules: - result += bytes(module.toProjectModuleString(), codePageName) + eol - result += b'Name="VBAProject"' + eol - for key in self.attributes: - result += self._attr(key, self.attributes[key]) - cmg = ms_ovba_crypto.encrypt( - project_id, - project.get_protection_state() - ) - dpb = ms_ovba_crypto.encrypt(project_id, project.get_password()) - gc = ms_ovba_crypto.encrypt(project_id, project.get_visibility_state()) - result += (bytes('CMG="', codePageName) - + binascii.hexlify(cmg).upper() - + b'\x22\x0D\x0A') - result += (bytes('DPB="', codePageName) - + binascii.hexlify(dpb).upper() - + b'\x22\x0D\x0A') - result += (bytes('GC="', codePageName) - + binascii.hexlify(gc).upper() - + b'\x22\x0D\x0A') - result += eol - result += b'[Host Extender Info]' + eol - result += bytes(self.hostExtenderInfo, codePageName) - result += eol + eol - result += b'[Workspace]' + eol - for module in modules: - separator = ", " - result += bytes(module.modName.value, codePageName) + b'=' - joined = separator.join(map(str, module.workspace)) - result += bytes(joined, codePageName) - result += eol - return result - - def _attr(self, name, value): - codePageName = self.project.getCodePageName() - eol = b'\x0D\x0A' - b_name = bytes(name, codePageName) - b_value = bytes(value, codePageName) - return b_name + b'="' + b_value + b'"' + eol diff --git a/vbaProjectCompiler/Views/vba_Project.py b/vbaProjectCompiler/Views/vba_Project.py deleted file mode 100644 index 9543458d9..000000000 --- a/vbaProjectCompiler/Views/vba_Project.py +++ /dev/null @@ -1,22 +0,0 @@ -import struct - - -class Vba_Project: - """ - The _VBA_PROJECT data view for the vbaProject - """ - def __init__(self, project): - self.project = project - - def toBytes(self): - endienSymbol = '<' if self.project.endien == 'little' else '>' - format = endienSymbol + "HHBH" - output = b'' - reserved1 = 0x61CC - reserved2 = 0x00 - reserved3 = 0x0003 - cache_version = self.project.getPerformanceCacheVersion() - - output += struct.pack(format, reserved1, cache_version, - reserved2, reserved3) - return output + self.project.getPerformanceCache() diff --git a/vbaProjectCompiler/__init__.py b/vbaProjectCompiler/__init__.py deleted file mode 100644 index 33123adf7..000000000 --- a/vbaProjectCompiler/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Does a comment help this pass? diff --git a/vbaProjectCompiler/main.py b/vbaProjectCompiler/main.py deleted file mode 100644 index 6df828706..000000000 --- a/vbaProjectCompiler/main.py +++ /dev/null @@ -1,2 +0,0 @@ -def main(args): - pass diff --git a/vbaProjectCompiler/vbaProject.py b/vbaProjectCompiler/vbaProject.py deleted file mode 100644 index c22da84d0..000000000 --- a/vbaProjectCompiler/vbaProject.py +++ /dev/null @@ -1,117 +0,0 @@ -# from ms_cfb import OleFile -# from ms_cfb.Models.Directories.storage_directory import StorageDirectory -# from ms_cfb.Models.Directories.stream_directory import StreamDirectory - -# from vbaProjectCompiler.Views.dirStream import DirStream -# from vbaProjectCompiler.Views.vba_Project import Vba_Project -# from vbaProjectCompiler.Views.project import Project -# from vbaProjectCompiler.Views.projectWm import ProjectWm - - -class VbaProject: - - def __init__(self): - - self.endien = 'little' - - # Protected Instance Attributes - self._codePageName = 'cp1252' - self._projectId = '{}' - self._protection_state = b'\x00\x00\x00\x00' - self._password = b'\x00' - self._visibility_state = b'\xFF' - self._performanceCache = b'' - self._performanceCacheVersion = 0xFFFF - - # A list of directories - self.directories = [] - self.references = [] - self.modules = [] - - self.projectCookie = 0xFFFF - - # Getters and Setters - def setProjectId(self, id): - self._projectId = id - - def getProjectId(self): - return self._projectId - - def set_protection_state(self, state): - self._protection_state = state - - def get_protection_state(self): - return self._protection_state - - def set_visibility_state(self, state): - """ - 0 = not visible - 255 = visible - """ - if state != 0 and state != 255: - raise Exception("Bad visibility value.") - self._visibility_state = state - - def get_visibility_state(self): - return self._visibility_state - - def set_password(self, value): - self._password = value - - def get_password(self): - return self._password - - def setPerformanceCache(self, cache): - self._performanceCache = cache - - def getPerformanceCache(self): - return self._performanceCache - - def setPerformanceCacheVersion(self, version): - self._performanceCacheVersion = version - - def getPerformanceCacheVersion(self): - return self._performanceCacheVersion - - def getCodePageName(self): - return self._codePageName - - def setProjectCookie(self, value): - self.projectCookie = value - - # Appenders - def addModule(self, ref): - self.modules.append(ref) - - def addReference(self, ref): - self.references.append(ref) - - def _create_binary_files(self): - for module in self.modules: - module.write_file() - # views = ("_VBA_PROJECT", "dir", "projectWm", "Project") - # Create views and write - - def _build_ole_directory(self): - # directory = StorageDirectory() - # directory.set_name("VBA") - for module in self.modules: - # path = module.get_name() + '.bin' - # dir = StreamDirectory() - # dir.set_name(module.get_name()) - # dir.add_stream(path) - # directory.add_directory(dir) - pass - # return directory - - def _write_ole_file(self, dir): - # ole_file = OleFile() - # ole_file.add_directory(dir) - # ole_file.build_file() - # ole_file.write_file("vbaProject.bin") - pass - - def write_file(self): - self._create_binary_files() - directory = self._build_ole_directory() - self._write_ole_file(directory)