SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run

This commit is contained in:
TanTan committed 2026-11-13 09:02:00 +08:00
commit fa9c73363d
1367 files changed
+355735

No files matched your search

@@ -0,0 +1,164 @@
import { afterAll, describe, expect, it, vi } from 'vitest';
import {
mkdtempSync,
promises as fsPromises,
readFileSync,
readdirSync,
rmSync,
writeFileSync,
} from 'fs';
import os from 'os';
import path from 'path';
import {
AUDIO_OUT_FORMATS,
convertAudioBytes,
isAudioOutFormat,
} from '@snowluma/protocol/highway/ffmpeg-addon';
// convertAudioBytes is the pure (#165) transcode seam: write input → the
// addon's dedicated audio transcoder → read output as base64 → always clean up
// temp files. Unit tests inject a fake; native/audio-transcode.test.ts covers
// each bundled release-platform addon in the build matrices.
const tmpDir = mkdtempSync(path.join(os.tmpdir(), 'sl-audio-test-'));
afterAll(() => rmSync(tmpDir, { recursive: true, force: true }));
/** Fake addon: writes deterministic bytes to the output path and reports ok. */
const fakeOk = {
decodeAudioToFmt: async (_in: string, out: string, format: string) => {
writeFileSync(out, Buffer.from('WAVDATA')); // stand-in converted payload
return { result: true, sampleRate: 24_000, channels: 1, format };
},
};
describe('isAudioOutFormat / AUDIO_OUT_FORMATS', () => {
it("accepts exactly NapCat's allowlist", () => {
expect([...AUDIO_OUT_FORMATS]).toEqual([
'mp3',
'amr',
'wma',
'm4a',
'spx',
'ogg',
'wav',
'flac',
]);
expect(isAudioOutFormat('wav')).toBe(true);
expect(isAudioOutFormat('WAV')).toBe(false); // case-sensitive
expect(isAudioOutFormat('opus')).toBe(false);
expect(isAudioOutFormat('')).toBe(false);
});
});
describe('convertAudioBytes', () => {
it('accepts the QQ AI SILK container without mutating the caller bytes', async () => {
const input = Buffer.concat([
Buffer.from([0x03]),
Buffer.from('#!SILK_V3', 'ascii'),
Buffer.from([0x04, 0x00, 0x11, 0x22, 0x33, 0x44]),
]);
const originalInput = Buffer.from(input);
let stagedInput: Buffer | null = null;
const addon = {
decodeAudioToFmt: async (inputPath: string, outputPath: string, format: string) => {
stagedInput = readFileSync(inputPath);
writeFileSync(outputPath, Buffer.from('MP3DATA'));
return { result: true, sampleRate: 24_000, channels: 1, format };
},
};
await convertAudioBytes(input, 'mp3', { addon, tmpDir });
expect(stagedInput?.subarray(0, 10)).toEqual(
Buffer.concat([Buffer.from([0x02]), Buffer.from('#!SILK_V3', 'ascii')]),
);
expect(stagedInput?.subarray(10)).toEqual(input.subarray(10));
expect(input).toEqual(originalInput);
});
it('transcodes and returns base64 + size of the addon output', async () => {
const r = await convertAudioBytes(new Uint8Array([1, 2, 3]), 'wav', {
addon: fakeOk,
tmpDir,
});
expect(Buffer.from(r.base64, 'base64').toString()).toBe('WAVDATA');
expect(r.size).toBe('WAVDATA'.length);
});
it('rejects an unsupported format before touching the addon', async () => {
let called = false;
const spy = {
decodeAudioToFmt: async (_in: string, _out: string, format: string) => {
called = true;
return { result: true, sampleRate: 24_000, channels: 1, format };
},
};
await expect(
convertAudioBytes(new Uint8Array([1]), 'opus', { addon: spy, tmpDir }),
).rejects.toThrow(/unsupported out_format/);
expect(called).toBe(false);
});
it('propagates a native conversion rejection and cleans up', async () => {
const fakeFail = {
decodeAudioToFmt: async () => {
throw new Error('native conversion failed');
},
};
await expect(
convertAudioBytes(new Uint8Array([9]), 'mp3', { addon: fakeFail, tmpDir }),
).rejects.toThrow(/native conversion failed/);
// no leftover temp files for this run
const leftover = readdirSync(tmpDir).filter((n) => n.endsWith('.mp3') || n.endsWith('.in'));
expect(leftover).toEqual([]);
});
it('rejects (and cleans up) when the output exceeds the cap', async () => {
await expect(
convertAudioBytes(new Uint8Array([1]), 'wav', {
addon: fakeOk,
tmpDir,
maxOutputBytes: 3,
}),
).rejects.toThrow(/too large/);
const leftover = readdirSync(tmpDir).filter((n) => n.endsWith('.wav') || n.endsWith('.in'));
expect(leftover).toEqual([]);
});
it('cleans up the temp input + output on success too', async () => {
await convertAudioBytes(new Uint8Array([4, 5]), 'flac', { addon: fakeOk, tmpDir });
const leftover = readdirSync(tmpDir).filter(
(n) => n.endsWith('.flac') || n.endsWith('.in'),
);
expect(leftover).toEqual([]);
});
it('surfaces temporary-file cleanup failures', async () => {
const rmSpy = vi.spyOn(fsPromises, 'rm').mockRejectedValue(new Error('cleanup denied'));
try {
await expect(
convertAudioBytes(new Uint8Array([6]), 'wav', { addon: fakeOk, tmpDir }),
).rejects.toThrow(/temporary audio file cleanup failed/);
} finally {
rmSpy.mockRestore();
}
});
it('reports both the conversion and cleanup causes when both fail', async () => {
const nativeFailure = {
decodeAudioToFmt: async () => {
throw new Error('native conversion failed');
},
};
const rmSpy = vi.spyOn(fsPromises, 'rm').mockRejectedValue(new Error('cleanup denied'));
try {
await expect(
convertAudioBytes(new Uint8Array([7]), 'mp3', { addon: nativeFailure, tmpDir }),
).rejects.toThrow(/native conversion failed.*cleanup denied/);
} finally {
rmSpy.mockRestore();
}
});
});
@@ -0,0 +1,86 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('@snowluma/common/native-path', () => ({
resolveRuntimeNativePath: () => '/test/ffmpeg.node',
}));
const addon = {
getDuration: vi.fn<(file: string) => Promise<number>>(),
convertToNTSilkTct: vi.fn<(input: string, output: string) => Promise<void>>(),
};
let root: string;
beforeEach(() => {
vi.resetModules();
addon.getDuration.mockReset().mockResolvedValue(2.5);
addon.convertToNTSilkTct.mockReset().mockImplementation(async (_input, output) => {
await fs.promises.writeFile(output, '\x02#!SILK_V3payload');
});
vi.spyOn(process, 'dlopen').mockImplementation((module) => {
(module as { exports: object }).exports = addon;
});
root = fs.mkdtempSync(path.join(os.tmpdir(), 'snowluma-encode-silk-'));
});
afterEach(() => {
vi.restoreAllMocks();
fs.rmSync(root, { recursive: true, force: true });
});
describe('SILK encoding', () => {
it('rejects unreadable files instead of treating them as a different audio format', async () => {
const { isSilkFile } = await import('../../src/highway/ffmpeg-addon');
expect(() => isSilkFile(path.join(root, 'missing'))).toThrow(/ENOENT/);
});
it('keeps native duration failures visible instead of guessing from SILK file size', async () => {
const input = path.join(root, 'invalid.silk');
fs.writeFileSync(input, '#!SILK_V3truncated');
const failure = new Error('truncated SILK packet');
addon.getDuration.mockRejectedValue(failure);
const { encodeSilk } = await import('../../src/highway/ffmpeg-addon');
await expect(encodeSilk(input, root)).rejects.toBe(failure);
expect(addon.convertToNTSilkTct).not.toHaveBeenCalled();
expect(fs.readdirSync(root)).toEqual(['invalid.silk']);
});
it('reuses a valid SILK input without creating a temporary file', async () => {
const input = path.join(root, 'voice.silk');
fs.writeFileSync(input, '\x02#!SILK_V3payload');
const { encodeSilk } = await import('../../src/highway/ffmpeg-addon');
await expect(encodeSilk(input, root)).resolves.toEqual({
path: input,
duration: 3,
converted: false,
});
expect(addon.convertToNTSilkTct).not.toHaveBeenCalled();
});
it('returns the converted file for caller-owned cleanup', async () => {
const input = path.join(root, 'voice.wav');
fs.writeFileSync(input, 'RIFF');
const { encodeSilk } = await import('../../src/highway/ffmpeg-addon');
const result = await encodeSilk(input, root);
expect(result.converted).toBe(true);
expect(result.duration).toBe(3);
expect(fs.readFileSync(result.path, 'utf8')).toBe('\x02#!SILK_V3payload');
expect(addon.convertToNTSilkTct).toHaveBeenCalledWith(input, result.path);
});
it('removes partial output when native conversion fails, preserving the source and error', async () => {
const input = path.join(root, 'voice.wav');
fs.writeFileSync(input, 'RIFF');
const failure = new Error('conversion interrupted');
addon.convertToNTSilkTct.mockImplementation(async (_input, output) => {
await fs.promises.writeFile(output, 'partial');
throw failure;
});
const { encodeSilk } = await import('../../src/highway/ffmpeg-addon');
await expect(encodeSilk(input, root)).rejects.toBe(failure);
expect(fs.readdirSync(root)).toEqual(['voice.wav']);
expect(fs.readFileSync(input, 'utf8')).toBe('RIFF');
});
});
@@ -0,0 +1,285 @@
// Streaming hasher must be byte-identical to the buffered helpers over the
// same bytes: computeHashes + computeVideoSha1Blocks (+ computeMd5 of the
// head) for hashFileStreaming, and computeHashes + computeSha1StateV for
// hashFlashFileStreaming. Flash Sha1StateV is not video sha1Blocks.
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { createHash } from 'node:crypto';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { hashFileStreaming, hashFlashFileStreaming } from '@snowluma/protocol/highway/hash-file';
import { computeHashes, computeMd5 } from '@snowluma/protocol/highway/utils';
import { computeSha1StateV, Sha1Stream } from '@snowluma/protocol/highway/sha1-stream';
import { computeVideoSha1Blocks } from '../../src/highway/video-upload';
const MB = 1024 * 1024;
// Same generator as video-sha1-blocks.test.ts (and its golden-capture script).
function det(len: number): Uint8Array {
const b = new Uint8Array(len);
for (let i = 0; i < len; i++) {
b[i] = (i * 31 + 7) & 0xff;
}
return b;
}
function hex(u: Uint8Array): string {
return Buffer.from(u).toString('hex');
}
function nodeMd5Hex(data: Uint8Array): string {
return createHash('md5').update(Buffer.from(data)).digest('hex');
}
function nodeSha1Hex(data: Uint8Array): string {
return createHash('sha1').update(Buffer.from(data)).digest('hex');
}
describe('hashFileStreaming', () => {
let dir: string;
beforeAll(async () => {
dir = await mkdtemp(join(tmpdir(), 'sl-hash-file-'));
});
afterAll(async () => {
await rm(dir, { recursive: true, force: true });
});
async function writeBin(name: string, data: Uint8Array): Promise<string> {
const file = join(dir, name);
await writeFile(file, data);
return file;
}
it('empty file: well-known empty digests and a single finalized sha1 block', async () => {
const file = await writeBin('empty.bin', new Uint8Array(0));
const streamed = await hashFileStreaming(file);
expect(streamed.md5Hex).toBe('d41d8cd98f00b204e9800998ecf8427e');
expect(streamed.sha1Hex).toBe('da39a3ee5e6b4b0d3255bfef95601890afd80709');
expect(hex(streamed.md5)).toBe('d41d8cd98f00b204e9800998ecf8427e');
expect(hex(streamed.sha1)).toBe('da39a3ee5e6b4b0d3255bfef95601890afd80709');
expect(streamed.headMd5).toBeUndefined();
expect(streamed.sha1Blocks.map(hex)).toEqual(['da39a3ee5e6b4b0d3255bfef95601890afd80709']);
});
it('empty file with headLimit still yields md5 of zero bytes', async () => {
const file = await writeBin('empty-head.bin', new Uint8Array(0));
const streamed = await hashFileStreaming(file, { headLimit: 10 });
expect(hex(streamed.headMd5!)).toBe('d41d8cd98f00b204e9800998ecf8427e');
expect(streamed.md5Hex).toBe('d41d8cd98f00b204e9800998ecf8427e');
});
it('sub-1 MiB file matches RFC hashes and has only the finalized sha1 block', async () => {
const data = Buffer.from('abc', 'utf8');
const file = await writeBin('abc.bin', data);
const streamed = await hashFileStreaming(file);
expect(streamed.md5Hex).toBe('900150983cd24fb0d6963f7d28e17f72');
expect(streamed.sha1Hex).toBe('a9993e364706816aba3e25717850c26c9cd0d89d');
expect(streamed.sha1Blocks.map(hex)).toEqual(['a9993e364706816aba3e25717850c26c9cd0d89d']);
expect(streamed.headMd5).toBeUndefined();
});
it('det(1000) matches computeHashes + computeVideoSha1Blocks + Node crypto', async () => {
const data = det(1000);
const file = await writeBin('det-1000.bin', data);
const streamed = await hashFileStreaming(file);
const buffered = computeHashes(data);
expect(streamed.md5Hex).toBe(nodeMd5Hex(data));
expect(streamed.sha1Hex).toBe(nodeSha1Hex(data));
expect(hex(streamed.md5)).toBe(hex(buffered.md5));
expect(hex(streamed.sha1)).toBe(hex(buffered.sha1));
expect(streamed.md5Hex).toBe(buffered.md5Hex);
expect(streamed.sha1Hex).toBe(buffered.sha1Hex);
expect(streamed.sha1Blocks.map(hex)).toEqual(computeVideoSha1Blocks(data).map(hex));
expect(streamed.sha1Blocks).toHaveLength(1);
});
it('exact 1 MiB emits an un-finalized boundary state then the overall sha1', async () => {
const data = det(MB);
const file = await writeBin('exact-1m.bin', data);
const streamed = await hashFileStreaming(file);
const overall = nodeSha1Hex(data);
const prefixDigest = nodeSha1Hex(data); // whole file *is* the first slice
const manual = new Sha1Stream();
manual.update(data);
expect(streamed.md5Hex).toBe(nodeMd5Hex(data));
expect(streamed.sha1Hex).toBe(overall);
expect(streamed.sha1Blocks).toHaveLength(2);
expect(hex(streamed.sha1Blocks[0]!)).toBe(hex(manual.hash(true)));
expect(hex(streamed.sha1Blocks[0]!)).not.toBe(prefixDigest);
expect(hex(streamed.sha1Blocks[1]!)).toBe(overall);
expect(streamed.sha1Blocks.map(hex)).toEqual(computeVideoSha1Blocks(data).map(hex));
});
it('2 MiB + 12345 reproduces the video-sha1-blocks golden states', async () => {
const data = det(2 * MB + 12345);
const file = await writeBin('golden-2m.bin', data);
const streamed = await hashFileStreaming(file);
expect(streamed.sha1Blocks.map(hex)).toEqual([
'ab9a20fd6c98fc1f8f0c985cd552ff14c6feacfd',
'2ccc5923b75f46ebd8ccb78e8c413c18025178b9',
'dd9cfd4648ab460260afa820c98bfec1b17655d4',
]);
expect(streamed.sha1Hex).toBe('dd9cfd4648ab460260afa820c98bfec1b17655d4');
expect(streamed.md5Hex).toBe(nodeMd5Hex(data));
expect(streamed.sha1Blocks.map(hex)).toEqual(computeVideoSha1Blocks(data).map(hex));
});
it('omitting opts leaves headMd5 undefined', async () => {
const file = await writeBin('no-opts.bin', det(64));
const streamed = await hashFileStreaming(file);
expect(streamed.headMd5).toBeUndefined();
});
it('headLimit 0 is present and is the empty-md5', async () => {
const file = await writeBin('head-0.bin', det(200));
const streamed = await hashFileStreaming(file, { headLimit: 0 });
expect(hex(streamed.headMd5!)).toBe('d41d8cd98f00b204e9800998ecf8427e');
expect(streamed.md5Hex).toBe(nodeMd5Hex(det(200)));
});
it('headMd5 is computeMd5 of the first N bytes inside the first 1 MiB chunk', async () => {
const data = det(8000);
const file = await writeBin('head-inside.bin', data);
const streamed = await hashFileStreaming(file, { headLimit: 10 });
expect(hex(streamed.headMd5!)).toBe(hex(computeMd5(data.subarray(0, 10))));
expect(hex(streamed.headMd5!)).toBe(nodeMd5Hex(data.subarray(0, 10)));
});
it('headMd5 across a 1 MiB boundary matches computeMd5 of the clamped prefix', async () => {
const data = det(MB + 500);
const file = await writeBin('head-span.bin', data);
const cases: Array<[number, number]> = [
[MB - 1, MB - 1],
[MB, MB],
[MB + 1, MB + 1],
[data.length + 1, data.length],
];
for (const [limit, take] of cases) {
const streamed = await hashFileStreaming(file, { headLimit: limit });
expect(hex(streamed.headMd5!)).toBe(hex(computeMd5(data.subarray(0, take))));
expect(streamed.md5Hex).toBe(nodeMd5Hex(data));
expect(streamed.sha1Hex).toBe(nodeSha1Hex(data));
}
});
it('rejects a missing path from stat before hashing', async () => {
await expect(hashFileStreaming(join(dir, 'no-such.bin'))).rejects.toMatchObject({
code: 'ENOENT',
});
});
});
describe('hashFlashFileStreaming', () => {
let dir: string;
beforeAll(async () => {
dir = await mkdtemp(join(tmpdir(), 'sl-flash-hash-file-'));
});
afterAll(async () => {
await rm(dir, { recursive: true, force: true });
});
async function writeBin(name: string, data: Uint8Array): Promise<string> {
const file = join(dir, name);
await writeFile(file, data);
return file;
}
it('empty file: empty digests, sliceCount 0, no Sha1StateV entries', async () => {
const file = await writeBin('empty.bin', new Uint8Array(0));
const streamed = await hashFlashFileStreaming(file);
expect(streamed.md5Hex).toBe('d41d8cd98f00b204e9800998ecf8427e');
expect(streamed.sha1Hex).toBe('da39a3ee5e6b4b0d3255bfef95601890afd80709');
expect(streamed.sliceCount).toBe(0);
expect(streamed.sha1StateV).toEqual([]);
});
it('sub-1 MiB file is a single finalized slice', async () => {
const data = Buffer.from('abc', 'utf8');
const file = await writeBin('abc.bin', data);
const streamed = await hashFlashFileStreaming(file);
expect(streamed.sliceCount).toBe(1);
expect(streamed.md5Hex).toBe('900150983cd24fb0d6963f7d28e17f72');
expect(streamed.sha1Hex).toBe('a9993e364706816aba3e25717850c26c9cd0d89d');
expect(streamed.sha1StateV.map(hex)).toEqual(['a9993e364706816aba3e25717850c26c9cd0d89d']);
});
it('exact 1 MiB omits the extra un-finalized last-slice state video sha1Blocks keeps', async () => {
const data = det(MB);
const file = await writeBin('exact-1m.bin', data);
const flash = await hashFlashFileStreaming(file);
const video = await hashFileStreaming(file);
const overall = nodeSha1Hex(data);
const refState = computeSha1StateV(data, 1, MB);
expect(flash.sliceCount).toBe(1);
expect(flash.sha1StateV).toHaveLength(1);
expect(hex(flash.sha1StateV[0]!)).toBe(overall);
expect(hex(flash.sha1StateV[0]!)).toBe(hex(refState[0]!));
expect(video.sha1Blocks).toHaveLength(2);
expect(hex(flash.sha1StateV[0]!)).toBe(hex(video.sha1Blocks[1]!));
expect(hex(flash.sha1StateV[0]!)).not.toBe(hex(video.sha1Blocks[0]!));
expect(flash.md5Hex).toBe(nodeMd5Hex(data));
});
it('1 MiB + 1: first state is the un-finalized 1 MiB snapshot', async () => {
const data = det(MB + 1);
const file = await writeBin('1m-plus.bin', data);
const flash = await hashFlashFileStreaming(file);
const manual = new Sha1Stream();
manual.update(data.subarray(0, MB));
const refState = computeSha1StateV(data, 2, MB);
expect(flash.sliceCount).toBe(2);
expect(flash.sha1StateV).toHaveLength(2);
expect(hex(flash.sha1StateV[0]!)).toBe(hex(manual.hash(true)));
expect(hex(flash.sha1StateV[0]!)).not.toBe(nodeSha1Hex(data.subarray(0, MB)));
expect(hex(flash.sha1StateV[1]!)).toBe(nodeSha1Hex(data));
expect(flash.sha1StateV.map(hex)).toEqual(refState.map(hex));
});
it('exact 2 MiB: flash has 2 states, video sha1Blocks has 3', async () => {
const data = det(2 * MB);
const file = await writeBin('exact-2m.bin', data);
const flash = await hashFlashFileStreaming(file);
const video = await hashFileStreaming(file);
expect(flash.sliceCount).toBe(2);
expect(flash.sha1StateV).toHaveLength(2);
expect(video.sha1Blocks).toHaveLength(3);
expect(hex(flash.sha1StateV[0]!)).toBe(hex(video.sha1Blocks[0]!));
expect(hex(flash.sha1StateV[1]!)).toBe(hex(video.sha1Blocks[2]!));
expect(hex(flash.sha1StateV[1]!)).toBe(nodeSha1Hex(data));
expect(flash.sha1StateV.map(hex)).toEqual(computeSha1StateV(data, 2, MB).map(hex));
expect(flash.md5Hex).toBe(computeHashes(data).md5Hex);
});
it('2 MiB + 12345 Sha1StateV matches computeSha1StateV and Node overall sha1', async () => {
const data = det(2 * MB + 12345);
const file = await writeBin('golden-2m.bin', data);
const flash = await hashFlashFileStreaming(file);
expect(flash.sliceCount).toBe(3);
expect(flash.sha1Hex).toBe('dd9cfd4648ab460260afa820c98bfec1b17655d4');
expect(hex(flash.sha1StateV[2]!)).toBe('dd9cfd4648ab460260afa820c98bfec1b17655d4');
expect(hex(flash.sha1StateV[0]!)).toBe('ab9a20fd6c98fc1f8f0c985cd552ff14c6feacfd');
expect(hex(flash.sha1StateV[1]!)).toBe('2ccc5923b75f46ebd8ccb78e8c413c18025178b9');
expect(flash.sha1StateV.map(hex)).toEqual(computeSha1StateV(data, 3, MB).map(hex));
});
it('rejects a missing path from stat before hashing', async () => {
await expect(hashFlashFileStreaming(join(dir, 'no-such.bin'))).rejects.toMatchObject({
code: 'ENOENT',
});
});
});
@@ -0,0 +1,92 @@
import type { MessageElement } from '../../src/events';
import { createHash } from 'node:crypto';
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { NotOnlineImagePbReserve } from '@snowluma/proto-defs/element';
import type {
EncodableMediaMsgInfo,
NTV2UploadRichMediaReq,
NTV2UploadRichMediaResp,
} from '@snowluma/proto-defs/highway';
import type { BridgeContext } from '../../src/bridge-context';
import { uploadImageMsgInfo } from '../../src/highway/image-upload';
const png =
'base64://iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jFZkAAAAASUVORK5CYII=';
describe('image sticker presentation (#468)', () => {
it.each([true, false])(
'carries sticker reserves through actual wire encoding, group=%s',
async (isGroup) => {
const requests: NTV2UploadRichMediaReq[] = [];
const sendRawPacket = vi.fn(async (_command: string, body: Uint8Array) => {
requests.push(protobuf_decode<OidbBase<NTV2UploadRichMediaReq>>(body).body!);
return {
success: true,
gotResponse: true,
errorCode: 0,
responseData: protobuf_encode<OidbBase<NTV2UploadRichMediaResp>>({
body: {
upload: {
msgInfo: {
msgInfoBody: [{ index: { fileUuid: 'cached-image' } }],
extBizInfo: {
pic: { bizType: 0, textSummary: '[图片]', oldFileId: 123 },
},
},
},
},
}),
};
});
const bridge = {
identity: { uin: '10001' },
sendRawPacket,
} as unknown as BridgeContext;
const element: MessageElement = {
type: 'image',
url: png,
subType: 1,
summary: '[收藏表情]',
};
const bytes = await uploadImageMsgInfo(
bridge,
isGroup,
isGroup ? 123 : 'u_peer',
element,
);
const imageBytes = Buffer.from(png.slice('base64://'.length), 'base64');
expect(element).toMatchObject({
md5Hex: createHash('md5').update(imageBytes).digest('hex'),
sha1Hex: createHash('sha1').update(imageBytes).digest('hex'),
fileSize: imageBytes.length,
width: 1,
height: 1,
picFormat: 1001,
imageUrl: `https://multimedia.nt.qq.com.cn/download?appid=${isGroup ? 1407 : 1406}&fileid=cached-image`,
});
expect(element.fileId).toBe(element.fileName);
const sent = protobuf_decode<EncodableMediaMsgInfo>(bytes);
expect(sendRawPacket).toHaveBeenCalledTimes(1);
expect(sent.msgInfoBody?.[0]?.index?.fileUuid).toBe('cached-image');
expect(sent.extBizInfo?.pic?.oldFileId).toBe(123);
for (const pic of [requests[0].upload?.extBizInfo?.pic, sent.extBizInfo?.pic]) {
expect(pic?.bizType).toBe(1);
expect(pic?.textSummary).toBe('[收藏表情]');
if (isGroup) {
expect(pic?.extData).toMatchObject({ subType: 1, textSummary: '[收藏表情]' });
} else {
expect(
protobuf_decode<NotOnlineImagePbReserve>(pic!.bytesPbReserveC2c!),
).toMatchObject({
subType: 1,
summary: '[收藏表情]',
});
}
}
},
);
});
@@ -0,0 +1,93 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
EncodableMediaMsgInfo,
HighwayMsgInfoBody,
NTV2UploadRichMediaResp,
} from '@snowluma/proto-defs/highway';
import type { BridgeContext } from '../../src/bridge-context';
import type { MessageElement } from '../../src/events';
import { uploadImageMsgInfo } from '../../src/highway/image-upload';
const webp = 'base64://UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA';
function bridge(body: HighwayMsgInfoBody): BridgeContext {
return {
identity: { uin: '10001' },
sendRawPacket: vi.fn(async () => ({
success: true,
gotResponse: true,
errorCode: 0,
responseData: protobuf_encode<OidbBase<NTV2UploadRichMediaResp>>({
body: { upload: { msgInfo: { msgInfoBody: [body] } } },
}),
})),
} as unknown as BridgeContext;
}
describe('uploaded image download references (#659)', () => {
it.each([false, true])(
'retains the returned host, original parameters and WebP filename (group=%s)',
async (isGroup) => {
const picture = {
domain: 'image.example',
urlPath: '/download?fileid=server-resource',
ext: { originalParameter: '&spec=0', thumbParameter: '&spec=small' },
};
const element: MessageElement = { type: 'image', url: webp };
const encoded = await uploadImageMsgInfo(
bridge({ index: { fileUuid: 'resource' }, picture }),
isGroup,
isGroup ? 20002 : 'u_peer',
element,
);
expect(element).toMatchObject({
imageUrl: 'https://image.example/download?fileid=server-resource&spec=0',
picFormat: 1002,
width: 1,
height: 1,
});
expect(element.fileName).toBe(`${element.md5Hex}.webp`);
expect(element.fileId).toBe(element.fileName);
expect(
protobuf_decode<EncodableMediaMsgInfo>(encoded).msgInfoBody?.[0]?.picture,
).toMatchObject(picture);
},
);
it.each([false, true])(
'uses an encoded upload resource id when the server omits the URL (group=%s)',
async (isGroup) => {
const element: MessageElement = { type: 'image', url: webp };
await uploadImageMsgInfo(
bridge({ index: { fileUuid: 'resource/+=' } }),
isGroup,
isGroup ? 20002 : 'u_peer',
element,
);
expect(element.imageUrl).toBe(
`https://multimedia.nt.qq.com.cn/download?appid=${isGroup ? 1407 : 1406}&fileid=resource%2F%2B%3D`,
);
},
);
it('uses the standard image host when a wire response omits the domain', async () => {
const element: MessageElement = { type: 'image', url: webp };
await uploadImageMsgInfo(
bridge({ picture: { urlPath: '/download?fileid=resource' } }),
false,
'u_peer',
element,
);
expect(element.imageUrl).toBe('https://multimedia.nt.qq.com.cn/download?fileid=resource');
});
it('rejects an unusable upload receipt before publishing incomplete send metadata', async () => {
const element: MessageElement = { type: 'image', url: webp };
await expect(uploadImageMsgInfo(bridge({}), false, 'u_peer', element)).rejects.toThrow(
'missing a download reference',
);
expect(element).toEqual({ type: 'image', url: webp });
});
});
@@ -0,0 +1,146 @@
import { describe, expect, it } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { EncodableMediaMsgInfo } from '@snowluma/proto-defs/highway';
import { finalizeMediaMsgInfo } from '@snowluma/protocol/highway/pipeline';
import {
PTT_WAVEFORM_BINS,
PTT_WAVEFORM_SILENCE,
amplitudesFromPcmS16le,
decodePttWaveform,
encodePttWaveform,
mergePttWaveform,
pcmS16leFromWav,
} from '@snowluma/protocol/highway/ptt-waveform';
function le16(value: number): number[] {
return [value & 0xff, (value >> 8) & 0xff];
}
function le32(value: number): number[] {
return [value & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff, (value >> 24) & 0xff];
}
function ascii(text: string): number[] {
return [...text].map((ch) => ch.charCodeAt(0));
}
function s16leBytes(samples: number[]): Uint8Array {
const out = new Uint8Array(samples.length * 2);
for (let i = 0; i < samples.length; i += 1) {
const v = samples[i]! < 0 ? samples[i]! + 0x10000 : samples[i]!;
out[i * 2] = v & 0xff;
out[i * 2 + 1] = (v >> 8) & 0xff;
}
return out;
}
/** Minimal PCM WAV: 16-bit, little-endian, one data chunk. */
function wavBytes(samples: number[], channels = 1, sampleRate = 24_000): Uint8Array {
const pcm = s16leBytes(samples);
const fmtSize = 16;
const dataSize = pcm.length;
const riffSize = 4 + (8 + fmtSize) + (8 + dataSize);
const out = new Uint8Array(12 + 8 + fmtSize + 8 + dataSize);
let off = 0;
const put = (bytes: number[]) => {
out.set(bytes, off);
off += bytes.length;
};
put(ascii('RIFF'));
put(le32(riffSize));
put(ascii('WAVE'));
put(ascii('fmt '));
put(le32(fmtSize));
put(le16(1));
put(le16(channels));
put(le32(sampleRate));
put(le32(sampleRate * channels * 2));
put(le16(channels * 2));
put(le16(16));
put(ascii('data'));
put(le32(dataSize));
out.set(pcm, off);
return out;
}
describe('pcmS16leFromWav', () => {
it('reads s16le PCM and channel count from a plain WAV', () => {
const wav = wavBytes([0, 32767, -32768], 1, 16_000);
const parsed = pcmS16leFromWav(wav);
expect(parsed.channels).toBe(1);
expect(parsed.sampleRate).toBe(16_000);
expect(parsed.pcm).toEqual(s16leBytes([0, 32767, -32768]));
});
it('rejects a non-PCM WAV', () => {
const wav = wavBytes([1, 2, 3]);
wav[20] = 7;
expect(() => pcmS16leFromWav(wav)).toThrow(/not PCM/);
});
});
describe('amplitudesFromPcmS16le', () => {
it('uses the official 30 bars and scales a full-scale peak to 255', () => {
const loud = new Array(30).fill(0);
loud[0] = 32767;
const amps = amplitudesFromPcmS16le(s16leBytes(loud));
expect(amps.length).toBe(PTT_WAVEFORM_BINS);
expect(amps[0]).toBe(255);
});
it('keeps quiet windows at 0 so the bar has valleys', () => {
const samples = new Array(60).fill(0);
samples[0] = 32767;
const amps = amplitudesFromPcmS16le(s16leBytes(samples), { bins: 2 });
expect([...amps]).toEqual([255, 0]);
});
it('uses the official silence floor when the clip is empty', () => {
const amps = amplitudesFromPcmS16le(new Uint8Array(0), { bins: 4 });
expect([...amps]).toEqual(new Array(4).fill(PTT_WAVEFORM_SILENCE));
});
it('peaks stereo frames from either channel', () => {
const pcm = s16leBytes([0, 32767, 0, 0]);
const amps = amplitudesFromPcmS16le(pcm, { channels: 2, bins: 1 });
expect(amps[0]).toBe(255);
});
});
describe('encodePttWaveform / decodePttWaveform', () => {
it('roundtrips with matching count and amplitudes', () => {
const amplitudes = new Uint8Array([25, 40, 80, 10]);
const encoded = encodePttWaveform(amplitudes);
expect(decodePttWaveform(encoded)).toEqual({ size: 4, amplitudes });
});
it('rejects an empty amplitude list', () => {
expect(() => encodePttWaveform(new Uint8Array(0))).toThrow(/empty/);
});
});
describe('mergePttWaveform', () => {
it('writes waveform onto msgInfo so finalize keeps it', () => {
const waveform = encodePttWaveform(new Uint8Array(PTT_WAVEFORM_BINS).fill(40));
const upload = {
msgInfo: {
msgInfoBody: [],
extBizInfo: { ptt: { bytesReserve: new Uint8Array([1]) } },
},
};
mergePttWaveform(upload, waveform);
const decoded: any = protobuf_decode<EncodableMediaMsgInfo>(finalizeMediaMsgInfo(upload));
expect(decoded.extBizInfo.ptt.waveform).toEqual(waveform);
expect(decoded.extBizInfo.ptt.bytesReserve).toEqual(new Uint8Array([1]));
});
it('no-ops when waveform is missing', () => {
const upload = { msgInfo: { msgInfoBody: [], extBizInfo: {} } };
mergePttWaveform(upload, undefined);
expect(upload.msgInfo.extBizInfo).toEqual({});
});
});
@@ -0,0 +1,198 @@
import { describe, expect, it } from 'vitest';
import { createHash } from 'node:crypto';
import { Sha1Stream, computeSha1StateV } from '@snowluma/protocol/highway/sha1-stream';
// SHA1 标准填充:msg + 0x80 + 0x00...(k 个) + 8 字节大端 bit 长度,总长为 64 倍数。
// 用于把任意输入变成「已 finalize」的完整 block 序列——这样 Sha1Stream 对它逐 block
// transform 后取大端 state,应当等于 Node crypto 对原始 msg 的 digest。
function sha1Pad(msg: Uint8Array): Uint8Array {
const msgLen = msg.length;
const k = (56 - ((msgLen + 1) % 64) + 64) % 64;
const padded = new Uint8Array(msgLen + 1 + k + 8);
padded.set(msg, 0);
padded[msgLen] = 0x80;
const view = new DataView(padded.buffer, padded.byteOffset + padded.byteLength - 8, 8);
view.setBigUint64(0, BigInt(msgLen) * 8n, false); // 大端 bit 长度
return padded;
}
// padding oracle 的核心:Node crypto 的 SHA1 digest 内部就是对 padding 后的输入逐 block
// transform、取最终 state 的大端表示。Sha1Stream 自行实现 transform 且不 finalize,
// 因此对「已 padding 的输入」transform 后大端输出必须等于 digest。Node crypto 走
// OpenSSL,与本实现完全独立,是验证 transform 正确性的可靠参照。
function expectSha1StreamMatchesCrypto(msg: Uint8Array): void {
const expected = createHash('sha1').update(Buffer.from(msg)).digest();
const stream = new Sha1Stream();
stream.update(sha1Pad(msg));
const bigEndian = stream.hash(false);
expect(Buffer.from(bigEndian).equals(expected)).toBe(true);
}
describe('Sha1Stream transform (padding oracle vs Node crypto)', () => {
it('matches for empty input (single padded block)', () => {
expectSha1StreamMatchesCrypto(Buffer.alloc(0));
});
it('matches for "abc" (single padded block)', () => {
expectSha1StreamMatchesCrypto(Buffer.from('abc', 'utf8'));
});
it('matches across the 56-byte boundary (forces a second padded block)', () => {
// 56 字节:0x80 后只剩 0 字节给 zeros 不够放 8 字节长度,padding 溢出到第二块。
expectSha1StreamMatchesCrypto(Buffer.alloc(56, 0x41));
});
it('matches for exactly one block (64 bytes → two padded blocks)', () => {
expectSha1StreamMatchesCrypto(Buffer.alloc(64, 0x42));
});
it('matches for multi-block input (200 bytes → four padded blocks)', () => {
expectSha1StreamMatchesCrypto(Buffer.alloc(200, 0x43));
});
it('matches for pseudo-random multi-block input', () => {
const data = Buffer.alloc(313, 0);
for (let i = 0; i < data.length; i++) {
data[i] = (i * 1103515245 + 12345) & 0xff;
}
expectSha1StreamMatchesCrypto(data);
});
});
describe('Sha1Stream update chunk-equivalence', () => {
// update 内部维护 64 字节 buffer + 128 位 bit 计数;分块喂入必须与一次性喂入
// 产生相同的内部 state。切分点覆盖 block 边界附近以暴露 buffer/count 管理 bug。
it('two-chunk split at various boundaries yields the same state as one update', () => {
const data = Buffer.alloc(200, 0x44);
const full = new Sha1Stream();
full.update(data);
const fullHash = Buffer.from(full.hash(true));
for (const split of [0, 1, 32, 63, 64, 65, 127, 128, 129, 199]) {
const s = new Sha1Stream();
s.update(data.subarray(0, split));
s.update(data.subarray(split));
expect(Buffer.from(s.hash(true)).equals(fullHash)).toBe(true);
}
});
it('three-chunk split yields the same state as one update', () => {
const data = Buffer.alloc(300, 0x45);
const full = new Sha1Stream();
full.update(data);
const fullHash = Buffer.from(full.hash(true));
const s = new Sha1Stream();
s.update(data.subarray(0, 100));
s.update(data.subarray(100, 200));
s.update(data.subarray(200));
expect(Buffer.from(s.hash(true)).equals(fullHash)).toBe(true);
});
it('empty updates do not change the state', () => {
const data = Buffer.alloc(130, 0x46);
const full = new Sha1Stream();
full.update(data);
const fullHash = Buffer.from(full.hash(true));
const s = new Sha1Stream();
s.update(Buffer.alloc(0));
s.update(data);
s.update(Buffer.alloc(0));
expect(Buffer.from(s.hash(true)).equals(fullHash)).toBe(true);
});
});
describe('Sha1Stream hash endianness', () => {
// hash(true) 小端与 hash(false) 大端是同一组 5 个 state word 的不同字节序表示。
// 结合上面的 padding oracle(大端 == digest),即可间接确认小端中间 state 的值。
it('little-endian is the per-word byte-swap of big-endian', () => {
const s = new Sha1Stream();
s.update(Buffer.alloc(64, 0x47));
const le = Buffer.from(s.hash(true));
const be = Buffer.from(s.hash(false));
expect(le.length).toBe(20);
for (let i = 0; i < 5; i++) {
expect(le.readUInt32LE(i * 4)).toBe(be.readUInt32BE(i * 4));
}
});
it('reset restores the initial SHA1 state', () => {
const s = new Sha1Stream();
s.update(Buffer.alloc(200, 0x48));
s.reset();
const initial = Buffer.from(s.hash(false));
const expected = Buffer.from([
0x67, 0x45, 0x23, 0x01, 0xef, 0xcd, 0xab, 0x89, 0x98, 0xba, 0xdc, 0xfe, 0x10, 0x32,
0x54, 0x76, 0xc3, 0xd2, 0xe1, 0xf0,
]);
expect(initial.equals(expected)).toBe(true);
});
});
describe('computeSha1StateV', () => {
const SLICE = 1024 * 1024; // 1 MB,与上传链路一致,且是 64 的倍数
it('returns one 20-byte state per slice', () => {
const data = Buffer.alloc(SLICE + 100, 0x49); // 2 片
const states = computeSha1StateV(data, 2, SLICE);
expect(states.length).toBe(2);
for (const st of states) {
expect(st.length).toBe(20);
}
});
// 末片定义为「标准整文件 SHA1(有 finalize/padding)」,直接对齐 Node crypto。
it('last slice equals the standard full-file SHA1 digest', () => {
const data = Buffer.alloc(SLICE + 100, 0x4a);
const states = computeSha1StateV(data, 2, SLICE);
const expected = createHash('sha1').update(data).digest();
expect(Buffer.from(states[1]).equals(expected)).toBe(true);
});
it('last slice equals full-file SHA1 for a three-slice file', () => {
const data = Buffer.alloc(SLICE * 2 + 1, 0x4b);
const states = computeSha1StateV(data, 3, SLICE);
expect(Buffer.from(states[2]).equals(createHash('sha1').update(data).digest())).toBe(true);
});
// 非末片是「累积 SHA1 中间 state(不 finalize,小端)」。若误用标准 finalize
// 计算,会得到 file[0:slice] 的完整 digest,与本实现必然不等——以此锁定
// 「中间 state 而非独立片 digest」的语义。
it('non-last slice is NOT the finalized digest of the prefix', () => {
const data = Buffer.alloc(SLICE + 100, 0x4c);
const states = computeSha1StateV(data, 2, SLICE);
const prefixDigest = createHash('sha1').update(data.subarray(0, SLICE)).digest();
expect(Buffer.from(states[0]).equals(prefixDigest)).toBe(false);
});
// 非末片中间 state 的正确性来自 transform(padding oracle 已验)与 update
// 分块等价(已验)。这里再做一次直接对照:computeSha1StateV 对首片的处理
// 等价于手动 reset + update(prefix) + 小端输出。
it('non-last slice equals manual Sha1Stream reset+update of the prefix', () => {
const data = Buffer.alloc(SLICE + 100, 0x4d);
const states = computeSha1StateV(data, 2, SLICE);
const manual = new Sha1Stream();
manual.update(data.subarray(0, SLICE));
expect(Buffer.from(states[0]).equals(Buffer.from(manual.hash(true)))).toBe(true);
});
// 单片文件只有末片,直接是整文件 digest。
it('single-slice file has only the finalized digest', () => {
const data = Buffer.alloc(100, 0x4e);
const states = computeSha1StateV(data, 1, SLICE);
expect(states.length).toBe(1);
expect(Buffer.from(states[0]).equals(createHash('sha1').update(data).digest())).toBe(true);
});
// 中间 state 与「把它当末片算出的 digest」必须不同——这是 sliceupload 服务端
// 校验「累积 state 而非独立片 digest」的核心区分点。
it('non-last slice differs from a single-slice digest of the same prefix', () => {
const data = Buffer.alloc(SLICE + 100, 0x4f);
const multiFirst = computeSha1StateV(data, 2, SLICE)[0];
const singleAsLast = computeSha1StateV(data.subarray(0, SLICE), 1, SLICE)[0];
expect(Buffer.from(multiFirst).equals(Buffer.from(singleAsLast))).toBe(false);
});
});
@@ -0,0 +1,523 @@
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { randomUUID } from 'crypto';
import fs, { promises as fsp } from 'fs';
import os from 'os';
import path from 'path';
import { pathToFileURL } from 'url';
const stageRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'sl-proto-stage-root-'));
const tmpdir = vi.spyOn(os, 'tmpdir').mockReturnValue(stageRoot);
const { STAGE_DIR, stageSourceToDisk, sweepStagedUploads } =
await import('@snowluma/protocol/highway/stage');
tmpdir.mockRestore();
const DOWNLOAD_UA =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
const STAGE_TTL_MS = 30 * 60 * 1000;
const SNOW_BYTES = Buffer.from([0x53, 0x4e, 0x4f, 0x57]);
const live: Array<{ cleanup(): Promise<void> }> = [];
let scratch = '';
function hold<T extends { cleanup(): Promise<void> }>(staged: T): T {
live.push(staged);
return staged;
}
async function stageNames(): Promise<string[]> {
try {
return await fsp.readdir(STAGE_DIR);
} catch {
return [];
}
}
async function newPartNames(before: readonly string[]): Promise<string[]> {
const prior = new Set(before);
return (await stageNames()).filter((name) => name.endsWith('.part') && !prior.has(name));
}
function expectStagedPath(filePath: string, ext: string): void {
expect(filePath.startsWith(`${STAGE_DIR}${path.sep}`)).toBe(true);
expect(path.basename(filePath)).toMatch(
new RegExp(
`^stage-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}${ext.replaceAll('.', '\\.')}$`,
),
);
}
beforeEach(async () => {
scratch = await fsp.mkdtemp(path.join(os.tmpdir(), 'sl-proto-stage-'));
});
afterEach(async () => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
while (live.length > 0) {
await live.pop()!.cleanup();
}
await fsp.rm(scratch, { recursive: true, force: true });
});
afterAll(async () => {
await fsp.rm(stageRoot, { recursive: true, force: true });
});
describe('STAGE_DIR', () => {
it('is os.tmpdir()/media-stage', () => {
expect(STAGE_DIR).toBe(path.join(stageRoot, 'media-stage'));
});
});
describe('stageSourceToDisk — empty / classification', () => {
it('rejects an empty source before touching disk', async () => {
const before = await stageNames();
await expect(stageSourceToDisk('', 64)).rejects.toThrow('stage source is empty');
expect(await stageNames()).toEqual(before);
});
it('rejects a data URL that is missing the comma separator', async () => {
await expect(stageSourceToDisk('data:image/png;base64', 64)).rejects.toThrow(
'data URL source is missing its payload separator',
);
});
it('rejects a data URL that is not base64-encoded', async () => {
await expect(stageSourceToDisk('data:text/plain,hello', 64)).rejects.toThrow(
'data URL source must use base64 encoding',
);
});
it('rejects a missing local path via realpath', async () => {
const missing = path.join(scratch, 'no-such-file.bin');
await expect(stageSourceToDisk(missing, 64)).rejects.toThrow(/ENOENT/);
});
});
describe('stageSourceToDisk — base64', () => {
it('writes base64:// bytes to a .bin stage file with an empty fileName', async () => {
const staged = hold(await stageSourceToDisk('base64://U05PVw==', 64));
expect(staged.fileSize).toBe(4);
expect(staged.fileName).toBe('');
expectStagedPath(staged.filePath, '.bin');
expect(await fsp.readFile(staged.filePath)).toEqual(SNOW_BYTES);
});
it('accepts a case-insensitive BASE64:// prefix and one-byte padding', async () => {
const staged = hold(await stageSourceToDisk('BASE64:///w==', 1));
expect(staged.fileSize).toBe(1);
expect(staged.fileName).toBe('');
expect(await fsp.readFile(staged.filePath)).toEqual(Buffer.from([0xff]));
});
it('accepts two-byte and three-byte payloads at the decoded-size ceiling', async () => {
const two = hold(await stageSourceToDisk('base64://AAA=', 2));
const three = hold(await stageSourceToDisk('base64://AAAA', 3));
expect(two.fileSize).toBe(2);
expect(three.fileSize).toBe(3);
expect(await fsp.readFile(two.filePath)).toEqual(Buffer.from([0x00, 0x00]));
expect(await fsp.readFile(three.filePath)).toEqual(Buffer.from([0x00, 0x00, 0x00]));
});
it('stages an RFC 2397 data URL and keeps fileName empty', async () => {
const staged = hold(await stageSourceToDisk('data:image/png;base64,U05PVw==', 4));
expect(staged.fileSize).toBe(4);
expect(staged.fileName).toBe('');
expect(await fsp.readFile(staged.filePath)).toEqual(SNOW_BYTES);
});
it('accepts DATA:…;BASE64, payloads (case-insensitive metadata)', async () => {
const staged = hold(
await stageSourceToDisk('DATA:application/octet-stream;BASE64,AA==', 1),
);
expect(staged.fileSize).toBe(1);
expect(await fsp.readFile(staged.filePath)).toEqual(Buffer.from([0x00]));
});
it('strips non-base64 characters before the decoded-length check', async () => {
const staged = hold(await stageSourceToDisk('base64://U05P\nVw==', 4));
expect(staged.fileSize).toBe(4);
expect(await fsp.readFile(staged.filePath)).toEqual(SNOW_BYTES);
});
it('stages an empty base64:// payload as a zero-byte file', async () => {
const staged = hold(await stageSourceToDisk('base64://', 0));
expect(staged.fileSize).toBe(0);
expect(staged.fileName).toBe('');
expect(await fsp.readFile(staged.filePath)).toEqual(Buffer.alloc(0));
});
it('rejects oversized base64 on decoded length and leaves no new .part', async () => {
const before = await stageNames();
await expect(stageSourceToDisk('base64://aGVsbG8=', 4)).rejects.toThrow(
'stage source too large: 5 > 4',
);
expect(await newPartNames(before)).toEqual([]);
});
it('rejects an oversized data URL on decoded length and leaves no new .part', async () => {
const before = await stageNames();
await expect(stageSourceToDisk('data:audio/ogg;base64,aGVsbG8=', 4)).rejects.toThrow(
'stage source too large: 5 > 4',
);
expect(await newPartNames(before)).toEqual([]);
});
it('unlinks the .part when finalize rename fails', async () => {
const before = await stageNames();
vi.spyOn(fsp, 'rename').mockRejectedValueOnce(new Error('rename failed'));
await expect(stageSourceToDisk('base64://U05PVw==', 64)).rejects.toThrow('rename failed');
expect(await newPartNames(before)).toEqual([]);
});
});
describe('stageSourceToDisk — http', () => {
it('streams the body to disk and uses the decoded URL basename', async () => {
vi.stubGlobal(
'fetch',
vi.fn(
async () =>
new Response(Uint8Array.from([0x10, 0x20, 0x30, 0x40]), { status: 200 }),
),
);
const staged = hold(
await stageSourceToDisk('https://media.example.test/packs/clip%20v2.mp4?sig=9', 64),
);
expect(staged.fileName).toBe('clip v2.mp4');
expect(staged.fileSize).toBe(4);
expectStagedPath(staged.filePath, '.bin');
expect(await fsp.readFile(staged.filePath)).toEqual(Buffer.from([0x10, 0x20, 0x30, 0x40]));
});
it('keeps fileName empty when the URL has no basename', async () => {
vi.stubGlobal(
'fetch',
vi.fn(async () => new Response(Uint8Array.from([0x7f]), { status: 200 })),
);
const staged = hold(await stageSourceToDisk('https://media.example.test/', 8));
expect(staged.fileName).toBe('');
expect(staged.fileSize).toBe(1);
expect(await fsp.readFile(staged.filePath)).toEqual(Buffer.from([0x7f]));
});
it('creates an empty staged file for an empty HTTP body', async () => {
vi.stubGlobal(
'fetch',
vi.fn(async () => new Response(new Uint8Array(0), { status: 200 })),
);
const staged = hold(await stageSourceToDisk('HTTP://cdn.example.test/empty.bin', 8));
expect(staged.fileName).toBe('empty.bin');
expect(staged.fileSize).toBe(0);
expect(await fsp.readFile(staged.filePath)).toEqual(Buffer.alloc(0));
});
it('rejects a declared Content-Length overflow without retrying and leaves no .part', async () => {
const fetchMock = vi.fn(
async () =>
new Response(new Uint8Array(80), {
status: 200,
headers: { 'content-length': '80' },
}),
);
vi.stubGlobal('fetch', fetchMock);
const before = await stageNames();
await expect(stageSourceToDisk('https://cdn.example.test/huge.bin', 16)).rejects.toThrow(
'stage too large: 80 > 16',
);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(await newPartNames(before)).toEqual([]);
});
it('rejects a streamed overflow after the sink opened and discards the .part', async () => {
const stream = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new Uint8Array(10).fill(0x11));
controller.enqueue(new Uint8Array(10).fill(0x22));
controller.enqueue(new Uint8Array(10).fill(0x33));
controller.close();
},
});
vi.stubGlobal(
'fetch',
vi.fn(async () => new Response(stream, { status: 200 })),
);
const before = await stageNames();
await expect(stageSourceToDisk('https://cdn.example.test/stream.bin', 16)).rejects.toThrow(
'stage too large: > 16',
);
expect(await newPartNames(before)).toEqual([]);
});
it('retries once with Referer after a non-size failure and stages the retry body', async () => {
const url = 'https://media.example.test/packs/retry.bin';
const fetchMock = vi.fn(async (_input: string, init?: RequestInit) => {
const headers = init?.headers as Record<string, string>;
if (!headers.Referer) {
throw new TypeError('fetch failed');
}
expect(headers.Referer).toBe(url);
expect(headers['User-Agent']).toBe(DOWNLOAD_UA);
expect(headers.Accept).toBe('*/*');
expect(init?.redirect).toBe('follow');
return new Response(Uint8Array.from([0xaa, 0xbb, 0xcc]), { status: 200 });
});
vi.stubGlobal('fetch', fetchMock);
const staged = hold(await stageSourceToDisk(url, 32));
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(staged.fileSize).toBe(3);
expect(await fsp.readFile(staged.filePath)).toEqual(Buffer.from([0xaa, 0xbb, 0xcc]));
});
it('surfaces the first HTTP status error when the Referer retry also fails', async () => {
const fetchMock = vi.fn(async () => new Response(null, { status: 503 }));
vi.stubGlobal('fetch', fetchMock);
await expect(stageSourceToDisk('https://cdn.example.test/down.bin', 32)).rejects.toThrow(
'HTTP download failed: 503',
);
expect(fetchMock).toHaveBeenCalledTimes(2);
});
it('unlinks the http .part when rename after download fails', async () => {
vi.stubGlobal(
'fetch',
vi.fn(async () => new Response(Uint8Array.from([0x01, 0x02]), { status: 200 })),
);
vi.spyOn(fsp, 'rename').mockRejectedValueOnce(new Error('http rename failed'));
const before = await stageNames();
await expect(stageSourceToDisk('https://cdn.example.test/r.bin', 32)).rejects.toThrow(
'http rename failed',
);
expect(await newPartNames(before)).toEqual([]);
});
});
describe('stageSourceToDisk — local files', () => {
it('hardlinks a raw path, preserves basename, and pins the inode after source unlink', async () => {
const src = path.join(scratch, 'payload.dat');
await fsp.writeFile(src, Buffer.from([0xde, 0xad, 0xbe, 0xef]));
const srcStat = await fsp.stat(src);
const staged = hold(await stageSourceToDisk(src, 64));
expect(staged.fileName).toBe('payload.dat');
expect(staged.fileSize).toBe(4);
expectStagedPath(staged.filePath, '.dat');
const stagedStat = await fsp.stat(staged.filePath);
expect(stagedStat.ino).toBe(srcStat.ino);
expect(stagedStat.nlink).toBe(2);
await fsp.unlink(src);
expect(await fsp.readFile(staged.filePath)).toEqual(Buffer.from([0xde, 0xad, 0xbe, 0xef]));
});
it('accepts a file:// URL and uses the pre-resolution basename', async () => {
const src = path.join(scratch, 'video.mp4');
await fsp.writeFile(src, Buffer.from([0x00, 0x00, 0x00, 0x18]));
const staged = hold(await stageSourceToDisk(pathToFileURL(src).href, 64));
expect(staged.fileName).toBe('video.mp4');
expect(staged.fileSize).toBe(4);
expectStagedPath(staged.filePath, '.mp4');
expect(await fsp.readFile(staged.filePath)).toEqual(Buffer.from([0x00, 0x00, 0x00, 0x18]));
});
it('resolves a symlink target but keeps the symlink basename and extension', async () => {
const target = path.join(scratch, 'blob-real.bin');
const linkPath = path.join(scratch, 'alias.mov');
await fsp.writeFile(target, Buffer.from([0x6d, 0x6f, 0x6f, 0x76]));
await fsp.symlink(target, linkPath);
const staged = hold(await stageSourceToDisk(linkPath, 64));
expect(staged.fileName).toBe('alias.mov');
expectStagedPath(staged.filePath, '.mov');
expect(await fsp.readFile(staged.filePath)).toEqual(Buffer.from([0x6d, 0x6f, 0x6f, 0x76]));
});
it('uses .bin when the local path has no extension', async () => {
const src = path.join(scratch, 'noext');
await fsp.writeFile(src, Buffer.from([0x42]));
const staged = hold(await stageSourceToDisk(src, 8));
expect(staged.fileName).toBe('noext');
expectStagedPath(staged.filePath, '.bin');
});
it('preserves a mixed-case extension on the staged path', async () => {
const src = path.join(scratch, 'Photo.JPEG');
await fsp.writeFile(src, Buffer.from([0xff, 0xd8]));
const staged = hold(await stageSourceToDisk(src, 8));
expect(staged.fileName).toBe('Photo.JPEG');
expectStagedPath(staged.filePath, '.JPEG');
});
it('allows a local file whose size equals maxBytes', async () => {
const src = path.join(scratch, 'exact.bin');
await fsp.writeFile(src, Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]));
const staged = hold(await stageSourceToDisk(src, 8));
expect(staged.fileSize).toBe(8);
});
it('stages an empty regular file', async () => {
const src = path.join(scratch, 'zero.dat');
await fsp.writeFile(src, Buffer.alloc(0));
const staged = hold(await stageSourceToDisk(src, 0));
expect(staged.fileSize).toBe(0);
expect(staged.fileName).toBe('zero.dat');
expect(await fsp.readFile(staged.filePath)).toEqual(Buffer.alloc(0));
});
it('rejects a directory before creating a stage file', async () => {
await expect(stageSourceToDisk(scratch, 64)).rejects.toThrow(
`stage source is not a regular file: ${scratch}`,
);
});
it('rejects an oversized local file before staging', async () => {
const src = path.join(scratch, 'huge.bin');
await fsp.writeFile(src, Buffer.alloc(80, 0xab));
await expect(stageSourceToDisk(src, 16)).rejects.toThrow('stage source too large: 80 > 16');
});
it('copies when link() fails and the copy survives source deletion', async () => {
const src = path.join(scratch, 'xfs.bin');
const payload = Buffer.from([0xc0, 0xff, 0xee, 0x11]);
await fsp.writeFile(src, payload);
const srcIno = (await fsp.stat(src)).ino;
const linkSpy = vi
.spyOn(fsp, 'link')
.mockRejectedValueOnce(Object.assign(new Error('EXDEV'), { code: 'EXDEV' }));
const staged = hold(await stageSourceToDisk(src, 64));
expect(linkSpy).toHaveBeenCalledOnce();
expect(staged.fileName).toBe('xfs.bin');
expect(staged.fileSize).toBe(4);
expect((await fsp.stat(staged.filePath)).ino).not.toBe(srcIno);
await fsp.unlink(src);
expect(await fsp.readFile(staged.filePath)).toEqual(payload);
});
it('cleans temps when both link() and the copy fallback fail', async () => {
const src = path.join(scratch, 'fail.bin');
await fsp.writeFile(src, Buffer.from([0x01]));
vi.spyOn(fsp, 'link').mockRejectedValueOnce(
Object.assign(new Error('EXDEV'), { code: 'EXDEV' }),
);
vi.spyOn(fsp, 'copyFile').mockRejectedValueOnce(new Error('ENOSPC: copy failed'));
const before = await stageNames();
await expect(stageSourceToDisk(src, 64)).rejects.toThrow('ENOSPC: copy failed');
expect(await newPartNames(before)).toEqual([]);
});
it('cleans temps when link() fails and the copy rename fails', async () => {
const src = path.join(scratch, 'rename-fail.bin');
await fsp.writeFile(src, Buffer.from([0x02, 0x03]));
vi.spyOn(fsp, 'link').mockRejectedValueOnce(
Object.assign(new Error('EPERM'), { code: 'EPERM' }),
);
vi.spyOn(fsp, 'rename').mockRejectedValueOnce(new Error('copy rename failed'));
const before = await stageNames();
await expect(stageSourceToDisk(src, 64)).rejects.toThrow('copy rename failed');
expect(await newPartNames(before)).toEqual([]);
});
});
describe('stageSourceToDisk — cleanup', () => {
it('removes only the staged temp, never the caller file, and is idempotent', async () => {
const src = path.join(scratch, 'keep-me.bin');
await fsp.writeFile(src, Buffer.from([0xde, 0xad, 0xbe, 0xef]));
const staged = hold(await stageSourceToDisk(src, 64));
expect(fs.existsSync(staged.filePath)).toBe(true);
await staged.cleanup();
expect(fs.existsSync(staged.filePath)).toBe(false);
expect(fs.existsSync(src)).toBe(true);
expect(await fsp.readFile(src)).toEqual(Buffer.from([0xde, 0xad, 0xbe, 0xef]));
await expect(staged.cleanup()).resolves.toBeUndefined();
});
it('tolerates the staged path already being gone', async () => {
const src = path.join(scratch, 'gone.bin');
await fsp.writeFile(src, Buffer.from([0x09]));
const staged = hold(await stageSourceToDisk(src, 64));
await fsp.unlink(staged.filePath);
await expect(staged.cleanup()).resolves.toBeUndefined();
});
});
describe('sweepStagedUploads', () => {
it('returns 0 when the stage directory does not exist', async () => {
const parked = `${STAGE_DIR}.park-${randomUUID()}`;
let moved = false;
try {
try {
await fsp.rename(STAGE_DIR, parked);
moved = true;
} catch (err) {
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
throw err;
}
}
await expect(sweepStagedUploads(2_000_000_000_000)).resolves.toBe(0);
} finally {
if (moved) {
await fsp.rm(STAGE_DIR, { recursive: true, force: true }).catch(() => undefined);
await fsp.rename(parked, STAGE_DIR);
}
}
});
it('reaps a temp older than 30 minutes, keeps a TTL-edge temp, and skips an active staged file', async () => {
const src = path.join(scratch, 'inflight.bin');
await fsp.writeFile(src, Buffer.from([0x51, 0x52]));
const staged = hold(await stageSourceToDisk(src, 64));
await fsp.mkdir(STAGE_DIR, { recursive: true });
const now = 2_000_000_000_000;
const aged = path.join(STAGE_DIR, `stage-${randomUUID()}.aged`);
const edge = path.join(STAGE_DIR, `stage-${randomUUID()}.edge`);
const recent = path.join(STAGE_DIR, `stage-${randomUUID()}.recent`);
await fsp.writeFile(aged, Buffer.from('aged-orphan'));
await fsp.writeFile(edge, Buffer.from('ttl-edge'));
await fsp.writeFile(recent, Buffer.from('fresh-orphan'));
// Age by whole seconds: some filesystems store mtime at 1s resolution, so a
// 1ms-past-TTL stamp can round back onto the keep side of `> 30min`.
await fsp.utimes(aged, new Date(now), new Date(now - STAGE_TTL_MS - 1000));
await fsp.utimes(edge, new Date(now), new Date(now - STAGE_TTL_MS));
await fsp.utimes(recent, new Date(now), new Date(now));
await fsp.utimes(
staged.filePath,
new Date(now - STAGE_TTL_MS - 1000),
new Date(now - STAGE_TTL_MS - 1000),
);
try {
const removed = await sweepStagedUploads(now);
expect(fs.existsSync(aged)).toBe(false);
expect(fs.existsSync(edge)).toBe(true);
expect(fs.existsSync(recent)).toBe(true);
expect(fs.existsSync(staged.filePath)).toBe(true);
expect(removed).toBeGreaterThanOrEqual(1);
} finally {
await fsp.rm(edge, { force: true });
await fsp.rm(recent, { force: true });
await fsp.rm(aged, { force: true });
}
});
it('uses Date.now() when now is omitted', async () => {
await fsp.mkdir(STAGE_DIR, { recursive: true });
const orphan = path.join(STAGE_DIR, `stage-${randomUUID()}.epoch`);
await fsp.writeFile(orphan, Buffer.from('epoch-orphan'));
await fsp.utimes(
orphan,
new Date('1990-01-01T00:00:00.000Z'),
new Date('1990-01-01T00:00:00.000Z'),
);
try {
const removed = await sweepStagedUploads();
expect(fs.existsSync(orphan)).toBe(false);
expect(removed).toBeGreaterThanOrEqual(1);
} finally {
await fsp.rm(orphan, { force: true });
}
});
});
@@ -0,0 +1,56 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { UploadNodeHistory } from '../../src/highway/upload-nodes';
const a = { host: '192.0.2.1', port: 14000 };
const b = { host: '192.0.2.1', port: 80 };
const c = { host: '192.0.2.2', port: 80 };
afterEach(() => vi.useRealTimers());
describe('upload node observations', () => {
it('orders current candidates by confirmed success, unknown, then recent failure', () => {
const history = new UploadNodeHistory();
history.record(1004, a, false);
history.record(1004, b, true);
const original = [a, c, b, a];
expect(history.order(1004, original)).toEqual([b, c, a]);
expect(original).toEqual([a, c, b, a]);
expect(history.order(1003, [a, c, b])).toEqual([a, c, b]);
expect(history.order(1004, [c])).toEqual([c]);
});
it('expires failures and successes and still allows every failed candidate', () => {
vi.useFakeTimers();
const history = new UploadNodeHistory();
history.record(1004, a, false);
history.record(1004, b, false);
expect(history.order(1004, [a, b])).toEqual([a, b]);
expect(history.order(1004, [a, c])).toEqual([c, a]);
vi.advanceTimersByTime(30_000);
expect(history.order(1004, [a, c])).toEqual([a, c]);
history.record(1004, b, true);
expect(history.order(1004, [a, b])).toEqual([b, a]);
vi.advanceTimersByTime(5 * 60_000);
expect(history.order(1004, [a, b])).toEqual([a, b]);
});
it('lets a new failure replace success and a subsequent success recover', () => {
const history = new UploadNodeHistory();
history.record(1004, b, true);
history.record(1004, b, false);
expect(history.order(1004, [b, a])).toEqual([a, b]);
history.record(1004, b, true);
expect(history.order(1004, [a, b])).toEqual([b, a]);
});
it('evicts old observations when many distinct endpoints arrive', () => {
const history = new UploadNodeHistory();
history.record(1004, a, false);
for (let port = 10000; port < 10200; port++) {
history.record(1004, { host: '192.0.2.3', port }, false);
}
expect(history.order(1004, [a, c])).toEqual([a, c]);
});
});
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';
import { highwayUploadTimeouts } from '../../src/highway/upload-settings';
describe('highway upload timeouts', () => {
it('retains the idle default and supplies a finite whole-file deadline', () => {
expect(highwayUploadTimeouts({})).toEqual({ idleMs: 30_000, totalMs: 1_800_000 });
});
it('reads independent millisecond budgets without clamping', () => {
expect(
highwayUploadTimeouts({
SNOWLUMA_HIGHWAY_UPLOAD_IDLE_TIMEOUT_MS: ' 5000 ',
SNOWLUMA_HIGHWAY_UPLOAD_TIMEOUT_MS: '120000',
}),
).toEqual({ idleMs: 5000, totalMs: 120000 });
expect(highwayUploadTimeouts({ SNOWLUMA_HIGHWAY_UPLOAD_TIMEOUT_MS: '1' }).totalMs).toBe(1);
expect(
highwayUploadTimeouts({ SNOWLUMA_HIGHWAY_UPLOAD_TIMEOUT_MS: '2147483647' }).totalMs,
).toBe(2147483647);
});
it.each(['', ' ', '0', '-1', '1.5', 'NaN', 'Infinity', '1e3', '200ms', '2147483648'])(
'rejects invalid configuration %j',
(value) => {
for (const name of [
'SNOWLUMA_HIGHWAY_UPLOAD_IDLE_TIMEOUT_MS',
'SNOWLUMA_HIGHWAY_UPLOAD_TIMEOUT_MS',
]) {
expect(() => highwayUploadTimeouts({ [name]: value })).toThrow(name);
}
},
);
});
@@ -0,0 +1,133 @@
import net from 'node:net';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { BridgeContext } from '../../src/bridge-context';
import {
BufferChunkSource,
uploadHighwayHttp,
type HighwayServer,
} from '../../src/highway/highway-client';
import { packHighwayFrame } from '../../src/highway/utils';
const servers: net.Server[] = [];
const sockets = new Set<net.Socket>();
afterEach(async () => {
vi.unstubAllEnvs();
for (const socket of sockets) {
socket.destroy();
}
await Promise.all(
servers.splice(0).map(
(server) =>
new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
}),
),
);
});
async function listen(
reply: boolean,
): Promise<{ address: HighwayServer; connections: () => number }> {
let connections = 0;
const server = net.createServer((socket) => {
connections++;
sockets.add(socket);
socket.once('close', () => sockets.delete(socket));
let received = Buffer.alloc(0);
socket.on('data', (bytes: Buffer) => {
received = Buffer.concat([received, bytes]);
const end = received.indexOf('\r\n\r\n');
if (!reply || end < 0) {
return;
}
const header = received.subarray(0, end).toString('ascii');
const length = Number(header.match(/Content-Length: (\d+)/i)?.[1]);
if (received.length < end + 4 + length) {
return;
}
const frame = packHighwayFrame(new Uint8Array(), new Uint8Array());
socket.write(
Buffer.concat([
Buffer.from(`HTTP/1.1 200 OK\r\nContent-Length: ${frame.byteLength}\r\n\r\n`),
frame,
]),
);
received = received.subarray(end + 4 + length);
});
});
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
server.off('error', reject);
resolve();
});
});
servers.push(server);
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('missing loopback address');
}
return {
address: { host: address.address, port: address.port },
connections: () => connections,
};
}
function bridge(uin: string): BridgeContext {
return { identity: { uin } } as unknown as BridgeContext;
}
function send(
owner: BridgeContext,
candidates: HighwayServer[],
source = new BufferChunkSource(new Uint8Array([1, 2, 3])),
) {
return uploadHighwayHttp(
owner,
{ sigSession: new Uint8Array([1]), sessionKey: new Uint8Array(), servers: candidates },
1004,
source,
new Uint8Array(16),
new Uint8Array(),
);
}
describe('highway upload over real loopback sockets', () => {
it('switches idle ports and remembers only confirmed success for that account', async () => {
vi.stubEnv('SNOWLUMA_HIGHWAY_UPLOAD_IDLE_TIMEOUT_MS', '100');
vi.stubEnv('SNOWLUMA_HIGHWAY_UPLOAD_TIMEOUT_MS', '5000');
const idle = await listen(false);
const healthy = await listen(true);
const owner = bridge('10001');
const candidates = [idle.address, healthy.address];
await send(owner, candidates);
await send(owner, candidates);
expect(idle.connections()).toBe(1);
expect(healthy.connections()).toBe(2);
await send(bridge('10002'), candidates);
expect(idle.connections()).toBe(2);
expect(healthy.connections()).toBe(3);
});
it('rejects an unresponsive peer at the total deadline and closes its source once', async () => {
vi.stubEnv('SNOWLUMA_HIGHWAY_UPLOAD_IDLE_TIMEOUT_MS', '10000');
vi.stubEnv('SNOWLUMA_HIGHWAY_UPLOAD_TIMEOUT_MS', '100');
const idle = await listen(false);
const source = new BufferChunkSource(new Uint8Array([1, 2, 3]));
const close = vi.spyOn(source, 'close');
await expect(send(bridge('10003'), [idle.address], source)).rejects.toThrow(
'timed out after 100ms',
);
expect(close).toHaveBeenCalledTimes(1);
expect(idle.connections()).toBe(1);
});
});
@@ -0,0 +1,175 @@
import { describe, it, expect } from 'vitest';
import { detectImageFormat } from '@snowluma/protocol/highway/utils';
// Regression coverage for detectImageFormat (was previously untested). The
// header-parsing is all hand-rolled byte math, so each format's offset AND
// endianness is pinned here. The VP8X cases exist because that branch read the
// canvas width/height big-endian when the WebP spec stores them 24-bit
// little-endian (issue #112: a 1920×1080 image came back as 8324865×3605505,
// cropping the QQ thumbnail). VP8 / VP8L were already little-endian.
// ── little-endian byte writers (match how the formats store dimensions) ──
function le16(v: number): number[] {
return [v & 0xff, (v >> 8) & 0xff];
}
function le24(v: number): number[] {
return [v & 0xff, (v >> 8) & 0xff, (v >> 16) & 0xff];
}
function le32(v: number): number[] {
return [v & 0xff, (v >> 8) & 0xff, (v >> 16) & 0xff, (v >> 24) & 0xff];
}
function be16(v: number): number[] {
return [(v >> 8) & 0xff, v & 0xff];
}
function be32(v: number): number[] {
return [(v >> 24) & 0xff, (v >> 16) & 0xff, (v >> 8) & 0xff, v & 0xff];
}
function ascii(s: string): number[] {
return [...s].map((c) => c.charCodeAt(0));
}
/** Build a buffer from segments placed at explicit offsets; gaps are 0. */
function buf(len: number, ...segs: Array<[number, number[]]>): Uint8Array {
const b = new Uint8Array(len);
for (const [off, bytes] of segs) {
b.set(bytes, off);
}
return b;
}
describe('detectImageFormat', () => {
it('PNG — IHDR width/height (big-endian @16/@20) → 1001', () => {
const b = buf(24, [0, [0x89, 0x50, 0x4e, 0x47]], [16, be32(800)], [20, be32(600)]);
expect(detectImageFormat(b)).toEqual({ format: 1001, width: 800, height: 600 });
});
it('GIF — logical screen width/height (little-endian @6/@8) → 2000', () => {
const b = buf(10, [0, ascii('GIF89a')], [6, le16(320)], [8, le16(240)]);
expect(detectImageFormat(b)).toEqual({ format: 2000, width: 320, height: 240 });
// GIF87a header variant is also accepted.
const b87 = buf(10, [0, ascii('GIF87a')], [6, le16(48)], [8, le16(16)]);
expect(detectImageFormat(b87)).toEqual({ format: 2000, width: 48, height: 16 });
});
it('BMP — DIB width/height (little-endian @18/@22) → 1005', () => {
const b = buf(26, [0, ascii('BM')], [18, le32(1024)], [22, le32(768)]);
expect(detectImageFormat(b)).toEqual({ format: 1005, width: 1024, height: 768 });
});
it('WebP VP8 (lossy) — little-endian @26/@28 → 1002', () => {
const b = buf(
32,
[0, ascii('RIFF')],
[8, ascii('WEBP')],
[12, ascii('VP8 ')],
[26, le16(640)],
[28, le16(480)],
);
expect(detectImageFormat(b)).toEqual({ format: 1002, width: 640, height: 480 });
});
it('WebP VP8L (lossless) — 14-bit fields packed in LE32 @21 → 1002', () => {
// width-1 in bits[0..13], height-1 in bits[14..27].
const bits = (100 - 1) | ((200 - 1) << 14);
const b = buf(
32,
[0, ascii('RIFF')],
[8, ascii('WEBP')],
[12, ascii('VP8L')],
[21, le32(bits >>> 0)],
);
expect(detectImageFormat(b)).toEqual({ format: 1002, width: 100, height: 200 });
});
it('WebP VP8X (extended) — canvas width/height are 24-bit LITTLE-endian +1 @24/@27 → 1002 (issue #112)', () => {
const b = buf(
34,
[0, ascii('RIFF')],
[8, ascii('WEBP')],
[12, ascii('VP8X')],
[24, le24(1920 - 1)],
[27, le24(1080 - 1)],
);
expect(detectImageFormat(b)).toEqual({ format: 1002, width: 1920, height: 1080 });
});
it('WebP VP8X — asymmetric dims pin byte order (BE misread would give huge/swapped values)', () => {
// 256×1: width-1=255 → LE bytes [ff,00,00]; a BE read of those = 16711680.
// height-1=0 → [00,00,00]. Distinct w≠h also catches an offset swap.
const b = buf(
34,
[0, ascii('RIFF')],
[8, ascii('WEBP')],
[12, ascii('VP8X')],
[24, le24(256 - 1)],
[27, le24(1 - 1)],
);
expect(detectImageFormat(b)).toEqual({ format: 1002, width: 256, height: 1 });
});
it('WebP VP8X — exact bytes from issue #112 decode to 1920×1080, not 8324865×3605505', () => {
// The reported broken values came from these literal canvas bytes.
const b = buf(
34,
[0, ascii('RIFF')],
[8, ascii('WEBP')],
[12, ascii('VP8X')],
[24, [0x7f, 0x07, 0x00]],
[27, [0x37, 0x04, 0x00]],
);
expect(detectImageFormat(b)).toEqual({ format: 1002, width: 1920, height: 1080 });
});
it('JPEG — dimensions from the SOF0 segment (big-endian, height before width) → 1000', () => {
// FFD8 SOI, then SOF0 (FFC0) segment: len(BE16)=17, precision, height@+5, width@+7.
// Buffer must hold the whole declared 17-byte segment (offset 2..20) or the
// parser's bounds-check bails before reading — hence length 24, not 20.
const b = buf(
24,
[0, [0xff, 0xd8]],
[2, [0xff, 0xc0]],
[4, be16(17)],
[6, [8]],
[7, be16(720)],
[9, be16(1280)],
);
expect(detectImageFormat(b)).toEqual({ format: 1000, width: 1280, height: 720 });
});
it('JPEG — skips a DHT (0xFFC4) segment before SOF0 (must not misread Huffman bytes)', () => {
// SOI, DHT(FFC4) len=4 (2 payload bytes), then SOF0 with real 640×480.
const b = buf(
28,
[0, [0xff, 0xd8]],
[2, [0xff, 0xc4]],
[4, be16(4)],
[6, [0xde, 0xad]],
[8, [0xff, 0xc0]],
[10, be16(17)],
[12, [8]],
[13, be16(480)],
[15, be16(640)],
);
expect(detectImageFormat(b)).toEqual({ format: 1000, width: 640, height: 480 });
});
it('unknown bytes fall back to {1000, 0, 0} without throwing', () => {
expect(detectImageFormat(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]))).toEqual({
format: 1000,
width: 0,
height: 0,
});
});
it('a truncated WebP header (<30 bytes) does not crash and falls through', () => {
const b = buf(20, [0, ascii('RIFF')], [8, ascii('WEBP')], [12, ascii('VP8X')]);
expect(detectImageFormat(b)).toEqual({ format: 1000, width: 0, height: 0 });
});
});
@@ -0,0 +1,51 @@
import { describe, it, expect } from 'vitest';
import { createHash } from 'node:crypto';
import { computeVideoSha1Blocks } from '../../src/highway/video-upload';
// Deterministic input generator (matches the golden-capture script).
function det(len: number): Uint8Array {
const b = new Uint8Array(len);
for (let i = 0; i < len; i++) {
b[i] = (i * 31 + 7) & 0xff;
}
return b;
}
const hex = (u: Uint8Array): string => Buffer.from(u).toString('hex');
const nodeSha1 = (b: Uint8Array): string => createHash('sha1').update(Buffer.from(b)).digest('hex');
describe('computeVideoSha1Blocks', () => {
// GOLDEN: captured from the previous hand-rolled Sha1StreamState implementation
// before the F3 rewrite (Sha1Stream + Node crypto), so this locks the rewrite
// byte-identical for the block states AND the overall digest.
it('reproduces the pre-refactor golden 1MB-block states + overall sha1', () => {
const input = det(2 * 1024 * 1024 + 12345); // 2 full 1MB blocks + a partial tail
expect(computeVideoSha1Blocks(input).map(hex)).toEqual([
'ab9a20fd6c98fc1f8f0c985cd552ff14c6feacfd', // intermediate un-finalized state @1MB (little-endian)
'2ccc5923b75f46ebd8ccb78e8c413c18025178b9', // intermediate un-finalized state @2MB
'dd9cfd4648ab460260afa820c98bfec1b17655d4', // overall (finalized)
]);
});
it('the final block equals the standard full-file SHA1', () => {
const input = det(2 * 1024 * 1024 + 12345);
const blocks = computeVideoSha1Blocks(input);
expect(hex(blocks[blocks.length - 1]!)).toBe(nodeSha1(input));
});
it('a file shorter than 1MB yields just the overall sha1', () => {
const input = det(1000);
const blocks = computeVideoSha1Blocks(input);
expect(blocks).toHaveLength(1);
expect(hex(blocks[0]!)).toBe(nodeSha1(input));
});
it('a file that is an exact 1MB multiple emits the boundary state then the overall', () => {
const input = det(1024 * 1024);
const blocks = computeVideoSha1Blocks(input);
expect(blocks).toHaveLength(2);
expect(hex(blocks[1]!)).toBe(nodeSha1(input)); // last is the finalized overall
});
});