Spaces:
Sleeping
Sleeping
import {File} from 'fetch-blob/from.js'; | |
import {FormData} from 'formdata-polyfill/esm.min.js'; | |
let s = 0; | |
const S = { | |
START_BOUNDARY: s++, | |
HEADER_FIELD_START: s++, | |
HEADER_FIELD: s++, | |
HEADER_VALUE_START: s++, | |
HEADER_VALUE: s++, | |
HEADER_VALUE_ALMOST_DONE: s++, | |
HEADERS_ALMOST_DONE: s++, | |
PART_DATA_START: s++, | |
PART_DATA: s++, | |
END: s++ | |
}; | |
let f = 1; | |
const F = { | |
PART_BOUNDARY: f, | |
LAST_BOUNDARY: f *= 2 | |
}; | |
const LF = 10; | |
const CR = 13; | |
const SPACE = 32; | |
const HYPHEN = 45; | |
const COLON = 58; | |
const A = 97; | |
const Z = 122; | |
const lower = c => c | 0x20; | |
const noop = () => {}; | |
class MultipartParser { | |
/** | |
* @param {string} boundary | |
*/ | |
constructor(boundary) { | |
this.index = 0; | |
this.flags = 0; | |
this.onHeaderEnd = noop; | |
this.onHeaderField = noop; | |
this.onHeadersEnd = noop; | |
this.onHeaderValue = noop; | |
this.onPartBegin = noop; | |
this.onPartData = noop; | |
this.onPartEnd = noop; | |
this.boundaryChars = {}; | |
boundary = '\r\n--' + boundary; | |
const ui8a = new Uint8Array(boundary.length); | |
for (let i = 0; i < boundary.length; i++) { | |
ui8a[i] = boundary.charCodeAt(i); | |
this.boundaryChars[ui8a[i]] = true; | |
} | |
this.boundary = ui8a; | |
this.lookbehind = new Uint8Array(this.boundary.length + 8); | |
this.state = S.START_BOUNDARY; | |
} | |
/** | |
* @param {Uint8Array} data | |
*/ | |
write(data) { | |
let i = 0; | |
const length_ = data.length; | |
let previousIndex = this.index; | |
let {lookbehind, boundary, boundaryChars, index, state, flags} = this; | |
const boundaryLength = this.boundary.length; | |
const boundaryEnd = boundaryLength - 1; | |
const bufferLength = data.length; | |
let c; | |
let cl; | |
const mark = name => { | |
this[name + 'Mark'] = i; | |
}; | |
const clear = name => { | |
delete this[name + 'Mark']; | |
}; | |
const callback = (callbackSymbol, start, end, ui8a) => { | |
if (start === undefined || start !== end) { | |
this[callbackSymbol](ui8a && ui8a.subarray(start, end)); | |
} | |
}; | |
const dataCallback = (name, clear) => { | |
const markSymbol = name + 'Mark'; | |
if (!(markSymbol in this)) { | |
return; | |
} | |
if (clear) { | |
callback(name, this[markSymbol], i, data); | |
delete this[markSymbol]; | |
} else { | |
callback(name, this[markSymbol], data.length, data); | |
this[markSymbol] = 0; | |
} | |
}; | |
for (i = 0; i < length_; i++) { | |
c = data[i]; | |
switch (state) { | |
case S.START_BOUNDARY: | |
if (index === boundary.length - 2) { | |
if (c === HYPHEN) { | |
flags |= F.LAST_BOUNDARY; | |
} else if (c !== CR) { | |
return; | |
} | |
index++; | |
break; | |
} else if (index - 1 === boundary.length - 2) { | |
if (flags & F.LAST_BOUNDARY && c === HYPHEN) { | |
state = S.END; | |
flags = 0; | |
} else if (!(flags & F.LAST_BOUNDARY) && c === LF) { | |
index = 0; | |
callback('onPartBegin'); | |
state = S.HEADER_FIELD_START; | |
} else { | |
return; | |
} | |
break; | |
} | |
if (c !== boundary[index + 2]) { | |
index = -2; | |
} | |
if (c === boundary[index + 2]) { | |
index++; | |
} | |
break; | |
case S.HEADER_FIELD_START: | |
state = S.HEADER_FIELD; | |
mark('onHeaderField'); | |
index = 0; | |
// falls through | |
case S.HEADER_FIELD: | |
if (c === CR) { | |
clear('onHeaderField'); | |
state = S.HEADERS_ALMOST_DONE; | |
break; | |
} | |
index++; | |
if (c === HYPHEN) { | |
break; | |
} | |
if (c === COLON) { | |
if (index === 1) { | |
// empty header field | |
return; | |
} | |
dataCallback('onHeaderField', true); | |
state = S.HEADER_VALUE_START; | |
break; | |
} | |
cl = lower(c); | |
if (cl < A || cl > Z) { | |
return; | |
} | |
break; | |
case S.HEADER_VALUE_START: | |
if (c === SPACE) { | |
break; | |
} | |
mark('onHeaderValue'); | |
state = S.HEADER_VALUE; | |
// falls through | |
case S.HEADER_VALUE: | |
if (c === CR) { | |
dataCallback('onHeaderValue', true); | |
callback('onHeaderEnd'); | |
state = S.HEADER_VALUE_ALMOST_DONE; | |
} | |
break; | |
case S.HEADER_VALUE_ALMOST_DONE: | |
if (c !== LF) { | |
return; | |
} | |
state = S.HEADER_FIELD_START; | |
break; | |
case S.HEADERS_ALMOST_DONE: | |
if (c !== LF) { | |
return; | |
} | |
callback('onHeadersEnd'); | |
state = S.PART_DATA_START; | |
break; | |
case S.PART_DATA_START: | |
state = S.PART_DATA; | |
mark('onPartData'); | |
// falls through | |
case S.PART_DATA: | |
previousIndex = index; | |
if (index === 0) { | |
// boyer-moore derrived algorithm to safely skip non-boundary data | |
i += boundaryEnd; | |
while (i < bufferLength && !(data[i] in boundaryChars)) { | |
i += boundaryLength; | |
} | |
i -= boundaryEnd; | |
c = data[i]; | |
} | |
if (index < boundary.length) { | |
if (boundary[index] === c) { | |
if (index === 0) { | |
dataCallback('onPartData', true); | |
} | |
index++; | |
} else { | |
index = 0; | |
} | |
} else if (index === boundary.length) { | |
index++; | |
if (c === CR) { | |
// CR = part boundary | |
flags |= F.PART_BOUNDARY; | |
} else if (c === HYPHEN) { | |
// HYPHEN = end boundary | |
flags |= F.LAST_BOUNDARY; | |
} else { | |
index = 0; | |
} | |
} else if (index - 1 === boundary.length) { | |
if (flags & F.PART_BOUNDARY) { | |
index = 0; | |
if (c === LF) { | |
// unset the PART_BOUNDARY flag | |
flags &= ~F.PART_BOUNDARY; | |
callback('onPartEnd'); | |
callback('onPartBegin'); | |
state = S.HEADER_FIELD_START; | |
break; | |
} | |
} else if (flags & F.LAST_BOUNDARY) { | |
if (c === HYPHEN) { | |
callback('onPartEnd'); | |
state = S.END; | |
flags = 0; | |
} else { | |
index = 0; | |
} | |
} else { | |
index = 0; | |
} | |
} | |
if (index > 0) { | |
// when matching a possible boundary, keep a lookbehind reference | |
// in case it turns out to be a false lead | |
lookbehind[index - 1] = c; | |
} else if (previousIndex > 0) { | |
// if our boundary turned out to be rubbish, the captured lookbehind | |
// belongs to partData | |
const _lookbehind = new Uint8Array(lookbehind.buffer, lookbehind.byteOffset, lookbehind.byteLength); | |
callback('onPartData', 0, previousIndex, _lookbehind); | |
previousIndex = 0; | |
mark('onPartData'); | |
// reconsider the current character even so it interrupted the sequence | |
// it could be the beginning of a new sequence | |
i--; | |
} | |
break; | |
case S.END: | |
break; | |
default: | |
throw new Error(`Unexpected state entered: ${state}`); | |
} | |
} | |
dataCallback('onHeaderField'); | |
dataCallback('onHeaderValue'); | |
dataCallback('onPartData'); | |
// Update properties for the next call | |
this.index = index; | |
this.state = state; | |
this.flags = flags; | |
} | |
end() { | |
if ((this.state === S.HEADER_FIELD_START && this.index === 0) || | |
(this.state === S.PART_DATA && this.index === this.boundary.length)) { | |
this.onPartEnd(); | |
} else if (this.state !== S.END) { | |
throw new Error('MultipartParser.end(): stream ended unexpectedly'); | |
} | |
} | |
} | |
function _fileName(headerValue) { | |
// matches either a quoted-string or a token (RFC 2616 section 19.5.1) | |
const m = headerValue.match(/\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i); | |
if (!m) { | |
return; | |
} | |
const match = m[2] || m[3] || ''; | |
let filename = match.slice(match.lastIndexOf('\\') + 1); | |
filename = filename.replace(/%22/g, '"'); | |
filename = filename.replace(/&#(\d{4});/g, (m, code) => { | |
return String.fromCharCode(code); | |
}); | |
return filename; | |
} | |
export async function toFormData(Body, ct) { | |
if (!/multipart/i.test(ct)) { | |
throw new TypeError('Failed to fetch'); | |
} | |
const m = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i); | |
if (!m) { | |
throw new TypeError('no or bad content-type header, no multipart boundary'); | |
} | |
const parser = new MultipartParser(m[1] || m[2]); | |
let headerField; | |
let headerValue; | |
let entryValue; | |
let entryName; | |
let contentType; | |
let filename; | |
const entryChunks = []; | |
const formData = new FormData(); | |
const onPartData = ui8a => { | |
entryValue += decoder.decode(ui8a, {stream: true}); | |
}; | |
const appendToFile = ui8a => { | |
entryChunks.push(ui8a); | |
}; | |
const appendFileToFormData = () => { | |
const file = new File(entryChunks, filename, {type: contentType}); | |
formData.append(entryName, file); | |
}; | |
const appendEntryToFormData = () => { | |
formData.append(entryName, entryValue); | |
}; | |
const decoder = new TextDecoder('utf-8'); | |
decoder.decode(); | |
parser.onPartBegin = function () { | |
parser.onPartData = onPartData; | |
parser.onPartEnd = appendEntryToFormData; | |
headerField = ''; | |
headerValue = ''; | |
entryValue = ''; | |
entryName = ''; | |
contentType = ''; | |
filename = null; | |
entryChunks.length = 0; | |
}; | |
parser.onHeaderField = function (ui8a) { | |
headerField += decoder.decode(ui8a, {stream: true}); | |
}; | |
parser.onHeaderValue = function (ui8a) { | |
headerValue += decoder.decode(ui8a, {stream: true}); | |
}; | |
parser.onHeaderEnd = function () { | |
headerValue += decoder.decode(); | |
headerField = headerField.toLowerCase(); | |
if (headerField === 'content-disposition') { | |
// matches either a quoted-string or a token (RFC 2616 section 19.5.1) | |
const m = headerValue.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i); | |
if (m) { | |
entryName = m[2] || m[3] || ''; | |
} | |
filename = _fileName(headerValue); | |
if (filename) { | |
parser.onPartData = appendToFile; | |
parser.onPartEnd = appendFileToFormData; | |
} | |
} else if (headerField === 'content-type') { | |
contentType = headerValue; | |
} | |
headerValue = ''; | |
headerField = ''; | |
}; | |
for await (const chunk of Body) { | |
parser.write(chunk); | |
} | |
parser.end(); | |
return formData; | |
} | |