From c7e7c88873710118420ae3a6d91e31d45fa833c6 Mon Sep 17 00:00:00 2001 From: stephan Date: Sat, 26 Aug 2023 22:34:26 +0000 Subject: [PATCH] Apply the JNI OOM checks to memory returned by JDK APIs, as distinct from our APIs. FossilOrigin-Name: 1ff78582bfd934e0c76464b5f23ed9bf09a3491b145e0ca34acb6e59c4f53995 --- ext/jni/GNUmakefile | 2 +- ext/jni/src/c/sqlite3-jni.c | 191 ++++++++++++++++++++---------------- manifest | 14 +-- manifest.uuid | 2 +- 4 files changed, 115 insertions(+), 94 deletions(-) diff --git a/ext/jni/GNUmakefile b/ext/jni/GNUmakefile index 5f7038f051..dd8df07bc4 100644 --- a/ext/jni/GNUmakefile +++ b/ext/jni/GNUmakefile @@ -182,7 +182,7 @@ SQLITE_OPT = \ -DSQLITE_TEMP_STORE=2 \ -DSQLITE_USE_URI=1 \ -DSQLITE_C=$(sqlite3.c) \ - -DSQLITE_JNI_FATAL_OOM=0 \ + -DSQLITE_JNI_FATAL_OOM=1 \ -DSQLITE_DEBUG SQLITE_OPT += -g -DDEBUG -UNDEBUG diff --git a/ext/jni/src/c/sqlite3-jni.c b/ext/jni/src/c/sqlite3-jni.c index 353ea552d5..c210d298a0 100644 --- a/ext/jni/src/c/sqlite3-jni.c +++ b/ext/jni/src/c/sqlite3-jni.c @@ -706,20 +706,39 @@ static void s3jni_incr( volatile unsigned int * const p ){ #define S3JniMutex_S3JniDb_leave #endif -/* Helpers for jstring and jbyteArray. */ -#define s3jni_jstring_to_mutf8(ARG) (*env)->GetStringUTFChars(env, ARG, NULL) -#define s3jni_mutf8_release(ARG,VAR) if( VAR ) (*env)->ReleaseStringUTFChars(env, ARG, VAR) -#define s3jni_jbytearray_bytes(ARG) (*env)->GetByteArrayElements(env,ARG, NULL) -#define s3jni_jbytearray_release(ARG,VAR) if( VAR ) (*env)->ReleaseByteArrayElements(env, ARG, VAR, JNI_ABORT) - - /* Fail fatally with an OOM message. */ static inline void s3jni_oom(JNIEnv * const env){ (*env)->FatalError(env, "Out of memory.") /* does not return */; } -/* Fail fatally if !VAR. */ -#define s3jni_oom_check(VAR) if( !(VAR) ) s3jni_oom(env) +/* Fail fatally if !EXPR. */ +#define s3jni_oom_fatal(EXPR) if( !(EXPR) ) s3jni_oom(env) +/* Maybe fail fatally if !EXPR. */ +#ifdef SQLITE_JNI_FATAL_OOM +#define s3jni_oom_check s3jni_oom_fatal +#else +#define s3jni_oom_check(EXPR) +#endif + +/* Helpers for jstring and jbyteArray. */ +static const char * s3jni__jstring_to_mutf8_bytes(JNIEnv * const env, jstring v ){ + const char *z = v ? (*env)->GetStringUTFChars(env, v, NULL) : 0; + s3jni_oom_check( v ? !!z : !z ); + return z; +} + +#define s3jni_jstring_to_mutf8(ARG) s3jni__jstring_to_mutf8_bytes(env, (ARG)) +#define s3jni_mutf8_release(ARG,VAR) if( VAR ) (*env)->ReleaseStringUTFChars(env, ARG, VAR) + +static jbyte * s3jni__jbytearray_bytes(JNIEnv * const env, jbyteArray jBA ){ + jbyte * const rv = jBA ? (*env)->GetByteArrayElements(env, jBA, NULL) : 0; + s3jni_oom_check( jBA ? !!rv : 1 ); + return rv; +} + +#define s3jni_jbytearray_bytes(jByteArray) s3jni__jbytearray_bytes(env, (jByteArray)) +#define s3jni_jbytearray_release(jByteArray,jBytes) \ + if( jBytes ) (*env)->ReleaseByteArrayElements(env, jByteArray, jBytes, JNI_ABORT) /* ** sqlite3_malloc() proxy which fails fatally on OOM. This should @@ -841,13 +860,17 @@ static int s3jni_db_error(sqlite3* const db, int err_code, } /* -** Creates a new jByteArray of length nP, copies p's contents into it, and -** returns that byte array (NULL on OOM). +** Creates a new jByteArray of length nP, copies p's contents into it, +** and returns that byte array (NULL on OOM unless fail-fast alloc +** errors are enabled). p may be NULL, in which case the array is +** created but no bytes are filled. */ static jbyteArray s3jni_new_jbyteArray(JNIEnv * const env, - const unsigned char * const p, int nP){ + const void * const p, int nP){ jbyteArray jba = (*env)->NewByteArray(env, (jint)nP); - if( jba ){ + + s3jni_oom_check( jba ); + if( jba && p ){ (*env)->SetByteArrayRegion(env, jba, 0, (jint)nP, (const jbyte*)p); } return jba; @@ -876,13 +899,18 @@ static jstring s3jni_utf8_to_jstring(JNIEnv * const env, }else if( z ){ jbyteArray jba; if( n<0 ) n = sqlite3Strlen30(z); - jba = s3jni_new_jbyteArray(env, (unsigned const char *)z, (jsize)n); + jba = s3jni_new_jbyteArray(env, (unsigned const char *)z, n); if( jba ){ rv = (*env)->NewObject(env, SJG.g.cString, SJG.g.ctorStringBA, jba, SJG.g.oCharsetUtf8); + S3JniIfThrew{ + S3JniExceptionReport; + S3JniExceptionClear; + } S3JniUnrefLocal(jba); } } + s3jni_oom_check( rv ); return rv; } @@ -910,9 +938,11 @@ static char * s3jni_jstring_to_utf8(JNIEnv * const env, if( !jstr ) return 0; jba = (*env)->CallObjectMethod(env, jstr, SJG.g.stringGetBytes, SJG.g.oCharsetUtf8); + if( (*env)->ExceptionCheck(env) || !jba /* order of these checks is significant for -Xlint:jni */ ) { S3JniExceptionReport; + s3jni_oom_check( jba ); if( nLen ) *nLen = 0; return 0; } @@ -935,9 +965,11 @@ static char * s3jni_jstring_to_utf8(JNIEnv * const env, ** if !p or (*env)->NewString() fails. */ static jstring s3jni_text16_to_jstring(JNIEnv * const env, const void * const p, int nP){ - return p + jstring const rv = p ? (*env)->NewString(env, (const jchar *)p, (jsize)(nP/2)) : NULL; + s3jni_oom_check( p ? !!rv : 1 ); + return rv; } /* @@ -1488,15 +1520,14 @@ static int CollationState_xCompare(void *pArg, int nLhs, const void *lhs, S3JniHook_localdup(env, &ps->hooks.collation, &hook ); if( hook.jObj ){ - jbyteArray jbaLhs = (*env)->NewByteArray(env, (jint)nLhs); - jbyteArray jbaRhs = jbaLhs ? (*env)->NewByteArray(env, (jint)nRhs) : NULL; + jbyteArray jbaLhs = s3jni_new_jbyteArray(env, lhs, (jint)nLhs); + jbyteArray jbaRhs = jbaLhs + ? s3jni_new_jbyteArray(env, rhs, (jint)nRhs) : 0; if( !jbaRhs ){ S3JniUnrefLocal(jbaLhs); s3jni_db_error(ps->pDb, SQLITE_NOMEM, 0); return 0; } - (*env)->SetByteArrayRegion(env, jbaLhs, 0, (jint)nLhs, (const jbyte*)lhs); - (*env)->SetByteArrayRegion(env, jbaRhs, 0, (jint)nRhs, (const jbyte*)rhs); rc = (*env)->CallIntMethod(env, ps->hooks.collation.jObj, ps->hooks.collation.midCallback, jbaLhs, jbaRhs); @@ -1673,6 +1704,7 @@ static int udf_args(JNIEnv *env, ja = (*env)->NewObjectArray(env, argc, SJG.g.cObj, NULL); + s3jni_oom_check( ja ); if( !ja ) goto error_oom; for(i = 0; i < argc; ++i){ jobject jsv = new_sqlite3_value_wrapper(env, argv[i]); @@ -1840,9 +1872,11 @@ static void udf_xInverse(sqlite3_context* cx, int argc, ** CName(void)). This is only valid for functions which are known to ** return ASCII or text which is equivalent in UTF-8 and MUTF-8. */ -#define WRAP_MUTF8_VOID(JniNameSuffix,CName) \ - JniDecl(jstring,JniNameSuffix)(JniArgsEnvClass){ \ - return (*env)->NewStringUTF( env, CName() ); \ +#define WRAP_MUTF8_VOID(JniNameSuffix,CName) \ + JniDecl(jstring,JniNameSuffix)(JniArgsEnvClass){ \ + jstring const rv = (*env)->NewStringUTF( env, CName() ); \ + s3jni_oom_check(rv); \ + return rv; \ } /** Create a trivial JNI wrapper for (int CName(sqlite3_stmt*)). */ #define WRAP_INT_STMT(JniNameSuffix,CName) \ @@ -2053,9 +2087,16 @@ S3JniApi(sqlite3_bind_blob(),jint,1bind_1blob)( JniArgsEnvClass, jobject jpStmt, jint ndx, jbyteArray baData, jint nMax ){ jbyte * const pBuf = baData ? s3jni_jbytearray_bytes(baData) : 0; - int const rc = sqlite3_bind_blob(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, - pBuf, (int)nMax, SQLITE_TRANSIENT); - s3jni_jbytearray_release(baData,pBuf); + int rc; + if( pBuf ){ + rc = sqlite3_bind_blob(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, + pBuf, (int)nMax, SQLITE_TRANSIENT); + s3jni_jbytearray_release(baData, pBuf); + }else{ + rc = baData + ? SQLITE_NOMEM + : sqlite3_bind_null( PtrGet_sqlite3_stmt(jpStmt), ndx ); + } return (jint)rc; } @@ -2277,6 +2318,8 @@ static void s3jni_collation_needed_impl16(void *pState, sqlite3 *pDb, if( hook.jObj ){ unsigned int const nName = s3jni_utf16_strlen(z16Name); jstring jName = (*env)->NewString(env, (jchar const *)z16Name, nName); + + s3jni_oom_check( jName ); S3JniIfThrew{ s3jni_db_error(ps->pDb, SQLITE_NOMEM, 0); S3JniExceptionClear; @@ -2339,12 +2382,8 @@ S3JniApi(sqlite3_column_blob(),jbyteArray,1column_1blob)( sqlite3_stmt * const pStmt = PtrGet_sqlite3_stmt(jpStmt); void const * const p = sqlite3_column_blob(pStmt, (int)ndx); int const n = p ? sqlite3_column_bytes(pStmt, (int)ndx) : 0; - if( 0==p ) return NULL; - else{ - jbyteArray const jba = (*env)->NewByteArray(env, n); - (*env)->SetByteArrayRegion(env, jba, 0, n, (const jbyte *)p); - return jba; - } + + return p ? s3jni_new_jbyteArray(env, p, n) : 0; } S3JniApi(sqlite3_column_double(),jdouble,1column_1double)( @@ -2483,14 +2522,17 @@ S3JniApi(sqlite3_commit_hook(),jobject,1commit_1hook)( S3JniApi(sqlite3_compileoption_get(),jstring,1compileoption_1get)( JniArgsEnvClass, jint n ){ - return (*env)->NewStringUTF( env, sqlite3_compileoption_get(n) ) + jstring const rv = (*env)->NewStringUTF( env, sqlite3_compileoption_get(n) ) /* We know these to be ASCII, so MUTF-8 is fine. */; + s3jni_oom_check(rv); + return rv; } S3JniApi(sqlite3_compileoption_used(),jboolean,1compileoption_1used)( JniArgsEnvClass, jstring name ){ - const char *zUtf8 = s3jni_jstring_to_mutf8(name); + const char *zUtf8 = s3jni_jstring_to_mutf8(name) + /* We know these to be ASCII, so MUTF-8 is fine. */; const jboolean rc = 0==sqlite3_compileoption_used(zUtf8) ? JNI_FALSE : JNI_TRUE; s3jni_mutf8_release(name, zUtf8); @@ -2840,9 +2882,11 @@ S3JniApi(sqlite3_errmsg(),jstring,1errmsg)( S3JniApi(sqlite3_errstr(),jstring,1errstr)( JniArgsEnvClass, jint rcCode ){ - return (*env)->NewStringUTF(env, sqlite3_errstr((int)rcCode)) + jstring const rv = (*env)->NewStringUTF(env, sqlite3_errstr((int)rcCode)) /* We know these values to be plain ASCII, so pose no MUTF-8 ** incompatibility */; + s3jni_oom_check( rv ); + return rv; } S3JniApi(sqlite3_expanded_sql(),jstring,1expanded_1sql)( @@ -2852,7 +2896,7 @@ S3JniApi(sqlite3_expanded_sql(),jstring,1expanded_1sql)( jstring rv = 0; if( pStmt ){ char * zSql = sqlite3_expanded_sql(pStmt); - s3jni_oom_check(zSql); + s3jni_oom_fatal(zSql); if( zSql ){ rv = s3jni_utf8_to_jstring(env, zSql, -1); sqlite3_free(zSql); @@ -2862,8 +2906,7 @@ S3JniApi(sqlite3_expanded_sql(),jstring,1expanded_1sql)( } S3JniApi(sqlite3_extended_result_codes(),jboolean,1extended_1result_1codes)( - JniArgsEnvClass, jobject jpDb, - jboolean onoff + JniArgsEnvClass, jobject jpDb, jboolean onoff ){ int const rc = sqlite3_extended_result_codes(PtrGet_sqlite3(jpDb), onoff ? 1 : 0); return rc ? JNI_TRUE : JNI_FALSE; @@ -3695,7 +3738,7 @@ S3JniApi(sqlite3_set_authorizer(),jint,1set_1authorizer)( S3JniApi(sqlite3_set_last_insert_rowid(),void,1set_1last_1insert_1rowid)( JniArgsEnvClass, jobject jpDb, jlong rowId ){ - sqlite3_set_last_insert_rowid(PtrGet_sqlite3_context(jpDb), + sqlite3_set_last_insert_rowid(PtrGet_sqlite3(jpDb), (sqlite3_int64)rowId); } @@ -3731,7 +3774,7 @@ static int s3jni_strlike_glob(int isLike, JNIEnv *const env, jbyte * const pG = s3jni_jbytearray_bytes(baG); jbyte * const pT = pG ? s3jni_jbytearray_bytes(baT) : 0; - s3jni_oom_check(pT); + s3jni_oom_fatal(pT); /* Note that we're relying on the byte arrays having been NUL-terminated on the Java side. */ rc = isLike @@ -3828,6 +3871,7 @@ static int s3jni_trace_impl(unsigned traceflag, void *pC, void *pP, void *pX){ (jlong)*((sqlite3_int64*)pX)); // hmm. ^^^ (*pX) really is zero. // MARKER(("profile time = %llu\n", *((sqlite3_int64*)pX))); + s3jni_oom_check( jX ); if( !jX ) rc = SQLITE_NOMEM; break; case SQLITE_TRACE_ROW: @@ -3902,13 +3946,11 @@ S3JniApi(sqlite3_value_blob(),jbyteArray,1value_1blob)( sqlite3_value * const sv = PtrGet_sqlite3_value(jpSVal); int const nLen = sqlite3_value_bytes(sv); const jbyte * pBytes = sqlite3_value_blob(sv); - jbyteArray const jba = pBytes - ? (*env)->NewByteArray(env, (jsize)nLen) + + s3jni_oom_check( nLen ? !!pBytes : 1 ); + return pBytes + ? s3jni_new_jbyteArray(env, pBytes, nLen) : NULL; - if( jba ){ - (*env)->SetByteArrayRegion(env, jba, 0, nLen, pBytes); - } - return jba; } @@ -3960,38 +4002,20 @@ S3JniApi(sqlite3_value_text_utf8(),jbyteArray,1value_1text_1utf8)( return p ? s3jni_new_jbyteArray(env, p, n) : 0; } -static jbyteArray value_text16(int mode, JNIEnv * const env, jobject jpSVal){ - sqlite3_value * const sv = PtrGet_sqlite3_value(jpSVal); - int const nLen = sqlite3_value_bytes16(sv); - jbyteArray jba; - const jbyte * pBytes; - switch( mode ){ - case SQLITE_UTF16: - pBytes = sqlite3_value_text16(sv); - break; - case SQLITE_UTF16LE: - pBytes = sqlite3_value_text16le(sv); - break; - case SQLITE_UTF16BE: - pBytes = sqlite3_value_text16be(sv); - break; - default: - assert(!"not possible"); - return NULL; - } - jba = pBytes - ? (*env)->NewByteArray(env, (jsize)nLen) - : NULL; - if( jba ){ - (*env)->SetByteArrayRegion(env, jba, 0, nLen, pBytes); - } - return jba; -} - S3JniApi(sqlite3_value_text16(),jbyteArray,1value_1text16)( JniArgsEnvClass, jobject jpSVal ){ - return value_text16(SQLITE_UTF16, env, jpSVal); + sqlite3_value * const sv = PtrGet_sqlite3_value(jpSVal); + jbyteArray jba = 0; + if( sv ){ + int const nLen = sqlite3_value_bytes16(sv); + const jbyte * const pBytes = + nLen ? sqlite3_value_text16(sv) : 0; + + s3jni_oom_check( nLen ? !!pBytes : 1 ); + jba = s3jni_new_jbyteArray(env, pBytes, nLen); + } + return jba; } JniDecl(void,1jni_1internal_1details)(JniArgsEnvClass){ @@ -4567,15 +4591,12 @@ static int s3jni_xTokenize_xToken(void *p, int tFlags, const char* z, if( s->tok.zPrev == z && s->tok.nPrev == nZ ){ jba = s->tok.jba; }else{ - if(s->tok.jba){ - S3JniUnrefLocal(s->tok.jba); - } + S3JniUnrefLocal(s->tok.jba); s->tok.zPrev = z; s->tok.nPrev = nZ; - s->tok.jba = (*env)->NewByteArray(env, (jint)nZ); + s->tok.jba = s3jni_new_jbyteArray(env, z, nZ); if( !s->tok.jba ) return SQLITE_NOMEM; jba = s->tok.jba; - (*env)->SetByteArrayRegion(env, jba, 0, (jint)nZ, (const jbyte*)z); } rc = (int)(*env)->CallIntMethod(env, s->jCallback, s->midCallback, (jint)tFlags, jba, (jint)iStart, @@ -4833,7 +4854,7 @@ Java_org_sqlite_jni_tester_SQLTester_strglob( jbyte * const pG = s3jni_jbytearray_bytes(baG); jbyte * const pT = pG ? s3jni_jbytearray_bytes(baT) : 0; - s3jni_oom_check(pT); + s3jni_oom_fatal(pT); /* Note that we're relying on the byte arrays having been NUL-terminated on the Java side. */ rc = !SQLTester_strnotglob((const char *)pG, (const char *)pT); @@ -4970,17 +4991,17 @@ Java_org_sqlite_jni_SQLite3Jni_init(JniArgsEnvClass){ #endif SJG.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST); - s3jni_oom_check( SJG.mutex ); + s3jni_oom_fatal( SJG.mutex ); SJG.envCache.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST); - s3jni_oom_check( SJG.envCache.mutex ); + s3jni_oom_fatal( SJG.envCache.mutex ); SJG.perDb.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST); - s3jni_oom_check( SJG.perDb.mutex ); + s3jni_oom_fatal( SJG.perDb.mutex ); SJG.autoExt.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST); - s3jni_oom_check( SJG.autoExt.mutex ); + s3jni_oom_fatal( SJG.autoExt.mutex ); #if S3JNI_METRICS_MUTEX SJG.metrics.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST); - s3jni_oom_check( SJG.metrics.mutex ); + s3jni_oom_fatal( SJG.metrics.mutex ); #endif sqlite3_shutdown() diff --git a/manifest b/manifest index 29c3be6deb..3e1f473740 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Eliminate\sa\ssuperfluous\sJNI-internal\smiddle-man\sclass. -D 2023-08-26T21:13:57.847 +C Apply\sthe\sJNI\sOOM\schecks\sto\smemory\sreturned\sby\sJDK\sAPIs,\sas\sdistinct\sfrom\sour\sAPIs. +D 2023-08-26T22:34:26.393 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -233,10 +233,10 @@ F ext/fts5/tool/showfts5.tcl d54da0e067306663e2d5d523965ca487698e722c F ext/icu/README.txt 7ab7ced8ae78e3a645b57e78570ff589d4c672b71370f5aa9e1cd7024f400fc9 F ext/icu/icu.c c074519b46baa484bb5396c7e01e051034da8884bad1a1cb7f09bbe6be3f0282 F ext/icu/sqliteicu.h fa373836ed5a1ee7478bdf8a1650689294e41d0c89c1daab26e9ae78a32075a8 -F ext/jni/GNUmakefile 4e60cdca419ac6783719da98379480b6f04d5d1b5fa1408c46fcb0c32565c571 +F ext/jni/GNUmakefile c1893766d909a9748151a0d52a3c18eb6aa37d2aaba187eb5fe45df344f96d2a F ext/jni/README.md 1332b1fa27918bd5d9ca2d0d4f3ac3a6ab86b9e3699dc5bfe32904a027f3d2a9 F ext/jni/jar-dist.make 030aaa4ae71dd86e4ec5e7c1e6cd86f9dfa47c4592c070d2e35157e42498e1fa -F ext/jni/src/c/sqlite3-jni.c b5777fb1da4cd81bc91a051ebfa59ba28722536c4c98560350a7c219f9da265e +F ext/jni/src/c/sqlite3-jni.c 9440a767fd1019cffec368b3542af0c0a8c7c6da1a7c6f285dc19e07f670ced8 F ext/jni/src/c/sqlite3-jni.h a410d05ca47a676b75ff7b8980e75ad604ea15f3c29965f88989703abc2eeaf6 F ext/jni/src/org/sqlite/jni/AggregateFunction.java 0a5a74bea5ee12a99407e9432d0ca393525af912c2b0ca55c7ee5dbd019c00ef F ext/jni/src/org/sqlite/jni/AuthorizerCallback.java c374bb76409cce7a0bdba94877706b59ac6127fa5d9e6af3e8058c99ce99c030 @@ -2103,8 +2103,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 320a34c080d8bc1feae1578697923dfa7c4144b78de36f704c24cc4a4ce9d535 -R bd8a050119f034bd484ec7a8a853653c +P daede0f801f59d6501a863c4688e4635b34171e98b56b8ab4432c779113f1997 +R 6bd1f56d9b8c83901a03e2a9ce4434ac U stephan -Z 713c3c8622f9cf1b7dfe5fb5695c9634 +Z 362c439d994aeafc4f637309d1fc43bd # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index a04046908a..128decbd67 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -daede0f801f59d6501a863c4688e4635b34171e98b56b8ab4432c779113f1997 \ No newline at end of file +1ff78582bfd934e0c76464b5f23ed9bf09a3491b145e0ca34acb6e59c4f53995 \ No newline at end of file