react-native-nitro-storage 0.10.2 → 0.10.4
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 +45 -0
- package/README.md +38 -8
- package/android/CMakeLists.txt +2 -0
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +47 -29
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +12 -25
- package/android/src/main/java/com/nitrostorage/DiskSqliteStore.kt +174 -0
- package/cpp/core/SqliteDiskStore.cpp +416 -0
- package/cpp/core/SqliteDiskStore.hpp +69 -0
- package/docs/api-reference.md +4 -1
- package/docs/benchmarks.md +37 -0
- package/docs/native-libraries.md +30 -0
- package/docs/secure-storage.md +5 -0
- package/ios/IOSStorageAdapterCpp.mm +146 -52
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/shared.js.map +1 -1
- package/lib/commonjs/storage-core.js +57 -11
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/testing.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/shared.js.map +1 -1
- package/lib/module/storage-core.js +57 -11
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/testing.js.map +1 -1
- package/lib/typescript/index.d.ts +2 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +2 -1
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/shared.d.ts +6 -0
- package/lib/typescript/shared.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts +2 -1
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts +3 -1
- package/lib/typescript/testing.d.ts.map +1 -1
- package/package.json +1 -1
- package/react-native-nitro-storage.podspec +2 -0
- package/src/index.ts +1 -0
- package/src/index.web.ts +1 -0
- package/src/shared.ts +7 -0
- package/src/storage-core.ts +65 -14
- package/src/testing.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,51 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
Breaking changes are always listed first in each release section.
|
|
8
8
|
|
|
9
|
+
## [0.10.4] - 2026-09-27
|
|
10
|
+
|
|
11
|
+
### Breaking changes
|
|
12
|
+
|
|
13
|
+
- None.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Disk prefix queries and namespace removal now distinguish ASCII case, so clearing `user` does not remove `User` keys.
|
|
18
|
+
- Android Disk and Secure storage preserve embedded NUL characters in keys and values across scalar and batch operations, preventing truncated-key collisions.
|
|
19
|
+
- iOS Secure storage preserves embedded NUL keys and values, and legacy Disk migration no longer aliases a shorter host-app defaults key when a key contains NUL.
|
|
20
|
+
- Memory prefix reads return the original raw string, including values beginning with the reserved internal prefix.
|
|
21
|
+
- Raw enumeration, export, and set items preserve arbitrary keys such as `__proto__`, `constructor`, and `toString` as own properties.
|
|
22
|
+
|
|
23
|
+
## [0.10.3] - 2026-09-18
|
|
24
|
+
|
|
25
|
+
### Breaking changes
|
|
26
|
+
|
|
27
|
+
- None.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- `storage.getCacheMetrics()` reports live Disk/Secure raw-cache hits, misses,
|
|
32
|
+
entries, and estimated bytes. The cache stays unbounded; reset zeros the
|
|
33
|
+
counters only.
|
|
34
|
+
- Example integrity, keychain, and Disk/Secure stress labs at
|
|
35
|
+
`nitrostorage://e2e-integrity`, `nitrostorage://e2e-keychain`, and
|
|
36
|
+
`nitrostorage://e2e-stress`.
|
|
37
|
+
- Native Disk now uses SQLite WAL (`SqliteDiskStore` on iOS, `DiskSqliteStore`
|
|
38
|
+
on Android). Existing UserDefaults suite keys and `NitroStorage`
|
|
39
|
+
SharedPreferences values are imported once. See
|
|
40
|
+
[docs/native-libraries.md](docs/native-libraries.md).
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- The example Expo pin follows SDK 57.0.24 (`expo-doctor` / `expo install --check`).
|
|
45
|
+
React Native stays `0.86.3`.
|
|
46
|
+
- The example iOS host uses a `SceneDelegate` so the app can present a window on
|
|
47
|
+
iOS 27 physical devices.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- Android Disk no longer crashes on first open when enabling SQLite WAL:
|
|
52
|
+
`PRAGMA journal_mode=WAL` is applied with `rawQuery` instead of `execSQL`.
|
|
53
|
+
|
|
9
54
|
## [0.10.2] - 2026-09-10
|
|
10
55
|
|
|
11
56
|
### Breaking changes
|
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ pagination, conflict resolution, or remote synchronization.
|
|
|
35
35
|
- [Legacy Key Migration And Secure Resilience](#legacy-key-migration-and-secure-resilience)
|
|
36
36
|
- [React Hooks](#react-hooks)
|
|
37
37
|
- [Storage Scopes](#storage-scopes)
|
|
38
|
+
- [Prefix Queries](#prefix-queries)
|
|
38
39
|
- [Secure Storage](#secure-storage)
|
|
39
40
|
- [Batch Operations](#batch-operations)
|
|
40
41
|
- [Events And Observability](#events-and-observability)
|
|
@@ -68,7 +69,7 @@ Nitro peer requirement: `react-native-nitro-modules >=0.37.0 <0.38.0`.
|
|
|
68
69
|
The package gate uses React Native `0.86.3` and the Strict TypeScript API.
|
|
69
70
|
`check:ci` also compiles the public source against React Native `0.87.0`'s
|
|
70
71
|
Strict TypeScript API; this does not change the runtime baseline. The Expo
|
|
71
|
-
example uses Expo SDK `57.0.
|
|
72
|
+
example uses Expo SDK `57.0.25`, React Native
|
|
72
73
|
`0.86.3`, React `19.2.3`, and Nitro Modules `0.37.1`, which is the React Native
|
|
73
74
|
version supported by that Expo SDK. Do not override Expo's React Native version.
|
|
74
75
|
|
|
@@ -77,7 +78,7 @@ before installing this package, then rebuild the native app so the generated
|
|
|
77
78
|
Nitro bindings and native runtime use the same major-minor version:
|
|
78
79
|
|
|
79
80
|
```sh
|
|
80
|
-
bun add react-native-nitro-modules@0.37.1 react-native-nitro-storage@0.10.
|
|
81
|
+
bun add react-native-nitro-modules@0.37.1 react-native-nitro-storage@0.10.4
|
|
81
82
|
bunx expo prebuild
|
|
82
83
|
```
|
|
83
84
|
|
|
@@ -387,11 +388,31 @@ const tokenActions = useStorageActions(tokenItem); // { set, merge, reset, remov
|
|
|
387
388
|
|
|
388
389
|
## Storage Scopes
|
|
389
390
|
|
|
390
|
-
| Scope | Backing store
|
|
391
|
-
| --------------------- |
|
|
392
|
-
| `StorageScope.Memory` | In-process memory
|
|
393
|
-
| `StorageScope.Disk` |
|
|
394
|
-
| `StorageScope.Secure` | Keychain on iOS, Android Keystore-backed preferences
|
|
391
|
+
| Scope | Backing store | Use it for |
|
|
392
|
+
| --------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
393
|
+
| `StorageScope.Memory` | In-process memory | Session-only state, fast counters, and render-time caches. |
|
|
394
|
+
| `StorageScope.Disk` | SQLite WAL on iOS/Android (imports UserDefaults / SharedPreferences once); configured web backend | Preferences, feature flags, onboarding state, and non-secret persisted data. |
|
|
395
|
+
| `StorageScope.Secure` | Keychain on iOS, Android Keystore-backed preferences | Refresh tokens, credentials, API tokens, and biometric-protected values. |
|
|
396
|
+
|
|
397
|
+
## Prefix Queries
|
|
398
|
+
|
|
399
|
+
Prefix queries use literal, case-sensitive matching on every backend. `User::`
|
|
400
|
+
and `user::` are separate namespaces; `%`, `_`, and `\\` are literal characters.
|
|
401
|
+
Raw enumeration returns the original strings and preserves arbitrary keys as own
|
|
402
|
+
properties on an ordinary object, including `__proto__`.
|
|
403
|
+
|
|
404
|
+
Disk and Secure strings preserve embedded NUL characters in keys and values,
|
|
405
|
+
including batch operations. Earlier versions could truncate strings at Android
|
|
406
|
+
JNI or iOS Foundation boundaries; this fix cannot reconstruct previously lost
|
|
407
|
+
suffixes. iOS legacy Disk migration also preserves full keys without aliasing
|
|
408
|
+
shorter host-app defaults keys.
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
storage.setString("User::theme", "dark", StorageScope.Disk);
|
|
412
|
+
storage.setString("user::theme", "light", StorageScope.Disk);
|
|
413
|
+
const settings = storage.getByPrefix("User::", StorageScope.Disk);
|
|
414
|
+
// settings["User::theme"] === "dark"; no lowercase namespace entries.
|
|
415
|
+
```
|
|
395
416
|
|
|
396
417
|
## Secure Storage
|
|
397
418
|
|
|
@@ -520,13 +541,21 @@ storage.setMetricsObserver((event) => {
|
|
|
520
541
|
|
|
521
542
|
const metrics = storage.getMetricsSnapshot();
|
|
522
543
|
const scopedMetrics = storage.getScopedMetricsSnapshot();
|
|
544
|
+
const cacheMetrics = storage.getCacheMetrics();
|
|
523
545
|
storage.resetMetrics();
|
|
524
546
|
unsubscribe();
|
|
525
547
|
```
|
|
526
548
|
|
|
527
549
|
`getMetricsSnapshot()` aggregates each operation across scopes for backward
|
|
528
550
|
compatibility. `getScopedMetricsSnapshot()` adds the numeric scope suffix for
|
|
529
|
-
per-scope analysis, for example `item:set:1`.
|
|
551
|
+
per-scope analysis, for example `item:set:1`. `getCacheMetrics()` reports live
|
|
552
|
+
Disk/Secure raw-cache hits, misses, entries, and estimated bytes. The cache is
|
|
553
|
+
unbounded; `resetMetrics()` zeros the hit/miss counters and leaves entries in
|
|
554
|
+
place.
|
|
555
|
+
|
|
556
|
+
The example app includes hidden integrity, keychain, and Disk/Secure stress
|
|
557
|
+
labs at `nitrostorage://e2e-integrity`, `nitrostorage://e2e-keychain`, and
|
|
558
|
+
`nitrostorage://e2e-stress`.
|
|
530
559
|
|
|
531
560
|
Secure event observer values are redacted by default. Pass
|
|
532
561
|
`{ redactSecureValues: false }` only in trusted debug tooling where raw values
|
|
@@ -674,6 +703,7 @@ the error for diagnostics.
|
|
|
674
703
|
| Web backends | [docs/web-backends.md](docs/web-backends.md) |
|
|
675
704
|
| Batch, transactions, and migrations | [docs/batch-transactions-migrations.md](docs/batch-transactions-migrations.md) |
|
|
676
705
|
| MMKV migration | [docs/mmkv-migration.md](docs/mmkv-migration.md) |
|
|
706
|
+
| Native libraries | [docs/native-libraries.md](docs/native-libraries.md) |
|
|
677
707
|
| Recipes | [docs/recipes.md](docs/recipes.md) |
|
|
678
708
|
| Benchmarks | [docs/benchmarks.md](docs/benchmarks.md) |
|
|
679
709
|
| Security policy | [SECURITY.md](SECURITY.md) |
|
package/android/CMakeLists.txt
CHANGED
|
@@ -13,6 +13,8 @@ file(GLOB SOURCES
|
|
|
13
13
|
)
|
|
14
14
|
# Unit/integration C++ tests define `main()` and must never be linked into the Android shared library.
|
|
15
15
|
list(FILTER SOURCES EXCLUDE REGEX ".*/[^/]*Test\\.cpp$")
|
|
16
|
+
# iOS/macOS Disk SQLite lives in this translation unit; Android uses DiskSqliteStore.kt.
|
|
17
|
+
list(FILTER SOURCES EXCLUDE REGEX ".*/SqliteDiskStore\\.cpp$")
|
|
16
18
|
|
|
17
19
|
# 2. Create the library target
|
|
18
20
|
add_library(
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#include "AndroidStorageAdapterCpp.hpp"
|
|
2
2
|
|
|
3
|
+
#include <limits>
|
|
4
|
+
#include <stdexcept>
|
|
5
|
+
|
|
3
6
|
namespace NitroStorage {
|
|
4
7
|
|
|
5
8
|
using namespace facebook::jni;
|
|
@@ -7,10 +10,25 @@ using JavaStringArray = JArrayClass<jstring>;
|
|
|
7
10
|
|
|
8
11
|
namespace {
|
|
9
12
|
|
|
13
|
+
local_ref<JString> toJavaString(const std::string& value) {
|
|
14
|
+
// fbjni's std::string overload uses c_str(), which truncates embedded NUL.
|
|
15
|
+
if (value.find('\0') == std::string::npos) return make_jstring(value);
|
|
16
|
+
if (value.size() > static_cast<size_t>(std::numeric_limits<jsize>::max())) {
|
|
17
|
+
throw std::length_error("Storage string exceeds the Java array limit");
|
|
18
|
+
}
|
|
19
|
+
const auto size = static_cast<jsize>(value.size());
|
|
20
|
+
auto bytes = JArrayByte::newArray(size);
|
|
21
|
+
bytes->setRegion(0, size, reinterpret_cast<const jbyte*>(value.data()));
|
|
22
|
+
static auto constructor = JString::javaClassStatic()->getConstructor<
|
|
23
|
+
jstring(jbyteArray, jstring)>();
|
|
24
|
+
auto charset = make_jstring("UTF-8");
|
|
25
|
+
return JString::javaClassStatic()->newObject(constructor, bytes.get(), charset.get());
|
|
26
|
+
}
|
|
27
|
+
|
|
10
28
|
local_ref<JavaStringArray> toJavaStringArray(const std::vector<std::string>& values) {
|
|
11
29
|
auto javaArray = JavaStringArray::newArray(static_cast<jsize>(values.size()));
|
|
12
30
|
for (size_t i = 0; i < values.size(); ++i) {
|
|
13
|
-
auto javaValue =
|
|
31
|
+
auto javaValue = toJavaString(values[i]);
|
|
14
32
|
javaArray->setElement(static_cast<jsize>(i), javaValue.get());
|
|
15
33
|
}
|
|
16
34
|
return javaArray;
|
|
@@ -61,25 +79,25 @@ AndroidStorageAdapterCpp::~AndroidStorageAdapterCpp() = default;
|
|
|
61
79
|
// --- Disk ---
|
|
62
80
|
|
|
63
81
|
void AndroidStorageAdapterCpp::setDisk(const std::string& key, const std::string& value) {
|
|
64
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
65
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key, value);
|
|
82
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>, alias_ref<JString>)>("setDisk");
|
|
83
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key), toJavaString(value));
|
|
66
84
|
}
|
|
67
85
|
|
|
68
86
|
std::optional<std::string> AndroidStorageAdapterCpp::getDisk(const std::string& key) {
|
|
69
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(
|
|
70
|
-
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
87
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(alias_ref<JString>)>("getDisk");
|
|
88
|
+
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
71
89
|
if (!result) return std::nullopt;
|
|
72
90
|
return result->toStdString();
|
|
73
91
|
}
|
|
74
92
|
|
|
75
93
|
void AndroidStorageAdapterCpp::deleteDisk(const std::string& key) {
|
|
76
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
77
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
94
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>)>("deleteDisk");
|
|
95
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
78
96
|
}
|
|
79
97
|
|
|
80
98
|
bool AndroidStorageAdapterCpp::hasDisk(const std::string& key) {
|
|
81
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(
|
|
82
|
-
return method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
99
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(alias_ref<JString>)>("hasDisk");
|
|
100
|
+
return method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysDisk() {
|
|
@@ -92,9 +110,9 @@ std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysDisk() {
|
|
|
92
110
|
|
|
93
111
|
std::vector<std::string> AndroidStorageAdapterCpp::getKeysByPrefixDisk(const std::string& prefix) {
|
|
94
112
|
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<
|
|
95
|
-
local_ref<JavaStringArray>(
|
|
113
|
+
local_ref<JavaStringArray>(alias_ref<JString>)
|
|
96
114
|
>("getKeysByPrefixDisk");
|
|
97
|
-
auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), prefix);
|
|
115
|
+
auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(prefix));
|
|
98
116
|
return fromJavaStringArray(keys);
|
|
99
117
|
}
|
|
100
118
|
|
|
@@ -145,25 +163,25 @@ void AndroidStorageAdapterCpp::clearDisk() {
|
|
|
145
163
|
// --- Secure ---
|
|
146
164
|
|
|
147
165
|
void AndroidStorageAdapterCpp::setSecure(const std::string& key, const std::string& value) {
|
|
148
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
149
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key, value);
|
|
166
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>, alias_ref<JString>)>("setSecure");
|
|
167
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key), toJavaString(value));
|
|
150
168
|
}
|
|
151
169
|
|
|
152
170
|
std::optional<std::string> AndroidStorageAdapterCpp::getSecure(const std::string& key) {
|
|
153
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(
|
|
154
|
-
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
171
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(alias_ref<JString>)>("getSecure");
|
|
172
|
+
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
155
173
|
if (!result) return std::nullopt;
|
|
156
174
|
return result->toStdString();
|
|
157
175
|
}
|
|
158
176
|
|
|
159
177
|
void AndroidStorageAdapterCpp::deleteSecure(const std::string& key) {
|
|
160
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
161
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
178
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>)>("deleteSecure");
|
|
179
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
162
180
|
}
|
|
163
181
|
|
|
164
182
|
bool AndroidStorageAdapterCpp::hasSecure(const std::string& key) {
|
|
165
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(
|
|
166
|
-
return method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
183
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(alias_ref<JString>)>("hasSecure");
|
|
184
|
+
return method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
167
185
|
}
|
|
168
186
|
|
|
169
187
|
std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysSecure() {
|
|
@@ -176,9 +194,9 @@ std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysSecure() {
|
|
|
176
194
|
|
|
177
195
|
std::vector<std::string> AndroidStorageAdapterCpp::getKeysByPrefixSecure(const std::string& prefix) {
|
|
178
196
|
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<
|
|
179
|
-
local_ref<JavaStringArray>(
|
|
197
|
+
local_ref<JavaStringArray>(alias_ref<JString>)
|
|
180
198
|
>("getKeysByPrefixSecure");
|
|
181
|
-
auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), prefix);
|
|
199
|
+
auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(prefix));
|
|
182
200
|
return fromJavaStringArray(keys);
|
|
183
201
|
}
|
|
184
202
|
|
|
@@ -242,25 +260,25 @@ void AndroidStorageAdapterCpp::setSecureBiometric(const std::string& key, const
|
|
|
242
260
|
}
|
|
243
261
|
|
|
244
262
|
void AndroidStorageAdapterCpp::setSecureBiometricWithLevel(const std::string& key, const std::string& value, int level) {
|
|
245
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
246
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key, value, level);
|
|
263
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>, alias_ref<JString>, jint)>("setSecureBiometricWithLevel");
|
|
264
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key), toJavaString(value), level);
|
|
247
265
|
}
|
|
248
266
|
|
|
249
267
|
std::optional<std::string> AndroidStorageAdapterCpp::getSecureBiometric(const std::string& key) {
|
|
250
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(
|
|
251
|
-
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
268
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jstring(alias_ref<JString>)>("getSecureBiometric");
|
|
269
|
+
auto result = method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
252
270
|
if (!result) return std::nullopt;
|
|
253
271
|
return result->toStdString();
|
|
254
272
|
}
|
|
255
273
|
|
|
256
274
|
void AndroidStorageAdapterCpp::deleteSecureBiometric(const std::string& key) {
|
|
257
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(
|
|
258
|
-
method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
275
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(alias_ref<JString>)>("deleteSecureBiometric");
|
|
276
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
259
277
|
}
|
|
260
278
|
|
|
261
279
|
bool AndroidStorageAdapterCpp::hasSecureBiometric(const std::string& key) {
|
|
262
|
-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(
|
|
263
|
-
return method(AndroidStorageAdapterJava::javaClassStatic(), key);
|
|
280
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jboolean(alias_ref<JString>)>("hasSecureBiometric");
|
|
281
|
+
return method(AndroidStorageAdapterJava::javaClassStatic(), toJavaString(key));
|
|
264
282
|
}
|
|
265
283
|
|
|
266
284
|
void AndroidStorageAdapterCpp::clearSecureBiometric() {
|
|
@@ -61,6 +61,7 @@ private fun Throwable.wrapStorageException(
|
|
|
61
61
|
class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
62
62
|
private val sharedPreferences: SharedPreferences =
|
|
63
63
|
context.getSharedPreferences("NitroStorage", Context.MODE_PRIVATE)
|
|
64
|
+
private val diskStore: DiskSqliteStore = DiskSqliteStore(context, sharedPreferences)
|
|
64
65
|
|
|
65
66
|
private val masterKeyAlias = "${context.packageName}.nitro_storage.master_key"
|
|
66
67
|
|
|
@@ -519,71 +520,57 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
519
520
|
|
|
520
521
|
@JvmStatic
|
|
521
522
|
fun setDisk(key: String, value: String) {
|
|
522
|
-
getInstanceOrThrow().
|
|
523
|
+
getInstanceOrThrow().diskStore.set(key, value)
|
|
523
524
|
}
|
|
524
525
|
|
|
525
526
|
@JvmStatic
|
|
526
527
|
fun setDiskBatch(keys: Array<String>, values: Array<String>) {
|
|
527
|
-
|
|
528
|
-
val count = minOf(keys.size, values.size)
|
|
529
|
-
for (index in 0 until count) {
|
|
530
|
-
editor.putString(keys[index], values[index])
|
|
531
|
-
}
|
|
532
|
-
editor.apply()
|
|
528
|
+
getInstanceOrThrow().diskStore.setBatch(keys, values)
|
|
533
529
|
}
|
|
534
530
|
|
|
535
531
|
@JvmStatic
|
|
536
532
|
fun getDisk(key: String): String? {
|
|
537
|
-
return getInstanceOrThrow().
|
|
533
|
+
return getInstanceOrThrow().diskStore.get(key)
|
|
538
534
|
}
|
|
539
535
|
|
|
540
536
|
@JvmStatic
|
|
541
537
|
fun getDiskBatch(keys: Array<String>): Array<String?> {
|
|
542
|
-
|
|
543
|
-
return Array(keys.size) { index ->
|
|
544
|
-
prefs.getString(keys[index], null)
|
|
545
|
-
}
|
|
538
|
+
return getInstanceOrThrow().diskStore.getBatch(keys)
|
|
546
539
|
}
|
|
547
540
|
|
|
548
541
|
@JvmStatic
|
|
549
542
|
fun deleteDisk(key: String) {
|
|
550
|
-
getInstanceOrThrow().
|
|
543
|
+
getInstanceOrThrow().diskStore.remove(key)
|
|
551
544
|
}
|
|
552
545
|
|
|
553
546
|
@JvmStatic
|
|
554
547
|
fun deleteDiskBatch(keys: Array<String>) {
|
|
555
|
-
|
|
556
|
-
for (key in keys) {
|
|
557
|
-
editor.remove(key)
|
|
558
|
-
}
|
|
559
|
-
editor.apply()
|
|
548
|
+
getInstanceOrThrow().diskStore.removeBatch(keys)
|
|
560
549
|
}
|
|
561
550
|
|
|
562
551
|
@JvmStatic
|
|
563
552
|
fun hasDisk(key: String): Boolean {
|
|
564
|
-
return getInstanceOrThrow().
|
|
553
|
+
return getInstanceOrThrow().diskStore.has(key)
|
|
565
554
|
}
|
|
566
555
|
|
|
567
556
|
@JvmStatic
|
|
568
557
|
fun getAllKeysDisk(): Array<String> {
|
|
569
|
-
return getInstanceOrThrow().
|
|
558
|
+
return getInstanceOrThrow().diskStore.getAllKeys()
|
|
570
559
|
}
|
|
571
560
|
|
|
572
561
|
@JvmStatic
|
|
573
562
|
fun getKeysByPrefixDisk(prefix: String): Array<String> {
|
|
574
|
-
return getInstanceOrThrow().
|
|
575
|
-
.filter { it.startsWith(prefix) }
|
|
576
|
-
.toTypedArray()
|
|
563
|
+
return getInstanceOrThrow().diskStore.getKeysByPrefix(prefix)
|
|
577
564
|
}
|
|
578
565
|
|
|
579
566
|
@JvmStatic
|
|
580
567
|
fun sizeDisk(): Int {
|
|
581
|
-
return getInstanceOrThrow().
|
|
568
|
+
return getInstanceOrThrow().diskStore.size()
|
|
582
569
|
}
|
|
583
570
|
|
|
584
571
|
@JvmStatic
|
|
585
572
|
fun clearDisk() {
|
|
586
|
-
getInstanceOrThrow().
|
|
573
|
+
getInstanceOrThrow().diskStore.clear()
|
|
587
574
|
}
|
|
588
575
|
|
|
589
576
|
// --- Secure (async apply by default, sync commit when requested) ---
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
package com.nitrostorage
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.SharedPreferences
|
|
5
|
+
import android.database.sqlite.SQLiteDatabase
|
|
6
|
+
import java.io.File
|
|
7
|
+
|
|
8
|
+
internal class DiskSqliteStore(
|
|
9
|
+
context: Context,
|
|
10
|
+
private val legacyPreferences: SharedPreferences,
|
|
11
|
+
) {
|
|
12
|
+
private val db: SQLiteDatabase
|
|
13
|
+
|
|
14
|
+
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()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Synchronized
|
|
30
|
+
fun set(key: String, value: String) {
|
|
31
|
+
db.execSQL(
|
|
32
|
+
"INSERT OR REPLACE INTO kv(key, value) VALUES(?, ?)",
|
|
33
|
+
arrayOf(key, value),
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Synchronized
|
|
38
|
+
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
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Synchronized
|
|
45
|
+
fun remove(key: String) {
|
|
46
|
+
db.execSQL("DELETE FROM kv WHERE key = ?", arrayOf(key))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Synchronized
|
|
50
|
+
fun has(key: String): Boolean {
|
|
51
|
+
db.rawQuery("SELECT 1 FROM kv WHERE key = ? LIMIT 1", arrayOf(key)).use { cursor ->
|
|
52
|
+
return cursor.moveToFirst()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@Synchronized
|
|
57
|
+
fun setBatch(keys: Array<String>, values: Array<String>) {
|
|
58
|
+
val count = minOf(keys.size, values.size)
|
|
59
|
+
db.beginTransaction()
|
|
60
|
+
try {
|
|
61
|
+
for (index in 0 until count) {
|
|
62
|
+
db.execSQL(
|
|
63
|
+
"INSERT OR REPLACE INTO kv(key, value) VALUES(?, ?)",
|
|
64
|
+
arrayOf(keys[index], values[index]),
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
db.setTransactionSuccessful()
|
|
68
|
+
} finally {
|
|
69
|
+
db.endTransaction()
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Synchronized
|
|
74
|
+
fun getBatch(keys: Array<String>): Array<String?> {
|
|
75
|
+
return Array(keys.size) { index -> get(keys[index]) }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Synchronized
|
|
79
|
+
fun removeBatch(keys: Array<String>) {
|
|
80
|
+
db.beginTransaction()
|
|
81
|
+
try {
|
|
82
|
+
for (key in keys) {
|
|
83
|
+
db.execSQL("DELETE FROM kv WHERE key = ?", arrayOf(key))
|
|
84
|
+
}
|
|
85
|
+
db.setTransactionSuccessful()
|
|
86
|
+
} finally {
|
|
87
|
+
db.endTransaction()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Synchronized
|
|
92
|
+
fun getAllKeys(): Array<String> {
|
|
93
|
+
db.rawQuery("SELECT key FROM kv", null).use { cursor ->
|
|
94
|
+
val keys = ArrayList<String>(cursor.count)
|
|
95
|
+
while (cursor.moveToNext()) {
|
|
96
|
+
keys.add(cursor.getString(0))
|
|
97
|
+
}
|
|
98
|
+
return keys.toTypedArray()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@Synchronized
|
|
103
|
+
fun getKeysByPrefix(prefix: String): Array<String> {
|
|
104
|
+
val pattern = buildString {
|
|
105
|
+
for (character in prefix) {
|
|
106
|
+
// LIKE stops at NUL; startsWith below checks the complete prefix.
|
|
107
|
+
if (character == '\u0000') break
|
|
108
|
+
if (character == '%' || character == '_' || character == '\\') append('\\')
|
|
109
|
+
append(character)
|
|
110
|
+
}
|
|
111
|
+
append('%')
|
|
112
|
+
}
|
|
113
|
+
db.rawQuery(
|
|
114
|
+
"SELECT key FROM kv WHERE key LIKE ? ESCAPE '\\'",
|
|
115
|
+
arrayOf(pattern),
|
|
116
|
+
).use { cursor ->
|
|
117
|
+
val keys = ArrayList<String>(cursor.count)
|
|
118
|
+
while (cursor.moveToNext()) {
|
|
119
|
+
val key = cursor.getString(0)
|
|
120
|
+
if (key.startsWith(prefix)) {
|
|
121
|
+
keys.add(key)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return keys.toTypedArray()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Synchronized
|
|
129
|
+
fun size(): Int {
|
|
130
|
+
db.rawQuery("SELECT COUNT(*) FROM kv", null).use { cursor ->
|
|
131
|
+
return if (cursor.moveToFirst()) cursor.getInt(0) else 0
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@Synchronized
|
|
136
|
+
fun clear() {
|
|
137
|
+
db.execSQL("DELETE FROM kv")
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private fun migrateLegacyPreferences() {
|
|
141
|
+
db.rawQuery(
|
|
142
|
+
"SELECT v FROM meta WHERE k = ? LIMIT 1",
|
|
143
|
+
arrayOf(PREFS_MIGRATION_KEY),
|
|
144
|
+
).use { cursor ->
|
|
145
|
+
if (cursor.moveToFirst() && cursor.getString(0) == "1") {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
db.beginTransaction()
|
|
151
|
+
try {
|
|
152
|
+
for ((key, value) in legacyPreferences.all) {
|
|
153
|
+
if (value is String) {
|
|
154
|
+
db.execSQL(
|
|
155
|
+
"INSERT OR IGNORE INTO kv(key, value) VALUES(?, ?)",
|
|
156
|
+
arrayOf(key, value),
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
db.execSQL(
|
|
161
|
+
"INSERT OR REPLACE INTO meta(k, v) VALUES(?, ?)",
|
|
162
|
+
arrayOf(PREFS_MIGRATION_KEY, "1"),
|
|
163
|
+
)
|
|
164
|
+
db.setTransactionSuccessful()
|
|
165
|
+
} finally {
|
|
166
|
+
db.endTransaction()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private companion object {
|
|
171
|
+
const val DATABASE_NAME = "nitro-storage-disk.sqlite"
|
|
172
|
+
const val PREFS_MIGRATION_KEY = "prefs_v1"
|
|
173
|
+
}
|
|
174
|
+
}
|