| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | 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); |
| | }, |
| | }); |
| | } |
| |
|