From d45ac3947de2d3b5c3e84667bac22a044c72fa64 Mon Sep 17 00:00:00 2001 From: stephan Date: Sat, 25 Jun 2022 02:37:57 +0000 Subject: [PATCH] wasm binding: consolidated the two sqlite3_prepare_v2() bindings behind a single dispathcer. Various internal cleanups and refactoring. Branched because trunk is in pencils-down mode for pending 3.39 release. FossilOrigin-Name: ab3e50dab4d71557ab5d179bbd6caf7fb61ab7c51dffc8e4714441c189ce3e5c --- ext/fiddle/sqlite3-api.js | 239 +++++++++++++++++++++++++++++--------- ext/fiddle/testing1.js | 8 +- manifest | 19 +-- manifest.uuid | 2 +- 4 files changed, 202 insertions(+), 66 deletions(-) diff --git a/ext/fiddle/sqlite3-api.js b/ext/fiddle/sqlite3-api.js index 5057eb53f8..3ee57f6818 100644 --- a/ext/fiddle/sqlite3-api.js +++ b/ext/fiddle/sqlite3-api.js @@ -166,9 +166,29 @@ Module.postRun.push(function(namespace/*the module object, the target for SQLITE_INNOCUOUS: 0x000200000, /* sqlite encodings, used for creating UDFs, noting that we will only support UTF8. */ - SQLITE_UTF8: 1 + SQLITE_UTF8: 1, + /* Values for the final argument of sqlite3_result_blob(), + noting that these are interpreted in WASM as pointer + values. */ + SQLITE_TRANSIENT: -1, + SQLITE_STATIC: 0, + + /** + Holds state which are specific to the WASM-related + infrastructure and glue code. It is not expected that client + code will normally need these, but they're exposed here in case it + does. + */ + wasm: { + /** + Proxy for SQM.allocate(). TODO: remove deprecated allocate(), + use _malloc(). The kicker is that allocate() uses + module-init-internal state which isn't normally visible to + us. + */ + allocate: (slab, allocator=SQM.ALLOC_NORMAL)=>SQM.allocate(slab, allocator) + } }; - const cwrap = SQM.cwrap; [/* C-side functions to bind. Each entry is an array with 3 or 4 elements: @@ -219,14 +239,8 @@ Module.postRun.push(function(namespace/*the module object, the target for ["sqlite3_open", "number", ["string", "number"]], //["sqlite3_open_v2", "number", ["string", "number", "number", "string"]], //^^^^ TODO: add the flags needed for the 3rd arg - ["sqlite3_prepare_v2", "number", ["number", "string", "number", "number", "number"]], - ["sqlite3_prepare_v2_sqlptr", "sqlite3_prepare_v2", - /* Impl which requires that the 2nd argument be a pointer to - the SQL string, instead of being converted to a - string. This is used for cases where we require a non-NULL - value for the final argument (exec()'ing multiple - statements from one input string). */ - "number", ["number", "number", "number", "number", "number"]], + /* sqlite3_prepare_v2() is handled separately due to us requiring two + different sets of semantics for that function. */ ["sqlite3_reset", "number", ["number"]], ["sqlite3_result_blob",null,["number", "number", "number", "number"]], ["sqlite3_result_double",null,["number", "number"]], @@ -245,7 +259,104 @@ Module.postRun.push(function(namespace/*the module object, the target for //["sqlite3_normalized_sql", "string", ["number"]] ].forEach(function(a){ const k = (4==a.length) ? a.shift() : a[0]; - api[k] = cwrap.apply(this, a); + api[k] = SQM.cwrap.apply(this, a); + }); + + /** + Proxies for variants of sqlite3_prepare_v2() which have + differing JS/WASM binding semantics. + */ + const prepareMethods = { + /** + This binding expects a JS string as its 2nd argument and + null as its final argument. In order to compile multiple + statements from a single string, the "full" impl (see + below) must be used. + */ + basic: SQM.cwrap('sqlite3_prepare_v2', + "number", ["number", "string", "number"/*MUST always be negative*/, + "number", "number"/*MUST be 0 or null or undefined!*/]), + /* Impl which requires that the 2nd argument be a pointer to + the SQL string, instead of being converted to a + string. This variant is necessary for cases where we + require a non-NULL value for the final argument + (exec()'ing multiple statements from one input + string). For simpler cases, where only the first statement + in the SQL string is required, the wrapper named + sqlite3_prepare_v2() is sufficient and easier to use + because it doesn't require dealing with pointers. + + TODO: hide both of these methods behind a single hand-written + sqlite3_prepare_v2() wrapper which dispatches to the appropriate impl. + */ + full: SQM.cwrap('sqlite3_prepare_v2', + "number", ["number", "number", "number"/*MUST always be negative*/, + "number", "number"]), + }; + + const uint8ToString = (str)=>new TextDecoder('utf-8').decode(str); + //const stringToUint8 = (sql)=>new TextEncoder('utf-8').encode(sql); + + /** + sqlite3_prepare_v2() binding which handles two different uses + with differing JS/WASM semantics: + + 1) sqlite3_prepare_v2(pDb, sqlString, -1, ppStmt [, null]) + + 2) sqlite3_prepare_v2(pDb, sqlPointer, -1, ppStmt, sqlPointerToPointer) + + Note that the SQL length argument (the 3rd argument) must + always be negative because it must be a byte length and that + value is expensive to calculate from JS (where we get the + character length of strings). It is retained in this API's + interface for code/documentation compatibility reasons but is + currently _always_ ignored. When using the 2nd form of this + call, it is critical that the custom-allocated string be + terminated with a 0 byte. (Potential TODO: if this value is >0, + assume the caller knows precisely what they're doing and pass + it on as-is. That approach currently seems fraught with peril.) + + In usage (1), the 2nd argument must be of type string or + Uint8Array (which is assumed to hold SQL). If it is, this + function assumes case (1) and calls the underling C function + with: + + (pDb, sql, -1, ppStmt, null) + + If sql is not a string or Uint8Array, it must be a _pointer_ to + a string which was allocated via api.wasm.allocateUTF8OnStack() + or equivalent (TODO: define "or equivalent"). In that case, the + final argument may be 0/null/undefined or must be a pointer to + which the "tail" of the compiled SQL is written, as documented + for the C-side sqlite3_prepare_v2(). In case (2), the + underlying C function is called with: + + (pDb, sql, -1, ppStmt, pzTail) + + It returns its result and compiled statement as documented in + the C API. Fetching the output pointer (4th argument) requires using + api.wasm.getValue(). + */ + api.sqlite3_prepare_v2 = function(pDb, sql, sqlLen, ppStmt, pzTail){ + if(sql instanceof Uint8Array) sql = uint8ToString(sql); + /* ^^^ TODO: confirm whether this conversion is really + necessary or whether passing on the array as-is will work + as if it were a string. */ + switch(typeof sql){ + case 'string': return prepareMethods.basic(pDb, sql, -1, ppStmt, null); + case 'number': return prepareMethods.full(pDb, sql, -1, ppStmt, pzTail); + default: toss("Invalid SQL argument type for sqlite3_prepare_v2()."); + } + }; + + /** Populate api.wasm... */ + ['getValue','setValue', 'stackSave', 'stackRestore', 'stackAlloc', + 'allocateUTF8OnStack', '_malloc', '_free', + 'addFunction', 'removeFunction' + ].forEach(function(m){ + if(undefined === (api.wasm[m] = SQM[m])){ + toss("Internal init error: Module."+m+" not found."); + } }); /* What follows is colloquially known as "OO API #1". It is a @@ -255,8 +366,6 @@ Module.postRun.push(function(namespace/*the module object, the target for the sqlite3 binding if, e.g., the wrapper is in the main thread and the sqlite3 API is in a worker. */ - /** Memory for use in some pointer-to-pointer-passing routines. */ - const pPtrArg = stackAlloc(4); /** Throws a new error, concatenating all args with a space between each. */ const toss = function(){ @@ -322,9 +431,12 @@ Module.postRun.push(function(namespace/*the module object, the target for } FS.createDataFile("/", fn, buffer, true, true); } - setValue(pPtrArg, 0, "i32"); - this.checkRc(api.sqlite3_open(fn, pPtrArg)); - this._pDb = getValue(pPtrArg, "i32"); + const stack = api.wasm.stackSave(); + const ppDb = api.wasm.stackAlloc(4) /* output (sqlite3**) arg */; + api.wasm.setValue(ppDb, 0, "i32"); + try {this.checkRc(api.sqlite3_open(fn, ppDb));} + finally{api.wasm.stackRestore(stack);} + this._pDb = api.wasm.getValue(ppDb, "i32"); this.filename = fn; this._statements = {/*map of open Stmt _pointers_ to Stmt*/}; this._udfs = {/*map of UDF names to wasm function _pointers_*/}; @@ -378,7 +490,7 @@ Module.postRun.push(function(namespace/*the module object, the target for DB.execMulti(). Does the argument processing/validation, throws on error, and returns a new object on success: - { sql: the SQL, obt: optionsObj, cbArg: function} + { sql: the SQL, opt: optionsObj, cbArg: function} cbArg is only set if the opt.callback is set, in which case it's a function which expects to be passed the current Stmt @@ -386,12 +498,13 @@ Module.postRun.push(function(namespace/*the module object, the target for the input arguments. */ const parseExecArgs = function(args){ - const out = {}; + const out = {opt:{}}; switch(args.length){ case 1: if('string'===typeof args[0]){ out.sql = args[0]; - out.opt = {}; + }else if(args[0] instanceof Uint8Array){ + out.sql = args[0]; }else if(args[0] && 'object'===typeof args[0]){ out.opt = args[0]; out.sql = out.opt.sql; @@ -403,7 +516,11 @@ Module.postRun.push(function(namespace/*the module object, the target for break; default: toss("Invalid argument count for exec()."); }; - if('string'!==typeof out.sql) toss("Missing SQL argument."); + if(out.sql instanceof Uint8Array){ + out.sql = uint8ToString(out.sql); + }else if('string'!==typeof out.sql){ + toss("Missing SQL argument."); + } if(out.opt.callback || out.opt.resultRows){ switch((undefined===out.opt.rowMode) ? 'stmt' : out.opt.rowMode) { @@ -453,7 +570,7 @@ Module.postRun.push(function(namespace/*the module object, the target for delete that._statements[k]; if(s && s._pStmt) s.finalize(); }); - Object.values(this._udfs).forEach(SQM.removeFunction); + Object.values(this._udfs).forEach(api.wasm.removeFunction); delete this._udfs; delete this._statements; api.sqlite3_close_v2(this._pDb); @@ -481,13 +598,21 @@ Module.postRun.push(function(namespace/*the module object, the target for /** Compiles the given SQL and returns a prepared Stmt. This is the only way to create new Stmt objects. Throws on error. + + The given SQL must be a string, a Uint8Array holding SQL, + or a WASM pointer to memory allocated using + api.wasm.allocateUTF8OnStack() (or equivalent (a term which + is yet to be defined precisely)). */ prepare: function(sql){ affirmDbOpen(this); - setValue(pPtrArg,0,"i32"); - this.checkRc(api.sqlite3_prepare_v2(this._pDb, sql, -1, pPtrArg, null)); - const pStmt = getValue(pPtrArg, "i32"); - if(!pStmt) toss("Empty SQL is not permitted."); + const stack = api.wasm.stackSave(); + const ppStmt = api.wasm.stackAlloc(4)/* output (sqlite3_stmt**) arg */; + api.wasm.setValue(ppStmt, 0, "i32"); + try {this.checkRc(api.sqlite3_prepare_v2(this._pDb, sql, -1, ppStmt, null));} + finally {api.wasm.stackRestore(stack);} + const pStmt = api.wasm.getValue(ppStmt, "i32"); + if(!pStmt) toss("Cannot prepare empty SQL."); const stmt = new Stmt(this, pStmt, BindTypes); this._statements[pStmt] = stmt; return stmt; @@ -585,7 +710,7 @@ Module.postRun.push(function(namespace/*the module object, the target for properties: - .sql = the SQL to run (unless it's provided as the first - argument). + argument). This must be of type string or Uint8Array. - .bind = a single value valid as an argument for Stmt.bind(). This is ONLY applied to the FIRST non-empty @@ -638,23 +763,27 @@ Module.postRun.push(function(namespace/*the module object, the target for ? arguments[0] : parseExecArgs(arguments)); if(!arg.sql) return this; const opt = arg.opt; - const stack = stackSave(); + const stack = api.wasm.stackSave(); let stmt; let bind = opt.bind; let rowMode = ( (opt.callback && opt.rowMode) ? opt.rowMode : false); try{ - let pSql = SQM.allocateUTF8OnStack(arg.sql) - const pzTail = stackAlloc(4); - while(getValue(pSql, "i8")){ - setValue(pPtrArg, 0, "i32"); - setValue(pzTail, 0, "i32"); - this.checkRc(api.sqlite3_prepare_v2_sqlptr( - this._pDb, pSql, -1, pPtrArg, pzTail + const sql = (arg.sql instanceof Uint8Array) + ? uint8ToString(arg.sql) + : arg.sql; + let pSql = api.wasm.allocateUTF8OnStack(sql) + const ppStmt = api.wasm.stackAlloc(8) /* output (sqlite3_stmt**) arg */; + const pzTail = ppStmt + 4 /* final arg to sqlite3_prepare_v2_sqlptr() */; + while(api.wasm.getValue(pSql, "i8")){ + api.wasm.setValue(ppStmt, 0, "i32"); + api.wasm.setValue(pzTail, 0, "i32"); + this.checkRc(api.sqlite3_prepare_v2( + this._pDb, pSql, -1, ppStmt, pzTail )); - const pStmt = getValue(pPtrArg, "i32"); - pSql = getValue(pzTail, "i32"); + const pStmt = api.wasm.getValue(ppStmt, "i32"); + pSql = api.wasm.getValue(pzTail, "i32"); if(!pStmt) continue; if(opt.saveSql){ opt.saveSql.push(api.sqlite3_sql(pStmt).trim()); @@ -683,7 +812,7 @@ Module.postRun.push(function(namespace/*the module object, the target for delete stmt._isLocked; stmt.finalize(); } - stackRestore(stack); + api.wasm.stackRestore(stack); } return this; }/*execMulti()*/, @@ -736,7 +865,6 @@ Module.postRun.push(function(namespace/*the module object, the target for - .directOnly = SQLITE_DIRECTONLY - .innocuous = SQLITE_INNOCUOUS - Maintenance reminder: the ability to add new WASM-accessible functions to the runtime requires that the WASM build is compiled with emcc's `-sALLOW_TABLE_GROWTH` @@ -769,7 +897,7 @@ Module.postRun.push(function(namespace/*the module object, the target for let i, pVal, valType, arg; const tgt = []; for(i = 0; i < argc; ++i){ - pVal = getValue(pArgv + (4 * i), "i32"); + pVal = api.wasm.getValue(pArgv + (4 * i), "i32"); valType = api.sqlite3_value_type(pVal); switch(valType){ case api.SQLITE_INTEGER: @@ -806,8 +934,7 @@ Module.postRun.push(function(namespace/*the module object, the target for break; } case 'string': - api.sqlite3_result_text(pCx, val, -1, - -1/*==SQLITE_TRANSIENT*/); + api.sqlite3_result_text(pCx, val, -1, api.SQLITE_TRANSIENT); break; case 'object': if(null===val) { @@ -815,9 +942,10 @@ Module.postRun.push(function(namespace/*the module object, the target for break; }else if(undefined!==val.length){ const pBlob = - SQM.allocate(val, SQM.ALLOC_NORMAL); - api.sqlite3_result_blob(pCx, pBlob, val.length, -1/*==SQLITE_TRANSIENT*/); - SQM._free(blobptr); + api.wasm.allocate(val); + api.sqlite3_result_blob(pCx, pBlob, val.length, + api.SQLITE_TRANSIENT); + api.wasm._free(blobptr); break; } // else fall through @@ -833,7 +961,7 @@ Module.postRun.push(function(namespace/*the module object, the target for api.sqlite3_result_error(pCx, e.message, -1); } }; - const pUdf = SQM.addFunction(wrapper, "viii"); + const pUdf = api.wasm.addFunction(wrapper, "viii"); let fFlags = 0; if(getOwnOption(opt, 'deterministic')) fFlags |= api.SQLITE_DETERMINISTIC; if(getOwnOption(opt, 'directOnly')) fFlags |= api.SQLITE_DIRECTONLY; @@ -846,11 +974,11 @@ Module.postRun.push(function(namespace/*the module object, the target for api.SQLITE_UTF8 | fFlags, null/*pApp*/, pUdf, null/*xStep*/, null/*xFinal*/, null/*xDestroy*/)); }catch(e){ - SQM.removeFunction(pUdf); + api.wasm.removeFunction(pUdf); throw e; } if(this._udfs.hasOwnProperty(name)){ - SQM.removeFunction(this._udfs[name]); + api.wasm.removeFunction(this._udfs[name]); } this._udfs[name] = pUdf; return this; @@ -999,7 +1127,7 @@ Module.postRun.push(function(namespace/*the module object, the target for f._ = { string: function(stmt, ndx, val, asBlob){ const bytes = intArrayFromString(val,true); - const pStr = SQM.allocate(bytes, ALLOC_NORMAL); + const pStr = api.wasm.allocate(bytes); stmt._allocs.push(pStr); const func = asBlob ? api.sqlite3_bind_blob : api.sqlite3_bind_text; return func(stmt._pStmt, ndx, pStr, bytes.length, 0); @@ -1038,7 +1166,7 @@ Module.postRun.push(function(namespace/*the module object, the target for toss("Binding a value as a blob requires", "that it have a length member."); } - const pBlob = SQM.allocate(val, ALLOC_NORMAL); + const pBlob = api.wasm.allocate(val); stmt._allocs.push(pBlob); rc = api.sqlite3_bind_blob(stmt._pStmt, ndx, pBlob, len, 0); } @@ -1054,7 +1182,7 @@ Module.postRun.push(function(namespace/*the module object, the target for const freeBindMemory = function(stmt){ let m; while(undefined !== (m = stmt._allocs.pop())){ - SQM._free(m); + api.wasm._free(m); } return stmt; }; @@ -1481,7 +1609,7 @@ Module.postRun.push(function(namespace/*the module object, the target for if(self === self.window){ /* This is running in the main window thread, so we're done. */ - setTimeout(()=>postMessage({type:'sqlite3-api',data:'loaded'}), 0); + postMessage({type:'sqlite3-api',data:'loaded'}); return; } /****************************************************************** @@ -1489,14 +1617,15 @@ Module.postRun.push(function(namespace/*the module object, the target for in Worker threads. ******************************************************************/ - /* + /** UNDER CONSTRUCTION We need an API which can proxy the DB API via a Worker message interface. The primary quirky factor in such an API is that we cannot pass callback functions between the window thread and a worker thread, so we have to receive all db results via - asynchronous message-passing. + asynchronous message-passing. That requires an asychronous API + with a distinctly different shape that the main OO API. Certain important considerations here include: @@ -1777,5 +1906,5 @@ Module.postRun.push(function(namespace/*the module object, the target for wState.post(evType, response, wMsgHandler.xfer); }; - setTimeout(()=>postMessage({type:'sqlite3-api',data:'loaded'}), 0); -}); + postMessage({type:'sqlite3-api',data:'loaded'}); +})/*postRun.push(...)*/; diff --git a/ext/fiddle/testing1.js b/ext/fiddle/testing1.js index a6f0062dcc..ceea0e6200 100644 --- a/ext/fiddle/testing1.js +++ b/ext/fiddle/testing1.js @@ -27,7 +27,10 @@ const api = sqlite3.api; log("Basic sanity tests..."); T.assert(db._pDb); - let st = db.prepare("select 3 as a"); + let st = db.prepare( + new TextEncoder('utf-8').encode("select 3 as a") + /* Testing handling of Uint8Array input */ + ); //log("statement =",st); T.assert(st._pStmt) .assert(!st._mayGet) @@ -47,6 +50,7 @@ .assert(3.0 === st.get(0,api.SQLITE_FLOAT)) .assert(3.0 === st.getFloat(0)) .assert(st.get(0,api.SQLITE_BLOB) instanceof Uint8Array) + .assert(1===st.get(0,api.SQLITE_BLOB).length) .assert(st.getBlob(0) instanceof Uint8Array) .assert(3 === st.get([])[0]) .assert(3 === st.get({}).a) @@ -72,7 +76,7 @@ INSERT INTO t(a,b) VALUES(1,2),(3,4),(?,?);`, //log("Exec'd SQL:", list); let counter = 0, colNames = []; list.length = 0; - db.exec("SELECT a a, b b FROM t",{ + db.exec(new TextEncoder('utf-8').encode("SELECT a a, b b FROM t"),{ rowMode: 'object', resultRows: list, columnNames: colNames, diff --git a/manifest b/manifest index 621dd03662..dffd8233a8 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Fix\sa\sharmless\sUBSAN\swarning\sassociated\swith\sPRAGMA\sschema_version\nfound\sby\sOSSFuzz. -D 2022-06-24T12:56:48.588 +C wasm\sbinding:\sconsolidated\sthe\stwo\ssqlite3_prepare_v2()\sbindings\sbehind\sa\ssingle\sdispathcer.\sVarious\sinternal\scleanups\sand\srefactoring.\sBranched\sbecause\strunk\sis\sin\spencils-down\smode\sfor\spending\s3.39\srelease. +D 2022-06-25T02:37:57.122 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -65,11 +65,11 @@ F ext/fiddle/fiddle-worker.js 88bc2193a6cb6a3f04d8911bed50a4401fe6f277de7a71ba83 F ext/fiddle/fiddle.html 550c5aafce40bd218de9bf26192749f69f9b10bc379423ecd2e162bcef885c08 F ext/fiddle/fiddle.js 812f9954cc7c4b191884ad171f36fcf2d0112d0a7ecfdf6087896833a0c079a8 F ext/fiddle/index.md d9c1c308d8074341bc3b11d1d39073cd77754cb3ca9aeb949f23fdd8323d81cf -F ext/fiddle/sqlite3-api.js ccf4bd0c1c5bbb3be3469573423d6c53991941bec497eac63e9f17ea13bf8952 +F ext/fiddle/sqlite3-api.js aff4fe5583d02cb7207cc27a92d2cc708fca8a0eba14339f519059298f6bbc5e F ext/fiddle/sqlite3-worker.js a9c2b614beca187dbdd8c053ec2770cc61ec1ac9c0ec6398ceb49a79f705a421 F ext/fiddle/testing.css 750572dded671d2cf142bbcb27af5542522ac08db128245d0b9fe410aa1d7f2a F ext/fiddle/testing1.html ea1f3be727f78e420007f823912c1a03b337ecbb8e79449abc2244ad4fe15d9a -F ext/fiddle/testing1.js e2fa02ac8adbd21c69bc50cfcb79bfc26af0d30a8d6b95ac473a17e0dc9de733 +F ext/fiddle/testing1.js 2ba8d14b335cdfc694757c25a3ffcfa76f4696bb21187db5c12ea6e505c44af0 F ext/fiddle/testing2.html 9063b2430ade2fe9da4e711addd1b51a2741cf0c7ebf6926472a5e5dd63c0bc4 F ext/fiddle/testing2.js 7b45b4e7fddbd51dbaf89b6722c02758051b34bac5a98c11b569a7e7572f88ee F ext/fts1/README.txt 20ac73b006a70bcfd80069bdaf59214b6cf1db5e @@ -1978,8 +1978,11 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P db5266dec601a9513bc8dd09a9f8bb4aef55b780d22610946099e8edd4836587 -R 09efcead2b1be4144ef279312bf3e194 -U drh -Z 1fc91f4cca35906afdee86eebc313610 +P e93fd170ce4ae91d572c46d03f68f55d00091d0188030517455017d90d212587 +R 3edce7228a2d729bf332a89ed3af6305 +T *branch * wasm-cleanups +T *sym-wasm-cleanups * +T -sym-trunk * Cancelled\sby\sbranch. +U stephan +Z e1adc8db1d84e5f34db3da1c68e91f04 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 1a1f800066..e24c1ef14b 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -e93fd170ce4ae91d572c46d03f68f55d00091d0188030517455017d90d212587 \ No newline at end of file +ab3e50dab4d71557ab5d179bbd6caf7fb61ab7c51dffc8e4714441c189ce3e5c \ No newline at end of file