@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 CHANGED
@@ -14,8 +14,7 @@ npm install @ubercode/multipart-stream
14
14
  yarn add @ubercode/multipart-stream
15
15
  ```
16
16
 
17
- Requires Node `>= 20.18.0`. Single runtime dependency: `dicer@0.3.1` (pinned
18
- exact).
17
+ Requires Node `>= 20.18.0`. The sole runtime dependency is `streamsearch@1.1.0`; multipart headers and part streams are parsed inside this package.
19
18
 
20
19
  ## Quickstart
21
20
 
@@ -101,10 +100,7 @@ for await (const part of parseMultipartRelated(req as unknown as Readable, {
101
100
  }
102
101
  ```
103
102
 
104
- The library never pauses dicer's internal state machine on your behalf
105
- your parser must drain or destroy each `part.body` before requesting the
106
- next part. If you don't, the iterator's `finally` destroys leftover bodies
107
- for you (FR-010), but that costs latency.
103
+ Drain or destroy each `part.body` before requesting the next part. The parser applies backpressure while a body is waiting for a reader; the iterator destroys leftover bodies when it closes.
108
104
 
109
105
  ## API
110
106
 
package/dist/index.cjs CHANGED
@@ -1,11 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  var stream = require('stream');
4
- var dicerMod = require('dicer');
4
+ var StreamSearch = require('streamsearch');
5
5
 
6
6
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
7
 
8
- var dicerMod__default = /*#__PURE__*/_interopDefault(dicerMod);
8
+ var StreamSearch__default = /*#__PURE__*/_interopDefault(StreamSearch);
9
9
 
10
10
  // src/errors.ts
11
11
  var MultipartIdleTimeoutError = class extends Error {
@@ -299,7 +299,7 @@ function flattenHeaderValue(v) {
299
299
  }
300
300
  return "";
301
301
  }
