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 1c46c24..c346920 100755 Binary files a/tests/fixtures/dispid/dispid.exe and b/tests/fixtures/dispid/dispid.exe differ diff --git a/tests/fixtures/errors-native/Guard.cls b/tests/fixtures/errors-native/Guard.cls new file mode 100644 index 0000000..ca1084c --- /dev/null +++ b/tests/fixtures/errors-native/Guard.cls @@ -0,0 +1,38 @@ +VERSION 1.0 CLASS +BEGIN + MultiUse = -1 'True + Persistable = 0 'NotPersistable + DataBindingBehavior = 0 'vbNone + DataSourceBehavior = 0 'vbNone + MTSTransactionMode = 0 'NotAnMTSObject +END +Attribute VB_Name = "Guard" +Attribute VB_GlobalNameSpace = False +Attribute VB_Creatable = True +Attribute VB_PredeclaredId = False +Attribute VB_Exposed = False +Option Explicit + +' Error handling in class methods, whose unwind record also releases Me. + +Private m_Count As Long + +' A handler label. +Public Function Divide(ByVal a As Long, ByVal b As Long) As Long + On Error GoTo Failed + Divide = a \ b + Exit Function +Failed: + m_Count = m_Count + 1 + Divide = 0 +End Function + +' Resume after fixing the cause. +Public Function Retry(ByVal b As Long) As Long + On Error GoTo Repair + Retry = 100 \ b + Exit Function +Repair: + b = 1 + Resume +End Function diff --git a/tests/fixtures/errors-native/Program.bas b/tests/fixtures/errors-native/Program.bas new file mode 100644 index 0000000..accd0fd --- /dev/null +++ b/tests/fixtures/errors-native/Program.bas @@ -0,0 +1,110 @@ +Attribute VB_Name = "Program" +Option Explicit + +' Error handling compiled to native code: the unwind record a procedure +' stores in its frame names its handler labels, the address of each +' statement for Resume and Resume Next, and the line number of each +' statement for Erl. Each procedure varies one of their lengths. + +Private m_Log As String + +' Two handlers, switched between: a handler table of two entries. +Private Function TwoHandlers(ByVal d As Long) As Long + Dim s As Long + On Error GoTo First + s = 10 \ d + On Error GoTo Second + s = s + 20 \ (d - 1) + TwoHandlers = s + Exit Function +First: + TwoHandlers = -1 + Exit Function +Second: + TwoHandlers = -2 +End Function + +' Three handlers: a table of three entries. +Private Function ThreeHandlers(ByVal d As Long) As Long + On Error GoTo A + ThreeHandlers = 1 \ d + On Error GoTo B + ThreeHandlers = 2 \ (d - 1) + On Error GoTo C + ThreeHandlers = 3 \ (d - 2) + Exit Function +A: + ThreeHandlers = -1 + Exit Function +B: + ThreeHandlers = -2 + Exit Function +C: + ThreeHandlers = -3 +End Function + +' Resume Next over an even number of statements. +Private Function ResumeEven(ByVal d As Long) As Long + Dim s As Long + On Error Resume Next + s = 1 \ d + s = s + 1 + ResumeEven = s +End Function + +' Resume Next over an odd number of statements. +Private Function ResumeOdd(ByVal d As Long) As Long + Dim s As Long + On Error Resume Next + s = 1 \ d + s = s + 1 + s = s * 2 + ResumeOdd = s +End Function + +' Line numbers on an odd number of statements, read by Erl. +Private Function LinesOdd(ByVal d As Long) As Long +10 On Error GoTo Handler +20 LinesOdd = 1 \ d +30 Exit Function +Handler: + LinesOdd = Erl +End Function + +' Line numbers on an even number of statements, with Resume Next. +Private Function LinesEven(ByVal d As Long) As Long + Dim s As Long +100 On Error GoTo Handler +110 s = 1 \ d +120 s = s + 1 +130 LinesEven = s +140 Exit Function +Handler: + s = Erl + Resume Next +End Function + +' A handler, Resume, Resume Next and line numbers in one procedure that also +' frees a String and a Variant. +Private Function Everything(ByVal d As Long) As String + Dim t As String, v As Variant +1 On Error GoTo Handler +2 t = CStr(10 \ d) +3 v = t & "x" +4 Everything = v +5 Exit Function +Handler: + If d = 0 Then + d = 1 + Resume + End If + Resume Next +End Function + +Sub Main() + Dim g As Guard + m_Log = m_Log & TwoHandlers(0) & ThreeHandlers(1) & ResumeEven(0) + m_Log = m_Log & ResumeOdd(0) & LinesOdd(0) & LinesEven(0) & Everything(0) + Set g = New Guard + m_Log = m_Log & g.Divide(1, 0) & g.Retry(0) +End Sub diff --git a/tests/fixtures/errors-native/errors-native.exe b/tests/fixtures/errors-native/errors-native.exe new file mode 100755 index 0000000..b8e3efd Binary files /dev/null and b/tests/fixtures/errors-native/errors-native.exe differ 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 0000000..7b0056c Binary files /dev/null and b/tests/fixtures/eventnames/eventnames.exe differ 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 0000000..0432104 Binary files /dev/null and b/tests/fixtures/records/records.exe differ 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 0000000..f7ae07e Binary files /dev/null and b/tests/fixtures/typerefs/typerefs.dll differ diff --git a/tests/fixtures/typerefs/typerefs.vbp b/tests/fixtures/typerefs/typerefs.vbp new file mode 100644 index 0000000..0efc428 --- /dev/null +++ b/tests/fixtures/typerefs/typerefs.vbp @@ -0,0 +1,30 @@ +Type=OleDll +Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#C:\windows\system32\stdole2.tlb#OLE Automation +Class=Shapes; Shapes.cls +Class=Watcher; Watcher.cls +Startup="(None)" +ExeName32="typerefs.dll" +Command32="" +Name="TypeRefs" +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/udts/Mod1.bas b/tests/fixtures/udts/Mod1.bas new file mode 100644 index 0000000..9b97f16 --- /dev/null +++ b/tests/fixtures/udts/Mod1.bas @@ -0,0 +1,13 @@ +Attribute VB_Name = "Mod1" +Option Explicit + +' A standard module's Types, which no private object descriptor lists. + +Public Type ModT + T As String + U As Long +End Type + +Public Type ModU + A As Variant +End Type diff --git a/tests/fixtures/udts/Second.cls b/tests/fixtures/udts/Second.cls new file mode 100644 index 0000000..d7c7eb1 --- /dev/null +++ b/tests/fixtures/udts/Second.cls @@ -0,0 +1,30 @@ +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 = True +Option Explicit + +' A Type with the same members as Shapes' PrivT, and a parameter of +' another class's Type. + +Public Type Pair + K As String + V As Long +End Type + +Public Function Mk(ByVal k As String) As Pair + Mk.K = k +End Function + +Public Function Pt(p As Point) As Long + Pt = p.Y +End Function diff --git a/tests/fixtures/udts/Shapes.cls b/tests/fixtures/udts/Shapes.cls new file mode 100644 index 0000000..d66b2ae --- /dev/null +++ b/tests/fixtures/udts/Shapes.cls @@ -0,0 +1,69 @@ +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 + +' Public and Private Types of a class: a nested record, an array of +' records, an object and a Double member, records passed and returned, and +' a module's Types used in a class. + +Public Type Point + X As Long + Y As Long +End Type + +Public Type Named + Name As String + At As Point + Tags() As String + V As Variant +End Type + +Public Type Third + O As Object + D As Double +End Type + +Private Type PrivT + S As String + Z As Long +End Type + +Private Type Holder + Ns() As Named + Q As String +End Type + +Public Function Local1() As String + Dim n As Named, p As PrivT, h As Holder, m As ModT + n.Name = "a" + p.S = "b" + m.T = "c" + ReDim h.Ns(1) + Open "x" For Random As #1 Len = 100 + Put #1, , h + Close #1 + Local1 = n.Name & p.S & m.T +End Function + +Public Function Make(ByVal X As Long) As Point + Make.X = X +End Function + +Public Function Name(n As Named) As String + Name = n.Name +End Function + +Public Function Thr(t As Third) As Double + Thr = t.D +End Function diff --git a/tests/fixtures/udts/udts.dll b/tests/fixtures/udts/udts.dll new file mode 100644 index 0000000..732c6af Binary files /dev/null and b/tests/fixtures/udts/udts.dll differ diff --git a/tests/fixtures/udts/udts.vbp b/tests/fixtures/udts/udts.vbp new file mode 100644 index 0000000..0cd48b3 --- /dev/null +++ b/tests/fixtures/udts/udts.vbp @@ -0,0 +1,31 @@ +Type=OleDll +Reference=*\G{00020430-0000-0000-C000-000000000046}#2.0#0#C:\windows\system32\stdole2.tlb#OLE Automation +Class=Shapes; Shapes.cls +Class=Second; Second.cls +Module=Mod1; Mod1.bas +Startup="(None)" +ExeName32="udts.dll" +Command32="" +Name="Udts" +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/structures.rs b/tests/structures.rs index 591a3bb..470b054 100644 --- a/tests/structures.rs +++ b/tests/structures.rs @@ -20,13 +20,17 @@ use std::{ use visualbasic::{ CompilationMode, EntrypointKind, Error, MethodEntry, RecognitionFailure, VbObject, VbProject, - project::{CodeEntryKind, MethodLinkKind, MethodNameResult}, + project::{CodeEntry, CodeEntryKind, MethodLinkKind, MethodNameResult, NameOwner}, vb::{ comreg::ComRegData, constantpool::PoolEntry, - controlprop::ControlPropertyType, + control::Guid, + controlprop::{ControlPropertyEntry, ControlPropertyType, RecordLayout}, external::{CallApiStub, ConversionKind, ExternalKind}, - projectinfo2::{ControlTypeIter, read_name_strings}, + functype::ArgType, + member::MemberKind, + native::{ProcUnwindInfo, ProcedureUnwind}, + projectinfo2::{ControlTypeIter, ProjectInfo2, read_name_strings}, }, }; @@ -345,7 +349,7 @@ fn private_descriptor_counts() { } } -/// The ProjectInfo2 records and parameter names interleave: the walk +/// The ProjectInfo2 records and names interleave: the walk /// reads past names and records with no type library, and stops at the /// P-Code. #[test] @@ -391,6 +395,710 @@ fn project_info2_records_and_names() { } } +/// The member array lists the public variables and the `Implements`, with +/// a null entry per private variable: plain variables in declaration +/// order, then the interfaces, then the `WithEvents` variables. +#[test] +fn members_match_declarations() { + let members = |project_name: &str, name: &str| -> 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.