initial commit

This commit is contained in:
Pierre
2026-01-11 17:26:49 +01:00
commit 888790fd11
240 changed files with 48691 additions and 0 deletions
@@ -0,0 +1,236 @@
import {describe, expect, it} from 'vitest';
import {BtihV2Hasher} from '../lib/file-id/BtihV2Hasher';
import {CID_ALGORITHM_NAMES, HashComputerWorker} 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([
CID_ALGORITHM_NAMES.btih_v2,
CID_ALGORITHM_NAMES.sha256
]);
const metadata: any = {};
await hashComputer.computeMissingHash('./src/tests/test.txt', metadata);
console.log('Computed metadata:', metadata);
// Verify btih_v2 CID was computed
expect(metadata).toHaveProperty('cid_btih_v2');
expect(typeof metadata.cid_btih_v2).toBe('string');
expect(metadata.cid_btih_v2.length).toBeGreaterThan(0);
// Verify CID format (should start with 'b' for base32 CIDv1)
expect(metadata.cid_btih_v2.startsWith('b')).toBe(true);
// Should also have SHA-256 for comparison
expect(metadata).toHaveProperty('cid_sha2-256');
});
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([
CID_ALGORITHM_NAMES.btih_v2
]);
const metadata: any = {};
await hashComputer.computeMissingHash('./src/tests/test.txt', metadata);
const cid = metadata.cid_btih_v2;
console.log('BitTorrent v2 CID:', cid);
// 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);
});
});
@@ -0,0 +1,175 @@
import {describe, expect, it} from 'vitest';
import {BtihV2Hasher, MagnetLinkGenerator} from '../lib';
import {readFile, stat} from 'fs/promises';
/**
* End-to-End Magnet Link Generation Tests
*
* Tests the complete workflow:
* 1. Compute BitTorrent v2 info hash from file
* 2. Generate magnet link
* 3. Parse and validate magnet link
*/
describe('Magnet Link Generation (End-to-End)', () => {
it('should generate valid magnet link from file', async () => {
const filePath = './src/tests/test.txt';
const fileName = 'test.txt';
// Step 1: Read file and compute info hash
const fileData = await readFile(filePath);
const fileStats = await stat(filePath);
const fileSize = fileStats.size;
const hasher = new BtihV2Hasher();
hasher.setFileName(fileName);
hasher.update(fileData);
const infoHash = hasher.digest();
// Verify info hash computed
expect(infoHash).toBeInstanceOf(Buffer);
expect(infoHash.length).toBe(32);
console.log('File:', fileName);
console.log('Size:', fileSize, 'bytes');
console.log('Info hash (hex):', infoHash.toString('hex'));
// Note: We can't test magnet link generation end-to-end without
// the CID encoding working properly through HashComputerWorker.
// This would require the worker integration to be fixed.
});
it('should generate magnet link from mock CID', () => {
// Mock CID format (this is a valid CIDv1 with our custom codec 0x10B7)
// In real usage, this would come from HashComputerWorker
const fileName = 'test-video.mp4';
const fileSize = 1024 * 1024 * 100; // 100MB
// Create a mock info hash (32 bytes of test data)
const mockInfoHash = Buffer.from(
'b04c198dd3148719db3e32fecc54ec41ec355d7f866a1c3f71bf980c3cde01bf',
'hex'
);
// For this test, we'll construct the magnet link manually
// since we can't get a real CID without the worker
const infoHashHex = mockInfoHash.toString('hex');
const magnetLink = `magnet:?xt=urn:btmh:1220${infoHashHex}&dn=${encodeURIComponent(fileName)}&xl=${fileSize}`;
console.log('\nGenerated magnet link:');
console.log(magnetLink);
// Verify magnet link format
expect(magnetLink.startsWith('magnet:?')).toBe(true);
expect(magnetLink.includes('xt=urn:btmh:1220')).toBe(true);
expect(magnetLink.includes(infoHashHex)).toBe(true);
expect(magnetLink.includes(`dn=${encodeURIComponent(fileName)}`)).toBe(true);
expect(magnetLink.includes(`xl=${fileSize}`)).toBe(true);
});
it('should validate magnet link format', () => {
const validLink = 'magnet:?xt=urn:btmh:1220' + 'a'.repeat(64) + '&dn=file.mp4&xl=1024';
const invalidLink1 = 'http://example.com';
const invalidLink2 = 'magnet:?xt=urn:btih:' + 'a'.repeat(40); // v1 format (SHA-1)
const invalidLink3 = 'magnet:?xt=urn:btmh:1220' + 'a'.repeat(32); // Too short
expect(MagnetLinkGenerator.isValid(validLink)).toBe(true);
expect(MagnetLinkGenerator.isValid(invalidLink1)).toBe(false);
expect(MagnetLinkGenerator.isValid(invalidLink2)).toBe(false);
expect(MagnetLinkGenerator.isValid(invalidLink3)).toBe(false);
});
it('should extract info hash from magnet link', () => {
const infoHash = 'b04c198dd3148719db3e32fecc54ec41ec355d7f866a1c3f71bf980c3cde01bf';
const magnetLink = `magnet:?xt=urn:btmh:1220${infoHash}&dn=file.mp4&xl=1024`;
const extracted = MagnetLinkGenerator.extractInfoHash(magnetLink);
expect(extracted).toBe(infoHash);
});
it('should extract filename from magnet link', () => {
const fileName = 'Big Buck Bunny.mp4';
const infoHash = 'a'.repeat(64);
const magnetLink = `magnet:?xt=urn:btmh:1220${infoHash}&dn=${encodeURIComponent(fileName)}&xl=1024`;
const extracted = MagnetLinkGenerator.extractFileName(magnetLink);
expect(extracted).toBe(fileName);
});
it('should extract file size from magnet link', () => {
const fileSize = 1024000;
const infoHash = 'a'.repeat(64);
const magnetLink = `magnet:?xt=urn:btmh:1220${infoHash}&dn=file.mp4&xl=${fileSize}`;
const extracted = MagnetLinkGenerator.extractFileSize(magnetLink);
expect(extracted).toBe(fileSize);
});
it('should parse complete magnet link', () => {
const infoHash = 'b04c198dd3148719db3e32fecc54ec41ec355d7f866a1c3f71bf980c3cde01bf';
const fileName = 'test-video.mp4';
const fileSize = 1024000;
const magnetLink = `magnet:?xt=urn:btmh:1220${infoHash}&dn=${encodeURIComponent(fileName)}&xl=${fileSize}`;
const parsed = MagnetLinkGenerator.parse(magnetLink);
expect(parsed).not.toBeNull();
expect(parsed!.infoHash).toBe(infoHash);
expect(parsed!.fileName).toBe(fileName);
expect(parsed!.fileSize).toBe(fileSize);
});
it('should handle special characters in filename', () => {
const fileName = 'My Video [1080p] (2024).mp4';
const infoHash = 'a'.repeat(64);
const magnetLink = `magnet:?xt=urn:btmh:1220${infoHash}&dn=${encodeURIComponent(fileName)}&xl=1024`;
const extracted = MagnetLinkGenerator.extractFileName(magnetLink);
expect(extracted).toBe(fileName);
// Verify round-trip
const parsed = MagnetLinkGenerator.parse(magnetLink);
expect(parsed?.fileName).toBe(fileName);
});
it('should demonstrate complete workflow (conceptual)', async () => {
// This test demonstrates the intended workflow once worker integration is fixed
const filePath = './src/tests/test.txt';
const fileStats = await stat(filePath);
console.log('\n=== Complete Workflow Demo ===');
console.log('1. File:', filePath);
console.log('2. Size:', fileStats.size, 'bytes');
// Step 1: Compute info hash
const fileData = await readFile(filePath);
const hasher = new BtihV2Hasher();
hasher.setFileName('test.txt');
hasher.update(fileData);
const infoHash = hasher.digest();
console.log('3. Info hash:', infoHash.toString('hex'));
// Step 2: In production, this would be the CID from metadata
// const cid = metadata.cid_btih_v2;
// const magnetLink = MagnetLinkGenerator.generate(metadata, 'test.txt', fileStats.size);
// Step 3: For demo, construct magnet link manually
const magnetLink = `magnet:?xt=urn:btmh:1220${infoHash.toString('hex')}&dn=test.txt&xl=${fileStats.size}`;
console.log('4. Magnet link:', magnetLink);
// Step 4: Verify it's valid
const isValid = MagnetLinkGenerator.isValid(magnetLink);
console.log('5. Valid:', isValid);
expect(isValid).toBe(true);
// Step 5: Parse it back
const parsed = MagnetLinkGenerator.parse(magnetLink);
console.log('6. Parsed:', parsed);
expect(parsed).not.toBeNull();
expect(parsed!.infoHash).toBe(infoHash.toString('hex'));
expect(parsed!.fileName).toBe('test.txt');
expect(parsed!.fileSize).toBe(fileStats.size);
});
});
@@ -0,0 +1,97 @@
import {CID_ALGORITHM_NAMES, HashComputerIndexCache, HashComputerWorker} from "../lib";
import {describe, expect, it} from 'vitest';
import * as process from "process";
import { mkdir, rm, rmdir } from "fs/promises";
process.env.WORKER_URL = "./dist/worker.js";
/**
* /!\ WARNING /!\
* Remember to build the worker each time you change the worker code.
*/
describe('add', () => {
it('meta data compute', async () => {
let hashComputer = new HashComputerWorker([
CID_ALGORITHM_NAMES.sha1,
CID_ALGORITHM_NAMES.sha256,
CID_ALGORITHM_NAMES.md5,
CID_ALGORITHM_NAMES.sha3_256,
CID_ALGORITHM_NAMES.crc32,
CID_ALGORITHM_NAMES.sha3_384
]);
let metadata = {};
await hashComputer.computeMissingHash('./src/tests/test.txt', metadata);
console.log(metadata);
/**
* test.txt CF20B62A (CRC32)
* 26f1157f4349a6fb5d207ac5f5995140 *test.txt (MD5)
* bb242b7b821074f4b9eb9a94d439ab668705aac4 ?SHA1*test.txt
* 2aa7a80d2fd1eb96d31afc7cdda9211a97524202c63f04151d6163fba35061a0 ?SHA256*test.txt
* 865a1ea4977b25e04b8ea84af93b21f1a031cb48359f3805089cdae1d89842b9a3faf87654989dcae418144a8bbb378c ?SHA3-384*test.txt
*/
expect(metadata).toEqual({
"cid_crc32": "bagzafmqcathsbnrk",
"cid_sha1": 'baeircff3eqvxxaqqot2lt242stkdtk3gq4c2vra',
"cid_sha2-256": 'baejbeibku6ua2l6r5olnggx4pto2sii2s5jeeawgh4cbkhlbmp52gudbua',
"cid_md5": 'bahkqdvibcatpcfl7ine2n625eb5ml5mzkfaa',
"cid_sha3-256": 'baelbmidner7x3dd7t2kldh6wiq4w6lsye7qrwo6uoqot2t52zkhqk56fnq',
"cid_sha3-384": 'baekrkmeglipkjf33exqexdvijl4twipruay4wsbvt44akce43lq5rgccxgr7v6dwksmj3sxedakevc53g6ga'
});
});
it('meta data compute with index', async () => {
await rm("./tmp",{recursive:true});
await mkdir("./tmp",{recursive:true});
let hashComputer = new HashComputerIndexCache("./tmp/index.csv",[
CID_ALGORITHM_NAMES.sha1,
CID_ALGORITHM_NAMES.sha256,
CID_ALGORITHM_NAMES.md5,
CID_ALGORITHM_NAMES.sha3_256,
CID_ALGORITHM_NAMES.crc32,
CID_ALGORITHM_NAMES.sha3_384
]);
let metadata = {};
await hashComputer.computeMissingHash('./src/tests/test.txt', metadata);
console.log(metadata);
/**
* test.txt CF20B62A (CRC32)
* 26f1157f4349a6fb5d207ac5f5995140 *test.txt (MD5)
* bb242b7b821074f4b9eb9a94d439ab668705aac4 ?SHA1*test.txt
* 2aa7a80d2fd1eb96d31afc7cdda9211a97524202c63f04151d6163fba35061a0 ?SHA256*test.txt
* 865a1ea4977b25e04b8ea84af93b21f1a031cb48359f3805089cdae1d89842b9a3faf87654989dcae418144a8bbb378c ?SHA3-384*test.txt
*/
expect(metadata).toEqual({
"cid_crc32": "bagzafmqcathsbnrk",
"cid_sha1": 'baeircff3eqvxxaqqot2lt242stkdtk3gq4c2vra',
"cid_sha2-256": 'baejbeibku6ua2l6r5olnggx4pto2sii2s5jeeawgh4cbkhlbmp52gudbua',
"cid_md5": 'bahkqdvibcatpcfl7ine2n625eb5ml5mzkfaa',
"cid_sha3-256": 'baelbmidner7x3dd7t2kldh6wiq4w6lsye7qrwo6uoqot2t52zkhqk56fnq',
"cid_sha3-384": 'baekrkmeglipkjf33exqexdvijl4twipruay4wsbvt44akce43lq5rgccxgr7v6dwksmj3sxedakevc53g6ga'
});
await (await hashComputer.getHashIndexManager()).saveCacheToFile();
});
it('cache', async () => {
await mkdir("./tmp",{recursive:true});
let hashComputer = new HashComputerIndexCache("./tmp/index.csv",[
CID_ALGORITHM_NAMES.sha1,
CID_ALGORITHM_NAMES.sha256,
CID_ALGORITHM_NAMES.md5,
CID_ALGORITHM_NAMES.sha3_256,
CID_ALGORITHM_NAMES.crc32,
CID_ALGORITHM_NAMES.sha3_384
]);
let indexLine = await (await hashComputer.getHashIndexManager()).getCidForFileAsync('./src/tests/test.txt');
delete indexLine['mtime'];
expect(indexLine).toEqual({
"cid_sha1": 'baeircff3eqvxxaqqot2lt242stkdtk3gq4c2vra',
"cid_sha2-256": 'baejbeibku6ua2l6r5olnggx4pto2sii2s5jeeawgh4cbkhlbmp52gudbua',
"cid_md5": 'bahkqdvibcatpcfl7ine2n625eb5ml5mzkfaa',
"cid_sha3-256": 'baelbmidner7x3dd7t2kldh6wiq4w6lsye7qrwo6uoqot2t52zkhqk56fnq',
"cid_crc32": "bagzafmqcathsbnrk",
"cid_sha3-384": 'baekrkmeglipkjf33exqexdvijl4twipruay4wsbvt44akce43lq5rgccxgr7v6dwksmj3sxedakevc53g6ga',
"path": "test.txt",
"size": "499",
});
});
});
@@ -0,0 +1,10 @@
this is a file that is just some data to be hashed
loreum ipsum dolor sit amet
consectetur adipiscing elit
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua
ut enim ad minim veniam
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat
duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur
excepteur sint occaecat cupidatat non proident
sunt in culpa qui officia deserunt mollit anim id est laborum