'use strict'; const {createHash}=require('node:crypto'); const {detectType}=require('./file-transfer.cjs'); // Typed binary carriers supplement text normalization. No whole decoded file // may be released on the strength of an allow verdict for its outer JSON text. function extractEncodedDocuments(bytes,{maxDecodedBytes=16777216,maxParts=16,maxNodes=20000,maxDepth=32,jsonRequired=false}={}){ if(!Buffer.isBuffer(bytes)||bytes.length>16777216||![maxDecodedBytes,maxParts,maxNodes,maxDepth].every(Number.isSafeInteger)||maxDecodedBytes<1||maxDecodedBytes>16777216||maxParts<1||maxParts>64||maxNodes<1||maxNodes>20000||maxDepth<1||maxDepth>32)throw Error('encoded-document-configuration-invalid'); if(typeof jsonRequired!=='boolean')throw Error('encoded-document-configuration-invalid'); const parts=[],seen=new Set(),encodings=new Set();let total=0,nodes=0,complete=true; const result=()=>Object.freeze({parts:Object.freeze(parts),complete,decodedBytes:total,visitedNodes:nodes,dispose}); const dispose=()=>{for(const part of parts)part.bytes.fill(0);}; function candidate(value){ if(!complete||typeof value!=='string')return; let encoded=value.trim(),explicit=false,mime; if(/^data:/i.test(encoded)){ const match=/^data:([a-z0-9.+/-]+)?(?:;[a-z0-9-]+=[a-z0-9._-]+)*;base64,([\s\S]*)$/i.exec(encoded); if(!match){complete=false;return;}mime=(match[1]||'application/octet-stream').toLowerCase();encoded=match[2];explicit=true; if(encoded.includes('%')){try{encoded=decodeURIComponent(encoded);}catch{complete=false;return;}} } if(!explicit&&encoded.length<16)return; if(!/^[A-Za-z0-9+/_-]*={0,2}$/.test(encoded)||encoded.length%4===1){if(explicit)complete=false;return;} const normalized=encoded.replace(/-/g,'+').replace(/_/g,'/').replace(/=+$/,''); const encodingKey=createHash('sha256').update(normalized).update('\0'+(mime||'')).digest('hex'); if(encodings.has(encodingKey))return; const projected=Math.floor(normalized.length*3/4); if(total+projected>maxDecodedBytes){complete=false;return;} const decoded=Buffer.from(normalized,'base64'); total+=decoded.length;encodings.add(encodingKey); const type=detectType(decoded); if(decoded.toString('base64').replace(/=+$/,'')!==normalized){decoded.fill(0);if(explicit||type!=='unknown')complete=false;return;} if(!explicit&&type==='unknown'){decoded.fill(0);return;} const digest=createHash('sha256').update(decoded).digest('hex'); const identity=digest+'\0'+(mime||''); if(seen.has(identity)){decoded.fill(0);return;} if(parts.length>=maxParts){decoded.fill(0);complete=false;return;} seen.add(identity);parts.push(Object.freeze({bytes:decoded,...(mime?{mime}:{})})); } function scanText(value,percentDepth=0){ candidate(value); const carriers=[]; for(const match of value.matchAll(/data:[^\s,"'<>]+,[A-Za-z0-9+/_=%-]+/gi)){if(++nodes>maxNodes){complete=false;break;}carriers.push([match.index,match.index+match[0].length]);candidate(match[0]);} let carrier=0; for(const match of value.matchAll(/[A-Za-z0-9+/_-]{16,}={0,2}/g)){ while(carrier=carriers[carrier][0]&&match.indexmaxNodes){complete=false;break;}candidate(match[0]); } if(complete&&value.includes('%')){ if(percentDepth>=4){complete=false;return;} try{const decoded=decodeURIComponent(value);if(decoded!==value)scanText(decoded,percentDepth+1);}catch{if(/data%3a/i.test(value))complete=false;} } } try{ const text=new TextDecoder('utf-8',{fatal:true}).decode(bytes),trimmed=text.trim(); if(jsonRequired||trimmed.startsWith('{')||trimmed.startsWith('[')){ let root;try{root=JSON.parse(trimmed);}catch{if(jsonRequired)complete=false;else scanText(trimmed);return result();} const stack=[{value:root,depth:0}]; while(stack.length&&complete){const {value,depth}=stack.pop();if(++nodes>maxNodes||depth>maxDepth){complete=false;break;} if(typeof value==='string')scanText(value); else if(Array.isArray(value)){ if(value.length>maxNodes-nodes||stack.length+value.length>maxNodes){complete=false;break;} for(const child of value)stack.push({value:child,depth:depth+1}); }else if(value&&typeof value==='object'){ // Avoid Object.entries allocating an unbounded array of key/value pairs // before the traversal budget is checked on a hostile JSON collection. for(const key in value){if(!Object.hasOwn(value,key))continue;scanText(key);stack.push({value:value[key],depth:depth+1});if(stack.length>maxNodes||!complete){complete=false;break;}} } } }else{ scanText(trimmed); } }catch{complete=false;} return result(); } module.exports={extractEncodedDocuments};