-
-
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; | |
| })() | |
| }; |
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);
});
There is bug in this code.
The following part is buggy,
You are removing boundary length two times for last section. You already remove boundary when define
section.end. So, fix is following