strata-storage 2.6.1 → 2.7.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/AI-INTEGRATION-GUIDE.md +88 -10
- package/README.md +42 -64
- package/android/AGENTS.md +18 -3
- package/android/CLAUDE.md +36 -3
- package/android/build.gradle +5 -1
- package/android/src/main/java/com/strata/storage/EncryptedStorage.java +31 -18
- package/android/src/main/java/com/strata/storage/FilesystemStorage.java +62 -10
- package/android/src/main/java/com/strata/storage/SQLiteStorage.java +65 -13
- package/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +63 -51
- package/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +80 -11
- package/dist/README.md +42 -64
- package/dist/adapters/capacitor/FilesystemAdapter.d.ts +2 -2
- package/dist/adapters/capacitor/FilesystemAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitor/FilesystemAdapter.js +6 -2
- package/dist/adapters/capacitor/PreferencesAdapter.d.ts +2 -2
- package/dist/adapters/capacitor/PreferencesAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitor/PreferencesAdapter.js +6 -2
- package/dist/adapters/capacitor/SecureAdapter.d.ts +3 -3
- package/dist/adapters/capacitor/SecureAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitor/SecureAdapter.js +6 -2
- package/dist/adapters/capacitor/SqliteAdapter.d.ts +24 -3
- package/dist/adapters/capacitor/SqliteAdapter.d.ts.map +1 -1
- package/dist/adapters/capacitor/SqliteAdapter.js +105 -23
- package/dist/adapters/capacitor/index.d.ts +4 -4
- package/dist/adapters/web/CacheAdapter.d.ts +2 -2
- package/dist/adapters/web/CookieAdapter.d.ts +2 -2
- package/dist/adapters/web/IndexedDBAdapter.d.ts +10 -3
- package/dist/adapters/web/IndexedDBAdapter.d.ts.map +1 -1
- package/dist/adapters/web/IndexedDBAdapter.js +14 -3
- package/dist/adapters/web/LocalStorageAdapter.d.ts +2 -2
- package/dist/adapters/web/MemoryAdapter.d.ts +2 -2
- package/dist/adapters/web/SessionStorageAdapter.d.ts +10 -10
- package/dist/adapters/web/SessionStorageAdapter.d.ts.map +1 -1
- package/dist/adapters/web/SessionStorageAdapter.js +3 -2
- package/dist/adapters/web/URLAdapter.d.ts +2 -2
- package/dist/adapters/web/index.d.ts +7 -7
- package/dist/capacitor.d.ts +6 -6
- package/dist/capacitor.d.ts.map +1 -1
- package/dist/capacitor.js +5 -4
- package/dist/core/AdapterRegistry.d.ts +1 -1
- package/dist/core/BaseAdapter.d.ts +21 -5
- package/dist/core/BaseAdapter.d.ts.map +1 -1
- package/dist/core/BaseAdapter.js +29 -1
- package/dist/core/Strata.d.ts +41 -7
- package/dist/core/Strata.d.ts.map +1 -1
- package/dist/core/Strata.js +476 -118
- package/dist/features/compression/index.d.ts +1 -1
- package/dist/features/compression.d.ts +5 -1
- package/dist/features/compression.d.ts.map +1 -1
- package/dist/features/encryption/index.d.ts +1 -1
- package/dist/features/encryption.d.ts +55 -2
- package/dist/features/encryption.d.ts.map +1 -1
- package/dist/features/encryption.js +139 -26
- package/dist/features/migration.d.ts +1 -1
- package/dist/features/observer/index.d.ts +1 -1
- package/dist/features/observer.d.ts +1 -1
- package/dist/features/query/index.d.ts +1 -1
- package/dist/features/query.d.ts +1 -1
- package/dist/features/query.d.ts.map +1 -1
- package/dist/features/query.js +3 -1
- package/dist/features/sync/index.d.ts +1 -1
- package/dist/features/sync.d.ts +19 -3
- package/dist/features/sync.d.ts.map +1 -1
- package/dist/features/sync.js +57 -9
- package/dist/features/ttl/index.d.ts +1 -1
- package/dist/features/ttl.d.ts +2 -2
- package/dist/firebase.d.ts +1 -1
- package/dist/firebase.d.ts.map +1 -1
- package/dist/firebase.js +34 -9
- package/dist/index.d.ts +24 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/integrations/angular/index.d.ts +2 -2
- package/dist/integrations/angular/index.d.ts.map +1 -1
- package/dist/integrations/angular/index.js +7 -8
- package/dist/integrations/react/index.d.ts +3 -3
- package/dist/integrations/react/index.d.ts.map +1 -1
- package/dist/integrations/react/index.js +16 -4
- package/dist/integrations/vue/index.d.ts +2 -2
- package/dist/integrations/vue/index.d.ts.map +1 -1
- package/dist/integrations/vue/index.js +5 -2
- package/dist/package.json +11 -2
- package/dist/plugin/definitions.d.ts +29 -3
- package/dist/plugin/definitions.d.ts.map +1 -1
- package/dist/plugin/index.d.ts +2 -2
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +3 -0
- package/dist/plugin/web.d.ts +11 -2
- package/dist/plugin/web.d.ts.map +1 -1
- package/dist/plugin/web.js +23 -4
- package/dist/types/index.d.ts +50 -20
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -10
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +7 -0
- package/ios/AGENTS.md +14 -3
- package/ios/CLAUDE.md +25 -3
- package/ios/Plugin/FilesystemStorage.swift +19 -0
- package/ios/Plugin/SQLiteStorage.swift +59 -9
- package/ios/Plugin/StrataStoragePlugin.m +1 -0
- package/ios/Plugin/StrataStoragePlugin.swift +87 -34
- package/ios/Plugin/UserDefaultsStorage.swift +47 -46
- package/package.json +27 -24
- package/scripts/build.js +16 -14
- package/scripts/cli.js +1 -1
- package/scripts/configure.js +2 -2
- package/scripts/postinstall.js +3 -2
- package/dist/android/AGENTS.md +0 -51
- package/dist/android/CLAUDE.md +0 -89
- package/dist/android/build.gradle +0 -58
- package/dist/android/proguard-rules.pro +0 -11
- package/dist/android/settings.gradle +0 -2
- package/dist/android/src/main/AndroidManifest.xml +0 -2
- package/dist/android/src/main/java/com/strata/storage/EncryptedStorage.java +0 -218
- package/dist/android/src/main/java/com/strata/storage/FilesystemStorage.java +0 -287
- package/dist/android/src/main/java/com/strata/storage/SQLiteStorage.java +0 -371
- package/dist/android/src/main/java/com/strata/storage/SharedPreferencesStorage.java +0 -209
- package/dist/android/src/main/java/com/stratastorage/StrataStoragePlugin.java +0 -616
- package/dist/core/StorageStrategy.d.ts +0 -55
- package/dist/core/StorageStrategy.d.ts.map +0 -1
- package/dist/core/StorageStrategy.js +0 -200
- package/dist/ios/AGENTS.md +0 -48
- package/dist/ios/CLAUDE.md +0 -84
- package/dist/ios/Plugin/FilesystemStorage.swift +0 -218
- package/dist/ios/Plugin/KeychainStorage.swift +0 -233
- package/dist/ios/Plugin/SQLiteStorage.swift +0 -426
- package/dist/ios/Plugin/StrataStoragePlugin.m +0 -23
- package/dist/ios/Plugin/StrataStoragePlugin.swift +0 -411
- package/dist/ios/Plugin/UserDefaultsStorage.swift +0 -87
|
@@ -14,6 +14,7 @@ import java.util.HashSet;
|
|
|
14
14
|
import java.util.List;
|
|
15
15
|
import java.util.Map;
|
|
16
16
|
import java.util.Set;
|
|
17
|
+
import java.util.regex.Pattern;
|
|
17
18
|
import org.json.JSONException;
|
|
18
19
|
import org.json.JSONObject;
|
|
19
20
|
|
|
@@ -47,6 +48,9 @@ public class SQLiteStorage extends SQLiteOpenHelper {
|
|
|
47
48
|
private static final String KEY_TAGS = "tags";
|
|
48
49
|
private static final String KEY_METADATA = "metadata";
|
|
49
50
|
|
|
51
|
+
/** Strict SQL-identifier allow-list for table names (cannot be bound). */
|
|
52
|
+
private static final Pattern IDENTIFIER = Pattern.compile("^[A-Za-z_][A-Za-z0-9_]*$");
|
|
53
|
+
|
|
50
54
|
/** Tables already created in this DB during the process lifetime. */
|
|
51
55
|
private final Set<String> ensuredTables = new HashSet<>();
|
|
52
56
|
|
|
@@ -60,16 +64,25 @@ public class SQLiteStorage extends SQLiteOpenHelper {
|
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
/**
|
|
63
|
-
*
|
|
64
|
-
* Table names cannot be passed as bound
|
|
65
|
-
* be validated before being interpolated
|
|
67
|
+
* Validate a table name as a strict SQL identifier
|
|
68
|
+
* ({@code ^[A-Za-z_][A-Za-z0-9_]*$}). Table names cannot be passed as bound
|
|
69
|
+
* parameters in SQLite, so they MUST be validated before being interpolated
|
|
70
|
+
* into DDL/DML. A null/empty name resolves to the default table; any other
|
|
71
|
+
* non-conforming name is REJECTED ({@link IllegalArgumentException}) rather
|
|
72
|
+
* than silently stripped — the TS adapter applies the same allow-list and
|
|
73
|
+
* rejects before the call ever reaches here, so this is defense-in-depth
|
|
74
|
+
* against a direct-plugin caller.
|
|
66
75
|
*/
|
|
67
76
|
public static String sanitizeTable(String table) {
|
|
68
77
|
if (table == null || table.isEmpty()) {
|
|
69
78
|
return DEFAULT_TABLE;
|
|
70
79
|
}
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
if (!IDENTIFIER.matcher(table).matches()) {
|
|
81
|
+
throw new IllegalArgumentException(
|
|
82
|
+
"Invalid SQLite table name: " + table
|
|
83
|
+
+ " (must match ^[A-Za-z_][A-Za-z0-9_]*$)");
|
|
84
|
+
}
|
|
85
|
+
return table;
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
@Override
|
|
@@ -209,17 +222,23 @@ public class SQLiteStorage extends SQLiteOpenHelper {
|
|
|
209
222
|
|
|
210
223
|
public synchronized List<String> keys(String table, String pattern) {
|
|
211
224
|
String safeTable = sanitizeTable(table);
|
|
225
|
+
long now = System.currentTimeMillis();
|
|
212
226
|
List<String> keys = new ArrayList<>();
|
|
213
227
|
SQLiteDatabase db = getReadableDatabase();
|
|
214
228
|
ensureTable(db, safeTable);
|
|
215
229
|
Cursor cursor = null;
|
|
230
|
+
// Exclude expired rows in SQL so the TS adapter does not need a per-key
|
|
231
|
+
// get() to drop them (eliminates the keys() N+1).
|
|
232
|
+
String notExpired = "(" + KEY_EXPIRES + " IS NULL OR " + KEY_EXPIRES + " > ?)";
|
|
233
|
+
String nowArg = String.valueOf(now);
|
|
216
234
|
try {
|
|
217
235
|
if (pattern != null) {
|
|
218
|
-
cursor = db.query(safeTable, new String[]{KEY_ID},
|
|
219
|
-
|
|
236
|
+
cursor = db.query(safeTable, new String[]{KEY_ID},
|
|
237
|
+
KEY_ID + " LIKE ? AND " + notExpired,
|
|
238
|
+
new String[]{"%" + pattern + "%", nowArg}, null, null, null, null);
|
|
220
239
|
} else {
|
|
221
240
|
cursor = db.query(safeTable, new String[]{KEY_ID},
|
|
222
|
-
|
|
241
|
+
notExpired, new String[]{nowArg}, null, null, null, null);
|
|
223
242
|
}
|
|
224
243
|
if (cursor != null && cursor.moveToFirst()) {
|
|
225
244
|
do {
|
|
@@ -251,23 +270,39 @@ public class SQLiteStorage extends SQLiteOpenHelper {
|
|
|
251
270
|
}
|
|
252
271
|
|
|
253
272
|
/**
|
|
254
|
-
* Enumerate
|
|
255
|
-
*
|
|
256
|
-
*
|
|
273
|
+
* Enumerate non-expired rows as {@code { key, value }} where {@code value}
|
|
274
|
+
* is the full {@link StorageValue} wrapper (parsed from the stored JSON).
|
|
275
|
+
* Returning the wrapper here lets the TS {@code SqliteAdapter} run the real
|
|
276
|
+
* query condition in JS from a SINGLE round-trip instead of re-fetching
|
|
277
|
+
* each key (eliminates the query() N+1). Expired rows are filtered in SQL;
|
|
278
|
+
* an unparseable legacy payload surfaces as key-only (the TS side then
|
|
279
|
+
* falls back to {@code get()}, which treats it as a miss).
|
|
257
280
|
*/
|
|
258
281
|
public synchronized List<Map<String, Object>> query(String table) {
|
|
259
282
|
String safeTable = sanitizeTable(table);
|
|
283
|
+
long now = System.currentTimeMillis();
|
|
260
284
|
List<Map<String, Object>> rows = new ArrayList<>();
|
|
261
285
|
SQLiteDatabase db = getReadableDatabase();
|
|
262
286
|
ensureTable(db, safeTable);
|
|
263
287
|
Cursor cursor = null;
|
|
264
288
|
try {
|
|
265
|
-
cursor = db.query(safeTable, new String[]{KEY_ID},
|
|
266
|
-
|
|
289
|
+
cursor = db.query(safeTable, new String[]{KEY_ID, KEY_VALUE},
|
|
290
|
+
"(" + KEY_EXPIRES + " IS NULL OR " + KEY_EXPIRES + " > ?)",
|
|
291
|
+
new String[]{String.valueOf(now)}, null, null, null, null);
|
|
267
292
|
if (cursor != null && cursor.moveToFirst()) {
|
|
268
293
|
do {
|
|
269
294
|
Map<String, Object> row = new HashMap<>();
|
|
270
295
|
row.put("key", cursor.getString(0));
|
|
296
|
+
String stored = cursor.getString(1);
|
|
297
|
+
if (stored != null) {
|
|
298
|
+
try {
|
|
299
|
+
row.put("value", new JSObject(stored));
|
|
300
|
+
} catch (JSONException parseError) {
|
|
301
|
+
// Unparseable legacy payload: surface key only.
|
|
302
|
+
Log.w("StrataStorage", "Unparseable SQLite value in query for key "
|
|
303
|
+
+ cursor.getString(0));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
271
306
|
rows.add(row);
|
|
272
307
|
} while (cursor.moveToNext());
|
|
273
308
|
}
|
|
@@ -279,6 +314,23 @@ public class SQLiteStorage extends SQLiteOpenHelper {
|
|
|
279
314
|
}
|
|
280
315
|
}
|
|
281
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Delete every expired row ({@code expires <= now}) in one statement and
|
|
319
|
+
* return the number removed. Because {@link #keys} and {@link #query} now
|
|
320
|
+
* filter expired rows in SQL (no lazy per-key deletion on read), this is
|
|
321
|
+
* what reclaims their physical storage; the TS adapter calls it on its TTL
|
|
322
|
+
* cleanup tick.
|
|
323
|
+
*/
|
|
324
|
+
public synchronized int cleanupExpired(String table) {
|
|
325
|
+
String safeTable = sanitizeTable(table);
|
|
326
|
+
long now = System.currentTimeMillis();
|
|
327
|
+
SQLiteDatabase db = getWritableDatabase();
|
|
328
|
+
ensureTable(db, safeTable);
|
|
329
|
+
return db.delete(safeTable,
|
|
330
|
+
KEY_EXPIRES + " IS NOT NULL AND " + KEY_EXPIRES + " <= ?",
|
|
331
|
+
new String[]{String.valueOf(now)});
|
|
332
|
+
}
|
|
333
|
+
|
|
282
334
|
/**
|
|
283
335
|
* Size information. {@code total} = Σ(length(key) + length(value)), matching
|
|
284
336
|
* the web adapters' convention (key bytes are included). When {@code
|
|
@@ -3,6 +3,7 @@ package com.strata.storage;
|
|
|
3
3
|
import android.content.Context;
|
|
4
4
|
import android.content.SharedPreferences;
|
|
5
5
|
import android.util.Log;
|
|
6
|
+
import java.lang.reflect.Array;
|
|
6
7
|
import java.util.Map;
|
|
7
8
|
import java.util.Set;
|
|
8
9
|
import java.util.HashSet;
|
|
@@ -13,8 +14,8 @@ import org.json.JSONArray;
|
|
|
13
14
|
|
|
14
15
|
public class SharedPreferencesStorage {
|
|
15
16
|
private final SharedPreferences prefs;
|
|
16
|
-
private final SharedPreferences.Editor editor;
|
|
17
17
|
private static final String DEFAULT_NAME = "StrataStorage";
|
|
18
|
+
private static final String KEY_PREFIX = "strata_storage::";
|
|
18
19
|
|
|
19
20
|
public SharedPreferencesStorage(Context context) {
|
|
20
21
|
this(context, DEFAULT_NAME);
|
|
@@ -22,21 +23,30 @@ public class SharedPreferencesStorage {
|
|
|
22
23
|
|
|
23
24
|
public SharedPreferencesStorage(Context context, String name) {
|
|
24
25
|
this.prefs = context.getSharedPreferences(name != null ? name : DEFAULT_NAME, Context.MODE_PRIVATE);
|
|
25
|
-
this.editor = prefs.edit();
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
|
|
28
|
+
private String physicalKey(String key) {
|
|
29
|
+
return KEY_PREFIX + key;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private String logicalKey(String key) {
|
|
33
|
+
return key.startsWith(KEY_PREFIX) ? key.substring(KEY_PREFIX.length()) : key;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public synchronized boolean set(String key, Object value) {
|
|
29
37
|
try {
|
|
38
|
+
SharedPreferences.Editor editor = prefs.edit();
|
|
39
|
+
String targetKey = physicalKey(key);
|
|
30
40
|
if (value instanceof String) {
|
|
31
|
-
editor.putString(
|
|
41
|
+
editor.putString(targetKey, (String) value);
|
|
32
42
|
} else if (value instanceof Integer) {
|
|
33
|
-
editor.putInt(
|
|
43
|
+
editor.putInt(targetKey, (Integer) value);
|
|
34
44
|
} else if (value instanceof Long) {
|
|
35
|
-
editor.putLong(
|
|
45
|
+
editor.putLong(targetKey, (Long) value);
|
|
36
46
|
} else if (value instanceof Float) {
|
|
37
|
-
editor.putFloat(
|
|
47
|
+
editor.putFloat(targetKey, (Float) value);
|
|
38
48
|
} else if (value instanceof Boolean) {
|
|
39
|
-
editor.putBoolean(
|
|
49
|
+
editor.putBoolean(targetKey, (Boolean) value);
|
|
40
50
|
} else if (value instanceof Set) {
|
|
41
51
|
// Safely convert to Set<String>
|
|
42
52
|
Set<?> rawSet = (Set<?>) value;
|
|
@@ -44,7 +54,7 @@ public class SharedPreferencesStorage {
|
|
|
44
54
|
for (Object item : rawSet) {
|
|
45
55
|
stringSet.add(item != null ? item.toString() : "null");
|
|
46
56
|
}
|
|
47
|
-
editor.putStringSet(
|
|
57
|
+
editor.putStringSet(targetKey, stringSet);
|
|
48
58
|
} else {
|
|
49
59
|
// Convert complex objects to JSON
|
|
50
60
|
String json;
|
|
@@ -59,7 +69,7 @@ public class SharedPreferencesStorage {
|
|
|
59
69
|
json = value.toString();
|
|
60
70
|
}
|
|
61
71
|
}
|
|
62
|
-
editor.putString(
|
|
72
|
+
editor.putString(targetKey, json);
|
|
63
73
|
}
|
|
64
74
|
return editor.commit();
|
|
65
75
|
} catch (Exception e) {
|
|
@@ -68,13 +78,14 @@ public class SharedPreferencesStorage {
|
|
|
68
78
|
}
|
|
69
79
|
}
|
|
70
80
|
|
|
71
|
-
public Object get(String key) {
|
|
81
|
+
public synchronized Object get(String key) {
|
|
72
82
|
Map<String, ?> all = prefs.getAll();
|
|
73
|
-
return all.get(key);
|
|
83
|
+
return all.get(physicalKey(key));
|
|
74
84
|
}
|
|
75
|
-
|
|
76
|
-
public boolean remove(String key) {
|
|
77
|
-
editor.
|
|
85
|
+
|
|
86
|
+
public synchronized boolean remove(String key) {
|
|
87
|
+
SharedPreferences.Editor editor = prefs.edit();
|
|
88
|
+
editor.remove(physicalKey(key));
|
|
78
89
|
return editor.commit();
|
|
79
90
|
}
|
|
80
91
|
|
|
@@ -82,21 +93,20 @@ public class SharedPreferencesStorage {
|
|
|
82
93
|
return clear(null);
|
|
83
94
|
}
|
|
84
95
|
|
|
85
|
-
public boolean clear(String prefix) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (key.startsWith(prefix) || key.contains(prefix)) {
|
|
91
|
-
keysToRemove.add(key);
|
|
92
|
-
}
|
|
96
|
+
public synchronized boolean clear(String prefix) {
|
|
97
|
+
Set<String> keysToRemove = new HashSet<>();
|
|
98
|
+
for (String key : prefs.getAll().keySet()) {
|
|
99
|
+
if (!key.startsWith(KEY_PREFIX)) {
|
|
100
|
+
continue;
|
|
93
101
|
}
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
String logical = logicalKey(key);
|
|
103
|
+
if (prefix == null || logical.startsWith(prefix) || logical.contains(prefix)) {
|
|
104
|
+
keysToRemove.add(key);
|
|
96
105
|
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
}
|
|
107
|
+
SharedPreferences.Editor editor = prefs.edit();
|
|
108
|
+
for (String key : keysToRemove) {
|
|
109
|
+
editor.remove(key);
|
|
100
110
|
}
|
|
101
111
|
return editor.commit();
|
|
102
112
|
}
|
|
@@ -106,37 +116,38 @@ public class SharedPreferencesStorage {
|
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
public List<String> keys(String pattern) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
for (String key : allKeys) {
|
|
118
|
-
if (key.startsWith(pattern) || key.contains(pattern)) {
|
|
119
|
-
filteredKeys.add(key);
|
|
119
|
+
List<String> keys = new ArrayList<>();
|
|
120
|
+
for (String physical : prefs.getAll().keySet()) {
|
|
121
|
+
if (!physical.startsWith(KEY_PREFIX)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
String key = logicalKey(physical);
|
|
125
|
+
if (pattern == null || key.startsWith(pattern) || key.contains(pattern)) {
|
|
126
|
+
keys.add(key);
|
|
120
127
|
}
|
|
121
128
|
}
|
|
122
|
-
return
|
|
129
|
+
return keys;
|
|
123
130
|
}
|
|
124
|
-
|
|
125
|
-
public boolean has(String key) {
|
|
126
|
-
return prefs.contains(key);
|
|
131
|
+
|
|
132
|
+
public synchronized boolean has(String key) {
|
|
133
|
+
return prefs.contains(physicalKey(key));
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
public Map<String, ?> getAll() {
|
|
130
137
|
return prefs.getAll();
|
|
131
138
|
}
|
|
132
139
|
|
|
133
|
-
public SizeInfo size() {
|
|
140
|
+
public synchronized SizeInfo size() {
|
|
134
141
|
Map<String, ?> all = prefs.getAll();
|
|
135
142
|
long totalSize = 0;
|
|
136
|
-
int count =
|
|
137
|
-
|
|
143
|
+
int count = 0;
|
|
144
|
+
|
|
138
145
|
for (Map.Entry<String, ?> entry : all.entrySet()) {
|
|
139
|
-
String
|
|
146
|
+
String physical = entry.getKey();
|
|
147
|
+
if (!physical.startsWith(KEY_PREFIX)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
String key = logicalKey(physical);
|
|
140
151
|
Object value = entry.getValue();
|
|
141
152
|
|
|
142
153
|
// Estimate size (key + value in bytes)
|
|
@@ -144,6 +155,7 @@ public class SharedPreferencesStorage {
|
|
|
144
155
|
if (value != null) {
|
|
145
156
|
totalSize += value.toString().getBytes().length;
|
|
146
157
|
}
|
|
158
|
+
count++;
|
|
147
159
|
}
|
|
148
160
|
|
|
149
161
|
return new SizeInfo(totalSize, count);
|
|
@@ -179,9 +191,9 @@ public class SharedPreferencesStorage {
|
|
|
179
191
|
jsonArray.put(item);
|
|
180
192
|
}
|
|
181
193
|
} else {
|
|
182
|
-
|
|
183
|
-
for (
|
|
184
|
-
jsonArray.put(
|
|
194
|
+
int len = Array.getLength(obj);
|
|
195
|
+
for (int i = 0; i < len; i++) {
|
|
196
|
+
jsonArray.put(Array.get(obj, i));
|
|
185
197
|
}
|
|
186
198
|
}
|
|
187
199
|
return jsonArray.toString();
|
|
@@ -12,9 +12,11 @@ import com.strata.storage.FilesystemStorage;
|
|
|
12
12
|
import com.strata.storage.SQLiteStorage;
|
|
13
13
|
import android.util.Log;
|
|
14
14
|
import org.json.JSONArray;
|
|
15
|
+
import org.json.JSONException;
|
|
15
16
|
import java.util.HashMap;
|
|
16
17
|
import java.util.List;
|
|
17
18
|
import java.util.Map;
|
|
19
|
+
import java.util.regex.Pattern;
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Main Capacitor plugin for Strata Storage
|
|
@@ -32,6 +34,9 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
32
34
|
private static final String DEFAULT_DATABASE = "strata_storage";
|
|
33
35
|
private static final String DEFAULT_TABLE = "storage";
|
|
34
36
|
|
|
37
|
+
/** Strict SQL-identifier allow-list for the database file stem. */
|
|
38
|
+
private static final Pattern SQLITE_IDENTIFIER = Pattern.compile("^[A-Za-z_][A-Za-z0-9_]*$");
|
|
39
|
+
|
|
35
40
|
private SharedPreferencesStorage sharedPrefsStorage;
|
|
36
41
|
private EncryptedStorage encryptedStorage;
|
|
37
42
|
private FilesystemStorage filesystemStorage;
|
|
@@ -61,25 +66,37 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
/**
|
|
64
|
-
*
|
|
65
|
-
* and return the
|
|
69
|
+
* Validate a database name as a strict SQL identifier
|
|
70
|
+
* ({@code ^[A-Za-z_][A-Za-z0-9_]*$}) and return it as the file stem. The
|
|
71
|
+
* ".db" suffix is appended when opening. A null/empty name resolves to the
|
|
72
|
+
* default; any other non-conforming name is REJECTED rather than silently
|
|
73
|
+
* stripped (the TS adapter applies the same allow-list and rejects first —
|
|
74
|
+
* this is defense-in-depth, and also blocks path separators / traversal).
|
|
66
75
|
*/
|
|
67
76
|
private static String sanitizeDatabase(String database) {
|
|
68
77
|
if (database == null || database.isEmpty()) {
|
|
69
78
|
return DEFAULT_DATABASE;
|
|
70
79
|
}
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
if (!SQLITE_IDENTIFIER.matcher(database).matches()) {
|
|
81
|
+
throw new IllegalArgumentException("Invalid SQLite database name: " + database);
|
|
82
|
+
}
|
|
83
|
+
return database;
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
/**
|
|
76
87
|
* Return (creating + caching on first use) the SQLite helper for the given
|
|
77
|
-
* database name. The cache is keyed by the
|
|
78
|
-
* connection is reused across bridge calls.
|
|
79
|
-
* helper cannot be constructed.
|
|
88
|
+
* database name. The cache is keyed by the validated DB stem so the
|
|
89
|
+
* connection is reused across bridge calls. Returns {@code null} if the name
|
|
90
|
+
* is invalid or the helper cannot be constructed — callers reject the call.
|
|
80
91
|
*/
|
|
81
92
|
private synchronized SQLiteStorage getSqliteStore(String database) {
|
|
82
|
-
String dbStem
|
|
93
|
+
final String dbStem;
|
|
94
|
+
try {
|
|
95
|
+
dbStem = sanitizeDatabase(database);
|
|
96
|
+
} catch (IllegalArgumentException e) {
|
|
97
|
+
Log.e("StrataStorage", "Invalid SQLite database name", e);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
83
100
|
SQLiteStorage store = sqliteStores.get(dbStem);
|
|
84
101
|
if (store == null) {
|
|
85
102
|
try {
|
|
@@ -189,12 +206,32 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
189
206
|
JSObject result = new JSObject();
|
|
190
207
|
if (value == null) {
|
|
191
208
|
result.put("value", JSObject.NULL);
|
|
209
|
+
} else if (value instanceof String) {
|
|
210
|
+
// preferences/secure persist the full StorageValue wrapper as a JSON
|
|
211
|
+
// string; parse it back to an object so TTL/tags/metadata round-trip
|
|
212
|
+
// (matching the sqlite/filesystem contract). Legacy/plain strings are
|
|
213
|
+
// returned unchanged.
|
|
214
|
+
result.put("value", parseWrapperOrRaw((String) value));
|
|
192
215
|
} else {
|
|
193
216
|
result.put("value", value);
|
|
194
217
|
}
|
|
195
218
|
call.resolve(result);
|
|
196
219
|
}
|
|
197
220
|
|
|
221
|
+
/** Parse a stored JSON-object wrapper string back into a {@link JSObject};
|
|
222
|
+
* return the raw string for non-JSON-object payloads. */
|
|
223
|
+
private Object parseWrapperOrRaw(String raw) {
|
|
224
|
+
String trimmed = raw.trim();
|
|
225
|
+
if (trimmed.startsWith("{")) {
|
|
226
|
+
try {
|
|
227
|
+
return new JSObject(raw);
|
|
228
|
+
} catch (JSONException ignored) {
|
|
229
|
+
// Not a JSON object — fall through and return the raw string.
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return raw;
|
|
233
|
+
}
|
|
234
|
+
|
|
198
235
|
/**
|
|
199
236
|
* Set value in storage.
|
|
200
237
|
*
|
|
@@ -513,9 +550,10 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
513
550
|
|
|
514
551
|
/**
|
|
515
552
|
* Query SQLite-backed storage.
|
|
516
|
-
* Matches the optional `query` method in the JS contract:
|
|
517
|
-
*
|
|
518
|
-
*
|
|
553
|
+
* Matches the optional `query` method in the JS contract: resolves
|
|
554
|
+
* { results: [{ key, value }] } where `value` is the full StorageValue
|
|
555
|
+
* wrapper for each non-expired row, so the JS SqliteAdapter can apply the
|
|
556
|
+
* condition in one round-trip without re-fetching each key.
|
|
519
557
|
*/
|
|
520
558
|
@PluginMethod
|
|
521
559
|
public void query(PluginCall call) {
|
|
@@ -536,6 +574,10 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
536
574
|
for (Map<String, Object> row : rows) {
|
|
537
575
|
JSObject obj = new JSObject();
|
|
538
576
|
obj.put("key", row.get("key"));
|
|
577
|
+
Object value = row.get("value");
|
|
578
|
+
if (value != null) {
|
|
579
|
+
obj.put("value", value);
|
|
580
|
+
}
|
|
539
581
|
results.put(obj);
|
|
540
582
|
}
|
|
541
583
|
JSObject result = new JSObject();
|
|
@@ -546,6 +588,33 @@ public class StrataStoragePlugin extends Plugin {
|
|
|
546
588
|
}
|
|
547
589
|
}
|
|
548
590
|
|
|
591
|
+
/**
|
|
592
|
+
* Delete every expired row for a SQLite store in one statement and resolve
|
|
593
|
+
* { removed: <count> }. The JS adapter calls this on its TTL cleanup tick,
|
|
594
|
+
* since keys()/query() now exclude expired rows in SQL (no per-key delete).
|
|
595
|
+
*/
|
|
596
|
+
@PluginMethod
|
|
597
|
+
public void cleanupExpired(PluginCall call) {
|
|
598
|
+
String storage = call.getString("storage", "sqlite");
|
|
599
|
+
if (!"sqlite".equals(storage)) {
|
|
600
|
+
call.reject("cleanupExpired is only supported for the 'sqlite' storage type");
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
SQLiteStorage store = getSqliteStore(call.getString("database", DEFAULT_DATABASE));
|
|
604
|
+
if (store == null) {
|
|
605
|
+
call.reject("SQLite storage not available");
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
try {
|
|
609
|
+
int removed = store.cleanupExpired(call.getString("table", DEFAULT_TABLE));
|
|
610
|
+
JSObject result = new JSObject();
|
|
611
|
+
result.put("removed", removed);
|
|
612
|
+
call.resolve(result);
|
|
613
|
+
} catch (Exception e) {
|
|
614
|
+
call.reject("Failed to clean up expired SQLite rows", e);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
549
618
|
/**
|
|
550
619
|
* Android-specific: read an encrypted preference.
|
|
551
620
|
* Resolves { value: unknown }. Honors an optional `fileName`.
|