libfw-client 0.3.4 → 0.4.1

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/index.d.ts CHANGED
@@ -36,6 +36,17 @@ export interface UploadEntry {
36
36
  mtime: number;
37
37
  }
38
38
 
39
+ /**
40
+ * A `File` together with a caller-provided virtual path override. Passed
41
+ * verbatim to `options.resolveUploadPath`; when no resolver is configured,
42
+ * `relPath` itself is used as the upload path.
43
+ */
44
+ export interface UploadFileEntry {
45
+ file: File;
46
+ /** Virtual path (POSIX separators) to upload the file as. */
47
+ relPath?: string;
48
+ }
49
+
39
50
  /** Adaptive-tuning parameters the engine is currently tuned to. */
40
51
  export interface TuningParams {
41
52
  /** Cross-file transfer concurrency. */
@@ -171,6 +182,30 @@ export interface LibfwClientOptions {
171
182
  tuneTtlMs?: number;
172
183
  /** Optional progress/state listener. Tuning updates arrive as `{ type: 'tuning', phase, params, stats }`. */
173
184
  onEvent?: (event: LibfwEvent | LibfwTuningEvent) => void;
185
+ /**
186
+ * Pre-selected directory handle (or a resolver returning one) used instead
187
+ * of `showDirectoryPicker()` for every fs-mode download/upload.
188
+ */
189
+ directoryHandle?:
190
+ | FileSystemDirectoryHandle
191
+ | (() => FileSystemDirectoryHandle | Promise<FileSystemDirectoryHandle>);
192
+ /**
193
+ * Map a virtual server path to the display name used for fs-mode on-disk
194
+ * paths (directories/files created in the picked download folder),
195
+ * browser downloads, `.zip` entry names and the `.zip` archive name. May
196
+ * be async; returned values are still validated against zip-slip.
197
+ */
198
+ resolveDisplayName?: (path: string) => string | Promise<string>;
199
+ /**
200
+ * Map an upload entry to its virtual path on the server. `entry` is the
201
+ * original item passed to `upload()` (a bare `File` or an
202
+ * `UploadFileEntry` wrapper). `defaultPath` is
203
+ * `entry.relPath || file.webkitRelativePath || file.name`. May be async.
204
+ */
205
+ resolveUploadPath?: (
206
+ entry: File | UploadFileEntry,
207
+ defaultPath: string
208
+ ) => string | Promise<string>;
174
209
  }
175
210
 
176
211
  /**
@@ -204,9 +239,12 @@ export declare class LibfwClient {
204
239
  *
205
240
  * @param token bearer token
206
241
  * @param filePath virtual server path of the file to download
242
+ * @param opts optional overrides; `fileName` is the local name the file is
243
+ * saved as (leaf name only — parent directories of `filePath` still
244
+ * apply in fs mode).
207
245
  * @returns total bytes written
208
246
  */
209
- downloadFile(token: string, filePath: string): Promise<number>;
247
+ downloadFile(token: string, filePath: string, opts?: { fileName?: string }): Promise<number>;
210
248
 
211
249
  /**
212
250
  * Upload files to the server.
@@ -220,7 +258,7 @@ export declare class LibfwClient {
220
258
  */
221
259
  upload(
222
260
  token: string,
223
- files?: FileList | File[] | UploadEntry[],
261
+ files?: FileList | File[] | UploadFileEntry[] | UploadEntry[],
224
262
  ): Promise<number>;
225
263
 
226
264
  /** Pause the active transfer. */
package/index.js CHANGED
@@ -245,6 +245,26 @@ export class LibfwClient {
245
245
  * @param {(event: {type: string, done: number, total: number, path?: string, error?: string}) => void} [options.onEvent]
246
246
  * optional progress/state listener. Tuning updates arrive as
247
247
  * `{ type: 'tuning', phase, params, stats }` events.
248
+ * @param {FileSystemDirectoryHandle|(() => FileSystemDirectoryHandle|Promise<FileSystemDirectoryHandle>)} [options.directoryHandle]
249
+ * pre-selected directory handle (or a resolver returning one) used
250
+ * instead of popping `showDirectoryPicker()` for every fs-mode
251
+ * download/upload. Callers that already hold a handle (e.g. from a
252
+ * picker of their own, or a persisted handle) can inject it here.
253
+ * @param {(path: string) => string|Promise<string>} [options.resolveDisplayName]
254
+ * optional mapping from a virtual server path to the display name
255
+ * used for fs-mode on-disk paths (`_ensureFileHandle` — the
256
+ * directories/files actually created in the picked folder), browser
257
+ * downloads (`_downloadName`), `.zip` entry names (fallback folder
258
+ * downloads) and the `.zip` archive name. The resolver may be sync
259
+ * or async (return a `Promise`); returned values are still validated
260
+ * against zip-slip/path traversal.
261
+ * @param {(entry: File|{file: File, relPath?: string}, defaultPath: string) => string|Promise<string>} [options.resolveUploadPath]
262
+ * optional mapping from an upload entry to its virtual path on the
263
+ * server. `entry` is the ORIGINAL item passed to `upload()` — either
264
+ * a bare `File` or a caller wrapper such as `{ file, relPath }` — so
265
+ * custom metadata survives into the resolver. `defaultPath` is
266
+ * `entry.relPath || file.webkitRelativePath || file.name`. The
267
+ * resolver may be sync or async (return a `Promise`).
248
268
  */
249
269
  constructor(options = {}) {
250
270
  this._options = {
@@ -265,6 +285,9 @@ export class LibfwClient {
265
285
  autoTune: false,
266
286
  tuneTtlMs: 3600000,
267
287
  onEvent: null,
288
+ directoryHandle: null,
289
+ resolveDisplayName: null,
290
+ resolveUploadPath: null,
268
291
  ...options,
269
292
  };
270
293
  /** @type {WasmEngine|null} */
@@ -283,9 +306,11 @@ export class LibfwClient {
283
306
  this._uploadPlan = [];
284
307
  /**
285
308
  * Active browser-download fallback state, or `null`.
286
- * @type {{isFolder:boolean, buffers:Map<string,Uint8Array[]>, order:string[], sizes:Map<string,number>}|null}
309
+ * @type {{isFolder:boolean, buffers:Map<string,Uint8Array[]>, order:string[], sizes:Map<string,number>, total:number, buffered:number}|null}
287
310
  */
288
311
  this._fallback = null;
312
+ /** Leaf-name override for the current single-file download, or `null`. */
313
+ this._singleFileName = null;
289
314
  }
290
315
 
291
316
  // ------------------------------------------------------------------ setup
@@ -434,6 +459,22 @@ export class LibfwClient {
434
459
  );
435
460
  }
436
461
 
462
+ /**
463
+ * Obtain the destination directory handle for fs-mode transfers: the
464
+ * injected `directoryHandle` option (a handle or a resolver function)
465
+ * wins; otherwise `showDirectoryPicker()` is popped.
466
+ * @returns {Promise<FileSystemDirectoryHandle>}
467
+ * @private
468
+ */
469
+ async _pickDirectory() {
470
+ const provided = this._options.directoryHandle;
471
+ if (provided) {
472
+ const handle = typeof provided === 'function' ? await provided() : provided;
473
+ if (handle) return handle;
474
+ }
475
+ return window.showDirectoryPicker();
476
+ }
477
+
437
478
  /**
438
479
  * Resolve the effective download mode from the `downloadMode` option:
439
480
  * an explicit `'fs'`/`'browser'` wins; `'auto'` falls back to the browser
@@ -469,7 +510,7 @@ export class LibfwClient {
469
510
  if (this._effectiveMode() === 'browser') {
470
511
  return this._downloadViaBrowser(engine, token, dirPath, true);
471
512
  }
472
- this._dirHandle = await window.showDirectoryPicker();
513
+ this._dirHandle = await this._pickDirectory();
473
514
  this._fileHandles.clear();
474
515
  try {
475
516
  return await engine.download_folder(this._options.baseUrl, token, dirPath);
@@ -491,22 +532,35 @@ export class LibfwClient {
491
532
  *
492
533
  * @param {string} token bearer token
493
534
  * @param {string} filePath virtual server path of the file to download
535
+ * @param {{fileName?: string}} [opts] optional overrides; `fileName` is the
536
+ * local name the file is saved as (leaf name only — parent
537
+ * directories of `filePath` still apply in fs mode).
494
538
  * @returns {Promise<number>} total bytes transferred
495
539
  * @throws {LibfwError}
496
540
  */
497
- async downloadFile(token, filePath) {
541
+ async downloadFile(token, filePath, opts = {}) {
498
542
  const engine = await this._ready();
499
543
  if (!filePath) throw new LibfwError('downloadFile requires a file path', 'path');
500
- if (this._effectiveMode() === 'browser') {
501
- return this._downloadViaBrowser(engine, token, filePath, false);
544
+ if (opts.fileName !== undefined) {
545
+ const name = String(opts.fileName);
546
+ // Reject anything that could escape the destination directory (the
547
+ // FS Access API would throw a raw TypeError anyway; fail uniformly).
548
+ if (!name || name === '.' || name === '..' || /[/\\]/.test(name)) {
549
+ throw new LibfwError(`invalid fileName: ${name}`, 'path');
550
+ }
502
551
  }
503
- this._dirHandle = await window.showDirectoryPicker();
504
- this._fileHandles.clear();
552
+ this._singleFileName = opts.fileName ? String(opts.fileName) : null;
505
553
  try {
554
+ if (this._effectiveMode() === 'browser') {
555
+ return await this._downloadViaBrowser(engine, token, filePath, false);
556
+ }
557
+ this._dirHandle = await this._pickDirectory();
558
+ this._fileHandles.clear();
506
559
  return await engine.download_file(this._options.baseUrl, token, filePath);
507
560
  } catch (err) {
508
561
  throw toLibfwError(err);
509
562
  } finally {
563
+ this._singleFileName = null;
510
564
  await this._flushWritables();
511
565
  await this._syncResumeOffsets();
512
566
  }
@@ -531,7 +585,7 @@ export class LibfwClient {
531
585
  * @private
532
586
  */
533
587
  async _downloadViaBrowser(engine, token, path, isFolder) {
534
- this._fallback = { isFolder, buffers: new Map(), order: [], sizes: new Map(), total: 0 };
588
+ this._fallback = { isFolder, buffers: new Map(), order: [], sizes: new Map(), total: 0, buffered: 0 };
535
589
  try {
536
590
  const total = isFolder
537
591
  ? await engine.download_folder(this._options.baseUrl, token, path)
@@ -539,22 +593,36 @@ export class LibfwClient {
539
593
  const { buffers, order, sizes } = this._fallback;
540
594
  if (isFolder) {
541
595
  const entries = [];
596
+ // Distinct virtual paths may map to the same display name (a custom
597
+ // `resolveDisplayName` is not required to be injective); duplicate
598
+ // names would silently shadow each other on extraction.
599
+ const names = new Set();
600
+ const pushEntry = (name, data) => {
601
+ if (names.has(name)) {
602
+ throw new LibfwError(`duplicate download entry name: ${name}`, 'path');
603
+ }
604
+ names.add(name);
605
+ entries.push({ name, data });
606
+ };
542
607
  for (const p of order) {
543
- entries.push({
544
- name: this._safeEntryName(p),
545
- data: this._concatBuffers(buffers.get(p)?.chunks || []),
546
- });
608
+ pushEntry(
609
+ await this._safeEntryName(p),
610
+ this._concatBuffers(buffers.get(p)?.chunks || [])
611
+ );
547
612
  }
548
613
  // Include files that were announced but produced no bytes (empty).
549
614
  for (const p of sizes.keys()) {
550
615
  if (!buffers.has(p)) {
551
- entries.push({ name: this._safeEntryName(p), data: new Uint8Array(0) });
616
+ pushEntry(await this._safeEntryName(p), new Uint8Array(0));
552
617
  }
553
618
  }
554
- this._triggerBrowserDownload(createZip(entries), this._archiveName(path));
619
+ this._triggerBrowserDownload(createZip(entries), await this._archiveName(path));
555
620
  } else {
556
621
  const data = this._concatBuffers(buffers.get(path)?.chunks || []);
557
- this._triggerBrowserDownload(new Blob([data], { type: 'application/octet-stream' }), this._downloadName(path));
622
+ this._triggerBrowserDownload(
623
+ new Blob([data], { type: 'application/octet-stream' }),
624
+ this._singleFileName || (await this._downloadName(path))
625
+ );
558
626
  }
559
627
  return total;
560
628
  } catch (err) {
@@ -584,25 +652,36 @@ export class LibfwClient {
584
652
  }
585
653
 
586
654
  /**
587
- * Strip a leading `/` so an entry path is archive/OS friendly.
655
+ * Strip a leading `/` so an entry path is archive/OS/disk friendly,
656
+ * applying the `resolveDisplayName` option when configured. The resolver
657
+ * may be sync or async (return a `Promise`); both fs-mode on-disk paths
658
+ * (`_ensureFileHandle`) and browser-fallback archive/file names go
659
+ * through here, so a display-name mapping applies uniformly to every
660
+ * download destination.
588
661
  * @param {string} path
589
- * @returns {string}
662
+ * @returns {Promise<string>}
590
663
  * @private
591
664
  */
592
- _cleanPath(path) {
593
- return String(path).replace(/^\/+/, '');
665
+ async _cleanPath(path) {
666
+ const resolve = this._options.resolveDisplayName;
667
+ let resolved = null;
668
+ if (typeof resolve === 'function') {
669
+ resolved = await resolve(String(path));
670
+ }
671
+ return String(resolved != null ? resolved : path).replace(/^\/+/, '');
594
672
  }
595
673
 
596
674
  /**
597
- * Validate a virtual path for use as a ZIP entry name, rejecting any
598
- * traversal (`..`), absolute/drive-letter prefixes or Windows-style
599
- * separators that could escape the archive on extraction (zip-slip).
675
+ * Validate a virtual path for use as a ZIP entry name or an on-disk
676
+ * fs-mode path, rejecting any traversal (`..`), absolute/drive-letter
677
+ * prefixes or Windows-style separators that could escape the archive on
678
+ * extraction (zip-slip) or the picked download directory.
600
679
  * @param {string} path
601
- * @returns {string}
680
+ * @returns {Promise<string>}
602
681
  * @private
603
682
  */
604
- _safeEntryName(path) {
605
- const cleaned = this._cleanPath(path);
683
+ async _safeEntryName(path) {
684
+ const cleaned = await this._cleanPath(path);
606
685
  const segs = String(cleaned).split('/');
607
686
  if (segs.some((seg) => seg === '..' || seg.includes('\\') || /^[a-zA-Z]:/.test(seg))) {
608
687
  throw new LibfwError(`unsafe path in download: ${path}`, 'path');
@@ -623,22 +702,22 @@ export class LibfwClient {
623
702
  /**
624
703
  * Derive a safe file name from a virtual path.
625
704
  * @param {string} path
626
- * @returns {string}
705
+ * @returns {Promise<string>}
627
706
  * @private
628
707
  */
629
- _downloadName(path) {
630
- const name = this._cleanPath(path).split('/').pop();
708
+ async _downloadName(path) {
709
+ const name = (await this._cleanPath(path)).split('/').pop();
631
710
  return name || 'download';
632
711
  }
633
712
 
634
713
  /**
635
714
  * Derive the `.zip` archive name for a folder download.
636
715
  * @param {string} path
637
- * @returns {string}
716
+ * @returns {Promise<string>}
638
717
  * @private
639
718
  */
640
- _archiveName(path) {
641
- const base = this._cleanPath(path).split('/').pop() || 'download';
719
+ async _archiveName(path) {
720
+ const base = (await this._cleanPath(path)).split('/').pop() || 'download';
642
721
  return `${base.replace(/[^\w.\- ]+/g, '_') || 'download'}.zip`;
643
722
  }
644
723
 
@@ -703,6 +782,18 @@ export class LibfwClient {
703
782
  }
704
783
  buf.chunks.push(data);
705
784
  buf.len += data.length;
785
+ // Runtime backstop for the in-memory cap: `onFileStart` pre-checks use
786
+ // the engine's declared size; an under-reported size (or a skipped
787
+ // fileStart on an unusual path) must still fail instead of growing the
788
+ // buffer unbounded.
789
+ this._fallback.buffered += data.length;
790
+ const max = this._maxFallbackBytes();
791
+ if (max > 0 && this._fallback.buffered > max) {
792
+ throw new LibfwError(
793
+ `browser download buffered more than the ${max}-byte in-memory limit`,
794
+ 'too-large'
795
+ );
796
+ }
706
797
  return;
707
798
  }
708
799
  let entry = this._writables.get(path);
@@ -797,7 +888,9 @@ export class LibfwClient {
797
888
  * @private
798
889
  */
799
890
  async _ensureFileHandle(path) {
800
- const segments = splitPath(path);
891
+ // Map through the display-name resolver (sync or async) so fs-mode
892
+ // streaming lands under the same names the browser fallback would use.
893
+ const segments = splitPath(await this._safeEntryName(path));
801
894
  if (segments.length === 0) {
802
895
  throw new LibfwError(`invalid download path: ${path}`, 'path');
803
896
  }
@@ -805,7 +898,7 @@ export class LibfwClient {
805
898
  for (let i = 0; i < segments.length - 1; i += 1) {
806
899
  dir = await dir.getDirectoryHandle(segments[i], { create: true });
807
900
  }
808
- const name = segments[segments.length - 1];
901
+ const name = this._singleFileName || segments[segments.length - 1];
809
902
  const handle = await dir.getFileHandle(name, { create: true });
810
903
  return { dir, name, handle };
811
904
  }
@@ -836,7 +929,9 @@ export class LibfwClient {
836
929
  async _resolveFileHandle(path) {
837
930
  if (!this._dirHandle) return null;
838
931
  try {
839
- const segments = splitPath(path);
932
+ // Same display-name mapping as `_ensureFileHandle` so a resume finds
933
+ // the file previously written under its resolved (display) name.
934
+ const segments = splitPath(await this._safeEntryName(path));
840
935
  if (segments.length === 0) return null;
841
936
  let dir = this._dirHandle;
842
937
  for (let i = 0; i < segments.length - 1; i += 1) {
@@ -945,12 +1040,14 @@ export class LibfwClient {
945
1040
  *
946
1041
  * If `files` is omitted, `showDirectoryPicker()` is used to select a
947
1042
  * local folder whose structure is mirrored on the server. Otherwise
948
- * `files` may be a `FileList`, an array of `File`s, or an array of
1043
+ * `files` may be a `FileList`, an array of `File`s, an array of
1044
+ * `{ file, relPath }` wrappers (custom per-file virtual paths, passed
1045
+ * verbatim to `resolveUploadPath`), or an array of
949
1046
  * `{ path, size, mtime }` plan entries (when you want to drive reading
950
1047
  * yourself).
951
1048
  *
952
1049
  * @param {string} token bearer token
953
- * @param {FileList|File[]|Array<{path:string,size:number,mtime:number}>} [files]
1050
+ * @param {FileList|File[]|Array<{file:File,relPath?:string}>|Array<{path:string,size:number,mtime:number}>} [files]
954
1051
  * @returns {Promise<number>} total bytes uploaded
955
1052
  * @throws {LibfwError}
956
1053
  */
@@ -977,7 +1074,7 @@ export class LibfwClient {
977
1074
  if (typeof window === 'undefined' || typeof window.showDirectoryPicker !== 'function') {
978
1075
  throw new LibfwError('File System Access API is not available in this browser', 'unsupported');
979
1076
  }
980
- const dir = await window.showDirectoryPicker();
1077
+ const dir = await this._pickDirectory();
981
1078
  this._dirHandle = dir;
982
1079
  this._uploadPlan = await this._collectDirectoryFiles(dir, '');
983
1080
  } else {
@@ -1031,9 +1128,35 @@ export class LibfwClient {
1031
1128
  }
1032
1129
  const list = Array.from(files || []);
1033
1130
  const plan = [];
1034
- for (const file of list) {
1035
- if (!(file instanceof File)) continue;
1036
- const path = file.webkitRelativePath || file.name;
1131
+ const resolve = this._options.resolveUploadPath;
1132
+ const seen = new Set();
1133
+ for (const entry of list) {
1134
+ // Accept bare `File`s as well as wrappers like `{ file, relPath }`;
1135
+ // the resolver receives the ORIGINAL entry so caller-side metadata
1136
+ // (e.g. a custom `relPath`) is visible without WeakMap workarounds.
1137
+ const file = entry instanceof File ? entry : entry && entry.file instanceof File ? entry.file : null;
1138
+ if (!file) continue;
1139
+ // A wrapper-provided `relPath` is the most specific default; fall
1140
+ // back to the browser-provided relative path, then the file name.
1141
+ const defaultPath =
1142
+ (entry && typeof entry === 'object' && typeof entry.relPath === 'string' && entry.relPath
1143
+ ? entry.relPath
1144
+ : null) ||
1145
+ file.webkitRelativePath ||
1146
+ file.name;
1147
+ // The resolver may be async; a `null`/`undefined` mapping falls back
1148
+ // to the default path (same leniency as `resolveDisplayName`).
1149
+ let path = defaultPath;
1150
+ if (typeof resolve === 'function') {
1151
+ const mapped = await resolve(entry, defaultPath);
1152
+ if (mapped != null) path = String(mapped);
1153
+ }
1154
+ // A mapping that collides would silently shadow one File with another
1155
+ // (_uploadFiles keeps only the last) and upload the same path twice.
1156
+ if (seen.has(path)) {
1157
+ throw new LibfwError(`duplicate upload path: ${path}`, 'path');
1158
+ }
1159
+ seen.add(path);
1037
1160
  this._uploadFiles.set(path, file);
1038
1161
  plan.push({ path, size: file.size, mtime: Math.floor(file.lastModified / 1000) });
1039
1162
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libfw-client",
3
- "version": "0.3.4",
3
+ "version": "0.4.1",
4
4
  "description": "High-performance streaming file & folder transfer SDK for the browser (libfw WASM engine + File System Access API + IndexedDB resume).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -109,10 +109,10 @@ export interface InitOutput {
109
109
  readonly libfwclient_tune_state: (a: number) => any;
110
110
  readonly libfwclient_upload: (a: number, b: number, c: number, d: number, e: number) => any;
111
111
  readonly start: () => void;
112
- readonly wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke___wasm_bindgen_1e05ddb24c0b7df4___JsValue__core_9b3796e30d99ddb7___result__Result_____wasm_bindgen_1e05ddb24c0b7df4___JsError___true_: (a: number, b: number, c: any) => [number, number];
113
- readonly wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke___js_sys_bb6c79d0abe11c10___Function_fn_wasm_bindgen_1e05ddb24c0b7df4___JsValue_____wasm_bindgen_1e05ddb24c0b7df4___sys__Undefined___js_sys_bb6c79d0abe11c10___Function_fn_wasm_bindgen_1e05ddb24c0b7df4___JsValue_____wasm_bindgen_1e05ddb24c0b7df4___sys__Undefined_______true_: (a: number, b: number, c: any, d: any) => void;
114
- readonly wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke___web_sys_cda36541fa15086d___features__gen_ProgressEvent__ProgressEvent______true_: (a: number, b: number, c: any) => void;
115
- readonly wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke_______true_: (a: number, b: number) => void;
112
+ readonly wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke___wasm_bindgen_b2c787b844a3ac79___JsValue__core_ed718c3d60ebd546___result__Result_____wasm_bindgen_b2c787b844a3ac79___JsError___true_: (a: number, b: number, c: any) => [number, number];
113
+ readonly wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke___js_sys_209827a80d159db4___Function_fn_wasm_bindgen_b2c787b844a3ac79___JsValue_____wasm_bindgen_b2c787b844a3ac79___sys__Undefined___js_sys_209827a80d159db4___Function_fn_wasm_bindgen_b2c787b844a3ac79___JsValue_____wasm_bindgen_b2c787b844a3ac79___sys__Undefined_______true_: (a: number, b: number, c: any, d: any) => void;
114
+ readonly wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke___web_sys_b45f848915fa3e2d___features__gen_ProgressEvent__ProgressEvent______true_: (a: number, b: number, c: any) => void;
115
+ readonly wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke_______true_: (a: number, b: number) => void;
116
116
  readonly __wbindgen_malloc: (a: number, b: number) => number;
117
117
  readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
118
118
  readonly __wbindgen_exn_store: (a: number) => void;
@@ -250,6 +250,9 @@ function __wbg_get_imports() {
250
250
  __wbg_abort_4426bc6bd4153680: function() { return handleError(function (arg0) {
251
251
  arg0.abort();
252
252
  }, arguments); },
253
+ __wbg_abort_8bae0f33e7833997: function(arg0) {
254
+ arg0.abort();
255
+ },
253
256
  __wbg_apply_3ac86a26fdb56c05: function() { return handleError(function (arg0, arg1, arg2) {
254
257
  const ret = arg0.apply(arg1, arg2);
255
258
  return ret;
@@ -384,6 +387,10 @@ function __wbg_get_imports() {
384
387
  const ret = new Array();
385
388
  return ret;
386
389
  },
390
+ __wbg_new_4339b2a2675a03e3: function() { return handleError(function () {
391
+ const ret = new AbortController();
392
+ return ret;
393
+ }, arguments); },
387
394
  __wbg_new_6b1dd7bed0e5462c: function() { return handleError(function () {
388
395
  const ret = new XMLHttpRequest();
389
396
  return ret;
@@ -395,7 +402,7 @@ function __wbg_get_imports() {
395
402
  const a = state0.a;
396
403
  state0.a = 0;
397
404
  try {
398
- return wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke___js_sys_bb6c79d0abe11c10___Function_fn_wasm_bindgen_1e05ddb24c0b7df4___JsValue_____wasm_bindgen_1e05ddb24c0b7df4___sys__Undefined___js_sys_bb6c79d0abe11c10___Function_fn_wasm_bindgen_1e05ddb24c0b7df4___JsValue_____wasm_bindgen_1e05ddb24c0b7df4___sys__Undefined_______true_(a, state0.b, arg0, arg1);
405
+ return wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke___js_sys_209827a80d159db4___Function_fn_wasm_bindgen_b2c787b844a3ac79___JsValue_____wasm_bindgen_b2c787b844a3ac79___sys__Undefined___js_sys_209827a80d159db4___Function_fn_wasm_bindgen_b2c787b844a3ac79___JsValue_____wasm_bindgen_b2c787b844a3ac79___sys__Undefined_______true_(a, state0.b, arg0, arg1);
399
406
  } finally {
400
407
  state0.a = a;
401
408
  }
@@ -429,7 +436,7 @@ function __wbg_get_imports() {
429
436
  const a = state0.a;
430
437
  state0.a = 0;
431
438
  try {
432
- return wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke___js_sys_bb6c79d0abe11c10___Function_fn_wasm_bindgen_1e05ddb24c0b7df4___JsValue_____wasm_bindgen_1e05ddb24c0b7df4___sys__Undefined___js_sys_bb6c79d0abe11c10___Function_fn_wasm_bindgen_1e05ddb24c0b7df4___JsValue_____wasm_bindgen_1e05ddb24c0b7df4___sys__Undefined_______true_(a, state0.b, arg0, arg1);
439
+ return wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke___js_sys_209827a80d159db4___Function_fn_wasm_bindgen_b2c787b844a3ac79___JsValue_____wasm_bindgen_b2c787b844a3ac79___sys__Undefined___js_sys_209827a80d159db4___Function_fn_wasm_bindgen_b2c787b844a3ac79___JsValue_____wasm_bindgen_b2c787b844a3ac79___sys__Undefined_______true_(a, state0.b, arg0, arg1);
433
440
  } finally {
434
441
  state0.a = a;
435
442
  }
@@ -537,6 +544,13 @@ function __wbg_get_imports() {
537
544
  __wbg_set_onreadystatechange_14ce44725c7e0789: function(arg0, arg1) {
538
545
  arg0.onreadystatechange = arg1;
539
546
  },
547
+ __wbg_set_signal_c4ef8faddb4c1446: function(arg0, arg1) {
548
+ arg0.signal = arg1;
549
+ },
550
+ __wbg_signal_dad7cb35193abd31: function(arg0) {
551
+ const ret = arg0.signal;
552
+ return ret;
553
+ },
540
554
  __wbg_stack_3b0d974bbf31e44f: function(arg0, arg1) {
541
555
  const ret = arg1.stack;
542
556
  const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
@@ -585,18 +599,18 @@ function __wbg_get_imports() {
585
599
  return ret;
586
600
  }, arguments); },
587
601
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
588
- // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 198, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
589
- const ret = makeMutClosure(arg0, arg1, wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke___wasm_bindgen_1e05ddb24c0b7df4___JsValue__core_9b3796e30d99ddb7___result__Result_____wasm_bindgen_1e05ddb24c0b7df4___JsError___true_);
602
+ // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 196, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
603
+ const ret = makeMutClosure(arg0, arg1, wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke___wasm_bindgen_b2c787b844a3ac79___JsValue__core_ed718c3d60ebd546___result__Result_____wasm_bindgen_b2c787b844a3ac79___JsError___true_);
590
604
  return ret;
591
605
  },
592
606
  __wbindgen_cast_0000000000000002: function(arg0, arg1) {
593
607
  // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("ProgressEvent")], shim_idx: 5, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
594
- const ret = makeMutClosure(arg0, arg1, wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke___web_sys_cda36541fa15086d___features__gen_ProgressEvent__ProgressEvent______true_);
608
+ const ret = makeMutClosure(arg0, arg1, wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke___web_sys_b45f848915fa3e2d___features__gen_ProgressEvent__ProgressEvent______true_);
595
609
  return ret;
596
610
  },
597
611
  __wbindgen_cast_0000000000000003: function(arg0, arg1) {
598
612
  // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 7, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
599
- const ret = makeMutClosure(arg0, arg1, wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke_______true_);
613
+ const ret = makeMutClosure(arg0, arg1, wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke_______true_);
600
614
  return ret;
601
615
  },
602
616
  __wbindgen_cast_0000000000000004: function(arg0) {
@@ -625,23 +639,23 @@ function __wbg_get_imports() {
625
639
  };
626
640
  }
627
641
 
628
- function wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke_______true_(arg0, arg1) {
629
- wasm.wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke_______true_(arg0, arg1);
642
+ function wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke_______true_(arg0, arg1) {
643
+ wasm.wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke_______true_(arg0, arg1);
630
644
  }
631
645
 
632
- function wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke___web_sys_cda36541fa15086d___features__gen_ProgressEvent__ProgressEvent______true_(arg0, arg1, arg2) {
633
- wasm.wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke___web_sys_cda36541fa15086d___features__gen_ProgressEvent__ProgressEvent______true_(arg0, arg1, arg2);
646
+ function wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke___web_sys_b45f848915fa3e2d___features__gen_ProgressEvent__ProgressEvent______true_(arg0, arg1, arg2) {
647
+ wasm.wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke___web_sys_b45f848915fa3e2d___features__gen_ProgressEvent__ProgressEvent______true_(arg0, arg1, arg2);
634
648
  }
635
649
 
636
- function wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke___wasm_bindgen_1e05ddb24c0b7df4___JsValue__core_9b3796e30d99ddb7___result__Result_____wasm_bindgen_1e05ddb24c0b7df4___JsError___true_(arg0, arg1, arg2) {
637
- const ret = wasm.wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke___wasm_bindgen_1e05ddb24c0b7df4___JsValue__core_9b3796e30d99ddb7___result__Result_____wasm_bindgen_1e05ddb24c0b7df4___JsError___true_(arg0, arg1, arg2);
650
+ function wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke___wasm_bindgen_b2c787b844a3ac79___JsValue__core_ed718c3d60ebd546___result__Result_____wasm_bindgen_b2c787b844a3ac79___JsError___true_(arg0, arg1, arg2) {
651
+ const ret = wasm.wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke___wasm_bindgen_b2c787b844a3ac79___JsValue__core_ed718c3d60ebd546___result__Result_____wasm_bindgen_b2c787b844a3ac79___JsError___true_(arg0, arg1, arg2);
638
652
  if (ret[1]) {
639
653
  throw takeFromExternrefTable0(ret[0]);
640
654
  }
641
655
  }
642
656
 
643
- function wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke___js_sys_bb6c79d0abe11c10___Function_fn_wasm_bindgen_1e05ddb24c0b7df4___JsValue_____wasm_bindgen_1e05ddb24c0b7df4___sys__Undefined___js_sys_bb6c79d0abe11c10___Function_fn_wasm_bindgen_1e05ddb24c0b7df4___JsValue_____wasm_bindgen_1e05ddb24c0b7df4___sys__Undefined_______true_(arg0, arg1, arg2, arg3) {
644
- wasm.wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke___js_sys_bb6c79d0abe11c10___Function_fn_wasm_bindgen_1e05ddb24c0b7df4___JsValue_____wasm_bindgen_1e05ddb24c0b7df4___sys__Undefined___js_sys_bb6c79d0abe11c10___Function_fn_wasm_bindgen_1e05ddb24c0b7df4___JsValue_____wasm_bindgen_1e05ddb24c0b7df4___sys__Undefined_______true_(arg0, arg1, arg2, arg3);
657
+ function wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke___js_sys_209827a80d159db4___Function_fn_wasm_bindgen_b2c787b844a3ac79___JsValue_____wasm_bindgen_b2c787b844a3ac79___sys__Undefined___js_sys_209827a80d159db4___Function_fn_wasm_bindgen_b2c787b844a3ac79___JsValue_____wasm_bindgen_b2c787b844a3ac79___sys__Undefined_______true_(arg0, arg1, arg2, arg3) {
658
+ wasm.wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke___js_sys_209827a80d159db4___Function_fn_wasm_bindgen_b2c787b844a3ac79___JsValue_____wasm_bindgen_b2c787b844a3ac79___sys__Undefined___js_sys_209827a80d159db4___Function_fn_wasm_bindgen_b2c787b844a3ac79___JsValue_____wasm_bindgen_b2c787b844a3ac79___sys__Undefined_______true_(arg0, arg1, arg2, arg3);
645
659
  }
646
660
 
647
661
  const LibfwClientFinalization = (typeof FinalizationRegistry === 'undefined')
Binary file
@@ -19,10 +19,10 @@ export const libfwclient_total_bytes: (a: number) => number;
19
19
  export const libfwclient_tune_state: (a: number) => any;
20
20
  export const libfwclient_upload: (a: number, b: number, c: number, d: number, e: number) => any;
21
21
  export const start: () => void;
22
- export const wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke___wasm_bindgen_1e05ddb24c0b7df4___JsValue__core_9b3796e30d99ddb7___result__Result_____wasm_bindgen_1e05ddb24c0b7df4___JsError___true_: (a: number, b: number, c: any) => [number, number];
23
- export const wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke___js_sys_bb6c79d0abe11c10___Function_fn_wasm_bindgen_1e05ddb24c0b7df4___JsValue_____wasm_bindgen_1e05ddb24c0b7df4___sys__Undefined___js_sys_bb6c79d0abe11c10___Function_fn_wasm_bindgen_1e05ddb24c0b7df4___JsValue_____wasm_bindgen_1e05ddb24c0b7df4___sys__Undefined_______true_: (a: number, b: number, c: any, d: any) => void;
24
- export const wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke___web_sys_cda36541fa15086d___features__gen_ProgressEvent__ProgressEvent______true_: (a: number, b: number, c: any) => void;
25
- export const wasm_bindgen_1e05ddb24c0b7df4___convert__closures_____invoke_______true_: (a: number, b: number) => void;
22
+ export const wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke___wasm_bindgen_b2c787b844a3ac79___JsValue__core_ed718c3d60ebd546___result__Result_____wasm_bindgen_b2c787b844a3ac79___JsError___true_: (a: number, b: number, c: any) => [number, number];
23
+ export const wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke___js_sys_209827a80d159db4___Function_fn_wasm_bindgen_b2c787b844a3ac79___JsValue_____wasm_bindgen_b2c787b844a3ac79___sys__Undefined___js_sys_209827a80d159db4___Function_fn_wasm_bindgen_b2c787b844a3ac79___JsValue_____wasm_bindgen_b2c787b844a3ac79___sys__Undefined_______true_: (a: number, b: number, c: any, d: any) => void;
24
+ export const wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke___web_sys_b45f848915fa3e2d___features__gen_ProgressEvent__ProgressEvent______true_: (a: number, b: number, c: any) => void;
25
+ export const wasm_bindgen_b2c787b844a3ac79___convert__closures_____invoke_______true_: (a: number, b: number) => void;
26
26
  export const __wbindgen_malloc: (a: number, b: number) => number;
27
27
  export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
28
28
  export const __wbindgen_exn_store: (a: number) => void;
package/pkg/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "libfw-client",
3
3
  "type": "module",
4
4
  "description": "WASM engine + JS SDK for libfw browser clients",
5
- "version": "0.3.4",
5
+ "version": "0.4.1",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
package/zip.js CHANGED
@@ -51,8 +51,21 @@ export function createZip(entries) {
51
51
  const central = [];
52
52
  let offset = 0;
53
53
 
54
+ // This writer uses classic 32-bit ZIP fields (no ZIP64): entry sizes, the
55
+ // local-header offset, the central-directory size/offset and the entry
56
+ // count are all stored as u32/u16. Exceeding any of them would silently
57
+ // corrupt the archive, so fail loudly instead.
58
+ const MAX_U32 = 0xffffffff;
59
+ const MAX_U16 = 0xffff;
60
+ if (entries.length > MAX_U16) {
61
+ throw new Error(`too many zip entries: ${entries.length} > ${MAX_U16}`);
62
+ }
63
+
54
64
  for (const entry of entries) {
55
65
  const nameBytes = encoder.encode(entry.name);
66
+ if (nameBytes.length > MAX_U16) {
67
+ throw new Error(`zip entry name too long: ${entry.name.slice(0, 64)}…`);
68
+ }
56
69
  const data = entry.data;
57
70
  const crc = crc32(data);
58
71
 
@@ -75,6 +88,9 @@ export function createZip(entries) {
75
88
  body.push(local, data);
76
89
  central.push({ nameBytes, crc, size: data.length, offset });
77
90
  offset += local.length + data.length;
91
+ if (offset > MAX_U32) {
92
+ throw new Error('zip archive exceeds the 4 GiB limit of this writer');
93
+ }
78
94
  }
79
95
 
80
96
  // Central directory.
@@ -104,6 +120,9 @@ export function createZip(entries) {
104
120
  dir.push(cd);
105
121
  cdSize += cd.length;
106
122
  }
123
+ if (cdSize > MAX_U32) {
124
+ throw new Error('zip central directory exceeds the 4 GiB limit of this writer');
125
+ }
107
126
 
108
127
  const cdOffset = offset;
109
128