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.
- package/CHANGELOG.md +47 -0
- package/README.md +71 -11
- package/SECURITY.md +2 -2
- package/android/build.gradle +5 -0
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +12 -13
- package/android/src/main/cpp/JniSize.hpp +21 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +124 -16
- package/android/src/main/java/com/nitrostorage/DiskSqliteStore.kt +160 -43
- package/cpp/bindings/HybridStorage.cpp +12 -4
- package/cpp/core/SqliteDiskStore.cpp +191 -33
- package/cpp/core/SqliteDiskStore.hpp +11 -1
- package/docs/api-reference.md +11 -10
- package/docs/native-libraries.md +28 -0
- package/ios/IOSStorageAdapterCpp.mm +89 -22
- package/lib/commonjs/index.web.js +2 -1
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/storage-core.js +26 -6
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/storage-runtime.js +1 -1
- package/lib/commonjs/storage-runtime.js.map +1 -1
- package/lib/module/index.web.js +2 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/storage-core.js +26 -6
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/storage-runtime.js +1 -1
- package/lib/module/storage-runtime.js.map +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +1 -1
- package/lib/typescript/storage-runtime.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/index.web.ts +5 -1
- package/src/storage-core.ts +26 -6
- 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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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(
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
527
|
+
return runAdapterOperation(
|
|
528
|
+
[&] { return nativeAdapter_->hasSecureBiometric(key); }, "Biometric has");
|
|
521
529
|
}
|
|
522
530
|
|
|
523
531
|
void HybridStorage::clearSecureBiometric() {
|