'use strict'; class MultipartError extends Error{ constructor(code){super(code);this.name='MultipartError';this.code=code;} } const fail=code=>{throw new MultipartError(code);}; const token=/^[!#$%&'*+.^_`|~0-9a-z-]+$/i; function parameters(value){ const main=/^([^;]+)(.*)$/.exec(value);if(!main)fail('invalid-multipart'); const result=Object.create(null);let rest=main[2]; while(rest){ const key=/^;[ \t]*([!#$%&'*+.^_`|~0-9a-z-]+)[ \t]*=[ \t]*/i.exec(rest); if(!key)fail('invalid-multipart');rest=rest.slice(key[0].length); let parameter; if(rest.startsWith('"')){ let index=1;parameter='';let closed=false; for(;index{const code=character.charCodeAt(0);return code<32&&code!==9||code===127;}))fail('invalid-multipart'); const name=match[1].toLowerCase();if(Object.hasOwn(headers,name))fail('invalid-multipart');headers[name]=match[2]; } if(Object.keys(headers).some(name=>!['content-disposition','content-type'].includes(name)))fail('unsupported-multipart'); if(!headers['content-disposition'])fail('invalid-multipart'); const disposition=parameters(headers['content-disposition']),values=disposition.values; if(Object.keys(values).some(name=>name.endsWith('*')))fail('unsupported-multipart'); if(disposition.main!=='form-data'||Object.keys(values).some(name=>!['name','filename'].includes(name))||typeof values.name!=='string'||!values.name||Buffer.byteLength(values.name)>256||[values.name,values.filename].some(value=>value!==undefined&&(Buffer.byteLength(value)>1024||[...value].some(character=>character.charCodeAt(0)<32||character.charCodeAt(0)===127))))fail('invalid-multipart'); let contentType; if(headers['content-type']!==undefined){ if(headers['content-type'].length>256)fail('multipart-limit'); const type=parameters(headers['content-type']); if(!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(type.main))fail('invalid-multipart'); if(type.main.startsWith('multipart/'))fail('unsupported-multipart'); contentType=headers['content-type']; } return {name:values.name,...values.filename!==undefined?{filename:values.filename}:{},...contentType!==undefined?{contentType}:{}}; } /** Full-buffer parser. No filesystem access or content logging. Returned part * buffers are independent private copies; dispose only after scanners finish. * Strict unsupported forms fail closed instead of claiming partial coverage. */ function parseMultipart(body,contentType,{maxBytes=16777216,maxParts=64,maxHeaderBytes=8192}={}){ if(!Buffer.isBuffer(body)||typeof contentType!=='string'||![maxBytes,maxParts,maxHeaderBytes].every(Number.isSafeInteger)||maxBytes<0||maxBytes>16777216||maxParts<1||maxParts>256||maxHeaderBytes<1||maxHeaderBytes>32768)fail('invalid-multipart'); if(body.length>maxBytes||contentType.length>256)fail('multipart-limit'); const type=parameters(contentType); if(type.main!=='multipart/form-data'||Object.keys(type.values).some(name=>name!=='boundary')||!/^[0-9a-z'()+_,./:=?-]{1,70}$/i.test(type.values.boundary||''))fail('invalid-multipart'); const boundary=Buffer.from('--'+type.values.boundary),separator=Buffer.from('\r\n--'+type.values.boundary),parts=[]; const dispose=()=>{for(const part of parts)part.body.fill(0);}; try{ if(!body.subarray(0,boundary.length).equals(boundary))fail('invalid-multipart'); let position=boundary.length; while(true){ if(body.subarray(position,position+2).equals(Buffer.from('--'))){ position+=2;if(body.subarray(position).equals(Buffer.from('\r\n')))position+=2; if(position!==body.length)fail('invalid-multipart');break; } if(!body.subarray(position,position+2).equals(Buffer.from('\r\n')))fail('invalid-multipart');position+=2; if(parts.length>=maxParts)fail('multipart-limit'); const headerEnd=body.indexOf('\r\n\r\n',position); if(headerEnd<0)fail('invalid-multipart');if(headerEnd-position>maxHeaderBytes)fail('multipart-limit'); const record=metadata(body.subarray(position,headerEnd));position=headerEnd+4; let end=position; while(true){ end=body.indexOf(separator,end);if(end<0)fail('invalid-multipart'); const suffix=body.subarray(end+separator.length,end+separator.length+2); if(suffix.equals(Buffer.from('\r\n'))||suffix.equals(Buffer.from('--')))break; end++; } parts.push(Object.freeze({...record,body:Buffer.from(body.subarray(position,end))}));position=end+separator.length; } return Object.freeze({coverage:Object.freeze({complete:true}),parts:Object.freeze(parts),dispose}); }catch(error){dispose();throw error;} } module.exports={parseMultipart,MultipartError};