Skip to content

Commit

Permalink
Support arbitrary key name length in localStorage (#145)
Browse files Browse the repository at this point in the history
  • Loading branch information
TooTallNate authored Jul 11, 2024
1 parent 2f7bc0d commit 12a725e
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/tender-hairs-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nxjs-runtime": patch
---

Support arbitrary key name length in `localStorage`
1 change: 1 addition & 0 deletions packages/runtime/src/$.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export interface Init {
// crypto.c
cryptoDigest(algorithm: string, buf: ArrayBuffer): Promise<ArrayBuffer>;
cryptoRandomBytes(buf: ArrayBuffer, offset: number, length: number): void;
sha256Hex(str: string): string;

// dommatrix.c
dommatrixNew(values?: number[]): DOMMatrix | DOMMatrixReadOnly;
Expand Down
65 changes: 30 additions & 35 deletions packages/runtime/src/storage.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -137,61 +133,60 @@ 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<string, string> = (() => {
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
const storage = new Storage(INTERNAL_SYMBOL, impl);
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;
Expand All @@ -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;
Expand Down
23 changes: 23 additions & 0 deletions source/crypto.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit 12a725e

Please sign in to comment.