]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
JNI: add sqlite3_bind_nio_buffer() and initial tests for binding ByteBuffer objects...
authorstephan <stephan@noemail.net>
Mon, 13 Nov 2023 14:58:37 +0000 (14:58 +0000)
committerstephan <stephan@noemail.net>
Mon, 13 Nov 2023 14:58:37 +0000 (14:58 +0000)
FossilOrigin-Name: b10ce1ef82d84726fbf6a8f624d6530f84fefb505f7868b4a0ea910fed7a877f

ext/jni/src/c/sqlite3-jni.c
ext/jni/src/c/sqlite3-jni.h
ext/jni/src/org/sqlite/jni/capi/CApi.java
ext/jni/src/org/sqlite/jni/capi/Tester1.java
manifest
manifest.uuid

index fafb2ab5fd343951087b62f6af48dce397ccd7bd..e3814825b191d631ee5d79d44ff31f30a57ed7ad 100644 (file)
@@ -870,6 +870,31 @@ static jbyte * s3jni__jbyteArray_bytes2(JNIEnv * const env, jbyteArray jBA, jsiz
 #define s3jni_jbyteArray_commit(jByteArray,jBytes) \
   if( jBytes ) (*env)->ReleaseByteArrayElements(env, jByteArray, jBytes, JNI_COMMIT)
 
+/*
+** If jbb is-a java.nio.Buffer object and the JNI environment
+** supports it, *pBuf is set to the buffer's memory and *pN is set to
+** its length. If jbb is NULL, not a Buffer, or the JNI environment
+** does not support that operation, *pBuf is set to 0 and *pN is set
+** to 0.
+**
+** Note that the length of the buffer can be larger than SQLITE_LIMIT
+** but this function does not know what byte range of the buffer is
+** required so cannot check for that violation. The caller is required
+** to ensure that any to-be-bind()ed range fits within SQLITE_LIMIT.
+ */
+/*static*/ void s3jni__get_nio_buffer(JNIEnv * const env, jobject jbb, void **pBuf, jint * pN ){
+  *pBuf = 0;
+  *pN = 0;
+  if( jbb ){
+    *pBuf = (*env)->GetDirectBufferAddress(env, jbb);
+    *pN = *pBuf ? (jint)(*env)->GetDirectBufferCapacity(env, jbb) : 0
+      /* why the Java limits the buffer length to int but the JNI API
+         uses a jlong for the length is a mystery. */;
+  }
+}
+#define s3jni_get_nio_buffer(JOBJ,vpOut,jpOut) \
+  s3jni__get_nio_buffer(env,(JOBJ),(vpOut),(jpOut))
+
 /*
 ** Returns the current JNIEnv object. Fails fatally if it cannot find
 ** the object.
@@ -1479,13 +1504,13 @@ static void * NativePointerHolder__get(JNIEnv * env, jobject jNph,
 ** argument is a Java sqlite3 object, as this operation only has void
 ** pointers to work with.
 */
-#define PtrGet_T(T,OBJ) (T*)NativePointerHolder_get(OBJ, S3JniNph(T))
-#define PtrGet_sqlite3(OBJ) PtrGet_T(sqlite3, OBJ)
-#define PtrGet_sqlite3_backup(OBJ) PtrGet_T(sqlite3_backup, OBJ)
-#define PtrGet_sqlite3_blob(OBJ) PtrGet_T(sqlite3_blob, OBJ)
-#define PtrGet_sqlite3_context(OBJ) PtrGet_T(sqlite3_context, OBJ)
-#define PtrGet_sqlite3_stmt(OBJ) PtrGet_T(sqlite3_stmt, OBJ)
-#define PtrGet_sqlite3_value(OBJ) PtrGet_T(sqlite3_value, OBJ)
+#define PtrGet_T(T,JOBJ) (T*)NativePointerHolder_get((JOBJ), S3JniNph(T))
+#define PtrGet_sqlite3(JOBJ) PtrGet_T(sqlite3, (JOBJ))
+#define PtrGet_sqlite3_backup(JOBJ) PtrGet_T(sqlite3_backup, (JOBJ))
+#define PtrGet_sqlite3_blob(JOBJ) PtrGet_T(sqlite3_blob, (JOBJ))
+#define PtrGet_sqlite3_context(JOBJ) PtrGet_T(sqlite3_context, (JOBJ))
+#define PtrGet_sqlite3_stmt(JOBJ) PtrGet_T(sqlite3_stmt, (JOBJ))
+#define PtrGet_sqlite3_value(JOBJ) PtrGet_T(sqlite3_value, (JOBJ))
 /*
 ** LongPtrGet_T(X,Y) expects X to be an unqualified sqlite3 struct
 ** type name and Y to be a native pointer to such an object in the
@@ -1505,12 +1530,12 @@ static void * NativePointerHolder__get(JNIEnv * env, jobject jNph,
 ** a difference of microseconds (i.e. below our testing measurement
 ** threshold) might add up.
 */
-#define LongPtrGet_T(T,JLongAsPtr) (T*)((intptr_t)(JLongAsPtr))
-#define LongPtrGet_sqlite3(JLongAsPtr) LongPtrGet_T(sqlite3,JLongAsPtr)
-#define LongPtrGet_sqlite3_backup(JLongAsPtr) LongPtrGet_T(sqlite3_backup,JLongAsPtr)
-#define LongPtrGet_sqlite3_blob(JLongAsPtr) LongPtrGet_T(sqlite3_blob,JLongAsPtr)
-#define LongPtrGet_sqlite3_stmt(JLongAsPtr) LongPtrGet_T(sqlite3_stmt,JLongAsPtr)
-#define LongPtrGet_sqlite3_value(JLongAsPtr) LongPtrGet_T(sqlite3_value,JLongAsPtr)
+#define LongPtrGet_T(T,JLongAsPtr) (T*)((intptr_t)((JLongAsPtr)))
+#define LongPtrGet_sqlite3(JLongAsPtr) LongPtrGet_T(sqlite3,(JLongAsPtr))
+#define LongPtrGet_sqlite3_backup(JLongAsPtr) LongPtrGet_T(sqlite3_backup,(JLongAsPtr))
+#define LongPtrGet_sqlite3_blob(JLongAsPtr) LongPtrGet_T(sqlite3_blob,(JLongAsPtr))
+#define LongPtrGet_sqlite3_stmt(JLongAsPtr) LongPtrGet_T(sqlite3_stmt,(JLongAsPtr))
+#define LongPtrGet_sqlite3_value(JLongAsPtr) LongPtrGet_T(sqlite3_value,(JLongAsPtr))
 /*
 ** Extracts the new S3JniDb instance from the free-list, or allocates
 ** one if needed, associates it with pDb, and returns.  Returns NULL
@@ -2408,6 +2433,34 @@ S3JniApi(sqlite3_bind_blob(),jint,1bind_1blob)(
   return (jint)rc;
 }
 
+S3JniApi(sqlite3_bind_nio_buffer(),jint,1bind_1nio_1buffer)(
+  JniArgsEnvClass, jobject jpStmt, jint ndx, jobject jBuffer,
+  jint iBegin, jint iN
+){
+  sqlite3_stmt * pStmt = PtrGet_sqlite3_stmt(jpStmt);
+  void * pBuf = 0;
+  jint nBuf = 0;
+  jlong iEnd = 0;
+  if( !SJG.g.cByteBuffer || !pStmt || iBegin<0 ){
+    return (jint)SQLITE_MISUSE;
+  }
+  s3jni_get_nio_buffer(jBuffer, &pBuf, &nBuf);
+  if( !pBuf || iBegin>=nBuf ){
+    return (jint)sqlite3_bind_null(pStmt, ndx);
+  }
+  assert( nBuf > 0 );
+  assert( iBegin < nBuf );
+  iEnd = iN<0 ? nBuf - iBegin : iBegin + iN;
+  if( iEnd>(jlong)nBuf ) iEnd = nBuf-iBegin;
+  if( iEnd-iBegin >(jlong)SQLITE_MAX_LENGTH ){
+    return SQLITE_MISUSE;
+  }
+  assert( iBegin>=0 );
+  assert( iEnd > iBegin );
+  return (jint)sqlite3_bind_blob(pStmt, (int)ndx, pBuf + iBegin,
+                                 (int)(iEnd - iBegin), SQLITE_TRANSIENT);
+}
+
 S3JniApi(sqlite3_bind_double(),jint,1bind_1double)(
   JniArgsEnvClass, jlong jpStmt, jint ndx, jdouble val
 ){
index 51d49bba3c8dfc15b8eff8bb3f1708d85b7feebd..f306c53fcb1785d7ab3a0d8f916bee57426ffee1 100644 (file)
@@ -881,6 +881,14 @@ JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1bind_1int64
 JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1bind_1java_1object
   (JNIEnv *, jclass, jlong, jint, jobject);
 
+/*
+ * Class:     org_sqlite_jni_capi_CApi
+ * Method:    sqlite3_bind_nio_buffer
+ * Signature: (Lorg/sqlite/jni/capi/sqlite3_stmt;ILjava/nio/ByteBuffer;II)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_capi_CApi_sqlite3_1bind_1nio_1buffer
+  (JNIEnv *, jclass, jobject, jint, jobject, jint, jint);
+
 /*
  * Class:     org_sqlite_jni_capi_CApi
  * Method:    sqlite3_bind_null
index 2dc238ce3e2fabaf3200e10750244904d23a3031..569d8f4a97b95baf7fcb22c6d053f5ef229e09b5 100644 (file)
@@ -215,7 +215,7 @@ public final class CApi {
      If n is negative, SQLITE_MISUSE is returned. If n>data.length
      then n is silently truncated to data.length.
   */
-  static int sqlite3_bind_blob(
+  public static int sqlite3_bind_blob(
     @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data, int n
   ){
     return sqlite3_bind_blob(stmt.getNativePointer(), ndx, data, n);
@@ -229,6 +229,28 @@ public final class CApi {
       : sqlite3_bind_blob(stmt.getNativePointer(), ndx, data, data.length);
   }
 
+  /**
+     Convenience overload which is a simple proxy for
+     sqlite3_bind_nio_buffer().
+  */
+  public static int sqlite3_bind_blob(
+    @NotNull sqlite3_stmt stmt, int ndx, @Nullable java.nio.ByteBuffer data,
+    int begin, int n
+  ){
+    return sqlite3_bind_nio_buffer(stmt, ndx, data, begin, n);
+  }
+
+  /**
+     Convenience overload which is equivalant to passing its arguments
+     to sqlite3_bind_nio_buffer() with the values 0 and -1 for the
+     final two arguments.
+  */
+  public static int sqlite3_bind_blob(
+    @NotNull sqlite3_stmt stmt, int ndx, @Nullable java.nio.ByteBuffer data
+  ){
+    return sqlite3_bind_nio_buffer(stmt, ndx, data, 0, -1);
+  }
+
   private static native int sqlite3_bind_double(
     @NotNull long ptrToStmt, int ndx, double v
   );
@@ -261,6 +283,57 @@ public final class CApi {
     @NotNull long ptrToStmt, int ndx, @Nullable Object o
   );
 
+  /**
+     Binds the contents of the given buffer object as a blob.
+
+     The byte range of the buffer may be restricted by providing a
+     start index and a number of bytes. beginPos may not be negative
+     but a negative howMany is interpretated as the remainder of the
+     buffer past the given start position.
+
+     If beginPos+howMany would extend past the end of the buffer, the
+     range is silently truncated to fit the buffer.
+
+     If any of the following are true, this function behaves like
+     sqlite3_bind_null(): the buffer is null, beginPos is past the end
+     of the buffer, howMany is 0, or the calculated slice of the blob
+     has a length of 0.
+
+     If ndx is out of range, it returns SQLITE_RANGE, as documented
+     for sqlite3_bind_blob().  If any other arguments are invalid or
+     if sqlite3_jni_supports_nio() is false then SQLITE_MISUSE is
+     returned.  Note that this function is bound (as it were) by the
+     SQLITE_LIMIT_LENGTH constraint and SQLITE_MISUSE is returned if
+     that's violated.
+
+     This function does not modify the buffer's streaming-related
+     cursors.
+
+     If the buffer is modified in a separate thread while this
+     operation is running, results are undefined and will likely
+     result in corruption of the bound data or a segmentation fault.
+
+     Design note: this function should arguably take a java.nio.Buffer
+     instead of ByteBuffer, but it can only operate on "direct"
+     buffers and the only such class offered by Java is (apparently)
+     ByteBuffer.
+
+     @see https://docs.oracle.com/javase/8/docs/api/java/nio/Buffer.html
+  */
+  public static native int sqlite3_bind_nio_buffer(
+    @NotNull sqlite3_stmt stmt, int ndx, @Nullable java.nio.ByteBuffer data,
+    int beginPos, int howMany
+  );
+
+  /**
+     Convenience overload which binds the given buffer's entire contents.
+  */
+  public static int sqlite3_bind_nio_buffer(
+    @NotNull sqlite3_stmt stmt, int ndx, @Nullable java.nio.ByteBuffer data
+  ){
+    return sqlite3_bind_nio_buffer(stmt, ndx, data, 0, -1);
+  }
+
   /**
      Binds the given object at the given index. If o is null then this behaves like
      sqlite3_bind_null().
index 3ac58c67d3e27096ae4d80c27b0a75eab41d5f67..0587eb631835c3b6fecaa4b3d3ae4f21ca23d7c7 100644 (file)
@@ -38,6 +38,14 @@ import java.util.concurrent.Future;
 @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD})
 @interface SingleThreadOnly{}
 
+/**
+   Annotation for Tester1 tests which must only be run if JNI-level support for
+   java.nio.Buffer is available.
+*/
+@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
+@java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD})
+@interface RequiresNioBuffer{}
+
 public class Tester1 implements Runnable {
   //! True when running in multi-threaded mode.
   private static boolean mtMode = false;
@@ -557,6 +565,45 @@ public class Tester1 implements Runnable {
     sqlite3_close_v2(db);
   }
 
+  @RequiresNioBuffer
+  private void testBindByteBuffer(){
+    sqlite3 db = createNewDb();
+    execSql(db, "CREATE TABLE t(a)");
+    sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);");
+    java.nio.ByteBuffer buf = java.nio.ByteBuffer.allocateDirect(10);
+    buf.put((byte)0x31)/*note that we'll skip this one*/
+      .put((byte)0x32)
+      .put((byte)0x33)
+      .put((byte)0x34);
+    int rc = sqlite3_bind_blob(stmt, 1, buf, -1, 0);
+    affirm( SQLITE_MISUSE==rc );
+    rc = sqlite3_bind_blob(stmt, 1, buf, 1, 3);
+    affirm( 0==rc );
+    rc = sqlite3_step(stmt);
+    affirm(SQLITE_DONE == rc);
+    sqlite3_finalize(stmt);
+    stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;");
+    int n = 0;
+    int total = 0;
+    while( SQLITE_ROW == sqlite3_step(stmt) ){
+      byte[] blob = sqlite3_column_blob(stmt, 0);
+      affirm(3 == blob.length);
+      int i = 0;
+      for(byte b : blob){
+        affirm( i<=3 );
+        affirm(b == buf.get(1 + i++));
+        total += b;
+      }
+      ++n;
+    }
+    sqlite3_finalize(stmt);
+    affirm(1 == n);
+    affirm(total == 0x32 + 0x33 + 0x34);
+    /* TODO: these tests need to be much more extensive to check the
+       begin range handling. */
+    sqlite3_close_v2(db);
+  }
+
   private void testSql(){
     sqlite3 db = createNewDb();
     sqlite3_stmt stmt = prepare(db, "SELECT 1");
@@ -1877,18 +1924,19 @@ public class Tester1 implements Runnable {
           if( forceFail ){
             testMethods.add(m);
           }
+        }else if( m.isAnnotationPresent( RequiresNioBuffer.class )
+                  && !sqlite3_jni_supports_nio() ){
+          outln("Skipping test for lack JNI nio.Buffer support: ",name,"()\n");
+          ++nSkipped;
         }else if( !m.isAnnotationPresent( ManualTest.class ) ){
           if( nThread>1 && m.isAnnotationPresent( SingleThreadOnly.class ) ){
-            if( 0==nSkipped++ ){
-              out("Skipping tests in multi-thread mode:");
-            }
-            out(" "+name+"()");
+            out("Skipping test in multi-thread mode: ",name,"()\n");
+            ++nSkipped;
           }else if( name.startsWith("test") ){
             testMethods.add(m);
           }
         }
       }
-      if( nSkipped>0 ) out("\n");
     }
 
     final long timeStart = System.currentTimeMillis();
