🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@huggingface/xetchunk-wasm

Package Overview
Dependencies
Maintainers
5
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@huggingface/xetchunk-wasm - npm Package Compare versions

Comparing version
0.0.3
to
0.0.4
+51
src/hash-utils.ts
import { Hasher } from "@huggingface/blake3-jit";
import type { Chunk } from "./xet-chunker.js";
import { xorbHash } from "./xorb-hash.js";
const ZERO_KEY = new Uint8Array(32);
const VERIFICATION_KEY = new Uint8Array([
127, 24, 87, 214, 206, 86, 237, 102, 18, 127, 249, 19, 231, 165, 195, 243, 164, 205, 38, 213, 181, 219, 73, 230,
65, 36, 152, 127, 40, 251, 148, 195,
]);
const fileHasher = Hasher.newKeyed(ZERO_KEY);
const verificationHasher = Hasher.newKeyed(VERIFICATION_KEY);
/**
* file_hash = hmac(xorb_hash(chunks), zero_key)
*
* Matches Rust's `merklehash::file_hash` which calls
* `file_hash_with_salt(chunks, &[0; 32])`.
*/
export function fileHash(chunks: Chunk[]): Uint8Array {
const xorb = xorbHash(chunks);
return fileHasher.reset().update(xorb).finalize(32);
}
/**
* HMAC: blake3_keyed_hash(key_bytes, hash_bytes)
*
* Both inputs are 32-byte Uint8Arrays.
* Matches Rust's `DataHash::hmac`.
*
* Uses a fresh hasher per call since the key varies.
*/
export function hmac(hash: Uint8Array, key: Uint8Array): Uint8Array {
return Hasher.newKeyed(key).update(hash).finalize(32);
}
/**
* Verification hash for a range of chunk hashes.
* Concatenates all 32-byte hashes and applies blake3_keyed_hash
* with VERIFICATION_KEY.
*
* Matches Rust's `chunk_verification::range_hash_from_chunks`.
*/
export function verificationHash(chunkHashes: Uint8Array[]): Uint8Array {
const combined = new Uint8Array(chunkHashes.length * 32);
for (let i = 0; i < chunkHashes.length; i++) {
combined.set(chunkHashes[i], i * 32);
}
return verificationHasher.reset().update(combined).finalize(32);
}
export { createChunker, finalize, nextBlock, getChunks, hashToHex, hexToBytes, type Chunk } from "./xet-chunker.js";
export { xorbHash } from "./xorb-hash.js";
export { fileHash, hmac, verificationHash } from "./hash-utils.js";
import { Hasher } from "gearhash-jit";
import { createKeyed, Hasher as Blake3Hasher } from "@huggingface/blake3-jit";
const TARGET_CHUNK_SIZE = 64 * 1024; // 64KB
const MINIMUM_CHUNK_DIVISOR = 8;
const MAXIMUM_CHUNK_MULTIPLIER = 2;
const HASH_WINDOW_SIZE = 64;
const BLAKE3_DATA_KEY = new Uint8Array([
102, 151, 245, 119, 91, 149, 80, 222, 49, 53, 203, 172, 165, 151, 24, 28, 157, 228, 33, 16, 155, 235, 43, 88, 180,
208, 176, 75, 147, 173, 242, 41,
]);
export interface Chunk {
hash: Uint8Array;
length: number;
}
interface NextResult {
chunk: Chunk | null;
bytesConsumed: number;
}
class XetChunker {
private minimumChunk: number;
private maximumChunk: number;
private chunkBuf: Uint8Array;
private curChunkLen: number;
private gear: Hasher;
private blake3: Blake3Hasher;
constructor(targetChunkSize: number = TARGET_CHUNK_SIZE) {
if (targetChunkSize <= 0) {
throw new Error("Target chunk size must be greater than 0");
}
if ((targetChunkSize & (targetChunkSize - 1)) !== 0) {
throw new Error("Target chunk size must be a power of 2");
}
if (targetChunkSize <= HASH_WINDOW_SIZE) {
throw new Error("Target chunk size must be greater than hash window size");
}
if (targetChunkSize >= Number.MAX_SAFE_INTEGER) {
throw new Error("Target chunk size must be less than Number.MAX_SAFE_INTEGER");
}
let mask = BigInt(targetChunkSize - 1);
let leadingZeros = 0;
for (let i = 63; i >= 0; i--) {
if ((mask & (1n << BigInt(i))) !== 0n) {
break;
}
leadingZeros++;
}
mask = mask << BigInt(leadingZeros);
const maximumChunk = targetChunkSize * MAXIMUM_CHUNK_MULTIPLIER;
this.minimumChunk = targetChunkSize / MINIMUM_CHUNK_DIVISOR;
this.maximumChunk = maximumChunk;
this.chunkBuf = new Uint8Array(maximumChunk);
this.curChunkLen = 0;
this.gear = new Hasher(mask);
this.blake3 = Blake3Hasher.newKeyed(BLAKE3_DATA_KEY);
}
/**
* Streaming entry point: accepts an arbitrary slice of data, accumulates
* it, and emits a chunk when a boundary (or max size) is reached.
* Data is copied into an internal buffer because it may span calls.
*/
next(data: Uint8Array, isFinal: boolean): NextResult {
const nBytes = data.length;
let createChunk = false;
let consumeLen = 0;
if (nBytes !== 0) {
if (this.curChunkLen + HASH_WINDOW_SIZE < this.minimumChunk) {
const maxAdvance = Math.min(this.minimumChunk - this.curChunkLen - HASH_WINDOW_SIZE - 1, nBytes - consumeLen);
consumeLen += maxAdvance;
this.curChunkLen += maxAdvance;
}
const readEnd = Math.min(nBytes, consumeLen + this.maximumChunk - this.curChunkLen);
let bytesToNextBoundary: number;
const position = this.gear.nextMatch(data.subarray(consumeLen, readEnd));
if (position !== -1) {
bytesToNextBoundary = position;
createChunk = true;
} else {
bytesToNextBoundary = readEnd - consumeLen;
}
if (bytesToNextBoundary + this.curChunkLen >= this.maximumChunk) {
bytesToNextBoundary = this.maximumChunk - this.curChunkLen;
createChunk = true;
}
this.curChunkLen += bytesToNextBoundary;
consumeLen += bytesToNextBoundary;
this.chunkBuf.set(data.subarray(0, consumeLen), this.curChunkLen - consumeLen);
}
if (createChunk || (isFinal && this.curChunkLen > 0)) {
const chunkData = this.chunkBuf.subarray(0, this.curChunkLen);
const hash = this.blake3.reset().update(chunkData).finalize(32);
const chunk: Chunk = {
length: chunkData.length,
hash: hash,
};
this.curChunkLen = 0;
this.gear.resetHash();
return {
chunk,
bytesConsumed: consumeLen,
};
}
return {
chunk: null,
bytesConsumed: consumeLen,
};
}
/**
* Batch entry point: processes a large contiguous buffer and returns all
* complete chunks. Hashes directly from `data` — no intermediate copy
* to chunkBuf — for every chunk whose bytes are fully within `data`.
*/
nextBlock(data: Uint8Array, isFinal: boolean): Chunk[] {
const chunks: Chunk[] = [];
let pos = 0;
// Drain any leftover from a previous nextBlock / next call.
while (pos < data.length && this.curChunkLen > 0) {
const result = this.next(data.subarray(pos), false);
if (result.chunk) chunks.push(result.chunk);
pos += result.bytesConsumed;
}
const minSkip = this.minimumChunk > HASH_WINDOW_SIZE
? this.minimumChunk - HASH_WINDOW_SIZE - 1
: 0;
while (pos < data.length) {
const chunkStart = pos;
const scanStart = Math.min(pos + minSkip, data.length);
const scanEnd = Math.min(data.length, pos + this.maximumChunk);
const position = this.gear.nextMatch(data.subarray(scanStart, scanEnd));
let chunkEnd: number;
let foundBoundary: boolean;
if (position !== -1 && scanStart + position - chunkStart <= this.maximumChunk) {
chunkEnd = scanStart + position;
foundBoundary = true;
} else if (scanEnd - chunkStart >= this.maximumChunk) {
chunkEnd = chunkStart + this.maximumChunk;
foundBoundary = true;
} else {
foundBoundary = false;
chunkEnd = scanEnd;
}
if (foundBoundary) {
const hash = this.blake3.reset()
.update(data.subarray(chunkStart, chunkEnd))
.finalize(32);
chunks.push({ length: chunkEnd - chunkStart, hash });
pos = chunkEnd;
this.gear.resetHash();
} else if (isFinal) {
const hash = this.blake3.reset()
.update(data.subarray(chunkStart))
.finalize(32);
chunks.push({ length: data.length - chunkStart, hash });
pos = data.length;
} else {
this.chunkBuf.set(data.subarray(chunkStart), 0);
this.curChunkLen = data.length - chunkStart;
pos = data.length;
}
}
return chunks;
}
finish(): Chunk | null {
if (this.curChunkLen > 0) {
const chunkData = this.chunkBuf.subarray(0, this.curChunkLen);
const hash = this.blake3.reset().update(chunkData).finalize(32);
const chunk: Chunk = { length: this.curChunkLen, hash };
this.curChunkLen = 0;
this.gear.resetHash();
return chunk;
}
return null;
}
}
export function createChunker(targetChunkSize: number = TARGET_CHUNK_SIZE): XetChunker {
return new XetChunker(targetChunkSize);
}
export function nextBlock(chunker: XetChunker, data: Uint8Array): Chunk[] {
return chunker.nextBlock(data, false);
}
export function finalize(chunker: XetChunker): Chunk | null {
return chunker.finish();
}
export function getChunks(data: Uint8Array, targetChunkSize: number = TARGET_CHUNK_SIZE): Chunk[] {
const chunker = createChunker(targetChunkSize);
return chunker.nextBlock(data, true);
}
export function hashToHex(hash: Uint8Array): string {
const view = new DataView(hash.buffer, hash.byteOffset, hash.byteLength);
const u64 = view.getBigUint64(0, true);
const u64_2 = view.getBigUint64(8, true);
const u64_3 = view.getBigUint64(16, true);
const u64_4 = view.getBigUint64(24, true);
return (
u64.toString(16).padStart(16, "0") +
u64_2.toString(16).padStart(16, "0") +
u64_3.toString(16).padStart(16, "0") +
u64_4.toString(16).padStart(16, "0")
);
}
export function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(32);
const view = new DataView(bytes.buffer);
view.setBigUint64(0, BigInt("0x" + hex.slice(0, 16)), true);
view.setBigUint64(8, BigInt("0x" + hex.slice(16, 32)), true);
view.setBigUint64(16, BigInt("0x" + hex.slice(32, 48)), true);
view.setBigUint64(24, BigInt("0x" + hex.slice(48, 64)), true);
return bytes;
}
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 };
}
+3
-2
{
"name": "@huggingface/xetchunk-wasm",
"version": "0.0.3",
"version": "0.0.4",
"description": "Content-defined chunking and hashing for Hugging Face Xet storage",

@@ -17,2 +17,3 @@ "keywords": [

"dist",
"src",
"README.md"

@@ -33,3 +34,3 @@ ],

"gearhash-jit": "workspace:*",
"@huggingface/blake3-jit": "^0.0.1"
"@huggingface/blake3-jit": "workspace:*"
},

@@ -36,0 +37,0 @@ "devDependencies": {