@remix-run/multipart-parser 0.12.0 → 0.14.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.
@@ -0,0 +1,51 @@
1
+ export function createSearch(pattern) {
2
+ let needle = new TextEncoder().encode(pattern);
3
+ let search;
4
+ if ('Buffer' in globalThis && !('Bun' in globalThis || 'Deno' in globalThis)) {
5
+ // Use the built-in Buffer.indexOf method on Node.js for better perf.
6
+ search = (haystack, start = 0) => Buffer.prototype.indexOf.call(haystack, needle, start);
7
+ }
8
+ else {
9
+ let needleEnd = needle.length - 1;
10
+ let skipTable = new Uint8Array(256).fill(needle.length);
11
+ for (let i = 0; i < needleEnd; ++i) {
12
+ skipTable[needle[i]] = needleEnd - i;
13
+ }
14
+ search = (haystack, start = 0) => {
15
+ let haystackLength = haystack.length;
16
+ let i = start + needleEnd;
17
+ while (i < haystackLength) {
18
+ for (let j = needleEnd, k = i; j >= 0 && haystack[k] === needle[j]; --j, --k) {
19
+ if (j === 0)
20
+ return k;
21
+ }
22
+ i += skipTable[haystack[i]];
23
+ }
24
+ return -1;
25
+ };
26
+ }
27
+ return search;
28
+ }
29
+ export function createPartialTailSearch(pattern) {
30
+ let needle = new TextEncoder().encode(pattern);
31
+ let byteIndexes = {};
32
+ for (let i = 0; i < needle.length; ++i) {
33
+ let byte = needle[i];
34
+ if (byteIndexes[byte] === undefined)
35
+ byteIndexes[byte] = [];
36
+ byteIndexes[byte].push(i);
37
+ }
38
+ return function (haystack) {
39
+ let haystackEnd = haystack.length - 1;
40
+ if (haystack[haystackEnd] in byteIndexes) {
41
+ let indexes = byteIndexes[haystack[haystackEnd]];
42
+ for (let i = indexes.length - 1; i >= 0; --i) {
43
+ for (let j = indexes[i], k = haystackEnd; j >= 0 && haystack[k] === needle[j]; --j, --k) {
44
+ if (j === 0)
45
+ return k;
46
+ }
47
+ }
48
+ }
49
+ return -1;
50
+ };
51
+ }
@@ -0,0 +1,46 @@
1
+ import { MultipartParseError, parseMultipartStream } from "./multipart.js";
2
+ /**
3
+ * Extracts the boundary string from a `multipart/*` content type.
4
+ *
5
+ * @param contentType The `Content-Type` header value from the request
6
+ * @return The boundary string if found, or null if not present
7
+ */
8
+ export function getMultipartBoundary(contentType) {
9
+ let match = /boundary=(?:"([^"]+)"|([^;]+))/i.exec(contentType);
10
+ return match ? (match[1] ?? match[2]) : null;
11
+ }
12
+ /**
13
+ * Returns true if the given request contains multipart data.
14
+ *
15
+ * @param request The `Request` object to check
16
+ * @return `true` if the request is a multipart request, `false` otherwise
17
+ */
18
+ export function isMultipartRequest(request) {
19
+ let contentType = request.headers.get('Content-Type');
20
+ return contentType != null && contentType.startsWith('multipart/');
21
+ }
22
+ /**
23
+ * Parse a multipart [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and yield each part as
24
+ * a `MultipartPart` object. Useful in HTTP server contexts for handling incoming `multipart/*` requests.
25
+ *
26
+ * @param request The `Request` object containing multipart data
27
+ * @param options Optional parser options, such as `maxHeaderSize` and `maxFileSize`
28
+ * @return An async generator yielding `MultipartPart` objects
29
+ */
30
+ export async function* parseMultipartRequest(request, options) {
31
+ if (!isMultipartRequest(request)) {
32
+ throw new MultipartParseError('Request is not a multipart request');
33
+ }
34
+ if (!request.body) {
35
+ throw new MultipartParseError('Request body is empty');
36
+ }
37
+ let boundary = getMultipartBoundary(request.headers.get('Content-Type'));
38
+ if (!boundary) {
39
+ throw new MultipartParseError('Invalid Content-Type header: missing boundary');
40
+ }
41
+ yield* parseMultipartStream(request.body, {
42
+ boundary,
43
+ maxHeaderSize: options?.maxHeaderSize,
44
+ maxFileSize: options?.maxFileSize,
45
+ });
46
+ }
@@ -3,20 +3,32 @@ import Headers from '@remix-run/headers';
3
3
  * The base class for errors thrown by the multipart parser.
