ed081ec477
Plan 3 Task 4:media-writer(attachment 行三级对拍 + 内容行引用提取双事件消费) + magic-bytes 手写签名表(零新增依赖)。 - 三级对拍(前置事实 7):①uploads 相对路径清单精确命中 ②侧车 source_url→ file_path→清单实体(精确 miss 按文件名候选,多候选歧义不自动选全列 issue) ③missing 只进报告不插行(sha256 NOT NULL 库层防线) - 魔数校验:命中实体读存储副本头部实际字节——SVG 检出 status=quarantined 不进 产物语义(引用不建 usage);未知签名 unsupported;扩展名声明 MIME 与检测不符 → issue(按检测登记) - usage 两阶段时序(WXR 文档序 items 先于 attachment):内容行提取引用 + 修订 锚点(依赖 content-writer 先处理同事件——Task 6 编排顺序契约),finish 统一 对 media_assets 反查(id→source_id / URL→source_url→public_path),顺序无关 - 悬空引用计数 mediaDanglingRefs 不静默(mini 203/204 钉死) - counts 六键 media 前缀独占(勿复用 'skipped' 的键契约注记) - 幂等:(site,source_url) 定向 onConflictDoNothing + usage 复合主键——重跑零增行 308 测试(296+6 单测+6 集成);typecheck/lint 三绿。 Co-Authored-By: Claude Code <noreply@anthropic.com>
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
|
||
}
|