esp32tool 1.1.4 → 1.1.5

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/js/script.js CHANGED
@@ -2219,8 +2219,48 @@ function refreshLittleFS() {
2219
2219
  littlefsBreadcrumb.textContent = currentLittleFSPath || '/';
2220
2220
  butLittlefsUp.disabled = currentLittleFSPath === '/' || !currentLittleFSPath;
2221
2221
 
2222
- // List files
2223
- const entries = currentLittleFS.list(currentLittleFSPath);
2222
+ // List files - the list() function behavior differs between filesystems
2223
+ let entries;
2224
+
2225
+ if (currentFilesystemType === 'fatfs') {
2226
+ // FatFS returns ALL files recursively from root, so we always list from root and filter
2227
+ const allEntries = currentLittleFS.list('/');
2228
+ const isRoot = currentLittleFSPath === '/';
2229
+
2230
+ // Filter to show only direct children
2231
+ entries = allEntries.filter(entry => {
2232
+ // Remove /fatfs prefix from entry path for comparison
2233
+ let entryPath = entry.path;
2234
+ if (entryPath.startsWith('/fatfs/')) {
2235
+ entryPath = entryPath.slice(6);
2236
+ } else if (entryPath === '/fatfs') {
2237
+ entryPath = '/';
2238
+ }
2239
+
2240
+ if (isRoot) {
2241
+ // In root: only show top-level entries
2242
+ const withoutLeadingSlash = entryPath.slice(1);
2243
+ return withoutLeadingSlash && !withoutLeadingSlash.includes('/');
2244
+ } else {
2245
+ // In subdirectory: entry must be direct child of current path
2246
+ const expectedPrefix = currentLittleFSPath + '/';
2247
+ if (!entryPath.startsWith(expectedPrefix)) {
2248
+ return false;
2249
+ }
2250
+ const relativePath = entryPath.slice(expectedPrefix.length);
2251
+ return relativePath && !relativePath.includes('/');
2252
+ }
2253
+ });
2254
+
2255
+ // Add name attribute for FatFS entries
2256
+ entries = entries.map(entry => ({
2257
+ ...entry,
2258
+ name: entry.path.split('/').pop() || entry.path
2259
+ }));
2260
+ } else {
2261
+ // LittleFS and SPIFFS return only direct children
2262
+ entries = currentLittleFS.list(currentLittleFSPath);
2263
+ }
2224
2264
 
2225
2265
  // Clear table
2226
2266
  littlefsFileList.innerHTML = '';
@@ -2252,7 +2292,8 @@ function refreshLittleFS() {
2252
2292
  icon.className = 'file-icon';
2253
2293
  icon.textContent = entry.type === 'dir' ? '📁' : '📄';
2254
2294
 
2255
- const name = entry.path.split('/').filter(Boolean).pop() || '/';
2295
+ // Use entry.name instead of parsing the path
2296
+ const name = entry.name || entry.path.split('/').filter(Boolean).pop() || '/';
2256
2297
  const nameText = document.createElement('span');
2257
2298
  nameText.textContent = name;
2258
2299
 
@@ -2313,7 +2354,20 @@ function refreshLittleFS() {
2313
2354
  * Navigate to a directory in LittleFS
2314
2355
  */
2315
2356
  function navigateLittleFS(path) {
2316
- currentLittleFSPath = path;
2357
+ // Remove /fatfs prefix if present (for FatFS compatibility)
2358
+ let normalizedPath = path;
2359
+ if (normalizedPath.startsWith('/fatfs/')) {
2360
+ normalizedPath = normalizedPath.slice(6); // Remove '/fatfs' keeping the /
2361
+ } else if (normalizedPath === '/fatfs') {
2362
+ normalizedPath = '/';
2363
+ }
2364
+
2365
+ // Remove trailing slash except for root
2366
+ if (normalizedPath !== '/' && normalizedPath.endsWith('/')) {
2367
+ normalizedPath = normalizedPath.slice(0, -1);
2368
+ }
2369
+
2370
+ currentLittleFSPath = normalizedPath;
2317
2371
  refreshLittleFS();
2318
2372
  }
2319
2373
 
@@ -2323,12 +2377,12 @@ function navigateLittleFS(path) {
2323
2377
  function clickLittlefsUp() {
2324
2378
  if (currentLittleFSPath === '/' || !currentLittleFSPath) return;
2325
2379
 
2380
+ // Split path and remove last segment
2326
2381
  const parts = currentLittleFSPath.split('/').filter(Boolean);
2327
2382
  parts.pop();
2328
- currentLittleFSPath = '/' + parts.join('/');
2329
- if (currentLittleFSPath !== '/' && !currentLittleFSPath.endsWith('/')) {
2330
- currentLittleFSPath += '/';
2331
- }
2383
+
2384
+ // Reconstruct path
2385
+ currentLittleFSPath = parts.length ? '/' + parts.join('/') : '/';
2332
2386
  refreshLittleFS();
2333
2387
  }
2334
2388
 
@@ -2433,7 +2487,8 @@ async function clickLittlefsWrite() {
2433
2487
  butLittlefsWrite.disabled = false;
2434
2488
  butLittlefsClose.disabled = false;
2435
2489
  butLittlefsUpload.disabled = !littlefsFileInput.files.length;
2436
- butLittlefsMkdir.disabled = false;
2490
+ // Re-enable mkdir only if not SPIFFS
2491
+ butLittlefsMkdir.disabled = (currentFilesystemType === 'spiffs');
2437
2492
  }
2438
2493
  }
2439
2494
 
@@ -2522,10 +2577,22 @@ async function clickLittlefsUpload() {
2522
2577
  /**
2523
2578
  * Create new directory
2524
2579
  */
2525
- function clickLittlefsMkdir() {
2580
+ async function clickLittlefsMkdir() {
2526
2581
  if (!currentLittleFS) return;
2527
2582
 
2528
- const dirName = prompt('Enter directory name:');
2583
+ // Check if mkdir is supported (SPIFFS doesn't support directories)
2584
+ if (currentFilesystemType === 'spiffs') {
2585
+ errorMsg('SPIFFS does not support directories. Files are stored in a flat structure.');
2586
+ return;
2587
+ }
2588
+
2589
+ let dirName;
2590
+ if (isElectron) {
2591
+ dirName = await window.electronAPI.showPrompt('Enter directory name:');
2592
+ } else {
2593
+ dirName = prompt('Enter directory name:');
2594
+ }
2595
+
2529
2596
  if (!dirName || !dirName.trim()) return;
2530
2597
 
2531
2598
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esp32tool",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "Flash & Read ESP devices using WebSerial",
5
5
  "main": "electron/main.js",
6
6
  "repository": {
Binary file
@@ -1,26 +1,30 @@
1
- import type { FileSource, BinarySource } from "../shared/types";
1
+ import type { BinarySource, FileSource, FileSystemUsage } from "../shared/types";
2
+ export declare const FAT_MOUNT = "/fatfs";
2
3
  export interface FatFSEntry {
3
4
  path: string;
4
5
  size: number;
5
- type: 'file' | 'dir';
6
+ type: "file" | "dir";
6
7
  }
7
8
  export interface FatFSOptions {
8
9
  blockSize?: number;
9
10
  blockCount?: number;
10
- wasmURL?: string | URL;
11
11
  formatOnInit?: boolean;
12
+ wasmURL?: string | URL;
12
13
  }
13
14
  export interface FatFS {
14
- format(): void;
15
- list(): FatFSEntry[];
15
+ list(path?: string): FatFSEntry[];
16
16
  readFile(path: string): Uint8Array;
17
+ toImage(): Uint8Array;
18
+ getUsage(): FileSystemUsage;
19
+ format(): void;
17
20
  writeFile(path: string, data: FileSource): void;
18
21
  deleteFile(path: string): void;
19
- toImage(): Uint8Array;
22
+ mkdir(path: string): void;
23
+ rename(oldPath: string, newPath: string): void;
20
24
  }
21
25
  export declare class FatFSError extends Error {
22
26
  readonly code: number;
23
27
  constructor(message: string, code: number);
24
28
  }
25
- export declare function createFatFS(options?: FatFSOptions): Promise<FatFS>;
26
29
  export declare function createFatFSFromImage(image: BinarySource, options?: FatFSOptions): Promise<FatFS>;
30
+ export declare function createFatFS(options?: FatFSOptions): Promise<FatFS>;
@@ -1,7 +1,9 @@
1
- const DEFAULT_BLOCK_SIZE = 512;
2
- const DEFAULT_BLOCK_COUNT = 1024;
1
+ export const FAT_MOUNT = "/fatfs";
3
2
  const INITIAL_LIST_BUFFER = 4096;
4
- const FATFS_ERR_NOT_ENOUGH_CORE = -17;
3
+ const DEFAULT_BLOCK_SIZE = 4096;
4
+ const DEFAULT_BLOCK_COUNT = 128;
5
+ const FATFS_ERR_INVAL = -1;
6
+ const FATFS_ERR_NOSPC = -3;
5
7
  export class FatFSError extends Error {
6
8
  constructor(message, code) {
7
9
  super(message);
@@ -9,61 +11,60 @@ export class FatFSError extends Error {
9
11
  this.name = "FatFSError";
10
12
  }
11
13
  }
12
- export async function createFatFS(options = {}) {
13
- console.info("[fatfs-wasm] createFatFS() starting", options);
14
- const wasmURL = options.wasmURL ?? new URL("./fatfs.wasm", import.meta.url);
15
- const exports = await instantiateFatFSModule(wasmURL);
16
- const blockSize = options.blockSize ?? DEFAULT_BLOCK_SIZE;
17
- const blockCount = options.blockCount ?? DEFAULT_BLOCK_COUNT;
18
- const initResult = exports.fatfsjs_init(blockSize, blockCount);
19
- console.info("[fatfs-wasm] fatfsjs_init returned", initResult);
20
- if (initResult < 0) {
21
- throw new FatFSError("Failed to initialize FatFS", initResult);
22
- }
23
- if (options.formatOnInit) {
24
- const formatResult = exports.fatfsjs_format();
25
- console.info("[fatfs-wasm] fatfsjs_format returned", formatResult);
26
- if (formatResult < 0) {
27
- throw new FatFSError("Failed to format FatFS volume", formatResult);
28
- }
29
- }
30
- const client = new FatFSClient(exports);
31
- console.info("[fatfs-wasm] Filesystem initialized");
32
- return client;
33
- }
34
14
  export async function createFatFSFromImage(image, options = {}) {
35
15
  console.info("[fatfs-wasm] createFatFSFromImage() starting");
36
16
  const wasmURL = options.wasmURL ?? new URL("./fatfs.wasm", import.meta.url);
37
17
  const exports = await instantiateFatFSModule(wasmURL);
38
18
  const bytes = asBinaryUint8Array(image);
39
19
  const blockSize = options.blockSize ?? DEFAULT_BLOCK_SIZE;
40
- if (blockSize === 0) {
41
- throw new Error("blockSize must be a positive integer");
20
+ if (blockSize !== DEFAULT_BLOCK_SIZE) {
21
+ throw new Error(`blockSize must be ${DEFAULT_BLOCK_SIZE}`);
42
22
  }
43
- const inferredBlocks = bytes.length / blockSize;
44
- const blockCount = options.blockCount ?? inferredBlocks;
45
- if (blockCount * blockSize !== bytes.length) {
23
+ if (options.blockCount && options.blockCount * blockSize !== bytes.length) {
46
24
  throw new Error("Image size must equal blockSize * blockCount");
47
25
  }
48
- const ptr = exports.malloc(bytes.length || 1);
49
- if (!ptr) {
50
- throw new FatFSError("Failed to allocate WebAssembly memory", FATFS_ERR_NOT_ENOUGH_CORE);
26
+ const heap = new Uint8Array(exports.memory.buffer);
27
+ const imagePtr = exports.malloc(bytes.length || 1);
28
+ if (!imagePtr) {
29
+ throw new FatFSError("Failed to allocate WebAssembly memory", FATFS_ERR_NOSPC);
51
30
  }
52
31
  try {
53
- // Get heap AFTER malloc to avoid stale reference if memory grows
54
- const heap = new Uint8Array(exports.memory.buffer);
55
- heap.set(bytes, ptr);
56
- const initResult = exports.fatfsjs_init_from_image(blockSize, blockCount, ptr, bytes.length);
32
+ heap.set(bytes, imagePtr);
33
+ const initResult = exports.fatfsjs_init_from_image(imagePtr, bytes.length);
57
34
  if (initResult < 0) {
58
- throw new FatFSError("Failed to initialize FatFS from image", initResult);
35
+ throw new FatFSError("Failed to initialize FAT16 image", initResult);
59
36
  }
60
37
  }
61
38
  finally {
62
- exports.free(ptr);
39
+ exports.free(imagePtr);
63
40
  }
64
- const client = new FatFSClient(exports);
65
41
  console.info("[fatfs-wasm] Filesystem initialized from image");
66
- return client;
42
+ return new FatFSClient(exports);
43
+ }
44
+ export async function createFatFS(options = {}) {
45
+ console.info("[fatfs-wasm] createFatFS() starting", options);
46
+ const wasmURL = options.wasmURL ?? new URL("./fatfs.wasm", import.meta.url);
47
+ const exports = await instantiateFatFSModule(wasmURL);
48
+ const blockSize = options.blockSize ?? DEFAULT_BLOCK_SIZE;
49
+ const blockCount = options.blockCount ?? DEFAULT_BLOCK_COUNT;
50
+ if (blockSize !== DEFAULT_BLOCK_SIZE) {
51
+ throw new Error(`blockSize must be ${DEFAULT_BLOCK_SIZE}`);
52
+ }
53
+ if (!Number.isFinite(blockCount) || blockCount <= 0) {
54
+ throw new Error("blockCount must be a positive integer");
55
+ }
56
+ const initResult = exports.fatfsjs_init(blockSize, blockCount);
57
+ if (initResult < 0) {
58
+ throw new FatFSError("Failed to initialize FatFS", initResult);
59
+ }
60
+ if (options.formatOnInit) {
61
+ const formatResult = exports.fatfsjs_format();
62
+ if (formatResult < 0) {
63
+ throw new FatFSError("Failed to format filesystem", formatResult);
64
+ }
65
+ }
66
+ console.info("[fatfs-wasm] Filesystem initialized");
67
+ return new FatFSClient(exports);
67
68
  }
68
69
  class FatFSClient {
69
70
  constructor(exports) {
@@ -73,46 +74,56 @@ class FatFSClient {
73
74
  this.exports = exports;
74
75
  this.heapU8 = new Uint8Array(this.exports.memory.buffer);
75
76
  }
76
- format() {
77
- const result = this.exports.fatfsjs_format();
78
- this.assertOk(result, "format filesystem");
79
- }
80
- list() {
77
+ list(path = FAT_MOUNT) {
78
+ const normalized = normalizeMountPath(path);
79
+ const basePath = normalized;
80
+ const pathPtr = this.allocString(normalized);
81
81
  let capacity = this.listBufferSize;
82
- while (true) {
83
- const ptr = this.alloc(capacity);
84
- try {
85
- const used = this.exports.fatfsjs_list(ptr, capacity);
86
- if (used === FATFS_ERR_NOT_ENOUGH_CORE) {
87
- this.listBufferSize = capacity * 2;
88
- capacity = this.listBufferSize;
89
- continue;
82
+ try {
83
+ while (true) {
84
+ const ptr = this.alloc(capacity);
85
+ try {
86
+ const used = this.exports.fatfsjs_list(pathPtr, ptr, capacity);
87
+ if (used === FATFS_ERR_NOSPC) {
88
+ this.listBufferSize = capacity * 2;
89
+ capacity = this.listBufferSize;
90
+ continue;
91
+ }
92
+ this.assertOk(used, "list files");
93
+ if (used === 0) {
94
+ return [];
95
+ }
96
+ const payload = this.decoder.decode(this.heapU8.subarray(ptr, ptr + used));
97
+ return parseListPayload(payload).map((entry) => ({
98
+ ...entry,
99
+ path: joinListPath(basePath, entry.path),
100
+ }));
90
101
  }
91
- this.assertOk(used, "list files");
92
- if (used === 0) {
93
- return [];
102
+ finally {
103
+ this.exports.free(ptr);
94
104
  }
95
- const payload = this.decoder.decode(this.heapU8.subarray(ptr, ptr + used));
96
- return parseListPayload(payload);
97
- }
98
- finally {
99
- this.exports.free(ptr);
100
105
  }
101
106
  }
107
+ finally {
108
+ this.exports.free(pathPtr);
109
+ }
102
110
  }
103
111
  readFile(path) {
104
- const normalizedPath = normalizePath(path);
105
- const pathPtr = this.allocString(normalizedPath);
112
+ const normalized = normalizeMountPath(path);
113
+ if (normalized === FAT_MOUNT) {
114
+ throw new FatFSError("Path must point to a file", FATFS_ERR_INVAL);
115
+ }
116
+ const pathPtr = this.allocString(normalized);
106
117
  try {
107
118
  const size = this.exports.fatfsjs_file_size(pathPtr);
108
- this.assertOk(size, `stat file "${normalizedPath}"`);
119
+ this.assertOk(size, `stat file "${normalized}"`);
109
120
  if (size === 0) {
110
121
  return new Uint8Array();
111
122
  }
112
123
  const dataPtr = this.alloc(size);
113
124
  try {
114
125
  const read = this.exports.fatfsjs_read_file(pathPtr, dataPtr, size);
115
- this.assertOk(read, `read file "${normalizedPath}"`);
126
+ this.assertOk(read, `read file "${normalized}"`);
116
127
  return this.heapU8.slice(dataPtr, dataPtr + size);
117
128
  }
118
129
  finally {
@@ -123,17 +134,50 @@ class FatFSClient {
123
134
  this.exports.free(pathPtr);
124
135
  }
125
136
  }
137
+ toImage() {
138
+ const size = this.exports.fatfsjs_storage_size();
139
+ if (size === 0) {
140
+ return new Uint8Array();
141
+ }
142
+ const ptr = this.alloc(size);
143
+ try {
144
+ const copied = this.exports.fatfsjs_export_image(ptr, size);
145
+ this.assertOk(copied, "export filesystem image");
146
+ return this.heapU8.slice(ptr, ptr + size);
147
+ }
148
+ finally {
149
+ this.exports.free(ptr);
150
+ }
151
+ }
152
+ getUsage() {
153
+ const capacityBytes = this.exports.fatfsjs_storage_size();
154
+ const entries = this.list(FAT_MOUNT);
155
+ const usedBytes = entries.reduce((acc, entry) => (entry.type === "file" ? acc + entry.size : acc), 0);
156
+ const freeBytes = capacityBytes > usedBytes ? capacityBytes - usedBytes : 0;
157
+ return {
158
+ capacityBytes,
159
+ usedBytes,
160
+ freeBytes,
161
+ };
162
+ }
163
+ format() {
164
+ const result = this.exports.fatfsjs_format();
165
+ this.assertOk(result, "format filesystem");
166
+ }
126
167
  writeFile(path, data) {
127
- const normalizedPath = normalizePath(path);
168
+ const normalized = normalizeMountPath(path);
169
+ if (normalized === FAT_MOUNT) {
170
+ throw new FatFSError("Path must point to a file", FATFS_ERR_INVAL);
171
+ }
128
172
  const payload = asUint8Array(data, this.encoder);
129
- const pathPtr = this.allocString(normalizedPath);
130
- const dataPtr = this.alloc(payload.length);
173
+ const pathPtr = this.allocString(normalized);
174
+ const dataPtr = payload.length ? this.alloc(payload.length) : 0;
131
175
  try {
132
- if (payload.length > 0 && dataPtr) {
176
+ if (payload.length > 0) {
133
177
  this.heapU8.set(payload, dataPtr);
134
178
  }
135
- const result = this.exports.fatfsjs_write_file(pathPtr, payload.length > 0 ? dataPtr : 0, payload.length);
136
- this.assertOk(result, `write file "${normalizedPath}"`);
179
+ const result = this.exports.fatfsjs_write_file(pathPtr, dataPtr, payload.length);
180
+ this.assertOk(result, `write file "${normalized}"`);
137
181
  }
138
182
  finally {
139
183
  if (dataPtr) {
@@ -143,29 +187,45 @@ class FatFSClient {
143
187
  }
144
188
  }
145
189
  deleteFile(path) {
146
- const normalizedPath = normalizePath(path);
147
- const pathPtr = this.allocString(normalizedPath);
190
+ const normalized = normalizeMountPath(path);
191
+ if (normalized === FAT_MOUNT) {
192
+ throw new FatFSError("Path must point to a file", FATFS_ERR_INVAL);
193
+ }
194
+ const pathPtr = this.allocString(normalized);
148
195
  try {
149
196
  const result = this.exports.fatfsjs_delete_file(pathPtr);
150
- this.assertOk(result, `delete file "${normalizedPath}"`);
197
+ this.assertOk(result, `delete file "${normalized}"`);
151
198
  }
152
199
  finally {
153
200
  this.exports.free(pathPtr);
154
201
  }
155
202
  }
156
- toImage() {
157
- const size = this.exports.fatfsjs_storage_size();
158
- if (size === 0) {
159
- return new Uint8Array();
203
+ mkdir(path) {
204
+ const normalized = normalizeMountPath(path);
205
+ if (normalized === FAT_MOUNT) {
206
+ return;
160
207
  }
161
- const ptr = this.alloc(size);
208
+ const pathPtr = this.allocString(normalized);
162
209
  try {
163
- const copied = this.exports.fatfsjs_export_image(ptr, size);
164
- this.assertOk(copied, "export filesystem image");
165
- return this.heapU8.slice(ptr, ptr + size);
210
+ const result = this.exports.fatfsjs_mkdir(pathPtr);
211
+ this.assertOk(result, `mkdir "${normalized}"`);
166
212
  }
167
213
  finally {
168
- this.exports.free(ptr);
214
+ this.exports.free(pathPtr);
215
+ }
216
+ }
217
+ rename(oldPath, newPath) {
218
+ const from = normalizeMountPath(oldPath);
219
+ const to = normalizeMountPath(newPath);
220
+ const fromPtr = this.allocString(from);
221
+ const toPtr = this.allocString(to);
222
+ try {
223
+ const result = this.exports.fatfsjs_rename(fromPtr, toPtr);
224
+ this.assertOk(result, `rename "${from}" -> "${to}"`);
225
+ }
226
+ finally {
227
+ this.exports.free(fromPtr);
228
+ this.exports.free(toPtr);
169
229
  }
170
230
  }
171
231
  refreshHeap() {
@@ -179,7 +239,7 @@ class FatFSClient {
179
239
  }
180
240
  const ptr = this.exports.malloc(size);
181
241
  if (!ptr) {
182
- throw new FatFSError("Failed to allocate WebAssembly memory", FATFS_ERR_NOT_ENOUGH_CORE);
242
+ throw new FatFSError("Failed to allocate WebAssembly memory", FATFS_ERR_NOSPC);
183
243
  }
184
244
  this.refreshHeap();
185
245
  return ptr;
@@ -204,7 +264,7 @@ async function instantiateFatFSModule(input) {
204
264
  const imports = createDefaultImports(wasmContext);
205
265
  let response = await fetch(source);
206
266
  if (!response.ok) {
207
- throw new Error(`Unable to fetch FatFS wasm from ${response.url}`);
267
+ throw new Error(`Unable to fetch FATFS wasm from ${response.url}`);
208
268
  }
209
269
  console.info("[fatfs-wasm] Fetch complete, status", response.status);
210
270
  if ("instantiateStreaming" in WebAssembly && typeof WebAssembly.instantiateStreaming === "function") {
@@ -216,10 +276,10 @@ async function instantiateFatFSModule(input) {
216
276
  return streaming.instance.exports;
217
277
  }
218
278
  catch (error) {
219
- console.warn("Unable to instantiate FatFS wasm via streaming, retrying with arrayBuffer()", error);
279
+ console.warn("Unable to instantiate FATFS wasm via streaming, retrying with arrayBuffer()", error);
220
280
  response = await fetch(source);
221
281
  if (!response.ok) {
222
- throw new Error(`Unable to fetch FatFS wasm from ${response.url}`);
282
+ throw new Error(`Unable to fetch FATFS wasm from ${response.url}`);
223
283
  }
224
284
  console.info("[fatfs-wasm] Fallback fetch complete, status", response.status);
225
285
  }
@@ -239,26 +299,38 @@ function parseListPayload(payload) {
239
299
  .split("\n")
240
300
  .filter((line) => line.length > 0)
241
301
  .map((line) => {
242
- const [rawPath, rawSize] = line.split("\t");
243
- const path = rawPath ?? "";
244
- const size = Number(rawSize ?? "0") || 0;
245
- // Determine type: directories end with '/', files don't
246
- const type = path.endsWith('/') ? 'dir' : 'file';
302
+ const [rawPath, rawSize, rawType] = line.split("\t");
247
303
  return {
248
- path: path,
249
- size: size,
250
- type: type
304
+ path: rawPath ?? "",
305
+ size: Number(rawSize ?? "0") || 0,
306
+ type: rawType === "d" ? "dir" : "file",
251
307
  };
252
308
  });
253
309
  }
254
- function normalizePath(input) {
255
- const value = input.trim().replace(/\\/g, "/");
256
- const withoutRoot = value.replace(/^\/+/, "");
257
- if (!withoutRoot) {
258
- throw new Error('Path must point to a file (e.g. "docs/readme.txt")');
259
- }
260
- const collapsed = withoutRoot.replace(/\/{2,}/g, "/");
261
- return collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
310
+ function normalizeMountPath(input) {
311
+ const raw = (input ?? "").trim();
312
+ if (!raw || raw === "/") {
313
+ return FAT_MOUNT;
314
+ }
315
+ const normalized = raw.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
316
+ const collapsed = normalized.endsWith("/") && normalized !== "/" ? normalized.slice(0, -1) : normalized;
317
+ const lower = collapsed.toLowerCase();
318
+ if (lower.startsWith(FAT_MOUNT)) {
319
+ const rest = collapsed.slice(FAT_MOUNT.length);
320
+ return rest ? `${FAT_MOUNT}${rest}` : FAT_MOUNT;
321
+ }
322
+ if (collapsed.startsWith("/")) {
323
+ return `${FAT_MOUNT}${collapsed}`;
324
+ }
325
+ return `${FAT_MOUNT}/${collapsed}`;
326
+ }
327
+ function joinListPath(basePath, entryPath) {
328
+ const base = basePath.replace(/\/+$/, "");
329
+ if (!entryPath || entryPath === "/") {
330
+ return base || FAT_MOUNT;
331
+ }
332
+ const trimmed = entryPath.replace(/^\/+/, "");
333
+ return base === FAT_MOUNT ? `${FAT_MOUNT}/${trimmed}` : `${base}/${trimmed}`;
262
334
  }
263
335
  function asUint8Array(source, encoder) {
264
336
  if (typeof source === "string") {
@@ -301,13 +373,13 @@ function createDefaultImports(context) {
301
373
  const ok = () => 0;
302
374
  return {
303
375
  env: {
304
- emscripten_notify_memory_growth: noop
376
+ emscripten_notify_memory_growth: noop,
305
377
  },
306
378
  wasi_snapshot_preview1: {
307
379
  fd_close: ok,
308
380
  fd_seek: ok,
309
- fd_write: (fd, iov, iovcnt, pnum) => handleFdWrite(context, fd, iov, iovcnt, pnum)
310
- }
381
+ fd_write: (fd, iov, iovcnt, pnum) => handleFdWrite(context, fd, iov, iovcnt, pnum),
382
+ },
311
383
  };
312
384
  }
313
385
  function handleFdWrite(context, fd, iov, iovcnt, pnum) {
@@ -322,13 +394,11 @@ function handleFdWrite(context, fd, iov, iovcnt, pnum) {
322
394
  const ptr = view.getUint32(base, true);
323
395
  const len = view.getUint32(base + 4, true);
324
396
  total += len;
325
- // Suppress fd_write logging in production (fd 1=stdout, 2=stderr)
326
- // Uncomment for debugging:
327
- // if (fd === 1 || fd === 2) {
328
- // const bytes = new Uint8Array(memory.buffer, ptr, len);
329
- // const text = new TextDecoder().decode(bytes);
330
- // console.info(`[fatfs-wasm::fd_write fd=${fd}] ${text}`);
331
- // }
397
+ if (fd === 1 || fd === 2) {
398
+ const bytes = new Uint8Array(memory.buffer, ptr, len);
399
+ const text = new TextDecoder().decode(bytes);
400
+ console.info(`[fatfs-wasm::fd_write fd=${fd}] ${text}`);
401
+ }
332
402
  }
333
403
  view.setUint32(pnum, total, true);
334
404
  return 0;