解析 PNG 与 ZIP:从二进制里抠出结构化数据
本节目标
- 理解"块(chunk)/ 段(entry)"式的二进制布局
- 手写解析 PNG 的 IHDR 与 ZIP 的中央目录
- 体会"按偏移读字段"的通用解析套路
PNG 结构:8 字节签名 + 若干 chunk,每个 chunk = 4 字节长度 + 4 字节类型 + 数据 + 4 字节 CRC。IHDR 是第一个 chunk,存宽高/色深。
// 运行环境:Node.js 14+
// 保存为 aj-l7.js,执行:node aj-l7.js(需有一个真实 png 文件,或见函数逻辑)
const fs = require('fs')
function parsePNG(buf) {
// 跳过 8 字节签名
let off = 8
const chunks = []
while (off < buf.length) {
const len = buf.readUInt32BE(off); off += 4
const type = buf.toString('ascii', off, off + 4); off += 4
const data = buf.slice(off, off + len); off += len
off += 4 // 跳过 CRC
chunks.push({ type, data })
if (type === 'IHDR') {
return {
width: data.readUInt32BE(0),
height: data.readUInt32BE(4),
bitDepth: data[8],
colorType: data[9]
}
}
}
return { chunks }
}
function parseZIP(buf) {
// 中央目录结尾记录 EOCD 签名 0x06054b50,从末尾向前找
let off = buf.length - 22
while (off >= 0) {
if (buf.readUInt32LE(off) === 0x06054b50) break
off--
}
const total = buf.readUInt16LE(off + 10)
let p = off + 46 // 跳到文件名列表起点(简化:假设无注释/扩展)
const names = []
for (let i = 0; i < total; i++) {
const nlen = buf.readUInt16LE(p + 28)
names.push(buf.toString('ascii', p + 46, p + 46 + nlen))
const clen = buf.readUInt32LE(p + 20)
const elen = buf.readUInt16LE(p + 30)
p += 46 + nlen + elen + clen // 粗略前进到下一 central directory header
}
return names
}
// 调用示例(需真实文件):
// const png = parsePNG(fs.readFileSync('a.png')); console.log(png)
// const zips = parseZIP(fs.readFileSync('a.zip')); console.log(zips)
console.log('parsePNG/parseZIP 已就绪,传入 Buffer 即可')名词解释
- 二进制块(Chunk/Entry):很多二进制格式把数据切成"头部带长度+类型、后面跟数据"的小块,逐个遍历即可。PNG 的 chunk、ZIP 的 entry 都是这种思想。
- 大端/小端(BE/LE):多字节整数在内存里的字节顺序。PNG 用大端(
readUInt32BE),ZIP 用小端(readUInt32LE);读错端序数值全乱。 - CRC(循环冗余校验):附在 chunk 后的校验值,接收方重算比对即可发现数据损坏。
课后练习
- PNG 为什么每个 chunk 都带长度字段?
- 答案:让解析器能"跳过不关心/不认识的 chunk"前进到下一块,实现向前兼容(旧程序读新格式不会卡死)。
- ZIP 的 EOCD 为什么从文件末尾向前找?
- 答案:中央目录在文件尾部,EOCD 标记其结束;从末尾反向扫描固定签名 0x06054b50 可快速定位,再向前读目录。
总结
解析 PNG/ZIP 让你第一次"用程序的眼睛看二进制"。你会发现,再复杂的文件格式,拆开无非是"签名 + 一连串带长度的类型化块 + 校验"。PNG 的 chunk 带长度,所以你能优雅地跳过不认识的块——这体现了格式设计的前向兼容智慧;ZIP 把目录放在末尾并用 EOCD 收尾,则展示了"索引后置"的检索巧思。我特别想强调端序(大端/小端)这个隐形杀手:readUInt32BE 与 readUInt32LE 一字之差,读出来的可能是天壤之别的数。手写这两个解析器,你会真正理解"文件格式"不是魔法,而是一份你和工具都遵守的"字节约定"。这种能力在逆向、数据恢复、自定义存储格式时价值连城。