Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
1b755a0
Update dirStream.py
Beakerboy Apr 3, 2026
7ea7b17
Update dirStream.py
Beakerboy Apr 3, 2026
2cca6e9
Update dirStream.py
Beakerboy Apr 3, 2026
cbd9555
Update dirStream.py
Beakerboy Apr 3, 2026
ad88e1f
Update dirStream.py
Beakerboy Apr 3, 2026
3b03231
Update dirStream.py
Beakerboy Apr 3, 2026
9ed06ad
Update dirStream.py
Beakerboy Apr 3, 2026
48836a0
Update dirStream.py
Beakerboy Apr 3, 2026
e0cc3fc
Update dirStream.py
Beakerboy Apr 3, 2026
e67157e
Create test_dir.py
Beakerboy Apr 3, 2026
24f15be
Update test_dir.py
Beakerboy Apr 3, 2026
8d9c0ae
Update test_dir.py
Beakerboy Apr 3, 2026
d2c4694
Update test_dir.py
Beakerboy Apr 3, 2026
4e4fc3e
Update test_dir.py
Beakerboy Apr 3, 2026
bd2b3ef
Update test_dir.py
Beakerboy Apr 3, 2026
ad7ab1d
Update test_dir.py
Beakerboy Apr 3, 2026
e69c868
Update test_dir.py
Beakerboy Apr 3, 2026
3322f30
Update test_dir.py
Beakerboy Apr 3, 2026
086a63c
Update dirStream.py
Beakerboy Apr 3, 2026
13f3826
Update dirStream.py
Beakerboy Apr 3, 2026
c479cdc
Update dirStream.py
Beakerboy Apr 3, 2026
557634c
Update dirStream.py
Beakerboy Apr 3, 2026
2c05d14
Update dirStream.py
Beakerboy Apr 3, 2026
6f75714
Update dirStream.py
Beakerboy Apr 3, 2026
eb2df34
Update dirStream.py
Beakerboy Apr 3, 2026
916c725
Update dirStream.py
Beakerboy Apr 3, 2026
2f27b59
Update dirStream.py
Beakerboy Apr 3, 2026
0caf0bf
Update dirStream.py
Beakerboy Apr 3, 2026
c53d846
Update dirStream.py
Beakerboy Apr 3, 2026
df71cb8
Update dirStream.py
Beakerboy Apr 3, 2026
db479a0
Update dirStream.py
Beakerboy Apr 3, 2026
a4720be
Update dirStream.py
Beakerboy Apr 3, 2026
3e3db64
Update dirStream.py
Beakerboy Apr 3, 2026
86ca4c2
Update dirStream.py
Beakerboy Apr 3, 2026
2424913
Update dirStream.py
Beakerboy Apr 3, 2026
245baed
Update test_dir.py
Beakerboy Apr 3, 2026
925808a
Update test_dir.py
Beakerboy Apr 3, 2026
7876ac3
Update test_dir.py
Beakerboy Apr 3, 2026
0ce9887
Update test_dir.py
Beakerboy Apr 4, 2026
a8ba111
Update test_dir.py
Beakerboy Apr 4, 2026
00bbc1a
Update test_dir.py
Beakerboy Apr 4, 2026
9affbda
Update test_dir.py
Beakerboy Apr 4, 2026
a02dc2d
Update test_dir.py
Beakerboy Apr 4, 2026
6269753
Update test_dir.py
Beakerboy Apr 4, 2026
e3367a2
Update dirStream.py
Beakerboy Apr 4, 2026
b7684f5
Update dirStream.py
Beakerboy Apr 4, 2026
90ad079
Update dirStream.py
Beakerboy Apr 4, 2026
4020ecf
Update dirStream.py
Beakerboy Apr 4, 2026
cbb51d0
Update dirStream.py
Beakerboy Apr 4, 2026
abe12f6
Update dirStream.py
Beakerboy Apr 4, 2026
5f01835
Update test_dir.py
Beakerboy Apr 4, 2026
ed5abf9
Update dirStream.py
Beakerboy Apr 4, 2026
b0138e6
Update dirStream.py
Beakerboy Apr 4, 2026
83f568d
Update dirStream.py
Beakerboy Apr 4, 2026
7f83e63
Update test_dir.py
Beakerboy Apr 4, 2026
c5c4957
Update dirStream.py
Beakerboy Apr 4, 2026
ceccf73
Update dir
Beakerboy Apr 4, 2026
ec79c0c
Update dirStream.py
Beakerboy Apr 4, 2026
7bc55ec
Update dirStream.py
Beakerboy Apr 4, 2026
708ac38
Update dirStream.py
Beakerboy Apr 4, 2026
b0ffd29
Update dirStream.py
Beakerboy Apr 4, 2026
59f2d1a
Update dirStream.py
Beakerboy Apr 4, 2026
d3406a2
Update dirStream.py
Beakerboy Apr 4, 2026
6bcd520
Update dirStream.py
Beakerboy Apr 4, 2026
7814c77
Update dirStream.py
Beakerboy Apr 4, 2026
643aa57
Update dirStream.py
Beakerboy Apr 4, 2026
6b2cc35
Update dirStream.py
Beakerboy Apr 4, 2026
5163e93
Update dirStream.py
Beakerboy Apr 4, 2026
0a0219e
Update dirStream.py
Beakerboy Apr 4, 2026
68a9585
Update dirStream.py
Beakerboy Apr 4, 2026
8f0d63b
Update dirStream.py
Beakerboy Apr 4, 2026
7cbc60e
Update dirStream.py
Beakerboy Apr 4, 2026
f6a22b2
Update dirStream.py
Beakerboy Apr 4, 2026
b7873ff
Update dirStream.py
Beakerboy Apr 4, 2026
d0c8488
Update dirStream.py
Beakerboy Apr 4, 2026
ce67d31
Update dirStream.py
Beakerboy Apr 4, 2026
a463718
Update dirStream.py
Beakerboy Apr 4, 2026
2da0d58
Update dirStream.py
Beakerboy Apr 4, 2026
491b816
Update reference_record.py
Beakerboy Apr 4, 2026
c41aa1b
Update reference_record.py
Beakerboy Apr 4, 2026
cbf1014
Update dirStream.py
Beakerboy Apr 4, 2026
09bbe1c
Update reference_record.py
Beakerboy Apr 4, 2026
6852ed8
Update reference_record.py
Beakerboy Apr 4, 2026
611c510
Update dirStream.py
Beakerboy Apr 4, 2026
53d98ee
Update reference.py
Beakerboy Apr 4, 2026
bd37376
Update reference.py
Beakerboy Apr 4, 2026
74490cd
Update reference.py
Beakerboy Apr 4, 2026
c0d8e3a
Update dirStream.py
Beakerboy Apr 4, 2026
a1db67b
Update dirStream.py
Beakerboy Apr 4, 2026
671229b
Update dirStream.py
Beakerboy Apr 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions src/ms_ovba/Models/Entities/reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,12 @@ def unpack(data: bytes, endien: str) -> Reference:
endien_symbol = '<' if endien == 'little' else '>'
name = ''
offset = 0
id = struct.unpack_from(endien_symbol + "H", data, offset)
id, size1 = struct.unpack_from(f"{endien_symbol}HI", 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
offset += 6
format = f"{endien_symbol}{size1}sHI"
name, id, size2 = struct.unpack_from(format, data, offset)
offset += size1 + 6
if size2 != size1 * 2:
# raise warning
pass
Expand Down
4 changes: 2 additions & 2 deletions src/ms_ovba/Models/Entities/reference_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def unpack(bytestring: bytes, endien: str) -> ReferenceRecord:
ReferenceOriginal
)
endien_symbol = '<' if endien == 'little' else '>'
id = struct.unpack(endien_symbol + "H", bytestring)
id, = struct.unpack_from(f"{endien_symbol}H", bytestring, 0)
ref: ReferenceRecord
if id == 0x000D:
ref = ReferenceRegistered.unpack(bytestring, endien)
Expand All @@ -33,5 +33,5 @@ def unpack(bytestring: bytes, endien: str) -> ReferenceRecord:
elif id == 0x0033:
ref = ReferenceOriginal.unpack(bytestring, endien)
else:
raise Exception("Unknown Reference Type")
raise Exception(f"Unknown Reference Type: {id}")
return ref
192 changes: 191 additions & 1 deletion src/ms_ovba/Views/dirStream.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import struct
import warnings
from ms_ovba_compression.ms_ovba import MsOvba
from ms_ovba.vbaProject import VbaProject
from ms_ovba.Models.Entities.reference import Reference
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
from typing import List, TypedDict, TypeVar


