react-native-nitro-storage 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +71 -11
  3. package/SECURITY.md +2 -2
  4. package/android/build.gradle +5 -0
  5. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +12 -13
  6. package/android/src/main/cpp/JniSize.hpp +21 -0
  7. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +124 -16
  8. package/android/src/main/java/com/nitrostorage/DiskSqliteStore.kt +160 -43
  9. package/cpp/bindings/HybridStorage.cpp +12 -4
  10. package/cpp/core/SqliteDiskStore.cpp +191 -33
  11. package/cpp/core/SqliteDiskStore.hpp +11 -1
  12. package/docs/api-reference.md +11 -10
  13. package/docs/native-libraries.md +28 -0
  14. package/ios/IOSStorageAdapterCpp.mm +89 -22
  15. package/lib/commonjs/index.web.js +2 -1
  16. package/lib/commonjs/index.web.js.map +1 -1
  17. package/lib/commonjs/storage-core.js +26 -6
  18. package/lib/commonjs/storage-core.js.map +1 -1
  19. package/lib/commonjs/storage-runtime.js +1 -1
  20. package/lib/commonjs/storage-runtime.js.map +1 -1
  21. package/lib/module/index.web.js +2 -1
  22. package/lib/module/index.web.js.map +1 -1
  23. package/lib/module/storage-core.js +26 -6
  24. package/lib/module/storage-core.js.map +1 -1
  25. package/lib/module/storage-runtime.js +1 -1
  26. package/lib/module/storage-runtime.js.map +1 -1
  27. package/lib/typescript/index.d.ts.map +1 -1
  28. package/lib/typescript/index.web.d.ts.map +1 -1
  29. package/lib/typescript/storage-core.d.ts.map +1 -1
  30. package/lib/typescript/storage-runtime.d.ts +1 -1
  31. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  32. package/lib/typescript/testing.d.ts.map +1 -1
  33. package/package.json +2 -1
  34. package/src/index.web.ts +5 -1
  35. package/src/storage-core.ts +26 -6
  36. package/src/storage-runtime.ts +2 -0
@@ -2,28 +2,71 @@ package com.nitrostorage
2
2
 
3
3
  import android.content.Context
4
4
  import android.content.SharedPreferences
5
+ import android.database.DatabaseErrorHandler
5
6
  import android.database.sqlite.SQLiteDatabase
7
+ import android.database.sqlite.SQLiteDatabaseCorruptException
8
+ import android.database.sqlite.SQLiteException
9
+ import java.io.ByteArrayOutputStream
6
10
  import java.io.File
7
11
 