4
4
  */
5
5
  export declare class MultipartParseError extends Error {
6
+ /**
7
+ * @param message The error message
8
+ */
6
9
  constructor(message: string);
7
10
  }
8
11
  /**
9
12
  * An error thrown when the maximum allowed size of a header is exceeded.
10
13
  */
11
14
  export declare class MaxHeaderSizeExceededError extends MultipartParseError {
15
+ /**
16
+ * @param maxHeaderSize The maximum header size that was exceeded
17
+ */
12
18
  constructor(maxHeaderSize: number);
13
19
  }
14
20
  /**
15
21
  * An error thrown when the maximum allowed size of a file is exceeded.
16
22
  */
17
23
  export declare class MaxFileSizeExceededError extends MultipartParseError {
24
+ /**
25
+ * @param maxFileSize The maximum file size that was exceeded
26
+ */
18
27
  constructor(maxFileSize: number);
19
28
  }
29
+ /**
30
+ * Options for parsing a multipart message.
31
+ */
20
32
  export interface ParseMultipartOptions {
21
33
  /**
22
34
  * The boundary string used to separate parts in the multipart message,
@@ -27,14 +39,14 @@ export interface ParseMultipartOptions {
27
39
  * The maximum allowed size of a header in bytes. If an individual part's header
28
40
  * exceeds this size, a `MaxHeaderSizeExceededError` will be thrown.
29
41
  *
30
- * Default: 8 KiB
42
+ * @default 8192 (8 KiB)
31
43
  */
32
44
  maxHeaderSize?: number;
33
45
  /**
34
46
  * The maximum allowed size of a file in bytes. If an individual part's content
35
47
  * exceeds this size, a `MaxFileSizeExceededError` will be thrown.
36
48
  *
37
- * Default: 2 MiB
49
+ * @default 2097152 (2 MiB)
38
50
  */
39
51
  maxFileSize?: number;
40
52
  }
