232 lines
8.0 KiB
TypeScript
232 lines
8.0 KiB
TypeScript
import {describe, expect, it} from 'vitest';
|
|
import {BtihV2Hasher} from '../lib/file-id/BtihV2Hasher';
|
|
import {HashComputerWorker, pickCidByAlgorithm} from "../lib";
|
|
import {readFile} from 'fs/promises';
|
|
import {createHash} from 'crypto';
|
|
import * as process from "process";
|
|
|
|
process.env.WORKER_URL = "./dist/worker.js";
|
|
|
|
/**
|
|
* BitTorrent v2 Info Hash Tests (BEP 52)
|
|
*
|
|
* Tests verify:
|
|
* - 16KB block hashing
|
|
* - Merkle tree construction
|
|
* - Info hash computation
|
|
* - CID encoding format
|
|
* - Edge cases (empty files, partial blocks)
|
|
*/
|
|
describe('BitTorrent v2 Info Hash (BEP 52)', () => {
|
|
it('should compute info hash for small file', async () => {
|
|
const hasher = new BtihV2Hasher();
|
|
hasher.setFileName('test.txt');
|
|
|
|
// Read test file and feed to hasher
|
|
const testData = await readFile('./src/tests/test.txt');
|
|
hasher.update(testData);
|
|
|
|
// Get info hash
|
|
const infoHash = hasher.digest();
|
|
|
|
// Verify hash format
|
|
expect(infoHash).toBeInstanceOf(Buffer);
|
|
expect(infoHash.length).toBe(32); // SHA-256 produces 32 bytes
|
|
|
|
// Verify it's non-zero
|
|
const isNonZero = infoHash.some(byte => byte !== 0);
|
|
expect(isNonZero).toBe(true);
|
|
|
|
console.log('Info hash (hex):', infoHash.toString('hex'));
|
|
});
|
|
|
|
it('should handle empty file', () => {
|
|
const hasher = new BtihV2Hasher();
|
|
hasher.setFileName('empty.txt');
|
|
|
|
// Don't update with any data (empty file)
|
|
const infoHash = hasher.digest();
|
|
|
|
// Verify hash format
|
|
expect(infoHash).toBeInstanceOf(Buffer);
|
|
expect(infoHash.length).toBe(32);
|
|
|
|
// Should produce valid hash even for empty file
|
|
const isNonZero = infoHash.some(byte => byte !== 0);
|
|
expect(isNonZero).toBe(true);
|
|
|
|
console.log('Empty file info hash (hex):', infoHash.toString('hex'));
|
|
});
|
|
|
|
it('should handle file exactly one block (16KB)', () => {
|
|
const hasher = new BtihV2Hasher();
|
|
hasher.setFileName('exactly-16kb.bin');
|
|
|
|
// Create exactly 16KB of data
|
|
const oneBlock = Buffer.alloc(16 * 1024);
|
|
for (let i = 0; i < oneBlock.length; i++) {
|
|
oneBlock[i] = i % 256;
|
|
}
|
|
|
|
hasher.update(oneBlock);
|
|
const infoHash = hasher.digest();
|
|
|
|
expect(infoHash).toBeInstanceOf(Buffer);
|
|
expect(infoHash.length).toBe(32);
|
|
|
|
console.log('16KB file info hash (hex):', infoHash.toString('hex'));
|
|
});
|
|
|
|
it('should handle streaming updates (partial blocks)', () => {
|
|
const hasher = new BtihV2Hasher();
|
|
hasher.setFileName('streamed.bin');
|
|
|
|
// Simulate streaming: feed data in small chunks
|
|
const chunkSize = 1024; // 1KB chunks
|
|
const totalSize = 50 * 1024; // 50KB total
|
|
const data = Buffer.alloc(totalSize);
|
|
for (let i = 0; i < data.length; i++) {
|
|
data[i] = i % 256;
|
|
}
|
|
|
|
// Stream in chunks
|
|
for (let offset = 0; offset < data.length; offset += chunkSize) {
|
|
const end = Math.min(offset + chunkSize, data.length);
|
|
hasher.update(data.subarray(offset, end));
|
|
}
|
|
|
|
const infoHash = hasher.digest();
|
|
|
|
expect(infoHash).toBeInstanceOf(Buffer);
|
|
expect(infoHash.length).toBe(32);
|
|
|
|
console.log('Streamed 50KB info hash (hex):', infoHash.toString('hex'));
|
|
});
|
|
|
|
it('should compute consistent hash for same data', () => {
|
|
const testData = Buffer.from('Hello BitTorrent v2!');
|
|
|
|
// Compute hash twice
|
|
const hasher1 = new BtihV2Hasher();
|
|
hasher1.setFileName('hello.txt');
|
|
hasher1.update(testData);
|
|
const hash1 = hasher1.digest();
|
|
|
|
const hasher2 = new BtihV2Hasher();
|
|
hasher2.setFileName('hello.txt');
|
|
hasher2.update(testData);
|
|
const hash2 = hasher2.digest();
|
|
|
|
// Should produce identical hashes
|
|
expect(hash1.equals(hash2)).toBe(true);
|
|
});
|
|
|
|
it('should produce different hashes for different data', () => {
|
|
const data1 = Buffer.from('File A content');
|
|
const data2 = Buffer.from('File B content');
|
|
|
|
const hasher1 = new BtihV2Hasher();
|
|
hasher1.setFileName('fileA.txt');
|
|
hasher1.update(data1);
|
|
const hash1 = hasher1.digest();
|
|
|
|
const hasher2 = new BtihV2Hasher();
|
|
hasher2.setFileName('fileB.txt');
|
|
hasher2.update(data2);
|
|
const hash2 = hasher2.digest();
|
|
|
|
// Should produce different hashes
|
|
expect(hash1.equals(hash2)).toBe(false);
|
|
});
|
|
|
|
it('should produce different hashes for different filenames', () => {
|
|
const data = Buffer.from('Same content');
|
|
|
|
const hasher1 = new BtihV2Hasher();
|
|
hasher1.setFileName('nameA.txt');
|
|
hasher1.update(data);
|
|
const hash1 = hasher1.digest();
|
|
|
|
const hasher2 = new BtihV2Hasher();
|
|
hasher2.setFileName('nameB.txt');
|
|
hasher2.update(data);
|
|
const hash2 = hasher2.digest();
|
|
|
|
// Different filenames should produce different info hashes
|
|
expect(hash1.equals(hash2)).toBe(false);
|
|
});
|
|
|
|
it('should integrate with HashComputerWorker', async () => {
|
|
const hashComputer = new HashComputerWorker(['btih_v2', 'sha256']);
|
|
|
|
const metadata: any = {};
|
|
await hashComputer.computeMissingHash('./src/tests/test.txt', metadata);
|
|
|
|
// The digests land in a bare CID list; each is recovered by its
|
|
// multicodec, not by a field name.
|
|
const btih = pickCidByAlgorithm(metadata.cids, 'btih_v2');
|
|
expect(btih).toBeDefined();
|
|
expect(typeof btih).toBe('string');
|
|
expect(btih!.startsWith('b')).toBe(true);
|
|
|
|
// Should also have SHA-256 for comparison.
|
|
expect(pickCidByAlgorithm(metadata.cids, 'sha256')).toBeDefined();
|
|
});
|
|
|
|
it('should compute piece length correctly', () => {
|
|
// Test piece length formula: 16KB * 2^n where pieces < 2048
|
|
const testCases = [
|
|
{fileSize: 0, expectedMin: 16 * 1024, expectedMax: 16 * 1024}, // Empty file
|
|
{fileSize: 1024, expectedMin: 16 * 1024, expectedMax: 16 * 1024}, // 1KB
|
|
{fileSize: 16 * 1024, expectedMin: 16 * 1024, expectedMax: 16 * 1024}, // 16KB
|
|
{fileSize: 1024 * 1024, expectedMin: 16 * 1024, expectedMax: 16 * 1024}, // 1MB
|
|
{fileSize: 100 * 1024 * 1024, expectedMin: 64 * 1024, expectedMax: 128 * 1024}, // 100MB
|
|
];
|
|
|
|
for (const {fileSize, expectedMin, expectedMax} of testCases) {
|
|
const hasher = new BtihV2Hasher();
|
|
hasher.setFileName('size-test.bin');
|
|
|
|
// Feed dummy data of specified size
|
|
const chunkSize = 16 * 1024;
|
|
let remaining = fileSize;
|
|
while (remaining > 0) {
|
|
const toWrite = Math.min(chunkSize, remaining);
|
|
hasher.update(Buffer.alloc(toWrite));
|
|
remaining -= toWrite;
|
|
}
|
|
|
|
// Digest triggers piece length computation
|
|
const infoHash = hasher.digest();
|
|
|
|
// We can't directly access piece length from outside,
|
|
// but we can verify the hash was computed successfully
|
|
expect(infoHash).toBeInstanceOf(Buffer);
|
|
expect(infoHash.length).toBe(32);
|
|
}
|
|
});
|
|
|
|
it('should produce valid CID format with codec 0x10B7', async () => {
|
|
const hashComputer = new HashComputerWorker(['btih_v2']);
|
|
|
|
const metadata: any = {};
|
|
await hashComputer.computeMissingHash('./src/tests/test.txt', metadata);
|
|
|
|
const cid = pickCidByAlgorithm(metadata.cids, 'btih_v2');
|
|
|
|
// The selector proves the codec: pickCidByAlgorithm('btih_v2') matches on
|
|
// multihash 0x10B7, so a hit here IS the "valid CID with codec 0x10B7"
|
|
// assertion this test is named for.
|
|
|
|
// Verify CID format
|
|
expect(cid).toBeDefined();
|
|
expect(typeof cid).toBe('string');
|
|
|
|
// CIDv1 base32 starts with 'b'
|
|
expect(cid.startsWith('b')).toBe(true);
|
|
|
|
// Should be longer than minimum CID length
|
|
expect(cid.length).toBeGreaterThan(10);
|
|
});
|
|
});
|