index a6ae666a58df7f7f7bde8420a93a22bfe23d0765..47b34117a71b61ccf7779258a1a2b01a1febadf5 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C JNI\swrapper1:\swhen\schecking\sfor\san\sout-of-bounds\sstatement\scolumn\sindex,\sperform\sthe\sis-statement-finalized\scheck\sbefore\sthe\srange\scheck\sso\sthat\sthe\sformer\sexception\strumps\sthe\slatter.
-D 2023-11-11T14:50:01.933
+C JNI:\sadd\ssqlite3_bind_nio_buffer()\sand\sinitial\stests\sfor\sbinding\sByteBuffer\sobjects\sas\sblobs\son\sJVMs\swhich\shave\sJNI\ssupport\sfor\snio\sbuffers.
+D 2023-11-13T14:58:37.421
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
 F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
@@ -241,8 +241,8 @@ F ext/icu/sqliteicu.h fa373836ed5a1ee7478bdf8a1650689294e41d0c89c1daab26e9ae78a3
 F ext/jni/GNUmakefile f2f3a31923293659b95225e932a286af1f2287d75bf88ad6c0fd1b9d9cd020d4
 F ext/jni/README.md ef9ac115e97704ea995d743b4a8334e23c659e5534c3b64065a5405256d5f2f4
 F ext/jni/jar-dist.make 030aaa4ae71dd86e4ec5e7c1e6cd86f9dfa47c4592c070d2e35157e42498e1fa
