'use strict'; const {spawn}=require('node:child_process'); const path=require('node:path'); const crypto=require('node:crypto'); const fs=require('node:fs'); const VERSION='safetynet-upload-lease-v1'; const LIMIT=10*1024*1024; let disabled=false; function trustedFile(p){ if(typeof p!=='string'||!path.win32.isAbsolute(p)||p.startsWith('\\\\'))return false; try{const real=fs.realpathSync.native(p);return real.toLowerCase()===p.toLowerCase()&&fs.statSync(p).isFile();}catch{return false;} } function refused(){return Object.assign(new Error('Upload lease unavailable'),{code:'upload-lease-refused'});} async function acquireUploadLease({paths,maxBytes=LIMIT,pythonPath,helperPath,onLost=()=>{},signal}){ // eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input. if(process.platform!=='win32'||!Array.isArray(paths)||paths.length<1||paths.length>16||!Number.isInteger(maxBytes)||maxBytes<1||maxBytes>LIMIT||typeof onLost!=='function'||![pythonPath,helperPath].every(p=>typeof p==='string'&&path.win32.isAbsolute(p)&&!p.startsWith('\\\\'))||!paths.every(p=>typeof p==='string'&&/^[A-Za-z]:\\[^\x00-\x1f:]*$/.test(p)&&p.length<=240&&path.win32.normalize(p)===p))throw refused(); if(disabled||signal?.aborted||!trustedFile(pythonPath)||!trustedFile(helperPath))throw refused(); const child=spawn(pythonPath,['-I','-B',helperPath],{windowsHide:true,stdio:['pipe','pipe','pipe'],env:{SystemRoot:process.env.SystemRoot,WINDIR:process.env.WINDIR},shell:false}); let live=true,releasing=false,ready=false,buffer=Buffer.alloc(0),total=0,settled=false,releaseResolve,releaseReject,receipt; let resolve,reject; const acquired=new Promise((a,b)=>{resolve=a;reject=b;}); const limit=Math.ceil(maxBytes*4/3)+65536; const timer=setTimeout(()=>fail(),30000);timer.unref(); const lost=()=>{if(live&&!releasing){try{onLost();}catch{} }live=false;}; function fail(){disabled=true;lost();clearTimeout(timer);if(!settled){settled=true;reject(refused());}releaseReject?.(refused());child.kill();} const abort=()=>fail();signal?.addEventListener('abort',abort,{once:true}); child.on('error',fail); child.on('exit',()=>{clearTimeout(timer);signal?.removeEventListener('abort',abort);if(!releasing)fail();else{live=false;releaseReject?.(refused());}}); child.stdin.on('error',fail);child.stderr.on('data',()=>fail()); child.stdout.on('data',chunk=>{ total+=chunk.length;if(total>limit){fail();return;} buffer=Buffer.concat([buffer,chunk]); let newline; while((newline=buffer.indexOf(10))!==-1){ const line=buffer.subarray(0,newline);buffer=Buffer.from(buffer.subarray(newline+1)); try{ const msg=JSON.parse(line.toString('utf8')); if(!ready&&JSON.stringify(Object.keys(msg).sort())===JSON.stringify(['code','ok','version'])&&msg.version===VERSION&&msg.ok===false&&msg.code==='upload-lease-refused'){ releasing=true;live=false;settled=true;clearTimeout(timer);reject(refused());continue; } if(ready){ if(!releasing||JSON.stringify(Object.keys(msg).sort())!==JSON.stringify(['released','version'])||msg.version!==VERSION||msg.released!==true)throw refused(); live=false;receipt=null;releaseResolve?.();releaseResolve=null;releaseReject=null;continue; } if(JSON.stringify(Object.keys(msg).sort())!==JSON.stringify(['files','handles','ok','version'])||msg.version!==VERSION||msg.ok!==true||!Array.isArray(msg.files)||msg.files.length!==paths.length||!Array.isArray(msg.handles)||msg.handles.length<2||msg.handles.length>4096||!msg.handles.every(v=>typeof v==='string'&&/^[1-9][0-9]{0,19}$/.test(v)&&BigInt(v)<2n**64n)||new Set(msg.handles).size!==msg.handles.length)throw refused(); receipt=msg.handles; let bytesTotal=0; const files=msg.files.map((f,i)=>{ if(JSON.stringify(Object.keys(f).sort())!==JSON.stringify(['bytes','fileId','path','sha256','size','volume'])||f.path!==paths[i]||!Number.isInteger(f.size)||f.size<0||!Number.isInteger(f.volume)||f.volume<0||f.volume>0xffffffff||typeof f.fileId!=='string'||!/^[a-f0-9]{16}$/.test(f.fileId)||typeof f.sha256!=='string'||!/^[a-f0-9]{64}$/.test(f.sha256)||typeof f.bytes!=='string')throw refused(); const bytes=Buffer.from(f.bytes,'base64');bytesTotal+=bytes.length; if(bytesTotal>maxBytes||bytes.length!==f.size||bytes.toString('base64')!==f.bytes||crypto.createHash('sha256').update(bytes).digest('hex')!==f.sha256)throw refused(); return Object.freeze({...f,bytes}); }); ready=true;settled=true;clearTimeout(timer); resolve({files:Object.freeze(files),isAlive:()=>live&&!releasing,release:()=>{ if(releasing)return Promise.reject(refused()); if(!live){ releasing=true; return new Promise((a,b)=>{ const cleanup=spawn(pythonPath,['-I','-B',helperPath],{windowsHide:true,stdio:['pipe','pipe','pipe'],env:{SystemRoot:process.env.SystemRoot,WINDIR:process.env.WINDIR},shell:false}); const cleanupTimer=setTimeout(()=>{disabled=true;cleanup.kill();b(refused());},5000);cleanupTimer.unref(); let output='';cleanup.stdout.on('data',c=>{output+=c.toString('utf8');if(output.length>1024)cleanup.kill();});cleanup.stderr.on('data',()=>cleanup.kill());cleanup.on('error',()=>{clearTimeout(cleanupTimer);disabled=true;b(refused());});cleanup.on('exit',code=>{clearTimeout(cleanupTimer);try{const msg=JSON.parse(output);if(code!==0||msg.version!==VERSION||msg.released!==true||Object.keys(msg).length!==2)throw refused();receipt=null;a();}catch{disabled=true;b(refused());}}); cleanup.stdin.on('error',()=>b(refused()));cleanup.stdin.end(JSON.stringify({version:VERSION,parentPid:process.pid,closeHandles:receipt})+'\n'); }); } releasing=true; return new Promise((a,b)=>{const deadline=setTimeout(()=>fail(),5000);deadline.unref();releaseResolve=()=>{clearTimeout(deadline);a();};releaseReject=e=>{clearTimeout(deadline);b(e);};child.stdin.end('{"release":true}\n');}); }}); }catch{fail();}finally{line.fill(0);} } }); child.stdin.write(JSON.stringify({version:VERSION,paths,maxBytes,parentPid:process.pid})+'\n'); return acquired; } module.exports={acquireUploadLease};