File size: 1,882 Bytes
0865492
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import { Hasher } from "@huggingface/blake3-jit";
import type { Chunk } from "./xet-chunker.js";
import { hashToHex } from "./xet-chunker.js";

const MEAN_CHUNK_PER_NODE = 4;

const BLAKE3_NODE_KEY = new Uint8Array([
	1, 126, 197, 199, 165, 71, 41, 150, 253, 148, 102, 102, 180, 138, 2, 230, 93, 221, 83, 111, 55, 199, 109, 210, 248,
	99, 82, 230, 74, 83, 113, 63,
]);

const INDEX_OF_LAST_BYTE_OF_LAST_U64_IN_CHUNK_HASH = 3 * 8;

const nodeHasher = Hasher.newKeyed(BLAKE3_NODE_KEY);

export function xorbHash(chunks: Chunk[]): Uint8Array {
	if (chunks.length === 0) {
		return new Uint8Array(32);
	}

	let currentChunks = chunks;

	while (currentChunks.length > 1) {
		const nodes: Chunk[] = [];
		let currentIndex = 0;
		let numOfChildrenSoFar = 0;

		for (let i = 0; i < currentChunks.length; i++) {
			if (
				i === currentChunks.length - 1 ||
				numOfChildrenSoFar === 2 * MEAN_CHUNK_PER_NODE ||
				(numOfChildrenSoFar >= 2 &&
					currentChunks[i].hash[INDEX_OF_LAST_BYTE_OF_LAST_U64_IN_CHUNK_HASH] % MEAN_CHUNK_PER_NODE === 0)
			) {
				nodes.push(mergedHashOfSequence(currentChunks.slice(currentIndex, i + 1)));
				currentIndex = i + 1;
				numOfChildrenSoFar = 0;
			} else {
				numOfChildrenSoFar++;
			}
		}
		currentChunks = nodes;
	}

	return currentChunks[0].hash;
}

/**
 * Matches Rust's `merged_hash_of_sequence`: serializes each entry as
 * "{hash_hex} : {length_decimal}\n" then hashes with BLAKE3_NODE_KEY.
 */
function mergedHashOfSequence(chunks: Chunk[]): Chunk {
	let text = "";
	let totalLength = 0;
	for (const chunk of chunks) {
		text += hashToHex(chunk.hash) + " : " + chunk.length + "\n";
		totalLength += chunk.length;
	}
	const bytes = new Uint8Array(text.length);
	for (let i = 0; i < text.length; i++) {
		bytes[i] = text.charCodeAt(i);
	}
	const hash = nodeHasher.reset().update(bytes).finalize(32);
	return { hash, length: totalLength };
}