circuitjson-toolkit 1.3.0 → 1.4.0

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,11 +1,226 @@
1
1
  import { BinaryDataSnapshot } from './BinaryDataSnapshot.mjs'
2
2
 
3
3
  const PROTECTED_BINARY_GETTERS = new WeakSet()
4
+ const CAPTURED_BINARY_PROPERTIES = new WeakSet()
5
+ const PENDING_BINARY_CAPTURES = new WeakMap()
6
+ const BINARY_COPY_CHUNK_BYTES = 64 * 1024
4
7
 
5
8
  /**
6
9
  * Protects byte-backed values embedded in canonical extension containers.
7
10
  */
8
11
  export class ProtectedExtensionBinaryBoundary {
12
+ /**
13
+ * Protects the exact binary properties collected by a proven clone
14
+ * adoption traversal without rescanning every ordinary container.
15
+ * @param {{ owner: object, key: PropertyKey }[]} properties Binary properties.
16
+ * @returns {void}
17
+ */
18
+ static protectProperties(properties) {
19
+ if (!Array.isArray(properties)) {
20
+ throw new TypeError(
21
+ 'Canonical extension binary properties must be an array.'
22
+ )
23
+ }
24
+ for (const property of properties) {
25
+ const owner = property?.owner
26
+ const key = property?.key
27
+ let descriptor
28
+ try {
29
+ descriptor = Object.getOwnPropertyDescriptor(owner, key)
30
+ } catch {
31
+ throw new TypeError(
32
+ 'Canonical extension data could not be inspected safely.'
33
+ )
34
+ }
35
+ if (ProtectedExtensionBinaryBoundary.isProtected(descriptor)) {
36
+ continue
37
+ }
38
+ const captured = ProtectedExtensionBinaryBoundary.captureProperty(
39
+ owner,
40
+ key,
41
+ descriptor
42
+ )
43
+ ProtectedExtensionBinaryBoundary.protectCapturedProperty(captured)
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Captures one binary property without changing its owner.
49
+ * @param {object} owner Binary property owner.
50
+ * @param {PropertyKey} key Binary property key.
51
+ * @param {PropertyDescriptor | undefined} [knownDescriptor] Previously validated descriptor.
52
+ * @returns {object} Opaque captured binary property.
53
+ */
54
+ static captureProperty(owner, key, knownDescriptor = undefined) {
55
+ const capture = ProtectedExtensionBinaryBoundary.beginPropertyCapture(
56
+ owner,
57
+ key,
58
+ knownDescriptor
59
+ )
60
+ while (
61
+ !ProtectedExtensionBinaryBoundary.copyPropertyCaptureChunk(
62
+ capture,
63
+ Number.MAX_SAFE_INTEGER
64
+ )
65
+ ) {
66
+ // The synchronous API deliberately owns the full payload atomically.
67
+ }
68
+ return ProtectedExtensionBinaryBoundary.finishPropertyCapture(capture)
69
+ }
70
+
71
+ /**
72
+ * Starts one branded binary property capture without copying its payload.
73
+ * @param {object} owner Binary property owner.
74
+ * @param {PropertyKey} key Binary property key.
75
+ * @param {PropertyDescriptor | undefined} [knownDescriptor] Previously validated descriptor.
76
+ * @returns {object} Opaque pending capture token.
77
+ * @internal
78
+ */
79
+ static beginPropertyCapture(owner, key, knownDescriptor = undefined) {
80
+ let descriptor = knownDescriptor
81
+ if (descriptor === undefined) {
82
+ try {
83
+ descriptor = Object.getOwnPropertyDescriptor(owner, key)
84
+ } catch {
85
+ throw new TypeError(
86
+ 'Canonical extension data could not be inspected safely.'
87
+ )
88
+ }
89
+ }
90
+ if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
91
+ throw new TypeError(
92
+ 'Canonical extension data may contain only data properties.'
93
+ )
94
+ }
95
+ const binary = BinaryDataSnapshot.describeStandard(descriptor.value)
96
+ if (!binary) {
97
+ throw new TypeError(
98
+ 'Canonical extension binary property changed during adoption.'
99
+ )
100
+ }
101
+ const capture = Object.freeze({})
102
+ PENDING_BINARY_CAPTURES.set(capture, {
103
+ bytes: new Uint8Array(binary.byteLength),
104
+ descriptor: Object.freeze({ ...descriptor }),
105
+ key,
106
+ offset: 0,
107
+ owner,
108
+ source: descriptor.value,
109
+ sourceRange: Object.freeze({ ...binary })
110
+ })
111
+ return capture
112
+ }
113
+
114
+ /**
115
+ * Copies the next bounded portion of one branded binary capture.
116
+ * @param {object} capture Opaque pending capture token.
117
+ * @param {number} [maxBytes] Maximum bytes copied in this call.
118
+ * @returns {boolean} Whether the entire payload has been copied.
119
+ * @internal
120
+ */
121
+ static copyPropertyCaptureChunk(
122
+ capture,
123
+ maxBytes = BINARY_COPY_CHUNK_BYTES
124
+ ) {
125
+ const state = PENDING_BINARY_CAPTURES.get(capture)
126
+ if (!state || !Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
127
+ throw new TypeError(
128
+ 'Canonical extension binary capture is not pending.'
129
+ )
130
+ }
131
+ const remaining = state.sourceRange.byteLength - state.offset
132
+ const count = Math.min(remaining, maxBytes)
133
+ BinaryDataSnapshot.copyBytesInto(
134
+ state.sourceRange,
135
+ state.bytes,
136
+ state.offset,
137
+ count
138
+ )
139
+ state.offset += count
140
+ return state.offset === state.sourceRange.byteLength
141
+ }
142
+
143
+ /**
144
+ * Finalizes one completely copied binary property into a trusted record.
145
+ * @param {object} capture Opaque pending capture token.
146
+ * @returns {object} Opaque captured binary property.
147
+ * @internal
148
+ */
149
+ static finishPropertyCapture(capture) {
150
+ const state = PENDING_BINARY_CAPTURES.get(capture)
151
+ if (!state || state.offset !== state.sourceRange.byteLength) {
152
+ throw new TypeError(
153
+ 'Canonical extension binary capture is incomplete.'
154
+ )
155
+ }
156
+ PENDING_BINARY_CAPTURES.delete(capture)
157
+ const ownedBinary = BinaryDataSnapshot.cloneFromBytes(
158
+ state.source,
159
+ state.sourceRange,
160
+ state.bytes
161
+ )
162
+ const ownedRange = BinaryDataSnapshot.describeStandard(ownedBinary)
163
+ if (!ownedRange) {
164
+ throw new TypeError(
165
+ 'Canonical extension binary could not be isolated.'
166
+ )
167
+ }
168
+ const captured = Object.freeze({
169
+ descriptor: state.descriptor,
170
+ key: state.key,
171
+ ownedBinary,
172
+ ownedRange: Object.freeze({ ...ownedRange }),
173
+ owner: state.owner
174
+ })
175
+ CAPTURED_BINARY_PROPERTIES.add(captured)
176
+ return captured
177
+ }
178
+
179
+ /**
180
+ * Installs one previously captured binary as a defensive-copy getter after
181
+ * proving its property descriptor still matches the transferred source.
182
+ * @param {object} captured Opaque captured binary property.
183
+ * @returns {void}
184
+ */
185
+ static protectCapturedProperty(captured) {
186
+ if (!CAPTURED_BINARY_PROPERTIES.has(captured)) {
187
+ throw new TypeError(
188
+ 'Canonical extension binary capture is not trusted.'
189
+ )
190
+ }
191
+ CAPTURED_BINARY_PROPERTIES.delete(captured)
192
+ let descriptor
193
+ try {
194
+ descriptor = Object.getOwnPropertyDescriptor(
195
+ captured.owner,
196
+ captured.key
197
+ )
198
+ } catch {
199
+ throw new TypeError(
200
+ 'Canonical extension data could not be inspected safely.'
201
+ )
202
+ }
203
+ if (
204
+ !ProtectedExtensionBinaryBoundary.#sameDataDescriptor(
205
+ descriptor,
206
+ captured.descriptor
207
+ )
208
+ ) {
209
+ throw new TypeError(
210
+ 'Canonical extension binary property changed during adoption.'
211
+ )
212
+ }
213
+ /** @returns {ArrayBuffer | Uint8Array | DataView} A defensive binary copy. */
214
+ const readBinary = () =>
215
+ BinaryDataSnapshot.clone(captured.ownedBinary, captured.ownedRange)
216
+ PROTECTED_BINARY_GETTERS.add(readBinary)
217
+ Object.defineProperty(captured.owner, captured.key, {
218
+ configurable: false,
219
+ enumerable: descriptor.enumerable,
220
+ get: readBinary
221
+ })
222
+ }
223
+
9
224
  /**
10
225
  * Replaces owned binary data properties with defensive-copy getters.
11
226
  * @param {unknown} root Owned extension graph.
@@ -98,6 +313,24 @@ export class ProtectedExtensionBinaryBoundary {
98
313
  )
99
314
  }
100
315
 
316
+ /**
317
+ * Compares exact ordinary data descriptor semantics without coercion.
318
+ * @param {PropertyDescriptor | undefined} current Current descriptor.
319
+ * @param {PropertyDescriptor} expected Previously captured descriptor.
320
+ * @returns {boolean} Whether both descriptors still match.
321
+ */
322
+ static #sameDataDescriptor(current, expected) {
323
+ return Boolean(
324
+ current &&
325
+ Object.hasOwn(current, 'value') &&
326
+ Object.hasOwn(expected, 'value') &&
327
+ Object.is(current.value, expected.value) &&
328
+ current.configurable === expected.configurable &&
329
+ current.enumerable === expected.enumerable &&
330
+ current.writable === expected.writable
331
+ )
332
+ }
333
+
101
334
  /**
102
335
  * Returns true for plain extension containers.
103
336
  * @param {unknown} value Candidate value.