1/* 2 2022-07-22 3 4 The author disclaims copyright to this source code. In place of a 5 legal notice, here is a blessing: 6 7 * May you do good and not evil. 8 * May you find forgiveness for yourself and forgive others. 9 * May you share freely, never taking more than you give. 10 11 *********************************************************************** 12 13 This file contains extensions to the sqlite3 WASM API related to the 14 Origin-Private FileSystem (OPFS). It is intended to be appended to 15 the main JS deliverable somewhere after sqlite3-api-glue.js and 16 before sqlite3-api-cleanup.js. 17 18 Significant notes and limitations: 19 20 - As of this writing, OPFS is still very much in flux and only 21 available in bleeding-edge versions of Chrome (v102+, noting that 22 that number will increase as the OPFS API matures). 23 24 - The _synchronous_ family of OPFS features (which is what this API 25 requires) are only available in non-shared Worker threads. This 26 file tries to detect that case and becomes a no-op if those 27 features do not seem to be available. 28*/ 29 30// FileSystemHandle 31// FileSystemDirectoryHandle 32// FileSystemFileHandle 33// FileSystemFileHandle.prototype.createSyncAccessHandle 34self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ 35 const warn = console.warn.bind(console), 36 error = console.error.bind(console); 37 if(!self.importScripts || !self.FileSystemFileHandle){ 38 //|| !self.FileSystemFileHandle.prototype.createSyncAccessHandle){ 39 // ^^^ sync API is not required with WASMFS/OPFS backend. 40 warn("OPFS is not available in this environment."); 41 return; 42 }else if(!sqlite3.capi.wasm.bigIntEnabled){ 43 error("OPFS requires BigInt support but sqlite3.capi.wasm.bigIntEnabled is false."); 44 return; 45 } 46 //warn('self.FileSystemFileHandle =',self.FileSystemFileHandle); 47 //warn('self.FileSystemFileHandle.prototype =',self.FileSystemFileHandle.prototype); 48 const toss = (...args)=>{throw new Error(args.join(' '))}; 49 const capi = sqlite3.capi, 50 wasm = capi.wasm; 51 const sqlite3_vfs = capi.sqlite3_vfs 52 || toss("Missing sqlite3.capi.sqlite3_vfs object."); 53 const sqlite3_file = capi.sqlite3_file 54 || toss("Missing sqlite3.capi.sqlite3_file object."); 55 const sqlite3_io_methods = capi.sqlite3_io_methods 56 || toss("Missing sqlite3.capi.sqlite3_io_methods object."); 57 const StructBinder = sqlite3.StructBinder || toss("Missing sqlite3.StructBinder."); 58 const debug = console.debug.bind(console), 59 log = console.log.bind(console); 60 warn("UNDER CONSTRUCTION: setting up OPFS VFS..."); 61 62 const pDVfs = capi.sqlite3_vfs_find(null)/*pointer to default VFS*/; 63 const dVfs = pDVfs 64 ? new sqlite3_vfs(pDVfs) 65 : null /* dVfs will be null when sqlite3 is built with 66 SQLITE_OS_OTHER. Though we cannot currently handle 67 that case, the hope is to eventually be able to. */; 68 const oVfs = new sqlite3_vfs(); 69 const oIom = new sqlite3_io_methods(); 70 oVfs.$iVersion = 2/*yes, two*/; 71 oVfs.$szOsFile = capi.sqlite3_file.structInfo.sizeof; 72 oVfs.$mxPathname = 1024/*sure, why not?*/; 73 oVfs.$zName = wasm.allocCString("opfs"); 74 oVfs.ondispose = [ 75 '$zName', oVfs.$zName, 76 'cleanup dVfs', ()=>(dVfs ? dVfs.dispose() : null) 77 ]; 78 if(dVfs){ 79 oVfs.$xSleep = dVfs.$xSleep; 80 oVfs.$xRandomness = dVfs.$xRandomness; 81 } 82 // All C-side memory of oVfs is zeroed out, but just to be explicit: 83 oVfs.$xDlOpen = oVfs.$xDlError = oVfs.$xDlSym = oVfs.$xDlClose = null; 84 85 /** 86 Pedantic sidebar about oVfs.ondispose: the entries in that array 87 are items to clean up when oVfs.dispose() is called, but in this 88 environment it will never be called. The VFS instance simply 89 hangs around until the WASM module instance is cleaned up. We 90 "could" _hypothetically_ clean it up by "importing" an 91 sqlite3_os_end() impl into the wasm build, but the shutdown order 92 of the wasm engine and the JS one are undefined so there is no 93 guaranty that the oVfs instance would be available in one 94 environment or the other when sqlite3_os_end() is called (_if_ it 95 gets called at all in a wasm build, which is undefined). 96 */ 97 98 /** 99 Installs a StructBinder-bound function pointer member of the 100 given name and function in the given StructType target object. 101 It creates a WASM proxy for the given function and arranges for 102 that proxy to be cleaned up when tgt.dispose() is called. Throws 103 on the slightest hint of error (e.g. tgt is-not-a StructType, 104 name does not map to a struct-bound member, etc.). 105 106 Returns a proxy for this function which is bound to tgt and takes 107 2 args (name,func). That function returns the same thing, 108 permitting calls to be chained. 109 110 If called with only 1 arg, it has no side effects but returns a 111 func with the same signature as described above. 112 */ 113 const installMethod = function callee(tgt, name, func){ 114 if(!(tgt instanceof StructBinder.StructType)){ 115 toss("Usage error: target object is-not-a StructType."); 116 } 117 if(1===arguments.length){ 118 return (n,f)=>callee(tgt,n,f); 119 } 120 if(!callee.argcProxy){ 121 callee.argcProxy = function(func,sig){ 122 return function(...args){ 123 if(func.length!==arguments.length){ 124 toss("Argument mismatch. Native signature is:",sig); 125 } 126 return func.apply(this, args); 127 } 128 }; 129 callee.removeFuncList = function(){ 130 if(this.ondispose.__removeFuncList){ 131 this.ondispose.__removeFuncList.forEach( 132 (v,ndx)=>{ 133 if('number'===typeof v){ 134 try{wasm.uninstallFunction(v)} 135 catch(e){/*ignore*/} 136 } 137 /* else it's a descriptive label for the next number in 138 the list. */ 139 } 140 ); 141 delete this.ondispose.__removeFuncList; 142 } 143 }; 144 }/*static init*/ 145 const sigN = tgt.memberSignature(name); 146 if(sigN.length<2){ 147 toss("Member",name," is not a function pointer. Signature =",sigN); 148 } 149 const memKey = tgt.memberKey(name); 150 //log("installMethod",tgt, name, sigN); 151 const fProxy = 1 152 // We can remove this proxy middle-man once the VFS is working 153 ? callee.argcProxy(func, sigN) 154 : func; 155 const pFunc = wasm.installFunction(fProxy, tgt.memberSignature(name, true)); 156 tgt[memKey] = pFunc; 157 if(!tgt.ondispose) tgt.ondispose = []; 158 if(!tgt.ondispose.__removeFuncList){ 159 tgt.ondispose.push('ondispose.__removeFuncList handler', 160 callee.removeFuncList); 161 tgt.ondispose.__removeFuncList = []; 162 } 163 tgt.ondispose.__removeFuncList.push(memKey, pFunc); 164 return (n,f)=>callee(tgt, n, f); 165 }/*installMethod*/; 166 167 /** 168 Map of sqlite3_file pointers to OPFS handles. 169 */ 170 const __opfsHandles = Object.create(null); 171 172 const randomFilename = function f(len=16){ 173 if(!f._chars){ 174 f._chars = "abcdefghijklmnopqrstuvwxyz"+ 175 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"+ 176 "012346789"; 177 f._n = f._chars.length; 178 } 179 const a = []; 180 let i = 0; 181 for( ; i < len; ++i){ 182 const ndx = Math.random() * (f._n * 64) % f._n | 0; 183 a[i] = f._chars[ndx]; 184 } 185 return a.join(''); 186 }; 187 188 //const rootDir = await navigator.storage.getDirectory(); 189 190 //////////////////////////////////////////////////////////////////////// 191 // Set up OPFS VFS methods... 192 let inst = installMethod(oVfs); 193 inst('xOpen', function(pVfs, zName, pFile, flags, pOutFlags){ 194 const f = new sqlite3_file(pFile); 195 f.$pMethods = oIom.pointer; 196 __opfsHandles[pFile] = f; 197 f.opfsHandle = null /* TODO */; 198 if(flags & capi.SQLITE_OPEN_DELETEONCLOSE){ 199 f.deleteOnClose = true; 200 } 201 f.filename = zName ? wasm.cstringToJs(zName) : randomFilename(); 202 error("OPFS sqlite3_vfs::xOpen is not yet full implemented."); 203 return capi.SQLITE_IOERR; 204 }) 205 ('xFullPathname', function(pVfs,zName,nOut,pOut){ 206 /* Until/unless we have some notion of "current dir" 207 in OPFS, simply copy zName to pOut... */ 208 const i = wasm.cstrncpy(pOut, zName, nOut); 209 return i<nOut ? 0 : capi.SQLITE_CANTOPEN 210 /*CANTOPEN is required by the docs but SQLITE_RANGE would be a closer match*/; 211 }) 212 ('xAccess', function(pVfs,zName,flags,pOut){ 213 error("OPFS sqlite3_vfs::xAccess is not yet implemented."); 214 let fileExists = 0; 215 switch(flags){ 216 case capi.SQLITE_ACCESS_EXISTS: break; 217 case capi.SQLITE_ACCESS_READWRITE: break; 218 case capi.SQLITE_ACCESS_READ/*docs say this is never used*/: 219 default: 220 error("Unexpected flags value for sqlite3_vfs::xAccess():",flags); 221 return capi.SQLITE_MISUSE; 222 } 223 wasm.setMemValue(pOut, fileExists, 'i32'); 224 return 0; 225 }) 226 ('xDelete', function(pVfs, zName, doSyncDir){ 227 error("OPFS sqlite3_vfs::xDelete is not yet implemented."); 228 return capi.SQLITE_IOERR; 229 }) 230 ('xGetLastError', function(pVfs,nOut,pOut){ 231 debug("OPFS sqlite3_vfs::xGetLastError() has nothing sensible to return."); 232 return 0; 233 }) 234 ('xCurrentTime', function(pVfs,pOut){ 235 /* If it turns out that we need to adjust for timezone, see: 236 https://stackoverflow.com/a/11760121/1458521 */ 237 wasm.setMemValue(pOut, 2440587.5 + (new Date().getTime()/86400000), 238 'double'); 239 return 0; 240 }) 241 ('xCurrentTimeInt64',function(pVfs,pOut){ 242 // TODO: confirm that this calculation is correct 243 wasm.setMemValue(pOut, (2440587.5 * 86400000) + new Date().getTime(), 244 'i64'); 245 return 0; 246 }); 247 if(!oVfs.$xSleep){ 248 inst('xSleep', function(pVfs,ms){ 249 error("sqlite3_vfs::xSleep(",ms,") cannot be implemented from "+ 250 "JS and we have no default VFS to copy the impl from."); 251 return 0; 252 }); 253 } 254 if(!oVfs.$xRandomness){ 255 inst('xRandomness', function(pVfs, nOut, pOut){ 256 const heap = wasm.heap8u(); 257 let i = 0; 258 for(; i < nOut; ++i) heap[pOut + i] = (Math.random()*255000) & 0xFF; 259 return i; 260 }); 261 } 262 263 //////////////////////////////////////////////////////////////////////// 264 // Set up OPFS sqlite3_io_methods... 265 inst = installMethod(oIom); 266 inst('xClose', async function(pFile){ 267 warn("xClose(",arguments,") uses await"); 268 const f = __opfsHandles[pFile]; 269 delete __opfsHandles[pFile]; 270 if(f.opfsHandle){ 271 await f.opfsHandle.close(); 272 if(f.deleteOnClose){ 273 // TODO 274 } 275 } 276 f.dispose(); 277 return 0; 278 }) 279 ('xRead', /*i(ppij)*/function(pFile,pDest,n,offset){ 280 /* int (*xRead)(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst) */ 281 try { 282 const f = __opfsHandles[pFile]; 283 const heap = wasm.heap8u(); 284 const b = new Uint8Array(heap.buffer, pDest, n); 285 const nRead = f.opfsHandle.read(b, {at: offset}); 286 if(nRead<n){ 287 // MUST zero-fill short reads (per the docs) 288 heap.fill(0, dest + nRead, n - nRead); 289 } 290 return 0; 291 }catch(e){ 292 error("xRead(",arguments,") failed:",e); 293 return capi.SQLITE_IOERR_READ; 294 } 295 }) 296 ('xWrite', /*i(ppij)*/function(pFile,pSrc,n,offset){ 297 /* int (*xWrite)(sqlite3_file*, const void*, int iAmt, sqlite3_int64 iOfst) */ 298 try { 299 const f = __opfsHandles[pFile]; 300 const b = new Uint8Array(wasm.heap8u().buffer, pSrc, n); 301 const nOut = f.opfsHandle.write(b, {at: offset}); 302 if(nOut<n){ 303 error("xWrite(",arguments,") short write!"); 304 return capi.SQLITE_IOERR_WRITE; 305 } 306 return 0; 307 }catch(e){ 308 error("xWrite(",arguments,") failed:",e); 309 return capi.SQLITE_IOERR_WRITE; 310 } 311 }) 312 ('xTruncate', /*i(pj)*/async function(pFile,sz){ 313 /* int (*xTruncate)(sqlite3_file*, sqlite3_int64 size) */ 314 try{ 315 warn("xTruncate(",arguments,") uses await"); 316 const f = __opfsHandles[pFile]; 317 await f.opfsHandle.truncate(sz); 318 return 0; 319 } 320 catch(e){ 321 error("xTruncate(",arguments,") failed:",e); 322 return capi.SQLITE_IOERR_TRUNCATE; 323 } 324 }) 325 ('xSync', /*i(pi)*/async function(pFile,flags){ 326 /* int (*xSync)(sqlite3_file*, int flags) */ 327 try { 328 warn("xSync(",arguments,") uses await"); 329 const f = __opfsHandles[pFile]; 330 await f.opfsHandle.flush(); 331 return 0; 332 }catch(e){ 333 error("xSync(",arguments,") failed:",e); 334 return capi.SQLITE_IOERR_SYNC; 335 } 336 }) 337 ('xFileSize', /*i(pp)*/async function(pFile,pSz){ 338 /* int (*xFileSize)(sqlite3_file*, sqlite3_int64 *pSize) */ 339 try { 340 warn("xFileSize(",arguments,") uses await"); 341 const f = __opfsHandles[pFile]; 342 const fsz = await f.opfsHandle.getSize(); 343 capi.wasm.setMemValue(pSz, fsz,'i64'); 344 return 0; 345 }catch(e){ 346 error("xFileSize(",arguments,") failed:",e); 347 return capi.SQLITE_IOERR_SEEK; 348 } 349 }) 350 ('xLock', /*i(pi)*/function(pFile,lockType){ 351 /* int (*xLock)(sqlite3_file*, int) */ 352 // Opening a handle locks it automatically. 353 warn("xLock(",arguments,") is a no-op"); 354 return 0; 355 }) 356 ('xUnlock', /*i(pi)*/function(pFile,lockType){ 357 /* int (*xUnlock)(sqlite3_file*, int) */ 358 // Opening a handle locks it automatically. 359 warn("xUnlock(",arguments,") is a no-op"); 360 return 0; 361 }) 362 ('xCheckReservedLock', /*i(pp)*/function(pFile,pOut){ 363 /* int (*xCheckReservedLock)(sqlite3_file*, int *pResOut) */ 364 // Exclusive lock is automatically acquired when opened 365 warn("xCheckReservedLock(",arguments,") is a no-op"); 366 wasm.setMemValue(pOut,1,'i32'); 367 return 0; 368 }) 369 ('xFileControl', /*i(pip)*/function(pFile,op,pArg){ 370 /* int (*xFileControl)(sqlite3_file*, int op, void *pArg) */ 371 debug("xFileControl(",arguments,") is a no-op"); 372 return capi.SQLITE_NOTFOUND; 373 }) 374 ('xDeviceCharacteristics',/*i(p)*/function(pFile){ 375 /* int (*xDeviceCharacteristics)(sqlite3_file*) */ 376 debug("xDeviceCharacteristics(",pFile,")"); 377 return capi.SQLITE_IOCAP_UNDELETABLE_WHEN_OPEN; 378 }); 379 // xSectorSize may be NULL 380 //('xSectorSize', function(pFile){ 381 // /* int (*xSectorSize)(sqlite3_file*) */ 382 // log("xSectorSize(",pFile,")"); 383 // return 4096 /* ==> SQLITE_DEFAULT_SECTOR_SIZE */; 384 //}) 385 386 const rc = capi.sqlite3_vfs_register(oVfs.pointer, 0); 387 if(rc){ 388 oVfs.dispose(); 389 toss("sqlite3_vfs_register(OPFS) failed with rc",rc); 390 } 391 capi.sqlite3_vfs_register.addReference(oVfs, oIom); 392 warn("End of (very incomplete) OPFS setup.", oVfs); 393 //oVfs.dispose()/*only because we can't yet do anything with it*/; 394}); 395