esp-nvs-utils 0.1.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.
@@ -0,0 +1,276 @@
1
+ import { a as PAGE_STATE_FULL, i as PAGE_STATE_ACTIVE, n as ENCODING_TO_TYPE_CODE, r as PAGE_SIZE, t as decodeNVS } from "./decoder.js";
2
+ //#region src/crc.ts
3
+ /**
4
+ * Pure CRC-32 implementation compatible with zlib.crc32 (polynomial 0xEDB88320).
5
+ * Browser-safe: no Node.js dependencies.
6
+ */
7
+ const CRC32_TABLE = (() => {
8
+ const table = new Uint32Array(256);
9
+ for (let i = 0; i < 256; i++) {
10
+ let c = i;
11
+ for (let j = 0; j < 8; j++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
12
+ table[i] = c;
13
+ }
14
+ return table;
15
+ })();
16
+ /**
17
+ * Compute CRC-32 of data, matching Python's `zlib.crc32(data, seed)`.
18
+ * The default seed of 0xFFFFFFFF matches the NVS usage pattern.
19
+ */
20
+ function crc32(data, seed = 4294967295) {
21
+ let crc = seed >>> 0;
22
+ for (let i = 0; i < data.length; i++) crc = CRC32_TABLE[(crc ^ data[i]) & 255] ^ crc >>> 8;
23
+ return crc >>> 0;
24
+ }
25
+ //#endregion
26
+ //#region src/encoder.ts
27
+ var PageFullError = class extends Error {
28
+ constructor() {
29
+ super("Page is full");
30
+ }
31
+ };
32
+ /** A single 4096-byte NVS page being written. */
33
+ var PageWriter = class {
34
+ constructor(pageNum) {
35
+ this.entryNum = 0;
36
+ this.buf = new Uint8Array(PAGE_SIZE).fill(255);
37
+ this.view = new DataView(this.buf.buffer);
38
+ this.initHeader(pageNum);
39
+ this.initBitmap();
40
+ }
41
+ initHeader(pageNum) {
42
+ this.view.setUint32(0, PAGE_STATE_ACTIVE, true);
43
+ this.view.setUint32(4, pageNum, true);
44
+ this.buf[8] = 254;
45
+ const checksum = crc32(this.buf.subarray(4, 28));
46
+ this.view.setUint32(28, checksum, true);
47
+ }
48
+ initBitmap() {
49
+ this.buf.subarray(32, 64).fill(255);
50
+ }
51
+ markBitmapWritten(idx) {
52
+ const bitNum = idx * 2;
53
+ const byteIdx = 32 + (bitNum >> 3);
54
+ const bitOffset = bitNum & 7;
55
+ this.buf[byteIdx] &= ~(1 << bitOffset);
56
+ }
57
+ hasRoom(span) {
58
+ return this.entryNum + span <= 126;
59
+ }
60
+ /**
61
+ * Write `span` 32-byte chunks starting from entryNum.
62
+ * First chunk is the entry header; subsequent ones are data chunks.
63
+ */
64
+ writeRawEntry(header, dataChunks) {
65
+ const span = 1 + (dataChunks?.length ?? 0);
66
+ if (!this.hasRoom(span)) throw new PageFullError();
67
+ const off = 64 + this.entryNum * 32;
68
+ this.buf.set(header.subarray(0, 32), off);
69
+ this.markBitmapWritten(this.entryNum++);
70
+ if (dataChunks) for (const chunk of dataChunks) {
71
+ const chunkOff = 64 + this.entryNum * 32;
72
+ this.buf.set(chunk.subarray(0, Math.min(32, chunk.length)), chunkOff);
73
+ this.markBitmapWritten(this.entryNum++);
74
+ }
75
+ }
76
+ markFull() {
77
+ this.view.setUint32(0, PAGE_STATE_FULL, true);
78
+ }
79
+ getData() {
80
+ return this.buf;
81
+ }
82
+ };
83
+ function buildEntryHeader(nsIdx, typeCode, span, chunkIndex, key) {
84
+ const header = new Uint8Array(32).fill(255);
85
+ header[0] = nsIdx;
86
+ header[1] = typeCode;
87
+ header[2] = span;
88
+ header[3] = chunkIndex;
89
+ header.subarray(8, 24).fill(0);
90
+ const keyBytes = new TextEncoder().encode(key);
91
+ header.set(keyBytes.subarray(0, 15), 8);
92
+ return header;
93
+ }
94
+ function computeAndSetHeaderCrc(header) {
95
+ const crcData = new Uint8Array(28);
96
+ crcData.set(header.subarray(0, 4), 0);
97
+ crcData.set(header.subarray(8, 32), 4);
98
+ const checksum = crc32(crcData);
99
+ new DataView(header.buffer, header.byteOffset).setUint32(4, checksum, true);
100
+ }
101
+ /** Split data into 32-byte-aligned chunks for NVS storage. */
102
+ function toDataChunks(data) {
103
+ const chunks = [];
104
+ const roundedSize = data.length + 31 & -32;
105
+ const padded = new Uint8Array(roundedSize).fill(255);
106
+ padded.set(data);
107
+ for (let i = 0; i < roundedSize; i += 32) chunks.push(padded.subarray(i, i + 32));
108
+ return chunks;
109
+ }
110
+ /** NVS partition writer — manages pages and entry placement. */
111
+ var NVSWriter = class {
112
+ constructor() {
113
+ this.pages = [];
114
+ this.pageNum = 0;
115
+ this.curPage = this.newPage();
116
+ }
117
+ newPage() {
118
+ const p = new PageWriter(this.pageNum++);
119
+ this.pages.push(p);
120
+ return p;
121
+ }
122
+ ensureRoom(span) {
123
+ if (!this.curPage.hasRoom(span)) {
124
+ this.curPage.markFull();
125
+ this.curPage = this.newPage();
126
+ }
127
+ }
128
+ writeEntry(header, dataChunks) {
129
+ const span = 1 + (dataChunks?.length ?? 0);
130
+ this.ensureRoom(span);
131
+ this.curPage.writeRawEntry(header, dataChunks);
132
+ }
133
+ /** Write a namespace entry at ns_idx=0. Returns the namespace index assigned. */
134
+ writeNamespace(key, nsIdx) {
135
+ const header = buildEntryHeader(0, 1, 1, 255, key);
136
+ new DataView(header.buffer, header.byteOffset).setUint8(24, nsIdx);
137
+ computeAndSetHeaderCrc(header);
138
+ this.writeEntry(header);
139
+ }
140
+ /** Write a primitive-encoded entry. */
141
+ writePrimitive(nsIdx, key, encoding, value) {
142
+ const typeCode = ENCODING_TO_TYPE_CODE[encoding];
143
+ if (typeCode === void 0) throw new Error(`Unknown encoding: ${encoding}`);
144
+ const header = buildEntryHeader(nsIdx, typeCode, 1, 255, key);
145
+ const view = new DataView(header.buffer, header.byteOffset);
146
+ switch (encoding) {
147
+ case "u8":
148
+ view.setUint8(24, Number(value));
149
+ break;
150
+ case "i8":
151
+ view.setInt8(24, Number(value));
152
+ break;
153
+ case "u16":
154
+ view.setUint16(24, Number(value), true);
155
+ break;
156
+ case "i16":
157
+ view.setInt16(24, Number(value), true);
158
+ break;
159
+ case "u32":
160
+ view.setUint32(24, Number(value), true);
161
+ break;
162
+ case "i32":
163
+ view.setInt32(24, Number(value), true);
164
+ break;
165
+ case "u64":
166
+ view.setBigUint64(24, BigInt(value), true);
167
+ break;
168
+ case "i64":
169
+ view.setBigInt64(24, BigInt(value), true);
170
+ break;
171
+ }
172
+ computeAndSetHeaderCrc(header);
173
+ this.writeEntry(header);
174
+ }
175
+ /** Write a string entry (NUL-terminated). */
176
+ writeString(nsIdx, key, value) {
177
+ const encoded = new TextEncoder().encode(value);
178
+ const data = new Uint8Array(encoded.length + 1);
179
+ data.set(encoded);
180
+ data[encoded.length] = 0;
181
+ const chunks = toDataChunks(data);
182
+ const header = buildEntryHeader(nsIdx, 33, 1 + chunks.length, 255, key);
183
+ const hView = new DataView(header.buffer, header.byteOffset);
184
+ hView.setUint16(24, data.length, true);
185
+ const payloadCrc = crc32(data);
186
+ hView.setUint32(28, payloadCrc, true);
187
+ computeAndSetHeaderCrc(header);
188
+ this.writeEntry(header, chunks);
189
+ }
190
+ /** Write a blob (binary data) as V2 blob_data chunks + blob_index. */
191
+ writeBlobV2(nsIdx, key, data) {
192
+ let chunkStart = 0;
193
+ let chunkCount = 0;
194
+ let offset = 0;
195
+ let remainingSize = data.length;
196
+ while (remainingSize > 0 || chunkCount === 0) {
197
+ const tailroomBytes = (126 - this.curPage.entryNum - 1) * 32;
198
+ let chunkSize;
199
+ if (tailroomBytes < remainingSize) chunkSize = tailroomBytes;
200
+ else chunkSize = remainingSize;
201
+ if (chunkSize < 0) chunkSize = 0;
202
+ const chunkData = data.subarray(offset, offset + chunkSize);
203
+ const chunks = toDataChunks(chunkData);
204
+ const header = buildEntryHeader(nsIdx, 66, 1 + chunks.length, chunkStart + chunkCount, key);
205
+ const hView = new DataView(header.buffer, header.byteOffset);
206
+ hView.setUint16(24, chunkSize, true);
207
+ const payloadCrc = crc32(chunkData.length > 0 ? chunkData : new Uint8Array(0));
208
+ hView.setUint32(28, payloadCrc, true);
209
+ computeAndSetHeaderCrc(header);
210
+ this.writeEntry(header, chunks);
211
+ chunkCount++;
212
+ offset += chunkSize;
213
+ remainingSize -= chunkSize;
214
+ if (remainingSize > 0) {
215
+ this.curPage.markFull();
216
+ this.curPage = this.newPage();
217
+ }
218
+ if (chunkSize === 0 && remainingSize === 0) break;
219
+ }
220
+ const idxHeader = buildEntryHeader(nsIdx, 72, 1, 255, key);
221
+ new DataView(idxHeader.buffer, idxHeader.byteOffset).setUint32(24, data.length, true);
222
+ idxHeader[28] = chunkCount;
223
+ idxHeader[29] = chunkStart;
224
+ computeAndSetHeaderCrc(idxHeader);
225
+ this.writeEntry(idxHeader);
226
+ }
227
+ getData() {
228
+ const totalSize = this.pages.length * PAGE_SIZE;
229
+ const result = new Uint8Array(totalSize);
230
+ for (let i = 0; i < this.pages.length; i++) result.set(this.pages[i].getData(), i * PAGE_SIZE);
231
+ return result;
232
+ }
233
+ };
234
+ /**
235
+ * Encode an NVSNamespaces object into a binary NVS partition.
236
+ * Output is always V2 (multipage blob support enabled).
237
+ * Partition size is auto-calculated based on data volume.
238
+ */
239
+ function encodeNVS(namespaces) {
240
+ const writer = new NVSWriter();
241
+ const nsMap = /* @__PURE__ */ new Map();
242
+ let nextNsIdx = 1;
243
+ for (const [nsName, values] of Object.entries(namespaces)) {
244
+ let nsIdx = nsMap.get(nsName);
245
+ if (nsIdx === void 0) {
246
+ nsIdx = nextNsIdx++;
247
+ nsMap.set(nsName, nsIdx);
248
+ writer.writeNamespace(nsName, nsIdx);
249
+ }
250
+ for (const entry of values) {
251
+ const { name: key, encoding, value } = entry;
252
+ switch (encoding) {
253
+ case "u8":
254
+ case "i8":
255
+ case "u16":
256
+ case "i16":
257
+ case "u32":
258
+ case "i32":
259
+ case "u64":
260
+ case "i64":
261
+ writer.writePrimitive(nsIdx, key, encoding, value);
262
+ break;
263
+ case "string":
264
+ writer.writeString(nsIdx, key, value);
265
+ break;
266
+ case "blob_data":
267
+ writer.writeBlobV2(nsIdx, key, value);
268
+ break;
269
+ default: throw new Error(`Unsupported encoding: ${encoding}`);
270
+ }
271
+ }
272
+ }
273
+ return writer.getData();
274
+ }
275
+ //#endregion
276
+ export { decodeNVS, encodeNVS };
@@ -0,0 +1 @@
1
+ export declare function main(args: string[]): void;
@@ -0,0 +1,49 @@
1
+ /** NVS page size in bytes. */
2
+ export declare const PAGE_SIZE = 4096;
3
+ /** Size of a single NVS entry in bytes. */
4
+ export declare const ENTRY_SIZE = 32;
5
+ /** Offset of the entry state bitmap within a page. */
6
+ export declare const BITMAP_OFFSET = 32;
7
+ /** Size of the entry state bitmap in bytes. */
8
+ export declare const BITMAP_SIZE = 32;
9
+ /** Offset of the first entry within a page. */
10
+ export declare const FIRST_ENTRY_OFFSET = 64;
11
+ /** Maximum number of data entries per page (126; last 2 bitmap slots are unused). */
12
+ export declare const MAX_ENTRIES = 126;
13
+ /** Chunk index value meaning "any chunk" (used for strings, V1 blobs, and blob_index). */
14
+ export declare const CHUNK_ANY = 255;
15
+ /** Page state: all bytes 0xFF, page not yet written. */
16
+ export declare const PAGE_STATE_EMPTY = 4294967295;
17
+ /** Page state: page is currently being written. */
18
+ export declare const PAGE_STATE_ACTIVE = 4294967294;
19
+ /** Page state: page is full, no more entries can be written. */
20
+ export declare const PAGE_STATE_FULL = 4294967292;
21
+ /** Page state: page is being erased. */
22
+ export declare const PAGE_STATE_ERASING = 4294967288;
23
+ /** Page state: page is corrupted. */
24
+ export declare const PAGE_STATE_CORRUPTED = 0;
25
+ /** NVS version byte for V1 (multipage blob disabled). */
26
+ export declare const VERSION1 = 255;
27
+ /** NVS version byte for V2 (multipage blob enabled). */
28
+ export declare const VERSION2 = 254;
29
+ /** Entry bitmap bit-pair values. */
30
+ export declare const ENTRY_STATE_EMPTY = 3;
31
+ export declare const ENTRY_STATE_WRITTEN = 2;
32
+ export declare const ENTRY_STATE_ERASED = 0;
33
+ /** Item type codes (byte 1 of entry header). */
34
+ export declare const TYPE_U8 = 1;
35
+ export declare const TYPE_I8 = 17;
36
+ export declare const TYPE_U16 = 2;
37
+ export declare const TYPE_I16 = 18;
38
+ export declare const TYPE_U32 = 4;
39
+ export declare const TYPE_I32 = 20;
40
+ export declare const TYPE_U64 = 8;
41
+ export declare const TYPE_I64 = 24;
42
+ export declare const TYPE_STRING = 33;
43
+ export declare const TYPE_BLOB = 65;
44
+ export declare const TYPE_BLOB_DATA = 66;
45
+ export declare const TYPE_BLOB_INDEX = 72;
46
+ /** Map from type code to NVS encoding string. */
47
+ export declare const TYPE_CODE_TO_ENCODING: Readonly<Record<number, string>>;
48
+ /** Map from NVS encoding string to type code. */
49
+ export declare const ENCODING_TO_TYPE_CODE: Readonly<Record<string, number>>;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Pure CRC-32 implementation compatible with zlib.crc32 (polynomial 0xEDB88320).
3
+ * Browser-safe: no Node.js dependencies.
4
+ */
5
+ /**
6
+ * Compute CRC-32 of data, matching Python's `zlib.crc32(data, seed)`.
7
+ * The default seed of 0xFFFFFFFF matches the NVS usage pattern.
8
+ */
9
+ export declare function crc32(data: Uint8Array, seed?: number): number;
@@ -0,0 +1,8 @@
1
+ import type { NVSNamespaces } from "./types.js";
2
+ /**
3
+ * Decode an NVS partition binary into structured namespaces.
4
+ *
5
+ * Handles V1 (single-page blobs) and V2 (multi-page blobs).
6
+ * Does not support encrypted partitions.
7
+ */
8
+ export declare function decodeNVS(data: ArrayBuffer | Uint8Array): NVSNamespaces;
@@ -0,0 +1,7 @@
1
+ import type { NVSNamespaces } from "./types.js";
2
+ /**
3
+ * Encode an NVSNamespaces object into a binary NVS partition.
4
+ * Output is always V2 (multipage blob support enabled).
5
+ * Partition size is auto-calculated based on data volume.
6
+ */
7
+ export declare function encodeNVS(namespaces: NVSNamespaces): Uint8Array;
@@ -0,0 +1,3 @@
1
+ export { decodeNVS } from "./decoder.js";
2
+ export { encodeNVS } from "./encoder.js";
3
+ export type { NVSEncoding, NVSNamespaces, NVSValue } from "./types.js";
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Encoding/type of an NVS entry value.
3
+ */
4
+ export type NVSEncoding = "u8" | "i8" | "u16" | "i16" | "u32" | "i32" | "u64" | "i64" | "string" | "blob_data";
5
+ /**
6
+ * A single key-value entry within a namespace.
7
+ *
8
+ * value type by encoding:
9
+ * u8/i8/u16/i16/u32/i32 → number
10
+ * u64/i64 → bigint (full 64-bit range)
11
+ * string → string
12
+ * blob_data → Uint8Array
13
+ */
14
+ export interface NVSValue {
15
+ name: string;
16
+ encoding: NVSEncoding;
17
+ value: number | bigint | string | Uint8Array;
18
+ }
19
+ /**
20
+ * A decoded NVS partition.
21
+ * Keys are namespace names, values are the entries within that namespace.
22
+ */
23
+ export type NVSNamespaces = Record<string, NVSValue[]>;
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "esp-nvs-utils",
3
+ "version": "0.1.0",
4
+ "description": "ESP-IDF NVS partition encoder/decoder for Node.js and browsers",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/cjs/index.cjs",
8
+ "module": "./dist/esm/index.js",
9
+ "bin": {
10
+ "esp-nvs-decode": "bin/esp-nvs-decode.cjs"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/esm/index.js",
15
+ "require": "./dist/cjs/index.cjs",
16
+ "types": "./dist/types/index.d.ts"
17
+ }
18
+ },
19
+ "types": "./dist/types/index.d.ts",
20
+ "files": [
21
+ "bin",
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "npm run build:types && rolldown -c rolldown.config.ts",
26
+ "build:types": "tsc -p tsconfig.build.json",
27
+ "decode": "node ./bin/esp-nvs-decode.cjs",
28
+ "test": "node --experimental-vm-modules node_modules/.bin/jest --config jest.config.cjs",
29
+ "lint": "oxlint --deny-warnings src test",
30
+ "format": "oxfmt src test",
31
+ "format:check": "oxfmt --check src test",
32
+ "typecheck": "tsc --noEmit"
33
+ },
34
+ "devDependencies": {
35
+ "@types/jest": "^29.5.14",
36
+ "@types/node": "^22.15.18",
37
+ "jest": "^29.7.0",
38
+ "oxfmt": "^0.48.0",
39
+ "oxlint": "^0.16.7",
40
+ "rolldown": "^1.0.0-beta.9",
41
+ "ts-jest": "^29.3.2",
42
+ "typescript": "^5.8.3"
43
+ }
44
+ }