From 74076b7ff4a838f751240cf444309b1cf1ada892 Mon Sep 17 00:00:00 2001 From: BinFlip Date: Fri, 9 Oct 2026 18:57:33 -0700 Subject: [PATCH] chore: release 0.5.0 Every name of the ProjectInfo2 name area now has an owner: the private object descriptor's +0x20 and +0x24 arrays, misread as a method name table and a parameter name table, describe an object's public variables and implemented interfaces, and its events' prototypes. Adds structure extents for every structure the crate reads, member and event prototype descriptors, type library, interface and record type references, UDT layouts, constant pool entry roles and pool descriptors, DISPID maps, native unwind records with their error handler, Resume and line number tables, native code entry discovery, event names from the embedded type library, and late-bound call resolution by DISPID. Breaking: renames PrivateObjectDescriptor, ControlInfo, ArgType and InterfaceMetadata (now TypeLibRef) accessors, widens FuncTypDesc::dispid to i32, and removes vb::varstub, whose structure does not exist. Adds fixtures errors-native, eventnames, records, typerefs and udts. See CHANGELOG.md for the full list of changes. --- CHANGELOG.md | 197 ++- Cargo.lock | 9 +- Cargo.toml | 3 +- README.md | 28 +- build.rs | 20 +- data/msvbvm60_exports.csv | 1273 +++++++++-------- examples/dump.rs | 69 +- src/entrypoint.rs | 102 ++ src/extents.rs | 1132 +++++++++++++++ src/lib.rs | 3 + src/pcode/calltarget.rs | 409 +++++- src/pcode/decoder.rs | 142 ++ src/project/methodentry.rs | 9 +- src/project/methodlink.rs | 35 +- src/project/mod.rs | 8 +- src/project/vbcontrol.rs | 34 +- src/project/vbobject.rs | 262 +++- src/project/vbproject.rs | 462 +++++- src/vb/control.rs | 213 ++- src/vb/controlprop.rs | 190 ++- src/vb/events.rs | 80 +- src/vb/exports.rs | 8 + src/vb/external.rs | 7 + src/vb/formdata.rs | 26 + src/vb/functype.rs | 114 +- src/vb/header.rs | 30 +- src/vb/member.rs | 249 ++++ src/vb/mod.rs | 10 +- src/vb/native.rs | 497 +++++++ src/vb/object.rs | 7 +- src/vb/pooldesc.rs | 495 +++++++ src/vb/privateobj.rs | 91 +- src/vb/procedure.rs | 27 + src/vb/projectdata.rs | 16 +- src/vb/projectinfo2.rs | 299 +--- src/vb/typeref.rs | 339 +++++ src/vb/varstub.rs | 330 ----- tests/api.rs | 456 ++++++ tests/descriptors.rs | 334 +++++ tests/extents.rs | 229 +++ tests/fixtures/README.md | 7 +- tests/fixtures/dispid/Host.frm | 20 + tests/fixtures/dispid/dispid.exe | Bin 20480 -> 20480 bytes tests/fixtures/errors-native/Guard.cls | 38 + tests/fixtures/errors-native/Program.bas | 110 ++ .../fixtures/errors-native/errors-native.exe | Bin 0 -> 20480 bytes .../fixtures/errors-native/errors-native.vbp | 30 + tests/fixtures/eventnames/First.cls | 22 + tests/fixtures/eventnames/Program.bas | 8 + tests/fixtures/eventnames/Quiet.cls | 24 + tests/fixtures/eventnames/Second.cls | 24 + tests/fixtures/eventnames/Third.cls | 23 + tests/fixtures/eventnames/eventnames.exe | Bin 0 -> 12288 bytes tests/fixtures/eventnames/eventnames.vbp | 33 + tests/fixtures/records/Item.cls | 31 + tests/fixtures/records/Program.bas | 70 + tests/fixtures/records/records.exe | Bin 0 -> 12288 bytes tests/fixtures/records/records.vbp | 30 + tests/fixtures/typerefs/Shapes.cls | 47 + tests/fixtures/typerefs/Watcher.cls | 36 + tests/fixtures/typerefs/typerefs.dll | Bin 0 -> 20480 bytes tests/fixtures/typerefs/typerefs.vbp | 30 + tests/fixtures/udts/Mod1.bas | 13 + tests/fixtures/udts/Second.cls | 30 + tests/fixtures/udts/Shapes.cls | 69 + tests/fixtures/udts/udts.dll | Bin 0 -> 24576 bytes tests/fixtures/udts/udts.vbp | 31 + tests/structures.rs | 850 ++++++++++- 68 files changed, 8305 insertions(+), 1515 deletions(-) create mode 100644 src/extents.rs create mode 100644 src/vb/member.rs create mode 100644 src/vb/native.rs create mode 100644 src/vb/pooldesc.rs create mode 100644 src/vb/typeref.rs delete mode 100644 src/vb/varstub.rs create mode 100644 tests/descriptors.rs create mode 100644 tests/extents.rs create mode 100644 tests/fixtures/errors-native/Guard.cls create mode 100644 tests/fixtures/errors-native/Program.bas create mode 100755 tests/fixtures/errors-native/errors-native.exe create mode 100644 tests/fixtures/errors-native/errors-native.vbp create mode 100644 tests/fixtures/eventnames/First.cls create mode 100644 tests/fixtures/eventnames/Program.bas create mode 100644 tests/fixtures/eventnames/Quiet.cls create mode 100644 tests/fixtures/eventnames/Second.cls create mode 100644 tests/fixtures/eventnames/Third.cls create mode 100755 tests/fixtures/eventnames/eventnames.exe create mode 100644 tests/fixtures/eventnames/eventnames.vbp create mode 100644 tests/fixtures/records/Item.cls create mode 100644 tests/fixtures/records/Program.bas create mode 100755 tests/fixtures/records/records.exe create mode 100644 tests/fixtures/records/records.vbp create mode 100644 tests/fixtures/typerefs/Shapes.cls create mode 100644 tests/fixtures/typerefs/Watcher.cls create mode 100644 tests/fixtures/typerefs/typerefs.dll create mode 100644 tests/fixtures/typerefs/typerefs.vbp create mode 100644 tests/fixtures/udts/Mod1.bas create mode 100644 tests/fixtures/udts/Second.cls create mode 100644 tests/fixtures/udts/Shapes.cls create mode 100644 tests/fixtures/udts/udts.dll create mode 100644 tests/fixtures/udts/udts.vbp diff --git a/CHANGELOG.md b/CHANGELOG.md index cd1c15f..c6723bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,200 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.5.0] - 2026-10-09 + +Every name of the ProjectInfo2 name area now has an owner: the private +object descriptor's +0x20 and +0x24 arrays, misread as a method name table +and a parameter name table, describe an object's public variables and +implemented interfaces, and its events' prototypes. + +### Added + +- **Structure extents** (`extents`): `VbProject::structure_extents` + states the extent of every structure the crate reads (`StructureExtent`, + `StructureKind`), each measured by the walk that reads it: the headers + and tables, each object's descriptors, method, method link, GUID and + control tables, event sinks and DISPID maps, prototypes, members, record + layouts, constant pools and the data their entries name, procedure + descriptors with their line tables, P-Code, the external and component + tables, form data, COM registration data, the code markers, and native + procedures' unwind records with their tables. Native stubs and procedures + are never an extent. +- **Pool entry roles**: `Instruction::pool_references` lists every constant + pool entry an instruction names with what its opcode says it holds + (`PoolReference`, `PoolEntryRole`). +- **Pool descriptors** (`vb::pooldesc`): `RecordIoDescriptor` (the + recursive descriptor `Get #` and `Put #` of a record read, with + `RecordIoEntry`, `RecordIoKind`), `IoItems` (the item list of `Print`, + `Write` and `Input`, with `IoItem`, `IoSeparator`), `ArrayDescriptor` + (the `SAFEARRAY` header of a fixed array in a record) and + `CreationDescriptor` (what `New` creates when it is not a project class). +- **Record layouts of an object's `Type`s**: `VbObject::record_layouts`. +- **DISPID maps**: `ControlInfo::{dispid_map, dispid_map_count, + dispid_map_va}` read the `{source DISPID, handler MEMID}` pairs + (`DispidMapping`) of a `WithEvents` variable's or an `Implements`' entry; + `ControlInfo::sink_slots` gives its event sink vtable's slot count. +- `MethodLinkKind::Variable::pushed`: the pool index and pool VA a `New` + variable's accessor pushes. +- `VbObject::interface_iid_va`: the IID the compiler writes before an + object's CLSID. +- `VbHeader::reserved_guid`, `TypeLibRef::flags`, `RecordLayout::ansi_size`, + `ExternalDeclareInfo::BLOCK_SIZE`, `FuncTypDesc::size`, `MemberDesc::size`, + `EventSinkVtable::size`, `ProcDscInfo::extent_size` (with the line-number + table), `ProjectInfo2Iter::position`. +- Fixtures `records` (record I/O, arrays in records, record arrays, `For + Each` into a typed variable) and `udts` (`Public` and `Private` `Type`s of + classes). +- **Member descriptors** (`vb::member`): `MemberDesc` reads a public + variable's or an `Implements`' name, DISPID, accessor vtable offset, + instance offset and type (with the class's or interface's `ObjectInfo` + VA); `MemberKind` tells variables, `WithEvents` variables and + `Implements` apart. `VbObject::members` walks an object's array, so its + public variables and their count are recoverable. +- **Event prototypes**: `VbObject::event_type_descs` yields the + `FuncTypDesc` of each `Event` declaration, with its parameter names. +- **Name ownership**: `VbProject::name_references` lists every pointer to a + parameter, event parameter, variable or interface name with its owner + (`NameOwner`), including the names past where the ProjectInfo2 walk + stops. +- **Control kinds**: `ControlInfo::kind` (`ControlKind::{Control, + WithEvents, Implements}`, from the flags at +0x00, documented as always + 0x0040 before), `ControlInfo::{dispatch_slots, event_count}`; + `EventSinkVtable::{dispatch_va, dispatch_slots}` read a dual sink's + `IDispatch` slots. +- **Type references** (`vb::typeref`): `TypeLibRef` (a type library's GUID, + version, LCID, path and name), `InterfaceRef` (an interface's library and + IID) and `RecordRef` (a user-defined type's library, GUID, version and + LCID). `ArgType::{interface, record}` read the descriptor of an interface + (codes 0x1C, 0x1D) or record (0x14) type; `ArgType::{IUNKNOWN, + INTERFACE}` name the interface codes. +- **UDT layouts and array element types** (`vb::controlprop`): + `RecordLayout` reads a user-defined type's size and the members that need + init or cleanup; `ControlPropertyEntry::{record_layout_va, + element_layout_va, record_layout}` name it from a UDT member or an array + of UDTs, `element_type` and `safearray_{offset, features, iid_va, + vartype}` read an array's element type, inline `SAFEARRAY` descriptor and + the IID or `VARTYPE` after it. +- `ProjectInfo2::object_descs` reads the per-object PrivateObjectDescriptor + array. +- `VbProject::export_stubs` decodes an ActiveX DLL's or OCX's COM export + stubs (`entrypoint::ExportStub`): the VBHeader and `.data` VAs each + pushes and the `VBDll*` runtime function it jumps to. +- **Native unwind records** (`vb::native`): `ProcUnwindInfo` reads the + record a native procedure's prologue stores in its frame, in all four + sizes: the addresses of the code that releases its `Me`, its variables + and its statement's temporaries when it unwinds, and of its error + handling's tables, `ErrorHandlerTable` (each `On Error GoTo` label, + `ErrorHandler`), `ResumeTable` (each statement's address, for `Resume` + and `Resume Next`) and `LineNumberTable` (each statement's line number, + for `Erl`). `ProcUnwindInfo::from_prologue` finds the record from the + procedure's start, `ProcUnwindInfo::code_vas` lists the code it names, + and `VbProject::unwind_records` (`ProcedureUnwind::scan`) finds every + procedure that stores one, including those no table names. +- **Native code**: `VbProject::native_entries` lists every address of + native code the project's structures name (the entry point and its stub + target, export stubs, entry stubs, method link thunks and targets, event + sink thunks and handlers, pool stubs, dual-entry partners, the procedures + adjustor thunks enter and every procedure that stores an unwind record); + `VbProject::native_code_range` gives the region between the code markers + `ProjectData::{code_start_va, code_end_va}` name, empty for a P-Code + build; `VbProject::entry_point_va` and `entrypoint::entry_stub_target` + decode the PE entry point's stub (an executable's `push imm32; call` and + a DLL's `pop edx; push imm32; push imm32; push edx; jmp`). + `NativeEventThunk::JUMP_OFFSET` is the bare jump of a native adjustor + thunk. +- `ExportSignature::noreturn` (a sixth column of the export table): the + runtime functions that never return to their caller (`__vbaError`, + `__vbaErrorOverflow`, `__vbaGenerateBoundsError`, `__vbaFailedFriend`, + `__vbaFPException`, `__vbaEnd`, `__vbaStopExe`, `rtcAppleScript`, + `ThunRTMain`). +- Fixture `errors-native`: error handling compiled to native code, each + procedure varying one table of its unwind record. +- `CodeEntry::target_va`: a native build's event handler stub enters the + procedure at this VA (a native class has no method table to index). +- **Event names**: `VbObject::event_names` names each `Event` declaration + from the project's type library (`VbProject::type_library_bytes`, the + `TYPELIB` resource of an ActiveX DLL, OCX or EXE), matching the event's + DISPID in the events dispinterface. A Standard EXE has no type library: + its event names are pooled strings no structure points to, and stay + unnamed. +- Fixture `eventnames`: classes sharing member and event name spellings. +- Fixture `typerefs`: an ActiveX DLL with interface- and UDT-typed members, + parameters and return values, and `WithEvents` variables of a project + class and of a `TextBox`. +- **Late-bound calls by DISPID resolve**: `Callee::Late` carries the + member its DISPID names (`LateTarget`) when the receiver's class is + known: a project UserControl's procedure, a hosted ActiveX control's + member, or the control extender's (`_VBControlExtender`, whose members + the compiler calls 0x3000 below the DISPIDs it declares, + `RuntimeInterfaces::{CONTROL_EXTENDER, extender_member}`). + `InterfaceCatalog::dispatched` describes a member by DISPID (empty by + default); `InvokeKind::{of_late_call, admits}` tell how a late-bound + opcode invokes its member, `InvokeKind::accessor_prefix` the name COM + gives the accessor. +- **Hosted controls name their class**: `FormControlRecord::{prog_id, + prog_id_bytes}` read the ProgID a hosted control's record begins with, + with or without an event handler; a form's getter for the control + returns its class's default interface + (`CallResolver::returned_interface`), and the getter of a control array + of one its elements' (`CallResolver::returned_elements`), which + `SlotInterfaces` gives to the slot the array's `Item` + (`RuntimeInterfaces::CONTROL_ARRAY_ITEM`) writes. + +### Changed + +- `PrivateObjectDescriptor::{var_stub_count, var_stubs_va}` are now + `record_layout_count` and `record_layouts_va`: the array holds the record + layouts of the object's `Type` declarations. +- `ControlInfo::{dispid_count_or_zero, dispid_table_va}` are now + `dispid_map_count` (the u16 at +0x10) and `dispid_map_va`: the map is on + disk, not filled at runtime. +- `VbHeader::SIZE` is 0x78 and `TypeLibRef::SIZE` 0x28, the sizes the + compiler writes. +- `ControlPropertyEntry::{record_layout_va, element_layout_va}` return + `None` for 0 and 0xFFFFFFFF, which a UDT with no member to initialize or + release holds. +- Depends on `msft-typelib` 0.2.0 to read the embedded type library. +- `PrivateObjectDescriptor::method_name_table_va` is now `member_descs_va` + and `param_names_va` is now `event_descs_va`. +- `read_name_strings` documents what its strings are (parameter, event + parameter, variable and interface names) and where its walk stops. +- `EventSinkVtable::parse` takes the entry's `ControlInfo`. +- `InterfaceMetadata` is now `vb::typeref::TypeLibRef`, with + `ControlTypeEntry::typelib_va` and `typelib` for `interface_metadata_va` + and `interface_metadata`; its fields at +0x08 and +0x0C, documented as + always 6 and 9, are the library's version and LCID (`MSMask` 1.1, LCID 0). +- `ArgType::object_va` is now `descriptor_va`: for an interface or record + type it is not an object. +- `FuncTypDesc::dispid` returns the whole DISPID as an `i32`, the four + bytes at +0x0C (0x60030000 plus the index for a Sub or Function, + 0x68030000 for a property procedure, an event's number from 1), which + is what a late-bound caller names the member by; it read only the low + word. + +### Removed + +- `vb::varstub` (`VarStubDesc`, `VarStubIter`) and `VbObject::{var_stubs, + var_stub_count}`: no structure of that format exists; the array they read + is the record layouts of the object's `Type`s. +- `InterfaceMetadata::{dispatch_names, all_dispatch_names}`: the first read + the library name, now `TypeLibRef::name`; the second scanned 4 KiB past + it for identifier-like strings that belong to other structures. + +### Fixed + +- `EventSinkVtable` of an `Implements` entry spans its four trailing zero + slots: every sink is `0x0C + 4 * (slots + 3)` bytes, `+ 7` with flag bit + 0x08. +- The event handlers of a `WithEvents` variable: its sink vtable is a dual + interface's, with four `IDispatch` slots before the handlers, and + `VbControl::event_handler_va`, `EventSinkVtable::handler_va`, + `VbObject::events` and `VbObject::code_entries` read those slots as the + first handlers (`Listener.m_Source` gave the `IDispatch` thunks for + `Started`, `Ticked` and `Named`). An `Implements` sink has the same four + slots, which its handler count included: they no longer appear as event + handlers. + ## [0.4.0] - 2026-10-08 P-Code analysis: the opcode table covers every handler of `MSVBVM60.DLL` @@ -493,7 +687,8 @@ Affects `vbobject.rs`, `pcodemethod.rs`, `methodlink.rs`, Initial public release. -[Unreleased]: https://github.com/ATRAPSLLC/visualbasic-rs/compare/v0.4.0...HEAD +[Unreleased]: https://github.com/ATRAPSLLC/visualbasic-rs/compare/v0.5.0...HEAD +[0.5.0]: https://github.com/ATRAPSLLC/visualbasic-rs/compare/v0.4.0...v0.5.0 [0.4.0]: https://github.com/ATRAPSLLC/visualbasic-rs/compare/v0.3.2...v0.4.0 [0.3.2]: https://github.com/ATRAPSLLC/visualbasic-rs/compare/v0.3.1...v0.3.2 [0.3.1]: https://github.com/ATRAPSLLC/visualbasic-rs/compare/v0.3.0...v0.3.1 diff --git a/Cargo.lock b/Cargo.lock index b64036d..24d94a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,12 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +[[package]] +name = "msft-typelib" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e512f180d2ef529bfc4bb9354857f3cd2f9e8da320d74d8fc177c8cd0ce48aa1" + [[package]] name = "num-traits" version = "0.2.19" @@ -357,9 +363,10 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "visualbasic" -version = "0.4.0" +version = "0.5.0" dependencies = [ "goblin", + "msft-typelib", "proptest", "tracing", ] diff --git a/Cargo.toml b/Cargo.toml index 218afae..57b690c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "visualbasic" -version = "0.4.0" +version = "0.5.0" edition = "2024" rust-version = "1.88" description = "Parse and inspect Visual Basic 6 compiled binaries" @@ -37,6 +37,7 @@ tracing = ["dep:tracing"] [dependencies] goblin = "0.10.7" +msft-typelib = "0.2.0" tracing = { version = "0.1.44", optional = true, default-features = false, features = ["std"] } [dev-dependencies] diff --git a/README.md b/README.md index 1e877a1..f9412a4 100644 --- a/README.md +++ b/README.md @@ -47,17 +47,34 @@ for obj in project.objects()? { ## What it parses -- **PE entry point** detection (EXE push-stub and DLL export patterns) +- **PE entry point** detection (EXE push-stub and DLL export patterns) and + the COM export stubs of an ActiveX DLL or OCX - **VBHeader**, **ProjectData**, **ObjectTable** and the full structure chain - **PublicObjectDescriptor**, **ObjectInfo**, **OptionalObjectInfo**, **PrivateObjectDescriptor** +- **Members and prototypes**: public variables and `Implements`, procedure + and event prototypes with their parameter names, and who owns each name +- **Type references**: type libraries, interfaces and user-defined types a + member or parameter is typed by; UDT layouts and array element types - **P-Code bytecode**: opcode tables, operand decoding, streaming instruction iterator; each opcode's effect on the evaluation and x87 stacks and on Pr, - the object register, read from the runtime's handlers -- **Controls**: ControlInfo, event sink vtables, event handler thunks + the object register, read from the runtime's handlers; what each constant + pool entry an instruction names holds +- **Pool descriptors**: the record I/O descriptors of `Get #` and `Put #`, + the item lists of `Print`, `Write` and `Input`, the array headers of + fixed arrays in records, and the creation descriptors of `New` +- **Structure extents**: where every structure the crate reads lies in the + image, for a disassembler that must not decode it +- **Controls**: ControlInfo, event sink vtables (controls, `WithEvents`, + `Implements`), event handler thunks +- **Native code**: every procedure that stores an unwind record, named by a + table or not, with its error handlers, `Resume` targets and `Erl` line + numbers; every address of native code the structures name; the region + between the code markers - **COM metadata**: GUIDs, TypeLib registration, external component tables - **Form binary data**: control trees, property streams, font/picture resources - **MSVBVM60.DLL exports**: every export by name and ordinal, with signatures - for most, so the runtime functions P-Code imports by ordinal are named + for most and whether it returns, so the runtime functions P-Code imports + by ordinal are named - **Imports**: the PE import table by import address table slot ## High-level walkers @@ -76,6 +93,9 @@ each substructure by hand: `Mixed` binaries (combines the project flag with a per-object scan). - [`VbProject::diagnostics()`] - eager parse-health probe surfacing missing optional structures and known-anomaly patterns. +- [`VbProject::structure_extents()`] - the extent of every structure the + crate reads (headers, tables, descriptors, procedure descriptors and + P-Code, constant pool data), each measured by the walk that reads it. ## Analysing P-Code diff --git a/build.rs b/build.rs index 81553ce..9d1a1fa 100644 --- a/build.rs +++ b/build.rs @@ -1275,6 +1275,7 @@ fn generate_msvbvm60_exports(out_dir: &str) { cc: String, ret_type: String, variadic: bool, + noreturn: bool, params: Vec<(String, String)>, // (type, name) category: String, } @@ -1290,9 +1291,9 @@ fn generate_msvbvm60_exports(out_dir: &str) { continue; } - // name,ordinal,calling_convention,return_type,variadic,params,category,notes - let parts: Vec<&str> = line.splitn(8, ',').collect(); - if parts.len() < 7 { + // name,ordinal,calling_convention,return_type,variadic,noreturn,params,category,notes + let parts: Vec<&str> = line.splitn(9, ',').collect(); + if parts.len() < 8 { panic!("too few columns at line {}", line_num + 1); } @@ -1304,8 +1305,13 @@ fn generate_msvbvm60_exports(out_dir: &str) { let cc = parts[2].trim().to_string(); let ret_type = parts[3].trim().to_string(); let variadic = parts[4].trim() == "1"; - let params_str = parts[5].trim(); - let category = parts[6].trim().to_string(); + let noreturn = match parts[5].trim() { + "0" => false, + "1" => true, + other => panic!("bad noreturn '{other}' at line {}", line_num + 1), + }; + let params_str = parts[6].trim(); + let category = parts[7].trim().to_string(); // Validate calling convention and return type at build time map_calling_conv(&cc); @@ -1336,6 +1342,7 @@ fn generate_msvbvm60_exports(out_dir: &str) { cc, ret_type, variadic, + noreturn, params, category, }); @@ -1379,12 +1386,13 @@ fn generate_msvbvm60_exports(out_dir: &str) { }; writeln!( out, - " ExportSignature {{ name: {:?}, ordinal: {}, calling_convention: {}, return_type: {}, variadic: {}, params: {}, category: {:?} }},", + " ExportSignature {{ name: {:?}, ordinal: {}, calling_convention: {}, return_type: {}, variadic: {}, noreturn: {}, params: {}, category: {:?} }},", entry.name, entry.ordinal, map_calling_conv(&entry.cc), map_param_type(&entry.ret_type), entry.variadic, + entry.noreturn, params_ref, entry.category, ) diff --git a/data/msvbvm60_exports.csv b/data/msvbvm60_exports.csv index 7b442bf..688f8b7 100644 --- a/data/msvbvm60_exports.csv +++ b/data/msvbvm60_exports.csv @@ -7,704 +7,705 @@ # calling_convention - fastcall|stdcall|cdecl|special|unknown # return_type - unknown|void|int16|uint16|int32|uint32|int64|uint8|float|double|bool|Bstr|BstrPtr|VariantPtr|SafeArrayPtr|SafeArrayPtrPtr|IUnknownPtr|IUnknownPtrPtr|IDispatchPtr|IDispatchPtrPtr|Hresult|GuidPtr|VoidPtr|Int32Ptr|Int16Ptr|UInt8Ptr|Int64Ptr # variadic - 0|1 (1 = cdecl varargs) +# noreturn - 0|1 (1 = never returns to its caller: it raises an error or ends the process) # params - Semicolon-separated: Type name (e.g. BstrPtr pbstr;Bstr src) # category - free|string|variant|convert|object|array|foreach|late|error|currency|fpu|file|misc|crt|fdiv|evtsink|entry|mem|internal # notes - Freeform RE notes -name,ordinal,calling_convention,return_type,variadic,params,category,notes +name,ordinal,calling_convention,return_type,variadic,noreturn,params,category,notes # # === FREE / CLEANUP === # -__vbaFreeObj,129,fastcall,void,0,VoidPtr ppobj,free,ecx=ppobj; *ppobj→Release then clear -__vbaFreeObjList,243,cdecl,void,1,int32 count,free,varargs: count void** ptrs follow -__vbaFreeStr,130,fastcall,void,0,BstrPtr pbstr,free,ecx=pbstr; *pbstr→SysFreeString then clear -__vbaFreeStrList,244,cdecl,void,1,int32 count,free,varargs: count BSTR* ptrs follow -__vbaFreeVar,131,fastcall,void,0,VariantPtr pvar,free,ecx=pvar; dispatches by vt (SysFreeString/Release/SafeArrayDestroy/VariantClear) -__vbaFreeVarg,132,fastcall,void,0,,free,Frees a VARIANT arg; dispatches by vt: SysFreeString for BSTR; Release for objects; SafeArrayDestroy for arrays; else __vbaFreeVar -__vbaFreeVarList,245,cdecl,void,1,int32 count,free,varargs: count VARIANT* ptrs follow +__vbaFreeObj,129,fastcall,void,0,0,VoidPtr ppobj,free,ecx=ppobj; *ppobj→Release then clear +__vbaFreeObjList,243,cdecl,void,1,0,int32 count,free,varargs: count void** ptrs follow +__vbaFreeStr,130,fastcall,void,0,0,BstrPtr pbstr,free,ecx=pbstr; *pbstr→SysFreeString then clear +__vbaFreeStrList,244,cdecl,void,1,0,int32 count,free,varargs: count BSTR* ptrs follow +__vbaFreeVar,131,fastcall,void,0,0,VariantPtr pvar,free,ecx=pvar; dispatches by vt (SysFreeString/Release/SafeArrayDestroy/VariantClear) +__vbaFreeVarg,132,fastcall,void,0,0,,free,Frees a VARIANT arg; dispatches by vt: SysFreeString for BSTR; Release for objects; SafeArrayDestroy for arrays; else __vbaFreeVar +__vbaFreeVarList,245,cdecl,void,1,0,int32 count,free,varargs: count VARIANT* ptrs follow # # === STRING OPERATIONS === # -__vbaInStr,277,stdcall,int32,0,int32 compare_mode;Bstr src;Bstr search;int32 start,string,InStr function; retn 0x10 -__vbaInStrB,278,stdcall,int32,0,int32 compare_mode;Bstr str1;Bstr str2;int32 start,string,InStrB (byte-level); binary search with memcmp -__vbaInStrVar,279,stdcall,VariantPtr,0,VariantPtr presult;int32 compare_mode;VariantPtr pstr1;VariantPtr pstr2;int32 start,string,InStr for variants -__vbaInStrVarB,280,stdcall,VariantPtr,0,VariantPtr presult;int32 compare_mode;VariantPtr pstr1;VariantPtr pstr2;int32 start,string,InStrB for variants -__vbaLenBstr,327,stdcall,int32,0,Bstr bstr,string,returns char count (byte_len/2); retn 4 -__vbaLenBstrB,328,stdcall,int32,0,Bstr bstr,string,returns byte count (raw [bstr-4]); retn 4 -__vbaLenVar,329,stdcall,VariantPtr,0,VariantPtr pResult;VariantPtr pVar,string,0x6604c83a; retn 0x8; returns Len() of variant; coerces to string then calls __vbaLenBstr -__vbaLenVarB,330,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pvar,string,LenB for variants; returns VT_I4 with byte count -__vbaLineInputStr,331,stdcall,void,0,BstrPtr pdst;int16 fileNum,string,Line Input# statement -__vbaLineInputVar,332,stdcall,void,0,VariantPtr pVar;int16 fileNum,string,0x660ea5f6; retn 0x8; reads a line into VARIANT (delegates to __vbaLineInputStr for BSTR subtype) -__vbaLsetFixstr,333,stdcall,Bstr,0,uint32 fixedLen;Bstr dest;Bstr src,string,LSet for fixed strings; left-aligned with space padding -__vbaLsetFixstrFree,334,stdcall,Bstr,0,uint32 fixedLen;Bstr dest;Bstr src,string,LSet wrapper; frees src after -__vbaMidStmtBstr,335,stdcall,void,0,int32 destMaxChars;Bstr src;int32 len;int32 start;BstrPtr pdst,string,Mid$ statement (char-level) -__vbaMidStmtBstrB,336,stdcall,void,0,int32 destMaxChars;Bstr src;int32 lenBytes;int32 startByte;BstrPtr pdst,string,Mid$ statement (byte-level); core impl -__vbaMidStmtVar,337,stdcall,void,0,VariantPtr pdst;int32 len;int32 start;VariantPtr psrc,string,Mid$ for variants; doubles len/start and calls VarB -__vbaMidStmtVarB,338,stdcall,void,0,VariantPtr pdst;int32 lenBytes;int32 startByte;VariantPtr psrc,string,Mid$ for variants (byte-level) -__vbaR4Str,367,stdcall,float,0,Bstr bstr,string,BSTR→float via VarR4FromStr; returns on FPU st0 -__vbaRsetFixstr,392,stdcall,Bstr,0,uint32 fixedLen;Bstr dest;Bstr src,string,RSet for fixed strings; right-aligned with space padding -__vbaRsetFixstrFree,393,stdcall,Bstr,0,uint32 fixedLen;Bstr dest;Bstr src,string,RSet wrapper; frees src after -__vbaStr2Vec,396,stdcall,void,0,SafeArrayPtrPtr ppArray;Bstr bstr,string,BSTR→1D byte SAFEARRAY -__vbaStrAryToAnsi,397,stdcall,VoidPtr,0,SafeArrayPtr ppArray;SafeArrayPtr ppAnsiArray,string,Wraps internal helper with direction=1 (to ANSI); converts SAFEARRAY of BSTRs to ANSI -__vbaStrAryToUnicode,398,stdcall,VoidPtr,0,SafeArrayPtr ppArray;SafeArrayPtr ppUniArray,string,Wraps internal helper with direction=0 (to Unicode); converts SAFEARRAY of ANSI strings to BSTRs -__vbaStrBool,399,stdcall,Bstr,0,int16 boolVal,string,Converts VB Boolean (int16) to BSTR via VariantChangeType(VT_BOOL->VT_BSTR) -__vbaStrCat,405,stdcall,Bstr,0,Bstr bstr1;Bstr bstr2,string,concatenation via internal helper; retn 8 -__vbaStrCmp,406,stdcall,int32,0,Bstr bstr1;Bstr bstr2,string,binary compare wrapper; calls __vbaStrComp(0 bstr1 bstr2) -__vbaStrComp,407,stdcall,int32,0,int32 mode;Bstr bstr1;Bstr bstr2,string,mode: 0=binary 1=text; retn 0xC -__vbaStrCompVar,408,stdcall,VariantPtr,0,VariantPtr presult;int32 compare_mode;VariantPtr pvar1;VariantPtr pvar2,string,StrComp for variants; returns VT_I2 -__vbaStrCopy,138,fastcall,Bstr,0,BstrPtr pdst;Bstr src,string,ecx=pdst edx=src; SysAllocStringByteLen+SysFreeString -__vbaStrCy,409,stdcall,Bstr,0,int64 cyVal,string,Converts Currency (8-byte CY) to BSTR via VarBstrFromCy -__vbaStrDate,415,stdcall,Bstr,0,double dateVal,string,Converts DATE (double) to BSTR via VarBstrFromDate; respects calendar setting -__vbaStrErrVarCopy,111,stdcall,Bstr,0,VariantPtr pVar,string,Thin wrapper around rtBstrFromErrVar; extracts error BSTR from Variant -__vbaStrFixstr,416,stdcall,Bstr,0,Bstr bstrSrc;uint32 cchLen,string,Allocates fixed-length BSTR via SysAllocStringLen; error 0xe (out of memory) on failure -__vbaStrI2,417,stdcall,Bstr,0,int16 value,string,int16→BSTR via VarBstrFromI2 -__vbaStrI4,418,stdcall,Bstr,0,int32 value,string,int32→BSTR via VarBstrFromI4 -__vbaStrLike,419,stdcall,int32,0,Bstr bstrText;Bstr bstrPattern,string,Case-sensitive Like comparison; passes mode=0 to internal __vbaLikeStr; returns bool as i32 -__vbaStrMove,139,fastcall,Bstr,0,BstrPtr pdst;Bstr src,string,ecx=pdst edx=src; moves ownership (no copy) -__vbaStrR4,422,stdcall,Bstr,0,float value,string,float→BSTR via VarBstrFromR4 -__vbaStrR8,423,stdcall,Bstr,0,double value,string,double→BSTR via VarBstrFromR8 -__vbaStrTextCmp,424,stdcall,int32,0,Bstr bstr1;Bstr bstr2,string,text compare wrapper; calls __vbaStrComp(1 bstr1 bstr2) -__vbaStrTextLike,425,stdcall,int32,0,Bstr bstrText;Bstr bstrPattern,string,Case-insensitive Like comparison; passes mode=1 to internal __vbaLikeStr; returns bool as i32 -__vbaStrToAnsi,426,stdcall,VoidPtr,0,VoidPtr ppAnsi;Bstr bstrSrc,string,Frees old *ppAnsi; converts Unicode BSTR to ANSI char*; returns ppAnsi content -__vbaStrToUnicode,427,stdcall,Bstr,0,BstrPtr ppBstr;VoidPtr pszAnsi,string,Frees old *ppBstr via SysFreeString; converts ANSI to Unicode BSTR; returns new BSTR -__vbaStrUI1,428,stdcall,Bstr,0,uint8 byteVal,string,Converts Byte to BSTR via VarBstrFromI2 (zero-extends to i16 first) -__vbaStrVarCopy,429,stdcall,Bstr,0,VariantPtr pVar,string,Extracts BSTR from VARIANT; if vt!=VT_BSTR; coerces via VariantChangeType; duplicates via SysAllocStringByteLen -__vbaStrVarMove,437,stdcall,Bstr,0,VariantPtr pvar,string,extracts BSTR from VT_BSTR variant (moves ownership); retn 4 -__vbaStrVarVal,438,stdcall,Bstr,0,BstrPtr pdst;VariantPtr pvar,string,extracts or converts variant to BSTR; retn 8 -__vbaVar2Vec,448,stdcall,void,0,SafeArrayPtrPtr ppArray;VariantPtr pvar,string,Variant(BSTR)→1D byte SAFEARRAY; calls __vbaStr2Vec -rtcFormatCurrency,703,stdcall,Bstr,0,VariantPtr expression;int32 numDigitsAfterDecimal;int32 includeLeadingDigit;int32 useParensForNeg;int32 groupDigits,string,retn 0x14; FormatCurrency(); indirect call through function pointer -rtcFormatDateTime,705,stdcall,Bstr,0,VariantPtr expression;int32 namedFormat,string,retn 0x8; FormatDateTime(); indirect call through function pointer -rtcFormatNumber,702,stdcall,Bstr,0,VariantPtr expression;int32 numDigitsAfterDecimal;int32 includeLeadingDigit;int32 useParensForNeg;int32 groupDigits,string,retn 0x14; FormatNumber(); indirect call through function pointer -rtcFormatPercent,704,stdcall,Bstr,0,VariantPtr expression;int32 numDigitsAfterDecimal;int32 includeLeadingDigit;int32 useParensForNeg;int32 groupDigits,string,retn 0x14; FormatPercent(); indirect call through function pointer -rtcHexBstrFromVar,572,stdcall,Bstr,0,VariantPtr pVar,string,retn 0x4; converts variant to hex string BSTR -rtcHexVarFromVar,573,stdcall,void,0,VariantPtr pResult;VariantPtr pVar,string,retn 0x8; wraps rtcHexBstrFromVar; returns VT_BSTR VARIANT -rtcInStr,630,stdcall,void,0,VariantPtr pResult;VariantPtr start;VariantPtr string1;VariantPtr string2;VariantPtr compare,string,retn 0x14; InStr with all-VARIANT args; returns VARIANT -rtcInStrChar,633,stdcall,void,0,VariantPtr pResult;VariantPtr start;VariantPtr string1;VariantPtr string2;int32 compare,string,retn 0x14; InStr with int compare mode; returns VARIANT -rtcInStrRev,709,stdcall,int32,0,Bstr string1;Bstr string2;int32 start;int32 compare,string,retn 0x10; searches from right; returns position as int32 -rtcJoin,710,stdcall,Bstr,0,VariantPtr sourceArray;VariantPtr delimiter,string,retn 0x8; joins array elements with delimiter; returns BSTR -rtcLeftBstr,512,stdcall,Bstr,0,Bstr string;int32 length,string,retn 0x8; returns leftmost length bytes of string via SysAllocStringByteLen -rtcLeftCharBstr,616,stdcall,Bstr,0,Bstr string;int32 length,string,retn 0x8; wrapper doubles length then calls rtcLeftBstr (char=2 bytes) -rtcLeftCharVar,617,stdcall,void,0,VariantPtr pResult;VariantPtr pString;int32 length,string,retn 0xc; doubles length and calls rtcLeftVar -rtcLeftTrimBstr,521,stdcall,Bstr,0,Bstr string,string,retn 0x4; trims leading whitespace; returns new BSTR -rtcLeftTrimVar,522,stdcall,void,0,VariantPtr pResult;VariantPtr pString,string,retn 0x8; wraps rtcLeftTrimBstr; returns VT_BSTR VARIANT -rtcLeftVar,513,stdcall,void,0,VariantPtr pResult;VariantPtr pString;int32 length,string,retn 0xc; extracts string from VARIANT; calls rtcLeftBstr; returns VT_BSTR VARIANT -rtcLenCharVar,653,stdcall,void,0,VariantPtr pvarResult;VariantPtr pvarSrc,string,retval via hidden ptr; calls __vbaLenVar; retn 0x8 -rtcLenVar,654,stdcall,void,0,VariantPtr pvarResult;VariantPtr pvarSrc,string,retval via hidden ptr; calls __vbaLenVarB; retn 0x8 -rtcLowerCaseBstr,517,stdcall,Bstr,0,Bstr bstr,string,no frame ptr; SysAllocStringLen + lowercase fill; retn 0x4 -rtcLowerCaseVar,518,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr psrc,string,VB6 LCase$() -rtcMidBstr,628,stdcall,Bstr,0,Bstr bstr;int32 start;VariantPtr pvarLen,string,no frame ptr; SysAllocStringByteLen; optional len via VARIANT; retn 0xc -rtcMidCharBstr,631,stdcall,Bstr,0,Bstr bstr;int32 start;VariantPtr pvarLen,string,push ebp; char-based mid; delegates to rtcMidBstr after doubling start; retn 0xc -rtcMidCharVar,632,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr psrc;int32 start;int32 length,string,VB6 Mid$() -rtcMidVar,629,stdcall,void,0,VariantPtr pvarResult;VariantPtr pvarSrc;int32 start;VariantPtr pvarLen,string,push ebp; retval via hidden ptr; calls rtcMidBstr; retn 0x10 -rtcOctBstrFromVar,574,stdcall,Bstr,0,VariantPtr pvarSrc,string,push ebp; converts variant to octal BSTR via TLS; retn 0x4 -rtcOctVarFromVar,575,stdcall,void,0,VariantPtr pvarResult;VariantPtr pvarSrc,string,push ebp; calls rtcOctBstrFromVar; wraps result in VARIANT; retn 0x8 -rtcReplace,712,stdcall,Bstr,0,Bstr bstrExpr;Bstr bstrFind;Bstr bstrReplace;int32 start;int32 count;int32 compare,string,push ebp; compare mode resolution; delegates to internal replacer; retn 0x18 -rtcRightBstr,514,stdcall,Bstr,0,Bstr bstr;int32 length,string,no frame ptr; SysAllocStringByteLen from tail; retn 0x8 -rtcRightCharBstr,618,stdcall,Bstr,0,Bstr bstr;int32 length,string,no frame ptr; doubles length then calls rtcRightBstr; retn 0x8 -rtcRightCharVar,619,stdcall,void,0,VariantPtr pvarResult;VariantPtr pvarSrc;int32 length,string,push ebp; doubles length; delegates to rtcRightVar; retn 0xc -rtcRightTrimBstr,523,stdcall,Bstr,0,Bstr bstr,string,push ebp; trims trailing spaces via internal helper; retn 0x4 -rtcRightTrimVar,524,stdcall,void,0,VariantPtr pvarResult;VariantPtr pvarSrc,string,push ebp; TLS variant coerce; calls rtcRightTrimBstr; retn 0x8 -rtcRightVar,515,stdcall,void,0,VariantPtr pvarResult;VariantPtr pvarSrc;int32 length,string,push ebp; TLS variant coerce; calls rtcRightBstr; retn 0xc -rtcSpaceBstr,525,stdcall,Bstr,0,int32 count,string,no frame ptr; SysAllocStringLen + fill 0x20; retn 0x4 -rtcSpaceVar,526,stdcall,void,0,VariantPtr pvarResult;int32 count,string,push ebp; calls rtcSpaceBstr; wraps in VARIANT(VT_BSTR); retn 0x8 -rtcSplit,711,stdcall,VariantPtr,0,VariantPtr retval;VariantPtr pExpression;VariantPtr pDelimiter;int32 nLimit;int32 nCompareMethod,string,retn 0x14; VB Split(); creates SafeArray of BSTR -rtcStrConvVar,622,stdcall,VariantPtr,0,VariantPtr retval;VariantPtr pSrcVar;int32 nConversion,string,retn 0xc; delegates to rtcStrConvVar2 with nLocaleID=0 -rtcStrConvVar2,717,stdcall,VariantPtr,0,VariantPtr retval;VariantPtr pSrcVar;int32 nConversion;int32 nLocaleID,string,retn 0x10; VB StrConv() with locale; uses GetLocaleInfoA; LCMapString -rtcStrFromVar,536,stdcall,Bstr,0,VariantPtr pSrcVar,string,retn 0x4; VB Str() on variant; coerces to string with leading-space formatting -rtcStringBstr,606,stdcall,Bstr,0,int32 nRepeat;VariantPtr pCharVar,string,retn 0x8; VB String$(n; char); fills BSTR with repeated character -rtcStringVar,607,stdcall,VariantPtr,0,VariantPtr retval;int32 nRepeat;VariantPtr pCharVar,string,retn 0xc; Variant wrapper for rtcStringBstr -rtcStrReverse,713,stdcall,Bstr,0,Bstr bstrSrc,string,retn 0x4; VB StrReverse(); allocates new BSTR; copies chars in reverse -rtcTrimBstr,519,stdcall,Bstr,0,Bstr bstrSrc,string,retn 0x4; VB Trim$(); delegates to internal trimmer -rtcTrimVar,520,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr psrc,string,VB6 Trim$() -rtcUpperCaseBstr,527,stdcall,Bstr,0,Bstr bstrSrc,string,retn 0x4; VB UCase$(); delegates to internal case converter -rtcUpperCaseVar,528,stdcall,VariantPtr,0,VariantPtr retval;VariantPtr pSrcVar,string,retn 0x8; Variant wrapper; calls rtcUpperCaseBstr internally -rtcVarFromFormatVar,660,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pexpr;VariantPtr pformat;int32 firstDayOfWeek;int32 firstWeekOfYear,string,VB6 Format(); retn 0x14; tests/fixtures/data passes 0x14 bytes (the two day/week options as Longs) +__vbaInStr,277,stdcall,int32,0,0,int32 compare_mode;Bstr src;Bstr search;int32 start,string,InStr function; retn 0x10 +__vbaInStrB,278,stdcall,int32,0,0,int32 compare_mode;Bstr str1;Bstr str2;int32 start,string,InStrB (byte-level); binary search with memcmp +__vbaInStrVar,279,stdcall,VariantPtr,0,0,VariantPtr presult;int32 compare_mode;VariantPtr pstr1;VariantPtr pstr2;int32 start,string,InStr for variants +__vbaInStrVarB,280,stdcall,VariantPtr,0,0,VariantPtr presult;int32 compare_mode;VariantPtr pstr1;VariantPtr pstr2;int32 start,string,InStrB for variants +__vbaLenBstr,327,stdcall,int32,0,0,Bstr bstr,string,returns char count (byte_len/2); retn 4 +__vbaLenBstrB,328,stdcall,int32,0,0,Bstr bstr,string,returns byte count (raw [bstr-4]); retn 4 +__vbaLenVar,329,stdcall,VariantPtr,0,0,VariantPtr pResult;VariantPtr pVar,string,0x6604c83a; retn 0x8; returns Len() of variant; coerces to string then calls __vbaLenBstr +__vbaLenVarB,330,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pvar,string,LenB for variants; returns VT_I4 with byte count +__vbaLineInputStr,331,stdcall,void,0,0,BstrPtr pdst;int16 fileNum,string,Line Input# statement +__vbaLineInputVar,332,stdcall,void,0,0,VariantPtr pVar;int16 fileNum,string,0x660ea5f6; retn 0x8; reads a line into VARIANT (delegates to __vbaLineInputStr for BSTR subtype) +__vbaLsetFixstr,333,stdcall,Bstr,0,0,uint32 fixedLen;Bstr dest;Bstr src,string,LSet for fixed strings; left-aligned with space padding +__vbaLsetFixstrFree,334,stdcall,Bstr,0,0,uint32 fixedLen;Bstr dest;Bstr src,string,LSet wrapper; frees src after +__vbaMidStmtBstr,335,stdcall,void,0,0,int32 destMaxChars;Bstr src;int32 len;int32 start;BstrPtr pdst,string,Mid$ statement (char-level) +__vbaMidStmtBstrB,336,stdcall,void,0,0,int32 destMaxChars;Bstr src;int32 lenBytes;int32 startByte;BstrPtr pdst,string,Mid$ statement (byte-level); core impl +__vbaMidStmtVar,337,stdcall,void,0,0,VariantPtr pdst;int32 len;int32 start;VariantPtr psrc,string,Mid$ for variants; doubles len/start and calls VarB +__vbaMidStmtVarB,338,stdcall,void,0,0,VariantPtr pdst;int32 lenBytes;int32 startByte;VariantPtr psrc,string,Mid$ for variants (byte-level) +__vbaR4Str,367,stdcall,float,0,0,Bstr bstr,string,BSTR→float via VarR4FromStr; returns on FPU st0 +__vbaRsetFixstr,392,stdcall,Bstr,0,0,uint32 fixedLen;Bstr dest;Bstr src,string,RSet for fixed strings; right-aligned with space padding +__vbaRsetFixstrFree,393,stdcall,Bstr,0,0,uint32 fixedLen;Bstr dest;Bstr src,string,RSet wrapper; frees src after +__vbaStr2Vec,396,stdcall,void,0,0,SafeArrayPtrPtr ppArray;Bstr bstr,string,BSTR→1D byte SAFEARRAY +__vbaStrAryToAnsi,397,stdcall,VoidPtr,0,0,SafeArrayPtr ppArray;SafeArrayPtr ppAnsiArray,string,Wraps internal helper with direction=1 (to ANSI); converts SAFEARRAY of BSTRs to ANSI +__vbaStrAryToUnicode,398,stdcall,VoidPtr,0,0,SafeArrayPtr ppArray;SafeArrayPtr ppUniArray,string,Wraps internal helper with direction=0 (to Unicode); converts SAFEARRAY of ANSI strings to BSTRs +__vbaStrBool,399,stdcall,Bstr,0,0,int16 boolVal,string,Converts VB Boolean (int16) to BSTR via VariantChangeType(VT_BOOL->VT_BSTR) +__vbaStrCat,405,stdcall,Bstr,0,0,Bstr bstr1;Bstr bstr2,string,concatenation via internal helper; retn 8 +__vbaStrCmp,406,stdcall,int32,0,0,Bstr bstr1;Bstr bstr2,string,binary compare wrapper; calls __vbaStrComp(0 bstr1 bstr2) +__vbaStrComp,407,stdcall,int32,0,0,int32 mode;Bstr bstr1;Bstr bstr2,string,mode: 0=binary 1=text; retn 0xC +__vbaStrCompVar,408,stdcall,VariantPtr,0,0,VariantPtr presult;int32 compare_mode;VariantPtr pvar1;VariantPtr pvar2,string,StrComp for variants; returns VT_I2 +__vbaStrCopy,138,fastcall,Bstr,0,0,BstrPtr pdst;Bstr src,string,ecx=pdst edx=src; SysAllocStringByteLen+SysFreeString +__vbaStrCy,409,stdcall,Bstr,0,0,int64 cyVal,string,Converts Currency (8-byte CY) to BSTR via VarBstrFromCy +__vbaStrDate,415,stdcall,Bstr,0,0,double dateVal,string,Converts DATE (double) to BSTR via VarBstrFromDate; respects calendar setting +__vbaStrErrVarCopy,111,stdcall,Bstr,0,0,VariantPtr pVar,string,Thin wrapper around rtBstrFromErrVar; extracts error BSTR from Variant +__vbaStrFixstr,416,stdcall,Bstr,0,0,Bstr bstrSrc;uint32 cchLen,string,Allocates fixed-length BSTR via SysAllocStringLen; error 0xe (out of memory) on failure +__vbaStrI2,417,stdcall,Bstr,0,0,int16 value,string,int16→BSTR via VarBstrFromI2 +__vbaStrI4,418,stdcall,Bstr,0,0,int32 value,string,int32→BSTR via VarBstrFromI4 +__vbaStrLike,419,stdcall,int32,0,0,Bstr bstrText;Bstr bstrPattern,string,Case-sensitive Like comparison; passes mode=0 to internal __vbaLikeStr; returns bool as i32 +__vbaStrMove,139,fastcall,Bstr,0,0,BstrPtr pdst;Bstr src,string,ecx=pdst edx=src; moves ownership (no copy) +__vbaStrR4,422,stdcall,Bstr,0,0,float value,string,float→BSTR via VarBstrFromR4 +__vbaStrR8,423,stdcall,Bstr,0,0,double value,string,double→BSTR via VarBstrFromR8 +__vbaStrTextCmp,424,stdcall,int32,0,0,Bstr bstr1;Bstr bstr2,string,text compare wrapper; calls __vbaStrComp(1 bstr1 bstr2) +__vbaStrTextLike,425,stdcall,int32,0,0,Bstr bstrText;Bstr bstrPattern,string,Case-insensitive Like comparison; passes mode=1 to internal __vbaLikeStr; returns bool as i32 +__vbaStrToAnsi,426,stdcall,VoidPtr,0,0,VoidPtr ppAnsi;Bstr bstrSrc,string,Frees old *ppAnsi; converts Unicode BSTR to ANSI char*; returns ppAnsi content +__vbaStrToUnicode,427,stdcall,Bstr,0,0,BstrPtr ppBstr;VoidPtr pszAnsi,string,Frees old *ppBstr via SysFreeString; converts ANSI to Unicode BSTR; returns new BSTR +__vbaStrUI1,428,stdcall,Bstr,0,0,uint8 byteVal,string,Converts Byte to BSTR via VarBstrFromI2 (zero-extends to i16 first) +__vbaStrVarCopy,429,stdcall,Bstr,0,0,VariantPtr pVar,string,Extracts BSTR from VARIANT; if vt!=VT_BSTR; coerces via VariantChangeType; duplicates via SysAllocStringByteLen +__vbaStrVarMove,437,stdcall,Bstr,0,0,VariantPtr pvar,string,extracts BSTR from VT_BSTR variant (moves ownership); retn 4 +__vbaStrVarVal,438,stdcall,Bstr,0,0,BstrPtr pdst;VariantPtr pvar,string,extracts or converts variant to BSTR; retn 8 +__vbaVar2Vec,448,stdcall,void,0,0,SafeArrayPtrPtr ppArray;VariantPtr pvar,string,Variant(BSTR)→1D byte SAFEARRAY; calls __vbaStr2Vec +rtcFormatCurrency,703,stdcall,Bstr,0,0,VariantPtr expression;int32 numDigitsAfterDecimal;int32 includeLeadingDigit;int32 useParensForNeg;int32 groupDigits,string,retn 0x14; FormatCurrency(); indirect call through function pointer +rtcFormatDateTime,705,stdcall,Bstr,0,0,VariantPtr expression;int32 namedFormat,string,retn 0x8; FormatDateTime(); indirect call through function pointer +rtcFormatNumber,702,stdcall,Bstr,0,0,VariantPtr expression;int32 numDigitsAfterDecimal;int32 includeLeadingDigit;int32 useParensForNeg;int32 groupDigits,string,retn 0x14; FormatNumber(); indirect call through function pointer +rtcFormatPercent,704,stdcall,Bstr,0,0,VariantPtr expression;int32 numDigitsAfterDecimal;int32 includeLeadingDigit;int32 useParensForNeg;int32 groupDigits,string,retn 0x14; FormatPercent(); indirect call through function pointer +rtcHexBstrFromVar,572,stdcall,Bstr,0,0,VariantPtr pVar,string,retn 0x4; converts variant to hex string BSTR +rtcHexVarFromVar,573,stdcall,void,0,0,VariantPtr pResult;VariantPtr pVar,string,retn 0x8; wraps rtcHexBstrFromVar; returns VT_BSTR VARIANT +rtcInStr,630,stdcall,void,0,0,VariantPtr pResult;VariantPtr start;VariantPtr string1;VariantPtr string2;VariantPtr compare,string,retn 0x14; InStr with all-VARIANT args; returns VARIANT +rtcInStrChar,633,stdcall,void,0,0,VariantPtr pResult;VariantPtr start;VariantPtr string1;VariantPtr string2;int32 compare,string,retn 0x14; InStr with int compare mode; returns VARIANT +rtcInStrRev,709,stdcall,int32,0,0,Bstr string1;Bstr string2;int32 start;int32 compare,string,retn 0x10; searches from right; returns position as int32 +rtcJoin,710,stdcall,Bstr,0,0,VariantPtr sourceArray;VariantPtr delimiter,string,retn 0x8; joins array elements with delimiter; returns BSTR +rtcLeftBstr,512,stdcall,Bstr,0,0,Bstr string;int32 length,string,retn 0x8; returns leftmost length bytes of string via SysAllocStringByteLen +rtcLeftCharBstr,616,stdcall,Bstr,0,0,Bstr string;int32 length,string,retn 0x8; wrapper doubles length then calls rtcLeftBstr (char=2 bytes) +rtcLeftCharVar,617,stdcall,void,0,0,VariantPtr pResult;VariantPtr pString;int32 length,string,retn 0xc; doubles length and calls rtcLeftVar +rtcLeftTrimBstr,521,stdcall,Bstr,0,0,Bstr string,string,retn 0x4; trims leading whitespace; returns new BSTR +rtcLeftTrimVar,522,stdcall,void,0,0,VariantPtr pResult;VariantPtr pString,string,retn 0x8; wraps rtcLeftTrimBstr; returns VT_BSTR VARIANT +rtcLeftVar,513,stdcall,void,0,0,VariantPtr pResult;VariantPtr pString;int32 length,string,retn 0xc; extracts string from VARIANT; calls rtcLeftBstr; returns VT_BSTR VARIANT +rtcLenCharVar,653,stdcall,void,0,0,VariantPtr pvarResult;VariantPtr pvarSrc,string,retval via hidden ptr; calls __vbaLenVar; retn 0x8 +rtcLenVar,654,stdcall,void,0,0,VariantPtr pvarResult;VariantPtr pvarSrc,string,retval via hidden ptr; calls __vbaLenVarB; retn 0x8 +rtcLowerCaseBstr,517,stdcall,Bstr,0,0,Bstr bstr,string,no frame ptr; SysAllocStringLen + lowercase fill; retn 0x4 +rtcLowerCaseVar,518,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr psrc,string,VB6 LCase$() +rtcMidBstr,628,stdcall,Bstr,0,0,Bstr bstr;int32 start;VariantPtr pvarLen,string,no frame ptr; SysAllocStringByteLen; optional len via VARIANT; retn 0xc +rtcMidCharBstr,631,stdcall,Bstr,0,0,Bstr bstr;int32 start;VariantPtr pvarLen,string,push ebp; char-based mid; delegates to rtcMidBstr after doubling start; retn 0xc +rtcMidCharVar,632,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr psrc;int32 start;int32 length,string,VB6 Mid$() +rtcMidVar,629,stdcall,void,0,0,VariantPtr pvarResult;VariantPtr pvarSrc;int32 start;VariantPtr pvarLen,string,push ebp; retval via hidden ptr; calls rtcMidBstr; retn 0x10 +rtcOctBstrFromVar,574,stdcall,Bstr,0,0,VariantPtr pvarSrc,string,push ebp; converts variant to octal BSTR via TLS; retn 0x4 +rtcOctVarFromVar,575,stdcall,void,0,0,VariantPtr pvarResult;VariantPtr pvarSrc,string,push ebp; calls rtcOctBstrFromVar; wraps result in VARIANT; retn 0x8 +rtcReplace,712,stdcall,Bstr,0,0,Bstr bstrExpr;Bstr bstrFind;Bstr bstrReplace;int32 start;int32 count;int32 compare,string,push ebp; compare mode resolution; delegates to internal replacer; retn 0x18 +rtcRightBstr,514,stdcall,Bstr,0,0,Bstr bstr;int32 length,string,no frame ptr; SysAllocStringByteLen from tail; retn 0x8 +rtcRightCharBstr,618,stdcall,Bstr,0,0,Bstr bstr;int32 length,string,no frame ptr; doubles length then calls rtcRightBstr; retn 0x8 +rtcRightCharVar,619,stdcall,void,0,0,VariantPtr pvarResult;VariantPtr pvarSrc;int32 length,string,push ebp; doubles length; delegates to rtcRightVar; retn 0xc +rtcRightTrimBstr,523,stdcall,Bstr,0,0,Bstr bstr,string,push ebp; trims trailing spaces via internal helper; retn 0x4 +rtcRightTrimVar,524,stdcall,void,0,0,VariantPtr pvarResult;VariantPtr pvarSrc,string,push ebp; TLS variant coerce; calls rtcRightTrimBstr; retn 0x8 +rtcRightVar,515,stdcall,void,0,0,VariantPtr pvarResult;VariantPtr pvarSrc;int32 length,string,push ebp; TLS variant coerce; calls rtcRightBstr; retn 0xc +rtcSpaceBstr,525,stdcall,Bstr,0,0,int32 count,string,no frame ptr; SysAllocStringLen + fill 0x20; retn 0x4 +rtcSpaceVar,526,stdcall,void,0,0,VariantPtr pvarResult;int32 count,string,push ebp; calls rtcSpaceBstr; wraps in VARIANT(VT_BSTR); retn 0x8 +rtcSplit,711,stdcall,VariantPtr,0,0,VariantPtr retval;VariantPtr pExpression;VariantPtr pDelimiter;int32 nLimit;int32 nCompareMethod,string,retn 0x14; VB Split(); creates SafeArray of BSTR +rtcStrConvVar,622,stdcall,VariantPtr,0,0,VariantPtr retval;VariantPtr pSrcVar;int32 nConversion,string,retn 0xc; delegates to rtcStrConvVar2 with nLocaleID=0 +rtcStrConvVar2,717,stdcall,VariantPtr,0,0,VariantPtr retval;VariantPtr pSrcVar;int32 nConversion;int32 nLocaleID,string,retn 0x10; VB StrConv() with locale; uses GetLocaleInfoA; LCMapString +rtcStrFromVar,536,stdcall,Bstr,0,0,VariantPtr pSrcVar,string,retn 0x4; VB Str() on variant; coerces to string with leading-space formatting +rtcStringBstr,606,stdcall,Bstr,0,0,int32 nRepeat;VariantPtr pCharVar,string,retn 0x8; VB String$(n; char); fills BSTR with repeated character +rtcStringVar,607,stdcall,VariantPtr,0,0,VariantPtr retval;int32 nRepeat;VariantPtr pCharVar,string,retn 0xc; Variant wrapper for rtcStringBstr +rtcStrReverse,713,stdcall,Bstr,0,0,Bstr bstrSrc,string,retn 0x4; VB StrReverse(); allocates new BSTR; copies chars in reverse +rtcTrimBstr,519,stdcall,Bstr,0,0,Bstr bstrSrc,string,retn 0x4; VB Trim$(); delegates to internal trimmer +rtcTrimVar,520,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr psrc,string,VB6 Trim$() +rtcUpperCaseBstr,527,stdcall,Bstr,0,0,Bstr bstrSrc,string,retn 0x4; VB UCase$(); delegates to internal case converter +rtcUpperCaseVar,528,stdcall,VariantPtr,0,0,VariantPtr retval;VariantPtr pSrcVar,string,retn 0x8; Variant wrapper; calls rtcUpperCaseBstr internally +rtcVarFromFormatVar,660,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pexpr;VariantPtr pformat;int32 firstDayOfWeek;int32 firstWeekOfYear,string,VB6 Format(); retn 0x14; tests/fixtures/data passes 0x14 bytes (the two day/week options as Longs) # # === TYPE CONVERSION === # -__vbaBoolErrVar,109,stdcall,int32,0,VariantPtr pVar,convert,Calls rtBoolFromErrVar; movsx result; retn 4 -__vbaBoolStr,174,stdcall,int16,0,Bstr bstr,convert,BSTR→VARIANT_BOOL via VarBoolFromStr -__vbaBoolVar,175,stdcall,int16,0,VariantPtr pvar,convert,variant→bool; uses VariantChangeTypeEx (VT_BOOL=0xB) -__vbaBoolVarNull,176,stdcall,int32,0,VariantPtr pvar,convert,variant→bool (null-aware); optimized inline VT dispatch -__vbaDateR4,206,stdcall,double,0,float value,convert,Widens float->double; range checks VB date bounds; retn 4 -__vbaDateR8,207,stdcall,double,0,double val,convert,range-checks double against DATE bounds; returns on ST(0) -__vbaDateStr,208,stdcall,double,0,Bstr pStr,convert,Calls VarDateFromStr (fallback VarR8FromStr + range check); retn 4 -__vbaDateVar,209,stdcall,double,0,VariantPtr pvar,convert,variant→date; calls __vbaVarDateVar then loads qword to ST(0) -__vbaI2Abs,133,fastcall,int16,0,int16 value,convert,Negates if negative; overflow -> error 6 -__vbaI2Cy,259,stdcall,int16,0,int64 cyValue,convert,fild qword; fdiv 10000.0; call __vbaFpI2; retn 8 -__vbaI2ErrVar,260,stdcall,int16,0,VariantPtr pVar,convert,Dispatches via vtype jump table; entry thunk loads ECX=vtable; jmps to shared dispatcher -__vbaI2ForNextCheck,261,stdcall,int32,0,,convert,export exists at ordinal 35; integer For/Next boundary check for Int16 -__vbaI2I4,134,fastcall,int16,0,int32 val,convert,ecx=val; int32→int16 with overflow check; raises error 6 -__vbaI2Sgn,135,fastcall,int32,0,int16 value,convert,Branchless: returns -1/0/1; bare retn -__vbaI2Str,262,stdcall,int16,0,Bstr pStr,convert,Calls VarI2FromStr; retn 4 -__vbaI2Var,263,stdcall,int16,0,VariantPtr pvar,convert,variant→int16; dispatch table on VT -__vbaI4Abs,136,fastcall,int32,0,int32 val,convert,ecx=val; absolute value; raises error 6 if INT_MIN -__vbaI4Cy,264,stdcall,int32,0,int64 cyValue,convert,fild qword; fdiv 10000.0; call __vbaFpI4; retn 8 -__vbaI4ErrVar,267,stdcall,int32,0,VariantPtr pvar,convert,error variant→int32; shared dispatch body -__vbaI4ForNextCheck,270,stdcall,int32,0,,convert,export exists at ordinal 172; integer For/Next boundary check for Int32 -__vbaI4Sgn,137,fastcall,int32,0,int32 value,convert,Branchless: returns -1/0/1; bare retn -__vbaI4Str,272,stdcall,int32,0,Bstr pStr,convert,Calls VarI4FromStr; retn 4 -__vbaI4Var,273,stdcall,int32,0,VariantPtr pvar,convert,variant→int32; shared dispatch body with I2Var -__vbaR4Cy,363,stdcall,float,0,int64 cyValue,convert,fild qword; fdiv 10000.0f; retn 8; returns float in ST0 -__vbaR4ErrVar,364,stdcall,float,0,VariantPtr pVar,convert,Like R4Var but with Error variant support; thunk -> shared dispatcher -__vbaR4ForNextCheck,365,stdcall,int32,0,float counter;float limit;float step,convert,0x66112f1e; retn 0xc; For/Next loop check for Single; compares step sign; returns -1 (continue) or 0 (done) -__vbaR4Sgn,366,stdcall,int16,0,float value,convert,Compares with 0.0f; returns -1/0/1 in AX; retn 4 -__vbaR4Var,368,stdcall,float,0,VariantPtr pVar,convert,Dispatches via vtype jump table; returns float in ST0; retn 4 -__vbaR8Cy,369,stdcall,double,0,int64 cyValue,convert,Identical body to R4Cy but divides by 10000.0f returning double; retn 8 -__vbaR8ErrVar,370,stdcall,double,0,VariantPtr pVar,convert,Like R8Var but with Error variant support; thunk -> shared dispatcher -__vbaR8FixI2,371,cdecl,int16,0,VoidPtr ST0,convert,Calls __vbaFPFix; fistp word; bare retn; input in FPU ST0 -__vbaR8FixI4,372,cdecl,int32,0,VoidPtr ST0,convert,Calls __vbaFPFix; fistp dword; bare retn; input in FPU ST0 -__vbaR8IntI2,374,cdecl,int16,0,VoidPtr ST0,convert,Calls __vbaFPInt; fistp word; bare retn; input in FPU ST0 -__vbaR8IntI4,375,cdecl,int32,0,VoidPtr ST0,convert,Calls __vbaFPInt; fistp dword; bare retn; input in FPU ST0 -__vbaR8Sgn,376,stdcall,int16,0,double value,convert,Compares with 0.0; returns -1/0/1 in AX; retn 8 -__vbaR8Str,377,stdcall,double,0,Bstr pStr,convert,Calls VarR8FromStr; retn 4 -__vbaR8Var,378,stdcall,double,0,VariantPtr pvar,convert,variant→double; returns on FPU ST(0) -__vbaUI1Cy,439,stdcall,uint8,0,int64 cyValue,convert,fild qword; fdiv 10000.0; call __vbaFpUI1; retn 8 -__vbaUI1ErrVar,442,stdcall,uint8,0,VariantPtr pVar,convert,Like UI1Var but with Error variant support; thunk -> shared dispatcher -__vbaUI1I2,140,fastcall,uint8,0,int16 val,convert,ecx=val; int16→uint8 with overflow check; raises error 6 -__vbaUI1I4,141,fastcall,uint8,0,int32 value,convert,Range checks value<=0xFF; overflow -> error 6; bare retn -__vbaUI1Sgn,142,fastcall,int32,0,uint8 value,convert,Branchless: returns 0 or 1 (unsigned byte can't be negative); bare retn -__vbaUI1Str,443,stdcall,uint8,0,Bstr pStr,convert,Calls __vbaI2Str then range-checks <=0xFF; retn 4 -__vbaUI1Var,444,stdcall,uint8,0,VariantPtr pVar,convert,Dispatches via vtype jump table; range checks 0-255; thunk -> shared dispatcher -__vbaVarDateVar,459,stdcall,VariantPtr,0,VariantPtr pdest;VariantPtr psrc,convert,coerces src to VT_DATE into dest VARIANT -rtcAbsVar,656,stdcall,void,0,VariantPtr result;VariantPtr var,convert,retn 0x8; wraps __vbaVarAbs; writes 16-byte VARIANT to result ptr -rtcBstrFromError,651,stdcall,Bstr,0,VariantPtr errVar,convert,retn 0x4; error code to message string via LoadString/SysAllocString -rtcFixVar,655,stdcall,void,0,VariantPtr result;VariantPtr var,convert,retn 0x8; wraps __vbaVarFix; truncates toward zero -rtcIntVar,657,stdcall,void,0,VariantPtr pResult;VariantPtr pVar,convert,retn 0x8; calls __vbaVarInt; returns truncated integer VARIANT -rtcR8ValFromBstr,581,stdcall,double,0,Bstr bstr,convert,push ebp; strips whitespace then calls VariantChangeTypeEx; FPU return; retn 0x4 -rtcRound,714,stdcall,void,0,VariantPtr pvarResult;VariantPtr pvarSrc;int32 numDigits,convert,push ebp; calls VarRound (OLEAUT32); retval via hidden ptr; retn 0xc -rtcSgnVar,658,stdcall,void,0,VariantPtr pvarResult;VariantPtr pvarSrc,convert,push ebp; sign of variant; returns -1/0/1 in VARIANT; switch table; retn 0x8 -rtcVarBstrFromAnsi,608,stdcall,VariantPtr,0,VariantPtr retval;int32 nAnsiCode,convert,retn 0x8; wraps rtcBstrFromAnsi result in VARIANT(VT_BSTR) -rtcVarBstrFromByte,695,stdcall,VariantPtr,0,VariantPtr retval;uint16 nByte,convert,retn 0x8; wraps rtcBstrFromByte result in VARIANT(VT_BSTR) -rtcVarBstrFromChar,698,stdcall,VariantPtr,0,VariantPtr retval;uint16 nChar,convert,retn 0x8; wraps rtcBstrFromChar result in VARIANT(VT_BSTR) -rtcVarDateFromVar,687,stdcall,VariantPtr,0,VariantPtr retval;VariantPtr pSrcVar,convert,retn 0x8; VB CDate() variant; calls __vbaVarDateVar -rtcVarFromError,652,stdcall,VariantPtr,0,VariantPtr retval;int32 nErrorCode,convert,retn 0x8; wraps rtcBstrFromError in VARIANT(VT_BSTR) -rtcVarFromVar,642,stdcall,VariantPtr,0,VariantPtr retval;VariantPtr pSrcVar,convert,retn 0x8; copies VARIANT via VariantCopyInd -rtcVarStrFromVar,613,stdcall,VariantPtr,0,VariantPtr retval;VariantPtr pSrcVar,convert,retn 0x8; Variant wrapper for rtcStrFromVar; handles Null->Null +__vbaBoolErrVar,109,stdcall,int32,0,0,VariantPtr pVar,convert,Calls rtBoolFromErrVar; movsx result; retn 4 +__vbaBoolStr,174,stdcall,int16,0,0,Bstr bstr,convert,BSTR→VARIANT_BOOL via VarBoolFromStr +__vbaBoolVar,175,stdcall,int16,0,0,VariantPtr pvar,convert,variant→bool; uses VariantChangeTypeEx (VT_BOOL=0xB) +__vbaBoolVarNull,176,stdcall,int32,0,0,VariantPtr pvar,convert,variant→bool (null-aware); optimized inline VT dispatch +__vbaDateR4,206,stdcall,double,0,0,float value,convert,Widens float->double; range checks VB date bounds; retn 4 +__vbaDateR8,207,stdcall,double,0,0,double val,convert,range-checks double against DATE bounds; returns on ST(0) +__vbaDateStr,208,stdcall,double,0,0,Bstr pStr,convert,Calls VarDateFromStr (fallback VarR8FromStr + range check); retn 4 +__vbaDateVar,209,stdcall,double,0,0,VariantPtr pvar,convert,variant→date; calls __vbaVarDateVar then loads qword to ST(0) +__vbaI2Abs,133,fastcall,int16,0,0,int16 value,convert,Negates if negative; overflow -> error 6 +__vbaI2Cy,259,stdcall,int16,0,0,int64 cyValue,convert,fild qword; fdiv 10000.0; call __vbaFpI2; retn 8 +__vbaI2ErrVar,260,stdcall,int16,0,0,VariantPtr pVar,convert,Dispatches via vtype jump table; entry thunk loads ECX=vtable; jmps to shared dispatcher +__vbaI2ForNextCheck,261,stdcall,int32,0,0,,convert,export exists at ordinal 35; integer For/Next boundary check for Int16 +__vbaI2I4,134,fastcall,int16,0,0,int32 val,convert,ecx=val; int32→int16 with overflow check; raises error 6 +__vbaI2Sgn,135,fastcall,int32,0,0,int16 value,convert,Branchless: returns -1/0/1; bare retn +__vbaI2Str,262,stdcall,int16,0,0,Bstr pStr,convert,Calls VarI2FromStr; retn 4 +__vbaI2Var,263,stdcall,int16,0,0,VariantPtr pvar,convert,variant→int16; dispatch table on VT +__vbaI4Abs,136,fastcall,int32,0,0,int32 val,convert,ecx=val; absolute value; raises error 6 if INT_MIN +__vbaI4Cy,264,stdcall,int32,0,0,int64 cyValue,convert,fild qword; fdiv 10000.0; call __vbaFpI4; retn 8 +__vbaI4ErrVar,267,stdcall,int32,0,0,VariantPtr pvar,convert,error variant→int32; shared dispatch body +__vbaI4ForNextCheck,270,stdcall,int32,0,0,,convert,export exists at ordinal 172; integer For/Next boundary check for Int32 +__vbaI4Sgn,137,fastcall,int32,0,0,int32 value,convert,Branchless: returns -1/0/1; bare retn +__vbaI4Str,272,stdcall,int32,0,0,Bstr pStr,convert,Calls VarI4FromStr; retn 4 +__vbaI4Var,273,stdcall,int32,0,0,VariantPtr pvar,convert,variant→int32; shared dispatch body with I2Var +__vbaR4Cy,363,stdcall,float,0,0,int64 cyValue,convert,fild qword; fdiv 10000.0f; retn 8; returns float in ST0 +__vbaR4ErrVar,364,stdcall,float,0,0,VariantPtr pVar,convert,Like R4Var but with Error variant support; thunk -> shared dispatcher +__vbaR4ForNextCheck,365,stdcall,int32,0,0,float counter;float limit;float step,convert,0x66112f1e; retn 0xc; For/Next loop check for Single; compares step sign; returns -1 (continue) or 0 (done) +__vbaR4Sgn,366,stdcall,int16,0,0,float value,convert,Compares with 0.0f; returns -1/0/1 in AX; retn 4 +__vbaR4Var,368,stdcall,float,0,0,VariantPtr pVar,convert,Dispatches via vtype jump table; returns float in ST0; retn 4 +__vbaR8Cy,369,stdcall,double,0,0,int64 cyValue,convert,Identical body to R4Cy but divides by 10000.0f returning double; retn 8 +__vbaR8ErrVar,370,stdcall,double,0,0,VariantPtr pVar,convert,Like R8Var but with Error variant support; thunk -> shared dispatcher +__vbaR8FixI2,371,cdecl,int16,0,0,VoidPtr ST0,convert,Calls __vbaFPFix; fistp word; bare retn; input in FPU ST0 +__vbaR8FixI4,372,cdecl,int32,0,0,VoidPtr ST0,convert,Calls __vbaFPFix; fistp dword; bare retn; input in FPU ST0 +__vbaR8IntI2,374,cdecl,int16,0,0,VoidPtr ST0,convert,Calls __vbaFPInt; fistp word; bare retn; input in FPU ST0 +__vbaR8IntI4,375,cdecl,int32,0,0,VoidPtr ST0,convert,Calls __vbaFPInt; fistp dword; bare retn; input in FPU ST0 +__vbaR8Sgn,376,stdcall,int16,0,0,double value,convert,Compares with 0.0; returns -1/0/1 in AX; retn 8 +__vbaR8Str,377,stdcall,double,0,0,Bstr pStr,convert,Calls VarR8FromStr; retn 4 +__vbaR8Var,378,stdcall,double,0,0,VariantPtr pvar,convert,variant→double; returns on FPU ST(0) +__vbaUI1Cy,439,stdcall,uint8,0,0,int64 cyValue,convert,fild qword; fdiv 10000.0; call __vbaFpUI1; retn 8 +__vbaUI1ErrVar,442,stdcall,uint8,0,0,VariantPtr pVar,convert,Like UI1Var but with Error variant support; thunk -> shared dispatcher +__vbaUI1I2,140,fastcall,uint8,0,0,int16 val,convert,ecx=val; int16→uint8 with overflow check; raises error 6 +__vbaUI1I4,141,fastcall,uint8,0,0,int32 value,convert,Range checks value<=0xFF; overflow -> error 6; bare retn +__vbaUI1Sgn,142,fastcall,int32,0,0,uint8 value,convert,Branchless: returns 0 or 1 (unsigned byte can't be negative); bare retn +__vbaUI1Str,443,stdcall,uint8,0,0,Bstr pStr,convert,Calls __vbaI2Str then range-checks <=0xFF; retn 4 +__vbaUI1Var,444,stdcall,uint8,0,0,VariantPtr pVar,convert,Dispatches via vtype jump table; range checks 0-255; thunk -> shared dispatcher +__vbaVarDateVar,459,stdcall,VariantPtr,0,0,VariantPtr pdest;VariantPtr psrc,convert,coerces src to VT_DATE into dest VARIANT +rtcAbsVar,656,stdcall,void,0,0,VariantPtr result;VariantPtr var,convert,retn 0x8; wraps __vbaVarAbs; writes 16-byte VARIANT to result ptr +rtcBstrFromError,651,stdcall,Bstr,0,0,VariantPtr errVar,convert,retn 0x4; error code to message string via LoadString/SysAllocString +rtcFixVar,655,stdcall,void,0,0,VariantPtr result;VariantPtr var,convert,retn 0x8; wraps __vbaVarFix; truncates toward zero +rtcIntVar,657,stdcall,void,0,0,VariantPtr pResult;VariantPtr pVar,convert,retn 0x8; calls __vbaVarInt; returns truncated integer VARIANT +rtcR8ValFromBstr,581,stdcall,double,0,0,Bstr bstr,convert,push ebp; strips whitespace then calls VariantChangeTypeEx; FPU return; retn 0x4 +rtcRound,714,stdcall,void,0,0,VariantPtr pvarResult;VariantPtr pvarSrc;int32 numDigits,convert,push ebp; calls VarRound (OLEAUT32); retval via hidden ptr; retn 0xc +rtcSgnVar,658,stdcall,void,0,0,VariantPtr pvarResult;VariantPtr pvarSrc,convert,push ebp; sign of variant; returns -1/0/1 in VARIANT; switch table; retn 0x8 +rtcVarBstrFromAnsi,608,stdcall,VariantPtr,0,0,VariantPtr retval;int32 nAnsiCode,convert,retn 0x8; wraps rtcBstrFromAnsi result in VARIANT(VT_BSTR) +rtcVarBstrFromByte,695,stdcall,VariantPtr,0,0,VariantPtr retval;uint16 nByte,convert,retn 0x8; wraps rtcBstrFromByte result in VARIANT(VT_BSTR) +rtcVarBstrFromChar,698,stdcall,VariantPtr,0,0,VariantPtr retval;uint16 nChar,convert,retn 0x8; wraps rtcBstrFromChar result in VARIANT(VT_BSTR) +rtcVarDateFromVar,687,stdcall,VariantPtr,0,0,VariantPtr retval;VariantPtr pSrcVar,convert,retn 0x8; VB CDate() variant; calls __vbaVarDateVar +rtcVarFromError,652,stdcall,VariantPtr,0,0,VariantPtr retval;int32 nErrorCode,convert,retn 0x8; wraps rtcBstrFromError in VARIANT(VT_BSTR) +rtcVarFromVar,642,stdcall,VariantPtr,0,0,VariantPtr retval;VariantPtr pSrcVar,convert,retn 0x8; copies VARIANT via VariantCopyInd +rtcVarStrFromVar,613,stdcall,VariantPtr,0,0,VariantPtr retval;VariantPtr pSrcVar,convert,retn 0x8; Variant wrapper for rtcStrFromVar; handles Null->Null # # === OBJECT OPERATIONS === # -__vbaCastObj,178,stdcall,VoidPtr,0,IUnknownPtr pobj;VoidPtr piid,object,QueryInterface wrapper; returns requested interface -__vbaCastObjVar,179,stdcall,VoidPtr,0,VariantPtr pvar;VoidPtr piid,object,extracts obj from VT_DISPATCH/VT_UNKNOWN variant then QI -__vbaCheckType,180,stdcall,int32,0,IUnknownPtr pobj;VoidPtr piid,object,QI to piid; succeeds→Release and return -1; fails→return 0 -__vbaCheckTypeVar,181,stdcall,int32,0,VariantPtr pVar;VoidPtr pGuid,object,Extracts IDispatch; QueryInterface with GUID; returns -1 match / 0 no match; retn 8 -__vbaNew,341,stdcall,IUnknownPtr,0,VoidPtr pClassDesc,object,creates object from class descriptor -__vbaNew2,340,stdcall,void,0,VoidPtr pClassDesc;IUnknownPtrPtr ppobj,object,creates object and stores into *ppobj -__vbaObjAddref,347,stdcall,VoidPtr,0,VariantPtr pVar,object,Extracts IDispatch* from VT_DISPATCH variant (handles VT_BYREF); returns object ptr in EAX; retn 4. Despite name; does NOT call AddRef. -__vbaObjIs,348,stdcall,int32,0,IUnknownPtr pobj1;IUnknownPtr pobj2,object,QI both to IUnknown and compare identity; -1=same 0=different -__vbaObjSet,349,stdcall,IUnknownPtr,0,IUnknownPtrPtr ppobj;IUnknownPtr pnew,object,releases old (flag=1) stores new; returns *ppobj -__vbaObjSetAddref,350,stdcall,IUnknownPtr,0,IUnknownPtrPtr ppobj;IUnknownPtr pnew,object,AddRefs new without releasing old (flag=0); returns *ppobj -__vbaObjVar,351,stdcall,VoidPtr,0,VariantPtr pVar,object,NOTE: export at 0x6600b6ca shares address with __vbaPrintFile (file I/O function). Likely export table alias collision. Code at that address is a Print operation; not object extraction. -__vbaUdtVar,446,stdcall,VoidPtr,0,VoidPtr pRecInfo;VariantPtr pvar,object,extracts UDT record data from VT_RECORD variant -__vbaUnkVar,447,stdcall,IUnknownPtr,0,VariantPtr pvar,object,extracts IUnknown* from VT_DISPATCH/VT_UNKNOWN variant +__vbaCastObj,178,stdcall,VoidPtr,0,0,IUnknownPtr pobj;VoidPtr piid,object,QueryInterface wrapper; returns requested interface +__vbaCastObjVar,179,stdcall,VoidPtr,0,0,VariantPtr pvar;VoidPtr piid,object,extracts obj from VT_DISPATCH/VT_UNKNOWN variant then QI +__vbaCheckType,180,stdcall,int32,0,0,IUnknownPtr pobj;VoidPtr piid,object,QI to piid; succeeds→Release and return -1; fails→return 0 +__vbaCheckTypeVar,181,stdcall,int32,0,0,VariantPtr pVar;VoidPtr pGuid,object,Extracts IDispatch; QueryInterface with GUID; returns -1 match / 0 no match; retn 8 +__vbaNew,341,stdcall,IUnknownPtr,0,0,VoidPtr pClassDesc,object,creates object from class descriptor +__vbaNew2,340,stdcall,void,0,0,VoidPtr pClassDesc;IUnknownPtrPtr ppobj,object,creates object and stores into *ppobj +__vbaObjAddref,347,stdcall,VoidPtr,0,0,VariantPtr pVar,object,Extracts IDispatch* from VT_DISPATCH variant (handles VT_BYREF); returns object ptr in EAX; retn 4. Despite name; does NOT call AddRef. +__vbaObjIs,348,stdcall,int32,0,0,IUnknownPtr pobj1;IUnknownPtr pobj2,object,QI both to IUnknown and compare identity; -1=same 0=different +__vbaObjSet,349,stdcall,IUnknownPtr,0,0,IUnknownPtrPtr ppobj;IUnknownPtr pnew,object,releases old (flag=1) stores new; returns *ppobj +__vbaObjSetAddref,350,stdcall,IUnknownPtr,0,0,IUnknownPtrPtr ppobj;IUnknownPtr pnew,object,AddRefs new without releasing old (flag=0); returns *ppobj +__vbaObjVar,351,stdcall,VoidPtr,0,0,VariantPtr pVar,object,NOTE: export at 0x6600b6ca shares address with __vbaPrintFile (file I/O function). Likely export table alias collision. Code at that address is a Print operation; not object extraction. +__vbaUdtVar,446,stdcall,VoidPtr,0,0,VoidPtr pRecInfo;VariantPtr pvar,object,extracts UDT record data from VT_RECORD variant +__vbaUnkVar,447,stdcall,IUnknownPtr,0,0,VariantPtr pvar,object,extracts IUnknown* from VT_DISPATCH/VT_UNKNOWN variant # # === VARIANT OPERATIONS === # -__vbaVarAbs,449,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pvar,variant,calls VarAbs via [0x66119848]; retn 8 -__vbaVarAdd,450,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,thin wrapper over VarAdd -__vbaVarAnd,451,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,thin wrapper over VarAnd -__vbaVarCat,452,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,string concat; handles calendar/date coercion -__vbaVarCmpEq,453,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,binary; writes VT_BOOL; NULL→VT_NULL; lookup 0x6601d9a4 -__vbaVarCmpGe,454,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,binary; writes VT_BOOL; NULL→VT_NULL; lookup 0x6601d9c4 -__vbaVarCmpGt,455,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,binary; writes VT_BOOL; NULL→VT_NULL; lookup 0x6601d9b0 -__vbaVarCmpLe,456,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,binary; writes VT_BOOL; NULL→VT_NULL; lookup 0x6601d9c8 -__vbaVarCmpLt,457,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,writes VT_BOOL result variant; handles NULL→VT_NULL -__vbaVarCmpNe,458,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,writes VT_BOOL result variant; handles NULL→VT_NULL -__vbaVarCopy,143,fastcall,VariantPtr,0,VariantPtr pdst;VariantPtr psrc,variant,ecx=dst edx=src; deep copy (BSTR dup/IUnknown AddRef/ByRef deref) -__vbaVarDiv,460,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,thin wrapper over VarDiv -__vbaVarDup,144,fastcall,VariantPtr,0,VariantPtr pdst;VariantPtr psrc,variant,ecx=dst edx=src; fast memcpy 16 bytes for simple types -__vbaVarEqv,461,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,calls VarEqv via [0x66119828]; retn 0xC -__vbaVarErrI4,462,stdcall,VariantPtr,0,VariantPtr presult;int32 errcode,variant,creates VT_ERROR (vt=0xA); errcode OR'd with 0x800a0000; validates 0..0xffff; retn 8 -__vbaVarFix,463,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pvar,variant,calls VarFix via [0x6611984c]; retn 8 -__vbaVarForInit,464,stdcall,int32,0,VariantPtr pcounter;VariantPtr pstart;VariantPtr plimit;VariantPtr pstep;VariantPtr ploopvar;int32 flags,variant,For...Next init; 6 params -__vbaVarForNext,465,stdcall,int32,0,VariantPtr pcounter;VariantPtr pstep;VariantPtr plimit,variant,For...Next step; returns 0=continue -1=exit -__vbaVargObj,510,stdcall,VariantPtr,0,VariantPtr pVar;IDispatchPtr pObj,variant,Sets pVar to VT_DISPATCH with AddRef; if already holds obj; releases old first -__vbaVargObjAddref,511,stdcall,VariantPtr,0,VariantPtr pVar;IDispatchPtr pObj,variant,Like VargObj but does NOT AddRef the source (flag=0 to internal helper) -__vbaVargParmRef,148,fastcall,VariantPtr,0,,variant,Dereferences VT_BYREF|VT_VARIANT; if flag set returns pVar->pvarVal; else returns pVar -__vbaVargUnk,602,stdcall,VariantPtr,0,VariantPtr pVar;IUnknownPtr pUnk,variant,Sets pVar to VT_UNKNOWN with AddRef; parallel to VargObj for IUnknown -__vbaVargUnkAddref,603,stdcall,VariantPtr,0,VariantPtr pVar;IUnknownPtr pUnk,variant,Like VargUnk but does NOT AddRef (flag=0) -__vbaVargVar,149,fastcall,VariantPtr,0,VoidPtr pDstVar),variant,Copies 16-byte VARIANT from ecx to pDstVar; if not VT_BYREF; sets VT_BYREF|VT_VARIANT pointing to src -__vbaVargVarCopy,150,fastcall,VariantPtr,0,,variant,Copies VARIANT via internal helper with flag=0 (deep copy); returns pSrcVar in eax -__vbaVargVarMove,151,fastcall,VariantPtr,0,,variant,Moves VARIANT via internal helper with flag=1 (move/steal); returns pSrcVar -__vbaVargVarRef,152,fastcall,VariantPtr,0,,variant,Copies 16 bytes from pSrcVar to pDstVar; if not VT_BYREF; makes it VT_BYREF|VT_VARIANT -__vbaVarIdiv,466,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,thin wrapper over VarIdiv -__vbaVarImp,467,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,calls VarImp via [0x66119830]; retn 0xC -__vbaVarIndexLoad,468,cdecl,VariantPtr,1,VariantPtr presult;int32 cDims;VariantPtr parray,variant,bare retn; variadic indices -__vbaVarIndexLoadRef,469,cdecl,VariantPtr,1,VariantPtr presult;int32 cDims;VariantPtr parray,variant,bare retn; frees presult if non-empty first -__vbaVarIndexLoadRefLock,470,cdecl,VariantPtr,1,VariantPtr presult;int32 cDims;VariantPtr parray,variant,bare retn; frees presult if non-empty; locks array data -__vbaVarIndexStore,471,cdecl,VariantPtr,1,VariantPtr pvalue;int32 cDims;VariantPtr parray,variant,bare retn; push flags=4; store value at index -__vbaVarIndexStoreObj,472,cdecl,VariantPtr,1,VariantPtr pvalue;int32 cDims;VariantPtr parray,variant,bare retn; push flags=8; Set store at index -__vbaVarInt,473,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pvar,variant,calls VarInt via [0x66119850]; retn 8 -__vbaVarLike,474,stdcall,int32,0,VariantPtr pstring;VariantPtr ppattern,variant,Like operator (binary compare); flag=0 -__vbaVarLikeVar,475,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pstring;VariantPtr ppattern,variant,binary Like; push flag=0; calls LikeHelper; writes result variant; retn 0xC -__vbaVarMod,476,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,calls VarMod via [0x66119834]; retn 0xC -__vbaVarMove,145,fastcall,VariantPtr,0,VariantPtr pdst;VariantPtr psrc,variant,ecx=dst edx=src; frees dst then moves src; zeroes src vt -__vbaVarMul,477,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,thin wrapper over VarMul -__vbaVarNeg,478,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr poperand,variant,unary; thin wrapper over VarNeg -__vbaVarNot,479,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr poperand,variant,unary; thin wrapper over VarNot -__vbaVarOr,480,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,thin wrapper over VarOr -__vbaVarPow,481,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,thin wrapper over VarPow -__vbaVarSetObj,482,stdcall,VariantPtr,0,VariantPtr pdst;IDispatchPtr pobj,variant,frees dst; sets VT_DISPATCH(9); ObjSet flag=1 (release old) -__vbaVarSetObjAddref,483,stdcall,VariantPtr,0,VariantPtr pdst;IDispatchPtr pobj,variant,frees dst; sets VT_DISPATCH(9); ObjSet flag=0 (no release) -__vbaVarSetUnk,484,stdcall,VariantPtr,0,VariantPtr pdst;IUnknownPtr punk,variant,frees dst; sets VT_UNKNOWN(0xd); ObjSet flag=1 -__vbaVarSetUnkAddref,485,stdcall,VariantPtr,0,VariantPtr pdst;IUnknownPtr punk,variant,frees dst; sets VT_UNKNOWN(0xd); ObjSet flag=0 -__vbaVarSetVar,486,stdcall,VariantPtr,0,VariantPtr pdst;VariantPtr psrc,variant,calls internal VarCopyObj addref flag=1; retn 8 -__vbaVarSetVarAddref,487,stdcall,VariantPtr,0,VariantPtr pdst;VariantPtr psrc,variant,calls internal VarCopyObj addref flag=0; retn 8 -__vbaVarSub,488,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,thin wrapper over VarSub -__vbaVarTextCmpEq,489,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,text; writes VT_BOOL; NULL→VT_NULL -__vbaVarTextCmpGe,490,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,text; writes VT_BOOL; NULL→VT_NULL -__vbaVarTextCmpGt,491,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,text; writes VT_BOOL; NULL→VT_NULL -__vbaVarTextCmpLe,492,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,text; writes VT_BOOL; NULL→VT_NULL -__vbaVarTextCmpLt,493,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,text; writes VT_BOOL; NULL→VT_NULL -__vbaVarTextCmpNe,494,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,text; writes VT_BOOL; NULL→VT_NULL -__vbaVarTextLike,495,stdcall,int32,0,VariantPtr pstring;VariantPtr ppattern,variant,Like operator (text compare); flag=1 -__vbaVarTextLikeVar,496,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pstring;VariantPtr ppattern,variant,text Like; push flag=1; calls LikeHelper; writes result variant; retn 0xC -__vbaVarTextTstEq,497,stdcall,int32,0,VariantPtr pleft;VariantPtr pright,variant,text cmp mode=1; lookup 0x6601d9a4; 0/-1 -__vbaVarTextTstGe,498,stdcall,int32,0,VariantPtr pleft;VariantPtr pright,variant,text cmp mode=1; lookup 0x6601d9c4; 0/-1 -__vbaVarTextTstGt,499,stdcall,int32,0,VariantPtr pleft;VariantPtr pright,variant,text cmp mode=1; lookup 0x6601d9b0; 0/-1 -__vbaVarTextTstLe,500,stdcall,int32,0,VariantPtr pleft;VariantPtr pright,variant,text cmp mode=1; lookup 0x6601d9c8; 0/-1 -__vbaVarTextTstLt,501,stdcall,int32,0,VariantPtr pleft;VariantPtr pright,variant,text cmp mode=1; lookup 0x6601d9a8; 0/-1 -__vbaVarTextTstNe,502,stdcall,int32,0,VariantPtr pleft;VariantPtr pright,variant,text cmp mode=1; lookup 0x6601d9b8; 0/-1 -__vbaVarTstEq,503,stdcall,int32,0,VariantPtr pleft;VariantPtr pright,variant,test equality; returns 0 or -1 -__vbaVarTstGe,504,stdcall,int32,0,VariantPtr pleft;VariantPtr pright,variant,binary cmp mode=0; lookup 0x6601d9c4; 0/-1 -__vbaVarTstGt,505,stdcall,int32,0,VariantPtr pleft;VariantPtr pright,variant,test greater than; returns 0 or -1 -__vbaVarTstLe,506,stdcall,int32,0,VariantPtr pleft;VariantPtr pright,variant,test less or equal; returns 0 or -1 -__vbaVarTstLt,507,stdcall,int32,0,VariantPtr pleft;VariantPtr pright,variant,test less than; returns 0 or -1 -__vbaVarTstNe,508,stdcall,int32,0,VariantPtr pleft;VariantPtr pright,variant,test not equal; returns 0 or -1 -__vbaVarVargNofree,146,fastcall,VariantPtr,0,VariantPtr pdst;VariantPtr psrc,variant,ecx=dst edx=src; resolves VT_VARIANT|VT_BYREF; no free on dst -__vbaVarXor,509,stdcall,VariantPtr,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,calls VarXor via [0x66119844]; retn 0xC -__vbaVerifyVarObj,604,stdcall,VariantPtr,0,VariantPtr pvar,variant,validates VT_DISPATCH(9) or VT_UNKNOWN(0xd) with VT_BYREF mask (0xbfff); error 0x1a8 if invalid; retn 4 +__vbaVarAbs,449,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pvar,variant,calls VarAbs via [0x66119848]; retn 8 +__vbaVarAdd,450,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,thin wrapper over VarAdd +__vbaVarAnd,451,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,thin wrapper over VarAnd +__vbaVarCat,452,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,string concat; handles calendar/date coercion +__vbaVarCmpEq,453,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,binary; writes VT_BOOL; NULL→VT_NULL; lookup 0x6601d9a4 +__vbaVarCmpGe,454,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,binary; writes VT_BOOL; NULL→VT_NULL; lookup 0x6601d9c4 +__vbaVarCmpGt,455,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,binary; writes VT_BOOL; NULL→VT_NULL; lookup 0x6601d9b0 +__vbaVarCmpLe,456,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,binary; writes VT_BOOL; NULL→VT_NULL; lookup 0x6601d9c8 +__vbaVarCmpLt,457,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,writes VT_BOOL result variant; handles NULL→VT_NULL +__vbaVarCmpNe,458,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,writes VT_BOOL result variant; handles NULL→VT_NULL +__vbaVarCopy,143,fastcall,VariantPtr,0,0,VariantPtr pdst;VariantPtr psrc,variant,ecx=dst edx=src; deep copy (BSTR dup/IUnknown AddRef/ByRef deref) +__vbaVarDiv,460,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,thin wrapper over VarDiv +__vbaVarDup,144,fastcall,VariantPtr,0,0,VariantPtr pdst;VariantPtr psrc,variant,ecx=dst edx=src; fast memcpy 16 bytes for simple types +__vbaVarEqv,461,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,calls VarEqv via [0x66119828]; retn 0xC +__vbaVarErrI4,462,stdcall,VariantPtr,0,0,VariantPtr presult;int32 errcode,variant,creates VT_ERROR (vt=0xA); errcode OR'd with 0x800a0000; validates 0..0xffff; retn 8 +__vbaVarFix,463,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pvar,variant,calls VarFix via [0x6611984c]; retn 8 +__vbaVarForInit,464,stdcall,int32,0,0,VariantPtr pcounter;VariantPtr pstart;VariantPtr plimit;VariantPtr pstep;VariantPtr ploopvar;int32 flags,variant,For...Next init; 6 params +__vbaVarForNext,465,stdcall,int32,0,0,VariantPtr pcounter;VariantPtr pstep;VariantPtr plimit,variant,For...Next step; returns 0=continue -1=exit +__vbaVargObj,510,stdcall,VariantPtr,0,0,VariantPtr pVar;IDispatchPtr pObj,variant,Sets pVar to VT_DISPATCH with AddRef; if already holds obj; releases old first +__vbaVargObjAddref,511,stdcall,VariantPtr,0,0,VariantPtr pVar;IDispatchPtr pObj,variant,Like VargObj but does NOT AddRef the source (flag=0 to internal helper) +__vbaVargParmRef,148,fastcall,VariantPtr,0,0,,variant,Dereferences VT_BYREF|VT_VARIANT; if flag set returns pVar->pvarVal; else returns pVar +__vbaVargUnk,602,stdcall,VariantPtr,0,0,VariantPtr pVar;IUnknownPtr pUnk,variant,Sets pVar to VT_UNKNOWN with AddRef; parallel to VargObj for IUnknown +__vbaVargUnkAddref,603,stdcall,VariantPtr,0,0,VariantPtr pVar;IUnknownPtr pUnk,variant,Like VargUnk but does NOT AddRef (flag=0) +__vbaVargVar,149,fastcall,VariantPtr,0,0,VoidPtr pDstVar),variant,Copies 16-byte VARIANT from ecx to pDstVar; if not VT_BYREF; sets VT_BYREF|VT_VARIANT pointing to src +__vbaVargVarCopy,150,fastcall,VariantPtr,0,0,,variant,Copies VARIANT via internal helper with flag=0 (deep copy); returns pSrcVar in eax +__vbaVargVarMove,151,fastcall,VariantPtr,0,0,,variant,Moves VARIANT via internal helper with flag=1 (move/steal); returns pSrcVar +__vbaVargVarRef,152,fastcall,VariantPtr,0,0,,variant,Copies 16 bytes from pSrcVar to pDstVar; if not VT_BYREF; makes it VT_BYREF|VT_VARIANT +__vbaVarIdiv,466,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,thin wrapper over VarIdiv +__vbaVarImp,467,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,calls VarImp via [0x66119830]; retn 0xC +__vbaVarIndexLoad,468,cdecl,VariantPtr,1,0,VariantPtr presult;int32 cDims;VariantPtr parray,variant,bare retn; variadic indices +__vbaVarIndexLoadRef,469,cdecl,VariantPtr,1,0,VariantPtr presult;int32 cDims;VariantPtr parray,variant,bare retn; frees presult if non-empty first +__vbaVarIndexLoadRefLock,470,cdecl,VariantPtr,1,0,VariantPtr presult;int32 cDims;VariantPtr parray,variant,bare retn; frees presult if non-empty; locks array data +__vbaVarIndexStore,471,cdecl,VariantPtr,1,0,VariantPtr pvalue;int32 cDims;VariantPtr parray,variant,bare retn; push flags=4; store value at index +__vbaVarIndexStoreObj,472,cdecl,VariantPtr,1,0,VariantPtr pvalue;int32 cDims;VariantPtr parray,variant,bare retn; push flags=8; Set store at index +__vbaVarInt,473,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pvar,variant,calls VarInt via [0x66119850]; retn 8 +__vbaVarLike,474,stdcall,int32,0,0,VariantPtr pstring;VariantPtr ppattern,variant,Like operator (binary compare); flag=0 +__vbaVarLikeVar,475,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pstring;VariantPtr ppattern,variant,binary Like; push flag=0; calls LikeHelper; writes result variant; retn 0xC +__vbaVarMod,476,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,calls VarMod via [0x66119834]; retn 0xC +__vbaVarMove,145,fastcall,VariantPtr,0,0,VariantPtr pdst;VariantPtr psrc,variant,ecx=dst edx=src; frees dst then moves src; zeroes src vt +__vbaVarMul,477,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,thin wrapper over VarMul +__vbaVarNeg,478,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr poperand,variant,unary; thin wrapper over VarNeg +__vbaVarNot,479,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr poperand,variant,unary; thin wrapper over VarNot +__vbaVarOr,480,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,thin wrapper over VarOr +__vbaVarPow,481,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,thin wrapper over VarPow +__vbaVarSetObj,482,stdcall,VariantPtr,0,0,VariantPtr pdst;IDispatchPtr pobj,variant,frees dst; sets VT_DISPATCH(9); ObjSet flag=1 (release old) +__vbaVarSetObjAddref,483,stdcall,VariantPtr,0,0,VariantPtr pdst;IDispatchPtr pobj,variant,frees dst; sets VT_DISPATCH(9); ObjSet flag=0 (no release) +__vbaVarSetUnk,484,stdcall,VariantPtr,0,0,VariantPtr pdst;IUnknownPtr punk,variant,frees dst; sets VT_UNKNOWN(0xd); ObjSet flag=1 +__vbaVarSetUnkAddref,485,stdcall,VariantPtr,0,0,VariantPtr pdst;IUnknownPtr punk,variant,frees dst; sets VT_UNKNOWN(0xd); ObjSet flag=0 +__vbaVarSetVar,486,stdcall,VariantPtr,0,0,VariantPtr pdst;VariantPtr psrc,variant,calls internal VarCopyObj addref flag=1; retn 8 +__vbaVarSetVarAddref,487,stdcall,VariantPtr,0,0,VariantPtr pdst;VariantPtr psrc,variant,calls internal VarCopyObj addref flag=0; retn 8 +__vbaVarSub,488,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,thin wrapper over VarSub +__vbaVarTextCmpEq,489,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,text; writes VT_BOOL; NULL→VT_NULL +__vbaVarTextCmpGe,490,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,text; writes VT_BOOL; NULL→VT_NULL +__vbaVarTextCmpGt,491,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,text; writes VT_BOOL; NULL→VT_NULL +__vbaVarTextCmpLe,492,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,text; writes VT_BOOL; NULL→VT_NULL +__vbaVarTextCmpLt,493,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,text; writes VT_BOOL; NULL→VT_NULL +__vbaVarTextCmpNe,494,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,text; writes VT_BOOL; NULL→VT_NULL +__vbaVarTextLike,495,stdcall,int32,0,0,VariantPtr pstring;VariantPtr ppattern,variant,Like operator (text compare); flag=1 +__vbaVarTextLikeVar,496,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pstring;VariantPtr ppattern,variant,text Like; push flag=1; calls LikeHelper; writes result variant; retn 0xC +__vbaVarTextTstEq,497,stdcall,int32,0,0,VariantPtr pleft;VariantPtr pright,variant,text cmp mode=1; lookup 0x6601d9a4; 0/-1 +__vbaVarTextTstGe,498,stdcall,int32,0,0,VariantPtr pleft;VariantPtr pright,variant,text cmp mode=1; lookup 0x6601d9c4; 0/-1 +__vbaVarTextTstGt,499,stdcall,int32,0,0,VariantPtr pleft;VariantPtr pright,variant,text cmp mode=1; lookup 0x6601d9b0; 0/-1 +__vbaVarTextTstLe,500,stdcall,int32,0,0,VariantPtr pleft;VariantPtr pright,variant,text cmp mode=1; lookup 0x6601d9c8; 0/-1 +__vbaVarTextTstLt,501,stdcall,int32,0,0,VariantPtr pleft;VariantPtr pright,variant,text cmp mode=1; lookup 0x6601d9a8; 0/-1 +__vbaVarTextTstNe,502,stdcall,int32,0,0,VariantPtr pleft;VariantPtr pright,variant,text cmp mode=1; lookup 0x6601d9b8; 0/-1 +__vbaVarTstEq,503,stdcall,int32,0,0,VariantPtr pleft;VariantPtr pright,variant,test equality; returns 0 or -1 +__vbaVarTstGe,504,stdcall,int32,0,0,VariantPtr pleft;VariantPtr pright,variant,binary cmp mode=0; lookup 0x6601d9c4; 0/-1 +__vbaVarTstGt,505,stdcall,int32,0,0,VariantPtr pleft;VariantPtr pright,variant,test greater than; returns 0 or -1 +__vbaVarTstLe,506,stdcall,int32,0,0,VariantPtr pleft;VariantPtr pright,variant,test less or equal; returns 0 or -1 +__vbaVarTstLt,507,stdcall,int32,0,0,VariantPtr pleft;VariantPtr pright,variant,test less than; returns 0 or -1 +__vbaVarTstNe,508,stdcall,int32,0,0,VariantPtr pleft;VariantPtr pright,variant,test not equal; returns 0 or -1 +__vbaVarVargNofree,146,fastcall,VariantPtr,0,0,VariantPtr pdst;VariantPtr psrc,variant,ecx=dst edx=src; resolves VT_VARIANT|VT_BYREF; no free on dst +__vbaVarXor,509,stdcall,VariantPtr,0,0,VariantPtr presult;VariantPtr pleft;VariantPtr pright,variant,calls VarXor via [0x66119844]; retn 0xC +__vbaVerifyVarObj,604,stdcall,VariantPtr,0,0,VariantPtr pvar,variant,validates VT_DISPATCH(9) or VT_UNKNOWN(0xd) with VT_BYREF mask (0xbfff); error 0x1a8 if invalid; retn 4 # # === ARRAY OPERATIONS === # -__vbaAryConstruct,165,stdcall,void,0,SafeArrayPtr ppAry;VoidPtr pDescriptor,array,Thin wrapper: calls the array constructor at 0x660482BA (ppAry; pDescriptor; 0) then error handler. Creates array from descriptor with no extra bounds. retn 0x8. -__vbaAryConstruct2,164,stdcall,void,0,SafeArrayPtr ppAry;VoidPtr pDescriptor;VoidPtr pBounds,array,Like AryConstruct but with explicit bounds pointer (3rd param). If descriptor has FADF_AUTO (0x20) bit; dereferences pBounds. retn 0xc. -__vbaAryCopy,166,stdcall,void,0,SafeArrayPtr ppDst;SafeArrayPtr ppSrc,array,Erases dst if different from src; then copies src array to dst via SafeArrayCopy. retn 0x8. -__vbaAryDestruct,167,stdcall,void,0,uint16 arrayDesc;SafeArrayPtrPtr ppArray,array,calls __vbaErase then SafeArrayDestroyData -__vbaAryLock,108,stdcall,SafeArrayPtr,0,SafeArrayPtr ppLocked;SafeArrayPtr psa,array,Calls SafeArrayLock(psa); stores psa into *ppLocked; returns psa. Raises error 9 if psa is NULL. retn 0x8. -__vbaAryMove,168,stdcall,void,0,SafeArrayPtrPtr ppdst;SafeArrayPtrPtr ppsrc,array,erases dst; moves src ptr to dst; NULLs src -__vbaAryRebase1Var,169,stdcall,VariantPtr,0,VariantPtr pVar,array,If pVar holds VT_ARRAY|VT_VARIANT (0x200c) with a 1-dim SAFEARRAY; sets lLbound=1 (Option Base 1). retn 0x4. -__vbaAryRecCopy,170,stdcall,void,0,VoidPtr pRecInfo;SafeArrayPtr ppDst;SafeArrayPtr ppSrc,array,Copies a record (UDT) array. Uses pRecInfo for element-level copy semantics. Erases dst first if needed. retn 0xc. -__vbaAryRecMove,171,stdcall,void,0,VoidPtr pRecInfo;SafeArrayPtr ppDst;SafeArrayPtr ppSrc,array,Moves a record (UDT) array. Erases dst; then moves src pointer to dst and zeroes src. retn 0xc. -__vbaAryUnlock,172,stdcall,void,0,SafeArrayPtr ppLocked,array,Calls SafeArrayUnlock(*ppLocked); then zeroes *ppLocked. retn 0x4. -__vbaAryVar,173,stdcall,SafeArrayPtr,0,uint16 vt;VariantPtr pVar,array,Extracts SAFEARRAY* from a VARIANT. Validates VT_ARRAY bit (0x2000); handles VT_BYREF indirection. Validates element type matches vt. retn 0x8. -__vbaAryVarVarg,117,fastcall,VariantPtr,0,VariantPtr pDst;int32 cArgs,array,Wraps a varg array into a VARIANT. Sets pDst->vt=VT_ARRAY|VT_VARIANT (0x200c); calls internal to build SAFEARRAY from stack args. bare retn. -__vbaCVarAryUdt,177,stdcall,VariantPtr,0,VoidPtr pRecInfo;VariantPtr pDst;int32 vt;SafeArrayPtr ppAry,array,Converts a UDT array to a VARIANT. Creates SAFEARRAY descriptor if needed; sets element size/type; copies IRecordInfo. Stores result in pDst. retn 0x10. -__vbaDerefAry,210,cdecl,VoidPtr,1,uint16 cDims;SafeArrayPtr psa,array,Multi-dimensional array element dereference. Variadic indices follow on stack. Validates dims/bounds; computes pvData + element offset. bare retn (cdecl; caller cleans). -__vbaDerefAry1,211,stdcall,VoidPtr,0,SafeArrayPtr psa;int32 index,array,1-dimensional array element dereference. Validates dims==1 and bounds; returns pvData + cbElements*(index-lLbound). retn 0x8. -__vbaErase,213,stdcall,void,0,uint16 arrayDesc;SafeArrayPtrPtr ppArray,array,calls SafeArrayDestroyData -__vbaEraseKeepData,214,stdcall,void,0,int32 fCleanup;SafeArrayPtr ppAry,array,Erases array data but keeps the descriptor. Sets FADF_FIXEDSIZE|FADF_STATIC (0x12) before erase; clears after. retn 0x8. -__vbaEraseNoPop,215,stdcall,void,0,SafeArrayPtr ppAry,array,Validates array not locked (fFeatures & 0x16 == 0); then calls __vbaErase(0; ppAry). retn 0x4. -__vbaLbound,325,stdcall,int32,0,int16 dimension;SafeArrayPtr psa,array,Returns lLbound for the given 1-based dimension. Validates dimension >= 1 and <= cDims. retn 0x8. -__vbaLdZeroAry,326,stdcall,SafeArrayPtr,0,SafeArrayPtr ppAry;VoidPtr pTemplate,array,Destroys existing array via SafeArrayDestroy; allocates new SAFEARRAY descriptor from template; copies bounds and element info; transfers pvData. retn 0x8. -__vbaRedim,385,cdecl,SafeArrayPtr,1,uint32 flags;uint32 arrayDesc;SafeArrayPtrPtr ppArray;uint32 elemSize;uint32 numDims,array,variadic dim bounds (lbound;ubound pairs) follow numDims -__vbaRedimPreserve,386,cdecl,SafeArrayPtr,1,uint32 flags;uint32 arrayDesc;SafeArrayPtrPtr ppArray;uint32 elemSize;uint32 numDims,array,same as Redim with preserve semantics -__vbaRedimPreserveVar,387,cdecl,SafeArrayPtr,1,uint32 flags;SafeArrayPtr ppsa;uint16 vt;uint32 numDims;VoidPtr pBounds,array,0x660417cc; same entry as __vbaRedim; flag 0x20 bit determines preserve behavior -__vbaRedimPreserveVar2,388,cdecl,SafeArrayPtr,1,uint32 flags;SafeArrayPtr ppsa;uint16 vt;uint32 numDims;VoidPtr pBounds,array,0x66014f60; same entry as __vbaRedimPreserve -__vbaRedimVar,389,cdecl,VariantPtr,1,uint16 flags;VariantPtr pVar;uint16 numDims;VoidPtr pBounds,array,0x660eda6e; var-args for bounds; delegates to internal redim helper 0x660ed604 with preserve=-1 -__vbaRedimVar2,110,cdecl,VariantPtr,1,uint16 flags;uint16 numDims;VariantPtr pVar;VoidPtr pUdtInfo;VoidPtr pBounds,array,0x660eda90; UDT-aware version with extra pUdtInfo param; delegates to 0x660ed604 with preserve=0 -__vbaRefVarAry,390,stdcall,SafeArrayPtr,0,VariantPtr pVar,array,Returns a pointer to the SAFEARRAY* inside a VARIANT. If VT_BYREF; returns *(pVar+8); otherwise returns &pVar->data. Validates VT_ARRAY. retn 0x4. -__vbaUbound,445,stdcall,int32,0,SafeArrayPtr pArray;uint16 dimension,array,validates dim and returns lbound+cElements-1 +__vbaAryConstruct,165,stdcall,void,0,0,SafeArrayPtr ppAry;VoidPtr pDescriptor,array,Thin wrapper: calls the array constructor at 0x660482BA (ppAry; pDescriptor; 0) then error handler. Creates array from descriptor with no extra bounds. retn 0x8. +__vbaAryConstruct2,164,stdcall,void,0,0,SafeArrayPtr ppAry;VoidPtr pDescriptor;VoidPtr pBounds,array,Like AryConstruct but with explicit bounds pointer (3rd param). If descriptor has FADF_AUTO (0x20) bit; dereferences pBounds. retn 0xc. +__vbaAryCopy,166,stdcall,void,0,0,SafeArrayPtr ppDst;SafeArrayPtr ppSrc,array,Erases dst if different from src; then copies src array to dst via SafeArrayCopy. retn 0x8. +__vbaAryDestruct,167,stdcall,void,0,0,uint16 arrayDesc;SafeArrayPtrPtr ppArray,array,calls __vbaErase then SafeArrayDestroyData +__vbaAryLock,108,stdcall,SafeArrayPtr,0,0,SafeArrayPtr ppLocked;SafeArrayPtr psa,array,Calls SafeArrayLock(psa); stores psa into *ppLocked; returns psa. Raises error 9 if psa is NULL. retn 0x8. +__vbaAryMove,168,stdcall,void,0,0,SafeArrayPtrPtr ppdst;SafeArrayPtrPtr ppsrc,array,erases dst; moves src ptr to dst; NULLs src +__vbaAryRebase1Var,169,stdcall,VariantPtr,0,0,VariantPtr pVar,array,If pVar holds VT_ARRAY|VT_VARIANT (0x200c) with a 1-dim SAFEARRAY; sets lLbound=1 (Option Base 1). retn 0x4. +__vbaAryRecCopy,170,stdcall,void,0,0,VoidPtr pRecInfo;SafeArrayPtr ppDst;SafeArrayPtr ppSrc,array,Copies a record (UDT) array. Uses pRecInfo for element-level copy semantics. Erases dst first if needed. retn 0xc. +__vbaAryRecMove,171,stdcall,void,0,0,VoidPtr pRecInfo;SafeArrayPtr ppDst;SafeArrayPtr ppSrc,array,Moves a record (UDT) array. Erases dst; then moves src pointer to dst and zeroes src. retn 0xc. +__vbaAryUnlock,172,stdcall,void,0,0,SafeArrayPtr ppLocked,array,Calls SafeArrayUnlock(*ppLocked); then zeroes *ppLocked. retn 0x4. +__vbaAryVar,173,stdcall,SafeArrayPtr,0,0,uint16 vt;VariantPtr pVar,array,Extracts SAFEARRAY* from a VARIANT. Validates VT_ARRAY bit (0x2000); handles VT_BYREF indirection. Validates element type matches vt. retn 0x8. +__vbaAryVarVarg,117,fastcall,VariantPtr,0,0,VariantPtr pDst;int32 cArgs,array,Wraps a varg array into a VARIANT. Sets pDst->vt=VT_ARRAY|VT_VARIANT (0x200c); calls internal to build SAFEARRAY from stack args. bare retn. +__vbaCVarAryUdt,177,stdcall,VariantPtr,0,0,VoidPtr pRecInfo;VariantPtr pDst;int32 vt;SafeArrayPtr ppAry,array,Converts a UDT array to a VARIANT. Creates SAFEARRAY descriptor if needed; sets element size/type; copies IRecordInfo. Stores result in pDst. retn 0x10. +__vbaDerefAry,210,cdecl,VoidPtr,1,0,uint16 cDims;SafeArrayPtr psa,array,Multi-dimensional array element dereference. Variadic indices follow on stack. Validates dims/bounds; computes pvData + element offset. bare retn (cdecl; caller cleans). +__vbaDerefAry1,211,stdcall,VoidPtr,0,0,SafeArrayPtr psa;int32 index,array,1-dimensional array element dereference. Validates dims==1 and bounds; returns pvData + cbElements*(index-lLbound). retn 0x8. +__vbaErase,213,stdcall,void,0,0,uint16 arrayDesc;SafeArrayPtrPtr ppArray,array,calls SafeArrayDestroyData +__vbaEraseKeepData,214,stdcall,void,0,0,int32 fCleanup;SafeArrayPtr ppAry,array,Erases array data but keeps the descriptor. Sets FADF_FIXEDSIZE|FADF_STATIC (0x12) before erase; clears after. retn 0x8. +__vbaEraseNoPop,215,stdcall,void,0,0,SafeArrayPtr ppAry,array,Validates array not locked (fFeatures & 0x16 == 0); then calls __vbaErase(0; ppAry). retn 0x4. +__vbaLbound,325,stdcall,int32,0,0,int16 dimension;SafeArrayPtr psa,array,Returns lLbound for the given 1-based dimension. Validates dimension >= 1 and <= cDims. retn 0x8. +__vbaLdZeroAry,326,stdcall,SafeArrayPtr,0,0,SafeArrayPtr ppAry;VoidPtr pTemplate,array,Destroys existing array via SafeArrayDestroy; allocates new SAFEARRAY descriptor from template; copies bounds and element info; transfers pvData. retn 0x8. +__vbaRedim,385,cdecl,SafeArrayPtr,1,0,uint32 flags;uint32 arrayDesc;SafeArrayPtrPtr ppArray;uint32 elemSize;uint32 numDims,array,variadic dim bounds (lbound;ubound pairs) follow numDims +__vbaRedimPreserve,386,cdecl,SafeArrayPtr,1,0,uint32 flags;uint32 arrayDesc;SafeArrayPtrPtr ppArray;uint32 elemSize;uint32 numDims,array,same as Redim with preserve semantics +__vbaRedimPreserveVar,387,cdecl,SafeArrayPtr,1,0,uint32 flags;SafeArrayPtr ppsa;uint16 vt;uint32 numDims;VoidPtr pBounds,array,0x660417cc; same entry as __vbaRedim; flag 0x20 bit determines preserve behavior +__vbaRedimPreserveVar2,388,cdecl,SafeArrayPtr,1,0,uint32 flags;SafeArrayPtr ppsa;uint16 vt;uint32 numDims;VoidPtr pBounds,array,0x66014f60; same entry as __vbaRedimPreserve +__vbaRedimVar,389,cdecl,VariantPtr,1,0,uint16 flags;VariantPtr pVar;uint16 numDims;VoidPtr pBounds,array,0x660eda6e; var-args for bounds; delegates to internal redim helper 0x660ed604 with preserve=-1 +__vbaRedimVar2,110,cdecl,VariantPtr,1,0,uint16 flags;uint16 numDims;VariantPtr pVar;VoidPtr pUdtInfo;VoidPtr pBounds,array,0x660eda90; UDT-aware version with extra pUdtInfo param; delegates to 0x660ed604 with preserve=0 +__vbaRefVarAry,390,stdcall,SafeArrayPtr,0,0,VariantPtr pVar,array,Returns a pointer to the SAFEARRAY* inside a VARIANT. If VT_BYREF; returns *(pVar+8); otherwise returns &pVar->data. Validates VT_ARRAY. retn 0x4. +__vbaUbound,445,stdcall,int32,0,0,SafeArrayPtr pArray;uint16 dimension,array,validates dim and returns lbound+cElements-1 # # === FOREACH === # -__vbaExitEachAry,219,stdcall,void,0,SafeArrayPtr ppsa,foreach,0x6611197d; retn 0x4; calls SafeArrayUnlock and clears pointer -__vbaExitEachColl,221,stdcall,void,0,VoidPtr ppEnum,foreach,0x6611199f; retn 0x4; Release()s the enumerator and clears pointer -__vbaExitEachVar,222,stdcall,void,0,VoidPtr ppEnum;SafeArrayPtr ppsa;VoidPtr ppColl,foreach,0x6611195d; retn 0xc; dispatches to __vbaExitEachAry or __vbaExitEachColl based on ppsa -__vbaForEachAry,238,stdcall,int32,0,IUnknownPtr pCollection;VariantPtr pVar;VoidPtr ppDisp,foreach,0x6610c6a8; retn 0xc; calls __vbaForEachCollVar internally; extracts IDispatch from VARIANT -__vbaForEachCollAd,239,stdcall,int32,0,VoidPtr ppEnum;VoidPtr ppObj;IUnknownPtr pCollection,foreach,wraps ForEachCollVar; extracts IDispatch* -__vbaForEachCollObj,240,stdcall,int32,0,uint32 vtType;VoidPtr ppObj;VoidPtr ppEnum;IUnknownPtr pCollection,foreach,calls ForEachCollAd; QI for target interface -__vbaForEachCollVar,241,stdcall,int32,0,VoidPtr ppEnum;VariantPtr pVar;IUnknownPtr pCollection,foreach,obtains IEnumVARIANT via _NewEnum; calls Next -__vbaForEachVar,242,stdcall,int32,0,IUnknownPtr pCollection;VariantPtr pVar;VoidPtr ppEnum,foreach,0x6610c6fa; retn 0xc; creates IEnumVARIANT; calls __vbaNextEachCollVar -__vbaNextEachAry,342,stdcall,int32,0,uint16 vt;SafeArrayPtr ppsa;Int32Ptr pIndex;VariantPtr pVar,foreach,0x66111672; retn 0x10; iterates SAFEARRAY element by type; returns -1 if more or 0 if done -__vbaNextEachCollAd,343,stdcall,int32,0,VoidPtr pEnum;VoidPtr ppObj,foreach,wraps NextEachCollVar; extracts IDispatch -__vbaNextEachCollObj,344,stdcall,int32,0,uint32 vtType;VoidPtr pEnum;VoidPtr ppObj,foreach,wraps NextEachCollAd; QI result for target type -__vbaNextEachCollVar,345,stdcall,int32,0,VoidPtr pEnum;VariantPtr pVar,foreach,calls IEnumVARIANT::Next(1); returns -1 if exhausted -__vbaNextEachVar,346,stdcall,int32,0,uint16 vt;SafeArrayPtr ppsa;Int32Ptr pIndex;VoidPtr pEnum;VariantPtr pVar,foreach,0x66111915; retn 0x14; dispatcher: if array type calls __vbaNextEachAry; else __vbaNextEachCollVar +__vbaExitEachAry,219,stdcall,void,0,0,SafeArrayPtr ppsa,foreach,0x6611197d; retn 0x4; calls SafeArrayUnlock and clears pointer +__vbaExitEachColl,221,stdcall,void,0,0,VoidPtr ppEnum,foreach,0x6611199f; retn 0x4; Release()s the enumerator and clears pointer +__vbaExitEachVar,222,stdcall,void,0,0,VoidPtr ppEnum;SafeArrayPtr ppsa;VoidPtr ppColl,foreach,0x6611195d; retn 0xc; dispatches to __vbaExitEachAry or __vbaExitEachColl based on ppsa +__vbaForEachAry,238,stdcall,int32,0,0,IUnknownPtr pCollection;VariantPtr pVar;VoidPtr ppDisp,foreach,0x6610c6a8; retn 0xc; calls __vbaForEachCollVar internally; extracts IDispatch from VARIANT +__vbaForEachCollAd,239,stdcall,int32,0,0,VoidPtr ppEnum;VoidPtr ppObj;IUnknownPtr pCollection,foreach,wraps ForEachCollVar; extracts IDispatch* +__vbaForEachCollObj,240,stdcall,int32,0,0,uint32 vtType;VoidPtr ppObj;VoidPtr ppEnum;IUnknownPtr pCollection,foreach,calls ForEachCollAd; QI for target interface +__vbaForEachCollVar,241,stdcall,int32,0,0,VoidPtr ppEnum;VariantPtr pVar;IUnknownPtr pCollection,foreach,obtains IEnumVARIANT via _NewEnum; calls Next +__vbaForEachVar,242,stdcall,int32,0,0,IUnknownPtr pCollection;VariantPtr pVar;VoidPtr ppEnum,foreach,0x6610c6fa; retn 0xc; creates IEnumVARIANT; calls __vbaNextEachCollVar +__vbaNextEachAry,342,stdcall,int32,0,0,uint16 vt;SafeArrayPtr ppsa;Int32Ptr pIndex;VariantPtr pVar,foreach,0x66111672; retn 0x10; iterates SAFEARRAY element by type; returns -1 if more or 0 if done +__vbaNextEachCollAd,343,stdcall,int32,0,0,VoidPtr pEnum;VoidPtr ppObj,foreach,wraps NextEachCollVar; extracts IDispatch +__vbaNextEachCollObj,344,stdcall,int32,0,0,uint32 vtType;VoidPtr pEnum;VoidPtr ppObj,foreach,wraps NextEachCollAd; QI result for target type +__vbaNextEachCollVar,345,stdcall,int32,0,0,VoidPtr pEnum;VariantPtr pVar,foreach,calls IEnumVARIANT::Next(1); returns -1 if exhausted +__vbaNextEachVar,346,stdcall,int32,0,0,uint16 vt;SafeArrayPtr ppsa;Int32Ptr pIndex;VoidPtr pEnum;VariantPtr pVar,foreach,0x66111915; retn 0x14; dispatcher: if array type calls __vbaNextEachAry; else __vbaNextEachCollVar # # === LATE BINDING === # -__vbaLateIdCall,282,cdecl,void,1,IDispatchPtr pObj;VoidPtr pwszName;uint32 callFlags;uint32 argCount,late,varargs are VARIANT*; IDispatch::Invoke DISPATCH_METHOD -__vbaLateIdCallLd,283,cdecl,VariantPtr,1,VariantPtr pRetVar;IDispatchPtr pObj;VoidPtr pwszName;uint32 callFlags;uint32 argCount,late,like LateIdCall but captures return value -__vbaLateIdCallSt,285,cdecl,void,1,IUnknownPtr pObj;int32 lateFlags;Bstr name;int32 argCount,late,0x6610c2f3; wraps __vbaLateIdCall with DISPATCH_METHOD flag; pushes 0 result then delegates to common late-bind dispatcher at 0x66106631 -__vbaLateIdSt,290,stdcall,void,0,IDispatchPtr pObj;VoidPtr pwszName;uint32 callFlags;uint32 argCount;VariantPtr pVal1;VariantPtr pVal2,late,property store via DISPATCH_PROPERTYPUT -__vbaLateIdStAd,291,cdecl,void,1,IUnknownPtr pObj;IUnknownPtr pObjRef;int32 lateFlags;Bstr name;int32 argCount,late,0x6610c29d; late-bound property store by DispID with address result; flag 0x8 (DISPATCH_PROPERTYPUTREF) -__vbaLateMemCall,292,cdecl,void,1,IUnknownPtr pObj;VoidPtr pwszName;uint32 callFlags;uint32 argCount,late,resolves IUnknown→IDispatch first -__vbaLateMemCallLd,293,cdecl,VariantPtr,1,VariantPtr pRetVar;IUnknownPtr pObj;VoidPtr pwszName;uint32 callFlags;uint32 argCount,late,like LateMemCall but captures return value -__vbaLateMemSt,323,stdcall,void,0,IUnknownPtr pObj;VoidPtr pwszName;uint32 callFlags;uint32 argCount;VariantPtr pVal1;VariantPtr pVal2,late,resolves IUnknown→IDispatch then property store -__vbaLateMemStAd,324,cdecl,void,1,IUnknownPtr pObj;VoidPtr pwszName;uint32 callFlags;uint32 argCount,late,store-by-address variant; flag=0x8 -__vbaVarLateMemCallLd,112,cdecl,VariantPtr,1,VariantPtr pRetVar;VariantPtr pObjVar;VoidPtr pwszName;uint32 callFlags;uint32 argCount,late,object passed as VARIANT; flag=3 -__vbaVarLateMemCallLdRf,113,cdecl,VariantPtr,1,VariantPtr pRetVar;VariantPtr pObjVar;VoidPtr pwszName;uint32 callFlags;uint32 argCount,late,same as VarLateMemCallLd but flag=0x80000003 (ByRef) -__vbaVarLateMemSt,115,stdcall,void,0,VariantPtr pObjVar;VoidPtr pwszName;uint32 callFlags;uint32 argCount;VariantPtr pVal1;VariantPtr pVal2,late,object as VARIANT; store via flag=4 -__vbaVarLateMemStAd,116,cdecl,void,1,VariantPtr pObjVar;VoidPtr pwszName;uint32 callFlags;uint32 argCount,late,store-by-address; flag=8; object as VARIANT +__vbaLateIdCall,282,cdecl,void,1,0,IDispatchPtr pObj;VoidPtr pwszName;uint32 callFlags;uint32 argCount,late,varargs are VARIANT*; IDispatch::Invoke DISPATCH_METHOD +__vbaLateIdCallLd,283,cdecl,VariantPtr,1,0,VariantPtr pRetVar;IDispatchPtr pObj;VoidPtr pwszName;uint32 callFlags;uint32 argCount,late,like LateIdCall but captures return value +__vbaLateIdCallSt,285,cdecl,void,1,0,IUnknownPtr pObj;int32 lateFlags;Bstr name;int32 argCount,late,0x6610c2f3; wraps __vbaLateIdCall with DISPATCH_METHOD flag; pushes 0 result then delegates to common late-bind dispatcher at 0x66106631 +__vbaLateIdSt,290,stdcall,void,0,0,IDispatchPtr pObj;VoidPtr pwszName;uint32 callFlags;uint32 argCount;VariantPtr pVal1;VariantPtr pVal2,late,property store via DISPATCH_PROPERTYPUT +__vbaLateIdStAd,291,cdecl,void,1,0,IUnknownPtr pObj;IUnknownPtr pObjRef;int32 lateFlags;Bstr name;int32 argCount,late,0x6610c29d; late-bound property store by DispID with address result; flag 0x8 (DISPATCH_PROPERTYPUTREF) +__vbaLateMemCall,292,cdecl,void,1,0,IUnknownPtr pObj;VoidPtr pwszName;uint32 callFlags;uint32 argCount,late,resolves IUnknown→IDispatch first +__vbaLateMemCallLd,293,cdecl,VariantPtr,1,0,VariantPtr pRetVar;IUnknownPtr pObj;VoidPtr pwszName;uint32 callFlags;uint32 argCount,late,like LateMemCall but captures return value +__vbaLateMemSt,323,stdcall,void,0,0,IUnknownPtr pObj;VoidPtr pwszName;uint32 callFlags;uint32 argCount;VariantPtr pVal1;VariantPtr pVal2,late,resolves IUnknown→IDispatch then property store +__vbaLateMemStAd,324,cdecl,void,1,0,IUnknownPtr pObj;VoidPtr pwszName;uint32 callFlags;uint32 argCount,late,store-by-address variant; flag=0x8 +__vbaVarLateMemCallLd,112,cdecl,VariantPtr,1,0,VariantPtr pRetVar;VariantPtr pObjVar;VoidPtr pwszName;uint32 callFlags;uint32 argCount,late,object passed as VARIANT; flag=3 +__vbaVarLateMemCallLdRf,113,cdecl,VariantPtr,1,0,VariantPtr pRetVar;VariantPtr pObjVar;VoidPtr pwszName;uint32 callFlags;uint32 argCount,late,same as VarLateMemCallLd but flag=0x80000003 (ByRef) +__vbaVarLateMemSt,115,stdcall,void,0,0,VariantPtr pObjVar;VoidPtr pwszName;uint32 callFlags;uint32 argCount;VariantPtr pVal1;VariantPtr pVal2,late,object as VARIANT; store via flag=4 +__vbaVarLateMemStAd,116,cdecl,void,1,0,VariantPtr pObjVar;VoidPtr pwszName;uint32 callFlags;uint32 argCount,late,store-by-address; flag=8; object as VARIANT # # === CURRENCY === # -__vbaCyAbs,185,stdcall,int64,0,int64 cyValue,currency,If high dword negative; neg low + not/sbb high; overflow check; retn 8 -__vbaCyAdd,188,stdcall,int64,0,int32 cyLo1;int32 cyHi1;int32 cyLo2;int32 cyHi2,currency,64-bit add via add/adc; overflow→error 6; returns EDX:EAX -__vbaCyErrVar,189,stdcall,int64,0,VariantPtr pVar,currency,Like CyVar but with Error variant support; thunk -> shared dispatcher -__vbaCyFix,191,stdcall,int64,0,int32 cyLo;int32 cyHi,currency,FPU truncate-toward-zero (chop rounding); returns EDX:EAX -__vbaCyForInit,192,stdcall,int32,0,Int64Ptr pCounter;int32 startLo;int32 startHi;int32 limitLo;int32 limitHi;int32 stepLo;int32 stepHi,currency,Stores start into *pCounter; then calls ForNext check; retn 0x1C -__vbaCyForNext,193,stdcall,int32,0,Int64Ptr pCounter;int32 limitLo;int32 limitHi;int32 stepLo;int32 stepHi,currency,Adds step via __vbaCyAdd; stores; then checks limit; retn 0x14 -__vbaCyI2,194,stdcall,int64,0,int16 value,currency,movsx; imul * 10000; returns in EDX:EAX; retn 4 -__vbaCyI4,196,stdcall,int64,0,int32 value,currency,imul value * 10000; returns in EDX:EAX; retn 4 -__vbaCyInt,197,stdcall,int64,0,int32 cyLo;int32 cyHi,currency,FPU floor (round toward -inf); returns EDX:EAX -__vbaCyMul,198,stdcall,int64,0,int32 cyLo1;int32 cyHi1;int32 cyLo2;int32 cyHi2,currency,FPU fild/fmul/fdiv by 10000.0; returns EDX:EAX -__vbaCyMulI2,200,stdcall,int64,0,int16 multiplier;int32 cyLo;int32 cyHi,currency,sign-extends I2→I4; delegates to VarCyMulI4 -__vbaCySgn,201,stdcall,int16,0,int64 cyValue,currency,Pushes 0:0 and cyValue; calls internal CY compare; retn 8 -__vbaCyStr,202,stdcall,int64,0,Bstr pStr,currency,Calls VarCyFromStr; returns CY in EDX:EAX; retn 4 -__vbaCySub,203,stdcall,int64,0,int32 cyLo1;int32 cyHi1;int32 cyLo2;int32 cyHi2,currency,64-bit sub via sub/sbb; overflow→error 6; returns EDX:EAX -__vbaCyUI1,204,stdcall,int64,0,uint8 value,currency,movzx byte; imul 10000; returns in EDX:EAX; retn 4 -__vbaCyVar,205,stdcall,int64,0,VariantPtr pVar,currency,Dispatches via vtype jump table; returns CY in EDX:EAX; thunk -> shared dispatcher +__vbaCyAbs,185,stdcall,int64,0,0,int64 cyValue,currency,If high dword negative; neg low + not/sbb high; overflow check; retn 8 +__vbaCyAdd,188,stdcall,int64,0,0,int32 cyLo1;int32 cyHi1;int32 cyLo2;int32 cyHi2,currency,64-bit add via add/adc; overflow→error 6; returns EDX:EAX +__vbaCyErrVar,189,stdcall,int64,0,0,VariantPtr pVar,currency,Like CyVar but with Error variant support; thunk -> shared dispatcher +__vbaCyFix,191,stdcall,int64,0,0,int32 cyLo;int32 cyHi,currency,FPU truncate-toward-zero (chop rounding); returns EDX:EAX +__vbaCyForInit,192,stdcall,int32,0,0,Int64Ptr pCounter;int32 startLo;int32 startHi;int32 limitLo;int32 limitHi;int32 stepLo;int32 stepHi,currency,Stores start into *pCounter; then calls ForNext check; retn 0x1C +__vbaCyForNext,193,stdcall,int32,0,0,Int64Ptr pCounter;int32 limitLo;int32 limitHi;int32 stepLo;int32 stepHi,currency,Adds step via __vbaCyAdd; stores; then checks limit; retn 0x14 +__vbaCyI2,194,stdcall,int64,0,0,int16 value,currency,movsx; imul * 10000; returns in EDX:EAX; retn 4 +__vbaCyI4,196,stdcall,int64,0,0,int32 value,currency,imul value * 10000; returns in EDX:EAX; retn 4 +__vbaCyInt,197,stdcall,int64,0,0,int32 cyLo;int32 cyHi,currency,FPU floor (round toward -inf); returns EDX:EAX +__vbaCyMul,198,stdcall,int64,0,0,int32 cyLo1;int32 cyHi1;int32 cyLo2;int32 cyHi2,currency,FPU fild/fmul/fdiv by 10000.0; returns EDX:EAX +__vbaCyMulI2,200,stdcall,int64,0,0,int16 multiplier;int32 cyLo;int32 cyHi,currency,sign-extends I2→I4; delegates to VarCyMulI4 +__vbaCySgn,201,stdcall,int16,0,0,int64 cyValue,currency,Pushes 0:0 and cyValue; calls internal CY compare; retn 8 +__vbaCyStr,202,stdcall,int64,0,0,Bstr pStr,currency,Calls VarCyFromStr; returns CY in EDX:EAX; retn 4 +__vbaCySub,203,stdcall,int64,0,0,int32 cyLo1;int32 cyHi1;int32 cyLo2;int32 cyHi2,currency,64-bit sub via sub/sbb; overflow→error 6; returns EDX:EAX +__vbaCyUI1,204,stdcall,int64,0,0,uint8 value,currency,movzx byte; imul 10000; returns in EDX:EAX; retn 4 +__vbaCyVar,205,stdcall,int64,0,0,VariantPtr pVar,currency,Dispatches via vtype jump table; returns CY in EDX:EAX; thunk -> shared dispatcher # # === ERROR HANDLING === # -__vbaEnd,212,cdecl,void,0,,error,terminates VB6 program; noreturn -__vbaError,216,stdcall,void,0,uint16 errNum,error,0x660e6a99; retn 0x4; raises VB error by number; 0 or >0xFFFF maps to error 5 (invalid call) -__vbaErrorOverflow,217,cdecl,void,0,,error,0x660e6ab2; pushes 6 (Overflow) and calls error handler; noreturn -__vbaExceptHandler,218,cdecl,int32,0,VoidPtr pExcRec;VoidPtr pFrame;VoidPtr pCtx;VoidPtr pDispCtx,error,standard SEH handler; handles stack overflow specially -__vbaExitProc,223,cdecl,void,0,,error,walks SEH chain via fs:[0]; clears TLS error state -__vbaFailedFriend,228,cdecl,void,0,,error,0x660e592d; pushes error 0x61 (97 = Friend access violation) and calls error handler; noreturn -__vbaGenerateBoundsError,246,cdecl,void,0,,error,0x660edaf4; pushes error 9 (Subscript out of range) and calls error handler; noreturn -__vbaHresultCheck,256,stdcall,void,0,Hresult hr;IUnknownPtr pObj;GuidPtr piid;VoidPtr pVTable,error,0x660427c2; retn 0x10; checks TLS error state; converts specific HRESULTs (0x800A9C68->0x800A01B8); dispatches to error mapper -__vbaHresultCheckNonvirt,257,stdcall,void,0,Hresult hr;IUnknownPtr pObj;GuidPtr piid;VoidPtr pVTable,error,export exists; similar to HresultCheck but for non-virtual calls -__vbaHresultCheckObj,258,stdcall,void,0,int32 hr;VoidPtr pUnk;VoidPtr pGuid;VoidPtr pAdjust,error,checks HRESULT; raises error if failed -__vbaOnError,352,stdcall,void,0,int32 nHandler,error,On Error handler; 0xFFFFFFFD=Resume Next -__vbaResume,391,stdcall,void,0,int32 nResumeKind,error,Resume handler; -2=Resume -1=Resume Next >0=label -__vbaSetSystemError,394,cdecl,void,0,,error,calls GetLastError(); stores in TLS+0x9C -__vbaStopExe,395,stdcall,void,0,,error,Displays 'Stop statement encountered' message then raises error 0x9c68; never returns normally +__vbaEnd,212,cdecl,void,0,1,,error,terminates VB6 program; noreturn +__vbaError,216,stdcall,void,0,1,uint16 errNum,error,0x660e6a99; raises VB error by number; 0 or >0xFFFF maps to error 5 (invalid call); noreturn +__vbaErrorOverflow,217,cdecl,void,0,1,,error,0x660e6ab2; pushes 6 (Overflow) and calls error handler; noreturn +__vbaExceptHandler,218,cdecl,int32,0,0,VoidPtr pExcRec;VoidPtr pFrame;VoidPtr pCtx;VoidPtr pDispCtx,error,standard SEH handler; handles stack overflow specially +__vbaExitProc,223,cdecl,void,0,0,,error,walks SEH chain via fs:[0]; clears TLS error state +__vbaFailedFriend,228,cdecl,void,0,1,,error,0x660e592d; pushes error 0x61 (97 = Friend access violation) and calls error handler; noreturn +__vbaGenerateBoundsError,246,cdecl,void,0,1,,error,0x660edaf4; pushes error 9 (Subscript out of range) and calls error handler; noreturn +__vbaHresultCheck,256,stdcall,void,0,0,Hresult hr;IUnknownPtr pObj;GuidPtr piid;VoidPtr pVTable,error,0x660427c2; retn 0x10; checks TLS error state; converts specific HRESULTs (0x800A9C68->0x800A01B8); dispatches to error mapper +__vbaHresultCheckNonvirt,257,stdcall,void,0,0,Hresult hr;IUnknownPtr pObj;GuidPtr piid;VoidPtr pVTable,error,export exists; similar to HresultCheck but for non-virtual calls +__vbaHresultCheckObj,258,stdcall,void,0,0,int32 hr;VoidPtr pUnk;VoidPtr pGuid;VoidPtr pAdjust,error,checks HRESULT; raises error if failed +__vbaOnError,352,stdcall,void,0,0,int32 nHandler,error,On Error handler; 0xFFFFFFFD=Resume Next +__vbaResume,391,stdcall,void,0,0,int32 nResumeKind,error,Resume handler; -2=Resume -1=Resume Next >0=label +__vbaSetSystemError,394,cdecl,void,0,0,,error,calls GetLastError(); stores in TLS+0x9C +__vbaStopExe,395,stdcall,void,0,1,,error,Displays 'Stop statement encountered' message then raises error 0x9c68; never returns normally # # === FILE I/O === # -__vbaFileClose,229,stdcall,void,0,int16 nFileNum,file,if 0=no-op; otherwise delegates to internal close -__vbaFileCloseAll,233,stdcall,void,0,int16 fileNum,file,0x660f3c8e; retn 0x4; shares implementation with __vbaFileLock/FileClose path; fileNum=0 closes all -__vbaFileLock,234,stdcall,void,0,int32 nLockType;int32 nRecStart;int32 nRecEnd;int16 nFileNum,file,nLockType bits: 0x4=lock 0x2=unlock -__vbaFileOpen,235,stdcall,void,0,int32 nMode;int16 nRecLen;int16 nFileNum;VoidPtr szFilename,file,nMode bitfield: 0-7=access 8-11=share 12-15=open mode -__vbaFileSeek,236,stdcall,void,0,int32 recordNum;int16 fileNum,file,0x660f33b0; retn 0x8; validates file/record num; sets seek position in file channel structure -__vbaGet3,247,stdcall,void,0,int16 nFileNum;int32 nRecNum;VoidPtr pBuffer,file,thin wrapper for Get# statement -__vbaGet4,248,stdcall,void,0,int16 fileNum;int32 recordNum;VoidPtr pData;int32 dataLen,file,0x660f34fd; retn 0x10; file Get# with 4 params (includes length) -__vbaGetFxStr3,249,stdcall,void,0,int16 fileNum;int32 recordNum;BstrPtr pStr,file,0x66056863; retn 0xc; Get# for fixed-length string; 3 params -__vbaGetFxStr4,250,stdcall,void,0,int16 fileNum;int32 recordNum;BstrPtr pStr;int32 strLen,file,0x660f358d; retn 0x10; Get# for fixed-length string; 4 params -__vbaGetOwner3,251,stdcall,void,0,int16 fileNum;int32 recordNum;VoidPtr pOwner,file,0x660567f5; retn 0xc; Get# with owner object; 3 params -__vbaGetOwner4,252,stdcall,void,0,int16 fileNum;int32 recordNum;VoidPtr pOwner;int32 dataLen,file,0x660f3538; retn 0x10; Get# with owner object; 4 params -__vbaInputFile,281,cdecl,void,1,VoidPtr pTypeDescArray;int16 fileNum,file,0x660eace1; var-args; reads typed values from file per type descriptor array -__vbaPrintFile,355,cdecl,void,0,VoidPtr pVar;int16 nFileNum;int16 nPrintKind,file,validates file mode; calls internal print formatter -__vbaPrintObj,356,stdcall,void,0,int16 fileNum,file,export exists at ordinal 255; likely alias for __vbaPrintFile at 0x6600b6ca -__vbaPut3,357,stdcall,void,0,int16 fileNum;int32 recordNum;VoidPtr pData,file,0x66056727; retn 0xc; file Put# with 3 params -__vbaPut4,358,stdcall,void,0,int16 fileNum;int32 recordNum;VoidPtr pData;int32 dataLen,file,0x660f351b; retn 0x10; file Put# with 4 params -__vbaPutFxStr3,359,stdcall,void,0,int16 fileNum;int32 recordNum;BstrPtr pStr,file,0x660f3573; retn 0xc; Put# for fixed-length string; 3 params -__vbaPutFxStr4,360,stdcall,void,0,int16 fileNum;int32 recordNum;BstrPtr pStr;int32 strLen,file,0x660f35a9; retn 0x10; Put# for fixed-length string; 4 params -__vbaPutOwner3,361,stdcall,void,0,int16 nFileNum;int32 nRecNum;VoidPtr pBuffer,file,thin wrapper for Put# statement -__vbaPutOwner4,362,stdcall,void,0,int16 fileNum;int32 recordNum;VoidPtr pData;VoidPtr pOwner,file,0x660f3556; retn 0x10; Put# with owner object; 4 params -__vbaWriteFile,623,cdecl,void,0,int32 nFields;int16 fileNum;VoidPtr pFieldTypes;int32 nArgs,file,Write # statement: opens channel; writes fields with type-dispatched formatting; caller cleans stack -rtcChangeDir,530,stdcall,void,0,Bstr path,file,retn 0x4; ChDir; calls SetCurrentDirectoryA -rtcChangeDrive,533,stdcall,void,0,Bstr drive,file,retn 0x4; ChDrive; extracts drive letter A-Z; validates -rtcDir,645,stdcall,Bstr,0,VariantPtr pathSpec;int16 attributes,file,retn 0x8; Dir$(); FindFirstFile/FindNextFile -rtcFileAttributes,555,stdcall,int32,0,int16 fileNum;int16 mode,file,retn 0x8; FileAttr(); returns file mode (1=input; 2=output) -rtcFileCopy,576,stdcall,void,0,Bstr source;Bstr destination,file,retn 0x8; FileCopy; reads/writes in blocks -rtcFileDateTime,577,stdcall,void,0,VariantPtr result;Bstr pathName,file,retn 0x8; FileDateTime(); stat + FileTimeToSystemTime -> DATE variant -rtcFileLen,578,stdcall,int32,0,Bstr pathName,file,retn 0x4; FileLen(); stat file; returns nFileSizeLow -rtcFileLength,570,stdcall,int32,0,int16 fileNum,file,retn 0x4; LOF(); seek to end; get position; seek back -rtcFreeFile,648,stdcall,int16,0,VariantPtr pvar,file,retn 0x4; finds free file number; returns file number as int16 -rtcGetFileAttr,579,stdcall,int32,0,Bstr pathname,file,retn 0x4; calls internal attr helper; masks out 0x80 (normal) -rtcInputCharCount,620,stdcall,Bstr,0,int32 charCount;int32 fileNum,file,retn 0x8; reads charCount characters from file; returns BSTR (wide chars) -rtcInputCharCountVar,621,stdcall,void,0,VariantPtr pResult;int32 charCount;int32 fileNum,file,retn 0xc; wraps rtcInputCharCount; returns VT_BSTR VARIANT -rtcInputCount,566,stdcall,Bstr,0,int32 byteCount;int32 fileNum,file,retn 0x8; reads byteCount bytes from file; returns byte-string BSTR -rtcInputCountVar,567,stdcall,void,0,VariantPtr pResult;int32 byteCount;int32 fileNum,file,retn 0xc; wraps rtcInputCount; returns VT_BSTR VARIANT -rtcKillFiles,529,stdcall,void,0,VariantPtr filespec,file,retn 0x4; deletes files matching filespec pattern -rtcMakeDir,531,stdcall,void,0,Bstr bstrPath,file,no frame ptr; thin wrapper pushing path+const to directory creation helper; retn 0x4 -rtcRemoveDir,532,stdcall,void,0,Bstr bstrPath,file,no frame ptr; thin wrapper; delegates to directory removal helper; retn 0x4 -rtcSetFileAttr,580,stdcall,void,0,Bstr bstrPath;int16 attr,file,push ebp; calls SetFileAttributesA; retn 0x8 +__vbaFileClose,229,stdcall,void,0,0,int16 nFileNum,file,if 0=no-op; otherwise delegates to internal close +__vbaFileCloseAll,233,stdcall,void,0,0,int16 fileNum,file,0x660f3c8e; retn 0x4; shares implementation with __vbaFileLock/FileClose path; fileNum=0 closes all +__vbaFileLock,234,stdcall,void,0,0,int32 nLockType;int32 nRecStart;int32 nRecEnd;int16 nFileNum,file,nLockType bits: 0x4=lock 0x2=unlock +__vbaFileOpen,235,stdcall,void,0,0,int32 nMode;int16 nRecLen;int16 nFileNum;VoidPtr szFilename,file,nMode bitfield: 0-7=access 8-11=share 12-15=open mode +__vbaFileSeek,236,stdcall,void,0,0,int32 recordNum;int16 fileNum,file,0x660f33b0; retn 0x8; validates file/record num; sets seek position in file channel structure +__vbaGet3,247,stdcall,void,0,0,int16 nFileNum;int32 nRecNum;VoidPtr pBuffer,file,thin wrapper for Get# statement +__vbaGet4,248,stdcall,void,0,0,int16 fileNum;int32 recordNum;VoidPtr pData;int32 dataLen,file,0x660f34fd; retn 0x10; file Get# with 4 params (includes length) +__vbaGetFxStr3,249,stdcall,void,0,0,int16 fileNum;int32 recordNum;BstrPtr pStr,file,0x66056863; retn 0xc; Get# for fixed-length string; 3 params +__vbaGetFxStr4,250,stdcall,void,0,0,int16 fileNum;int32 recordNum;BstrPtr pStr;int32 strLen,file,0x660f358d; retn 0x10; Get# for fixed-length string; 4 params +__vbaGetOwner3,251,stdcall,void,0,0,int16 fileNum;int32 recordNum;VoidPtr pOwner,file,0x660567f5; retn 0xc; Get# with owner object; 3 params +__vbaGetOwner4,252,stdcall,void,0,0,int16 fileNum;int32 recordNum;VoidPtr pOwner;int32 dataLen,file,0x660f3538; retn 0x10; Get# with owner object; 4 params +__vbaInputFile,281,cdecl,void,1,0,VoidPtr pTypeDescArray;int16 fileNum,file,0x660eace1; var-args; reads typed values from file per type descriptor array +__vbaPrintFile,355,cdecl,void,0,0,VoidPtr pVar;int16 nFileNum;int16 nPrintKind,file,validates file mode; calls internal print formatter +__vbaPrintObj,356,stdcall,void,0,0,int16 fileNum,file,export exists at ordinal 255; likely alias for __vbaPrintFile at 0x6600b6ca +__vbaPut3,357,stdcall,void,0,0,int16 fileNum;int32 recordNum;VoidPtr pData,file,0x66056727; retn 0xc; file Put# with 3 params +__vbaPut4,358,stdcall,void,0,0,int16 fileNum;int32 recordNum;VoidPtr pData;int32 dataLen,file,0x660f351b; retn 0x10; file Put# with 4 params +__vbaPutFxStr3,359,stdcall,void,0,0,int16 fileNum;int32 recordNum;BstrPtr pStr,file,0x660f3573; retn 0xc; Put# for fixed-length string; 3 params +__vbaPutFxStr4,360,stdcall,void,0,0,int16 fileNum;int32 recordNum;BstrPtr pStr;int32 strLen,file,0x660f35a9; retn 0x10; Put# for fixed-length string; 4 params +__vbaPutOwner3,361,stdcall,void,0,0,int16 nFileNum;int32 nRecNum;VoidPtr pBuffer,file,thin wrapper for Put# statement +__vbaPutOwner4,362,stdcall,void,0,0,int16 fileNum;int32 recordNum;VoidPtr pData;VoidPtr pOwner,file,0x660f3556; retn 0x10; Put# with owner object; 4 params +__vbaWriteFile,623,cdecl,void,0,0,int32 nFields;int16 fileNum;VoidPtr pFieldTypes;int32 nArgs,file,Write # statement: opens channel; writes fields with type-dispatched formatting; caller cleans stack +rtcChangeDir,530,stdcall,void,0,0,Bstr path,file,retn 0x4; ChDir; calls SetCurrentDirectoryA +rtcChangeDrive,533,stdcall,void,0,0,Bstr drive,file,retn 0x4; ChDrive; extracts drive letter A-Z; validates +rtcDir,645,stdcall,Bstr,0,0,VariantPtr pathSpec;int16 attributes,file,retn 0x8; Dir$(); FindFirstFile/FindNextFile +rtcFileAttributes,555,stdcall,int32,0,0,int16 fileNum;int16 mode,file,retn 0x8; FileAttr(); returns file mode (1=input; 2=output) +rtcFileCopy,576,stdcall,void,0,0,Bstr source;Bstr destination,file,retn 0x8; FileCopy; reads/writes in blocks +rtcFileDateTime,577,stdcall,void,0,0,VariantPtr result;Bstr pathName,file,retn 0x8; FileDateTime(); stat + FileTimeToSystemTime -> DATE variant +rtcFileLen,578,stdcall,int32,0,0,Bstr pathName,file,retn 0x4; FileLen(); stat file; returns nFileSizeLow +rtcFileLength,570,stdcall,int32,0,0,int16 fileNum,file,retn 0x4; LOF(); seek to end; get position; seek back +rtcFreeFile,648,stdcall,int16,0,0,VariantPtr pvar,file,retn 0x4; finds free file number; returns file number as int16 +rtcGetFileAttr,579,stdcall,int32,0,0,Bstr pathname,file,retn 0x4; calls internal attr helper; masks out 0x80 (normal) +rtcInputCharCount,620,stdcall,Bstr,0,0,int32 charCount;int32 fileNum,file,retn 0x8; reads charCount characters from file; returns BSTR (wide chars) +rtcInputCharCountVar,621,stdcall,void,0,0,VariantPtr pResult;int32 charCount;int32 fileNum,file,retn 0xc; wraps rtcInputCharCount; returns VT_BSTR VARIANT +rtcInputCount,566,stdcall,Bstr,0,0,int32 byteCount;int32 fileNum,file,retn 0x8; reads byteCount bytes from file; returns byte-string BSTR +rtcInputCountVar,567,stdcall,void,0,0,VariantPtr pResult;int32 byteCount;int32 fileNum,file,retn 0xc; wraps rtcInputCount; returns VT_BSTR VARIANT +rtcKillFiles,529,stdcall,void,0,0,VariantPtr filespec,file,retn 0x4; deletes files matching filespec pattern +rtcMakeDir,531,stdcall,void,0,0,Bstr bstrPath,file,no frame ptr; thin wrapper pushing path+const to directory creation helper; retn 0x4 +rtcRemoveDir,532,stdcall,void,0,0,Bstr bstrPath,file,no frame ptr; thin wrapper; delegates to directory removal helper; retn 0x4 +rtcSetFileAttr,580,stdcall,void,0,0,Bstr bstrPath;int16 attr,file,push ebp; calls SetFileAttributesA; retn 0x8 # # === EVENT SINK (COM) === # -__vbaRaiseEvent,379,stdcall,void,0,VoidPtr pEventObj;int32 eventOffset;VoidPtr pArgStack;int32 argCount,evtsink,0x660e90b6; retn 0x10; walks event sink chain; invokes IDispatch::Invoke on each connected sink -EVENT_SINK2_AddRef,440,stdcall,uint32,0,VoidPtr pThis,evtsink,Resolves sink base via 0x6601756d; delegates to base vtable[1] (AddRef); retn 0x4 -EVENT_SINK2_Release,441,stdcall,uint32,0,VoidPtr pThis,evtsink,Resolves sink base via 0x6601756d; delegates to base vtable[2] (Release); retn 0x4 -EVENT_SINK_AddRef,401,stdcall,uint32,0,VoidPtr pthis,evtsink,COM IUnknown::AddRef; retn 4 -EVENT_SINK_Invoke,404,stdcall,Hresult,0,VoidPtr pthis;int32 dispIdMember;VoidPtr riid;uint32 lcid;uint16 wFlags;VoidPtr pDispParams;VariantPtr pVarResult;VoidPtr pExcepInfo;Int32Ptr puArgErr,evtsink,COM IDispatch::Invoke; retn 0x24 -EVENT_SINK_QueryInterface,400,stdcall,Hresult,0,VoidPtr pthis;VoidPtr riid;VoidPtr ppvObject,evtsink,COM IUnknown::QueryInterface; retn 0xC -EVENT_SINK_Release,402,stdcall,uint32,0,VoidPtr pthis,evtsink,COM IUnknown::Release; retn 4 +__vbaRaiseEvent,379,stdcall,void,0,0,VoidPtr pEventObj;int32 eventOffset;VoidPtr pArgStack;int32 argCount,evtsink,0x660e90b6; retn 0x10; walks event sink chain; invokes IDispatch::Invoke on each connected sink +EVENT_SINK2_AddRef,440,stdcall,uint32,0,0,VoidPtr pThis,evtsink,Resolves sink base via 0x6601756d; delegates to base vtable[1] (AddRef); retn 0x4 +EVENT_SINK2_Release,441,stdcall,uint32,0,0,VoidPtr pThis,evtsink,Resolves sink base via 0x6601756d; delegates to base vtable[2] (Release); retn 0x4 +EVENT_SINK_AddRef,401,stdcall,uint32,0,0,VoidPtr pthis,evtsink,COM IUnknown::AddRef; retn 4 +EVENT_SINK_Invoke,404,stdcall,Hresult,0,0,VoidPtr pthis;int32 dispIdMember;VoidPtr riid;uint32 lcid;uint16 wFlags;VoidPtr pDispParams;VariantPtr pVarResult;VoidPtr pExcepInfo;Int32Ptr puArgErr,evtsink,COM IDispatch::Invoke; retn 0x24 +EVENT_SINK_QueryInterface,400,stdcall,Hresult,0,0,VoidPtr pthis;VoidPtr riid;VoidPtr ppvObject,evtsink,COM IUnknown::QueryInterface; retn 0xC +EVENT_SINK_Release,402,stdcall,uint32,0,0,VoidPtr pthis,evtsink,COM IUnknown::Release; retn 4 # # === FDIV BUG WORKAROUNDS === # -_adj_fdiv_m16i,659,special,void,0,,fdiv,Pentium FDIV bug workaround -_adj_fdiv_m32,688,special,void,0,,fdiv,Pentium FDIV bug workaround -_adj_fdiv_m32i,701,special,void,0,,fdiv,Pentium FDIV bug workaround -_adj_fdiv_m64,718,special,void,0,,fdiv,Pentium FDIV bug workaround -_adj_fdiv_r,719,special,void,0,,fdiv,Pentium FDIV bug workaround -_adj_fdivr_m16i,720,special,void,0,,fdiv,Pentium FDIV bug workaround -_adj_fdivr_m32,721,special,void,0,,fdiv,Pentium FDIV bug workaround -_adj_fdivr_m32i,722,special,void,0,,fdiv,Pentium FDIV bug workaround -_adj_fdivr_m64,723,special,void,0,,fdiv,Pentium FDIV bug workaround -_adj_fpatan,724,special,void,0,,fdiv,Pentium FDIV bug workaround -_adj_fprem,725,special,void,0,,fdiv,Pentium FDIV bug workaround -_adj_fprem1,726,special,void,0,,fdiv,Pentium FDIV bug workaround -_adj_fptan,727,special,void,0,,fdiv,Pentium FDIV bug workaround +_adj_fdiv_m16i,659,special,void,0,0,,fdiv,Pentium FDIV bug workaround +_adj_fdiv_m32,688,special,void,0,0,,fdiv,Pentium FDIV bug workaround +_adj_fdiv_m32i,701,special,void,0,0,,fdiv,Pentium FDIV bug workaround +_adj_fdiv_m64,718,special,void,0,0,,fdiv,Pentium FDIV bug workaround +_adj_fdiv_r,719,special,void,0,0,,fdiv,Pentium FDIV bug workaround +_adj_fdivr_m16i,720,special,void,0,0,,fdiv,Pentium FDIV bug workaround +_adj_fdivr_m32,721,special,void,0,0,,fdiv,Pentium FDIV bug workaround +_adj_fdivr_m32i,722,special,void,0,0,,fdiv,Pentium FDIV bug workaround +_adj_fdivr_m64,723,special,void,0,0,,fdiv,Pentium FDIV bug workaround +_adj_fpatan,724,special,void,0,0,,fdiv,Pentium FDIV bug workaround +_adj_fprem,725,special,void,0,0,,fdiv,Pentium FDIV bug workaround +_adj_fprem1,726,special,void,0,0,,fdiv,Pentium FDIV bug workaround +_adj_fptan,727,special,void,0,0,,fdiv,Pentium FDIV bug workaround # # === CRT MATH INTRINSICS === # -_allmul,728,stdcall,int64,0,int32 lo1;int32 hi1;int32 lo2;int32 hi2,crt,CRT 64-bit multiply; result in edx:eax; retn 0x10 -_CIatan,156,cdecl,void,0,,crt,FPU intrinsic: pops st(0) to stack; calls atan helper; pushes result back to FPU -_CIcos,157,cdecl,void,0,,crt,FPU intrinsic: pops st(0) to stack; calls cos helper; pushes result back to FPU -_CIexp,158,special,void,0,,crt,x87: ST(0) = exp(ST(0)) -_CIlog,159,cdecl,void,0,,crt,FPU intrinsic: pops st(0) to stack; calls log helper; pushes result back to FPU -_CIsin,160,cdecl,void,0,,crt,FPU intrinsic: pops st(0) to stack; calls sin helper; pushes result back to FPU -_CIsqrt,161,cdecl,void,0,,crt,FPU intrinsic: pops st(0) to stack; calls sqrt helper; pushes result back to FPU -_CItan,162,special,void,0,,crt,x87: ST(0) = tan(ST(0)) +_allmul,728,stdcall,int64,0,0,int32 lo1;int32 hi1;int32 lo2;int32 hi2,crt,CRT 64-bit multiply; result in edx:eax; retn 0x10 +_CIatan,156,cdecl,void,0,0,,crt,FPU intrinsic: pops st(0) to stack; calls atan helper; pushes result back to FPU +_CIcos,157,cdecl,void,0,0,,crt,FPU intrinsic: pops st(0) to stack; calls cos helper; pushes result back to FPU +_CIexp,158,special,void,0,0,,crt,x87: ST(0) = exp(ST(0)) +_CIlog,159,cdecl,void,0,0,,crt,FPU intrinsic: pops st(0) to stack; calls log helper; pushes result back to FPU +_CIsin,160,cdecl,void,0,0,,crt,FPU intrinsic: pops st(0) to stack; calls sin helper; pushes result back to FPU +_CIsqrt,161,cdecl,void,0,0,,crt,FPU intrinsic: pops st(0) to stack; calls sqrt helper; pushes result back to FPU +_CItan,162,special,void,0,0,,crt,x87: ST(0) = tan(ST(0)) # # === FPU CONVERSION INTRINSICS === # -__vbaFpCDblR4,118,cdecl,void,0,,fpu,0x661120d2; validates FPU st0 fits in double precision (store/reload via qword) -__vbaFpCDblR8,119,cdecl,void,0,,fpu,export at ordinal 20; round-trip through qword to validate -__vbaFpCmpCy,122,stdcall,int32,0,int32 cyLo;int32 cyHi,fpu,0x661120e9; retn 0x8; compares FPU st0 against CY value on stack; returns -1/0/1 -__vbaFpCSngR4,120,cdecl,void,0,,fpu,0x661120bb; validates FPU st0 fits in single precision (store/reload via dword) -__vbaFpCSngR8,121,cdecl,void,0,,fpu,export at ordinal 22; likely same validation path as FpCDblR4 -__vbaFpCy,123,cdecl,int64,0,,fpu,0x6611206a; converts FPU st0 to scaled CY (multiply by 10000); returns EDX:EAX -__vbaFPException,224,cdecl,void,0,,fpu,export at ordinal 124; 0x660f5daa is FPInt; FPException likely shares overflow check path at 0x66112026 -__vbaFPFix,225,cdecl,void,0,,fpu,export at ordinal 125; FPU st0 truncation toward zero (uses fldcw to set rounding mode) -__vbaFpI2,124,cdecl,int16,0,,fpu,pops ST(0)→int16; frndint+fist; overflow check via fnstsw -__vbaFpI4,125,cdecl,int32,0,,fpu,pops ST(0)→int32; frndint+fist; overflow check via fnstsw -__vbaFPInt,226,cdecl,void,0,,fpu,0x660f5daa; operates on FPU st0; fwait; set truncation rounding mode; frndint; restore rounding mode -__vbaFpR4,126,cdecl,float,0,,fpu,ST(0)→float; fld1+fmulp with single-precision FPCW -__vbaFpR8,127,cdecl,double,0,,fpu,ST(0)→double; fld1+fmulp with double-precision FPCW -__vbaFpUI1,128,cdecl,uint8,0,,fpu,0x66112045; rounds FPU st0 to integer; validates 0..255 range; returns in EAX +__vbaFpCDblR4,118,cdecl,void,0,0,,fpu,0x661120d2; validates FPU st0 fits in double precision (store/reload via qword) +__vbaFpCDblR8,119,cdecl,void,0,0,,fpu,export at ordinal 20; round-trip through qword to validate +__vbaFpCmpCy,122,stdcall,int32,0,0,int32 cyLo;int32 cyHi,fpu,0x661120e9; retn 0x8; compares FPU st0 against CY value on stack; returns -1/0/1 +__vbaFpCSngR4,120,cdecl,void,0,0,,fpu,0x661120bb; validates FPU st0 fits in single precision (store/reload via dword) +__vbaFpCSngR8,121,cdecl,void,0,0,,fpu,export at ordinal 22; likely same validation path as FpCDblR4 +__vbaFpCy,123,cdecl,int64,0,0,,fpu,0x6611206a; converts FPU st0 to scaled CY (multiply by 10000); returns EDX:EAX +__vbaFPException,224,cdecl,void,0,1,,fpu,export at ordinal 124; 0x660f5daa is FPInt; FPException likely shares overflow check path at 0x66112026 +__vbaFPFix,225,cdecl,void,0,0,,fpu,export at ordinal 125; FPU st0 truncation toward zero (uses fldcw to set rounding mode) +__vbaFpI2,124,cdecl,int16,0,0,,fpu,pops ST(0)→int16; frndint+fist; overflow check via fnstsw +__vbaFpI4,125,cdecl,int32,0,0,,fpu,pops ST(0)→int32; frndint+fist; overflow check via fnstsw +__vbaFPInt,226,cdecl,void,0,0,,fpu,0x660f5daa; operates on FPU st0; fwait; set truncation rounding mode; frndint; restore rounding mode +__vbaFpR4,126,cdecl,float,0,0,,fpu,ST(0)→float; fld1+fmulp with single-precision FPCW +__vbaFpR8,127,cdecl,double,0,0,,fpu,ST(0)→double; fld1+fmulp with double-precision FPCW +__vbaFpUI1,128,cdecl,uint8,0,0,,fpu,0x66112045; rounds FPU st0 to integer; validates 0..255 range; returns in EAX # # === RUNTIME ENTRY / DLL EXPORTS === # -DllCanUnloadNow,154,cdecl,int32,0,,entry,Checks global refcount at 0x66119c78; returns S_FALSE(1) if nonzero; S_OK(0) if zero; bare retn -DllFunctionCall,187,stdcall,VoidPtr,0,VoidPtr pDllCallInfo,entry,resolves Declare imports; returns function pointer -DllGetClassObject,155,stdcall,int32,0,VoidPtr rclsid;VoidPtr riid;VoidPtr ppv,entry,Compares rclsid to internal CLSID; creates class factory; retn 0xc -DLLGetDocumentation,153,stdcall,int32,0,VoidPtr hInstance;uint32 lcid;uint32 dwResId;uint32 dwStringType;VoidPtr ppBstr,entry,Loads string resource; converts to BSTR; retn 0x14 -DllRegisterServer,106,cdecl,int32,0,,entry,Gets module filename; loads type library; registers; bare retn -DllUnregisterServer,107,cdecl,int32,0,,entry,Unregisters type library; bare retn -MethCallEngine,199,special,void,0,VoidPtr param,entry,Custom CC: sub [esp+4];eax adjusts 'this' offset; tail-jumps to ProcCallEngine_Body -ProcCallEngine,186,special,void,0,,entry,Custom CC: edx=RTMI ptr; return addr on stack; sets up P-Code frame; dispatches opcodes via jump table at 0x6610aa24 -ThunRTMain,100,stdcall,void,0,VoidPtr pVbHeader,entry,runtime initialization; noreturn -UserDllMain,105,stdcall,int32,0,VoidPtr ppProject;VoidPtr pProjectData;VoidPtr hInstance;uint32 fdwReason;VoidPtr lpvReserved,entry,VB6 user DLL entry; handles DLL_PROCESS_ATTACH(1); DLL_PROCESS_DETACH(0); DLL_THREAD_ATTACH(2); retn 0x14 -VBDllCanUnloadNow,102,stdcall,Hresult,0,VoidPtr stubArg,entry,0x6605CC5B (6.00.8176); retn 4; the one value its DllCanUnloadNow stub (58 68 imm32 50 E9) pushes; returns S_OK (0) or S_FALSE (1) -VBDllGetClassObject,104,stdcall,Hresult,0,VoidPtr phModule;VoidPtr stubArg2;VoidPtr pVBHeader;GuidPtr rclsid;GuidPtr riid;VoidPtr ppv,entry,0x6604D2E7 (6.00.8176); retn 0x18; the DllGetClassObject stub (58 68 imm32 68 imm32 68 imm32 50 E9) pushes the first three under the caller's rclsid/riid/ppv; reads *phModule and the VBHeader; compares riid with IID_IClassFactory -VBDllRegisterServer,103,stdcall,Hresult,0,VoidPtr phModule;VoidPtr stubArg2;VoidPtr pVBHeader,entry,0x660CAAE0 (6.00.8176); retn 0xC; the three values its DllRegisterServer stub pushes; GetModuleFileNameA(*phModule) and VBHeader +0x3C and +0x22; E_UNEXPECTED (0x8000FFFF) or E_FAIL (0x80004005) on failure -VBDllUnRegisterServer,101,stdcall,Hresult,0,VoidPtr phModule;VoidPtr stubArg2;VoidPtr pVBHeader,entry,0x6605CF04 (6.00.8176); retn 0xC on every path; the three values its DllUnregisterServer stub pushes; reads *phModule and VBHeader +0x22 and +0x54; E_UNEXPECTED (0x8000FFFF) on failure +DllCanUnloadNow,154,cdecl,int32,0,0,,entry,Checks global refcount at 0x66119c78; returns S_FALSE(1) if nonzero; S_OK(0) if zero; bare retn +DllFunctionCall,187,stdcall,VoidPtr,0,0,VoidPtr pDllCallInfo,entry,resolves Declare imports; returns function pointer +DllGetClassObject,155,stdcall,int32,0,0,VoidPtr rclsid;VoidPtr riid;VoidPtr ppv,entry,Compares rclsid to internal CLSID; creates class factory; retn 0xc +DLLGetDocumentation,153,stdcall,int32,0,0,VoidPtr hInstance;uint32 lcid;uint32 dwResId;uint32 dwStringType;VoidPtr ppBstr,entry,Loads string resource; converts to BSTR; retn 0x14 +DllRegisterServer,106,cdecl,int32,0,0,,entry,Gets module filename; loads type library; registers; bare retn +DllUnregisterServer,107,cdecl,int32,0,0,,entry,Unregisters type library; bare retn +MethCallEngine,199,special,void,0,0,VoidPtr param,entry,Custom CC: sub [esp+4];eax adjusts 'this' offset; tail-jumps to ProcCallEngine_Body +ProcCallEngine,186,special,void,0,0,,entry,Custom CC: edx=RTMI ptr; return addr on stack; sets up P-Code frame; dispatches opcodes via jump table at 0x6610aa24 +ThunRTMain,100,stdcall,void,0,1,VoidPtr pVbHeader,entry,runtime initialization; noreturn +UserDllMain,105,stdcall,int32,0,0,VoidPtr ppProject;VoidPtr pProjectData;VoidPtr hInstance;uint32 fdwReason;VoidPtr lpvReserved,entry,VB6 user DLL entry; handles DLL_PROCESS_ATTACH(1); DLL_PROCESS_DETACH(0); DLL_THREAD_ATTACH(2); retn 0x14 +VBDllCanUnloadNow,102,stdcall,Hresult,0,0,VoidPtr stubArg,entry,0x6605CC5B (6.00.8176); retn 4; the one value its DllCanUnloadNow stub (58 68 imm32 50 E9) pushes; returns S_OK (0) or S_FALSE (1) +VBDllGetClassObject,104,stdcall,Hresult,0,0,VoidPtr phModule;VoidPtr stubArg2;VoidPtr pVBHeader;GuidPtr rclsid;GuidPtr riid;VoidPtr ppv,entry,0x6604D2E7 (6.00.8176); retn 0x18; the DllGetClassObject stub (58 68 imm32 68 imm32 68 imm32 50 E9) pushes the first three under the caller's rclsid/riid/ppv; reads *phModule and the VBHeader; compares riid with IID_IClassFactory +VBDllRegisterServer,103,stdcall,Hresult,0,0,VoidPtr phModule;VoidPtr stubArg2;VoidPtr pVBHeader,entry,0x660CAAE0 (6.00.8176); retn 0xC; the three values its DllRegisterServer stub pushes; GetModuleFileNameA(*phModule) and VBHeader +0x3C and +0x22; E_UNEXPECTED (0x8000FFFF) or E_FAIL (0x80004005) on failure +VBDllUnRegisterServer,101,stdcall,Hresult,0,0,VoidPtr phModule;VoidPtr stubArg2;VoidPtr pVBHeader,entry,0x6605CF04 (6.00.8176); retn 0xC on every path; the three values its DllUnregisterServer stub pushes; reads *phModule and VBHeader +0x22 and +0x54; E_UNEXPECTED (0x8000FFFF) on failure # # === MEMORY ACCESS (GetMem/PutMem/SetMem) === # -GetMem1,317,stdcall,int32,0,VoidPtr src;VoidPtr dst,mem,Reads 1 byte from *src into *dst; returns 0 (S_OK); retn 0x8 -GetMem2,300,stdcall,int32,0,VoidPtr src;Int16Ptr dst,mem,Reads 2 bytes (word) from *src into *dst; returns 0; retn 0x8 -GetMem4,301,stdcall,int32,0,VoidPtr src;Int32Ptr dst,mem,Reads 4 bytes (dword) from *src into *dst; returns 0; retn 0x8 -GetMem8,302,stdcall,int32,0,VoidPtr src;Int64Ptr dst,mem,Reads 8 bytes (qword) from *src into *dst; returns 0; retn 0x8 -GetMemEvent,319,stdcall,int32,0,VoidPtr pClassModule;VoidPtr pEventSink;VoidPtr src;VoidPtr dst,mem,Copies IUnknown* from event source with AddRef; 4 params; retn 0x10 -GetMemNewObj,314,stdcall,int32,0,VoidPtr pProject;VoidPtr pObjectInfo;VoidPtr pObjSlot;VoidPtr ppObj,mem,Auto-creates object if slot empty; then copies with AddRef; ebp-based; retn 0x10 -GetMemObj,305,stdcall,int32,0,VoidPtr src;VoidPtr dst,mem,Copies IUnknown* and calls AddRef; returns 0; retn 0x8 -GetMemStr,303,stdcall,int32,0,VoidPtr src;VoidPtr dst,mem,Copies BSTR via SysAllocStringByteLen; returns 0 or 0x8007000e (E_OUTOFMEMORY); retn 0x8 -GetMemVar,304,stdcall,int32,0,VoidPtr src;VoidPtr dst,mem,Copies VARIANT via VariantCopy; returns 0; retn 0x8 -PutMem1,318,stdcall,int32,0,VoidPtr dst;int32 val,mem,Writes low byte of val to *dst; returns 0; retn 0x8 -PutMem2,306,stdcall,int32,0,VoidPtr dst;int32 val,mem,Writes low word of val to *dst; returns 0; retn 0x8 -PutMem4,307,stdcall,int32,0,VoidPtr dst;int32 val,mem,Writes dword val to *dst; returns 0; retn 0x8 -PutMem8,308,stdcall,int32,0,VoidPtr dst;int32 lo;int32 hi,mem,Writes qword lo:hi to *dst; returns 0; retn 0xc -PutMemEvent,320,stdcall,int32,0,VoidPtr pClassModule;VoidPtr pEventSink;VoidPtr dst;VoidPtr pObj,mem,Tail-calls PutMemObj with inner 2 params; retn 0x10 -PutMemNewObj,315,stdcall,int32,0,VoidPtr pProject;VoidPtr pObjectInfo;VoidPtr pObjSlot;VoidPtr pObj,mem,Auto-creates object if slot empty; then calls PutMemObj; ebp-based; retn 0x10 -PutMemObj,311,stdcall,int32,0,VoidPtr dst;VoidPtr pVariant,mem,Complex: extracts IDispatch from Variant; calls Invoke to assign; ebp-based; retn 0x8 -PutMemStr,309,stdcall,int32,0,VoidPtr dst;VoidPtr bstrSrc,mem,Allocates new BSTR copy; frees old; stores new; retn 0x8 -PutMemVar,310,stdcall,int32,0,VoidPtr dst;VoidPtr pVariant,mem,Copies VARIANT via VariantCopyInd; handles VT_DISPATCH extraction; ebp-based; retn 0x8 -SetMemEvent,321,stdcall,int32,0,VoidPtr pThis;uint16 eventIndex;VoidPtr pObj1;VoidPtr pObj2,mem,Complex event Set: resolves object; calls helper; ebp-based; retn 0x10 -SetMemNewObj,316,stdcall,int32,0,VoidPtr pProject;VoidPtr pObjectInfo;VoidPtr pObjSlot;VoidPtr pObj,mem,Tail-calls SetMemObj with inner 2 params; retn 0x10 -SetMemObj,313,stdcall,int32,0,VoidPtr dst;VoidPtr pObj,mem,AddRef new obj; swap into *dst; Release old; retn 0x8 -SetMemVar,312,stdcall,int32,0,VoidPtr dst;VoidPtr pVariant,mem,Validates VT_DISPATCH/VT_UNKNOWN; swaps object references; ebp-based; retn 0x8 +GetMem1,317,stdcall,int32,0,0,VoidPtr src;VoidPtr dst,mem,Reads 1 byte from *src into *dst; returns 0 (S_OK); retn 0x8 +GetMem2,300,stdcall,int32,0,0,VoidPtr src;Int16Ptr dst,mem,Reads 2 bytes (word) from *src into *dst; returns 0; retn 0x8 +GetMem4,301,stdcall,int32,0,0,VoidPtr src;Int32Ptr dst,mem,Reads 4 bytes (dword) from *src into *dst; returns 0; retn 0x8 +GetMem8,302,stdcall,int32,0,0,VoidPtr src;Int64Ptr dst,mem,Reads 8 bytes (qword) from *src into *dst; returns 0; retn 0x8 +GetMemEvent,319,stdcall,int32,0,0,VoidPtr pClassModule;VoidPtr pEventSink;VoidPtr src;VoidPtr dst,mem,Copies IUnknown* from event source with AddRef; 4 params; retn 0x10 +GetMemNewObj,314,stdcall,int32,0,0,VoidPtr pProject;VoidPtr pObjectInfo;VoidPtr pObjSlot;VoidPtr ppObj,mem,Auto-creates object if slot empty; then copies with AddRef; ebp-based; retn 0x10 +GetMemObj,305,stdcall,int32,0,0,VoidPtr src;VoidPtr dst,mem,Copies IUnknown* and calls AddRef; returns 0; retn 0x8 +GetMemStr,303,stdcall,int32,0,0,VoidPtr src;VoidPtr dst,mem,Copies BSTR via SysAllocStringByteLen; returns 0 or 0x8007000e (E_OUTOFMEMORY); retn 0x8 +GetMemVar,304,stdcall,int32,0,0,VoidPtr src;VoidPtr dst,mem,Copies VARIANT via VariantCopy; returns 0; retn 0x8 +PutMem1,318,stdcall,int32,0,0,VoidPtr dst;int32 val,mem,Writes low byte of val to *dst; returns 0; retn 0x8 +PutMem2,306,stdcall,int32,0,0,VoidPtr dst;int32 val,mem,Writes low word of val to *dst; returns 0; retn 0x8 +PutMem4,307,stdcall,int32,0,0,VoidPtr dst;int32 val,mem,Writes dword val to *dst; returns 0; retn 0x8 +PutMem8,308,stdcall,int32,0,0,VoidPtr dst;int32 lo;int32 hi,mem,Writes qword lo:hi to *dst; returns 0; retn 0xc +PutMemEvent,320,stdcall,int32,0,0,VoidPtr pClassModule;VoidPtr pEventSink;VoidPtr dst;VoidPtr pObj,mem,Tail-calls PutMemObj with inner 2 params; retn 0x10 +PutMemNewObj,315,stdcall,int32,0,0,VoidPtr pProject;VoidPtr pObjectInfo;VoidPtr pObjSlot;VoidPtr pObj,mem,Auto-creates object if slot empty; then calls PutMemObj; ebp-based; retn 0x10 +PutMemObj,311,stdcall,int32,0,0,VoidPtr dst;VoidPtr pVariant,mem,Complex: extracts IDispatch from Variant; calls Invoke to assign; ebp-based; retn 0x8 +PutMemStr,309,stdcall,int32,0,0,VoidPtr dst;VoidPtr bstrSrc,mem,Allocates new BSTR copy; frees old; stores new; retn 0x8 +PutMemVar,310,stdcall,int32,0,0,VoidPtr dst;VoidPtr pVariant,mem,Copies VARIANT via VariantCopyInd; handles VT_DISPATCH extraction; ebp-based; retn 0x8 +SetMemEvent,321,stdcall,int32,0,0,VoidPtr pThis;uint16 eventIndex;VoidPtr pObj1;VoidPtr pObj2,mem,Complex event Set: resolves object; calls helper; ebp-based; retn 0x10 +SetMemNewObj,316,stdcall,int32,0,0,VoidPtr pProject;VoidPtr pObjectInfo;VoidPtr pObjSlot;VoidPtr pObj,mem,Tail-calls SetMemObj with inner 2 params; retn 0x10 +SetMemObj,313,stdcall,int32,0,0,VoidPtr dst;VoidPtr pObj,mem,AddRef new obj; swap into *dst; Release old; retn 0x8 +SetMemVar,312,stdcall,int32,0,0,VoidPtr dst;VoidPtr pVariant,mem,Validates VT_DISPATCH/VT_UNKNOWN; swaps object references; ebp-based; retn 0x8 # # === INTERNAL / COM / ENGINE APIs === # -__vbaAptOffset,163,stdcall,int32,0,VoidPtr pObj,internal,Returns apartment thread offset; returns 0 if single-threaded; reads TLS for multi-apartment -BASIC_CLASS_AddRef,411,stdcall,uint32,0,VoidPtr pThis,internal,Delegates to inner object at pThis+0x8 via vtable[1] (AddRef) -BASIC_CLASS_GetIDsOfNames,413,stdcall,int32,0,VoidPtr pThis;VoidPtr riid;VoidPtr rgszNames;uint32 cNames;uint32 lcid;Int32Ptr rgDispId,internal,Compares riid to IID_NULL; resolves via EventSink_GetVtableBase; retn 0x18 -BASIC_CLASS_Invoke,414,stdcall,int32,0,VoidPtr pThis;int32 dispId;VoidPtr riid;uint32 lcid;uint16 wFlags;VoidPtr pDispParams;VoidPtr pVarResult;VoidPtr pExcepInfo;Int32Ptr puArgErr,internal,Full IDispatch::Invoke; validates riid=IID_NULL; calls IDispatchInvoke; fallback chains through pThis+0x18/+0x10; retn 0x24 -BASIC_CLASS_QueryInterface,410,stdcall,int32,0,VoidPtr pThis;VoidPtr riid;VoidPtr ppvObj,internal,Delegates to inner object at pThis+0x8 via vtable[0] (QI); retn 0xc -BASIC_CLASS_Release,412,stdcall,uint32,0,VoidPtr pThis,internal,Delegates to inner object at pThis+0x8 via vtable[2] (Release) -BASIC_DISPINTERFACE_GetTICount,420,stdcall,int32,0,VoidPtr pThis;Int32Ptr pctinfo,internal,Trivially stores 1 into *pctinfo and returns S_OK; retn 0x8 -BASIC_DISPINTERFACE_GetTypeInfo,421,stdcall,int32,0,VoidPtr pThis;uint32 iTInfo;uint32 lcid;VoidPtr ppTInfo,internal,Validates iTInfo==0; resolves via EventSink_GetVtableBase; retn 0x10 -CreateIExprSrvObj,2000,stdcall,VoidPtr,0,VoidPtr pProject;uint16 wType;uint16 wSubType,internal,Creates expression server COM object; allocates 0x18c bytes; returns IUnknown*; retn 0xc -EbCreateContext,274,stdcall,int32,0,VoidPtr ppContext,internal,Allocates 0x1c-byte context; zeroes it; returns ptr via *ppContext; retn 0x4 -EbDestroyContext,275,stdcall,int32,0,VoidPtr pContext,internal,Destroys execution context; retn 0x4 -EbGetErrorInfo,284,stdcall,int32,0,VoidPtr ppErrorInfo,internal,Returns error information pointer; retn 0x4 -EbGetHandleOfExecutingProject,232,stdcall,int32,0,VoidPtr ppHandle,internal,Reads TLS -> execution context -> project handle; retn 0x4 -EbIsProjectOnStack,298,stdcall,int32,0,VoidPtr pProject,internal,Checks TLS for project presence on call stack; retn 0x4 -EbLibraryLoad,268,stdcall,int32,0,VoidPtr pCallback,internal,Stores callback ptr to global 0x66119380; retn 0x4 -EbLibraryUnload,269,stdcall,int32,0,VoidPtr pCallback,internal,Library unload callback registration; retn 0x4 -EbLoadRunTime,271,stdcall,int32,0,VoidPtr pTlsState;VoidPtr pProjectData,internal,Massive function: loads ObjectTable; sets up ExecProj; iterates objects; resolves vtables; ebp-based; retn 0x8 -EbResetProject,231,stdcall,int32,0,VoidPtr pProject,internal,Resets project state; retn 0x4 -EbResetProjectNormal,265,stdcall,int32,0,VoidPtr pProject,internal,Normal project reset; retn 0x4 -EbSetContextWorkerThread,276,stdcall,int32,0,VoidPtr pContext,internal,Sets worker thread context; retn 0x4 -TipCreateInstanceEx,299,stdcall,int32,0,VoidPtr pProject;VoidPtr rclsid;VoidPtr pUnkOuter;uint32 dwClsCtx;VoidPtr riid;VoidPtr ppv,internal,Extended CoCreateInstance wrapper for VB projects; retn varies -TipGetAddressOfPredeclaredInstance,195,stdcall,int32,0,VoidPtr pProject;uint16 wObjectIndex;VoidPtr ppAddress,internal,Returns ptr to ObjectData+0x8 for predeclared instance; retn 0xc -TipSetOption,220,stdcall,int32,0,VoidPtr pProject;uint32 dwOption;uint32 dwValue,internal,Sets project option; retn varies -TipUnloadInstance,266,stdcall,int32,0,VoidPtr pInstance,internal,Unloads VB object instance; retn varies -TipUnloadProject,227,stdcall,int32,0,VoidPtr pProject,internal,Checks project on stack; enters critsec; cleans up object table; releases ExecProj; retn 0x4 -Zombie_GetIDsOfNames,435,stdcall,int32,0,VoidPtr pThis;VoidPtr riid;VoidPtr rgszNames;uint32 cNames;uint32 lcid;Int32Ptr rgDispId,internal,Returns E_ZOMBIE (0x80010007) immediately; retn 0x18 -Zombie_GetTypeInfo,434,stdcall,int32,0,VoidPtr pThis;uint32 iTInfo;uint32 lcid;VoidPtr ppTInfo,internal,Returns E_ZOMBIE (0x80010007) immediately; retn 0x10 -Zombie_GetTypeInfoCount,433,stdcall,int32,0,VoidPtr pThis;Int32Ptr pctinfo,internal,Returns E_ZOMBIE (0x80010007) immediately; retn 0x8 -Zombie_Invoke,436,stdcall,int32,0,VoidPtr pThis;int32 dispId;VoidPtr riid;uint32 lcid;uint16 wFlags;VoidPtr pDispParams;VoidPtr pVarResult;VoidPtr pExcepInfo;Int32Ptr puArgErr,internal,Returns E_ZOMBIE (0x80010007) immediately; retn 0x24 -Zombie_QueryInterface,430,stdcall,int32,0,VoidPtr pThis;VoidPtr riid;VoidPtr ppvObj,internal,Delegates QI to inner obj at pThis+0x8; validates ppvObj non-null; retn 0xc +__vbaAptOffset,163,stdcall,int32,0,0,VoidPtr pObj,internal,Returns apartment thread offset; returns 0 if single-threaded; reads TLS for multi-apartment +BASIC_CLASS_AddRef,411,stdcall,uint32,0,0,VoidPtr pThis,internal,Delegates to inner object at pThis+0x8 via vtable[1] (AddRef) +BASIC_CLASS_GetIDsOfNames,413,stdcall,int32,0,0,VoidPtr pThis;VoidPtr riid;VoidPtr rgszNames;uint32 cNames;uint32 lcid;Int32Ptr rgDispId,internal,Compares riid to IID_NULL; resolves via EventSink_GetVtableBase; retn 0x18 +BASIC_CLASS_Invoke,414,stdcall,int32,0,0,VoidPtr pThis;int32 dispId;VoidPtr riid;uint32 lcid;uint16 wFlags;VoidPtr pDispParams;VoidPtr pVarResult;VoidPtr pExcepInfo;Int32Ptr puArgErr,internal,Full IDispatch::Invoke; validates riid=IID_NULL; calls IDispatchInvoke; fallback chains through pThis+0x18/+0x10; retn 0x24 +BASIC_CLASS_QueryInterface,410,stdcall,int32,0,0,VoidPtr pThis;VoidPtr riid;VoidPtr ppvObj,internal,Delegates to inner object at pThis+0x8 via vtable[0] (QI); retn 0xc +BASIC_CLASS_Release,412,stdcall,uint32,0,0,VoidPtr pThis,internal,Delegates to inner object at pThis+0x8 via vtable[2] (Release) +BASIC_DISPINTERFACE_GetTICount,420,stdcall,int32,0,0,VoidPtr pThis;Int32Ptr pctinfo,internal,Trivially stores 1 into *pctinfo and returns S_OK; retn 0x8 +BASIC_DISPINTERFACE_GetTypeInfo,421,stdcall,int32,0,0,VoidPtr pThis;uint32 iTInfo;uint32 lcid;VoidPtr ppTInfo,internal,Validates iTInfo==0; resolves via EventSink_GetVtableBase; retn 0x10 +CreateIExprSrvObj,2000,stdcall,VoidPtr,0,0,VoidPtr pProject;uint16 wType;uint16 wSubType,internal,Creates expression server COM object; allocates 0x18c bytes; returns IUnknown*; retn 0xc +EbCreateContext,274,stdcall,int32,0,0,VoidPtr ppContext,internal,Allocates 0x1c-byte context; zeroes it; returns ptr via *ppContext; retn 0x4 +EbDestroyContext,275,stdcall,int32,0,0,VoidPtr pContext,internal,Destroys execution context; retn 0x4 +EbGetErrorInfo,284,stdcall,int32,0,0,VoidPtr ppErrorInfo,internal,Returns error information pointer; retn 0x4 +EbGetHandleOfExecutingProject,232,stdcall,int32,0,0,VoidPtr ppHandle,internal,Reads TLS -> execution context -> project handle; retn 0x4 +EbIsProjectOnStack,298,stdcall,int32,0,0,VoidPtr pProject,internal,Checks TLS for project presence on call stack; retn 0x4 +EbLibraryLoad,268,stdcall,int32,0,0,VoidPtr pCallback,internal,Stores callback ptr to global 0x66119380; retn 0x4 +EbLibraryUnload,269,stdcall,int32,0,0,VoidPtr pCallback,internal,Library unload callback registration; retn 0x4 +EbLoadRunTime,271,stdcall,int32,0,0,VoidPtr pTlsState;VoidPtr pProjectData,internal,Massive function: loads ObjectTable; sets up ExecProj; iterates objects; resolves vtables; ebp-based; retn 0x8 +EbResetProject,231,stdcall,int32,0,0,VoidPtr pProject,internal,Resets project state; retn 0x4 +EbResetProjectNormal,265,stdcall,int32,0,0,VoidPtr pProject,internal,Normal project reset; retn 0x4 +EbSetContextWorkerThread,276,stdcall,int32,0,0,VoidPtr pContext,internal,Sets worker thread context; retn 0x4 +TipCreateInstanceEx,299,stdcall,int32,0,0,VoidPtr pProject;VoidPtr rclsid;VoidPtr pUnkOuter;uint32 dwClsCtx;VoidPtr riid;VoidPtr ppv,internal,Extended CoCreateInstance wrapper for VB projects; retn varies +TipGetAddressOfPredeclaredInstance,195,stdcall,int32,0,0,VoidPtr pProject;uint16 wObjectIndex;VoidPtr ppAddress,internal,Returns ptr to ObjectData+0x8 for predeclared instance; retn 0xc +TipSetOption,220,stdcall,int32,0,0,VoidPtr pProject;uint32 dwOption;uint32 dwValue,internal,Sets project option; retn varies +TipUnloadInstance,266,stdcall,int32,0,0,VoidPtr pInstance,internal,Unloads VB object instance; retn varies +TipUnloadProject,227,stdcall,int32,0,0,VoidPtr pProject,internal,Checks project on stack; enters critsec; cleans up object table; releases ExecProj; retn 0x4 +Zombie_GetIDsOfNames,435,stdcall,int32,0,0,VoidPtr pThis;VoidPtr riid;VoidPtr rgszNames;uint32 cNames;uint32 lcid;Int32Ptr rgDispId,internal,Returns E_ZOMBIE (0x80010007) immediately; retn 0x18 +Zombie_GetTypeInfo,434,stdcall,int32,0,0,VoidPtr pThis;uint32 iTInfo;uint32 lcid;VoidPtr ppTInfo,internal,Returns E_ZOMBIE (0x80010007) immediately; retn 0x10 +Zombie_GetTypeInfoCount,433,stdcall,int32,0,0,VoidPtr pThis;Int32Ptr pctinfo,internal,Returns E_ZOMBIE (0x80010007) immediately; retn 0x8 +Zombie_Invoke,436,stdcall,int32,0,0,VoidPtr pThis;int32 dispId;VoidPtr riid;uint32 lcid;uint16 wFlags;VoidPtr pDispParams;VoidPtr pVarResult;VoidPtr pExcepInfo;Int32Ptr puArgErr,internal,Returns E_ZOMBIE (0x80010007) immediately; retn 0x24 +Zombie_QueryInterface,430,stdcall,int32,0,0,VoidPtr pThis;VoidPtr riid;VoidPtr ppvObj,internal,Delegates QI to inner obj at pThis+0x8; validates ppvObj non-null; retn 0xc # # === MISCELLANEOUS / RTC === # -__vbaChkstk,182,special,void,0,,misc,custom CC: EAX=alloc size; probes pages in 0x1000 increments -__vbaCopyBytes,183,stdcall,VoidPtr,0,uint32 cbSize;VoidPtr pDst;VoidPtr pSrc,misc,memcpy-like: copies cbSize bytes from pSrc to pDst; returns pDst -__vbaCopyBytesZero,184,stdcall,VoidPtr,0,uint32 cbSize;VoidPtr pDst;VoidPtr pSrc,misc,Copies cbSize bytes from pSrc to pDst; then zeros pSrc; returns pDst -__vbaFixstrConstruct,237,stdcall,void,0,uint32 cchLen;BstrPtr ppBstr,misc,Allocates zero-filled fixed-length BSTR; stores in *ppBstr; error 0xe on OOM -__vbaGosub,253,stdcall,int32,0,VoidPtr ppGosubStack,misc,Pushes gosub entry: allocates 8-byte node (next; retaddr); links to stack head; returns 0 (success); raises error 7 (OOM) on failure -__vbaGosubFree,254,stdcall,void,0,VoidPtr ppGosubStack,misc,Frees entire gosub stack linked list; walks list freeing each node -__vbaGosubReturn,255,stdcall,int32,0,VoidPtr ppGosubStack,misc,Pops gosub stack: restores return address from linked list node; frees node; returns 1 on success; raises error 3 if stack empty -__vbaNameFile,339,stdcall,void,0,Bstr bstrOldName;Bstr bstrNewName,misc,VB 'Name' statement: converts BSTRs to ANSI; calls MoveFileA; raises VB file errors on failure -__vbaOnGoCheck,353,stdcall,int16,0,int16 index,misc,Validates On...GoTo/GoSub index: clamps to [0; 255] range; raises error 5 if negative -__vbaPowerR8,354,stdcall,double,0,double dBase;double dExponent,misc,double exponentiation; handles special cases -__vbaR8ForNextCheck,373,stdcall,int32,0,double dCounter;double dStep;double dLimit,misc,For...Next boundary check for doubles; 0=continue -1=exit -__vbaRecAnsiToUni,380,stdcall,VoidPtr,0,VoidPtr pRecInfo;VoidPtr pDst;VoidPtr pSrc,misc,Destroys pDst fields; then converts ANSI record fields to Unicode; returns pDst -__vbaRecAssign,381,stdcall,VoidPtr,0,VoidPtr pRecInfo;VoidPtr pDst;VoidPtr pSrc,misc,Assigns UDT record: ecx=pRecInfo used by internal helper; returns pDst -__vbaRecDestruct,382,stdcall,VoidPtr,0,VoidPtr pRecInfo;VoidPtr pRec,misc,Destroys UDT record fields via CleanupPropertyEntries with flags=0; returns pRec -__vbaRecDestructAnsi,383,stdcall,void,0,VoidPtr pRecInfo;VoidPtr pRec,misc,Destroys UDT with ANSI conversion flag=2 via CleanupPropertyEntries -__vbaRecUniToAnsi,384,stdcall,VoidPtr,0,VoidPtr pRecInfo;VoidPtr pDst;VoidPtr pSrc,misc,Destroys pDst fields (ANSI flag=2); then converts Unicode record to ANSI (direction=1); returns pDst -rtcAnsiValueBstr,516,stdcall,int16,0,Bstr bstr,misc,retn 0x4; WideCharToMultiByte on first char; returns ANSI value in AX -rtcAppActivate,597,stdcall,void,0,VariantPtr pTitle;VariantPtr pWait,misc,retn 0x8; VB AppActivate; finds window by title/PID; calls SetForegroundWindow -rtcAppleScript,627,stdcall,void,0,VariantPtr pScript,misc,stub; always raises error 5 (not supported on Windows) -rtcArray,601,stdcall,void,0,VariantPtr result;VariantPtr element,misc,retn 0x8; builds VT_ARRAY|VT_VARIANT (0x200c); copies 16-byte result -rtcAtn,585,stdcall,double,0,double value,misc,VB6 Atn(); arctangent via fpatan -rtcBeep,534,cdecl,void,0,,misc,bare retn; calls MessageBeep(0) -rtcBstrFromAnsi,537,stdcall,Bstr,0,int32 ansiCode,misc,retn 0x4; MultiByteToWideChar; returns SysAllocStringLen result -rtcBstrFromByte,694,stdcall,Bstr,0,uint8 byteVal,misc,retn 0x4; SysAllocStringByteLen with 1 byte -rtcBstrFromChar,697,stdcall,Bstr,0,int32 charCode,misc,retn 0x4; range-checks 0x8000..0xFFFF; SysAllocStringLen 1 wchar -rtcBstrFromFormatVar,650,stdcall,Bstr,0,VariantPtr var;VariantPtr format;int16 firstDayOfWeek;int16 firstWeekOfYear,misc,retn 0x10; Format$() implementation -rtcByteValueBstr,693,stdcall,uint8,0,Bstr bstr,misc,retn 0x4; returns first byte of BSTR (ANSI byte value) -rtcCallByName,715,stdcall,void,0,VariantPtr result;IDispatchPtr object;Bstr methodName;int32 callType;SafeArrayPtr args;int32 argCount,misc,retn 0x18; IDispatch::Invoke wrapper -rtcCharValueBstr,696,stdcall,int16,0,Bstr bstr,misc,retn 0x4; returns first Unicode char value (AscW) -rtcChoose,665,stdcall,void,0,VariantPtr result;float index;SafeArrayPtr choices,misc,retn 0xC; Choose() function; 1-based index into variant array -rtcCommandBstr,669,cdecl,Bstr,0,,misc,bare retn; Command$(); returns SysAllocString of command line -rtcCommandVar,670,stdcall,VariantPtr,0,VariantPtr presult,misc,VB6 Command$ -rtcCompareBstr,649,stdcall,void,0,VariantPtr result;Bstr str1;Bstr str2;int32 compare,misc,retn 0x10; wraps __vbaStrCompVar -rtcCos,583,stdcall,double,0,double value,misc,retn 0x8; fcos instruction; returns via FPU ST(0) -rtcCreateObject,625,stdcall,void,0,VariantPtr result;Bstr progId,misc,retn 0x8; CoCreateInstance wrapper; VT_DISPATCH result -rtcCreateObject2,716,stdcall,void,0,VariantPtr result;Bstr progId;Bstr serverName,misc,retn 0xC; CreateObject with remote server; MkParseDisplayName/BindToObject -rtcCurrentDir,647,stdcall,void,0,VariantPtr result;VariantPtr drive,misc,retn 0x8; CurDir variant version; returns VT_BSTR in VARIANT -rtcCurrentDirBstr,646,stdcall,Bstr,0,VariantPtr drive,misc,retn 0x4; CurDir$ string version -rtcCVErrFromVar,643,stdcall,void,0,VariantPtr result;VariantPtr errVar,misc,retn 0x8; converts error number to VT_ERROR variant (0x800A0000 | errnum) -rtcDateAdd,661,stdcall,void,0,VariantPtr result;Bstr interval;double number;VariantPtr date,misc,retn 0x14; DateAdd(); tests/fixtures/data passes the result pointer (top); the interval BSTR; the Double; the date Variant -rtcDateDiff,662,stdcall,void,0,VariantPtr result;Bstr interval;VariantPtr date1;VariantPtr date2;int16 firstDayOfWeek;int16 firstWeekOfYear,misc,retn 0x18; DateDiff(); computes interval difference -rtcDateFromVar,641,stdcall,double,0,VariantPtr var,misc,retn 0x4; CDate(); wraps __vbaDateVar; returns DATE double via FPU -rtcDatePart,663,stdcall,void,0,VariantPtr result;Bstr interval;VariantPtr date;int16 firstDayOfWeek;int16 firstWeekOfYear,misc,retn 0x14; DatePart(); extracts year/month/day/etc. -rtcDDB,673,stdcall,double,0,double cost;double salvage;double life;double period;VariantPtr factor,misc,retn 0x24; double-declining-balance depreciation; returns via FPU -rtcDeleteSetting,691,stdcall,void,0,Bstr appName;VariantPtr section;VariantPtr key,misc,retn 0x24; DeleteSetting; registry HKCU\Software\VB and VBA...; large stack frame with variant args -rtcDoEvents,598,stdcall,int32,0,,misc,VB6 DoEvents; processes message queue -rtcEndOfFile,571,stdcall,int16,0,int16 fileNum,misc,retn 0x4; EOF(); checks file position vs length -rtcEnvironBstr,667,stdcall,Bstr,0,VariantPtr envString,misc,retn 0x4; Environ$(); GetEnvironmentVariableA -rtcEnvironVar,666,stdcall,void,0,VariantPtr result;VariantPtr envString,misc,retn 0x8; Environ() variant wrapper; calls rtcEnvironBstr then wraps in VT_BSTR variant -rtcErrObj,685,cdecl,IDispatchPtr,0,,misc,bare retn; returns TLS error object IDispatch ptr; AddRef before return -rtcExp,586,stdcall,double,0,double value,misc,VB6 Exp(); calls _CIexp -rtcFileLocation,569,stdcall,int32,0,int16 fileNum,misc,retn 0x4; Loc(); current file position / 128 for seq files -rtcFileReset,554,cdecl,void,0,,misc,bare retn; Reset; flushes and closes all open files -rtcFileSeek,568,stdcall,int32,0,int16 fileNum,misc,retn 0x4; Seek(); returns current byte position + 1 -rtcFileWidth,565,stdcall,void,0,int16 fileNum;int16 width,misc,retn 0x8; Width #; sets record width on file channel -rtcFilter,708,stdcall,void,0,VariantPtr result;VariantPtr sourceArray;Bstr match;int16 include;int32 compare,misc,retn 0x14; Filter(); searches string array for matches -rtcFV,678,stdcall,double,0,double rate;double nper;double pmt;VariantPtr pv;VariantPtr type,misc,retn 0x20; FV() financial function; future value calculation -rtcGetAllSettings,692,stdcall,void,0,VariantPtr pResult;Bstr appName;Bstr section,misc,retn 0xc; reads registry HKCU; returns 2D VARIANT array via pResult -rtcGetCurrentCalendar,700,cdecl,int32,0,,misc,retn; reads TLS thread state; returns calendar type from locale data -rtcGetDateBstr,609,cdecl,Bstr,0,,misc,retn; calls GetLocalTime; formats date as MM-DD-YYYY BSTR -rtcGetDateValue,540,stdcall,void,0,VariantPtr pResult;Bstr dateStr,misc,retn 0x8; calls VarDateFromStr; returns VT_DATE VARIANT -rtcGetDateVar,610,stdcall,void,0,VariantPtr pResult,misc,retn 0x4; calls rtcGetPresentDate; returns date-only VARIANT -rtcGetDayOfMonth,542,stdcall,void,0,VariantPtr pResult;VariantPtr pDate,misc,retn 0x8; extracts day field from decoded date; returns VT_I2 VARIANT -rtcGetDayOfWeek,552,stdcall,void,0,VariantPtr pResult;VariantPtr pDate;int32 firstDayOfWeek,misc,retn 0xc; computes weekday with adjustable first-day; returns VT_I2 VARIANT -rtcGetErl,605,cdecl,int32,0,,misc,retn; reads TLS+0x98 error line number from thread state -rtcGetHostLCID,624,cdecl,int32,0,,misc,retn; reads global LCID from static [0x66114318] -rtcGetHourOfDay,543,stdcall,void,0,VariantPtr pResult;VariantPtr pDate,misc,retn 0x8; extracts hour field; returns VT_I2 VARIANT -rtcGetMinuteOfHour,544,stdcall,void,0,VariantPtr pResult;VariantPtr pDate,misc,retn 0x8; extracts minute field; returns VT_I2 VARIANT -rtcGetMonthOfYear,545,stdcall,void,0,VariantPtr pResult;VariantPtr pDate,misc,retn 0x8; extracts month field; returns VT_I2 VARIANT -rtcGetObject,626,stdcall,void,0,VariantPtr pResult;VariantPtr pathname;VariantPtr className,misc,retn 0xc; uses MkParseDisplayName/GetActiveObject/CoCreateInstance -rtcGetPresentDate,546,stdcall,void,0,VariantPtr pResult,misc,retn 0x4; gets current date+time via GetLocalTime; returns VT_DATE VARIANT -rtcGetSecondOfMinute,547,stdcall,void,0,VariantPtr pResult;VariantPtr pDate,misc,retn 0x8; extracts second field; returns VT_I2 VARIANT -rtcGetSetting,689,stdcall,Bstr,0,Bstr appName;Bstr section;Bstr key;VariantPtr bytes),misc,retn 0x1c; registry read from HKCU; last param is inline VARIANT (7 dwords total) -rtcGetTimeBstr,611,cdecl,Bstr,0,,misc,retn; formats current time as HH:MM:SS BSTR -rtcGetTimer,535,cdecl,float,0,,misc,retn; returns seconds since midnight as Single via FPU (GetLocalTime) -rtcGetTimeValue,541,stdcall,void,0,VariantPtr pResult;Bstr timeStr,misc,retn 0x8; calls VarDateFromStr with time flags; returns VT_DATE VARIANT -rtcGetTimeVar,612,stdcall,void,0,VariantPtr pResult,misc,retn 0x4; computes seconds since midnight / 86400.0; returns VT_DATE VARIANT -rtcGetYear,553,stdcall,void,0,VariantPtr pResult;VariantPtr pDate,misc,retn 0x8; extracts year field; returns VT_I2 VARIANT -rtcIMEStatus,615,cdecl,int32,0,,misc,retn; checks locale flag; calls GetFocus+internal IME helper -rtcImmediateIf,681,stdcall,void,0,VariantPtr pResult;VariantPtr condition;VariantPtr truePart;VariantPtr falsePart,misc,retn 0x10; IIf(); evaluates condition then copies truePart or falsePart -rtcInputBox,596,stdcall,Hresult,0,VariantPtr pResult;VariantPtr prompt;VariantPtr title;VariantPtr xPos;VariantPtr yPos;VariantPtr helpFile;VariantPtr context,misc,retn 0x1c; displays input dialog; 7 params -rtcIPMT,674,stdcall,double,0,double rate;double per;double nper;double pv;int32 due;VariantPtr fv,misc,retn 0x28; interest payment calculation; returns double via FPU -rtcIRR,682,stdcall,double,0,SafeArrayPtr values;VariantPtr guess,misc,retn 0x8; internal rate of return via Newton-Raphson iteration; returns double via FPU -rtcIsArray,556,stdcall,int16,0,VariantPtr pVar,misc,retn 0x4; checks VT_ARRAY bit (bit 13); returns VB boolean (-1/0) -rtcIsDate,557,stdcall,int16,0,VariantPtr pVar,misc,retn 0x4; checks if variant can be parsed as date; complex type checking -rtcIsEmpty,558,stdcall,int16,0,VariantPtr pVar,misc,retn 0x4; checks if VT == VT_EMPTY (0); returns VB boolean -rtcIsError,559,stdcall,int16,0,VariantPtr pVar,misc,retn 0x4; checks if VT == VT_ERROR (0xa); returns VB boolean -rtcIsMissing,592,stdcall,int16,0,VariantPtr pVar,misc,retn 0x4; checks for VT_ERROR with E_PARAMNOTFOUND (0x80020004) -rtcIsNull,560,stdcall,int16,0,VariantPtr pVar,misc,retn 0x4; checks if VT == VT_NULL (1); returns VB boolean -rtcIsNumeric,561,stdcall,int16,0,VariantPtr pvar,misc,VB6 IsNumeric(); returns VB bool -rtcIsObject,562,stdcall,int16,0,VariantPtr pvar,misc,VB6 IsObject(); checks vt==9 -rtcLog,587,stdcall,double,0,double value,misc,VB6 Log(); natural log via fyl2x -rtcMacId,590,stdcall,Bstr,0,Bstr bstr,misc,push ebp; WideCharToMultiByte conversion; retn 0x4 -rtcMIRR,683,stdcall,double,0,SafeArrayPtr ppsa;double financeRate;double reinvestRate,misc,push ebp; SafeArray 1D; FPU return st0; retn 0x14 -rtcMonthName,707,stdcall,Bstr,0,int32 month;int16 bAbbreviate,misc,push ebp; calendar-aware; calls GetDateFormatW; retn 0x8 -rtcMsgBox,595,stdcall,int32,0,VariantPtr prompt;int32 flags;VariantPtr title;VariantPtr helpfile;VariantPtr context,misc,VB6 MsgBox(); retn 0x14 -rtcNPer,679,stdcall,double,0,double rate;double pmt;double pv;VariantPtr pvarFv;VariantPtr pvarType,misc,push ebp; optional VARIANT params with 0x80020004 Missing check; retn 0x20 -rtcNPV,684,stdcall,double,0,double rate;SafeArrayPtr ppsa,misc,push ebp; net present value; SafeArray 1D; FPU return; retn 0xc -rtcPackDate,538,stdcall,void,0,VariantPtr pvarResult;int16 year;int16 month;int16 day,misc,push ebp; packs SYSTEMTIME; retval via hidden ptr; retn 0x10 -rtcPackTime,539,stdcall,void,0,VariantPtr pvarResult;int16 hour;int16 minute;int16 second,misc,push ebp; packs SYSTEMTIME with fixed date 1899-12-30; retn 0x10 -rtcPartition,664,stdcall,void,0,VariantPtr pvarResult;VariantPtr pvarNumber;VariantPtr pvarStart;VariantPtr pvarStop;VariantPtr pvarInterval,misc,push ebp; string partition ranges; complex formatting; retn 0x14 -rtcPMT,676,stdcall,double,0,double rate;double nper;double pv;VariantPtr pvarFv;VariantPtr pvarType,misc,push ebp; FPU financial calc; optional VARIANT Missing checks; retn 0x20 -rtcPPMT,675,stdcall,double,0,double rate;double per;double nper;double pv;VariantPtr pvarFv;VariantPtr pvarType,misc,push ebp; calls rtcPMT+rtcIPMT; retn 0x28 -rtcPV,677,stdcall,double,0,double rate;double nper;double pmt;VariantPtr pvarFv;VariantPtr pvarType,misc,push ebp; present value; optional VARIANT Missing; retn 0x20 -rtcQBColor,589,stdcall,int32,0,int16 color,misc,no frame ptr; table lookup (16-entry QBColor table); retn 0x4 -rtcRandomize,594,stdcall,void,0,VariantPtr pvarSeed,misc,no frame ptr; optional VARIANT seed; calls Randomize helper; retn 0x4 -rtcRandomNext,593,stdcall,float,0,VariantPtr pvarSeed,misc,VB Rnd(); optional VARIANT seed; returns the Single in ST0 (tests/fixtures/data stores it with FStFPR4); retn 0x4 -rtcRate,680,stdcall,double,0,double nper;double pmt;double pv;VariantPtr pvarFv;VariantPtr pvarType;VariantPtr pvarGuess,misc,push ebp; Newton-Raphson iteration (max 40); FPU return; retn 0x24 -rtcRgb,588,stdcall,int32,0,int16 red;int16 green;int16 blue,misc,no frame ptr; clamps 0-255; packs BGR into int32; retn 0xc -rtcSaveSetting,690,stdcall,void,0,Bstr bstrAppName;Bstr bstrSection;Bstr bstrKey;Bstr bstrValue,misc,push ebp; registry write to HKCU; retn 0x10 -rtcSendKeys,599,stdcall,void,0,Bstr bstrKeys;VariantPtr pvarWait,misc,no frame ptr; optional wait; posts keystrokes; retn 0x8 -rtcSetCurrentCalendar,699,stdcall,void,0,int32 calendarType,misc,no frame ptr; validates 0-1; delegates to internal setter; retn 0x4 -rtcSetDateBstr,549,stdcall,void,0,Bstr bstrDate,misc,no frame ptr; thin wrapper; delegates to internal date setter; retn 0x4 -rtcSetDateVar,548,stdcall,void,0,VariantPtr pvarDate,misc,push ebp; converts variant to DATE; calls SetLocalTime; retn 0x4 -rtcSetTimeBstr,551,stdcall,void,0,Bstr bstrTime,misc,no frame ptr; thin wrapper; delegates to internal time setter; retn 0x4 -rtcSetTimeVar,550,stdcall,void,0,VariantPtr pvarTime,misc,push ebp; converts variant to DATE; calls SetLocalTime with fixed date; retn 0x4 -rtcShell,600,stdcall,double,0,VariantPtr pCommand;int16 nWindowStyle,misc,retn 0x8; VB Shell(); calls CreateProcessA/W; returns PID as double -rtcSin,582,stdcall,double,0,double value,misc,push ebp; fabs range check then fsin; FPU return; retn 0x8 -rtcSLN,671,stdcall,double,0,double cost;double salvage;double life,misc,no frame ptr; (cost-salvage)/life; FPU return; retn 0x18 -rtcSqr,614,stdcall,double,0,double value,misc,retn 0x8; VB Sqr(); validates value >= 0; then FSQRT -rtcSwitch,668,stdcall,VariantPtr,0,VariantPtr retval;SafeArrayPtr ppParamArray,misc,retn 0x8; VB Switch(); walks expr/value pairs in SafeArray -rtcSYD,672,stdcall,double,0,double cost;double salvage;double life;double period,misc,push ebp; sum-of-years-digits depreciation; FPU return; retn 0x20 -rtcTan,584,stdcall,double,0,double value,misc,retn 0x8; VB Tan(); validates range; FPTAN + FDIVP -rtcTypeName,591,stdcall,Bstr,0,VariantPtr pVar,misc,retn 0x4; VB TypeName(); inspects VT; IDispatch/IUnknown; returns type string -rtcVarType,563,stdcall,int32,0,VariantPtr pVar,misc,retn 0x4; VB VarType(); returns VT code as zero-extended uint16 -rtcWeekdayName,706,stdcall,Bstr,0,int32 nWeekday;int16 bAbbreviate;int32 nFirstDayOfWeek,misc,retn 0xc; VB WeekdayName(); uses calendar-aware lookup +__vbaChkstk,182,special,void,0,0,,misc,custom CC: EAX=alloc size; probes pages in 0x1000 increments +__vbaCopyBytes,183,stdcall,VoidPtr,0,0,uint32 cbSize;VoidPtr pDst;VoidPtr pSrc,misc,memcpy-like: copies cbSize bytes from pSrc to pDst; returns pDst +__vbaCopyBytesZero,184,stdcall,VoidPtr,0,0,uint32 cbSize;VoidPtr pDst;VoidPtr pSrc,misc,Copies cbSize bytes from pSrc to pDst; then zeros pSrc; returns pDst +__vbaFixstrConstruct,237,stdcall,void,0,0,uint32 cchLen;BstrPtr ppBstr,misc,Allocates zero-filled fixed-length BSTR; stores in *ppBstr; error 0xe on OOM +__vbaGosub,253,stdcall,int32,0,0,VoidPtr ppGosubStack,misc,Pushes gosub entry: allocates 8-byte node (next; retaddr); links to stack head; returns 0 (success); raises error 7 (OOM) on failure +__vbaGosubFree,254,stdcall,void,0,0,VoidPtr ppGosubStack,misc,Frees entire gosub stack linked list; walks list freeing each node +__vbaGosubReturn,255,stdcall,int32,0,0,VoidPtr ppGosubStack,misc,Pops gosub stack: restores return address from linked list node; frees node; returns 1 on success; raises error 3 if stack empty +__vbaNameFile,339,stdcall,void,0,0,Bstr bstrOldName;Bstr bstrNewName,misc,VB 'Name' statement: converts BSTRs to ANSI; calls MoveFileA; raises VB file errors on failure +__vbaOnGoCheck,353,stdcall,int16,0,0,int16 index,misc,Validates On...GoTo/GoSub index: clamps to [0; 255] range; raises error 5 if negative +__vbaPowerR8,354,stdcall,double,0,0,double dBase;double dExponent,misc,double exponentiation; handles special cases +__vbaR8ForNextCheck,373,stdcall,int32,0,0,double dCounter;double dStep;double dLimit,misc,For...Next boundary check for doubles; 0=continue -1=exit +__vbaRecAnsiToUni,380,stdcall,VoidPtr,0,0,VoidPtr pRecInfo;VoidPtr pDst;VoidPtr pSrc,misc,Destroys pDst fields; then converts ANSI record fields to Unicode; returns pDst +__vbaRecAssign,381,stdcall,VoidPtr,0,0,VoidPtr pRecInfo;VoidPtr pDst;VoidPtr pSrc,misc,Assigns UDT record: ecx=pRecInfo used by internal helper; returns pDst +__vbaRecDestruct,382,stdcall,VoidPtr,0,0,VoidPtr pRecInfo;VoidPtr pRec,misc,Destroys UDT record fields via CleanupPropertyEntries with flags=0; returns pRec +__vbaRecDestructAnsi,383,stdcall,void,0,0,VoidPtr pRecInfo;VoidPtr pRec,misc,Destroys UDT with ANSI conversion flag=2 via CleanupPropertyEntries +__vbaRecUniToAnsi,384,stdcall,VoidPtr,0,0,VoidPtr pRecInfo;VoidPtr pDst;VoidPtr pSrc,misc,Destroys pDst fields (ANSI flag=2); then converts Unicode record to ANSI (direction=1); returns pDst +rtcAnsiValueBstr,516,stdcall,int16,0,0,Bstr bstr,misc,retn 0x4; WideCharToMultiByte on first char; returns ANSI value in AX +rtcAppActivate,597,stdcall,void,0,0,VariantPtr pTitle;VariantPtr pWait,misc,retn 0x8; VB AppActivate; finds window by title/PID; calls SetForegroundWindow +rtcAppleScript,627,stdcall,void,0,1,VariantPtr pScript,misc,stub; always raises error 5 (not supported on Windows) +rtcArray,601,stdcall,void,0,0,VariantPtr result;VariantPtr element,misc,retn 0x8; builds VT_ARRAY|VT_VARIANT (0x200c); copies 16-byte result +rtcAtn,585,stdcall,double,0,0,double value,misc,VB6 Atn(); arctangent via fpatan +rtcBeep,534,cdecl,void,0,0,,misc,bare retn; calls MessageBeep(0) +rtcBstrFromAnsi,537,stdcall,Bstr,0,0,int32 ansiCode,misc,retn 0x4; MultiByteToWideChar; returns SysAllocStringLen result +rtcBstrFromByte,694,stdcall,Bstr,0,0,uint8 byteVal,misc,retn 0x4; SysAllocStringByteLen with 1 byte +rtcBstrFromChar,697,stdcall,Bstr,0,0,int32 charCode,misc,retn 0x4; range-checks 0x8000..0xFFFF; SysAllocStringLen 1 wchar +rtcBstrFromFormatVar,650,stdcall,Bstr,0,0,VariantPtr var;VariantPtr format;int16 firstDayOfWeek;int16 firstWeekOfYear,misc,retn 0x10; Format$() implementation +rtcByteValueBstr,693,stdcall,uint8,0,0,Bstr bstr,misc,retn 0x4; returns first byte of BSTR (ANSI byte value) +rtcCallByName,715,stdcall,void,0,0,VariantPtr result;IDispatchPtr object;Bstr methodName;int32 callType;SafeArrayPtr args;int32 argCount,misc,retn 0x18; IDispatch::Invoke wrapper +rtcCharValueBstr,696,stdcall,int16,0,0,Bstr bstr,misc,retn 0x4; returns first Unicode char value (AscW) +rtcChoose,665,stdcall,void,0,0,VariantPtr result;float index;SafeArrayPtr choices,misc,retn 0xC; Choose() function; 1-based index into variant array +rtcCommandBstr,669,cdecl,Bstr,0,0,,misc,bare retn; Command$(); returns SysAllocString of command line +rtcCommandVar,670,stdcall,VariantPtr,0,0,VariantPtr presult,misc,VB6 Command$ +rtcCompareBstr,649,stdcall,void,0,0,VariantPtr result;Bstr str1;Bstr str2;int32 compare,misc,retn 0x10; wraps __vbaStrCompVar +rtcCos,583,stdcall,double,0,0,double value,misc,retn 0x8; fcos instruction; returns via FPU ST(0) +rtcCreateObject,625,stdcall,void,0,0,VariantPtr result;Bstr progId,misc,retn 0x8; CoCreateInstance wrapper; VT_DISPATCH result +rtcCreateObject2,716,stdcall,void,0,0,VariantPtr result;Bstr progId;Bstr serverName,misc,retn 0xC; CreateObject with remote server; MkParseDisplayName/BindToObject +rtcCurrentDir,647,stdcall,void,0,0,VariantPtr result;VariantPtr drive,misc,retn 0x8; CurDir variant version; returns VT_BSTR in VARIANT +rtcCurrentDirBstr,646,stdcall,Bstr,0,0,VariantPtr drive,misc,retn 0x4; CurDir$ string version +rtcCVErrFromVar,643,stdcall,void,0,0,VariantPtr result;VariantPtr errVar,misc,retn 0x8; converts error number to VT_ERROR variant (0x800A0000 | errnum) +rtcDateAdd,661,stdcall,void,0,0,VariantPtr result;Bstr interval;double number;VariantPtr date,misc,retn 0x14; DateAdd(); tests/fixtures/data passes the result pointer (top); the interval BSTR; the Double; the date Variant +rtcDateDiff,662,stdcall,void,0,0,VariantPtr result;Bstr interval;VariantPtr date1;VariantPtr date2;int16 firstDayOfWeek;int16 firstWeekOfYear,misc,retn 0x18; DateDiff(); computes interval difference +rtcDateFromVar,641,stdcall,double,0,0,VariantPtr var,misc,retn 0x4; CDate(); wraps __vbaDateVar; returns DATE double via FPU +rtcDatePart,663,stdcall,void,0,0,VariantPtr result;Bstr interval;VariantPtr date;int16 firstDayOfWeek;int16 firstWeekOfYear,misc,retn 0x14; DatePart(); extracts year/month/day/etc. +rtcDDB,673,stdcall,double,0,0,double cost;double salvage;double life;double period;VariantPtr factor,misc,retn 0x24; double-declining-balance depreciation; returns via FPU +rtcDeleteSetting,691,stdcall,void,0,0,Bstr appName;VariantPtr section;VariantPtr key,misc,retn 0x24; DeleteSetting; registry HKCU\Software\VB and VBA...; large stack frame with variant args +rtcDoEvents,598,stdcall,int32,0,0,,misc,VB6 DoEvents; processes message queue +rtcEndOfFile,571,stdcall,int16,0,0,int16 fileNum,misc,retn 0x4; EOF(); checks file position vs length +rtcEnvironBstr,667,stdcall,Bstr,0,0,VariantPtr envString,misc,retn 0x4; Environ$(); GetEnvironmentVariableA +rtcEnvironVar,666,stdcall,void,0,0,VariantPtr result;VariantPtr envString,misc,retn 0x8; Environ() variant wrapper; calls rtcEnvironBstr then wraps in VT_BSTR variant +rtcErrObj,685,cdecl,IDispatchPtr,0,0,,misc,bare retn; returns TLS error object IDispatch ptr; AddRef before return +rtcExp,586,stdcall,double,0,0,double value,misc,VB6 Exp(); calls _CIexp +rtcFileLocation,569,stdcall,int32,0,0,int16 fileNum,misc,retn 0x4; Loc(); current file position / 128 for seq files +rtcFileReset,554,cdecl,void,0,0,,misc,bare retn; Reset; flushes and closes all open files +rtcFileSeek,568,stdcall,int32,0,0,int16 fileNum,misc,retn 0x4; Seek(); returns current byte position + 1 +rtcFileWidth,565,stdcall,void,0,0,int16 fileNum;int16 width,misc,retn 0x8; Width #; sets record width on file channel +rtcFilter,708,stdcall,void,0,0,VariantPtr result;VariantPtr sourceArray;Bstr match;int16 include;int32 compare,misc,retn 0x14; Filter(); searches string array for matches +rtcFV,678,stdcall,double,0,0,double rate;double nper;double pmt;VariantPtr pv;VariantPtr type,misc,retn 0x20; FV() financial function; future value calculation +rtcGetAllSettings,692,stdcall,void,0,0,VariantPtr pResult;Bstr appName;Bstr section,misc,retn 0xc; reads registry HKCU; returns 2D VARIANT array via pResult +rtcGetCurrentCalendar,700,cdecl,int32,0,0,,misc,retn; reads TLS thread state; returns calendar type from locale data +rtcGetDateBstr,609,cdecl,Bstr,0,0,,misc,retn; calls GetLocalTime; formats date as MM-DD-YYYY BSTR +rtcGetDateValue,540,stdcall,void,0,0,VariantPtr pResult;Bstr dateStr,misc,retn 0x8; calls VarDateFromStr; returns VT_DATE VARIANT +rtcGetDateVar,610,stdcall,void,0,0,VariantPtr pResult,misc,retn 0x4; calls rtcGetPresentDate; returns date-only VARIANT +rtcGetDayOfMonth,542,stdcall,void,0,0,VariantPtr pResult;VariantPtr pDate,misc,retn 0x8; extracts day field from decoded date; returns VT_I2 VARIANT +rtcGetDayOfWeek,552,stdcall,void,0,0,VariantPtr pResult;VariantPtr pDate;int32 firstDayOfWeek,misc,retn 0xc; computes weekday with adjustable first-day; returns VT_I2 VARIANT +rtcGetErl,605,cdecl,int32,0,0,,misc,retn; reads TLS+0x98 error line number from thread state +rtcGetHostLCID,624,cdecl,int32,0,0,,misc,retn; reads global LCID from static [0x66114318] +rtcGetHourOfDay,543,stdcall,void,0,0,VariantPtr pResult;VariantPtr pDate,misc,retn 0x8; extracts hour field; returns VT_I2 VARIANT +rtcGetMinuteOfHour,544,stdcall,void,0,0,VariantPtr pResult;VariantPtr pDate,misc,retn 0x8; extracts minute field; returns VT_I2 VARIANT +rtcGetMonthOfYear,545,stdcall,void,0,0,VariantPtr pResult;VariantPtr pDate,misc,retn 0x8; extracts month field; returns VT_I2 VARIANT +rtcGetObject,626,stdcall,void,0,0,VariantPtr pResult;VariantPtr pathname;VariantPtr className,misc,retn 0xc; uses MkParseDisplayName/GetActiveObject/CoCreateInstance +rtcGetPresentDate,546,stdcall,void,0,0,VariantPtr pResult,misc,retn 0x4; gets current date+time via GetLocalTime; returns VT_DATE VARIANT +rtcGetSecondOfMinute,547,stdcall,void,0,0,VariantPtr pResult;VariantPtr pDate,misc,retn 0x8; extracts second field; returns VT_I2 VARIANT +rtcGetSetting,689,stdcall,Bstr,0,0,Bstr appName;Bstr section;Bstr key;VariantPtr bytes),misc,retn 0x1c; registry read from HKCU; last param is inline VARIANT (7 dwords total) +rtcGetTimeBstr,611,cdecl,Bstr,0,0,,misc,retn; formats current time as HH:MM:SS BSTR +rtcGetTimer,535,cdecl,float,0,0,,misc,retn; returns seconds since midnight as Single via FPU (GetLocalTime) +rtcGetTimeValue,541,stdcall,void,0,0,VariantPtr pResult;Bstr timeStr,misc,retn 0x8; calls VarDateFromStr with time flags; returns VT_DATE VARIANT +rtcGetTimeVar,612,stdcall,void,0,0,VariantPtr pResult,misc,retn 0x4; computes seconds since midnight / 86400.0; returns VT_DATE VARIANT +rtcGetYear,553,stdcall,void,0,0,VariantPtr pResult;VariantPtr pDate,misc,retn 0x8; extracts year field; returns VT_I2 VARIANT +rtcIMEStatus,615,cdecl,int32,0,0,,misc,retn; checks locale flag; calls GetFocus+internal IME helper +rtcImmediateIf,681,stdcall,void,0,0,VariantPtr pResult;VariantPtr condition;VariantPtr truePart;VariantPtr falsePart,misc,retn 0x10; IIf(); evaluates condition then copies truePart or falsePart +rtcInputBox,596,stdcall,Hresult,0,0,VariantPtr pResult;VariantPtr prompt;VariantPtr title;VariantPtr xPos;VariantPtr yPos;VariantPtr helpFile;VariantPtr context,misc,retn 0x1c; displays input dialog; 7 params +rtcIPMT,674,stdcall,double,0,0,double rate;double per;double nper;double pv;int32 due;VariantPtr fv,misc,retn 0x28; interest payment calculation; returns double via FPU +rtcIRR,682,stdcall,double,0,0,SafeArrayPtr values;VariantPtr guess,misc,retn 0x8; internal rate of return via Newton-Raphson iteration; returns double via FPU +rtcIsArray,556,stdcall,int16,0,0,VariantPtr pVar,misc,retn 0x4; checks VT_ARRAY bit (bit 13); returns VB boolean (-1/0) +rtcIsDate,557,stdcall,int16,0,0,VariantPtr pVar,misc,retn 0x4; checks if variant can be parsed as date; complex type checking +rtcIsEmpty,558,stdcall,int16,0,0,VariantPtr pVar,misc,retn 0x4; checks if VT == VT_EMPTY (0); returns VB boolean +rtcIsError,559,stdcall,int16,0,0,VariantPtr pVar,misc,retn 0x4; checks if VT == VT_ERROR (0xa); returns VB boolean +rtcIsMissing,592,stdcall,int16,0,0,VariantPtr pVar,misc,retn 0x4; checks for VT_ERROR with E_PARAMNOTFOUND (0x80020004) +rtcIsNull,560,stdcall,int16,0,0,VariantPtr pVar,misc,retn 0x4; checks if VT == VT_NULL (1); returns VB boolean +rtcIsNumeric,561,stdcall,int16,0,0,VariantPtr pvar,misc,VB6 IsNumeric(); returns VB bool +rtcIsObject,562,stdcall,int16,0,0,VariantPtr pvar,misc,VB6 IsObject(); checks vt==9 +rtcLog,587,stdcall,double,0,0,double value,misc,VB6 Log(); natural log via fyl2x +rtcMacId,590,stdcall,Bstr,0,0,Bstr bstr,misc,push ebp; WideCharToMultiByte conversion; retn 0x4 +rtcMIRR,683,stdcall,double,0,0,SafeArrayPtr ppsa;double financeRate;double reinvestRate,misc,push ebp; SafeArray 1D; FPU return st0; retn 0x14 +rtcMonthName,707,stdcall,Bstr,0,0,int32 month;int16 bAbbreviate,misc,push ebp; calendar-aware; calls GetDateFormatW; retn 0x8 +rtcMsgBox,595,stdcall,int32,0,0,VariantPtr prompt;int32 flags;VariantPtr title;VariantPtr helpfile;VariantPtr context,misc,VB6 MsgBox(); retn 0x14 +rtcNPer,679,stdcall,double,0,0,double rate;double pmt;double pv;VariantPtr pvarFv;VariantPtr pvarType,misc,push ebp; optional VARIANT params with 0x80020004 Missing check; retn 0x20 +rtcNPV,684,stdcall,double,0,0,double rate;SafeArrayPtr ppsa,misc,push ebp; net present value; SafeArray 1D; FPU return; retn 0xc +rtcPackDate,538,stdcall,void,0,0,VariantPtr pvarResult;int16 year;int16 month;int16 day,misc,push ebp; packs SYSTEMTIME; retval via hidden ptr; retn 0x10 +rtcPackTime,539,stdcall,void,0,0,VariantPtr pvarResult;int16 hour;int16 minute;int16 second,misc,push ebp; packs SYSTEMTIME with fixed date 1899-12-30; retn 0x10 +rtcPartition,664,stdcall,void,0,0,VariantPtr pvarResult;VariantPtr pvarNumber;VariantPtr pvarStart;VariantPtr pvarStop;VariantPtr pvarInterval,misc,push ebp; string partition ranges; complex formatting; retn 0x14 +rtcPMT,676,stdcall,double,0,0,double rate;double nper;double pv;VariantPtr pvarFv;VariantPtr pvarType,misc,push ebp; FPU financial calc; optional VARIANT Missing checks; retn 0x20 +rtcPPMT,675,stdcall,double,0,0,double rate;double per;double nper;double pv;VariantPtr pvarFv;VariantPtr pvarType,misc,push ebp; calls rtcPMT+rtcIPMT; retn 0x28 +rtcPV,677,stdcall,double,0,0,double rate;double nper;double pmt;VariantPtr pvarFv;VariantPtr pvarType,misc,push ebp; present value; optional VARIANT Missing; retn 0x20 +rtcQBColor,589,stdcall,int32,0,0,int16 color,misc,no frame ptr; table lookup (16-entry QBColor table); retn 0x4 +rtcRandomize,594,stdcall,void,0,0,VariantPtr pvarSeed,misc,no frame ptr; optional VARIANT seed; calls Randomize helper; retn 0x4 +rtcRandomNext,593,stdcall,float,0,0,VariantPtr pvarSeed,misc,VB Rnd(); optional VARIANT seed; returns the Single in ST0 (tests/fixtures/data stores it with FStFPR4); retn 0x4 +rtcRate,680,stdcall,double,0,0,double nper;double pmt;double pv;VariantPtr pvarFv;VariantPtr pvarType;VariantPtr pvarGuess,misc,push ebp; Newton-Raphson iteration (max 40); FPU return; retn 0x24 +rtcRgb,588,stdcall,int32,0,0,int16 red;int16 green;int16 blue,misc,no frame ptr; clamps 0-255; packs BGR into int32; retn 0xc +rtcSaveSetting,690,stdcall,void,0,0,Bstr bstrAppName;Bstr bstrSection;Bstr bstrKey;Bstr bstrValue,misc,push ebp; registry write to HKCU; retn 0x10 +rtcSendKeys,599,stdcall,void,0,0,Bstr bstrKeys;VariantPtr pvarWait,misc,no frame ptr; optional wait; posts keystrokes; retn 0x8 +rtcSetCurrentCalendar,699,stdcall,void,0,0,int32 calendarType,misc,no frame ptr; validates 0-1; delegates to internal setter; retn 0x4 +rtcSetDateBstr,549,stdcall,void,0,0,Bstr bstrDate,misc,no frame ptr; thin wrapper; delegates to internal date setter; retn 0x4 +rtcSetDateVar,548,stdcall,void,0,0,VariantPtr pvarDate,misc,push ebp; converts variant to DATE; calls SetLocalTime; retn 0x4 +rtcSetTimeBstr,551,stdcall,void,0,0,Bstr bstrTime,misc,no frame ptr; thin wrapper; delegates to internal time setter; retn 0x4 +rtcSetTimeVar,550,stdcall,void,0,0,VariantPtr pvarTime,misc,push ebp; converts variant to DATE; calls SetLocalTime with fixed date; retn 0x4 +rtcShell,600,stdcall,double,0,0,VariantPtr pCommand;int16 nWindowStyle,misc,retn 0x8; VB Shell(); calls CreateProcessA/W; returns PID as double +rtcSin,582,stdcall,double,0,0,double value,misc,push ebp; fabs range check then fsin; FPU return; retn 0x8 +rtcSLN,671,stdcall,double,0,0,double cost;double salvage;double life,misc,no frame ptr; (cost-salvage)/life; FPU return; retn 0x18 +rtcSqr,614,stdcall,double,0,0,double value,misc,retn 0x8; VB Sqr(); validates value >= 0; then FSQRT +rtcSwitch,668,stdcall,VariantPtr,0,0,VariantPtr retval;SafeArrayPtr ppParamArray,misc,retn 0x8; VB Switch(); walks expr/value pairs in SafeArray +rtcSYD,672,stdcall,double,0,0,double cost;double salvage;double life;double period,misc,push ebp; sum-of-years-digits depreciation; FPU return; retn 0x20 +rtcTan,584,stdcall,double,0,0,double value,misc,retn 0x8; VB Tan(); validates range; FPTAN + FDIVP +rtcTypeName,591,stdcall,Bstr,0,0,VariantPtr pVar,misc,retn 0x4; VB TypeName(); inspects VT; IDispatch/IUnknown; returns type string +rtcVarType,563,stdcall,int32,0,0,VariantPtr pVar,misc,retn 0x4; VB VarType(); returns VT code as zero-extended uint16 +rtcWeekdayName,706,stdcall,Bstr,0,0,int32 nWeekday;int16 bAbbreviate;int32 nFirstDayOfWeek,misc,retn 0xc; VB WeekdayName(); uses calendar-aware lookup # # === EXPORTS WHOSE SIGNATURE IS NOT ANALYZED === # # Named so an ordinal import resolves; convention and types unknown. # -VarPtr,644,stdcall,int32,0,VoidPtr p,misc,mov eax [esp+4]; retn 4; VB VarPtr() returns its ByRef argument's address (8176 0x660eb536) -__vbaVarLateMemCallSt,114,unknown,unknown,0,,unanalyzed, -__vbaVarZero,147,unknown,unknown,0,,unanalyzed, -CopyRecord,190,unknown,unknown,0,,unanalyzed, -TipCreateInstanceProject2,230,unknown,unknown,0,,unanalyzed, -__vbaLateIdNamedCall,286,unknown,unknown,0,,unanalyzed, -__vbaLateIdNamedCallLd,287,unknown,unknown,0,,unanalyzed, -__vbaLateIdNamedCallSt,288,unknown,unknown,0,,unanalyzed, -__vbaLateIdNamedStAd,289,unknown,unknown,0,,unanalyzed, -__vbaLateMemCallSt,294,unknown,unknown,0,,unanalyzed, -__vbaLateMemNamedCall,295,unknown,unknown,0,,unanalyzed, -__vbaLateMemNamedCallLd,296,unknown,unknown,0,,unanalyzed, -__vbaLateMemNamedCallSt,297,unknown,unknown,0,,unanalyzed, -__vbaLateMemNamedStAd,322,unknown,unknown,0,,unanalyzed, -EVENT_SINK_GetIDsOfNames,403,unknown,unknown,0,,unanalyzed, -Zombie_AddRef,431,unknown,unknown,0,,unanalyzed, -Zombie_Release,432,unknown,unknown,0,,unanalyzed, -rtDecFromVar,564,stdcall,Hresult,0,VariantPtr pSrc;VariantPtr pResult,convert,CDec(); tests/fixtures/data calls it through ImpAdCallHresult with 8 bytes: the source Variant on top; the result temp below -rtBstrFromErrVar,634,unknown,unknown,0,,unanalyzed, -rtBoolFromErrVar,635,unknown,unknown,0,,unanalyzed, -rtCyFromErrVar,636,unknown,unknown,0,,unanalyzed, -rtI2FromErrVar,637,unknown,unknown,0,,unanalyzed, -rtI4FromErrVar,638,unknown,unknown,0,,unanalyzed, -rtR4FromErrVar,639,unknown,unknown,0,,unanalyzed, -rtR8FromErrVar,640,unknown,unknown,0,,unanalyzed, -rtUI1FromErrVar,686,unknown,unknown,0,,unanalyzed, -TipInvokeMethod2,999,unknown,unknown,0,,unanalyzed, -TipInvokeMethod,1016,unknown,unknown,0,,unanalyzed, -IID_IVbaHost,1024,unknown,unknown,0,,unanalyzed, -EbGetObjConnectionCounts,1025,unknown,unknown,0,,unanalyzed, -EbGetVBAObject,2010,unknown,unknown,0,,unanalyzed, +VarPtr,644,stdcall,int32,0,0,VoidPtr p,misc,mov eax [esp+4]; retn 4; VB VarPtr() returns its ByRef argument's address (8176 0x660eb536) +__vbaVarLateMemCallSt,114,unknown,unknown,0,0,,unanalyzed, +__vbaVarZero,147,unknown,unknown,0,0,,unanalyzed, +CopyRecord,190,unknown,unknown,0,0,,unanalyzed, +TipCreateInstanceProject2,230,unknown,unknown,0,0,,unanalyzed, +__vbaLateIdNamedCall,286,unknown,unknown,0,0,,unanalyzed, +__vbaLateIdNamedCallLd,287,unknown,unknown,0,0,,unanalyzed, +__vbaLateIdNamedCallSt,288,unknown,unknown,0,0,,unanalyzed, +__vbaLateIdNamedStAd,289,unknown,unknown,0,0,,unanalyzed, +__vbaLateMemCallSt,294,unknown,unknown,0,0,,unanalyzed, +__vbaLateMemNamedCall,295,unknown,unknown,0,0,,unanalyzed, +__vbaLateMemNamedCallLd,296,unknown,unknown,0,0,,unanalyzed, +__vbaLateMemNamedCallSt,297,unknown,unknown,0,0,,unanalyzed, +__vbaLateMemNamedStAd,322,unknown,unknown,0,0,,unanalyzed, +EVENT_SINK_GetIDsOfNames,403,unknown,unknown,0,0,,unanalyzed, +Zombie_AddRef,431,unknown,unknown,0,0,,unanalyzed, +Zombie_Release,432,unknown,unknown,0,0,,unanalyzed, +rtDecFromVar,564,stdcall,Hresult,0,0,VariantPtr pSrc;VariantPtr pResult,convert,CDec(); tests/fixtures/data calls it through ImpAdCallHresult with 8 bytes: the source Variant on top; the result temp below +rtBstrFromErrVar,634,unknown,unknown,0,0,,unanalyzed, +rtBoolFromErrVar,635,unknown,unknown,0,0,,unanalyzed, +rtCyFromErrVar,636,unknown,unknown,0,0,,unanalyzed, +rtI2FromErrVar,637,unknown,unknown,0,0,,unanalyzed, +rtI4FromErrVar,638,unknown,unknown,0,0,,unanalyzed, +rtR4FromErrVar,639,unknown,unknown,0,0,,unanalyzed, +rtR8FromErrVar,640,unknown,unknown,0,0,,unanalyzed, +rtUI1FromErrVar,686,unknown,unknown,0,0,,unanalyzed, +TipInvokeMethod2,999,unknown,unknown,0,0,,unanalyzed, +TipInvokeMethod,1016,unknown,unknown,0,0,,unanalyzed, +IID_IVbaHost,1024,unknown,unknown,0,0,,unanalyzed, +EbGetObjConnectionCounts,1025,unknown,unknown,0,0,,unanalyzed, +EbGetVBAObject,2010,unknown,unknown,0,0,,unanalyzed, diff --git a/examples/dump.rs b/examples/dump.rs index 9fd2e6d..251fcde 100644 --- a/examples/dump.rs +++ b/examples/dump.rs @@ -14,15 +14,16 @@ use visualbasic::{ project::MethodLinkKind, vb::{ comreg::{ComRegData, ComRegObject}, + control::ControlKind, eventname, external::ExternalKind, formdata::{FormControlType, FormDataParser}, functype::FuncTypDesc, guitable::{GuiTableEntry, GuiTableIter}, + member::MemberKind, projectinfo2::{ControlTypeIter, ProjectInfo2, read_name_strings}, property::{Property, PropertyValue}, publicbytes::ClassFormPublicBytes, - varstub::VarStubIter, }, }; @@ -340,7 +341,8 @@ fn print_control_types(project: &VbProject<'_>) -> DynResult<()> { return Ok(()); } - // The parameter names between and after the records + // The parameter, event parameter, variable and interface names between + // and after the records let param_names = read_name_strings(map, pi2_va); println!(); @@ -355,7 +357,7 @@ fn print_control_types(project: &VbProject<'_>) -> DynResult<()> { } } if !param_names.is_empty() { - println!(" // Parameter names: {}", param_names.join(", ")); + println!(" // Names: {}", param_names.join(", ")); } println!("}}"); Ok(()) @@ -547,9 +549,21 @@ fn print_object( if ftd_va != 0 { println!(" // FuncTypDescs VA: 0x{ftd_va:08X}"); } - let pn_va = priv_obj.param_names_va()?; - if pn_va != 0 { - println!(" // Param Names VA: 0x{pn_va:08X}"); + let ev_va = priv_obj.event_descs_va()?; + if ev_va != 0 && priv_obj.event_count()? != 0 { + println!(" // Event Descs VA: 0x{ev_va:08X}"); + } + let map = obj.project().address_map(); + for (i, member) in obj.members()? { + let name = String::from_utf8_lossy(member.name(map).unwrap_or(b"")); + match (member.kind()?, member.instance_offset()?) { + (MemberKind::Implements, _) => println!(" // Member {i}: Implements {name}"), + (kind, offset) => println!( + " // Member {i}: {kind:?} {name} As {} (offset 0x{:X})", + member.var_type()?.type_name(), + offset.unwrap_or(0) + ), + } } } @@ -564,25 +578,16 @@ fn print_object( print_variable_table("statics", &table); } - // Print variable implementation stubs (compiler metadata) - if let Some(priv_obj) = obj.private_object() { - let stubs_va = priv_obj.var_stubs_va()?; - let var_count = priv_obj.var_stub_count()?; - if stubs_va != 0 && var_count > 0 { - println!(); - for (i, stub) in - VarStubIter::new(obj.project().address_map(), stubs_va, var_count).enumerate() - { - let name = stub.name(); - let name_str = if name.is_empty() { "?" } else { name }; - let pcount = stub.param_count().unwrap_or(0); - let params = if pcount > 0 { - format!(" ({pcount} params)") - } else { - String::new() - }; - println!(" .varimpl /*{i:02}*/ {name_str}{params}"); - } + // The record layouts of the object's `Type`s + let layouts = obj.record_layouts()?; + if !layouts.is_empty() { + println!(); + for (i, (va, layout)) in layouts.iter().enumerate() { + println!( + " .type /*{i:02}*/ layout=0x{va:08X} size=0x{:X} entries={}", + layout.record_size().unwrap_or(0), + layout.entry_count().unwrap_or(0), + ); } } @@ -673,12 +678,14 @@ fn print_object( } }; - // The library's event names per (control index, sink slot). - let bound_names: HashMap<(u16, u16), &'static str> = obj + // The library's event names per (control name, sink slot): the object + // itself, its WithEvents variables and its Implements all have index + // 0xFFFF. + let bound_names: HashMap<(String, u16), &'static str> = obj .events_all_slots(None) .unwrap_or_default() .into_iter() - .filter_map(|b| Some(((b.control_index, b.event_slot), b.event_name?))) + .filter_map(|b| Some(((b.control_name.into_owned(), b.event_slot), b.event_name?))) .collect(); if !controls.is_empty() { @@ -724,11 +731,15 @@ fn print_object( _ => FormControlType::Unknown(0xFF), } }); + // The templates name a control's events; a WithEvents or + // Implements sink's slots are its interface's. + let templated = ctrl.info().kind().ok() == Some(ControlKind::Control); let ev_name = ctrl .class_name() .and_then(|class| eventname::event_name_for_class(class, slot)) .or_else(|| eventname::event_name(slot, ev_ctype)) - .or_else(|| bound_names.get(&(ctrl_index, slot)).copied()) + .filter(|_| templated) + .or_else(|| bound_names.get(&(ctrl.name().into_owned(), slot)).copied()) .unwrap_or("?"); if let Some(thunk) = sink.resolve_handler_thunk(slot, map) { diff --git a/src/entrypoint.rs b/src/entrypoint.rs index 8cdb114..08f3fa1 100644 --- a/src/entrypoint.rs +++ b/src/entrypoint.rs @@ -28,11 +28,26 @@ //! //! The `DllCanUnloadNow` stub pushes one value (`58 68 50 E9 `). //! +//! The DLL's own entry point is a stub of the same kind that slips two +//! `.data` VAs under the loader's arguments and jumps to the import thunk of +//! the runtime's DLL entry function: +//! +//! ```x86asm +//! pop edx ; 0x5A: the return address +//! push ; 0x68 +//! push ; 0x68 +//! push edx ; 0x52 +//! jmp ; 0xE9 +//! ``` +//! +//! [`entry_stub_target`] reads either entry stub's target. +//! //! [`extract_vb_header_va`] reads the EXE stub and //! [`extract_vb_header_va_from_exports`] the DLL stubs, each accepting only //! a pushed VA that holds the `"VB5!"` magic; //! [`VbProject::from_goblin`](crate::project::VbProject::from_goblin) tries //! the first, then the second. Nothing scans the file for the magic. +//! [`ExportStub`] decodes every export stub. use crate::{addressmap::AddressMap, error::Error}; @@ -45,6 +60,42 @@ const PUSH_IMM32: u8 = 0x68; /// VBHeader magic signature. const VB5_MAGIC: &[u8; 4] = b"VB5!"; +/// Returns the import thunk the entry point stub at `entry_va` transfers +/// to: an executable's `push imm32; call rel32` or a DLL's `pop edx; push +/// imm32; push imm32; push edx; jmp rel32`. `None` for any other code. +pub fn entry_stub_target(map: &AddressMap<'_>, entry_va: u32) -> Option { + let code = map.slice_from_va(entry_va, 10).ok()?; + let (rel, end) = match code { + [0x68, _, _, _, _, 0xE8, r0, r1, r2, r3, ..] => ([*r0, *r1, *r2, *r3], 10u32), + [ + 0x5A, + 0x68, + _, + _, + _, + _, + 0x68, + _, + _, + _, + _, + 0x52, + 0xE9, + r0, + r1, + r2, + r3, + .., + ] => ([*r0, *r1, *r2, *r3], 17), + _ => return None, + }; + Some( + entry_va + .wrapping_add(end) + .wrapping_add(i32::from_le_bytes(rel).cast_unsigned()), + ) +} + /// Extracts the VBHeader virtual address from the PE entry point. /// /// Returns the immediate of the `push imm32` (`0x68`) that begins the entry @@ -105,6 +156,57 @@ pub fn extract_vb_header_va(map: &AddressMap<'_>, entry_point_rva: u32) -> Resul } } +/// One COM export stub of an ActiveX DLL or OCX (see the +/// [module documentation](self)), read by +/// [`VbProject::export_stubs`](crate::project::VbProject::export_stubs). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExportStub { + /// The export's name (`DllGetClassObject`, `DllCanUnloadNow`, ...). + pub name: Option, + /// VA of the stub. + pub va: u32, + /// The VBHeader VA the stub pushes first. + pub vb_header_va: u32, + /// The values it pushes after the VBHeader VA: two `.data` section VAs, + /// none for `DllCanUnloadNow` (`docs`: 0x11003B18 and 0x11003B1C). + pub data_vas: Vec, + /// Target of its `jmp rel32`: the import thunk of the runtime function. + pub target_va: u32, + /// The runtime function that thunk jumps to (`VBDllGetClassObject`, + /// `VBDllCanUnloadNow`, `VBDllRegisterServer`, `VBDllUnRegisterServer`). + pub function: Option, +} + +impl ExportStub { + /// Decodes the stub at `va` whose code starts with `code`: + /// `pop eax`, a `push imm32` per value, `push eax`, `jmp rel32`. + /// `None` if the code is not one. + pub fn decode(code: &[u8], va: u32) -> Option { + let mut rest = code.strip_prefix(&[0x58])?; + let mut pushed = Vec::new(); + while let [PUSH_IMM32, b0, b1, b2, b3, tail @ ..] = rest { + pushed.push(u32::from_le_bytes([*b0, *b1, *b2, *b3])); + rest = tail; + } + let [0x50, 0xE9, r0, r1, r2, r3, ..] = rest else { + return None; + }; + let (&vb_header_va, data_vas) = pushed.split_first()?; + let consumed = code.len().checked_sub(rest.len())?.checked_add(6)?; + let target_va = va + .checked_add(u32::try_from(consumed).ok()?)? + .wrapping_add_signed(i32::from_le_bytes([*r0, *r1, *r2, *r3])); + Some(Self { + name: None, + va, + vb_header_va, + data_vas: data_vas.to_vec(), + target_va, + function: None, + }) + } +} + /// Extracts the VBHeader VA from a VB6 DLL by checking its exports. /// /// The COM exports of a VB6 ActiveX DLL or OCX (`DllGetClassObject`, diff --git a/src/extents.rs b/src/extents.rs new file mode 100644 index 0000000..c662f06 --- /dev/null +++ b/src/extents.rs @@ -0,0 +1,1132 @@ +//! Where the structures this crate reads lie in the image. +//! +//! A VB6 executable keeps its project metadata in the section that holds +//! its code: the VB header, the project and object tables, each object's +//! descriptors, method tables, constant pools and control tables, and every +//! procedure's descriptor with the P-Code before it sit between the native +//! stubs that enter the runtime. A disassembler that does not know where +//! they are decodes them as instructions. [`VbProject::structure_extents`] +//! states each one's extent, measured by the walk that reads it. + +use std::{collections::BTreeSet, mem, ops::Range}; + +use crate::{ + addressmap::AddressMap, + pcode::decoder::{InstructionIterator, PoolEntryRole}, + project::{MethodEntry, MethodLinkKind, VbObject, VbProject}, + util::read_u32_le, + vb::{ + constantpool::{ConstantPool, PoolEntry}, + control::ControlInfo, + controlprop::{ControlPropertyEntry, RecordLayout}, + events::{EventHandlerThunk, EventSinkVtable}, + external::{ExternalComponentEntry, ExternalDeclareInfo}, + functype::{ArgType, FuncTypDesc}, + header::VbHeader, + member::MemberDesc, + object::{ObjectInfo, OptionalObjectInfo, PublicObjectDescriptor}, + objecttable::ObjectTable, + pooldesc::{ArrayDescriptor, CreationDescriptor, IoItems, RecordIoDescriptor}, + privateobj::PrivateObjectDescriptor, + procedure::ProcDscInfo, + projectdata::ProjectData, + projectinfo2::{ProjectInfo2, ProjectInfo2Item, ProjectInfo2Iter}, + publicbytes::ClassFormPublicBytes, + typeref::{InterfaceRef, RecordRef, TypeLibRef}, + }, +}; + +/// What a [`StructureExtent`] holds. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum StructureKind { + /// The VB header ([`VbHeader`]). + VbHeader, + /// The project data ([`ProjectData`]). + ProjectData, + /// The object table ([`ObjectTable`]). + ObjectTable, + /// The [`ProjectInfo2`] header. + ProjectInfo2, + /// An event-source record of the ProjectInfo2 region. + EventSource, + /// A public object descriptor ([`PublicObjectDescriptor`]). + ObjectDescriptor, + /// An [`ObjectInfo`]. + ObjectInfo, + /// An [`OptionalObjectInfo`]. + OptionalObjectInfo, + /// A [`PrivateObjectDescriptor`]. + PrivateObjectDescriptor, + /// An object's method table: a slot per method, a standard module's + /// `Declare` slots, which the compiler leaves unwritten, first. + MethodTable, + /// An object's method link table, the code addresses of its vtable. + MethodLinkTable, + /// Another array of VAs: method names, prototypes, members, events, + /// variable stubs, parameter names, GUIDs or private object descriptors. + PointerTable, + /// A null-terminated ANSI string. + Name, + /// A 16-byte GUID. + Guid, + /// A procedure's or event's prototype ([`FuncTypDesc`]). + FunctionType, + /// A prototype's optional-parameter defaults: their header and values. + OptionalDefaults, + /// A public variable's or `Implements`' descriptor ([`MemberDesc`]). + Member, + /// An object's module-level or `Static` variable table + /// ([`ClassFormPublicBytes`]). + VariableTable, + /// An object's constant pool: its entries' VAs. + ConstantPool, + /// A string constant: a BSTR's length prefix, characters and terminator. + StringConstant, + /// The descriptor `Get #` and `Put #` of a record read + /// ([`RecordIoDescriptor`]), with its nested ones. + RecordIoDescriptor, + /// The item list of a `Print`, `Write` or `Input` ([`IoItems`]), with + /// the byte after it the compiler's length counts. + IoItems, + /// The `SAFEARRAY` header of a fixed array in a record + /// ([`ArrayDescriptor`]). + ArrayDescriptor, + /// The u32 byte length the compiler writes before a blob: a `Declare`'s + /// library and function names, a record I/O descriptor, an item list. + LengthPrefix, + /// What `New` creates when it is not a project class + /// ([`CreationDescriptor`]). + CreationDescriptor, + /// A class's licence key: its byte length and UTF-16 characters. + LicenseKey, + /// A member name a late-bound call passes: UTF-16, NUL-terminated. + MemberName, + /// A record's layout + /// ([`RecordLayout`]): the members + /// to initialize and release. + RecordLayout, + /// A procedure descriptor ([`ProcDscInfo`]) with its cleanup and + /// line-number tables. + ProcedureDescriptor, + /// A procedure's P-Code, the bytecode the runtime interprets. + Pcode, + /// A native procedure's unwind record + /// ([`ProcUnwindInfo`](crate::vb::native::ProcUnwindInfo)). + UnwindRecord, + /// A native procedure's `On Error GoTo` labels + /// ([`ErrorHandlerTable`](crate::vb::native::ErrorHandlerTable)). + ErrorHandlerTable, + /// A native procedure's statement addresses, for `Resume` + /// ([`ResumeTable`](crate::vb::native::ResumeTable)). + ResumeTable, + /// A native procedure's statement line numbers, for `Erl` + /// ([`LineNumberTable`](crate::vb::native::LineNumberTable)). + LineNumberTable, + /// A control table entry ([`ControlInfo`]). + ControlInfo, + /// A control's event sink vtable ([`EventSinkVtable`]). + EventSinkVtable, + /// A control table entry's DISPID map: 8-byte pairs. + DispidMap, + /// A GUI table entry. + GuiTableEntry, + /// A form's or UserControl's form data. + FormData, + /// An entry of the project's external table. + ExternalTable, + /// A `Declare`d function's descriptor, which `DllFunctionCall` reads, + /// with the 8 unread bytes before its call stub. + DeclareDescriptor, + /// A type library reference: an external table entry's, or a + /// [`TypeLibRef`]. + TypeLibReference, + /// An interface reference ([`InterfaceRef`]). + InterfaceReference, + /// A record reference ([`RecordRef`]). + RecordReference, + /// An external table entry's global object reference: a CLSID's VA and + /// the data slot the runtime places the object in. + GlobalObjectReference, + /// A marker that bounds the project's native code: 16 bytes at its + /// start, 4 at its end. Never executed. + CodeMarker, + /// An ActiveX component entry + /// ([`ExternalComponentEntry`]). + Component, + /// The COM registration data + /// ([`ComRegData`](crate::vb::comreg::ComRegData)): its header, records, + /// strings and GUID arrays. + ComRegistration, +} + +impl StructureKind { + /// Returns a short human-readable name, for provenance labels. + #[must_use] + pub const fn label(self) -> &'static str { + match self { + Self::VbHeader => "VB header", + Self::ProjectData => "project data", + Self::ObjectTable => "object table", + Self::ProjectInfo2 => "ProjectInfo2", + Self::EventSource => "event source record", + Self::ObjectDescriptor => "object descriptor", + Self::ObjectInfo => "ObjectInfo", + Self::OptionalObjectInfo => "OptionalObjectInfo", + Self::PrivateObjectDescriptor => "private object descriptor", + Self::MethodTable => "method table", + Self::MethodLinkTable => "method link table", + Self::PointerTable => "pointer table", + Self::Name => "name", + Self::Guid => "GUID", + Self::FunctionType => "prototype", + Self::OptionalDefaults => "optional defaults", + Self::Member => "member descriptor", + Self::VariableTable => "variable table", + Self::ConstantPool => "constant pool", + Self::StringConstant => "string constant", + Self::RecordIoDescriptor => "record I/O descriptor", + Self::IoItems => "Print item list", + Self::ArrayDescriptor => "array descriptor", + Self::LengthPrefix => "length prefix", + Self::CreationDescriptor => "creation descriptor", + Self::LicenseKey => "licence key", + Self::MemberName => "member name", + Self::RecordLayout => "record layout", + Self::ProcedureDescriptor => "procedure descriptor", + Self::Pcode => "P-Code", + Self::UnwindRecord => "unwind record", + Self::ErrorHandlerTable => "error handler table", + Self::ResumeTable => "resume table", + Self::LineNumberTable => "line number table", + Self::ControlInfo => "control info", + Self::EventSinkVtable => "event sink vtable", + Self::DispidMap => "DISPID map", + Self::GuiTableEntry => "GUI table entry", + Self::FormData => "form data", + Self::ExternalTable => "external table entry", + Self::DeclareDescriptor => "Declare descriptor", + Self::TypeLibReference => "type library reference", + Self::InterfaceReference => "interface reference", + Self::RecordReference => "record reference", + Self::GlobalObjectReference => "global object reference", + Self::CodeMarker => "code marker", + Self::Component => "component entry", + Self::ComRegistration => "COM registration data", + } + } +} + +/// The virtual addresses one structure occupies. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StructureExtent { + /// Its half-open virtual-address range. + pub extent: Range, + /// What it is. + pub kind: StructureKind, +} + +impl<'a> VbProject<'a> { + /// Returns the extent of every structure this crate reads in the image, + /// ascending by address. + /// + /// Each is measured by the walk that decodes it, so a structure whose + /// walk does not read is left out rather than guessed at, and so is one + /// not wholly backed by the file (the zero-filled `.data` the runtime + /// writes). Structures several owners share, such as a name or a type + /// library reference, appear once. Native code is never an extent: the + /// method link and event stubs, the `Declare` and import thunks and a + /// native build's procedures stay out. + pub fn structure_extents(&self) -> Vec { + let mut walk = ExtentWalk { + project: self, + out: Vec::new(), + procedures: BTreeSet::new(), + object_infos: BTreeSet::new(), + layouts: BTreeSet::new(), + }; + walk.project_level(); + walk.objects(); + walk.procedures(); + walk.unwind_records(); + let mut out = walk.out; + out.sort_by_key(|structure| (structure.extent.start, structure.extent.end, structure.kind)); + out.dedup(); + out + } +} + +/// The walk behind [`VbProject::structure_extents`]. +struct ExtentWalk<'p> { + /// The project walked. + project: &'p VbProject<'p>, + /// The extents found so far. + out: Vec, + /// Every `ProcDscInfo` VA a structure names, measured last. + procedures: BTreeSet, + /// The VA of every object's `ObjectInfo`: a `ProcDscInfo` points back at + /// its owner's. + object_infos: BTreeSet, + /// Every record layout walked so far. + layouts: BTreeSet, +} + +impl<'p> ExtentWalk<'p> { + /// The project's address map. + fn map(&self) -> &'p AddressMap<'p> { + self.project.address_map() + } + + /// Each native procedure's unwind record and the tables it names. + fn unwind_records(&mut self) { + let map = self.map(); + for procedure in self.project.unwind_records() { + let info = procedure.info; + self.push( + procedure.record_va, + info.size(), + StructureKind::UnwindRecord, + ); + if let (Some(va), Some(table)) = (info.resume_va(), info.resume(map)) { + self.push(va, table.size(), StructureKind::ResumeTable); + } + if let (Some(va), Some(table)) = (info.handlers_va(), info.handlers(map)) { + self.push(va, table.size(), StructureKind::ErrorHandlerTable); + } + if let (Some(va), Some(table)) = (info.lines_va(), info.lines(map)) { + self.push(va, table.size(), StructureKind::LineNumberTable); + } + } + } + + /// Records `len` bytes at `va` as a `kind`, when the file backs every + /// one of them contiguously. Returns whether it was recorded. + fn push(&mut self, va: u32, len: usize, kind: StructureKind) -> bool { + let map = self.map(); + let Some(last) = len + .checked_sub(1) + .and_then(|last| u32::try_from(last).ok()) + .and_then(|last| va.checked_add(last)) + else { + return false; + }; + let contiguous = match (map.va_to_offset(va), map.va_to_offset(last)) { + (Ok(start), Ok(end)) => end.checked_sub(start) == len.checked_sub(1), + _ => false, + }; + if va == 0 || !contiguous { + return false; + } + self.out.push(StructureExtent { + extent: va..last.saturating_add(1), + kind, + }); + true + } + + /// Records the u32 the compiler writes before a blob at `va` as its + /// length, when it equals `len`. + fn length_prefix(&mut self, va: u32, len: usize) { + let Some(prefix) = va.checked_sub(4) else { + return; + }; + let held = self + .map() + .slice_from_va(prefix, 4) + .ok() + .and_then(|data| read_u32_le(data, 0).ok()); + if held.and_then(|held| usize::try_from(held).ok()) == Some(len) { + self.push(prefix, 4, StructureKind::LengthPrefix); + } + } + + /// Records the null-terminated string at `va`. + fn name(&mut self, va: u32) { + if va == 0 || va == u32::MAX { + return; + } + if let Ok(name) = self.project.read_string_at_va(va) { + self.push(va, name.len().saturating_add(1), StructureKind::Name); + } + } + + /// Records the 16-byte GUID at `va`. + fn guid(&mut self, va: u32) { + self.push(va, 16, StructureKind::Guid); + } + + /// Records the array of `count` VAs at `va` as a `kind` and returns its + /// entries; empty when it is not wholly in the file. + fn pointers(&mut self, va: u32, count: usize, kind: StructureKind) -> Vec { + if !self.push(va, count.saturating_mul(4), kind) { + return Vec::new(); + } + let Ok(data) = self.map().slice_from_va(va, count.saturating_mul(4)) else { + return Vec::new(); + }; + (0..count) + .filter_map(|index| read_u32_le(data, index.checked_mul(4)?).ok()) + .collect() + } + + /// The VB header and its strings, the project data, the object table, + /// ProjectInfo2, the external, component and GUI tables, the COM + /// registration data and `Sub Main`'s procedure. + fn project_level(&mut self) { + let project = self.project; + let header_va = project.vb_header_va(); + let header = project.vb_header(); + self.push(header_va, VbHeader::SIZE, StructureKind::VbHeader); + for offset in [ + header.project_description_offset(), + header.project_exe_name_offset(), + header.project_help_file_offset(), + header.project_name_offset(), + ] + .into_iter() + .flatten() + .filter(|&offset| offset != 0) + { + self.name(header_va.wrapping_add(offset)); + } + if let Ok(va) = header.project_data_va() { + self.push(va, ProjectData::SIZE, StructureKind::ProjectData); + } + self.code_markers(); + let table = project.object_table(); + if let Ok(va) = project.project_data().object_table_va() { + self.push(va, ObjectTable::SIZE, StructureKind::ObjectTable); + } + if let Ok(va) = table.project_name_va() { + self.name(va); + } + self.project_info2(); + self.externals(); + self.components(); + for entry in project.gui_entries().into_iter().flatten() { + if let Ok(size) = entry.entry_size() { + self.push(entry.va(), size as usize, StructureKind::GuiTableEntry); + } + if let (Ok(va), Ok(size)) = (entry.form_data_va(), entry.form_data_size()) { + self.push(va, size as usize, StructureKind::FormData); + } + } + if let Some(registration) = project.com_registration() + && let Ok(size) = registration.total_size(self.map()) + { + self.push(registration.base_va(), size, StructureKind::ComRegistration); + } + // `lpSubMain` of a P-Code build is a `mov edx, ` stub. + if let Ok(sub_main) = header.sub_main_va() + && let Ok([0xBA, a, b, c, d, ..]) = self.map().slice_from_va(sub_main, 5) + { + self.procedures.insert(u32::from_le_bytes([*a, *b, *c, *d])); + } + } + + /// The markers at the start and the end of the project's native code + /// ([`ProjectData::code_start_va`], [`ProjectData::code_end_va`]), each + /// taken only where its bytes are the marker. + fn code_markers(&mut self) { + /// The marker at the start: four `E9`, then twelve `CC`. + const START: [u8; 16] = [ + 0xE9, 0xE9, 0xE9, 0xE9, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, + 0xCC, 0xCC, + ]; + /// The marker at the end. + const END: [u8; 4] = [0x9E; 4]; + let data = self.project.project_data(); + let map = self.map(); + for (va, marker) in [ + (data.code_start_va(), &START[..]), + (data.code_end_va(), &END[..]), + ] { + let Ok(va) = va else { + continue; + }; + if map + .slice_from_va(va, marker.len()) + .is_ok_and(|bytes| bytes.starts_with(marker)) + { + self.push(va, marker.len(), StructureKind::CodeMarker); + } + } + } + + /// The ProjectInfo2 header, its private object descriptor array and the + /// event-source records and names after it. + fn project_info2(&mut self) { + let map = self.map(); + let Ok(va) = self.project.object_table().project_info2_va() else { + return; + }; + let Some(header) = map + .slice_from_va(va, ProjectInfo2::HEADER_SIZE) + .ok() + .and_then(|data| ProjectInfo2::parse(data).ok()) + else { + return; + }; + self.push(va, ProjectInfo2::HEADER_SIZE, StructureKind::ProjectInfo2); + if let (Ok(descs), Ok(count)) = ( + header.object_descs_va(), + self.project.object_table().total_objects(), + ) { + self.pointers(descs, usize::from(count), StructureKind::PointerTable); + } + let mut items = ProjectInfo2Iter::new(map, va); + loop { + let start = items.position(); + let Some(item) = items.next() else { + break; + }; + match item { + ProjectInfo2Item::Record(record) => { + self.push(start, ProjectInfo2::ENTRY_SIZE, StructureKind::EventSource); + self.guid(record.guid_data_va); + self.name(record.guid_data_va.wrapping_add(16)); + self.type_library(record.typelib_va); + } + ProjectInfo2Item::Name(_) => self.name(start), + } + } + } + + /// The external table: its entries, the `Declare` descriptors and their + /// strings, and the type library references. + fn externals(&mut self) { + let map = self.map(); + let data = self.project.project_data(); + let (Ok(table_va), Ok(count)) = (data.external_table_va(), data.external_count()) else { + return; + }; + if table_va == 0 { + return; + } + for entry in self.project.externals().into_iter().flatten().flatten() { + let Ok(object_va) = entry.external_object_va() else { + continue; + }; + if let Some(declare) = entry.as_declare(map) { + self.push( + object_va, + ExternalDeclareInfo::BLOCK_SIZE, + StructureKind::DeclareDescriptor, + ); + for va in [declare.library_name_va(), declare.function_name_va()] + .into_iter() + .flatten() + { + self.name(va); + if let Ok(name) = self.project.read_string_at_va(va) { + self.length_prefix(va, name.len().saturating_add(1)); + } + } + } else if let Some(typelib) = entry.as_typelib(map) { + self.push(object_va, 8, StructureKind::GlobalObjectReference); + if let Ok(guid) = typelib.typelib_guid_va() { + self.guid(guid); + } + } + } + self.push( + table_va, + (count as usize).saturating_mul(8), + StructureKind::ExternalTable, + ); + } + + /// The ActiveX component entries of the VB header's external table. + fn components(&mut self) { + let header = self.project.vb_header(); + let (Ok(mut va), Ok(count)) = (header.external_table_va(), header.external_count()) else { + return; + }; + if va == 0 { + return; + } + for _ in 0..count { + let Some(size) = self + .map() + .slice_from_va(va, ExternalComponentEntry::HEADER_SIZE) + .ok() + .and_then(|data| ExternalComponentEntry::parse(data).ok()) + .and_then(|entry| entry.entry_size().ok()) + .filter(|&size| size != 0) + else { + break; + }; + self.push(va, size as usize, StructureKind::Component); + va = va.wrapping_add(size); + } + } + + /// Every object's structures, by its descriptor. + fn objects(&mut self) { + let project = self.project; + let Ok(array_va) = project.object_table().object_array_va() else { + return; + }; + let Ok(objects) = project.objects() else { + return; + }; + let objects: Vec> = objects.flatten().collect(); + self.object_infos = objects + .iter() + .filter_map(|object| object.descriptor().object_info_va().ok()) + .collect(); + for (index, object) in objects.iter().enumerate() { + let descriptor_va = u32::try_from(index) + .ok() + .and_then(|index| index.checked_mul(PublicObjectDescriptor::SIZE as u32)) + .and_then(|offset| array_va.checked_add(offset)); + if let Some(va) = descriptor_va { + self.push( + va, + PublicObjectDescriptor::SIZE, + StructureKind::ObjectDescriptor, + ); + } + self.object(object); + } + } + + /// One object's structures. + fn object(&mut self, object: &VbObject<'p, 'p>) { + let descriptor = object.descriptor(); + if let Ok(va) = descriptor.object_name_va() { + self.name(va); + } + if let (Ok(va), Ok(count)) = (descriptor.method_names_va(), object.method_count()) { + for name in self.pointers(va, usize::from(count), StructureKind::PointerTable) { + self.name(name); + } + } + let Ok(info_va) = descriptor.object_info_va() else { + return; + }; + self.push(info_va, ObjectInfo::SIZE, StructureKind::ObjectInfo); + for (variables, va) in [ + (object.public_bytes(), descriptor.public_bytes_va()), + (object.static_bytes(), descriptor.static_bytes_va()), + ] { + if let (Some(variables), Ok(va)) = (variables, va) + && let Ok(size) = variables.data_size() + { + let size = usize::from(size).max(ClassFormPublicBytes::MIN_SIZE); + self.push(va, size, StructureKind::VariableTable); + self.entries(variables.control_entries()); + } + } + self.method_table(object); + self.constant_pool(object); + if let Some(optional) = object.optional_info() { + self.push( + info_va.wrapping_add(ObjectInfo::SIZE as u32), + OptionalObjectInfo::SIZE, + StructureKind::OptionalObjectInfo, + ); + self.optional(object, optional); + } + if let Some(private) = object.private_object() + && let Ok(va) = object.info().private_object_va() + { + self.push( + va, + PrivateObjectDescriptor::SIZE, + StructureKind::PrivateObjectDescriptor, + ); + self.private(object, private); + } + } + + /// The object's method table, a standard module's `Declare` slots + /// included, and the procedures it names. + fn method_table(&mut self, object: &VbObject<'p, 'p>) { + let Ok(methods) = object.methods() else { + return; + }; + let entries: Vec<_> = methods.collect(); + for entry in entries.iter().flatten() { + if let MethodEntry::PCode(method) = entry { + self.procedures.insert(method.proc_dsc_va()); + } + } + if let Ok(methods_va) = object.info().methods_va() { + self.push( + methods_va, + entries.len().saturating_mul(4), + StructureKind::MethodTable, + ); + } + } + + /// The object's constant pool and the procedures its thunks enter. The + /// entries the P-Code names are measured with its procedures. + fn constant_pool(&mut self, object: &VbObject<'p, 'p>) { + let info = object.info(); + let (Ok(va), Ok(count)) = (info.constants_va(), info.constants_count()) else { + return; + }; + if count == 0 { + return; + } + self.push( + va, + usize::from(count).saturating_mul(4), + StructureKind::ConstantPool, + ); + let Ok(pool) = object.constants_pool() else { + return; + }; + for (_, entry) in pool.entries(count) { + if let Ok(PoolEntry::Procedure { proc_dsc_va }) = entry { + self.procedures.insert(proc_dsc_va); + } + } + } + + /// The structures an [`OptionalObjectInfo`] points at: the CLSID, the + /// GUID tables, the method link table and the control table. + fn optional(&mut self, object: &VbObject<'p, 'p>, optional: &OptionalObjectInfo<'p>) { + if let Ok(va) = optional.object_clsid_va() { + self.guid(va); + } + if let Some(va) = object.interface_iid_va() { + self.guid(va); + } + for (table, count) in [ + (optional.gui_guid_table_va(), optional.gui_guids_count()), + ( + optional.default_iid_table_va(), + optional.default_iid_count(), + ), + (optional.events_iid_table_va(), optional.events_iid_count()), + ] { + let (Ok(table), Ok(count)) = (table, count) else { + continue; + }; + for guid in self.pointers(table, count as usize, StructureKind::PointerTable) { + self.guid(guid); + } + } + if let (Ok(va), Ok(count)) = ( + optional.method_link_table_va(), + optional.method_link_count(), + ) { + self.pointers(va, usize::from(count), StructureKind::MethodLinkTable); + } + let pool = object.info().constants_va().ok(); + for link in object.method_links().into_iter().flatten().flatten() { + match link.kind { + MethodLinkKind::Procedure { proc_dsc_va } => { + self.procedures.insert(proc_dsc_va); + } + // A `New` variable's accessor names what to create by its + // index in the object's constant pool. + MethodLinkKind::Variable { + pushed: Some([index, pushed_pool]), + .. + } if Some(pushed_pool) == pool => { + if let Ok(entry) = object + .constants_pool() + .and_then(|pool| pool.va_at(u16::try_from(index).unwrap_or(u16::MAX))) + { + self.creation(entry); + } + } + _ => {} + } + } + let (Ok(controls_va), Ok(count)) = (optional.controls_va(), optional.control_count()) + else { + return; + }; + for index in 0..count { + let Some(va) = index + .checked_mul(ControlInfo::MIN_SIZE as u32) + .and_then(|offset| controls_va.checked_add(offset)) + else { + break; + }; + self.control(va); + } + } + + /// One control table entry, its name, GUID and event sink vtable, and + /// the procedures its event stubs enter. + fn control(&mut self, va: u32) { + let map = self.map(); + let Some(info) = map + .slice_from_va(va, ControlInfo::MIN_SIZE) + .ok() + .and_then(|data| ControlInfo::parse(data).ok()) + else { + return; + }; + self.push(va, ControlInfo::MIN_SIZE, StructureKind::ControlInfo); + if let Ok(name) = info.name_va() { + self.name(name); + } + if let Ok(guid) = info.guid_va() { + self.guid(guid); + } + if let (Ok(count), Ok(table)) = (info.dispid_map_count(), info.dispid_map_va()) { + self.push( + table, + usize::from(count).saturating_mul(8), + StructureKind::DispidMap, + ); + } + let Ok(sink_va) = info.event_sink_vtable_va() else { + return; + }; + let Some(sink) = map + .slice_from_va(sink_va, EventSinkVtable::HEADER_SIZE) + .ok() + .and_then(|data| EventSinkVtable::parse(data, &info).ok()) + else { + return; + }; + self.push(sink_va, sink.size(), StructureKind::EventSinkVtable); + for (_, handler) in sink.connected_handlers() { + if let Some(thunk) = map + .slice_from_va(handler, EventHandlerThunk::SIZE) + .ok() + .and_then(|data| EventHandlerThunk::parse_from_event_entry(data, handler)) + { + self.procedures.insert(thunk.proc_dsc_info_va); + } + } + } + + /// The structures a [`PrivateObjectDescriptor`] points at: the + /// prototypes of the procedures and events, the members and the + /// record layouts of the object's `Type`s. + fn private(&mut self, object: &VbObject<'p, 'p>, private: &PrivateObjectDescriptor<'p>) { + if let (Ok(va), Ok(count)) = (private.func_type_descs_va(), object.method_count()) { + self.prototypes(va, usize::from(count)); + } + if let (Ok(va), Ok(count)) = (private.event_descs_va(), private.event_count()) { + self.prototypes(va, usize::from(count)); + } + if let (Ok(va), Ok(count)) = (private.member_descs_va(), private.member_count()) { + for member in self.pointers(va, usize::from(count), StructureKind::PointerTable) { + self.member(member); + } + } + if let (Ok(va), Ok(count)) = (private.record_layouts_va(), private.record_layout_count()) { + for layout in self.pointers(va, usize::from(count), StructureKind::PointerTable) { + self.record_layout(layout); + } + } + } + + /// The record layout at `va`, measured by its own size field, and what + /// its entries name. Each layout is walked once. + fn record_layout(&mut self, va: u32) { + if !self.layouts.insert(va) { + return; + } + let Ok(layout) = RecordLayout::at(self.map(), va) else { + return; + }; + if let Ok(size) = layout.size() { + self.push(va, usize::from(size), StructureKind::RecordLayout); + } + self.entries(layout.entries()); + } + + /// What variable, cleanup or record layout entries name: the layouts of + /// records and of record arrays' elements, and the IID of a fixed + /// array of objects. + fn entries(&mut self, entries: impl Iterator>) { + let entries: Vec> = entries.collect(); + for entry in entries { + for layout in [entry.record_layout_va(), entry.element_layout_va()] + .into_iter() + .flatten() + { + self.record_layout(layout); + } + if let Some(iid) = entry.safearray_iid_va() { + self.guid(iid); + } + } + } + + /// The array of `count` prototypes at `va`, each prototype with its + /// parameter names, optional defaults and type references. + fn prototypes(&mut self, va: u32, count: usize) { + for desc_va in self.pointers(va, count, StructureKind::PointerTable) { + let Some(desc) = self + .map() + .slice_from_va(desc_va, FuncTypDesc::MIN_SIZE) + .ok() + .and_then(|data| FuncTypDesc::parse(data).ok()) + else { + continue; + }; + if let Some(size) = desc.size() { + self.push(desc_va, size, StructureKind::FunctionType); + } + if let Ok(names) = desc.param_names_va() { + for name in self.pointers( + names, + usize::from(desc.entry_count()), + StructureKind::PointerTable, + ) { + self.name(name); + } + } + self.optional_defaults(&desc); + for arg in desc.arg_types().into_iter().chain(desc.return_type()) { + self.type_reference(arg); + } + } + } + + /// A prototype's optional-parameter defaults: the 8-byte header (the + /// values' size and VA) and the values. + fn optional_defaults(&mut self, desc: &FuncTypDesc<'_>) { + let Ok(header_va) = desc.optional_defaults_va() else { + return; + }; + if !self.push(header_va, 8, StructureKind::OptionalDefaults) { + return; + } + let Ok(header) = self.map().slice_from_va(header_va, 8) else { + return; + }; + if let (Ok(size), Ok(values_va)) = (read_u32_le(header, 0), read_u32_le(header, 4)) { + self.push(values_va, size as usize, StructureKind::OptionalDefaults); + } + } + + /// One member descriptor, its name and its type's reference. + fn member(&mut self, va: u32) { + let Some(member) = self + .map() + .slice_from_va(va, MemberDesc::MIN_SIZE) + .ok() + .and_then(|data| MemberDesc::parse(data).ok()) + else { + return; + }; + if let Ok(size) = member.size() { + self.push(va, size, StructureKind::Member); + } + if let Ok(name) = member.name_va() { + self.name(name); + } + if let Ok(arg) = member.var_type() { + self.type_reference(arg); + } + } + + /// The reference an interface or record type carries, with its library. + /// A class type's descriptor is the class's `ObjectInfo`, measured with + /// its object. + fn type_reference(&mut self, arg: ArgType) { + let Some(va) = arg.descriptor_va() else { + return; + }; + let map = self.map(); + match arg.base_type() { + ArgType::IUNKNOWN | ArgType::INTERFACE => { + let Ok(reference) = InterfaceRef::at(map, va) else { + return; + }; + self.push(va, InterfaceRef::SIZE, StructureKind::InterfaceReference); + if let Ok(iid) = reference.iid_va() { + self.guid(iid); + } + if let Ok(typelib) = reference.typelib_va() { + self.type_library(typelib); + } + } + ArgType::RECORD => { + let Ok(reference) = RecordRef::at(map, va) else { + return; + }; + self.push(va, RecordRef::SIZE, StructureKind::RecordReference); + for guid in [reference.libid_va(), reference.guid_va()] + .into_iter() + .flatten() + { + self.guid(guid); + } + } + _ => {} + } + } + + /// A [`TypeLibRef`], its GUID, path and name. + fn type_library(&mut self, va: u32) { + if va == 0 { + return; + } + let Ok(typelib) = TypeLibRef::at(self.map(), va) else { + return; + }; + self.push(va, TypeLibRef::SIZE, StructureKind::TypeLibReference); + if let Ok(guid) = typelib.guid_va() { + self.guid(guid); + } + for name in [typelib.path_va(), typelib.name_va()].into_iter().flatten() { + self.name(name); + } + } + + /// Every procedure a structure named: its descriptor and the P-Code + /// before it. A VA whose descriptor does not point back at one of the + /// project's objects is not a procedure of the project. + fn procedures(&mut self) { + let map = self.map(); + let procedures = mem::take(&mut self.procedures); + for va in procedures { + let Some(desc) = map + .slice_from_va(va, ProcDscInfo::MIN_SIZE) + .ok() + .and_then(|data| ProcDscInfo::parse(data).ok()) + else { + continue; + }; + let owned = desc + .object_info_va() + .is_ok_and(|owner| self.object_infos.contains(&owner)); + let (Ok(pcode), Ok(size)) = (desc.proc_size(), desc.extent_size()) else { + continue; + }; + if !owned || pcode == 0 { + continue; + } + let pcode_va = va.wrapping_sub(u32::from(pcode)); + self.push(pcode_va, usize::from(pcode), StructureKind::Pcode); + self.push(va, size, StructureKind::ProcedureDescriptor); + self.entries(desc.cleanup_entries()); + if let Some(secondary) = desc.secondary_table() { + self.entries(secondary.entries()); + } + if let (Ok(owner), Ok(code)) = ( + desc.object_info_va(), + map.slice_from_va(pcode_va, usize::from(pcode)), + ) { + self.pool_entries(owner, code, pcode); + } + } + } + + /// What a `New` names when it is not a project class (whose + /// `ObjectInfo` is measured with its object): the creation descriptor, + /// the CLSID, the IID and the licence key. + fn creation(&mut self, va: u32) { + let Some(descriptor) = CreationDescriptor::at(self.map(), va) else { + return; + }; + self.push( + va, + CreationDescriptor::SIZE, + StructureKind::CreationDescriptor, + ); + for guid in [descriptor.clsid_va(), descriptor.iid_va()] + .into_iter() + .flatten() + { + self.guid(guid); + } + if let Ok(key) = descriptor.license_va() + && key != 0 + && let Some(len) = self + .map() + .slice_from_va(key, 4) + .ok() + .and_then(|data| read_u32_le(data, 0).ok()) + { + self.push( + key, + (len as usize).saturating_add(4), + StructureKind::LicenseKey, + ); + } + } + + /// The constant pool entries a procedure's P-Code names, each measured + /// as what its opcode says it holds. Entries no opcode types (an + /// address, a `Print` item descriptor) are left out. + fn pool_entries(&mut self, owner: u32, code: &'p [u8], size: u16) { + let map = self.map(); + let Some(pool_va) = map + .slice_from_va(owner, ObjectInfo::SIZE) + .ok() + .and_then(|data| ObjectInfo::parse(data).ok()) + .and_then(|info| info.constants_va().ok()) + else { + return; + }; + let pool = ConstantPool::new(map, pool_va); + let references: BTreeSet<(u16, PoolEntryRole)> = InstructionIterator::new(code, size) + .flatten() + .flat_map(|instruction| instruction.pool_references(code)) + .map(|reference| (reference.index, reference.role)) + .collect(); + for (index, role) in references { + let Ok(va) = pool.va_at(index) else { + continue; + }; + match role { + PoolEntryRole::String => { + if let Ok(Some(string)) = pool.string_at(index) { + self.push( + string.length_prefix_va(), + string.total_binary_size(), + StructureKind::StringConstant, + ); + } + } + PoolEntryRole::MemberName => { + let units = map.slice_from_va(va, 2).ok().and_then(|data| { + data.as_chunks::<2>() + .0 + .iter() + .take(ConstantPool::MAX_NAME_CHARS) + .position(|&unit| unit == [0, 0]) + }); + if let Some(units) = units { + self.push( + va, + units.saturating_add(1).saturating_mul(2), + StructureKind::MemberName, + ); + } + } + PoolEntryRole::Guid => self.guid(va), + PoolEntryRole::RecordLayout => self.record_layout(va), + PoolEntryRole::RecordIo => { + if let Some(size) = RecordIoDescriptor::at(map, va) + .ok() + .and_then(|descriptor| descriptor.extent_size()) + { + self.push(va, size, StructureKind::RecordIoDescriptor); + self.length_prefix(va, size); + } + } + // The compiler's length counts a byte after the items, which + // the runtime does not read. + PoolEntryRole::IoItems => { + if let Ok(items) = IoItems::at(map, va) { + let size = items.size().saturating_add(1); + self.push(va, size, StructureKind::IoItems); + self.length_prefix(va, size); + } + } + PoolEntryRole::ArrayDescriptor => { + if let Ok(array) = ArrayDescriptor::at(map, va) { + self.push(va, array.size(), StructureKind::ArrayDescriptor); + } + } + PoolEntryRole::Creation => self.creation(va), + PoolEntryRole::Address => {} + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index c36afd9..f55b6ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,8 @@ //! internal format (VBHeader, ProjectData, ObjectTable, etc.). //! - **P-Code decoding** ([`pcode`]): Opcode tables, operand types, and a streaming //! instruction iterator. +//! - **Extents** ([`extents`]): Where each structure the crate reads lies in +//! the image, for a disassembler that must not decode it. //! - **Imports** ([`imports::ImportTable`]): The PE import table by address //! table slot, naming the runtime functions P-Code calls by ordinal. //! - **High-level API** ([`VbProject`]): Ties everything together into a convenient @@ -141,6 +143,7 @@ pub mod addressmap; pub mod entrypoint; pub mod error; +pub mod extents; pub mod imports; pub mod pcode; pub mod project; diff --git a/src/pcode/calltarget.rs b/src/pcode/calltarget.rs index 90b6288..9b82f86 100644 --- a/src/pcode/calltarget.rs +++ b/src/pcode/calltarget.rs @@ -16,7 +16,7 @@ //! | `ThisVCall*` | `Me`'s object (the calling object): the method at the vtable offset | //! | `VCall*` (`%2`) | the receiver's class is not in the instruction: [`Callee::Unknown`]; a caller that knows Pr's source asks [`CallResolver::resolve_with_pr`] (Pr loaded by `FLdPrThis` is `Me`), one that knows its class asks [`CallResolver::resolve_vtable`] | //! | `ImpAdCall*` (`%x`) | the pool entry: a procedure thunk (module procedure, `Friend` method), a `Declare` stub or an import thunk; the operand's byte count is exact either way | -//! | `Late*` | the member name (pool, [`ConstantPool::name_at`]) or the DISPID, and the argument count operand | +//! | `Late*` | the member name (pool, [`ConstantPool::name_at`]) or the DISPID, and the argument count operand; a `LateId*` on a receiver whose class the frame slots know reaches the member its DISPID names ([`LateTarget`], [`CallResolver::resolve_with_slots`]) | //! //! # The vtable of a project object //! @@ -72,6 +72,20 @@ //! return pointer; `LBound` 0x44, `UBound` 0x48 and `Count` 0x4C the return //! pointer). //! +//! # Hosted controls +//! +//! A form's getter for a control the project hosts (an ActiveX control, or +//! one of the project's own UserControls) returns the control's extender: +//! the compiler calls it late, by DISPID. The control's form record names +//! its class by ProgID, and [`VbProject::components`] gives the class's +//! default interface, which the getter's result is typed with +//! ([`CallResolver::returned_interface`]). A DISPID of the class names one +//! of its members: a UserControl's procedure by its prototype's +//! [`FuncTypDesc::dispid`], an ActiveX control's member through the +//! caller's catalog ([`InterfaceCatalog::dispatched`]). The extender's own +//! members are called 0x3000 below the DISPIDs `_VBControlExtender` +//! declares ([`RuntimeInterfaces::extender_member`]). +//! //! [`Instruction::stack_effect`]: super::decoder::Instruction::stack_effect use std::{ @@ -96,7 +110,7 @@ use crate::{ constantpool::{ConstantPool, PoolEntry}, control::Guid, exports::VbParamType, - functype::{ArgType, FuncTypDesc}, + functype::{ArgType, FuncTypDesc, PropertyKind}, }, }; @@ -205,6 +219,10 @@ pub enum Callee { name: Option, /// The member's DISPID (`LateId*`). dispid: Option, + /// The member the DISPID names, when the receiver's class is known + /// ([`CallResolver::resolve_with_slots`]). The call still goes + /// through `IDispatch::Invoke`, its arguments Variants by value. + target: Option, }, /// The instruction does not say (`VCall*` on Pr of unknown interface). Unknown { @@ -214,6 +232,35 @@ pub enum Callee { }, } +/// The member a late-bound call by DISPID reaches ([`Callee::Late`]). +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LateTarget { + /// A procedure of the project: a public method or property procedure + /// of one of its classes or UserControls, whose + /// [`FuncTypDesc::dispid`] is the call's. + Procedure { + /// Index of the object. + object: u16, + /// Index of the method in the object's method table. + method: u16, + /// How the call invokes it. + invoke: InvokeKind, + }, + /// Members of an external interface the [`InterfaceCatalog`] describes + /// ([`InterfaceCatalog::dispatched`]): a hosted control's own, or its + /// extender's (`_VBControlExtender`, + /// [`RuntimeInterfaces::extender_member`]). Each is invoked the way the + /// call invokes it. + Members { + /// The interface's IID. + iid: Guid, + /// The member's DISPID in that interface. + dispid: i32, + /// The member, under each name the catalog gives the DISPID. + members: Vec, + }, +} + /// What a call opcode passes and gets back. #[derive(Debug, Clone)] pub struct CallSignature<'a> { @@ -269,8 +316,8 @@ struct ObjectFacts<'a> { methods: Vec>, /// Method index by vtable offset, for the methods whose slot is known. by_vtable: HashMap, - /// Control getters by vtable offset: the control's index and name. - controls: HashMap, + /// Control getters by vtable offset. + controls: HashMap, /// Member variable accessors by vtable offset: the variable's instance /// offset and the runtime function. variables: HashMap, @@ -281,6 +328,24 @@ struct ObjectFacts<'a> { info_va: u32, } +/// The getter a form's or UserControl's vtable has for one of its controls. +#[derive(Debug, Clone, Default)] +struct ControlGetter { + /// The control's index. + index: u16, + /// Its name. + name: String, + /// The default interface of a hosted control's class, the class its + /// form record names ([`FormControlRecord::prog_id`]); `None` for an + /// intrinsic control and a control array. + /// + /// [`FormControlRecord::prog_id`]: crate::vb::formdata::FormControlRecord::prog_id + interface: Option, + /// For a control array of a hosted class, that class's default + /// interface: the array's `Item` returns an element. + elements: Option, +} + /// The interfaces a procedure's own code states for the objects in its frame /// slots, which a `VCall*` without an IID needs. /// @@ -316,6 +381,10 @@ pub struct SlotInterfaces { /// Each store into a frame slot, by slot and P-Code offset, with the /// interface of what it stores when known. stores: BTreeMap>>, + /// The interface of the elements of the control array a slot holds + /// ([`CallResolver::returned_elements`]); `None` for a slot two arrays + /// of different classes pass through. + elements: HashMap>, } /// What one store into a frame slot stores. @@ -404,9 +473,11 @@ impl SlotInterfaces { last_load = Some(source); } let call = resolver.resolve_with_slots(object, insn, pr.as_ref(), known); - let returned = call - .as_ref() - .and_then(|call| resolver.returned_interface(call)); + let returned = call.as_ref().and_then(|call| { + resolver + .returned_interface(call) + .or_else(|| known.element_at(call, pr.as_ref())) + }); // A call that returns through its [retval] pointer fills the slot // whose address it consumes last (pushed first). if let (Some(iid), Some(call)) = (returned, call.as_ref()) @@ -433,6 +504,21 @@ impl SlotInterfaces { let Some(target) = frame_operand(next).filter(|_| is_store(next)) else { continue; }; + if insn.info.pushes != 0 + && let Some(class) = call + .as_ref() + .and_then(|call| resolver.returned_elements(call)) + { + found + .elements + .entry(target) + .and_modify(|known| { + if *known != Some(class) { + *known = None; + } + }) + .or_insert(Some(class)); + } let stored = match insn.info.mnemonic { "CastAd" => match insn.operands.first() { Some(Some(Operand::ConstPoolIndex(entry))) => pool @@ -572,6 +658,25 @@ impl SlotInterfaces { found } + /// The element a control array's `Item` returns when `call` is that + /// `Item` ([`RuntimeInterfaces::CONTROL_ARRAY_ITEM`]) on an array held + /// in the frame slot Pr was loaded from. + fn element_at(&self, call: &CallSignature<'_>, pr: Option<&PrSource>) -> Option { + let Callee::Interface { + iid, vtable_offset, .. + } = &call.callee + else { + return None; + }; + let Some(PrSource::Frame { offset }) = pr else { + return None; + }; + if iid.bytes != [0; 16] || *vtable_offset != RuntimeInterfaces::CONTROL_ARRAY_ITEM { + return None; + } + self.elements.get(offset).copied().flatten() + } + /// Records `iid` for slot `offset`; a second, different one makes the /// slot conflicting. fn add(&mut self, offset: i16, iid: Guid) { @@ -674,6 +779,10 @@ pub struct CallResolver<'a, 'p> { by_proc_dsc: HashMap, /// The descriptions of external interfaces, if supplied. interfaces: Option>, + /// The default interfaces of the classes the project hosts as controls + /// ([`VbProject::components`]): an object of one, reached through its + /// control getter, is the control's extender. + hosted: Vec, } /// How a member of an interface is invoked. @@ -689,6 +798,44 @@ pub enum InvokeKind { Set, } +impl InvokeKind { + /// How the late-bound call `mnemonic` invokes its member: a store + /// (`LateIdSt`, `LateIdCallSt`, `LateMemSt` ...) lets a property, a + /// store of an object (`*StAd`) sets one, every other form (`LateIdCall`, + /// `LateIdLdVar`, `LateIdCallLdVar` ...) calls a method or gets a + /// property, which [`admits`](Self::admits) both stand for. + pub fn of_late_call(mnemonic: &str) -> Self { + if mnemonic.ends_with("StAd") { + Self::Set + } else if mnemonic.ends_with("St") { + Self::Let + } else { + Self::Method + } + } + + /// The prefix COM gives the function of a property accessor invoked + /// this way (`get_`, `put_`, `putref_`); none for a method. + pub fn accessor_prefix(self) -> &'static str { + match self { + Self::Method => "", + Self::Get => "get_", + Self::Let => "put_", + Self::Set => "putref_", + } + } + + /// Returns whether a call invoking this way reaches a member invoked + /// as `member`: the same kind, or for a call, a method or a property + /// get ([`of_late_call`](Self::of_late_call)). + pub fn admits(self, member: Self) -> bool { + match self { + Self::Method => matches!(member, Self::Method | Self::Get), + _ => self == member, + } + } +} + /// A member of an external interface, as its type library describes it. #[derive(Debug, Clone, PartialEq, Eq)] pub struct InterfaceMember { @@ -727,6 +874,14 @@ pub trait InterfaceCatalog { /// property's `Let` and `Set`, a `_Default` alias), all with the same /// arguments. Empty when the interface or the slot is unknown. fn members(&self, iid: &Guid, vtable_offset: u16) -> Vec; + + /// Returns the members of interface `iid` with DISPID `dispid`, which a + /// late-bound call names it by: a property's get and its let or set + /// share one. Empty when the interface or the DISPID is unknown, which + /// it is unless the catalog overrides this. + fn dispatched(&self, _iid: &Guid, _dispid: i32) -> Vec { + Vec::new() + } } /// The runtime's interfaces that no type library describes, built into the @@ -749,6 +904,9 @@ impl RuntimeInterfaces { /// The name given to the control-array object's interface. pub const CONTROL_ARRAY: &'static str = "ControlArray"; + /// The vtable offset of the control-array object's `Item`. + pub const CONTROL_ARRAY_ITEM: u16 = 0x40; + /// The VBA library's `_ErrObject` interface, /// `{A4C466B8-499F-101B-BB78-00AA00383CBB}` (`Err.Raise` at 0x44, /// `Err.Clear` at 0x48). @@ -759,6 +917,48 @@ impl RuntimeInterfaces { ], }; + /// VB's `_VBControlExtender` interface, + /// `{164CBDD0-7321-11D1-A1E8-00A0C90F2731}` (`VB6.OLB`): the properties + /// and methods the extender of a hosted control adds to the control's + /// own (`Name`, `Left`, `Visible`, `Tag`, `SetFocus`, `Move`, ...). + pub const CONTROL_EXTENDER: Guid = Guid { + bytes: [ + 0xD0, 0xBD, 0x4C, 0x16, 0x21, 0x73, 0xD1, 0x11, 0xA1, 0xE8, 0x00, 0xA0, 0xC9, 0x0F, + 0x27, 0x31, + ], + }; + + /// The first DISPID the compiler calls an extender member by. + const EXTENDER_FIRST: u32 = 0x8001_0000; + + /// One past the last DISPID the compiler calls an extender member by. + const EXTENDER_END: u32 = 0x8001_2000; + + /// How far above the DISPID a call names an extender member by + /// `_VBControlExtender` declares it. + const EXTENDER_SHIFT: u32 = 0x3000; + + /// Returns the DISPID [`CONTROL_EXTENDER`](Self::CONTROL_EXTENDER) + /// declares for the extender member a late-bound call on a hosted + /// control names by `dispid`, or `None` for a DISPID outside the + /// extender's. + /// + /// The compiler calls the extender's members 0x3000 below the DISPIDs + /// the interface declares: its properties from 0x80010000 (`Name`, + /// declared 0x80013000), its methods from 0x80011000 (`SetFocus`, + /// declared 0x80014000). Measured on every extender member + /// `tests/fixtures/dispid` calls: the properties `Name`, `Left`, `Top`, + /// `Width`, `Height`, `Visible`, `Parent`, `DragMode`, `Tag`, + /// `TabIndex`, `object`, `HelpContextID`, `WhatsThisHelpID`, + /// `Container`, `CausesValidation` and `ToolTipText`, and the methods + /// `SetFocus`, `ZOrder`, `Move`, `Drag` and `ShowWhatsThis`. + pub fn extender_member(dispid: i32) -> Option { + let raw = dispid.cast_unsigned(); + (Self::EXTENDER_FIRST..Self::EXTENDER_END) + .contains(&raw) + .then(|| raw.wrapping_add(Self::EXTENDER_SHIFT).cast_signed()) + } + /// Returns the interface of the object a runtime export returns, for /// the exports that return one fixed interface: `rtcErrObj` (`Err`) /// returns `_ErrObject` (`tests/fixtures/flow`: its `VCallHresult`s on @@ -774,7 +974,7 @@ impl InterfaceCatalog for RuntimeInterfaces { return Vec::new(); } let (name, invoke, arg_widths) = match vtable_offset { - 0x40 => ("Item", InvokeKind::Get, vec![1, 1]), + Self::CONTROL_ARRAY_ITEM => ("Item", InvokeKind::Get, vec![1, 1]), 0x44 => ("LBound", InvokeKind::Get, vec![1]), 0x48 => ("UBound", InvokeKind::Get, vec![1]), 0x4C => ("Count", InvokeKind::Get, vec![1]), @@ -804,12 +1004,23 @@ impl<'a, 'p: 'a> CallResolver<'a, 'p> { /// /// Returns an error if the project's object table cannot be read. pub fn new(project: &'p VbProject<'a>) -> Result { + // The hosted classes' default interfaces, by ProgID. + let classes: HashMap = project + .components() + .map(|components| { + components + .filter_map(|component| { + Some((component.prog_id().into_owned(), component.default_iid()?)) + }) + .collect() + }) + .unwrap_or_default(); let mut objects = Vec::new(); let mut by_proc_dsc = HashMap::new(); for (object_index, object) in project.objects()?.enumerate() { let object_index = u16::try_from(object_index).unwrap_or(u16::MAX); objects.push(match object { - Ok(object) => Self::object_facts(&object, object_index, &mut by_proc_dsc), + Ok(object) => Self::object_facts(&object, object_index, &classes, &mut by_proc_dsc), Err(_) => ObjectFacts::default(), }); } @@ -818,6 +1029,7 @@ impl<'a, 'p: 'a> CallResolver<'a, 'p> { objects, by_proc_dsc, interfaces: None, + hosted: classes.into_values().collect(), }) } @@ -862,9 +1074,12 @@ impl<'a, 'p: 'a> CallResolver<'a, 'p> { } /// Collects one object's facts, recording its methods' ProcDscInfo VAs. + /// `classes` gives the default interface of each class the project + /// hosts as a control, by ProgID. fn object_facts( object: &VbObject<'a, 'p>, object_index: u16, + classes: &HashMap, by_proc_dsc: &mut HashMap, ) -> ObjectFacts<'a> { let mut iids: Vec = object.default_iids().map(|(_, guid)| guid).collect(); @@ -930,18 +1145,48 @@ impl<'a, 'p: 'a> CallResolver<'a, 'p> { .flatten() }) }); + // A hosted control's record names its class. A control array has a + // record per element, each with its array index, and its getter + // returns the array. + let mut hosted: HashMap, bool)> = HashMap::new(); if let Some(form_data) = form_data { for record in form_data.controls() { let index = u16::from(record.cid()); if !named.iter().any(|(known, _)| *known == index) { named.push((index, record.name().into_owned())); } + let class = record + .prog_id() + .and_then(|prog_id| classes.get(prog_id.as_ref()).copied()); + let array = record.array_index().is_some(); + hosted + .entry(index) + .and_modify(|(known, in_array)| { + if *known != class { + *known = None; + } + *in_array |= array; + }) + .or_insert((class, array)); } } if let Some((_, size)) = builtin { for (index, name) in named { if let Some(offset) = index.checked_mul(4).and_then(|o| o.checked_add(size)) { - controls.insert(offset, (index, name)); + let (interface, elements) = match hosted.get(&index) { + Some(&(class, false)) => (class, None), + Some(&(class, true)) => (None, class), + None => (None, None), + }; + controls.insert( + offset, + ControlGetter { + index, + name, + interface, + elements, + }, + ); } } } @@ -1072,7 +1317,9 @@ impl<'a, 'p: 'a> CallResolver<'a, 'p> { .map(|link| match link.map(|link| link.kind) { Ok(MethodLinkKind::Empty) => Link::Empty, Ok(MethodLinkKind::Procedure { proc_dsc_va }) => Link::Procedure(proc_dsc_va), - Ok(MethodLinkKind::Variable { offset, thunk_va }) => Link::Variable { + Ok(MethodLinkKind::Variable { + offset, thunk_va, .. + }) => Link::Variable { offset, function: function(thunk_va), }, @@ -1154,12 +1401,12 @@ impl<'a, 'p: 'a> CallResolver<'a, 'p> { if let Some(&method) = facts.by_vtable.get(&offset) { return self.procedure(object, method); } - if let Some((index, name)) = facts.controls.get(&offset) { + if let Some(getter) = facts.controls.get(&offset) { return CallSignature { callee: Callee::Control { object, - index: *index, - name: name.clone(), + index: getter.index, + name: getter.name.clone(), }, arg_slots: Some(0), arg_widths: Some(Vec::new()), @@ -1265,16 +1512,27 @@ impl<'a, 'p: 'a> CallResolver<'a, 'p> { /// Returns the interface of the object a resolved call returns: a /// catalog member's [`InterfaceMember::returns_interface`], a project /// method whose prototype returns one of the project's classes (that - /// class's default interface), or a runtime export with a fixed object - /// return ([`RuntimeInterfaces::import_return`]). + /// class's default interface), a runtime export with a fixed object + /// return ([`RuntimeInterfaces::import_return`]), or the getter of a + /// hosted control (its class's default interface: the getter returns + /// the control's extender, whose members late-bound calls reach, + /// [`resolve_with_slots`](Self::resolve_with_slots)). pub fn returned_interface(&self, call: &CallSignature<'_>) -> Option { match &call.callee { Callee::Interface { members, .. } => members.first()?.returns_interface, Callee::Import { function, .. } => RuntimeInterfaces::import_return(function), + Callee::Control { object, index, .. } => { + self.objects + .get(usize::from(*object))? + .controls + .values() + .find(|getter| getter.index == *index)? + .interface + } Callee::Procedure { .. } => { let returns = call.signature.as_ref()?.return_type()?; let info_va = returns - .object_va() + .descriptor_va() .filter(|_| returns.code() & 0x5F == 0x13)?; self.objects .iter() @@ -1287,10 +1545,28 @@ impl<'a, 'p: 'a> CallResolver<'a, 'p> { } } + /// Returns the interface of the elements of the control array a + /// resolved call returns: the getter of a control array of a hosted + /// class gives that class's default interface, which the array's `Item` + /// returns ([`SlotInterfaces`] carries it from the slot holding the + /// array to the one `Item` writes). + pub fn returned_elements(&self, call: &CallSignature<'_>) -> Option { + let Callee::Control { object, index, .. } = &call.callee else { + return None; + }; + self.objects + .get(usize::from(*object))? + .controls + .values() + .find(|getter| getter.index == *index)? + .elements + } + /// Resolves `instruction` like [`resolve_with_pr`](Self::resolve_with_pr), - /// and a `VCall*` that carries no IID (the typed forms, `VCallUI1`, - /// `VCallFPR8`, ...) through the interface `slots` know for the frame - /// slot Pr was loaded from. + /// and through the interface `slots` know for the frame slot Pr was + /// loaded from: a `VCall*` that carries no IID (the typed forms, + /// `VCallUI1`, `VCallFPR8`, ...) to the member at its vtable offset, a + /// `LateId*` to the member its DISPID names ([`LateTarget`]). pub fn resolve_with_slots( &self, object: u16, @@ -1298,20 +1574,82 @@ impl<'a, 'p: 'a> CallResolver<'a, 'p> { pr: Option<&PrSource>, slots: &SlotInterfaces, ) -> Option> { - let resolved = self.resolve_with_pr(object, instruction, pr)?; - match resolved.callee { + let mut resolved = self.resolve_with_pr(object, instruction, pr)?; + if instruction.info.receiver != Receiver::Pr { + return Some(resolved); + } + let Some(iid) = slots.for_pr_at(pr, instruction.offset) else { + return Some(resolved); + }; + match &mut resolved.callee { Callee::Unknown { vtable_offset: Some(offset), - } if instruction.info.receiver == Receiver::Pr => { - match slots.for_pr_at(pr, instruction.offset) { - Some(iid) => Some(self.interface_vtable(iid, offset)), - None => Some(resolved), - } + } => Some(self.interface_vtable(iid, *offset)), + Callee::Late { + dispid: Some(dispid), + target, + .. + } => { + *target = self.late_target(iid, *dispid, instruction.info.mnemonic); + Some(resolved) } _ => Some(resolved), } } + /// The member DISPID `dispid` names on an object of interface `iid`, + /// invoked the way the late-bound call `mnemonic` invokes it + /// ([`InvokeKind::of_late_call`]): a hosted control's extender member + /// ([`RuntimeInterfaces::extender_member`], whichever class the control + /// is), a project class's procedure whose prototype carries the DISPID, + /// or a member the catalog describes. `None` when none is invoked that + /// way. + fn late_target(&self, iid: Guid, dispid: i32, mnemonic: &str) -> Option { + let invoke = InvokeKind::of_late_call(mnemonic); + let (iid, dispid) = match RuntimeInterfaces::extender_member(dispid) { + Some(declared) if self.hosted.contains(&iid) => { + (RuntimeInterfaces::CONTROL_EXTENDER, declared) + } + _ => (iid, dispid), + }; + if let Some(object) = self + .objects + .iter() + .position(|facts| facts.iids.contains(&iid)) + { + let methods = &self.objects.get(object)?.methods; + return methods.iter().enumerate().find_map(|(method, facts)| { + let ftd = facts.ftd?; + let kind = match ftd.property_kind() { + PropertyKind::None => InvokeKind::Method, + PropertyKind::Get => InvokeKind::Get, + PropertyKind::Let => InvokeKind::Let, + PropertyKind::Set => InvokeKind::Set, + PropertyKind::Unknown(_) => return None, + }; + (ftd.dispid().ok()? == dispid && invoke.admits(kind)).then(|| { + LateTarget::Procedure { + object: u16::try_from(object).unwrap_or(u16::MAX), + method: u16::try_from(method).unwrap_or(u16::MAX), + invoke: kind, + } + }) + }); + } + let members: Vec = self + .interfaces + .as_ref()? + .dispatched(&iid, dispid) + .into_iter() + .filter(|member| invoke.admits(member.invoke)) + .collect(); + (!members.is_empty()).then_some(LateTarget::Members { + iid, + dispid, + members, + }) + } + /// Simulates a procedure's stacks ([`ProcedureStack::simulate`]) with /// the interfaces its frame slots hold: it alternates simulation and /// [`SlotInterfaces::infer`] until they agree (a resolved call can @@ -1585,7 +1923,11 @@ impl<'a, 'p: 'a> CallResolver<'a, 'p> { _ => Some(0), }; CallSignature { - callee: Callee::Late { name, dispid }, + callee: Callee::Late { + name, + dispid, + target: None, + }, arg_slots: argc.map(|n| n.saturating_mul(4)), arg_widths: argc.map(|n| vec![4; usize::from(n)]), signature: None, @@ -1618,6 +1960,17 @@ impl fmt::Display for Callee { function, } => write!(f, "object{object}.var_{offset:X}:{function}"), Self::Event { id } => write!(f, "event#{id}"), + Self::Late { + target: Some(LateTarget::Procedure { object, method, .. }), + .. + } => write!(f, "late:object{object}.method{method}"), + Self::Late { + target: Some(LateTarget::Members { members, .. }), + .. + } => match members.first() { + Some(member) => write!(f, "late:{}.{}", member.interface, member.name), + None => f.write_str("late:"), + }, Self::Late { name: Some(name), .. } => write!(f, "late:{name}"), diff --git a/src/pcode/decoder.rs b/src/pcode/decoder.rs index ddbdd8c..fec46ff 100644 --- a/src/pcode/decoder.rs +++ b/src/pcode/decoder.rs @@ -335,6 +335,53 @@ impl Instruction { slots } + /// Returns every constant pool entry the instruction names, with what + /// the opcode says the entry holds ([`PoolEntryRole`]): its `%s`, `%c`, + /// `%v` and `%x` operands and the names of a named-argument list + /// ([`Operand::NamedArgs`], read from `code`, the procedure's P-Code). + pub fn pool_references(&self, code: &[u8]) -> Vec { + let mnemonic = self.info.mnemonic; + let mut specs = self + .info + .operand_format + .split('%') + .skip(1) + .filter_map(|spec| spec.bytes().next()); + let mut references = Vec::new(); + for operand in self.operands.iter().flatten() { + let spec = specs.next(); + match *operand { + Operand::ConstPoolIndex(index) => references.push(PoolReference { + index, + role: PoolEntryRole::of_operand(mnemonic, spec.unwrap_or(b'c')), + }), + Operand::VTableRef { interface, .. } => references.push(PoolReference { + index: interface, + role: PoolEntryRole::Guid, + }), + Operand::ExternalCall { import, .. } => references.push(PoolReference { + index: import, + role: PoolEntryRole::Address, + }), + Operand::NamedArgs { + at, + count, + kind: operand::NamedArgKind::Name, + } => references.extend( + (0..usize::from(count)) + .map_while(|i| usize::from(at).checked_add(i.checked_mul(2)?)) + .map_while(|pos| read_u16_le(code, pos).ok()) + .map(|index| PoolReference { + index, + role: PoolEntryRole::MemberName, + }), + ), + _ => {} + } + } + references + } + /// Returns how an `ExitProc*` instruction returns its procedure's result /// ([`ProcedureReturn`]); `None` for any other instruction. pub fn procedure_return(&self) -> Option { @@ -543,6 +590,101 @@ impl fmt::Display for ErrorFlow { } } +/// What a constant pool entry holds, as the opcode that names it says +/// (see [`Instruction::pool_references`]). +/// +/// Read from the handlers of MSVBVM60 6.00.8176 and the runtime functions +/// they call. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum PoolEntryRole { + /// A BSTR string literal (`LitStr`, `LitVarStr`). + String, + /// A member name a late-bound call passes to `GetIDsOfNames`: UTF-16, + /// NUL-terminated, with no length prefix (`LateMem*`, `VarLateMem*`, + /// and each name of a named-argument list). + MemberName, + /// A 16-byte GUID: the IID of a vtable call's interface (the second + /// half of `%v`), of a cast's or `TypeOf`'s interface (`CastAd`, + /// `CastAdVar`, `CheckType`, `CheckTypeVar`, which compares a record's + /// GUID), or of a `For Each` loop variable's (`ForEachCollObj`, + /// `NextEachCollObj`). + Guid, + /// A record's layout + /// ([`RecordLayout`](crate::vb::controlprop::RecordLayout)): + /// `AssignRecord`, `CRec*`, `CVar*Udt`, `CDargRefUdt`, `Destruct*`, + /// `StUdtVar`, `Redim*VarUdt`, and the element layout of `EraseDestruct`, + /// `EraseDestrKeepData` and `StAryRec*`. + RecordLayout, + /// The descriptor `Get #` and `Put #` of a record read + /// ([`RecordIoDescriptor`](crate::vb::pooldesc::RecordIoDescriptor): + /// `GetRecOwn*`, `PutRecOwn*`). + RecordIo, + /// The item list of a `Print`, `Write` or `Input` statement + /// ([`IoItems`](crate::vb::pooldesc::IoItems): `PrintObject`, + /// `PrintFile`, `WriteFile`, `InputFile`). + IoItems, + /// The `SAFEARRAY` header of a fixed array inside a record + /// ([`ArrayDescriptor`](crate::vb::pooldesc::ArrayDescriptor): + /// `AryInRecLdPr`, `AryInRecLdRf`). + ArrayDescriptor, + /// What `New` creates (`New`, `NewIfNull*`): a project class's + /// `ObjectInfo`, or a + /// [`CreationDescriptor`](crate::vb::pooldesc::CreationDescriptor). + Creation, + /// An address (`%c`), or the procedure or stub an `ImpAdCall*` calls + /// (`%x`). + Address, +} + +impl PoolEntryRole { + /// The role of the entry a `%s` or `%c` operand of `mnemonic` names. + fn of_operand(mnemonic: &str, spec: u8) -> Self { + match (spec, mnemonic) { + (b's', "LitStr" | "LitVarStr") => Self::String, + ( + b's', + "AssignRecord" + | "CDargRefUdt" + | "CRec2Ansi" + | "CRec2Uni" + | "CRecAnsi2Uni" + | "CRecUni2Ansi" + | "CVarAryUdt" + | "CVarRefUdt" + | "CVarUdt" + | "DestructAnsiOFrame" + | "DestructOFrame" + | "DestructRecord" + | "StUdtVar" + | "RedimVarUdt" + | "RedimPreserveVarUdt" + | "EraseDestruct" + | "EraseDestrKeepData" + | "StAryRecCopy" + | "StAryRecMove", + ) => Self::RecordLayout, + (b's', "GetRecOwn3" | "GetRecOwn4" | "PutRecOwn3" | "PutRecOwn4") => Self::RecordIo, + (b's', "PrintObject" | "PrintFile" | "WriteFile" | "InputFile") => Self::IoItems, + (b's', "AryInRecLdPr" | "AryInRecLdRf") => Self::ArrayDescriptor, + (b's', "ForEachCollObj" | "NextEachCollObj") + | (b'c', "CastAd" | "CastAdVar" | "CheckType" | "CheckTypeVar") => Self::Guid, + (b'c', "New" | "NewIfNullPr" | "NewIfNullAd" | "NewIfNullRf") => Self::Creation, + (b's', _) if mnemonic.contains("LateMem") => Self::MemberName, + _ => Self::Address, + } + } +} + +/// A constant pool entry an instruction names: see +/// [`Instruction::pool_references`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PoolReference { + /// The entry's index in the procedure's constant pool. + pub index: u16, + /// What the opcode says the entry holds. + pub role: PoolEntryRole, +} + /// Streaming iterator over P-Code instructions. /// /// Yields one [`Instruction`] per call to [`next()`](Iterator::next), diff --git a/src/project/methodentry.rs b/src/project/methodentry.rs index 3011bdd..103e989 100644 --- a/src/project/methodentry.rs +++ b/src/project/methodentry.rs @@ -18,10 +18,11 @@ use crate::{ /// [`ProcDscInfo`](crate::vb::procedure::ProcDscInfo), whose first dword /// points back at the object's `ObjectInfo`, in modules, classes, forms and /// UserControls alike. A standard module also counts each `Declare` as a -/// method, ahead of its procedures, but stores no pointer for it: its first -/// `Declare`-count slots read whatever bytes precede the procedure pointers -/// (`vtable`: 13 `Declare`s, slots 0-12 hold values such as `0x00000409` -/// and `0x62617456`, ASCII `"Vtab"`). Those slots are +/// method, ahead of its procedures, and its table holds a slot for each, but +/// the compiler writes nothing there: a `Declare` has no procedure, so its +/// slot keeps whatever the compiler's output buffer held (`vtable`: 13 +/// `Declare`s, slots 0-12 hold values such as `0x00000409` and +/// `0x62617456`, ASCII `"Vtab"`). Those slots are /// [`Declare`](Self::Declare). #[derive(Debug)] pub enum MethodEntry<'a> { diff --git a/src/project/methodlink.rs b/src/project/methodlink.rs index 7863e23..c570b84 100644 --- a/src/project/methodlink.rs +++ b/src/project/methodlink.rs @@ -96,6 +96,13 @@ pub enum MethodLinkKind { offset: u32, /// VA of the import thunk the accessor jumps through. thunk_va: u32, + /// The two values a `New` or `WithEvents` variable's accessor pushes + /// for the runtime, in push order; `None` for the other variables' + /// accessors, which push nothing. For a `New` variable + /// (`GetMemNewObj`, `PutMemNewObj`, `SetMemNewObj`) they are the + /// constant pool index of what to create and the pool's VA + /// (`members` `Holder.Auto As New Collection`: 0 and 0x00401F6C). + pushed: Option<[u32; 2]>, }, /// An `E9 rel32` jump to [`MethodLink::code_va`]. Jump, @@ -120,15 +127,15 @@ impl MethodLinkKind { o2, o3, 0x68, - _, - _, - _, - _, + a0, + a1, + a2, + a3, 0x68, - _, - _, - _, - _, + b0, + b1, + b2, + b3, 0x50, 0xB9, t0, @@ -138,8 +145,15 @@ impl MethodLinkKind { 0xFF, 0xE1, .., - ] - | [ + ] => Self::Variable { + offset: u32::from_le_bytes([*o0, *o1, *o2, *o3]), + thunk_va: u32::from_le_bytes([*t0, *t1, *t2, *t3]), + pushed: Some([ + u32::from_le_bytes([*a0, *a1, *a2, *a3]), + u32::from_le_bytes([*b0, *b1, *b2, *b3]), + ]), + }, + [ 0x81, 0x44, 0x24, @@ -159,6 +173,7 @@ impl MethodLinkKind { ] => Self::Variable { offset: u32::from_le_bytes([*o0, *o1, *o2, *o3]), thunk_va: u32::from_le_bytes([*t0, *t1, *t2, *t3]), + pushed: None, }, [0xE9, ..] => Self::Jump, _ => Self::Other, diff --git a/src/project/mod.rs b/src/project/mod.rs index df2dbbc..a749859 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -35,11 +35,11 @@ pub use methodlink::{MethodLink, MethodLinkIterator, MethodLinkKind}; pub use pcodemethod::{ErrorHandling, PCodeMethod, StatementMarker}; pub use vbcontrol::{ControlEntryIterator, VbControl}; pub use vbobject::{ - CodeEntry, CodeEntryKind, EventBinding, FuncTypDescIter, Instancing, MethodIterator, - MethodNameResult, PCodeMethodIterator, VbObject, format_signature, + CodeEntry, CodeEntryKind, EventBinding, FuncTypDescIter, Instancing, MemberIter, + MethodIterator, MethodNameResult, PCodeMethodIterator, VbObject, format_signature, }; pub use vbproject::{ CodeEntrypoint, CompilationMode, DiagnosticKind, DiagnosticSeverity, EntrypointKind, - ExternalIterator, GuiEntriesWithFormData, GuiEntryWithFormData, ObjectIterator, - ParseDiagnostic, VbProject, + ExternalIterator, GuiEntriesWithFormData, GuiEntryWithFormData, NameOwner, NameReference, + ObjectIterator, ParseDiagnostic, VbProject, }; diff --git a/src/project/vbcontrol.rs b/src/project/vbcontrol.rs index 51f676a..4f71feb 100644 --- a/src/project/vbcontrol.rs +++ b/src/project/vbcontrol.rs @@ -80,17 +80,17 @@ impl<'a> VbControl<'a> { self.info.control_type() } - /// Number of event handler slots on this control. - /// - /// This is the number of entries in the event sink vtable at +0x18, - /// NOT the dispatch_offset at +0x04 (which is a byte offset, not a count). + /// Number of event handler slots on this control: + /// [`ControlInfo::event_count`], the events (an `Implements`' interface + /// members), without a `WithEvents` or `Implements` sink's `IDispatch` + /// slots. /// /// # Errors /// - /// Returns an error if the underlying ControlInfo field cannot be read. + /// Returns an error if the underlying ControlInfo fields cannot be read. #[inline] pub fn event_count(&self) -> Result { - self.info.event_handler_slots() + self.info.event_count() } /// Control index within the form. @@ -140,8 +140,9 @@ impl<'a> VbControl<'a> { self.guid.as_ref().and_then(|g| g.control_class_name()) } - /// Returns the VA of the event handler at `event_index`: slot - /// `event_index` of the control's [`EventSinkVtable`]. + /// Returns the VA of the event handler at `event_index`: handler slot + /// `event_index` of the control's [`EventSinkVtable`] + /// ([`EventSinkVtable::handler_va`]), after any `IDispatch` slots. /// /// A return value of `0` means the event is not handled. A non-zero VA /// is the handler's entry stub, filled in on disk @@ -171,7 +172,7 @@ impl<'a> VbControl<'a> { let slots = self.info.event_handler_slots().ok()?; let size = EventSinkVtable::HEADER_SIZE.checked_add((slots as usize).checked_mul(4)?)?; let data = map.slice_from_va(va, size).ok()?; - EventSinkVtable::parse(data, slots).ok() + EventSinkVtable::parse(data, &self.info).ok() } /// Returns the number of events with handler VAs connected. @@ -301,15 +302,18 @@ impl<'a, 'p> Iterator for ControlEntryIterator<'a, 'p> { Ok(v) => v, Err(e) => return Some(Err(e)), }; - let slots = match info.event_handler_slots() { - Ok(v) => v, - Err(e) => return Some(Err(e)), + let (count, dispatch_slots) = match (info.event_count(), info.dispatch_slots()) { + (Ok(count), Ok(dispatch_slots)) => (count, dispatch_slots), + (Err(e), _) | (_, Err(e)) => return Some(Err(e)), }; - // The handler slots follow the sink vtable's 0x18-byte header. - let event_handler_vas: &[u8] = if sink_va != 0 && slots > 0 { - let size = usize::from(slots).saturating_mul(4); + // The handler slots follow the sink vtable's 0x18-byte header and + // the IDispatch slots of a dual interface's vtable. + let event_handler_vas: &[u8] = if sink_va != 0 && count > 0 { + let size = usize::from(count).saturating_mul(4); + let skip = u32::from(dispatch_slots).saturating_mul(4); sink_va .checked_add(EventSinkVtable::HEADER_SIZE as u32) + .and_then(|va| va.checked_add(skip)) .and_then(|va| self.map.slice_from_va(va, size).ok()) .and_then(|data| data.get(..size)) .unwrap_or(b"") diff --git a/src/project/vbobject.rs b/src/project/vbobject.rs index c123d91..da20b6c 100644 --- a/src/project/vbobject.rs +++ b/src/project/vbobject.rs @@ -6,7 +6,7 @@ //! (the array entry), an [`ObjectInfo`] with method/constant table pointers, //! an optional [`OptionalObjectInfo`] (controls, method links, vtable //! layout), and an optional [`PrivateObjectDescriptor`] (function type -//! descriptors, parameter name tables). +//! descriptors, member and event descriptors). //! //! [`VbObject`] ties these structures together and provides iterators over //! methods, controls, and method link thunks. @@ -17,6 +17,8 @@ use std::{ str, }; +use msft_typelib::TypeLib; + use crate::{ addressmap::AddressMap, error::Error, @@ -28,17 +30,18 @@ use crate::{ vb::{ constantpool::ConstantPool, control::Guid, + controlprop::RecordLayout, designer::Designer, eventname, - events::EventHandlerThunk, + events::{EventHandlerThunk, NativeEventThunk}, flags::ObjectTypeFlags, formdata::{FormControlType, FormDataParser}, functype::FuncTypDesc, guitable::GuiTableEntry, + member::MemberDesc, object::{GuidTableIter, ObjectInfo, OptionalObjectInfo, PublicObjectDescriptor}, privateobj::PrivateObjectDescriptor, publicbytes::ClassFormPublicBytes, - varstub::VarStubIter, }, }; @@ -156,8 +159,8 @@ pub enum Instancing { /// A single VB6 object (form, module, class) within the project. /// /// Provides access to the object's descriptor, info, optional info, -/// and private object descriptor (which contains function type -/// descriptors and parameter name tables). +/// and private object descriptor (which contains function type, +/// member and event descriptors). /// /// Holds a reference to the parent [`VbProject`] so all accessor methods /// can resolve VAs without requiring the project as a parameter. @@ -300,7 +303,7 @@ impl<'a, 'p: 'a> VbObject<'a, 'p> { /// Returns the [`PrivateObjectDescriptor`] if present. /// - /// Contains function type descriptors and parameter name tables. Not + /// Contains function type, member and event descriptors. Not /// available for standard modules (BAS files) - those have /// `private_object_va == 0xFFFFFFFF`. #[inline] @@ -326,26 +329,6 @@ impl<'a, 'p: 'a> VbObject<'a, 'p> { Ok(u32::try_from(self.func_type_descs()?.count()).unwrap_or(u32::MAX)) } - /// The private object descriptor's - /// [`var_stub_count`](PrivateObjectDescriptor::var_stub_count), or 0 if - /// no private object descriptor is available. - /// - /// It is 0 in every fixture, including `data`'s `Item`, which declares - /// `Public Name As String`, so it does not count public variables; what - /// it counts is unconfirmed. - /// - /// # Errors - /// - /// Returns an error if the underlying PrivateObjectDescriptor field - /// cannot be read. - #[inline] - pub fn var_stub_count(&self) -> Result { - match self.private_object.as_ref() { - Some(p) => Ok(u32::from(p.var_stub_count()?)), - None => Ok(0), - } - } - /// Reads the object name as a lossy UTF-8 string. /// /// Borrows when the underlying bytes are already valid UTF-8 (the @@ -845,6 +828,7 @@ impl<'a, 'p: 'a> VbObject<'a, 'p> { stub_va: stub_by_dsc.get(&proc_dsc_va).copied(), proc_dsc_va: Some(proc_dsc_va), pcode_size: Some(pcode_size), + target_va: None, }); } MethodEntry::Native { va } => { @@ -860,6 +844,7 @@ impl<'a, 'p: 'a> VbObject<'a, 'p> { stub_va: None, proc_dsc_va: None, pcode_size: None, + target_va: None, }); } MethodEntry::Null | MethodEntry::Declare | MethodEntry::Runtime { .. } => {} @@ -893,6 +878,7 @@ impl<'a, 'p: 'a> VbObject<'a, 'p> { stub_va, proc_dsc_va, pcode_size: None, + target_va: None, }); } @@ -955,6 +941,11 @@ impl<'a, 'p: 'a> VbObject<'a, 'p> { .ok() .and_then(|data| EventHandlerThunk::parse_from_event_entry(data, handler_va)) .map(|thunk| thunk.proc_dsc_info_va); + let target_va = map + .slice_from_va(handler_va, NativeEventThunk::SIZE) + .ok() + .and_then(|data| NativeEventThunk::parse(data, handler_va)) + .map(|thunk| thunk.handler_va); entries.push(CodeEntry { va: handler_va, kind: CodeEntryKind::EventHandler, @@ -964,6 +955,7 @@ impl<'a, 'p: 'a> VbObject<'a, 'p> { stub_va: None, proc_dsc_va, pcode_size: None, + target_va, }); } } @@ -1168,6 +1160,28 @@ impl<'a, 'p: 'a> VbObject<'a, 'p> { .and_then(|opt| opt.resolve_clsid(self.project.address_map())) } + /// Returns the VA of the 16-byte GUID the compiler writes right before + /// the object's CLSID ([`OptionalObjectInfo::object_clsid_va`]), when + /// it is one. + /// + /// For a form, UserControl, PropertyPage or UserDocument it is the IID + /// of the object's own interface, which a `VCallHresult` through the + /// object names (`controls` `Form1`); for one class module of a project + /// with classes it is the IID of the runtime's `_DClass` + /// (`{FCFB3D2B-A0FA-1068-A738-08002B3371B5}`), taken by its value. + /// `None` for a module and any other class. + pub fn interface_iid_va(&self) -> Option { + /// The IID of `_DClass`. + const DCLASS: [u8; 16] = [ + 0x2B, 0x3D, 0xFB, 0xFC, 0xFA, 0xA0, 0x68, 0x10, 0xA7, 0x38, 0x08, 0x00, 0x2B, 0x33, + 0x71, 0xB5, + ]; + let clsid = self.optional_info.as_ref()?.object_clsid_va().ok()?; + let va = clsid.checked_sub(16).filter(|_| clsid != 0)?; + let bytes = self.project.address_map().slice_from_va(va, 16).ok()?; + (self.designer().is_some() || bytes.get(..16) == Some(&DCLASS[..])).then_some(va) + } + /// Returns an iterator over GUI GUIDs for this object. /// /// Delegates to [`OptionalObjectInfo::gui_guids`]. Returns an empty @@ -1233,34 +1247,142 @@ impl<'a, 'p: 'a> VbObject<'a, 'p> { }) } - /// Returns an iterator over [`VarStubDesc`](crate::vb::varstub::VarStubDesc) entries. + /// Returns an iterator over the prototypes of the object's `Event` + /// declarations, in declaration order. /// - /// Walks the pointer array at [`PrivateObjectDescriptor::var_stubs_va`]. - /// Returns an empty iterator if no private object descriptor is present - /// or if there are no variable stubs. + /// Walks the [`PrivateObjectDescriptor::event_descs_va`] array, yielding + /// `(event index, FuncTypDesc)`. Each descriptor's + /// [`param_names`](FuncTypDesc::param_names) are the event's parameter + /// names (`ocx` `Knob`: `Turned(Position, Delta)`). Empty without a + /// private object descriptor or events. /// /// # Errors /// - /// Returns an error if the private object descriptor's `var_stubs_va` - /// or `var_stub_count` cannot be read. - pub fn var_stubs(&self) -> Result, Error> { - let (stubs_va, count) = match self.private_object.as_ref() { - Some(p) => { - let va = p.var_stubs_va()?; - let cnt = p.var_stub_count()?; - if va != 0 && cnt > 0 { - (va, cnt) - } else { - (0, 0) - } - } + /// Returns an error if the private object descriptor's event array VA + /// or count cannot be read. + pub fn event_type_descs(&self) -> Result, Error> { + let (va, total) = match self.private_object.as_ref() { + Some(p) => (p.event_descs_va()?, u32::from(p.event_count()?)), None => (0, 0), }; - Ok(VarStubIter::new( - self.project.address_map(), - stubs_va, - count, - )) + Ok(FuncTypDescIter { + map: self.project.address_map(), + ftd_array_va: va, + index: 0, + total: if va == 0 { 0 } else { total }, + }) + } + + /// Returns the names of the object's `Event` declarations, in + /// declaration order (the order of [`event_type_descs`](Self::event_type_descs)). + /// + /// The names come from the project's embedded type library + /// ([`VbProject::type_library_bytes`]): the dispinterface whose GUID is + /// the object's events IID ([`events_iids`](Self::events_iids)) names + /// each event by its DISPID, which the event's prototype carries + /// ([`FuncTypDesc::dispid`]). `ocx` `Knob`: `Turned`, `Reset`. An event + /// is `None` when the project has no type library, as in a Standard EXE: + /// there the compiler writes the names into its string pool with no + /// pointer to them and shares each spelling between objects + /// (`eventnames`: `Second`'s property `N` is `First`'s string, `Third`'s + /// method `Pong` is `Second`'s event name), so nothing ties a name to its + /// event. + /// + /// # Errors + /// + /// Returns an error if the private object descriptor's event array + /// cannot be read. + pub fn event_names(&self) -> Result>, Error> { + let dispids: Vec> = self + .event_type_descs()? + .map(|(_, ftd)| ftd.dispid().ok()) + .collect(); + if dispids.is_empty() { + return Ok(Vec::new()); + } + let names = self + .project + .type_library_bytes() + .and_then(|bytes| TypeLib::parse(bytes).ok()) + .and_then(|lib| self.events_interface_names(&lib)) + .unwrap_or_default(); + Ok(dispids + .into_iter() + .map(|dispid| names.get(&dispid?).copied()) + .collect()) + } + + /// The member names, by DISPID, of the type library's interface whose + /// GUID is one of the object's events IIDs. + fn events_interface_names(&self, lib: &TypeLib<'a>) -> Option> { + let iids: Vec = self.events_iids().map(|(_, iid)| iid).collect(); + (0..lib.typeinfo_count()).find_map(|index| { + let ti = lib.typeinfo(index).ok()?; + let guid = lib.guid(ti.guid_offset())?; + iids.iter() + .any(|iid| iid.bytes[..] == *guid.as_bytes()) + .then(|| { + (0..usize::from(ti.func_count())) + .filter_map(|k| Some((lib.func_memid(&ti, k)?, lib.func_name(&ti, k)?))) + .collect() + }) + }) + } + + /// Returns an iterator over the object's public variables and + /// implemented interfaces. + /// + /// Walks the [`PrivateObjectDescriptor::member_descs_va`] array (one + /// entry per [`member_count`](PrivateObjectDescriptor::member_count)), + /// yielding `(member index, MemberDesc)` and skipping the null entries + /// of `Private` variables. [`MemberDesc::kind`] tells a `Public` + /// variable from an `Implements`, so the object's public variables are + /// the members that are not [`MemberKind::Implements`](crate::vb::member::MemberKind::Implements) + /// (`members` `Holder`: 13, `Mixed`: `Value`, `Note`, `Shown`; `data` + /// `Item`: `Name`). Empty for a standard module. + /// + /// # Errors + /// + /// Returns an error if the private object descriptor's member array VA + /// or count cannot be read. + pub fn members(&self) -> Result, Error> { + let (va, total) = match self.private_object.as_ref() { + Some(p) => (p.member_descs_va()?, p.member_count()?), + None => (0, 0), + }; + Ok(MemberIter { + map: self.project.address_map(), + array_va: va, + index: 0, + total: if va == 0 { 0 } else { total }, + }) + } + + /// Returns the record layouts of the object's `Type` declarations, in + /// declaration order, each with its VA: the + /// [`PrivateObjectDescriptor::record_layouts_va`] array. + /// + /// Empty for an object that declares no `Type` and for a standard + /// module, which has no private object descriptor. A layout that does + /// not read is left out. + /// + /// # Errors + /// + /// Returns an error if the private object descriptor's array VA or count + /// cannot be read. + pub fn record_layouts(&self) -> Result)>, Error> { + let Some(private) = self.private_object.as_ref() else { + return Ok(Vec::new()); + }; + let (array_va, count) = (private.record_layouts_va()?, private.record_layout_count()?); + let map = self.project.address_map(); + Ok((0..u32::from(count)) + .filter_map(|index| { + let slot = array_va.checked_add(index.checked_mul(4)?)?; + let va = read_u32_le(map.slice_from_va(slot, 4).ok()?, 0).ok()?; + Some((va, RecordLayout::at(map, va).ok()?)) + }) + .collect()) } } @@ -1306,6 +1428,40 @@ impl<'a, 'p> Iterator for FuncTypDescIter<'a, 'p> { } } +/// Iterator over the [`MemberDesc`]s of a [`PrivateObjectDescriptor`]'s +/// member array. +/// +/// Created by [`VbObject::members`]. Yields `(member index, MemberDesc)`; +/// null entries (`Private` variables) are skipped. +#[must_use = "iterators are lazy and do nothing unless consumed"] +pub struct MemberIter<'a, 'p> { + map: &'p AddressMap<'a>, + array_va: u32, + index: u16, + total: u16, +} + +impl<'a, 'p> Iterator for MemberIter<'a, 'p> { + type Item = (u16, MemberDesc<'a>); + + fn next(&mut self) -> Option { + while self.index < self.total { + let i = self.index; + self.index = self.index.saturating_add(1); + let ptr_va = self.array_va.wrapping_add(u32::from(i).saturating_mul(4)); + let desc_va = read_u32_le(self.map.slice_from_va(ptr_va, 4).ok()?, 0).ok()?; + if desc_va == 0 { + continue; + } + let data = self.map.slice_from_va(desc_va, MemberDesc::MIN_SIZE).ok()?; + if let Ok(desc) = MemberDesc::parse(data) { + return Some((i, desc)); + } + } + None + } +} + /// A code entry point discovered in a VB6 object. /// /// Returned by [`VbObject::code_entries`], which combines method table entries, @@ -1318,8 +1474,9 @@ pub struct CodeEntry { pub kind: CodeEntryKind, /// Method table index: of the method for a method table entry, of the /// procedure the stub enters for an event handler; `None` for a - /// [`CodeEntryKind::NativeThunk`] and for an event handler whose stub - /// does not decode. + /// [`CodeEntryKind::NativeThunk`], for a native build's event handler + /// (see [`target_va`](Self::target_va)) and for an event handler whose + /// stub does not decode. pub method_index: Option, /// Human-readable name (method name or "ControlName_EventName"). pub name: Option, @@ -1341,6 +1498,11 @@ pub struct CodeEntry { /// Size of the P-Code byte stream in bytes. /// Present for [`CodeEntryKind::PCode`] entries. pub pcode_size: Option, + /// For an event handler of a native build, the procedure its + /// `sub [esp+4], adj; jmp` stub ([`NativeEventThunk`](crate::vb::events::NativeEventThunk)) + /// enters: a native class has no method table, so the procedure is the + /// [`CodeEntryKind::NativeThunk`] entry at this VA. + pub target_va: Option, } /// Classification of a code entry point. diff --git a/src/project/vbproject.rs b/src/project/vbproject.rs index 93667b6..209f4bd 100644 --- a/src/project/vbproject.rs +++ b/src/project/vbproject.rs @@ -4,7 +4,7 @@ //! together PE parsing, VB structure navigation, and P-Code access into //! a single convenient type with lifetime `'a` tied to the file buffer. -use std::borrow::Cow; +use std::{borrow::Cow, collections::BTreeSet, ops::Range}; use goblin::{ options::ParseMode, @@ -13,17 +13,22 @@ use goblin::{ use crate::{ addressmap::AddressMap, - entrypoint, + entrypoint::{self, ExportStub}, error::Error, imports::ImportTable, - project::{CodeEntryKind, PCodeMethod, VbObject}, - util::read_cstr, + project::{CodeEntryKind, MethodLinkKind, PCodeMethod, VbObject}, + util::{read_cstr, read_u16_le, read_u32_le}, vb::{ comreg::ComRegData, + constantpool::PoolEntry, + events::{EventHandlerThunk, NativeEventThunk}, external::{ExternalComponentIter, ExternalTableEntry}, formdata::FormDataParser, + functype::FuncTypDesc, guitable::{GuiTableEntry, GuiTableIter}, header::VbHeader, + member::{MemberDesc, MemberKind}, + native::ProcedureUnwind, object::PublicObjectDescriptor, objecttable::ObjectTable, projectdata::ProjectData, @@ -229,6 +234,8 @@ pub enum CompilationMode { pub struct VbProject<'a> { /// VA-to-file-offset resolver built from PE section headers. map: AddressMap<'a>, + /// VA of the PE entry point. + entry_point_va: u32, /// VA of the VbHeader in the PE image. vb_header_va: u32, /// Root VBHeader (EXEPROJECTINFO) parsed from the entry point. @@ -239,6 +246,10 @@ pub struct VbProject<'a> { object_table: ObjectTable<'a>, /// The PE imports, by import address table slot. imports: ImportTable, + /// The PE exports: name and RVA. + exports: Vec<(Option, u32)>, + /// RVA of the PE resource directory, 0 when there is none. + resource_rva: u32, } impl<'a> VbProject<'a> { @@ -372,15 +383,126 @@ impl<'a> VbProject<'a> { .unwrap_or_default(); Ok(Self { + entry_point_va: map.image_base().wrapping_add(entry_rva), map, vb_header_va, vb_header, project_data, object_table, imports, + resource_rva: pe + .header + .optional_header + .as_ref() + .and_then(|oh| oh.data_directories.get_resource_table()) + .map_or(0, |dir| dir.virtual_address), + exports: pe + .exports + .iter() + .filter_map(|e| Some((e.name.map(String::from), u32::try_from(e.rva).ok()?))) + .collect(), }) } + /// Returns the type library the compiler embeds in an ActiveX DLL, OCX + /// or ActiveX EXE: the data of the `TYPELIB` resource (an MSFT type + /// library naming the project's public classes, their members and + /// events). `None` for a Standard EXE, which has none. + /// + /// The resource directory is read here, fail-soft: a malformed one + /// gives `None`. + pub fn type_library_bytes(&self) -> Option<&'a [u8]> { + let rva = self.resource_rva; + if rva == 0 { + return None; + } + let root = self.map.slice_from_rva(rva, 16).ok()?; + // Root: the TYPELIB type; then its first name or ID; then its first + // language. + let typelib = self.resource_entry(root, |entry| { + entry & 0x8000_0000 != 0 && Self::resource_name_is(root, entry, "TYPELIB") + })?; + let names = self.resource_subdir(root, typelib)?; + let languages = self.resource_subdir(root, self.resource_entry(names, |_| true)?)?; + let leaf = self.resource_entry(languages, |_| true)?; + let data = root.get(usize::try_from(leaf & 0x7FFF_FFFF).ok()?..)?; + let data_rva = read_u32_le(data, 0).ok()?; + let size = usize::try_from(read_u32_le(data, 4).ok()?).ok()?; + self.map.slice_from_rva(data_rva, size).ok()?.get(..size) + } + + /// The offset field of the first entry of resource directory `dir` + /// whose name field satisfies `matches`. + fn resource_entry(&self, dir: &[u8], matches: impl Fn(u32) -> bool) -> Option { + let named = read_u16_le(dir, 12).ok()?; + let ids = read_u16_le(dir, 14).ok()?; + (0..usize::from(named).saturating_add(usize::from(ids))).find_map(|i| { + let at = 16usize.checked_add(i.checked_mul(8)?)?; + let name = read_u32_le(dir, at).ok()?; + let offset = read_u32_le(dir, at.checked_add(4)?).ok()?; + matches(name).then_some(offset) + }) + } + + /// The subdirectory an entry's offset field names (bit 31 set), as a + /// slice from the subdirectory to the end of the section. + fn resource_subdir<'r>(&self, root: &'r [u8], offset: u32) -> Option<&'r [u8]> { + (offset & 0x8000_0000 != 0) + .then(|| root.get(usize::try_from(offset & 0x7FFF_FFFF).ok()?..)) + .flatten() + } + + /// Whether the UTF-16 name an entry's name field points to is the + /// ASCII `expected`. + fn resource_name_is(root: &[u8], name: u32, expected: &str) -> bool { + let Ok(at) = usize::try_from(name & 0x7FFF_FFFF) else { + return false; + }; + let Ok(len) = read_u16_le(root, at) else { + return false; + }; + let units = at + .checked_add(2) + .and_then(|start| Some(start..start.checked_add(usize::from(len).checked_mul(2)?)?)) + .and_then(|range| root.get(range)); + units.is_some_and(|units| { + units + .as_chunks::<2>() + .0 + .iter() + .map(|&[low, high]| if high == 0 { low } else { 0 }) + .eq(expected.bytes()) + }) + } + + /// Decodes the COM export stubs of an ActiveX DLL or OCX + /// ([`ExportStub`]): per export, the VBHeader and `.data` VAs it pushes + /// and the runtime function it jumps to. Exports that are not such a + /// stub are skipped; an executable has none. + pub fn export_stubs(&self) -> Vec { + self.exports + .iter() + .filter_map(|(name, rva)| { + let va = self.map.image_base().checked_add(*rva)?; + let code = self.map.slice_from_va(va, 8).ok()?; + let mut stub = ExportStub::decode(code.get(..code.len().min(32))?, va)?; + stub.name.clone_from(name); + stub.function = self + .map + .slice_from_va(stub.target_va, 6) + .ok() + .and_then(|thunk| match thunk { + [0xFF, 0x25, s0, s1, s2, s3, ..] => self + .imports + .by_slot(u32::from_le_bytes([*s0, *s1, *s2, *s3])), + _ => None, + }) + .map(|import| import.function().into_owned()); + Some(stub) + }) + .collect() + } + /// Returns the executable's imports, by import address table slot. /// /// Read from the PE import directory independently of goblin's import @@ -391,6 +513,172 @@ impl<'a> VbProject<'a> { &self.imports } + /// Returns the VA of the PE entry point: the stub that pushes the + /// [`VbHeader`] and calls `ThunRTMain`, for an executable. + #[inline] + pub fn entry_point_va(&self) -> u32 { + self.entry_point_va + } + + /// Returns every address of native code the project's structures name. + /// + /// The PE entry point, the import thunk it transfers to and the + /// exception handler thunk ([`ProjectData::vba_seh_va`]); the COM export + /// stubs with the import thunks they jump to; every [`code_entrypoints`](Self::code_entrypoints) + /// entry's stub and, unless it is P-Code, the entry itself; each object's + /// method link entries and the code their jumps reach; each event sink's + /// `IUnknown` thunks and connected handler stubs; and the procedure, + /// import and `Declare` stubs its constant pool names; the procedures + /// native adjustor thunks enter, and every procedure that stores an + /// unwind record ([`unwind_records`](Self::unwind_records)). Not the + /// code a record names: the runtime enters that in its procedure's + /// frame. A dual-entry stub + /// (`mov eax, imm32; cmp ax, 0xC033; mov edx, ...`, whose `xor eax, + /// eax` at +7 is the method entry) gives both entries. In a P-Code build + /// these stubs are all the native code there is; in a native build the + /// procedures between the code markers + /// ([`native_code_range`](Self::native_code_range)) are what they reach. + /// + /// Fail-soft: a structure that does not read contributes nothing. + pub fn native_entries(&self) -> BTreeSet { + let mut code = BTreeSet::new(); + code.insert(self.entry_point_va); + code.extend(entrypoint::entry_stub_target( + &self.map, + self.entry_point_va, + )); + code.extend(self.project_data.vba_seh_va()); + for stub in self.export_stubs() { + code.extend([stub.va, stub.target_va]); + } + for entry in self.code_entrypoints().unwrap_or_default() { + if entry.kind != EntrypointKind::PCodeStub { + code.insert(entry.va); + } + code.extend(entry.stub_va); + } + for object in self.objects().into_iter().flatten().flatten() { + for link in object.method_links().into_iter().flatten().flatten() { + code.insert(link.thunk_va); + if link.kind == MethodLinkKind::Jump { + code.insert(link.code_va); + } + } + for control in object.controls().into_iter().flatten().flatten() { + let Some(sink) = control.event_sink(&self.map) else { + continue; + }; + code.extend( + [ + sink.query_interface_va(), + sink.add_ref_va(), + sink.release_va(), + ] + .into_iter() + .flatten(), + ); + code.extend(sink.connected_handlers().map(|(_, va)| va)); + } + if let (Ok(pool), Ok(count)) = + (object.constants_pool(), object.info().constants_count()) + { + for (index, entry) in pool.entries(count) { + if matches!( + entry, + Ok(PoolEntry::Procedure { .. } + | PoolEntry::Import { .. } + | PoolEntry::Declare(_)) + ) && let Ok(va) = pool.va_at(index) + { + code.insert(va); + } + } + } + } + let partners: Vec = code + .iter() + .filter_map(|&va| self.dual_entry_partner(va)) + .collect(); + code.extend(partners); + // A native adjustor thunk enters its procedure; a procedure that + // stores an unwind record is one too, whether or not a table names it. + let procedures: Vec = code + .iter() + .filter_map(|&va| { + let data = self.map.slice_from_va(va, NativeEventThunk::SIZE).ok()?; + NativeEventThunk::parse(data, va).map(|thunk| thunk.handler_va) + }) + .collect(); + code.extend(procedures); + code.extend( + self.unwind_records() + .iter() + .map(|procedure| procedure.procedure_va), + ); + code.remove(&0); + code + } + + /// Returns every native procedure that stores an unwind record, with + /// the record: [`ProcedureUnwind::scan`] over + /// [`native_code_range`](Self::native_code_range). This finds the + /// procedures no table names (a module's private ones, reached only by + /// direct calls) along with the rest. Empty for a P-Code build. + pub fn unwind_records(&self) -> Vec> { + self.native_code_range() + .map(|range| ProcedureUnwind::scan(&self.map, range)) + .unwrap_or_default() + } + + /// The other entry of the two-entry stub with an entry at `va`: a P-Code + /// method stub's method entry at +7 or its first entry 7 bytes before, + /// or a native adjustor thunk's (`sub [esp+4], imm32; jmp rel32`) bare + /// jump at +8 or its first entry 8 bytes before. + fn dual_entry_partner(&self, va: u32) -> Option { + let method_entry = |at: u32| { + self.map + .slice_from_va(at, EventHandlerThunk::SIZE) + .ok() + .and_then(|data| EventHandlerThunk::parse_from_event_entry(data, at)) + .map(|thunk| thunk.method_entry_va) + }; + let jump_entry = |at: u32| { + self.map + .slice_from_va(at, NativeEventThunk::SIZE) + .ok() + .and_then(|data| NativeEventThunk::parse(data, at)) + .and_then(|_| at.checked_add(NativeEventThunk::JUMP_OFFSET)) + }; + let back = |offset: u32, entry: &dyn Fn(u32) -> Option| { + let start = va.checked_sub(offset)?; + (entry(start)? == va).then_some(start) + }; + method_entry(va) + .or_else(|| jump_entry(va)) + .or_else(|| { + back( + u32::try_from(EventHandlerThunk::METHOD_ENTRY_OFFSET).ok()?, + &method_entry, + ) + }) + .or_else(|| back(NativeEventThunk::JUMP_OFFSET, &jump_entry)) + } + + /// Returns the range of the project's native procedures: from the end of + /// the marker [`ProjectData::code_start_va`] names to the marker + /// [`ProjectData::code_end_va`] names. Empty for a P-Code build, whose + /// region is the start marker alone; `None` when either marker is not + /// where its field points. + pub fn native_code_range(&self) -> Option> { + let start = self.project_data.code_start_va().ok()?; + let end = self.project_data.code_end_va().ok()?; + let at = |va: u32, len: usize| self.map.slice_from_va(va, len).ok(); + let opens = at(start, 16)?.get(..4)? == [0xE9; 4]; + let closes = at(end, 4)?.get(..4)? == [0x9E; 4]; + let first = start.checked_add(16)?; + (opens && closes && first <= end).then_some(first..end) + } + /// Returns the VA of the [`VbHeader`] structure in the PE image. #[inline] pub fn vb_header_va(&self) -> u32 { @@ -940,6 +1228,117 @@ impl<'a> VbProject<'a> { }) } + /// Lists every pointer to a parameter, event parameter, public variable + /// or implemented interface name, with the structure it belongs to. + /// + /// Three pointer arrays per object refer to these names: the + /// [`FuncTypDesc::param_names_va`] arrays of its procedures, those of + /// its events' prototypes ([`VbObject::event_type_descs`]), and its + /// [`MemberDesc`]s ([`VbObject::members`]). In every fixture they + /// account for every pointer to every string of the ProjectInfo2 name + /// area ([`read_name_strings`](crate::vb::projectinfo2::read_name_strings)), + /// and for the names past where that walk stops (`members`: `Count`, + /// `IFirst`, `Value`, `Amount`, ...). The compiler stores one string per + /// spelling, so a name can have several references (`extender`: the + /// nine controls' `Event Ping(ByVal N)` share one `N`). Property and + /// method names are not among them: those are in the + /// [method names table](crate::vb::object::PublicObjectDescriptor::method_names_va). + /// + /// The order is by object, then procedures, events and members. + /// + /// # Errors + /// + /// Returns an error if an object cannot be parsed or its private object + /// descriptor's arrays cannot be read. + pub fn name_references(&self) -> Result>, Error> { + let mut refs = Vec::new(); + for (object, obj) in self.objects()?.enumerate() { + let obj = obj?; + let object = u16::try_from(object).unwrap_or(u16::MAX); + for (method, ftd) in obj.func_type_descs()? { + self.push_param_refs(&mut refs, &ftd, |index| NameOwner::Parameter { + object, + method, + index, + }); + } + for (event, ftd) in obj.event_type_descs()? { + self.push_param_refs(&mut refs, &ftd, |index| NameOwner::EventParameter { + object, + event, + index, + }); + } + let Some(private) = obj.private_object() else { + continue; + }; + let array_va = private.member_descs_va()?; + for member in 0..private.member_count()? { + let entry_va = array_va.wrapping_add(u32::from(member).saturating_mul(4)); + let Some(desc_va) = self.read_va(entry_va).filter(|&va| va != 0) else { + continue; + }; + let Ok(desc) = self + .map + .slice_from_va(desc_va, MemberDesc::MIN_SIZE) + .and_then(MemberDesc::parse) + else { + continue; + }; + let (Ok(name_va), Ok(kind)) = (desc.name_va(), desc.kind()) else { + continue; + }; + if let Ok(name) = self.read_string_at_va(name_va) { + refs.push(NameReference { + pointer_va: desc_va, + name_va, + name, + owner: NameOwner::Member { + object, + member, + kind, + }, + }); + } + } + } + Ok(refs) + } + + /// Appends a reference per named parameter of `ftd`. + fn push_param_refs( + &self, + refs: &mut Vec>, + ftd: &FuncTypDesc<'_>, + owner: impl Fn(u8) -> NameOwner, + ) { + let Ok(base) = ftd.param_names_va() else { + return; + }; + if base == 0 { + return; + } + for index in 0..ftd.arg_count() { + let pointer_va = base.wrapping_add(u32::from(index).saturating_mul(4)); + let Some(name_va) = self.read_va(pointer_va).filter(|&va| va != 0) else { + continue; + }; + if let Ok(name) = self.read_string_at_va(name_va) { + refs.push(NameReference { + pointer_va, + name_va, + name, + owner: owner(index), + }); + } + } + } + + /// Reads the DWORD at `va`. + fn read_va(&self, va: u32) -> Option { + read_u32_le(self.map.slice_from_va(va, 4).ok()?, 0).ok() + } + /// Reads a null-terminated string at the given VA. /// /// # Errors @@ -954,6 +1353,61 @@ impl<'a> VbProject<'a> { } } +/// The structure field that points at a name: see +/// [`VbProject::name_references`]. +/// +/// `object` is the object's index in the object table, `method` its method +/// table slot, `event` the index of its `Event` declaration and `member` +/// the index in its member array (see [`VbObject::members`]). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NameOwner { + /// Parameter `index` of a procedure: an entry of its + /// [`FuncTypDesc::param_names_va`] array. + Parameter { + /// Object index. + object: u16, + /// Method table slot of the procedure. + method: u32, + /// Parameter index. + index: u8, + }, + /// Parameter `index` of an `Event` declaration: an entry of the + /// [`FuncTypDesc::param_names_va`] array of the event's prototype (see + /// [`VbObject::event_type_descs`]). + EventParameter { + /// Object index. + object: u16, + /// Index of the event, in declaration order. + event: u32, + /// Parameter index. + index: u8, + }, + /// A public variable or an `Implements`: the + /// [`MemberDesc::name_va`] field. + Member { + /// Object index. + object: u16, + /// Index in the object's member array. + member: u16, + /// Variable, `WithEvents` variable or `Implements`. + kind: MemberKind, + }, +} + +/// A name and the pointer to it: see [`VbProject::name_references`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NameReference<'a> { + /// VA of the pointer: the parameter name array entry or the + /// [`MemberDesc`]. + pub pointer_va: u32, + /// VA of the name. + pub name_va: u32, + /// The name's bytes, without the terminating NUL. + pub name: &'a [u8], + /// The structure the pointer belongs to. + pub owner: NameOwner, +} + /// Iterator over the project's external table. /// /// Yields one [`ExternalTableEntry`] per entry of the table at diff --git a/src/vb/control.rs b/src/vb/control.rs index fb4ea34..51baae8 100644 --- a/src/vb/control.rs +++ b/src/vb/control.rs @@ -10,6 +10,7 @@ use std::{fmt, str::FromStr}; use crate::{ + addressmap::AddressMap, error::Error, util::{read_u16_le, read_u32_le}, }; @@ -146,6 +147,52 @@ pub(crate) mod generated { // Event-related types (EventHandlerThunk, NativeEventThunk, EventSinkVtable) +/// What a [`ControlInfo`] entry describes, from its flags at offset 0x00. +/// +/// The flag values of `tests/fixtures` (P-Code and native builds), and the +/// layout of the entry's event sink vtable after its 0x18-byte header: +/// +/// | Flags | Kind | Sink vtable | +/// |-------|------|-------------| +/// | 0x0040 | [`Control`](Self::Control) | one handler slot per event (`controls` `Command1`: slot 0, `Click`, at +0x18) | +/// | 0x002E | [`WithEvents`](Self::WithEvents) of a project class | four `IDispatch` slots, then one slot per event; the slot count at 0x02 is the events only | +/// | 0x0046 | [`WithEvents`](Self::WithEvents) of a dispinterface source (`typerefs` `Watcher.Txt As VB.TextBox`) | one handler slot per event | +/// | 0x007A | [`Implements`](Self::Implements) | four `IDispatch` slots, then one slot per interface member; the slot count at 0x02 includes the four | +/// +/// Bit 0x10 marks an `Implements`, bit 0x04 a `WithEvents` variable, and +/// bits 0x28 a sink that is a dual interface's vtable +/// ([`ControlInfo::dispatch_slots`]): after `QueryInterface`, `AddRef` and +/// `Release` (+0x0C..+0x14) come `GetTypeInfoCount`, `GetTypeInfo`, +/// `GetIDsOfNames` and `Invoke`, import thunks to +/// `Zombie_GetTypeInfoCount`, `Zombie_GetTypeInfo`, +/// `EVENT_SINK_GetIDsOfNames` and `EVENT_SINK_Invoke` (`events` +/// `Listener.m_Source`: 0x0040106E-0x00401080, then the handlers of +/// `Started`, `Ticked` and `Named`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ControlKind { + /// A control, a control array, or the object itself (`Form`, + /// `UserControl`, `Class`). + Control, + /// A `WithEvents` variable. + WithEvents, + /// An `Implements` statement. + Implements, +} + +impl ControlKind { + /// Classifies the flags at ControlInfo offset 0x00. + #[must_use] + pub fn from_flags(flags: u16) -> Self { + if flags & 0x10 != 0 { + Self::Implements + } else if flags & 0x04 != 0 { + Self::WithEvents + } else { + Self::Control + } + } +} + /// View over a ControlInfo structure (0x28 bytes). /// /// Each entry describes one GUI control on a VB6 form. The array is @@ -156,30 +203,33 @@ pub(crate) mod generated { /// /// | Offset | Size | Field | Description | /// |--------|------|-------|-------------| -/// | 0x00 | 2 | `wFlags` | Control flags (always 0x0040 in compiled binaries) | -/// | 0x02 | 2 | `wEventHandlerSlots` | Event handler slot count in the event sink vtable | +/// | 0x00 | 2 | `wFlags` | What the entry describes ([`ControlKind`]): 0x0040 a control or the object itself, 0x002E or 0x0046 a `WithEvents` variable, 0x007A an `Implements` | +/// | 0x02 | 2 | `wEventHandlerSlots` | Slot count of the event sink vtable after its 0x18-byte header (see [`ControlKind`]) | /// | 0x04 | 2 | `wDispatchOffset` | Offset of this control's event sink in the object's instance (the value the event stubs load into eax) | /// | 0x06 | 2 | Reserved | Always 0 | /// | 0x08 | 4 | `lpGuid` | VA of the control's events (source) IID: `TextBoxEvents` `{33AD4EE2-...}`; IID + 1 for a control array of an intrinsic control (CommandButton, TextBox, Label: `tests/fixtures/mdi`); for a hosted ActiveX control or UserControl, the build-generated IID its [`ExternalComponentEntry`](crate::vb::external::ExternalComponentEntry) names (`instance_events_iid`, `array_events_iid`); `FormEvents` / `UserControlEvents` / `IClassModuleEvt` for the object itself; the interface's IID for an `Implements` entry; the source's events IID for a `WithEvents` variable | /// | 0x0C | 2 | `wIndex` | Control index (Name property ID, 0xFFFF = the object itself); a form's or UserControl's vtable has this control's getter at `designer interface size + 4 * index` | /// | 0x0E | 2 | `wMemberType` | Member type constant (always 3 for normal controls, 0xFFFF for default) | -/// | 0x10 | 4 | `wDispIdCount` / `lpDispIdTable` | On disk: 0. At runtime: DISPID dispatch entry count (u16 at +0x10) | -/// | 0x14 | 4 | `lpDispIdTable` | On disk: 0. At runtime: pointer to DISPID→handler dispatch table | +/// | 0x10 | 2 | Number of pairs in the DISPID map ([`dispid_map`](ControlInfo::dispid_map)); 0 for a control | +/// | 0x12 | 2 | 4 or 0x10 when there is a map (unread; 4 for 1 to 3 pairs, 0x10 for 9 in the fixtures) | +/// | 0x14 | 4 | VA of the DISPID map, 0 for a control | /// | 0x18 | 4 | `lpEventSinkVtable` | VA of event sink vtable (0x18 header + slots×4) | /// | 0x1C | 4 | `lpLinkerTypeData` | Per-control-type linker workspace VA (unpatched, not in PE image) | /// | 0x20 | 4 | `lpName` | VA of control name string (null-terminated ANSI) | /// | 0x24 | 4 | `dwControlId` | Packed control identifier: `(wMemberType << 16) \| wIndex` | /// -/// # Runtime Fields (+0x10, +0x14) -/// -/// The DISPID dispatch fields are zeroed in the compiled binary and populated -/// at runtime by `EVENT_SINK_Invoke_Inner` (0x6600FF9C) in MSVBVM60.DLL: -/// - `+0x10` (u16): Number of entries in the DISPID dispatch table -/// - `+0x14` (u32): Pointer to an array of `{u32 DISPID, u32 handler_offset}` pairs +/// # DISPID map (+0x10, +0x14) /// -/// The dispatch table maps COM DISPIDs to event handler offsets. The runtime -/// accesses these fields through the event sink vtable's back-pointer at -/// vtable_base+0x04 (resolved by `EventSink_GetVtableBase` at 0x6600B7CC). +/// A `WithEvents` variable's and an `Implements`' entry name the object +/// procedure that handles each member of the source interface: `{source +/// DISPID, handler MEMID}` pairs, the source DISPID being the event's number +/// for a `WithEvents` variable and the interface member's DISPID for an +/// `Implements` (`events` `Listener`'s `m_Source`: 1 to 0x60030002, 2 to +/// 0x60030003, 3 to 0x60030004). The compiler writes the map right before +/// the entry's event sink vtable. The sink's `Invoke` looks the incoming +/// DISPID up in it and calls the object's own `Invoke` with the handler's +/// MEMID, and its `GetIDsOfNames` maps back (MSVBVM60 6.00.8176 +/// `0x6601846A`, `0x660E2614`). /// /// # Control Identifier (+0x24) /// @@ -223,28 +273,64 @@ impl<'a> ControlInfo<'a> { Ok(Self { bytes }) } - /// Control flags at offset 0x00 (u16, always 0x0040 in compiled binaries). + /// Flags at offset 0x00: what the entry describes (see [`kind`](Self::kind)). /// - /// **Not read by the runtime.** Exhaustive search of MSVBVM60.DLL control - /// consumers (`CreateControlEntry`, `ControlNameHashLookup`, `LoadFormControls`, - /// `FormWrapper_Init`, `InitControlProperties`, `EventSink_GetVtableBase`) - /// confirmed none access this field. Value 0x0040 is compiler/linker metadata - /// indicating a compiled control entry; other values may exist in IDE/debug - /// contexts but are irrelevant for static analysis of compiled binaries. + /// The runtime's control consumers (`CreateControlEntry`, + /// `ControlNameHashLookup`, `LoadFormControls`, `FormWrapper_Init`, + /// `InitControlProperties`, `EventSink_GetVtableBase`) do not read it. #[inline] pub fn flags(&self) -> Result { read_u16_le(self.bytes, 0x00) } - /// Event handler slot count at offset 0x02. + /// What the entry describes, from the [`flags`](Self::flags) at 0x00. /// - /// Varies by control type (e.g., PictureBox=20, TextBox=24, Menu=15, - /// CheckBox=1, OptionButton=31). + /// # Errors + /// + /// Returns an error if the flags cannot be read. + #[inline] + pub fn kind(&self) -> Result { + Ok(ControlKind::from_flags(self.flags()?)) + } + + /// Slot count at offset 0x02: the entries of the event sink vtable + /// after its 0x18-byte header. + /// + /// For a control or a `WithEvents` variable, its events (PictureBox 20, + /// TextBox 24, Menu 15, CheckBox 1, OptionButton 31), which follow the + /// [`dispatch_slots`](Self::dispatch_slots) when there are any; for an + /// `Implements`, the four `IDispatch` slots and the interface's members. + /// See [`ControlKind`] and [`event_count`](Self::event_count). #[inline] pub fn event_handler_slots(&self) -> Result { read_u16_le(self.bytes, 0x02) } + /// Number of `IDispatch` slots the event sink vtable has before its + /// handlers: 4 when it is a dual interface's vtable (flag bits 0x28: a + /// `WithEvents` variable of a project class, an `Implements`), else 0. + /// + /// # Errors + /// + /// Returns an error if the flags cannot be read. + pub fn dispatch_slots(&self) -> Result { + Ok(if self.flags()? & 0x28 == 0x28 { 4 } else { 0 }) + } + + /// Number of handler slots of the event sink vtable: its events, or an + /// `Implements`' members, without the `IDispatch` slots. + /// + /// # Errors + /// + /// Returns an error if the flags or the slot count cannot be read. + pub fn event_count(&self) -> Result { + let slots = self.event_handler_slots()?; + Ok(match self.kind()? { + ControlKind::Implements => slots.saturating_sub(self.dispatch_slots()?), + ControlKind::Control | ControlKind::WithEvents => slots, + }) + } + /// Control type flags at offset 0x00 (u32 view of flags + event_handler_slots). #[inline] pub fn control_type(&self) -> Result { @@ -292,27 +378,64 @@ impl<'a> ControlInfo<'a> { read_u16_le(self.bytes, 0x0E) } - /// DISPID dispatch count / table at offset 0x10. - /// - /// On disk: always 0. - /// At runtime: the low u16 is the number of entries in the DISPID dispatch - /// table, used by `EVENT_SINK_Invoke_Inner` for event handler resolution. + /// Number of pairs in the DISPID map, the u16 at offset 0x10: one per + /// event of a `WithEvents` variable or member of an `Implements`, 0 for + /// a control. #[inline] - pub fn dispid_count_or_zero(&self) -> Result { - read_u32_le(self.bytes, 0x10) + pub fn dispid_map_count(&self) -> Result { + read_u16_le(self.bytes, 0x10) } - /// DISPID dispatch table pointer at offset 0x14. - /// - /// On disk: always 0. - /// At runtime: pointer to an array of `{u32 DISPID, u32 handler_offset}` - /// pairs. `EVENT_SINK_Invoke_Inner` walks this table to find the handler - /// for a given COM DISPID. + /// VA of the DISPID map at offset 0x14, 0 for a control. #[inline] - pub fn dispid_table_va(&self) -> Result { + pub fn dispid_map_va(&self) -> Result { read_u32_le(self.bytes, 0x14) } + /// Reads the DISPID map: for each source interface member, the MEMID of + /// the object procedure that handles it. Empty for a control, or when + /// the map does not read. + pub fn dispid_map(&self, map: &AddressMap<'_>) -> Vec { + let (Ok(count), Ok(va)) = (self.dispid_map_count(), self.dispid_map_va()) else { + return Vec::new(); + }; + let len = usize::from(count).saturating_mul(8); + let Some(data) = map + .slice_from_va(va, len) + .ok() + .and_then(|data| data.get(..len)) + else { + return Vec::new(); + }; + data.as_chunks::<8>() + .0 + .iter() + .filter_map(|pair| { + Some(DispidMapping { + source: read_u32_le(pair, 0).ok()?, + handler: read_u32_le(pair, 4).ok()?, + }) + }) + .collect() + } + + /// Number of slots of the event sink vtable after its 0x18-byte + /// header: [`event_handler_slots`](Self::event_handler_slots), and 4 + /// more for a `WithEvents` variable or an `Implements` (flag bit 0x08). + /// + /// For a `WithEvents` variable the 4 are its `IDispatch` slots; an + /// `Implements`' slot count already includes them and its last 4 slots + /// are zero (the compiler sizes every sink `0x0C + 4 * (slots + 3)`, + /// `+ 7` with bit 0x08: VBA6 6.0.9782 `0x0FA92F98`). + /// + /// # Errors + /// + /// Returns an error if the flags or the slot count cannot be read. + pub fn sink_slots(&self) -> Result { + let extra = if self.flags()? & 0x08 != 0 { 4 } else { 0 }; + Ok(self.event_handler_slots()?.saturating_add(extra)) + } + /// VA of the control's event sink vtable at offset 0x18. /// /// Points to a variable-length structure: @@ -368,6 +491,16 @@ impl<'a> ControlInfo<'a> { } } +/// One pair of a [`ControlInfo`]'s DISPID map. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DispidMapping { + /// The source interface member's DISPID: an event's number, or an + /// implemented interface member's DISPID. + pub source: u32, + /// The MEMID of the object procedure that handles it. + pub handler: u32, +} + /// Iterator over control info entries in an object. /// /// Created from [`OptionalObjectInfo`](crate::vb::object::OptionalObjectInfo) @@ -487,7 +620,8 @@ mod tests { buf[0x08..0x0C].copy_from_slice(&0x00405000u32.to_le_bytes()); // guid_va buf[0x0C..0x0E].copy_from_slice(&1u16.to_le_bytes()); // index buf[0x0E..0x10].copy_from_slice(&3u16.to_le_bytes()); // field_0e - buf[0x10..0x14].copy_from_slice(&0x00406000u32.to_le_bytes()); // event_table_va + buf[0x10..0x12].copy_from_slice(&2u16.to_le_bytes()); // DISPID map pairs + buf[0x14..0x18].copy_from_slice(&0x00406000u32.to_le_bytes()); // DISPID map buf[0x20..0x24].copy_from_slice(&0x00407000u32.to_le_bytes()); // name_va (+0x20) buf } @@ -503,7 +637,8 @@ mod tests { assert_eq!(ctrl.guid_va().unwrap(), 0x00405000); assert_eq!(ctrl.index().unwrap(), 1); assert_eq!(ctrl.member_type().unwrap(), 3); - assert_eq!(ctrl.dispid_count_or_zero().unwrap(), 0x00406000); + assert_eq!(ctrl.dispid_map_count().unwrap(), 2); + assert_eq!(ctrl.dispid_map_va().unwrap(), 0x00406000); assert_eq!(ctrl.name_va().unwrap(), 0x00407000); } diff --git a/src/vb/controlprop.rs b/src/vb/controlprop.rs index 7d3ae93..b881b59 100644 --- a/src/vb/controlprop.rs +++ b/src/vb/controlprop.rs @@ -38,7 +38,11 @@ use std::fmt; -use crate::{error::Error, util::read_u16_le}; +use crate::{ + addressmap::AddressMap, + error::Error, + util::{read_u16_le, read_u32_le}, +}; /// The resource-release action the runtime performs on a member at destruct. /// @@ -237,6 +241,98 @@ impl<'a> ControlPropertyEntry<'a> { self.bytes.get(Self::HEADER_SIZE..).unwrap_or(&[]) } + /// VA of the [`RecordLayout`] of a UDT member (nibble 9), the u32 at + /// entry+0x08; `None` for other types, and for a UDT none of whose + /// members needs init or cleanup, whose entry holds 0 (`records`' + /// local `p As Plain`). + /// + /// Every member of one UDT points to the same layout (`coverage`: + /// `Globals.Rec` members and locals at 0x004021C4). + pub fn record_layout_va(&self) -> Option { + (self.property_type() == ControlPropertyType::Udt) + .then(|| read_u32_le(self.bytes, 0x08).ok()) + .flatten() + .filter(|&va| va != 0 && va != u32::MAX) + } + + /// The raw type byte of an array's elements (nibble 5), at entry+0x08: + /// its low nibble is a [`ControlPropertyType`] (`data`: 0x69, a UDT, + /// for `Dim r(1) As Record`); `None` for other types. + pub fn element_type(&self) -> Option { + (self.property_type() == ControlPropertyType::Array) + .then(|| self.bytes.get(0x08).copied()) + .flatten() + } + + /// VA of the [`RecordLayout`] of an array's UDT elements, the u32 at + /// entry+0x0C; `None` unless the entry is an array of a UDT, and for a + /// UDT none of whose members needs init or cleanup, whose entry holds + /// 0xFFFFFFFF (`records`' `m_Plain(2) As Plain`). + pub fn element_layout_va(&self) -> Option { + let element = self.element_type()?; + (ControlPropertyType::from_raw(element) == ControlPropertyType::Udt) + .then(|| read_u32_le(self.bytes, 0x0C).ok()) + .flatten() + .filter(|&va| va != 0 && va != u32::MAX) + } + + /// Byte offset in the entry of a fixed-size array's inline `SAFEARRAY` + /// descriptor (`cDims`, `fFeatures`, `cbElements`, `cLocks`, `pvData`, + /// then a `{cElements, lLbound}` per dimension): 0x20 when the element + /// type byte has bits 0x60, else 0x10. `None` for a dynamic array + /// (flag bit 0) and for other types. + pub fn safearray_offset(&self) -> Option { + let element = self.element_type()?; + (self.flags() & 0x01 == 0).then_some(if element & 0x60 != 0 { 0x20 } else { 0x10 }) + } + + /// The `fFeatures` of a fixed-size array's `SAFEARRAY` descriptor + /// (`dispid` `Bag.m_Slots(7) As Object`: 0x0452, `FADF_DISPATCH`, + /// `FADF_HAVEIID`, `FADF_FIXEDSIZE`, `FADF_STATIC`). + pub fn safearray_features(&self) -> Option { + let offset = self.safearray_offset()?.checked_add(2)?; + read_u16_le(self.bytes, offset).ok() + } + + /// The u32 that follows a fixed-size array's `SAFEARRAY` descriptor + /// when its `fFeatures` has `FADF_RECORD` (0x20), `FADF_HAVEIID` (0x40) + /// or `FADF_HAVEVARTYPE` (0x80): the entry's last 4 bytes. + fn safearray_extra(&self, feature: u16) -> Option { + let offset = self.safearray_offset()?; + if self.safearray_features()? & feature == 0 { + return None; + } + let dims = usize::from(read_u16_le(self.bytes, offset).ok()?); + let extra = offset + .checked_add(0x10)? + .checked_add(dims.checked_mul(8)?)?; + read_u32_le(self.bytes, extra).ok() + } + + /// VA of the elements' IID of a fixed-size array whose `fFeatures` has + /// `FADF_HAVEIID` (`dispid` `Bag.m_Slots(7) As Object`: + /// `IID_IDispatch`). + pub fn safearray_iid_va(&self) -> Option { + self.safearray_extra(0x40) + } + + /// The elements' `VARTYPE` of a fixed-size array whose `fFeatures` has + /// `FADF_HAVEVARTYPE`: the low word of the u32 after the descriptor + /// (`data` `m_Static(1 To 10) As Integer`: 2, `VT_I2`; the high word + /// holds no value). + pub fn safearray_vartype(&self) -> Option { + self.safearray_extra(0x80) + .and_then(|v| u16::try_from(v & 0xFFFF).ok()) + } + + /// Reads the [`RecordLayout`] a UDT member or an array of UDTs names. + pub fn record_layout<'m>(&self, map: &AddressMap<'m>) -> Option> { + let va = self + .record_layout_va() + .or_else(|| self.element_layout_va())?; + RecordLayout::at(map, va).ok() + } + /// Total size (byte stride to the next entry) of this entry. /// /// Faithful port of `CalcPropertyDataSize` (0x660169D3); the MLIL of @@ -351,6 +447,98 @@ impl<'a> ControlPropertyEntry<'a> { } } +/// The layout of a user-defined type: the members that need init or +/// cleanup, in [`ControlPropertyEntry`] form. +/// +/// Named by a UDT member's entry ([`ControlPropertyEntry::record_layout_va`]) +/// and by an array of UDTs ([`ControlPropertyEntry::element_layout_va`]), +/// in instance tables and procedure cleanup tables alike. +/// +/// | Offset | Size | Field | +/// |--------|------|-------| +/// | 0x00 | 2 | Size of the layout in bytes, header included | +/// | 0x02 | 2 | Size of the UDT | +/// | 0x04 | 2 | Reserved (0 in every fixture) | +/// | 0x06 | 2 | Number of entries | +/// | 0x08 | 2 | Size of the UDT's ANSI form ([`ansi_size`](RecordLayout::ansi_size)) | +/// | 0x0A | 2 | Unknown (0x3C04, 0x3404, 0x3404, 0x2C04) | +/// | 0x0C | var | The entries | +/// +/// `coverage` `Globals.bas`' `Rec` (0x50 bytes) lists `fStr As String` +/// (0x24), `fV As Variant` (0x28), `fO As Object` (0x3C) and `fFx As String +/// * 8` (0x40); its layout is 0x22 bytes. +#[derive(Clone, Copy, Debug)] +pub struct RecordLayout<'a> { + bytes: &'a [u8], +} + +impl<'a> RecordLayout<'a> { + /// Size of the header before the entries. + pub const HEADER_SIZE: usize = 0x0C; + + /// Parses a record layout from the given byte slice. + /// + /// # Errors + /// + /// Returns [`Error::TooShort`] if `data` is shorter than the header or + /// the size at 0x00. + pub fn parse(data: &'a [u8]) -> Result { + let size = usize::from(read_u16_le(data, 0x00)?).max(Self::HEADER_SIZE); + let bytes = data.get(..size).ok_or(Error::TooShort { + expected: size, + actual: data.len(), + context: "RecordLayout", + })?; + Ok(Self { bytes }) + } + + /// Reads the record layout at `va`. + /// + /// # Errors + /// + /// Returns an error if `va` is not mapped or the layout does not fit. + pub fn at(map: &AddressMap<'a>, va: u32) -> Result { + Self::parse(map.slice_from_va(va, Self::HEADER_SIZE)?) + } + + /// Size of the layout in bytes, header included, at offset 0x00. + #[inline] + pub fn size(&self) -> Result { + read_u16_le(self.bytes, 0x00) + } + + /// Size of the UDT at offset 0x02. + #[inline] + pub fn record_size(&self) -> Result { + read_u16_le(self.bytes, 0x02) + } + + /// Size of the UDT's ANSI form at offset 0x08: its size with each + /// `String * N` member taking N bytes instead of 2N, rounded up to 4 + /// (`coverage` `Rec` 0x50 to 0x48, `FileRec` 0x24 to 0x1C). The + /// runtime's Unicode-to-ANSI record conversion copies this many bytes + /// (MSVBVM60 6.00.8176 `0x6605B12D`). + #[inline] + pub fn ansi_size(&self) -> Result { + read_u16_le(self.bytes, 0x08) + } + + /// Number of entries at offset 0x06. + #[inline] + pub fn entry_count(&self) -> Result { + read_u16_le(self.bytes, 0x06) + } + + /// The members that need init or cleanup: offset in the UDT, type and + /// type data. + pub fn entries(&self) -> ControlPropertyIter<'a> { + ControlPropertyIter::new( + self.bytes.get(Self::HEADER_SIZE..).unwrap_or(&[]), + self.entry_count().unwrap_or(0), + ) + } +} + /// Iterator over class/form instance property initialization entries. /// /// Created by [`ClassFormPublicBytes::control_entries`](super::publicbytes::ClassFormPublicBytes::control_entries). diff --git a/src/vb/events.rs b/src/vb/events.rs index 1047233..497ab62 100644 --- a/src/vb/events.rs +++ b/src/vb/events.rs @@ -8,7 +8,7 @@ use std::fmt; -use crate::{addressmap::AddressMap, error::Error, util::read_u32_le}; +use crate::{addressmap::AddressMap, error::Error, util::read_u32_le, vb::control::ControlInfo}; /// Parsed P-Code event handler thunk (20-byte dual-entry method stub). /// @@ -117,10 +117,13 @@ impl fmt::Display for EventHandlerThunk { } } -/// Parsed native event handler thunk (13-byte `this`-adjusting JMP stub). +/// Parsed native adjustor thunk (13-byte `this`-adjusting JMP stub). /// -/// The crate reads natively compiled event handlers as this pattern; no -/// fixture has one, so it is unverified: +/// A natively compiled object enters its procedures through these: an +/// event sink's handler slot names a thunk's first entry, and a method link +/// its bare jump at +8 ([`JUMP_OFFSET`](Self::JUMP_OFFSET)). The compiler +/// also places thunks nothing names, whose adjustment is 0xFFFF +/// (`events-native`). /// /// ```text /// +0x00 81 6C 24 04 XX XX XX XX sub dword [esp+4], this_adjust @@ -138,6 +141,9 @@ impl NativeEventThunk { /// Total size of the native thunk in bytes. pub const SIZE: usize = 13; + /// Offset of the thunk's second entry, its `jmp rel32`. + pub const JUMP_OFFSET: u32 = 8; + /// Parses a native event thunk from the given bytes. pub fn parse(data: &[u8], thunk_va: u32) -> Option { let bytes: &[u8; Self::SIZE] = data.get(..Self::SIZE)?.try_into().ok()?; @@ -212,17 +218,21 @@ impl fmt::Display for IUnknownThunk { /// for each event the object handles (`Private Sub Command1_Click()`); the /// other slots are 0. /// -/// # Layout (variable-length: 0x18 + event_handler_slots * 4) +/// # Layout (variable-length: 0x18 + 4 * [`ControlInfo::sink_slots`]) /// /// | Offset | Field | /// |--------|-------| /// | 0x00 | Reserved (always 0) | -/// | 0x04 | Back-pointer to this control's [`ControlInfo`](crate::vb::control::ControlInfo) entry | +/// | 0x04 | Back-pointer to this control's [`ControlInfo`] entry | /// | 0x08 | Back-pointer to parent [`ObjectInfo`](crate::vb::object::ObjectInfo) | /// | 0x0C | `EVENT_SINK_QueryInterface` thunk VA - `jmp [IAT]` to MSVBVM60 | /// | 0x10 | `EVENT_SINK_AddRef` thunk VA - `jmp [IAT]` to MSVBVM60 | /// | 0x14 | `EVENT_SINK_Release` thunk VA - `jmp [IAT]` to MSVBVM60 | -/// | 0x18+ | Event handler VAs (0 = not connected) | +/// | 0x18 | `IDispatch` thunk VAs ([`dispatch_va`](Self::dispatch_va)), 4 for a `WithEvents` or `Implements` sink, none for a control | +/// | then | Event handler VAs ([`handler_va`](Self::handler_va); 0 = not connected) | +/// +/// [`ControlInfo::dispatch_slots`] says which sinks have the `IDispatch` +/// slots (see [`ControlKind`](crate::vb::control::ControlKind)). /// /// The IUnknown thunks at +0x0C-0x14 are 6-byte `FF 25 imm32` indirect jumps /// through the Import Address Table to MSVBVM60.DLL. All controls in the same @@ -234,6 +244,7 @@ impl fmt::Display for IUnknownThunk { #[derive(Clone, Copy, Debug)] pub struct EventSinkVtable<'a> { bytes: &'a [u8], + dispatch_slots: u16, handler_count: u16, } @@ -241,12 +252,22 @@ impl<'a> EventSinkVtable<'a> { /// Header size before event handler entries. pub const HEADER_SIZE: usize = 0x18; - /// Parses an EventSinkVtable from a byte slice. + /// Parses the event sink vtable of the ControlInfo entry `info` from a + /// byte slice. + /// + /// The entry gives the slot count and whether `IDispatch` slots come + /// first ([`ControlInfo::dispatch_slots`]). + /// + /// # Errors /// - /// `handler_count` is [`ControlInfo::event_handler_slots`](crate::vb::control::ControlInfo::event_handler_slots). - pub fn parse(data: &'a [u8], handler_count: u16) -> Result { - let entries_size = (handler_count as usize).saturating_mul(4); - let total = Self::HEADER_SIZE.saturating_add(entries_size); + /// Returns an error if the entry's fields cannot be read, or + /// [`Error::TooShort`] if `data` is shorter than the header and its + /// slots. + pub fn parse(data: &'a [u8], info: &ControlInfo<'_>) -> Result { + let dispatch_slots = info.dispatch_slots()?; + let handler_count = info.event_count()?; + let total = + Self::HEADER_SIZE.saturating_add(usize::from(info.sink_slots()?).saturating_mul(4)); let bytes = data.get(..total).ok_or(Error::TooShort { expected: total, actual: data.len(), @@ -254,10 +275,36 @@ impl<'a> EventSinkVtable<'a> { })?; Ok(Self { bytes, + dispatch_slots, handler_count, }) } + /// Returns the VA of `IDispatch` slot `index` (0 `GetTypeInfoCount`, + /// 1 `GetTypeInfo`, 2 `GetIDsOfNames`, 3 `Invoke`), or `None` for a + /// sink without them or an index past them. + pub fn dispatch_va(&self, index: u16) -> Option { + if index >= self.dispatch_slots { + return None; + } + let offset = Self::HEADER_SIZE.checked_add((index as usize).checked_mul(4)?)?; + read_u32_le(self.bytes, offset).ok() + } + + /// Number of `IDispatch` slots before the handlers (4 or 0). + #[inline] + pub fn dispatch_slots(&self) -> u16 { + self.dispatch_slots + } + + /// Returns the bytes the vtable occupies: its header and + /// [`ControlInfo::sink_slots`] slots, the `IDispatch` and handler slots + /// and, for an `Implements`, four zero slots after them. + #[inline] + pub fn size(&self) -> usize { + self.bytes.len() + } + /// Back-pointer to this control's ControlInfo entry at +0x04. #[inline] pub fn control_info_va(&self) -> Result { @@ -288,13 +335,15 @@ impl<'a> EventSinkVtable<'a> { read_u32_le(self.bytes, 0x14) } - /// Number of event handler slots. + /// Number of handler slots after the `IDispatch` ones: the events, or + /// an `Implements`' interface members. #[inline] pub fn handler_count(&self) -> u16 { self.handler_count } - /// Returns the VA of the event handler at the given slot index. + /// Returns the VA of the event handler at the given slot index (the + /// event's index in its interface; for an `Implements`, the member's). /// /// Returns 0 if the object does not handle the event. /// Returns `None` if `slot >= handler_count`. @@ -302,7 +351,8 @@ impl<'a> EventSinkVtable<'a> { if slot >= self.handler_count { return None; } - let offset = Self::HEADER_SIZE.checked_add((slot as usize).checked_mul(4)?)?; + let index = usize::from(self.dispatch_slots).checked_add(usize::from(slot))?; + let offset = Self::HEADER_SIZE.checked_add(index.checked_mul(4)?)?; read_u32_le(self.bytes, offset).ok() } diff --git a/src/vb/exports.rs b/src/vb/exports.rs index 2482f02..11e7d97 100644 --- a/src/vb/exports.rs +++ b/src/vb/exports.rs @@ -114,6 +114,14 @@ pub struct ExportSignature { pub return_type: VbParamType, /// Whether this function accepts variable arguments after fixed params. pub variadic: bool, + /// Whether this function never returns to its caller: it raises a VB + /// error on every path (`__vbaErrorOverflow`, `__vbaGenerateBoundsError`, + /// `__vbaError`, `__vbaFPException`, `__vbaFailedFriend`, + /// `rtcAppleScript`), ends the program (`__vbaEnd`, `__vbaStopExe`, both + /// through the error the runtime unwinds on) or the process + /// (`ThunRTMain`). The error raiser ends in a noncontinuable + /// `RaiseException` (MSVBVM60 6.00.8176 `0x66026D91`). + pub noreturn: bool, /// Fixed parameter list (in calling order). pub params: &'static [ExportParam], /// Functional category (e.g., `"free"`, `"string"`, `"variant"`). diff --git a/src/vb/external.rs b/src/vb/external.rs index 5564ed6..34f8056 100644 --- a/src/vb/external.rs +++ b/src/vb/external.rs @@ -882,6 +882,13 @@ impl<'a> ExternalDeclareInfo<'a> { /// Size of the structure in bytes. pub const SIZE: usize = 0x10; + /// Bytes from the descriptor to the call stub the compiler places after + /// it: the descriptor, then 8 bytes nothing reads (0 in every fixture). + /// The stub (`mov eax, [cache + 8]; ...; push ; call + /// `) starts at the descriptor + 0x18 (VBA6 6.0.9782 + /// `0x0FB10155`), the address the module's constant pool names. + pub const BLOCK_SIZE: usize = 0x18; + /// Parses an external declare info from the given byte slice. pub fn parse(data: &'a [u8]) -> Result { let bytes = data.get(..Self::SIZE).ok_or(Error::TooShort { diff --git a/src/vb/formdata.rs b/src/vb/formdata.rs index 3f075fa..5c9e353 100644 --- a/src/vb/formdata.rs +++ b/src/vb/formdata.rs @@ -595,6 +595,32 @@ impl<'a> FormControlRecord<'a> { self.ctype } + /// The ProgID of a hosted control's class as raw bytes: an ActiveX + /// control's (`MSWinsockLib.Winsock`) or a project's own UserControl's + /// (`DispId.Dial`). + /// + /// A hosted control's record has type 0xFF and begins its properties + /// with the ProgID, as a `u16` length, the bytes and a NUL + /// (`tests/fixtures/activex`, `dispid`, `ocx`), whether or not the + /// control has an event handler. It is the + /// [`ExternalComponentEntry::prog_id`](crate::vb::external::ExternalComponentEntry::prog_id) + /// of the control's class. `None` for any other record. + pub fn prog_id_bytes(&self) -> Option<&'a [u8]> { + if self.ctype != FormControlType::Unknown(0xFF) { + return None; + } + let length = usize::from(read_u16_le(self.properties, 0).ok()?); + let end = length.checked_add(2)?; + let bytes = self.properties.get(2..end)?; + (self.properties.get(end) == Some(&0)).then_some(bytes) + } + + /// The ProgID of a hosted control's class + /// ([`prog_id_bytes`](Self::prog_id_bytes)) as a lossy UTF-8 string. + pub fn prog_id(&self) -> Option> { + self.prog_id_bytes().map(String::from_utf8_lossy) + } + /// Raw property stream bytes (opcode+value pairs, excluding the 0xFF terminator). #[inline] pub fn raw_properties(&self) -> &'a [u8] { diff --git a/src/vb/functype.rs b/src/vb/functype.rs index 138d0b2..0633b3f 100644 --- a/src/vb/functype.rs +++ b/src/vb/functype.rs @@ -17,9 +17,7 @@ //! | 0x04 | 2 | `iObjectIndex` - signed; -1 (0xFFFF) = no COM object type reference | //! | 0x06 | 2 | Reserved (always 0) | //! | 0x08 | 4 | `lpOptionalDefaults` - VA to optional param default values header (see below) | -//! | 0x0C | 2 | `wDispId` - the member's DISPID | -//! | 0x0E | 1 | Always 3 in compiled projects (not the return type) | -//! | 0x0F | 1 | `bFuncFlags` - 0x60 for regular Sub/Function, 0x68 for Property | +//! | 0x0C | 4 | `memid` - the member's DISPID: a procedure's index with 0x60030000 (a Sub or Function) or 0x68030000 (a property) above it, an event's number from 1 | //! | 0x10 | 4 | `lpParamNames` - VA to parameter name string pointer array | //! | 0x20 | n | Type list: `this` (0x1E), each parameter, then the return value | //! @@ -49,8 +47,11 @@ use std::fmt; use crate::{ addressmap::AddressMap, error::Error, - util::{read_cstr, read_u16_le, read_u32_le}, - vb::external::VarType, + util::{read_cstr, read_i32_le, read_u16_le, read_u32_le}, + vb::{ + external::VarType, + typeref::{InterfaceRef, RecordRef}, + }, }; /// Property type encoded in the lowest 3 bits of `arg_size`. @@ -248,14 +249,22 @@ impl<'a> FuncTypDesc<'a> { read_u32_le(self.bytes, 0x08) } - /// Method DISPID at offset 0x0C. + /// The member's DISPID, the four bytes at offset 0x0C. /// - /// Used by `ResolveDispatchToFuncTypDesc` (0x6600EFC3) in the runtime - /// for `IDispatch::GetIDsOfNames` resolution. Matches the DISPID that - /// COM clients use to invoke this method. + /// For a procedure, its index in the low word, 3 in the third byte and + /// [`func_flags`](Self::func_flags) in the top one: 0x60030000 plus the + /// index for a Sub or Function, 0x68030000 for each of a property's + /// procedures (`dispid` `Dial`: `Value` 0x68030003, `Spin` 0x60030004). + /// It is what a late-bound caller names the member by (the `LateId*` + /// operands calling `Dial` through its extender), and what + /// `ResolveDispatchToFuncTypDesc` (0x6600EFC3) in the runtime matches + /// for `IDispatch::Invoke`. For an event + /// ([`VbObject::event_type_descs`](crate::project::VbObject::event_type_descs)), + /// its number from 1 in declaration order, the DISPID of its events + /// interface (`ocx` `Knob`: `Turned` 1, `Reset` 2). #[inline] - pub fn dispid(&self) -> Result { - read_u16_le(self.bytes, 0x0C) + pub fn dispid(&self) -> Result { + read_i32_le(self.bytes, 0x0C) } /// Return type: the last type-list entry, when the procedure has one. @@ -271,7 +280,8 @@ impl<'a> FuncTypDesc<'a> { self.type_list().last().copied() } - /// Secondary function flags at offset 0x0F. + /// Secondary function flags at offset 0x0F, the top byte of the + /// [`dispid`](Self::dispid). /// /// **Not read by the runtime.** Exhaustive search of MSVBVM60.DLL /// FuncTypDesc consumers (`ResolveDispatchToFuncTypDesc`, `IDispatchInvoke`, @@ -305,8 +315,11 @@ impl<'a> FuncTypDesc<'a> { /// VA of the parameter name string pointer array at offset 0x10. /// - /// Points to an array of VAs, one per parameter. Each VA points to - /// a null-terminated ANSI parameter name string. + /// Points to an array of [`entry_count`](Self::entry_count) VAs, one per + /// parameter, then one for the return value, which is 0. Each other VA + /// points to a null-terminated ANSI parameter name string. The + /// runtime's `GetIDsOfNames` matches a named argument against every slot + /// (MSVBVM60 6.00.8176 `0x66016D57`). #[inline] pub fn param_names_va(&self) -> Result { read_u32_le(self.bytes, 0x10) @@ -375,7 +388,7 @@ impl<'a> FuncTypDesc<'a> { /// (0x1E), then one per parameter, then the return value. Each entry is /// one [`ArgType`] byte; an array type is followed by padding to a 4-byte /// boundary, and a typed object, record or interface by padding and a - /// 4-byte descriptor VA (see [`ArgType::object_va`]). + /// 4-byte descriptor VA (see [`ArgType::descriptor_va`]). /// /// Empty when the slice the descriptor was parsed from ends before the /// type list. @@ -416,9 +429,24 @@ impl<'a> FuncTypDesc<'a> { .collect() } + /// Returns the bytes the descriptor occupies: the header, `this` and the + /// type list, to the end of its last entry. + /// + /// `None` when the type list is not in the parsed slice. + pub fn size(&self) -> Option { + let (list, end) = self.type_list_and_end(); + (list.len() == usize::from(self.entry_count())).then_some(end) + } + /// Parses the type list's entries after `this`: the parameters, then the /// return value. fn type_list(&self) -> Vec { + self.type_list_and_end().0 + } + + /// Parses the type list's entries after `this`, with the offset past the + /// last one read. + fn type_list_and_end(&self) -> (Vec, usize) { let count = usize::from(self.entry_count()); let mut types = Vec::with_capacity(count); // Entry 0 is `this`; positions are relative to the descriptor. @@ -440,7 +468,7 @@ impl<'a> FuncTypDesc<'a> { }; types.push(t); } - types + (types, pos) } /// Parses optional parameter default values from the defaults area. @@ -641,14 +669,31 @@ impl ArgType { self.code } - /// Returns the VA of the class's ObjectInfo (typed object, 0x13), record - /// descriptor (0x11, 0x14) or interface (0x1C, 0x1D) that follows the - /// entry in the type list. + /// Returns the VA of the descriptor that follows the entry in the type + /// list: the class's `ObjectInfo` for a project class (0x13), an + /// [`InterfaceRef`] for an interface (0x1C, 0x1D), a [`RecordRef`] for a + /// record (0x14). Code 0x11 carries one too; no fixture has it. #[inline] - pub fn object_va(self) -> Option { + pub fn descriptor_va(self) -> Option { (self.descriptor != 0).then_some(self.descriptor) } + /// Reads the [`InterfaceRef`] of an interface type (codes 0x1C, 0x1D): + /// the interface's library and IID. + pub fn interface<'a>(self, map: &AddressMap<'a>) -> Option> { + matches!(self.base_type(), Self::IUNKNOWN | Self::INTERFACE) + .then(|| InterfaceRef::at(map, self.descriptor_va()?).ok()) + .flatten() + } + + /// Reads the [`RecordRef`] of a record type (code 0x14): the record's + /// library and GUID. + pub fn record<'a>(self, map: &AddressMap<'a>) -> Option> { + (self.base_type() == Self::RECORD) + .then(|| RecordRef::at(map, self.descriptor_va()?).ok()) + .flatten() + } + /// Returns the evaluation-stack slots (4 bytes each) a call passes for /// this parameter. /// @@ -677,12 +722,15 @@ impl ArgType { } /// Returns `true` if a descriptor VA follows the entry. - fn has_descriptor(self) -> bool { - matches!(self.base_type(), 0x11 | 0x13 | 0x14 | 0x1C | 0x1D) + pub(crate) fn has_descriptor(self) -> bool { + matches!( + self.base_type(), + Self::UDT | Self::OBJECT | Self::RECORD | Self::IUNKNOWN | Self::INTERFACE + ) } /// Returns the type with the descriptor VA read from `bytes`. - fn with_descriptor(self, bytes: Option<&[u8]>) -> Self { + pub(crate) fn with_descriptor(self, bytes: Option<&[u8]>) -> Self { let descriptor = bytes.and_then(|b| read_u32_le(b, 0).ok()).unwrap_or(0); Self { descriptor, ..self } } @@ -722,8 +770,16 @@ impl ArgType { /// A class of the project (0x13), followed by its ObjectInfo VA. Maps to /// VT_DISPATCH. pub const OBJECT: u8 = 0x13; - /// Record (0x14). Maps to VT_RECORD. + /// Record (0x14), a public user-defined type, followed by its + /// [`RecordRef`] VA. Maps to VT_RECORD. pub const RECORD: u8 = 0x14; + /// An interface of a type library that derives from `IUnknown` alone + /// (0x1C; `typerefs`: `IUnknown`), followed by its [`InterfaceRef`] VA. + pub const IUNKNOWN: u8 = 0x1C; + /// An interface of a type library that derives from `IDispatch` (0x1D; + /// `typerefs`: `Collection`, `StdFont`, `VB.TextBox`), followed by its + /// [`InterfaceRef`] VA. + pub const INTERFACE: u8 = 0x1D; /// Dispatch pointer (0x1E). Internal VB type for ByVal object/string refs. pub const DISPATCH_PTR: u8 = 0x1E; @@ -779,10 +835,10 @@ impl ArgType { Self::STRING => "String", Self::UDT => "UDT", Self::OBJECT => "Class", - 0x1B | 0x1D => "Object", + 0x1B | Self::INTERFACE => "Object", Self::RECORD => "Record", 0x16 => "IDispatch", - 0x1C => "IUnknown", + Self::IUNKNOWN => "IUnknown", Self::DISPATCH_PTR => "DispPtr", _ => "Unknown", } @@ -837,7 +893,7 @@ mod tests { assert_eq!(f.arg_slots(), Some(3)); assert_eq!(f.param_offsets(), vec![0x0C, 0x10, 0x14]); assert_eq!(f.vtable_offset().unwrap(), 0x0028); - assert_eq!(f.dispid().unwrap(), 2); + assert_eq!(f.dispid().unwrap(), 0x6003_0002); assert_eq!(f.kind_keyword(), "Function"); } @@ -891,10 +947,10 @@ mod tests { let f = FuncTypDesc::parse(&data).unwrap(); let args = f.arg_types(); assert_eq!(args.len(), 1); - assert_eq!(args[0].object_va(), Some(0x004014F4)); + assert_eq!(args[0].descriptor_va(), Some(0x004014F4)); assert_eq!(args[0].type_name(), "Class"); assert_eq!( - f.return_type().and_then(|t| t.object_va()), + f.return_type().and_then(|t| t.descriptor_va()), Some(0x004014F4) ); } diff --git a/src/vb/header.rs b/src/vb/header.rs index 10f67ef..a23045b 100644 --- a/src/vb/header.rs +++ b/src/vb/header.rs @@ -4,11 +4,12 @@ //! located via the `push ` instruction at the PE entry point and //! always starts with the `"VB5!"` magic signature. //! -//! Size: `0x68` bytes parsed (104 bytes). The compiler writes a `0x78`-byte -//! fixed part: `0x68` to `0x77` are zero in every fixture, and the strings -//! the `bSZ*` fields at `0x58` to `0x64` point to start at `0x78`, right -//! after it. The `bSZ*` fields are byte offsets from the start of the -//! VBHeader, not VAs. The runtime reads at least one of them: MSVBVM60 +//! Size: `0x78` bytes. The compiler writes the fixed part, then the four +//! strings the `bSZ*` fields at `0x58` to `0x64` point to, each with its NUL +//! and no padding, from `0x78` on. The `bSZ*` fields are byte offsets from +//! the start of the VBHeader, not VAs. The last 16 bytes of the fixed part, +//! `0x68` to `0x77`, are a GUID the compiler always writes as `GUID_NULL` +//! ([`reserved_guid`](VbHeader::reserved_guid)). The runtime reads at least one of them: MSVBVM60 //! 6.00.8176 at `0x660B1B11` adds `bSZProjectExeName` (+0x5C) to the //! VBHeader address and passes the string as the caption of an error //! `MessageBoxA`. @@ -16,11 +17,12 @@ use crate::{ error::Error, util::{read_fixed_cstr, read_u16_le, read_u32_le}, + vb::control::Guid, }; /// View over a VBHeader (EXEPROJECTINFO) structure. /// -/// The VBHeader is 0x68 bytes and begins with `"VB5!"`. It is the top-level +/// The VBHeader is 0x78 bytes and begins with `"VB5!"`. It is the top-level /// structure that the VB6 runtime reads when initializing a VB6 executable. /// /// # Layout @@ -50,6 +52,7 @@ use crate::{ /// | 0x5C | 4 | `bSZProjectExeName` (offset from VBHeader start) | /// | 0x60 | 4 | `bSZProjectHelpFile` (offset from VBHeader start) | /// | 0x64 | 4 | `bSZProjectName` (offset from VBHeader start) | +/// | 0x68 | 16 | A GUID, `GUID_NULL` in every binary (see [`reserved_guid`](Self::reserved_guid)) | #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct VbHeader<'a> { bytes: &'a [u8], @@ -57,7 +60,7 @@ pub struct VbHeader<'a> { impl<'a> VbHeader<'a> { /// Total size of the VBHeader structure in bytes. - pub const SIZE: usize = 0x68; + pub const SIZE: usize = 0x78; /// Expected magic signature at offset 0x00. pub const MAGIC: &'static [u8; 4] = b"VB5!"; @@ -70,11 +73,11 @@ impl<'a> VbHeader<'a> { /// # Arguments /// /// * `data` - Byte slice containing the VBHeader. Only the first - /// `0x68` bytes are used; additional bytes are ignored. + /// `0x78` bytes are used; additional bytes are ignored. /// /// # Errors /// - /// - [`Error::TooShort`] if `data.len() < 0x68`. + /// - [`Error::TooShort`] if `data.len() < 0x78`. /// - [`Error::BadMagic`] if the first 4 bytes are not `"VB5!"`. pub fn parse(data: &'a [u8]) -> Result { let bytes = data.get(..Self::SIZE).ok_or(Error::TooShort { @@ -296,6 +299,15 @@ impl<'a> VbHeader<'a> { pub fn project_name_offset(&self) -> Result { read_u32_le(self.bytes, 0x64) } + + /// The GUID at offset 0x68: `GUID_NULL` in every binary. + /// + /// The compiler copies its `IID_NULL` here when it writes the header + /// (VB6.EXE 6.00.8176 `0x0045A2B0`); no runtime read of it was found. + /// `None` when the bytes do not read as a GUID. + pub fn reserved_guid(&self) -> Option { + Guid::from_bytes(self.bytes.get(0x68..0x78)?) + } } #[cfg(test)] diff --git a/src/vb/member.rs b/src/vb/member.rs new file mode 100644 index 0000000..c7fb124 --- /dev/null +++ b/src/vb/member.rs @@ -0,0 +1,249 @@ +//! Member descriptors: an object's public variables and implemented +//! interfaces. +//! +//! [`PrivateObjectDescriptor::member_descs_va`](super::privateobj::PrivateObjectDescriptor::member_descs_va) +//! (+0x20) points to an array of +//! [`member_count`](super::privateobj::PrivateObjectDescriptor::member_count) +//! VAs, one per module-level variable and per `Implements`. The entry of a +//! `Public` variable or an `Implements` points to a [`MemberDesc`]; the entry +//! of a `Private` variable (`WithEvents` or not) is 0. +//! +//! # Order +//! +//! The order of the object's vtable groups (see +//! [`MethodLink`](crate::project::MethodLink)): the variables in declaration +//! order (`Private` ones as null entries), then the `Implements`, then the +//! `WithEvents` variables. `members` `Mixed` declares `Value`, +//! `Implements IFirst`, `Public WithEvents Shown`, `Private WithEvents +//! Hidden`, `Note`, and its array is `Value`, `Note`, `IFirst`, `Shown`, 0; +//! `calls` `Square` declares `Implements Shape` before `Private m_Side`, and +//! its array is 0, `Shape`. +//! +//! # Layout (0x1C bytes, 0x20 with a type reference) +//! +//! | Offset | Size | Field | +//! |--------|------|-------| +//! | 0x00 | 4 | `lpName` - VA of the null-terminated name | +//! | 0x04 | 4 | Reserved (0 in every fixture) | +//! | 0x08 | 4 | 0xFFFFFFFF for an `Implements` or a `WithEvents` variable, 0 otherwise | +//! | 0x0C | 4 | `memid` - the variable's DISPID (`0x40030000` + index); 0xFFFFFFFF for an `Implements` | +//! | 0x10 | 2 | Flags (2 in every fixture) | +//! | 0x12 | 2 | vtable byte offset of the variable's first accessor; 0xFFFF for an `Implements` | +//! | 0x14 | 4 | Byte offset of the variable in the instance; 0xFFFFFFFF for an `Implements` | +//! | 0x18 | 4 | Type code, an [`ArgType`] code | +//! | 0x1C | 4 | The type's descriptor VA, present only for the [`ArgType`] codes that carry one | +//! +//! `members` `Holder` declares thirteen `Public` variables `B As Byte` ... +//! `Auto As New Collection`: their memids are `0x40030000` to `0x4003000C`, +//! their accessors at vtable 0x1C (`B`) to 0x88 (`Auto`), their instance +//! offsets 0x34 (`B`) to 0x78 (`Auto`), the offsets the accessors' +//! [`MethodLinkKind::Variable`](crate::project::MethodLinkKind::Variable) +//! entries add. + +use crate::{ + addressmap::AddressMap, + error::Error, + util::{read_cstr, read_u16_le, read_u32_le}, + vb::functype::ArgType, +}; + +/// What a [`MemberDesc`] declares. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MemberKind { + /// A `Public` variable. + Variable, + /// A `Public WithEvents` variable. + WithEvents, + /// An `Implements` statement; the [`MemberDesc::var_type`] names the + /// implemented interface. + Implements, +} + +/// View over a member descriptor (0x1C bytes, 0x20 with a type reference). +/// +/// See the [module documentation](self) for the layout. +#[derive(Clone, Copy, Debug)] +pub struct MemberDesc<'a> { + bytes: &'a [u8], +} + +impl<'a> MemberDesc<'a> { + /// Size of a descriptor without the type reference at 0x1C. + pub const MIN_SIZE: usize = 0x1C; + + /// Parses a member descriptor from the given byte slice. + /// + /// Pass at least 0x20 bytes so [`var_type`](Self::var_type) can read the + /// type reference of a typed object. + /// + /// # Errors + /// + /// Returns [`Error::TooShort`] if `data.len() < 0x1C`. + pub fn parse(data: &'a [u8]) -> Result { + if data.len() < Self::MIN_SIZE { + return Err(Error::TooShort { + expected: Self::MIN_SIZE, + actual: data.len(), + context: "MemberDesc", + }); + } + Ok(Self { bytes: data }) + } + + /// VA of the member's null-terminated name, at offset 0x00. + #[inline] + pub fn name_va(&self) -> Result { + read_u32_le(self.bytes, 0x00) + } + + /// Reads the member's name through `map`. + /// + /// # Errors + /// + /// Returns an error if the name VA cannot be read or resolved. + pub fn name<'b>(&self, map: &AddressMap<'b>) -> Result<&'b [u8], Error> { + let off = map.va_to_offset(self.name_va()?)?; + read_cstr(map.file(), off) + } + + /// The DWORD at offset 0x08: 0xFFFFFFFF for an `Implements` and a + /// `WithEvents` variable (`members` `Mixed.Shown`), 0 for every other + /// variable. + #[inline] + pub fn sink_raw(&self) -> Result { + read_u32_le(self.bytes, 0x08) + } + + /// The member's DISPID at offset 0x0C, `None` for an `Implements`. + /// + /// `0x40030000` plus the variable's index among the object's public + /// variables, in the order of the member array (`members` `Mixed`: + /// `Value` 0, `Note` 1, `Shown` 2). + pub fn memid(&self) -> Result, Error> { + let memid = read_u32_le(self.bytes, 0x0C)?; + Ok((memid != u32::MAX).then_some(memid)) + } + + /// The u16 flags at offset 0x10 (2 for every descriptor in + /// `tests/fixtures`). + #[inline] + pub fn flags(&self) -> Result { + read_u16_le(self.bytes, 0x10) + } + + /// The vtable byte offset of the variable's first accessor (its `Get`), + /// at offset 0x12; `None` for an `Implements`. + /// + /// The `Let` and `Set` accessors follow in the next slots (see + /// [`MethodLink`](crate::project::MethodLink)). + pub fn vtable_offset(&self) -> Result, Error> { + let offset = read_u16_le(self.bytes, 0x12)?; + Ok((offset != u16::MAX).then_some(offset)) + } + + /// Byte offset of the variable in an instance of the object, at offset + /// 0x14; `None` for an `Implements`. + pub fn instance_offset(&self) -> Result, Error> { + let offset = read_u32_le(self.bytes, 0x14)?; + Ok((offset != u32::MAX).then_some(offset)) + } + + /// The member's type, from the code at offset 0x18 and, for a typed + /// object, record or interface, the descriptor VA at 0x1C. + /// + /// For a variable of a project class (0x13) and for an `Implements` + /// (also 0x13), [`ArgType::descriptor_va`] is that class's `ObjectInfo` VA: + /// `members` `Holder.Peer As Holder` gives `Holder`'s, `Two`'s + /// `Implements IFirst` gives `IFirst`'s. For an interface of a type + /// library, [`ArgType::interface`] reads its library and IID (`members` + /// `Holder.Auto As New Collection`: VBA's `_Collection`). + /// + /// # Errors + /// + /// Returns an error if the type code cannot be read. + pub fn var_type(&self) -> Result { + let code = u8::try_from(read_u32_le(self.bytes, 0x18)? & 0xFF).unwrap_or(0); + let t = ArgType::new(code); + Ok(if t.has_descriptor() { + t.with_descriptor(self.bytes.get(0x1C..0x20)) + } else { + t + }) + } + + /// Returns the bytes the descriptor occupies: 0x20 when its type carries + /// a descriptor VA, else [`MIN_SIZE`](Self::MIN_SIZE). + /// + /// # Errors + /// + /// Returns an error if the type code cannot be read. + pub fn size(&self) -> Result { + Ok(if self.var_type()?.has_descriptor() { + Self::MIN_SIZE.saturating_add(4) + } else { + Self::MIN_SIZE + }) + } + + /// Classifies the member: an `Implements` has no DISPID, a `WithEvents` + /// variable a non-zero [`sink_raw`](Self::sink_raw). + /// + /// # Errors + /// + /// Returns an error if the DISPID or the field at 0x08 cannot be read. + pub fn kind(&self) -> Result { + Ok(if self.memid()?.is_none() { + MemberKind::Implements + } else if self.sink_raw()? != 0 { + MemberKind::WithEvents + } else { + MemberKind::Variable + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // `members` Holder.Peer As Holder (VA 0x0040309C). + const PEER: [u8; 0x20] = [ + 0xE4, 0x3E, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x03, + 0x40, 0x02, 0x00, 0x7C, 0x00, 0x74, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0xF4, 0x1E, + 0x40, 0x00, + ]; + + // `members` Two's `Implements IFirst` (VA 0x00402FDC). + const IFIRST: [u8; 0x20] = [ + 0x08, 0x3F, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x02, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x13, 0x00, 0x00, 0x00, 0x08, 0x16, + 0x40, 0x00, + ]; + + #[test] + fn test_variable() { + let m = MemberDesc::parse(&PEER).unwrap(); + assert_eq!(m.name_va().unwrap(), 0x00403EE4); + assert_eq!(m.memid().unwrap(), Some(0x4003000B)); + assert_eq!(m.flags().unwrap(), 2); + assert_eq!(m.vtable_offset().unwrap(), Some(0x7C)); + assert_eq!(m.instance_offset().unwrap(), Some(0x74)); + assert_eq!(m.var_type().unwrap().descriptor_va(), Some(0x00401EF4)); + assert_eq!(m.kind().unwrap(), MemberKind::Variable); + } + + #[test] + fn test_implements() { + let m = MemberDesc::parse(&IFIRST).unwrap(); + assert_eq!(m.memid().unwrap(), None); + assert_eq!(m.vtable_offset().unwrap(), None); + assert_eq!(m.instance_offset().unwrap(), None); + assert_eq!(m.var_type().unwrap().descriptor_va(), Some(0x00401608)); + assert_eq!(m.kind().unwrap(), MemberKind::Implements); + } + + #[test] + fn test_parse_too_short() { + assert!(MemberDesc::parse(&[0u8; 0x1B]).is_err()); + } +} diff --git a/src/vb/mod.rs b/src/vb/mod.rs index 1252870..bc8a7ed 100644 --- a/src/vb/mod.rs +++ b/src/vb/mod.rs @@ -11,7 +11,7 @@ //! //! ```text //! PE Entry Point -//! └─ push → VbHeader (0x68 bytes) +//! └─ push → VbHeader (0x78 bytes) //! ├─ lpProjectData → ProjectData (0x23C bytes) //! │ ├─ lpObjectTable → ObjectTable (0x54 bytes) //! │ │ └─ lpObjectArray → PublicObjectDescriptor[] (0x30 each) @@ -20,7 +20,8 @@ //! │ │ │ ├─ lpConstants → Constant pool //! │ │ │ └─ lpPrivateObject → PrivateObjectDescriptor (0x40 bytes) //! │ │ │ ├─ lpFuncTypDescs → FuncTypDesc pointer array -//! │ │ │ └─ lpParamNames → Parameter name strings +//! │ │ │ ├─ lpMemberDescs → MemberDesc pointer array +//! │ │ │ └─ lpEventDescs → event FuncTypDesc pointer array //! │ │ └─ [if flag 0x01] → OptionalObjectInfo (0x40 bytes) //! │ │ ├─ lpControls → ControlInfo[] (0x28 each) //! │ │ └─ wPCodeCount, event offsets @@ -66,12 +67,15 @@ pub mod formdata; pub mod functype; pub mod guitable; pub mod header; +pub mod member; +pub mod native; pub mod object; pub mod objecttable; +pub mod pooldesc; pub mod privateobj; pub mod procedure; pub mod projectdata; pub mod projectinfo2; pub mod property; pub mod publicbytes; -pub mod varstub; +pub mod typeref; diff --git a/src/vb/native.rs b/src/vb/native.rs new file mode 100644 index 0000000..f35fc49 --- /dev/null +++ b/src/vb/native.rs @@ -0,0 +1,497 @@ +//! Structures of natively compiled procedures. +//! +//! A native procedure that releases anything when it unwinds (an error +//! raised inside it, or in what it calls), or that handles errors, stores +//! the VA of a [`ProcUnwindInfo`] in its frame in its prologue, after +//! installing `__vbaExceptHandler` as its SEH handler: +//! +//! ```x86asm +//! push ebp +//! mov ebp, esp +//! sub esp, ... +//! push offset __vbaExceptHandler +//! mov eax, fs:[0] +//! push eax +//! mov fs:[0], esp +//! ... +//! mov [ebp-8], esp +//! mov dword ptr [ebp-4], offset ProcUnwindInfo ; C7 45 FC imm32 +//! ``` +//! +//! The record names the frame slot it is stored in: `ebp-4`, `ebp-8` in a +//! class method and in some module procedures (`C7 45 F8 imm32`), `ebp-0x10` +//! in a procedure with an `On Error GoTo` handler (`C7 45 F0 imm32`), and +//! `ebp-0x14` in one that also uses `Resume`, `Resume Next`, +//! `On Error Resume Next` or `Erl` (`C7 45 EC imm32`), which numbers its +//! statements in `ebp-4` as it runs them. The words between the slot and the +//! registration are the handler's state: `ebp-slot+4` its flags, +//! `ebp-slot+8` the number of the active handler (`__vbaOnError`'s +//! argument). + +use std::ops::Range; + +use crate::{ + addressmap::AddressMap, + error::Error, + util::{read_u16_le, read_u32_le}, +}; + +/// The unwind record of a native procedure (8, 16, 24 or 28 bytes): the +/// code that releases what the procedure holds when it unwinds, and the +/// tables its error handling reads. +/// +/// | Offset | Size | Field | +/// |--------|------|-------| +/// | 0x00 | 2 | Flags: bit 0x01 [`exit_va`](Self::exit_va), bit 0x02 [`locals_va`](Self::locals_va), bits 0x04 or 0x08 [`temps_va`](Self::temps_va), bit 0x10 [`handlers_va`](Self::handlers_va), bit 0x20 [`resume_va`](Self::resume_va), bit 0x40 [`lines_va`](Self::lines_va); bit 0x80 set on some class methods (`events-native`), meaning unmeasured | +/// | 0x02 | 2 | Frame slot: the record's VA is stored at `ebp - slot` (4, 8, 0x10 or 0x14) | +/// | 0x04 | 4 | Exit block VA (bit 0x01), else 0 | +/// | 0x08 | 4 | Locals routine VA (bit 0x02), else 0 | +/// | 0x0C | 4 | Temporaries routine VA (bit 0x04 or 0x08), else 0 | +/// | 0x10 | 4 | [`ErrorHandlerTable`] VA (bit 0x10), else 0 | +/// | 0x14 | 4 | [`ResumeTable`] VA (bit 0x20), else 0 | +/// | 0x18 | 4 | [`LineNumberTable`] VA (bit 0x40) | +/// +/// The record runs to the last field its flags use: 28 bytes with bit 0x40, +/// 24 with bit 0x10 or 0x20, 16 with any of bits 0x0E, else 8 (a class +/// method that holds only `Me`: `events-native` `Source`'s methods, flags +/// 0x01). Its tables follow it in the order resume, handlers, lines, each +/// at the next multiple of 8. +/// +/// The runtime enters every address the record names in the procedure's +/// own frame, with `ebp` and `esp` restored from it. The locals routine +/// (freeing the procedure's `String`, object and `Variant` variables: +/// `__vbaFreeStr`, `__vbaFreeObjList`, `__vbaFreeVarList`) and the +/// temporaries routine (freeing the current statement's) are called, and +/// end in `ret`; the runtime calls the temporaries routine on every unwind +/// through the procedure with bit 0x04, only on the one that leaves it with +/// bit 0x08, and the locals routine when the unwind leaves it. The exit +/// block, a handler and a `Resume` target are jumped to. The exit block is +/// a class method's epilogue, which its own code also runs into: it +/// releases `Me` (`mov eax, [ebp+8]; push eax; call [ecx+8]`) and returns +/// the word at `ebp-slot+4`, where the runtime stores the error when one +/// leaves the method with no handler active. The records sit together in +/// `.text`, among the native code's floating-point constants, in no table. +#[derive(Clone, Copy, Debug)] +pub struct ProcUnwindInfo<'a> { + bytes: &'a [u8], +} + +impl<'a> ProcUnwindInfo<'a> { + /// How far into a procedure [`from_prologue`](Self::from_prologue) + /// looks for the store of the record's VA. + pub const PROLOGUE_SCAN: usize = 0x60; + + /// The frame slots a record can name: `ebp-4`, `ebp-8`, `ebp-0x10` and + /// `ebp-0x14`. + pub const FRAME_SLOTS: [u16; 4] = [0x04, 0x08, 0x10, 0x14]; + + /// Parses an unwind record from the given byte slice. + /// + /// # Errors + /// + /// Returns [`Error::TooShort`] if `data` is shorter than the record its + /// flags call for. + pub fn parse(data: &'a [u8]) -> Result { + let flags = read_u16_le(data, 0)?; + let size = if flags & 0x40 != 0 { + 0x1C + } else if flags & 0x30 != 0 { + 0x18 + } else if flags & 0x0E != 0 { + 0x10 + } else { + 0x08 + }; + let bytes = data.get(..size).ok_or(Error::TooShort { + expected: size, + actual: data.len(), + context: "ProcUnwindInfo", + })?; + Ok(Self { bytes }) + } + + /// Reads the unwind record of the native procedure that starts at + /// `proc_va` with `push ebp; mov ebp, esp` (`55 8B EC`): the first + /// `mov [ebp-slot], imm32` (`C7 45 disp8 imm32`) in its first + /// [`PROLOGUE_SCAN`](Self::PROLOGUE_SCAN) bytes, `slot` one of + /// [`FRAME_SLOTS`](Self::FRAME_SLOTS), whose immediate is a record + /// naming that slot. Returns the record's VA and the record; `None` if + /// the code is not such a procedure or it stores none. + pub fn from_prologue(map: &AddressMap<'a>, proc_va: u32) -> Option<(u32, Self)> { + let code = map.slice_from_va(proc_va, 3).ok()?; + if !code.starts_with(&[0x55, 0x8B, 0xEC]) { + return None; + } + let code = code.get(..code.len().min(Self::PROLOGUE_SCAN))?; + code.windows(7) + .find_map(|window| Self::stored_by(map, window)) + } + + /// The record `instruction` stores, when it is a `mov [ebp-slot], + /// imm32` whose immediate is a record naming that slot. + fn stored_by(map: &AddressMap<'a>, instruction: &[u8]) -> Option<(u32, Self)> { + let [0xC7, 0x45, disp, a, b, c, d] = *instruction else { + return None; + }; + let slot = 0x100u16.checked_sub(u16::from(disp))?; + if !Self::FRAME_SLOTS.contains(&slot) { + return None; + } + let va = u32::from_le_bytes([a, b, c, d]); + let info = Self::parse(map.slice_from_va(va, 8).ok()?).ok()?; + (info.frame_slot().ok()? == slot).then_some((va, info)) + } + + /// Size of the record: 28, 24, 16 or 8 bytes, by its flags. + #[inline] + pub fn size(&self) -> usize { + self.bytes.len() + } + + /// The flags at offset 0x00. + #[inline] + pub fn flags(&self) -> Result { + read_u16_le(self.bytes, 0x00) + } + + /// The frame slot at offset 0x02: the record's VA is at `ebp - slot`. + #[inline] + pub fn frame_slot(&self) -> Result { + read_u16_le(self.bytes, 0x02) + } + + /// Reads the VA at `offset` when flag `mask` is set. + fn va_at(&self, mask: u16, offset: usize) -> Option { + if self.flags().ok()? & mask == 0 { + return None; + } + read_u32_le(self.bytes, offset).ok().filter(|&va| va != 0) + } + + /// VA of a class method's exit block (flag 0x01): its epilogue, which + /// returns the error the runtime stores when one leaves the method. + pub fn exit_va(&self) -> Option { + self.va_at(0x01, 0x04) + } + + /// VA of the routine that frees the procedure's variables (flag 0x02). + pub fn locals_va(&self) -> Option { + self.va_at(0x02, 0x08) + } + + /// VA of the routine that frees the current statement's temporaries + /// (flag 0x04 or 0x08). + pub fn temps_va(&self) -> Option { + self.va_at(0x0C, 0x0C) + } + + /// VA of the procedure's [`ErrorHandlerTable`] (flag 0x10): the labels + /// its `On Error GoTo` statements name. + pub fn handlers_va(&self) -> Option { + self.va_at(0x10, 0x10) + } + + /// VA of the procedure's [`ResumeTable`] (flag 0x20): its statements' + /// addresses, for `Resume` and `Resume Next`. + pub fn resume_va(&self) -> Option { + self.va_at(0x20, 0x14) + } + + /// VA of the procedure's [`LineNumberTable`] (flag 0x40): its + /// statements' line numbers, for `Erl`. + pub fn lines_va(&self) -> Option { + self.va_at(0x40, 0x18) + } + + /// Reads the procedure's [`ErrorHandlerTable`], when it has one. + pub fn handlers(&self, map: &AddressMap<'a>) -> Option> { + let va = self.handlers_va()?; + ErrorHandlerTable::parse(map.slice_from_va(va, 4).ok()?).ok() + } + + /// Reads the procedure's [`ResumeTable`], when it has one. + pub fn resume(&self, map: &AddressMap<'a>) -> Option> { + let va = self.resume_va()?; + ResumeTable::parse(map.slice_from_va(va, 4).ok()?).ok() + } + + /// Reads the procedure's [`LineNumberTable`], when it has one. + pub fn lines(&self, map: &AddressMap<'a>) -> Option> { + let va = self.lines_va()?; + LineNumberTable::parse(map.slice_from_va(va, 2).ok()?).ok() + } + + /// Every code address the record names: the exit block, the two + /// routines, each handler and each statement a `Resume` can go to. + pub fn code_vas(&self, map: &AddressMap<'a>) -> Vec { + let mut code: Vec = [self.exit_va(), self.locals_va(), self.temps_va()] + .into_iter() + .flatten() + .collect(); + if let Some(handlers) = self.handlers(map) { + code.extend(handlers.iter().map(|handler| handler.va)); + } + if let Some(resume) = self.resume(map) { + code.extend(resume.iter()); + } + code.sort_unstable(); + code.dedup(); + code + } +} + +/// One `On Error GoTo` label of a native procedure. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ErrorHandler { + /// The handler's number: what the procedure passes `__vbaOnError` to + /// make it the active one (from 1, in the order the labels are first + /// named). + pub number: u32, + /// VA of the label's code. + pub va: u32, +} + +/// The labels a native procedure's `On Error GoTo` statements name +/// ([`ProcUnwindInfo::handlers_va`]). +/// +/// | Offset | Size | Field | +/// |--------|------|-------| +/// | 0x00 | 4 | Count | +/// | 0x04 | 8 x count | `{u32 number; u32 va}` per handler ([`ErrorHandler`]) | +/// +/// On an error the runtime looks up the active handler's number and jumps +/// to its label in the procedure's frame. +#[derive(Clone, Copy, Debug)] +pub struct ErrorHandlerTable<'a> { + bytes: &'a [u8], +} + +impl<'a> ErrorHandlerTable<'a> { + /// Parses the table from the given byte slice. + /// + /// # Errors + /// + /// Returns [`Error::TooShort`] if `data` is shorter than its count + /// calls for, or [`Error::ArithmeticOverflow`] if the count is too + /// large to size. + pub fn parse(data: &'a [u8]) -> Result { + let count = read_u32_le(data, 0)? as usize; + let size = count + .checked_mul(8) + .and_then(|entries| entries.checked_add(4)) + .ok_or(Error::ArithmeticOverflow { + context: "ErrorHandlerTable", + })?; + let bytes = data.get(..size).ok_or(Error::TooShort { + expected: size, + actual: data.len(), + context: "ErrorHandlerTable", + })?; + Ok(Self { bytes }) + } + + /// Size of the table in bytes. + #[inline] + pub fn size(&self) -> usize { + self.bytes.len() + } + + /// Number of handlers. + #[inline] + pub fn count(&self) -> usize { + self.bytes.len().saturating_sub(4) / 8 + } + + /// The handlers, in table order. + pub fn iter(&self) -> impl Iterator + 'a { + let bytes = self.bytes; + (0..self.count()).filter_map(move |index| { + let at = index.checked_mul(8)?.checked_add(4)?; + Some(ErrorHandler { + number: read_u32_le(bytes, at).ok()?, + va: read_u32_le(bytes, at.checked_add(4)?).ok()?, + }) + }) + } +} + +/// The address of each statement of a native procedure, for `Resume` and +/// `Resume Next` ([`ProcUnwindInfo::resume_va`]). +/// +/// | Offset | Size | Field | +/// |--------|------|-------| +/// | 0x00 | 4 | Count | +/// | 0x04 | 4 x count | VA of statement 1, 2, ... | +/// | 4 x (count + 1) | 4 | 0 | +/// +/// The procedure stores the number of the statement it is running in +/// `ebp-4`; `Resume` jumps to word `n` of the table (the statement that +/// failed), `Resume Next` to word `n + 1`, the closing 0 after the last +/// statement. A statement that compiles to no code (a label) shares the +/// next one's address. +#[derive(Clone, Copy, Debug)] +pub struct ResumeTable<'a> { + bytes: &'a [u8], +} + +impl<'a> ResumeTable<'a> { + /// Parses the table from the given byte slice. + /// + /// # Errors + /// + /// Returns [`Error::TooShort`] if `data` is shorter than its count + /// calls for, or [`Error::ArithmeticOverflow`] if the count is too + /// large to size. + pub fn parse(data: &'a [u8]) -> Result { + let count = read_u32_le(data, 0)? as usize; + let size = count + .checked_add(2) + .and_then(|words| words.checked_mul(4)) + .ok_or(Error::ArithmeticOverflow { + context: "ResumeTable", + })?; + let bytes = data.get(..size).ok_or(Error::TooShort { + expected: size, + actual: data.len(), + context: "ResumeTable", + })?; + Ok(Self { bytes }) + } + + /// Size of the table in bytes, the closing 0 included. + #[inline] + pub fn size(&self) -> usize { + self.bytes.len() + } + + /// Number of statements. + #[inline] + pub fn count(&self) -> usize { + (self.bytes.len() / 4).saturating_sub(2) + } + + /// VA of statement `number` (from 1); `None` outside the table. + pub fn statement_va(&self, number: usize) -> Option { + if number == 0 || number > self.count() { + return None; + } + read_u32_le(self.bytes, number.checked_mul(4)?).ok() + } + + /// The statements' VAs, statement 1 first. + pub fn iter(&self) -> impl Iterator + 'a { + let table = *self; + (1..=self.count()).filter_map(move |number| table.statement_va(number)) + } +} + +/// The line number of each statement of a native procedure, for `Erl` +/// ([`ProcUnwindInfo::lines_va`]). +/// +/// | Offset | Size | Field | +/// |--------|------|-------| +/// | 0x00 | 2 | Count | +/// | 0x02 | 2 x (count + 1) | Line number of statement 0, 1, ..., count | +/// +/// Indexed by the statement number the procedure stores in `ebp-4`, from +/// statement 0 (before the first, line 0). A statement with no line number +/// of its own has the last one before it; a numbered line is two +/// statements (its label and its code), both with its number. +#[derive(Clone, Copy, Debug)] +pub struct LineNumberTable<'a> { + bytes: &'a [u8], +} + +impl<'a> LineNumberTable<'a> { + /// Parses the table from the given byte slice. + /// + /// # Errors + /// + /// Returns [`Error::TooShort`] if `data` is shorter than its count + /// calls for. + pub fn parse(data: &'a [u8]) -> Result { + let count = usize::from(read_u16_le(data, 0)?); + let size = count.saturating_add(2).saturating_mul(2); + let bytes = data.get(..size).ok_or(Error::TooShort { + expected: size, + actual: data.len(), + context: "LineNumberTable", + })?; + Ok(Self { bytes }) + } + + /// Size of the table in bytes. + #[inline] + pub fn size(&self) -> usize { + self.bytes.len() + } + + /// Number of statements (statement 0 not counted). + #[inline] + pub fn count(&self) -> usize { + (self.bytes.len() / 2).saturating_sub(2) + } + + /// Line number of statement `number` (0 to [`count`](Self::count)); + /// `None` outside the table. + pub fn line(&self, number: usize) -> Option { + if number > self.count() { + return None; + } + read_u16_le(self.bytes, number.checked_mul(2)?.checked_add(2)?).ok() + } +} + +/// A native procedure and the unwind record its prologue stores +/// ([`VbProject::unwind_records`](crate::VbProject::unwind_records)). +#[derive(Clone, Copy, Debug)] +pub struct ProcedureUnwind<'a> { + /// VA of the procedure's first instruction (`push ebp`). + pub procedure_va: u32, + /// VA of its record. + pub record_va: u32, + /// The record. + pub info: ProcUnwindInfo<'a>, +} + +impl<'a> ProcedureUnwind<'a> { + /// Finds every procedure in `code` that stores an unwind record: each + /// `mov [ebp-slot], imm32` there whose immediate is a record naming + /// that slot, paired with the nearest `push ebp; mov ebp, esp` at most + /// [`ProcUnwindInfo::PROLOGUE_SCAN`] bytes before it whose + /// [`from_prologue`](ProcUnwindInfo::from_prologue) is that store. + /// Ordered by procedure; empty when `code` does not read. + pub fn scan(map: &AddressMap<'a>, code: Range) -> Vec { + let Some(bytes) = code.end.checked_sub(code.start).and_then(|len| { + map.slice_from_va(code.start, len as usize) + .ok()? + .get(..len as usize) + }) else { + return Vec::new(); + }; + let mut found: Vec = Vec::new(); + for (offset, window) in bytes.windows(7).enumerate() { + let Some((record_va, _)) = ProcUnwindInfo::stored_by(map, window) else { + continue; + }; + let earliest = offset + .saturating_add(7) + .saturating_sub(ProcUnwindInfo::PROLOGUE_SCAN); + let procedure = (earliest..=offset).rev().find_map(|start| { + if !bytes.get(start..)?.starts_with(&[0x55, 0x8B, 0xEC]) { + return None; + } + let procedure_va = code.start.checked_add(u32::try_from(start).ok()?)?; + let (stored, info) = ProcUnwindInfo::from_prologue(map, procedure_va)?; + (stored == record_va).then_some(Self { + procedure_va, + record_va, + info, + }) + }); + found.extend(procedure); + } + found.dedup_by_key(|procedure| procedure.procedure_va); + found + } +} diff --git a/src/vb/object.rs b/src/vb/object.rs index 16894f2..a8f21a8 100644 --- a/src/vb/object.rs +++ b/src/vb/object.rs @@ -382,9 +382,10 @@ impl<'a> ObjectInfo<'a> { /// method in source order, each the VA of the method's /// [`ProcDscInfo`](super::procedure::ProcDscInfo) (whose +0x00 points /// back to this `ObjectInfo`). In a module the slots of `Declare` - /// statements hold no `ProcDscInfo` VA (`tests/fixtures/exprs`: 0 and - /// 0xFFFFFFFF; `vtable`, whose 13 `Declare` slots overlap other data: - /// small integers and the text `Vtable`). The runtime + /// statements hold no `ProcDscInfo` VA: the compiler reserves them and + /// writes nothing, so they hold stale bytes (`tests/fixtures/exprs`: 0 + /// and 0xFFFFFFFF; `vtable`'s 13: small integers and the text + /// `Vtable`). The table follows the constant pool. The runtime /// builds an object's vtable from the method link table, not from this /// one (see [`OptionalObjectInfo::basic_class_object_va`]). When /// `method_count() == 0`, this value is not an address (`flow-native` diff --git a/src/vb/pooldesc.rs b/src/vb/pooldesc.rs new file mode 100644 index 0000000..6b0f7b9 --- /dev/null +++ b/src/vb/pooldesc.rs @@ -0,0 +1,495 @@ +//! Descriptors the constant pool holds for the P-Code that names them. +//! +//! Besides strings, GUIDs and addresses, a procedure's constant pool points +//! at descriptors the runtime reads when an instruction names them (see +//! [`PoolEntryRole`](crate::pcode::decoder::PoolEntryRole)): +//! +//! - [`RecordIoDescriptor`]: how `Get #` and `Put #` transfer a record; +//! - [`IoItems`]: the items of a `Print`, `Write` or `Input` statement; +//! - [`ArrayDescriptor`]: the `SAFEARRAY` header of a fixed array inside a +//! record; +//! - [`CreationDescriptor`]: the class `New` creates when it is not one of +//! the project's own. +//! +//! The layouts are the ones MSVBVM60 6.00.8176 reads; the fixtures `records` +//! and `udts` hold nested records, record arrays and record I/O. + +use crate::{ + addressmap::AddressMap, + error::Error, + util::{read_i16_le, read_u16_le, read_u32_le}, +}; + +/// How deep [`RecordIoDescriptor::extent_size`] follows nested records. +const MAX_RECORD_DEPTH: usize = 16; + +/// What a [`RecordIoEntry`] transfers, its kind (the low nibble of its +/// flags). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum RecordIoKind { + /// A `String` member (a BSTR). + String, + /// A `Variant` member. + Variant, + /// A nested record, whose own descriptor + /// [`RecordIoDescriptor::nested`] reads. + Record, + /// A plain element of [`RecordIoEntry::value`] bytes. + Raw, + /// A plain scalar of the size in the flags' high byte. No fixture has + /// one. + Scalar, + /// Bytes skipped without a transfer, the size in the flags' high byte. + Skip, + /// A `String * N` member, N in [`RecordIoEntry::value`] (2N bytes in + /// memory). + FixedString, + /// Another kind value. + Other(u8), +} + +/// One entry of a [`RecordIoDescriptor`] (12 bytes). +/// +/// | Offset | Size | Field | +/// |--------|------|-------| +/// | 0x00 | 4 | Plain bytes before the member, transferred as they are | +/// | 0x04 | 2 | Flags: kind in bits 0-3, 0x10 a fixed array, 0x20 a dynamic array (a `SAFEARRAY` pointer); the size of a [`Scalar`](RecordIoKind::Scalar) or [`Skip`](RecordIoKind::Skip) in the high byte | +/// | 0x06 | 4 | Element count of a fixed array; 1 for a single member, 0xFFFFFFFF for a dynamic array | +/// | 0x0A | 2 | A nested record's descriptor, as an offset from this entry; a [`Raw`](RecordIoKind::Raw) element's size; a [`FixedString`](RecordIoKind::FixedString)'s length | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RecordIoEntry { + /// Plain bytes before the member. + pub gap: u32, + /// The flags. + pub flags: u16, + /// The element count. + pub count: u32, + /// The kind-dependent value at 0x0A. + pub value: u16, +} + +impl RecordIoEntry { + /// Size of one entry. + pub const SIZE: usize = 12; + + /// What the entry transfers. + pub fn kind(&self) -> RecordIoKind { + match self.flags & 0x0F { + 0 => RecordIoKind::String, + 1 => RecordIoKind::Variant, + 2 => RecordIoKind::Record, + 3 => RecordIoKind::Raw, + 4 => RecordIoKind::Scalar, + 5 => RecordIoKind::Skip, + 6 => RecordIoKind::FixedString, + other => RecordIoKind::Other(u8::try_from(other).unwrap_or(u8::MAX)), + } + } + + /// Returns `true` for a fixed array of [`count`](Self::count) elements. + pub fn is_fixed_array(&self) -> bool { + self.flags & 0x10 != 0 + } + + /// Returns `true` for a dynamic array, a `SAFEARRAY` pointer. + pub fn is_dynamic_array(&self) -> bool { + self.flags & 0x20 != 0 + } +} + +/// The descriptor `Get #` and `Put #` of a record read (`GetRecOwn*`, +/// `PutRecOwn*`): a 10-byte header, its entries, then the descriptors of +/// its nested records. +/// +/// | Offset | Size | Field | +/// |--------|------|-------| +/// | 0x00 | 4 | Bytes of the record the transfer covers | +/// | 0x04 | 4 | Number of [`RecordIoEntry`]s | +/// | 0x08 | 2 | Padding after the record: a nested record's array stride is its size plus this | +/// | 0x0A | 12 each | The entries | +/// +/// The runtime walks the entries recursively (MSVBVM60 6.00.8176 +/// `0x6601C660`, from `__vbaGetOwner3`, `__vbaPutOwner3` and their record +/// number forms), then transfers the bytes the entries left as they are. +/// Each nested record's descriptor follows the entry array, in entry order, +/// each followed by its own nested ones (`records`' `Outer`: 9 entries and +/// three copies of `Inner`'s). `coverage`'s `FileRec` (`n As Long`, `x As +/// Double`, `c As Currency`, `fx As String * 8`) has one entry, the +/// fixed-length string 0x14 bytes in. +#[derive(Clone, Copy, Debug)] +pub struct RecordIoDescriptor<'a> { + bytes: &'a [u8], +} + +impl<'a> RecordIoDescriptor<'a> { + /// Size of the header before the entries. + pub const HEADER_SIZE: usize = 10; + + /// Parses a descriptor from a slice that runs at least to its end (to + /// the end of the file for [`nested`](Self::nested)). + /// + /// # Errors + /// + /// Returns [`Error::TooShort`] if `data` is shorter than the header. + pub fn parse(data: &'a [u8]) -> Result { + if data.len() < Self::HEADER_SIZE { + return Err(Error::TooShort { + expected: Self::HEADER_SIZE, + actual: data.len(), + context: "RecordIoDescriptor", + }); + } + Ok(Self { bytes: data }) + } + + /// Reads the descriptor at `va`. + /// + /// # Errors + /// + /// Returns an error if `va` is not mapped or the header does not fit. + pub fn at(map: &AddressMap<'a>, va: u32) -> Result { + Self::parse(map.slice_from_va(va, Self::HEADER_SIZE)?) + } + + /// Bytes of the record the transfer covers, at offset 0x00. + #[inline] + pub fn record_size(&self) -> Result { + read_u32_le(self.bytes, 0x00) + } + + /// Number of entries, at offset 0x04. + #[inline] + pub fn entry_count(&self) -> Result { + read_u32_le(self.bytes, 0x04) + } + + /// Padding after the record, at offset 0x08. + #[inline] + pub fn tail_padding(&self) -> Result { + read_i16_le(self.bytes, 0x08) + } + + /// Reads the entries; empty when they do not all read. + pub fn entries(&self) -> Vec { + let Some(count) = self + .entry_count() + .ok() + .and_then(|count| usize::try_from(count).ok()) + .filter(|&count| { + count + .checked_mul(RecordIoEntry::SIZE) + .and_then(|len| len.checked_add(Self::HEADER_SIZE)) + .is_some_and(|end| end <= self.bytes.len()) + }) + else { + return Vec::new(); + }; + (0..count) + .map_while(|index| { + let at = Self::HEADER_SIZE.checked_add(index.checked_mul(RecordIoEntry::SIZE)?)?; + Some(RecordIoEntry { + gap: read_u32_le(self.bytes, at).ok()?, + flags: read_u16_le(self.bytes, at.checked_add(4)?).ok()?, + count: read_u32_le(self.bytes, at.checked_add(6)?).ok()?, + value: read_u16_le(self.bytes, at.checked_add(10)?).ok()?, + }) + }) + .collect() + } + + /// Returns the descriptor of entry `index`'s nested record: at the + /// entry's offset plus its [`value`](RecordIoEntry::value). `None` for + /// another kind or when it is outside the parsed slice. + pub fn nested(&self, index: usize) -> Option { + let entry = self.entries().get(index).copied()?; + if entry.kind() != RecordIoKind::Record { + return None; + } + let at = Self::HEADER_SIZE + .checked_add(index.checked_mul(RecordIoEntry::SIZE)?)? + .checked_add(usize::from(entry.value))?; + Self::parse(self.bytes.get(at..)?).ok() + } + + /// Returns the bytes the descriptor occupies with its nested + /// descriptors: `0x0A + 12 * entries` and every nested descriptor's + /// extent. + /// + /// `None` when the entries or a nested descriptor do not read, or + /// nesting goes deeper than 16 records. + pub fn extent_size(&self) -> Option { + self.extent_size_at(0) + } + + /// [`extent_size`](Self::extent_size), `depth` records deep. + fn extent_size_at(&self, depth: usize) -> Option { + if depth > MAX_RECORD_DEPTH { + return None; + } + let entries = self.entries(); + if entries.len() != usize::try_from(self.entry_count().ok()?).ok()? { + return None; + } + let mut end = + Self::HEADER_SIZE.checked_add(entries.len().checked_mul(RecordIoEntry::SIZE)?)?; + for (index, entry) in entries.iter().enumerate() { + if entry.kind() != RecordIoKind::Record { + continue; + } + let start = Self::HEADER_SIZE + .checked_add(index.checked_mul(RecordIoEntry::SIZE)?)? + .checked_add(usize::from(entry.value))?; + let nested = self.nested(index)?.extent_size_at(depth.checked_add(1)?)?; + end = end.max(start.checked_add(nested)?); + } + Some(end) + } +} + +/// What separates an [`IoItem`] from the next (bits 6-7 of its byte). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum IoSeparator { + /// `,`: `Print` moves to the next print zone; `Write` writes a comma. + Comma, + /// `;`: nothing between the items; `Write` writes a comma. + Semicolon, + /// The end of the line: a carriage return and line feed. + NewLine, + /// The fourth value, which no statement emits. + Other, +} + +/// One item of an [`IoItems`] list. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct IoItem(pub u8); + +impl IoItem { + /// The item's type (bits 0-5): 0 none (a separator alone), 2 `Integer`, + /// 3 `Long`, 4 `Single`, 5 `Double`, 6 `Currency`, 7 `Date`, 8 `String`, + /// 9 `Object`, 0x0A error, 0x0B `Boolean`, 0x0C a Variant's address, + /// 0x11 `Byte`, 0x20 `Spc(n)`, 0x21 `Tab(n)`. It decides how many + /// argument dwords the item takes: none for 0, two for a `Double`, + /// `Currency` or `Date`, one otherwise. + pub fn item_type(self) -> u8 { + self.0 & 0x3F + } + + /// What follows the item. + pub fn separator(self) -> IoSeparator { + match self.0 >> 6 { + 0 => IoSeparator::Comma, + 1 => IoSeparator::Semicolon, + 2 => IoSeparator::NewLine, + _ => IoSeparator::Other, + } + } +} + +/// The item list of a `Print`, `Write` or `Input` statement (`PrintObject`, +/// `PrintFile`, `WriteFile`, `InputFile`): a u16 count, then one +/// [`IoItem`] byte per item. +/// +/// `Print`, `Print #` and `Write #` read the items with the arguments +/// pushed for them (MSVBVM60 6.00.8176 `0x6600D080`, from `__vbaPrintObj`, +/// `__vbaPrintFile` and `__vbaWriteFile`); `Input #` reads only the types, +/// one destination per item (`__vbaInputFile`). `forms`: `01 00 88`, one +/// `String` and a new line. +#[derive(Clone, Copy, Debug)] +pub struct IoItems<'a> { + bytes: &'a [u8], +} + +impl<'a> IoItems<'a> { + /// Parses an item list from a slice that runs at least to its end. + /// + /// # Errors + /// + /// Returns [`Error::TooShort`] if `data` is shorter than its count says. + pub fn parse(data: &'a [u8]) -> Result { + let size = usize::from(read_u16_le(data, 0)?).saturating_add(2); + let bytes = data.get(..size).ok_or(Error::TooShort { + expected: size, + actual: data.len(), + context: "IoItems", + })?; + Ok(Self { bytes }) + } + + /// Reads the item list at `va`. + /// + /// # Errors + /// + /// Returns an error if `va` is not mapped or the list does not fit. + pub fn at(map: &AddressMap<'a>, va: u32) -> Result { + Self::parse(map.slice_from_va(va, 2)?) + } + + /// Returns the bytes the list occupies: 2 plus one per item. + #[inline] + pub fn size(&self) -> usize { + self.bytes.len() + } + + /// The items. + pub fn items(&self) -> impl Iterator + '_ { + self.bytes + .get(2..) + .unwrap_or(&[]) + .iter() + .copied() + .map(IoItem) + } +} + +/// The `SAFEARRAY` header of a fixed array inside a record, which +/// `AryInRecLdPr` and `AryInRecLdRf` index: 0x10 bytes, then a bound per +/// dimension. +/// +/// | Offset | Size | Field | +/// |--------|------|-------| +/// | 0x00 | 2 | `cDims` | +/// | 0x02 | 2 | `fFeatures` | +/// | 0x04 | 4 | `cbElements` | +/// | 0x08 | 4 | `cLocks` | +/// | 0x0C | 4 | `pvData`: 0, the array's offset in the record | +/// | 0x10 | 8 each | `{cElements, lLbound}`, rightmost dimension first | +/// +/// The runtime requires `cDims` to equal the instruction's dimension count +/// and adds the element's offset to the record's address (MSVBVM60 +/// 6.00.8176 `0x66108E4E`). `data`'s `Record.Flags(3) As Byte`: one +/// dimension of 4 one-byte elements. +#[derive(Clone, Copy, Debug)] +pub struct ArrayDescriptor<'a> { + bytes: &'a [u8], +} + +impl<'a> ArrayDescriptor<'a> { + /// Size of the header before the bounds. + pub const HEADER_SIZE: usize = 0x10; + + /// Parses a descriptor from a slice that runs at least to its end. + /// + /// # Errors + /// + /// Returns [`Error::TooShort`] if `data` is shorter than its dimension + /// count says. + pub fn parse(data: &'a [u8]) -> Result { + let size = usize::from(read_u16_le(data, 0)?) + .saturating_mul(8) + .saturating_add(Self::HEADER_SIZE); + let bytes = data.get(..size).ok_or(Error::TooShort { + expected: size, + actual: data.len(), + context: "ArrayDescriptor", + })?; + Ok(Self { bytes }) + } + + /// Reads the descriptor at `va`. + /// + /// # Errors + /// + /// Returns an error if `va` is not mapped or the descriptor does not + /// fit. + pub fn at(map: &AddressMap<'a>, va: u32) -> Result { + Self::parse(map.slice_from_va(va, Self::HEADER_SIZE)?) + } + + /// Returns the bytes the descriptor occupies: `0x10 + 8 * cDims`. + #[inline] + pub fn size(&self) -> usize { + self.bytes.len() + } + + /// Number of dimensions (`cDims`), at offset 0x00. + #[inline] + pub fn dimensions(&self) -> Result { + read_u16_le(self.bytes, 0x00) + } + + /// `fFeatures`, at offset 0x02. + #[inline] + pub fn features(&self) -> Result { + read_u16_le(self.bytes, 0x02) + } + + /// Size of an element (`cbElements`), at offset 0x04. + #[inline] + pub fn element_size(&self) -> Result { + read_u32_le(self.bytes, 0x04) + } + + /// The bounds, `(element count, lower bound)` per dimension, rightmost + /// dimension first. + pub fn bounds(&self) -> Vec<(u32, i32)> { + self.bytes + .get(Self::HEADER_SIZE..) + .unwrap_or(&[]) + .as_chunks::<8>() + .0 + .iter() + .filter_map(|bound| { + Some(( + read_u32_le(bound, 0).ok()?, + read_u32_le(bound, 4).ok()?.cast_signed(), + )) + }) + .collect() + } +} + +/// What `New` creates when it is not a class of the project (0x10 bytes): +/// the class's CLSID, the interface to query, and its licence key. +/// +/// | Offset | Size | Field | +/// |--------|------|-------| +/// | 0x00 | 4 | Flags: bit 0 clear (a project class's `ObjectInfo` has it set); bit 1, try the running object first | +/// | 0x04 | 4 | VA of the CLSID | +/// | 0x08 | 4 | VA of the IID queried after creation | +/// | 0x0C | 4 | VA of the licence key, `{u32 byte length, UTF-16}`, or 0 | +/// +/// The runtime creates the class with `CoCreateInstance`, or with +/// `IClassFactory2::CreateInstanceLic` when there is a key, and queries +/// the interface (MSVBVM60 6.00.8176 `0x66011021`, from the `New` and +/// `NewIfNull*` handlers). `forms`: VB's `Global` (`{FCFB3D23-...}`) as +/// `VBGlobal`, flags 2; `coverage`: VBA's `Collection` as `_Collection`. +#[derive(Clone, Copy, Debug)] +pub struct CreationDescriptor<'a> { + bytes: &'a [u8], +} + +impl<'a> CreationDescriptor<'a> { + /// Size of the structure in bytes. + pub const SIZE: usize = 0x10; + + /// Reads the creation descriptor at `va`, `None` when the bytes there + /// are a project class's `ObjectInfo` (flag bit 0 set) or do not read. + pub fn at(map: &AddressMap<'a>, va: u32) -> Option { + let bytes = map.slice_from_va(va, Self::SIZE).ok()?.get(..Self::SIZE)?; + (bytes.first()? & 1 == 0).then_some(Self { bytes }) + } + + /// The flags at offset 0x00. + #[inline] + pub fn flags(&self) -> Result { + read_u32_le(self.bytes, 0x00) + } + + /// VA of the class's CLSID, at offset 0x04. + #[inline] + pub fn clsid_va(&self) -> Result { + read_u32_le(self.bytes, 0x04) + } + + /// VA of the interface's IID, at offset 0x08. + #[inline] + pub fn iid_va(&self) -> Result { + read_u32_le(self.bytes, 0x08) + } + + /// VA of the licence key, at offset 0x0C; 0 for none. + #[inline] + pub fn license_va(&self) -> Result { + read_u32_le(self.bytes, 0x0C) + } +} diff --git a/src/vb/privateobj.rs b/src/vb/privateobj.rs index c39110f..dacad9f 100644 --- a/src/vb/privateobj.rs +++ b/src/vb/privateobj.rs @@ -1,7 +1,8 @@ //! PrivateObjectDescriptor structure parser. //! //! The PrivateObjectDescriptor contains per-object private data including -//! function type descriptors, variable counts, and parameter name tables. +//! function type descriptors, its public variables and implemented +//! interfaces, its events' prototypes and the layouts of its `Type`s. //! It is referenced by [`ObjectInfo::private_object_va()`](super::object::ObjectInfo::private_object_va) //! at offset 0x0C. //! @@ -15,13 +16,13 @@ //! | 0x0C | 4 | Reserved (0 in every fixture) | //! | 0x10 | 2 | Module-level variables, plus 1 per `Implements` (see [`member_count`](PrivateObjectDescriptor::member_count)) | //! | 0x12 | 2 | `Event` declarations (see [`event_count`](PrivateObjectDescriptor::event_count)) | -//! | 0x14 | 2 | Count of [`var_stubs_va`](PrivateObjectDescriptor::var_stubs_va) entries (0 in every fixture; see [`var_stub_count`](PrivateObjectDescriptor::var_stub_count)) | +//! | 0x14 | 2 | `Type` declarations (see [`record_layout_count`](PrivateObjectDescriptor::record_layout_count)) | //! | 0x16 | 2 | Padding (0 in every fixture) | //! | 0x18 | 4 | `lpFuncTypDescs` - VA to array of FuncTypDesc pointers, one per method (null for the unnamed ones) | //! | 0x1C | 4 | `lpExtendedFuncData` - secondary per-method array (0 in every fixture) | -//! | 0x20 | 4 | `lpMethodNameTable` - secondary method table | -//! | 0x24 | 4 | `lpParamNames` - parameter name table | -//! | 0x28 | 4 | `lpVarStubs` - variable stub table | +//! | 0x20 | 4 | `lpMemberDescs` - VA to array of [`MemberDesc`](super::member::MemberDesc) pointers, one per +0x10 member (null for a `Private` variable) | +//! | 0x24 | 4 | `lpEventDescs` - VA to array of FuncTypDesc pointers, one per +0x12 `Event` | +//! | 0x28 | 4 | VA to array of [`RecordLayout`](super::controlprop::RecordLayout) pointers, one per +0x14 `Type` | //! | 0x2C | 12 | Reserved (0 in every fixture) | //! | 0x38 | 4 | Instance size (equal to the PublicBytes `+0x02`; see [`instance_size`](PrivateObjectDescriptor::instance_size)) | //! | 0x3C | 4 | `dwFlags` - 0x0104 for classes and UserControls, 0x0004 for forms | @@ -43,7 +44,7 @@ use crate::{ /// View over a PrivateObjectDescriptor structure (0x40 bytes). /// /// Contains per-object private data: function type descriptor pointers, -/// variable counts, and parameter name tables. +/// member and event descriptor pointers, and their counts. /// /// # Accessor fallibility /// @@ -99,7 +100,8 @@ impl<'a> PrivateObjectDescriptor<'a> { /// `m_Owner`), Square 2 (`m_Side`, `Implements Shape`), Shape 0; /// `events` Listener 2 (`WithEvents m_Source`, `m_Log`); `data` Item 1 /// (`Public Name`). It is not a method count: the FuncTypDesc array has - /// one entry per method of the object's method table. + /// one entry per method of the object's method table. It is the length + /// of the [`member_descs_va`](Self::member_descs_va) array. #[inline] pub fn member_count(&self) -> Result { read_u16_le(self.bytes, 0x10) @@ -109,20 +111,22 @@ impl<'a> PrivateObjectDescriptor<'a> { /// /// `calls` Counter 1, /// `events` Source 3 (`Started`, `Ticked`, `Named`), `types` Kinds 1, - /// `forms` Gauge 1, 0 for every other fixture object. + /// `forms` Gauge 1, 0 for every other fixture object. It is the length + /// of the [`event_descs_va`](Self::event_descs_va) array. #[inline] pub fn event_count(&self) -> Result { read_u16_le(self.bytes, 0x12) } - /// The u16 count at offset 0x14, read as the length of the - /// [`var_stubs_va`](Self::var_stubs_va) array. + /// Number of `Type` declarations in the object, `Public` or `Private`, + /// the u16 at offset 0x14: the length of the + /// [`record_layouts_va`](Self::record_layouts_va) array. /// - /// Not the number of public variables: `tests/fixtures/data` `Item` - /// declares `Public Name As String` and has 0 here. 0 in every fixture, - /// so what it counts is unconfirmed. + /// `typerefs` `Shapes` 1 (`Point`), `records` `Item` 1 (`CT`), `udts` + /// `Shapes` 5; 0 for an object that declares none. A standard module's + /// `Type`s are in no such array: a module has no descriptor. #[inline] - pub fn var_stub_count(&self) -> Result { + pub fn record_layout_count(&self) -> Result { read_u16_le(self.bytes, 0x14) } @@ -143,39 +147,46 @@ impl<'a> PrivateObjectDescriptor<'a> { read_u32_le(self.bytes, 0x18) } - /// VA of the secondary method table at offset 0x20. + /// VA of the [`MemberDesc`](super::member::MemberDesc) pointer array at + /// offset 0x20. /// - /// Its layout is unconfirmed. When the object has nothing to put in - /// it, it shares its VA with [`param_names_va`](Self::param_names_va) - /// and [`var_stubs_va`](Self::var_stubs_va) (`calls` Shape: all three - /// 0x00401C58). + /// One VA per [`member_count`](Self::member_count): a `MemberDesc` for a + /// `Public` variable (`WithEvents` or not) or an `Implements`, 0 for a + /// `Private` variable. `members` `Holder` points to its thirteen + /// `Public` variables `B` ... `Auto`, `calls` `Square` to 0 (`m_Side`) + /// and `Shape` (its `Implements`). See [`member`](super::member) for the + /// order. With no members it shares its VA with + /// [`event_descs_va`](Self::event_descs_va) and + /// [`record_layouts_va`](Self::record_layouts_va) (`calls` Shape: all + /// three 0x00401C58). #[inline] - pub fn method_name_table_va(&self) -> Result { + pub fn member_descs_va(&self) -> Result { read_u32_le(self.bytes, 0x20) } - /// VA of the parameter name string table at offset 0x24. + /// VA of the event prototype array at offset 0x24. /// - /// Points to an array of VAs, among them VAs of null-terminated - /// parameter name strings shared across the object's functions - /// (`calls` Counter: `"Value"`, `"o"`, `"a"`, `"values"`, `"which"`, - /// `"factor"`), but also VAs of other structures (its first entry, - /// 0x00401E54, is not a string) and zeros. The exact layout is - /// unconfirmed. + /// One [`FuncTypDesc`](super::functype::FuncTypDesc) VA per + /// [`event_count`](Self::event_count), in `Event` declaration order; + /// each descriptor's parameter names are the event's (`calls` Counter: + /// `Changed(ByVal value)`; `events` Source: `Started`, `Ticked(n, ratio, + /// Cancel)`, `Named(s, v, o)`). #[inline] - pub fn param_names_va(&self) -> Result { + pub fn event_descs_va(&self) -> Result { read_u32_le(self.bytes, 0x24) } - /// VA of variable implementation stub array at offset 0x28. + /// VA of the record layout array at offset 0x28. /// - /// Points to an array of [`var_stub_count`](Self::var_stub_count) VA pointers, - /// each to a [`VarStubDesc`](super::varstub::VarStubDesc) structure. - /// Use [`VarStubIter`](super::varstub::VarStubIter) to iterate. Every - /// fixture object has a `var_stub_count` of 0, so the format is unconfirmed - /// by `tests/fixtures`. + /// One [`RecordLayout`](super::controlprop::RecordLayout) VA per + /// [`record_layout_count`](Self::record_layout_count), in `Type` + /// declaration order (`udts` `Shapes`: `Point`, `Named`, `Third`, + /// `PrivT`, `Holder`). The compiler stores one layout per distinct shape + /// in the project, so two `Type`s with the same members share one + /// (`udts`: `Second.Pair` and `Shapes.PrivT`), as does an array + /// member's element (`Holder.Ns()` names `Named`'s). #[inline] - pub fn var_stubs_va(&self) -> Result { + pub fn record_layouts_va(&self) -> Result { read_u32_le(self.bytes, 0x28) } @@ -241,10 +252,10 @@ mod tests { let pod = PrivateObjectDescriptor::parse(&CLS_ZIP).unwrap(); assert_eq!(pod.object_info_va().unwrap(), 0x0040281C); assert_eq!(pod.member_count().unwrap(), 14); - assert_eq!(pod.var_stub_count().unwrap(), 5); + assert_eq!(pod.record_layout_count().unwrap(), 5); assert_eq!(pod.func_type_descs_va().unwrap(), 0x00405CA8); - assert_eq!(pod.param_names_va().unwrap(), 0x00405544); - assert_eq!(pod.var_stubs_va().unwrap(), 0x00405644); + assert_eq!(pod.event_descs_va().unwrap(), 0x00405544); + assert_eq!(pod.record_layouts_va().unwrap(), 0x00405644); assert_eq!(pod.instance_size().unwrap(), 0x4C); assert_eq!(pod.flags().unwrap(), 0x0104); assert!(pod.is_class()); @@ -255,7 +266,7 @@ mod tests { let pod = PrivateObjectDescriptor::parse(&FORM1).unwrap(); assert_eq!(pod.object_info_va().unwrap(), 0x0040243C); assert_eq!(pod.member_count().unwrap(), 0); - assert_eq!(pod.var_stub_count().unwrap(), 0); + assert_eq!(pod.record_layout_count().unwrap(), 0); assert_eq!(pod.func_type_descs_va().unwrap(), 0x0040558C); assert_eq!(pod.instance_size().unwrap(), 0x44); assert_eq!(pod.flags().unwrap(), 0x0004); @@ -276,7 +287,7 @@ mod tests { let pod = PrivateObjectDescriptor::parse(&COOLBAR_OCX).unwrap(); assert_eq!(pod.member_count().unwrap(), 30); assert_eq!(pod.event_count().unwrap(), 13); - assert_eq!(pod.var_stub_count().unwrap(), 0); + assert_eq!(pod.record_layout_count().unwrap(), 0); assert_eq!(pod.flags().unwrap(), 0x0104); assert!(pod.is_class()); } diff --git a/src/vb/procedure.rs b/src/vb/procedure.rs index 14d517d..3cb844e 100644 --- a/src/vb/procedure.rs +++ b/src/vb/procedure.rs @@ -590,6 +590,33 @@ impl<'a> ProcDscInfo<'a> { } } + /// Returns the bytes the descriptor occupies with the tables that follow + /// it: [`actual_size`](Self::actual_size), or the end of its line-number + /// table when it has one. + /// + /// # Errors + /// + /// Returns an error if a table's size or the line-number count cannot be + /// read. + pub fn extent_size(&self) -> Result { + let base = self.actual_size()?; + let Some(start) = usize::try_from(self.line_table_offset()?) + .ok() + .filter(|&start| start != 0) + else { + return Ok(base); + }; + let count = usize::from(read_u16_le(self.bytes, start)?); + let end = count + .checked_mul(4) + .and_then(|pairs| pairs.checked_add(2)) + .and_then(|len| len.checked_add(start)) + .ok_or(Error::ArithmeticOverflow { + context: "ProcDscInfo::extent_size line table", + })?; + Ok(base.max(end)) + } + /// Returns an iterator over primary cleanup table entries. /// /// Each entry describes a local variable that needs resource release diff --git a/src/vb/projectdata.rs b/src/vb/projectdata.rs index 2729ff5..f587093 100644 --- a/src/vb/projectdata.rs +++ b/src/vb/projectdata.rs @@ -103,11 +103,15 @@ impl<'a> ProjectData<'a> { read_u32_le(self.bytes, 0x08) } - /// Start of the native/P-Code region in .text at offset 0x0C. + /// Start of the project's native code region, at offset 0x0C. /// - /// For native binaries, this spans the compiled native code. - /// For P-Code binaries, it is a 16-byte region holding `E9 E9 E9 E9` - /// followed by twelve `CC` bytes (every P-Code fixture). + /// The region opens with a 16-byte marker, `E9 E9 E9 E9` and twelve + /// `CC`, which nothing executes; a native build's procedures follow it, + /// up to [`code_end_va`](Self::code_end_va), and a P-Code build has + /// none, its region being the marker alone (every fixture). The runtime + /// reads the bounds only to turn an access violation at a near-null + /// address inside the region into error 91 (MSVBVM60 6.00.8176 + /// `0x660E3ADC`). /// /// # Errors /// @@ -117,7 +121,9 @@ impl<'a> ProjectData<'a> { read_u32_le(self.bytes, 0x0C) } - /// End of the native/P-Code region in .text at offset 0x10. + /// End of the project's native code region, at offset 0x10: a 4-byte + /// marker, `9E 9E 9E 9E`, which nothing executes. The import directory + /// follows it in every fixture. /// /// # Errors /// diff --git a/src/vb/projectinfo2.rs b/src/vb/projectinfo2.rs index 5c6d887..7dc4cb5 100644 --- a/src/vb/projectinfo2.rs +++ b/src/vb/projectinfo2.rs @@ -8,11 +8,17 @@ //! //! The compiler places the 0x28-byte header at the end of the data that //! precedes the first procedure's P-Code. After it, up to the P-Code, come -//! 12-byte event-source records interleaved with null-terminated parameter -//! name strings (each padded to a 4-byte boundary). There is no count: in -//! `dispid` and `forms` records follow name strings. Projects with only -//! standard modules have nothing after the header. [`ProjectInfo2Iter`] -//! walks the region and tells the two apart (see [`ProjectInfo2Item`]). +//! 12-byte event-source records interleaved with null-terminated name +//! strings (each padded to a 4-byte boundary) and the descriptors some +//! member types carry. There is no count: in `dispid` and `forms` records +//! follow name strings. Projects with only standard modules have nothing +//! after the header. [`ProjectInfo2Iter`] walks the region and tells +//! records from names (see [`ProjectInfo2Item`]). +//! +//! The names are those of the objects' procedure parameters, event +//! parameters, public variables and implemented interfaces, not property +//! or method names. [`VbProject::name_references`](crate::VbProject::name_references) +//! attributes each one through the pointers that refer to it. //! //! ## Header (0x28 bytes) //! @@ -39,7 +45,7 @@ //! //! | Offset | Size | Field | //! |--------|------|-------| -//! | 0x00 | 4 | `lpInterfaceMetadata` (the type library reference; 0 for a control array or the project's own UserControl) | +//! | 0x00 | 4 | `lpTypeLib` (the event interface's [`TypeLibRef`]; 0 for a control array or the project's own UserControl) | //! | 0x04 | 4 | `lpGuidData` (16-byte event IID + source name string) | //! | 0x08 | 4 | `lpDispatchSlot` (.data section slot, one per record) | @@ -49,7 +55,7 @@ use crate::{ addressmap::AddressMap, error::Error, util::{read_cstr, read_u32_le}, - vb::control::Guid, + vb::{control::Guid, typeref::TypeLibRef}, }; /// View over a ProjectInfo2 header (0x28 bytes). @@ -97,151 +103,38 @@ impl<'a> ProjectInfo2<'a> { /// Contains one DWORD per object (total_objects entries), in object /// table order. Each entry equals that object's /// `ObjectInfo.lpPrivateObject`: a PrivateObjectDescriptor VA, or - /// 0xFFFFFFFF for a standard module (every fixture). + /// 0xFFFFFFFF for a standard module; except that a module of an + /// ActiveX DLL declaring `Public Type`s (`udts` `Mod1`) holds an + /// address outside the image, a compiler address left unrelocated. #[inline] pub fn object_descs_va(&self) -> Result { read_u32_le(self.bytes, 0x10) } -} - -/// Type library reference (0x24 bytes) at each entry's `interface_metadata_va`. -/// -/// Names the type library that declares the record's event interface. -/// Records of one library share one structure. In the fixtures it is -/// either `VB` (`VB6.OLB`, GUID `FCFB3D2E-A0FA-1068-A738-08002B3371B5`, -/// path `C:\VB98\VB6.OLB`) or `VBRUN` (GUID -/// `EA544A21-C82D-11D1-A3E4-00A0C90AEA82`, no path). -/// -/// | Offset | Size | Field | -/// |--------|------|-------| -/// | 0x00 | 4 | `lpTypelibGuid` (VA of the library's 16-byte GUID) | -/// | 0x04 | 4 | Reserved (always 0) | -/// | 0x08 | 4 | Always 6 (both libraries' `MSFT` headers carry version 6) | -/// | 0x0C | 4 | Always 9 (both libraries' `MSFT` headers carry LCID 9) | -/// | 0x10 | 4 | `lpTypelibPath` (null-terminated path string, or 0) | -/// | 0x14 | 4 | `lpNameTable` (the library name, e.g. `"VB"`, `"VBRUN"`) | -/// | 0x18 | 4 | `lpDataSlot` (.data section VA, just below the slots of the records that use it) | -/// | 0x1C | 4 | Reserved (always 0) | -/// | 0x20 | 4 | Reserved (always 0) | -#[derive(Clone, Copy, Debug)] -pub struct InterfaceMetadata<'a> { - bytes: &'a [u8], -} - -impl<'a> InterfaceMetadata<'a> { - /// Total size of the structure in bytes. - pub const SIZE: usize = 0x24; - - /// Parses interface metadata from the given byte slice. - pub fn parse(data: &'a [u8]) -> Result { - if data.len() < Self::SIZE { - return Err(Error::TooShort { - expected: Self::SIZE, - actual: data.len(), - context: "InterfaceMetadata", - }); - } - let bytes = data.get(..Self::SIZE).ok_or(Error::TooShort { - expected: Self::SIZE, - actual: data.len(), - context: "InterfaceMetadata", - })?; - Ok(Self { bytes }) - } - - /// VA of the library name at offset 0x14. - /// - /// Points to the null-terminated library name (`"VB"`, `"VBRUN"`); in - /// the fixtures this structure itself follows the name, so no method or - /// property names follow it. - #[inline] - pub fn name_table_va(&self) -> Result { - read_u32_le(self.bytes, 0x14) - } - - /// VA of the typelib GUID at offset 0x00. - #[inline] - pub fn typelib_guid_va(&self) -> Result { - read_u32_le(self.bytes, 0x00) - } - - /// VA of the typelib path string at offset 0x10. - /// - /// Points to a null-terminated ANSI path of the type library file - /// (`"C:\VB98\VB6.OLB"` in `controls` and `forms`). `0` for the - /// runtime's own `VBRUN` library. - /// - /// Pair with [`typelib_guid_va`](Self::typelib_guid_va) to get the - /// typelib's CLSID - together they identify which DLL/OCX provides - /// the type information for this interface (a supply-chain signal - /// useful for malware triage). - /// - /// # Errors - /// - /// Returns [`Error::Truncated`] if the backing buffer is shorter than expected. - #[inline] - pub fn typelib_path_va(&self) -> Result { - read_u32_le(self.bytes, 0x10) - } - /// Raw `.data` section VA at offset 0x18. - /// - /// Points into the binary's `.data` section (zero on disk), 4 to 12 - /// bytes below the slots of the records that use this library. How the - /// runtime uses it is not verified. + /// Reads the [`object_descs_va`](Self::object_descs_va) array's `count` + /// entries ([`ObjectTable::total_objects`](super::objecttable::ObjectTable::total_objects)): + /// per object, its PrivateObjectDescriptor VA, or for a standard module + /// 0xFFFFFFFF or an address outside the image. /// /// # Errors /// - /// Returns [`Error::Truncated`] if the backing buffer is shorter than expected. - #[inline] - pub fn data_slot_va(&self) -> Result { - read_u32_le(self.bytes, 0x18) - } - - /// Reads the identifier strings that start at the name table. - /// - /// In the fixtures this is the library name alone (`["VB"]`, - /// `["VBRUN"]`). The scan stops at the first byte sequence that is not - /// an identifier. - pub fn dispatch_names(&self, map: &AddressMap<'a>) -> Vec<&'a str> { - let Ok(va) = self.name_table_va() else { - return Vec::new(); - }; - if va == 0 { - return Vec::new(); - } - let Ok(data) = map.slice_from_va(va, 512) else { - return Vec::new(); - }; - extract_name_block(data, 0).0 - } - - /// Scans up to 4096 bytes from the name table for runs of identifier - /// strings. - /// - /// Heuristic: the first run is the library name; later runs are - /// whatever identifier-like strings follow in the image (the scan does - /// not know where the structure ends). Returns one vector per run. - pub fn all_dispatch_names(&self, map: &AddressMap<'a>) -> Vec> { - let Ok(va) = self.name_table_va() else { - return Vec::new(); - }; - if va == 0 { - return Vec::new(); - } - let Ok(data) = map.slice_from_va(va, 4096) else { - return Vec::new(); - }; - extract_all_name_blocks(data) + /// Returns an error if the array VA cannot be read or the array is not + /// mapped. + pub fn object_descs(&self, map: &AddressMap<'_>, count: u16) -> Result, Error> { + let len = usize::from(count).saturating_mul(4); + let data = map.slice_from_va(self.object_descs_va()?, len)?; + (0..usize::from(count)) + .map(|i| read_u32_le(data, i.saturating_mul(4))) + .collect() } } /// A single event-source record (0x0C bytes). #[derive(Debug, Clone, Copy)] pub struct ControlTypeEntry { - /// VA of the [`InterfaceMetadata`] type library reference; 0 for a + /// VA of the [`TypeLibRef`] of the event interface's library; 0 for a /// control array and for an instance of the project's own UserControl. - pub interface_metadata_va: u32, + pub typelib_va: u32, /// VA of the GUID data: 16-byte event interface IID followed by the /// null-terminated source name (`"Class"`, `"Form"`, `"UserControl"` or /// a control instance name). @@ -269,12 +162,10 @@ impl ControlTypeEntry { str::from_utf8(name).ok() } - /// Parses the interface metadata for this entry. - pub fn interface_metadata<'a>(&self, map: &'a AddressMap<'a>) -> Option> { - let data = map - .slice_from_va(self.interface_metadata_va, InterfaceMetadata::SIZE) - .ok()?; - InterfaceMetadata::parse(data).ok() + /// Reads the [`TypeLibRef`] of the event interface's library, `None` + /// for a record with none. + pub fn typelib<'a>(&self, map: &AddressMap<'a>) -> Option> { + TypeLibRef::at(map, self.typelib_va).ok() } } @@ -288,7 +179,7 @@ pub enum ProjectInfo2Item<'a> { } /// Walks the region after a ProjectInfo2 header: the event-source records -/// and the parameter name strings between them, in file order. +/// and the name strings between them, in file order. /// /// At each 4-byte-aligned position it reads either /// @@ -326,15 +217,21 @@ impl<'a> ProjectInfo2Iter<'a> { } } + /// Returns the VA of the next item: read before an item is yielded, its + /// start; after the walk ends, the end of the last item. + #[inline] + pub fn position(&self) -> u32 { + self.va + } + /// Reads a record at the current position, if there is one. fn record(&self, data: &[u8]) -> Option { let entry = ControlTypeEntry { - interface_metadata_va: read_u32_le(data, 0).ok()?, + typelib_va: read_u32_le(data, 0).ok()?, guid_data_va: read_u32_le(data, 4).ok()?, dispatch_slot_va: read_u32_le(data, 8).ok()?, }; - let metadata_ok = entry.interface_metadata_va == 0 - || self.map.is_va_in_image(entry.interface_metadata_va); + let typelib_ok = entry.typelib_va == 0 || self.map.is_va_in_image(entry.typelib_va); // The slot is in the zero-filled .data: inside a section, not // necessarily file-backed. let slot_ok = matches!( @@ -346,7 +243,7 @@ impl<'a> ProjectInfo2Iter<'a> { .slice_from_va(entry.guid_data_va.checked_add(16)?, 1) .ok() .and_then(|tail| identifier(tail)); - (metadata_ok && slot_ok && name.is_some()).then_some(entry) + (typelib_ok && slot_ok && name.is_some()).then_some(entry) } } @@ -421,10 +318,18 @@ impl<'a> Iterator for ControlTypeIter<'a> { /// Collects the name strings of the region after the ProjectInfo2 header at /// `pi2_va` (the [`ProjectInfo2Item::Name`]s of a [`ProjectInfo2Iter`]). /// -/// The strings are parameter names of the objects' procedures, event -/// handlers included (`calls`: `v`, `o`, `a`, `b`, `factor`, ...; `forms`: -/// `NewValue` of `Gauge1_Changed`), not property names. The walk ends -/// before the P-Code that follows (`data` yields `Name` alone). +/// The strings are the names of the objects' procedure parameters, event +/// parameters, public variables and implemented interfaces, one string per +/// spelling (`calls`: `v`, `o`, `factor`, ... parameters, `Value` the +/// parameter of `Counter`'s `Event Changed`, `Shape` `Square`'s +/// `Implements`; `members`: `Holder`'s variables `B` ... `Auto`). Property +/// and method names are not among them. The walk ends at the first data +/// that is neither a record nor a name: before the P-Code (`data` yields +/// `Name` alone), or at a member type's descriptor (`members` stops after +/// `Auto`, before `Count`, `IFirst`, `Value`, `Amount`, ...). +/// +/// For every name with its owner, use +/// [`VbProject::name_references`](crate::VbProject::name_references). pub fn read_name_strings<'a>(map: &'a AddressMap<'a>, pi2_va: u32) -> Vec<&'a str> { ProjectInfo2Iter::new(map, pi2_va) .filter_map(|item| match item { @@ -447,96 +352,6 @@ fn identifier(data: &[u8]) -> Option<&str> { .flatten() } -/// Checks if a byte sequence looks like a VB6 identifier. -fn is_vb_identifier(name: &[u8]) -> bool { - name.len() >= 2 - && name - .iter() - .all(|&b| b.is_ascii_alphanumeric() || b == b'_' || b == b'.') -} - -/// Extracts one name block starting at `pos` in `data`. -/// -/// A name block is a sequence of null-terminated VB6 identifier strings, -/// each null-padded to 4-byte alignment. The block ends at the first -/// non-identifier byte sequence or a 4+ byte null run. -/// -/// Returns `(names, end_pos)` where `end_pos` is the byte offset -/// after the block (including the null terminator). -fn extract_name_block(data: &[u8], start: usize) -> (Vec<&str>, usize) { - let mut names = Vec::new(); - let mut pos = start; - - while pos < data.len() { - let Some(&b) = data.get(pos) else { break }; - // Skip null padding - if b == 0 { - let tail = data.get(pos..).unwrap_or(&[]); - let nulls = tail.iter().take_while(|&&b| b == 0).count(); - if nulls >= 4 && !names.is_empty() { - // End of name block - return (names, pos.saturating_add(nulls)); - } - pos = pos.saturating_add(nulls); - continue; - } - let Ok(name) = read_cstr(data, pos) else { - break; - }; - if !is_vb_identifier(name) { - break; - } - if let Ok(s) = str::from_utf8(name) { - names.push(s); - } - pos = pos.saturating_add(name.len()).saturating_add(1); - } - (names, pos) -} - -/// Extracts ALL name blocks from a name table, skipping binary metadata -/// between blocks. -/// -/// The name table contains per-class blocks interleaved with binary -/// metadata (dispatch tables, GUIDs, paths). This function scans for -/// runs of VB6 identifier strings, collecting each run as a separate -/// block. -fn extract_all_name_blocks(data: &[u8]) -> Vec> { - let mut blocks = Vec::new(); - let mut pos = 0usize; - - while pos < data.len() { - let Some(&b) = data.get(pos) else { break }; - // Skip non-identifier bytes (binary metadata between blocks) - if b == 0 { - pos = pos.saturating_add(1); - continue; - } - if !(0x20..=0x7E).contains(&b) { - pos = pos.saturating_add(1); - continue; - } - - // Try to extract a name block starting here - let Ok(name) = read_cstr(data, pos) else { - pos = pos.saturating_add(1); - continue; - }; - if !is_vb_identifier(name) { - pos = pos.saturating_add(1); - continue; - } - - // Found a valid identifier - extract the full block - let (block, end) = extract_name_block(data, pos); - if !block.is_empty() { - blocks.push(block); - } - pos = end; - } - blocks -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/vb/typeref.rs b/src/vb/typeref.rs new file mode 100644 index 0000000..730c67a --- /dev/null +++ b/src/vb/typeref.rs @@ -0,0 +1,339 @@ +//! References to types defined in type libraries: the libraries +//! themselves, their interfaces and their records (user-defined types). +//! +//! A type-list entry that names such a type ([`ArgType`](super::functype::ArgType) +//! codes 0x1C, 0x1D for an interface, 0x14 for a record) is followed by the +//! VA of an [`InterfaceRef`] or a [`RecordRef`]; an event-source record of +//! the ProjectInfo2 region ([`ControlTypeEntry`](super::projectinfo2::ControlTypeEntry)) +//! points to the [`TypeLibRef`] of its event interface's library. +//! +//! In `tests/fixtures/typerefs`, `Shapes` declares `Public Items As +//! Collection` (an [`InterfaceRef`] to VBA's `_Collection`), `Public Font As +//! StdFont` (stdole's `Font`), `Public Unk As IUnknown` (code 0x1C, stdole's +//! `IUnknown`) and a parameter of its own `Public Type Point` (a +//! [`RecordRef`] to the project's library). + +use std::str; + +use crate::{ + addressmap::AddressMap, + error::Error, + util::{read_cstr, read_u16_le, read_u32_le}, + vb::control::Guid, +}; + +/// Reads the 16-byte GUID at `va`. +fn guid_at(map: &AddressMap<'_>, va: u32) -> Option { + Guid::from_bytes(map.slice_from_va(va, 16).ok()?) +} + +/// Reads the null-terminated ANSI string at `va`, `None` for VA 0. +fn str_at<'a>(map: &AddressMap<'a>, va: u32) -> Option<&'a str> { + if va == 0 { + return None; + } + let off = map.va_to_offset(va).ok()?; + str::from_utf8(read_cstr(map.file(), off).ok()?).ok() +} + +/// A reference to a type library (0x28 bytes). +/// +/// One per library the project uses a type of; every reference to the +/// library's types points to it. In the fixtures: `VB` (`VB6.OLB`, GUID +/// `FCFB3D2E-A0FA-1068-A738-08002B3371B5`, version 6.0, LCID 9, path +/// `C:\VB98\VB6.OLB`), `VBRUN` (GUID `EA544A21-C82D-11D1-A3E4-00A0C90AEA82`, +/// no path), `VBA` (`VBA6.DLL`), `stdole` (version 2.0, LCID 0) and the +/// libraries of hosted controls (`MSComctlLib` 2.0, `MSMask` 1.1, LCID 0). +/// +/// | Offset | Size | Field | +/// |--------|------|-------| +/// | 0x00 | 4 | `lpGuid` (VA of the library's 16-byte GUID) | +/// | 0x04 | 4 | Development-environment field, 0 on disk | +/// | 0x08 | 2 | Major version | +/// | 0x0A | 2 | Minor version | +/// | 0x0C | 4 | LCID | +/// | 0x10 | 4 | `lpPath` (null-terminated path of the library file, or 0) | +/// | 0x14 | 4 | `lpName` (the library name, e.g. `"VB"`, `"VBRUN"`) | +/// | 0x18 | 4 | `lpDataSlot` (.data section VA, just below the slots of the references that use it) | +/// | 0x1C | 4 | Development-environment field, 0 on disk | +/// | 0x20 | 4 | Flags ([`flags`](Self::flags)): 0 in every fixture | +/// | 0x24 | 4 | A compiler heap address copied unrelocated, the same in every reference of a binary (`typerefs`: 0x02085A90) | +/// +/// The runtime loads the library by GUID, version and LCID, else from the +/// path, and checks the loaded library's version and LCID, and its GUID +/// unless flag bits 0-1 are set (MSVBVM60 6.00.8176 `0x660F291E`); it +/// stores the `ITypeLib` at the data slot. It reads none of +0x04, +0x1C +/// or +0x24. +#[derive(Clone, Copy, Debug)] +pub struct TypeLibRef<'a> { + bytes: &'a [u8], +} + +impl<'a> TypeLibRef<'a> { + /// Total size of the structure in bytes. + pub const SIZE: usize = 0x28; + + /// Parses a type library reference from the given byte slice. + /// + /// # Errors + /// + /// Returns [`Error::TooShort`] if `data.len() < 0x28`. + pub fn parse(data: &'a [u8]) -> Result { + let bytes = data.get(..Self::SIZE).ok_or(Error::TooShort { + expected: Self::SIZE, + actual: data.len(), + context: "TypeLibRef", + })?; + Ok(Self { bytes }) + } + + /// Reads the type library reference at `va`. + /// + /// # Errors + /// + /// Returns an error if `va` is not mapped or too few bytes follow it. + pub fn at(map: &AddressMap<'a>, va: u32) -> Result { + Self::parse(map.slice_from_va(va, Self::SIZE)?) + } + + /// VA of the library's GUID at offset 0x00. + #[inline] + pub fn guid_va(&self) -> Result { + read_u32_le(self.bytes, 0x00) + } + + /// Reads the library's GUID. + pub fn guid(&self, map: &AddressMap<'_>) -> Option { + guid_at(map, self.guid_va().ok()?) + } + + /// The library's version, `(major, minor)`, at offsets 0x08 and 0x0A. + /// + /// # Errors + /// + /// Returns an error if the fields cannot be read. + pub fn version(&self) -> Result<(u16, u16), Error> { + Ok(( + read_u16_le(self.bytes, 0x08)?, + read_u16_le(self.bytes, 0x0A)?, + )) + } + + /// The library's LCID at offset 0x0C (9 for `VB`, `VBRUN` and `VBA`, 0 + /// for `stdole` and the controls' libraries). + #[inline] + pub fn lcid(&self) -> Result { + read_u32_le(self.bytes, 0x0C) + } + + /// VA of the library file's path at offset 0x10; 0 for `VBRUN`. + #[inline] + pub fn path_va(&self) -> Result { + read_u32_le(self.bytes, 0x10) + } + + /// Reads the library file's path (`"C:\VB98\VB6.OLB"`), `None` when + /// there is none. + pub fn path(&self, map: &AddressMap<'a>) -> Option<&'a str> { + str_at(map, self.path_va().ok()?) + } + + /// VA of the library name at offset 0x14. + #[inline] + pub fn name_va(&self) -> Result { + read_u32_le(self.bytes, 0x14) + } + + /// Reads the library name (`"VB"`, `"VBRUN"`, `"stdole"`). + pub fn name(&self, map: &AddressMap<'a>) -> Option<&'a str> { + str_at(map, self.name_va().ok()?) + } + + /// The `.data` section VA at offset 0x18 (zero-filled on disk), 4 to + /// 12 bytes below the slots of the references that use the library. + #[inline] + pub fn data_slot_va(&self) -> Result { + read_u32_le(self.bytes, 0x18) + } + + /// Flags at offset 0x20: with bit 0 or 1 set the runtime does not check + /// the loaded library's GUID against [`guid`](Self::guid); the compiler + /// reads bit 1 as an extended type library. 0 in every fixture. + #[inline] + pub fn flags(&self) -> Result { + read_u32_le(self.bytes, 0x20) + } +} + +/// A reference to an interface of a type library (0x0C bytes): the +/// descriptor an [`ArgType`](super::functype::ArgType) of code 0x1C or 0x1D +/// names. +/// +/// | Offset | Size | Field | +/// |--------|------|-------| +/// | 0x00 | 4 | `lpTypeLib` ([`TypeLibRef`] of the interface's library) | +/// | 0x04 | 4 | `lpIid` (VA of the interface's 16-byte IID) | +/// | 0x08 | 4 | `lpDataSlot` (.data section VA, zero-filled on disk) | +/// +/// `typerefs`: `Collection` is VBA's `_Collection` +/// (`A4C46780-499F-101B-BB78-00AA00383CBB`), `StdFont` stdole's `Font`, +/// `IUnknown` stdole's `IUnknown` (code 0x1C), `VB.TextBox` VB's `TextBox` +/// (`33AD4EE1-6699-11CF-B70C-00AA0060D393`). One reference serves every use +/// of the interface in the project. +#[derive(Clone, Copy, Debug)] +pub struct InterfaceRef<'a> { + bytes: &'a [u8], +} + +impl<'a> InterfaceRef<'a> { + /// Size of the structure in bytes. + pub const SIZE: usize = 0x0C; + + /// Parses an interface reference from the given byte slice. + /// + /// # Errors + /// + /// Returns [`Error::TooShort`] if `data.len() < 0x0C`. + pub fn parse(data: &'a [u8]) -> Result { + let bytes = data.get(..Self::SIZE).ok_or(Error::TooShort { + expected: Self::SIZE, + actual: data.len(), + context: "InterfaceRef", + })?; + Ok(Self { bytes }) + } + + /// Reads the interface reference at `va`. + /// + /// # Errors + /// + /// Returns an error if `va` is not mapped or too few bytes follow it. + pub fn at(map: &AddressMap<'a>, va: u32) -> Result { + Self::parse(map.slice_from_va(va, Self::SIZE)?) + } + + /// VA of the interface's library ([`TypeLibRef`]) at offset 0x00. + #[inline] + pub fn typelib_va(&self) -> Result { + read_u32_le(self.bytes, 0x00) + } + + /// Reads the interface's library. + pub fn typelib(&self, map: &AddressMap<'a>) -> Option> { + TypeLibRef::at(map, self.typelib_va().ok()?).ok() + } + + /// VA of the interface's IID at offset 0x04. + #[inline] + pub fn iid_va(&self) -> Result { + read_u32_le(self.bytes, 0x04) + } + + /// Reads the interface's IID. + pub fn iid(&self, map: &AddressMap<'_>) -> Option { + guid_at(map, self.iid_va().ok()?) + } + + /// The `.data` section VA at offset 0x08. + #[inline] + pub fn data_slot_va(&self) -> Result { + read_u32_le(self.bytes, 0x08) + } +} + +/// A reference to a record (user-defined type) of a type library (0x14 +/// bytes): the descriptor an [`ArgType`](super::functype::ArgType) of code +/// 0x14 names. +/// +/// | Offset | Size | Field | +/// |--------|------|-------| +/// | 0x00 | 4 | `lpLibId` (VA of the defining library's 16-byte GUID) | +/// | 0x04 | 4 | `lpGuid` (VA of the record's 16-byte GUID) | +/// | 0x08 | 2 | The library's major version | +/// | 0x0A | 2 | The library's minor version | +/// | 0x0C | 4 | The library's LCID | +/// | 0x10 | 4 | `lpDataSlot` (.data section VA, zero-filled on disk) | +/// +/// `typerefs`: `Shapes`' `Public Type Point` names the project's own library +/// (`ACCFB576-1C34-4F63-9142-D21358736A52`, the +/// [`ComRegData::project_guid`](super::comreg::ComRegData::project_guid)), +/// version 1.0 (`MajorVer`, `MinorVer`), LCID 0x409. +#[derive(Clone, Copy, Debug)] +pub struct RecordRef<'a> { + bytes: &'a [u8], +} + +impl<'a> RecordRef<'a> { + /// Size of the structure in bytes. + pub const SIZE: usize = 0x14; + + /// Parses a record reference from the given byte slice. + /// + /// # Errors + /// + /// Returns [`Error::TooShort`] if `data.len() < 0x14`. + pub fn parse(data: &'a [u8]) -> Result { + let bytes = data.get(..Self::SIZE).ok_or(Error::TooShort { + expected: Self::SIZE, + actual: data.len(), + context: "RecordRef", + })?; + Ok(Self { bytes }) + } + + /// Reads the record reference at `va`. + /// + /// # Errors + /// + /// Returns an error if `va` is not mapped or too few bytes follow it. + pub fn at(map: &AddressMap<'a>, va: u32) -> Result { + Self::parse(map.slice_from_va(va, Self::SIZE)?) + } + + /// VA of the defining library's GUID at offset 0x00. + #[inline] + pub fn libid_va(&self) -> Result { + read_u32_le(self.bytes, 0x00) + } + + /// Reads the defining library's GUID. + pub fn libid(&self, map: &AddressMap<'_>) -> Option { + guid_at(map, self.libid_va().ok()?) + } + + /// VA of the record's GUID at offset 0x04. + #[inline] + pub fn guid_va(&self) -> Result { + read_u32_le(self.bytes, 0x04) + } + + /// Reads the record's GUID. + pub fn guid(&self, map: &AddressMap<'_>) -> Option { + guid_at(map, self.guid_va().ok()?) + } + + /// The defining library's version, `(major, minor)`, at offsets 0x08 + /// and 0x0A. + /// + /// # Errors + /// + /// Returns an error if the fields cannot be read. + pub fn version(&self) -> Result<(u16, u16), Error> { + Ok(( + read_u16_le(self.bytes, 0x08)?, + read_u16_le(self.bytes, 0x0A)?, + )) + } + + /// The defining library's LCID at offset 0x0C. + #[inline] + pub fn lcid(&self) -> Result { + read_u32_le(self.bytes, 0x0C) + } + + /// The `.data` section VA at offset 0x10. + #[inline] + pub fn data_slot_va(&self) -> Result { + read_u32_le(self.bytes, 0x10) + } +} diff --git a/src/vb/varstub.rs b/src/vb/varstub.rs deleted file mode 100644 index ccac9d3..0000000 --- a/src/vb/varstub.rs +++ /dev/null @@ -1,330 +0,0 @@ -//! Variable implementation stub descriptors. -//! -//! Pointed to by `PrivateObjectDescriptor.var_stubs_va` (+0x28), this is an -//! array of `wVarCount` VA pointers. Each VA points to a variable-length -//! descriptor that tells the compiler which VBA runtime helper functions -//! implement the property Get/Let/Set accessors for a public variable. -//! -//! **Not read by MSVBVM60.DLL at runtime** - this is compiler/IDE metadata -//! only. However, the data is present in compiled binaries and useful for -//! understanding which runtime functions a variable depends on. -//! -//! # Entry Layout -//! -//! | Offset | Size | Field | -//! |--------|------|-------| -//! | 0x00 | 2 | `wHeaderSize` - header bytes before name data (0x0C + params*4) | -//! | 0x02 | 2 | `wDataSize` - size of name/data section after header | -//! | 0x04 | 2 | Reserved (zero) | -//! | 0x06 | 2 | `wParamCount` - indexed property parameter count | -//! | 0x08 | 2 | `wDataSize2` - copy of wDataSize | -//! | 0x0A | 1 | `bFlags1` | -//! | 0x0B | 1 | `bFlags2` | -//! | 0x0C | N×4 | Parameter descriptors: `[{u16 offset, u16 type}]` × wParamCount | -//! | +hdr | var | Null-terminated name strings (VBA runtime function names) | - -use std::str; - -use crate::{ - addressmap::AddressMap, - error::Error, - util::{read_cstr, read_u16_le, read_u32_le}, -}; - -/// A variable implementation stub descriptor. -/// -/// Describes which VBA runtime helper functions implement the property -/// accessors for a public variable (e.g., `__vbaDateVar` for Date Get, -/// `__vbaVarSetVar` for Variant Set). -#[derive(Clone, Copy, Debug)] -pub struct VarStubDesc<'a> { - bytes: &'a [u8], -} - -impl<'a> VarStubDesc<'a> { - /// Minimum header size. - pub const MIN_SIZE: usize = 0x0C; - - /// Parses a variable stub descriptor from the given byte slice. - pub fn parse(data: &'a [u8]) -> Option { - if data.len() < Self::MIN_SIZE { - return None; - } - Some(Self { bytes: data }) - } - - /// Header size at offset 0x00 (0x0C + param_count * 4). - #[inline] - pub fn header_size(&self) -> Result { - read_u16_le(self.bytes, 0x00) - } - - /// Data section size at offset 0x02. - #[inline] - pub fn data_size(&self) -> Result { - read_u16_le(self.bytes, 0x02) - } - - /// Number of indexed property parameters at offset 0x06. - #[inline] - pub fn param_count(&self) -> Result { - read_u16_le(self.bytes, 0x06) - } - - /// Flags byte 1 at offset 0x0A. - #[inline] - pub fn flags1(&self) -> u8 { - self.bytes.get(0x0A).copied().unwrap_or(0) - } - - /// Flags byte 2 at offset 0x0B. - /// - /// Determines the data section format: - /// - /// | Value | Meaning | - /// |-------|---------| - /// | `0x04` | Inline VBA runtime function names | - /// | `0x2C` | Inline property names (Default, CaseSensitive) or VA pointers to DLL+API | - /// | `0x34` | Extended Declare-style entries (DLL+API function references) | - /// - /// Bit 2 (0x04) is always set. Bit 3 (0x08) = has property accessor names. - /// Bit 5 (0x20) = has COM interface data. - #[inline] - pub fn flags2(&self) -> u8 { - self.bytes.get(0x0B).copied().unwrap_or(0) - } - - /// Total size of this entry (header + data). - #[inline] - pub fn total_size(&self) -> usize { - let hdr = self.header_size().unwrap_or(0) as usize; - let data = self.data_size().unwrap_or(0) as usize; - hdr.saturating_add(data) - } - - /// Returns `true` if the data section contains VA pointers instead of - /// inline strings. Detected by checking if the first data byte is - /// non-printable ASCII (binary data / VA pointer). - pub fn has_va_data(&self) -> bool { - let Ok(hdr) = self.header_size() else { - return false; - }; - let Some(&first) = self.bytes.get(hdr as usize) else { - return false; - }; - first != 0 && !(0x20..=0x7E).contains(&first) - } - - /// Resolves VA-pointer data section entries to DLL+API name pairs. - /// - /// When [`has_va_data()`](Self::has_va_data) is true, the data section - /// contains VA pointers to null-terminated strings (DLL library name - /// and API function name). Returns empty if the data is inline strings. - pub fn resolve_api_names<'b>(&self, map: &AddressMap<'b>) -> Vec<&'b str> { - if !self.has_va_data() { - return Vec::new(); - } - let Ok(hdr) = self.header_size() else { - return Vec::new(); - }; - let Some(data) = self.bytes.get(hdr as usize..) else { - return Vec::new(); - }; - let mut result = Vec::new(); - let mut pos: usize = 0; - while pos.saturating_add(4) <= data.len() { - let chunk = match data.get(pos..pos.saturating_add(4)) { - Some(c) => c, - None => break, - }; - let arr: [u8; 4] = match chunk.try_into() { - Ok(a) => a, - Err(_) => break, - }; - let va = u32::from_le_bytes(arr); - pos = pos.saturating_add(4); - if va == 0 { - continue; - } - if let Ok(off) = map.va_to_offset(va) - && let Ok(name) = read_cstr(map.file(), off) - && let Ok(s) = str::from_utf8(name) - && !s.is_empty() - { - result.push(s); - } - } - result - } - - /// Returns the first name string from the data section. - /// - /// This is typically a VBA runtime function name (e.g., `__vbaDateVar`) - /// or a method name (e.g., `Pack`). Returns empty if the data section - /// starts with binary data (VA pointers) rather than a string. - pub fn name(&self) -> &'a str { - let Ok(hdr) = self.header_size() else { - return ""; - }; - let Some(data) = self.bytes.get(hdr as usize..) else { - return ""; - }; - // Skip up to 4 leading null bytes (alignment padding) - let start = data - .iter() - .take(4) - .position(|&b| b != 0) - .unwrap_or(4) - .min(data.len()); - let Some(data) = data.get(start..) else { - return ""; - }; - // Validate first byte is printable ASCII (not a VA/binary data) - let Some(&first) = data.first() else { - return ""; - }; - if !(0x20..=0x7E).contains(&first) { - return ""; - } - let end = data.iter().position(|&b| b == 0).unwrap_or(data.len()); - match data.get(..end) { - Some(s) => str::from_utf8(s).unwrap_or(""), - None => "", - } - } - - /// Returns all name strings from the data section. - pub fn names(&self) -> Vec<&'a str> { - let Ok(hdr) = self.header_size() else { - return Vec::new(); - }; - let Some(data) = self.bytes.get(hdr as usize..) else { - return Vec::new(); - }; - let mut result = Vec::new(); - let mut pos: usize = 0; - while pos < data.len() { - // Skip nulls - while data.get(pos).copied() == Some(0) { - pos = pos.saturating_add(1); - } - if pos >= data.len() { - break; - } - let start = pos; - while let Some(&b) = data.get(pos) { - if b == 0 { - break; - } - pos = pos.saturating_add(1); - } - if let Some(slice) = data.get(start..pos) - && let Ok(s) = str::from_utf8(slice) - && !s.is_empty() - { - result.push(s); - } - } - result - } -} - -/// Iterator over variable stub descriptors from PrivateObjectDescriptor. -#[must_use = "iterators are lazy and do nothing unless consumed"] -pub struct VarStubIter<'a> { - map: &'a AddressMap<'a>, - ptr_array_va: u32, - index: usize, - count: usize, -} - -impl<'a> VarStubIter<'a> { - /// Creates an iterator over `count` variable stubs from the pointer array at `va`. - pub fn new(map: &'a AddressMap<'a>, va: u32, count: u16) -> Self { - Self { - map, - ptr_array_va: va, - index: 0, - count: count as usize, - } - } -} - -impl<'a> Iterator for VarStubIter<'a> { - type Item = VarStubDesc<'a>; - - fn next(&mut self) -> Option { - if self.index >= self.count { - return None; - } - let offset = (self.index as u32).wrapping_mul(4); - let ptr_va = self.ptr_array_va.wrapping_add(offset); - self.index = self.index.saturating_add(1); - - let ptr_data = self.map.slice_from_va(ptr_va, 4).ok()?; - let stub_va = read_u32_le(ptr_data, 0).ok()?; - if stub_va == 0 { - return None; - } - - // Read enough for the header to determine total size - let header_data = self - .map - .slice_from_va(stub_va, VarStubDesc::MIN_SIZE) - .ok()?; - let hdr_size = read_u16_le(header_data, 0x00).ok()? as usize; - let data_size = read_u16_le(header_data, 0x02).ok()? as usize; - let total = hdr_size.saturating_add(data_size); - - let full_data = self - .map - .slice_from_va(stub_va, total.max(VarStubDesc::MIN_SIZE)) - .ok()?; - VarStubDesc::parse(full_data) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // Entry 0 from Cls_Zip: "__vbaDateVar" - const STUB_DATEVAR: [u8; 28] = [ - 0x0C, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x04, 0x04, - // name data: "__vbaDateVar\0\0\0\0" - 0x5F, 0x5F, 0x76, 0x62, 0x61, 0x44, 0x61, 0x74, 0x65, 0x56, 0x61, 0x72, 0x00, 0x00, 0x00, - 0x00, - ]; - - // Entry 3 from Cls_Zip: "Pack" - const STUB_PACK: [u8; 20] = [ - 0x0C, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x04, 0x04, 0x50, 0x61, 0x63, - 0x6B, 0x00, 0x00, 0x00, 0x00, // "Pack\0\0\0\0" - ]; - - #[test] - fn test_simple_stub() { - let stub = VarStubDesc::parse(&STUB_DATEVAR).unwrap(); - assert_eq!(stub.header_size().unwrap(), 0x0C); - assert_eq!(stub.data_size().unwrap(), 0x1C); - assert_eq!(stub.param_count().unwrap(), 0); - assert_eq!(stub.name(), "__vbaDateVar"); - } - - #[test] - fn test_method_stub() { - let stub = VarStubDesc::parse(&STUB_PACK).unwrap(); - assert_eq!(stub.header_size().unwrap(), 0x0C); - assert_eq!(stub.data_size().unwrap(), 0x0C); - assert_eq!(stub.param_count().unwrap(), 0); - assert_eq!(stub.name(), "Pack"); - } - - #[test] - fn test_names() { - let stub = VarStubDesc::parse(&STUB_DATEVAR).unwrap(); - let names = stub.names(); - assert_eq!(names.len(), 1); - assert_eq!(names[0], "__vbaDateVar"); - } -} diff --git a/tests/api.rs b/tests/api.rs index 584c85e..15057cf 100644 --- a/tests/api.rs +++ b/tests/api.rs @@ -1401,3 +1401,459 @@ fn arguments_carry_interfaces_between_procedures() { resolver.simulate_procedure(program, &code, method.pcode_bytes()); assert_eq!(own.get(-0x88), None); } + +/// The late-bound calls by DISPID of method `index` of object `object`, in +/// code order: each DISPID with the member it reaches, resolved through the +/// interfaces the procedure's frame slots hold. +fn late_targets( + project: &'static VbProject<'static>, + resolver: &CallResolver<'static, 'static>, + object: u16, + index: u16, +) -> Vec<(i32, Option)> { + use visualbasic::pcode::stacksim::InstructionStack; + + let method = method(project, object, usize::from(index)); + let code: Vec = method.instructions().unwrap().flatten().collect(); + let (stack, slots) = resolver.simulate_procedure(object, &code, method.pcode_bytes()); + let mut last_load = None; + let mut found = Vec::new(); + for (insn, record) in code.iter().zip(&stack.instructions) { + let pr = match record { + InstructionStack::Reached { pr, .. } | InstructionStack::Cut { pr, .. } => *pr, + _ => last_load, + }; + if let Some(source) = insn.pr_source() { + last_load = Some(source); + } + if let Some(Callee::Late { + dispid: Some(dispid), + target, + .. + }) = resolver + .resolve_with_slots(object, insn, pr.as_ref(), &slots) + .map(|call| call.callee) + { + found.push((dispid, target)); + } + } + found +} + +/// `dispid`: a form's late-bound calls on the UserControl it hosts reach +/// the control's procedures by their prototypes' DISPIDs: its control +/// getter returns the control's class, which names the procedure each call +/// invokes the way the opcode does (a load gets, a store lets, a store of +/// an object sets, a call calls). +#[test] +fn late_calls_reach_a_hosted_user_controls_procedures() { + use visualbasic::pcode::calltarget::{InvokeKind, LateTarget}; + + let project = project_with_dispids(); + let resolver = CallResolver::new(project).unwrap(); + let host = object_index(project, "Host"); + let dial = object_index(project, "Dial"); + let name = |target: &Option| match target { + Some(LateTarget::Procedure { + object, + method, + invoke, + }) => { + assert_eq!(*object, dial); + let object = project + .objects() + .unwrap() + .nth(usize::from(*object)) + .unwrap() + .unwrap(); + let name = object.method_name(*method).unwrap(); + let accessor = match invoke { + InvokeKind::Method => "", + InvokeKind::Get => "get ", + InvokeKind::Let => "let ", + InvokeKind::Set => "set ", + }; + format!("{accessor}{}", name.as_str().unwrap()) + } + other => panic!("{other:?}"), + }; + let named = |method: &str| -> Vec { + late_targets( + project, + &resolver, + host, + method_index(project, host, method), + ) + .iter() + .map(|(_, target)| name(target)) + .collect() + }; + assert_eq!( + named("Properties"), + ["let Value", "get Value", "set Target", "get Target"] + ); + assert_eq!( + named("Indexed"), + [ + "let Item", "get Item", "let Item", "set Slot", "get Item", "get Slot" + ] + ); + // `Scaled(1.5) + Len(Describe("v"))` calls `Describe` first. + assert_eq!( + named("Methods"), + ["Clear", "Spin", "Spin", "Describe", "Scaled"] + ); + assert_eq!( + named("Named"), + ["Spin", "let Item", "get Target", "set Slot", "Describe"] + ); +} + +/// `dispid`: the extender's members of a hosted control are called 0x3000 +/// below the DISPIDs `_VBControlExtender` declares, and reach the members +/// it declares there when the caller's catalog describes it; with no +/// catalog they stay unresolved. +#[test] +fn late_calls_reach_a_hosted_controls_extender() { + use std::sync::Arc; + + use visualbasic::{ + pcode::calltarget::{ + InterfaceCatalog, InterfaceMember, InvokeKind, LateTarget, RuntimeInterfaces, + }, + vb::control::Guid, + }; + + /// `_VBControlExtender`'s members at the DISPIDs `VB6.OLB` declares. + struct Extender; + impl InterfaceCatalog for Extender { + fn members(&self, _iid: &Guid, _vtable_offset: u16) -> Vec { + Vec::new() + } + fn dispatched(&self, iid: &Guid, dispid: i32) -> Vec { + if *iid != RuntimeInterfaces::CONTROL_EXTENDER { + return Vec::new(); + } + let property = match dispid.cast_unsigned() { + 0x8001_3000 => "Name", + 0x8001_3003 => "Left", + 0x8001_3004 => "Top", + 0x8001_3005 => "Width", + 0x8001_3006 => "Height", + 0x8001_3007 => "Visible", + 0x8001_3008 => "Parent", + 0x8001_3009 => "DragMode", + 0x8001_300B => "Tag", + 0x8001_300F => "TabIndex", + 0x8001_301C => "object", + 0x8001_3032 => "HelpContextID", + 0x8001_3046 => "WhatsThisHelpID", + 0x8001_3047 => "Container", + 0x8001_3048 => "CausesValidation", + 0x8001_304A => "ToolTipText", + _ => "", + }; + let method = match dispid.cast_unsigned() { + 0x8001_4000 => "SetFocus", + 0x8001_4001 => "ZOrder", + 0x8001_4002 => "Move", + 0x8001_4003 => "Drag", + 0x8001_4004 => "ShowWhatsThis", + _ => "", + }; + let member = |name: &str, invoke| InterfaceMember { + interface: "_VBControlExtender".to_string(), + name: name.to_string(), + invoke, + dispid: Some(dispid), + arg_widths: Vec::new(), + float_result: false, + returns_interface: None, + }; + match (property, method) { + ("", "") => Vec::new(), + ("", method) => vec![member(method, InvokeKind::Method)], + (property, _) => vec![ + member(property, InvokeKind::Get), + member(property, InvokeKind::Let), + ], + } + } + } + + assert_eq!( + RuntimeInterfaces::CONTROL_EXTENDER.to_string(), + "{164CBDD0-7321-11D1-A1E8-00A0C90F2731}" + ); + let project = project_with_dispids(); + let host = object_index(project, "Host"); + let bare = CallResolver::new(project).unwrap(); + assert!( + late_targets( + project, + &bare, + host, + method_index(project, host, "Extender") + ) + .iter() + .all(|(_, target)| target.is_none()) + ); + + let resolver = CallResolver::new(project) + .unwrap() + .with_interfaces(Arc::new(Extender)); + let named = |method: &str| -> Vec { + late_targets( + project, + &resolver, + host, + method_index(project, host, method), + ) + .iter() + .map(|(dispid, target)| match target { + Some(LateTarget::Members { + iid, + dispid: declared, + members, + }) => { + assert_eq!(*iid, RuntimeInterfaces::CONTROL_EXTENDER); + assert_eq!(declared.wrapping_sub(*dispid), 0x3000); + let invoke = match members.as_slice() { + [only] => only.invoke, + other => panic!("{other:?}"), + }; + let accessor = match invoke { + InvokeKind::Method => "", + InvokeKind::Get => "get ", + InvokeKind::Let => "let ", + InvokeKind::Set => "set ", + }; + format!("{accessor}{}", members[0].name) + } + other => panic!("{dispid:#x}: {other:?}"), + }) + .collect() + }; + assert_eq!( + named("Extender"), + [ + "get Left", + "let Left", + "let Visible", + "get Name", + "let Tag", + "get Tag", + "get Width" + ] + ); + assert_eq!( + named("ExtenderMembers"), + [ + "SetFocus", + "ZOrder", + "Move", + "Drag", + "ShowWhatsThis", + "get Height", + "let Top", + "let TabIndex", + "let ToolTipText", + "let HelpContextID", + "let WhatsThisHelpID", + "let CausesValidation", + "let DragMode", + "get Parent", + "get Container", + "get object" + ] + ); +} + +/// `activex`: a hosted ActiveX control's form record names its class by +/// ProgID, with or without an event handler (`CommonDialog1` has none), +/// so the late-bound calls on it, and on an element of a control array of +/// it, ask the caller's catalog for the members of its class's default +/// interface. +#[test] +fn late_calls_reach_an_activex_controls_class() { + use std::sync::Arc; + + use visualbasic::{ + pcode::calltarget::{InterfaceCatalog, InterfaceMember, InvokeKind, LateTarget}, + vb::control::Guid, + }; + + /// Any member of any interface, named by its interface's IID. + struct Any; + impl InterfaceCatalog for Any { + fn members(&self, _iid: &Guid, _vtable_offset: u16) -> Vec { + Vec::new() + } + fn dispatched(&self, iid: &Guid, dispid: i32) -> Vec { + [InvokeKind::Method, InvokeKind::Let] + .into_iter() + .map(|invoke| InterfaceMember { + interface: iid.to_string(), + name: format!("{dispid:#x}"), + invoke, + dispid: Some(dispid), + arg_widths: Vec::new(), + float_result: false, + returns_interface: None, + }) + .collect() + } + } + + let project = project("activex"); + let components: Vec = project + .components() + .unwrap() + .map(|component| component.prog_id().into_owned()) + .collect(); + let client = object_index(project, "Client"); + let object = project + .objects() + .unwrap() + .nth(usize::from(client)) + .unwrap() + .unwrap(); + let gui = project + .gui_entries() + .unwrap() + .find(|entry| entry.object_index().ok() == Some(u32::from(client))) + .unwrap(); + let form = object.form_data_from_gui_entry(&gui).unwrap(); + let records: Vec<(String, String)> = form + .controls() + .iter() + .map(|record| { + ( + record.name().into_owned(), + record.prog_id().unwrap().into_owned(), + ) + }) + .collect(); + assert!( + records + .iter() + .all(|(_, prog_id)| components.contains(prog_id)) + ); + assert!(records.contains(&( + "CommonDialog1".to_string(), + "MSComDlg.CommonDialog".to_string() + ))); + + let resolver = CallResolver::new(project) + .unwrap() + .with_interfaces(Arc::new(Any)); + let interfaces = |method: &str| -> Vec { + late_targets( + project, + &resolver, + client, + method_index(project, client, method), + ) + .into_iter() + .map(|(dispid, target)| match target { + Some(LateTarget::Members { iid, members, .. }) => { + assert!(members.iter().all(|m| m.name == format!("{dispid:#x}"))); + iid.to_string() + } + other => format!("{other:?}"), + }) + .collect() + }; + // `CommonDialog`'s default interface, `ICommonDialog`. + assert_eq!( + interfaces("PickFile"), + ["{083039C2-13F4-11D1-8B7E-0000F8754DA1}"; 3] + ); + // `Winsock`'s, `IMSWinsockControl`: the four calls on `Winsock1`, and + // the two on `Peers(0)`, an element `Item` returns from the array. + assert_eq!( + interfaces("Start"), + ["{248DD892-BB45-11CF-9ABC-0080C7E7B78D}"; 6] + ); + // Every call by DISPID of the form, its handlers' included. + let mut resolved = 0; + for (index, entry) in object.methods().unwrap().enumerate() { + if !matches!(entry, Ok(MethodEntry::PCode(_))) { + continue; + } + let index = u16::try_from(index).unwrap(); + for (dispid, target) in late_targets(project, &resolver, client, index) { + assert!(target.is_some(), "method {index}: {dispid:#x}"); + resolved += 1; + } + } + assert!(resolved >= 20, "{resolved}"); +} + +/// Every fixture: each call by DISPID reaches a member once its receiver's +/// class is known, given a catalog that describes every external member. +#[test] +fn every_call_by_dispid_reaches_a_member() { + use std::sync::Arc; + + use visualbasic::{ + pcode::calltarget::{InterfaceCatalog, InterfaceMember, InvokeKind}, + vb::control::Guid, + }; + + /// Every member of every interface, each invoked every way. + struct Every; + impl InterfaceCatalog for Every { + fn members(&self, _iid: &Guid, _vtable_offset: u16) -> Vec { + Vec::new() + } + fn dispatched(&self, iid: &Guid, dispid: i32) -> Vec { + [ + InvokeKind::Method, + InvokeKind::Get, + InvokeKind::Let, + InvokeKind::Set, + ] + .into_iter() + .map(|invoke| InterfaceMember { + interface: iid.to_string(), + name: format!("{dispid:#x}"), + invoke, + dispid: Some(dispid), + arg_widths: Vec::new(), + float_result: false, + returns_interface: None, + }) + .collect() + } + } + + let mut unresolved = Vec::new(); + let mut resolved = 0; + for name in common::projects() { + let project = project(&name); + let resolver = CallResolver::new(project) + .unwrap() + .with_interfaces(Arc::new(Every)); + for (object, entry) in project.objects().unwrap().enumerate() { + let object = u16::try_from(object).unwrap(); + let Ok(entry) = entry else { + continue; + }; + for (index, method) in entry.methods().unwrap().enumerate() { + if !matches!(method, Ok(MethodEntry::PCode(_))) { + continue; + } + let index = u16::try_from(index).unwrap(); + for (dispid, target) in late_targets(project, &resolver, object, index) { + match target { + Some(_) => resolved += 1, + None => unresolved.push(format!("{name} {object}.{index} {dispid:#x}")), + } + } + } + } + } + assert!(unresolved.is_empty(), "{unresolved:#?}"); + assert!(resolved >= 60, "{resolved}"); +} diff --git a/tests/descriptors.rs b/tests/descriptors.rs new file mode 100644 index 0000000..19aa41a --- /dev/null +++ b/tests/descriptors.rs @@ -0,0 +1,334 @@ +//! The descriptors the constant pool holds for the P-Code that names them, +//! and the structures whose layout the extents walk measures, checked on +//! the compiled fixtures against their source. + +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] + +mod common; + +use visualbasic::{ + VbObject, VbProject, + pcode::decoder::PoolEntryRole, + project::MethodLinkKind, + vb::{ + control::{ControlKind, DispidMapping, Guid}, + events::EventSinkVtable, + pooldesc::{ + ArrayDescriptor, CreationDescriptor, IoItems, IoSeparator, RecordIoDescriptor, + RecordIoKind, + }, + projectinfo2::ControlTypeIter, + typeref::TypeLibRef, + }, +}; + +use crate::common::{fixture, projects}; + +/// Reads fixture `name`, leaked so its project outlives the test's borrows. +fn project(name: &str) -> &'static VbProject<'static> { + let bytes: &'static [u8] = Box::leak(std::fs::read(fixture(name)).unwrap().into_boxed_slice()); + Box::leak(Box::new(VbProject::from_bytes(bytes).unwrap())) +} + +/// The object named `name`. +fn object(project: &'static VbProject<'static>, name: &str) -> VbObject<'static, 'static> { + project + .objects() + .unwrap() + .map(Result::unwrap) + .find(|o| o.name().unwrap() == name) + .unwrap_or_else(|| panic!("no object {name}")) +} + +/// The VA of every constant pool entry an instruction `mnemonic` names in +/// `project`, with the role the opcode gives it. +fn pool_entries(project: &'static VbProject<'static>, mnemonic: &str) -> Vec<(u32, PoolEntryRole)> { + let map = project.address_map(); + let mut out = Vec::new(); + for object in project.objects().unwrap().map(Result::unwrap) { + for method in object.pcode_methods().unwrap().map(Result::unwrap) { + let pool = method.constant_pool(map); + let code = method.pcode_bytes(); + for instruction in method.instructions().unwrap().flatten() { + if instruction.info.mnemonic != mnemonic { + continue; + } + for reference in instruction.pool_references(code) { + out.push((pool.va_at(reference.index).unwrap(), reference.role)); + } + } + } + } + out.sort_unstable(); + out.dedup(); + out +} + +/// `records`' `Get #f, 1, o` of an `Outer`: nine entries, three of them +/// `Inner` records (one, a fixed array of three and a dynamic array) whose +/// descriptors follow the entries; `coverage`'s `FileRec` has one, its +/// `String * 8` 0x14 bytes in. +#[test] +fn record_io_descriptors_describe_their_records() { + let records = project("records"); + let [(va, role)] = pool_entries(records, "GetRecOwn4")[..] else { + panic!("one GetRecOwn4 descriptor"); + }; + assert_eq!(role, PoolEntryRole::RecordIo); + let outer = RecordIoDescriptor::at(records.address_map(), va).unwrap(); + let entries = outer.entries(); + assert_eq!(entries.len(), 9); + let nested: Vec = entries + .iter() + .enumerate() + .filter(|(_, e)| e.kind() == RecordIoKind::Record) + .map(|(i, _)| i) + .collect(); + assert_eq!(nested.len(), 3, "In1, Ins(2) and DynIn()"); + assert!(entries[nested[1]].is_fixed_array()); + assert_eq!(entries[nested[1]].count, 3); + assert!(entries[nested[2]].is_dynamic_array()); + for index in nested { + let inner = outer.nested(index).unwrap(); + let kinds: Vec = inner.entries().iter().map(|e| e.kind()).collect(); + assert_eq!(kinds[0], RecordIoKind::String, "Inner.S"); + } + assert_eq!(outer.extent_size(), Some(0xDC)); + + let coverage = project("coverage"); + let [(va, _)] = pool_entries(coverage, "GetRecOwn4")[..] else { + panic!("one GetRecOwn4 descriptor"); + }; + let file_rec = RecordIoDescriptor::at(coverage.address_map(), va).unwrap(); + assert_eq!(file_rec.record_size().unwrap(), 0x24); + let [entry] = file_rec.entries()[..] else { + panic!("one entry"); + }; + assert_eq!( + (entry.gap, entry.kind(), entry.value), + (0x14, RecordIoKind::FixedString, 8) + ); + assert_eq!(file_rec.extent_size(), Some(22)); +} + +/// `forms`' `Print s`: one `String` item and a new line. +#[test] +fn io_items_read_their_items() { + let forms = project("forms"); + let [(va, role)] = pool_entries(forms, "PrintObject")[..] else { + panic!("one PrintObject list"); + }; + assert_eq!(role, PoolEntryRole::IoItems); + let items = IoItems::at(forms.address_map(), va).unwrap(); + let [item] = items.items().collect::>()[..] else { + panic!("one item"); + }; + assert_eq!( + (item.item_type(), item.separator()), + (8, IoSeparator::NewLine) + ); + assert_eq!(items.size(), 3); +} + +/// `data`'s `Record.Flags(3) As Byte` and `records`' `Outer.Grid(1 To 2, 3 +/// To 5) As Integer`, the fixed arrays `AryInRecLdPr` indexes. +#[test] +fn array_descriptors_give_their_bounds() { + let data = project("data"); + let [(va, role)] = pool_entries(data, "AryInRecLdPr")[..] else { + panic!("one array"); + }; + assert_eq!(role, PoolEntryRole::ArrayDescriptor); + let flags = ArrayDescriptor::at(data.address_map(), va).unwrap(); + assert_eq!( + (flags.dimensions().unwrap(), flags.element_size().unwrap()), + (1, 1) + ); + assert_eq!(flags.bounds(), [(4, 0)]); + assert_eq!(flags.size(), 0x18); + + let records = project("records"); + let [(va, _)] = pool_entries(records, "AryInRecLdPr")[..] else { + panic!("one array"); + }; + let grid = ArrayDescriptor::at(records.address_map(), va).unwrap(); + assert_eq!(grid.bounds(), [(3, 3), (2, 1)], "rightmost dimension first"); + assert_eq!(grid.size(), 0x20); +} + +/// `New` of a class outside the project names a creation descriptor +/// (`forms`: VB's `Global` as `VBGlobal`); of a project class, its +/// `ObjectInfo`; `members`' `Holder.Auto As New Collection` reaches its +/// descriptor through its accessors' pushed pool index. +#[test] +fn creation_descriptors_name_their_class() { + let forms = project("forms"); + let map = forms.address_map(); + let guid = |va: u32| Guid::from_bytes(map.slice_from_va(va, 16).unwrap()).unwrap(); + let created: Vec<(u32, PoolEntryRole)> = pool_entries(forms, "NewIfNullPr"); + assert!( + created + .iter() + .all(|(_, role)| *role == PoolEntryRole::Creation) + ); + let global = created + .iter() + .find_map(|&(va, _)| CreationDescriptor::at(map, va)) + .unwrap(); + assert_eq!(global.flags().unwrap(), 2); + assert_eq!( + guid(global.clsid_va().unwrap()).to_string(), + "{FCFB3D23-A0FA-1068-A738-08002B3371B5}" + ); + + let members = project("members"); + let holder = object(members, "Holder"); + let pool_va = holder.info().constants_va().unwrap(); + let pushed: Vec<[u32; 2]> = holder + .method_links() + .unwrap() + .filter_map(|link| match link.unwrap().kind { + MethodLinkKind::Variable { + pushed: Some(pushed), + .. + } => Some(pushed), + _ => None, + }) + .collect(); + assert!(!pushed.is_empty()); + assert!( + pushed + .iter() + .all(|&[index, pool]| (index, pool) == (0, pool_va)) + ); + let entry = holder.constants_pool().unwrap().va_at(0).unwrap(); + let auto = CreationDescriptor::at(members.address_map(), entry).unwrap(); + assert_eq!( + Guid::from_bytes( + members + .address_map() + .slice_from_va(auto.clsid_va().unwrap(), 16) + .unwrap() + ) + .unwrap() + .to_string(), + "{A4C4671C-499F-101B-BB78-00AA00383CBB}", + "VBA's Collection" + ); +} + +/// The private object descriptor lists the layouts of a class's `Type`s in +/// declaration order, one layout per shape: `udts` `Shapes` declares five, +/// `Second.Pair` has `Shapes.PrivT`'s members and shares its layout. +#[test] +fn record_layouts_follow_the_type_declarations() { + let udts = project("udts"); + let sizes: Vec = object(udts, "Shapes") + .record_layouts() + .unwrap() + .iter() + .map(|(_, layout)| layout.record_size().unwrap()) + .collect(); + // Point, Named, Third, PrivT, Holder. + assert_eq!(sizes, [0x08, 0x20, 0x0C, 0x08, 0x08]); + let shapes = object(udts, "Shapes").record_layouts().unwrap(); + let second = object(udts, "Second").record_layouts().unwrap(); + assert_eq!(second.len(), 1); + assert_eq!(second[0].0, shapes[3].0, "Pair shares PrivT's layout"); + for name in projects() { + let project = project(&name); + for object in project.objects().unwrap().map(Result::unwrap) { + assert!(object.record_layouts().is_ok(), "{name}"); + } + } +} + +/// A `WithEvents` variable's entry maps each event of its source to the +/// procedure that handles it: `events` `Listener.m_Source` handles +/// `Started`, `Ticked` and `Named`. +#[test] +fn dispid_maps_name_the_handlers() { + let events = project("events"); + let listener = object(events, "Listener"); + let map = events.address_map(); + let control = listener + .controls() + .unwrap() + .map(Result::unwrap) + .find(|c| c.info().kind().unwrap() == ControlKind::WithEvents) + .unwrap(); + let pairs = control.info().dispid_map(map); + assert_eq!( + pairs.iter().map(|p| p.source).collect::>(), + [1, 2, 3], + "event numbers" + ); + assert!( + pairs + .iter() + .all(|DispidMapping { handler, .. }| handler >> 16 == 0x6003) + ); + // The sink vtable follows the map. + let sink = control.info().event_sink_vtable_va().unwrap(); + assert_eq!( + sink, + control.info().dispid_map_va().unwrap() + 8 * u32::try_from(pairs.len()).unwrap() + ); +} + +/// An `Implements`' sink vtable has four zero slots after the interface's +/// members: `calls` `Square` implements `Shape`. +#[test] +fn implements_sinks_end_in_four_zero_slots() { + let calls = project("calls"); + let map = calls.address_map(); + let square = object(calls, "Square"); + let control = square + .controls() + .unwrap() + .map(Result::unwrap) + .find(|c| c.info().kind().unwrap() == ControlKind::Implements) + .unwrap(); + let info = control.info(); + let va = info.event_sink_vtable_va().unwrap(); + let sink = EventSinkVtable::parse(map.slice_from_va(va, 0x18).unwrap(), info).unwrap(); + let slots = usize::from(info.event_handler_slots().unwrap()); + assert_eq!(sink.size(), 0x18 + 4 * (slots + 4)); + let tail = &map.slice_from_va(va, sink.size()).unwrap()[sink.size() - 16..sink.size()]; + assert_eq!(tail, [0; 16]); +} + +/// The VB header is 0x78 bytes, its last 16 `GUID_NULL`; every type library +/// reference ends in one address the binary repeats. +#[test] +fn header_and_type_library_references_have_their_size() { + for name in projects() { + let project = project(&name); + assert_eq!( + project.vb_header().reserved_guid().unwrap().to_string(), + "{00000000-0000-0000-0000-000000000000}", + "{name}" + ); + assert_eq!(project.vb_header().as_bytes().len(), 0x78); + } + let typerefs = project("typerefs"); + let map = typerefs.address_map(); + let mut tails = Vec::new(); + for entry in ControlTypeIter::new(map, typerefs.object_table().project_info2_va().unwrap()) { + if let Ok(typelib) = TypeLibRef::at(map, entry.typelib_va) { + let bytes = map + .slice_from_va(entry.typelib_va, TypeLibRef::SIZE) + .unwrap(); + tails.push(u32::from_le_bytes(bytes[0x24..0x28].try_into().unwrap())); + assert_eq!(typelib.flags().unwrap(), 0); + } + } + tails.dedup(); + assert_eq!(tails, [0x02085A90]); +} diff --git a/tests/extents.rs b/tests/extents.rs new file mode 100644 index 0000000..d7b51fc --- /dev/null +++ b/tests/extents.rs @@ -0,0 +1,229 @@ +//! The extents [`VbProject::structure_extents`] states, checked on the +//! compiled fixtures against what the structures' own fields say and against +//! every native code address the crate knows. + +#![allow( + clippy::arithmetic_side_effects, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic, + clippy::unwrap_used +)] + +mod common; + +use std::collections::BTreeSet; + +use goblin::{ + options::ParseMode, + pe::{PE, options::ParseOptions}, +}; +use visualbasic::{ + VbProject, + extents::{StructureExtent, StructureKind}, + vb::{ + object::{ObjectInfo, OptionalObjectInfo, PublicObjectDescriptor}, + privateobj::PrivateObjectDescriptor, + }, +}; + +use crate::common::{fixture, projects}; + +/// Reads fixture `name`, leaked so its project outlives the test's borrows. +fn project(name: &str) -> (&'static [u8], &'static VbProject<'static>) { + let bytes: &'static [u8] = Box::leak(std::fs::read(fixture(name)).unwrap().into_boxed_slice()); + ( + bytes, + Box::leak(Box::new(VbProject::from_bytes(bytes).unwrap())), + ) +} + +/// The bytes `extent` covers. +fn bytes(project: &VbProject<'static>, extent: &StructureExtent) -> &'static [u8] { + let len = (extent.extent.end - extent.extent.start) as usize; + &project + .address_map() + .slice_from_va(extent.extent.start, len) + .unwrap()[..len] +} + +/// The PE entry point goblin reads, which [`VbProject::native_entries`] +/// must hold. +fn pe_entry(file: &[u8], project: &VbProject<'static>) -> u32 { + let options = ParseOptions::default() + .with_parse_resources(false) + .with_parse_mode(ParseMode::Permissive); + let pe = PE::parse_with_opts(file, &options).unwrap(); + project.address_map().image_base() + + pe.header + .optional_header + .unwrap() + .standard_fields + .address_of_entry_point +} + +/// **No extent covers native code.** Every address of native code the crate +/// knows lies outside every extent, on every fixture: the entries +/// ([`VbProject::native_entries`]) and the code each unwind record names. +#[test] +fn no_extent_covers_code() { + for name in projects() { + let (file, project) = project(&name); + let extents = project.structure_extents(); + let mut code = project.native_entries(); + assert!( + code.contains(&pe_entry(file, project)), + "{name}: entry point" + ); + for procedure in project.unwind_records() { + code.extend(procedure.info.code_vas(project.address_map())); + } + for va in &code { + if let Some(structure) = extents.iter().find(|s| s.extent.contains(va)) { + panic!("{name}: code at {va:#x} inside {structure:?}"); + } + } + } +} + +/// **Extents do not overlap.** Two structures share bytes only when they +/// are the same structure, which the walk states once. +#[test] +fn extents_are_disjoint() { + for name in projects() { + let (_, project) = project(&name); + let extents = project.structure_extents(); + for pair in extents.windows(2) { + assert!( + pair[0].extent.end <= pair[1].extent.start, + "{name}: {:?} overlaps {:?}", + pair[0], + pair[1] + ); + } + } +} + +/// Every fixed-size structure has its size, every name ends at its NUL, +/// every string constant's length prefix gives its length, and every +/// procedure's P-Code is the bytes its method reads. +#[test] +fn extents_match_their_structures() { + for name in projects() { + let (_, project) = project(&name); + let extents = project.structure_extents(); + for structure in &extents { + let held = bytes(project, structure); + let fixed = match structure.kind { + StructureKind::ObjectDescriptor => Some(PublicObjectDescriptor::SIZE), + StructureKind::ObjectInfo => Some(ObjectInfo::SIZE), + StructureKind::OptionalObjectInfo => Some(OptionalObjectInfo::SIZE), + StructureKind::PrivateObjectDescriptor => Some(PrivateObjectDescriptor::SIZE), + StructureKind::Guid => Some(16), + _ => None, + }; + if let Some(size) = fixed { + assert_eq!(held.len(), size, "{name}: {structure:?}"); + } + match structure.kind { + StructureKind::Name => { + let (last, text) = held.split_last().unwrap(); + assert_eq!(*last, 0, "{name}: {structure:?}"); + assert!(!text.contains(&0), "{name}: {structure:?}"); + } + StructureKind::StringConstant => { + let prefix = u32::from_le_bytes(held[..4].try_into().unwrap()); + assert_eq!(prefix as usize, held.len() - 6, "{name}: {structure:?}"); + assert_eq!(&held[held.len() - 2..], [0, 0], "{name}: {structure:?}"); + } + StructureKind::MemberName => { + assert_eq!(&held[held.len() - 2..], [0, 0], "{name}: {structure:?}"); + } + StructureKind::ResumeTable => { + assert_eq!(&held[held.len() - 4..], [0; 4], "{name}: {structure:?}"); + } + _ => {} + } + } + let pcode: BTreeSet<(u32, u32)> = extents + .iter() + .filter(|s| s.kind == StructureKind::Pcode) + .map(|s| (s.extent.start, s.extent.end)) + .collect(); + for object in project.objects().unwrap().map(Result::unwrap) { + for method in object.pcode_methods().unwrap().map(Result::unwrap) { + let extent = (method.pcode_va(), method.proc_dsc_va()); + assert!(pcode.contains(&extent), "{name}: P-Code {extent:x?}"); + assert_eq!( + (extent.1 - extent.0) as usize, + method.pcode_bytes().len(), + "{name}" + ); + } + } + } +} + +/// The kinds a form project and an ActiveX project hold appear: the project +/// tables, the object structures, procedures, controls, forms and, for the +/// OCX, its components and COM registration. +#[test] +fn every_kind_is_found() { + let expect = |name: &str, kinds: &[StructureKind]| { + let (_, project) = project(name); + let found: BTreeSet = + project.structure_extents().iter().map(|s| s.kind).collect(); + for kind in kinds { + assert!(found.contains(kind), "{name}: no {kind:?}"); + } + }; + use StructureKind::*; + expect( + "forms", + &[ + VbHeader, + ProjectData, + ObjectTable, + ProjectInfo2, + EventSource, + ObjectDescriptor, + ObjectInfo, + OptionalObjectInfo, + PrivateObjectDescriptor, + MethodTable, + MethodLinkTable, + PointerTable, + Name, + Guid, + FunctionType, + ConstantPool, + StringConstant, + ProcedureDescriptor, + Pcode, + ControlInfo, + EventSinkVtable, + GuiTableEntry, + FormData, + TypeLibReference, + ], + ); + expect("ocx", &[Component, ComRegistration]); + expect("events", &[DispidMap]); + expect( + "types", + &[ExternalTable, DeclareDescriptor, OptionalDefaults], + ); + expect("members", &[Member, InterfaceReference]); + expect("late", &[MemberName]); + expect("coverage", &[RecordLayout]); + expect("typerefs", &[RecordReference, RecordLayout]); + expect( + "errors-native", + &[ + UnwindRecord, + ErrorHandlerTable, + ResumeTable, + LineNumberTable, + ], + ); +} diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index 9a3e4d4..340369b 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -18,7 +18,7 @@ contains none of it. | `flow-native` | `flow`'s source compiled to native code (`CompilationType=0`): a project with no P-code. | | `data` | `ReDim`, `ReDim Preserve`, multi-dimensional, static and UDT arrays, `Erase`, `LBound`/`UBound`; UDTs with fixed-length strings and arrays; `Variant` operators, `Like`, `Null`; `Currency` and `Date` arithmetic; the string functions and the `Mid` statement; every conversion function; math; file I/O in each mode (`Print #`, `Write #`, `Input #`, `Line Input #`, `Get`/`Put` of a UDT with and without a record number, `Seek`, `EOF`, `LOF`); `Debug.Print`; `Is`, `TypeOf`, `IsMissing`, `IsNull`, `VarType`, `TypeName`. | | `forms` | A form's private Subs and Function called from event handlers and from each other, public members called from a module, `Me`, a control array (an element loaded at run time, the `Index` argument), the `Controls` collection, and a UserControl with a property, an event and a constituent control. | -| `dispid` | Calls by DISPID (`LateId*`): a form's calls on the UserControl it hosts, which go through the control's extender - properties, indexed and object properties, methods, named arguments. Also the `LateMem*` forms `late` lacks: a plain property store, named arguments to a Sub, a named indexed store and a named `Set`. | +| `dispid` | Calls by DISPID (`LateId*`): a form's calls on the UserControl it hosts, which go through the control's extender - properties, indexed and object properties, methods, named arguments, and the extender's own properties and methods. Also the `LateMem*` forms `late` lacks: a plain property store, named arguments to a Sub, a named indexed store and a named `Set`. | | `events` | `WithEvents` handlers, `RaiseEvent` with `ByVal` and `ByRef` arguments, `Class_Initialize`/`Terminate`, `Implements` of an interface with methods of each return type called through it, private class methods of each return type called through `Me`, a `Friend` call. | | `statics` | Storage outside the frame: a module's own `Private` and `Public` variables (`FMem*` on the module data block at `ebp+8`), `Static` locals and a `Static Function` in a module and in a class (a block whose pointer is at module data + 0x3C, `Me` + 0x40, loaded into Pr), a UDT and an array member, `With` on a module UDT, another module's `Public` variables (`ImpAd*`). | | `vtable` | Calls that return their value without an HRESULT: through an interface of `Raw.tlb` (typed `VCall*`), and `Declare`d functions of every return type (typed `ImpAdCall*`). Compiles only; nothing in it is meant to run. | @@ -31,8 +31,13 @@ contains none of it. | `docs` | An ActiveX document DLL (`docs.dll`): a UserDocument with a property, a method, handlers for its own and its controls' events, and a public class. | | `server` | An ActiveX EXE (`Type=OleExe`): one class per `Instancing` value (`Private`, `PublicNotCreatable`, `SingleUse`, `GlobalSingleUse`, `MultiUse`, `GlobalMultiUse`), each with a property, a method and an event, used from `Sub Main`. | | `events-native` | `events`' source compiled to native code: classes with publics, `Implements` and `WithEvents`, with no P-code. | +| `errors-native` | Error handling compiled to native code, each procedure varying one table of its unwind record: two and three `On Error GoTo` labels, `On Error Resume Next` over an even and an odd number of statements, line numbers read by `Erl` on an odd and an even number of lines (with `Resume Next`), all of them in one procedure that also frees a `String` and a `Variant`, and a class method with a handler and one with `Resume`. | | `props` | A form with every intrinsic control, each scalar property of its runtime table set to a value other than its default (generated from the table), PictureBoxes with a user scale and with `AutoRedraw` and `FontTransparent`, and a menu tree three levels deep. The form records are checked against the source, property by property. | +| `typerefs` | An ActiveX DLL (`typerefs.dll`) whose members, parameters, return values and an event parameter are typed by interfaces of other libraries (VBA's `Collection`, `New Collection`, stdole's `StdFont` and `IUnknown`, VB's `TextBox`) and by a public UDT, and whose private class holds `WithEvents` variables of a project class and of a `TextBox`. | +| `eventnames` | Classes that share member and event name spellings (`First` and `Second` a property `N`, `Third` a method named like `Second`'s event and an event named like the property) and one with events and no public procedure: the compiler pools each spelling once, so a Standard EXE's event name strings belong to no object. | | `coverage` | Generated source for the opcode table: per type (`Byte` to `Boolean`, `String`, `Variant`), arithmetic, comparisons and logic, loads and stores through every storage class (locals, `ByRef` parameters, the module's own and another module's variables, array elements, UDT fields, `With`), every conversion function from every type, string and `Variant` comparisons under `Option Compare Text`, `Variant`s passed by value, objects and `IUnknown`s in every storage class, file input of every type and the printing forms, `Print` on a form and on `Printer` with every item separator, a class's members of every type used in its own code and its typed functions called through `Me`. Compiles only. | +| `records` | Records the runtime reads through descriptors: `Get #` and `Put #` of a record with nested records, fixed and dynamic arrays (of records too), a `Variant` and a fixed-length string; a two-dimensional fixed array inside a record; record arrays assigned, returned and erased; `For Each` over a collection into a typed object variable; a class with a `Private Type`. | +| `udts` | An ActiveX DLL (`udts.dll`) whose classes declare `Public` and `Private` `Type`s (a nested record, an array of records, an object and a `Double` member), pass and return them, `Put #` one, and use a module's `Type`s; `Second` declares a `Type` with the same members as `Shapes`' `PrivT`. | ## Opcode coverage diff --git a/tests/fixtures/dispid/Host.frm b/tests/fixtures/dispid/Host.frm index a0783da..46191cf 100644 --- a/tests/fixtures/dispid/Host.frm +++ b/tests/fixtures/dispid/Host.frm @@ -76,3 +76,23 @@ Public Function Extender() As Long Dial1.Tag = Dial1.Name Extender = Dial1.Width + Len(Dial1.Tag) End Function + +' The extender's methods, and its properties of each kind: a Single, a +' Boolean, an Integer, a Long, a String, an enumeration and objects. +Public Function ExtenderMembers() As Long + Dial1.SetFocus + Dial1.ZOrder 0 + Dial1.Move 1, 2, 3, 4 + Dial1.Drag 1 + Dial1.ShowWhatsThis + Dial1.Top = Dial1.Height + Dial1.TabIndex = 2 + Dial1.ToolTipText = "t" + Dial1.HelpContextID = 3 + Dial1.WhatsThisHelpID = 4 + Dial1.CausesValidation = False + Dial1.DragMode = 1 + If Dial1.Parent Is Me Then ExtenderMembers = 1 + If Dial1.Container Is Me Then ExtenderMembers = 2 + If Dial1.Object Is Nothing Then ExtenderMembers = 3 +End Function diff --git a/tests/fixtures/dispid/dispid.exe b/tests/fixtures/dispid/dispid.exe index 1c46c24a2196da774392a4c78b49cce045a31619..c346920e5048ac94b3f976af7671b21f84010fdd 100755 GIT binary patch delta 4714 zcmai24^ULc8UNnn4iAIjltV!dxH~xl+#x3(N|3Z$3u*+#PDq9tnqma!#-3J_YeO5F zV-l01aWc@Q)wHydj58rAO=&QR8f#H$LpvnK#w2t!33h0tjZIC5G*OZ3?|b(Lcuc3u z{NA_w{oil*c6Vs$Qd+u{w*I&S9}Tvj`HossDod3|RIf`OS8|lvzDr8ht(h;C5>>0z zG5|UZd;-i}MRXr{wpB9Iyv=N{SmL~X`gHZrQ@X$W;NJE9vcv2(SIlcYx>Af+9x;7= z^zWm++vKp>?cCdX|KpFHU0bSs-XW+0Y}hu8&v8=L;H17G$sqvH)%%6|eziKouZ>TBfjd z3qd`LpkEG|9Xdbb|+= z?77eV(!aoK?O6g(5VAu4PdoO#a8K3x*Bm{W`yWpJhdi;s>eScZXHev3hG8e@7*Nt~ zUnDKb)}UTlUmJbxb|1R>Zb`X!^$!a@-miWC&Ryk0gRlHy^7QJ~hcBjF|6bZtWgN){ ztZ4})sYL&*-+On;yxw*DKK=K#$qk=BIk@!M{SUoitbFS5!b4;4{6ewG-em9XV^#qZ zB8v-kK;yn2t6)a;=Ad6^5u`*`q5P}yYOOXV>XsRn!n-YZ2{xeA7NhDzeH-AVClBjW zfEq6q^aW@eXhWf(qnUyR0bbo^*bJi|jmSO*;Cc8lqdF#^v1|ykCtnTLXY|3ifj&RclxiuV!NmZxpZze2E$&V?QW2AE*k-<9K>i@sXcSpr4Ap%kAg^qq-mU z?E-r2B1j%oW&ndB;5;x2i~|!u{W9=YqR`7x_+sSk#X{UJXb*z(4?s{BdExXh`qLNS zJObMG7}X;YzMupufu0FI6^i3LwB}30+IXOt8WH?5P0;Uvw}H#R4IpW;(3JghOWH~) z)HYjhYvvw>!} zpera_um}6li$Oi;ATzNC$FQ%vA-saUXLF#ofMAfDS_$V3`qLN6gJ1&nuaEFU7$;++ z-Uh4!s)4P*L%@^3A)pU<-71pk0y^9{d+TEPre&e@q`ws8MD=y##PvL$h@Qt2)AM+q zdLGYP&*M6P7o71!=fda}xlno@7f#RPLh5;3*f5W0UWaw}dZtQRg>hczqN96}pMKLP0RcJ)N`9zI#}dgeVz z6-|N0oehl-Y-rqZf8zrW%Id5P*_>6cY?AM1Z56Ixe;|Ld@lP8LJbLP#d&hf!oPN6E zyHt0$s@vZ5F>Pya*Wb@|13CMiemZf(-oI2|es9Gs@5vThS;`5U&|*fT4`F;!p0g#J z^>?Ejw`JQMx*4f3ZbPm3;lQm;=Gm7U{TMaNE%vRoEDr$B-#F(VsM%T3{^`Uk^+U(^ z=`VKuQ$c^R>o0z3aqLuISL7Z?`La!j&vEjB(9p8k?+$%%dCk%6&{s!a&C+Ul!%-yv zxa7m24?l$IEub4WMaKRJ?E&ot*zO1r~Q z3NnHALvb*o%s&Typzw9e$GE=n0{6l$PCK`Wx^i%sVr&NW0#!ICnn4YqBe{YO0-oCt zCl@DYKRQi-LAe&a4T>@+%2mM~%N7VW)iPZKOJhtYj^gUSU`u(` zX;vnrl9N2&pv;TOjgVhtE<9h0#UpS{NO*e7%;n$B^D#?!_KnLsbJ7#Kp*aT@&2mT1 z5|y0tM>&q5S8r}gg3@nd=1AUW(CefVYF&)uCLEZPFW2jslPE!XIpznY@zv>dOr9X! zLd#-~Fdv_LmQp(2M8{@0vdq#kBUr%o<8B2&~pEygWkW3!>z2;~aga!n&&QfZ_YT9FtRB6vi zNhUHX8B;tszruS%cIKL$7xg<0Q!^+Dv2;1g2x}MAVrvfoiP*(8KqoK?xUtOxzzHB1 zTYnSim@Lk#H1Zu|kf-uovNtbL#WUb!UeU(r#e+DQLb&aM8MlL&4kx5fssl0u?k$sv z_Lb4@rckIbk&W49{!;1BPt7?RsYKLV@1mStkj&3toby&hwc&q6Yt^F37xGt2x66{i z37fHjE%H)Px*W+%l)GFOZEhxc&SjSyms;dW*R4FiH(hIDf=n4Ia3t)aiEh*%vKQTU znO^V>xz3%+svi^-$&PUMMR$=LC|IbFyiiaqn+j6mkbrC}@JjR20P8-nbi&|bA5Rpn zpT`ePKB=HXt@8cCMMe`&?_qhZ@V1N*Ts{2WPAnHJCVo?!(P#dqzB@c0s(o&+S?RMb z$EU2gSq<8gCe*Y*$P@@oc9B_WAzB;=4IyW4c?d^BZgwaR zUqi?+Q(c9+A>v7yDT<4F(%c6lp$*Z{Q$e?kPR5hcGLf&v)A%Ekrjbdv0o*(T1wtc} z#iNtR6^TrlxsxD9qNNR?gcZgIbZ;`;R?WAEvZp>S3O@$a|kM<3q1S6AiG&-r$5Sl!?(z>9zRFi3+ z!`wHw+ZXh Yen@A}qSWw{Rg<3;DIBNovPet%FCZDzAOHXW delta 4105 zcmai1eQcA*8Gqh4cH)5BIK(CnNt`%wJ{%jzAptxH+5rYa$smJtWXyEwU@T)87Farm5m->epdyn}wM=MG##h&Dr4rhpL>pa=vfuN@Ht_Pt z&icJ~_q*qwue*Eib)-v)bSdovDH)%i={WVgdTzR$p=?xp%jI#URB4QUq7*#pm=Yjb zqEaM?x(Rp+*akcSt}7rNmNk~OFYZor|9QRG^G;RISHtgZlAV?s%Zc@!-hT6{IdZXo z=?>2mJLLt7x8(SVLkGTd@$~#X-G40kG~;($ru^e4*AJ}~OQN;|&(Hg~YH=zNl}mT# zE@e=j%*+bs%uwXeJ`bMI&$&%8=rk_dsfWwizRP- zUJK<~y5>hlGgP9PZCiIuI`6CB8~)A7vxlx+DmXLbE*frJ{L;+Jr|Mq|DR*U0R?XK2 z^90W$Zw2yz!tHvjf(6-2Lpx*@q{LUjH9J*p()vW3WxloYamzBn6UZJhsx`=QKePnh zz+^$~z$KvEhcyB^9fI})yt-|e(~EY(BI`H}%@;Nq)zfmH^&4T<)Y6FcIMv64Yi3ms z9B0iBaj>B0*s`8uPxKsnrsvpGaCR6TQHT=h8JI5UBcL(5#pY4N3A**ZthFI)E@aIJ zck3`eJ)X5-FjaM-4{g*?DDXs9^crS4WqA63ftF8C`XVVawCN}Nj$I@L=-p!{1x~Yz-6`kan5up)R3|rt=VCAN}p$T zQqQr|dX9t8a~#wd$6h4X;s$m{BCKw#ML-BL!<)A7Bu;K2dq%DM4C-^}^J1JszPNP9 z`@QkHCtmlVHq+m<#khvHf6scbLO= z3P&IyKyI!SGz_C7fB_2dj-st_z>0Rlb9Bj)i!mhH73Z&_e;Y`{Sd)?ZfkpNdR0 zj3a~g)9xRh6)#QFou8Z8f;SZx6J~O$m5=8a{!%W`Oe9#I1q_>0C6K+Vuk6yWNB2B1lMp3`~W_14DE4tK~44*$d<_{oKD^mMI-OgN;Y;9L1-3dRYdVx6uc z*-`Mc;+0C_Dp6YX?9w&aKMn3GetTEtmAmcPp%<22XnAe!z7H;5rlo)Srv6S2dGda^ z)-g2y*l*j$eZB6B8o9r)Hn1Bx;F9p;Ao_jG#owenc4zrnMSnX=yVI4aVlaf9@uvcp zkX-4kHU^N=I{B(|Rm0VPFZzB%M`-SzyT0HLoUd%xU*Y(NnM~2a1AK zUf5*&6~O3F)x%#HdgF3xaa*B2C;A*l&lYDX8H5+G8Q@~^iw>tPY>0Pxs2&K$`^-Zm z*>8;VJYEMR=2_9U1I)8L^K7CCXa=|#fItcMq}IFlz}lFo0Rt(durIv-`TK)!Q+|ur zl;Ox?xDE9{Ic_}QMk(~8ZUpwDJqTEV(~wD{chknsHX(&6Exj&r&lp;cT==5gO-;z) z)@UBQCnJ}X<}4hz*VpDc%8ABPm3H>Vl%=!`>t#&2*k`{=+D?J(Zh5ObE7Po;)z~^j zkp^on`DJOb+UJsvvLYo<&MwP(Bwd+&Pq}=u%o;Y$B%_j_Bsr2Ey+wGHt{h2PL?kVj z^fD$tqWv-%E#4i?{dyVb8Arz+&{GoA3z;lXq$GvlP;-}bma^|Bk}Szgx3vbon`mm% zcVwd3jM(ol4oe0ydh))Bx5{i;oIz)D9>;u;Gx%*2y{}37p~V1g=N@vd9hoRe&g4uC zJtR|!0Me5dSq?EikfjGxG)e2|n$h0KUvX1s@P!s7sTW&}8h zwgo%13Frc@1O9R`j^4yJ9tJ}4E%zcLU(^QquG=H+6=pS-BM-ZMZn%`Z9k^}a2Z{+N zgi8~2a~xgr#fofAn*SZ~S>n+YexPiJ6qnkh^eoSqH2*hBNNXZ^Y=}#1dNtj5E3##y zN934Hw5^t&*)TdFX$VFpFb`fKz68GiA(MFTFn7IJn8N*(0J6BV=E>2t}w-Xi(3Z#&tK^1 z>oe9Gqe6|S{6Ac#i4v`@6t-OS*N!Pvv@g`%L=o<@+;>IE1LNt4^v;;DHojLkpNH&Y S-x%8Xhr-5oqMwPJjQ;_T_y_QLd& z1rbB_$%2h;Pn+52Z{6;9G@Bb8EiJ79^H!JHA8avuTFjN}H<+7Sovxx8GqM&auK#fJ zvG*QXe5gys`7562dKmKP^2fV=4jx$k6qoIuMmO0cM`RNit2U&w$G-c9SFIalvkc!b zT*jCg8bUx%4u}a@Op-+|L#BQkfW$f2K*Z>8R zoO&-(IV=jeIs&NJcO?kLXI40v3TDQ(6geFM2NX?olZ%05OI!mMq^HQ==5K_K_W=rS zM_dDzOB)uN7J5<7fSv(819}GZ4CooqGoWWc&w!o*Jp+0M{$Dci{vyVrz|Vljs~F1z z76QwFYk?bqTY+~1e+2v~@DMNp{5^00_!r zoe%9YQTHd|6+=VZxy=9Jt zjiId#oM0BP$c*?EtOO_Aq@V|!uwB6paKbL&Pk<_@aZnur`K7z)WG(n~;ByZ?`!pYJ z!0uM!`=G$WUIz{#t^*XK65kw{U4+kRs3Xi#a0NKwRt4_{Ck!dr4gNFWAq9_s6R2^L z9Rc|#tpTGX4R<^p-vDK>X%jw|K}+CVU}5FJ*%(Pta23{FE|*pRZz*+4(Z9&TxZMHR z`G7_=^g_QA7y|wn_@IK1gFg=>Q2kMz7Nmuh80pf&Eu8u|E#tJ1)4Mp$;dCFT<(zhL z8sfBx(`ruLoaS+w!RdZZ8K+Nk8sOBz>1Ix!;PfD;hdEuq=|)aVIsHDTCQhH_^kGiT zoMv-s8>j0ywQ+hsC}Zw*xfnd>B>L`>pcrVO0w833vUTM% zw^lvV_4$kWu0NlVwUk*I1i!!4-&W%Yc-md6j%stRv~UqlIgsFn&R8t#^X5-W;|2<$ z5bD<~FBr*RtW{%-YaxqGmxmHFecQtNudpySf@pCC8WU<%XHho6AK{4{%sI$E0PY7q z2uubS@57Q7xf6*JOX?tvcC~N5xfn$N@^=7z%TxN+_dsIboQRWR%loEHY@sLklz9lkl!eB z@g#EbEbw`t3g2s5kU>2-`{E47V!}-`seTket9?C;zK&oXIn(isLO*|vdC0&Qv)kAW z>=w2Xd>3nFezpT87r38GZEOkFFb4}T4{L{hs=6XrY-K(*G@XA9GeC1rg>sCKQq3H> z7V?r((+?lm^4qn)xY~EJ1p>ku{zsb0sn63vRNfnaj0K&!-}HH>S_`i8rruy*7{wp^;`MxKwMtu zceMqZ0u^po;|{I9#>6;5Ti3yUV|=sTGm(wnB#|fScQh3v+pW3%8;; z2%0cf324mF+HL|yzhz-=aJ9~8DBxp;>Q$h-o&n>tfD;<16QiBc z95*fL&d12u<_zN}ImSApc^m3Cgv}HiGv$)$F1Ur zx4)Xfq+EJqxgRh^JUOv=h7#jh#p5|0ph?`{mCmxGxhUk1sDpz9MtscIc76OA-%r=(rFyW50mxK@T=6vvCUYxG+vp zd65zd9U{df$A5m0F)Z{$g$0m_LVstE!M8}(Oe2l8KWrTkg#pnzm^zD^n~UNX#QWD# zO}v}`<#-2T@eWYDWnqCda=ZgO-bh0atfZmUN~{|Q*Y%@km(+tv<5$NgCHepYr|~(0 zA&RSLQ*20`;^7%e#8NK3F^ePd!8swVG%RBFeJSz zuA;2ju+%|5BfV?$8irniHQ7Nk2#s7fKy_@$av@gNuhbo(Af$J@>-xKe0Xy2*y6eu= zb_+vxn|GR&OM6f`(9UCdwQ%uV+yw&AUYZTnj>p_jxb$oBI`z`s)>Cp=yRisUynV?r z^>quU5K}*58bC~gD4#)jsMhYylw<2WW_I(ZVJ_{QZQVi?V~w`Yq7dz_J7M=S(daeU zq}sXn_PqE0o;jj5>YeMRy;Lm>7bu1pSuix&B)eHLtBZQg+{~_+Jw|yFttV`?a&8kk zFV^+i?1?#7^C}F+V@_9NHo05yEyr+Sw}4)sLXY~RgphJ$0GU=%SUYcs;ZL3IHVh3tpU?=fNg4d+t5t+ex1gYUYq zt@OAsh&hA#$sgTHQ=?dyBVw$kiNffFhq+1fFvqGn`TCLvzeCOV<{|dH+>9_2pO-vX zvCe2myqGsrEbR2Ijy_~#0u9qTKu?3sH(DX(2Y!n3(Evw)o6P`)gEb)2z zC}8d)xKrR1l})OY$iJ@N4O=(Vuc^1LD30w7DykY=+o*314yU(an=jyK;S%i`ly`FF ztvdOQUHkUW+d4h%4b3a?60iF5yPB7hM81@Fctuin0*aJsFB11yvP|_c#e2T|-Y@T0 z)a&zjmag#d{;2-^4b8Hup`xnI6YpzlS2etFG$~~y4YHHoi?J0_+J(3zf&qv9!3Hgv!JQ}*ExHqv~ zo+fMjHYk}OPZTV2sN{sVPTsdcR`-7tLsL_8ut{#EjR|o%MNPG2IKQwz*(&?aQ zK+k}l0X+kH2J{T*8PGGJXF$(@o&h}rdIt0i=o!#6pl3kOfSv(819}F&stm;cJ5T4J z8QYAbvopah;PlfF4*Z0`H2<-ulcZdkQh#8AeL7njWNZ-nW#GNwCE$lA*ctelxiR=A zh{r`&!`V%lP0;5}qA#38Uk1JH2FB?3xWjZz1yEffpB+}^yC740RequpzxLtKiz@HC z5%!vT*$erk--hbh2G#&7Fgr?BY(1-iTm`xoWrE*je0)(p{RAf*R4z*tR-%R}C*SIE zmdX!59@d02sZ}_kx(%mQ2^KJFwGp)rXxmUPpIvQ*L@+($aK>$LYCuc@Sb3n|p28F5 z$AFX6rNHYsU&GH;m$SLh+t9KV=lg@K5ngI=jb_9_F!{+@{3nEEp{*KqJ-2N`1Zr$c zSrM*L%>5K|+r;Z6x6s6Pa%u?4vn3Q_ zC;{;y6-ZA{4>UN6N2l>{B#$#tI-RE+(L+UQFpkhsNFxrW?((r&upaT@PaMRd*66|xkskI9O;DyzQ! zJ91Z-_Ih!NZW&4E&Z+f@5^3Xg6D76P|A?!}OXROe(m2~Ig;+kGOPN68d&ihTx^!GE zjipP{*lJ0_`J+i8ahzk45GYI*;uKD3Zl?f>w|uX3bUc4-^?2q5X*?ge_k#2}u00jL zB3&3?C0#iBiZoWf8vKjnO>uFklzjF4=kpcMhF;P$pl3kOfSv(819}F&Oa}f5TZ@uM literal 0 HcmV?d00001 diff --git a/tests/fixtures/errors-native/errors-native.vbp b/tests/fixtures/errors-native/errors-native.vbp new file mode 100644 index 0000000..d1d3295 --- /dev/null +++ b/tests/fixtures/errors-native/errors-native.vbp @@ -0,0 +1,30 @@ +Type=Exe +Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#C:\windows\system32\stdole2.tlb#OLE Automation +Module=Program; Program.bas +Class=Guard; Guard.cls +Startup="Sub Main" +ExeName32="errors-native.exe" +Command32="" +Name="ErrorsNative" +HelpContextID="0" +CompatibleMode="0" +MajorVer=1 +MinorVer=0 +RevisionVer=0 +AutoIncrementVer=0 +ServerSupportFiles=0 +CompilationType=0 +OptimizationType=0 +FavorPentiumPro(tm)=0 +CodeViewDebugInfo=0 +NoAliasing=0 +BoundsCheck=0 +OverflowCheck=0 +FlPointCheck=0 +FDIVCheck=0 +UnroundedFP=0 +StartMode=0 +Unattended=0 +Retained=0 +ThreadPerObject=0 +MaxNumberOfThreads=1 diff --git a/tests/fixtures/eventnames/First.cls b/tests/fixtures/eventnames/First.cls new file mode 100644 index 0000000..8fbb262 --- /dev/null +++ b/tests/fixtures/eventnames/First.cls @@ -0,0 +1,22 @@ +VERSION 1.0 CLASS +BEGIN + MultiUse = -1 'True + Persistable = 0 'NotPersistable + DataBindingBehavior = 0 'vbNone + DataSourceBehavior = 0 'vbNone + MTSTransactionMode = 0 'NotAnMTSObject +END +Attribute VB_Name = "First" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = True +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = False +Option Explicit + +' A property N and an event Ping. + +Public Event Ping(ByVal k As Long) + +Public Property Get N() As Long + RaiseEvent Ping(1) +End Property diff --git a/tests/fixtures/eventnames/Program.bas b/tests/fixtures/eventnames/Program.bas new file mode 100644 index 0000000..23c308d --- /dev/null +++ b/tests/fixtures/eventnames/Program.bas @@ -0,0 +1,8 @@ +Attribute VB_Name = "Program" +Option Explicit + +Sub Main() + Dim f As New First, s As New Second, q As New Quiet, t As New Third + Debug.Print f.N + s.N + t.Pong +End Sub diff --git a/tests/fixtures/eventnames/Quiet.cls b/tests/fixtures/eventnames/Quiet.cls new file mode 100644 index 0000000..488611a --- /dev/null +++ b/tests/fixtures/eventnames/Quiet.cls @@ -0,0 +1,24 @@ +VERSION 1.0 CLASS +BEGIN + MultiUse = -1 'True + Persistable = 0 'NotPersistable + DataBindingBehavior = 0 'vbNone + DataSourceBehavior = 0 'vbNone + MTSTransactionMode = 0 'NotAnMTSObject +END +Attribute VB_Name = "Quiet" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = True +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = False +Option Explicit + +' Events and no public procedure. + +Public Event Hush() +Public Event Ping(ByVal k As Long) + +Private Sub Class_Initialize() + RaiseEvent Hush + RaiseEvent Ping(3) +End Sub diff --git a/tests/fixtures/eventnames/Second.cls b/tests/fixtures/eventnames/Second.cls new file mode 100644 index 0000000..7aa615b --- /dev/null +++ b/tests/fixtures/eventnames/Second.cls @@ -0,0 +1,24 @@ +VERSION 1.0 CLASS +BEGIN + MultiUse = -1 'True + Persistable = 0 'NotPersistable + DataBindingBehavior = 0 'vbNone + DataSourceBehavior = 0 'vbNone + MTSTransactionMode = 0 'NotAnMTSObject +END +Attribute VB_Name = "Second" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = True +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = False +Option Explicit + +' The same property name as First, other events. + +Public Event Pong(ByVal k As Long) +Public Event Peal() + +Public Property Get N() As Long + RaiseEvent Pong(2) + RaiseEvent Peal +End Property diff --git a/tests/fixtures/eventnames/Third.cls b/tests/fixtures/eventnames/Third.cls new file mode 100644 index 0000000..0f937fc --- /dev/null +++ b/tests/fixtures/eventnames/Third.cls @@ -0,0 +1,23 @@ +VERSION 1.0 CLASS +BEGIN + MultiUse = -1 'True + Persistable = 0 'NotPersistable + DataBindingBehavior = 0 'vbNone + DataSourceBehavior = 0 'vbNone + MTSTransactionMode = 0 'NotAnMTSObject +END +Attribute VB_Name = "Third" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = True +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = False +Option Explicit + +' A method named like another class's event, and an event named like +' another class's property. + +Public Event N() + +Public Sub Pong() + RaiseEvent N +End Sub diff --git a/tests/fixtures/eventnames/eventnames.exe b/tests/fixtures/eventnames/eventnames.exe new file mode 100755 index 0000000000000000000000000000000000000000..7b0056c2c05244b5da61c6693ba8c025bf341862 GIT binary patch literal 12288 zcmeHNeQ;D)6+fG7J`G`kXk$r}WhP(*jDbXn43tNbr3p>KLSiOGI$_B!*_CAz_X8rL zY?0~2DPz$3M~V&;wWYLzIx+PU>_<%22vmTDMK)`8)T$n|-@% z0#obP%%03S=bm%VJ@=gR?tSmww`tjaj53MH3{+L3QE&!D(76fQ7M7hUq@U)!v}DxO z{L+$kwvUDlo0ng%yFR@z=Z|j% zJOdK%_c{mu26>yO+s(S!BCy+tnoXH>vTgkH(q@w8n?7X9CbB?d)wIpro{<%;4Vm*b z0htR>K-mtEAt(ePOFq*rdT*5yRdB0{Xe3vtJ{W@OtaEs(47&P*=)W`{gyWNF7-J>c zS?P2H9nh6zfk2T86rxC_Kj801r^o@h2!B>^11R^l91J7U&2$CQ6-ZYgU4e83(iKQo zAYFlU1=1BrS0G)1|FsHiE+g6j>;pay{2p)==p+hrtm;yWMFA}k)GTPOpd@IKpv9m> z?rE@VH&yS3BxHwIo`$T=u7%m{fwQ!e#l=1 zejPXnOa>R{>Jzydnv_ri6TU*o+|3UlS^)d0C`K-N?v{m0f@_vJrg7xVP^8NxIxp6_ zRp$jb>^Gu)ps}s;;u#4c@w@q$vlsg#OLTlp3QomG2OTH!!qaDfNRPyt;=Ry zXK_t*o~v`tvkL7YL!MF8l=Ezd4<~T8JZmt{nCIspGqCS4<~8QouJ?^{!9<=si_FtJ zljj9Po&(6UR?l-%A1@{VVVUzO((`;xmv89&n$Dv-=RAwC_PvHY+c6jC$?Lynjyy{+ z&X{LCWCr%l`^T84=l~hzhRBm=k$IYC@@z1y+Zgt&MbC3w_bvUf%y||gPKHam{Ibrk z=zK!woF`!~Mh$rm>({duJ{p0uT~9N{8S`Ahdm6yL`MxoCo;-_|=Pd***X<-$a}=n3 zYg5RouleP)gErE3S_{6PddW|F&~ni}ppSw;2kk{WFpIVlmb<7=#Fr-j{}tfpgM>cp z1&iT6uoLn#du;XIKOb*;s^;W3zj1Z>yZMwQ{>1Z64g`rfZLaQKA6{a%hCD8o@ymoCV!}hV zfZ`wSSFE3!es9=qdA2r}R#boCXODHgZ3$iuZ5}$jcBk{!=!xY?{l|~2$XWBJo3_z4 z`MwuS=l^hV&)}(^{5O=7yB~X^anQbDNs@iN*Ad`XnVogTM#C5LuOIHH z+xFR3===0sveC{1GK8UX>i0O7t3H0!#I|;iuZQIVYGfwh?Tc?Ud~C%pT7EQ^`QUG! zTm0@LtJSQ|ccxoL&L98b!P8&9{>pHkbDpcPWuPV+8oc5ewD)R0>@J5_`rua`6W(0j zyqx{;u@^Sko_D&Mm-chM-9zaCeT-ZrFlWj`TWsF(ut__+T?daH`eDK_{z;CwLrVIEaJV) zYpH=jOVd0w&k55jJLRPVtT+W8+oK5H^fIa+mS) z{w4vg~- z1DpVql!;-2tm8IbCsV=Q&c-cm9d#WopRTHGXl~~5Ev}%u-r@Dyd_5kYi#4_mTWfn) zTT|=9T^pUwZLZykx@|76%MnPn+!}KEzu4ppy8OEx-Q3yLwQrZh*5B>w3pP4@POr<) zzVXECPMnRk?+4<)p=XN!1?a@qDTE^hoJ)h-OBl@XTf0f@Fep>%hwjj4(y9>7GtjRF z9|x}nAGyQMB;ILan2RAE7w-<-V>EV$zIYaW#Vq<-=weJ;D?t{$Z zr96b?cMWS^%HcJzH}s1DL82yJrybNrU7$AFhE@}8p;pLEpqtQUIA?zIwsxO_69JNI z5{H$j!Shjcb>P(ChaV4ladv3JDPlLy77XRYquSByfHr`B2TmCt^!h}9o_WMG^16;;B26`O*xZQ>lsBaHT1Ok9Pa@XKc^uOX`0VrryP%(F#Qp+{7L$H^Gw z$t-nFm6)j?r~!Y$uv#WC#Yz?;CgyLODC+M)#vLS5>3?`)EF_qkW`tsD-TY zFc{4bQ{f~x3U6vmP*%(dS{#T4AHZUG3Pq58BnLP@1s)>$wTN4!B68q5S%C^r)+ww# zbD&9vFc*glqcKo4|K&Qt;|B+WY7VL_VGvc%LG=aTN-*T?*A$u*@1K!%b16$-{~38; zz&KxA;!DQT&6~16QDSYp?@mdM`Y&-=yhJ{RgESpd!&HW6Q4Wy#p%|Ied&doGEK^lu z8&q{F(RqTDxX!Uj7<8ryaf*Sww^IPeyY7&Be!48SVLE3@oh}0&no|FPx#z+$b!vLO uI(2?bjjh`Ne)o7&Ob(S^ugJf@Uh!(ATj>g{s9b literal 0 HcmV?d00001 diff --git a/tests/fixtures/eventnames/eventnames.vbp b/tests/fixtures/eventnames/eventnames.vbp new file mode 100644 index 0000000..32f1ee0 --- /dev/null +++ b/tests/fixtures/eventnames/eventnames.vbp @@ -0,0 +1,33 @@ +Type=Exe +Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#C:\windows\system32\stdole2.tlb#OLE Automation +Module=Program; Program.bas +Class=First; First.cls +Class=Second; Second.cls +Class=Quiet; Quiet.cls +Class=Third; Third.cls +Startup="Sub Main" +ExeName32="eventnames.exe" +Command32="" +Name="EventNames" +HelpContextID="0" +CompatibleMode="0" +MajorVer=1 +MinorVer=0 +RevisionVer=0 +AutoIncrementVer=0 +ServerSupportFiles=0 +CompilationType=-1 +OptimizationType=0 +FavorPentiumPro(tm)=0 +CodeViewDebugInfo=0 +NoAliasing=0 +BoundsCheck=0 +OverflowCheck=0 +FlPointCheck=0 +FDIVCheck=0 +UnroundedFP=0 +StartMode=0 +Unattended=0 +Retained=0 +ThreadPerObject=0 +MaxNumberOfThreads=1 diff --git a/tests/fixtures/records/Item.cls b/tests/fixtures/records/Item.cls new file mode 100644 index 0000000..c3c32ce --- /dev/null +++ b/tests/fixtures/records/Item.cls @@ -0,0 +1,31 @@ +VERSION 1.0 CLASS +BEGIN + MultiUse = -1 'True + Persistable = 0 'NotPersistable + DataBindingBehavior = 0 'vbNone + DataSourceBehavior = 0 'vbNone + MTSTransactionMode = 0 'NotAnMTSObject +END +Attribute VB_Name = "Item" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = True +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = False +Option Explicit + +' A class with a public variable and a Private Type, whose record +' layout its private object descriptor lists. + +Public Name As String + +Private Type CT + S As String +End Type + +Private m_C As CT + +Public Function Get1() As String + Dim t As CT + t.S = "q" + Get1 = t.S & m_C.S +End Function diff --git a/tests/fixtures/records/Program.bas b/tests/fixtures/records/Program.bas new file mode 100644 index 0000000..d7ed283 --- /dev/null +++ b/tests/fixtures/records/Program.bas @@ -0,0 +1,70 @@ +Attribute VB_Name = "Program" +Option Explicit + +' Records the runtime reads through descriptors: Get and Put of a record +' with nested records, fixed and dynamic arrays, Variants and a fixed-length +' string; a fixed array inside a record; record arrays assigned, returned +' and erased; and For Each over a collection into a typed object variable. + +Private Type Inner + S As String + N As Integer +End Type + +Private Type Outer + A As Long + In1 As Inner + Ins(2) As Inner + Strs(3) As String + V As Variant + Dyn() As Long + DynS() As String + DynIn() As Inner + Grid(1 To 2, 3 To 5) As Integer + Fx As String * 5 + B As Byte +End Type + +Private Type Plain + X As Long + Y As Double +End Type + +Private m_Fixed(3) As Outer +Private m_Plain(2) As Plain + +Private Function MakeArr() As Outer() + Dim t() As Outer + ReDim t(1) + MakeArr = t +End Function + +Private Sub Touch(ByRef n As Integer) + n = n + 1 +End Sub + +Public Sub Main() + Dim o As Outer, p As Plain, f As Integer + Dim a() As Outer, b() As Outer, loc(2) As Outer + Dim c As Collection, it As Item + o.Grid(2, 4) = 7 + Touch o.Grid(1, 5) + f = FreeFile + Open "x.bin" For Random As #f Len = 400 + Put #f, , o + Get #f, 1, o + Put #f, , p + Get #f, , p + Close #f + ReDim a(2) + b = a + b = MakeArr() + Erase loc + Erase m_Fixed + Erase m_Plain + Erase a + Set c = New Collection + For Each it In c + Debug.Print it.Name + Next +End Sub diff --git a/tests/fixtures/records/records.exe b/tests/fixtures/records/records.exe new file mode 100755 index 0000000000000000000000000000000000000000..04321042120fc59ba59180d9c87ebc0dd37f88e9 GIT binary patch literal 12288 zcmeHNZ)_CD6@PoT=FY~LgSrU`7OaJ75pm3c4RtG17GpQKHO4iLOA!qmzQcFnd^YDZ zcD~fth$wCv0a99ok|>Fkh|nsktctb{NmU~vl}MygR3cd|A4ug7B&w*DVAZORo&Mg; zu6=hnCROPlRr^kF-k*6h^XARInc1~>?K?$YBGQ3E5RHN|n6g~BVB3<_7fR@S;d|wy zTJwA5?a^4D(VOh)N`|_P&QK!JlQIrPjAVbph$W1M-FuAgo^YgU$&#W9yX#*s_xH$`B~XiJY=4) z24pJ1M9Fplk1Qcb*`c%5vbJ;;(K_aWL}&a`b#Dp`SGhn>RjJ5e3iZ<^DA+#-v)UL$ z2dl!NR0z5kd2ykb4@{!5s$^fX6P0ofn2YSs0L=pP^F0n(EnL=(fExif0&WD{2)Ge& zBj85Bjer{gHv(=1{?{UK4DYcUG)iv<4ugLY_zU3gffK-C52eFIX~%D|)(Ds+)~=9c zsVs|S8IYweOTR2hmSwW6Kp8ccnRH`P@OK6Ve%-8<9MRu`Cq5W?{IN$~7e6un_$a^B zGVmL^g0@I!Pcqym1=bp0G$oJDB(RWTongxF837DcXutR#Kb_J3g7N7-lg`|4(mK=y zA-@g&3a||FMV!25)iJ8M|{%qjP@FVc&F%sh-BjhxYKY;uYm=8%i9uB3f0BPveiO-MW zHV25?zlQiUAU-^1EBt^Jt4ZHm;AxgJQ3f$!Xt(A4pbUGnWd5X3j1L9Jp^L6Dxtan( z_q_dpNs}0_7?fja;!8>$>dGKHKCta6v^%90cq(oE2;|=aKL9fDm{$?2I>ezKxD)8W z4;m`;8VGN;Kq)?)72Bma%4UbRR*zj^Kcf4fyhL;h6L#W3t4s<2)prq-w%C2Ir~R~@ z_R-fs2dIaVbOc0Oz3tQRGZnRvztz-1TbZnk#Eo z?Rw$t%U|E{b1PVcXPtj?4{{oU-ayWA{yTes2RiPX^Pv)Z4}%~5F^t2ObBlp9R!eiH zlFS9Re-`1Ssc2F+F9}po|$QSszDcY@#YkQ5-qIJ>4gJ=CK%@c~qbs zQ3Yuz88kYr+1sI%9iF|aA&e&v|MH<#j?;=Zs>Va7 zu^My0{xGtRW5#qp6&YL>&|$RGCkn<6$^8A5XyEl)KRr}^^p|n$X1vCL zfKCMluSpCgpDWB~gGS4VBkO=OPs46kC|e7`c4|ar>BQ}B{ujY~5b-}mF-m~H2J8YB z0yV55&yeRC2NK5MexHmj8GiTI3zbgK4cFWT@pKE&hsdFty z{``EJ=cVB^?Wo_rrK+L1neC&`o$dZr+q2uBFFShjigzNmK5xCt^W+`zP+uR~m4CDS z9c3=Pm7Tw_X}i|OyYuc;N_RZ=ndp{O7(ozz}s5+v`J^oct_V*oIW_t#Xo(@Ct@JgZ`DG^Tzzn`9|rS%&J zh1h_UR{i?1Qu>7=G)6ZG{acfB#g1q6JtoRXEd_tvs*6MVXiZws4%`8lU(^QrQ z-mtuM-dZ^R6IOw-(E8->_ne~)z=ub-`AY+WykXQ zrgN4?qn3xR06qdRb>JmH9p3vKh%z5=q+I&XygTmN(^22CYfI4Cdv`@r(H)_9yfM)g zOUNCgv7@o2{lK24mTw){9uBuf4r8z^U0Wm`3H9Y$e!D-Ce4;6liX;z*I=S+|fn$e4 zjf0($-qg-eA{>t-*=O8_I&(*3tqUpwbALnk%CluJQJVj}Y=ZLAuLA|i`dhoYH$XYB z{>&YEFKz56x(@v&(959Jpd)wKAtSLbOk*sDIloluIvJd=UtwoG#fdu( zI)d>S)>W(T5sthUGPif+G!D)iNFPUzeg*bf^{N-{6Nc15d*}elMrs4sM7ya4audpL zfM@uP{#Q$#YUo6QLzTpCrxdLC=GZk)Po>f(h`U0Nz6z|Ndikv;h>bv_Rcykgj8A~^BI`z2{Yv=0jl$=)oCF%q(mtGViLqj4>t8wP| zfJ}#`NfVzRw~1-55YyX)n91!tN6LN9kt7T@QNmn_hMe2;0Q*}vBrauEPjAckW<+K+ zaA-!{#@GwtMKP1vDrPQS6w`IvK=1DFJcC1J$7}50AFsJ+xSSgSHv(=1+z7Z4a3gT9 GBk&K?R2z{1 literal 0 HcmV?d00001 diff --git a/tests/fixtures/records/records.vbp b/tests/fixtures/records/records.vbp new file mode 100644 index 0000000..912e22b --- /dev/null +++ b/tests/fixtures/records/records.vbp @@ -0,0 +1,30 @@ +Type=Exe +Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#C:\windows\system32\stdole2.tlb#OLE Automation +Module=Program; Program.bas +Class=Item; Item.cls +Startup="Sub Main" +ExeName32="records.exe" +Command32="" +Name="Records" +HelpContextID="0" +CompatibleMode="0" +MajorVer=1 +MinorVer=0 +RevisionVer=0 +AutoIncrementVer=0 +ServerSupportFiles=0 +CompilationType=-1 +OptimizationType=0 +FavorPentiumPro(tm)=0 +CodeViewDebugInfo=0 +NoAliasing=0 +BoundsCheck=0 +OverflowCheck=0 +FlPointCheck=0 +FDIVCheck=0 +UnroundedFP=0 +StartMode=0 +Unattended=0 +Retained=0 +ThreadPerObject=0 +MaxNumberOfThreads=1 diff --git a/tests/fixtures/typerefs/Shapes.cls b/tests/fixtures/typerefs/Shapes.cls new file mode 100644 index 0000000..118d0a0 --- /dev/null +++ b/tests/fixtures/typerefs/Shapes.cls @@ -0,0 +1,47 @@ +VERSION 1.0 CLASS +BEGIN + MultiUse = -1 'True + Persistable = 0 'NotPersistable + DataBindingBehavior = 0 'vbNone + DataSourceBehavior = 0 'vbNone + MTSTransactionMode = 0 'NotAnMTSObject +END +Attribute VB_Name = "Shapes" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = True +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = True +Option Explicit + +' Members, parameters and return values typed by an interface of another +' library (VBA's Collection, stdole's StdFont, IUnknown) and by a public +' UDT, and an event with an interface-typed parameter. + +Public Type Point + X As Long + Y As Long +End Type + +Public Event Moved(ByVal Where As Collection) + +Public Items As Collection +Public Auto As New Collection +Public Font As StdFont +Public Unk As IUnknown + +Public Sub Pass(ByVal c As Collection, ByRef f As StdFont, p As Point) + p.X = c.Count +End Sub + +Public Function Make(ByVal X As Long) As Point + Make.X = X +End Function + +Public Function Fonts(ByVal n As Long) As StdFont + Set Fonts = Font +End Function + +Public Function NewItems() As Collection + Set NewItems = New Collection + RaiseEvent Moved(NewItems) +End Function diff --git a/tests/fixtures/typerefs/Watcher.cls b/tests/fixtures/typerefs/Watcher.cls new file mode 100644 index 0000000..569bdd9 --- /dev/null +++ b/tests/fixtures/typerefs/Watcher.cls @@ -0,0 +1,36 @@ +VERSION 1.0 CLASS +BEGIN + MultiUse = -1 'True + Persistable = 0 'NotPersistable + DataBindingBehavior = 0 'vbNone + DataSourceBehavior = 0 'vbNone + MTSTransactionMode = 0 'NotAnMTSObject +END +Attribute VB_Name = "Watcher" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = False +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = False +Option Explicit + +' WithEvents variables of a project class and of a dispinterface event +' source (VB's TextBox), and members typed by a VB control. + +Public WithEvents Src As Shapes +Private WithEvents Txt As VB.TextBox + +Public Sub Watch(ByVal t As VB.TextBox, ByVal f As StdFont) + Set Txt = t +End Sub + +Public Function Box() As VB.TextBox + Set Box = Txt +End Function + +Private Sub Src_Moved(ByVal Where As Collection) + Debug.Print Where.Count +End Sub + +Private Sub Txt_Change() + Debug.Print Txt.Text +End Sub diff --git a/tests/fixtures/typerefs/typerefs.dll b/tests/fixtures/typerefs/typerefs.dll new file mode 100644 index 0000000000000000000000000000000000000000..f7ae07ed2a88e7e13d717c1bf08f3ed9de9e784f GIT binary patch literal 20480 zcmeHN4^UjkdH)Vh)*)g0u(692Y#+&~gJp!D;N9IjIF3U)$>4x=MhK9Glta2hhkd%k zaFWic>YNkeQky!sR$HZ`aWsiqGbwd6vEpJ&Nf_I0aYw0DS8X-5rj|2mDrGb=ZIuku zkX(P?zW0vD6UcUFJk#mi!f$uKKl|-?6tZkw~T-d!mk^0}01K!V%oQ!?8aZiB{iw z>nf*a`gh;G@4Yu)d3;pI`pLtiZ-9Pa@_a3qy`&5o3z(j_7fn)_=3I29p`nDr0}L<~aEda9DM>xljo@)2~PlJx>a6Ss55 z6hcIksv(o$BS=&izKKe8Dw<9~{)cxX;`TYT6p0=jFi{=pOZ9KCXG+jM3 zJhT^V(FZVuO_M&@f$E`XJPE~V?o&w9gTNR-ZZgvOeBJ|j59B?N_dwnQc@N}0koQ2| z19=bRJ&^an{~Zsk#rLcmXa;(KjTXubA$=M+4tPMniu7H;i+mkW5BPvaAWD?!2lfFm zU;qe!-vn?kv+FADRFYli6mhAD%SBu!;$jiws}njl5&J~kA>wWkTSaUau|vdFB9=rf zi&znHK*X&gZWeJ+#395)yJMv?mBh*rwO{@BEFX7JGr*s_0f587@4YiAm+#&F{HAyB z>>hr!UFGsgff_oV7>u?@`-gSfn2u|N{@K6FHbxh}`)?2Zansm)FFPOk54Lg?m;iDX z?OGu%*%9j-jKcc5)n7Y&@%HLX&)4tV)Bm4Oe)Y$X|6XRk_*mfOpUkX$ek{;=7xZV! zsibq0t77)P^^1%=xrkatMY%%`qK-S@iCOd*-7kWk-j1utPnA#!ga6W?p8}o%o(0Sy z1wRb2A95MWx#l{Io?pWrCmM88Ne0l)AxgqxB#72i4>i$lszZ8+k~Bn*A(x^jXpo|y zqSQ~rOK_^`Ank!pKK%dk0M9#~iwu44OgOMAsE;XCuEl(ksD!7o{VpmgAcxR>>zP#r zHqcz|0_z|+G`dntomyIjlpnWZEH@pv;jT9W55@I zao_|n1)K%gCbxCGluG*Xyu^Va?YUvuQ$eTIhxD}xFtZ=z$d?zBrv%ykl+_u4G8Sh< z?5fafuP-Z^URLtgIpaYXGE^+@N501}mwd=qp*+v038b0NQAr978$pAe$n%_HInO5 zylnA&szyI}ym=1tyt9y%e9#F3J^x%(;DasZZ>2I~_Xji@SrG=ISGLbq5YqTpKo?+r zE(e|kz5%=f{0-m+LcpT{7v_Fm#GG7e9ci~!F7lbX%5OXyB( zGPd<;1bz$@7>;qHW89i@gxwB2+t&q#{({SJhO6BpA?Bo&Va-70vc2a`dht!me>%iZu{=BYw?!%-?(}k zM$Za}wUw2$Fp2redj@^qz@6l^NZ7(0()Bom7J(x2A!kLvmv zRsT5F-;2Oy;2Llo<)(l$hVr=$2I|xINnL%Zy|L&JJ<#yW`Tss1v%jR+=z-eDzh~Zi ztvAU(YJXK>(%&5K8|F=4#lsyx-7xd_CEvPC$KPJ{(=TtX>p3)#h$IgU_Y6NVoQm$R zt?6mo(YdL!%`H_2TU)U%Q0J!ho!d}W!t;xSa6?7B_X>cH0Ft(Luxxy{t)Q4}oJDN4 zerQ#9lhcAjTTx%C^7>v_0v!SrQ+o<8{o)6y ziM94;Uq^eS;y>py9HvD~TSKbbZ`hZ=t$elO)DPWFKm3lg{*|I1{LTbyPXYXR^StFX zxUKIoe4Y?(O(s%2cH5$dS};PKrMBe3Xas3FbtC2(j54?h4FEx)8EAj%;jdl!&g!kt ze^^w1$LjFsf8KQcklb7q7#lAC$2FmIf3*WQXFuNd7qwsSf2ZUxi_!k@KfKU^+qr7< z+{ulZyLx-;CX5&M;o7{`=RyQ@3J93|Bjz{S_JOIMP02K&{@#UtgrKGNL(%%7r_8m` zSA^6;qZGt`oT1#Oj?ocqtOxH^Y2J77`!WC>6r~ta8SrVeq|pk}lPGfz7zbv#Jj$Jf zUAtym0zHcS1t1J`Yc|AMLWO|>i~z68cLKb|6l-#>7w;CxW${kC2XrZ9vslwurUtb< zFN-XngUo6uZxQ7yF{TT0he2K_2ZUEo@JM~SLd>*FHGzsL%6p=)w5Y1$?2ip;VQG9O%~sKR?CJ(dN#8j?De1@ zHp|YO9`~c%tY>Qw{@@44A8Zea&@ZO8Mf&m*`rL0cL+JDVhQCvhkp52K zyuL1kwDQGB+1F-&FU)GgI|${i#JfA9uvR{RbROlc8qIZZ5KlK0$%y`+Jl23BMWq zNGbR+yWv;YiC`(7&i*PH!-0{Fvc)_LETkLfF|lDxoy+(AhA}})t?682d0dX;S$!~- zU7ODGy2S0ZqP>or%oX;$KC4sMq4RiYV~x+Z`MH#6^szxvO#lsEJD9Uwj2YsjDq%sN zLq>aSi~Dc%#~^49{C%UZM5U&=uk+(r-}xIOhVHMoOa`R z%>b>C?Myr-=yA{?(EMAGeqfOGfCSKm4H&;@(|G>)+oJ<|{BC5Lzkj+xv*EJ{R}nbX z!;1)$`XdOPVw^K-9Ytnw__WU|jr%sPc}wS(Z5_QkTDE<@_n~O2B{;mje_P*vKApq< zA)1P9?u*B_B=!v?#4f|2-I6$%6kCto$^ClSALKZ|!AAOkj_4ZJS^N#H4fOX9tfJN^@U(O^8jxi7IZ5l{9-wj~b{ zXmnj{owx6KG`cs%oc8Fx0c@0qc0`8`qGjOhObjjNvH5(+dm!(Dya)0g$a^5~fxHLu z9{AKepzj9@v8R(Plu46%haX1T4H~yJ8UFT=ka8n*ecl7|{&9R6{=_o;>1FsAmf-VQ z-Yi#&V60a#xN-zD-@Xjr0e+?lpBDv0Cwaq*Fuob*6i9XYgo&O4J-d2AzLGj<7k&W0 z1vh~$___OP>P6auGpb(Nfs>>rA=^Q>3A&v&FDVsOi^yIeBke@lUc_6d9k~|TPTPdt zZL}464&S$a_-l2yu60g$UWi1*-9fh&g`l81sSo$*TWNsexK(e#7h6B>+c`LiOLain z2ktQBeYk-ifRw-L_#E}5br98A(54i$2EgC9gr!!YBjKb@A}=7rFPJMx??BlQN+u~n z2WT&>Y(pLU(GCvg+kWO%(JGYDTiYpq3ovYG(?+U>ZMM$yk#!f_HYV+7ifvOWBO zLbOFFBC-SK-+D05d|%PFV`~RnErU$Ir@_<)M-4o(`u9I``1c}RoI%j}4&*ZcmtlPl z5nu+W0L9J+IT}LG$d@=@HNl1AQ42GGm}4g^(NU*;YUZ9lgA5CC(5G z@{CROhb|un&1Vie%_+!b=_}CZGrW(hFINyh_->}!6eD}klzU>Vn83|xn^qCnIU zf}k1D>tGw^Fb)RpLB;*@Qg!fMGuOd(^|G9=s3@dk=tm4p9uvKe8&@Iq_fA}v+p|<1 zEMvEu>##wYd0|b)K4+T41>_|I&AbWF(p~GX%Q%C)jQ)eBmRQ%?U$ucoMv z>k8%ii=pH|B89n!YDGU*P&g*e&kZ~)eb}L&pL3m7+G*F%(U~r!$on`$&)Kk2*_ZfE zn`ImFwh-6KDN|it_fYmrypF6z%KqZi zhLridjxb%Or4EhH>j?8DEmbr=*Twv1&>XJbM`P@tOW->}uc0*==HG+8Ye`W32I2LG z$C!VU<-}^E>#W1Bs|l+Oiss%ioadW2UFUvbhrfQ*{RaD0wNseW*(mJrj>~Mvn$vL! zJ1c}vCGcac^7D2<%S%{c^|Fs(|56kz5^FK;tuu4b;j;Sk z$?f6&x30ev-Y*KS%IWhOpx4E1Q`^5fr@sq*EDEj`Iy`o|4q6pp@ZG(7f6sUPg#o^+ zXFwJPKg@U9e4ox^&PZ!upEHhXm_T|3;I)wF6$dBK4DjxJ95@BA59R>}bb5d>;0(aJ zj54AO5`)3Bd5ZJ;(D2KLD26G~?Jp?urm_vrwN)Bx6 zQwwmuj2G@6DJCVPl=MX@BRwsRNq-<6m0pxiNZ*!DO0P<1q&K7)>F=cT(!WTTq@PGX zlm1IOvvGD~agDvEw5GczTob8@*9_K-%17j5@`OAopOW8_&&ucJi}Gdpnmi}Z%QjcB z%kC<5mAT4Y4wuta>5^SRSF5YT)$QtWg%oTSHx>ByR>#!^18gY%f#$4mBqpoAF zw(z zN=Yk+m5ef?j4HoS<`uj5Ztr@p^gic(!TU|`OWs$#r@epceaCyj zd(nHvYppA-tE`ji0(H%G-F4x*!Mb$aXx&)dc-@h@qjksXj@QNO57%evN9t$lFVtVG zzf@o9EAw^xV!pU<&^O~d>$~7fH;gunHHWxwM0 z`2+r-zuDjF5BWR%-TtsY;*a^`{z3nkf80OmKj}Z~Kj**ZpY<0vIvVB1=Ek1J!N!rs zqm7e|(~ajFuQbj#VkSD&=glrTr79^Pbx1u@M2chPjAG`DOOw(xX3JUWoHQr-HjZpO zzj0N~?U*NynsqhnYuq*UHG!JenpDlX8fR^$cDnX#?d94lwbyFrYUgW7E|tsVa@i?Y z%2jf!9FjZa9yu&W_(L-v@*c>0An$>^2l5`sdm!(Dya)0g$a^5~fxHL)*B)u`^CQE+gKj&!sk-sxFH0B!_9o{#ZIN;lx$Yh4IzW$_d^kBv}l<_t1?C>2J9!Su3~7mvIHdgGacvfeecH^n}yC-D{`+8i!1{?O%TjlLPN z%D>XFOb8!1F^!w{pe6<+?UilNjMo8VEnx&Df_ITz5Uytez^YY zpX{&E*!T=Xixct_G?|kF%zdiPP=9Pc-w%@o+ z{j;*iR9N+=5+g}?o@+fm_%G4Wj@*Hg_tt#&r?377hPYP?FYB3ywR%#o!>AoRjrh2b zCk)7#1X`H{uPmVPXRnnwox(@9D1EoYdCe=l#}(l{0qolPS^w<14YJQDUN#!CULi{6 z4?%>uSO%T50UieBOVH2Q2)BSa=#_}M90&ap@D<<}fmS%kd6I>YQ|6#1J&y8EV5I;! z<}i4bh=aMk{W0M!(&M;TywW)Xn)auWAIi?@^j*}X@0e(gFU1n9d`NiDAa3@n{cb07 za1wY5cx0{c-mDPbSh?`dKxYiORUy|0+VF?tsviwr^>bd9v*Rb75UO;#`9WKnnGCdb_o|Vqqu+MP; z_(WS}=qFvl`2v)iXpUztn!&rSX#LDZHDXqQQ6O{DjJl2VI(UXTIZf2_6NjCp^3w^u z=KOpf^zQ@T0RAPA{?M;1e7=94q-9H<=!rFV^lSldKA&?;gr3h?#DcVW^>Z(8K%but zqE|GEC&gyehs3ZL75mXTC8|N8EKo=kb(47Y137aB#NIo83kQvSWV&=0@6`pvd4zqNVWSC;*v_!#n71!RUf_q^Yt ziTer1IXNyWM6sW|As?P0;2-CVw5FNw9vnWFZNqyLX=J-Su^*3KLRg=HwykQs@X7R7 zAD>M5&rY9xV-szkGTG)mZb#yP9Q)|szuVn)DfRf5F1+yc`UltiIc29!vfTGOQ=?-1 z-5>wQx8B^;@~7|qF!-gS?LVj79g`gIm!%>`cO)=J2_Wl!y~gVRN%L6%&x?BGXh;ak zHCV1w%##p4pmEia*0^jrWxJA1+yaeEQ9_ep&J6HicG%+)oGT}0uFz|J_Om|EweQDk z3%)=Blq23VYlQbI(1$xVc@=tHrq|_q?a^!Aulf9P`v)b?K8!mI6yQ|@uVaom&u-|Y z?l{op_=L;@bz@!i-LdQmYw@V%cKGLI9FVD^P7|zkt6~zm+!kFPtDHv_d}g%jb6e%` z&_jJyu;F&BpSSIV?eaxr`CJM&A^Vwm*&@jL7Lnz%>_*5EjD1j}a5bP2)yFUo#5RkL z7-U?S@~@R*p=7i9{G-~HNL`#~JmKl;J|$~jwFa2m$2k7EaXFqVj(aRwA9Or*8vC-L zU#>)p|936(8G|tWPnU}4;C&#Q|DXAve*_T-(PTuR#ZUh^YXmKRUJ3ej;g@Dx1WbEQ&3>)tq*fnV7_;AczZIN zYD%QjEt!2o8F{B_>1x^0xu;|Mj>q>j4h-x{4noSr+m%cw6Ju7%-#(ZeePMehn;acX z>}BVkJo1Oj7CiiB^*_GTkgol~Y zj^ya`@C=;YnbCWBRBeNu0XqYB2J8&j8L%^8XTZ*YodG)ob_VPW*ctc;GH^)$hQ0{D zaRUyK!+CD52X!Cn7;2oz=4_~Cj5=-=kIH$ z6?!l-)3Qqzbf0JOW@wf~7tZNNp=U^>@g>K0oaPPUeJ7z(a8xIH6X1@aKY_QOL+Is+ zAKzn~bz$CNsRR5`^kl@oMKq~WCV+Px0Z|83kq4{|{JVt}7#qjPVZ2v8DE30j4%j%r zFG>K$+g7$US;NaNNQ}axf-kkA20E#g>u#Z5>j<;FrkjyxrQQsDNSiz{`wr< zwEJYllr{X~NX>g=ObsGS zPkoL4Ea*OyKJr1%kGIlSA#e8A+Ha**Kfbl04fqfTB6JA|V*y|<6w^3i-`RKX{973ZP?H__m*7cyp}70i`&r7E6t;5TlG7ojVW}T1sXP} z%L>?wWgKDDi`&q4hlOzziDvy!oi+^{R{f+WfVABPT-=5h2Xkovb^84-X(I(@53pck zVI))fz&_NA*}cFvf)D?eMcohi({FH;$HDV z;+WOP9YMX64P#B~qSBt722z8LMrYOdAA_#cGFBv|E6F2bV>uWi;jQ~#MoY)>u1Nc*WW#L~(byl~r&XB*S-i-h`*_$FrldT@ySo=)@^^rbydfTr&rD0O;X>up3Z zMl3GRL&rK3`^F5~Ar5%DAaBIw@C&?#7UYlk>>tLnlX{C0=-%k$BIR9ZjEsJjPaZ|_P5IZq8Ppt$)PE3trJJ-|0LR6 ziw}#3bEu%RO_Vfi`xj}-;L!}v{Em4UKfcc_X_j9?1hjPEr|wHt9E?*Nxn3 zo`Io51uUEUTsog~4v0ZjY#I%pu9Lb(fUM~|DgdL^fc{aZJ|2|zEXiK3n+jw$MnXch zd{$@E2dsc6rJ^*4CZJNzDO*)6bYHNfVwjKE3>X1A2aKD1W4z3}5qF6w$$u(zGJ&zc;o76M$7)a1o~k`p zd%pH+?X}wLwVq&gFc4IO>0maP3my(04W0;|3Z4m02G0jC2B(8pgEPS!!JEO`!P($k z@J`UBlqnTTrBbB?6jcc;Q6;8yDNiZSD#OaSl2eW<6UuSrv~pHCr%WlAlxgLkC3RZ8s$N%T)H|D8A$O=O6bQvaouS@PI+P6^4jm1h2%QQ|hR%mBhOUHegl>jz zhh{@_p*x|;@Wt?@@Re}5E?U=Gm#UkrJ6CtHZl>-=-E5tEbLr;t&38A8h%4fbcp~Ky zU!*D$h^UchBo=9ov_{$@@knQ+C(;`ki5!cZj+~E7M{Y#!L_E>TXfzs+_C`mdQC3t)ZeamHMko}8$Q*ru_4e9ZD?%R*08IgyP>xs)o`R? zvLU)9z2(T3Q(LCCe59+yHfCqQ&VZc(I|FtG> Vec<(u16, String, MemberKind)> { + let project = project(project_name); + let map = project.address_map(); + object(project, name) + .members() + .unwrap() + .map(|(i, m)| { + let name = String::from_utf8_lossy(m.name(map).unwrap()).into_owned(); + (i, name, m.kind().unwrap()) + }) + .collect() + }; + let holder = members("members", "Holder"); + let names: Vec<&str> = holder.iter().map(|(_, n, _)| n.as_str()).collect(); + assert_eq!( + names, + [ + "B", "I", "L", "S", "D", "C", "Dt", "Flag", "Str", "V", "O", "Peer", "Auto" + ] + ); + assert!(holder.iter().all(|(_, _, k)| *k == MemberKind::Variable)); + assert_eq!( + members("members", "Mixed"), + [ + (0, "Value".into(), MemberKind::Variable), + (1, "Note".into(), MemberKind::Variable), + (2, "IFirst".into(), MemberKind::Implements), + (3, "Shown".into(), MemberKind::WithEvents), + ] + ); + assert_eq!( + members("calls", "Square"), + [(1, "Shape".into(), MemberKind::Implements)] + ); + assert_eq!( + members("data", "Item"), + [(0, "Name".into(), MemberKind::Variable)] + ); + // Control: private variables only. + assert!(members("calls", "Counter").is_empty()); + + // The fields agree with the method links and the type lists. + let project = project("members"); + let holder = object(project, "Holder"); + let (_, peer) = holder.members().unwrap().nth(11).unwrap(); + assert_eq!(peer.memid().unwrap(), Some(0x4003000B)); + assert_eq!(peer.vtable_offset().unwrap(), Some(0x7C)); + assert_eq!(peer.instance_offset().unwrap(), Some(0x74)); + assert_eq!(peer.var_type().unwrap().base_type(), ArgType::OBJECT); + assert_eq!( + peer.var_type().unwrap().descriptor_va(), + Some(holder.descriptor().object_info_va().unwrap()) + ); + let (_, first) = object(project, "Two").members().unwrap().nth(1).unwrap(); + assert_eq!(first.vtable_offset().unwrap(), None); + assert_eq!( + first.var_type().unwrap().descriptor_va(), + Some( + object(project, "IFirst") + .descriptor() + .object_info_va() + .unwrap() + ) + ); +} + +/// A type-list entry of a class, interface or record names its descriptor: +/// the class's ObjectInfo, an InterfaceRef (library and IID), a RecordRef +/// (library and record GUID). +#[test] +fn type_descriptors_resolve() { + let mut seen = BTreeMap::new(); + for name in projects() { + let fixture = project(&name); + let map = fixture.address_map(); + let infos: HashSet = fixture + .objects() + .unwrap() + .map(|o| o.unwrap().descriptor().object_info_va().unwrap()) + .collect(); + let mut types = Vec::new(); + for obj in fixture.objects().unwrap() { + let obj = obj.unwrap(); + types.extend(obj.members().unwrap().map(|(_, m)| m.var_type().unwrap())); + let descs = obj + .func_type_descs() + .unwrap() + .chain(obj.event_type_descs().unwrap()); + for (_, ftd) in descs { + types.extend(ftd.arg_types()); + types.extend(ftd.return_type()); + } + } + for t in types.into_iter().filter(|t| t.descriptor_va().is_some()) { + let resolved = match t.base_type() { + ArgType::OBJECT => infos.contains(&t.descriptor_va().unwrap()), + ArgType::IUNKNOWN | ArgType::INTERFACE => t.interface(map).is_some_and(|i| { + i.iid(map).is_some() && i.typelib(map).and_then(|l| l.name(map)).is_some() + }), + ArgType::RECORD => t + .record(map) + .is_some_and(|r| r.libid(map).is_some() && r.guid(map).is_some()), + _ => false, + }; + assert!(resolved, "{name}: {t:?}"); + *seen.entry(t.base_type()).or_insert(0) += 1; + } + } + assert_eq!( + seen.keys().copied().collect::>(), + [0x13, 0x14, 0x1C, 0x1D] + ); + + let typerefs = project("typerefs"); + let map = typerefs.address_map(); + let shapes = object(typerefs, "Shapes"); + let iface = |t: ArgType| { + let i = t.interface(map).unwrap(); + let lib = i.typelib(map).unwrap(); + (lib.name(map).unwrap(), i.iid(map).unwrap().to_string()) + }; + let members: Vec<_> = shapes + .members() + .unwrap() + .map(|(_, m)| iface(m.var_type().unwrap())) + .collect(); + let collection = ("VBA", "{A4C46780-499F-101B-BB78-00AA00383CBB}".to_string()); + assert_eq!( + members, + [ + collection.clone(), + collection.clone(), + ( + "stdole", + "{BEF6E003-A874-101A-8BBA-00AA00300CAB}".to_string() + ), + ( + "stdole", + "{00000000-0000-0000-C000-000000000046}".to_string() + ), + ] + ); + let unk = shapes + .members() + .unwrap() + .nth(3) + .unwrap() + .1 + .var_type() + .unwrap(); + assert_eq!(unk.base_type(), ArgType::IUNKNOWN); + + // `Pass(ByVal c As Collection, ByRef f As StdFont, p As Point)`. + let (_, pass) = shapes.func_type_descs().unwrap().next().unwrap(); + let args = pass.arg_types(); + assert_eq!(iface(args[0]), collection); + let point = args[2].record(map).unwrap(); + assert_eq!( + point.libid(map), + typerefs.com_registration().unwrap().project_guid() + ); + assert_eq!( + point.guid(map).unwrap().to_string(), + "{218538C9-BFB4-4136-BFAF-302AB309BE7D}" + ); + assert_eq!( + (point.version().unwrap(), point.lcid().unwrap()), + ((1, 0), 0x409) + ); + + // The libraries: VBA 6.0 LCID 9 from its file, stdole 2.0 LCID 0. + let vba = args[0].interface(map).unwrap().typelib(map).unwrap(); + assert_eq!( + (vba.version().unwrap(), vba.lcid().unwrap(), vba.path(map)), + ((6, 0), 9, Some("C:\\VB98\\VBA6.dll")) + ); + let stdole = args[1].interface(map).unwrap().typelib(map).unwrap(); + assert_eq!( + (stdole.version().unwrap(), stdole.lcid().unwrap()), + ((2, 0), 0) + ); + + // A watcher's `t As VB.TextBox` names VB's TextBox. + let watch = object(typerefs, "Watcher") + .func_type_descs() + .unwrap() + .next() + .unwrap() + .1; + assert_eq!( + iface(watch.arg_types()[0]), + ("VB", "{33AD4EE1-6699-11CF-B70C-00AA0060D393}".to_string()) + ); + + // The hosted controls' libraries carry their versions. + let activex = project("activex"); + let amap = activex.address_map(); + let pi2 = activex.object_table().project_info2_va().unwrap(); + let mut libs: Vec<(String, (u16, u16), u32)> = ControlTypeIter::new(amap, pi2) + .filter_map(|e| e.typelib(amap)) + .map(|l| { + ( + l.name(amap).unwrap().to_string(), + l.version().unwrap(), + l.lcid().unwrap(), + ) + }) + .collect(); + libs.sort(); + libs.dedup(); + assert_eq!( + libs, + [ + ("InetCtlsObjects".to_string(), (1, 0), 0), + ("MSComctlLib".to_string(), (2, 0), 0), + ("MSMask".to_string(), (1, 1), 0), + ("MSWinsockLib".to_string(), (1, 0), 0), + ("VB".to_string(), (6, 0), 9), + ] + ); +} + +/// UDT members and arrays of UDTs name their type's layout; fixed arrays +/// carry their elements' IID or VARTYPE after the SAFEARRAY descriptor. +#[test] +fn cleanup_entries_name_their_types() { + let entries = |name: &str| -> Vec<(String, ControlPropertyEntry<'static>)> { + let project = project(name); + let mut all = Vec::new(); + for obj in project.objects().unwrap() { + let obj = obj.unwrap(); + let owner = obj.name().unwrap().into_owned(); + for table in [obj.public_bytes(), obj.static_bytes()] + .into_iter() + .flatten() + { + all.extend(table.control_entries().map(|e| (owner.clone(), e))); + } + for method in obj.methods().unwrap() { + if let Ok(MethodEntry::PCode(m)) = method { + all.extend(m.cleanup_entries().map(|e| (owner.clone(), e))); + } + } + } + all + }; + let mut sizes = HashSet::new(); + for name in projects() { + let map = project(&name).address_map(); + for (_, entry) in entries(&name) { + let Some(layout) = entry.record_layout(map) else { + assert!(entry.record_layout_va().is_none() && entry.element_layout_va().is_none()); + continue; + }; + let strides: usize = layout.entries().map(|e| e.total_size().unwrap()).sum(); + assert_eq!( + strides + RecordLayout::HEADER_SIZE, + usize::from(layout.size().unwrap()), + "{name}" + ); + sizes.insert(layout.record_size().unwrap()); + } + } + // coverage's Rec and FileRec, data's Record and Pair, statics' Rec, + // types' Pair, records' Outer and Item.CT. + assert_eq!( + sizes, + HashSet::from([0x50, 0x24, 0x20, 0x14, 0x10, 0x08, 0x68, 0x04]) + ); + + // `Rec`: fStr, fV, fO and the fixed-length fFx need cleanup. + let coverage = project("coverage"); + let (_, rec) = entries("coverage") + .into_iter() + .find(|(owner, e)| owner == "Globals" && e.record_layout_va().is_some()) + .unwrap(); + let members: Vec<(u16, ControlPropertyType)> = rec + .record_layout(coverage.address_map()) + .unwrap() + .entries() + .map(|e| (e.frame_offset().unwrap(), e.property_type())) + .collect(); + assert_eq!( + members, + [ + (0x24, ControlPropertyType::String), + (0x28, ControlPropertyType::Variant), + (0x3C, ControlPropertyType::Object), + (0x40, ControlPropertyType::Value(0x0A)), + ] + ); + + // `Bag.m_Slots(7) As Object`: IID_IDispatch; `m_Static(1 To 10) As + // Integer`: VT_I2. + let dispid = project("dispid"); + let iids: Vec = entries("dispid") + .iter() + .filter_map(|(_, e)| e.safearray_iid_va()) + .map(|va| { + Guid::from_bytes(dispid.address_map().slice_from_va(va, 16).unwrap()) + .unwrap() + .to_string() + }) + .collect(); + assert_eq!(iids, ["{00020400-0000-0000-C000-000000000046}"; 2]); + assert!( + entries("data") + .iter() + .any(|(_, e)| e.frame_offset().unwrap() == 4 && e.safearray_vartype() == Some(2)) + ); +} + +/// ProjectInfo2's object descriptor array repeats each object's +/// `ObjectInfo.lpPrivateObject`, but for `udts`' module, whose slot holds +/// an address outside the image. +#[test] +fn project_info2_object_descs() { + let mut classes = 0; + for name in projects() { + let project = project(&name); + let map = project.address_map(); + let pi2_va = project.object_table().project_info2_va().unwrap(); + let pi2 = ProjectInfo2::parse( + map.slice_from_va(pi2_va, ProjectInfo2::HEADER_SIZE) + .unwrap(), + ) + .unwrap(); + let count = project.object_table().total_objects().unwrap(); + let descs = pi2.object_descs(map, count).unwrap(); + let expected: Vec = project + .objects() + .unwrap() + .map(|o| o.unwrap().info().private_object_va().unwrap()) + .collect(); + let descs: Vec = descs + .into_iter() + .map(|va| if map.is_va_in_image(va) { va } else { u32::MAX }) + .collect(); + assert_eq!(descs, expected, "{name}"); + classes += descs.iter().filter(|&&va| va != u32::MAX).count(); + } + assert!(classes > 50, "{classes}"); +} + +/// An ActiveX DLL's or OCX's COM exports are stubs that push the VBHeader +/// (and two `.data` VAs) and jump to the runtime's `VBDll*` function. +#[test] +fn export_stubs_enter_the_runtime() { + for name in projects() { + let project = project(&name); + let stubs: Vec<(String, usize, String)> = project + .export_stubs() + .into_iter() + .inspect(|s| assert_eq!(s.vb_header_va, project.vb_header_va(), "{name}")) + .map(|s| (s.name.unwrap(), s.data_vas.len(), s.function.unwrap())) + .collect(); + if !["docs", "extender", "ocx", "typerefs", "udts"].contains(&name.as_str()) { + assert!(stubs.is_empty(), "{name}"); + continue; + } + assert_eq!( + stubs, + [ + ("DllCanUnloadNow", 0, "VBDllCanUnloadNow"), + ("DllGetClassObject", 2, "VBDllGetClassObject"), + ("DllRegisterServer", 2, "VBDllRegisterServer"), + ("DllUnregisterServer", 2, "VBDllUnRegisterServer"), + ] + .map(|(n, d, f)| (n.to_string(), d, f.to_string())), + "{name}" + ); + } +} + +/// Every native procedure that stores an unwind record is found, named or +/// not, and everything its record names lies inside it: between its start +/// and the next procedure the project knows. +#[test] +fn native_procedures_store_unwind_records() { + let mut found = BTreeMap::new(); + for name in ["events-native", "flow-native", "errors-native"] { + let project = project(name); + let map = project.address_map(); + let code = project.native_code_range().unwrap(); + let starts: Vec = project + .native_entries() + .into_iter() + .filter(|va| code.contains(va)) + .collect(); + let records = project.unwind_records(); + for procedure in &records { + let (start, info) = (procedure.procedure_va, procedure.info); + assert_eq!( + ProcUnwindInfo::from_prologue(map, start).map(|(va, _)| va), + Some(procedure.record_va), + "{name} {start:#x}" + ); + assert!(starts.contains(&start), "{name} {start:#x} is no entry"); + let end = starts + .iter() + .copied() + .find(|&next| next > start) + .unwrap_or(code.end); + let flags = info.flags().unwrap(); + let slot = info.frame_slot().unwrap(); + let expected_slot = match flags & 0x70 { + 0 => None, + 0x10 => Some(0x10), + _ => Some(0x14), + }; + match expected_slot { + Some(expected) => assert_eq!(slot, expected, "{name} {start:#x}"), + None => assert!(slot == 4 || slot == 8, "{name} {start:#x}"), + } + let size = if flags & 0x40 != 0 { + 0x1C + } else if flags & 0x30 != 0 { + 0x18 + } else if flags & 0x0E != 0 { + 0x10 + } else { + 8 + }; + assert_eq!(info.size(), size, "{name} {start:#x}"); + for va in info.code_vas(map) { + assert!(start < va && va < end, "{name} {start:#x}: {va:#x}"); + } + } + found.insert(name, records.len()); + } + // events-native: 33 class methods, Sub Main and `ThroughInterface`; + // flow-native: Sub Main and 15 of Program's procedures, none of which a + // table names; errors-native: Sub Main, Program's 7 and Guard's 2. + assert_eq!( + found, + BTreeMap::from([ + ("events-native", 35), + ("flow-native", 16), + ("errors-native", 10) + ]) + ); + + // A class method holds Me: `Source`'s methods have an exit block. + let native = project("events-native"); + for link in object(native, "Source").method_links().unwrap() { + let link = link.unwrap(); + let (_, info) = ProcUnwindInfo::from_prologue(native.address_map(), link.code_va).unwrap(); + assert_eq!(info.flags().unwrap() & 0x01, 1); + assert_eq!(info.frame_slot().unwrap(), 8); + } +} + +/// The native code region lies between the code markers: empty in a P-Code +/// build, and holding every procedure of a native one (Sub Main and each +/// that stores an unwind record) while the stubs that enter them stay +/// outside it. +#[test] +fn native_code_lies_between_the_markers() { + for name in projects() { + let project = project(&name); + let code = project.native_code_range().unwrap(); + if !name.ends_with("-native") { + assert!(code.is_empty(), "{name}: {code:x?}"); + assert!(project.unwind_records().is_empty(), "{name}"); + continue; + } + assert!( + code.contains(&project.vb_header().sub_main_va().unwrap()), + "{name}" + ); + for procedure in project.unwind_records() { + assert!(code.contains(&procedure.procedure_va), "{name}"); + } + for entry in project.code_entrypoints().unwrap() { + assert!( + entry.stub_va.is_none_or(|va| !code.contains(&va)), + "{name}: {entry:x?}" + ); + } + } +} + +/// The tables an unwind record names hold what the source says: one handler +/// per `On Error GoTo` label, one address per statement for `Resume`, one +/// line number per statement for `Erl`. +#[test] +fn error_handling_tables_match_the_source() { + let project = project("errors-native"); + let map = project.address_map(); + let records = project.unwind_records(); + // Program's procedures in source order, Sub Main, then Guard's methods. + let [ + two, + three, + even, + odd, + lines_odd, + lines_even, + everything, + _main, + divide, + retry, + ] = records.as_slice() + else { + panic!("{} records", records.len()); + }; + let shape = |procedure: &ProcedureUnwind| { + let info = procedure.info; + (info.flags().unwrap(), info.frame_slot().unwrap()) + }; + let handlers = |procedure: &ProcedureUnwind| -> Vec { + procedure + .info + .handlers(map) + .map(|table| table.iter().map(|handler| handler.number).collect()) + .unwrap_or_default() + }; + let statements = + |procedure: &ProcedureUnwind| procedure.info.resume(map).map(|table| table.count()); + let lines = |procedure: &ProcedureUnwind| -> Vec { + let table = procedure.info.lines(map).unwrap(); + (0..=table.count()) + .map(|n| table.line(n).unwrap()) + .collect() + }; + + assert_eq!((shape(two), handlers(two)), ((0x10, 0x10), vec![1, 2])); + assert_eq!( + (shape(three), handlers(three)), + ((0x10, 0x10), vec![1, 2, 3]) + ); + assert_eq!((shape(even), statements(even)), ((0x20, 0x14), Some(6))); + assert_eq!((shape(odd), statements(odd)), ((0x20, 0x14), Some(7))); + assert_eq!(shape(lines_odd), (0x50, 0x14)); + assert_eq!(lines(lines_odd), [0, 0, 10, 20, 30]); + assert_eq!(shape(lines_even), (0x70, 0x14)); + // A numbered line is two statements, its label and its code. + assert_eq!( + lines(lines_even), + [ + 0, 0, 100, 100, 110, 110, 120, 120, 130, 130, 140, 140, 140, 140, 140, 140 + ] + ); + assert_eq!(statements(lines_even), Some(15)); + assert_eq!(shape(everything), (0x7E, 0x14)); + assert_eq!( + lines(everything)[..12], + [0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5] + ); + assert_eq!(statements(everything), Some(18)); + // A class method's record also names its exit block. + assert_eq!((shape(divide), handlers(divide)), ((0x11, 0x10), vec![1])); + assert_eq!((shape(retry), statements(retry)), ((0x31, 0x14), Some(8))); + assert!(divide.info.exit_va().is_some() && retry.info.exit_va().is_some()); + + for procedure in [even, odd, lines_even, everything, retry] { + let table = procedure.info.resume(map).unwrap(); + let vas: Vec = table.iter().collect(); + assert!(vas.is_sorted(), "{:#x}", procedure.procedure_va); + assert_eq!(table.statement_va(table.count() + 1), None); + // The first statement stores its number in `ebp-4`. + let first = map.slice_from_va(vas[0], 7).unwrap(); + assert_eq!(first[..7], [0xC7, 0x45, 0xFC, 1, 0, 0, 0]); + // The word after the last statement, which `Resume Next` from it + // reads, is 0. + let va = procedure.info.resume_va().unwrap(); + let sentinel = map + .slice_from_va(va + 4 * (table.count() as u32 + 1), 4) + .unwrap(); + assert_eq!(sentinel[..4], [0; 4]); + } +} + +/// An ActiveX project's type library names each class's events by DISPID; +/// a Standard EXE has none, and its pooled strings name no event. +#[test] +fn event_names_come_from_the_type_library() { + let names = |project_name: &str, object_name: &str| -> Vec> { + object(project(project_name), object_name) + .event_names() + .unwrap() + }; + assert_eq!(names("ocx", "Knob"), [Some("Turned"), Some("Reset")]); + assert_eq!(names("ocx", "Panel"), [Some("Summary")]); + assert_eq!( + names("extender", "Coords"), + [ + "Pixels", + "Himetric", + "Container", + "Others", + "Variants", + "Referenced" + ] + .map(Some) + ); + assert_eq!(names("typerefs", "Shapes"), [Some("Moved")]); + assert_eq!(names("server", "Multi"), [Some("Changed")]); + // A Private class is not in the library. + assert_eq!(names("server", "Hidden"), [None]); + // Standard EXEs. + assert_eq!(names("events", "Source"), [None, None, None]); + assert_eq!(names("eventnames", "Second"), [None, None]); + assert!(project("events").type_library_bytes().is_none()); + assert!( + project("docs") + .type_library_bytes() + .unwrap() + .starts_with(b"MSFT") + ); +} + +/// The event prototypes are in declaration order, with the events' +/// parameter names. +#[test] +fn event_type_descs_name_event_parameters() { + let project = project("events"); + let map = project.address_map(); + let params: Vec> = object(project, "Source") + .event_type_descs() + .unwrap() + .map(|(_, ftd)| ftd.param_names(map)) + .collect(); + assert_eq!( + params, + [ + vec![], + vec![&b"n"[..], b"ratio", b"Cancel"], + vec![&b"s"[..], b"v", b"o"] + ] + ); +} + +/// Every string of the ProjectInfo2 name area has an owner, and every +/// pointer to it in the file is one of its owners' pointers. +#[test] +fn name_references_own_every_name() { + let mut owned = 0; + for name in projects() { + let project = project(&name); + let map = project.address_map(); + let file = map.file(); + let refs = project.name_references().unwrap(); + let pi2 = project.object_table().project_info2_va().unwrap(); + for pool_name in read_name_strings(map, pi2) { + let offset = pool_name.as_ptr() as usize - file.as_ptr() as usize; + let va = map.offset_to_va(offset).unwrap(); + let owners: HashSet = refs + .iter() + .filter(|r| r.name_va == va) + .map(|r| r.pointer_va) + .collect(); + assert!(!owners.is_empty(), "{name}: {pool_name} has no owner"); + let pointers: HashSet = file + .windows(4) + .enumerate() + .filter(|(_, w)| u32::from_le_bytes([w[0], w[1], w[2], w[3]]) == va) + .filter_map(|(o, _)| map.offset_to_va(o)) + .collect(); + assert_eq!(pointers, owners, "{name}: {pool_name}"); + owned += 1; + } + } + assert_eq!(owned, 127); + + // Names past where the walk stops are attributed too. + let refs = project("members").name_references().unwrap(); + let amount: Vec = refs + .iter() + .filter(|r| r.name == b"Amount") + .map(|r| r.owner) + .collect(); + assert_eq!(amount.len(), 4, "{amount:?}"); + assert!(amount.iter().any(|o| matches!( + o, + NameOwner::EventParameter { + event: 0, + index: 0, + .. + } + ))); + // `calls` `Value` is the parameter of `Counter`'s `Event Changed`, not + // its property. + let refs = project("calls").name_references().unwrap(); + let value: Vec = refs + .iter() + .filter(|r| r.name == b"Value") + .map(|r| r.owner) + .collect(); + assert!(matches!( + value[..], + [NameOwner::EventParameter { + event: 0, + index: 0, + .. + }] + )); +} + /// `Sub Main` is found through the ProcDscInfo its P-Code stub loads. #[test] fn sub_main_names_its_procedure() { @@ -443,7 +1151,7 @@ fn sub_main_names_its_procedure() { assert_eq!(method.proc_dsc_va(), dsc); found += 1; } - assert_eq!(found, 19); + assert_eq!(found, 21); let hello = project("hello") .code_entrypoints() .unwrap() @@ -531,6 +1239,138 @@ fn code_entries_list_each_method_once() { } } +/// A `WithEvents` or `Implements` sink is a dual interface's vtable: four +/// `IDispatch` slots, then the handlers; a control's sink has the handlers +/// alone. Every handler enters the procedure that handles its slot. +#[test] +fn sinks_skip_idispatch_slots() { + const DISPATCH: [&str; 4] = [ + "Zombie_GetTypeInfoCount", + "Zombie_GetTypeInfo", + "EVENT_SINK_GetIDsOfNames", + "EVENT_SINK_Invoke", + ]; + let thunk_name = |project: &VbProject<'_>, va: u32| -> Option { + let code = project.address_map().slice_from_va(va, 6).ok()?; + (code[..2] == [0xFF, 0x25]).then_some(())?; + let slot = u32::from_le_bytes(code[2..6].try_into().unwrap()); + Some(project.imports().by_slot(slot)?.function().to_string()) + }; + let mut kinds = BTreeMap::new(); + for name in projects() { + let project = project(&name); + let map = project.address_map(); + for obj in project.objects().unwrap() { + let obj = obj.unwrap(); + for control in obj.controls().unwrap() { + let control = control.unwrap(); + let kind = control.info().kind().unwrap(); + *kinds.entry(format!("{kind:?}")).or_insert(0) += 1; + let Some(sink) = control.event_sink(map) else { + continue; + }; + let dispatch: Vec> = (0..4) + .map(|i| sink.dispatch_va(i).and_then(|va| thunk_name(project, va))) + .collect(); + // Every sink has the IDispatch slots iff ControlInfo says so, + // and those are the runtime's thunks. + let raw: Vec> = (0..4) + .map(|i| { + let va = control.info().event_sink_vtable_va().unwrap() + 0x18 + 4 * i; + let slot = map.slice_from_va(va, 4).ok()?; + thunk_name(project, u32::from_le_bytes(slot[..4].try_into().unwrap())) + }) + .collect(); + let dual = raw == DISPATCH.map(|f| Some(f.to_string())); + assert_eq!( + dual, + control.info().dispatch_slots().unwrap() == 4, + "{name}" + ); + if dual { + assert_eq!(dispatch, raw, "{name}"); + } else { + assert_eq!(dispatch, [None, None, None, None], "{name}"); + } + for (slot, va) in sink.connected_handlers() { + assert_eq!(control.event_handler_va(slot), Some(va)); + let imported = thunk_name(project, va); + assert!( + imported.as_deref().is_none_or(|f| !DISPATCH.contains(&f)), + "{name}: {} slot {slot} is {imported:?}", + control.name() + ); + } + } + } + } + assert_eq!( + kinds, + BTreeMap::from([ + ("Control".to_string(), 136), + ("Implements".to_string(), 9), + ("WithEvents".to_string(), 6), + ]) + ); + + // The handlers enter the procedures named for their control. + let handled = |project_name: &str, object_name: &str, control: &str| -> Vec { + let obj = object(project(project_name), object_name); + let entries = obj.code_entries(None).unwrap(); + let ctrl = obj + .controls() + .unwrap() + .map(Result::unwrap) + .find(|c| c.name() == control) + .unwrap(); + (0..ctrl.event_count().unwrap()) + .filter_map(|slot| ctrl.event_handler_va(slot).filter(|&va| va != 0)) + .map(|va| { + let entry = entries.iter().find(|e| e.va == va).unwrap(); + // The method's name; a handler with none (a private handler + // of a dispinterface source) by its entry's label. + let index = entry.method_index.unwrap(); + obj.method_name(index) + .unwrap() + .as_str() + .map(|n| n.into_owned()) + .unwrap_or_else(|| entry.name.clone().unwrap()) + }) + .collect() + }; + assert_eq!( + handled("events", "Listener", "m_Source"), + ["m_Source_Started", "m_Source_Ticked", "m_Source_Named"] + ); + // Native: each handler's stub jumps to one of the object's procedures, + // listed as its own entry. + let listener = object(project("events-native"), "Listener"); + let entries = listener.code_entries(None).unwrap(); + let handlers: Vec<&CodeEntry> = entries + .iter() + .filter(|e| e.kind == CodeEntryKind::EventHandler) + .collect(); + assert_eq!(handlers.len(), 3); + for handler in handlers { + let target = handler.target_va.unwrap(); + assert!( + entries + .iter() + .any(|e| e.kind == CodeEntryKind::NativeThunk && e.va == target), + "{target:#x}" + ); + } + let ring = handled("events", "Ring", "Measure"); + assert_eq!(ring.len(), 9); + assert!(ring.iter().all(|n| n.starts_with("Measure_")), "{ring:?}"); + assert_eq!(handled("members", "Mixed", "Shown"), ["Shown_Fed"]); + // A WithEvents variable of a dispinterface source has no IDispatch + // slots; one of a project class has them. + assert_eq!(handled("typerefs", "Watcher", "Txt"), ["Txt_Change"]); + assert_eq!(handled("typerefs", "Watcher", "Src"), ["Src_Moved"]); + assert_eq!(handled("members", "Mixed", "Hidden"), ["Hidden_Fed"]); +} + /// A control's event sink slots hold its handlers' event stubs on disk. /// Each stub loads the control's dispatch offset into eax, which /// MethCallEngine subtracts from `this`, and enters its procedure.