From: stephan Date: Thu, 27 Jul 2023 22:05:39 +0000 (+0000) Subject: Bind sqlite3_busy_handler(). Correct mapping of pointers for, and cleanup of, JNI... X-Git-Tag: version-3.43.0~47^2~148 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=1a95091b49738141022022aded69a305d5e309ec;p=thirdparty%2Fsqlite.git Bind sqlite3_busy_handler(). Correct mapping of pointers for, and cleanup of, JNI-level per-db state. FossilOrigin-Name: 524747796a30a5c1c6c7567b49ffb1e35e2626c73e09c335c0ab74d4ddb5f005 --- diff --git a/ext/jni/src/c/sqlite3-jni.c b/ext/jni/src/c/sqlite3-jni.c index ba892afc56..581300c9d5 100644 --- a/ext/jni/src/c/sqlite3-jni.c +++ b/ext/jni/src/c/sqlite3-jni.c @@ -316,6 +316,17 @@ static void JNIEnvCache_clear(JNIEnvCache * p){ memset(p, 0, sizeof(JNIEnvCache)); } +/** + State for binding Java-side busy handlers. +*/ +typedef struct { + JNIEnv * env; /* env registered from */; + jobject jObj /* BusyHandlerJni instance */; + jclass klazz /* jObj's class */; + jmethodID jmidxCallback; +} BusyHandlerJni; + + /** Per-(sqlite3*) state for bindings which do not have their own finalizer functions, e.g. tracing and commit/rollback hooks. This @@ -328,12 +339,12 @@ static void JNIEnvCache_clear(JNIEnvCache * p){ data (since they can(?) hypothetically be set via multiple JNIEnv objects)? */ -typedef struct PerDbState PerDbState; -struct PerDbState { +typedef struct PerDbStateJni PerDbStateJni; +struct PerDbStateJni { JNIEnv *env; sqlite3 * pDb; - PerDbState * pNext; - PerDbState * pPrev; + PerDbStateJni * pNext; + PerDbStateJni * pPrev; struct { jobject jObj; jmethodID midCallback; @@ -350,6 +361,7 @@ struct PerDbState { jobject jObj; jmethodID midCallback; } rollbackHook; + BusyHandlerJni busyHandler; }; static struct { @@ -367,8 +379,8 @@ static struct { JavaVM * jvm; struct JNIEnvCache envCache; struct { - PerDbState * aUsed; - PerDbState * aFree; + PerDbStateJni * aUsed; + PerDbStateJni * aFree; } perDb; } S3Global; @@ -389,13 +401,57 @@ static void * s3jni_malloc(JNIEnv *env, size_t n){ static void s3jni_free(void * p){ if(p) sqlite3_free(p); } + +/** + Clears s's state, releasing any Java references. Before doing so, + it calls s's xDestroy() method, ignoring the lack of that method or + any exceptions it throws. This is a no-op of s has no current + state. +*/ +static void BusyHandlerJni_clear(BusyHandlerJni * const s){ + if(s->jObj){ + JNIEnv * const env = s->env; + const jmethodID method = + (*env)->GetMethodID(env, s->klazz, "xDestroy", "()V"); + if(method){ + (*env)->CallVoidMethod(env, s->jObj, method); + EXCEPTION_IGNORE; + }else{ + EXCEPTION_CLEAR; + } + UNREF_G(s->jObj); + UNREF_G(s->klazz); + memset(s, 0, sizeof(BusyHandlerJni)); + } +} + +/** + Initializes s to wrap BusyHandlerJni-type object jObject, clearning + any current state of s beforehand. Returns 0 on success, non-0 on + error. On error, s's state is cleared. +*/ +static int BusyHandlerJni_init(JNIEnv * const env, BusyHandlerJni * const s, + jobject jObj){ + const char * zSig = "(I)I" /* callback signature */; + if(s->jObj) BusyHandlerJni_clear(s); + s->env = env; + s->jObj = REF_G(jObj); + s->klazz = REF_G((*env)->GetObjectClass(env, jObj)); + s->jmidxCallback = (*env)->GetMethodID(env, s->klazz, "xCallback", zSig); + IFTHREW { + BusyHandlerJni_clear(s); + return SQLITE_ERROR; + } + return 0; +} + /** - Extracts the new PerDbState instance from the free-list, or + Extracts the new PerDbStateJni instance from the free-list, or allocates one if needed, associats it with pDb, and returns. Returns NULL on OOM. */ -static PerDbState * PerDbState_alloc(JNIEnv *env, sqlite3 *pDb){ - PerDbState * rv; +static PerDbStateJni * PerDbStateJni_alloc(JNIEnv *env, sqlite3 *pDb){ + PerDbStateJni * rv; assert( pDb ); if(S3Global.perDb.aFree){ rv = S3Global.perDb.aFree; @@ -407,18 +463,18 @@ static PerDbState * PerDbState_alloc(JNIEnv *env, sqlite3 *pDb){ rv->pNext = 0; } }else{ - rv = s3jni_malloc(env, sizeof(PerDbState)); + rv = s3jni_malloc(env, sizeof(PerDbStateJni)); if(rv){ - memset(rv, 0, sizeof(PerDbState)); - rv->pNext = S3Global.perDb.aUsed; - S3Global.perDb.aUsed = rv; - if(rv->pNext){ - assert(!rv->pNext->pPrev); - rv->pNext->pPrev = rv; - } + memset(rv, 0, sizeof(PerDbStateJni)); } } if(rv){ + rv->pNext = S3Global.perDb.aUsed; + S3Global.perDb.aUsed = rv; + if(rv->pNext){ + assert(!rv->pNext->pPrev); + rv->pNext->pPrev = rv; + } rv->pDb = pDb; rv->env = env; } @@ -429,7 +485,7 @@ static PerDbState * PerDbState_alloc(JNIEnv *env, sqlite3 *pDb){ Clears s's state and moves it to the free-list. */ FIXME_THREADING -static void PerDbState_set_aside(PerDbState *s){ +static void PerDbStateJni_set_aside(PerDbStateJni *s){ if(s){ JNIEnv * const env = s->env; assert(s->pDb && "Else this object is already in the free-list."); @@ -443,27 +499,38 @@ static void PerDbState_set_aside(PerDbState *s){ UNREF_G(s->progress.jObj); UNREF_G(s->commitHook.jObj); UNREF_G(s->rollbackHook.jObj); - s->env = 0; - s->pDb = 0; - s->pPrev = 0; + BusyHandlerJni_clear(&s->busyHandler); + memset(s, 0, sizeof(PerDbStateJni)); s->pNext = S3Global.perDb.aFree; S3Global.perDb.aFree = s; } } +static void PerDbStateJni_dump(PerDbStateJni *s){ + MARKER(("PerDbStateJni->env @ %p\n", s->env)); + MARKER(("PerDbStateJni->pDb @ %p\n", s->pDb)); + MARKER(("PerDbStateJni->trace.jObj @ %p\n", s->trace.jObj)); + MARKER(("PerDbStateJni->progress.jObj @ %p\n", s->progress.jObj)); + MARKER(("PerDbStateJni->commitHook.jObj @ %p\n", s->commitHook.jObj)); + MARKER(("PerDbStateJni->rollbackHook.jObj @ %p\n", s->rollbackHook.jObj)); + MARKER(("PerDbStateJni->busyHandler.env @ %p\n", s->busyHandler.env)); + MARKER(("PerDbStateJni->busyHandler.jObj @ %p\n", s->busyHandler.jObj)); + MARKER(("PerDbStateJni->env @ %p\n", s->env)); +} + /** - Returns the PerDbState object for the given db. If allocIfNeeded is + Returns the PerDbStateJni object for the given db. If allocIfNeeded is true then a new instance will be allocated if no mapping currently exists, else NULL is returned if no mapping is found. */ FIXME_THREADING -static PerDbState * PerDbState_for_db(JNIEnv *env, sqlite3 *pDb, int allocIfNeeded){ - PerDbState * s = S3Global.perDb.aUsed; +static PerDbStateJni * PerDbStateJni_for_db(JNIEnv *env, sqlite3 *pDb, int allocIfNeeded){ + PerDbStateJni * s = S3Global.perDb.aUsed; for( ; s; s = s->pNext){ if(s->pDb == pDb) return s; } - if(allocIfNeeded) s = PerDbState_alloc(env, pDb); + if(allocIfNeeded) s = PerDbStateJni_alloc(env, pDb); return s; } @@ -471,12 +538,12 @@ static PerDbState * PerDbState_for_db(JNIEnv *env, sqlite3 *pDb, int allocIfNeed Cleans up and frees all state in S3Global.perDb. */ FIXME_THREADING -static void PerDbState_free_all(void){ - PerDbState * pS = S3Global.perDb.aUsed; - PerDbState * pSNext = 0; +static void PerDbStateJni_free_all(void){ + PerDbStateJni * pS = S3Global.perDb.aUsed; + PerDbStateJni * pSNext = 0; for( ; pS; pS = pSNext ){ pSNext = pS->pNext; - PerDbState_set_aside(pS); + PerDbStateJni_set_aside(pS); assert(pSNext ? !pSNext->pPrev : 1); } assert( 0==S3Global.perDb.aUsed ); @@ -1205,8 +1272,50 @@ JDECL(jint,1bind_1zeroblob64)(JENV_JSELF, jobject jpStmt, return (jint)sqlite3_bind_zeroblob(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, (sqlite3_uint64)n); } -JDECL(jint,1busy_1timeout)(JENV_JSELF, jobject pDb, jint ms){ - return sqlite3_busy_timeout(PtrGet_sqlite3(pDb), (int)ms); +static int s3jni_busy_handler(void* pState, int n){ + PerDbStateJni * const pS = (PerDbStateJni *)pState; + int rc = 0; + if( pS->busyHandler.jObj ){ + JNIEnv * const env = pS->env; + rc = (*env)->CallIntMethod(env, pS->busyHandler.jObj, + pS->busyHandler.jmidxCallback, (jint)n); + } + return rc; +} + +JDECL(jint,1busy_1handler)(JENV_JSELF, jobject jDb, jobject jBusy){ + sqlite3 * const pDb = PtrGet_sqlite3(jDb); + PerDbStateJni * const pS = PerDbStateJni_for_db(env, pDb, 1); + int rc; + if(!pS) return (jint)SQLITE_NOMEM; + if(jBusy){ + if(pS->busyHandler.jObj && + (*env)->IsSameObject(env, pS->busyHandler.jObj, jBusy)){ + /* Same object - this is a no-op. */ + return 0; + } + rc = BusyHandlerJni_init(env, &pS->busyHandler, jBusy); + if(rc){ + assert(!pS->busyHandler.jObj); + return (jint)rc; + } + assert(pS->busyHandler.jObj && pS->busyHandler.klazz); + assert( (*env)->IsSameObject(env, pS->busyHandler.jObj, jBusy) ); + }else{ + BusyHandlerJni_clear(&pS->busyHandler); + } + return jBusy + ? sqlite3_busy_handler(pDb, s3jni_busy_handler, pS) + : sqlite3_busy_handler(pDb, 0, 0); +} + +JDECL(jint,1busy_1timeout)(JENV_JSELF, jobject jDb, jint ms){ + sqlite3* const pDb = PtrGet_sqlite3(jDb); + PerDbStateJni * const pS = PerDbStateJni_for_db(env, pDb, 0); + if( pS && pS->busyHandler.jObj ){ + BusyHandlerJni_clear(&pS->busyHandler); + } + return sqlite3_busy_timeout(pDb, (int)ms); } /** @@ -1215,13 +1324,19 @@ JDECL(jint,1busy_1timeout)(JENV_JSELF, jobject pDb, jint ms){ static jint s3jni_close_db(JNIEnv *env, jobject jDb, int version){ sqlite3 * pDb; int rc = 0; - PerDbState * pS; + PerDbStateJni * pS = 0; assert(version == 1 || version == 2); + if(0){ + PerDbStateJni * s = S3Global.perDb.aUsed; + for( ; s; s = s->pNext){ + PerDbStateJni_dump(s); + } + } pDb = PtrGet_sqlite3(jDb); if(!pDb) return rc; - pS = PerDbState_for_db(env, pDb, 0); + pS = PerDbStateJni_for_db(env, pDb, 0); rc = 1==version ? (jint)sqlite3_close(pDb) : (jint)sqlite3_close_v2(pDb); - if(pS) PerDbState_set_aside(pS) + if(pS) PerDbStateJni_set_aside(pS) /* MUST come after close() because of pS->trace. */; setNativePointer(env, jDb, 0, ClassNames.sqlite3); return (jint)rc; @@ -1706,7 +1821,7 @@ JDECL(void,1set_1last_1insert_1rowid)(JENV_JSELF, jobject jpDb, jlong rowId){ } JDECL(jint,1shutdown)(JENV_JSELF){ - PerDbState_free_all(); + PerDbStateJni_free_all(); JNIEnvCache_clear(&S3Global.envCache); /* Do not clear S3Global.jvm: it's legal to call sqlite3_initialize() again to restart the lib. */ @@ -1714,7 +1829,7 @@ JDECL(jint,1shutdown)(JENV_JSELF){ } static int s3jni_trace_impl(unsigned traceflag, void *pC, void *pP, void *pX){ - PerDbState * const ps = (PerDbState *)pC; + PerDbStateJni * const ps = (PerDbStateJni *)pC; JNIEnv * const env = ps->env; jobject jX = NULL; JNIEnvCacheLine * const pEcl = S3Global_env_cache(env); @@ -1752,12 +1867,12 @@ static int s3jni_trace_impl(unsigned traceflag, void *pC, void *pP, void *pX){ JDECL(jint,1trace_1v2)(JENV_JSELF,jobject jDb, jint traceMask, jobject jTracer){ sqlite3 * const pDb = PtrGet_sqlite3(jDb); - PerDbState * ps; + PerDbStateJni * ps; jclass klazz; if( !traceMask || !jTracer ){ return (jint)sqlite3_trace_v2(pDb, 0, 0, 0); } - ps = PerDbState_for_db(env, pDb, 1); + ps = PerDbStateJni_for_db(env, pDb, 1); if(!ps) return SQLITE_NOMEM; klazz = (*env)->GetObjectClass(env, jTracer); ps->trace.midCallback = (*env)->GetMethodID(env, klazz, "xCallback", diff --git a/ext/jni/src/c/sqlite3-jni.h b/ext/jni/src/c/sqlite3-jni.h index a55d437152..524a9b4075 100644 --- a/ext/jni/src/c/sqlite3-jni.h +++ b/ext/jni/src/c/sqlite3-jni.h @@ -843,6 +843,14 @@ JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1zeroblob JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1zeroblob64 (JNIEnv *, jclass, jobject, jint, jlong); +/* + * Class: org_sqlite_jni_SQLite3Jni + * Method: sqlite3_busy_handler + * Signature: (Lorg/sqlite/jni/sqlite3;Lorg/sqlite/jni/BusyHandler;)I + */ +JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1busy_1handler + (JNIEnv *, jclass, jobject, jobject); + /* * Class: org_sqlite_jni_SQLite3Jni * Method: sqlite3_busy_timeout diff --git a/ext/jni/src/org/sqlite/jni/BusyHandler.java b/ext/jni/src/org/sqlite/jni/BusyHandler.java new file mode 100644 index 0000000000..d3a3675f06 --- /dev/null +++ b/ext/jni/src/org/sqlite/jni/BusyHandler.java @@ -0,0 +1,42 @@ +/* +** 2023-07-22 +** +** 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. +** +************************************************************************* +** This file is part of the JNI bindings for the sqlite3 C API. +*/ +package org.sqlite.jni; + +/** + Callback proxy for use with sqlite3_busy_handler(). +*/ +public abstract class BusyHandler { + /** + Must function as documented for the sqlite3_busy_handler() + callback argument, minus the (void*) argument the C-level + function requires. + */ + public abstract int xCallback(int n); + + /** + Optionally override to perform any cleanup when this busy + handler is destroyed. It is destroyed when: + + - The associated db is passed to sqlite3_close() or + sqlite3_close_v2(). + + - sqlite3_busy_handler() is called to replace the handler, + whether it's passed a null handler or any other instance of + this class. + + - sqlite3_busy_timeout() is called, which implicitly installs + a busy handler. + */ + public void xDestroy(){} +} diff --git a/ext/jni/src/org/sqlite/jni/SQLite3Jni.java b/ext/jni/src/org/sqlite/jni/SQLite3Jni.java index 53b920f808..b0ae33c640 100644 --- a/ext/jni/src/org/sqlite/jni/SQLite3Jni.java +++ b/ext/jni/src/org/sqlite/jni/SQLite3Jni.java @@ -133,7 +133,14 @@ public final class SQLite3Jni { public static native int sqlite3_bind_zeroblob64(@NotNull sqlite3_stmt stmt, int ndx, long n); - //TODO? public static native int sqlite3_busy_handler(sqlite3*,int(*)(void*,int),void*); + /** + As for the C-level function of the same name, with a BusyHandler + instance in place of a callback function. Pass it a null handler + to clear the busy handler. Calling this multiple times with the + same object is a no-op on the second and subsequent calls. + */ + public static native int sqlite3_busy_handler(@NotNull sqlite3 db, + @Nullable BusyHandler handler); public static native int sqlite3_busy_timeout(@NotNull sqlite3 db, int ms); diff --git a/ext/jni/src/org/sqlite/jni/Tester1.java b/ext/jni/src/org/sqlite/jni/Tester1.java index 910fc85005..b4c3f97e8c 100644 --- a/ext/jni/src/org/sqlite/jni/Tester1.java +++ b/ext/jni/src/org/sqlite/jni/Tester1.java @@ -640,6 +640,28 @@ public class Tester1 { myassert( 7 == counter.value ); } + private static void testBusy(){ + outln("testBusy()..."); + final sqlite3 db = createNewDb(); + final ValueHolder xDestroyed = new ValueHolder<>(false); + BusyHandler handler = new BusyHandler(){ + @Override public int xCallback(int n){ + /* How do we conveniently test this? */ + return 0; + } + @Override public void xDestroy(){ + xDestroyed.value = true; + } + }; + outln("setting busy handler..."); + int rc = sqlite3_busy_handler(db, handler); + outln("set busy handler"); + myassert(0 == rc); + myassert( false == xDestroyed.value ); + sqlite3_close_v2(db); + myassert( true == xDestroyed.value ); + } + private static void testMisc(){ outln("Sleeping..."); sqlite3_sleep(500); @@ -666,6 +688,7 @@ public class Tester1 { testUdfAggregate(); testUdfWindow(); testTrace(); + testBusy(); testMisc(); if(liArgs.indexOf("-v")>0){ listBoundMethods(); diff --git a/manifest b/manifest index 393405cc0d..90de8b5efc 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Replace\ssome\swww:\sinterwiki\sreferences\sin\sthe\sJNI\sreadme\swith\stheir\sfull\sURLs\sto\smake\sthe\sdoc\smore\sportable. -D 2023-07-27T20:32:16.722 +C Bind\ssqlite3_busy_handler().\sCorrect\smapping\sof\spointers\sfor,\sand\scleanup\sof,\sJNI-level\sper-db\sstate. +D 2023-07-27T22:05:39.096 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -232,15 +232,16 @@ F ext/icu/icu.c c074519b46baa484bb5396c7e01e051034da8884bad1a1cb7f09bbe6be3f0282 F ext/icu/sqliteicu.h fa373836ed5a1ee7478bdf8a1650689294e41d0c89c1daab26e9ae78a32075a8 F ext/jni/GNUmakefile 56a014dbff9516774d895ec1ae9df0ed442765b556f79a0fc0b5bc438217200d F ext/jni/README.md 042762dbf047667783a5bd0aec303535140f302debfbd259c612edf856661623 -F ext/jni/src/c/sqlite3-jni.c 55bf5624beee849b1c063bf929e6066dc95437564c3212d30e672280bec45da8 -F ext/jni/src/c/sqlite3-jni.h ef862321bb153135472ebe6be6df9db3e47448ae3ef6bb3cb7953c54971efcf8 +F ext/jni/src/c/sqlite3-jni.c 8274a016b5049651ca016486639f90abf050020cf6c880b5273f4e15b29d1ffc +F ext/jni/src/c/sqlite3-jni.h c9bb150a38dce09cc2794d5aac8fa097288d9946fbb15250fd0a23c31957f506 +F ext/jni/src/org/sqlite/jni/BusyHandler.java aa7574dcf08500ab2334a0ce09c24593374db89815d76fee16da09641c1e32ce F ext/jni/src/org/sqlite/jni/Collation.java 8dffbb00938007ad0967b2ab424d3c908413af1bbd3d212b9c9899910f1218d1 F ext/jni/src/org/sqlite/jni/NativePointerHolder.java 70dc7bc41f80352ff3d4331e2e24f45fcd23353b3641e2f68a81bd8262215861 F ext/jni/src/org/sqlite/jni/OutputPointer.java 08a752b58a33696c5eaf0eb9361a0966b188dec40f4a3613eb133123951f6c5f F ext/jni/src/org/sqlite/jni/ProgressHandler.java 5a1d7b2607eb2ef596fcf4492a49d1b3a5bdea3af9918e11716831ffd2f02284 F ext/jni/src/org/sqlite/jni/SQLFunction.java 2f5d197f6c7d73b6031ba1a19598d7e3eee5ebad467eeee62c72e585bd6556a5 -F ext/jni/src/org/sqlite/jni/SQLite3Jni.java d588c88c17290f5b0d1e4e2a1ea68cf9acab40891c98e08203f1b90ac2aaf8dd -F ext/jni/src/org/sqlite/jni/Tester1.java 512e545357ce1a5788b250395f2b198ae862f915aee1a8b7b8fae4620d0cfc8d +F ext/jni/src/org/sqlite/jni/SQLite3Jni.java 3582b30c0fb1cb39e25b9069fe8c9e2fe4f2659f4d38437b610e46143e163610 +F ext/jni/src/org/sqlite/jni/Tester1.java 1b5f638c9efa0a18579fca5172f4514cd73c8202eacf002fbcc2726efe67fcf0 F ext/jni/src/org/sqlite/jni/Tracer.java c2fe1eba4a76581b93b375a7b95ab1919e5ae60accfb06d6beb067b033e9bae1 F ext/jni/src/org/sqlite/jni/ValueHolder.java f022873abaabf64f3dd71ab0d6037c6e71cece3b8819fa10bf26a5461dc973ee F ext/jni/src/org/sqlite/jni/sqlite3.java c7d0500c7269882243aafb41425928d094b2fcbdbc2fd1caffc276871cd3fae3 @@ -2066,8 +2067,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 0514fd340ae15a95760d50c747d6fb9eae5109cb5045eeabc2bc199be0a5ae35 -R fbc3a722616dae826b754c2990ee20ad +P 63ce0c9bdde210cf2f8b6099ae5c73caac18e6debc13c2f77090b77f3de72beb +R c101ee50ca81ac0c17b72552c1b109f1 U stephan -Z 82e6c4339ed3624b75f36686218f69e5 +Z 3865f2d2a0d46095d9527e301bfc90cc # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 90c4a09c65..a432b84e8f 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -63ce0c9bdde210cf2f8b6099ae5c73caac18e6debc13c2f77090b77f3de72beb \ No newline at end of file +524747796a30a5c1c6c7567b49ffb1e35e2626c73e09c335c0ab74d4ddb5f005 \ No newline at end of file