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