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