From: stephan Date: Sat, 25 Jun 2022 10:30:24 +0000 (+0000) Subject: wasm: added utility C code to generate a JSON-format "enum" of the numerous SQLITE_xy... X-Git-Tag: version-3.40.0~246^2~8 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=fa17e10592371a28182edbabae3a7a9b748fcca5;p=thirdparty%2Fsqlite.git wasm: added utility C code to generate a JSON-format "enum" of the numerous SQLITE_xyz constants so that we do not risk those getting out of sync in the JS code. Renamed initSqlite3Module to sqlite3InitModule. Cleanups in the TypedArray handling. FossilOrigin-Name: 778062e3b415dca5104eee398950741b6dbb9d4bdf7c998eef18371a42669946 --- diff --git a/Makefile.in b/Makefile.in index da75b76a59..2e0768b829 100644 --- a/Makefile.in +++ b/Makefile.in @@ -1543,16 +1543,16 @@ $(fiddle_module_js): Makefile sqlite3.c shell.c \ sqlite3.c shell.c gzip < $@ > $@.gz gzip < $(fiddle_dir)/fiddle-module.wasm > $(fiddle_dir)/fiddle-module.wasm.gz -$(sqlite3_wasm_js): Makefile sqlite3.c \ +$(sqlite3_wasm_js): Makefile sqlite3.c $(fiddle_dir)/wasm_util.c \ $(fiddle_dir)/sqlite3-api.js \ $(fiddle_dir)/EXPORTED_RUNTIME_METHODS \ $(fiddle_dir)/EXPORTED_FUNCTIONS.sqlite3-api emcc -o $@ $(emcc_flags) \ - -sEXPORT_NAME=initSqlite3Module \ + -sEXPORT_NAME=sqlite3InitModule \ -sEXPORTED_FUNCTIONS=@$(fiddle_dir_abs)/EXPORTED_FUNCTIONS.sqlite3-api \ --post-js=$(fiddle_dir)/sqlite3-api.js \ --no-entry \ - sqlite3.c + sqlite3.c $(fiddle_dir)/wasm_util.c gzip < $@ > $@.gz gzip < $(sqlite3_wasm) > $(sqlite3_wasm).gz gzip < $(fiddle_dir)/sqlite3-api.js > $(fiddle_dir)/sqlite3-api.js.gz diff --git a/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3-api b/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3-api index 3127b294d9..59dfef45b6 100644 --- a/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3-api +++ b/ext/fiddle/EXPORTED_FUNCTIONS.sqlite3-api @@ -48,4 +48,5 @@ _sqlite3_value_bytes _sqlite3_value_double _sqlite3_value_text _sqlite3_value_type +_sqlite3_wasm_enum_json _free diff --git a/ext/fiddle/SqliteTestUtil.js b/ext/fiddle/SqliteTestUtil.js index cf78946dc7..bcbdc59c51 100644 --- a/ext/fiddle/SqliteTestUtil.js +++ b/ext/fiddle/SqliteTestUtil.js @@ -90,7 +90,7 @@ /** This is a module object for use with the emscripten-installed - initSqlite3Module() factory function. + sqlite3InitModule() factory function. */ self.sqlite3TestModule = { postRun: [ diff --git a/ext/fiddle/sqlite3-api.js b/ext/fiddle/sqlite3-api.js index 450bbb62ca..d4a5993ba5 100644 --- a/ext/fiddle/sqlite3-api.js +++ b/ext/fiddle/sqlite3-api.js @@ -13,11 +13,11 @@ This file is intended to be appended to the emcc-generated sqlite3.js via emcc: - emcc ... -sMODULARIZE -sEXPORT_NAME=initSqlite3Module --post-js=THIS_FILE + emcc ... -sMODULARIZE -sEXPORT_NAME=sqlite3InitModule --post-js=THIS_FILE It is loaded by importing the emcc-generated sqlite3.js, then: - initSqlite3Module({module object}).then( + sqlite3InitModule({module object}).then( function(theModule){ theModule.sqlite3 == an object containing this file's deliverables: @@ -29,7 +29,7 @@ It is up to the caller to provide a module object compatible with emcc, but it can be a plain empty object. The object passed to - initSqlite3Module() will get populated by the emscripten-generated + sqlite3InitModule() will get populated by the emscripten-generated bits and, in part, by the code from this file. Specifically, this file installs the `theModule.sqlite3` part shown above. @@ -151,49 +151,6 @@ Module.postRun.push(function(namespace/*the module object, the target for follows is strongly influenced by the sql.js implementation. */ const api = { - /* It is important that the following integer values match - those from the C code. Ideally we could fetch them from the - C API, e.g., in the form of a JSON object, but getting that - JSON string constructed within our current confines is - currently not worth the effort. - - Reminder to self: we could probably do so by adding the - proverbial level of indirection, calling in to C to get it, - and having that C func call an - emscripten-installed/JS-implemented library function which - builds the result object: - - const obj = {}; - sqlite3__get_enum(function(key,val){ - obj[key] = val; - }); - - but whether or not we can pass a function that way, via a - (void*) is as yet unknown. - */ - /* Minimum subset of sqlite result codes we'll need. */ - SQLITE_OK: 0, - SQLITE_ROW: 100, - SQLITE_DONE: 101, - /* sqlite data types */ - SQLITE_INTEGER: 1, - SQLITE_FLOAT: 2, - SQLITE_TEXT: 3, - SQLITE_BLOB: 4, - SQLITE_NULL: 5, - /* create_function() flags */ - SQLITE_DETERMINISTIC: 0x000000800, - SQLITE_DIRECTONLY: 0x000080000, - SQLITE_INNOCUOUS: 0x000200000, - /* sqlite encodings, used for creating UDFs, noting that we - will only support UTF8. */ - 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 @@ -343,7 +300,18 @@ Module.postRun.push(function(namespace/*the module object, the target for "number", "number"]), }; - const uint8ToString = (str)=>new TextDecoder('utf-8').decode(str); + /* Import C-level constants... */ + //console.log("wasmEnum=",SQM.ccall('sqlite3_wasm_enum_json', 'string', [])); + const wasmEnum = JSON.parse(SQM.ccall('sqlite3_wasm_enum_json', 'string', [])); + ['resultCodes','dataTypes','udfFlags', + 'encodings','blobFinalizers'].forEach(function(t){ + Object.keys(wasmEnum[t]).forEach(function(k){ + api[k] = wasmEnum[t][k]; + }); + }); + + const utf8Decoder = new TextDecoder('utf-8'); + const typedArrayToString = (str)=>utf8Decoder.decode(str); //const stringToUint8 = (sql)=>new TextEncoder('utf-8').encode(sql); /** @@ -393,7 +361,7 @@ Module.postRun.push(function(namespace/*the module object, the target for parameters) requires using api.wasm.getValue(). */ api.sqlite3_prepare_v2 = function(pDb, sql, sqlLen, ppStmt, pzTail){ - if(sql instanceof Uint8Array) sql = uint8ToString(sql); + if(isSupportedTypedArray(sql)) sql = typedArrayToString(sql); switch(typeof sql){ case 'string': return prepareMethods.basic(pDb, sql, -1, ppStmt, null); case 'number': return prepareMethods.full(pDb, sql, -1, ppStmt, pzTail); @@ -556,7 +524,7 @@ Module.postRun.push(function(namespace/*the module object, the target for case 1: if('string'===typeof args[0]){ out.sql = args[0]; - }else if(args[0] instanceof Uint8Array){ + }else if(isSupportedTypedArray(args[0])){ out.sql = args[0]; }else if(args[0] && 'object'===typeof args[0]){ out.opt = args[0]; @@ -569,8 +537,8 @@ Module.postRun.push(function(namespace/*the module object, the target for break; default: toss("Invalid argument count for exec()."); }; - if(out.sql instanceof Uint8Array){ - out.sql = uint8ToString(out.sql); + if(isSupportedTypedArray(out.sql)){ + out.sql = typedArrayToString(out.sql); }else if(Array.isArray(out.sql)){ out.sql = out.sql.join(''); }else if('string'!==typeof out.sql){ @@ -829,8 +797,8 @@ Module.postRun.push(function(namespace/*the module object, the target for (opt.callback && opt.rowMode) ? opt.rowMode : false); try{ - const sql = (arg.sql instanceof Uint8Array) - ? uint8ToString(arg.sql) + const sql = isSupportedTypedArray(arg.sql) + ? typedArrayToString(arg.sql) : arg.sql; let pSql = api.wasm.allocateUTF8OnStack(sql) const ppStmt = api.wasm.stackAlloc(8) /* output (sqlite3_stmt**) arg */; @@ -1638,7 +1606,9 @@ Module.postRun.push(function(namespace/*the module object, the target for If passed no arguments then it returns an object mapping all known compilation options to their compile-time values, - or boolean true if they are defined with no value. + or boolean true if they are defined with no value. This + result, which is relatively expensive to compute, is cached + and returned for future no-argument calls. In all other cases it returns true if the given option was active when when compiling the sqlite3 module, else false. @@ -1649,7 +1619,8 @@ Module.postRun.push(function(namespace/*the module object, the target for */ compileOptionUsed: function f(optName){ if(!arguments.length){ - if(!f._opt){ + if(f._result) return f._result; + else if(!f._opt){ f._rx = /^([^=]+)=(.+)/; f._rxInt = /^-?\d+$/; f._opt = function(opt, rv){ @@ -1664,16 +1635,14 @@ Module.postRun.push(function(namespace/*the module object, the target for f._opt(k,ov); rc[ov[0]] = ov[1]; } - return rc; - } - else if(Array.isArray(optName)){ + return f._result = rc; + }else if(Array.isArray(optName)){ const rc = {}; optName.forEach((v)=>{ rc[v] = api.sqlite3_compileoption_used(v); }); return rc; - } - else if('object' === typeof optName){ + }else if('object' === typeof optName){ Object.keys(optName).forEach((k)=> { optName[k] = api.sqlite3_compileoption_used(k); }); diff --git a/ext/fiddle/sqlite3-worker.js b/ext/fiddle/sqlite3-worker.js index fe7423f4c4..ed369ad35b 100644 --- a/ext/fiddle/sqlite3-worker.js +++ b/ext/fiddle/sqlite3-worker.js @@ -20,7 +20,7 @@ loading sqlite3.js via a Worker. Loading sqlite3.js from the main window thread elides the Worker-specific API. Instantiating a worker with new Worker("sqlite.js") will not (cannot) call - initSqlite3Module() to initialize the module due to a + sqlite3InitModule() to initialize the module due to a timing/order-of-operations conflict (and that symbol is not exported in a way that a Worker loading it that way can see it). Thus JS code wanting to load the sqlite3 Worker-specific API needs to pass @@ -39,6 +39,6 @@ */ "use strict"; importScripts('sqlite3.js'); -initSqlite3Module().then(function(){ +sqlite3InitModule().then(function(){ setTimeout(()=>self.postMessage({type:'sqlite3-api',data:'ready'}), 0); }); diff --git a/ext/fiddle/testing1.js b/ext/fiddle/testing1.js index 9c17115f38..44e285f73a 100644 --- a/ext/fiddle/testing1.js +++ b/ext/fiddle/testing1.js @@ -201,6 +201,13 @@ api.sqlite3_sourceid()); log("Build options:",oo.compileOptionUsed()); log("api.wasm.HEAP8 size =",api.wasm.HEAP8.length); + log("wasmEnum",JSON.parse(Module.ccall('sqlite3_wasm_enum_json', 'string', []))); + [ /* Spot-check a handful of constants to make sure they got installed... */ + 'SQLITE_SCHEMA','SQLITE_NULL','SQLITE_UTF8', + 'SQLITE_STATIC', 'SQLITE_DIRECTONLY' + ].forEach(function(k){ + T.assert('number' === typeof api[k]); + }); const db = new oo.DB(); try { log("DB:",db.filename); @@ -215,14 +222,12 @@ db.close(); } log("Total Test count:",T.counter); - log("api.wasm.HEAP8 size =",api.wasm.HEAP8.length); }; - initSqlite3Module(self.sqlite3TestModule).then(function(theModule){ - /** Use a timeout so that we are (hopefully) out from - under the module init stack when our setup gets - run. Just on principle, not because we _need_ to - be. */ + sqlite3InitModule(self.sqlite3TestModule).then(function(theModule){ + /** Use a timeout so that we are (hopefully) out from under + the module init stack when our setup gets run. Just on + principle, not because we _need_ to be. */ //console.debug("theModule =",theModule); setTimeout(()=>runTests(theModule), 0); }); diff --git a/ext/fiddle/wasm_util.c b/ext/fiddle/wasm_util.c new file mode 100644 index 0000000000..54326b2977 --- /dev/null +++ b/ext/fiddle/wasm_util.c @@ -0,0 +1,108 @@ +#include "sqlite3.h" +#include /*atexit()*/ +/* +** 2022-06-25 +** +** The author disclaims copyright to this source code. In place of a +** legal notice, here is a blessing: +** +** * May you do good and not evil. +** * May you find forgiveness for yourself and forgive others. +** * May you share freely, never taking more than you give. +** +*********************************************************************** +** +** Utility functions for use with the emscripten/WASM bits. These +** functions ARE NOT part of the sqlite3 public API. They are strictly +** for internal use by the JS/WASM bindings. +*/ + +/** Result value of sqlite3_wasm_enum_json(). */ +static char * zWasmEnum = 0; +/* atexit() handler to clean up any WASM-related state. */ +static void sqlite3_wasm_cleanup(void){ + free(zWasmEnum); +} + +/* +** Returns a string containing a JSON-format "enum" of C-level +** constants intended to be imported into the JS environment. The JSON +** is initialized the first time this function is called and that +** result is reused for all future calls and cleaned up via atexit(). +** (If we didn't cache the result, it would be leaked by the JS glue +** code on each call during the WASM-to-JS conversion.) +** +** This function is NOT part of the sqlite3 public API. It is strictly +** for use by the JS/WASM bindings. +*/ +const char * sqlite3_wasm_enum_json(void){ + sqlite3_str * s; + if(zWasmEnum) return zWasmEnum; + s = sqlite3_str_new(0); + sqlite3_str_appendall(s, "{"); + +#define SD_(X,S,FINAL) \ + sqlite3_str_appendf(s, "\"%s\": %d%s", S, (int)X, (FINAL ? "}" : ", ")) +#define SD(X) SD_(X,#X,0) +#define SDFinal(X) SD_(X,#X,1) + + sqlite3_str_appendall(s,"\"resultCodes\": {"); + SD(SQLITE_OK); + SD(SQLITE_ERROR); + SD(SQLITE_INTERNAL); + SD(SQLITE_PERM); + SD(SQLITE_ABORT); + SD(SQLITE_BUSY); + SD(SQLITE_LOCKED); + SD(SQLITE_NOMEM); + SD(SQLITE_READONLY); + SD(SQLITE_INTERRUPT); + SD(SQLITE_IOERR); + SD(SQLITE_CORRUPT); + SD(SQLITE_NOTFOUND); + SD(SQLITE_FULL); + SD(SQLITE_CANTOPEN); + SD(SQLITE_PROTOCOL); + SD(SQLITE_EMPTY); + SD(SQLITE_SCHEMA); + SD(SQLITE_TOOBIG); + SD(SQLITE_CONSTRAINT); + SD(SQLITE_MISMATCH); + SD(SQLITE_MISUSE); + SD(SQLITE_NOLFS); + SD(SQLITE_AUTH); + SD(SQLITE_FORMAT); + SD(SQLITE_RANGE); + SD(SQLITE_NOTADB); + SD(SQLITE_NOTICE); + SD(SQLITE_WARNING); + SD(SQLITE_ROW); + SDFinal(SQLITE_DONE); + + sqlite3_str_appendall(s,",\"dataTypes\": {"); + SD(SQLITE_INTEGER); + SD(SQLITE_FLOAT); + SD(SQLITE_TEXT); + SD(SQLITE_BLOB); + SDFinal(SQLITE_NULL); + + sqlite3_str_appendf(s,",\"encodings\": {"); + SDFinal(SQLITE_UTF8); + + sqlite3_str_appendall(s,",\"blobFinalizers\": {"); + SD(SQLITE_STATIC); + SDFinal(SQLITE_TRANSIENT); + + sqlite3_str_appendall(s,",\"udfFlags\": {"); + SD(SQLITE_DETERMINISTIC); + SD(SQLITE_DIRECTONLY); + SDFinal(SQLITE_INNOCUOUS); + +#undef SD_ +#undef SD +#undef SDFinal + sqlite3_str_appendall(s, "}"); + zWasmEnum = sqlite3_str_finish(s); + atexit(sqlite3_wasm_cleanup); + return zWasmEnum; +} diff --git a/manifest b/manifest index 58086c0033..3697a203f1 100644 --- a/manifest +++ b/manifest @@ -1,9 +1,9 @@ -C wasm:\seliminated\sthe\sneed\sfor\sStmt\sobjects\sto\skeep\sahold\sof\smemory\sallocated\sfor\sbound\sstrings\sand\sblobs.\sAdded\salternate\sstring/blob\sbind\simpls\swhich\sare\shypothetically\smore\sefficient\sbut\snot\syet\sproven\sto\sbe\sso. -D 2022-06-25T07:42:24.832 +C wasm:\sadded\sutility\sC\scode\sto\sgenerate\sa\sJSON-format\s"enum"\sof\sthe\snumerous\sSQLITE_xyz\sconstants\sso\sthat\swe\sdo\snot\srisk\sthose\sgetting\sout\sof\ssync\sin\sthe\sJS\scode.\sRenamed\sinitSqlite3Module\sto\ssqlite3InitModule.\sCleanups\sin\sthe\sTypedArray\shandling. +D 2022-06-25T10:30:24.409 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 -F Makefile.in bccb0ed3f05fc41aee15da77c844c48b5da419cbb9af35b8a147536c9ad1c822 +F Makefile.in b3ccd1a79e6364d49c465b38cec5eccdc74dfdc06501be62ef8eb01dc1f93f43 F Makefile.linux-gcc f609543700659711fbd230eced1f01353117621dccae7b9fb70daa64236c5241 F Makefile.msc de7cb3e095ce2fdc33513ccd76ebdaeda1483d0ddab0410fe65cbdeadd4c0ee1 F README.md 8b8df9ca852aeac4864eb1e400002633ee6db84065bd01b78c33817f97d31f5e @@ -56,22 +56,23 @@ F ext/expert/sqlite3expert.c 6ca30d73b9ed75bd56d6e0d7f2c962d2affaa72c505458619d0 F ext/expert/sqlite3expert.h ca81efc2679a92373a13a3e76a6138d0310e32be53d6c3bfaedabd158ea8969b F ext/expert/test_expert.c d56c194b769bdc90cf829a14c9ecbc1edca9c850b837a4d0b13be14095c32a72 F ext/fiddle/EXPORTED_FUNCTIONS.fiddle 7fb73f7150ab79d83bb45a67d257553c905c78cd3d693101699243f36c5ae6c3 -F ext/fiddle/EXPORTED_FUNCTIONS.sqlite3-api 540b9dec63a3a62a256e2f030827848a92e9b9d9b6fa5c0188295a4a1c5382cd +F ext/fiddle/EXPORTED_FUNCTIONS.sqlite3-api 2ef8a8d58815a96de196d085b8aaf126400b8d13a5b7f724f86dfe9f61d0600d F ext/fiddle/EXPORTED_RUNTIME_METHODS a004bd5eeeda6d3b28d16779b7f1a80305bfe009dfc7f0721b042967f0d39d02 F ext/fiddle/Makefile e25d34a0e1324f771d64c09c592601b97219282011587e6ce410fa8acdedb913 -F ext/fiddle/SqliteTestUtil.js 559731c3e8e0de330ec7d292e6c1846566408caee6637acc8a119ac338a8781c +F ext/fiddle/SqliteTestUtil.js 2e87d424b12674476bdf8139934dcacc3ff8a7a5f5ff4392ba5e5a8d8cee9fbd F ext/fiddle/emscripten.css 3d253a6fdb8983a2ac983855bfbdd4b6fa1ff267c28d69513dd6ef1f289ada3f F ext/fiddle/fiddle-worker.js 88bc2193a6cb6a3f04d8911bed50a4401fe6f277de7a71ba833865ab64a1b4ae F ext/fiddle/fiddle.html 550c5aafce40bd218de9bf26192749f69f9b10bc379423ecd2e162bcef885c08 F ext/fiddle/fiddle.js 812f9954cc7c4b191884ad171f36fcf2d0112d0a7ecfdf6087896833a0c079a8 F ext/fiddle/index.md d9c1c308d8074341bc3b11d1d39073cd77754cb3ca9aeb949f23fdd8323d81cf -F ext/fiddle/sqlite3-api.js 664a454d82694c6cfab36b8ad9e36620cfc44fa37e05642ba87109c37409f51f -F ext/fiddle/sqlite3-worker.js a9c2b614beca187dbdd8c053ec2770cc61ec1ac9c0ec6398ceb49a79f705a421 +F ext/fiddle/sqlite3-api.js 03ac065f4bc68eefd3b09cf3957a58891cbfcebe2a5ffe1c8a10f16a58dfc70b +F ext/fiddle/sqlite3-worker.js 50b7a9ce14c8fae0af965e35605fe12cafb79c1e01e99216d8110d8b02fbf4b5 F ext/fiddle/testing.css 750572dded671d2cf142bbcb27af5542522ac08db128245d0b9fe410aa1d7f2a F ext/fiddle/testing1.html ea1f3be727f78e420007f823912c1a03b337ecbb8e79449abc2244ad4fe15d9a -F ext/fiddle/testing1.js 5c04721d205b3d909ec6896682743051cb5e5202e9151a058e90600a42f48f48 +F ext/fiddle/testing1.js f9615ff58b9de6879e4836618b34322085510ec44c6754e725a92a097c908a6f F ext/fiddle/testing2.html 9063b2430ade2fe9da4e711addd1b51a2741cf0c7ebf6926472a5e5dd63c0bc4 F ext/fiddle/testing2.js 7b45b4e7fddbd51dbaf89b6722c02758051b34bac5a98c11b569a7e7572f88ee +F ext/fiddle/wasm_util.c b63e00c2f264ab4a9c45c9f9727627cbc4d8aa2f93c5dd09e8105d63ff7e0872 F ext/fts1/README.txt 20ac73b006a70bcfd80069bdaf59214b6cf1db5e F ext/fts1/ft_hash.c 3927bd880e65329bdc6f506555b228b28924921b F ext/fts1/ft_hash.h 06df7bba40dadd19597aa400a875dbc2fed705ea @@ -1978,8 +1979,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 140618b212e3aa9ff2df20f22af846a1c4c5ffaeefd84330446f61362b39a8f1 -R ecfc68999ba200e5f9d0ce8c6a78fe70 +P da1d3151a440567f34a2f6c0b2bfc2e9fab81c256cc361c9ce7b46f2c23a2aa8 +R ff53072845ce3be319c8a5687950f355 U stephan -Z ec8563cad502dab169a1c4538eaf6ec1 +Z 0ce9f393ac2f284b021257ae10338830 # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 63c94d8ff4..7a1e3598fa 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -da1d3151a440567f34a2f6c0b2bfc2e9fab81c256cc361c9ce7b46f2c23a2aa8 \ No newline at end of file +778062e3b415dca5104eee398950741b6dbb9d4bdf7c998eef18371a42669946 \ No newline at end of file