From 24b4cdd8d31b32deacf3e606d9f23de4a845b39c Mon Sep 17 00:00:00 2001 From: stephan Date: Mon, 14 Aug 2023 14:21:28 +0000 Subject: [PATCH 1/6] Remove some obsolete, commented-out makefile code. No functional or build changes. FossilOrigin-Name: ea574cd5de6dbc7db2f4cffe0b6c83c795c4ba634ae1984e5c95bb3529daa5bb --- ext/wasm/GNUmakefile | 7 ------- manifest | 17 ++++++++++------- manifest.uuid | 2 +- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/ext/wasm/GNUmakefile b/ext/wasm/GNUmakefile index 080c427045..2cd44ac55a 100644 --- a/ext/wasm/GNUmakefile +++ b/ext/wasm/GNUmakefile @@ -456,13 +456,6 @@ emcc.exportedRuntimeMethods := \ emcc.jsflags += $(emcc.exportedRuntimeMethods) emcc.jsflags += -sUSE_CLOSURE_COMPILER=0 emcc.jsflags += -sIMPORTED_MEMORY -#emcc.jsflags += -sASYNCIFY=2 -# ^^^ ASYNCIFY=2 is for experimental JSPI support -# (https://v8.dev/blog/jspi), but enabling it causes the lib-level -# init code to throw inexplicable complaints about C-level function -# signatures not matching what we expect them to be. JSPI requires, as of -# this writing, requires an experimental Chrome flag: -# chrome://flags/#enable-experimental-webassembly-stack-switching emcc.jsflags += -sSTRICT_JS=0 # STRICT_JS disabled due to: # https://github.com/emscripten-core/emscripten/issues/18610 diff --git a/manifest b/manifest index d123a018ac..291b52a5b2 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Change\sthe\sname\sof\sthe\s".binary"\scommand\sin\sthe\sCLI\sto\s".crnl".\s\sThe\ssense\sof\nthe\ssetting\sis\sinverted.\s\sThe\soriginal\s".binary"\sstill\sworks\sfor\sbackwards\ncompatibility\sbut\sis\snow\sundocumented.\s\s".crnl"\sis\salso\sundocumented\son\smachines\nwhere\sit\sis\sa\sno-op\s(all\smachines\sother\sthan\sWindows).\n[forum:/forumpost/8bd0b0fbdbc12477|forum\sthread\s8bd0b0fbdbc12477] -D 2023-08-14T13:33:19.213 +C Remove\ssome\sobsolete,\scommented-out\smakefile\scode.\sNo\sfunctional\sor\sbuild\schanges. +D 2023-08-14T14:21:28.009 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -528,7 +528,7 @@ F ext/userauth/sqlite3userauth.h 7f3ea8c4686db8e40b0a0e7a8e0b00fac13aa7a3 F ext/userauth/user-auth.txt e6641021a9210364665fe625d067617d03f27b04 F ext/userauth/userauth.c 7f00cded7dcaa5d47f54539b290a43d2e59f4b1eb5f447545fa865f002fc80cb F ext/wasm/EXPORTED_FUNCTIONS.fiddle.in 27450c8b8c70875a260aca55435ec927068b34cef801a96205adb81bdcefc65c -F ext/wasm/GNUmakefile d02f3c8798b754f68b1f6b422ccff894a10bf352fc9c4eb8945baeace1acac28 +F ext/wasm/GNUmakefile 594f5a6dc889f836ebdf0d4a35a9c39fbf56cd2827416cdd98b5244f51519901 F ext/wasm/README-dist.txt 6382cb9548076fca472fb3330bbdba3a55c1ea0b180ff9253f084f07ff383576 F ext/wasm/README.md a8a2962c3aebdf8d2104a9102e336c5554e78fc6072746e5daf9c61514e7d193 F ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api d6a5078f48a5301ed17b9a30331075d9b2506e1360c1f0dee0c7816c10acd9ab @@ -2091,8 +2091,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 391e21bb6e0b2f4632972b6617a3a18192f88deb29eee5bc060846468e624b21 -R 676721f5b95d5972349e28e60fde58e8 -U drh -Z 343246f1d1f398cedd6a7b7f3f3868a2 +P 544de2da09cd7b9bbb6cb4c52bb22325ace3391fca00c6a43847bfc158032b66 +R f380fd55d7d477e32c828eb055b6a7ec +T *branch * wasm-post-343 +T *sym-wasm-post-343 * +T -sym-trunk * Cancelled\sby\sbranch. +U stephan +Z 9a98a8a06619bcf87fff235150138679 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index e65d56dc16..416c400a52 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -544de2da09cd7b9bbb6cb4c52bb22325ace3391fca00c6a43847bfc158032b66 \ No newline at end of file +ea574cd5de6dbc7db2f4cffe0b6c83c795c4ba634ae1984e5c95bb3529daa5bb \ No newline at end of file From 39c3f5f363726f9f56ebbafb43df0f18f415560d Mon Sep 17 00:00:00 2001 From: stephan Date: Tue, 15 Aug 2023 09:16:41 +0000 Subject: [PATCH 2/6] Remove a piece of stray debug output. No functional changes. FossilOrigin-Name: 00ac653562a66aad3112ea322d08be68e05e6bf7413c814dd3f81bf850fcf43b --- ext/jni/src/org/sqlite/jni/Tester1.java | 1 - manifest | 15 ++++++--------- manifest.uuid | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/ext/jni/src/org/sqlite/jni/Tester1.java b/ext/jni/src/org/sqlite/jni/Tester1.java index ffe0b83846..dca49faf66 100644 --- a/ext/jni/src/org/sqlite/jni/Tester1.java +++ b/ext/jni/src/org/sqlite/jni/Tester1.java @@ -512,7 +512,6 @@ public class Tester1 { rc = sqlite3_db_status(db, SQLITE_DBSTATUS_SCHEMA_USED, cur32, high32, false); affirm( 0 == rc ); affirm( cur32.value > 0 ); - outln(cur32.value," ",high32.value); affirm( high32.value == 0 /* always 0 for SCHEMA_USED */ ); sqlite3_close_v2(db); diff --git a/manifest b/manifest index 291b52a5b2..ae82a99351 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Remove\ssome\sobsolete,\scommented-out\smakefile\scode.\sNo\sfunctional\sor\sbuild\schanges. -D 2023-08-14T14:21:28.009 +C Remove\sa\spiece\sof\sstray\sdebug\soutput.\sNo\sfunctional\schanges. +D 2023-08-15T09:16:41.186 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -255,7 +255,7 @@ F ext/jni/src/org/sqlite/jni/ResultCode.java ba701f20213a5f259e94cfbfdd36eb7ac7c F ext/jni/src/org/sqlite/jni/RollbackHook.java b04c8abcc6ade44a8a57129e33765793f69df0ba909e49ba18d73f4268d92564 F ext/jni/src/org/sqlite/jni/SQLFunction.java 09ce81c1c637e31c3a830d4c859cce95d65f5e02ff45f8bd1985b3479381bc46 F ext/jni/src/org/sqlite/jni/SQLite3Jni.java 4b6fd22e04e63eb65d8e4e38fda39ecf15ce244d034607517627ce2e766e7e65 -F ext/jni/src/org/sqlite/jni/Tester1.java 07c14a90427529ceba54b5e8344ca03602f5789dc53c4163ce22f92d8c577a11 +F ext/jni/src/org/sqlite/jni/Tester1.java 4253dc7bcff64500a9388f1a17d3d39dbe4eb9d7db9fc035ce6e2380d45ad5fc F ext/jni/src/org/sqlite/jni/TesterFts5.java 59e22dd24af033ea8827d36225a2f3297908fb6af8818ead8850c6c6847557b1 F ext/jni/src/org/sqlite/jni/Tracer.java a5cece9f947b0af27669b8baec300b6dd7ff859c3e6a6e4a1bd8b50f9714775d F ext/jni/src/org/sqlite/jni/UpdateHook.java e58645a1727f8a9bbe72dc072ec5b40d9f9362cb0aa24acfe93f49ff56a9016d @@ -2091,11 +2091,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P 544de2da09cd7b9bbb6cb4c52bb22325ace3391fca00c6a43847bfc158032b66 -R f380fd55d7d477e32c828eb055b6a7ec -T *branch * wasm-post-343 -T *sym-wasm-post-343 * -T -sym-trunk * Cancelled\sby\sbranch. +P ea574cd5de6dbc7db2f4cffe0b6c83c795c4ba634ae1984e5c95bb3529daa5bb +R 97fde9343e0b4cbe040d12a977e980b5 U stephan -Z 9a98a8a06619bcf87fff235150138679 +Z b97cc58b42337b264e35e5dff958b84d # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 416c400a52..864e2f6649 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -ea574cd5de6dbc7db2f4cffe0b6c83c795c4ba634ae1984e5c95bb3529daa5bb \ No newline at end of file +00ac653562a66aad3112ea322d08be68e05e6bf7413c814dd3f81bf850fcf43b \ No newline at end of file From abfe646c1223ff8b8c7e9de1ea69da75ff8136b7 Mon Sep 17 00:00:00 2001 From: stephan Date: Tue, 15 Aug 2023 13:01:20 +0000 Subject: [PATCH 3/6] Add note about the current threading limitation to ext/jni/README.md. No code changes. FossilOrigin-Name: 653ed92dc39185cdedfab3ea518bc7ec2d2826120e5fa4cbdee3343301396184 --- ext/jni/README.md | 5 +++-- manifest | 12 ++++++------ manifest.uuid | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/ext/jni/README.md b/ext/jni/README.md index 80486d4a07..cb51a21cd3 100644 --- a/ext/jni/README.md +++ b/ext/jni/README.md @@ -44,8 +44,9 @@ Non-goals: Significant TODOs ======================================================================== -- Lots of APIs left to bind. Most "day-to-day" functionality is already - in place and is believed to work well. +- The initial beta release with version 3.43 has severe threading + limitations. Namely, two threads cannot call into the JNI-bound API + at once. This limitation will be remove in a subsequent release. Building diff --git a/manifest b/manifest index ae82a99351..0da289e830 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Remove\sa\spiece\sof\sstray\sdebug\soutput.\sNo\sfunctional\schanges. -D 2023-08-15T09:16:41.186 +C Add\snote\sabout\sthe\scurrent\sthreading\slimitation\sto\sext/jni/README.md.\sNo\scode\schanges. +D 2023-08-15T13:01:20.690 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -232,7 +232,7 @@ F ext/icu/README.txt 7ab7ced8ae78e3a645b57e78570ff589d4c672b71370f5aa9e1cd7024f4 F ext/icu/icu.c c074519b46baa484bb5396c7e01e051034da8884bad1a1cb7f09bbe6be3f0282 F ext/icu/sqliteicu.h fa373836ed5a1ee7478bdf8a1650689294e41d0c89c1daab26e9ae78a32075a8 F ext/jni/GNUmakefile 3deba6bc0bf37c1ee5f15d1ff3c3512ae2f3cf44a2b8ae7b4af92690514b0cb4 -F ext/jni/README.md 7a614a2fa6c561205f7a53fd8626cf93a7b5711ff454fc1814517f796df398eb +F ext/jni/README.md 5c60e4580aa5c94ff74d7bef1fb6231e578f7764e831a07b5981b6ab62b35560 F ext/jni/jar-dist.make 93da95f8fe01ef22fccacc27f2e805938058e91e8c72c0532558d3a812a42e74 F ext/jni/src/c/sqlite3-jni.c bea6b8691a5fa3a8626a771757bb261208d3c5fc6598266d3b0ee23d88e35632 F ext/jni/src/c/sqlite3-jni.h c5f941b057a24ee62942e6e1bf5a7fd527e5004d20d9638e84a9382813c3cf2a @@ -2091,8 +2091,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P ea574cd5de6dbc7db2f4cffe0b6c83c795c4ba634ae1984e5c95bb3529daa5bb -R 97fde9343e0b4cbe040d12a977e980b5 +P 00ac653562a66aad3112ea322d08be68e05e6bf7413c814dd3f81bf850fcf43b +R 68ba6514d93093aa325f9f497a3147a9 U stephan -Z b97cc58b42337b264e35e5dff958b84d +Z 5cbd8ac0238d9d299017246e54070fc0 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 864e2f6649..c54d1a072a 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -00ac653562a66aad3112ea322d08be68e05e6bf7413c814dd3f81bf850fcf43b \ No newline at end of file +653ed92dc39185cdedfab3ea518bc7ec2d2826120e5fa4cbdee3343301396184 \ No newline at end of file From ccbfe97cd5ff9138cfe6134657aae13e9d27bcf5 Mon Sep 17 00:00:00 2001 From: stephan Date: Fri, 18 Aug 2023 14:16:26 +0000 Subject: [PATCH 4/6] Extend the importDb() method of both OPFS VFSes to (A) support reading in an async streaming fashion via a callback and (B) automatically disable WAL mode in the imported db. FossilOrigin-Name: 9b1398c96a4fd0b59e65faa8d5c98de4129f0f0357732f12cb2f5c53a08acdc2 --- ext/wasm/api/sqlite3-api-prologue.js | 39 +++++++- ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js | 61 +++++++++++- ext/wasm/api/sqlite3-vfs-opfs.c-pp.js | 95 +++++++++++++++---- ext/wasm/tester1.c-pp.js | 45 ++++++++- manifest | 18 ++-- manifest.uuid | 2 +- 6 files changed, 223 insertions(+), 37 deletions(-) diff --git a/ext/wasm/api/sqlite3-api-prologue.js b/ext/wasm/api/sqlite3-api-prologue.js index ca5d1c44ff..5abb13b990 100644 --- a/ext/wasm/api/sqlite3-api-prologue.js +++ b/ext/wasm/api/sqlite3-api-prologue.js @@ -772,8 +772,43 @@ globalThis.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( isSharedTypedArray, toss: function(...args){throw new Error(args.join(' '))}, toss3, - typedArrayPart - }; + typedArrayPart, + /** + Given a byte array or ArrayBuffer, this function throws if the + lead bytes of that buffer do not hold a SQLite3 database header, + else it returns without side effects. + + Added in 3.44. + */ + affirmDbHeader: function(bytes){ + if(bytes instanceof ArrayBuffer) bytes = new Uint8Array(bytes); + const header = "SQLite format 3"; + if( header.length > bytes.byteLength ){ + toss3("Input does not contain an SQLite3 database header."); + } + for(let i = 0; i < header.length; ++i){ + if( header.charCodeAt(i) !== bytes[i] ){ + toss3("Input does not contain an SQLite3 database header."); + } + } + }, + /** + Given a byte array or ArrayBuffer, this function throws if the + database does not, at a cursory glance, appear to be an SQLite3 + database. It only examines the size and header, but further + checks may be added in the future. + + Added in 3.44. + */ + affirmIsDb: function(bytes){ + if(bytes instanceof ArrayBuffer) bytes = new Uint8Array(bytes); + const n = bytes.byteLength; + if(n<512 || n%512!==0) { + toss3("Byte array size",n,"is invalid for an SQLite3 db."); + } + util.affirmDbHeader(bytes); + } + }/*util*/; Object.assign(wasm, { /** diff --git a/ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js b/ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js index 709d3414c3..8e874f7282 100644 --- a/ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js +++ b/ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js @@ -59,6 +59,7 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ const toss3 = sqlite3.util.toss3; const initPromises = Object.create(null); const capi = sqlite3.capi; + const util = sqlite3.util; const wasm = sqlite3.wasm; // Config opts for the VFS... const SECTOR_SIZE = 4096; @@ -869,9 +870,48 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ return b; } + //! Impl for importDb() when its 2nd arg is a function. + async importDbChunked(name, callback){ + const sah = this.#mapFilenameToSAH.get(name) + || this.nextAvailableSAH() + || toss("No available handles to import to."); + sah.truncate(0); + let nWrote = 0, chunk, checkedHeader = false, err = false; + try{ + while( undefined !== (chunk = await callback()) ){ + if(chunk instanceof ArrayBuffer) chunk = new Uint8Array(chunk); + if( 0===nWrote && chunk.byteLength>=15 ){ + util.affirmDbHeader(chunk); + checkedHeader = true; + } + sah.write(chunk, {at: HEADER_OFFSET_DATA + nWrote}); + nWrote += chunk.byteLength; + } + if( nWrote < 512 || 0!==nWrote % 512 ){ + toss("Input size",nWrote,"is not correct for an SQLite database."); + } + if( !checkedHeader ){ + const header = new Uint8Array(20); + sah.read( header, {at: 0} ); + util.affirmDbHeader( header ); + } + sah.write(new Uint8Array(2), { + at: HEADER_OFFSET_DATA + 18 + }/*force db out of WAL mode*/); + }catch(e){ + this.setAssociatedPath(sah, '', 0); + } + this.setAssociatedPath(sah, name, capi.SQLITE_OPEN_MAIN_DB); + return nWrote; + } + //! Documented elsewhere in this file. importDb(name, bytes){ - if(bytes instanceof ArrayBuffer) bytes = new Uint8Array(bytes); + if( bytes instanceof ArrayBuffer ) bytes = new Uint8Array(bytes); + else if( bytes instanceof Function ) return this.importDbChunked(name, bytes); + const sah = this.#mapFilenameToSAH.get(name) + || this.nextAvailableSAH() + || toss("No available handles to import to."); const n = bytes.byteLength; if(n<512 || n%512!=0){ toss("Byte array size is invalid for an SQLite db."); @@ -882,16 +922,16 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ toss("Input does not contain an SQLite database header."); } } - const sah = this.#mapFilenameToSAH.get(name) - || this.nextAvailableSAH() - || toss("No available handles to import to."); const nWrote = sah.write(bytes, {at: HEADER_OFFSET_DATA}); if(nWrote != n){ this.setAssociatedPath(sah, '', 0); toss("Expected to write "+n+" bytes but wrote "+nWrote+"."); }else{ + sah.write(new Uint8Array([0,0]), {at: HEADER_OFFSET_DATA+18} + /* force db out of WAL mode */); this.setAssociatedPath(sah, name, capi.SQLITE_OPEN_MAIN_DB); } + return nWrote; } }/*class OpfsSAHPool*/; @@ -1098,6 +1138,19 @@ globalThis.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ automatically clean up any non-database files so importing them is pointless. + If passed a function for its second argument, its behavior + changes to asynchronous and it imports its data in chunks fed to + it by the given callback function. It calls the callback (which + may be async) repeatedly, expecting either a Uint8Array or + ArrayBuffer (to denote new input) or undefined (to denote + EOF). For so long as the callback continues to return + non-undefined, it will append incoming data to the given + VFS-hosted database file. The result of the resolved Promise when + called this way is the size of the resulting database. + + On succes this routine rewrites the database header bytes in the + output file (not the input array) to force disabling of WAL mode. + On a write error, the handle is removed from the pool and made available for re-use. diff --git a/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js b/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js index 93482505ac..5edb129eaf 100644 --- a/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js +++ b/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js @@ -136,6 +136,7 @@ const installOpfsVfs = function callee(options){ const error = (...args)=>logImpl(0, ...args); const toss = sqlite3.util.toss; const capi = sqlite3.capi; + const util = sqlite3.util; const wasm = sqlite3.wasm; const sqlite3_vfs = capi.sqlite3_vfs; const sqlite3_file = capi.sqlite3_file; @@ -1168,40 +1169,98 @@ const installOpfsVfs = function callee(options){ doDir(opt.directory, 0); }; + /** + impl of importDb() when it's given a function as its second + argument. + */ + const importDbChunked = async function(filename, callback){ + const [hDir, fnamePart] = await opfsUtil.getDirForFilename(filename, true); + const hFile = await hDir.getFileHandle(fnamePart, {create:true}); + const sah = await hFile.createSyncAccessHandle(); + sah.truncate(0); + let nWrote = 0, chunk, checkedHeader = false, err = false; + try{ + while( undefined !== (chunk = await callback()) ){ + if(chunk instanceof ArrayBuffer) chunk = new Uint8Array(chunk); + if( 0===nWrote && chunk.byteLength>=15 ){ + util.affirmDbHeader(chunk); + checkedHeader = true; + } + sah.write(chunk, {at: nWrote}); + nWrote += chunk.byteLength; + } + if( nWrote < 512 || 0!==nWrote % 512 ){ + toss("Input size",nWrote,"is not correct for an SQLite database."); + } + if( !checkedHeader ){ + const header = new Uint8Array(20); + sah.read( header, {at: 0} ); + util.affirmDbHeader( header ); + } + sah.write(new Uint8Array(2), {at: 18}/*force db out of WAL mode*/); + return nWrote; + }catch(e){ + await hDir.removeEntry( fnamePart ).catch(()=>{}); + throw e; + }finally { + await sah.close(); + } + }; + /** Asynchronously imports the given bytes (a byte array or ArrayBuffer) into the given database file. + If passed a function for its second argument, its behaviour + changes to async and it imports its data in chunks fed to it by + the given callback function. It calls the callback (which may + be async) repeatedly, expecting either a Uint8Array or + ArrayBuffer (to denote new input) or undefined (to denote + EOF). For so long as the callback continues to return + non-undefined, it will append incoming data to the given + VFS-hosted database file. When called this way, the resolved + value of the returned Promise is the number of bytes written to + the target file. + It very specifically requires the input to be an SQLite3 database and throws if that's not the case. It does so in order to prevent this function from taking on a larger scope than it is specifically intended to. i.e. we do not want it to become a convenience for importing arbitrary files into OPFS. - Throws on error. Resolves to the number of bytes written. + This routine rewrites the database header bytes in the output + file (not the input array) to force disabling of WAL mode. + + On error this throws and the state of the input file is + undefined (it depends on where the exception was triggered). + + On success, resolves to the number of bytes written. */ opfsUtil.importDb = async function(filename, bytes){ + if( bytes instanceof Function ){ + return importDbChunked(filename, bytes); + } if(bytes instanceof ArrayBuffer) bytes = new Uint8Array(bytes); + util.affirmIsDb(bytes); const n = bytes.byteLength; - if(n<512 || n%512!=0){ - toss("Byte array size is invalid for an SQLite db."); - } - const header = "SQLite format 3"; - for(let i = 0; i < header.length; ++i){ - if( header.charCodeAt(i) !== bytes[i] ){ - toss("Input does not contain an SQLite database header."); - } - } const [hDir, fnamePart] = await opfsUtil.getDirForFilename(filename, true); - const hFile = await hDir.getFileHandle(fnamePart, {create:true}); - const sah = await hFile.createSyncAccessHandle(); - sah.truncate(0); - const nWrote = sah.write(bytes, {at: 0}); - sah.close(); - if(nWrote != n){ - toss("Expected to write "+n+" bytes but wrote "+nWrote+"."); + let sah, err, nWrote = 0; + try { + const hFile = await hDir.getFileHandle(fnamePart, {create:true}); + sah = await hFile.createSyncAccessHandle(); + sah.truncate(0); + nWrote = sah.write(bytes, {at: 0}); + if(nWrote != n){ + toss("Expected to write "+n+" bytes but wrote "+nWrote+"."); + } + sah.write(new Uint8Array(2), {at: 18}) /* force db out of WAL mode */; + return nWrote; + }catch(e){ + await hDir.removeEntry( fnamePart ).catch(()=>{}); + throw e; + }finally{ + if( sah ) await sah.close(); } - return nWrote; }; if(sqlite3.oo1){ diff --git a/ext/wasm/tester1.c-pp.js b/ext/wasm/tester1.c-pp.js index bd945dcab7..f694598eab 100644 --- a/ext/wasm/tester1.c-pp.js +++ b/ext/wasm/tester1.c-pp.js @@ -2939,8 +2939,27 @@ globalThis.sqlite3InitModule = sqlite3InitModule; let db; try { const exp = this.opfsDbExport; + const filename = this.opfsDbFile; delete this.opfsDbExport; - this.opfsImportSize = await sqlite3.oo1.OpfsDb.importDb(this.opfsDbFile, exp); + this.opfsImportSize = await sqlite3.oo1.OpfsDb.importDb(filename, exp); + db = new sqlite3.oo1.OpfsDb(this.opfsDbFile); + T.assert(6 === db.selectValue('select count(*) from p')). + assert( this.opfsImportSize == exp.byteLength ); + db.close(); + this.opfsUnlink(filename); + T.assert(!(await sqlite3.opfs.entryExists(filename))); + // Try again with a function as an input source: + let cursor = 0; + const blockSize = 512, end = exp.byteLength; + const reader = async function(){ + if(cursor >= exp.byteLength){ + return undefined; + } + const rv = exp.subarray(cursor, cursor+blockSize>end ? end : cursor+blockSize); + cursor += blockSize; + return rv; + }; + this.opfsImportSize = await sqlite3.oo1.OpfsDb.importDb(filename, reader); db = new sqlite3.oo1.OpfsDb(this.opfsDbFile); T.assert(6 === db.selectValue('select count(*) from p')). assert( this.opfsImportSize == exp.byteLength ); @@ -3059,8 +3078,9 @@ globalThis.sqlite3InitModule = sqlite3InitModule; const dbytes = u1.exportFile(dbName); T.assert(dbytes.length >= 4096); const dbName2 = '/exported.db'; - u1.importDb(dbName2, dbytes); - T.assert( 2 == u1.getFileCount() ); + let nWrote = u1.importDb(dbName2, dbytes); + T.assert( 2 == u1.getFileCount() ) + .assert( dbytes.byteLength == nWrote ); let db2 = new u1.OpfsSAHPoolDb(dbName2); T.assert(db2 instanceof sqlite3.oo1.DB) .assert(3 === db2.selectValue('select count(*) from t')); @@ -3069,6 +3089,25 @@ globalThis.sqlite3InitModule = sqlite3InitModule; .assert(false === u1.unlink(dbName2)) .assert(1 === u1.getFileCount()) .assert(1 === u1.getFileNames().length); + // Try again with a function as an input source: + let cursor = 0; + const blockSize = 1024, end = dbytes.byteLength; + const reader = async function(){ + if(cursor >= dbytes.byteLength){ + return undefined; + } + const rv = dbytes.subarray(cursor, cursor+blockSize>end ? end : cursor+blockSize); + cursor += blockSize; + return rv; + }; + nWrote = await u1.importDb(dbName2, reader); + T.assert( 2 == u1.getFileCount() ); + db2 = new u1.OpfsSAHPoolDb(dbName2); + T.assert(db2 instanceof sqlite3.oo1.DB) + .assert(3 === db2.selectValue('select count(*) from t')); + db2.close(); + T.assert(true === u1.unlink(dbName2)) + .assert(dbytes.byteLength == nWrote); } T.assert(true === u1.unlink(dbName)) diff --git a/manifest b/manifest index 0da289e830..a96201b44c 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Add\snote\sabout\sthe\scurrent\sthreading\slimitation\sto\sext/jni/README.md.\sNo\scode\schanges. -D 2023-08-15T13:01:20.690 +C Extend\sthe\simportDb()\smethod\sof\sboth\sOPFS\sVFSes\sto\s(A)\ssupport\sreading\sin\san\sasync\sstreaming\sfashion\svia\sa\scallback\sand\s(B)\sautomatically\sdisable\sWAL\smode\sin\sthe\simported\sdb. +D 2023-08-18T14:16:26.669 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -543,13 +543,13 @@ F ext/wasm/api/pre-js.c-pp.js ad906703f7429590f2fbf5e6498513bf727a1a4f0ebfa057af F ext/wasm/api/sqlite3-api-cleanup.js d235ad237df6954145404305040991c72ef8b1881715d2a650dda7b3c2576d0e F ext/wasm/api/sqlite3-api-glue.js b65e546568f1dfb35205b9792feb5146a6323d71b55cda58e2ed30def6dd52f3 F ext/wasm/api/sqlite3-api-oo1.js 9678dc4d9a5d39632b6ffe6ea94a023119260815bf32f265bf5f6c36c9516db8 -F ext/wasm/api/sqlite3-api-prologue.js 5f283b096b98bfb1ee2f2201e7ff0489dff00e29e1030c30953bdb4f5b87f4bd +F ext/wasm/api/sqlite3-api-prologue.js ef6f67c5ea718490806e5e17d2644b8b2f6e6ba5284d23dc1fbfd14d401c1ab5 F ext/wasm/api/sqlite3-api-worker1.js 9f32af64df1a031071912eea7a201557fe39b1738645c0134562bb84e88e2fec F ext/wasm/api/sqlite3-license-version-header.js 0c807a421f0187e778dc1078f10d2994b915123c1223fe752b60afdcd1263f89 F ext/wasm/api/sqlite3-opfs-async-proxy.js 8cf8a897726f14071fae6be6648125162b256dfb4f96555b865dbb7a6b65e379 F ext/wasm/api/sqlite3-v-helper.js 7daa0eab0a513a25b05e9abae7b5beaaa39209b3ed12f86aeae9ef8d2719ed25 -F ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js abb69b5e008961026bf5ff433d7116cb046359af92a5daf73208af2e7ac80ae7 -F ext/wasm/api/sqlite3-vfs-opfs.c-pp.js e04fc2fda6a0200ef80efdbb4ddfa0254453558adb17ec3a230f93d2bf1d711c +F ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js 561463ac5380e4ccf1839a1922e6d7a5585660f32e3b9701a270b78cd35566cf +F ext/wasm/api/sqlite3-vfs-opfs.c-pp.js 656952a75c36d96e3248b03ae26d6a7f8d6ff31e66432c63e1c0bb021f1234ab F ext/wasm/api/sqlite3-wasm.c d4d4c2b349b43b7b861e6d2994299630fb79e07573ea6b61e28e8071b7d16b61 F ext/wasm/api/sqlite3-worker1-promiser.c-pp.js bc06df0d599e625bde6a10a394e326dc68da9ff07fa5404354580f81566e591f F ext/wasm/api/sqlite3-worker1.c-pp.js da509469755035e919c015deea41b4514b5e84c12a1332e6cc8d42cb2cc1fb75 @@ -595,7 +595,7 @@ F ext/wasm/test-opfs-vfs.html 1f2d672f3f3fce810dfd48a8d56914aba22e45c6834e262555 F ext/wasm/test-opfs-vfs.js f09266873e1a34d9bdb6d3981ec8c9e382f31f215c9fd2f9016d2394b8ae9b7b F ext/wasm/tester1-worker.html ebc4b820a128963afce328ecf63ab200bd923309eb939f4110510ab449e9814c F ext/wasm/tester1.c-pp.html 1c1bc78b858af2019e663b1a31e76657b73dc24bede28ca92fbe917c3a972af2 -F ext/wasm/tester1.c-pp.js 64eb0ee6e695d5638d0f758f31a0ca2231e627ca5d768de3d8b44f9f494de8d4 +F ext/wasm/tester1.c-pp.js 9e0f4da49f02753a73a5f931bfb9b1458175518daa3fec40b5ebdc06c285539c F ext/wasm/tests/opfs/concurrency/index.html 0802373d57034d51835ff6041cda438c7a982deea6079efd98098d3e42fbcbc1 F ext/wasm/tests/opfs/concurrency/test.js a98016113eaf71e81ddbf71655aa29b0fed9a8b79a3cdd3620d1658eb1cc9a5d F ext/wasm/tests/opfs/concurrency/worker.js 0a8c1a3e6ebb38aabbee24f122693f1fb29d599948915c76906681bb7da1d3d2 @@ -2091,8 +2091,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P 00ac653562a66aad3112ea322d08be68e05e6bf7413c814dd3f81bf850fcf43b -R 68ba6514d93093aa325f9f497a3147a9 +P 653ed92dc39185cdedfab3ea518bc7ec2d2826120e5fa4cbdee3343301396184 +R 6b40a7c1cd4b822a33b7a897821b1648 U stephan -Z 5cbd8ac0238d9d299017246e54070fc0 +Z b0064c5da7f02d0dcda913e568fa1924 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index c54d1a072a..ff8af35694 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -653ed92dc39185cdedfab3ea518bc7ec2d2826120e5fa4cbdee3343301396184 \ No newline at end of file +9b1398c96a4fd0b59e65faa8d5c98de4129f0f0357732f12cb2f5c53a08acdc2 \ No newline at end of file From 656f6c0c80004aa1dc96bde9019761fb3d7cfacc Mon Sep 17 00:00:00 2001 From: stephan Date: Fri, 18 Aug 2023 14:41:21 +0000 Subject: [PATCH 5/6] Improve exception handling in OpfsDb.importDb(). FossilOrigin-Name: a4eedd63b58f5c273b671f687cb68a32259963dbb053a710770383c735fb7f26 --- ext/wasm/api/sqlite3-vfs-opfs.c-pp.js | 9 ++++++--- manifest | 12 ++++++------ manifest.uuid | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js b/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js index 5edb129eaf..ca2fde985e 100644 --- a/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js +++ b/ext/wasm/api/sqlite3-vfs-opfs.c-pp.js @@ -1176,10 +1176,10 @@ const installOpfsVfs = function callee(options){ const importDbChunked = async function(filename, callback){ const [hDir, fnamePart] = await opfsUtil.getDirForFilename(filename, true); const hFile = await hDir.getFileHandle(fnamePart, {create:true}); - const sah = await hFile.createSyncAccessHandle(); - sah.truncate(0); + let sah = await hFile.createSyncAccessHandle(); let nWrote = 0, chunk, checkedHeader = false, err = false; try{ + sah.truncate(0); while( undefined !== (chunk = await callback()) ){ if(chunk instanceof ArrayBuffer) chunk = new Uint8Array(chunk); if( 0===nWrote && chunk.byteLength>=15 ){ @@ -1200,10 +1200,12 @@ const installOpfsVfs = function callee(options){ sah.write(new Uint8Array(2), {at: 18}/*force db out of WAL mode*/); return nWrote; }catch(e){ + await sah.close(); + sah = undefined; await hDir.removeEntry( fnamePart ).catch(()=>{}); throw e; }finally { - await sah.close(); + if( sah ) await sah.close(); } }; @@ -1256,6 +1258,7 @@ const installOpfsVfs = function callee(options){ sah.write(new Uint8Array(2), {at: 18}) /* force db out of WAL mode */; return nWrote; }catch(e){ + if( sah ){ await sah.close(); sah = undefined; } await hDir.removeEntry( fnamePart ).catch(()=>{}); throw e; }finally{ diff --git a/manifest b/manifest index a96201b44c..25437bb3be 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Extend\sthe\simportDb()\smethod\sof\sboth\sOPFS\sVFSes\sto\s(A)\ssupport\sreading\sin\san\sasync\sstreaming\sfashion\svia\sa\scallback\sand\s(B)\sautomatically\sdisable\sWAL\smode\sin\sthe\simported\sdb. -D 2023-08-18T14:16:26.669 +C Improve\sexception\shandling\sin\sOpfsDb.importDb(). +D 2023-08-18T14:41:21.668 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -549,7 +549,7 @@ F ext/wasm/api/sqlite3-license-version-header.js 0c807a421f0187e778dc1078f10d299 F ext/wasm/api/sqlite3-opfs-async-proxy.js 8cf8a897726f14071fae6be6648125162b256dfb4f96555b865dbb7a6b65e379 F ext/wasm/api/sqlite3-v-helper.js 7daa0eab0a513a25b05e9abae7b5beaaa39209b3ed12f86aeae9ef8d2719ed25 F ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js 561463ac5380e4ccf1839a1922e6d7a5585660f32e3b9701a270b78cd35566cf -F ext/wasm/api/sqlite3-vfs-opfs.c-pp.js 656952a75c36d96e3248b03ae26d6a7f8d6ff31e66432c63e1c0bb021f1234ab +F ext/wasm/api/sqlite3-vfs-opfs.c-pp.js d9e62d42b86f7bb3143eb071628b24e2ba7dcc749e41a0e9d3e2451bfea1a6b6 F ext/wasm/api/sqlite3-wasm.c d4d4c2b349b43b7b861e6d2994299630fb79e07573ea6b61e28e8071b7d16b61 F ext/wasm/api/sqlite3-worker1-promiser.c-pp.js bc06df0d599e625bde6a10a394e326dc68da9ff07fa5404354580f81566e591f F ext/wasm/api/sqlite3-worker1.c-pp.js da509469755035e919c015deea41b4514b5e84c12a1332e6cc8d42cb2cc1fb75 @@ -2091,8 +2091,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P 653ed92dc39185cdedfab3ea518bc7ec2d2826120e5fa4cbdee3343301396184 -R 6b40a7c1cd4b822a33b7a897821b1648 +P 9b1398c96a4fd0b59e65faa8d5c98de4129f0f0357732f12cb2f5c53a08acdc2 +R db128ff4a61eed058302d85bb5b8cb1c U stephan -Z b0064c5da7f02d0dcda913e568fa1924 +Z 10bacbfa41c608cf282110ffda9bd8fa # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index ff8af35694..c605058805 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -9b1398c96a4fd0b59e65faa8d5c98de4129f0f0357732f12cb2f5c53a08acdc2 \ No newline at end of file +a4eedd63b58f5c273b671f687cb68a32259963dbb053a710770383c735fb7f26 \ No newline at end of file From 6043947eee46fdbd63bfede74bc969ad6ea22c30 Mon Sep 17 00:00:00 2001 From: stephan Date: Thu, 24 Aug 2023 14:17:30 +0000 Subject: [PATCH 6/6] Ensure that db mutex is held when using sqlite3ErrorWithMessage() to avoid a potential assertion in debug builds. FossilOrigin-Name: b8f6a50a4bf9478324f0272d79f2fe6992a49b753e79e39a268c6afb261bb01e --- ext/wasm/api/sqlite3-wasm.c | 2 ++ manifest | 12 ++++++------ manifest.uuid | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ext/wasm/api/sqlite3-wasm.c b/ext/wasm/api/sqlite3-wasm.c index fcfbc06929..ff15e3b4fb 100644 --- a/ext/wasm/api/sqlite3-wasm.c +++ b/ext/wasm/api/sqlite3-wasm.c @@ -352,7 +352,9 @@ int sqlite3_wasm_db_error(sqlite3*db, int err_code, const char *zMsg){ if( db!=0 ){ if( 0!=zMsg ){ const int nMsg = sqlite3Strlen30(zMsg); + sqlite3_mutex_enter(sqlite3_db_mutex(db)); sqlite3ErrorWithMsg(db, err_code, "%.*s", nMsg, zMsg); + sqlite3_mutex_leave(sqlite3_db_mutex(db)); }else{ sqlite3ErrorWithMsg(db, err_code, NULL); } diff --git a/manifest b/manifest index 25437bb3be..539b84375f 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Improve\sexception\shandling\sin\sOpfsDb.importDb(). -D 2023-08-18T14:41:21.668 +C Ensure\sthat\sdb\smutex\sis\sheld\swhen\susing\ssqlite3ErrorWithMessage()\sto\savoid\sa\spotential\sassertion\sin\sdebug\sbuilds. +D 2023-08-24T14:17:30.272 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -550,7 +550,7 @@ F ext/wasm/api/sqlite3-opfs-async-proxy.js 8cf8a897726f14071fae6be6648125162b256 F ext/wasm/api/sqlite3-v-helper.js 7daa0eab0a513a25b05e9abae7b5beaaa39209b3ed12f86aeae9ef8d2719ed25 F ext/wasm/api/sqlite3-vfs-opfs-sahpool.c-pp.js 561463ac5380e4ccf1839a1922e6d7a5585660f32e3b9701a270b78cd35566cf F ext/wasm/api/sqlite3-vfs-opfs.c-pp.js d9e62d42b86f7bb3143eb071628b24e2ba7dcc749e41a0e9d3e2451bfea1a6b6 -F ext/wasm/api/sqlite3-wasm.c d4d4c2b349b43b7b861e6d2994299630fb79e07573ea6b61e28e8071b7d16b61 +F ext/wasm/api/sqlite3-wasm.c 6773e949034369ddd2a1efdedc39b2808a10b7274b0769188905432e561feebe F ext/wasm/api/sqlite3-worker1-promiser.c-pp.js bc06df0d599e625bde6a10a394e326dc68da9ff07fa5404354580f81566e591f F ext/wasm/api/sqlite3-worker1.c-pp.js da509469755035e919c015deea41b4514b5e84c12a1332e6cc8d42cb2cc1fb75 F ext/wasm/batch-runner.html 4deeed44fe41496dc6898d9fb17938ea3291f40f4bfb977e29d0cef96fbbe4c8 @@ -2091,8 +2091,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P 9b1398c96a4fd0b59e65faa8d5c98de4129f0f0357732f12cb2f5c53a08acdc2 -R db128ff4a61eed058302d85bb5b8cb1c +P a4eedd63b58f5c273b671f687cb68a32259963dbb053a710770383c735fb7f26 +R a8ed4c07ea7e64b510f57a0d91378964 U stephan -Z 10bacbfa41c608cf282110ffda9bd8fa +Z 37b33fa5d7dd5ab7079270bdd3d45073 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index c605058805..73fc39b325 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -a4eedd63b58f5c273b671f687cb68a32259963dbb053a710770383c735fb7f26 \ No newline at end of file +b8f6a50a4bf9478324f0272d79f2fe6992a49b753e79e39a268c6afb261bb01e \ No newline at end of file