diff --git a/ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api b/ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api index b31f613576..416b08d23f 100644 --- a/ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api +++ b/ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api @@ -52,6 +52,7 @@ _sqlite3_open _sqlite3_open_v2 _sqlite3_prepare_v2 _sqlite3_prepare_v3 +_sqlite3_randomness _sqlite3_realloc _sqlite3_realloc64 _sqlite3_reset diff --git a/ext/wasm/api/sqlite3-api-glue.js b/ext/wasm/api/sqlite3-api-glue.js index 639e670792..d096af1c89 100644 --- a/ext/wasm/api/sqlite3-api-glue.js +++ b/ext/wasm/api/sqlite3-api-glue.js @@ -45,7 +45,7 @@ self.sqlite3ApiBootstrap.initializers.push(function(sqlite3){ if(v && v.constructor && v instanceof StructBinder.StructType){ v = v.pointer; } - return (v === (v | 0) /* v is a 32-bit integer */) + return wasm.isPtr(v) ? argPointer(v) : toss("Invalid (object) type for StructType-type argument."); }); diff --git a/ext/wasm/api/sqlite3-api-prologue.js b/ext/wasm/api/sqlite3-api-prologue.js index 1fcc9a64ab..71058d174e 100644 --- a/ext/wasm/api/sqlite3-api-prologue.js +++ b/ext/wasm/api/sqlite3-api-prologue.js @@ -215,6 +215,32 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( return (v && v.constructor && isInt32(v.constructor.BYTES_PER_ELEMENT)) ? v : false; }; + + /** Internal helper to use in operations which need to distinguish + between TypedArrays which are backed by a SharedArrayBuffer + from those which are not. */ + const __SAB = ('undefined'===typeof SharedArrayBuffer) + ? function(){} : SharedArrayBuffer; + /** Returns true if the given TypedArray object is backed by a + SharedArrayBuffer, else false. */ + const isSharedTypedArray = (aTypedArray)=>(aTypedArray.buffer instanceof __SAB); + + /** + Returns either aTypedArray.slice(begin,end) (if + aTypedArray.buffer is a SharedArrayBuffer) or + aTypedArray.subarray(begin,end) (if it's not). + + This distinction is important for APIs which don't like to + work on SABs, e.g. TextDecoder, and possibly for our + own APIs which work on memory ranges which "might" be + modified by other threads while it's working. + */ + const typedArrayPart = (aTypedArray, begin, end)=>{ + return isSharedTypedArray(aTypedArray) + ? aTypedArray.slice(begin, end) + : aTypedArray.subarray(begin, end); + }; + /** Returns true if v appears to be one of our bind()-able TypedArray types: Uint8Array or Int8Array. Support for @@ -246,16 +272,16 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( const utf8Decoder = new TextDecoder('utf-8'); - /** Internal helper to use in operations which need to distinguish - between SharedArrayBuffer heap memory and non-shared heap. */ - const __SAB = ('undefined'===typeof SharedArrayBuffer) - ? function(){} : SharedArrayBuffer; - const typedArrayToString = function(arrayBuffer, begin, end){ - return utf8Decoder.decode( - (arrayBuffer.buffer instanceof __SAB) - ? arrayBuffer.slice(begin, end) - : arrayBuffer.subarray(begin, end) - ); + /** + Uses TextDecoder to decode the given half-open range of the + given TypedArray to a string. This differs from a simple + call to TextDecoder in that it accounts for whether the + first argument is based by a SharedArrayBuffer or not, + and can work more efficiently if it's not (TextDecoder + refuses to act upon an SAB). + */ + const typedArrayToString = function(typedArray, begin, end){ + return utf8Decoder.decode(typedArrayPart(typedArray, begin,end)); }; /** @@ -502,11 +528,11 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( If the callback is a function, then for the duration of the sqlite3_exec() call, it installs a WASM-bound function which - acts as a proxy for the given callback. That proxy will - also perform a conversion of the callback's arguments from + acts as a proxy for the given callback. That proxy will also + perform a conversion of the callback's arguments from `(char**)` to JS arrays of strings. However, for API - consistency's sake it will still honor the C-level - callback parameter order and will call it like: + consistency's sake it will still honor the C-level callback + parameter order and will call it like: `callback(pVoid, colCount, listOfValues, listOfColNames)` @@ -517,12 +543,27 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( */ sqlite3_exec: (pDb, sql, callback, pVoid, pErrMsg)=>{}/*installed later*/, + /** + If passed a single argument which appears to be a byte-oriented + TypedArray (Int8Array or Uint8Array), this function treats that + TypedArray as an output target, fetches `theArray.byteLength` + bytes of randomness, and populates the whole array with it. As + a special case, if the array's length is 0, this function + behaves as if it were passed (0,0). When called this way, it + returns its argument, else it returns the `undefined` value. + + If called with any other arguments, they are passed on as-is + to the C API. Results are undefined if passed any incompatible + values. + */ + sqlite3_randomness: (n, outPtr)=>{/*installed later*/}, + /** Various internal-use utilities are added here as needed. They are bound to an object only so that we have access to them in the differently-scoped steps of the API bootstrapping process. At the end of the API setup process, this object gets - removed. + removed. These are NOT part of the public API. */ util:{ affirmBindableTypedArray, flexibleString, @@ -530,7 +571,9 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( isBindableTypedArray, isInt32, isSQLableTypedArray, isTypedArray, typedArrayToString, - isUIThread: ()=>'undefined'===typeof WorkerGlobalScope + isUIThread: ()=>'undefined'===typeof WorkerGlobalScope, + isSharedTypedArray, + typedArrayPart }, /** @@ -617,7 +660,7 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( }/*wasm*/ }/*capi*/; - const wasm = capi.wasm; + const wasm = capi.wasm, util = capi.util; /** wasm.alloc()'s srcTypedArray.byteLength bytes, @@ -743,8 +786,8 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( // Please keep these sorted by function name! ["sqlite3_aggregate_context","void*", "sqlite3_context*", "int"], ["sqlite3_bind_blob","int", "sqlite3_stmt*", "int", "*", "int", "*" - /* We should arguably write a custom wrapper which knows how - to handle Blob, TypedArrays, and JS strings. */ + /* TODO: we should arguably write a custom wrapper which knows + how to handle Blob, TypedArrays, and JS strings. */ ], ["sqlite3_bind_double","int", "sqlite3_stmt*", "int", "f64"], ["sqlite3_bind_int","int", "sqlite3_stmt*", "int", "int"], @@ -752,10 +795,10 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( ["sqlite3_bind_parameter_count", "int", "sqlite3_stmt*"], ["sqlite3_bind_parameter_index","int", "sqlite3_stmt*", "string"], ["sqlite3_bind_text","int", "sqlite3_stmt*", "int", "string", "int", "int" - /* We should arguably create a hand-written binding - which does more flexible text conversion, along the lines of - sqlite3_prepare_v3(). The slightly problematic part is the - final argument (text destructor). */ + /* We should arguably create a hand-written binding of + bind_text() which does more flexible text conversion, along + the lines of sqlite3_prepare_v3(). The slightly problematic + part is the final argument (text destructor). */ ], ["sqlite3_close_v2", "int", "sqlite3*"], ["sqlite3_changes", "int", "sqlite3*"], @@ -770,15 +813,16 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( ["sqlite3_column_type","int", "sqlite3_stmt*", "int"], ["sqlite3_compileoption_get", "string", "int"], ["sqlite3_compileoption_used", "int", "string"], - /* sqlite3_create_function_v2() is handled separate to simplify conversion - of its callback argument */ + /* sqlite3_create_function(), sqlite3_create_function_v2(), and + sqlite3_create_window_function() use hand-written bindings to + simplify handling of their function-type arguments. */ ["sqlite3_data_count", "int", "sqlite3_stmt*"], ["sqlite3_db_filename", "string", "sqlite3*", "string"], ["sqlite3_db_handle", "sqlite3*", "sqlite3_stmt*"], ["sqlite3_db_name", "string", "sqlite3*", "int"], ["sqlite3_deserialize", "int", "sqlite3*", "string", "*", "i64", "i64", "int"] /* Careful! Short version: de/serialize() are problematic because they - might use a different allocator that the user for managing the + might use a different allocator than the user for managing the deserialized block. de/serialize() are ONLY safe to use with sqlite3_malloc(), sqlite3_free(), and its 64-bit variants. */, ["sqlite3_errmsg", "string", "sqlite3*"], @@ -806,6 +850,8 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( /* sqlite3_prepare_v2() and sqlite3_prepare_v3() are handled separately due to us requiring two different sets of semantics for those, depending on how their SQL argument is provided. */ + /* sqlite3_randomness() uses a hand-written wrapper to extend + the range of supported argument types. */ ["sqlite3_realloc", "*","*","int"], ["sqlite3_reset", "int", "sqlite3_stmt*"], ["sqlite3_result_blob",undefined, "*", "*", "int", "*"], @@ -1052,8 +1098,41 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( throw new SQLite3Error(...args); }; + capi.sqlite3_randomness = (...args)=>{ + if(1===args.length && util.isTypedArray(args[0]) + && 1===args[0].BYTES_PER_ELEMENT){ + const ta = args[0]; + if(0===ta.byteLength){ + wasm.exports.sqlite3_randomness(0,0); + return ta; + } + const stack = wasm.pstack.pointer; + try { + let n = ta.byteLength, offset = 0; + const r = wasm.exports.sqlite3_randomness; + const heap = wasm.heap8u(); + const nAlloc = n < 512 ? n : 512; + const ptr = wasm.pstack.alloc(nAlloc); + do{ + const j = (n>nAlloc ? nAlloc : n); + r(j, ptr); + ta.set(typedArrayPart(heap, ptr, ptr+j), offset); + n -= j; + offset += j; + } while(n > 0); + }catch(e){ + console.error("Highly unexpected (and ignored!) "+ + "exception in sqlite3_randomness():",e); + }finally{ + wasm.pstack.restore(stack); + } + return ta; + } + capi.wasm.exports.sqlite3_randomness(...args); + }; + /** State for sqlite3_wasmfs_opfs_dir(). */ - let __persistentDir = undefined; + let __wasmfsOpfsDir = undefined; /** If the wasm environment has a WASMFS/OPFS-backed persistent storage directory, its path is returned by this function. If it @@ -1068,26 +1147,26 @@ self.sqlite3ApiBootstrap = function sqlite3ApiBootstrap( Emscripten-managed virtual filesystem. */ capi.sqlite3_wasmfs_opfs_dir = function(){ - if(undefined !== __persistentDir) return __persistentDir; + if(undefined !== __wasmfsOpfsDir) return __wasmfsOpfsDir; // If we have no OPFS, there is no persistent dir const pdir = config.wasmfsOpfsDir; if(!pdir || !self.FileSystemHandle || !self.FileSystemDirectoryHandle || !self.FileSystemFileHandle){ - return __persistentDir = ""; + return __wasmfsOpfsDir = ""; } try{ if(pdir && 0===wasm.xCallWrapped( 'sqlite3_wasm_init_wasmfs', 'i32', ['string'], pdir )){ - return __persistentDir = pdir; + return __wasmfsOpfsDir = pdir; }else{ - return __persistentDir = ""; + return __wasmfsOpfsDir = ""; } }catch(e){ // sqlite3_wasm_init_wasmfs() is not available - return __persistentDir = ""; + return __wasmfsOpfsDir = ""; } }; diff --git a/ext/wasm/common/whwasmutil.js b/ext/wasm/common/whwasmutil.js index e04886de8a..24c67ef78f 100644 --- a/ext/wasm/common/whwasmutil.js +++ b/ext/wasm/common/whwasmutil.js @@ -704,6 +704,23 @@ self.WhWasmUtilInstaller = function(target){ pointer-to-pointer values. */ target.setPtrValue = (ptr, value)=>target.setMemValue(ptr, value, ptrIR); + /** + Returns true if the given value appears to be legal for use as + a WASM pointer value. Its _range_ of values is not (cannot be) + validated except to ensure that it is a 32-bit integer with a + value of 0 or greater. Likewise, it cannot verify whether the + value actually refers to allocated memory in the WASM heap. + */ + target.isPtr32 = (ptr)=>('number'===typeof ptr && (ptr===(ptr|0)) && ptr>=0); + + /** + isPtr() is an alias for isPtr32(). If/when 64-bit WASM pointer + support becomes widespread, it will become an alias for either + isPtr32() or the as-yet-hypothetical isPtr64(), depending on a + configuration option. + */ + target.isPtr = target.isPtr32; + /** Expects ptr to be a pointer into the WASM heap memory which refers to a NUL-terminated C-style string encoded as UTF-8. @@ -1229,7 +1246,8 @@ self.WhWasmUtilInstaller = function(target){ xcv.result['*'] = xcv.result['pointer'] = xcv.arg['**'] = xcv.arg[ptrIR]; xcv.result['number'] = (v)=>Number(v); - { + { /* Copy certain xcv.arg[...] handlers to xcv.result[...] and + add pointer-style variants of them. */ const copyToResult = ['i8', 'i16', 'i32', 'int', 'f32', 'float', 'f64', 'double']; if(target.bigIntEnabled) copyToResult.push('i64'); diff --git a/ext/wasm/tester1.js b/ext/wasm/tester1.js index 4a6074d91b..d3f1c44fc5 100644 --- a/ext/wasm/tester1.js +++ b/ext/wasm/tester1.js @@ -377,6 +377,19 @@ } } + // isPtr32() + { + const ip = w.isPtr32; + T.assert(ip(0)) + .assert(!ip(-1)) + .assert(!ip(1.1)) + .assert(!ip(0xffffffff)) + .assert(ip(0x7fffffff)) + .assert(!ip()) + .assert(!ip(null)/*might change: under consideration*/) + ; + } + //log("jstrlen()..."); { T.assert(3 === w.jstrlen("abc")).assert(4 === w.jstrlen("äbc")); @@ -1056,6 +1069,49 @@ //////////////////////////////////////////////////////////////////// ;/*end of C/WASM utils checks*/ + T.g('sqlite3_randomness()') + .t('To memory buffer', function(sqlite3){ + const stack = wasm.pstack.pointer; + try{ + const n = 520; + const p = wasm.pstack.alloc(n); + T.assert(0===wasm.getMemValue(p)) + .assert(0===wasm.getMemValue(p+n-1)); + T.assert(undefined === capi.sqlite3_randomness(n - 10, p)); + let j, check = 0; + const heap = wasm.heap8u(); + for(j = 0; j < 10 && 0===check; ++j){ + check += heap[p + j]; + } + T.assert(check > 0); + check = 0; + // Ensure that the trailing bytes were not modified... + for(j = n - 10; j < n && 0===check; ++j){ + check += heap[p + j]; + } + T.assert(0===check); + }finally{ + wasm.pstack.restore(stack); + } + }) + .t('To byte array', function(sqlite3){ + const ta = new Uint8Array(117); + let i, n = 0; + for(i=0; i0); + const t0 = new Uint8Array(0); + T.assert(t0 === capi.sqlite3_randomness(t0), + "0-length array is a special case"); + }) + ;;/*end sqlite3_randomness() checks*/ + //////////////////////////////////////////////////////////////////////// T.g('sqlite3.oo1') .t('Create db', function(sqlite3){ diff --git a/manifest b/manifest index 1ba4ebfcf9..4c7e6bd142 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Disable\sthe\spush-down\soptimization\sfor\ssub-queries\sthat\sare\sINTERSECT,\sUNION\sor\sEXCEPT\scompounds.\sdbsqlfuzz\sa34f455c91ad75a0cf8cd9476841903f42930a7a. -D 2022-10-26T21:14:21.853 +C Expose\ssqlite3_randomness()\sto\sWASM\sand\sadd\sa\scustom\sbinding\sfor\sit\swhich\scan\spopulate\sa\sJS\sbyte\sarray.\sAdd\sWhWasmUtil.isPtr(). +D 2022-10-27T03:03:16.460 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -490,7 +490,7 @@ F ext/wasm/EXPORTED_FUNCTIONS.fiddle.in 27450c8b8c70875a260aca55435ec927068b34ce F ext/wasm/GNUmakefile 7a8c06f9bdbb791f8ef084ecd47e099da81e5797b9b1d60e33ac9a07eedd5dbd F ext/wasm/README-dist.txt 2d670b426fc7c613b90a7d2f2b05b433088fe65181abead970980f0a4a75ea20 F ext/wasm/README.md 1e5b28158b74ab3ffc9d54fcbc020f0bbeb82c2ff8bbd904214c86c70e8a3066 -F ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api 36f413ab4dbb057d2dec938fb366ac0a4c5e85ba14660a8d672f0277602c0fc5 +F ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api cfbe8efcb9d1444139d6c381eb54dfdd5e62cf1ddfe10ed8a38617149a664b9b F ext/wasm/api/EXPORTED_RUNTIME_METHODS.sqlite3-api 1ec3c73e7d66e95529c3c64ac3de2470b0e9e7fbf7a5b41261c367cf4f1b7287 F ext/wasm/api/README.md 1350088aee90e959ad9a94fab1bb6bcb5e99d4d27f976db389050f54f2640c78 F ext/wasm/api/extern-post-js.js 926d192b72fa808378e5e7843721dc7ba3908c163a0260e06d8aa501c12f5469 @@ -499,10 +499,10 @@ F ext/wasm/api/post-js-footer.js cd0a8ec768501d9bd45d325ab0442037fb0e33d1f3b4f08 F ext/wasm/api/post-js-header.js 2e5c886398013ba2af88028ecbced1e4b22dc96a86467f1ecc5ba9e64ef90a8b F ext/wasm/api/pre-js.js 151e0616614a49f3db19ed544fa13b38c87c108959fbcd4029ea8399a562d94f F ext/wasm/api/sqlite3-api-cleanup.js 4d07a7524dc9b7b050acfde57163e839243ad2383bd7ee0de0178b1b3e988588 -F ext/wasm/api/sqlite3-api-glue.js 6e4e472eb5afc732a695cd7c5ded6dee6ef8b480e61aa0d648a3fc9033c84745 +F ext/wasm/api/sqlite3-api-glue.js f024dc2f41418ad203edf1228d7cf7934249c11ffcbb65d21f9bb69333d63d55 F ext/wasm/api/sqlite3-api-oo1.js 38004e18001396c078124769e14737a0ff703f98317279734020121af72efdd5 F ext/wasm/api/sqlite3-api-opfs.js 62da8b7cac30d4e7bb940762d2ac948b0aeb89704a5a290b74eb268ecbd1a64e -F ext/wasm/api/sqlite3-api-prologue.js fa00d55f927e5a4ec51cf2c80f6f0eaed2f4f5774341ecf3d63a0ea4c738f8f5 +F ext/wasm/api/sqlite3-api-prologue.js 4706ca7fa125426019b8dd01f9e6a774021a2781874df8b9f435c69544383e79 F ext/wasm/api/sqlite3-api-worker1.js b2d650514ccc75f80dff666fd3ee68dc8fb4137bcd01caac2c62ff93a7ebf638 F ext/wasm/api/sqlite3-license-version-header.js a661182fc93fc2cf212dfd0b987f8e138a3ac98f850b1112e29b5fbdaecc87c3 F ext/wasm/api/sqlite3-opfs-async-proxy.js f04cb1eb483c92bc61fe02749f7afcf17ec803968171aedd7d96faf428c26bcb @@ -515,7 +515,7 @@ F ext/wasm/batch-runner.js 5bae81684728b6be157d1f92b39824153f0fd019345b39f2ab893 F ext/wasm/common/SqliteTestUtil.js 647bf014bd30bdd870a7e9001e251d12fc1c9ec9ce176a1004b838a4b33c5c05 F ext/wasm/common/emscripten.css 3d253a6fdb8983a2ac983855bfbdd4b6fa1ff267c28d69513dd6ef1f289ada3f F ext/wasm/common/testing.css 739b58c44511f642f16f57b701c84dc9ee412d8bc47b3d8a99d947babfa69d9d -F ext/wasm/common/whwasmutil.js 50d2ede0b0fa01c1d467e1801fab79f5e46bb02bcbd2b0232e4fdc6090a47818 +F ext/wasm/common/whwasmutil.js 77930367c2a65cf6fd6f99ad3644ede33e4d20466f5e506eb87b8d101a0a7655 F ext/wasm/demo-123-worker.html a0b58d9caef098a626a1a1db567076fca4245e8d60ba94557ede8684350a81ed F ext/wasm/demo-123.html 8c70a412ce386bd3796534257935eb1e3ea5c581e5d5aea0490b8232e570a508 F ext/wasm/demo-123.js ebae30756585bca655b4ab2553ec9236a87c23ad24fc8652115dcedb06d28df6 @@ -548,7 +548,7 @@ F ext/wasm/test-opfs-vfs.html 1f2d672f3f3fce810dfd48a8d56914aba22e45c6834e262555 F ext/wasm/test-opfs-vfs.js 48fc59110e8775bb43c9be25b6d634fc07ebadab7da8fbd44889e8129c6e2548 F ext/wasm/tester1-worker.html d02b9d38876b023854cf8955e77a40912f7e516956b4dbe1ec7f215faac273ee F ext/wasm/tester1.html c6c47e5a8071eb09cb1301104435c8e44fbb5719c92411f5b2384a461f9793c5 -F ext/wasm/tester1.js 21dad63165954a8a28dfa8eeab68a4881eb1d9b5c6bc9f2c83aa3ceea8c41fee +F ext/wasm/tester1.js 39dd4277944f79f475d78850ebbf48b0fe61b2caf4df2b5c5db6219f509d5c96 F ext/wasm/version-info.c 3b36468a90faf1bbd59c65fd0eb66522d9f941eedd364fabccd72273503ae7d5 F ext/wasm/wasmfs.make ee0004813e16c283ff633e08b482008d56adf9b7d42f6c5612f7ab002b924f69 F install-sh 9d4de14ab9fb0facae2f48780b874848cbf2f895 x @@ -2052,8 +2052,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 a029dddff4f4ed7275538610cbd9cea658b905b72924860ec9cda9e76dabcfac -R b53abb7e437a17a51c11b8e565290cc0 -U dan -Z f234279171f2df9582a4a9e673101d04 +P 346a3b12b861ce7ba369e98cd336f79a1d4f7a7bb9acd7a4f63f37b391755bf5 +R 5867ff413088cae7742d2b15dc451b3e +U stephan +Z c456212142f768a2abd7eb0069fb1a57 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 3ad43b42ae..db063108c1 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -346a3b12b861ce7ba369e98cd336f79a1d4f7a7bb9acd7a4f63f37b391755bf5 \ No newline at end of file +333e67076b4bc967bb543ef8e265c63f6e3498c38ac121a7d1eff4a1d7a71c63 \ No newline at end of file