Files
ycode/scripts/rea/cpp-expert-directory-provider.mjs
T

74 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** Formal local REA directory adapter. Uses REA inventory and stable non-following reader. */
import path from 'node:path'
import {lstat,realpath} from 'node:fs/promises'
import {createHash} from 'node:crypto'
import {DirectoryArtifactReader} from './DirectoryArtifactReader.js'
import {ArtifactResourceScope} from './ArtifactResourceScope.js'
import {scanCanonicalArtifactInventory} from './inventory/scanCanonical.js'
import {createEvidence} from '../domain/evidence.js'
const FILE=8*1024*1024,TOTAL=16*1024*1024,SCAN=64*1024*1024,ENTRIES=4096
const sha=s=>createHash('sha256').update(s).digest('hex')
const textPath=p=>/\.(js|cjs|mjs|ts|css|json|md|txt|yaml|yml|py|html|cs|csproj|slnx|xml|xsd|sh|ps1|LICENSE)$/i.test(p)||['LICENSE','.gitignore'].includes(path.posix.basename(p))
function selection(paths,binary=false){if(!Array.isArray(paths)||!paths.length||paths.length>16)throw Error('Select 1–16 exact paths');const seen=new Set;for(const p of paths){if(typeof p!=='string'||!p||p.includes('\\')||p.includes('\0')||path.posix.normalize(p)!==p||p.startsWith('/')||/^[a-z]:/i.test(p)||p.split('/').some(x=>!x||x==='.'||x==='..')||!(binary?/\.(png|ttf)$/i.test(p):textPath(p)))throw Error('Invalid canonical supported artifact path');if(seen.has(p))throw Error('Duplicate selected path');seen.add(p)}return paths}
async function rootIdentity(input){if(typeof input!=='string'||!path.isAbsolute(input))throw Error('Directory root must be explicitly absolute');const resolved=path.resolve(input);let current=path.parse(resolved).root;for(const part of path.relative(current,resolved).split(path.sep)){current=path.join(current,part);if((await lstat(current)).isSymbolicLink())throw Error('Directory root cannot contain symlink ancestors')}const canonical=await realpath(resolved),meta=await lstat(canonical,{bigint:true});if(!meta.isDirectory()||meta.isSymbolicLink())throw Error('Root is not an ordinary directory');return{path:canonical,dev:String(meta.dev),ino:String(meta.ino)}}
class BoundedReader extends DirectoryArtifactReader{async *entries(signal,filter){let bytes=0,count=0;for await(const entry of super.entries(signal,filter)){if(++count>ENTRIES)throw Error('Directory inventory entry budget exceeded');if(entry.kind==='file'){bytes+=entry.declaredSize;if(!Number.isSafeInteger(entry.declaredSize)||entry.declaredSize<0||bytes>SCAN)throw Error('Directory inventory byte budget exceeded')}yield entry}}}
async function inventory(input){const scope=new ArtifactResourceScope;try{return await scanCanonicalArtifactInventory(input,{resourceScope:scope,integrity:{mode:'fail'}},(root,format)=>{if(format!=='directory')throw Error('Directory format required');return new BoundedReader(root)})}finally{await scope.close()}}
function members(snapshot){const nodes=new Map(snapshot.nodes.map(n=>[n.artifact_id,n]));return snapshot.occurrences.filter(o=>o.logical_path!=='.').map(o=>({path:o.logical_path,kind:o.entry_kind,size_bytes:o.size_bytes??o.declared_size??null,sha256:nodes.get(o.artifact_id)?.sha256??null})).sort((a,b)=>a.path.localeCompare(b.path))}
export async function inspectSelectedDirectory(input){const identity=await rootIdentity(input),snapshot=await inventory(identity.path);return createEvidence({path:identity.path,sha256:snapshot.manifest.root_sha256,format:'directory'},{id:'rea-selected-directory-text',name:'REA bounded directory text adapter',version:'1+local'}, {predicateType:'rea.artifact.directory-inventory',operation:'inspect_selected_directory_text_root',parameters:{max_inventory_bytes:SCAN,max_inventory_entries:ENTRIES},result:{root_sha256:snapshot.manifest.root_sha256,root_identity:identity,inventory_graph_sha256:snapshot.manifest.graph_sha256,coverage:snapshot.coverage,members:members(snapshot)},confidence:'observed',authority:'shipped-artifact',limitations:['Directory content root digest uses the official REA canonical inventory. All inventoried member streams are hashed within independent inventory budgets; source text is not exported by this operation. Symlink targets are never followed.'],locations:[{kind:'artifact-path',path:identity.path}]})}
export async function exportSelectedDirectoryText(input,expectedRoot,paths){return exportSelectedDirectoryArtifact(input,expectedRoot,paths,false,{})}
export async function exportSelectedDirectoryBinary(input,expectedRoot,selected){
if(!Array.isArray(selected))throw Error('Select exact PNG/TTF paths and SHA values')
const hashes=Object.create(null)
for(const item of selected){if(!item||typeof item.path!=='string'||!/^[a-f0-9]{64}$/.test(item.sha256))throw Error('Expected selected binary SHA required');hashes[item.path]=item.sha256}
return exportSelectedDirectoryArtifact(input,expectedRoot,selected.map(item=>item.path),true,hashes)
}
async function exportSelectedDirectoryArtifact(input,expectedRoot,paths,binary,expectedHashes){
selection(paths,binary)
if(!/^[a-f0-9]{64}$/.test(expectedRoot))throw Error('Expected directory SHA must be64 lowercase hex')
const identity=await rootIdentity(input),snapshot=await inventory(identity.path)
if(snapshot.manifest.root_sha256!==expectedRoot)throw Error('Directory root SHA mismatch before selected export')
const listed=members(snapshot),reader=new BoundedReader(identity.path),entries=new Map,wanted=new Set(paths)
let total=0
try{
for await(const e of reader.entries(undefined,{descendInto:p=>paths.some(s=>s.startsWith(p+'/'))})){if(wanted.has(e.path)){if(entries.has(e.path))throw Error('Ambiguous selected member');entries.set(e.path,e)}}
const files=[]
for(const p of paths){
const entry=entries.get(p),known=listed.filter(e=>e.path===p)
if(!entry||known.length!==1||entry.kind!=='file')throw Error('Selected ordinary member absent: '+p)
if(binary&&known[0].sha256!==expectedHashes[p])throw Error('Expected selected PNG SHA mismatch')
if(entry.declaredSize>FILE||total+entry.declaredSize>TOTAL)throw Error('Selected text byte budget exceeded')
const chunks=[];let size=0
for await(const b of await reader.open(entry)){size+=b.length;if(size>FILE||size>entry.declaredSize||total+size>TOTAL)throw Error('Selected text byte budget exceeded');chunks.push(b)}
const bytes=Buffer.concat(chunks),digest=sha(bytes)
if(size!==entry.declaredSize||digest!==known[0].sha256||(binary&&digest!==expectedHashes[p]))throw Error('Selected member changed or SHA mismatch')
if(binary){
let mediaType
if(/\.png$/i.test(p)){
if(!bytes.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])))throw Error('Selected PNG signature mismatch')
mediaType='image/png'
}else{
if(bytes.length<12||![0x00010000,0x74727565].includes(bytes.readUInt32BE(0)))throw Error('Selected TTF signature mismatch')
const tables=bytes.readUInt16BE(4),directoryEnd=12+16*tables
if(!tables||tables>4096||directoryEnd>bytes.length)throw Error('Selected TTF table directory invalid')
for(let i=0;i<tables;i++){const start=12+16*i,offset=bytes.readUInt32BE(start+8),length=bytes.readUInt32BE(start+12);if(offset<directoryEnd||offset+length>bytes.length)throw Error('Selected TTF table range invalid')}
mediaType='font/ttf'
}
files.push({path:p,size_bytes:size,sha256:digest,encoding:'base64',media_type:mediaType,data_base64:bytes.toString('base64')})
}else{
let text;try{text=new TextDecoder('utf-8',{fatal:true,ignoreBOM:true}).decode(bytes)}catch{throw Error('Selected member is not strict UTF-8')}
files.push({path:p,size_bytes:size,sha256:digest,text})
}
total+=size
}
const after=await rootIdentity(input)
if(JSON.stringify(after)!==JSON.stringify(identity))throw Error('Directory physical root identity changed')
const verified=await inventory(identity.path)
if(verified.manifest.root_sha256!==expectedRoot)throw Error('Directory root SHA mismatch after selected export')
return createEvidence({path:identity.path,sha256:expectedRoot,format:'directory'},{id:binary?'rea-selected-directory-binary':'rea-selected-directory-text',name:'REA bounded directory artifact adapter',version:'1+binary-local'},
{predicateType:binary?'rea.artifact.selected-directory-binary':'rea.artifact.selected-directory-text',operation:binary?'export_selected_directory_binary':'export_selected_directory_text',
parameters:{expected_root_sha256:expectedRoot,selected_paths:paths,...(binary?{expected_file_sha256:expectedHashes}:{}),max_file_bytes:FILE,max_total_bytes:TOTAL,max_inventory_bytes_per_pass:SCAN,max_inventory_entries:ENTRIES},
result:{root_sha256:expectedRoot,root_identity:identity,inventory_graph_sha256:snapshot.manifest.graph_sha256,scope:{kind:'selected-files-only-export',selected_paths:paths,root_inventory:'Full bounded root inventory hashed before/after selected export; unselected content not exported.'},files},
confidence:'observed',authority:'shipped-artifact',limitations:['Only selected artifacts are returned. Full directory inventory bytes are hashed to validate the same REA root content digest before and after export.','No target code is executed, no HTTP resource is requested, no symlink target is followed. Mutable directories can fail pin validation; do not silently replace the expected pin.'],locations:paths.map(p=>({kind:'artifact-path',path:p}))})
}finally{await reader.close()}
}