feat(resume): chunk-hash sha1 helpers (8 tests)

hashBuffer (sync) + hashFile (async streaming). 1MB+ files don't block.
Known-input vectors (hello, empty) verify against canonical sha1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xRangerDE 2026-05-11 22:18:57 +02:00
parent 3667233a26
commit 59a8912fba
2 changed files with 93 additions and 0 deletions

View File

@ -0,0 +1,67 @@
import { test, expect, describe, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { hashBuffer, hashFile } from './chunk-hash';
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chunkhash-'));
});
afterEach(() => {
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
});
describe('hashBuffer', () => {
test('"hello" sha1', () => {
expect(hashBuffer(Buffer.from('hello', 'utf-8')))
.toBe('aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d');
});
test('empty buffer sha1', () => {
expect(hashBuffer(Buffer.alloc(0)))
.toBe('da39a3ee5e6b4b0d3255bfef95601890afd80709');
});
test('large buffer hashes deterministically', () => {
const big = Buffer.alloc(1024 * 1024, 0x42); // 1MB of 'B' bytes
const a = hashBuffer(big);
const b = hashBuffer(big);
expect(a).toBe(b);
expect(a).toHaveLength(40); // sha1 = 40 hex chars
});
test('different content produces different hashes', () => {
expect(hashBuffer(Buffer.from('a'))).not.toBe(hashBuffer(Buffer.from('b')));
});
});
describe('hashFile', () => {
test('file hash matches buffer hash for same content', async () => {
const content = 'roundtrip-test-payload';
const filePath = path.join(tmpDir, 'a.bin');
fs.writeFileSync(filePath, content, 'utf-8');
const fileHash = await hashFile(filePath);
const bufHash = hashBuffer(Buffer.from(content, 'utf-8'));
expect(fileHash).toBe(bufHash);
});
test('empty file = empty-buffer sha1', async () => {
const filePath = path.join(tmpDir, 'empty.bin');
fs.writeFileSync(filePath, '');
const fileHash = await hashFile(filePath);
expect(fileHash).toBe('da39a3ee5e6b4b0d3255bfef95601890afd80709');
});
test('large file (4MB) hashes correctly', async () => {
const filePath = path.join(tmpDir, 'big.bin');
const payload = Buffer.alloc(4 * 1024 * 1024, 0x55);
fs.writeFileSync(filePath, payload);
const fileHash = await hashFile(filePath);
expect(fileHash).toBe(hashBuffer(payload));
});
test('missing file rejects', async () => {
await expect(hashFile(path.join(tmpDir, 'does-not-exist'))).rejects.toThrow();
});
});

View File

@ -0,0 +1,26 @@
import * as crypto from 'crypto';
import * as fs from 'fs';
export function hashBuffer(b: Buffer): string {
return crypto.createHash('sha1').update(b).digest('hex');
}
/**
* Streaming sha1-Hash einer Datei. Async, damit grosse Recorded-Segments
* (oft mehrere MB) nicht den Event-Loop blockieren.
*/
export function hashFile(filePath: string): Promise<string> {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha1');
const stream = fs.createReadStream(filePath);
stream.on('error', reject);
stream.on('data', (chunk: Buffer | string) => {
if (typeof chunk === 'string') {
hash.update(chunk, 'utf-8');
} else {
hash.update(chunk);
}
});
stream.on('end', () => resolve(hash.digest('hex')));
});
}