@gyng/remote-zip 0.2.5 → 1.0.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.
@@ -1,5 +1,23 @@
1
+ export { crc32 } from "./crypto";
2
+ /** Machine-readable discriminant for {@link RemoteZipError}. */
3
+ export type RemoteZipErrorCode = "UNKNOWN" | "CONTENT_LENGTH_MISSING" | "INVALID_CONTENT_LENGTH" | "HTTP_ERROR" | "RANGE_NOT_SUPPORTED" | "RANGE_RESPONSE_MISMATCH" | "EOCD_NOT_FOUND" | "UNSUPPORTED_ZIP64" | "UNSUPPORTED_ENCRYPTION" | "FILE_NOT_FOUND" | "LOCAL_HEADER_PARSE_FAILED" | "CENTRAL_DIRECTORY_OUT_OF_BOUNDS" | "INVALID_ARCHIVE" | "TRUNCATED_ENTRY" | "UNSUPPORTED_COMPRESSION" | "UNSUPPORTED_MULTI_DISK" | "DECOMPRESSION_LIMIT_EXCEEDED" | "CRC_MISMATCH" | "WRONG_PASSWORD" | "DECRYPTION_FAILED";
1
4
  export declare class RemoteZipError extends Error {
2
- constructor(message: any);
5
+ /** Machine-readable error code, for programmatic handling without matching on `message`. */
6
+ code: RemoteZipErrorCode;
7
+ constructor(message: string, code?: RemoteZipErrorCode);
8
+ }
9
+ /** Per-call options for {@link RemoteZip.fetch} / {@link RemoteZip.fetchStream}. */
10
+ export interface EntryDecodeOptions {
11
+ /** If set, decompression aborts and throws once output would exceed this many bytes. */
12
+ maxUncompressedSize?: number;
13
+ /** Aborts this request (in addition to any instance-level signal). */
14
+ signal?: AbortSignal;
15
+ /** Per-request timeout in milliseconds. */
16
+ timeoutMs?: number;
17
+ /** If set, verify the decompressed output against the entry's CRC-32. */
18
+ verifyCrc?: boolean;
19
+ /** Password for an encrypted entry (traditional ZipCrypto or WinZip AES). */
20
+ password?: string;
3
21
  }
4
22
  export interface EndOfCentralDirectory {
5
23
  meta: Record<string, unknown>;
@@ -18,9 +36,9 @@ export interface EndOfCentralDirectory {
18
36
  centralDirectoryByteSize: number;
19
37
  /** Offset of start of central directory, relative to start of archive (or 0xffffffff for ZIP64) */
20
38
  centralDirectoryByteOffset: number;
21
- /** Comment length (n) */
22
- comment: string;
23
39
  /** Comment */
40
+ comment: string;
41
+ /** Comment length (n) */
24
42
  commentLength: number;
25
43
  };
26
44
  }
