@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/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 dicer observing the closing
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 dicer emits them.
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 the result of internal flattening over dicer's
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 captured from dicer (concatenated).
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: dicer streams
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
- * dicer's per-part stream — no intermediate buffering. Callers MUST consume
329
- * (drain, pipe, or destroy) before requesting the next part; the library
330
- * cannot pause dicer's state machine for them.
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 dicer or the source stream, it passes only
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 distinct headers per part (NFR-DR-S-004). Defaults to
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 dicer. The offending
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 dicer-activity harness sees zero
537
- * Dicer instances on this path.
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). Dicer is NEVER
590
- * constructed on this path — T-035 asserts via the dicer-activity
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, dicer error, cap overflow).
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` — Layer A (parser/dicer adapter). FR-001.
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 dicer 'part' counter is checked at
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 dicer, unpipes + destroys source, and KEEPS dicer's 'error'
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 dicer 'finish' having
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 dicer's emit order.
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
- * dicer emits `'finish'` (FR-022).
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 dicer observing the closing
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 dicer emits them.
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 the result of internal flattening over dicer's
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 captured from dicer (concatenated).
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: dicer streams
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
- * dicer's per-part stream — no intermediate buffering. Callers MUST consume
329
- * (drain, pipe, or destroy) before requesting the next part; the library
330
- * cannot pause dicer's state machine for them.
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 dicer or the source stream, it passes only
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 distinct headers per part (NFR-DR-S-004). Defaults to
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 dicer. The offending
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 dicer-activity harness sees zero
537
- * Dicer instances on this path.
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). Dicer is NEVER
590
- * constructed on this path — T-035 asserts via the dicer-activity
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, dicer error, cap overflow).
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` — Layer A (parser/dicer adapter). FR-001.
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 dicer 'part' counter is checked at
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 dicer, unpipes + destroys source, and KEEPS dicer's 'error'
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 dicer 'finish' having
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 dicer's emit order.
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
- * dicer emits `'finish'` (FR-022).
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 dicerMod from 'dicer';
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 flattenDicerHeaders(raw) {
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 dicer = new Dicer({ boundary });
688
+ const parser = new MultipartParser(boundary);
495
689
  const queue = createQueueNotifier();
496
690
  let bytesReceived = 0;
497
691
  let nextPartIndex = 0;
498
- let dicerFinished = false;
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
- let headerBytes = 0;
560
- if (bag != null) {
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 = flattenDicerHeaders(
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: Buffer.alloc(0),
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 dicer's per-part Readable; otherwise body is dicer's
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
- dicerFinished = true;
859
+ parserFinished = true;
674
860
  queue.signalEnd();
675
861
  };
676
- const onDicerError = (err) => {
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 (dicerFinished) return;
882
+ if (parserFinished) return;
697
883
  if (cleaned) return;
698
884
  queue.signalError(new MultipartTruncatedError(bytesReceived));
699
885
  });
700
886
  };
701
- dicer.on("part", onPart);
702
- dicer.on("finish", onFinish);
703
- dicer.on("error", onDicerError);
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(dicer);
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(dicer);
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
- dicer.off("part", onPart);
734
- dicer.off("finish", onFinish);
919
+ parser.off("part", onPart);
920
+ parser.off("finish", onFinish);
735
921
  timers.signal.removeEventListener("abort", onCombinedAbort);
736
922
  timers.cleanup();
737
923
  };