@@ -60,6 +72,9 @@ export declare function parseMultipart(message: Uint8Array | Iterable<Uint8Array
60
72
  * @return An async generator that yields `MultipartPart` objects
61
73
  */
62
74
  export declare function parseMultipartStream(stream: ReadableStream<Uint8Array>, options: ParseMultipartOptions): AsyncGenerator<MultipartPart, void, unknown>;
75
+ /**
76
+ * Options for configuring a `MultipartParser`.
77
+ */
63
78
  export type MultipartParserOptions = Omit<ParseMultipartOptions, 'boundary'>;
64
79
  /**
65
80
  * A streaming parser for `multipart/*` HTTP messages.
@@ -69,6 +84,10 @@ export declare class MultipartParser {
69
84
  readonly boundary: string;
70
85
  readonly maxHeaderSize: number;
71
86
  readonly maxFileSize: number;
87
+ /**
88
+ * @param boundary The boundary string used to separate parts
89
+ * @param options Options for the parser
90
+ */
72
91
  constructor(boundary: string, options?: MultipartParserOptions);
73
92
  /**
74
93
  * Write a chunk of data to the parser.
@@ -96,6 +115,10 @@ export declare class MultipartPart {
96
115
  * The raw content of this part as an array of `Uint8Array` chunks.
97
116
  */
98
117
  readonly content: Uint8Array[];
118
+ /**
119
+ * @param header The raw header bytes
120
+ * @param content The content chunks
121
+ */
99
122
  constructor(header: Uint8Array, content: Uint8Array[]);
100
123
  /**
101
124
  * The content of this part as an `ArrayBuffer`.
@@ -1 +1 @@
1
- {"version":3,"file":"multipart.d.ts","sourceRoot":"","sources":["../../src/lib/multipart.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,oBAAoB,CAAA;AAMxC;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,mBAAmB;gBACrD,aAAa,EAAE,MAAM;CAIlC;AAED;;GAEG;AACH,qBAAa,wBAAyB,SAAQ,mBAAmB;gBACnD,WAAW,EAAE,MAAM;CAIhC;AAED,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAiB,cAAc,CAC7B,OAAO,EAAE,UAAU,GAAG,QAAQ,CAAC,UAAU,CAAC,EAC1C,OAAO,EAAE,qBAAqB,GAC7B,SAAS,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAmBzC;AAED;;;;;;;;;GASG;AACH,wBAAuB,oBAAoB,CACzC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAe9C;AAED,MAAM,MAAM,sBAAsB,GAAG,IAAI,CAAC,qBAAqB,EAAE,UAAU,CAAC,CAAA;AAa5E;;GAEG;AACH,qBAAa,eAAe;;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;gBAahB,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB;IAY9D;;;;;OAKG;IACF,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC;IA0HlE;;;;;;;OAOG;IACH,MAAM,IAAI,IAAI;CAKf;AAID;;GAEG;AACH,qBAAa,aAAa;;IACxB;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,CAAA;gBAKlB,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE;IAKrD;;OAEG;IACH,IAAI,WAAW,IAAI,WAAW,CAE7B;IAED;;;OAGG;IACH,IAAI,KAAK,IAAI,UAAU,CAUtB;IAED;;OAEG;IACH,IAAI,OAAO,IAAI,OAAO,CAMrB;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED;;OAEG;IACH,IAAI,QAAQ,IAAI,MAAM,GAAG,SAAS,CAEjC;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,MAAM,GAAG,SAAS,CAElC;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,GAAG,SAAS,CAE7B;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAQjB;IAED;;;;;OAKG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
1
+ {"version":3,"file":"multipart.d.ts","sourceRoot":"","sources":["../../src/lib/multipart.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,oBAAoB,CAAA;AAUxC;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C;;OAEG;gBACS,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,mBAAmB;IACjE;;OAEG;gBACS,aAAa,EAAE,MAAM;CAIlC;AAED;;GAEG;AACH,qBAAa,wBAAyB,SAAQ,mBAAmB;IAC/D;;OAEG;gBACS,WAAW,EAAE,MAAM;CAIhC;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAiB,cAAc,CAC7B,OAAO,EAAE,UAAU,GAAG,QAAQ,CAAC,UAAU,CAAC,EAC1C,OAAO,EAAE,qBAAqB,GAC7B,SAAS,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAmBzC;AAED;;;;;;;;;GASG;AACH,wBAAuB,oBAAoB,CACzC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAe9C;AAED;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,IAAI,CAAC,qBAAqB,EAAE,UAAU,CAAC,CAAA;AAa5E;;GAEG;AACH,qBAAa,eAAe;;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAa5B;;;OAGG;gBACS,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB;IAY9D;;;;;OAKG;IACF,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC;IA0HlE;;;;;;;OAOG;IACH,MAAM,IAAI,IAAI;CAKf;AAID;;GAEG;AACH,qBAAa,aAAa;;IACxB;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,CAAA;IAK9B;;;OAGG;gBACS,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE;IAKrD;;OAEG;IACH,IAAI,WAAW,IAAI,WAAW,CAE7B;IAED;;;OAGG;IACH,IAAI,KAAK,IAAI,UAAU,CAUtB;IAED;;OAEG;IACH,IAAI,OAAO,IAAI,OAAO,CAMrB;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED;;OAEG;IACH,IAAI,QAAQ,IAAI,MAAM,GAAG,SAAS,CAEjC;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,MAAM,GAAG,SAAS,CAElC;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,GAAG,SAAS,CAE7B;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAQjB;IAED;;;;;OAKG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
@@ -0,0 +1,337 @@
1
+ import Headers from '@remix-run/headers';
2
+ import { createSearch, createPartialTailSearch, } from "./buffer-search.js";
3
+ import { readStream } from "./read-stream.js";
4
+ /**
5
+ * The base class for errors thrown by the multipart parser.
6
+ */
7
+ export class MultipartParseError extends Error {
8
+ /**
9
+ * @param message The error message
10
+ */
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = 'MultipartParseError';
14
+ }
15
+ }
16
+ /**
17
+ * An error thrown when the maximum allowed size of a header is exceeded.
18
+ */
19
+ export class MaxHeaderSizeExceededError extends MultipartParseError {
20
+ /**
21
+ * @param maxHeaderSize The maximum header size that was exceeded
22
+ */
23
+ constructor(maxHeaderSize) {
24
+ super(`Multipart header size exceeds maximum allowed size of ${maxHeaderSize} bytes`);
25
+ this.name = 'MaxHeaderSizeExceededError';
26
+ }
27
+ }
28
+ /**
29
+ * An error thrown when the maximum allowed size of a file is exceeded.
30
+ */
31
+ export class MaxFileSizeExceededError extends MultipartParseError {
32
+ /**
33
+ * @param maxFileSize The maximum file size that was exceeded
34
+ */
35
+ constructor(maxFileSize) {
36
+ super(`File size exceeds maximum allowed size of ${maxFileSize} bytes`);
37
+ this.name = 'MaxFileSizeExceededError';
38
+ }
39
+ }
40
+ /**
41
+ * Parse a `multipart/*` message from a buffer/iterable and yield each part as a `MultipartPart` object.
42
+ *
43
+ * Note: This is a low-level API that requires manual handling of the content and boundary. If you're
44
+ * building a web server, consider using `parseMultipartRequest(request)` instead.
45
+ *
46
+ * @param message The multipart message as a `Uint8Array` or an iterable of `Uint8Array` chunks
47
+ * @param options Options for the parser
48
+ * @return A generator that yields `MultipartPart` objects
49
+ */
50
+ export function* parseMultipart(message, options) {
51
+ let parser = new MultipartParser(options.boundary, {
52
+ maxHeaderSize: options.maxHeaderSize,
53
+ maxFileSize: options.maxFileSize,
54
+ });
55
+ if (message instanceof Uint8Array) {
56
+ if (message.length === 0) {
57
+ return; // No data to parse
58
+ }
59
+ yield* parser.write(message);
60
+ }
61
+ else {
62
+ for (let chunk of message) {
63
+ yield* parser.write(chunk);
64
+ }
65
+ }
66
+ parser.finish();
67
+ }
68
+ /**
69
+ * Parse a `multipart/*` message stream and yield each part as a `MultipartPart` object.
70
+ *
71
+ * Note: This is a low-level API that requires manual handling of the content and boundary. If you're
72
+ * building a web server, consider using `parseMultipartRequest(request)` instead.
73
+ *
74
+ * @param stream A stream containing multipart data as a `ReadableStream<Uint8Array>`
75
+ * @param options Options for the parser
76
+ * @return An async generator that yields `MultipartPart` objects
77
+ */
78
+ export async function* parseMultipartStream(stream, options) {
79
+ let parser = new MultipartParser(options.boundary, {
80
+ maxHeaderSize: options.maxHeaderSize,
81
+ maxFileSize: options.maxFileSize,
82
+ });
83
+ for await (let chunk of readStream(stream)) {
84
+ if (chunk.length === 0) {
85
+ continue; // No data to parse
86
+ }
87
+ yield* parser.write(chunk);
88
+ }
89
+ parser.finish();
90
+ }
91
+ const MultipartParserStateStart = 0;
92
+ const MultipartParserStateAfterBoundary = 1;
93
+ const MultipartParserStateHeader = 2;
94
+ const MultipartParserStateBody = 3;
95
+ const MultipartParserStateDone = 4;
96
+ const findDoubleNewline = createSearch('\r\n\r\n');
97
+ const oneKb = 1024;
98
+ const oneMb = 1024 * oneKb;
99
+ /**
100
+ * A streaming parser for `multipart/*` HTTP messages.
101
+ */
102
+ export class MultipartParser {
103
+ boundary;
104
+ maxHeaderSize;
105
+ maxFileSize;
106
+ #findOpeningBoundary;
107
+ #openingBoundaryLength;
108
+ #findBoundary;
109
+ #findPartialTailBoundary;
110
+ #boundaryLength;
111
+ #state = MultipartParserStateStart;
112
+ #buffer = null;
113
+ #currentPart = null;
114
+ #contentLength = 0;
115
+ /**
116
+ * @param boundary The boundary string used to separate parts
117
+ * @param options Options for the parser
118
+ */
119
+ constructor(boundary, options) {
120
+ this.boundary = boundary;
121
+ this.maxHeaderSize = options?.maxHeaderSize ?? 8 * oneKb;
122
+ this.maxFileSize = options?.maxFileSize ?? 2 * oneMb;
123
+ this.#findOpeningBoundary = createSearch(`--${boundary}`);
124
+ this.#openingBoundaryLength = 2 + boundary.length; // length of '--' + boundary
125
+ this.#findBoundary = createSearch(`\r\n--${boundary}`);
126
+ this.#findPartialTailBoundary = createPartialTailSearch(`\r\n--${boundary}`);
127
+ this.#boundaryLength = 4 + boundary.length; // length of '\r\n--' + boundary
128
+ }
129
+ /**
130
+ * Write a chunk of data to the parser.
131
+ *
132
+ * @param chunk A chunk of data to write to the parser
133
+ * @return A generator yielding `MultipartPart` objects as they are parsed
134
+ */
135
+ *write(chunk) {
136
+ if (this.#state === MultipartParserStateDone) {
137
+ throw new MultipartParseError('Unexpected data after end of stream');
138
+ }
139
+ let index = 0;
140
+ let chunkLength = chunk.length;
141
+ if (this.#buffer !== null) {
142
+ let newChunk = new Uint8Array(this.#buffer.length + chunkLength);
143
+ newChunk.set(this.#buffer, 0);
144
+ newChunk.set(chunk, this.#buffer.length);
145
+ chunk = newChunk;
146
+ chunkLength = chunk.length;
147
+ this.#buffer = null;
148
+ }
149
+ while (true) {
150
+ if (this.#state === MultipartParserStateBody) {
151
+ if (chunkLength - index < this.#boundaryLength) {
152
+ this.#buffer = chunk.subarray(index);
153
+ break;
154
+ }
155
+ let boundaryIndex = this.#findBoundary(chunk, index);
156
+ if (boundaryIndex === -1) {
157
+ // No boundary found, but there may be a partial match at the end of the chunk.
158
+ let partialTailIndex = this.#findPartialTailBoundary(chunk);
159
+ if (partialTailIndex === -1) {
160
+ this.#append(index === 0 ? chunk : chunk.subarray(index));
161
+ }
162
+ else {
163
+ this.#append(chunk.subarray(index, partialTailIndex));
164
+ this.#buffer = chunk.subarray(partialTailIndex);
165
+ }
166
+ break;
167
+ }
168
+ this.#append(chunk.subarray(index, boundaryIndex));
169
+ yield this.#currentPart;
170
+ index = boundaryIndex + this.#boundaryLength;
171
+ this.#state = MultipartParserStateAfterBoundary;
172
+ }
173
+ if (this.#state === MultipartParserStateAfterBoundary) {
174
+ if (chunkLength - index < 2) {
175
+ this.#buffer = chunk.subarray(index);
176
+ break;
177
+ }
178
+ if (chunk[index] === 45 && chunk[index + 1] === 45) {
179
+ this.#state = MultipartParserStateDone;
180
+ break;
181
+ }
182
+ index += 2; // Skip \r\n after boundary
183
+ this.#state = MultipartParserStateHeader;
184
+ }
185
+ if (this.#state === MultipartParserStateHeader) {
186
+ if (chunkLength - index < 4) {
187
+ this.#buffer = chunk.subarray(index);
188
+ break;
189
+ }
190
+ let headerEndIndex = findDoubleNewline(chunk, index);
191
+ if (headerEndIndex === -1) {
192
+ if (chunkLength - index > this.maxHeaderSize) {
193
+ throw new MaxHeaderSizeExceededError(this.maxHeaderSize);
194
+ }
195
+ this.#buffer = chunk.subarray(index);
196
+ break;
197
+ }
198
+ if (headerEndIndex - index > this.maxHeaderSize) {
199
+ throw new MaxHeaderSizeExceededError(this.maxHeaderSize);
200
+ }
201
+ this.#currentPart = new MultipartPart(chunk.subarray(index, headerEndIndex), []);
202
+ this.#contentLength = 0;
203
+ index = headerEndIndex + 4; // Skip header + \r\n\r\n
204
+ this.#state = MultipartParserStateBody;
205
+ continue;
206
+ }
207
+ if (this.#state === MultipartParserStateStart) {
208
+ if (chunkLength < this.#openingBoundaryLength) {
209
+ this.#buffer = chunk;
210
+ break;
211
+ }
212
+ if (this.#findOpeningBoundary(chunk) !== 0) {
213
+ throw new MultipartParseError('Invalid multipart stream: missing initial boundary');
214
+ }
215
+ index = this.#openingBoundaryLength;
216
+ this.#state = MultipartParserStateAfterBoundary;
217
+ }
218
+ }
219
+ }
220
+ #append(chunk) {
221
+ if (this.#contentLength + chunk.length > this.maxFileSize) {
222
+ throw new MaxFileSizeExceededError(this.maxFileSize);
223
+ }
224
+ this.#currentPart.content.push(chunk);
225
+ this.#contentLength += chunk.length;
226
+ }
227
+ /**
228
+ * Should be called after all data has been written to the parser.
229
+ *
230
+ * Note: This will throw if the multipart message is incomplete or
231
+ * wasn't properly terminated.
232
+ *
233
+ * @return void
234
+ */
235
+ finish() {
236
+ if (this.#state !== MultipartParserStateDone) {
237
+ throw new MultipartParseError('Multipart stream not finished');
238
+ }
239
+ }
240
+ }
241
+ const decoder = new TextDecoder('utf-8', { fatal: true });
242
+ /**
243
+ * A part of a `multipart/*` HTTP message.
244
+ */
245
+ export class MultipartPart {
246
+ /**
247
+ * The raw content of this part as an array of `Uint8Array` chunks.
248
+ */
249
+ content;
250
+ #header;
251
+ #headers;
252
+ /**
253
+ * @param header The raw header bytes
254
+ * @param content The content chunks
255
+ */
256
+ constructor(header, content) {
257
+ this.#header = header;
258
+ this.content = content;
259
+ }
260
+ /**
261
+ * The content of this part as an `ArrayBuffer`.
262
+ */
263
+ get arrayBuffer() {
264
+ return this.bytes.buffer;
265
+ }
266
+ /**
267
+ * The content of this part as a single `Uint8Array`. In `multipart/form-data` messages, this is useful
268
+ * for reading the value of files that were uploaded using `<input type="file">` fields.
269
+ */
270
+ get bytes() {
271
+ let buffer = new Uint8Array(this.size);
272
+ let offset = 0;
273
+ for (let chunk of this.content) {
274
+ buffer.set(chunk, offset);
275
+ offset += chunk.length;
276
+ }
277
+ return buffer;
278
+ }
279
+ /**
280
+ * The headers associated with this part.
281
+ */
282
+ get headers() {
283
+ if (!this.#headers) {
284
+ this.#headers = new Headers(decoder.decode(this.#header));
285
+ }
286
+ return this.#headers;
287
+ }
288
+ /**
289
+ * True if this part originated from a file upload.
290
+ */
291
+ get isFile() {
292
+ return this.filename !== undefined || this.mediaType === 'application/octet-stream';
293
+ }
294
+ /**
295
+ * True if this part originated from a text input field in a form submission.
296
+ */
297
+ get isText() {
298
+ return !this.isFile;
299
+ }
300
+ /**
301
+ * The filename of the part, if it is a file upload.
302
+ */
303
+ get filename() {
304
+ return this.headers.contentDisposition.preferredFilename;
305
+ }
306
+ /**
307
+ * The media type of the part.
308
+ */
309
+ get mediaType() {
310
+ return this.headers.contentType.mediaType;
311
+ }
312
+ /**
313
+ * The name of the part, usually the `name` of the field in the `<form>` that submitted the request.
314
+ */
315
+ get name() {
316
+ return this.headers.contentDisposition.name;
317
+ }
318
+ /**
319
+ * The size of the content in bytes.
320
+ */
321
+ get size() {
322
+ let size = 0;
323
+ for (let chunk of this.content) {
324
+ size += chunk.length;
325
+ }
326
+ return size;
327
+ }
328
+ /**
329
+ * The content of this part as a string. In `multipart/form-data` messages, this is useful for
330
+ * reading the value of parts that originated from `<input type="text">` fields.
331
+ *
332
+ * Note: Do not use this for binary data, use `part.bytes` or `part.arrayBuffer` instead.
333
+ */
334
+ get text() {
335
+ return decoder.decode(this.bytes);
336
+ }
337
+ }
@@ -0,0 +1,60 @@
1
+ import { Readable } from 'node:stream';
2
+ import { MultipartParseError, parseMultipart as parseMultipartWeb, parseMultipartStream as parseMultipartStreamWeb, } from "./multipart.js";
3
+ import { getMultipartBoundary } from "./multipart-request.js";
4
+ /**
5
+ * Parse a `multipart/*` Node.js `Buffer` and yield each part as a `MultipartPart` object.
6
+ *
7
+ * Note: This is a low-level API that requires manual handling of the content and boundary. If you're
8
+ * building a web server, consider using `parseMultipartRequest(request)` instead.
9
+ *
10
+ * @param message The multipart message as a `Buffer` or an iterable of `Buffer` chunks
11
+ * @param options Options for the parser
12
+ * @return A generator yielding `MultipartPart` objects
13
+ */
14
+ export function* parseMultipart(message, options) {
15
+ yield* parseMultipartWeb(message, options);
16
+ }
17
+ /**
18
+ * Parse a `multipart/*` Node.js `Readable` stream and yield each part as a `MultipartPart` object.
19
+ *
20
+ * Note: This is a low-level API that requires manual handling of the stream and boundary. If you're
21
+ * building a web server, consider using `parseMultipartRequest(request)` instead.
22
+ *
23
+ * @param stream A Node.js `Readable` stream containing multipart data
24
+ * @param options Options for the parser
25
+ * @return An async generator yielding `MultipartPart` objects
26
+ */
27
+ export async function* parseMultipartStream(stream, options) {
28
+ yield* parseMultipartStreamWeb(Readable.toWeb(stream), options);
29
+ }
30
+ /**
31
+ * Returns true if the given request is a multipart request.
32
+ *
33
+ * @param req The Node.js `http.IncomingMessage` object to check
34
+ * @return `true` if the request is a multipart request, `false` otherwise
35
+ */
36
+ export function isMultipartRequest(req) {
37
+ let contentType = req.headers['content-type'];
38
+ return contentType != null && /^multipart\//i.test(contentType);
39
+ }
40
+ /**
41
+ * Parse a multipart Node.js request and yield each part as a `MultipartPart` object.
42
+ *
43
+ * @param req The Node.js `http.IncomingMessage` object containing multipart data
44
+ * @param options Options for the parser
45
+ * @return An async generator yielding `MultipartPart` objects
46
+ */
47
+ export async function* parseMultipartRequest(req, options) {
48
+ if (!isMultipartRequest(req)) {
49
+ throw new MultipartParseError('Request is not a multipart request');
50
+ }
51
+ let boundary = getMultipartBoundary(req.headers['content-type']);
52
+ if (!boundary) {
53
+ throw new MultipartParseError('Invalid Content-Type header: missing boundary');
54
+ }
55
+ yield* parseMultipartStream(req, {
56
+ boundary,
57
+ maxHeaderSize: options?.maxHeaderSize,
58
+ maxFileSize: options?.maxFileSize,
59
+ });
60
+ }
@@ -0,0 +1,16 @@
1
+ // We need this little helper for environments that do not support
2
+ // ReadableStream.prototype[Symbol.asyncIterator] yet. See #46
3
+ export async function* readStream(stream) {
4
+ let reader = stream.getReader();
5
+ try {
6
+ while (true) {
7
+ let result = await reader.read();
8
+ if (result.done)
9
+ break;
10
+ yield result.value;
11
+ }
12
+ }
13
+ finally {
14
+ reader.releaseLock();
15
+ }
16
+ }