-F ext/jni/src/c/sqlite3-jni.c 3774703e5865e7ff776b762de5386af8aa703e569bbb3a85c423c3f8473a3c26
-F ext/jni/src/c/sqlite3-jni.h 891444578550a7aa69fe5e0dedb3e6dedad752501ba99801f17797be51796934
+F ext/jni/src/c/sqlite3-jni.c a04d0d77e7391a69f7f8ca4b38e24de59b3a8f61610f2e91698c190c07283850
+F ext/jni/src/c/sqlite3-jni.h 2848299f845d36b4b6123d360e7a4eb960d040637a10158079af49f4ded16453
 F ext/jni/src/org/sqlite/jni/annotation/NotNull.java 02091a8112e33389f1c160f506cd413168c8dfacbeda608a4946c6e3557b7d5a
 F ext/jni/src/org/sqlite/jni/annotation/Nullable.java 0b1879852707f752512d4db9d7edd0d8db2f0c2612316ce1c832715e012ff6ba
 F ext/jni/src/org/sqlite/jni/annotation/package-info.java 977b374aed9d5853cbf3438ba3b0940abfa2ea4574f702a2448ee143b98ac3ca
@@ -251,7 +251,7 @@ F ext/jni/src/org/sqlite/jni/capi/AggregateFunction.java 0b72cdff61533b564d65b63
 F ext/jni/src/org/sqlite/jni/capi/AuthorizerCallback.java c045a5b47e02bb5f1af91973814a905f12048c428a3504fbc5266d1c1be3de5a
 F ext/jni/src/org/sqlite/jni/capi/AutoExtensionCallback.java 74cc4998a73d6563542ecb90804a3c4f4e828cb4bd69e61226d1a51f4646e759
 F ext/jni/src/org/sqlite/jni/capi/BusyHandlerCallback.java 7b8e19810c42b0ad21a04b5d8c804b32ee5905d137148703f16a75b612c380ca
