import { describe, expect, it } from "vitest"; import { decodeText, imageSize, maskTracker, parseAss, parseSrtLike, parseSup, previewKindOf, } from "../../src/modules/file-manager/preview-parse.ts"; // FM-2 预览纯解析:编码探测、字幕、PGS、图片尺寸(不碰 PG)。 const enc = (s: string) => new TextEncoder().encode(s); const u16le = (s: string) => { const b = new Uint8Array(s.length * 2); for (let i = 0; i < s.length; i++) { b[i * 2] = s.charCodeAt(i) & 0xff; b[i * 2 + 1] = s.charCodeAt(i) >> 8; } return b; }; describe("previewKindOf", () => { it("按扩展名分类,大小写不敏感", () => { expect(previewKindOf("a.NFO")).toBe("text"); expect(previewKindOf("x.log")).toBe("text"); expect(previewKindOf("x.ass")).toBe("subtitle"); expect(previewKindOf("x.AVIF")).toBe("image"); expect(previewKindOf("x.torrent")).toBe("torrent"); expect(previewKindOf("x.mkv")).toBeNull(); expect(previewKindOf("x.svg")).toBeNull(); expect(previewKindOf("README")).toBeNull(); }); }); describe("decodeText", () => { it("BOM:UTF-8 / UTF-16LE / UTF-16BE", () => { expect(decodeText(new Uint8Array([0xef, 0xbb, 0xbf, ...enc("中文")]))).toEqual({ text: "中文", encoding: "utf-8" }); expect(decodeText(new Uint8Array([0xff, 0xfe, ...u16le("Hi 中")]))).toEqual({ text: "Hi 中", encoding: "utf-16le", }); expect(decodeText(new Uint8Array([0xfe, 0xff, 0x00, 0x41]))).toEqual({ text: "A", encoding: "utf-16be" }); }); it("无 BOM 的 UTF-16LE 按零字节分布识别", () => { expect(decodeText(u16le("[Script Info]\nTitle: x\n")).encoding).toBe("utf-16le"); }); it("UTF-8;截断在多字节字符中间也按 UTF-8", () => { const b = enc("沙丘 2 字幕"); expect(decodeText(b).encoding).toBe("utf-8"); expect(decodeText(b.subarray(0, b.length - 1), true)).toEqual({ text: "沙丘 2 字", encoding: "utf-8" }); }); it("GBK 与 Big5", () => { // "你好,世界。沙丘" 的 GBK 字节 const gbk = new Uint8Array([ 0xc4, 0xe3, 0xba, 0xc3, 0xa3, 0xac, 0xca, 0xc0, 0xbd, 0xe7, 0xa1, 0xa3, 0xc9, 0xb3, 0xc7, 0xf0, ]); expect(decodeText(gbk)).toEqual({ text: "你好,世界。沙丘", encoding: "gb18030" }); // "電影字幕" 的 Big5 字节 const big5 = new Uint8Array([0xb9, 0x71, 0xbc, 0x76, 0xa6, 0x72, 0xb9, 0xf5]); expect(decodeText(big5)).toEqual({ text: "電影字幕", encoding: "big5" }); }); it("都不像时退回 windows-1252", () => { expect(decodeText(new Uint8Array([0x43, 0x61, 0x66, 0xe9])).encoding).toBe("windows-1252"); }); }); describe("字幕", () => { it("SRT:时间轴、多行、去标签", () => { const cues = parseSrtLike( "1\r\n00:00:01,000 --> 00:00:04,500\r\n厄拉科斯\r\n第二行\r\n\r\n2\r\n00:01:05,20 --> 00:01:07,000\r\n{\\an8}上方\r\n", ); expect(cues).toEqual([ { start: 1000, end: 4500, text: "厄拉科斯\n第二行" }, { start: 65200, end: 67000, text: "上方" }, ]); }); it("VTT:mm:ss.mmm", () => { expect(parseSrtLike("WEBVTT\n\n00:01.500 --> 00:02.000 align:start\nhi\n")).toEqual([ { start: 1500, end: 2000, text: "hi" }, ]); }); it("ASS:按 Format 取列,Text 里的逗号保留,覆写标签与 \\N 处理", () => { const text = [ "[Script Info]", "Title: 沙丘 简中", "[Events]", "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text", "Dialogue: 0,0:00:02.50,0:00:04.00,Default,,0,0,0,,{\\fad(100,100)}你好,世界,再见\\N第二行", "Comment: 0,0:00:05.00,0:00:06.00,Default,,0,0,0,,注释", ].join("\n"); expect(parseAss(text)).toEqual({ title: "沙丘 简中", cues: [{ start: 2500, end: 4000, text: "你好,世界,再见\n第二行" }], }); }); it("PGS:带对象的组合段起、空组合段止", () => { const seg = (ptsMs: number, type: number, body: number[]) => { const pts = ptsMs * 90; return [ 0x50, 0x47, (pts >>> 24) & 255, (pts >>> 16) & 255, (pts >>> 8) & 255, pts & 255, 0, 0, 0, 0, type, 0, body.length, ...body, ]; }; const pcs = (objects: number) => [ 7, 128, 4, 56, 0x10, 0, 0, 0x80, 0, 0, objects, ...(objects ? [0, 0, 0, 0, 0, 0, 0, 0] : []), ]; const buf = new Uint8Array([ ...seg(1000, 0x16, pcs(1)), ...seg(1000, 0x80, []), ...seg(3000, 0x16, pcs(0)), ...seg(5000, 0x16, pcs(1)), ]); expect(parseSup(buf)).toEqual([{ start: 1000, end: 3000, text: "" }]); }); }); describe("imageSize", () => { it("PNG / GIF / JPEG / WebP VP8X", () => { const png = new Uint8Array(24); png.set([ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 13, 0x49, 0x48, 0x44, 0x52, 0, 0, 7, 128, 0, 0, 4, 56, ]); expect(imageSize(png)).toEqual({ width: 1920, height: 1080 }); const gif = new Uint8Array(24); gif.set([0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x2c, 0x01, 0xc2, 0x01]); expect(imageSize(gif)).toEqual({ width: 300, height: 450 }); const jpg = new Uint8Array([ 0xff, 0xd8, 0xff, 0xe0, 0, 4, 0, 0, 0xff, 0xc0, 0, 17, 8, 0x05, 0xdc, 0x03, 0xe8, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]); expect(imageSize(jpg)).toEqual({ width: 1000, height: 1500 }); const webp = new Uint8Array(30); webp.set([...enc("RIFF"), 0, 0, 0, 0, ...enc("WEBP"), ...enc("VP8X"), 10, 0, 0, 0, 0, 0, 0, 0]); webp.set([0x7f, 0x07, 0x00, 0x37, 0x04, 0x00], 24); expect(imageSize(webp)).toEqual({ width: 1920, height: 1080 }); expect(imageSize(enc("not an image at all, just text"))).toBeNull(); }); }); describe("maskTracker", () => { it("参数里的 passkey、路径里的长密钥段打码", () => { expect(maskTracker("https://t.example/announce.php?passkey=0123456789abcdef0123&foo=1")).toBe( "https://t.example/announce.php?passkey=***&foo=1", ); expect(maskTracker("https://t.example/0123456789abcdef0123456789abcdef/announce")).toBe( "https://t.example/***/announce", ); expect(maskTracker("udp://tracker.opentrackr.org:1337/announce")).toBe( "udp://tracker.opentrackr.org:1337/announce", ); }); });