什么是文件「魔数」?从is-png-cj看懂PNG文件签名识别原理入门
【免费下载链接】is-png-cj一个判断图片格式的库,根据图片的文件数据,判断图片是否为png格式项目地址: https://gitcode.com/Cangjie-TPC/is-png-cj
is-png-cj是一个用仓颉语言(Cangjie)编写的轻量级文件识别库,它通过读取文件头部的 8 个字节——也就是「魔数」(Magic Number)——来判断一个文件是否真的是PNG 图片格式。本文带你从 0 理解什么是文件魔数、PNG 签名长什么样,以及 is-png-cj 如何用极简代码实现可靠的图片格式识别(核心文件仅 26 行),适合刚接触文件处理的新手阅读。
一句话先记住:魔数 = 写在文件开头、用来"自报家门"的一串固定字节。
为什么不能只看文件后缀?🤔
我们平时靠.png、.jpg这类后缀区分图片,但后缀其实是"可以被随意改掉的标签":
- 把一个可执行文件改名为
可爱猫咪.png,只校验后缀的程序照样会放行; - 上传接口若只看后缀,就可能被"伪装成图片"的文件绕过;
- 文件被截断或重命名后,后缀与真实内容可能完全对不上。
真正可靠的判断方式是打开文件,看它开头的数据。这正是魔数登场的地方。
| 对比项 | 看文件后缀 | 看文件头部魔数 |
|---|---|---|
| 能否被重命名欺骗 | ❌ 能 | ✅ 不能 |
| 需要读取多少数据 | 0 字节(只看文件名) | 前几个字节 |
| 判断依据 | 命名习惯 | 格式规范定义的固定签名 |
什么是魔数:文件自带的"身份证"
魔数(Magic Number),也叫文件签名(File Signature / Magic Bytes),是每种文件格式在规范中约定的、出现在文件起始位置的固定字节序列。它就像身份证号——文件一"出生"就带着,改名字也改不掉。
先睹为快,几个常见格式的魔数(十六进制表示):
| 格式 | 魔数(十六进制) | 可读部分 |
|---|---|---|
| PNG | 89 50 4E 47 0D 0A 1A 0A | ..PNG\r\n.. |
| JPEG | FF D8 FF | — |
| GIF | 47 49 46 | GIF |
| ZIP / JAR | 50 4B 03 04 | PK.. |
💡 彩蛋:ZIP 魔数里的
50 4B正好是英文 "PK"——压缩软件创始人 Phil Katz 名字的首字母。
PNG 的 8 字节签名长什么样?
PNG 规范在文件最开始定义了 8 字节的签名,也就是 is-png-cj 用来比对的那份魔数模板,定义在 src/is_png.cj:
| 字节位置 | 十六进制 | 对应 ASCII |
|---|---|---|
| 1 | 89 | (非打印字符) |
| 2 | 50 | P |
| 3 | 4E | N |
| 4 | 47 | G |
| 5 | 0D | 回车符\r |
| 6 | 0A | 换行符\n |
| 7 | 1A | (非打印字符) |
| 8 | 0A | 换行符\n |
注意中间 3 个字节50 4E 47拼出来正好是"PNG"三个字母;签名里又特意混入回车换行(0D 0A 1A 0A),是为了在早期的 ASCII 文本传输场景中降低文件被破坏的概率。这种"可读 + 不可读"混合的签名设计,正是魔数的经典套路。
从源码看懂 is-png-cj 的识别原理 🔍
is-png-cj 的核心思路朴素到可以一眼看穿:只读文件开头 8 个字节,与魔数逐位比对,全部一致才是 PNG。
1. 用"规则模板"描述魔数
在 src/util.cj 中,库定义了Rule规则类型和Pattern(规则数组):
ByteMatch(b):这一位必须是指定字节b;AnyByte:这一位任意字节都可以。
这样设计的好处是模板天然可扩展——将来遇到"某字节位允许变化"的格式时,直接把对应位改成AnyByte即可。当前 PNG 魔数 8 位全部采用ByteMatch精确匹配。
2. 逐字节比对,只读 8 个字节
真正的比对逻辑封装在startsWith函数里,提供两个版本,分别对应两种输入:
- 输入流版本 src/util.cj:从
InputStream中按需读取头部字节; - 字节序列版本 src/util.cj:直接遍历
Iterable<Byte>。
它从第一个字节开始逐个与模板比对:一旦发现不一致立即返回false,8 位全部通过才返回true。这意味着:
- ✅ 最多只读 8 个字节,哪怕是一个几百 MB 的大图,判断成本几乎为零;
- ✅ 空文件、被截断的文件(不足 8 字节)都会被正确判为"不是 PNG"。
3. 两个开箱即用的接口
对外只暴露一个函数isPng,通过参数类型自动区分两种用法,见 src/is_png.cj:
isPng(inputStream: InputStream)—— 判断文件流;isPng(iter: Iterable<Byte>)—— 判断内存中的字节序列(比如从网络请求拿到的二进制数据)。
3 步上手:判断一个文件是不是 PNG 🚀
第 1 步:添加依赖。在 cjpm.toml 的[dependencies]中添加is_png_cj依赖即可(当前版本 0.2.0,完整写法参考 README.md 的"引入依赖"一节,版本演进见 CHANGELOG.md)。
第 2 步:调用isPng。官方示例 examples/is_png/src/main.cj 演示了最简用法,核心就几行:
import is_png import std.io.* import std.fs.* main() { let f = File("test.png", Open(true, false)) println(if (isPng(f)) { "是 PNG 格式" } else { "不是 PNG 格式" }) f.close() }第 3 步:看结果。只要test.png开头的 8 个字节与 PNG 签名一致,程序就会输出:
是 PNG 格式一行判断,替代了"看后缀、用软件打开"的土办法。
单元测试:100% 覆盖是怎么做到的
is-png-cj 的测试覆盖率是 100%。测试文件 src/is_png_test.cj 把正例和反例都钉死了:
| 测试场景 | 输入 | 期望结果 |
|---|---|---|
| 合法 PNG 签名 | 完整 8 字节魔数 | true |
| 真实 PNG 图片 | 1×1 像素 PNG(Base64 解码) | true |
| 空输入 | 空数组 / 空流 | false |
| 签名被截断 | 只有前 4 字节89 50 4E 47 | false |
| 末字节被篡改 | 第 8 字节从0A改成00 | false |
哪怕只有1 个字节不对,is-png-cj 也会果断返回false——这就是魔数识别"要么全对,要么不认"的严谨性。
魔数在开发中的实际应用场景 🌍
理解了原理,你会发现魔数判断在工程中无处不在:
- 上传接口安全校验:先验魔数再落盘,防止伪装成图片的恶意文件混入服务器;
- 未知文件"验明正身":拿到一个没有后缀的文件,读前几个字节对照签名表,就能认出它的真实格式;
- 文件恢复:文件丢失后缀后,扫描数据块开头的魔数,可以把散落的图片"捡"回来。
is-png-cj 正是这一类工具的最小可用实现:只聚焦 PNG 一种格式,把"读 8 字节 + 比对"这件事做到极致简单。
常见问题 FAQ
Q1:魔数判断为true,文件就一定是一张完好的 PNG 图片吗?不完全是。魔数只能证明"文件声称自己是 PNG",即头部签名合法;后续数据块是否完整、能否正常解码,还要交给图像解码器验证。但作为"第一步过滤",它已经排除了绝大多数非 PNG 文件。
Q2:用流判断会不会把整个文件读完?不会。isPng最多只读取开头的 8 个字节就返回。但注意它确实会推进流的读取位置,若之后还要继续读原文件,需要先处理流位置——接口注释里也专门提醒了这一点。
Q3:能不能用它判断 JPEG、GIF?目前 is-png-cj 只内置了 PNG 魔数模板。不过 src/util.cj 里的Pattern+startsWith是一套通用的"魔数匹配引擎",理论上换一份签名模板就能扩展到其他格式。
写在最后
回到开头的问题:文件魔数,就是写在文件最前面、用来"自报家门"的固定字节签名。
is-png-cj 用极简的公开接口展示了这套机制的完整闭环:定义 8 字节模板 → 逐字节比对 → 输出true/false,并配上 100% 覆盖的单元测试。下次再遇到"改后缀就能骗过程序"的场景,记得先看看文件开头的几个字节——那才是它真正的"身份证"。
| 关键文件 | 作用 |
|---|---|
| src/is_png.cj | 核心入口:PNG 魔数模板与isPng接口 |
| src/util.cj | 通用魔数匹配引擎(规则、模板、比对) |
| src/is_png_test.cj | 单元测试(正例/反例全覆盖) |
| examples/is_png/src/main.cj | 可运行的功能示例 |
| README.md | 项目介绍与完整使用说明 |
【免费下载链接】is-png-cj一个判断图片格式的库,根据图片的文件数据,判断图片是否为png格式项目地址: https://gitcode.com/Cangjie-TPC/is-png-cj
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考