| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const decoder = new TextDecoder();
|
| const encoder = new TextEncoder();
|
| const blankLine = encoder.encode('\r\n');
|
|
|
| const STATE_BOUNDARY = 0;
|
| const STATE_HEADERS = 1;
|
| const STATE_BODY = 2;
|
|
|
| |
| |
| |
| |
| |
|
|
| function compareArrays(a: Uint8Array, b: Uint8Array): boolean {
|
| if (a.length != b.length) {
|
| return false;
|
| }
|
| for (let i = 0; i < a.length; i++) {
|
| if (a[i] != b[i]) {
|
| return false;
|
| }
|
| }
|
| return true;
|
| }
|
|
|
| |
| |
| |
| |
|
|
| function getBoundary(contentType: string): Uint8Array | null {
|
|
|
|
|
| const MULTIPART_TYPE = 'multipart/';
|
| const BOUNDARY_PARAM = '; boundary=';
|
| if (!contentType.startsWith(MULTIPART_TYPE)) {
|
| return null;
|
| }
|
| const i = contentType.indexOf(BOUNDARY_PARAM, MULTIPART_TYPE.length);
|
| if (i == -1) {
|
| return null;
|
| }
|
| const suffix = contentType.substring(i + BOUNDARY_PARAM.length);
|
| return encoder.encode('--' + suffix + '\r\n');
|
| }
|
|
|
| |
| |
| |
| |
| |
| |
|
|
| export default function multipartStream(
|
| contentType: string,
|
| body: ReadableStream,
|
| ): ReadableStream {
|
| const reader = body.getReader();
|
| return new ReadableStream({
|
| async start(controller) {
|
|
|
| const boundary = getBoundary(contentType);
|
| if (boundary === null) {
|
| controller.error(
|
| new Error(
|
| 'Invalid content type for multipart stream: ' + contentType,
|
| ),
|
| );
|
| return;
|
| }
|
| let pos = 0;
|
| let buf = new Uint8Array();
|
| let state = STATE_BOUNDARY;
|
| let headers: Headers | null = null;
|
| let contentLength: number | null = null;
|
|
|
| |
| |
| |
|
|
| function processBuf() {
|
|
|
|
|
| while (true) {
|
| if (boundary === null) {
|
| controller.error(
|
| new Error(
|
| 'Invalid content type for multipart stream: ' + contentType,
|
| ),
|
| );
|
| return;
|
| }
|
| switch (state) {
|
| case STATE_BOUNDARY:
|
|
|
| while (
|
| buf.length >= pos + blankLine.length &&
|
| compareArrays(buf.slice(pos, pos + blankLine.length), blankLine)
|
| ) {
|
| pos += blankLine.length;
|
| }
|
|
|
|
|
| if (buf.length < pos + boundary.length) {
|
| return;
|
| }
|
|
|
| if (
|
| !compareArrays(buf.slice(pos, pos + boundary.length), boundary)
|
| ) {
|
| throw new Error('bad part boundary');
|
| }
|
| pos += boundary.length;
|
| state = STATE_HEADERS;
|
| headers = new Headers();
|
| break;
|
|
|
| case STATE_HEADERS: {
|
| const cr = buf.indexOf('\r'.charCodeAt(0), pos);
|
| if (cr == -1 || buf.length == cr + 1) {
|
| return;
|
| }
|
| if (buf[cr + 1] != '\n'.charCodeAt(0)) {
|
| throw new Error('bad part header line (CR without NL)');
|
| }
|
| const line = decoder.decode(buf.slice(pos, cr));
|
| pos = cr + 2;
|
| if (line == '') {
|
| const rawContentLength = headers?.get('Content-Length');
|
| if (rawContentLength == null) {
|
| throw new Error('missing/invalid part Content-Length');
|
| }
|
| contentLength = parseInt(rawContentLength, 10);
|
| if (isNaN(contentLength)) {
|
| throw new Error('missing/invalid part Content-Length');
|
| }
|
| state = STATE_BODY;
|
| break;
|
| }
|
| const colon = line.indexOf(':');
|
| const name = line.substring(0, colon);
|
| if (colon == line.length || line[colon + 1] != ' ') {
|
| throw new Error('bad part header line (no ": ")');
|
| }
|
| const value = line.substring(colon + 2);
|
| headers?.append(name, value);
|
| break;
|
| }
|
| case STATE_BODY: {
|
| if (contentLength === null) {
|
| throw new Error('content length not set');
|
| }
|
| if (buf.length < pos + contentLength) {
|
| return;
|
| }
|
| const body = buf.slice(pos, pos + contentLength);
|
| pos += contentLength;
|
| controller.enqueue({
|
| headers: headers,
|
| body: body,
|
| });
|
| headers = null;
|
| contentLength = null;
|
| state = STATE_BOUNDARY;
|
| break;
|
| }
|
| }
|
| }
|
| }
|
|
|
|
|
|
|
| while (true) {
|
| const {done, value} = await reader.read();
|
| const buffered = buf.length - pos;
|
| if (done) {
|
| if (state != STATE_BOUNDARY || buffered > 0) {
|
| throw Error('multipart stream ended mid-part');
|
| }
|
| controller.close();
|
| return;
|
| }
|
|
|
|
|
| if (buffered == 0) {
|
| buf = value;
|
| } else {
|
| const newLen = buffered + value.length;
|
| const newBuf = new Uint8Array(newLen);
|
| newBuf.set(buf.slice(pos), 0);
|
| newBuf.set(value, buffered);
|
| buf = newBuf;
|
| }
|
| pos = 0;
|
|
|
| processBuf();
|
| }
|
| },
|
| cancel(reason) {
|
| return body.cancel(reason);
|
| },
|
| });
|
| }
|
|
|