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