12
+ internal typealias DiskDatabaseOpener = (path: String, flags: Int) -> SQLiteDatabase
13
+
14
+ internal fun openDiskDatabase(path: String, flags: Int): SQLiteDatabase {
15
+ return SQLiteDatabase.openDatabase(path, null, flags, DatabaseErrorHandler { })
16
+ }
17
+
18
+ internal fun openDiskDatabaseWithWalFallback(
19
+ path: String,
20
+ opener: DiskDatabaseOpener,
21
+ ): SQLiteDatabase {
22
+ val flags = SQLiteDatabase.CREATE_IF_NECESSARY or SQLiteDatabase.NO_LOCALIZED_COLLATORS
23
+ return try {
24
+ opener(path, flags or SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING)
25
+ } catch (walError: SQLiteDatabaseCorruptException) {
26
+ throw walError
27
+ } catch (walError: SQLiteException) {
28
+ try {
29
+ opener(path, flags)
30
+ } catch (fallbackError: Exception) {
31
+ walError.addSuppressed(fallbackError)
32
+ throw walError
33
+ }
34
+ }
35
+ }
36
+
8
37
  internal class DiskSqliteStore(
9
38
  context: Context,
10
39
  private val legacyPreferences: SharedPreferences,
40
+ opener: DiskDatabaseOpener = ::openDiskDatabase,
11
41
  ) {
12
- private val db: SQLiteDatabase
42
+ private val db: SQLiteDatabase =
43
+ openDiskDatabaseWithWalFallback(databaseFile(context).path, opener)
13
44
 
14
45
  init {
15
- val file = File(context.filesDir, DATABASE_NAME)
16
- db = SQLiteDatabase.openOrCreateDatabase(file, null)
17
- // PRAGMA journal_mode returns a row; Android forbids result-bearing SQL on execSQL.
18
- db.rawQuery("PRAGMA journal_mode=WAL", null).close()
19
- db.execSQL("PRAGMA synchronous=NORMAL")
20
- db.execSQL(
21
- "CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL)",
22
- )
23
- db.execSQL(
24
- "CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY NOT NULL, v TEXT NOT NULL)",
25
- )
26
- migrateLegacyPreferences()
46
+ try {
47
+ db.execSQL(synchronousPragmaFor(queryPragma("PRAGMA journal_mode")))
48
+ db.execSQL(
49
+ "CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL)",
50
+ )
51
+ db.execSQL(
52
+ "CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY NOT NULL, v TEXT NOT NULL)",
53
+ )
54
+ migrateLegacyPreferences()
55
+ } catch (e: Exception) {
56
+ db.close()
57
+ throw e
58
+ }
59
+ }
60
+
61
+ private fun queryPragma(sql: String): String? {
62
+ return db.rawQuery(sql, null).use { cursor ->
63
+ if (cursor.moveToFirst()) cursor.getString(0) else null
64
+ }
65
+ }
66
+
67
+ @Synchronized
68
+ fun close() {
69
+ db.close()
27
70
  }
28
71
 
29
72
  @Synchronized
@@ -36,9 +79,39 @@ internal class DiskSqliteStore(
36
79
 
37
80
  @Synchronized
38
81
  fun get(key: String): String? {
39
- db.rawQuery("SELECT value FROM kv WHERE key = ? LIMIT 1", arrayOf(key)).use { cursor ->
40
- return if (cursor.moveToFirst()) cursor.getString(0) else null
82
+ db.rawQuery(
83
+ "SELECT length(CAST(value AS BLOB)), " +
84
+ "CASE WHEN length(CAST(value AS BLOB)) <= $VALUE_CHUNK_BYTES THEN value END " +
85
+ "FROM kv WHERE key = ? LIMIT 1",
86
+ arrayOf(key),
87
+ ).use { cursor ->
88
+ if (!cursor.moveToFirst()) {
89
+ return null
90
+ }
91
+ if (!cursor.isNull(1)) {
92
+ return cursor.getString(1)
93
+ }
94
+ return readLargeValue(key, cursor.getLong(0))
95
+ }
96
+ }
97
+
98
+ private fun readLargeValue(key: String, byteCount: Long): String {
99
+ val bytes = ByteArrayOutputStream(byteCount.coerceAtMost(Int.MAX_VALUE.toLong()).toInt())
100
+ var offset = 1L
101
+ while (offset <= byteCount) {
102
+ val chunk = db.rawQuery(
103
+ "SELECT substr(CAST(value AS BLOB), ?, $VALUE_CHUNK_BYTES) FROM kv WHERE key = ?",
104
+ arrayOf(offset.toString(), key),
105
+ ).use { cursor ->
106
+ if (cursor.moveToFirst()) cursor.getBlob(0) else null
107
+ }
108
+ if (chunk == null || chunk.isEmpty()) {
109
+ break
110
+ }
111
+ bytes.write(chunk, 0, chunk.size)
112
+ offset += chunk.size
41
113
  }
114
+ return String(bytes.toByteArray(), Charsets.UTF_8)
42
115
  }
43
116
 
44
117
  @Synchronized
@@ -57,17 +130,13 @@ internal class DiskSqliteStore(
57
130
  @Synchronized
58
131
  fun setBatch(keys: Array<String>, values: Array<String>) {
59
132
  val count = minOf(keys.size, values.size)
60
- db.beginTransaction()
61
- try {
133
+ inTransaction {
62
134
  for (index in 0 until count) {
63
135
  db.execSQL(
64
136
  "INSERT OR REPLACE INTO kv(key, value) VALUES(?, ?)",
65
137
  arrayOf(keys[index], values[index]),
66
138
  )
67
139
  }
68
- db.setTransactionSuccessful()
69
- } finally {
70
- db.endTransaction()
71
140
  }
72
141
  }
73
142
 
@@ -78,14 +147,10 @@ internal class DiskSqliteStore(
78
147
 
79
148
  @Synchronized
80
149
  fun removeBatch(keys: Array<String>) {
81
- db.beginTransaction()
82
- try {
150
+ inTransaction {
83
151
  for (key in keys) {
84
152
  db.execSQL("DELETE FROM kv WHERE key = ?", arrayOf(key))
85
153
  }
86
- db.setTransactionSuccessful()
87
- } finally {
88
- db.endTransaction()
89
154
  }
90
155
  removeLegacyKeys(keys.asIterable())
91
156
  }
@@ -103,19 +168,12 @@ internal class DiskSqliteStore(
103
168
 
104
169
  @Synchronized
105
170
  fun getKeysByPrefix(prefix: String): Array<String> {
106
- val pattern = buildString {
107
- for (character in prefix) {
108
- // LIKE stops at NUL; startsWith below checks the complete prefix.
109
- if (character == '\u0000') break
110
- if (character == '%' || character == '_' || character == '\\') append('\\')
111
- append(character)
112
- }
113
- append('%')
171
+ val cursor = if (hasUnpairedSurrogate(prefix)) {
172
+ db.rawQuery("SELECT key FROM kv WHERE key LIKE ? ESCAPE '\\'", arrayOf(likePattern(prefix)))
173
+ } else {
174
+ db.rawQuery("SELECT key FROM kv WHERE key >= ?1 AND key < (?1 || x'FF')", arrayOf(prefix))
114
175
  }
115
- db.rawQuery(
116
- "SELECT key FROM kv WHERE key LIKE ? ESCAPE '\\'",
117
- arrayOf(pattern),
118
- ).use { cursor ->
176
+ cursor.use {
119
177
  val keys = ArrayList<String>(cursor.count)
120
178
  while (cursor.moveToNext()) {
121
179
  val key = cursor.getString(0)
@@ -152,8 +210,7 @@ internal class DiskSqliteStore(
152
210
  }
153
211
  }
154
212
 
155
- db.beginTransaction()
156
- try {
213
+ inTransaction {
157
214
  for ((key, value) in legacyPreferences.all) {
158
215
  if (value is String) {
159
216
  db.execSQL(
@@ -166,9 +223,25 @@ internal class DiskSqliteStore(
166
223
  "INSERT OR REPLACE INTO meta(k, v) VALUES(?, ?)",
167
224
  arrayOf(PREFS_MIGRATION_KEY, "1"),
168
225
  )
226
+ }
227
+ }
228
+
229
+ private inline fun inTransaction(block: () -> Unit) {
230
+ db.beginTransaction()
231
+ var failure: Throwable? = null
232
+ try {
233
+ block()
169
234
  db.setTransactionSuccessful()
235
+ } catch (error: Throwable) {
236
+ failure = error
237
+ throw error
170
238
  } finally {
171
- db.endTransaction()
239
+ try {
240
+ db.endTransaction()
241
+ } catch (error: SQLiteException) {
242
+ val primary = failure ?: throw error
243
+ primary.addSuppressed(error)
244
+ }
172
245
  }
173
246
  }
174
247
 
@@ -182,8 +255,52 @@ internal class DiskSqliteStore(
182
255
  editor.apply()
183
256
  }
184
257
 
185
- private companion object {
186
- const val DATABASE_NAME = "nitro-storage-disk.sqlite"
187
- const val PREFS_MIGRATION_KEY = "prefs_v1"
258
+ companion object {
259
+ private const val DATABASE_NAME = "nitro-storage-disk.sqlite"
260
+ private const val PREFS_MIGRATION_KEY = "prefs_v1"
261
+ private const val VALUE_CHUNK_BYTES = 512 * 1024
262
+
263
+ fun databaseFile(context: Context): File = File(context.filesDir, DATABASE_NAME)
264
+
265
+ fun deleteDatabaseFiles(context: Context) {
266
+ SQLiteDatabase.deleteDatabase(databaseFile(context))
267
+ }
268
+ }
269
+ }
270
+
271
+ private fun hasUnpairedSurrogate(value: String): Boolean {
272
+ var index = 0
273
+ while (index < value.length) {
274
+ val character = value[index]
275
+ if (Character.isHighSurrogate(character)) {
276
+ if (index + 1 >= value.length || !Character.isLowSurrogate(value[index + 1])) {
277
+ return true
278
+ }
279
+ index += 2
280
+ } else if (Character.isLowSurrogate(character)) {
281
+ return true
282
+ } else {
283
+ index += 1
284
+ }
285
+ }
286
+ return false
287
+ }
288
+
289
+ private fun likePattern(prefix: String): String {
290
+ return buildString {
291
+ for (character in prefix) {
292
+ if (character == '\u0000') break
293
+ if (character == '%' || character == '_' || character == '\\') append('\\')
294
+ append(character)
295
+ }
296
+ append('%')
297
+ }
298
+ }
299
+
300
+ internal fun synchronousPragmaFor(journalMode: String?): String {
301
+ return if (journalMode.equals("wal", ignoreCase = true)) {
302
+ "PRAGMA synchronous=NORMAL"
303
+ } else {
304
+ "PRAGMA synchronous=FULL"
188
305
  }
189
306
  }
@@ -434,6 +434,7 @@ void HybridStorage::removeBatch(const std::vector<std::string>& keys, double sco
434
434
  }
435
435
 
436
436
  void HybridStorage::removeByPrefix(const std::string& prefix, double scope) {
437
+ toScope(scope);
437
438
  if (prefix.empty()) {
438
439
  return;
439
440
  }
@@ -462,17 +463,23 @@ void HybridStorage::setSecureAccessControl(double level) {
462
463
  throw std::runtime_error("NitroStorage: Invalid access control level");
463
464
  }
464
465
  ensureAdapter();
465
- nativeAdapter_->setSecureAccessControl(intLevel);
466
+ runAdapterOperation(
467
+ [&] { nativeAdapter_->setSecureAccessControl(intLevel); },
468
+ "Secure access control");
466
469
  }
467
470
 
468
471
  void HybridStorage::setSecureWritesAsync(bool enabled) {
469
472
  ensureAdapter();
470
- nativeAdapter_->setSecureWritesAsync(enabled);
473
+ runAdapterOperation(
474
+ [&] { nativeAdapter_->setSecureWritesAsync(enabled); },
475
+ "Secure write mode");
471
476
  }
472
477
 
473
478
  void HybridStorage::setKeychainAccessGroup(const std::string& group) {
474
479
  ensureAdapter();
475
- nativeAdapter_->setKeychainAccessGroup(group);
480
+ runAdapterOperation(
481
+ [&] { nativeAdapter_->setKeychainAccessGroup(group); },
482
+ "Keychain access group");
476
483
  }
477
484
 
478
485
  // --- Biometric ---
@@ -517,7 +524,8 @@ void HybridStorage::deleteSecureBiometric(const std::string& key) {
517
524
 
518
525
  bool HybridStorage::hasSecureBiometric(const std::string& key) {
519
526
  ensureAdapter();
520
- return nativeAdapter_->hasSecureBiometric(key);
527
+ return runAdapterOperation(
528
+ [&] { return nativeAdapter_->hasSecureBiometric(key); }, "Biometric has");
521
529
  }
522
530
 
523
531
  void HybridStorage::clearSecureBiometric() {