Add proof-backed parser byte reads and preserve retryable source failures
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import {readImportRange,isImportByteFailure,type ImportByteReader} from './import-byte-reader.ts';
|
||||
import type {ImportLine,ImportSource} from './imports.ts';
|
||||
import {createImportBudget,type ImportScheduling} from './import-budget.ts';
|
||||
import {openDocumentImportSource} from './import-document.ts';
|
||||
@@ -6,7 +7,7 @@ import {fault,validJson} from '../canvas-core/values.ts';
|
||||
type Range={start:number;end:number};
|
||||
type ScanCache={start:number;bytes:Uint8Array};
|
||||
type Canvas={id:string;parent:null|{canvasId:string;entryElementId:string}};
|
||||
type Options=ImportScheduling&{bytes:Blob;fingerprint:string;signal:AbortSignal;maxRecordBytes?:number};
|
||||
type Options=ImportScheduling&{bytes:Blob;fingerprint:string;readBytes?:ImportByteReader;signal:AbortSignal;maxRecordBytes?:number};
|
||||
const allowed=new Set(['documentFormatVersion','projectId','canvases','elements','tombstones']);
|
||||
const whitespace=(byte:number)=>byte===9||byte===10||byte===13||byte===32;
|
||||
|
||||
@@ -17,7 +18,7 @@ function scanner(options:Options,signal:AbortSignal,start=0,cache:ScanCache={sta
|
||||
const check=()=>{if(signal.aborted)throw fault('import_aborted');};
|
||||
async function peek(){
|
||||
check();if(cursor>=bytes.size)return -1;
|
||||
if(cursor<cache.start||cursor>=cache.start+cache.bytes.length){const loaded=new Uint8Array(await bytes.slice(cursor,cursor+16384).arrayBuffer());cache.start=cursor;cache.bytes=loaded;check();}
|
||||
if(cursor<cache.start||cursor>=cache.start+cache.bytes.length){const loaded=await readImportRange(bytes,options.readBytes,cursor,cursor+16384,signal);cache.start=cursor;cache.bytes=loaded;check();}
|
||||
return cache.bytes[cursor-cache.start];
|
||||
}
|
||||
function step(){cursor++;return budget.step();}
|
||||
@@ -40,8 +41,8 @@ function scanner(options:Options,signal:AbortSignal,start=0,cache:ScanCache={sta
|
||||
}
|
||||
async function decode(range:Range,limit=options.maxRecordBytes??256*1024):Promise<any>{
|
||||
check();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(error){check();throw fault('invalid_utf8');}
|
||||
const data=await readImportRange(bytes,options.readBytes,range.start,range.end,signal);let text:string;
|
||||
try{text=new TextDecoder('utf-8',{fatal:true}).decode(data);}catch(error){check();throw fault('invalid_utf8');}
|
||||
check();try{const value=JSON.parse(text);if(!validJson(value))throw Error();return value;}catch{throw fault('invalid_json');}
|
||||
}
|
||||
return{peek,step,spaces,take,value,decode,position:()=>cursor};
|
||||
@@ -124,7 +125,7 @@ export async function openStreamingDocumentImportSource(input:Options):Promise<I
|
||||
if(!Number.isSafeInteger(position)||position<0)throw fault('invalid_import_cursor');
|
||||
const elementIndex=position-children.length;
|
||||
while(elementIndex>=ranges.length&&phase!=='done'){
|
||||
try{await scanNext(readSignal);}catch(error){if(readSignal.aborted)throw fault('import_aborted');failure=(error as {code?:string}).code??'invalid_document_import';phase='done';}
|
||||
try{await scanNext(readSignal);}catch(error){if(readSignal.aborted)throw fault('import_aborted');if(isImportByteFailure(error))throw error;failure=(error as {code?:string}).code??'invalid_document_import';phase='done';}
|
||||
}
|
||||
const count=children.length+ranges.length;
|
||||
if(position>=count){
|
||||
@@ -137,7 +138,7 @@ export async function openStreamingDocumentImportSource(input:Options):Promise<I
|
||||
const kind=position<children.length?'canvas':'element',record=kind==='canvas'?children[position]:await scanner(options,readSignal).decode(ranges[elementIndex]);
|
||||
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'};}
|
||||
}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'};}
|
||||
}
|
||||
return Object.freeze({fingerprint:options.fingerprint,parserVersion:5,read(position:number,readSignal:AbortSignal){
|
||||
// A slow future scan must not block re-reading a pending unit for commit/recovery.
|
||||
|
||||
Reference in New Issue
Block a user