diff --git a/.changeset/tender-hairs-grab.md b/.changeset/tender-hairs-grab.md new file mode 100644 index 00000000..eb7713c3 --- /dev/null +++ b/.changeset/tender-hairs-grab.md @@ -0,0 +1,5 @@ +--- +"nxjs-runtime": patch +--- + +Support arbitrary key name length in `localStorage` diff --git a/packages/runtime/src/$.ts b/packages/runtime/src/$.ts index a0202c65..ccb077d0 100644 --- a/packages/runtime/src/$.ts +++ b/packages/runtime/src/$.ts @@ -118,6 +118,7 @@ export interface Init { // crypto.c cryptoDigest(algorithm: string, buf: ArrayBuffer): Promise; cryptoRandomBytes(buf: ArrayBuffer, offset: number, length: number): void; + sha256Hex(str: string): string; // dommatrix.c dommatrixNew(values?: number[]): DOMMatrix | DOMMatrixReadOnly; diff --git a/packages/runtime/src/storage.ts b/packages/runtime/src/storage.ts index e4f86cc8..2fc5c9da 100644 --- a/packages/runtime/src/storage.ts +++ b/packages/runtime/src/storage.ts @@ -1,21 +1,17 @@ -import { - readDirSync, - readFileSync, - removeSync, - statSync, - writeFileSync, -} from './fs'; +import { $ } from './$'; +import { readFileSync, removeSync, writeFileSync } from './fs'; import { URL } from './polyfills/url'; import { Profile } from './switch/profile'; import { Application } from './switch/ns'; import { INTERNAL_SYMBOL } from './internal'; import { + def, assertInternalConstructor, createInternal, decodeUTF16, - def, encodeUTF16, } from './utils'; +import { decoder } from './polyfills/text-decoder'; interface StorageImpl { clear(): void; @@ -137,52 +133,52 @@ Object.defineProperty(globalThis, 'localStorage', { saveData = self.createProfileSaveDataSync(profile); } const base = new URL('localStorage/', saveData.mount()); + const keyMapUrl = new URL('keys.json', base); + let keyMap: Record = (() => { + const keyMapBuffer = readFileSync(keyMapUrl); + if (!keyMapBuffer) return {}; + return JSON.parse(decoder.decode(keyMapBuffer)); + })(); - const keyToPath = (key: any) => { - const url = `${base.href}_${Array.from(String(key)) - .map((l) => l.charCodeAt(0).toString(16)) - .join('_')}`; - return url; - }; - - const pathToKey = (path: string) => { - const key = String.fromCharCode( - ...path - .slice(1) - .split('_') - .map((h) => parseInt(h, 16)), - ); - return key; - }; + const keyDigest = (key: any) => $.sha256Hex(String(key)); const impl: StorageImpl = { clear() { + keyMap = {}; removeSync(base); saveData!.commit(); }, getItem(key: string): string | null { - const b = readFileSync(keyToPath(key)); + const d = keyDigest(key); + const b = readFileSync(new URL(d, base)); if (!b) return null; return decodeUTF16(b); }, key(index: number): string | null { if (index < 0) return null; const i = index % 0x100000000; - const keys = readDirSync(base) || []; - if (i >= keys.length) return null; - return pathToKey(keys[i]); + const v = Object.values(keyMap); + if (i >= v.length) return null; + return v[i] ?? null; }, removeItem(key: string): void { - removeSync(keyToPath(key)); + const d = keyDigest(key); + removeSync(new URL(d, base)); + delete keyMap[d]; + writeFileSync(keyMapUrl, JSON.stringify(keyMap)); saveData!.commit(); }, setItem(key: string, value: string): void { - writeFileSync(keyToPath(key), encodeUTF16(String(value))); + const d = keyDigest(key); + writeFileSync(new URL(d, base), encodeUTF16(String(value))); + if (!keyMap[d]) { + keyMap[d] = key; + writeFileSync(keyMapUrl, JSON.stringify(keyMap)); + } saveData!.commit(); }, length(): number { - const keys = readDirSync(base); - return keys ? keys.length : 0; + return Object.keys(keyMap).length; }, }; // @ts-expect-error internal constructor @@ -190,8 +186,7 @@ Object.defineProperty(globalThis, 'localStorage', { const proxy = new Proxy(storage, { has(_, p) { if (typeof p !== 'string') return false; - const s = statSync(keyToPath(p)); - return s !== null; + return !!Object.values(keyMap).find((k) => k === p); }, get(target, p) { if (typeof p !== 'string') return undefined; @@ -211,7 +206,7 @@ Object.defineProperty(globalThis, 'localStorage', { return true; }, ownKeys() { - return (readDirSync(base) || []).map((p) => pathToKey(p)); + return Object.values(keyMap); }, getOwnPropertyDescriptor(target, p) { if (typeof p !== 'string') return; diff --git a/source/crypto.c b/source/crypto.c index d6deb69d..b84cfe22 100644 --- a/source/crypto.c +++ b/source/crypto.c @@ -96,9 +96,32 @@ static JSValue nx_crypto_random_bytes(JSContext *ctx, JSValueConst this_val, return JS_UNDEFINED; } +static JSValue nx_crypto_sha256_hex(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + size_t size; + const char *str = JS_ToCStringLen(ctx, &size, argv[0]); + if (!str) { + return JS_EXCEPTION; + } + u8 *digest = js_mallocz(ctx, SHA256_HASH_SIZE); + if (!digest) { + return JS_EXCEPTION; + } + sha256CalculateHash(digest, str, size); + JS_FreeCString(ctx, str); + char hex[SHA256_HASH_SIZE * 2 + 1]; + for (int i = 0; i < SHA256_HASH_SIZE; i++) { + snprintf(hex + i * 2, 3, "%02x", digest[i]); + } + JSValue hex_val = JS_NewString(ctx, hex); + js_free(ctx, digest); + return hex_val; +} + static const JSCFunctionListEntry function_list[] = { JS_CFUNC_DEF("cryptoDigest", 0, nx_crypto_digest), JS_CFUNC_DEF("cryptoRandomBytes", 0, nx_crypto_random_bytes), + JS_CFUNC_DEF("sha256Hex", 0, nx_crypto_sha256_hex), }; void nx_init_crypto(JSContext *ctx, JSValueConst init_obj) {