50 lines
2.9 KiB
TypeScript
50 lines
2.9 KiB
TypeScript
|
|
// 手写魔数签名表(Plan 3 Task 4,spec §11.1、计划"Tech Stack:零新增依赖"):
|
|||
|
|
// 常见六类媒体格式判别 + SVG 文本特征检出。刻意不引 file-type 依赖——导入管道的
|
|||
|
|
// 安全决策只须回答两问:"是什么格式"与"是否 SVG(隔离)",一张可审计的小签名表
|
|||
|
|
// 足够,且避免为一次头部读取引入整个 MIME 数据库。
|
|||
|
|
//
|
|||
|
|
// 判别语义(detectMediaType):
|
|||
|
|
// - 命中二进制签名 → {type: MIME, svg: false}(PNG/JPEG/GIF87a/GIF89a/WEBP/PDF);
|
|||
|
|
// - 无二进制命中但前 1KB 文本窗口含 "<svg"(大小写不敏感)→ {type: 'image/svg+xml',
|
|||
|
|
// svg: true}——调用方据此 status='quarantined'(spec §11.1:SVG 可携带脚本与外部
|
|||
|
|
// 引用,不进产物语义);
|
|||
|
|
// - 均未命中 → null(未知签名——调用方 status='unsupported' + issue,不猜格式)。
|
|||
|
|
//
|
|||
|
|
// 顺序纪律:二进制签名先于 SVG 文本扫描——PNG/JPEG 正例即便尾部字节碰巧含 "<svg"
|
|||
|
|
// 片段也按二进制格式判(文本窗口只对非二进制文件有意义)。窗口 1KB 是钉死的折中:
|
|||
|
|
// 覆盖常规 "<?xml …?><svg" 前导(声明/注释/doctype),不给深藏脚本的巨型前缀放行。
|
|||
|
|
//
|
|||
|
|
// Buffer 读取口径:latin1 单字节解码后小写化做包含检测——不做 UTF-8 多字节解码,
|
|||
|
|
// 避免截断的字节序列被解码成意外字符。
|
|||
|
|
/** 二进制签名表(前缀匹配;WEBP 的 RIFF+WEBP@8 双段结构在表外单查) */
|
|||
|
|
const SIGNATURES: ReadonlyArray<{ mime: string; bytes: Buffer }> = Object.freeze([
|
|||
|
|
{ mime: 'image/png', bytes: Buffer.from([0x89, 0x50, 0x4e, 0x47]) },
|
|||
|
|
{ mime: 'image/jpeg', bytes: Buffer.from([0xff, 0xd8, 0xff]) },
|
|||
|
|
{ mime: 'image/gif', bytes: Buffer.from('GIF87a', 'latin1') },
|
|||
|
|
{ mime: 'image/gif', bytes: Buffer.from('GIF89a', 'latin1') },
|
|||
|
|
{ mime: 'application/pdf', bytes: Buffer.from('%PDF', 'latin1') },
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
const RIFF = Buffer.from('RIFF', 'latin1')
|
|||
|
|
const WEBP = Buffer.from('WEBP', 'latin1')
|
|||
|
|
const SVG_WINDOW_BYTES = 1024
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 魔数检测:读文件头部字节判别媒体类型。返回 null = 未知签名(调用方按
|
|||
|
|
* unsupported 处置);svg=true 仅出现在 type='image/svg+xml'(文本特征命中)。
|
|||
|
|
*/
|
|||
|
|
export function detectMediaType(buf: Buffer): { type: string; svg: boolean } | null {
|
|||
|
|
for (const sig of SIGNATURES) {
|
|||
|
|
if (buf.length >= sig.bytes.length && buf.subarray(0, sig.bytes.length).equals(sig.bytes)) {
|
|||
|
|
return { type: sig.mime, svg: false }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// WEBP:RIFF 容器头(4B 长度)+ 'WEBP' Magic(@8)——RIFF 家族其他成员(WAV/AVI)不误报
|
|||
|
|
if (buf.length >= 12 && buf.subarray(0, 4).equals(RIFF) && buf.subarray(8, 12).equals(WEBP)) {
|
|||
|
|
return { type: 'image/webp', svg: false }
|
|||
|
|
}
|
|||
|
|
const head = buf.subarray(0, Math.min(buf.length, SVG_WINDOW_BYTES)).toString('latin1').toLowerCase()
|
|||
|
|
if (head.includes('<svg')) return { type: 'image/svg+xml', svg: true }
|
|||
|
|
return null
|
|||
|
|
}
|