@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.
- package/LICENSE +1 -1
- package/README.md +8 -8
- package/dist/index.js +2 -1985
- package/dist/lib/buffer-search.js +51 -0
- package/dist/lib/multipart-request.js +46 -0
- package/dist/lib/multipart.d.ts +25 -2
- package/dist/lib/multipart.d.ts.map +1 -1
- package/dist/lib/multipart.js +337 -0
- package/dist/lib/multipart.node.js +60 -0
- package/dist/lib/read-stream.js +16 -0
- package/dist/node.js +4 -1991
- package/package.json +6 -9
- package/src/lib/multipart.ts +31 -4
- package/dist/index.js.map +0 -7
- package/dist/node.js.map +0 -7
|
@@ -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
|
+
}
|
package/dist/lib/multipart.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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;
|
|
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
|
+
}
|