w-serv-orm 1.1.12 → 1.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,10 @@
1
1
  import assert from 'assert'
2
2
  import vm from 'vm'
3
+ import { createSecretKey } from 'crypto'
3
4
  import util from 'util'
4
5
  import WServOrm from '../src/WServOrm.mjs'
5
6
  import getMapOrm from '../src/getMapOrm.mjs'
6
- import { createStub, getReject, quiet, same } from './tools/ormStub.mjs'
7
+ import { createStub, getReject, quiet, same, mkForgedObjProto as mkForged12 } from './tools/ormStub.mjs'
7
8
 
8
9
 
9
10
  //R-OBJ-01 使用端物件之迭代與複製(L01-L04、P01-P03)、R-INPUT-01 規則3 之 select 與 delAll(D01-D03)
@@ -45,9 +46,9 @@ describe('R-OBJ-01 使用端物件之迭代與複製', function() {
45
46
  [{ length: 0, name: 'alpha' }],
46
47
  [{ id: 'a1', length: 3 }],
47
48
  [{ id: 5, length: 2 }, { id: '5', length: 2 }],
48
- [{ $and: [{ length: 1, name: 'x' }], $or: [{ length: 2 }, { id: 7, length: 1 }], $nor: [{ id: 'z', length: 3 }] }, { $and: [{ length: 1, name: 'x' }], $or: [{ length: 2 }, { id: '7', length: 1 }], $nor: [{ id: 'z', length: 3 }] }],
49
+ [{ $and: [{ length: 1, name: 'x' }], $or: [{ length: 2 }, { id: 7, length: 1 }] }, { $and: [{ length: 1, name: 'x' }], $or: [{ length: 2 }, { id: '7', length: 1 }] }],
49
50
  [{ id: { length: 2, $in: [1, 2] } }, { id: { length: 2, $in: ['1', '2'] } }],
50
- [{ id: { $not: { length: 1, $eq: 9 } } }, { id: { $not: { length: 1, $eq: '9' } } }],
51
+ [{ id: { length: 1, $eq: 9 } }, { id: { length: 1, $eq: '9' } }],
51
52
  [{ name: { length: 1, $regex: '^a' } }],
52
53
  //對照: 非陣列樣之length(非整數、負數、字串、null)修前修後皆原樣
53
54
  [{ length: 3.5, name: 'x' }],
@@ -231,6 +232,7 @@ describe('R-OBJ-01 規則3 自有__proto__鍵 fail-closed', function() {
231
232
 
232
233
  it('P01 find任何位置含自有__proto__鍵即reject, ORM未被呼叫, 訊息不回顯客戶端之值', async function() {
233
234
  //R-OBJ-01 規則3: 頂層、$and/$or/$nor、id之條件物件及其$not、他欄之值、陣列內之物件、深層
235
+ //檢查與運算子名稱無關(任何鍵之任何深度): 系列白名單外之$nor、$not雖由後端reject, 本檢查仍須先擋下含自有__proto__鍵者(R-WL-01 規則2)
234
236
  let finds = [
235
237
  '{"__proto__":{"x":1}}',
236
238
  '{"__proto__":"SECRET-VALUE"}',
@@ -350,12 +352,13 @@ describe('R-OBJ-01 規則3 自有__proto__鍵 fail-closed', function() {
350
352
  ['getter於id條件內', () => ({ id: getter({}, '$in', () => ['a']) }), 'unsupported accessor property'],
351
353
  ['setter', () => Object.defineProperty({}, 'name', { enumerable: true, set() {} }), 'unsupported accessor property'], // eslint-disable-line accessor-pairs
352
354
  ['getter回傳含__proto__之物件', () => getter({}, 'name', () => parse('{"__proto__":1}')), 'unsupported accessor property'],
353
- ['已撤銷之Proxy', () => revoked.proxy, 'unreadable value'],
355
+ //規則6起 find 內之Proxy(含已撤銷者)一律以 irregular reject, 且不觸發任何陷阱(寫入列之Proxy仍依下方 rows 歸入 unreadable value)
356
+ ['已撤銷之Proxy', () => revoked.proxy, 'unsupported irregular object or array'],
354
357
  ['ownKeys陷阱拋錯', () => new Proxy({}, {
355
358
  ownKeys() {
356
359
  throw new Error('SECRET-TRAP')
357
360
  }
358
- }), 'unreadable value'],
361
+ }), 'unsupported irregular object or array'],
359
362
  ]
360
363
  for (let { kind, proc } of getKinds()) {
361
364
  for (let mode of ['select', 'delAll']) {
@@ -397,7 +400,7 @@ describe('R-OBJ-01 規則3 自有__proto__鍵 fail-closed', function() {
397
400
  assert.deepStrictEqual(calls, [], 'ORM不應被呼叫')
398
401
  })
399
402
 
400
- it('P05 陣列本身含自有__proto__鍵(不論可否列舉)亦reject; 稀疏陣列之空洞照常', async function() {
403
+ it('P05 陣列本身含自有__proto__鍵(不論可否列舉)亦reject; 稀疏陣列之空洞於find改為reject(規則6)', async function() {
401
404
  //R-OBJ-01 規則3: 陣列以索引迭代之例外不及於「檢查該陣列本身有無自有__proto__鍵」(外審 sol F3)
402
405
  let mkArr = (enumerable) => Object.defineProperty([1, 2], '__proto__', { value: 'SECRET', enumerable })
403
406
  for (let { kind, proc } of getKinds()) {
@@ -413,11 +416,13 @@ describe('R-OBJ-01 規則3 自有__proto__鍵 fail-closed', function() {
413
416
  }
414
417
  assert.deepStrictEqual(calls, [], `${kind} ORM不應被呼叫`)
415
418
 
416
- //稀疏陣列之空洞: 無屬性描述子, 不視為不支援
419
+ //稀疏陣列之空洞: 規則6起整批reject(原照常交ORM): 空洞陣列之原型可帶資料索引, 複製之map以array[index]讀到繼承值而成為密陣列(外審第二輪 A-02)
417
420
  let sparse = [1, , 3] // eslint-disable-line no-sparse-arrays
418
- await proc('u1', 'tests', 'select', { id: { $in: sparse } })
419
- assert.strictEqual(calls.length, 1, `${kind} 稀疏陣列之條件照常交ORM`)
420
- stub.reset()
421
+ for (let mode of ['select', 'delAll']) {
422
+ let err = await quiet(() => getReject(proc('u1', 'tests', mode, { id: { $in: sparse } })))
423
+ assert.strictEqual(err, `invalid find of ${mode}, unsupported irregular object or array`, `${kind} ${mode} 稀疏陣列`)
424
+ }
425
+ assert.deepStrictEqual(calls, [], `${kind} ORM不應被呼叫`)
421
426
  }
422
427
  })
423
428
 
@@ -458,8 +463,9 @@ describe('R-OBJ-01 規則3 自有__proto__鍵 fail-closed', function() {
458
463
  assert.deepStrictEqual(calls, [], 'ORM不應被呼叫')
459
464
  })
460
465
 
461
- it('P07 偽造Symbol.toStringTag為Array之物件不當陣列走訪: 不呼叫其length getter', async function() {
466
+ it('P07 偽造Symbol.toStringTag為Array之物件不當陣列走訪: 不呼叫其length getter(規則6起整批reject)', async function() {
462
467
  //R-OBJ-01 規則3(陣列之判定用Array.isArray): isarr以Object.prototype.toString判定可被偽造, 走訪時讀其length(外審 opus 二輪 F6)
468
+ //規則6(2026-10-19): 原照常交ORM, 現以 irregular reject(isarr與Array.isArray不一致者, 轉型表之lodash map會讀其length並複製成真陣列); 仍不呼叫length getter
463
469
  let reads = 0
464
470
  let fake = { [Symbol.toStringTag]: 'Array' }
465
471
  Object.defineProperty(fake, 'length', {
@@ -471,15 +477,19 @@ describe('R-OBJ-01 規則3 自有__proto__鍵 fail-closed', function() {
471
477
  })
472
478
  for (let { kind, proc } of getKinds()) {
473
479
  for (let mode of ['select', 'delAll']) {
474
- await proc('u1', 'tests', mode, { name: { $eq: fake } })
480
+ for (let find of [{ name: { $eq: fake } }, { id: { $in: fake } }, { id: { $eq: fake } }]) {
481
+ let err = await quiet(() => getReject(proc('u1', 'tests', mode, find)))
482
+ assert.strictEqual(err, `invalid find of ${mode}, unsupported irregular object or array`, `${kind} ${mode} ${JSON.stringify(Object.keys(find))}`)
483
+ }
475
484
  }
476
485
  assert.strictEqual(reads, 0, `${kind} length getter不應被呼叫`)
486
+ assert.deepStrictEqual(calls, [], `${kind} ORM不應被呼叫`)
477
487
  }
478
488
  })
479
489
 
480
- it.skip('K01 [wsemi已知缺陷, 待升版後取消skip] 欄位值內具整數length鍵之巢狀物件, 其內字串亦須被清除且不新增索引鍵', async function() {
481
- //R-OBJ-01 已知限制: wsemi 1.9.17 之 clearXSS 以 lodash each 遞迴巢狀物件, {length:3,a:'<script>'} 之 a 不被清除並新增 0、1、2 鍵,
482
- //length 極大時更於複本建立同數量之屬性而耗盡記憶體(外審 opus #1); 本套件不於專案端繞道, 見 建議wsemi修正.md
490
+ it('K01 欄位值內具整數length鍵之巢狀物件, 其內字串亦須被清除且不新增索引鍵', async function() {
491
+ //R-OBJ-01: wsemi 1.9.17 以前之 clearXSS 以 lodash each 遞迴巢狀物件, {length:3,a:'<script>'} 之 a 不被清除並新增 0、1、2 鍵,
492
+ //length 極大時更於複本建立同數量之屬性而耗盡記憶體(外審 opus #1); wsemi 1.9.18 已修, 本案自此啟用
483
493
  let so = WServOrm(dsStr, WOrmStub, 'u', 'd')
484
494
  await so.procOrm('u1', 'tests', 'insert', [{ id: 'k1', meta: { length: 3, a: '<script>alert(1)</script>' } }])
485
495
  same(calls[0].data[0].meta, { length: 3, a: '&lt;script>alert(1)&lt;/script>' }, '巢狀物件之清除')
@@ -608,6 +618,591 @@ describe('R-INPUT-01 規則3 select與delAll之find須為物件', function() {
608
618
  })
609
619
 
610
620
 
621
+ describe('R-OBJ-01 規則6 find之複本須與原物件等價', function() {
622
+
623
+ beforeEach(function() {
624
+ stub.reset()
625
+ })
626
+
627
+ let hide = (o, k, v) => Object.defineProperty(o, k, { value: v, enumerable: false, configurable: true, writable: true })
628
+ class Cls {
629
+ constructor(o) {
630
+ Object.assign(this, o)
631
+ }
632
+ }
633
+ class TagG {
634
+ get [Symbol.toStringTag]() {
635
+ return 'Object'
636
+ }
637
+ }
638
+ //偽原型之constructor自洽: 鍵名同Object.prototype, constructor 為另一函數且其 prototype 指回偽原型(通過鍵名與指回檢查, 只有原始碼比對能辨)
639
+ let mkSelfCtor = () => {
640
+ let p = mkForged12()
641
+ let F = function() {}
642
+ F.prototype = p
643
+ Object.defineProperty(p, 'constructor', { value: F, enumerable: false, configurable: true, writable: true })
644
+ return p
645
+ }
646
+ //KeyObject(原型改為null): 無自有鍵、可結構化複製、不在 util.types 品牌清單內, 專供驗證 structuredClone 網
647
+ let keyObj = () => Object.setPrototypeOf(createSecretKey(Buffer.alloc(16)), null)
648
+ //包 k 層 {$and:[…]}: 容器層數增 2k(根為第 1 層)
649
+ let wrapAnd = (inner, k) => {
650
+ for (let i = 0; i < k; i++) {
651
+ inner = { $and: [inner] }
652
+ }
653
+ return inner
654
+ }
655
+ let msgHidden = 'unsupported symbol key or non-enumerable property'
656
+ let msgIrreg = 'unsupported irregular object or array'
657
+ let fakeArr = (a) => Object.assign({ length: a.length, [Symbol.toStringTag]: 'Array' }, a) //宣稱Symbol.toStringTag為Array: isarr為真而Array.isArray為假
658
+
659
+ it('D05 find內有Symbol鍵、不可列舉鍵、非純物件、不規則陣列: select與delAll皆整批reject固定字串, ORM未被呼叫(四種資料表型態)', async function() {
660
+ //R-OBJ-01 規則6(hidden、irregular): 字串型主鍵表之複本(mapOwn、lodash map)洗掉這些結構, 令後端白名單版本會reject之條件變成較寬或無條件(外審 sol F1)
661
+ let cases = [
662
+ //hidden: 物件節點之Symbol鍵與不可列舉鍵, 位置含頂層、$and/$or元素、巢狀、id運算子物件、非id欄位之運算子物件
663
+ ['頂層Symbol鍵(僅此鍵)', () => ({ [Symbol('s')]: 1 }), msgHidden],
664
+ ['頂層不可列舉id(僅此鍵)', () => hide({}, 'id', 'a1'), msgHidden],
665
+ ['頂層Symbol鍵並有其他鍵', () => Object.assign({ id: 'a1' }, { [Symbol('s')]: 1 }), msgHidden],
666
+ ['頂層另有不可列舉鍵', () => hide({ id: 'a1' }, 'name', 'x'), msgHidden],
667
+ ['$and元素Symbol鍵', () => ({ $and: [Object.assign({ id: 'a1' }, { [Symbol('s')]: 1 })] }), msgHidden],
668
+ ['$or元素不可列舉鍵', () => ({ $or: [hide({ id: 'a1' }, 'zz', 1)] }), msgHidden],
669
+ ['巢狀$and→$or元素Symbol鍵', () => ({ $and: [{ $or: [Object.assign({ name: 'x' }, { [Symbol('s')]: 1 })] }] }), msgHidden],
670
+ ['id運算子物件Symbol鍵', () => ({ id: Object.assign({ $eq: 'a1' }, { [Symbol('s')]: 1 }) }), msgHidden],
671
+ ['id運算子物件之$in為不可列舉', () => ({ id: hide({}, '$in', ['a1']) }), msgHidden],
672
+ ['非id欄位運算子物件不可列舉鍵', () => ({ name: hide({ $eq: 'x' }, 'zz', 1) }), msgHidden],
673
+ ['null原型物件之Symbol鍵', () => Object.assign(Object.create(null), { id: 'a1', [Symbol('s')]: 1 }), msgHidden],
674
+ //irregular: 巢狀位置之非純物件(頂層由 need an object 先擋)
675
+ ['$and元素為類別實例', () => ({ $and: [new Cls({ id: 'a1' })] }), msgIrreg],
676
+ ['$or元素為Object.create(proto)', () => ({ $or: [Object.create({ id: 'a1' })] }), msgIrreg],
677
+ ['id運算子物件為類別實例', () => ({ id: new Cls({ $in: ['a1'] }) }), msgIrreg],
678
+ ['非id欄位運算子物件為類別實例', () => ({ name: new Cls({ $eq: 'x' }) }), msgIrreg],
679
+ ['巢狀元素為偽造標籤之Map', () => ({ $and: [Object.defineProperty(new Map([['id', 'a1']]), Symbol.toStringTag, { value: 'Object' })] }), msgIrreg],
680
+ //irregular: 陣列節點
681
+ ['$in陣列額外可列舉屬性', () => ({ id: { $in: Object.assign(['a1'], { extra: 1 }) } }), msgIrreg],
682
+ ['$in陣列額外不可列舉屬性', () => ({ id: { $in: hide(['a1'], 'extra', 1) } }), msgIrreg],
683
+ ['$in陣列Symbol鍵', () => ({ id: { $in: Object.assign(['a1'], { [Symbol('s')]: 1 }) } }), msgIrreg],
684
+ ['$in陣列不可列舉元素', () => ({ id: { $in: hide(['a1'], '0', 'a1') } }), msgIrreg],
685
+ ['$nin陣列額外屬性(非id欄位)', () => ({ name: { $nin: Object.assign(['x'], { extra: 1 }) } }), msgIrreg],
686
+ ['$and陣列額外屬性', () => ({ $and: Object.assign([{ id: 'a1' }], { extra: 1 }) }), msgIrreg],
687
+ ['$or陣列Symbol鍵', () => ({ $or: Object.assign([{ id: 'a1' }], { [Symbol('s')]: 1 }) }), msgIrreg],
688
+ ['$and陣列不可列舉元素', () => ({ $and: hide([{ id: 'a1' }], '0', { id: 'a1' }) }), msgIrreg],
689
+ ['空洞並有額外屬性', () => ({ id: { $in: Object.assign(['a1', , 'a3'], { extra: 1 }) } }), msgIrreg], // eslint-disable-line no-sparse-arrays
690
+ ['偽造標籤為Array之物件於$and', () => ({ $and: fakeArr([{ id: 'a1' }]) }), msgIrreg],
691
+ ['偽造標籤為Array之物件於$or', () => ({ $or: fakeArr([{ id: 'a1' }]) }), msgIrreg],
692
+ ['偽造標籤為Array之物件於id.$in', () => ({ id: { $in: fakeArr(['a1']) } }), msgIrreg],
693
+ ['偽造標籤為Array之物件於id.$eq', () => ({ id: { $eq: fakeArr(['a1']) } }), msgIrreg],
694
+ ['偽造標籤為Array之物件於非id欄位', () => ({ name: { $in: fakeArr(['x']) } }), msgIrreg],
695
+ ['真陣列由原型設標籤為Object', () => ({ id: { $in: Object.setPrototypeOf(['a1'], Object.create(Array.prototype, { [Symbol.toStringTag]: { value: 'Object' } })) } }), msgIrreg],
696
+ ['真陣列由原型設標籤為Object於$and元素', () => ({ $and: [Object.setPrototypeOf(['a1'], Object.create(Array.prototype, { [Symbol.toStringTag]: { value: 'Object' } }))] }), msgIrreg],
697
+ ['真陣列由原型設標籤為getter', () => ({ id: { $in: Object.setPrototypeOf(['a1'], Object.create(Array.prototype, { [Symbol.toStringTag]: { get: () => 'Array' } })) } }), msgIrreg],
698
+ ['類別實例之標籤為getter於$and元素', () => ({ $and: [new TagG()] }), msgIrreg],
699
+ //深度邊界(第64層仍受strict檢查; 真陣列之標籤getter只有標籤檢查能辨, 直呼後端本接受, 故於此以替身ORM鎖定)
700
+ ['標籤為getter之真陣列恰在第64層', () => wrapAnd({ $and: Object.setPrototypeOf([{ id: 'a1' }], Object.create(Array.prototype, { [Symbol.toStringTag]: { get: () => 'Array' } })) }, 31), msgIrreg],
701
+ ['標籤為getter之真陣列在第64層以內(第2層)', () => ({ $and: Object.setPrototypeOf([{ id: 'a1' }], Object.create(Array.prototype, { [Symbol.toStringTag]: { get: () => 'Array' } })) }), msgIrreg],
702
+ //空洞: 密陣列才可交複製(繼承之索引會被map讀到, 外審 A-02)
703
+ ['空洞於$in', () => ({ id: { $in: ['a1', , 'a3'] } }), msgIrreg], // eslint-disable-line no-sparse-arrays
704
+ ['空洞於$and', () => ({ $and: [{ id: 'a1' }, , { id: 'a3' }] }), msgIrreg], // eslint-disable-line no-sparse-arrays
705
+ ['空洞且原型帶資料索引於$in', () => ({ id: { $in: Object.setPrototypeOf(new Array(1), Object.assign(Object.create(Array.prototype), { 0: 'a1' })) } }), msgIrreg],
706
+ ['空洞且原型帶資料索引於$and', () => ({ $and: Object.setPrototypeOf(new Array(1), Object.assign(Object.create(Array.prototype), { 0: { id: 'a1' } })) }), msgIrreg],
707
+ //內建品牌物件之原型改為null或Object.prototype: 內部槽不在自有鍵中, 無標籤可辨, 複本會洗成{}(外審 A-01), 由structuredClone網抓
708
+ ['原型為null之Map於頂層', () => Object.setPrototypeOf(new Map([['id', 'a1']]), null), msgIrreg],
709
+ ['原型為Object.prototype之Map於頂層', () => Object.setPrototypeOf(new Map([['id', 'a1']]), Object.prototype), msgIrreg],
710
+ ['原型為null之Set於$and元素', () => ({ $and: [Object.setPrototypeOf(new Set(['a1']), null)] }), msgIrreg],
711
+ ['原型為null之WeakMap於id運算子位置', () => ({ id: Object.setPrototypeOf(new WeakMap(), null) }), msgIrreg],
712
+ ['原型為null之Promise於$or元素', () => ({ $or: [Object.setPrototypeOf(Promise.resolve(0), null)] }), msgIrreg],
713
+ ['原型為null之ArrayBuffer於頂層', () => Object.setPrototypeOf(new ArrayBuffer(1), null), msgIrreg],
714
+ ['原型為null之DataView於頂層', () => Object.setPrototypeOf(new DataView(new ArrayBuffer(1)), null), msgIrreg],
715
+ ['原型為null之零長TypedArray於id運算子位置', () => ({ id: Object.setPrototypeOf(new Uint8Array(0), null) }), msgIrreg],
716
+ ['原型為null之Symbol包裝物件於頂層', () => Object.setPrototypeOf(Object(Symbol('s')), null), msgIrreg],
717
+ ['原型為null之BigInt包裝物件於$and元素', () => ({ $and: [Object.setPrototypeOf(Object(1n), null)] }), msgIrreg],
718
+ //偽原型: 只有constructor一鍵, 通過lodash式純物件判準而後端要求本realm之Object.prototype或null(外審第二輪)
719
+ ['偽原型之頂層', () => Object.create(Object.assign(Object.create(null), { constructor: Object })), 'need an object'],
720
+ ['偽原型之$and元素', () => ({ $and: [Object.create(Object.assign(Object.create(null), { constructor: Object }))] }), msgIrreg],
721
+ ['偽原型之id運算子物件', () => ({ id: Object.assign(Object.create(Object.assign(Object.create(null), { constructor: Object })), { $in: ['a1'] }) }), msgIrreg],
722
+ //函數與Symbol值: typeof非object不被走訪, 帶標籤之函數會被isobj當物件複製(外審 A-03); 由structuredClone網抓(後端本即reject)
723
+ ['帶Object標籤之函數於id運算子位置', () => ({ id: Object.assign(function() {}, { $eq: 'a1', [Symbol.toStringTag]: 'Object' }) }), msgIrreg],
724
+ ['帶Object標籤之函數於$and元素', () => ({ $and: [Object.assign(function() {}, { id: 'a1', [Symbol.toStringTag]: 'Object' })] }), msgIrreg],
725
+ ['帶Array標籤之函數於$in', () => ({ id: { $in: Object.assign(function() {}, { [Symbol.toStringTag]: 'Array' }) } }), msgIrreg],
726
+ ['函數值', () => ({ id: { $eq: function() {} } }), msgIrreg],
727
+ ['Symbol值', () => ({ name: Symbol('x') }), msgIrreg],
728
+ //Proxy: 陷阱可令檢查之值不等於使用之值, 一律reject(陷阱拋錯者由P04以unreadable處理)
729
+ ['Proxy包裝之一般物件於頂層', () => new Proxy({ id: 'a1' }, {}), msgIrreg],
730
+ ['Proxy包裝之一般物件於$and元素', () => ({ $and: [new Proxy({ id: 'a1' }, {})] }), msgIrreg],
731
+ ['陣列之原型為Proxy', () => ({ id: { $in: Object.setPrototypeOf(['a1'], new Proxy(Array.prototype, {})) } }), msgIrreg],
732
+ ['陣列之原型為Proxy於$and', () => ({ $and: Object.setPrototypeOf([{ id: 'a1' }], new Proxy(Array.prototype, {})) }), msgIrreg],
733
+ //偽原型(外審第二輪 A-01、A-03): 自有鍵名同Object.prototype, constructor 為本realm之Object, 但其prototype不指回偽原型; 或constructor為getter
734
+ ['仿Object.prototype之偽原型於頂層', () => Object.create(mkForged12()), 'need an object'],
735
+ ['仿Object.prototype之偽原型於$and元素', () => ({ $and: [Object.assign(Object.create(mkForged12()), { id: 'a1' })] }), msgIrreg],
736
+ ['仿Object.prototype之偽原型於id運算子物件', () => ({ id: Object.assign(Object.create(mkForged12()), { $in: ['a1'] }) }), msgIrreg],
737
+ ['偽原型之constructor為getter於頂層', () => Object.create(mkForged12({ get: () => Object, enumerable: false, configurable: true })), 'need an object'],
738
+ ['偽原型之constructor為getter於$and元素', () => ({ $and: [Object.create(mkForged12({ get: () => Object, enumerable: false, configurable: true }))] }), msgIrreg],
739
+ ['偽原型多一個繼承鍵(鍵名為Object.prototype之超集)於頂層', () => Object.create(Object.defineProperty(mkForged12(), 'id', { value: 'a1', enumerable: true })), 'need an object'],
740
+ ['偽原型多一個繼承鍵(鍵名為Object.prototype之超集)於$and元素', () => ({ $and: [Object.create(Object.defineProperty(mkForged12(), 'id', { value: 'a1', enumerable: true }))] }), msgIrreg],
741
+ ['偽原型之constructor自洽(其prototype指回偽原型)但原始碼不同於Object於頂層', () => Object.create(mkSelfCtor()), 'need an object'],
742
+ ['偽原型之constructor自洽(其prototype指回偽原型)但原始碼不同於Object於$and元素', () => ({ $and: [Object.create(mkSelfCtor())] }), msgIrreg],
743
+ ['偽原型之constructor為class且鍵名同Object.prototype', () => ({ $and: [Object.create(mkForged12({ value: class Foo {}, enumerable: false, configurable: true, writable: true }))] }), msgIrreg],
744
+ //真的另一個realm之Object.prototype(指回檢查與原始碼比對皆通過), 只多一個鍵: 只有鍵數檢查能辨
745
+ ['跨realm之Object.prototype被加id(鍵名超集)於頂層', () => vm.runInNewContext('Object.prototype.id = "a1"; ({})'), 'need an object'],
746
+ ['跨realm之Object.prototype被加id(鍵名超集)於$and元素', () => ({ $and: [vm.runInNewContext('Object.prototype.id = "a1"; ({})')] }), msgIrreg],
747
+ ['跨realm之Object.prototype被刪valueOf並加id於頂層', () => vm.runInNewContext('delete Object.prototype.valueOf; Object.prototype.id = "a1"; ({})'), 'need an object'],
748
+ ['跨realm之Object.prototype被刪valueOf並加id於$and元素', () => ({ $and: [vm.runInNewContext('delete Object.prototype.valueOf; Object.prototype.id = "a1"; ({})')] }), msgIrreg],
749
+ //Date帶自有屬性(外審第二輪 A-06): 葉節點仍須乾淨
750
+ ['Date帶自有屬性於欄位值', () => ({ name: Object.assign(new Date(0), { x: 1 }) }), msgIrreg],
751
+ ['Date帶自有__proto__鍵於比較運算子', () => ({ name: { $gt: Object.defineProperty(new Date(0), '__proto__', { value: 1, enumerable: true }) } }), msgIrreg],
752
+ ['Date帶不可列舉屬性', () => ({ name: hide(new Date(0), 'zz', 1) }), msgIrreg],
753
+ ['Date帶getter', () => ({ name: Object.defineProperty(new Date(0), 'g', { get: () => 1, enumerable: true }) }), msgIrreg],
754
+ //品牌物件在較後之兄弟位置(外審第二輪 C-02: 只走訪第0個鍵/元素之變異須轉紅); KeyObject 不在 util.types 品牌清單內, 由結構化複製網抓(Blob 帶自有 Symbol 鍵, 先被 hidden 抓到, 不適用)
755
+ ['品牌物件為第2個鍵', () => ({ name: 'x', id: Object.setPrototypeOf(new Map(), null) }), msgIrreg],
756
+ ['KeyObject(原型null)為第2個鍵', () => ({ name: 'x', id: keyObj() }), msgIrreg],
757
+ ['KeyObject(原型null)為頂層', () => keyObj(), msgIrreg],
758
+ ['KeyObject(原型null)為$and第2個元素', () => ({ $and: [{ id: 'a1' }, keyObj()] }), msgIrreg],
759
+ ['KeyObject(原型null)為$or第3個元素', () => ({ $or: [{ id: 'a1' }, { id: 'a2' }, keyObj()] }), msgIrreg],
760
+ ['KeyObject(原型null)為id運算子物件之第2個鍵', () => ({ id: { $eq: 'a1', $ne: keyObj() } }), msgIrreg],
761
+ ['Proxy為$and第2個元素', () => ({ $and: [{ id: 'a1' }, new Proxy({ id: 'a2' }, {})] }), msgIrreg],
762
+ ['帶標籤之函數為$or第3個元素', () => ({ $or: [{ id: 'a1' }, { id: 'a2' }, Object.assign(function() {}, { id: 'a3', [Symbol.toStringTag]: 'Object' })] }), msgIrreg],
763
+ ['品牌物件為$in之後續位置(陣列元素)', () => ({ id: { $in: ['a1', Object.setPrototypeOf(new Set(), null)] } }), msgIrreg],
764
+ //檢查順序: 自有__proto__鍵 > 非純物件 > 隱藏鍵 > 存取器
765
+ ['類別實例且有Symbol鍵: 非純物件優先', () => ({ $and: [Object.assign(new Cls({ id: 'a1' }), { [Symbol('s')]: 1 })] }), msgIrreg],
766
+ ['Symbol鍵且有存取器: 隱藏鍵優先', () => Object.assign(Object.defineProperty({}, 'id', { enumerable: true, get: () => 'a1' }), { [Symbol('s')]: 1 }), msgHidden],
767
+ ]
768
+ let done = 0
769
+ for (let { kind, proc } of getKinds()) {
770
+ for (let mode of ['select', 'delAll']) {
771
+ for (let [label, mk, msg] of cases) {
772
+ let err = await quiet(() => getReject(proc('u1', 'tests', mode, mk()))).catch((e) => assert.fail(`${kind} ${mode} ${label} ${e.message}`))
773
+ assert.strictEqual(err, `invalid find of ${mode}, ${msg}`, `${kind} ${mode} ${label}`)
774
+ done++
775
+ }
776
+ }
777
+ assert.deepStrictEqual(calls, [], `${kind} ORM不應被呼叫`)
778
+ }
779
+ assert.strictEqual(cases.length, 91, '語料數')
780
+ assert.strictEqual(done, 91 * 2 * 4, '完成數(語料 × 2 mode × 4 種資料表型態)')
781
+ })
782
+
783
+ it('D05b 檢查順序: 自有__proto__鍵優先於隱藏鍵與非純物件; 檢查不呼叫getter與length getter', async function() {
784
+ //R-OBJ-01 規則3、6: 檢查以屬性描述子與ownKeys讀取, 不呼叫getter; 偽造標籤之物件於分類時只讀標籤不讀length
785
+ let reads = 0
786
+ let counted = Object.defineProperty({ [Symbol.toStringTag]: 'Array' }, 'length', {
787
+ enumerable: true,
788
+ get() {
789
+ reads++
790
+ return 1
791
+ },
792
+ })
793
+ let withGetter = Object.defineProperty({ [Symbol('s')]: 1 }, 'id', {
794
+ enumerable: true,
795
+ get() {
796
+ reads++
797
+ return 'a1'
798
+ },
799
+ })
800
+ //標籤為繼承之getter: 分類(isobj、isarr)讀標籤時會執行, 檢查與複製各執行一次可回傳不同值(外審第二輪 B-01); 分類器以 in 運算子判斷標籤而不讀它
801
+ let tagReads = 0
802
+ let tagGetterProto = (base) => Object.create(base, {
803
+ [Symbol.toStringTag]: {
804
+ get() {
805
+ tagReads++
806
+ return 'Array'
807
+ },
808
+ },
809
+ })
810
+ for (let { kind, proc } of getKinds()) {
811
+ for (let mode of ['select', 'delAll']) {
812
+ for (let [label, find, msg] of [
813
+ ['陣列於id.$in', { id: { $in: Object.setPrototypeOf(['a1'], tagGetterProto(Array.prototype)) } }, msgIrreg],
814
+ ['陣列於$and', { $and: Object.setPrototypeOf([{ id: 'a1' }], tagGetterProto(Array.prototype)) }, msgIrreg],
815
+ ['物件於$and元素', { $and: [Object.create(tagGetterProto(Object.prototype))] }, msgIrreg],
816
+ ['頂層(isPlainObj亦不讀標籤)', Object.create(tagGetterProto(Object.prototype)), 'need an object'],
817
+ ]) {
818
+ let e = await quiet(() => getReject(proc('u1', 'tests', mode, find)))
819
+ assert.strictEqual(e, `invalid find of ${mode}, ${msg}`, `${kind} ${mode} 標籤getter ${label}`)
820
+ }
821
+ let e1 = await quiet(() => getReject(proc('u1', 'tests', mode, Object.assign(parse('{"__proto__":1}'), { [Symbol('s')]: 1 }))))
822
+ assert.strictEqual(e1, `invalid find of ${mode}, unsupported key[__proto__]`, `${kind} ${mode} proto優先`)
823
+ let e2 = await quiet(() => getReject(proc('u1', 'tests', mode, { id: { $in: counted } })))
824
+ assert.strictEqual(e2, `invalid find of ${mode}, ${msgIrreg}`, `${kind} ${mode} 偽造陣列`)
825
+ let e3 = await quiet(() => getReject(proc('u1', 'tests', mode, withGetter)))
826
+ assert.strictEqual(e3, `invalid find of ${mode}, ${msgHidden}`, `${kind} ${mode} 隱藏鍵優先於存取器`)
827
+ }
828
+ }
829
+ assert.strictEqual(reads, 0, 'getter與length getter不應被呼叫')
830
+ assert.strictEqual(tagReads, 0, '標籤getter不應被呼叫')
831
+ assert.deepStrictEqual(calls, [], 'ORM不應被呼叫')
832
+ })
833
+
834
+ it('D05c 檢查期間不執行使用端程式: Proxy之陷阱、原型為Proxy、自摘除之Proxy、偽原型之constructor getter、Date之getter皆不被呼叫, 條件不被改動', async function() {
835
+ //R-OBJ-01 規則6(外審第二輪 A-02~A-04、A-06): 陷阱或getter可在檢查途中改動已檢查之條件(檢查之值不等於使用之值); 檢查以util.types.isProxy(不觸發陷阱)與原型鏈描述子判斷, 不用 in 運算子與 p.constructor
836
+ let done = 0
837
+ let traps = 0
838
+ let ctorReads = 0
839
+ let dateReads = 0
840
+ let ctorDesc = {
841
+ get() {
842
+ ctorReads++
843
+ return Object
844
+ },
845
+ enumerable: false,
846
+ configurable: true,
847
+ }
848
+ let dateDesc = {
849
+ get() {
850
+ dateReads++
851
+ return 1
852
+ },
853
+ enumerable: true,
854
+ }
855
+ //每個陷阱皆計數後轉發
856
+ let counting = () => new Proxy({}, {
857
+ get: (t, name) => (...a) => {
858
+ traps++
859
+ return Reflect[name](...a)
860
+ },
861
+ })
862
+ for (let { kind, proc } of getKinds()) {
863
+ for (let mode of ['select', 'delAll']) {
864
+ //自摘除之Proxy: 任一陷阱被呼叫即清空根條件
865
+ let root = { id: 'a1' }
866
+ let detach = new Proxy({ id: 'a2' }, new Proxy({}, {
867
+ get: (t, name) => (...a) => {
868
+ traps++
869
+ delete root.id
870
+ delete root.$and
871
+ return Reflect[name](...a)
872
+ },
873
+ }))
874
+ root.$and = [detach]
875
+ let cases = [
876
+ ['Proxy於頂層', new Proxy({ id: 'a1' }, counting()), 'irregular'],
877
+ ['Proxy於$and元素', { $and: [new Proxy({ id: 'a1' }, counting())] }, 'irregular'],
878
+ ['Proxy於id運算子物件', { id: new Proxy({ $in: ['a1'] }, counting()) }, 'irregular'],
879
+ ['Proxy陣列於$in', { id: { $in: new Proxy(['a1'], counting()) } }, 'irregular'],
880
+ ['陣列之原型為Proxy於$in', { id: { $in: Object.setPrototypeOf(['a1'], new Proxy(Array.prototype, counting())) } }, 'irregular'],
881
+ ['陣列之原型為Proxy於$and', { $and: Object.setPrototypeOf([{ id: 'a1' }], new Proxy(Array.prototype, counting())) }, 'irregular'],
882
+ ['物件之原型為Proxy', { $and: [Object.create(new Proxy(Object.prototype, counting()))] }, 'irregular'],
883
+ ['自摘除之Proxy於$and元素', root, 'irregular'],
884
+ ['偽原型之constructor為getter於$and元素', { $and: [Object.create(mkForged12(ctorDesc))] }, 'irregular'],
885
+ ['偽原型之constructor為getter於頂層', Object.create(mkForged12(ctorDesc)), 'need an object'],
886
+ ['Date帶getter', { name: Object.defineProperty(new Date(0), 'g', dateDesc) }, 'irregular'],
887
+ ]
888
+ assert.strictEqual(cases.length, 11, '語料數')
889
+ for (let [label, find, msg] of cases) {
890
+ let e = await quiet(() => getReject(proc('u1', 'tests', mode, find))).catch((err) => assert.fail(`${kind} ${mode} ${label} ${err.message}`))
891
+ assert.strictEqual(e, `invalid find of ${mode}, ${msg === 'irregular' ? msgIrreg : msg}`, `${kind} ${mode} ${label}`)
892
+ done++
893
+ }
894
+ assert.strictEqual(root.id, 'a1', `${kind} ${mode} 根條件不被自摘除之Proxy改動`)
895
+ assert.strictEqual(root.$and.length, 1)
896
+ }
897
+ }
898
+ assert.strictEqual(done, 11 * 2 * 4, '完成數(語料 × 2 mode × 4 種資料表型態)')
899
+ assert.strictEqual(traps, 0, 'Proxy之陷阱不應被呼叫')
900
+ assert.strictEqual(ctorReads, 0, '偽原型之constructor getter不應被呼叫')
901
+ assert.strictEqual(dateReads, 0, 'Date之getter不應被呼叫')
902
+ assert.deepStrictEqual(calls, [], 'ORM不應被呼叫')
903
+ })
904
+
905
+ it('D05d 品牌物件之探針不依賴 structuredClone: 全域 structuredClone 被移除時, 原型改為null之內建品牌物件仍被reject', async function() {
906
+ //R-OBJ-01 規則6(外審第二輪 B-01): util.types 為主要探針, structuredClone 為通用網; 無 structuredClone 之環境不致 fail-open 於常見內建品牌物件
907
+ let orig = globalThis.structuredClone
908
+ let done = 0
909
+ let nul = (o) => Object.setPrototypeOf(o, null)
910
+ try {
911
+ globalThis.structuredClone = undefined
912
+ assert.strictEqual(typeof structuredClone, 'undefined')
913
+ for (let { kind, proc } of getKinds()) {
914
+ for (let mode of ['select', 'delAll']) {
915
+ for (let [label, mk] of [
916
+ ['Map', () => nul(new Map([['id', 'a1']]))],
917
+ ['Set', () => nul(new Set(['a1']))],
918
+ ['WeakMap', () => nul(new WeakMap())],
919
+ ['WeakSet', () => nul(new WeakSet())],
920
+ ['Promise', () => nul(Promise.resolve(0))],
921
+ ['ArrayBuffer', () => nul(new ArrayBuffer(1))],
922
+ ['DataView', () => nul(new DataView(new ArrayBuffer(1)))],
923
+ ['Uint8Array', () => nul(new Uint8Array(0))],
924
+ ['Buffer', () => nul(Buffer.alloc(0))],
925
+ ['Symbol包裝物件', () => nul(Object(Symbol('s')))],
926
+ ['BigInt包裝物件', () => nul(Object(1n))],
927
+ ['Number包裝物件', () => nul(Object(1))],
928
+ ['RegExp', () => nul(/x/)],
929
+ ['Error', () => nul(new Error('x'))],
930
+ ['WeakRef', () => nul(new WeakRef({}))], //util.types 無對應判定, 由 WeakRef.prototype.deref 之精確探針(規則6 ⑥)
931
+ ['FinalizationRegistry', () => nul(new FinalizationRegistry(() => {}))], //同上, FinalizationRegistry.prototype.unregister
932
+ ]) {
933
+ for (let [pos, build] of [['頂層', (n) => n], ['$and元素', (n) => ({ $and: [n] })], ['id運算子位置', (n) => ({ id: n })]]) {
934
+ let e = await quiet(() => getReject(proc('u1', 'tests', mode, build(mk())))).catch((err) => assert.fail(`${kind} ${mode} ${label} ${pos} ${err.message}`))
935
+ //頂層: 內部標籤非 Object 者(Number 包裝物件、RegExp、Error)isobj 為假, 由 getErrFind 先以 need an object 回絕
936
+ let msg = pos === '頂層' && ['Number包裝物件', 'RegExp', 'Error'].includes(label) ? 'need an object' : msgIrreg
937
+ assert.strictEqual(e, `invalid find of ${mode}, ${msg}`, `${kind} ${mode} ${label} ${pos}`)
938
+ done++
939
+ }
940
+ }
941
+ }
942
+ }
943
+ }
944
+ finally {
945
+ globalThis.structuredClone = orig
946
+ }
947
+ assert.strictEqual(typeof structuredClone, 'function', 'structuredClone 已還原')
948
+ assert.strictEqual(done, 16 * 3 * 2 * 4, '完成數(16 種品牌物件 × 3 位置 × 2 mode × 4 種資料表型態)')
949
+ assert.deepStrictEqual(calls, [], 'ORM不應被呼叫')
950
+ })
951
+
952
+ it('D07 被拒之find的診斷輸出不執行使用端程式, 亦不改動使用端物件: 標籤getter、inspect.custom、Proxy目標之標籤getter皆0次, 同一物件再送另一動作仍reject(四種資料表型態)', async function() {
953
+ //R-OBJ-01 規則6 ⑤(sol 外審最終版 R3-01): 原 logSafe(label, input) 以 util.inspect 格式化原物件, 在拒收之後讀標籤getter、呼叫 inspect.custom,
954
+ //其副作用可改動呼叫端的find, 呼叫端把同一個find再送 delAll 即成無條件而刪全表(修前真 lowdb 實測刪 3/3 列)
955
+ //不用 quiet(其以空函數取代 console.log, 不格式化引數而測不出): 以 util.format 格式化(同真 console.log)並收集輸出
956
+ let logOrig = console.log
957
+ let lines = []
958
+ console.log = (...a) => {
959
+ lines.push(util.format(...a))
960
+ }
961
+ let reads = 0
962
+ let tagGetter = (f) => ({
963
+ get() {
964
+ reads++
965
+ f()
966
+ return 'Object'
967
+ },
968
+ configurable: true,
969
+ })
970
+ let cases = [
971
+ ['原型帶標籤getter之真陣列於id.$in(getter刪除id條件)', () => {
972
+ let a = ['a1']
973
+ let find = { id: { $in: a } }
974
+ Object.setPrototypeOf(a, Object.create(Array.prototype, { [Symbol.toStringTag]: tagGetter(() => delete find.id) }))
975
+ return { find, intact: () => 'id' in find }
976
+ }, msgIrreg],
977
+ ['原型帶標籤getter之物件於$and元素(getter刪除$and)', () => {
978
+ let find = { id: 'a1', $and: [] }
979
+ find.$and.push(Object.create(Object.create(null, { [Symbol.toStringTag]: tagGetter(() => delete find.$and) })))
980
+ return { find, intact: () => '$and' in find }
981
+ }, msgIrreg],
982
+ ['inspect.custom(自有Symbol鍵之函數)於頂層(函數刪除id條件)', () => {
983
+ let find = { id: 'a1' }
984
+ find[util.inspect.custom] = () => {
985
+ reads++
986
+ delete find.id
987
+ return 'x'
988
+ }
989
+ return { find, intact: () => 'id' in find }
990
+ }, msgHidden],
991
+ ['Proxy之目標帶標籤getter於$and元素(getter刪除$and)', () => {
992
+ let find = { id: 'a1', $and: [] }
993
+ find.$and.push(new Proxy(Object.defineProperty({}, Symbol.toStringTag, tagGetter(() => delete find.$and)), {}))
994
+ return { find, intact: () => '$and' in find }
995
+ }, msgIrreg],
996
+ ['存取器屬性(對照: util.inspect 本不呼叫)', () => {
997
+ let find = { id: 'a1' }
998
+ Object.defineProperty(find, 'name', {
999
+ get() {
1000
+ reads++
1001
+ return 'x'
1002
+ },
1003
+ enumerable: true,
1004
+ })
1005
+ return { find, intact: () => 'id' in find }
1006
+ }, 'unsupported accessor property'],
1007
+ ]
1008
+ let done = 0
1009
+ try {
1010
+ for (let { kind, proc } of getKinds()) {
1011
+ for (let mode of ['select', 'delAll']) {
1012
+ let other = mode === 'select' ? 'delAll' : 'select'
1013
+ for (let [label, mk, msg] of cases) {
1014
+ stub.reset()
1015
+ lines.length = 0
1016
+ let { find, intact } = mk()
1017
+ let err = await getReject(proc('u1', 'tests', mode, find)).catch((e) => assert.fail(`${kind} ${mode} ${label} ${e.message}`))
1018
+ assert.strictEqual(err, `invalid find of ${mode}, ${msg}`, `${kind} ${mode} ${label}`)
1019
+ assert.strictEqual(reads, 0, `${kind} ${mode} ${label} 診斷輸出不得執行使用端程式`)
1020
+ assert.ok(intact(), `${kind} ${mode} ${label} 使用端物件不被改動`)
1021
+ assert.ok(lines.some((v) => v.includes('invalid find')), `${kind} ${mode} ${label} 仍輸出診斷`)
1022
+ //同一物件再送另一動作: 仍同一原因reject, 不成為無條件
1023
+ let err2 = await getReject(proc('u1', 'tests', other, find)).catch((e) => assert.fail(`${kind} ${other} ${label} 再送 ${e.message}`))
1024
+ assert.strictEqual(err2, `invalid find of ${other}, ${msg}`, `${kind} ${other} ${label} 再送`)
1025
+ assert.strictEqual(reads, 0, `${kind} ${other} ${label} 再送後仍0次`)
1026
+ assert.ok(intact(), `${kind} ${other} ${label} 再送後使用端物件不被改動`)
1027
+ assert.strictEqual(calls.length, 0, `${kind} ${mode} ${label} ORM不應被呼叫`)
1028
+ done++
1029
+ }
1030
+ }
1031
+ }
1032
+ }
1033
+ finally {
1034
+ console.log = logOrig
1035
+ }
1036
+ assert.strictEqual(cases.length, 5, '語料數')
1037
+ assert.strictEqual(done, 5 * 2 * 4, '完成數(語料 × 2 mode × 4 種資料表型態)')
1038
+ })
1039
+
1040
+ it('D07b 被拒之find的診斷輸出為快照: 保留資料(字串、數字、巢狀)而不隨輸入放大, 大陣列、長字串、極深巢狀皆有上限且不拋錯', async function() {
1041
+ //R-OBJ-01 規則6 ⑤: 快照僅含本層新建之純資料; 值個數(含葉)200、層數8、每層50鍵或元素、字串200字元為上限
1042
+ let logOrig = console.log
1043
+ let lines = []
1044
+ console.log = (...a) => {
1045
+ lines.push(util.format(...a))
1046
+ }
1047
+ let deep = { v: 'bottom' }
1048
+ for (let i = 0; i < 60; i++) {
1049
+ deep = { n: deep }
1050
+ }
1051
+ let cases = [
1052
+ ['保留資料', () => ({ id: { $in: ['a1', 'a2'] }, name: 'alpha', [Symbol('s')]: 1 }), (t) => t.includes('a1') && t.includes('a2') && t.includes('alpha'), 2000],
1053
+ ['10萬元素之陣列', () => ({ id: { $in: Array.from({ length: 100000 }, (_, i) => `a${i}`) }, [Symbol('s')]: 1 }), (t) => t.includes('a0'), 5000],
1054
+ ['100萬字元之字串', () => ({ name: 'x'.repeat(1000000), [Symbol('s')]: 1 }), (t) => t.includes('xxxx'), 3000],
1055
+ ['60層巢狀', () => ({ $and: [deep], [Symbol('s')]: 1 }), (t) => t.includes('$and'), 3000],
1056
+ ['5萬個鍵', () => Object.assign(Object.fromEntries(Array.from({ length: 50000 }, (_, i) => [`k${i}`, i])), { [Symbol('s')]: 1 }), (t) => t.includes('k0'), 5000],
1057
+ //2000 個 200 字元之字串葉(40 鍵 x 50 元素): 值個數上限 200(含葉)使輸出不超過約 200 x 200 字元; 無上限時約 41 萬字元
1058
+ ['2000個字串葉', () => Object.assign(Object.fromEntries(Array.from({ length: 40 }, (_, i) => [`k${i}`, Array.from({ length: 50 }, () => 'y'.repeat(200))])), { [Symbol('s')]: 1 }), (t) => t.includes('yyyy'), 60000],
1059
+ ]
1060
+ let done = 0
1061
+ try {
1062
+ for (let { kind, proc } of getKinds()) {
1063
+ for (let mode of ['select', 'delAll']) {
1064
+ for (let [label, mk, has, maxLen] of cases) {
1065
+ stub.reset()
1066
+ lines.length = 0
1067
+ let err = await getReject(proc('u1', 'tests', mode, mk())).catch((e) => assert.fail(`${kind} ${mode} ${label} ${e.message}`))
1068
+ assert.strictEqual(err, `invalid find of ${mode}, ${msgHidden}`, `${kind} ${mode} ${label}`)
1069
+ let text = lines.join('\n')
1070
+ assert.ok(has(text), `${kind} ${mode} ${label} 快照保留資料: ${text.slice(0, 200)}`)
1071
+ assert.ok(text.length <= maxLen, `${kind} ${mode} ${label} 輸出長度 ${text.length} 應不超過 ${maxLen}`)
1072
+ done++
1073
+ }
1074
+ }
1075
+ }
1076
+ }
1077
+ finally {
1078
+ console.log = logOrig
1079
+ }
1080
+ assert.strictEqual(cases.length, 6, '語料數')
1081
+ assert.strictEqual(done, 6 * 2 * 4, '完成數(語料 × 2 mode × 4 種資料表型態)')
1082
+ })
1083
+
1084
+ it('D08 品牌網的字元預算: 字串碼元總長逾4,000,000時不做結構化複製而改走精確探針(合法條件照常交ORM, WeakRef與FinalizationRegistry仍reject); 預算內仍做一次結構化複製', async function() {
1085
+ //R-OBJ-01 規則6 ⑥(sol 外審最終版 R3-02): structuredClone 對重複出現之字串每次皆寫出並讀回, 記憶體與(字串碼元總長 x 出現次數)成正比;
1086
+ //記憶體內僅一個64KB字串、於$in內重複1萬次即邏輯長度655M碼元(實測經本層峰值RSS 1.6GB, 直呼0.59GB); 觀察點為全域 structuredClone 之呼叫次數
1087
+ let orig = globalThis.structuredClone
1088
+ let nClone = 0
1089
+ let str = 'x'.repeat(65536)
1090
+ let big = (n) => Array(n).fill(str) //n x 65536 碼元: 40 -> 2.6M(預算內), 70 -> 4.6M(逾預算)
1091
+ let nul = (o) => Object.setPrototypeOf(o, null)
1092
+ let brands = [
1093
+ ['WeakRef', () => nul(new WeakRef({}))],
1094
+ ['FinalizationRegistry', () => nul(new FinalizationRegistry(() => {}))],
1095
+ ]
1096
+ let places = [
1097
+ ['較後鍵之$and元素', (n) => ({ name: { $in: big(70) }, $and: [n] })],
1098
+ ['id運算子位置', (n) => ({ name: { $in: big(70) }, id: n })],
1099
+ ['$or之較後兄弟', (n) => ({ $or: [{ name: { $in: big(70) } }, n] })],
1100
+ ]
1101
+ let done = 0
1102
+ try {
1103
+ for (let mode0 of ['spy', 'none']) {
1104
+ if (mode0 === 'spy') {
1105
+ globalThis.structuredClone = (...a) => {
1106
+ nClone++
1107
+ return orig(...a)
1108
+ }
1109
+ }
1110
+ else {
1111
+ globalThis.structuredClone = undefined
1112
+ }
1113
+ for (let { kind, proc } of getKinds()) {
1114
+ for (let mode of ['select', 'delAll']) {
1115
+ let tag = `${mode0} ${kind} ${mode}`
1116
+ //逾預算之合法條件: 照常交ORM, 實參等價; 有 structuredClone 時未被呼叫
1117
+ stub.reset()
1118
+ nClone = 0
1119
+ let find = { name: { $in: big(70) }, id: { $in: ['a1'] } }
1120
+ await proc('u1', 'tests', mode, find)
1121
+ assert.strictEqual(calls.length, 1, `${tag} 逾預算之合法條件交ORM`)
1122
+ assert.deepEqual(calls[0].data, find, `${tag} ORM收到`)
1123
+ assert.strictEqual(nClone, 0, `${tag} 逾預算不做結構化複製`)
1124
+ done++
1125
+ //逾預算者含物件鍵(70 個物件各有一個 64KB 之同名鍵): 鍵之碼元亦計入
1126
+ stub.reset()
1127
+ nClone = 0
1128
+ let longKey = 'k'.repeat(65536)
1129
+ await proc('u1', 'tests', mode, { $and: Array.from({ length: 70 }, () => ({ [longKey]: 1 })) })
1130
+ assert.strictEqual(calls.length, 1, `${tag} 逾預算之長鍵條件交ORM`)
1131
+ assert.strictEqual(nClone, 0, `${tag} 鍵之碼元計入預算, 逾預算不做結構化複製`)
1132
+ done++
1133
+ //預算內: 仍做一次結構化複製(spy 模式)
1134
+ if (mode0 === 'spy') {
1135
+ stub.reset()
1136
+ nClone = 0
1137
+ await proc('u1', 'tests', mode, { name: { $in: big(40) } })
1138
+ assert.strictEqual(calls.length, 1, `${tag} 預算內之合法條件交ORM`)
1139
+ assert.strictEqual(nClone, 1, `${tag} 預算內做一次結構化複製`)
1140
+ done++
1141
+ }
1142
+ //逾預算 + 品牌物件: 仍reject, 不做結構化複製, ORM未被呼叫
1143
+ for (let [bl, mkBrand] of brands) {
1144
+ for (let [pl, build] of places) {
1145
+ stub.reset()
1146
+ nClone = 0
1147
+ let e = await quiet(() => getReject(proc('u1', 'tests', mode, build(mkBrand())))).catch((err) => assert.fail(`${tag} ${bl} ${pl} ${err.message}`))
1148
+ assert.strictEqual(e, `invalid find of ${mode}, ${msgIrreg}`, `${tag} ${bl} ${pl}`)
1149
+ assert.strictEqual(calls.length, 0, `${tag} ${bl} ${pl} ORM不應被呼叫`)
1150
+ assert.strictEqual(nClone, 0, `${tag} ${bl} ${pl} 逾預算不做結構化複製`)
1151
+ done++
1152
+ }
1153
+ }
1154
+ }
1155
+ }
1156
+ }
1157
+ }
1158
+ finally {
1159
+ globalThis.structuredClone = orig
1160
+ }
1161
+ assert.strictEqual(typeof structuredClone, 'function', 'structuredClone 已還原')
1162
+ //spy: 4 表型態 x 2 mode x (逾預算合法 1 + 長鍵 1 + 預算內 1 + 品牌 2 x 3) = 72; none: 4 x 2 x (1 + 1 + 6) = 64
1163
+ assert.strictEqual(done, 72 + 64, '完成數')
1164
+ })
1165
+
1166
+ it('D06 規則6不誤擋: null原型、跨realm、JSON與structuredClone產生者、含Date、大陣列與巢狀陣列之條件照常交ORM, 實參與送入者等價', async function() {
1167
+ //R-OBJ-01 規則6: 檢查只拒絕複本會洗掉之結構; JSON可表達之資料與合法特殊形狀須照常(含 structuredClone 品牌網不誤擋)
1168
+ let nul = (o) => Object.assign(Object.create(null), o)
1169
+ let cross = (src) => vm.runInNewContext(src)
1170
+ let d = new Date(0)
1171
+ let big = Array.from({ length: 10000 }, (_, i) => `a${i}`)
1172
+ let cases = [
1173
+ ['JSON.parse產生者', () => parse('{"$or":[{"id":"a1"},{"name":{"$in":["x","y"]}}],"length":{"$gte":1}}'), { $or: [{ id: 'a1' }, { name: { $in: ['x', 'y'] } }], length: { $gte: 1 } }],
1174
+ ['structuredClone產生者', () => structuredClone({ $and: [{ id: 'a1' }, { name: { $ne: 'x' } }] }), { $and: [{ id: 'a1' }, { name: { $ne: 'x' } }] }],
1175
+ ['跨realm之頂層純物件', () => cross('({ id: "a1", name: { $eq: "x" } })'), { id: 'a1', name: { $eq: 'x' } }],
1176
+ ['1萬個元素之$in', () => ({ id: { $in: big } }), { id: { $in: big } }],
1177
+ ['Date為欄位等值之值', () => ({ name: d }), { name: d }],
1178
+ ['null原型之$and元素', () => ({ $and: [nul({ id: 'a1' })] }), { $and: [{ id: 'a1' }] }],
1179
+ ['null原型之id運算子物件', () => ({ id: nul({ $in: ['a1', 'a2'] }) }), { id: { $in: ['a1', 'a2'] } }],
1180
+ ['null原型之非id欄位運算子物件', () => ({ name: nul({ $eq: 'x' }) }), { name: { $eq: 'x' } }],
1181
+ ['跨realm之巢狀物件與陣列', () => cross('({ $or: [{ id: "a1" }, { $and: [{ name: { $in: ["x", "y"] } }] }] })'), { $or: [{ id: 'a1' }, { $and: [{ name: { $in: ['x', 'y'] } }] }] }],
1182
+ ['Date為比較運算子之值', () => ({ name: { $gt: d } }), { name: { $gt: d } }],
1183
+ ['合理複雜之查詢', () => ({ $and: [{ $or: [{ id: { $in: ['a1', 'a2'] } }, { name: 'x' }] }, { length: { $gte: 1 } }] }), { $and: [{ $or: [{ id: { $in: ['a1', 'a2'] } }, { name: 'x' }] }, { length: { $gte: 1 } }] }],
1184
+ ['空陣列與空運算子集合', () => ({ id: { $in: [] }, $or: [] }), { id: { $in: [] }, $or: [] }],
1185
+ ]
1186
+ let done = 0
1187
+ for (let { kind, proc } of getKinds()) {
1188
+ for (let mode of ['select', 'delAll']) {
1189
+ for (let [label, mk, expected] of cases) {
1190
+ stub.reset()
1191
+ await proc('u1', 'tests', mode, mk())
1192
+ assert.strictEqual(calls.length, 1, `${kind} ${mode} ${label}`)
1193
+ //不比原型: 整數型主鍵表與無該表者原樣交ORM(null原型、跨realm), 轉型表為複本(本realm之Object.prototype)
1194
+ assert.deepEqual(calls[0].data, expected, `${kind} ${mode} ${label} ORM收到`)
1195
+ done++
1196
+ }
1197
+ }
1198
+ }
1199
+ assert.strictEqual(cases.length, 12, '語料數')
1200
+ assert.strictEqual(done, 12 * 2 * 4, '完成數(語料 × 2 mode × 4 種資料表型態)')
1201
+ })
1202
+
1203
+ })
1204
+
1205
+
611
1206
  describe('R-INPUT-01 規則5 woName須為woItems之自有字串屬性', function() {
612
1207
 
613
1208
  beforeEach(function() {
@@ -672,6 +1267,7 @@ describe('R-OBJ-01 規則5 find之巢狀深度上限', function() {
672
1267
 
673
1268
  //builders, 容器 = 一般物件與陣列
674
1269
  //eqObj(n): n 個巢狀物件容器; andChain(n): n 層 $and(根物件 + 每層一個陣列與一個物件, 共 1+2n 個容器); notChain(n): { id: { $not: … } } 共 n+2 個容器(含根)
1270
+ //$not 為系列白名單外之鍵名(後端reject, 本套件castIdCond已不遞迴它, R-WL-01 規則3), 此處只作為 id 條件內之深層巢狀物件: 深度檢查與運算子名稱無關
675
1271
  let eqObj = (n) => {
676
1272
  let o = { k: 1 }
677
1273
  for (let i = 1; i < n; i++) {
@@ -733,12 +1329,15 @@ describe('R-OBJ-01 規則5 find之巢狀深度上限', function() {
733
1329
 
734
1330
  it('T02 合理之複雜查詢照常; 寫入列之深層巢狀由adjustData轉為固定字串(行為鎖定, 不設上限)', async function() {
735
1331
  //R-OBJ-01 規則5: 合理查詢最深約7個容器; 列之遞迴由wsemi clearXSS進行, 約1e4層起拋RangeError, 被adjustData之try/catch轉為 failed to adjust data
736
- let find = { $or: [{ id: { $in: [1, 2] } }, { $and: [{ name: 'x' }, { $nor: [{ name: 'y' }, { id: { $not: { $eq: 3 } } }] }] }] }
737
- for (let { kind, proc } of getKinds()) {
1332
+ let find = { $or: [{ id: { $in: [1, 2] } }, { $and: [{ name: 'x' }, { $or: [{ name: 'y' }, { id: { $ne: 3 } }] }] }] }
1333
+ //手寫之預期(不取自被測函式): 字串型與未知型主鍵表之最深一層 id 條件亦須轉為字串(R-WL-01 規則3: 遞迴 $and、$or 至任何層), 整數型與無該表者原樣
1334
+ let expectCast = { $or: [{ id: { $in: ['1', '2'] } }, { $and: [{ name: 'x' }, { $or: [{ name: 'y' }, { id: { $ne: '3' } }] }] }] }
1335
+ for (let { kind, cast, proc } of getKinds()) {
738
1336
  for (let mode of ['select', 'delAll']) {
739
1337
  stub.reset()
740
1338
  await proc('u1', 'tests', mode, find)
741
1339
  assert.strictEqual(calls.length, 1, `${kind} ${mode}`)
1340
+ same(calls[0].data, cast ? expectCast : find, `${kind} ${mode} ORM收到`)
742
1341
  }
743
1342
  }
744
1343
  let row = (n) => ({ id: 'r1', meta: eqObj(n) })
@@ -839,7 +1438,7 @@ describe('R-ERR-01 規則5 邊界保證: mapOrm 之非字串例外一律轉為
839
1438
  }
840
1439
  })
841
1440
 
842
- it('C04 循環或鏈狀共用子結構之find使遞迴爆堆疊: 以固定字串reject(字串型主鍵表; 其餘表型態原樣交ORM)', async function() {
1441
+ it('C04 循環或鏈狀共用子結構之find使遞迴爆堆疊: 以固定字串reject(循環: 字串型主鍵表, 其餘表型態原樣交ORM; 鏈狀共用: 所有表型態皆由規則6之品牌網reject)', async function() {
843
1442
  //R-ERR-01 規則5、R-OBJ-01 已知限制(外審 opus 二輪 F4): 深度檢查不涵蓋共用子結構, 由邊界保證兜底
844
1443
  let cyc = { $and: [] }
845
1444
  cyc.$and.push(cyc)
@@ -855,7 +1454,13 @@ describe('R-ERR-01 規則5 邊界保證: mapOrm 之非字串例外一律轉為
855
1454
  for (let mode of ['select', 'delAll']) {
856
1455
  for (let [label, find] of [['循環', cyc], ['鏈狀共用', dag]]) {
857
1456
  stub.reset()
858
- if (cast) {
1457
+ if (label === '鏈狀共用') {
1458
+ //規則6: 最長路徑約1e4個容器, 遠超結構化複製之遞迴上限(Node 24.19.0 實測約1576個容器, 與冷熱無關), 品牌網於複製前即以固定字串reject, 所有資料表型態皆然
1459
+ let err = await quiet(() => getReject(proc('u1', 'tests', mode, find)))
1460
+ assert.strictEqual(err, `invalid find of ${mode}, unsupported irregular object or array`, `${kind} ${mode} ${label}`)
1461
+ assert.strictEqual(calls.length, 0, `${kind} ${mode} ${label} ORM不應被呼叫`)
1462
+ }
1463
+ else if (cast) {
859
1464
  let err = await quiet(() => getReject(proc('u1', 'tests', mode, find)))
860
1465
  assert.strictEqual(err, FIXED, `${kind} ${mode} ${label}`)
861
1466
  assert.strictEqual(calls.length, 0, `${kind} ${mode} ${label} ORM不應被呼叫`)