2026-10-11 07:57:48 +02:00
// REA-backed WinRT ABI for WhatsAppNativeProjection.dll build 2.2639.100.0.
// Native calls only; no CDP, UI automation or restart.
const native = Process . getModuleByName ( 'WhatsAppNative.dll' );
2026-10-11 08:08:31 +02:00
const compatibility = { tested_build : '2.2639.100.0' , matches_tested_configuration : native . path . includes ( 'WhatsAppDesktop_2.2639.100.0_' )};
compatibility . warning = compatibility . matches_tested_configuration ? null : 'Untested Desktop build; attempting native SQLite ABI discovery. Compatibility is unverified.' ;
2026-10-11 07:57:48 +02:00
const combase = Process . getModuleByName ( 'combase.dll' );
function api ( name , ret , args ){ return new NativeFunction ( combase . getExportByName ( name ), ret , args );}
const createString = api ( 'WindowsCreateString' , 'int' ,[ 'pointer' , 'uint' , 'pointer' ]);
const deleteString = api ( 'WindowsDeleteString' , 'int' ,[ 'pointer' ]);
const rawString = api ( 'WindowsGetStringRawBuffer' , 'pointer' ,[ 'pointer' , 'pointer' ]);
function check ( hr ){ if ( hr < 0 ) throw Error ( 'HRESULT 0x' + ( hr >>> 0 ). toString ( 16 ));}
function hstring ( value ){ const out = Memory . alloc ( 8 ); check ( createString ( Memory . allocUtf16String ( value ), value . length , out )); return out . readPointer ();}
function readHString ( handle ){ if ( handle . isNull ()) return '' ; const len = Memory . alloc ( 4 ); const p = rawString ( handle , len ); return p . readUtf16String ( len . readU32 ());}
function guid ( hex ){ const p = Memory . alloc ( 16 ); p . writeByteArray ( hex . match ( /../g ). map ( x => parseInt ( x , 16 ))); return p ;}
const dbIid = guid ( 'eeb0ca996bf8523780d9cf1b934d62b5' );
const stmtIid = guid ( '94f8c705a4cb4b3baa1b9b2ce2ff2734' );
const propertyIid = guid ( 'dd82d64b5475e9409a9b82654ede7e62' );
function method ( obj , slot , ret , args ){ return new NativeFunction ( obj . readPointer (). add ( slot * 8 ). readPointer (), ret ,[ 'pointer' ,... args ]);}
function release ( obj ){ if ( ! obj . isNull ()) method ( obj , 2 , 'uint' ,[])( obj );}
function query ( obj , iid ){ const out = Memory . alloc ( 8 ); check ( method ( obj , 0 , 'int' ,[ 'pointer' , 'pointer' ])( obj , iid , out )); return out . readPointer ();}
let references = [];
function property ( value ){ if ( value . isNull ()) return null ; let p = ptr ( 0 ); try {
p = query ( value , propertyIid ); const out = Memory . alloc ( 8 ); check ( method ( p , 19 , 'int' ,[ 'pointer' ])( p , out ));
const h = out . readPointer (); try { return readHString ( h );} finally { deleteString ( h );}
} finally { release ( p ); release ( value );}}
function statement ( db , sql ){ const h = hstring ( sql ), error = Memory . alloc ( 4 ), partial = Memory . alloc ( 8 ), out = Memory . alloc ( 8 );
try { check ( method ( db , 9 , 'int' ,[ 'pointer' , 'pointer' , 'pointer' , 'pointer' ])( db , h , error , partial , out ));
// The integer out-parameter is the consumed SQL offset, not an error code.
const original = out . readPointer (); if ( original . isNull ()) throw Error ( 'No prepared statement' );
try { return query ( original , stmtIid );} finally { release ( original );}
} finally { deleteString ( h );}
}
function select ( db , sql , limit ){ let st = ptr ( 0 ); try {
st = statement ( db , sql ); const count = Memory . alloc ( 4 ); check ( method ( st , 10 , 'int' ,[ 'pointer' ])( st , count ));
const rows = []; while ( rows . length < limit ){ const has = Memory . alloc ( 1 ); check ( method ( st , 7 , 'int' ,[ 'pointer' ])( st , has )); if ( has . readU8 () === 0 ) break ;
const row = []; for ( let i = 0 ; i < count . readS32 (); i ++ ){ const out = Memory . alloc ( 8 ); check ( method ( st , 11 , 'int' ,[ 'int' , 'pointer' ])( st , i , out )); row . push ( property ( out . readPointer ()));} rows . push ( row );}
return rows ;
} finally { if ( ! st . isNull ()){ try { check ( method ( st , 6 , 'int' ,[])( st ));} finally { release ( st );}}}}
rpc . exports = {
discover ( maxMb ){
// Obtain the real interface vtable from a fresh closed Sqlite object. No DB
// is opened and no credentials are read from the existing session.
const init = api ( 'RoInitialize' , 'int' ,[ 'uint' ])( 1 ); if ( init < 0 && init !==- 2147417850 ) check ( init );
const name = hstring ( 'WhatsAppNative.Sqlite' ), factoryOut = Memory . alloc ( 8 );
const getFactory = new NativeFunction ( native . getExportByName ( 'DllGetActivationFactory' ), 'int' ,[ 'pointer' , 'pointer' ]);
let factory = ptr ( 0 ), sample = ptr ( 0 ), iface = ptr ( 0 );
try { check ( getFactory ( name , factoryOut )); factory = factoryOut . readPointer (); const out = Memory . alloc ( 8 );
check ( method ( factory , 6 , 'int' ,[ 'pointer' ])( factory , out )); sample = out . readPointer (); iface = query ( sample , dbIid );
const vtable = iface . readPointer (); const bytes = Memory . alloc ( 8 ); bytes . writePointer ( vtable );
const pattern = Array . from ( new Uint8Array ( bytes . readByteArray ( 8 ))). map ( v => v . toString ( 16 ). padStart ( 2 , '0' )). join ( ' ' );
let scanned = 0 , hitCount = 0 ; const found = [], addresses = new Set (), failures = [];
for ( const range of Process . enumerateRanges ({ protection : 'rw-' , coalesce : true })){
if ( scanned >= maxMb * 1024 * 1024 ) break ;
const size = Math . min ( range . size , maxMb * 1024 * 1024 - scanned ); scanned += size ;
let hits = []; try { hits = Memory . scanSync ( range . base , size , pattern );} catch { continue ;}
hitCount += hits . length ;
for ( const hit of hits ){ if ( hit . address . equals ( iface ) ||! hit . address . and ( 7 ). isNull ()) continue ;
let object = ptr ( 0 ); try { object = query ( hit . address , dbIid ); if ( addresses . has ( object . toString ())){ release ( object ); continue ;}
const tables = select ( object , "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" , 100 );
const id = references . length ; references . push ( object ); addresses . add ( object . toString ());
found . push ({ id , address : object . toString (), tables : tables . map ( r => r [ 0 ])}); object = ptr ( 0 );
} catch ( error ){ if ( failures . length < 20 ) failures . push ({ address : hit . address . toString (), error : String ( error )}); if ( ! object . isNull ()) release ( object );}
}
2026-10-11 08:08:31 +02:00
} return { compatibility , databases : found , bytesScanned : scanned , vtable : vtable . toString (), vtableHits : hitCount , failures };
2026-10-11 07:57:48 +02:00
} finally { release ( iface ); release ( sample ); release ( factory ); deleteString ( name );}
},
search ( id , term , limit ){ if ( ! Number . isInteger ( id ) || id < 0 || id >= references . length ) throw Error ( 'Unknown database' );
if ( ! Number . isInteger ( limit ) || limit < 1 || limit > 1000 ) throw Error ( 'Invalid row limit' );
const tokens = term . trim (). split ( /\s+/ ). filter ( Boolean ); if ( ! tokens . length ) throw Error ( 'Empty query' );
const match = tokens . map ( t => '"' + t . replace ( /"/g , '""' ) + '"*' ). join ( ' AND ' ). replace ( /'/g , "''" );
const sql = "SELECT CAST(t.id AS TEXT),CAST(t.chatId AS TEXT),CAST(t.timestamp AS TEXT),CAST(t.text AS TEXT) FROM message t JOIN message_fts f ON t.rowid=f.rowid WHERE message_fts MATCH '" + match + "' ORDER BY rank LIMIT " + limit ;
return select ( references [ id ], sql , limit );
},
oldest ( id , limit ){
if ( ! Number . isInteger ( id ) || id < 0 || id >= references . length ) throw Error ( 'Unknown database' );
if ( ! Number . isInteger ( limit ) || limit < 1 || limit > 1000 ) throw Error ( 'Invalid row limit' );
return select ( references [ id ], "SELECT CAST(id AS TEXT),CAST(chatId AS TEXT),CAST(timestamp AS TEXT),CAST(text AS TEXT) FROM message WHERE timestamp > 0 AND text IS NOT NULL AND length(text) > 0 ORDER BY timestamp,id LIMIT " + limit , limit );
},
stats ( id ){ return select ( references [ id ], "SELECT CAST(COUNT(*) AS TEXT),CAST(MIN(timestamp) AS TEXT),CAST(MAX(timestamp) AS TEXT) FROM message" , 1 )[ 0 ];},
indexbounds ( id ){ return select ( references [ id ], "SELECT CAST(COUNT(*) AS TEXT),CAST(COALESCE(MAX(rowid),0) AS TEXT) FROM message" , 1 )[ 0 ];},
exportindex ( id , after , upper , limit ){
if ( ! Number . isInteger ( id ) || id < 0 || id >= references . length ) throw Error ( 'Unknown database' );
if ( ! /^-?\d+$/ . test ( String ( after )) ||! /^\d+$/ . test ( String ( upper ))) throw Error ( 'Invalid row boundary' );
if ( ! Number . isInteger ( limit ) || limit < 1 || limit > 500 ) throw Error ( 'Invalid batch size' );
return select ( references [ id ], "SELECT CAST(rowid AS TEXT),CAST(id AS TEXT),CAST(chatId AS TEXT),CAST(timestamp AS TEXT),CAST(text AS TEXT) FROM message WHERE rowid > " + after + " AND rowid <= " + upper + " ORDER BY rowid LIMIT " + limit , limit );
},
close (){ for ( const object of references ) release ( object ); references = [];}
};