@ubercode/multipart-stream 1.0.0 → 1.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.
- package/README.md +2 -6
- package/dist/index.cjs +229 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -23
- package/dist/index.d.ts +22 -23
- package/dist/index.js +229 -43
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -90,7 +90,7 @@ declare class MultipartAbortError extends Error {
|
|
|
90
90
|
constructor(reason?: unknown, options?: ErrorOptions);
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
93
|
-
* Thrown when the source stream ends without
|
|
93
|
+
* Thrown when the source stream ends without parser observing the closing
|
|
94
94
|
* multipart boundary (FR-022) — typically a mid-flight server hangup or
|
|
95
95
|
* transport-layer cut. Cleanup per FR-010 still runs.
|
|
96
96
|
*
|
|
@@ -285,7 +285,7 @@ declare function extractBoundary(contentTypeHeader: string | null | undefined):
|
|
|
285
285
|
interface StreamingMultipartPart {
|
|
286
286
|
/**
|
|
287
287
|
* Zero-based ordinal of this part within the multipart envelope, in the
|
|
288
|
-
* order
|
|
288
|
+
* order parser emits them.
|
|
289
289
|
*/
|
|
290
290
|
readonly index: number;
|
|
291
291
|
/**
|
|
@@ -295,14 +295,13 @@ interface StreamingMultipartPart {
|
|
|
295
295
|
readonly boundary: string;
|
|
296
296
|
/**
|
|
297
297
|
* Lowercased part headers as flat strings. Names are normalized to
|
|
298
|
-
* lowercase; values are
|
|
299
|
-
* `Buffer | Buffer[] | Buffer[][]` shapes.
|
|
298
|
+
* lowercase; repeated values are joined with `, `.
|
|
300
299
|
*
|
|
301
300
|
* Reads are `string | undefined` due to `noUncheckedIndexedAccess`.
|
|
302
301
|
*/
|
|
303
302
|
readonly headers: Readonly<Record<string, string | undefined>>;
|
|
304
303
|
/**
|
|
305
|
-
* Raw header block bytes
|
|
304
|
+
* Raw header block bytes, including the terminating CRLF pair.
|
|
306
305
|
*
|
|
307
306
|
* Surfaced for callers who need byte-exact framing for re-emission or
|
|
308
307
|
* signature verification.
|
|
@@ -319,15 +318,15 @@ interface StreamingMultipartPart {
|
|
|
319
318
|
readonly contentId?: string | undefined;
|
|
320
319
|
/**
|
|
321
320
|
* Pre-extracted `content-length` parsed via `parseInt(_, 10)`, or
|
|
322
|
-
* `undefined` if the header is absent or non-numeric. Note:
|
|
321
|
+
* `undefined` if the header is absent or non-numeric. Note: parser streams
|
|
323
322
|
* regardless — this value is informational, not a contract.
|
|
324
323
|
*/
|
|
325
324
|
readonly contentLength?: number | undefined;
|
|
326
325
|
/**
|
|
327
326
|
* The streaming body of this part as a Node `Readable`. Backed directly by
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
327
|
+
* parser's per-part stream — no intermediate buffering. The parser applies
|
|
328
|
+
* backpressure when this stream's buffer fills. Callers should consume
|
|
329
|
+
* (drain, pipe, or destroy) before requesting the next part.
|
|
331
330
|
*/
|
|
332
331
|
readonly body: Readable;
|
|
333
332
|
}
|
|
@@ -401,7 +400,7 @@ interface ProgressSnapshot {
|
|
|
401
400
|
* union, not a positional argument.
|
|
402
401
|
*
|
|
403
402
|
* Per NFR-DR-S-008, `meta` NEVER contains raw chunk bytes. When the library
|
|
404
|
-
* logs an `Error` whose source is
|
|
403
|
+
* logs an `Error` whose source is parser or the source stream, it passes only
|
|
405
404
|
* an `errSummary: { name, message }` object with `message` truncated to <=
|
|
406
405
|
* 120 chars, control characters redacted, and the value JSON-stringified
|
|
407
406
|
* per NFR-DR-S-006.
|
|
@@ -471,7 +470,7 @@ interface ParseMultipartOptions {
|
|
|
471
470
|
*/
|
|
472
471
|
maxParts?: number | undefined;
|
|
473
472
|
/**
|
|
474
|
-
* Maximum number of
|
|
473
|
+
* Maximum number of header lines per part, including repeated names (NFR-DR-S-004). Defaults to
|
|
475
474
|
* `100` when omitted.
|
|
476
475
|
*/
|
|
477
476
|
maxHeadersPerPart?: number | undefined;
|
|
@@ -531,10 +530,10 @@ interface MultipartHandlerOptions<T> {
|
|
|
531
530
|
* forwards the caller's `AbortSignal` to fetch directly so a network-
|
|
532
531
|
* time abort cancels the request before any bytes flow.
|
|
533
532
|
* 3. Validates the response Content-Type (FR-021) case-insensitively
|
|
534
|
-
* against `multipart/related` BEFORE constructing
|
|
533
|
+
* against `multipart/related` BEFORE constructing parser. The offending
|
|
535
534
|
* Content-Type is sanitized via `truncateForErrorEmbed` per
|
|
536
|
-
* NFR-DR-S-006. T-035 asserts the
|
|
537
|
-
*
|
|
535
|
+
* NFR-DR-S-006. T-035 asserts the parser-activity harness sees zero
|
|
536
|
+
* Parser instances on this path.
|
|
538
537
|
* 4. Captures `status` + `headers` BEFORE consuming the body
|
|
539
538
|
* (FR-DR-A-029 — the Response is gone after iteration).
|
|
540
539
|
* 5. Forwards `idleTimeoutMs` / `totalTimeoutMs` / `signal` / `onProgress`
|
|
@@ -586,11 +585,11 @@ interface MultipartHandlerOptions<T> {
|
|
|
586
585
|
* — T-013 spies on `globalThis.fetch` to verify.
|
|
587
586
|
* @throws {Error} `multipart: response Content-Type is not multipart/related;
|
|
588
587
|
* got <actual>` when the response Content-Type doesn't start with
|
|
589
|
-
* `multipart/related` (case-insensitive) (FR-021).
|
|
590
|
-
* constructed on this path — T-035 asserts via the
|
|
588
|
+
* `multipart/related` (case-insensitive) (FR-021). Parser is NEVER
|
|
589
|
+
* constructed on this path — T-035 asserts via the parser-activity
|
|
591
590
|
* harness.
|
|
592
591
|
* @throws Any error from `parseMultipartRelated` (idle/total timeout,
|
|
593
|
-
* abort mid-stream, truncation, source error,
|
|
592
|
+
* abort mid-stream, truncation, source error, parser error, cap overflow).
|
|
594
593
|
* @throws Any error from `options.parser`.
|
|
595
594
|
*
|
|
596
595
|
* @example
|
|
@@ -604,7 +603,7 @@ interface MultipartHandlerOptions<T> {
|
|
|
604
603
|
declare function fetchAndHandleMultipart<T>(url: string | URL, options: MultipartHandlerOptions<T>): Promise<MultipartFetchResult<T>>;
|
|
605
604
|
|
|
606
605
|
/**
|
|
607
|
-
* `parseMultipartRelated` —
|
|
606
|
+
* `parseMultipartRelated` — streaming parser adapter. FR-001.
|
|
608
607
|
*
|
|
609
608
|
* Resource-cap enforcement (NFR-DR-S-001/004/012):
|
|
610
609
|
* - `maxPartBytes` (NFR-DR-S-001): each per-part body Readable gets a
|
|
@@ -612,7 +611,7 @@ declare function fetchAndHandleMultipart<T>(url: string | URL, options: Multipar
|
|
|
612
611
|
* the listener pushes `MultipartPartTooLargeError` into the queue and
|
|
613
612
|
* destroys the offending part body. No default — when undefined, no
|
|
614
613
|
* cap is enforced.
|
|
615
|
-
* - `maxParts` (NFR-DR-S-012): the
|
|
614
|
+
* - `maxParts` (NFR-DR-S-012): the parser 'part' counter is checked at
|
|
616
615
|
* each emit; on overflow, push `MultipartTooManyPartsError`. Default
|
|
617
616
|
* `10_000` when undefined.
|
|
618
617
|
* - `maxHeadersPerPart` + `maxHeaderBytesPerPart` (NFR-DR-S-004): on
|
|
@@ -638,9 +637,9 @@ declare function fetchAndHandleMultipart<T>(url: string | URL, options: Multipar
|
|
|
638
637
|
*
|
|
639
638
|
* Cleanup contract: the `finally` cleanup drains unyielded part bodies
|
|
640
639
|
* (FR-010), removes 'data'/'error'/'end' listeners on source and 'part'/
|
|
641
|
-
* 'finish' on
|
|
640
|
+
* 'finish' on parser, unpipes + destroys source, and KEEPS parser's 'error'
|
|
642
641
|
* listener for late-emit observability (FR-011). Truncation detection
|
|
643
|
-
* (FR-022) fires when source 'end' arrives without
|
|
642
|
+
* (FR-022) fires when source 'end' arrives without parser 'finish' having
|
|
644
643
|
* fired.
|
|
645
644
|
*/
|
|
646
645
|
|
|
@@ -656,7 +655,7 @@ declare function fetchAndHandleMultipart<T>(url: string | URL, options: Multipar
|
|
|
656
655
|
* are validated synchronously via `validatePositiveTimeout` — both must be
|
|
657
656
|
* positive finite integers in `[1, 2_147_483_647]` (NFR-DR-S-009).
|
|
658
657
|
* @returns An `AsyncGenerator<StreamingMultipartPart, void, void>` that
|
|
659
|
-
* yields parts in
|
|
658
|
+
* yields parts in parser's emit order.
|
|
660
659
|
*
|
|
661
660
|
* @throws {TypeError} `multipart: idleTimeoutMs must be a positive finite
|
|
662
661
|
* integer in [1, 2_147_483_647]; …` when `idleTimeoutMs` is missing,
|
|
@@ -680,7 +679,7 @@ declare function fetchAndHandleMultipart<T>(url: string | URL, options: Multipar
|
|
|
680
679
|
* aborted at call time — first `.next()` rejects synchronously per FR-009).
|
|
681
680
|
* `error.reason` is the caller's `signal.reason` verbatim (F-S-006).
|
|
682
681
|
* @throws {MultipartTruncatedError} when the source emits `'end'` before
|
|
683
|
-
*
|
|
682
|
+
* parser emits `'finish'` (FR-022).
|
|
684
683
|
*
|
|
685
684
|
* @example
|
|
686
685
|
* for await (const part of parseMultipartRelated(res, {
|
package/dist/index.d.ts
CHANGED
|
@@ -90,7 +90,7 @@ declare class MultipartAbortError extends Error {
|
|
|
90
90
|
constructor(reason?: unknown, options?: ErrorOptions);
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
93
|
-
* Thrown when the source stream ends without
|
|
93
|
+
* Thrown when the source stream ends without parser observing the closing
|
|
94
94
|
* multipart boundary (FR-022) — typically a mid-flight server hangup or
|
|
95
95
|
* transport-layer cut. Cleanup per FR-010 still runs.
|
|
96
96
|
*
|
|
@@ -285,7 +285,7 @@ declare function extractBoundary(contentTypeHeader: string | null | undefined):
|
|
|
285
285
|
interface StreamingMultipartPart {
|
|
286
286
|
/**
|
|
287
287
|
* Zero-based ordinal of this part within the multipart envelope, in the
|
|
288
|
-
* order
|
|
288
|
+
* order parser emits them.
|
|
289
289
|
*/
|
|
290
290
|
readonly index: number;
|
|
291
291
|
/**
|
|
@@ -295,14 +295,13 @@ interface StreamingMultipartPart {
|
|
|
295
295
|
readonly boundary: string;
|
|
296
296
|
/**
|
|
297
297
|
* Lowercased part headers as flat strings. Names are normalized to
|
|
298
|
-
* lowercase; values are
|
|
299
|
-
* `Buffer | Buffer[] | Buffer[][]` shapes.
|
|
298
|
+
* lowercase; repeated values are joined with `, `.
|
|
300
299
|
*
|
|
301
300
|
* Reads are `string | undefined` due to `noUncheckedIndexedAccess`.
|
|
302
301
|
*/
|
|
303
302
|
readonly headers: Readonly<Record<string, string | undefined>>;
|
|
304
303
|
/**
|
|
305
|
-
* Raw header block bytes
|
|
304
|
+
* Raw header block bytes, including the terminating CRLF pair.
|
|
306
305
|
*
|
|
307
306
|
* Surfaced for callers who need byte-exact framing for re-emission or
|
|
308
307
|
* signature verification.
|
|
@@ -319,15 +318,15 @@ interface StreamingMultipartPart {
|
|
|
319
318
|
readonly contentId?: string | undefined;
|
|
320
319
|
/**
|
|
321
320
|
* Pre-extracted `content-length` parsed via `parseInt(_, 10)`, or
|
|
322
|
-
* `undefined` if the header is absent or non-numeric. Note:
|
|
321
|
+
* `undefined` if the header is absent or non-numeric. Note: parser streams
|
|
323
322
|
* regardless — this value is informational, not a contract.
|
|
324
323
|
*/
|
|
325
324
|
readonly contentLength?: number | undefined;
|
|
326
325
|
/**
|
|
327
326
|
* The streaming body of this part as a Node `Readable`. Backed directly by
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
327
|
+
* parser's per-part stream — no intermediate buffering. The parser applies
|
|
328
|
+
* backpressure when this stream's buffer fills. Callers should consume
|
|
329
|
+
* (drain, pipe, or destroy) before requesting the next part.
|
|
331
330
|
*/
|
|
332
331
|
readonly body: Readable;
|
|
333
332
|
}
|
|
@@ -401,7 +400,7 @@ interface ProgressSnapshot {
|
|
|
401
400
|
* union, not a positional argument.
|
|
402
401
|
*
|
|
403
402
|
* Per NFR-DR-S-008, `meta` NEVER contains raw chunk bytes. When the library
|
|
404
|
-
* logs an `Error` whose source is
|
|
403
|
+
* logs an `Error` whose source is parser or the source stream, it passes only
|
|
405
404
|
* an `errSummary: { name, message }` object with `message` truncated to <=
|
|
406
405
|
* 120 chars, control characters redacted, and the value JSON-stringified
|
|
407
406
|
* per NFR-DR-S-006.
|
|
@@ -471,7 +470,7 @@ interface ParseMultipartOptions {
|
|
|
471
470
|
*/
|
|
472
471
|
maxParts?: number | undefined;
|
|
473
472
|
/**
|
|
474
|
-
* Maximum number of
|
|
473
|
+
* Maximum number of header lines per part, including repeated names (NFR-DR-S-004). Defaults to
|
|
475
474
|
* `100` when omitted.
|
|
476
475
|
*/
|
|
477
476
|
maxHeadersPerPart?: number | undefined;
|
|
@@ -531,10 +530,10 @@ interface MultipartHandlerOptions<T> {
|
|
|
531
530
|
* forwards the caller's `AbortSignal` to fetch directly so a network-
|
|
532
531
|
* time abort cancels the request before any bytes flow.
|
|
533
532
|
* 3. Validates the response Content-Type (FR-021) case-insensitively
|
|
534
|
-
* against `multipart/related` BEFORE constructing
|
|
533
|
+
* against `multipart/related` BEFORE constructing parser. The offending
|
|
535
534
|
* Content-Type is sanitized via `truncateForErrorEmbed` per
|
|
536
|
-
* NFR-DR-S-006. T-035 asserts the
|
|
537
|
-
*
|
|
535
|
+
* NFR-DR-S-006. T-035 asserts the parser-activity harness sees zero
|
|
536
|
+
* Parser instances on this path.
|
|
538
537
|
* 4. Captures `status` + `headers` BEFORE consuming the body
|
|
539
538
|
* (FR-DR-A-029 — the Response is gone after iteration).
|
|
540
539
|
* 5. Forwards `idleTimeoutMs` / `totalTimeoutMs` / `signal` / `onProgress`
|
|
@@ -586,11 +585,11 @@ interface MultipartHandlerOptions<T> {
|
|
|
586
585
|
* — T-013 spies on `globalThis.fetch` to verify.
|
|
587
586
|
* @throws {Error} `multipart: response Content-Type is not multipart/related;
|
|
588
587
|
* got <actual>` when the response Content-Type doesn't start with
|
|
589
|
-
* `multipart/related` (case-insensitive) (FR-021).
|
|
590
|
-
* constructed on this path — T-035 asserts via the
|
|
588
|
+
* `multipart/related` (case-insensitive) (FR-021). Parser is NEVER
|
|
589
|
+
* constructed on this path — T-035 asserts via the parser-activity
|
|
591
590
|
* harness.
|
|
592
591
|
* @throws Any error from `parseMultipartRelated` (idle/total timeout,
|
|
593
|
-
* abort mid-stream, truncation, source error,
|
|
592
|
+
* abort mid-stream, truncation, source error, parser error, cap overflow).
|
|
594
593
|
* @throws Any error from `options.parser`.
|
|
595
594
|
*
|
|
596
595
|
* @example
|
|
@@ -604,7 +603,7 @@ interface MultipartHandlerOptions<T> {
|
|
|
604
603
|
declare function fetchAndHandleMultipart<T>(url: string | URL, options: MultipartHandlerOptions<T>): Promise<MultipartFetchResult<T>>;
|
|
605
604
|
|
|
606
605
|
/**
|
|
607
|
-
* `parseMultipartRelated` —
|
|
606
|
+
* `parseMultipartRelated` — streaming parser adapter. FR-001.
|
|
608
607
|
*
|
|
609
608
|
* Resource-cap enforcement (NFR-DR-S-001/004/012):
|
|
610
609
|
* - `maxPartBytes` (NFR-DR-S-001): each per-part body Readable gets a
|
|
@@ -612,7 +611,7 @@ declare function fetchAndHandleMultipart<T>(url: string | URL, options: Multipar
|
|
|
612
611
|
* the listener pushes `MultipartPartTooLargeError` into the queue and
|
|
613
612
|
* destroys the offending part body. No default — when undefined, no
|
|
614
613
|
* cap is enforced.
|
|
615
|
-
* - `maxParts` (NFR-DR-S-012): the
|
|
614
|
+
* - `maxParts` (NFR-DR-S-012): the parser 'part' counter is checked at
|
|
616
615
|
* each emit; on overflow, push `MultipartTooManyPartsError`. Default
|
|
617
616
|
* `10_000` when undefined.
|
|
618
617
|
* - `maxHeadersPerPart` + `maxHeaderBytesPerPart` (NFR-DR-S-004): on
|
|
@@ -638,9 +637,9 @@ declare function fetchAndHandleMultipart<T>(url: string | URL, options: Multipar
|
|
|
638
637
|
*
|
|
639
638
|
* Cleanup contract: the `finally` cleanup drains unyielded part bodies
|
|
640
639
|
* (FR-010), removes 'data'/'error'/'end' listeners on source and 'part'/
|
|
641
|
-
* 'finish' on
|
|
640
|
+
* 'finish' on parser, unpipes + destroys source, and KEEPS parser's 'error'
|
|
642
641
|
* listener for late-emit observability (FR-011). Truncation detection
|
|
643
|
-
* (FR-022) fires when source 'end' arrives without
|
|
642
|
+
* (FR-022) fires when source 'end' arrives without parser 'finish' having
|
|
644
643
|
* fired.
|
|
645
644
|
*/
|
|
646
645
|
|
|
@@ -656,7 +655,7 @@ declare function fetchAndHandleMultipart<T>(url: string | URL, options: Multipar
|
|
|
656
655
|
* are validated synchronously via `validatePositiveTimeout` — both must be
|
|
657
656
|
* positive finite integers in `[1, 2_147_483_647]` (NFR-DR-S-009).
|
|
658
657
|
* @returns An `AsyncGenerator<StreamingMultipartPart, void, void>` that
|
|
659
|
-
* yields parts in
|
|
658
|
+
* yields parts in parser's emit order.
|
|
660
659
|
*
|
|
661
660
|
* @throws {TypeError} `multipart: idleTimeoutMs must be a positive finite
|
|
662
661
|
* integer in [1, 2_147_483_647]; …` when `idleTimeoutMs` is missing,
|
|
@@ -680,7 +679,7 @@ declare function fetchAndHandleMultipart<T>(url: string | URL, options: Multipar
|
|
|
680
679
|
* aborted at call time — first `.next()` rejects synchronously per FR-009).
|
|
681
680
|
* `error.reason` is the caller's `signal.reason` verbatim (F-S-006).
|
|
682
681
|
* @throws {MultipartTruncatedError} when the source emits `'end'` before
|
|
683
|
-
*
|
|
682
|
+
* parser emits `'finish'` (FR-022).
|
|
684
683
|
*
|
|
685
684
|
* @example
|
|
686
685
|
* for await (const part of parseMultipartRelated(res, {
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Readable, PassThrough } from 'stream';
|
|
2
|
-
import
|
|
1
|
+
import { Readable, Writable, PassThrough } from 'stream';
|
|
2
|
+
import StreamSearch from 'streamsearch';
|
|
3
3
|
|
|
4
4
|
// src/errors.ts
|
|
5
5
|
var MultipartIdleTimeoutError = class extends Error {
|
|
@@ -293,7 +293,7 @@ function flattenHeaderValue(v) {
|
|
|
293
293
|
}
|
|
294
294
|
return "";
|
|
295
295
|
}
|
|
296
|
-
function
|
|
296
|
+
function flattenPartHeaders(raw) {
|
|
297
297
|
if (raw == null) return {};
|
|
298
298
|
const out = {};
|
|
299
299
|
for (const key of Object.keys(raw)) {
|
|
@@ -303,6 +303,213 @@ function flattenDicerHeaders(raw) {
|
|
|
303
303
|
}
|
|
304
304
|
return out;
|
|
305
305
|
}
|
|
306
|
+
var HEADER_END = Buffer.from("\r\n\r\n");
|
|
307
|
+
var INITIAL_CRLF = Buffer.from("\r\n");
|
|
308
|
+
var CLOSING_DASHES = Buffer.from("--");
|
|
309
|
+
var CLOSING_CR = Buffer.from("--\r");
|
|
310
|
+
var MAX_HEADER_BYTES = 80 * 1024;
|
|
311
|
+
var MAX_HEADER_PAIRS = 2e3;
|
|
312
|
+
var MultipartPartStream = class extends Readable {
|
|
313
|
+
constructor(onRead) {
|
|
314
|
+
super();
|
|
315
|
+
this.onRead = onRead;
|
|
316
|
+
}
|
|
317
|
+
onRead;
|
|
318
|
+
_read() {
|
|
319
|
+
this.onRead();
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
function parseHeaders(block) {
|
|
323
|
+
const headers = /* @__PURE__ */ Object.create(null);
|
|
324
|
+
if (block.length === 0) return headers;
|
|
325
|
+
const lines = block.toString("latin1").split("\r\n");
|
|
326
|
+
let previousName;
|
|
327
|
+
let count = 0;
|
|
328
|
+
for (const line of lines) {
|
|
329
|
+
if (line.startsWith(" ") || line.startsWith(" ")) {
|
|
330
|
+
if (previousName === void 0) {
|
|
331
|
+
throw new Error("Unexpected folded header value");
|
|
332
|
+
}
|
|
333
|
+
const values = headers[previousName];
|
|
334
|
+
if (values === void 0) throw new Error("Malformed part header");
|
|
335
|
+
values[values.length - 1] += line;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const colon = line.indexOf(":");
|
|
339
|
+
if (colon <= 0) throw new Error("Malformed part header");
|
|
340
|
+
const name = line.slice(0, colon);
|
|
341
|
+
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) {
|
|
342
|
+
throw new Error("Malformed part header");
|
|
343
|
+
}
|
|
344
|
+
const value = line.slice(colon + 1).replace(/^[ \t]/, "");
|
|
345
|
+
if (/[\x00-\x08\x0a-\x1f\x7f]/.test(value)) {
|
|
346
|
+
throw new Error("Malformed part header");
|
|
347
|
+
}
|
|
348
|
+
const key = name.toLowerCase();
|
|
349
|
+
(headers[key] ??= []).push(value);
|
|
350
|
+
previousName = key;
|
|
351
|
+
if (++count > MAX_HEADER_PAIRS) throw new Error("Too many part headers");
|
|
352
|
+
}
|
|
353
|
+
return headers;
|
|
354
|
+
}
|
|
355
|
+
var MultipartParser = class extends Writable {
|
|
356
|
+
needle;
|
|
357
|
+
search;
|
|
358
|
+
state = "preamble";
|
|
359
|
+
part;
|
|
360
|
+
headerBytes = Buffer.alloc(0);
|
|
361
|
+
suffix = Buffer.alloc(0);
|
|
362
|
+
candidate = false;
|
|
363
|
+
pausedPart;
|
|
364
|
+
pendingWrite;
|
|
365
|
+
openParts = 0;
|
|
366
|
+
finalCallback;
|
|
367
|
+
constructor(boundary) {
|
|
368
|
+
super();
|
|
369
|
+
if (boundary.length === 0) throw new TypeError("Boundary required");
|
|
370
|
+
this.needle = Buffer.from(`\r
|
|
371
|
+
--${boundary}`);
|
|
372
|
+
this.search = new StreamSearch(this.needle, (matched, data, start, end, safe) => {
|
|
373
|
+
if (this.state !== "done" && data && start < end) {
|
|
374
|
+
const chunk = data.subarray(start, end);
|
|
375
|
+
this.consume(safe ? chunk : Buffer.from(chunk));
|
|
376
|
+
}
|
|
377
|
+
if (this.state === "done") return;
|
|
378
|
+
if (matched) {
|
|
379
|
+
if (this.state === "headers") throw new Error("Malformed part header");
|
|
380
|
+
this.candidate = true;
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
this.search.push(INITIAL_CRLF);
|
|
384
|
+
}
|
|
385
|
+
_write(chunk, _encoding, callback) {
|
|
386
|
+
try {
|
|
387
|
+
this.search.push(chunk);
|
|
388
|
+
if (this.pausedPart) this.pendingWrite = callback;
|
|
389
|
+
else callback();
|
|
390
|
+
} catch (error) {
|
|
391
|
+
callback(error instanceof Error ? error : new Error(String(error)));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
_final(callback) {
|
|
395
|
+
try {
|
|
396
|
+
this.search.destroy();
|
|
397
|
+
} catch (error) {
|
|
398
|
+
callback(error instanceof Error ? error : new Error(String(error)));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (this.candidate && this.suffix.equals(CLOSING_DASHES)) {
|
|
402
|
+
this.candidate = false;
|
|
403
|
+
this.endPart();
|
|
404
|
+
this.state = "done";
|
|
405
|
+
}
|
|
406
|
+
if (this.state !== "done") {
|
|
407
|
+
const error = new Error("Unexpected end of multipart data");
|
|
408
|
+
this.endPart();
|
|
409
|
+
callback(error);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (this.openParts === 0) callback();
|
|
413
|
+
else this.finalCallback = callback;
|
|
414
|
+
}
|
|
415
|
+
resumeWrite(part) {
|
|
416
|
+
if (this.pausedPart !== part) return;
|
|
417
|
+
this.pausedPart = void 0;
|
|
418
|
+
const callback = this.pendingWrite;
|
|
419
|
+
this.pendingWrite = void 0;
|
|
420
|
+
callback?.();
|
|
421
|
+
}
|
|
422
|
+
endPart() {
|
|
423
|
+
const part = this.part;
|
|
424
|
+
if (part === void 0) return;
|
|
425
|
+
this.part = void 0;
|
|
426
|
+
part.push(null);
|
|
427
|
+
}
|
|
428
|
+
beginPart() {
|
|
429
|
+
this.headerBytes = Buffer.alloc(0);
|
|
430
|
+
this.state = "headers";
|
|
431
|
+
const part = new MultipartPartStream(() => this.resumeWrite(part));
|
|
432
|
+
this.part = part;
|
|
433
|
+
this.openParts++;
|
|
434
|
+
let settled = false;
|
|
435
|
+
const onPartDone = () => {
|
|
436
|
+
if (settled) return;
|
|
437
|
+
settled = true;
|
|
438
|
+
this.openParts--;
|
|
439
|
+
this.resumeWrite(part);
|
|
440
|
+
if (this.openParts === 0 && this.finalCallback) {
|
|
441
|
+
const callback = this.finalCallback;
|
|
442
|
+
this.finalCallback = void 0;
|
|
443
|
+
callback();
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
part.once("end", onPartDone);
|
|
447
|
+
part.once("close", onPartDone);
|
|
448
|
+
this.emit("part", part);
|
|
449
|
+
}
|
|
450
|
+
consume(data) {
|
|
451
|
+
if (this.candidate) {
|
|
452
|
+
if (this.suffix.length < 2) {
|
|
453
|
+
const taken = Math.min(2 - this.suffix.length, data.length);
|
|
454
|
+
this.suffix = Buffer.concat([this.suffix, data.subarray(0, taken)]);
|
|
455
|
+
data = data.subarray(taken);
|
|
456
|
+
}
|
|
457
|
+
if (this.suffix.length < 2) return;
|
|
458
|
+
if (this.suffix.equals(CLOSING_DASHES)) {
|
|
459
|
+
if (data.length === 0) return;
|
|
460
|
+
if (data[0] === 13) {
|
|
461
|
+
this.suffix = CLOSING_CR;
|
|
462
|
+
data = data.subarray(1);
|
|
463
|
+
if (data.length === 0) return;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (this.suffix.equals(CLOSING_CR) && data.length === 0) return;
|
|
467
|
+
const suffix = this.suffix;
|
|
468
|
+
this.suffix = Buffer.alloc(0);
|
|
469
|
+
this.candidate = false;
|
|
470
|
+
if (suffix.equals(INITIAL_CRLF)) {
|
|
471
|
+
this.endPart();
|
|
472
|
+
this.beginPart();
|
|
473
|
+
} else if (suffix.equals(CLOSING_CR) && data[0] === 10) {
|
|
474
|
+
data = data.subarray(1);
|
|
475
|
+
this.endPart();
|
|
476
|
+
this.state = "done";
|
|
477
|
+
return;
|
|
478
|
+
} else if (this.state === "headers") {
|
|
479
|
+
data = Buffer.concat([this.needle, suffix, data]);
|
|
480
|
+
} else if (this.state === "body") {
|
|
481
|
+
this.consumeBody(this.needle);
|
|
482
|
+
this.consumeBody(suffix);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (this.state === "headers") {
|
|
486
|
+
const pending = this.headerBytes.length;
|
|
487
|
+
const scan = data.subarray(0, MAX_HEADER_BYTES + HEADER_END.length - pending);
|
|
488
|
+
const combined = Buffer.concat([this.headerBytes, scan]);
|
|
489
|
+
const empty = combined.length >= 2 && combined.subarray(0, 2).equals(INITIAL_CRLF);
|
|
490
|
+
const end = empty ? 0 : combined.indexOf(HEADER_END);
|
|
491
|
+
if (end < 0) {
|
|
492
|
+
if (combined.length > MAX_HEADER_BYTES) throw new Error("Part headers too large");
|
|
493
|
+
this.headerBytes = combined;
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (end > MAX_HEADER_BYTES) throw new Error("Part headers too large");
|
|
497
|
+
const headers = parseHeaders(combined.subarray(0, end));
|
|
498
|
+
const rawHeaders = Buffer.from(combined.subarray(0, end + (empty ? 2 : HEADER_END.length)));
|
|
499
|
+
this.headerBytes = Buffer.alloc(0);
|
|
500
|
+
this.state = "body";
|
|
501
|
+
this.part?.emit("header", headers, rawHeaders);
|
|
502
|
+
this.consumeBody(data.subarray(end + (empty ? 2 : HEADER_END.length) - pending));
|
|
503
|
+
} else if (this.state === "body") {
|
|
504
|
+
this.consumeBody(data);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
consumeBody(data) {
|
|
508
|
+
if (data.length > 0 && this.part && !this.part.destroyed && !this.part.push(data)) {
|
|
509
|
+
this.pausedPart = this.part;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
};
|
|
306
513
|
function looksLikeResponse(input) {
|
|
307
514
|
if (typeof Response !== "undefined" && input instanceof Response) {
|
|
308
515
|
return true;
|
|
@@ -464,19 +671,6 @@ function setupTimers(opts, startMs) {
|
|
|
464
671
|
}
|
|
465
672
|
|
|
466
673
|
// src/parse-multipart-related.ts
|
|
467
|
-
function measureHeaderValueBytes(value) {
|
|
468
|
-
if (typeof value === "string") return Buffer.byteLength(value);
|
|
469
|
-
if (Array.isArray(value)) {
|
|
470
|
-
let total = 0;
|
|
471
|
-
for (const inner of value) {
|
|
472
|
-
total += measureHeaderValueBytes(inner);
|
|
473
|
-
}
|
|
474
|
-
return total;
|
|
475
|
-
}
|
|
476
|
-
if (Buffer.isBuffer(value)) return value.length;
|
|
477
|
-
return 0;
|
|
478
|
-
}
|
|
479
|
-
var Dicer = dicerMod.default ?? dicerMod;
|
|
480
674
|
function parseMultipartRelated(input, opts) {
|
|
481
675
|
return parseMultipartRelatedImpl(input, opts);
|
|
482
676
|
}
|
|
@@ -491,11 +685,11 @@ async function* parseMultipartRelatedImpl(input, opts) {
|
|
|
491
685
|
boundary: opts.boundary
|
|
492
686
|
});
|
|
493
687
|
const logger = opts.logger ?? defaultLogger;
|
|
494
|
-
const
|
|
688
|
+
const parser = new MultipartParser(boundary);
|
|
495
689
|
const queue = createQueueNotifier();
|
|
496
690
|
let bytesReceived = 0;
|
|
497
691
|
let nextPartIndex = 0;
|
|
498
|
-
let
|
|
692
|
+
let parserFinished = false;
|
|
499
693
|
let cleaned = false;
|
|
500
694
|
let abortPushed = false;
|
|
501
695
|
const allPartStreams = /* @__PURE__ */ new Set();
|
|
@@ -553,21 +747,13 @@ async function* parseMultipartRelatedImpl(input, opts) {
|
|
|
553
747
|
const headersAccumulator = {
|
|
554
748
|
value: {}
|
|
555
749
|
};
|
|
556
|
-
const onHeader = (raw) => {
|
|
750
|
+
const onHeader = (raw, rawHeaderBlock) => {
|
|
557
751
|
const bag = raw;
|
|
558
752
|
let headerCount = 0;
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
for (const name of Object.keys(bag)) {
|
|
562
|
-
const value = bag[name];
|
|
563
|
-
const nameBytes = Buffer.byteLength(name);
|
|
564
|
-
const values = Array.isArray(value) ? value : [value];
|
|
565
|
-
for (const inner of values) {
|
|
566
|
-
headerCount += 1;
|
|
567
|
-
headerBytes += nameBytes + 4 + measureHeaderValueBytes(inner);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
753
|
+
if (bag !== void 0) {
|
|
754
|
+
for (const values of Object.values(bag)) headerCount += values.length;
|
|
570
755
|
}
|
|
756
|
+
const headerBytes = rawHeaderBlock.length;
|
|
571
757
|
if (headerCount > maxHeadersPerPart) {
|
|
572
758
|
if (!partStream.destroyed) partStream.destroy();
|
|
573
759
|
queue.signalError(
|
|
@@ -592,7 +778,7 @@ async function* parseMultipartRelatedImpl(input, opts) {
|
|
|
592
778
|
);
|
|
593
779
|
return;
|
|
594
780
|
}
|
|
595
|
-
headersAccumulator.value =
|
|
781
|
+
headersAccumulator.value = flattenPartHeaders(
|
|
596
782
|
raw
|
|
597
783
|
);
|
|
598
784
|
const headers = headersAccumulator.value;
|
|
@@ -644,12 +830,12 @@ async function* parseMultipartRelatedImpl(input, opts) {
|
|
|
644
830
|
index: partIndex,
|
|
645
831
|
boundary,
|
|
646
832
|
headers,
|
|
647
|
-
rawHeaders:
|
|
833
|
+
rawHeaders: rawHeaderBlock,
|
|
648
834
|
contentType,
|
|
649
835
|
...contentId !== void 0 ? { contentId } : {},
|
|
650
836
|
...contentLength !== void 0 ? { contentLength } : {},
|
|
651
837
|
// When maxPartBytes is configured, body is the PassThrough that
|
|
652
|
-
// wraps
|
|
838
|
+
// wraps parser's per-part Readable; otherwise body is parser's
|
|
653
839
|
// per-part Readable directly. Both expose Node `Readable`.
|
|
654
840
|
body: publicBody
|
|
655
841
|
};
|
|
@@ -670,10 +856,10 @@ async function* parseMultipartRelatedImpl(input, opts) {
|
|
|
670
856
|
});
|
|
671
857
|
};
|
|
672
858
|
const onFinish = () => {
|
|
673
|
-
|
|
859
|
+
parserFinished = true;
|
|
674
860
|
queue.signalEnd();
|
|
675
861
|
};
|
|
676
|
-
const
|
|
862
|
+
const onParserError = (err) => {
|
|
677
863
|
if (cleaned) {
|
|
678
864
|
logger({
|
|
679
865
|
level: "warn",
|
|
@@ -693,18 +879,18 @@ async function* parseMultipartRelatedImpl(input, opts) {
|
|
|
693
879
|
};
|
|
694
880
|
const onSourceEnd = () => {
|
|
695
881
|
setImmediate(() => {
|
|
696
|
-
if (
|
|
882
|
+
if (parserFinished) return;
|
|
697
883
|
if (cleaned) return;
|
|
698
884
|
queue.signalError(new MultipartTruncatedError(bytesReceived));
|
|
699
885
|
});
|
|
700
886
|
};
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
887
|
+
parser.on("part", onPart);
|
|
888
|
+
parser.on("finish", onFinish);
|
|
889
|
+
parser.on("error", onParserError);
|
|
704
890
|
source.on("data", onSourceData);
|
|
705
891
|
source.on("error", onSourceError);
|
|
706
892
|
source.on("end", onSourceEnd);
|
|
707
|
-
source.pipe(
|
|
893
|
+
source.pipe(parser);
|
|
708
894
|
const cleanup = () => {
|
|
709
895
|
if (cleaned) return;
|
|
710
896
|
cleaned = true;
|
|
@@ -712,7 +898,7 @@ async function* parseMultipartRelatedImpl(input, opts) {
|
|
|
712
898
|
source.off("error", onSourceError);
|
|
713
899
|
source.off("end", onSourceEnd);
|
|
714
900
|
try {
|
|
715
|
-
source.unpipe(
|
|
901
|
+
source.unpipe(parser);
|
|
716
902
|
} catch (err) {
|
|
717
903
|
logger({
|
|
718
904
|
level: "warn",
|
|
@@ -730,8 +916,8 @@ async function* parseMultipartRelatedImpl(input, opts) {
|
|
|
730
916
|
if (!partStream.destroyed) partStream.destroy();
|
|
731
917
|
}
|
|
732
918
|
allPartStreams.clear();
|
|
733
|
-
|
|
734
|
-
|
|
919
|
+
parser.off("part", onPart);
|
|
920
|
+
parser.off("finish", onFinish);
|
|
735
921
|
timers.signal.removeEventListener("abort", onCombinedAbort);
|
|
736
922
|
timers.cleanup();
|
|
737
923
|
};
|