-
-
Save dcollien/76d17f69afe748afad7ff3a15ff9a08a to your computer and use it in GitHub Desktop.
| var Multipart = { | |
| parse: (function() { | |
| function Parser(arraybuf, boundary) { | |
| this.array = arraybuf; | |
| this.token = null; | |
| this.current = null; | |
| this.i = 0; | |
| this.boundary = boundary; | |
| } | |
| Parser.prototype.skipPastNextBoundary = function() { | |
| var boundaryIndex = 0; | |
| var isBoundary = false; | |
| while (!isBoundary) { | |
| if (this.next() === null) { | |
| return false; | |
| } | |
| if (this.current === this.boundary[boundaryIndex]) { | |
| boundaryIndex++; | |
| if (boundaryIndex === this.boundary.length) { | |
| isBoundary = true; | |
| } | |
| } else { | |
| boundaryIndex = 0; | |
| } | |
| } | |
| return true; | |
| } | |
| Parser.prototype.parseHeader = function() { | |
| var header = ''; | |
| var _this = this; | |
| var skipUntilNextLine = function() { | |
| header += _this.next(); | |
| while (_this.current !== '\n' && _this.current !== null) { | |
| header += _this.next(); | |
| } | |
| if (_this.current === null) { | |
| return null; | |
| } | |
| }; | |
| var hasSkippedHeader = false; | |
| while (!hasSkippedHeader) { | |
| skipUntilNextLine(); | |
| header += this.next(); | |
| if (this.current === '\r') { | |
| header += this.next(); // skip | |
| } | |
| if (this.current === '\n') { | |
| hasSkippedHeader = true; | |
| } else if (this.current === null) { | |
| return null; | |
| } | |
| } | |
| return header; | |
| } | |
| Parser.prototype.next = function() { | |
| if (this.i >= this.array.byteLength) { | |
| this.current = null; | |
| return null; | |
| } | |
| this.current = String.fromCharCode(this.array[this.i]); | |
| this.i++; | |
| return this.current; | |
| } | |
| function buf2String(buf) { | |
| var string = ''; | |
| buf.forEach(function (byte) { | |
| string += String.fromCharCode(byte); | |
| }); | |
| return string; | |
| } | |
| function processSections(arraybuf, sections) { | |
| for (var i = 0; i !== sections.length; ++i) { | |
| var section = sections[i]; | |
| if (section.header['content-type'] === 'text/plain') { | |
| section.text = buf2String(arraybuf.slice(section.bodyStart, section.end)); | |
| } else { | |
| var imgData = arraybuf.slice(section.bodyStart, section.end); | |
| section.file = new Blob([imgData], { | |
| type: section.header['content-type'] | |
| }); | |
| var fileNameMatching = (/\bfilename\=\"([^\"]*)\"/g).exec(section.header['content-disposition']) || []; | |
| section.fileName = fileNameMatching[1] || ''; | |
| } | |
| var matching = (/\bname\=\"([^\"]*)\"/g).exec(section.header['content-disposition']) || []; | |
| section.name = matching[1] || ''; | |
| delete section.headerStart; | |
| delete section.bodyStart; | |
| delete section.end; | |
| } | |
| return sections; | |
| } | |
| function multiparts(arraybuf, boundary) { | |
| boundary = '--' + boundary; | |
| var parser = new Parser(arraybuf, boundary); | |
| var sections = []; | |
| while (parser.skipPastNextBoundary()) { | |
| var header = parser.parseHeader(); | |
| if (header !== null) { | |
| var headerLength = header.length; | |
| var headerParts = header.trim().split('\n'); | |
| var headerObj = {}; | |
| for (var i = 0; i !== headerParts.length; ++i) { | |
| var parts = headerParts[i].split(':'); | |
| headerObj[parts[0].trim().toLowerCase()] = (parts[1] || '').trim(); | |
| } | |
| sections.push({ | |
| 'bodyStart': parser.i, | |
| 'header': headerObj, | |
| 'headerStart': parser.i - headerLength | |
| }); | |
| } | |
| } | |
| // add dummy section for end | |
| sections.push({ | |
| 'headerStart': arraybuf.byteLength - 2 // 2 hyphens at end | |
| }); | |
| for (var i = 0; i !== sections.length - 1; ++i) { | |
| sections[i].end = sections[i+1].headerStart - boundary.length; | |
| if (String.fromCharCode(arraybuf[sections[i].end]) === '\r' || '\n') { | |
| sections[i].end -= 1; | |
| } | |
| if (String.fromCharCode(arraybuf[sections[i].end]) === '\r' || '\n') { | |
| sections[i].end -= 1; | |
| } | |
| } | |
| // remove dummy section | |
| sections.pop(); | |
| sections = processSections(arraybuf, sections); | |
| return sections; | |
| } | |
| return multiparts; | |
| })() | |
| }; |
There is bug in this code.
The following part is buggy,
sections.push({
'headerStart': arraybuf.byteLength - boundary.length - 2 // 2 hyphens at end
});
You are removing boundary length two times for last section. You already remove boundary when define section.end. So, fix is following
sections.push({
'headerStart': arraybuf.byteLength - 2 // 2 hyphens at end
});
Modern browsers can parse multipart/form-data natively. Example:
const payload =
`------WebKitFormBoundaryU5rJUDxGnj15hIGW\r
Content-Disposition: form-data; name="field1"\r
\r
Hello
World,
This is me\r
------WebKitFormBoundaryU5rJUDxGnj15hIGW--`
const boundary = payload.slice(2, payload.indexOf('\r\n'))
new Response(payload, {
headers: {
'Content-Type': `multipart/form-data; boundary=${boundary}`
}
})
.formData()
.then(formData => {
console.log([...formData]) // [['field1', 'Hello\nWorld,\nThis is me']]
})The \r inside payload are necessary, because the line breaks must be \r\n, except the values themselves. If you have a properly formed multipart/form-data blob, you don't need to add \r.
If you want to parse an HTTP response, you can use the fetch response directly:
fetch('/formdata-response')
.then(response => response.formData())
.then(formData => console.log([...formData]))Modern browsers can parse
multipart/form-datanatively. Example:
Thank you! We've come a long way since 2017
@Finesse entries() is not necessary, FormData spreads to an array of arrays (entries) [...fd].
@guest271314 You are right, I've amended my code snippet
Very helpful snippet. If you have access to fetch() it should be possible to use text() to get the raw multipart/form-data content with \r\n included
{
var formdata = new FormData();
var dirname = "web-directory";
formdata.append(dirname, new Blob(["123"], {
type: "text/plain"
}), `${dirname}/file.txt`);
formdata.append(dirname, new Blob(["src"], {
type: "text/plain"
}), `${dirname}/src/file.txt`);
var body = await new Response(formdata).text();
console.log(body);
const boundary = body.slice(2, body.indexOf('\r\n'))
console.log(boundary)
new Response(body, {
headers: {
'Content-Type': `multipart/form-data; boundary=${boundary}`
}
})
.formData()
.then(formData => {
console.log([...formData])
})
}
@Finesse Another way to do this when payload is a TypedArray (or ArrayBuffer)
let ab = new Uint8Array(await response.clone().arrayBuffer());
let boundary = ab.subarray(2, ab.indexOf(13) + 1);
let archive = await new Response(ab, {
headers: {
"Content-Type": `multipart/form-data; boundary=${new TextDecoder().decode(boundary)}`,
},
})
.formData()
.then((data) => {
console.log([...data]);
return data;
}).catch((e) => {
console.warn(e);
});
I've re-written it in TypeScript as a ES-module, but I've not tested it yet. It works fine in my case in Cypress anyway.