Add proof-backed parser byte reads and preserve retryable source failures
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
import {readImportRange,isImportByteFailure,type ImportByteReader} from './import-byte-reader.ts';
|
||||
import {createImportBudget,type ImportScheduling} from './import-budget.ts';
|
||||
import type {ImportSource,ImportLine} from './imports.ts';
|
||||
import {fault,validJson} from '../canvas-core/values.ts';
|
||||
type Range={start:number;end:number};
|
||||
/** Index the envelope without decoding element payloads. Original bytes remain the source.
|
||||
* Preparation scans O(bytes) and retains O(records) ranges; this is not constant memory. */
|
||||
export async function openDocumentImportSource(options:{bytes:Blob;fingerprint:string;signal:AbortSignal;maxRecordBytes?:number}&ImportScheduling):Promise<ImportSource>{
|
||||
export async function openDocumentImportSource(options:{bytes:Blob;fingerprint:string;readBytes?:ImportByteReader;signal:AbortSignal;maxRecordBytes?:number}&ImportScheduling):Promise<ImportSource>{
|
||||
const {bytes,signal}=options,max=options.maxRecordBytes??256*1024;
|
||||
if(!(bytes instanceof Blob)||bytes.size>64*1024*1024||!options.fingerprint||!Number.isSafeInteger(max)||max<1)throw fault('invalid_import_source');
|
||||
let cursor=0,cache={start:-1,bytes:new Uint8Array()};const budget=createImportBudget(signal,options);
|
||||
const space=(b:number)=>[9,10,13,32].includes(b);
|
||||
async function peek(){if(signal.aborted)throw fault('import_aborted');if(cursor>=bytes.size)return -1;if(cursor<cache.start||cursor>=cache.start+cache.bytes.length){cache={start:cursor,bytes:new Uint8Array(await bytes.slice(cursor,cursor+16384).arrayBuffer())};if(signal.aborted)throw fault('import_aborted');}return cache.bytes[cursor-cache.start];}
|
||||
async function peek(){if(signal.aborted)throw fault('import_aborted');if(cursor>=bytes.size)return -1;if(cursor<cache.start||cursor>=cache.start+cache.bytes.length){cache={start:cursor,bytes:await readImportRange(bytes,options.readBytes,cursor,cursor+16384,signal)};if(signal.aborted)throw fault('import_aborted');}return cache.bytes[cursor-cache.start];}
|
||||
function step(){cursor++;return budget.step();}
|
||||
async function spaces(){while(space(await peek()))await step();}
|
||||
async function take(byte:number){await spaces();if(await peek()!==byte)throw fault('invalid_document_import');await step();}
|
||||
@@ -19,7 +20,7 @@ export async function openDocumentImportSource(options:{bytes:Blob;fingerprint:s
|
||||
const ended=!quoted&&stack.length===0&&(b===34||b===93||b===125);await step();if(ended)break;
|
||||
}if(cursor===start)throw fault('invalid_document_import');return{start,end:cursor};
|
||||
}
|
||||
async function decode(range:Range,limit=max){if(range.end-range.start>limit)throw fault('record_too_large');let text:string;try{text=new TextDecoder('utf-8',{fatal:true}).decode(await bytes.slice(range.start,range.end).arrayBuffer());}catch{throw fault('invalid_utf8');}try{const result=JSON.parse(text);if(!validJson(result))throw Error();return result;}catch{throw fault('invalid_json');}}
|
||||
async function decode(range:Range,limit=max,readSignal=signal){if(range.end-range.start>limit)throw fault('record_too_large');const data=await readImportRange(bytes,options.readBytes,range.start,range.end,readSignal);let text:string;try{text=new TextDecoder('utf-8',{fatal:true}).decode(data);}catch{throw fault('invalid_utf8');}try{const result=JSON.parse(text);if(!validJson(result))throw Error();return result;}catch{throw fault('invalid_json');}}
|
||||
const fields=new Map<string,unknown>(),elements:Range[]=[];
|
||||
await take(123);await spaces();if(await peek()===125)throw fault('invalid_document_import');
|
||||
while(true){const key=await decode(await value(),1024);if(typeof key!=='string'||fields.has(key))throw fault('invalid_document_import');await take(58);
|
||||
@@ -38,6 +39,6 @@ export async function openDocumentImportSource(options:{bytes:Blob;fingerprint:s
|
||||
return Object.freeze({fingerprint:options.fingerprint,parserVersion:4,async read(position:number,readSignal:AbortSignal):Promise<ImportLine|null>{
|
||||
if(readSignal.aborted)throw fault('import_aborted');if(!Number.isSafeInteger(position)||position<0||position>count)throw fault('invalid_import_cursor');if(position===count)return null;
|
||||
const base={cursor:position,nextCursor:position+1,key:`record:${position}`};let record:any,kind:string;
|
||||
try{if(position<children.length){kind='canvas';record=children[position];}else{kind='element';record=await decode(elements[position-children.length]);}if(readSignal.aborted)throw fault('import_aborted');if(!record||typeof record!=='object'||Array.isArray(record)||typeof record.id!=='string'||!record.id)throw fault('invalid_native_import_record');return{...base,value:{kind,key:record.id,record,sourceRootId:rootId}};}catch(error){if(readSignal.aborted)throw fault('import_aborted');return{...base,error:(error as {code?:string}).code??'invalid_document_import'};}
|
||||
try{if(position<children.length){kind='canvas';record=children[position];}else{kind='element';record=await decode(elements[position-children.length],max,readSignal);}if(readSignal.aborted)throw fault('import_aborted');if(!record||typeof record!=='object'||Array.isArray(record)||typeof record.id!=='string'||!record.id)throw fault('invalid_native_import_record');return{...base,value:{kind,key:record.id,record,sourceRootId:rootId}};}catch(error){if(readSignal.aborted)throw fault('import_aborted');if(isImportByteFailure(error))throw error;return{...base,error:(error as {code?:string}).code??'invalid_document_import'};}
|
||||
}});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user