@@ -237,7 +255,15 @@ export declare class RemoteZip {
237
255
  method: string;
238
256
  /** Credentials passed to `fetch` when retrieving files. Defaults to `same-origin`. */
239
257
  credentials: "include" | "omit" | "same-origin";
240
- constructor({ contentLength, url, centralDirectoryRecords, endOfCentralDirectory, method, credentials, }: {
258
+ /** Redirect mode passed to `fetch`. Defaults to `"follow"`. */
259
+ redirect?: RequestRedirect;
260
+ /** Signal that aborts in-flight requests. */
261
+ signal?: AbortSignal;
262
+ /** Per-request timeout in milliseconds. */
263
+ timeoutMs?: number;
264
+ /** Extra RequestInit merged into every `fetch`. */
265
+ requestInit?: RequestInit;
266
+ constructor({ contentLength, url, centralDirectoryRecords, endOfCentralDirectory, method, credentials, redirect, signal, timeoutMs, requestInit, }: {
241
267
  /** Length of the remote ZIP archive in bytes */
242
268
  contentLength: number;
243
269
  /** Passed to fetch when performing a HTTP GET request for the file */
@@ -247,8 +273,10 @@ export declare class RemoteZip {
247
273
  /** Passed to fetch when performing a HTTP GET request for the file */
248
274
  method: string;
249
275
  /** Passed to fetch when performing a HTTP GET request for the file. */
250
- credentials: "include" | "omit" | "same-origin";
251
- });
276
+ credentials?: "include" | "omit" | "same-origin";
277
+ } & Pick<RemoteZipRequestOptions, "redirect" | "signal" | "timeoutMs" | "requestInit">);
278
+ /** Build a fetch RequestInit from this instance's network options. */
279
+ private requestInitFor;
252
280
  /**
253
281
  * Get a formatted file listing of the remote ZIP archive.
254
282
  *
@@ -269,10 +297,64 @@ export declare class RemoteZip {
269
297
  *
270
298
  * @param path Path of the file in the remote ZIP archive
271
299
  * @param additionalHeaders Additional headers, if any, to be passed to the `fetch` request
300
+ * @param options.maxUncompressedSize If set, inflation aborts and throws once the
301
+ * decompressed output would exceed this many bytes. Set this when handling
302
+ * untrusted archives to guard against decompression bombs.
303
+ * @param options.signal Aborts this request (in addition to any instance-level signal).
304
+ * @param options.timeoutMs Per-request timeout for this fetch.
305
+ * @param options.verifyCrc If set, verify the decompressed output against the
306
+ * entry's CRC-32 and throw a {@link RemoteZipError} (`CRC_MISMATCH`) on mismatch.
272
307
  * @returns Inflated (uncompressed) bytes of the requested file
273
- * @throws [RemoteZipError](RemoteZipError) if it fails to parse or fetch
308
+ * @throws {@link RemoteZipError} if it fails to parse, fetch, or exceeds limits
309
+ */
310
+ fetch(path: string, additionalHeaders?: Headers, options?: EntryDecodeOptions): Promise<Uint8Array>;
311
+ /**
312
+ * Decrypt (if needed), decompress, and CRC-verify one entry's bytes. Shared by
313
+ * {@link fetch} and the encrypted path of {@link fetchStream}.
314
+ */
315
+ private decodeEntry;
316
+ /**
317
+ * Like {@link fetch}, but returns a `ReadableStream` of the uncompressed bytes
318
+ * so large entries can be processed incrementally without buffering the whole
319
+ * file. `maxUncompressedSize` is enforced mid-stream (the stream errors with a
320
+ * {@link RemoteZipError} once exceeded).
321
+ *
322
+ * ```ts
323
+ * const stream = await remoteZip.fetchStream("big.bin");
324
+ * for await (const chunk of stream) {
325
+ * // process each chunk
326
+ * }
327
+ * ```
274
328
  */
275
- fetch(path: string, additionalHeaders?: Headers): Promise<ArrayBuffer>;
329
+ fetchStream(path: string, additionalHeaders?: Headers, options?: EntryDecodeOptions): Promise<ReadableStream<Uint8Array>>;
330
+ /**
331
+ * Find an entry, reject encrypted ones, and issue the Range request that covers
332
+ * its local file header + compressed data. Shared by {@link fetch} and
333
+ * {@link fetchStream}.
334
+ */
335
+ private fetchEntryResponse;
336
+ private fetchRange;
337
+ }
338
+ /**
339
+ * Network options shared by every request a {@link RemoteZip} / {@link RemoteZipPointer}
340
+ * makes. All are optional with sensible defaults.
341
+ */
342
+ export interface RemoteZipRequestOptions {
343
+ /** HTTP method for the Range GET requests. The metadata probe is always a HEAD. Defaults to `"GET"`. */
344
+ method?: string;
345
+ /** Passed to `fetch`. Defaults to `"same-origin"`. */
346
+ credentials?: "include" | "omit" | "same-origin";
347
+ /**
348
+ * Passed to `fetch`. Defaults to `"follow"`. Use `"manual"` or `"error"` to avoid
349
+ * leaking `Authorization`/cookies cross-origin when a server responds with a redirect.
350
+ */
351
+ redirect?: RequestRedirect;
352
+ /** Aborts in-flight requests when this signal fires. */
353
+ signal?: AbortSignal;
354
+ /** Per-request timeout in milliseconds; combined with `signal` via `AbortSignal.any`. */
355
+ timeoutMs?: number;
356
+ /** Escape hatch merged into every `fetch` RequestInit (lowest precedence — the options above win). */
357
+ requestInit?: RequestInit;
276
358
  }
277
359
  /**
278
360
  * An uninitialised pointer to a remote ZIP file.
@@ -283,7 +365,7 @@ export declare class RemoteZip {
283
365
  * const url = new URL("http://www.example.com/test.zip");
284
366
  * const remoteZip = await new RemoteZipPointer({ url }).populate();
285
367
  * const fileListing = remoteZip.files(); // RemoteZipFile[]
286
- * const uncompressedBytes = await remoteZip.fetch("test.txt"); // ArrayBuffer
368
+ * const uncompressedBytes = await remoteZip.fetch("test.txt"); // Uint8Array
287
369
  * ```
288
370
  */
289
371
  export declare class RemoteZipPointer {
@@ -297,7 +379,15 @@ export declare class RemoteZipPointer {
297
379
  method: string;
298
380
  /** Credentials passed to `fetch` when retrieving files. Defaults to `same-origin`. */
299
381
  credentials: "include" | "omit" | "same-origin";
300
- constructor({ url, headUrl, additionalHeaders, method, credentials, }: {
382
+ /** Redirect mode passed to `fetch`. Defaults to `"follow"`. */
383
+ redirect?: RequestRedirect;
384
+ /** Signal that aborts in-flight requests. */
385
+ signal?: AbortSignal;
386
+ /** Per-request timeout in milliseconds. */
387
+ timeoutMs?: number;
388
+ /** Extra RequestInit merged into every `fetch`. */
389
+ requestInit?: RequestInit;
390
+ constructor({ url, headUrl, additionalHeaders, method, credentials, redirect, signal, timeoutMs, requestInit, }: {
301
391
  /** URL for GET requests */
302
392
  url: URL;
303
393
  /** Passed to fetch when performing a HTTP GET request for the file */
@@ -308,15 +398,18 @@ export declare class RemoteZipPointer {
308
398
  credentials?: "include" | "omit" | "same-origin";
309
399
  /** URL for HEAD request. Defaults to `url`. This can, for example, differ from `url` if you are using a signed URL for S3. */
310
400
  headUrl?: URL;
311
- });
401
+ } & Pick<RemoteZipRequestOptions, "redirect" | "signal" | "timeoutMs" | "requestInit">);
402
+ /** Build a fetch RequestInit from this pointer's network options. */
403
+ private requestInitFor;
312
404
  /**
313
405
  * Gets metadata about the ZIP file and constructs an initialised `RemoteZip`.
314
406
  *
315
- * @returns An initialised [RemoteZip](RemoteZip)
316
- * @throws [RemoteZipError](RemoteZipError) if it fails to parse or fetch
407
+ * @returns An initialised {@link RemoteZip}
408
+ * @throws {@link RemoteZipError} if it fails to parse or fetch
317
409
  */
318
410
  populate(): Promise<RemoteZip>;
319
411
  private fetchEndOfCentralDirectory;
412
+ private fetchZip64EOCD;
320
413
  private fetchCentralDirectoryRecords;
321
414
  }
322
415
  /**
@@ -328,3 +421,39 @@ export declare class RemoteZipPointer {
328
421
  * @see https://github.com/Stuk/jszip/blob/112fcdb9953c6b9a2744afee451d73029f7cd2f8/lib/reader/DataReader.js#L105
329
422
  */
330
423
  export declare const parseZipDatetime: (zipDate: number, zipTime: number) => string;
424
+ /**
425
+ * Detects whether an EOCD record points at ZIP64 structures this library does
426
+ * not parse yet. Any ZIP64 sentinel value (`0xffff` for 16-bit fields,
427
+ * `0xffffffff` for 32-bit fields) means the real value lives in a ZIP64 EOCD.
428
+ */
429
+ export declare const isZip64: (eocd: EndOfCentralDirectory) => boolean;
430
+ /** Effective central-directory location from a ZIP64 End Of Central Directory record. */
431
+ export interface Zip64EndOfCentralDirectory {
432
+ diskNumber: number;
433
+ cdDisk: number;
434
+ centralDirectoryDiskRecordCount: number;
435
+ centralDirectoryRecordCount: number;
436
+ centralDirectoryByteSize: number;
437
+ centralDirectoryByteOffset: number;
438
+ }
439
+ /** Locate the ZIP64 EOCD locator in a tail buffer and return the ZIP64 EOCD offset. */
440
+ export declare const parseZip64EOCDLocator: (buffer: ArrayBuffer) => {
441
+ diskWithZip64EOCD: number;
442
+ zip64EOCDOffset: number;
443
+ totalDisks: number;
444
+ } | null;
445
+ /** Parse a ZIP64 End Of Central Directory record (the real CD offset/size/count). */
446
+ export declare const parseZip64EOCD: (buffer: ArrayBuffer) => Zip64EndOfCentralDirectory | null;
447
+ /**
448
+ * Decode a ZIP filename/comment. ZIP entries use CP437 unless general-purpose
449
+ * bit 11 marks the field as UTF-8; ASCII is identical in both.
450
+ */
451
+ export declare const decodeZipString: (bytes: ArrayBuffer, utf8: boolean) => string;
452
+ export declare const parseAllCDs: (buffer: ArrayBuffer) => CentralDirectoryRecord[];
453
+ export declare const parseOneCD: (buffer: ArrayBuffer) => CentralDirectoryRecord | null;
454
+ export declare const parseOneEOCD: (buffer: ArrayBuffer) => EndOfCentralDirectory | null;
455
+ export declare const parseOneLocalFile: (buffer: ArrayBuffer,
456
+ /** Sometimes, the local header does not have the compressed size and a data descriptor is used after the compressed data.
457
+ * If provided, will be used if the local header indicates a data descriptor block.
458
+ * It is used to find the correct offset for the data descriptor. */
459
+ compressedSizeOverride?: number) => LocalFileHeader | null;
package/package.json CHANGED
@@ -1,57 +1,83 @@
1
1
  {
2
2
  "name": "@gyng/remote-zip",
3
- "version": "0.2.5",
4
- "main": "lib/cjs/index.js",
5
- "module": "lib/esm/index.js",
6
- "types": "lib/types/index.d.ts",
7
- "author": "AIcadium, Ng Guoyou <gyng@users.noreply.github.com>",
8
- "license": "MIT",
3
+ "version": "1.0.1",
4
+ "description": "Fetch file listings and individual files from a remote ZIP archive over HTTP Range requests, without downloading the whole archive.",
5
+ "keywords": [
6
+ "browser",
7
+ "central-directory",
8
+ "fetch",
9
+ "http",
10
+ "range",
11
+ "remote",
12
+ "stream",
13
+ "zip"
14
+ ],
15
+ "homepage": "https://github.com/gyng/remote-zip#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/gyng/remote-zip/issues"
18
+ },
19
+ "license": "(MIT OR Apache-2.0)",
20
+ "author": "Ng Guoyou <gyng@users.noreply.github.com>",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/gyng/remote-zip.git"
24
+ },
9
25
  "files": [
10
- "LICENSE",
26
+ "LICENSE-MIT",
27
+ "LICENSE-APACHE",
28
+ "README.md",
11
29
  "package.json",
12
30
  "lib/**"
13
31
  ],
14
- "repository": {
15
- "type": "git",
16
- "url": "git://github.com/gyng/remote-zip.git"
32
+ "type": "commonjs",
33
+ "sideEffects": false,
34
+ "main": "lib/cjs/index.js",
35
+ "module": "lib/esm/index.mjs",
36
+ "types": "lib/types/index.d.ts",
37
+ "exports": {
38
+ ".": {
39
+ "import": {
40
+ "types": "./lib/types/index.d.mts",
41
+ "default": "./lib/esm/index.mjs"
42
+ },
43
+ "require": {
44
+ "types": "./lib/types/index.d.ts",
45
+ "default": "./lib/cjs/index.js"
46
+ }
47
+ },
48
+ "./package.json": "./package.json"
17
49
  },
18
50
  "scripts": {
19
- "build": "rm -rf lib/* && yarn ts-types && node ./esbuild.mjs",
20
- "d:server": "http-server fixtures",
21
- "d": "WATCH=1 yarn build",
22
- "doc:gen": "yarn typedoc src/index.ts --excludePrivate --cleanOutputDir --logLevel Verbose --out ./docs/api",
23
- "lint:eslint:fix": "yarn lint:eslint --fix",
24
- "lint:eslint": "eslint . --ext .js --ext .jsx --ext .ts --ext .tsx",
25
- "lint:fix": "yarn lint:prettier:fix && yarn lint:eslint:fix",
26
- "lint:prettier:fix": "prettier --write .",
27
- "lint:prettier": "prettier --check .",
28
- "lint": "yarn lint:prettier && yarn lint:eslint",
29
- "t:watch": "yarn test --watch",
30
- "t": "yarn test",
31
- "test:coverage": "yarn test --coverage",
32
- "test": "jest",
33
- "ts-types": " tsc --emitDeclarationOnly --outDir lib/types"
51
+ "build": "rm -rf lib/* && npm run ts-types && node ./esbuild.mjs",
52
+ "d": "WATCH=1 npm run build",
53
+ "doc:gen": "npm --prefix tools/typedoc run generate",
54
+ "doc:site": "npm run build && npm run doc:gen && node ./scripts/build-site.mjs",
55
+ "lint:fix": "oxfmt . && oxlint --type-aware --import-plugin --vitest-plugin --fix .",
56
+ "lint": "oxfmt --check . && oxlint --type-aware --import-plugin --vitest-plugin .",
57
+ "t:watch": "vitest",
58
+ "t": "npm test",
59
+ "test:coverage": "vitest run --coverage",
60
+ "test:package": "npm run build && node ./scripts/test-package.mjs",
61
+ "test": "vitest run",
62
+ "ts-types": "tsc -p tsconfig.build.json",
63
+ "typecheck": "tsc -p tsconfig.json --noEmit",
64
+ "prepublishOnly": "npm run build"
65
+ },
66
+ "dependencies": {
67
+ "pako": "^3.0.1"
34
68
  },
35
69
  "devDependencies": {
36
- "@types/http-server": "^0.12.1",
37
- "@types/jest": "^29.5.2",
38
- "@types/node": "^20.3.1",
39
- "@types/pako": "^2.0.0",
40
- "@typescript-eslint/eslint-plugin": "^5.60.0",
41
- "@typescript-eslint/parser": "^5.60.0",
42
- "esbuild": "^0.18.7",
43
- "esbuild-jest": "^0.5.0",
44
- "eslint": "^8.43.0",
45
- "eslint-config-prettier": "^8.8.0",
46
- "eslint-plugin-jest": "^27.2.2",
47
- "http-server": "^14.1.1",
48
- "jest": "^29.5.0",
49
- "prettier": "^2.8.8",
50
- "typedoc": "^0.24.8",
51
- "typescript": "^5.1.3"
70
+ "@types/node": "^26.1.1",
71
+ "@vitest/coverage-v8": "^4.1.10",
72
+ "esbuild": "^0.28.1",
73
+ "oxfmt": "^0.58.0",
74
+ "oxlint": "^1.73.0",
75
+ "oxlint-tsgolint": "^0.24.0",
76
+ "typescript": "~7.0.2",
77
+ "vitest": "^4.1.10"
52
78
  },
53
- "dependencies": {
54
- "cross-fetch": "^3.1.6",
55
- "pako": "^2.1.0"
56
- }
79
+ "engines": {
80
+ "node": ">=22"
81
+ },
82
+ "packageManager": "npm@11.11.0"
57
83
  }
@@ -1 +0,0 @@
1
- export {};