@amritk/nish-x86_64-linux 0.27.2 → 0.28.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.
@@ -0,0 +1,335 @@
1
+ /**
2
+ * `nish/json-equal` — whether two JSON values are equal by JSON Schema's
3
+ * rule, a canonical text and a hash that agree with it, and the first
4
+ * repeated element of an array, which is what `const`, `enum` and
5
+ * `uniqueItems` ask.
6
+ *
7
+ * import { jsonEqual, jsonEqualFirstDuplicate } from "nish/json-equal"
8
+ *
9
+ * const doc = new JsonDocument(new JsonLimits())
10
+ * const a = doc.parseText('[{"x":1,"y":[true]},{"y":[true],"x":1.0}]')
11
+ * jsonEqual(doc, doc.at(a, 0), doc, doc.at(a, 1)) // true
12
+ * jsonEqualFirstDuplicate(doc, a) // 1
13
+ *
14
+ * **The rule.** Two values are equal when they are the same kind and: two
15
+ * numbers have the same mathematical value (`1`, `1.0` and `1e0` are equal;
16
+ * `nish/json-number` decides, never through `f64`), two strings have the
17
+ * same string, two booleans the same value, two arrays the same length and
18
+ * equal elements in order, and two objects the same keys with equal values,
19
+ * in whatever order the members are written. `null` equals only `null`, and
20
+ * `true` is not `1`. A number whose exponent is past
21
+ * `JSON_NUMBER_MAX_EXPONENT` cannot be read exactly, and equals only a number
22
+ * written with the same text.
23
+ *
24
+ * **No depth limit.** Every walk keeps its own stack in an array, so a tree
25
+ * built with `push` and `set` deeper than any parser allows compares as well
26
+ * as a flat one, and nothing recurses.
27
+ *
28
+ * **What a call costs.** `jsonEqual` stops at the first difference. Up to
29
+ * there it is linear in the values' size, except that each pair of objects
30
+ * reads every key (one string each) and sorts them, `n log n` for `n`
31
+ * members; it also allocates one string per string value, its stack, and
32
+ * whatever `jsonNumberCompare` allocates per number pair. `jsonEqualCanonical`
33
+ * builds the canonical text, about the size of the value written as JSON,
34
+ * and sorts keys the same way. `jsonEqualHash` is FNV-1a of that text's
35
+ * units: its bytes natively, its code units under Node. Every order and
36
+ * length below is in those units too.
37
+ * `jsonEqualFirstDuplicate` makes the canonical text of every element up to
38
+ * the first repeat and keeps them in a `Map`.
39
+ *
40
+ * **The hash is not keyed.** FNV-1a (public domain) is the same in every
41
+ * process, so an attacker can make many values that share a hash, and a
42
+ * table keyed by `jsonEqualHash` alone degrades to a list. It is for a
43
+ * filter or a fingerprint; `jsonEqualFirstDuplicate` keys its `Map` by the
44
+ * canonical text, which the `Map`'s own keyed hash spreads.
45
+ */
46
+
47
+ import { jsonNumberCanonical, jsonNumberCompare } from "nish/json-number"
48
+ import {
49
+ JSON_VALUE_ARRAY,
50
+ JSON_VALUE_BOOLEAN,
51
+ JSON_VALUE_NONE,
52
+ JSON_VALUE_NULL,
53
+ JSON_VALUE_NUMBER,
54
+ JSON_VALUE_OBJECT,
55
+ JSON_VALUE_STRING,
56
+ JsonDocument,
57
+ } from "nish/json-value"
58
+
59
+ /** On the canonical walk's stack, in place of a value: write the next key on the key stack. */
60
+ const JSON_EQUAL_KEY_TASK: i32 = -1
61
+
62
+ /** Whether `a` sorts before `b`, unit by unit (byte or code unit) and a prefix first. */
63
+ const jsonEqualBefore = (a: string, b: string): boolean => {
64
+ for (let k: i32 = 0; k < toI32(a.length) && k < toI32(b.length); k++) {
65
+ const x: i32 = toI32(a.charCodeAt(k))
66
+ const y: i32 = toI32(b.charCodeAt(k))
67
+ if (x !== y) {
68
+ return x < y
69
+ }
70
+ }
71
+ return toI32(a.length) < toI32(b.length)
72
+ }
73
+
74
+ // Guarded reads and writes for indexes that stay inside their arrays by the
75
+ // algorithm's reasoning rather than by a test the compiler can see.
76
+
77
+ /** `a[i]`, or `0` outside `a`. */
78
+ const jsonEqualAt = (a: i32[], i: i32): i32 => (i >= 0 && i < toI32(a.length) ? a[i] : 0)
79
+
80
+ /** `keys[i]`, or `""` outside `keys`. */
81
+ const jsonEqualKeyAt = (keys: string[], i: i32): string => (i >= 0 && i < toI32(keys.length) ? keys[i] : "")
82
+
83
+ /** `a[i] = x`, nothing outside `a`. */
84
+ const jsonEqualSetAt = (a: i32[], i: i32, x: i32): void => {
85
+ if (i >= 0 && i < toI32(a.length)) {
86
+ a[i] = x
87
+ }
88
+ }
89
+
90
+ /**
91
+ * The member positions of object `v` in key order, with every key put
92
+ * in `keys` by its position. A bottom-up merge sort: `n log n` comparisons and
93
+ * no recursion. Keys in one object are distinct, so the order is total.
94
+ */
95
+ const jsonEqualSortedMembers = (doc: JsonDocument, v: i32, keys: string[]): i32[] => {
96
+ const n: i32 = doc.length(v)
97
+ let order: i32[] = []
98
+ let spare: i32[] = []
99
+ for (let i: i32 = 0; i < n; i++) {
100
+ keys.push(doc.keyAt(v, i))
101
+ order.push(i)
102
+ spare.push(i)
103
+ }
104
+ // A document holds at most 2^22 values, so the widths below never come
105
+ // near overflowing.
106
+ let width: i32 = 1
107
+ while (width < n) {
108
+ let lo: i32 = 0
109
+ while (lo < n) {
110
+ const mid: i32 = lo + width < n ? lo + width : n
111
+ const hi: i32 = mid + width < n ? mid + width : n
112
+ let i: i32 = lo
113
+ let j: i32 = mid
114
+ for (let k: i32 = lo; k < hi; k++) {
115
+ const left: i32 = jsonEqualAt(order, i)
116
+ const right: i32 = jsonEqualAt(order, j)
117
+ const takeLeft: boolean =
118
+ i < mid && (j >= hi || !jsonEqualBefore(jsonEqualKeyAt(keys, right), jsonEqualKeyAt(keys, left)))
119
+ jsonEqualSetAt(spare, k, takeLeft ? left : right)
120
+ if (takeLeft) {
121
+ i += 1
122
+ } else {
123
+ j += 1
124
+ }
125
+ }
126
+ lo = hi
127
+ }
128
+ const done: i32[] = spare
129
+ spare = order
130
+ order = done
131
+ width = width * 2
132
+ }
133
+ return order
134
+ }
135
+
136
+ /** Whether two number texts are equal by value, or by text when either is past the exponent bound. */
137
+ const jsonEqualNumbers = (a: string, b: string): boolean => {
138
+ const c: i32 = jsonNumberCompare(a, b)
139
+ return c < -1 ? a === b : c === 0
140
+ }
141
+
142
+ /**
143
+ * Whether value `a` of document `da` equals value `b` of document `db` by
144
+ * JSON Schema's rule (the module header): numbers by value, objects whatever
145
+ * their member order. The two may be one document. `false` when either
146
+ * handle names no value. Iterative, so any depth compares.
147
+ */
148
+ export const jsonEqual = (da: JsonDocument, a: i32, db: JsonDocument, b: i32): boolean => {
149
+ const left: i32[] = [a]
150
+ const right: i32[] = [b]
151
+ while (left.length > 0) {
152
+ const x: i32 = jsonEqualAt(left, toI32(left.length) - 1)
153
+ const y: i32 = jsonEqualAt(right, toI32(right.length) - 1)
154
+ left.pop()
155
+ right.pop()
156
+ const kind: i32 = da.kind(x)
157
+ if (kind === JSON_VALUE_NONE || kind !== db.kind(y)) {
158
+ return false
159
+ }
160
+ if (kind === JSON_VALUE_BOOLEAN) {
161
+ if (da.boolean(x) !== db.boolean(y)) {
162
+ return false
163
+ }
164
+ } else if (kind === JSON_VALUE_NUMBER) {
165
+ if (!jsonEqualNumbers(da.numberText(x), db.numberText(y))) {
166
+ return false
167
+ }
168
+ } else if (kind === JSON_VALUE_STRING) {
169
+ if (!db.stringEquals(y, da.string(x))) {
170
+ return false
171
+ }
172
+ } else if (kind === JSON_VALUE_ARRAY || kind === JSON_VALUE_OBJECT) {
173
+ const n: i32 = da.length(x)
174
+ if (n !== db.length(y)) {
175
+ return false
176
+ }
177
+ if (kind === JSON_VALUE_ARRAY) {
178
+ for (let i: i32 = 0; i < n; i++) {
179
+ left.push(da.at(x, i))
180
+ right.push(db.at(y, i))
181
+ }
182
+ } else {
183
+ const keysX: string[] = []
184
+ const keysY: string[] = []
185
+ const orderX: i32[] = jsonEqualSortedMembers(da, x, keysX)
186
+ const orderY: i32[] = jsonEqualSortedMembers(db, y, keysY)
187
+ for (let i: i32 = 0; i < n; i++) {
188
+ const mx: i32 = jsonEqualAt(orderX, i)
189
+ const my: i32 = jsonEqualAt(orderY, i)
190
+ if (jsonEqualKeyAt(keysX, mx) !== jsonEqualKeyAt(keysY, my)) {
191
+ return false
192
+ }
193
+ left.push(da.at(x, mx))
194
+ right.push(db.at(y, my))
195
+ }
196
+ }
197
+ }
198
+ }
199
+ return true
200
+ }
201
+
202
+ /** Appends `s` to `out` whole, so every unit of it is kept. */
203
+ const jsonEqualPut = (out: string[], s: string): void => {
204
+ out.push(s)
205
+ }
206
+
207
+ /** Appends `s` as the canonical form writes a string or a key: its length, `:`, the string. */
208
+ const jsonEqualPutText = (out: string[], s: string): void => {
209
+ jsonEqualPut(out, `${toI32(s.length)}:`)
210
+ jsonEqualPut(out, s)
211
+ }
212
+
213
+ /**
214
+ * Appends number `x`'s canonical form. Past the exponent bound a number is its
215
+ * own text, tagged apart so that it never meets a canonical one, which is
216
+ * `jsonEqual`'s fallback too. A function of its own so that the texts it makes
217
+ * are released when it returns rather than at the end of the walk.
218
+ */
219
+ const jsonEqualPutNumber = (out: string[], doc: JsonDocument, x: i32): void => {
220
+ const text: string = doc.numberText(x)
221
+ const canonical: string = jsonNumberCanonical(text)
222
+ jsonEqualPut(out, canonical === "" ? `x${text};` : `n${canonical};`)
223
+ }
224
+
225
+ /** The canonical text of value `v` (`jsonEqualCanonical`), or `""` for a handle that names no value. */
226
+ const jsonEqualCanonicalText = (doc: JsonDocument, v: i32): string => {
227
+ const out: string[] = []
228
+ if (doc.kind(v) === JSON_VALUE_NONE) {
229
+ return ""
230
+ }
231
+ const tasks: i32[] = [v]
232
+ const keyStack: string[] = []
233
+ while (tasks.length > 0) {
234
+ const x: i32 = jsonEqualAt(tasks, toI32(tasks.length) - 1)
235
+ tasks.pop()
236
+ if (x === JSON_EQUAL_KEY_TASK) {
237
+ jsonEqualPutText(out, jsonEqualKeyAt(keyStack, toI32(keyStack.length) - 1))
238
+ keyStack.pop()
239
+ continue
240
+ }
241
+ const kind: i32 = doc.kind(x)
242
+ if (kind === JSON_VALUE_NULL) {
243
+ jsonEqualPut(out, "z")
244
+ } else if (kind === JSON_VALUE_BOOLEAN) {
245
+ jsonEqualPut(out, doc.boolean(x) ? "t" : "f")
246
+ } else if (kind === JSON_VALUE_NUMBER) {
247
+ jsonEqualPutNumber(out, doc, x)
248
+ } else if (kind === JSON_VALUE_STRING) {
249
+ jsonEqualPut(out, "s")
250
+ jsonEqualPutText(out, doc.string(x))
251
+ } else if (kind === JSON_VALUE_ARRAY) {
252
+ const n: i32 = doc.length(x)
253
+ jsonEqualPut(out, `a${n}:`)
254
+ for (let i: i32 = n - 1; i >= 0; i--) {
255
+ tasks.push(doc.at(x, i))
256
+ }
257
+ } else {
258
+ const n: i32 = doc.length(x)
259
+ jsonEqualPut(out, `o${n}:`)
260
+ const keys: string[] = []
261
+ const order: i32[] = jsonEqualSortedMembers(doc, x, keys)
262
+ // Pushed last member first, each value before its key, so the key pops first.
263
+ for (let i: i32 = n - 1; i >= 0; i--) {
264
+ const m: i32 = jsonEqualAt(order, i)
265
+ tasks.push(doc.at(x, m))
266
+ tasks.push(JSON_EQUAL_KEY_TASK)
267
+ keyStack.push(jsonEqualKeyAt(keys, m))
268
+ }
269
+ }
270
+ }
271
+ return out.join("")
272
+ }
273
+
274
+ /**
275
+ * The canonical text of value `v`: the same text for exactly the values
276
+ * `jsonEqual` calls equal, so it is a key for a `Map` or `Set` of values. It
277
+ * is not JSON: each value is a tag and what it holds, `z`, `t` and `f` for
278
+ * null, true and false, `n` and a `jsonNumberCanonical` text and `;` for a
279
+ * number, `s`, a length, `:` and the string itself, `a` and a count and `:`
280
+ * before an array's elements, and `o` and a count and `:` before an object's
281
+ * members in key order, each key a length, `:` and the key (orders and lengths
282
+ * as the module header says).
283
+ * `""` for a handle that names no value.
284
+ */
285
+ export const jsonEqualCanonical = (doc: JsonDocument, v: i32): string => jsonEqualCanonicalText(doc, v)
286
+
287
+ /**
288
+ * A 64-bit hash of value `v` that agrees with `jsonEqual`: equal values hash
289
+ * the same. FNV-1a of the canonical text's units (its code units under
290
+ * Node, so a string past U+00FF hashes differently there), unkeyed (the
291
+ * module header says what that means for a table). The offset basis for a handle that names no
292
+ * value.
293
+ */
294
+ export const jsonEqualHash = (doc: JsonDocument, v: i32): u64 => {
295
+ const text: string = jsonEqualCanonicalText(doc, v)
296
+ // FNV-1a's 64-bit prime and offset basis (0xcbf29ce484222325, past what a
297
+ // literal writes exactly), public domain. A module constant cannot be a
298
+ // `u64`, so they are made here from 16-bit pieces, every one through
299
+ // `toU64`: under Node a `u64` is a BigInt, which no plain number may meet
300
+ // and which does not wrap, so the product is masked to 64 bits (a no-op
301
+ // natively).
302
+ const quarter: u64 = toU64(65535)
303
+ const half: u64 = (quarter << toU64(16)) | quarter
304
+ const all: u64 = (half << toU64(32)) | half
305
+ const prime: u64 = (toU64(256) << toU64(32)) | toU64(435)
306
+ const upper: u64 = (toU64(52210) << toU64(16)) | toU64(40164)
307
+ const lower: u64 = (toU64(33826) << toU64(16)) | toU64(8997)
308
+ let h: u64 = (upper << toU64(32)) | lower
309
+ for (let i: i32 = 0; i < toI32(text.length); i++) {
310
+ h = ((h ^ toU64(toI32(text.charCodeAt(i)))) * prime) & all
311
+ }
312
+ return h
313
+ }
314
+
315
+ /**
316
+ * The position of the first element of array `v` equal (`jsonEqual`) to an
317
+ * earlier one, or `-1` when every element is distinct or `v` is not an array:
318
+ * JSON Schema's `uniqueItems` holds exactly when this is `-1`. Linear in the
319
+ * elements read, through a `Map` of their canonical texts.
320
+ */
321
+ export const jsonEqualFirstDuplicate = (doc: JsonDocument, v: i32): i32 => {
322
+ if (doc.kind(v) !== JSON_VALUE_ARRAY) {
323
+ return -1
324
+ }
325
+ const seen = new Map<string, i32>()
326
+ const n: i32 = doc.length(v)
327
+ for (let i: i32 = 0; i < n; i++) {
328
+ const text: string = jsonEqualCanonical(doc, doc.at(v, i))
329
+ if (seen.has(text)) {
330
+ return i
331
+ }
332
+ seen.set(text, i)
333
+ }
334
+ return -1
335
+ }