Files
safetynet/scripts/native-patch-preflight.mjs
T

88 lines
6.0 KiB
JavaScript

import fs from 'node:fs/promises';
import path from 'node:path';
import {execFile} from 'node:child_process';
import {promisify} from 'node:util';
import {fileURLToPath} from 'node:url';
const execute=promisify(execFile);
const FONTCONFIG='src/third_party/fontconfig/src';
function inside(root,target){const relative=path.relative(root,target);return relative===''||(!relative.startsWith(`..${path.sep}`)&&relative!=='..'&&!path.isAbsolute(relative));}
async function contained(root,target){
const resolved=await fs.realpath(target);
if(!inside(root,resolved))throw Error('path-outside-checkout');
return resolved;
}
async function nearestContained(root,target){
if(!inside(root,target))throw Error('path-outside-checkout');
let current=target;
while(true){try{return await contained(root,current);}catch(error){if(error.code!=='ENOENT')throw error;const parent=path.dirname(current);if(parent===current)throw error;current=parent;}}
}
async function git(repo,args){
const {stdout}=await execute('git',['--no-optional-locks','-C',repo,...args],{env:{...process.env,GIT_OPTIONAL_LOCKS:'0'},timeout:120000,maxBuffer:8*1024*1024,windowsHide:true});
return stdout.trim();
}
// Read-only prerequisite check. In particular, an existing directory is not
// proof of an independent checkout: Git silently searches ancestor repositories.
export async function nativePatchPreflight({checkoutRoot,configPath,excludeWindowsFontconfig=false,platform=process.platform}){
const result={scope:'Read-only patch target preflight; not patch import or build proof.',passed:false,required:[],excluded:[],errors:[]};
try{
const root=await fs.realpath(checkoutRoot);result.checkoutRoot=root;
const config=await contained(root,path.resolve(configPath));
const targets=JSON.parse(await fs.readFile(config,'utf8'));
if(!Array.isArray(targets)||targets.length===0||targets.length>64)throw Error('invalid-patch-config');
const seen=new Set();
for(const target of targets){
const row={repo:target?.repo,patchDir:target?.patch_dir,errors:[]};
try{
if(!target||typeof target.repo!=='string'||typeof target.patch_dir!=='string'||!target.repo||!target.patch_dir||path.isAbsolute(target.repo)||path.isAbsolute(target.patch_dir)||seen.has(target.repo))throw Error('invalid-patch-target');
seen.add(target.repo);
const repo=path.resolve(root,target.repo),patchDir=path.resolve(root,target.patch_dir);
await nearestContained(root,repo);
const patches=await contained(root,patchDir),list=await contained(root,path.join(patches,'.patches'));
// Electron's official reader keeps every list line, including blanks.
const text=await fs.readFile(list,'utf8');const names=text.split(/\r?\n/);if(names.at(-1)==='')names.pop();
if(!names.length||names.some(name=>!name||path.isAbsolute(name)))throw Error('invalid-patch-list');
for(const name of names){const file=await contained(root,path.resolve(patches,name));if(!inside(patches,file))throw Error('patch-outside-patch-directory');}
row.patchCount=names.length;
if(target.repo===FONTCONFIG&&excludeWindowsFontconfig){
if(platform!=='win32')throw Error('fontconfig-exclusion-requires-windows');
const deps=await fs.readFile(await contained(root,path.join(root,'src/DEPS')),'utf8');
if(!/['"]src\/third_party\/fontconfig\/src['"]\s*:\s*\{[^}]*['"]condition['"]\s*:\s*['"]checkout_linux['"]/s.test(deps))throw Error('fontconfig-exclusion-condition-unverified');
row.reason='Explicit Windows exclusion: Chromium DEPS checkout_linux';result.excluded.push(row);continue;
}
const own=await contained(root,repo);
try{await contained(root,path.join(own,'.git'));}catch(error){if(error.code==='ENOENT')throw Error('missing-own-git-marker',{cause:error});throw error;}
const top=await git(own,['rev-parse','--show-toplevel']);
if(await fs.realpath(top)!==own)throw Error('ancestor-git-root');
const gitDir=await git(own,['rev-parse','--absolute-git-dir']);await contained(root,gitDir);
const am=path.resolve(own,await git(own,['rev-parse','--git-path','rebase-apply']));
await nearestContained(root,am);
try{await fs.stat(am);throw Error('git-am-in-progress');}catch(error){if(error.code!=='ENOENT')throw error;}
row.head=await git(own,['rev-parse','--verify','HEAD']);
row.trackedClean=(await git(own,['status','--porcelain=v1','--untracked-files=no']))==='';
if(!row.trackedClean)throw Error('tracked-worktree-not-clean');
}catch(error){row.errors.push(error.code==='ENOENT'?'missing-required-path':error.message||'preflight-failed');}
result.required.push(row);
}
result.errors=result.required.filter(row=>row.errors.length).map(row=>({repo:row.repo,errors:row.errors}));
result.passed=result.errors.length===0;
}catch(error){result.errors.push({errors:[error.code==='ENOENT'?'missing-required-path':error.message||'preflight-failed']});}
result.requiredCount=result.required.length;result.excludedCount=result.excluded.length;
return result;
}
if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
const args=process.argv.slice(2);const allowed=new Set(['--checkout-root','--config','--exclude-windows-fontconfig']);let checkoutRoot,configPath,excludeWindowsFontconfig=false;
try{
for(let i=0;i<args.length;i++){
if(!allowed.has(args[i]))throw Error('invalid-cli-argument');
if(args[i]==='--exclude-windows-fontconfig'){excludeWindowsFontconfig=true;continue;}
const value=args[++i];if(!value||value.startsWith('--'))throw Error('missing-cli-value');
if(args[i-1]==='--checkout-root')checkoutRoot=value;else configPath=value;
}
if(!checkoutRoot||!configPath)throw Error('checkout-root-and-config-required');
const result=await nativePatchPreflight({checkoutRoot,configPath,excludeWindowsFontconfig});console.log(JSON.stringify(result,null,2));process.exitCode=result.passed?0:1;
}catch(error){console.log(JSON.stringify({passed:false,errors:[error.message]}));process.exitCode=1;}
}