-F ext/jni/src/org/sqlite/jni/capi/CApi.java bd4a6490548f913bf9719443dee3d8a233f920ed1614b622738527d746e00f5d
+F ext/jni/src/org/sqlite/jni/capi/CApi.java 5ef54290c17dca46d7f24001ac3b689559e1b37ee40d06b88fa5315d64863789
 F ext/jni/src/org/sqlite/jni/capi/CallbackProxy.java 57e2d275dcebe690b1fc1f3d34eb96879b2d7039bce30b563aee547bf45d8a8b
 F ext/jni/src/org/sqlite/jni/capi/CollationCallback.java e29bcfc540fdd343e2f5cca4d27235113f2886acb13380686756d5cabdfd065a
 F ext/jni/src/org/sqlite/jni/capi/CollationNeededCallback.java 5bfa226a8e7a92e804fd52d6e42b4c7b875fa7a94f8e2c330af8cc244a8920ab
@@ -269,7 +269,7 @@ F ext/jni/src/org/sqlite/jni/capi/SQLFunction.java 0d1e9afc9ff8a2adb94a155b72385
 F ext/jni/src/org/sqlite/jni/capi/SQLTester.java 09bee15aa0eedac68d767ae21d9a6a62a31ade59182a3ccbf036d6463d9e30b1
 F ext/jni/src/org/sqlite/jni/capi/ScalarFunction.java 93b9700fca4c68075ccab12fe0fbbc76c91cafc9f368e835b9bd7cd7732c8615
 F ext/jni/src/org/sqlite/jni/capi/TableColumnMetadata.java addf120e0e76e5be1ff2260daa7ce305ff9b5fafd64153a7a28e9d8f000a815f