302
- function flattenDicerHeaders(raw) {
302
+ function flattenPartHeaders(raw) {
303
303
  if (raw == null) return {};
304
304
  const out = {};
305
305
  for (const key of Object.keys(raw)) {
@@ -309,6 +309,213 @@ function flattenDicerHeaders(raw) {
309
309
  }
310
310
  return out;
311
311
  }
312
+ var HEADER_END = Buffer.from("\r\n\r\n");
313
+ var INITIAL_CRLF = Buffer.from("\r\n");
314
+ var CLOSING_DASHES = Buffer.from("--");
315
+ var CLOSING_CR = Buffer.from("--\r");
316
+ var MAX_HEADER_BYTES = 80 * 1024;
317
+ var MAX_HEADER_PAIRS = 2e3;
318
+ var MultipartPartStream = class extends stream.Readable {
319
+ constructor(onRead) {
320
+ super();
321
+ this.onRead = onRead;
322
+ }
323
+ onRead;
324
+ _read() {
325
+ this.onRead();
326
+ }
327
+ };
328
+ function parseHeaders(block) {
329
+ const headers = /* @__PURE__ */ Object.create(null);
330
+ if (block.length === 0) return headers;
331
+ const lines = block.toString("latin1").split("\r\n");
332
+ let previousName;
333
+ let count = 0;
334
+ for (const line of lines) {
335
+ if (line.startsWith(" ") || line.startsWith(" ")) {
336
+ if (previousName === void 0) {
337
+ throw new Error("Unexpected folded header value");
338
+ }
339
+ const values = headers[previousName];
340
+ if (values === void 0) throw new Error("Malformed part header");
341
+ values[values.length - 1] += line;
342
+ continue;
343
+ }
344
+ const colon = line.indexOf(":");
345
+ if (colon <= 0) throw new Error("Malformed part header");
346
+ const name = line.slice(0, colon);
347
+ if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) {
348
+ throw new Error("Malformed part header");
349
+ }
350
+ const value = line.slice(colon + 1).replace(/^[ \t]/, "");
351
+ if (/[\x00-\x08\x0a-\x1f\x7f]/.test(value)) {
352
+ throw new Error("Malformed part header");
353
+ }
354
+ const key = name.toLowerCase();
355
+ (headers[key] ??= []).push(value);
356
+ previousName = key;
357
+ if (++count > MAX_HEADER_PAIRS) throw new Error("Too many part headers");
358
+ }
359
+ return headers;
360
+ }
361
+ var MultipartParser = class extends stream.Writable {
362
+ needle;
363
+ search;
364
+ state = "preamble";
365
+ part;
366
+ headerBytes = Buffer.alloc(0);
367
+ suffix = Buffer.alloc(0);
368
+ candidate = false;
369
+ pausedPart;
370
+ pendingWrite;
371
+ openParts = 0;
372
+ finalCallback;
373
+ constructor(boundary) {
374
+ super();
375
+ if (boundary.length === 0) throw new TypeError("Boundary required");
376
+ this.needle = Buffer.from(`\r
377
+ --${boundary}`);
378
+ this.search = new StreamSearch__default.default(this.needle, (matched, data, start, end, safe) => {
379
+ if (this.state !== "done" && data && start < end) {
380
+ const chunk = data.subarray(start, end);
381
+ this.consume(safe ? chunk : Buffer.from(chunk));
382
+ }
383
+ if (this.state === "done") return;
384
+ if (matched) {
385
+ if (this.state === "headers") throw new Error("Malformed part header");
386
+ this.candidate = true;
387
+ }
388
+ });
389
+ this.search.push(INITIAL_CRLF);
390
+ }
391
+ _write(chunk, _encoding, callback) {
392
+ try {
393
+ this.search.push(chunk);
394
+ if (this.pausedPart) this.pendingWrite = callback;
395
+ else callback();
396
+ } catch (error) {
397
+ callback(error instanceof Error ? error : new Error(String(error)));
398
+ }
399
+ }
400
+ _final(callback) {
401
+ try {
402
+ this.search.destroy();
403
+ } catch (error) {
404
+ callback(error instanceof Error ? error : new Error(String(error)));
405
+ return;
406
+ }
407
+ if (this.candidate && this.suffix.equals(CLOSING_DASHES)) {
408
+ this.candidate = false;
409
+ this.endPart();
410
+ this.state = "done";
411
+ }
412
+ if (this.state !== "done") {
413
+ const error = new Error("Unexpected end of multipart data");
414
+ this.endPart();
415
+ callback(error);
416
+ return;
417
+ }
418
+ if (this.openParts === 0) callback();
419
+ else this.finalCallback = callback;
420
+ }
421
+ resumeWrite(part) {
422
+ if (this.pausedPart !== part) return;
423
+ this.pausedPart = void 0;
424
+ const callback = this.pendingWrite;
425
+ this.pendingWrite = void 0;
426
+ callback?.();
427
+ }
428
+ endPart() {
429
+ const part = this.part;
430
+ if (part === void 0) return;
431
+ this.part = void 0;
432
+ part.push(null);
433
+ }
434
+ beginPart() {
435
+ this.headerBytes = Buffer.alloc(0);
436
+ this.state = "headers";
437
+ const part = new MultipartPartStream(() => this.resumeWrite(part));
438
+ this.part = part;
439
+ this.openParts++;
440
+ let settled = false;
441
+ const onPartDone = () => {
442
+ if (settled) return;
443
+ settled = true;
444
+ this.openParts--;
445
+ this.resumeWrite(part);
446
+ if (this.openParts === 0 && this.finalCallback) {
447
+ const callback = this.finalCallback;
448
+ this.finalCallback = void 0;
449
+ callback();
450
+ }
451
+ };
452
+ part.once("end", onPartDone);
453
+ part.once("close", onPartDone);
454
+ this.emit("part", part);
455
+ }
456
+ consume(data) {
457
+ if (this.candidate) {
458
+ if (this.suffix.length < 2) {
459
+ const taken = Math.min(2 - this.suffix.length, data.length);
460
+ this.suffix = Buffer.concat([this.suffix, data.subarray(0, taken)]);
461
+ data = data.subarray(taken);
462
+ }
463
+ if (this.suffix.length < 2) return;
464
+ if (this.suffix.equals(CLOSING_DASHES)) {
465
+ if (data.length === 0) return;
466
+ if (data[0] === 13) {
467
+ this.suffix = CLOSING_CR;
468
+ data = data.subarray(1);
469
+ if (data.length === 0) return;
470
+ }
471
+ }
472
+ if (this.suffix.equals(CLOSING_CR) && data.length === 0) return;
473
+ const suffix = this.suffix;
474
+ this.suffix = Buffer.alloc(0);
475
+ this.candidate = false;
476
+ if (suffix.equals(INITIAL_CRLF)) {
477
+ this.endPart();
478
+ this.beginPart();
479
+ } else if (suffix.equals(CLOSING_CR) && data[0] === 10) {
480
+ data = data.subarray(1);
481
+ this.endPart();
482
+ this.state = "done";
483
+ return;
484
+ } else if (this.state === "headers") {
485
+ data = Buffer.concat([this.needle, suffix, data]);
486
+ } else if (this.state === "body") {
487
+ this.consumeBody(this.needle);
488
+ this.consumeBody(suffix);
489
+ }
490
+ }
491
+ if (this.state === "headers") {
492
+ const pending = this.headerBytes.length;
493
+ const scan = data.subarray(0, MAX_HEADER_BYTES + HEADER_END.length - pending);
494
+ const combined = Buffer.concat([this.headerBytes, scan]);
495
+ const empty = combined.length >= 2 && combined.subarray(0, 2).equals(INITIAL_CRLF);
496
+ const end = empty ? 0 : combined.indexOf(HEADER_END);
497
+ if (end < 0) {
498
+ if (combined.length > MAX_HEADER_BYTES) throw new Error("Part headers too large");
499
+ this.headerBytes = combined;
500
+ return;
501
+ }
502
+ if (end > MAX_HEADER_BYTES) throw new Error("Part headers too large");
503
+ const headers = parseHeaders(combined.subarray(0, end));
504
+ const rawHeaders = Buffer.from(combined.subarray(0, end + (empty ? 2 : HEADER_END.length)));
505
+ this.headerBytes = Buffer.alloc(0);
506
+ this.state = "body";
507
+ this.part?.emit("header", headers, rawHeaders);
508
+ this.consumeBody(data.subarray(end + (empty ? 2 : HEADER_END.length) - pending));
509
+ } else if (this.state === "body") {
510
+ this.consumeBody(data);
511
+ }
512
+ }
513
+ consumeBody(data) {
514
+ if (data.length > 0 && this.part && !this.part.destroyed && !this.part.push(data)) {
515
+ this.pausedPart = this.part;
516
+ }
517
+ }
518
+ };
312
519
  function looksLikeResponse(input) {
313
520
  if (typeof Response !== "undefined" && input instanceof Response) {
314
521
  return true;
@@ -470,19 +677,6 @@ function setupTimers(opts, startMs) {
470
677
  }
471
678
 
472
679
  // src/parse-multipart-related.ts
473
- function measureHeaderValueBytes(value) {
474
- if (typeof value === "string") return Buffer.byteLength(value);
475
- if (Array.isArray(value)) {
476
- let total = 0;
477
- for (const inner of value) {
478
- total += measureHeaderValueBytes(inner);
479
- }
480
- return total;
481
- }
482
- if (Buffer.isBuffer(value)) return value.length;
483
- return 0;
484
- }
485
- var Dicer = dicerMod__default.default.default ?? dicerMod__default.default;
486
680
  function parseMultipartRelated(input, opts) {
487
681
  return parseMultipartRelatedImpl(input, opts);
488
682
  }
@@ -497,11 +691,11 @@ async function* parseMultipartRelatedImpl(input, opts) {
497
691
  boundary: opts.boundary
498
692
  });
499
693
  const logger = opts.logger ?? defaultLogger;
500
- const dicer = new Dicer({ boundary });
694
+ const parser = new MultipartParser(boundary);
501
695
  const queue = createQueueNotifier();
502
696
  let bytesReceived = 0;
503
697
  let nextPartIndex = 0;
504
- let dicerFinished = false;
698
+ let parserFinished = false;
505
699
  let cleaned = false;
506
700
  let abortPushed = false;
507
701
  const allPartStreams = /* @__PURE__ */ new Set();
@@ -559,21 +753,13 @@ async function* parseMultipartRelatedImpl(input, opts) {
559
753
  const headersAccumulator = {
560
754
  value: {}
561
755
  };
562
- const onHeader = (raw) => {
756
+ const onHeader = (raw, rawHeaderBlock) => {
563
757
  const bag = raw;
564
758
  let headerCount = 0;
565
- let headerBytes = 0;
566
- if (bag != null) {
567
- for (const name of Object.keys(bag)) {
568
- const value = bag[name];
569
- const nameBytes = Buffer.byteLength(name);
570
- const values = Array.isArray(value) ? value : [value];
571
- for (const inner of values) {
572
- headerCount += 1;
573
- headerBytes += nameBytes + 4 + measureHeaderValueBytes(inner);
574
- }
575
- }
759
+ if (bag !== void 0) {
760
+ for (const values of Object.values(bag)) headerCount += values.length;
576
761
  }
762
+ const headerBytes = rawHeaderBlock.length;
577
763
  if (headerCount > maxHeadersPerPart) {
578
764
  if (!partStream.destroyed) partStream.destroy();
579
765
  queue.signalError(
@@ -598,7 +784,7 @@ async function* parseMultipartRelatedImpl(input, opts) {
598
784
  );
599
785
  return;
600
786
  }
601
- headersAccumulator.value = flattenDicerHeaders(
787
+ headersAccumulator.value = flattenPartHeaders(
602
788
  raw
603
789
  );
604
790
  const headers = headersAccumulator.value;
@@ -650,12 +836,12 @@ async function* parseMultipartRelatedImpl(input, opts) {
650
836
  index: partIndex,
651
837
  boundary,
652
838
  headers,
653
- rawHeaders: Buffer.alloc(0),
839
+ rawHeaders: rawHeaderBlock,
654
840
  contentType,
655
841
  ...contentId !== void 0 ? { contentId } : {},
656
842
  ...contentLength !== void 0 ? { contentLength } : {},
657
843
  // When maxPartBytes is configured, body is the PassThrough that
658
- // wraps dicer's per-part Readable; otherwise body is dicer's
844
+ // wraps parser's per-part Readable; otherwise body is parser's
659
845
  // per-part Readable directly. Both expose Node `Readable`.
660
846
  body: publicBody
661
847
  };
@@ -676,10 +862,10 @@ async function* parseMultipartRelatedImpl(input, opts) {
676
862
  });
677
863
  };
678
864
  const onFinish = () => {
679
- dicerFinished = true;
865
+ parserFinished = true;
680
866
  queue.signalEnd();
681
867
  };
682
- const onDicerError = (err) => {
868
+ const onParserError = (err) => {
683
869
  if (cleaned) {
684
870
  logger({
685
871
  level: "warn",
@@ -699,18 +885,18 @@ async function* parseMultipartRelatedImpl(input, opts) {
699
885
  };
700
886
  const onSourceEnd = () => {
701
887
  setImmediate(() => {
702
- if (dicerFinished) return;
888
+ if (parserFinished) return;
703
889
  if (cleaned) return;
704
890
  queue.signalError(new MultipartTruncatedError(bytesReceived));
705
891
  });
706
892
  };
707
- dicer.on("part", onPart);
708
- dicer.on("finish", onFinish);
709
- dicer.on("error", onDicerError);
893
+ parser.on("part", onPart);
894
+ parser.on("finish", onFinish);
895
+ parser.on("error", onParserError);
710
896
  source.on("data", onSourceData);
711
897
  source.on("error", onSourceError);
712
898
  source.on("end", onSourceEnd);
713
- source.pipe(dicer);
899
+ source.pipe(parser);
714
900
  const cleanup = () => {
715
901
  if (cleaned) return;
716
902
  cleaned = true;
@@ -718,7 +904,7 @@ async function* parseMultipartRelatedImpl(input, opts) {
718
904
  source.off("error", onSourceError);
719
905
  source.off("end", onSourceEnd);
720
906
  try {
721
- source.unpipe(dicer);
907
+ source.unpipe(parser);
722
908
  } catch (err) {
723
909
  logger({
724
910
  level: "warn",
@@ -736,8 +922,8 @@ async function* parseMultipartRelatedImpl(input, opts) {
736
922
  if (!partStream.destroyed) partStream.destroy();
737
923
  }
738
924
  allPartStreams.clear();
739
- dicer.off("part", onPart);
740
- dicer.off("finish", onFinish);
925
+ parser.off("part", onPart);
926
+ parser.off("finish", onFinish);
741
927
  timers.signal.removeEventListener("abort", onCombinedAbort);
742
928
  timers.cleanup();
743
929
  };