文件签名是文件开头若干字节的固定二进制模式,用于准确识别文件类型;因扩展名可随意修改且无校验作用,必须读取原始字节比对签名以避免安全风险。

什么是文件签名,为什么不能只看扩展名
文件签名(Magic Number)是文件开头若干字节的固定二进制模式,比如 PNG 文件以 \x89\x50\x4E\x47\x0D\x0A\x1A\x0A 开头,JPEG 是 \xFF\xD8\xFF。操作系统和大多数工具依赖扩展名判断类型,但扩展名可随意修改,毫无校验意义。真实场景中(如上传文件校验、日志分析、取证解析),必须读取原始字节比对签名——否则可能把伪造的 .jpg shell 脚本当图片加载,引发严重安全问题。
用 std::ifstream 安全读取前 16 字节并比对
不要用 fopen + fread 手动管理缓冲区,C++ 标准流更可靠;但注意:必须用二进制模式打开,且显式检查读取长度,避免 EOF 或权限错误导致误判。
- 用
std::ifstream file(path, std::ios::binary)打开,否则 Windows 下换行符会被转换 - 用
file.read(buffer, size)后立刻检查file.gcount(),确保读够字节数(例如某些文件实际只有 2 字节,gcount()返回 2) - 比对时建议用
std::memcmp或std::equal,避免手动循环出错 - 常见陷阱:忘记
file.clear()就重复使用流对象,或未关闭导致句柄泄漏(RAII 会自动处理,但异常路径需确认)
char sig[16] = {0};
std::ifstream f("test.bin", std::ios::binary);
if (f && f.read(sig, sizeof(sig))) {
size_t got = f.gcount();
if (got >= 3 && std::memcmp(sig, "\xFF\xD8\xFF", 3) == 0) {
return "jpeg";
}
if (got >= 8 && std::memcmp(sig, "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A", 8) == 0) {
return "png";
}
}
支持常见格式的签名表怎么组织才不卡顿
硬编码 if-else 判断 20 种格式,既难维护又影响性能(每次都要顺序匹配)。推荐用 std::vector 存签名长度+模式+类型字符串,按签名长度降序排列(先匹配长签名,避免 JPEG 被 \xFF\xD8(EXIF 头)误判为其他格式)。
- 签名长度必须参与排序:比如
PDF是%PDF(4 字节),但某些 PDF 带 header 可能以\x25\x50\x44\x46\x2D\x31\x2E(7 字节)开头,优先匹配更长的 - 避免
std::string频繁构造:签名用std::array<uint8_t n></uint8_t>存储,类型名用const char*静态字符串 - 小文件(read 全部内容再查;大文件只读前 32 字节足够,多数格式签名都在前 16 字节内
遇到 libmagic 这类外部库要不要引入
如果项目已用 CMake 且允许第三方依赖,libmagic(file 命令底层)覆盖 500+ 格式,支持复合签名(如 ZIP 中嵌套 DOCX)、文本编码检测,值得接入;但纯嵌入式或沙箱环境(如 WASM、iOS App Extension)无法链接动态库时,必须手写轻量签名表。
立即学习“C++免费学习笔记(深入)”;
-
libmagic的magic_file加载的是文本规则文件(/usr/share/misc/magic),体积大(MB 级),不适合资源受限场景 - 其 C API 调用需手动
magic_open(MAGIC_MIME_TYPE)+magic_load+magic_file,错误码分散(-1表示失败,但没具体原因) - 手写方案只要维护 20–30 个核心格式(
ELF,PE,ZIP,PDF,JSON,XML等),代码不到 200 行,无依赖、无初始化开销
真正容易被忽略的是:某些格式签名不在文件开头——比如 FAT32 镜像的 BPB 在偏移 0x0B,而 NTFS 的 OEM ID 在 0x03;自动识别若只扫开头,会漏掉磁盘镜像类文件。这类需求得按协议规范跳转偏移再读,不能一概而论。



