T = TypeVar('T', bound='DirStream')
Expand All @@ -15,6 +17,14 @@
PackableData = DoubleEncodedString | IdSizeField | PackedData


class Parameters(TypedDict):
references: list[Reference]
modules: list
help_context_id: int
project_cookie: int
codepage_name: str


class DirStream():
"""
The dir stream is compressed on write
Expand Down Expand Up @@ -93,3 +103,183 @@ def _load_information(self: T) -> List:
constants
])
return information

@staticmethod
def is_valid(data: bytes) -> bool:
try:
DirStream.from_bytes(data)
except Exception as e:
warnings.warn(str(e), SyntaxWarning)
return False
return True

@staticmethod
def from_bytes(data: bytes) -> Parameters:
"""
Static validation: Checks if bytes follow the DirStream structure.
Expects decompressed bytes.
"""
project_data: Parameters = {
"references": [],
"modules": [],
"help_context_id": 0,
"project_cookie": 0,
"codepage_name": "cp1252"
}
offset = 0

# 1. Check PROJECTSYSKIND (Mandatory first record)
# IdSizeField(1, 4, 3) -> ID=1 (2 bytes), Size=4 (4 bytes)
record_id, size, value = struct.unpack_from("<HII", data, offset)
if record_id != 1 or size != 4 or not (0 <= value <= 3):
raise ValueError("Incorrect PROJECTSYSKIND")
offset += 10

record_id, size, value = struct.unpack_from("<HII", data, offset)
if record_id == 0x4A:
if size != 4:
raise ValueError("Incorrect PROJECTCOMPATVERSION")
offset += 10
record_id, size, value = (struct.unpack_from("<HII", data, offset))
if record_id != 2 or size != 4 or value != 0x409:
raise ValueError("Incorrect PROJECTLCID")
offset += 10

record_id, size, value = struct.unpack_from("<HII", data, offset)
if record_id != 0x14 or size != 4 or value != 0x409:
raise ValueError("Incorrect PROJECTLCIDINVOKE")
offset += 10

record_id, size, value = struct.unpack_from("<HIH", data, offset)
if record_id != 3 or size != 2:
raise ValueError("Incorrect PROJECTCODEPAGE")
offset += 8

record_id, size = struct.unpack_from("<HI", data, offset)
value, = struct.unpack_from(f"{size}s", data, offset + 6)
if record_id != 4 or not (1 <= size <= 128):
raise ValueError("Incorrect PROJECTNAME")
offset += 6 + size

record_id, size = struct.unpack_from("<HI", data, offset)
value, r2, s2, v2 = struct.unpack_from(
f"<{size}sHI{2*size}s", data, offset + 6)
if record_id != 5 or size > 2000 or s2 != 2 * size:
raise ValueError(
f"Incorrect PROJECTDOCSTRING({record_id}, {size}, {r2}, {s2})")
offset += 12 + size * 3

record_id, size = struct.unpack_from("<HI", data, offset)
value, r2, s2, v2 = struct.unpack_from(
f"<{size}sHI{size}s", data, offset + 6)
if record_id != 6 or size > 260 or s2 != size or value != v2:
raise ValueError(
f"Incorrect PROJECTHELPFILEPATH({record_id}, {size}, {s2})")
offset += 12 + size * 2

record_id, size, value = struct.unpack_from("<HII", data, offset)
if record_id != 7 or size != 4:
raise ValueError("Incorrect PROJECTHELPCONTEXT")
offset += 10

record_id, size, value = struct.unpack_from("<HII", data, offset)
if record_id != 8 or size != 4 or value != 0:
raise ValueError("Incorrect PROJECTLIBFLAGS")
offset += 10

record_id, size, value, v2 = struct.unpack_from("<HIIH", data, offset)
if record_id != 9:
raise ValueError("Incorrect PROJECTVERSION")
offset += 12

record_id, size = struct.unpack_from("<HI", data, offset)
value, r2, s2, v2 = struct.unpack_from(
f"<{size}sHI{size}s", data, offset + 6)
if record_id != 0x0c or size > 2015 or s2 != 2 * size:
raise ValueError("Incorrect PROJECTCONSTANTS")
offset += 12 + size * 3

record_id, size = struct.unpack_from("<HI", data, offset)
found_one_reference = False
while (record_id != 0x0f or not found_one_reference):
found_one_reference = True
record_size = 0
if record_id == 0x16:
record_size = 12 + size * 3
record_id, size = struct.unpack_from(
"<HI", data, offset + record_size)
match record_id:
case 0x2f:
record_size += 6 + size
record_id, size = struct.unpack_from(
"<HI", data, offset + record_size)
if record_id == 0x16:
record_size = 12 + size * 3
record_id, size = struct.unpack_from(
"<HI", data, offset + record_size)
record_size += 6 + size
case 0x33:
record_size += 6 + size
record_id, size = struct.unpack_from(
"<HI", data, offset + record_size)
record_size += 6 + size
record_id, size = struct.unpack_from(
"<HI", data, offset + record_size)
if record_id == 0x16:
record_size = 12 + size * 3
record_id, size = struct.unpack_from(
"<HI", data, offset + record_size)
record_size += 6 + size
case 0x0d | 0x0e:
record_size += 6 + size
case _:
raise ValueError(f"Unknown Reference Type: {record_id}")
ref = Reference.unpack(
data[offset:offset + record_size], "little")
project_data["references"] += [ref]
offset += record_size
record_id, size = struct.unpack_from("<HI", data, offset)

if record_id != 0x0f or size != 2:
raise ValueError("Expected ModuleRecord")
count, record_id, size, cookie = struct.unpack_from(
"<HHIH", data, offset)
if record_id != 0x13 or size != 2:
raise ValueError(f"Incorrect PROJECTCOOKIE({record_id}, {size})")
project_data["project_cookie"] = cookie
for _ in range(count):
module_data = {}
record_size = 0
record_id, size = struct.unpack_from("<HI", data, offset)
record_size += 6
value, r2, s2, v2 = struct.unpack_from(
f"<{size}sHI{size*2}s", data, offset + record_size)
record_size += size * 3 + 6
# validate sizes and that values match
module_data["name"] = value
record_id, size = struct.unpack_from(
f"<{size}sHI", data, offset + record_size)
record_size += 6
value, record_id, size = struct.unpack_from(
f"<{size}sHI", data, offset + record_size)
module_data["stream_name"] = value
record_size += size + 6
value, record_id, size = struct.unpack_from(
f"<{size}sHI", data, offset + record_size)
module_data["docstring"] = value
record_size += size + 6
project_data["modules"] += [module_data]
offset += record_size
return project_data

@staticmethod
def is_file_valid(file_path: str) -> bool:
"""Helper to validate a compressed .bin file on disk."""
try:
with open(file_path, "rb") as f:
compressed = f.read()
from ms_ovba_compression.ms_ovba import MsOvba
decompressed = MsOvba().decompress(compressed)
return DirStream.is_valid(decompressed)
except Exception:
return False
52 changes: 52 additions & 0 deletions tests/Unit/Views/test_dir.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import pytest
from pathlib import Path
from ms_cfb.ole_file import OleFile
from ms_ovba.Views.dirStream import DirStream
from ms_ovba_compression.ms_ovba import MsOvba
from unittest import mock


mock_vbaproject = mock.Mock()


@pytest.fixture
def my_fixture() -> None:
# Setup: Runs BEFORE the test
# print("\nSetting up...")
yield
# Teardown: Runs AFTER the test
Path("tests/blank/dir.bin").unlink(missing_ok=True)


def test_construct() -> None:
dir = DirStream(mock_vbaproject)
assert isinstance(dir, DirStream)


@pytest.mark.usefixtures("my_fixture")
def test_is_valid() -> None:
file = "tests/blank/vbaProject.bin"
ole_file = OleFile.create_from_file(file)
ole_file.extract_stream('dir', 'tests/blank')
with open('tests/blank/dir.bin', 'rb') as f:
compressed_data = f.read()

# Use MsOvba to decompress the stream
ms_ovba = MsOvba()
decompressed_data = ms_ovba.decompress(compressed_data)
assert DirStream.is_valid(decompressed_data)


@pytest.mark.usefixtures("my_fixture")
def test_from_bytes() -> None:
expected = {}
file = "tests/blank/vbaProject.bin"
ole_file = OleFile.create_from_file(file)
ole_file.extract_stream('dir', 'tests/blank')
with open('tests/blank/dir.bin', 'rb') as f:
compressed_data = f.read()

# Use MsOvba to decompress the stream
ms_ovba = MsOvba()
decompressed_data = ms_ovba.decompress(compressed_data)
assert DirStream.from_bytes(decompressed_data) == expected
27 changes: 16 additions & 11 deletions tests/blank/dir
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
00000000 01 00 04 00 00 00 03 00 00 00 4A 00 04 00 00 00 ..........J.....
00000010 03 00 00 00 02 00 04 00 00 00 09 04 00 00 14 00 ................
00000020 04 00 00 00 09 04 00 00 03 00 02 00 00 00 E4 04 ..............ä.
00000030 04 00 0A 00 00 00 56 42 41 50 72 6F 6A 65 63 74 ......VBAProject
00000040 05 00 00 00 00 00 40 00 00 00 00 00 06 00 00 00 ......@.........
00000050 00 00 3D 00 00 00 00 00 07 00 04 00 00 00 00 00 ..=.............
00000060 00 00 08 00 04 00 00 00 00 00 00 00 09 00 04 00 ................
00000070 00 00 57 02 BE 65 11 00 0C 00 00 00 00 00 3C 00 ..W.¾e........<.
00000080 00 00 00 00 16 00 06 00 00 00 73 74 64 6F 6C 65 ..........stdole References begin at 84
00000090 3E 00 0C 00 00 00 73 00 74 00 64 00 6F 00 6C 00 >.....s.t.d.o.l.
000000A0 65 00 0D 00 68 00 00 00 5E 00 00 00 2A 5C 47 7B e...h...^...*\G{
01 00 04 00 00 00 03 00 00 00 ..........
4A 00 04 00 00 00 03 00 00 00 J.........
02 00 04 00 00 00 09 04 00 00 ..........
14 00 04 00 00 00 09 04 00 00 ..........
03 00 02 00 00 00 E4 04 ......ä.
04 00 0A 00 00 00 56 42 41 50 72 6F 6A 65 63 74 ......VBAProject
05 00 00 00 00 00 40 00 00 00 00 00 ......@.....
06 00 00 00 00 00 3D 00 00 00 00 00 ......=.....
07 00 04 00 00 00 00 00 00 00 ..........
08 00 04 00 00 00 00 00 00 00 ..........
09 00 04 00 00 00 57 02 BE 65 11 00 ......W.¾e..
0C 00 00 00 00 00 3C 00 00 00 00 00 ......<.....

16 00 06 00 00 00 73 74 64 6F 6C 65 3E 00 0C 00 ......stdole>...
00 00 73 00 74 00 64 00 6F 00 6C 00 65 00 ..s.t.d.o.l.e.
0D 00 68 00 00 00 5E 00 00 00 2A 5C 47 7B ..h...^...*\G{
000000B0 30 30 30 32 30 34 33 30 2D 30 30 30 30 2D 30 30 00020430-0000-00
000000C0 30 30 2D 43 30 30 30 2D 30 30 30 30 30 30 30 30 00-C000-00000000
000000D0 30 30 34 36 7D 23 32 2E 30 23 30 23 43 3A 5C 57 0046}#2.0#0#C:\W
Expand Down
Loading