react-native-nitro-storage 0.12.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 +32 -0
- package/README.md +50 -12
- 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 +94 -7
- package/android/src/main/java/com/nitrostorage/DiskSqliteStore.kt +147 -34
- package/cpp/bindings/HybridStorage.cpp +12 -4
- package/cpp/core/SqliteDiskStore.cpp +172 -26
- package/cpp/core/SqliteDiskStore.hpp +10 -1
- package/docs/api-reference.md +11 -11
- package/docs/native-libraries.md +28 -0
- package/ios/IOSStorageAdapterCpp.mm +89 -22
- package/lib/commonjs/storage-core.js +26 -6
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/module/storage-core.js +26 -6
- package/lib/module/storage-core.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/testing.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/storage-core.ts +26 -6
|
@@ -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() {
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
#include <sqlite3.h>
|
|
4
4
|
|
|
5
5
|
#include <algorithm>
|
|
6
|
+
#include <cerrno>
|
|
6
7
|
#include <cstdlib>
|
|
7
8
|
#include <filesystem>
|
|
8
9
|
#include <memory>
|
|
9
10
|
#include <stdexcept>
|
|
11
|
+
#include <system_error>
|
|
10
12
|
|
|
11
13
|
namespace NitroStorage {
|
|
12
14
|
namespace {
|
|
@@ -21,13 +23,19 @@ std::mutex& sharedMutex() {
|
|
|
21
23
|
return mutex;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
const char* storageErrorTag(int code) {
|
|
26
|
+
const char* storageErrorTag(sqlite3* db, int code) {
|
|
25
27
|
switch (code & 0xff) {
|
|
26
28
|
case SQLITE_FULL:
|
|
27
29
|
return "[nitro-error:storage_full] ";
|
|
28
30
|
case SQLITE_CORRUPT:
|
|
29
31
|
case SQLITE_NOTADB:
|
|
30
32
|
return "[nitro-error:storage_corruption] ";
|
|
33
|
+
case SQLITE_IOERR: {
|
|
34
|
+
const int systemError = db != nullptr ? sqlite3_system_errno(db) : 0;
|
|
35
|
+
return systemError == ENOSPC || systemError == EDQUOT
|
|
36
|
+
? "[nitro-error:storage_full] "
|
|
37
|
+
: "";
|
|
38
|
+
}
|
|
31
39
|
default:
|
|
32
40
|
return "";
|
|
33
41
|
}
|
|
@@ -36,21 +44,22 @@ const char* storageErrorTag(int code) {
|
|
|
36
44
|
[[noreturn]] void throwSqlite(sqlite3* db, const char* operation, int code) {
|
|
37
45
|
const char* message = db != nullptr ? sqlite3_errmsg(db) : sqlite3_errstr(code);
|
|
38
46
|
throw std::runtime_error(
|
|
39
|
-
std::string(storageErrorTag(code)) + "NitroStorage: Disk SQLite " + operation +
|
|
47
|
+
std::string(storageErrorTag(db, code)) + "NitroStorage: Disk SQLite " + operation +
|
|
40
48
|
" failed: " + (message != nullptr ? message : "unknown error")
|
|
41
49
|
);
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
void bindText(sqlite3_stmt* stmt, int index, const std::string& value) {
|
|
45
|
-
const int rc =
|
|
53
|
+
const int rc = sqlite3_bind_text64(
|
|
46
54
|
stmt,
|
|
47
55
|
index,
|
|
48
56
|
value.data(),
|
|
49
|
-
static_cast<
|
|
50
|
-
SQLITE_TRANSIENT
|
|
57
|
+
static_cast<sqlite3_uint64>(value.size()),
|
|
58
|
+
SQLITE_TRANSIENT,
|
|
59
|
+
SQLITE_UTF8
|
|
51
60
|
);
|
|
52
61
|
if (rc != SQLITE_OK) {
|
|
53
|
-
|
|
62
|
+
throwSqlite(sqlite3_db_handle(stmt), "bind", rc);
|
|
54
63
|
}
|
|
55
64
|
}
|
|
56
65
|
|
|
@@ -69,16 +78,23 @@ std::string escapeLikePrefix(const std::string& prefix) {
|
|
|
69
78
|
return escaped;
|
|
70
79
|
}
|
|
71
80
|
|
|
81
|
+
std::optional<std::string> prefixUpperBound(const std::string& prefix) {
|
|
82
|
+
std::string upper = prefix;
|
|
83
|
+
while (!upper.empty() && static_cast<unsigned char>(upper.back()) == 0xFF) {
|
|
84
|
+
upper.pop_back();
|
|
85
|
+
}
|
|
86
|
+
if (upper.empty()) {
|
|
87
|
+
return std::nullopt;
|
|
88
|
+
}
|
|
89
|
+
upper.back() = static_cast<char>(static_cast<unsigned char>(upper.back()) + 1);
|
|
90
|
+
return upper;
|
|
91
|
+
}
|
|
92
|
+
|
|
72
93
|
} // namespace
|
|
73
94
|
|
|
74
95
|
SqliteDiskStore::SqliteDiskStore(std::string path) : path_(std::move(path)) {
|
|
75
96
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
76
|
-
|
|
77
|
-
openLocked();
|
|
78
|
-
} catch (...) {
|
|
79
|
-
closeLocked();
|
|
80
|
-
throw;
|
|
81
|
-
}
|
|
97
|
+
reopenLocked();
|
|
82
98
|
}
|
|
83
99
|
|
|
84
100
|
SqliteDiskStore::~SqliteDiskStore() {
|
|
@@ -95,6 +111,98 @@ SqliteDiskStore& SqliteDiskStore::shared(const std::string& path) {
|
|
|
95
111
|
return *store;
|
|
96
112
|
}
|
|
97
113
|
|
|
114
|
+
bool SqliteDiskStore::isValidUtf8(const std::string& value) {
|
|
115
|
+
const auto* bytes = reinterpret_cast<const unsigned char*>(value.data());
|
|
116
|
+
const size_t size = value.size();
|
|
117
|
+
size_t index = 0;
|
|
118
|
+
while (index < size) {
|
|
119
|
+
const unsigned char lead = bytes[index];
|
|
120
|
+
size_t length = 0;
|
|
121
|
+
unsigned int minimum = 0;
|
|
122
|
+
unsigned int codePoint = 0;
|
|
123
|
+
if (lead < 0x80) {
|
|
124
|
+
index += 1;
|
|
125
|
+
continue;
|
|
126
|
+
} else if ((lead & 0xE0) == 0xC0) {
|
|
127
|
+
length = 2;
|
|
128
|
+
minimum = 0x80;
|
|
129
|
+
codePoint = lead & 0x1Fu;
|
|
130
|
+
} else if ((lead & 0xF0) == 0xE0) {
|
|
131
|
+
length = 3;
|
|
132
|
+
minimum = 0x800;
|
|
133
|
+
codePoint = lead & 0x0Fu;
|
|
134
|
+
} else if ((lead & 0xF8) == 0xF0) {
|
|
135
|
+
length = 4;
|
|
136
|
+
minimum = 0x10000;
|
|
137
|
+
codePoint = lead & 0x07u;
|
|
138
|
+
} else {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
if (size - index < length) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
for (size_t offset = 1; offset < length; ++offset) {
|
|
145
|
+
const unsigned char continuation = bytes[index + offset];
|
|
146
|
+
if ((continuation & 0xC0) != 0x80) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
codePoint = (codePoint << 6) | (continuation & 0x3Fu);
|
|
150
|
+
}
|
|
151
|
+
if (codePoint < minimum || codePoint > 0x10FFFF || (codePoint >= 0xD800 && codePoint <= 0xDFFF)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
index += length;
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
void SqliteDiskStore::recreateShared(const std::string& path) {
|
|
160
|
+
std::lock_guard<std::mutex> lock(sharedMutex());
|
|
161
|
+
auto& store = sharedStore();
|
|
162
|
+
if (store && store->path() == path) {
|
|
163
|
+
store->recreate();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
store.reset();
|
|
167
|
+
removeDatabaseFiles(path);
|
|
168
|
+
store = std::make_unique<SqliteDiskStore>(path);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
void SqliteDiskStore::removeDatabaseFiles(const std::string& path) {
|
|
172
|
+
for (const char* suffix : {"", "-wal", "-shm", "-journal"}) {
|
|
173
|
+
std::error_code error;
|
|
174
|
+
std::filesystem::remove(path + suffix, error);
|
|
175
|
+
if (error) {
|
|
176
|
+
throw std::runtime_error(
|
|
177
|
+
"NitroStorage: Disk SQLite clear failed: cannot delete the database file: " +
|
|
178
|
+
error.message()
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
void SqliteDiskStore::recreate() {
|
|
185
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
186
|
+
closeLocked();
|
|
187
|
+
removeDatabaseFiles(path_);
|
|
188
|
+
reopenLocked();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
void SqliteDiskStore::reopenLocked() {
|
|
192
|
+
try {
|
|
193
|
+
openLocked();
|
|
194
|
+
} catch (...) {
|
|
195
|
+
closeLocked();
|
|
196
|
+
throw;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
void SqliteDiskStore::ensureOpenLocked() {
|
|
201
|
+
if (db_ == nullptr) {
|
|
202
|
+
reopenLocked();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
98
206
|
void SqliteDiskStore::resetShared() {
|
|
99
207
|
std::lock_guard<std::mutex> lock(sharedMutex());
|
|
100
208
|
sharedStore().reset();
|
|
@@ -121,17 +229,22 @@ void SqliteDiskStore::openLocked() {
|
|
|
121
229
|
const int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX;
|
|
122
230
|
const int openRc = sqlite3_open_v2(path_.c_str(), &db, flags, nullptr);
|
|
123
231
|
if (openRc != SQLITE_OK) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
232
|
+
try {
|
|
233
|
+
throwSqlite(db, "open", openRc);
|
|
234
|
+
} catch (...) {
|
|
235
|
+
if (db != nullptr) {
|
|
236
|
+
sqlite3_close(db);
|
|
237
|
+
}
|
|
238
|
+
throw;
|
|
127
239
|
}
|
|
128
|
-
throwSqlite(nullptr, "open", code);
|
|
129
240
|
}
|
|
130
241
|
db_ = db;
|
|
131
242
|
sqlite3_busy_timeout(db_, 5000);
|
|
132
243
|
execLocked("PRAGMA journal_mode=WAL;");
|
|
133
244
|
execLocked("PRAGMA synchronous=NORMAL;");
|
|
134
245
|
execLocked("PRAGMA temp_store=MEMORY;");
|
|
246
|
+
execLocked("PRAGMA wal_autocheckpoint=1000;");
|
|
247
|
+
execLocked("PRAGMA journal_size_limit=32768;");
|
|
135
248
|
execLocked(
|
|
136
249
|
"CREATE TABLE IF NOT EXISTS kv ("
|
|
137
250
|
"key TEXT PRIMARY KEY NOT NULL,"
|
|
@@ -149,7 +262,9 @@ void SqliteDiskStore::openLocked() {
|
|
|
149
262
|
removeStmt_ = prepareLocked("DELETE FROM kv WHERE key = ?1;");
|
|
150
263
|
hasStmt_ = prepareLocked("SELECT 1 FROM kv WHERE key = ?1 LIMIT 1;");
|
|
151
264
|
keysStmt_ = prepareLocked("SELECT key FROM kv;");
|
|
152
|
-
|
|
265
|
+
prefixRangeStmt_ = prepareLocked("SELECT key FROM kv WHERE key >= ?1 AND key < ?2;");
|
|
266
|
+
prefixFromStmt_ = prepareLocked("SELECT key FROM kv WHERE key >= ?1;");
|
|
267
|
+
prefixLikeStmt_ = prepareLocked("SELECT key FROM kv WHERE key LIKE ?1 ESCAPE '\\';");
|
|
153
268
|
sizeStmt_ = prepareLocked("SELECT COUNT(*) FROM kv;");
|
|
154
269
|
clearStmt_ = prepareLocked("DELETE FROM kv;");
|
|
155
270
|
insertAbsentStmt_ = prepareLocked(
|
|
@@ -171,7 +286,9 @@ void SqliteDiskStore::closeLocked() {
|
|
|
171
286
|
finalize(removeStmt_);
|
|
172
287
|
finalize(hasStmt_);
|
|
173
288
|
finalize(keysStmt_);
|
|
174
|
-
finalize(
|
|
289
|
+
finalize(prefixRangeStmt_);
|
|
290
|
+
finalize(prefixFromStmt_);
|
|
291
|
+
finalize(prefixLikeStmt_);
|
|
175
292
|
finalize(sizeStmt_);
|
|
176
293
|
finalize(clearStmt_);
|
|
177
294
|
finalize(insertAbsentStmt_);
|
|
@@ -192,9 +309,16 @@ void SqliteDiskStore::execLocked(const char* sql) {
|
|
|
192
309
|
|
|
193
310
|
void SqliteDiskStore::limitPageCountForTesting(int pages) {
|
|
194
311
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
312
|
+
ensureOpenLocked();
|
|
195
313
|
execLocked(("PRAGMA max_page_count=" + std::to_string(pages) + ";").c_str());
|
|
196
314
|
}
|
|
197
315
|
|
|
316
|
+
void SqliteDiskStore::limitValueLengthForTesting(int bytes) {
|
|
317
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
318
|
+
ensureOpenLocked();
|
|
319
|
+
sqlite3_limit(db_, SQLITE_LIMIT_LENGTH, bytes);
|
|
320
|
+
}
|
|
321
|
+
|
|
198
322
|
void SqliteDiskStore::beginLocked() {
|
|
199
323
|
execLocked("BEGIN IMMEDIATE;");
|
|
200
324
|
}
|
|
@@ -264,21 +388,25 @@ void SqliteDiskStore::removeLocked(const std::string& key) {
|
|
|
264
388
|
|
|
265
389
|
void SqliteDiskStore::set(const std::string& key, const std::string& value) {
|
|
266
390
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
391
|
+
ensureOpenLocked();
|
|
267
392
|
setLocked(key, value);
|
|
268
393
|
}
|
|
269
394
|
|
|
270
395
|
std::optional<std::string> SqliteDiskStore::get(const std::string& key) {
|
|
271
396
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
397
|
+
ensureOpenLocked();
|
|
272
398
|
return getLocked(key);
|
|
273
399
|
}
|
|
274
400
|
|
|
275
401
|
void SqliteDiskStore::remove(const std::string& key) {
|
|
276
402
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
403
|
+
ensureOpenLocked();
|
|
277
404
|
removeLocked(key);
|
|
278
405
|
}
|
|
279
406
|
|
|
280
407
|
bool SqliteDiskStore::has(const std::string& key) {
|
|
281
408
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
409
|
+
ensureOpenLocked();
|
|
282
410
|
sqlite3_reset(hasStmt_);
|
|
283
411
|
sqlite3_clear_bindings(hasStmt_);
|
|
284
412
|
bindText(hasStmt_, 1, key);
|
|
@@ -298,6 +426,7 @@ void SqliteDiskStore::setBatch(
|
|
|
298
426
|
const std::vector<std::string>& values
|
|
299
427
|
) {
|
|
300
428
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
429
|
+
ensureOpenLocked();
|
|
301
430
|
beginLocked();
|
|
302
431
|
try {
|
|
303
432
|
const size_t count = std::min(keys.size(), values.size());
|
|
@@ -315,6 +444,7 @@ std::vector<std::optional<std::string>> SqliteDiskStore::getBatch(
|
|
|
315
444
|
const std::vector<std::string>& keys
|
|
316
445
|
) {
|
|
317
446
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
447
|
+
ensureOpenLocked();
|
|
318
448
|
std::vector<std::optional<std::string>> results;
|
|
319
449
|
results.reserve(keys.size());
|
|
320
450
|
for (const auto& key : keys) {
|
|
@@ -325,6 +455,7 @@ std::vector<std::optional<std::string>> SqliteDiskStore::getBatch(
|
|
|
325
455
|
|
|
326
456
|
void SqliteDiskStore::removeBatch(const std::vector<std::string>& keys) {
|
|
327
457
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
458
|
+
ensureOpenLocked();
|
|
328
459
|
beginLocked();
|
|
329
460
|
try {
|
|
330
461
|
for (const auto& key : keys) {
|
|
@@ -339,6 +470,7 @@ void SqliteDiskStore::removeBatch(const std::vector<std::string>& keys) {
|
|
|
339
470
|
|
|
340
471
|
std::vector<std::string> SqliteDiskStore::getAllKeys() {
|
|
341
472
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
473
|
+
ensureOpenLocked();
|
|
342
474
|
sqlite3_reset(keysStmt_);
|
|
343
475
|
std::vector<std::string> keys;
|
|
344
476
|
while (true) {
|
|
@@ -363,21 +495,30 @@ std::vector<std::string> SqliteDiskStore::getAllKeys() {
|
|
|
363
495
|
|
|
364
496
|
std::vector<std::string> SqliteDiskStore::getKeysByPrefix(const std::string& prefix) {
|
|
365
497
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
498
|
+
ensureOpenLocked();
|
|
499
|
+
const bool useRange = isValidUtf8(prefix);
|
|
500
|
+
const auto upperBound = useRange ? prefixUpperBound(prefix) : std::nullopt;
|
|
501
|
+
sqlite3_stmt* stmt = !useRange
|
|
502
|
+
? prefixLikeStmt_
|
|
503
|
+
: upperBound.has_value() ? prefixRangeStmt_ : prefixFromStmt_;
|
|
504
|
+
sqlite3_reset(stmt);
|
|
505
|
+
sqlite3_clear_bindings(stmt);
|
|
506
|
+
bindText(stmt, 1, useRange ? prefix : escapeLikePrefix(prefix));
|
|
507
|
+
if (upperBound.has_value()) {
|
|
508
|
+
bindText(stmt, 2, *upperBound);
|
|
509
|
+
}
|
|
369
510
|
std::vector<std::string> keys;
|
|
370
511
|
while (true) {
|
|
371
|
-
const int rc = sqlite3_step(
|
|
512
|
+
const int rc = sqlite3_step(stmt);
|
|
372
513
|
if (rc == SQLITE_DONE) {
|
|
373
514
|
break;
|
|
374
515
|
}
|
|
375
516
|
if (rc != SQLITE_ROW) {
|
|
376
|
-
sqlite3_reset(
|
|
517
|
+
sqlite3_reset(stmt);
|
|
377
518
|
throwSqlite(db_, "getKeysByPrefix", rc);
|
|
378
519
|
}
|
|
379
|
-
const unsigned char* text = sqlite3_column_text(
|
|
380
|
-
const int bytes = sqlite3_column_bytes(
|
|
520
|
+
const unsigned char* text = sqlite3_column_text(stmt, 0);
|
|
521
|
+
const int bytes = sqlite3_column_bytes(stmt, 0);
|
|
381
522
|
std::string key(
|
|
382
523
|
text != nullptr ? reinterpret_cast<const char*>(text) : "",
|
|
383
524
|
static_cast<size_t>(bytes)
|
|
@@ -386,12 +527,13 @@ std::vector<std::string> SqliteDiskStore::getKeysByPrefix(const std::string& pre
|
|
|
386
527
|
keys.push_back(std::move(key));
|
|
387
528
|
}
|
|
388
529
|
}
|
|
389
|
-
sqlite3_reset(
|
|
530
|
+
sqlite3_reset(stmt);
|
|
390
531
|
return keys;
|
|
391
532
|
}
|
|
392
533
|
|
|
393
534
|
size_t SqliteDiskStore::size() {
|
|
394
535
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
536
|
+
ensureOpenLocked();
|
|
395
537
|
sqlite3_reset(sizeStmt_);
|
|
396
538
|
const int rc = sqlite3_step(sizeStmt_);
|
|
397
539
|
if (rc != SQLITE_ROW) {
|
|
@@ -405,6 +547,7 @@ size_t SqliteDiskStore::size() {
|
|
|
405
547
|
|
|
406
548
|
void SqliteDiskStore::clear() {
|
|
407
549
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
550
|
+
ensureOpenLocked();
|
|
408
551
|
sqlite3_reset(clearStmt_);
|
|
409
552
|
const int rc = sqlite3_step(clearStmt_);
|
|
410
553
|
sqlite3_reset(clearStmt_);
|
|
@@ -463,6 +606,7 @@ void SqliteDiskStore::migrateIfAbsent(
|
|
|
463
606
|
return;
|
|
464
607
|
}
|
|
465
608
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
609
|
+
ensureOpenLocked();
|
|
466
610
|
beginLocked();
|
|
467
611
|
try {
|
|
468
612
|
insertAbsentLocked(entries);
|
|
@@ -475,6 +619,7 @@ void SqliteDiskStore::migrateIfAbsent(
|
|
|
475
619
|
|
|
476
620
|
bool SqliteDiskStore::hasMigrationMarker(const std::string& name) {
|
|
477
621
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
622
|
+
ensureOpenLocked();
|
|
478
623
|
return hasMigrationMarkerLocked(name);
|
|
479
624
|
}
|
|
480
625
|
|
|
@@ -483,6 +628,7 @@ void SqliteDiskStore::migrateOnce(
|
|
|
483
628
|
const std::vector<std::pair<std::string, std::string>>& entries
|
|
484
629
|
) {
|
|
485
630
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
631
|
+
ensureOpenLocked();
|
|
486
632
|
beginLocked();
|
|
487
633
|
try {
|
|
488
634
|
if (hasMigrationMarkerLocked(name)) {
|
|
@@ -20,7 +20,10 @@ public:
|
|
|
20
20
|
|
|
21
21
|
static SqliteDiskStore& shared(const std::string& path);
|
|
22
22
|
static void resetShared();
|
|
23
|
+
static void recreateShared(const std::string& path);
|
|
24
|
+
static void removeDatabaseFiles(const std::string& path);
|
|
23
25
|
static std::string defaultPath();
|
|
26
|
+
static bool isValidUtf8(const std::string& value);
|
|
24
27
|
|
|
25
28
|
const std::string& path() const { return path_; }
|
|
26
29
|
|
|
@@ -44,10 +47,14 @@ public:
|
|
|
44
47
|
const std::string& name,
|
|
45
48
|
const std::vector<std::pair<std::string, std::string>>& entries
|
|
46
49
|
);
|
|
50
|
+
void recreate();
|
|
47
51
|
void limitPageCountForTesting(int pages);
|
|
52
|
+
void limitValueLengthForTesting(int bytes);
|
|
48
53
|
|
|
49
54
|
private:
|
|
50
55
|
void openLocked();
|
|
56
|
+
void reopenLocked();
|
|
57
|
+
void ensureOpenLocked();
|
|
51
58
|
void closeLocked();
|
|
52
59
|
void execLocked(const char* sql);
|
|
53
60
|
void beginLocked();
|
|
@@ -68,7 +75,9 @@ private:
|
|
|
68
75
|
sqlite3_stmt* removeStmt_ = nullptr;
|
|
69
76
|
sqlite3_stmt* hasStmt_ = nullptr;
|
|
70
77
|
sqlite3_stmt* keysStmt_ = nullptr;
|
|
71
|
-
sqlite3_stmt*
|
|
78
|
+
sqlite3_stmt* prefixRangeStmt_ = nullptr;
|
|
79
|
+
sqlite3_stmt* prefixFromStmt_ = nullptr;
|
|
80
|
+
sqlite3_stmt* prefixLikeStmt_ = nullptr;
|
|
72
81
|
sqlite3_stmt* sizeStmt_ = nullptr;
|
|
73
82
|
sqlite3_stmt* clearStmt_ = nullptr;
|
|
74
83
|
sqlite3_stmt* insertAbsentStmt_ = nullptr;
|
package/docs/api-reference.md
CHANGED
|
@@ -276,17 +276,17 @@ the native or web adapter. `isStorageError(error, code)` matches one exact code
|
|
|
276
276
|
without parsing platform message text. See [secure-storage.md](secure-storage.md)
|
|
277
277
|
for recovery semantics.
|
|
278
278
|
|
|
279
|
-
| Code | Raised when
|
|
280
|
-
| ----------------------------- |
|
|
281
|
-
| `keychain_locked` | The protected store is locked.
|
|
282
|
-
| `authentication_required` | The item needs user authentication, or the user cancelled the prompt.
|
|
283
|
-
| `key_invalidated` | The protecting key was invalidated, for example by a biometric enrolment change.
|
|
284
|
-
| `biometric_unavailable` | The requested biometric level is not available on this device or OS version.
|
|
285
|
-
| `storage_corruption` | Stored secure data could not be
|
|
286
|
-
| `storage_compensation_failed` | A multi-step write failed and restoring the previous state also failed.
|
|
287
|
-
| `unsupported` | The operation is not available on this platform or environment.
|
|
288
|
-
| `storage_full` | The device or database is out of space, or the web storage quota is exceeded. Free space before retrying.
|
|
289
|
-
| `invalid_key` | A storage key is empty.
|
|
279
|
+
| Code | Raised when |
|
|
280
|
+
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
281
|
+
| `keychain_locked` | The protected store is locked. |
|
|
282
|
+
| `authentication_required` | The item needs user authentication, or the user cancelled the prompt. |
|
|
283
|
+
| `key_invalidated` | The protecting key was invalidated, for example by a biometric enrolment change. |
|
|
284
|
+
| `biometric_unavailable` | The requested biometric level is not available on this device or OS version. |
|
|
285
|
+
| `storage_corruption` | Stored secure data could not be decoded, the Disk database is corrupt, or (Android) the Secure master key or store cannot be created. Do not retry. |
|
|
286
|
+
| `storage_compensation_failed` | A multi-step write failed and restoring the previous state also failed. |
|
|
287
|
+
| `unsupported` | The operation is not available on this platform or environment. |
|
|
288
|
+
| `storage_full` | The device or database is out of space, or the web storage quota is exceeded. Free space before retrying. |
|
|
289
|
+
| `invalid_key` | A storage key is empty. |
|
|
290
290
|
|
|
291
291
|
`StorageCompositeError` and `StorageCompensationError` describe errors that
|
|
292
292
|
carry a primary failure plus secondary failures from reconciliation or
|
package/docs/native-libraries.md
CHANGED
|
@@ -28,6 +28,34 @@ wipe leaves no pre-SQLite copy behind. Later Disk reads and writes use SQLite.
|
|
|
28
28
|
| RocksDB / LevelDB | Rejected. Heavy native footprint for small preference-sized values. |
|
|
29
29
|
| yyjson | Rejected for Disk. Typed TTL/JSON envelopes stay in JavaScript. |
|
|
30
30
|
|
|
31
|
+
## Limits and durability
|
|
32
|
+
|
|
33
|
+
- Disk writes wait up to 5 seconds for another connection that holds the
|
|
34
|
+
SQLite write lock. The library uses one connection per process, so this wait
|
|
35
|
+
only happens when another process (for example an app extension in the same
|
|
36
|
+
container) is writing. Reads do not wait in WAL mode. Five seconds gives the
|
|
37
|
+
other writer time to finish and stays below the iOS and Android
|
|
38
|
+
unresponsive-app limits.
|
|
39
|
+
- If the Disk database file is deleted while the app runs, the open connection
|
|
40
|
+
keeps reading and writing the deleted file, and the next launch starts with
|
|
41
|
+
an empty database. The library does not check for this on each call, because
|
|
42
|
+
the check costs a system call on every write. Do not delete
|
|
43
|
+
`nitro-storage-disk.sqlite` yourself; call
|
|
44
|
+
`storage.clear(StorageScope.Disk)`.
|
|
45
|
+
- Each platform keeps its system SQLite checkpoint settings. iOS checkpoints
|
|
46
|
+
every 1000 pages and truncates the WAL to 32 KiB; these values are set in
|
|
47
|
+
code so they do not depend on the SQLite build. Android checkpoints every
|
|
48
|
+
100 pages with a 512 KiB limit, applied by the Android framework to every
|
|
49
|
+
pooled connection.
|
|
50
|
+
- Android opens the Disk database in WAL mode. If that open fails for a reason
|
|
51
|
+
other than corruption (for example a full device while an older database
|
|
52
|
+
converts to WAL), it opens once without WAL and uses `synchronous=FULL`, so
|
|
53
|
+
reads keep working; the next launch tries WAL again. If WAL is active and the
|
|
54
|
+
device is so full that the WAL shared-memory file cannot be created, reads
|
|
55
|
+
fail with `storage_full` until space is free. iOS has the same limit.
|
|
56
|
+
- With WAL and `synchronous=NORMAL`, an app kill never loses a committed
|
|
57
|
+
write. A power loss can lose the most recent commits.
|
|
58
|
+
|
|
31
59
|
## Compatibility
|
|
32
60
|
|
|
33
61
|
- Public Disk APIs (`get`/`set`/`setBatch`/`getKeysByPrefix`/`import`) are
|