-F ext/jni/src/org/sqlite/jni/capi/Tester1.java b1a0c015d92a8d0c07a8f6751e9b057557cec9d803e002d48ee5f3b9963abd55
+F ext/jni/src/org/sqlite/jni/capi/Tester1.java 4ec21172917f641787767443f418854329bf9b9779807b644e000dac1ec77013
 F ext/jni/src/org/sqlite/jni/capi/TraceV2Callback.java 0a25e117a0daae3394a77f24713e36d7b44c67d6e6d30e9e1d56a63442eef723
 F ext/jni/src/org/sqlite/jni/capi/UpdateHookCallback.java c8bdf7848e6599115d601bcc9427ff902cb33129b9be32870ac6808e04b6ae56
 F ext/jni/src/org/sqlite/jni/capi/ValueHolder.java 22d365746a78c5cd7ae10c39444eb7bbf1a819aad4bb7eb77b1edc47773a3950
@@ -2139,8 +2139,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 a6ab88e9a67f23ab7885402776282b94033cb48dbe34d4d18356e4dc22aae7cd
-R 78a63cfa87011ebb24c6cac260db14c9
+P 0832f9a8e9f574b157c791c5cddc73aff7b2ff403509f5d78f310494d4a7f93d
+R 7292f27854539ba720bc8a8fcd0c273f
 U stephan
-Z ad92dad6ace48bd20f7cf67cbd6f4f40
+Z 1881d5bf6b59348a7807616e897efc0f
 # Remove this line to create a well-formed Fossil manifest.
index 4865106033244019578183bf2f4b9a9c8a99e2e5..693d450f8a3871e3ad51d9a5c96ea19d1d403dc2 100644 (file)
@@ -1 +1 @@
-0832f9a8e9f574b157c791c5cddc73aff7b2ff403509f5d78f310494d4a7f93d
\ No newline at end of file
+b10ce1ef82d84726fbf6a8f624d6530f84fefb505f7868b4a0ea910fed7a877f
\ No newline at end of file