]> git.ipfire.org Git - thirdparty/sqlite.git/commitdiff
Remove JNI sqlite3_column_java_object(), as the protection rules of sqlite3_values...
authorstephan <stephan@noemail.net>
Sun, 27 Aug 2023 13:43:45 +0000 (13:43 +0000)
committerstephan <stephan@noemail.net>
Sun, 27 Aug 2023 13:43:45 +0000 (13:43 +0000)
FossilOrigin-Name: 29bd4a23a4afd96b2cc06d2b91a4f30c0bbf2347af0b0d18f8d4cf8aafa63160

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

index 39feafc8a713b6d1a62378eebd99f25b1201a1ff..a24ed25180d85772efed9528bde60d2b4655f10c 100644 (file)
@@ -1260,14 +1260,16 @@ static S3JniNphClass * S3JniGlobal_nph(JNIEnv * const env, S3NphRef const* pRef)
 ** Returns the ID of the "nativePointer" field from the given
 ** NativePointerHolder<T> class.
 */
-static jfieldID NativePointerHolder_field(JNIEnv * const env, S3NphRef const* pRef){
+static jfieldID NativePointerHolder_field(JNIEnv * const env,
+                                          S3NphRef const* pRef){
   S3JniNphClass * const pNC = S3JniGlobal_nph(env, pRef);
   if( !pNC->fidValue ){
     S3JniMutex_Nph_enter;
     if( !pNC->fidValue ){
       pNC->fidValue = (*env)->GetFieldID(env, pNC->klazz,
                                          pRef->zMember, pRef->zTypeSig);
-      S3JniExceptionIsFatal("Code maintenance required: missing nativePointer field.");
+      S3JniExceptionIsFatal("Code maintenance required: missing "
+                            "nativePointer field.");
     }
     S3JniMutex_Nph_leave;
   }
@@ -1291,7 +1293,8 @@ static void NativePointerHolder_set(JNIEnv * env, S3NphRef const* pRef,
 ** zClassName must be a static string so we can use its address as a
 ** cache key. This is a no-op if pObj is NULL.
 */
-static void * NativePointerHolder_get(JNIEnv * env, jobject pObj, S3NphRef const* pRef){
+static void * NativePointerHolder_get(JNIEnv * env, jobject pObj,
+                                      S3NphRef const* pRef){
   if( pObj ){
     void * const rv = (void*)(*env)->GetLongField(
       env, pObj, NativePointerHolder_field(env, pRef)
@@ -1344,29 +1347,17 @@ static S3JniDb * S3JniDb_alloc(JNIEnv * const env, jobject jDb){
 }
 
 /*
-** Returns the S3JniDb object for the given db. At most, one of jDb or
-** pDb may be non-NULL.
+** Returns the S3JniDb object for the given org.sqlite.jni.sqlite3
+** object, or NULL if jDb is NULL, no pointer can be extracted
+** from it, or no matching entry can be found.
 **
-** The 3rd argument should normally only be non-0 for routines which
-** are called from the C library and pass a native db handle instead of
-** a Java handle. In normal usage, the 2nd argument is a Java-side sqlite3
-** object, from which the db is fished out.
-**
-** If the lockMutex argument is true then the S3JniDb mutex is locked
-** before starting work, else the caller is required to have locked
-** it.
-**
-** Returns NULL if jDb and pDb are both NULL or if there is no
-** matching S3JniDb entry for pDb or the pointer fished out of jDb.
+** Requires locking the S3JniDb mutex.
 */
-static S3JniDb * S3JniDb_get(JNIEnv * const env, jobject jDb, sqlite3 *pDb){
+static S3JniDb * S3JniDb__from_java(JNIEnv * const env, jobject jDb){
   S3JniDb * s = 0;
-
-  if( 0==jDb && 0==pDb ) return 0;
-  assert( jDb ? !pDb : !!pDb );
+  sqlite3 * pDb = 0;
   S3JniMutex_S3JniDb_enter;
   if( jDb ){
-    assert(!pDb);
     pDb = PtrGet_sqlite3(jDb);
   }
   s = SJG.perDb.aHead;
@@ -1379,8 +1370,28 @@ static S3JniDb * S3JniDb_get(JNIEnv * const env, jobject jDb, sqlite3 *pDb){
   return s;
 }
 
-#define S3JniDb_from_java(jObject) S3JniDb_get(env,(jObject),0)
-#define S3JniDb_from_c(sqlite3Ptr) S3JniDb_get(env,0,(sqlite3Ptr))
+/*
+** Returns the S3JniDb object for the sqlite3 object, or NULL if pDb
+** is NULL, or no matching entry
+** can be found.
+**
+** Requires locking the S3JniDb mutex.
+*/
+static S3JniDb * S3JniDb__from_c(JNIEnv * const env, sqlite3 *pDb){
+  S3JniDb * s = 0;
+  S3JniMutex_S3JniDb_enter;
+  s = SJG.perDb.aHead;
+  for( ; pDb && s; s = s->pNext){
+    if( s->pDb == pDb ){
+      break;
+    }
+  }
+  S3JniMutex_S3JniDb_leave;
+  return s;
+}
+
+#define S3JniDb_from_java(jObject) S3JniDb__from_java(env,(jObject))
+#define S3JniDb_from_c(sqlite3Ptr) S3JniDb__from_c(env,(sqlite3Ptr))
 
 /*
 ** Unref any Java-side state in (S3JniAutoExtension*) AX and zero out
@@ -2181,6 +2192,29 @@ S3JniApi(sqlite3_bind_int64(),jint,1bind_1int64)(
   return (jint)sqlite3_bind_int64(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, (sqlite3_int64)val);
 }
 
+/*
+** Bind a new global ref to Object `val` using sqlite3_bind_pointer().
+*/
+S3JniApi(sqlite3_bind_java_object(),jint,1bind_1java_1object)(
+  JniArgsEnvClass, jobject jpStmt, jint ndx, jobject val
+){
+  sqlite3_stmt * const pStmt = PtrGet_sqlite3_stmt(jpStmt);
+  int rc = 0;
+
+  if(pStmt){
+    jobject const rv = val ? S3JniRefGlobal(val) : 0;
+    if( rv ){
+      rc = sqlite3_bind_pointer(pStmt, ndx, rv, ResultJavaValuePtrStr,
+                                ResultJavaValue_finalizer);
+    }else if(val){
+      rc = SQLITE_NOMEM;
+    }
+  }else{
+    rc = SQLITE_MISUSE;
+  }
+  return rc;
+}
+
 S3JniApi(sqlite3_bind_null(),jint,1bind_1null)(
   JniArgsEnvClass, jobject jpStmt, jint ndx
 ){
@@ -2337,7 +2371,9 @@ static jint s3jni_close_db(JNIEnv * const env, jobject jDb, int version){
   S3JniDb * const ps = S3JniDb_from_java(jDb);
   assert(version == 1 || version == 2);
   if( ps ){
-    rc = 1==version ? (jint)sqlite3_close(ps->pDb) : (jint)sqlite3_close_v2(ps->pDb);
+    rc = 1==version
+      ? (jint)sqlite3_close(ps->pDb)
+      : (jint)sqlite3_close_v2(ps->pDb);
     if( 0==rc ){
       S3JniDb_set_aside(env, ps)
         /* MUST come after close() because of ps->trace. */;
index a4fc6619c2649604f60ae9035eda3064d1963493..a588914238256c1e082da7a488a3e8e290165f1e 100644 (file)
@@ -819,6 +819,14 @@ JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1int
 JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1int64
   (JNIEnv *, jclass, jobject, jint, jlong);
 
+/*
+ * Class:     org_sqlite_jni_SQLite3Jni
+ * Method:    sqlite3_bind_java_object
+ * Signature: (Lorg/sqlite/jni/sqlite3_stmt;ILjava/lang/Object;)I
+ */
+JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1java_1object
+  (JNIEnv *, jclass, jobject, jint, jobject);
+
 /*
  * Class:     org_sqlite_jni_SQLite3Jni
  * Method:    sqlite3_bind_null
index b53e921b6523036ccd20433860605a8738cf717f..1ad48250e5831ffa1a756ebc6992bf737e0204b0 100644 (file)
@@ -184,6 +184,15 @@ public final class SQLite3Jni {
     @NotNull sqlite3_stmt stmt, int ndx, long v
   );
 
+  /**
+     Binds the given object at the given index.
+
+     @see #sqlite3_result_java_object
+  */
+  public static native int sqlite3_bind_java_object(
+    @NotNull sqlite3_stmt cx, int ndx, @Nullable Object o
+  );
+
   public static native int sqlite3_bind_null(
     @NotNull sqlite3_stmt stmt, int ndx
   );
@@ -210,7 +219,7 @@ public final class SQLite3Jni {
      Works like the C-level sqlite3_bind_text() but assumes
      SQLITE_TRANSIENT for the final C API parameter.
 
-     Results are undefined if data is not null and
+     <p>Results are undefined if data is not null and
      maxBytes>=data.length. If maxBytes is negative then results are
      undefined if data is not null and does not contain a NUL byte.
   */
@@ -358,35 +367,6 @@ public final class SQLite3Jni {
     @NotNull sqlite3_stmt stmt, int ndx
   );
 
-  /**
-     Column counterpart of sqlite3_value_java_object().
-  */
-  public static Object sqlite3_column_java_object(
-    @NotNull sqlite3_stmt stmt, int ndx
-  ){
-    Object rv = null;
-    sqlite3_value v = sqlite3_column_value(stmt, ndx);
-    if(null!=v){
-      v = sqlite3_value_dup(v) /* we need a "protected" value */;
-      if(null!=v){
-        rv = sqlite3_value_java_object(v);
-        sqlite3_value_free(v);
-      }
-    }
-    return rv;
-  }
-
-  /**
-     Column counterpart of sqlite3_value_java_casted().
-  */
-  @SuppressWarnings("unchecked")
-  public static <T> T sqlite3_column_java_casted(
-    @NotNull sqlite3_stmt stmt, int ndx, @NotNull Class<T> type
-  ){
-    final Object o = sqlite3_column_java_object(stmt, ndx);
-    return type.isInstance(o) ? (T)o : null;
-  }
-
   public static native String sqlite3_column_origin_name(
     @NotNull sqlite3_stmt stmt, int ndx
   );
@@ -614,7 +594,7 @@ public final class SQLite3Jni {
      heed. Passing the object to sqlite3_close() or sqlite3_close_v2()
      will clear that pointer mapping.
 
-     Recall that even if opening fails, the output pointer might be
+     <p>Recall that even if opening fails, the output pointer might be
      non-null. Any error message about the failure will be in that
      object and it is up to the caller to sqlite3_close() that
      db handle.
@@ -628,7 +608,7 @@ public final class SQLite3Jni {
      object might not have been successfully opened: use sqlite3_errcode() to
      check whether it is in an error state.
 
-     Ownership of the returned value is passed to the caller, who must eventually
+     <p>Ownership of the returned value is passed to the caller, who must eventually
      pass it to sqlite3_close() or sqlite3_close_v2().
   */
   public static sqlite3 sqlite3_open(@Nullable String filename){
@@ -659,10 +639,10 @@ public final class SQLite3Jni {
      retain functionally equivalent semantics and (B) overloading
      allows us to install several convenience forms.
 
-     All of them which take their SQL in the form of a byte[] require
+     <p>All of them which take their SQL in the form of a byte[] require
      that it be in UTF-8 encoding unless explicitly noted otherwise.
 
-     The forms which take a "tail" output pointer return (via that
+     <p>The forms which take a "tail" output pointer return (via that
      output object) the index into their SQL byte array at which the
      end of the first SQL statement processed by the call was
      found. That's fundamentally how the C APIs work but making use of
@@ -959,19 +939,22 @@ public final class SQLite3Jni {
   /**
      Binds the SQL result to the given object, or
      {@link #sqlite3_result_null} if {@code o} is null. Use
-     {@link #sqlite3_value_java_object(sqlite3_value) sqlite3_value_java_object()} or
-     {@link #sqlite3_column_java_object(sqlite3_stmt,int) sqlite3_column_java_object()} to
+     {@link #sqlite3_value_java_object(sqlite3_value) sqlite3_value_java_object()} to
      fetch it.
 
-     This is implemented in terms of sqlite3_result_pointer(), but
-     that function is not exposed to JNI because its 3rd argument must
-     be a constant string (the library does not copy it), which we
-     cannot implement cross-language here unless, in the JNI layer, we
-     allocate such strings and store them somewhere for long-term use
-     (leaking them more likely than not). Even then, passing around a
-     pointer via Java like that has little practical use.
+     <p>This is implemented in terms of C's sqlite3_result_pointer(),
+     but that function is not exposed to JNI because its 3rd argument
+     must be a constant string (the library does not copy it), and
+     those semantics are cumbersome to bridge cross-language. Java
+     doesn't need that argument for type safety, in any case: the
+     object can, after extraction on the other end of the API, be
+     inspected with {@code instanceof}.
+
+     <p>Note that there is no sqlite3_column_java_object(), as the
+     C-level API has no sqlite3_column_pointer() to proxy.
 
-     Note that there is no sqlite3_bind_java_object() counterpart.
+     @see #sqlite3_value_java_object
+     @see #sqlite3_bind_java_object
   */
   public static native void sqlite3_result_java_object(
     @NotNull sqlite3_context cx, @NotNull Object o
@@ -1056,12 +1039,17 @@ public final class SQLite3Jni {
   /**
      Binds the given text using C's sqlite3_result_blob64() unless:
 
-     - @param blob is null ==> sqlite3_result_null()
+     <ul>
+
+     <li>@param blob is null: translates to sqlite3_result_null()</li>
 
-     - @param blob is too large ==> sqlite3_result_error_toobig()
+     <li>@param blob is too large: translates to
+     sqlite3_result_error_toobig()</li>
 
-     If @param maxLen is larger than blob.length, it is truncated to that
-     value. If it is negative, results are undefined.
+     </ul>
+
+     If @param maxLen is larger than blob.length, it is truncated to
+     that value. If it is negative, results are undefined.
   */
   private static native void sqlite3_result_blob64(
     @NotNull sqlite3_context cx, @Nullable byte[] blob, long maxLen
@@ -1096,17 +1084,21 @@ public final class SQLite3Jni {
   /**
      Binds the given text using C's sqlite3_result_text64() unless:
 
-     - text is null: translates to a call to sqlite3_result_null()
+     <ul>
+
+     <li>text is null: translates to a call to sqlite3_result_null()</li>
+
+     <li>text is too large: translates to a call to
+     {@link #sqlite3_result_error_toobig}</li>
 
-     - text is too large: translates to a call to
-       sqlite3_result_error_toobig()
+     <li>The @param encoding argument has an invalid value: translates to
+     {@link sqlite3_result_error_code} with code SQLITE_FORMAT.</li>
 
-       - The @param encoding argument has an invalid value: translates to
-       sqlite3_result_error_code() with code SQLITE_FORMAT.
+     </ul>
 
      If maxLength (in bytes, not characters) is larger than
      text.length, it is silently truncated to text.length. If it is
-     negative, results are undefined. If text is null, the following
+     negative, results are undefined. If text is null, the subsequent
      arguments are ignored.
   */
   private static native void sqlite3_result_text64(
@@ -1224,7 +1216,7 @@ public final class SQLite3Jni {
      function is elided here because the roles of that functions' 3rd and 4th
      arguments are encapsulated in the final argument to this function.
 
-     Unlike the C API, which is documented as always returning 0, this
+     <p>Unlike the C API, which is documented as always returning 0, this
      implementation returns non-0 if initialization of the tracer
      mapping state fails.
   */
@@ -1257,10 +1249,11 @@ public final class SQLite3Jni {
   public static native long sqlite3_value_int64(@NotNull sqlite3_value v);
 
   /**
-     If the given value was set using sqlite3_result_java_value() then
-     this function returns that object, else it returns null.
+     If the given value was set using {@link
+     #sqlite3_result_java_object} then this function returns that
+     object, else it returns null.
 
-     It is up to the caller to inspect the object to determine its
+     <p>It is up to the caller to inspect the object to determine its
      type, and cast it if necessary.
   */
   public static native Object sqlite3_value_java_object(
index 2647575ee52f4c31c0727600121c8c3f4d3cd054..a8e171233137b162645912d6a32b1731f955d012 100644 (file)
@@ -695,16 +695,20 @@ public class Tester1 implements Runnable {
   private void testUdfJavaObject(){
     final sqlite3 db = createNewDb();
     final ValueHolder<sqlite3> testResult = new ValueHolder<>(db);
+    final ValueHolder<Integer> boundObj = new ValueHolder<>(42);
     final SQLFunction func = new ScalarFunction(){
         public void xFunc(sqlite3_context cx, sqlite3_value args[]){
           sqlite3_result_java_object(cx, testResult.value);
+          affirm( sqlite3_value_java_object(args[0]) == boundObj );
         }
       };
     int rc = sqlite3_create_function(db, "myfunc", -1, SQLITE_UTF8, func);
     affirm(0 == rc);
-    final sqlite3_stmt stmt = prepare(db, "select myfunc()");
+    sqlite3_stmt stmt = prepare(db, "select myfunc(?)");
     affirm( 0 != stmt.getNativePointer() );
     affirm( testResult.value == db );
+    rc = sqlite3_bind_java_object(stmt, 1, boundObj);
+    affirm( 0==rc );
     int n = 0;
     if( SQLITE_ROW == sqlite3_step(stmt) ){
       final sqlite3_value v = sqlite3_column_value(stmt, 0);
@@ -1346,7 +1350,7 @@ public class Tester1 implements Runnable {
     affirm( 8 == val.value );
   }
 
-  @ManualTest /* because we only want to run this test manually */
+  @ManualTest /* we really only want to run this test manually. */
   private void testSleep(){
     out("Sleeping briefly... ");
     sqlite3_sleep(600);
index 9806221478b888ad72d5577e805e8ff2844f2f72..663f1823abb9745a62ffe36979d14ac8fdf05013 100644 (file)
--- a/manifest
+++ b/manifest
@@ -1,5 +1,5 @@
-C Correct\sthe\ssignature\smismatch\sbetween\sJNI\ssqlite3_column/value_text16()\sand\sadd\srelated\stests.
-D 2023-08-27T11:28:57.220
+C Remove\sJNI\ssqlite3_column_java_object(),\sas\sthe\sprotection\srules\sof\ssqlite3_values\smakes\sit\simpossible\sto\simplement\ssafely.\sAdd\sJNI\ssqlite3_bind_java_object().
+D 2023-08-27T13:43:45.514
 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
 F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
@@ -236,8 +236,8 @@ F ext/icu/sqliteicu.h fa373836ed5a1ee7478bdf8a1650689294e41d0c89c1daab26e9ae78a3
 F ext/jni/GNUmakefile 527f7c72360ba081c9ad120a9a00834973dac0115c6272fad94963651ed15bab
 F ext/jni/README.md 1332b1fa27918bd5d9ca2d0d4f3ac3a6ab86b9e3699dc5bfe32904a027f3d2a9
 F ext/jni/jar-dist.make 030aaa4ae71dd86e4ec5e7c1e6cd86f9dfa47c4592c070d2e35157e42498e1fa
-F ext/jni/src/c/sqlite3-jni.c fd9f52e536528ffaa4a98a8eebd0e15e438b3289e8f00947abcf011d5c8f9afb
-F ext/jni/src/c/sqlite3-jni.h c035d576158137e620da870eef685e6d96ade54565817fe3988fd209514eace1
+F ext/jni/src/c/sqlite3-jni.c 0078fdd79d31db6184a961bdf79d4c9d1138e78f6705bce7e35e17531365fbdf
+F ext/jni/src/c/sqlite3-jni.h 9c57a6e7efd466c4f96b190cea995353ff8897ed38fefb279b42913f352f73a6
 F ext/jni/src/org/sqlite/jni/AggregateFunction.java 0a5a74bea5ee12a99407e9432d0ca393525af912c2b0ca55c7ee5dbd019c00ef
 F ext/jni/src/org/sqlite/jni/AuthorizerCallback.java c374bb76409cce7a0bdba94877706b59ac6127fa5d9e6af3e8058c99ce99c030
 F ext/jni/src/org/sqlite/jni/AutoExtensionCallback.java 4290d8b0937b07d466b50e6ca4136cec037f3ce658277af0d0c2d371e5f4b459
@@ -262,9 +262,9 @@ F ext/jni/src/org/sqlite/jni/ResultCode.java ba701f20213a5f259e94cfbfdd36eb7ac7c
 F ext/jni/src/org/sqlite/jni/RollbackHookCallback.java be7f7a26d1102fb514d835e11198d51302af8053d97188bfb2e34c2133208568
 F ext/jni/src/org/sqlite/jni/SQLFunction.java d060f302b2cc4cf7a4f5a6b2d36458a2e6fc9648374b5d09c36a43665af41207
 F ext/jni/src/org/sqlite/jni/SQLite3CallbackProxy.java 13c4ea6f35871261eba63fa4117715515e0beecbdebfb879ec5b1f340ed36904
-F ext/jni/src/org/sqlite/jni/SQLite3Jni.java 2f45ac5e5fcfc03b8be6d3385a6a5a11fff40ba29735d6fde00b686d878017fe
+F ext/jni/src/org/sqlite/jni/SQLite3Jni.java b22b02b51eab5bab2e9068757c94c0978a7c2ccaf4c2ffa93b3b958c2f9e7dc9
 F ext/jni/src/org/sqlite/jni/ScalarFunction.java 21301a947e49f0dd9c682dfe2cc8a6518226c837253dd791cd512f847eeca52c
-F ext/jni/src/org/sqlite/jni/Tester1.java ec5622933b896679a1297db6ed70e0619149913c0043a063c3723ee4645f1c8a
+F ext/jni/src/org/sqlite/jni/Tester1.java ff13dc4babfa7a3f8bf91862c12a7f837db611bb66fcd82f7f8728c3e297a188
 F ext/jni/src/org/sqlite/jni/TesterFts5.java 6f135c60e24c89e8eecb9fe61dde0f3bb2906de668ca6c9186bcf34bdaf94629
 F ext/jni/src/org/sqlite/jni/TraceV2Callback.java 25a45e800b0c57f506c237d111bcfd09da584e936fee395d4bd802100ebeff8c
 F ext/jni/src/org/sqlite/jni/UpdateHookCallback.java f5eadfa44462c050658230884b41477274f34306accd85c8201a7afbc00d2429
@@ -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 3f9f7a9cb08b0687ad206605a5109306762df9ae8bdeab2d8d60bf9373c9ad32
-R e2ef8e56417f07ac504c27650de35a3d
+P 77f6e70f17c0cb6d031f983c458c9ec2e88d92b4716397533a029af39da2d128
+R 6ff41c97b17d551a7d85c6dd457042a1
 U stephan
-Z 3bfd1e616266221f5a840c6c97fab6d0
+Z 98f8ce6b232c9712186c8841f85ec5e1
 # Remove this line to create a well-formed Fossil manifest.
index 4be733d0913a57f30da3cff442d6a94420d0f5c0..04a026cc7d45b8c82903b381f9f6d8f92d17a81d 100644 (file)
@@ -1 +1 @@
-77f6e70f17c0cb6d031f983c458c9ec2e88d92b4716397533a029af39da2d128
\ No newline at end of file
+29bd4a23a4afd96b2cc06d2b91a4f30c0bbf2347af0b0d18f8d4cf8aafa63160
\ No newline at end of file