Visitar URL original
Fix Equal.equals treating same-length ArrayBuffers as equal (#9769) · Effect-TS/effect@5e25cc9 · GitHub
Skip to content

Commit 5e25cc9

Browse files
Marve10seffect-bot
andauthored
Fix Equal.equals treating same-length ArrayBuffers as equal (#9769)
Co-authored-by: Effect Bot <tech-ops@effectful.co>
1 parent 958307b commit 5e25cc9

6 files changed

Lines changed: 69 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"effect": patch
3+
---
4+
5+
Fix `Equal.equals` and `Hash.hash` treating any two same-length `ArrayBuffer`s as equal. `ArrayBuffer` and `SharedArrayBuffer` are now compared and hashed by their bytes.

‎packages/effect/src/Equal.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
*/
1313
import type { Equivalence } from "./Equivalence.ts"
1414
import * as Hash from "./Hash.ts"
15-
import { byReferenceInstances, getAllObjectKeys, viewBytes } from "./internal/equal.ts"
15+
import { bufferBytes, byReferenceInstances, getAllObjectKeys, isArrayBufferLike, viewBytes } from "./internal/equal.ts"
1616
import { hasProperty } from "./Predicate.ts"
1717

1818
/**
@@ -257,6 +257,13 @@ function compareStructure(self: object, that: object): boolean {
257257
return false
258258
}
259259
return compareArrays(self, that)
260+
} else if (isArrayBufferLike(self)) {
261+
if (!isArrayBufferLike(that) || (self instanceof ArrayBuffer) !== (that instanceof ArrayBuffer)) {
262+
return false
263+
}
264+
const selfBytes = bufferBytes(self)
265+
const thatBytes = bufferBytes(that)
266+
return selfBytes !== undefined && thatBytes !== undefined && compareTypedArrays(selfBytes, thatBytes)
260267
} else if (ArrayBuffer.isView(self)) {
261268
const selfIsDataView = self instanceof DataView
262269
if (

‎packages/effect/src/Hash.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* @since 2.0.0
1212
*/
1313
import { dual } from "./Function.ts"
14-
import { byReferenceInstances, getAllObjectKeys, viewBytes } from "./internal/equal.ts"
14+
import { bufferBytes, byReferenceInstances, getAllObjectKeys, isArrayBufferLike, viewBytes } from "./internal/equal.ts"
1515
import { addBackEdge, backEdges } from "./internal/hash.ts"
1616
import { hasProperty } from "./Predicate.ts"
1717

@@ -144,6 +144,9 @@ export const hash: <A>(self: A) => number = <A>(self: A) => {
144144
h = (self as Hash)[symbol]()
145145
} else if (typeof self === "function") {
146146
h = random(self)
147+
} else if (isArrayBufferLike(self)) {
148+
const bytes = bufferBytes(self)
149+
h = bytes === undefined ? random(self) : array(bytes)
147150
} else if (self instanceof DataView) {
148151
h = array(viewBytes(self))
149152
} else if (Array.isArray(self) || ArrayBuffer.isView(self)) {

‎packages/effect/src/internal/equal.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,20 @@ export const byReferenceInstances = new WeakSet<object>()
2929

3030
/** @internal */
3131
export const viewBytes = (view: DataView): Uint8Array => new Uint8Array(view.buffer, view.byteOffset, view.byteLength)
32+
33+
/** @internal */
34+
export const isArrayBufferLike = (u: unknown): u is ArrayBufferLike =>
35+
u instanceof ArrayBuffer || (typeof SharedArrayBuffer !== "undefined" && u instanceof SharedArrayBuffer)
36+
37+
/**
38+
* Returns `undefined` for detached buffers, which can no longer be read.
39+
*
40+
* @internal
41+
*/
42+
export const bufferBytes = (buffer: ArrayBufferLike): Uint8Array | undefined => {
43+
try {
44+
return new Uint8Array(buffer)
45+
} catch {
46+
return undefined
47+
}
48+
}

‎packages/effect/test/Equal.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,37 @@ describe("Equal.equals", () => {
416416
})
417417
})
418418

419+
describe("ArrayBuffer objects", () => {
420+
const make = (Buffer: typeof ArrayBuffer | typeof SharedArrayBuffer, bytes: ReadonlyArray<number>) => {
421+
const buffer = new Buffer(bytes.length)
422+
new Uint8Array(buffer).set(bytes)
423+
return buffer
424+
}
425+
426+
it("should compare bytes", () => {
427+
for (const Buffer of [ArrayBuffer, SharedArrayBuffer]) {
428+
expect(Equal.equals(make(Buffer, [1, 2]), make(Buffer, [1, 2]))).toBe(true)
429+
expect(Equal.equals(make(Buffer, [1, 2]), make(Buffer, [9, 9]))).toBe(false)
430+
}
431+
})
432+
433+
it("should not equal views or other buffer kinds with the same bytes", () => {
434+
const buffer = make(ArrayBuffer, [1, 2])
435+
for (const other of [new Uint8Array(buffer), make(SharedArrayBuffer, [1, 2])]) {
436+
expect(Equal.equals(buffer, other)).toBe(false)
437+
expect(Equal.equals(other, buffer)).toBe(false)
438+
}
439+
})
440+
441+
it("should compare detached buffers by reference", () => {
442+
const self = new ArrayBuffer(2)
443+
const that = new ArrayBuffer(2)
444+
structuredClone(self, { transfer: [self] })
445+
structuredClone(that, { transfer: [that] })
446+
expect(Equal.equals(self, that)).toBe(false)
447+
})
448+
})
449+
419450
describe("DataView objects", () => {
420451
it("should compare viewed bytes", () => {
421452
const self = new DataView(Uint8Array.of(1).buffer)

‎packages/effect/test/Hash.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ describe("Hash", () => {
137137
it("does not collide for 10k multiples of 0.001", () => {
138138
assertNoHashCollisions(range(10_000).map((i) => i * 0.001))
139139
})
140+
141+
it("does not collide for ArrayBuffers with different bytes", () => {
142+
assert.notStrictEqual(Hash.hash(Uint8Array.of(1, 2).buffer), Hash.hash(Uint8Array.of(9, 9).buffer))
143+
})
140144
})
141145

142146
describe("floats", () => {

0 commit comments

Comments
 (0)