chore: establish YCode replication baseline and TODO
This commit is contained in:
commit
6dc59f89ba
1118 files changed
+196002
No files matched your search
@@ -0,0 +1,98 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { authorizedElectronFile } from './ElectronFileScope.js';
|
||||
import { decodeCanonicalBase64 } from '../domain/webScreenshot.js';
|
||||
|
||||
const MAX_ASSETS=4, MAX_ASSET_BYTES=2*1024*1024, MAX_TOTAL_BYTES=8*1024*1024;
|
||||
const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
|
||||
const types=new Set(['image/png','image/jpeg','image/gif','image/webp']);
|
||||
const mime=value=>value==='image/jpg'?'image/jpeg':value;
|
||||
function sniff(bytes){
|
||||
if(bytes.length>=24&&bytes.subarray(0,8).equals(Buffer.from([137,80,78,71,13,10,26,10])))return 'image/png';
|
||||
if(bytes.length>=3&&bytes[0]===255&&bytes[1]===216&&bytes[2]===255)return 'image/jpeg';
|
||||
if(bytes.length>=10&&/^GIF8[79]a$/.test(bytes.toString('ascii',0,6)))return 'image/gif';
|
||||
if(bytes.length>=12&&bytes.toString('ascii',0,4)==='RIFF'&&bytes.toString('ascii',8,12)==='WEBP')return 'image/webp';
|
||||
}
|
||||
function safeSourceUrl(value){
|
||||
try{const u=new URL(value);if(!['file:','http:','https:'].includes(u.protocol))return null;u.username='';u.password='';u.search='';u.hash='';const result=u.href;return result.length<=2048?result:null;}catch{return null;}
|
||||
}
|
||||
|
||||
/** Only a caller-selected IMG and its already loaded main-frame resource are eligible. */
|
||||
export async function captureElectronMediaAssets(snapshot,frames,selectors,send){
|
||||
if(!selectors?.length)return undefined;
|
||||
const result={items:[],total_bytes:0,limits:{max_assets:MAX_ASSETS,max_asset_bytes:MAX_ASSET_BYTES,max_total_bytes:MAX_TOTAL_BYTES},limitations:[
|
||||
'Opt-in IMG resources only: exact unique selectors, authorized main file frame and snapshot backend identity. Visibility is reported separately and does not authorize new requests.',
|
||||
'Only Page.getResourceTree resources already loaded by the selected frame are read with Page.getResourceContent. No HTTP fetch, cookies, headers, input values or renderer JavaScript are used.',
|
||||
'PNG/JPEG/GIF/WebP only; SVG, canvas, video, CSS backgrounds and resources missing from the loaded frame tree remain unknown. Covered loaded images can be exported; bytes do not establish that this image is the currently visible layer.',
|
||||
'Resource URLs in metadata omit credentials, query strings and fragments; URL SHA-256 identifies the exact selected URL. DOM and resource observations are sequential.'
|
||||
]};
|
||||
const main=frames[0],strings=snapshot.strings??[],str=index=>strings[index]??'';
|
||||
const document=snapshot.documents?.find(value=>str(value.frameId)===main.frame_id);
|
||||
const stable=async()=>{const frame=(await send('Page.getFrameTree',{})).frameTree?.frame;if(!frame||frame.id!==main.frame_id||await authorizedElectronFile(frame.url)!==main.file_path)throw Error('frame_changed');};
|
||||
for(const selector of selectors.slice(0,MAX_ASSETS)){
|
||||
const item={selector,frame_id:main.frame_id,status:'unknown',reason:'unavailable'};
|
||||
result.items.push(item);
|
||||
try{
|
||||
await stable();
|
||||
if(!document||await authorizedElectronFile(str(document.documentURL))!==main.file_path)throw Error('snapshot_frame_unavailable');
|
||||
const root=(await send('DOM.getDocument',{depth:0,pierce:false})).root;
|
||||
const ids=(await send('DOM.querySelectorAll',{nodeId:root.nodeId,selector})).nodeIds??[];
|
||||
if(ids.length!==1)throw Error(ids.length?'selector_ambiguous':'selector_absent');
|
||||
const node=(await send('DOM.describeNode',{nodeId:ids[0],depth:0,pierce:false})).node;
|
||||
if(node?.nodeName!=='IMG')throw Error('selected_node_not_img');
|
||||
const raw=document.nodes??{},index=raw.backendNodeId?.indexOf(node.backendNodeId)??-1;
|
||||
if(index<0||str(raw.nodeName?.[index])!=='IMG')throw Error('snapshot_identity_unavailable');
|
||||
item.node_index=index;item.backend_node_id=node.backendNodeId;
|
||||
let excluded=false;
|
||||
for(let n=index;n>=0;n=raw.parentIndex?.[n]??-1){const tag=str(raw.nodeName?.[n]),attrs=raw.attributes?.[n]??[];if(/^(INPUT|TEXTAREA|SELECT|SCRIPT|STYLE)$/.test(tag))excluded=true;for(let a=0;a<attrs.length;a+=2)if(str(attrs[a])==='contenteditable'&&str(attrs[a+1])!=='false')excluded=true;}
|
||||
if(excluded)throw Error('editable_subtree_excluded');
|
||||
const layoutIndex=document.layout?.nodeIndex?.indexOf(index)??-1,bounds=document.layout?.bounds?.[layoutIndex];
|
||||
item.visibility={status:'unknown',reason:'no_visible_layout'};
|
||||
if(layoutIndex>=0&&bounds&&bounds[2]>0&&bounds[3]>0)try{
|
||||
const viewport=(await send('Page.getLayoutMetrics',{})).cssLayoutViewport;
|
||||
const x=bounds[0]-(viewport.pageX??document.scrollOffsetX??0),y=bounds[1]-(viewport.pageY??document.scrollOffsetY??0);
|
||||
const left=Math.max(0,x),top=Math.max(0,y),right=Math.min(viewport.clientWidth,x+bounds[2]),bottom=Math.min(viewport.clientHeight,y+bounds[3]);
|
||||
if(right<=left||bottom<=top)item.visibility={status:'unknown',reason:'outside_visible_viewport'};
|
||||
else{
|
||||
item.visibility={status:'covered',reason:'sampled_points_hit_other_layers'};item.visibility_samples=[];
|
||||
for(const [fx,fy] of [[.5,.05],[.5,.5],[.5,.95],[.05,.05],[.95,.05],[.05,.5],[.95,.5],[.05,.95],[.95,.95]]){
|
||||
const point={x:Math.floor(left+(right-left)*fx),y:Math.floor(top+(bottom-top)*fy)};
|
||||
const hit=await send('DOM.getNodeForLocation',{...point,includeUserAgentShadowDOM:false,ignorePointerEventsNone:true});
|
||||
const hitIndex=raw.backendNodeId?.indexOf(hit.backendNodeId)??-1,hitAttrs=raw.attributes?.[hitIndex]??[];
|
||||
let hitClass='';for(let a=0;a<hitAttrs.length;a+=2)if(str(hitAttrs[a])==='class')hitClass=str(hitAttrs[a+1]).slice(0,500);
|
||||
item.visibility_samples.push({...point,...(hit.backendNodeId?{hit_backend_node_id:hit.backendNodeId}:{}),...(hitIndex>=0?{hit_node_index:hitIndex,hit_node_name:str(raw.nodeName?.[hitIndex]),hit_class:hitClass}:{})});
|
||||
if(hit.backendNodeId===node.backendNodeId){item.visibility={status:'observed'};break;}
|
||||
}
|
||||
}
|
||||
}catch{item.visibility={status:'unknown',reason:'hit_test_unavailable'};}
|
||||
const current=raw.currentSourceURL,at=current?.index?.indexOf(index)??-1;
|
||||
let source=at>=0?str(current.value?.[at]):'',selection='current_source_url';
|
||||
if(!source){selection='src';const attrs=raw.attributes?.[index]??[];for(let a=0;a<attrs.length;a+=2)if(str(attrs[a])==='src')source=str(attrs[a+1]);}
|
||||
if(!source)throw Error('source_url_unavailable');
|
||||
const selectedUrl=new URL(source,str(document.documentURL)),url=new URL(selectedUrl);url.hash='';
|
||||
const selectedAttributes={},nodeAttrs=raw.attributes?.[index]??[];
|
||||
for(let a=0;a<nodeAttrs.length;a+=2){const name=str(nodeAttrs[a]),value=str(nodeAttrs[a+1]);if(['class','id','width','height','loading','decoding','alt'].includes(name))selectedAttributes[name]=value.slice(0,500);else if(name==='src'){try{selectedAttributes.src=safeSourceUrl(new URL(value,str(document.documentURL)).href)??'[URL omitted]';}catch{selectedAttributes.src='[URL unavailable]';}}}
|
||||
item.selected_node={node_name:'IMG',attributes:selectedAttributes,current_source_url:at>=0?safeSourceUrl(str(current.value?.[at])):null};
|
||||
const tree=(await send('Page.getResourceTree',{})).frameTree;
|
||||
if(!tree?.frame||tree.frame.id!==main.frame_id||await authorizedElectronFile(tree.frame.url)!==main.file_path)throw Error('frame_changed');
|
||||
const resource=tree.resources?.find(value=>value.url===url.href&&value.type==='Image'&&!value.failed&&!value.canceled);
|
||||
if(!resource)throw Error('selected_source_not_loaded_in_main_frame');
|
||||
const contentType=mime(resource.mimeType?.toLowerCase());
|
||||
item.source={url:safeSourceUrl(selectedUrl.href),url_sha256:sha(selectedUrl.href),selection,mime_type:contentType??'unknown'};
|
||||
if(!types.has(contentType))throw Error('unsupported_image_type');
|
||||
if(resource.contentSize>MAX_ASSET_BYTES)throw Error('asset_byte_budget_exceeded');
|
||||
const content=await send('Page.getResourceContent',{frameId:main.frame_id,url:resource.url});
|
||||
if(typeof content.content!=='string'||!content.base64Encoded)throw Error('binary_content_unavailable');
|
||||
if(content.content.length>4*Math.ceil(MAX_ASSET_BYTES/3))throw Error('asset_byte_budget_exceeded');
|
||||
const bytes=decodeCanonicalBase64(content.content);
|
||||
if(!bytes?.length)throw Error('invalid_base64');
|
||||
if(bytes.length>MAX_ASSET_BYTES)throw Error('asset_byte_budget_exceeded');
|
||||
if(result.total_bytes+bytes.length>MAX_TOTAL_BYTES)throw Error('total_byte_budget_exceeded');
|
||||
if(sniff(bytes)!==contentType)throw Error('image_signature_mismatch');
|
||||
await stable();
|
||||
item.status='exported';delete item.reason;
|
||||
item.artifact={mime_type:contentType,bytes:bytes.length,sha256:sha(bytes),data_base64:bytes.toString('base64')};
|
||||
result.total_bytes+=bytes.length;
|
||||
}catch(error){const allowed=new Set(['frame_changed','snapshot_frame_unavailable','selector_ambiguous','selector_absent','selected_node_not_img','snapshot_identity_unavailable','editable_subtree_excluded','no_visible_layout','outside_visible_viewport','visibility_unconfirmed_or_covered','source_url_unavailable','selected_source_not_loaded_in_main_frame','unsupported_image_type','asset_byte_budget_exceeded','binary_content_unavailable','invalid_base64','total_byte_budget_exceeded','image_signature_mismatch']);item.reason=allowed.has(error.message)?error.message:'protocol_resource_unavailable';}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in new issue
Block a user