From c8e5f6ac492a2e452679d3cddc989de58e1d5bd2 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Tue, 10 Nov 2026 15:13:27 +0800 Subject: [PATCH] Revert "Reuse immutable JSON validation proofs without weakening publication checks" This reverts commit 697716ee894a751897bf7d9c65589e7e65833d01. --- headless/README.md | 7 ------- src/lib/canvas-core/values.ts | 32 ++---------------------------- tests/headless/valid-json.test.mjs | 28 -------------------------- 3 files changed, 2 insertions(+), 65 deletions(-) delete mode 100644 tests/headless/valid-json.test.mjs diff --git a/headless/README.md b/headless/README.md index efdf10f..7201459 100644 --- a/headless/README.md +++ b/headless/README.md @@ -855,10 +855,3 @@ step. The existing recursive codec still checks nested mutability and depth, so mutable values and external Maps are re-read. This is an encoding cache, not a second state table; recovery JSON order, formats and journal checks remain the same. Replacing a table gets a new entry view and leaves prior images unchanged. - -JSON validation can reuse a successful proof for transitively immutable data -properties. A WeakMap stores subtree height so a cached value still obeys the -caller's depth bound. Shallow freezing, accessors, mutable descendants and -unproven array methods keep the original validation path; optional reflection -failures only disable proof reuse. Component/schema validators are still called, -and document ownership, endpoints and topology are still checked on publication. diff --git a/src/lib/canvas-core/values.ts b/src/lib/canvas-core/values.ts index d26a371..480dc91 100644 --- a/src/lib/canvas-core/values.ts +++ b/src/lib/canvas-core/values.ts @@ -46,38 +46,10 @@ export function readSet(input: Iterable): ReadonlyValueSet { }; return Object.freeze(view); } -// Successful proofs only: frozen data properties whose children have their own -// immutable proof. Neither shallow freeze nor a user-defined Array.every qualifies. -const jsonHeights=new WeakMap(),nativeEvery=Array.prototype.every; -function jsonShape(value:object,array:boolean,every:unknown){ - try{const own=array?Object.getOwnPropertyDescriptor(value,'every'):undefined;return Array.prototype.every===nativeEvery&&(!array||Object.getPrototypeOf(value)===Array.prototype&&!!own&&Object.hasOwn(own,'value')&&own.value===nativeEvery&&every===nativeEvery);} - catch{return false;} -} -function jsonHeight(value:unknown):number|undefined{ - if(value===null||typeof value==='string'||typeof value==='boolean'||typeof value==='number'&&Number.isFinite(value))return 0; - return value&&typeof value==='object'?jsonHeights.get(value):undefined; -} export function validJson(value: unknown, depth = 0): value is Json { if (depth > 64) return false; if (value === null || typeof value === 'string' || typeof value === 'boolean') return true; if (typeof value === 'number') return Number.isFinite(value); - if(!value||typeof value!=='object')return false; - const array=Array.isArray(value); - if(!array&&Object.getPrototypeOf(value)!==Object.prototype)return false; - const every=array?value.every:undefined,ordinary=jsonShape(value,array,every); - const proven=ordinary?jsonHeights.get(value):undefined; - if(proven!==undefined)return !(depth+proven>64); - const valid=array?Reflect.apply(every!,value,[(item:unknown)=>validJson(item,depth+1)]):Object.values(value).every(child=>validJson(child,depth+1)); - if(valid&&ordinary)try{if(Object.isFrozen(value)){ - let height=0,immutable=true; - const keys=array?undefined:Object.keys(value); - for(let index=0;index<(array?value.length:keys!.length);index++){ - const key=array?String(index):keys![index]; - const descriptor=Object.getOwnPropertyDescriptor(value,key),child=descriptor&&Object.hasOwn(descriptor,'value')?jsonHeight(descriptor.value):undefined; - if(child===undefined){immutable=false;break;} - height=Math.max(height,child+1); - } - if(immutable)jsonHeights.set(value,height); - }}catch{/* Optional proof inspection cannot reject an otherwise valid value. */} - return valid; + if (Array.isArray(value)) return value.every(item => validJson(item, depth + 1)); + return !!value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype && Object.values(value).every(child => validJson(child, depth + 1)); } diff --git a/tests/headless/valid-json.test.mjs b/tests/headless/valid-json.test.mjs deleted file mode 100644 index 4f28c3e..0000000 --- a/tests/headless/valid-json.test.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import {test} from 'node:test'; -import assert from 'node:assert/strict'; -import {validJson,freeze} from '../../src/lib/canvas-core/values.ts'; -test('transitively immutable JSON can reuse its proof without enumerating the same object again',()=>{ - const value=freeze({data:{text:'safe'},meta:{number:1,flag:true,empty:null}});assert.equal(validJson(value),true);const values=Object.values;try{Object.values=object=>{if(object===value)throw Error('immutable root enumerated twice');return values(object);};assert.equal(validJson(value),true);}finally{Object.values=values;} -}); -test('a cached subtree still observes the validation depth limit at each new position',()=>{ - const leaf=freeze({value:1});assert.equal(validJson(leaf),true);const nest=count=>{let value=leaf;while(count--)value=Object.freeze({child:value});return value;};assert.equal(validJson(nest(63)),true);assert.equal(validJson(nest(64)),false);assert.equal(validJson(leaf,64),false);assert.equal(validJson(Object.freeze({}),64),true); -}); -test('shallow freeze and getters do not hide mutable values or bypass subsequent validation',()=>{ - const child={value:1},parent=Object.freeze({child});assert.equal(validJson(parent),true);child.value=undefined;assert.equal(validJson(parent),false);child.value=2;assert.equal(validJson(parent),true); - let field=1,calls=0;const getter=Object.freeze({get value(){calls++;return field;}});assert.equal(validJson(getter),true);field=Infinity;assert.equal(validJson(getter),false);assert.equal(calls,2); -}); -test('sparse/inherited array entries and overridden every methods keep their observable behavior',()=>{ - const prototype=Object.create(Array.prototype);prototype[0]=1;const sparse=new Array(1);Object.setPrototypeOf(sparse,prototype);Object.freeze(sparse);assert.equal(validJson(sparse),true);prototype[0]=undefined;assert.equal(validJson(sparse),false); - let accepted=true,calls=0;const custom=[1];Object.defineProperty(custom,'every',{value(){calls++;return accepted;}});Object.freeze(custom);assert.equal(validJson(custom),true);accepted=false;assert.equal(validJson(custom),false);assert.equal(calls,2); - let native=true,methodReads=0;const getterArray=[1];Object.defineProperty(getterArray,'every',{get(){methodReads++;return native?Array.prototype.every:()=>false;}});Object.freeze(getterArray);const parent=Object.freeze({getterArray});assert.equal(validJson(parent),true);native=false;assert.equal(validJson(parent),false);assert.equal(methodReads,2); - const hole=Object.freeze(new Array(1));assert.equal(validJson(hole),true);assert.equal(validJson(hole,64),true); -}); -test('ordinary invalid JSON, cycles and unsupported prototypes remain invalid',()=>{ - const cycle={};cycle.self=cycle;for(const value of [undefined,NaN,Infinity,()=>{},Symbol('x'),1n,{value:undefined},[undefined],new Date(),Object.create(null),cycle])assert.equal(validJson(value),false);assert.equal(validJson({text:'中文',number:0,empty:[]}),true); -}); - -test('proxy reflection failures and mutable inherited array methods cannot produce an immutable proof',()=>{ - const target={value:1},proxy=new Proxy(target,{isExtensible(){throw Error('optional inspection denied');}});assert.equal(validJson(proxy),true);target.value=undefined;assert.equal(validJson(proxy),false); - const arrayTarget=Object.freeze([1]);let accepts=true;const arrayProxy=new Proxy(arrayTarget,{get(target,key,receiver){if(key==='every')return accepts?Array.prototype.every:()=>false;return Reflect.get(target,key,receiver);}}),parent=Object.freeze({arrayProxy});assert.equal(validJson(parent),true);accepts=false;assert.equal(validJson(parent),false); - const other=new Proxy([1],{getPrototypeOf(){throw Error('no shape proof');}});assert.equal(validJson(other),true); -});