Skip to content

Instantly share code, notes, and snippets.

@dcollien
Last active July 17, 2024 03:47
Show Gist options
  • Save dcollien/76d17f69afe748afad7ff3a15ff9a08a to your computer and use it in GitHub Desktop.
Save dcollien/76d17f69afe748afad7ff3a15ff9a08a to your computer and use it in GitHub Desktop.
Parse multi-part formdata in the browser
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;
})()
};
@Finesse
Copy link

Finesse commented May 24, 2024

@guest271314 You are right, I've amended my code snippet

@guest271314
Copy link

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])
    })
}

@guest271314
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment