首页 > 代码库 > [转]Patch文件结构详解

[转]Patch文件结构详解

N久不来 于是不知道扔在哪儿
于是放这里先 
如果你觉得碍事的话 帮我扔到合适的版块去..

导读
这是一篇说明文 它介绍了标准冒险岛更新文件(*.patch;*.exe)的格式
文章的最后附了一段C#的参考代码 你可以自由的下载 编译 或改写为其他语言
文章不附加任何有风险的可执行文件(*.exe) 对此没有兴趣的可以直接后退浏览其他帖子


目录
0 前言
1 文件结构
  1.1 patch文件结构
    1.1.1 文件头
    1.1.2 zlib段
  1.2 exe文件结构 
    1.2.1 exe段
    1.2.2 patch段
    1.2.3 notice段
    1.2.4 文件尾部
2 编程实现
  2.1 exe文件的分离
  2.2 校验和的算法
  2.3 patch文件预处理
  2.4 patch数据段的结构
    2.4.1 新增/替换文件指令
    2.4.2 重构文件指令
    2.4.3 删除文件指令
  2.5 patch结束
3 扩展
4 感谢

0 前言

我们经常要更新冒险岛
一部分是很自然的与官方版本同步 以正常登录游戏
一部分是可以随时关注外服的最新更新信息

但是更新冒险岛有一个很麻烦的问题
首先 我们需要有至少大于一个客户端大小的硬盘剩余空间
而在这里的很多人 硬盘里都有3个以上的客户端 多则10个...

我们眼中可以看到的更新过程?
我们下载了官方提供的exe补丁或者patch补丁 
如果下载了后者还要使用一些工具 把它转换成可以执行的exe文件
双击它 会提示浏览文件夹 选中自己的冒险岛客户端所在文件夹
正确的选中以后 补丁程序会在自身文件夹下创建一个log文件 并且在冒险岛文件夹下创建一个随机的文件夹以存放临时文件
更新结束以后 补丁程序把临时文件剪切回原来的客户端 
如果发生意外 程序则在没有任何提示的情况下中止 
你需要自行关闭程序 并且自行销毁临时文件

很容易推测 这是补丁程序在“重构”客户端文件 并且“简单替换”回原文件的过程
当补丁过程意外中止时 这不会对原客户端造成任何影响

为什么我们要研究patch文件结构?
简单的说 因为每次对比都要留3份客户端大小(旧版客户端+新版客户端+临时文件预留空间)的硬盘我很头痛
所以 了解patch文件的结构 有利于我们对这个过程的控制 优化 
当然 这对于普通的冒险岛玩家毫无意义

它很复杂么?
打补丁的过程出奇的简单 甚至我用三句话就可以描述了:
1) patch文件是用zlib压缩 使用crc32验证文件版本和完整性的
2) patch文件实际记录了修改过程 它包含三种操作:替换 重构 和删除文件
3) 重构文件是一个基于字节的过程 它包含三种操作:复制新区段 填充定长段 复制原区段

结果呢?
以后我打补丁只需要原客户端+1G左右的额外空间就行了(其他盘符 甚至是内存中都可以)
顺带我还能输出一份基于img内部节点粒度的wz对比报告来

就这样?
啊 还不够么...
难道你还要我能生成降版本的补丁么...(其实好像真的可以...)


1 文件结构

1.1 patch文件结构

完整的patch文件是由16字节文件头以及不定字节的zlib压缩段组成

1.1.1 文件头 (0x0000-0x000F)

前8个字节(0x0000-0x0007)是固定的 "WzPatch\x1A" 为文件标识符
继续4个字节(0x0008-0x000B)是固定的 02 00 00 00 大概为文件版本 值为2
----------------------------------------
|W|z|P|a|t|c|h|\x1A|\x02|\x00|\x00|\x00|
----------------------------------------
继续4个字节(0x000C-0x000F) 为zlib压缩段的校验和 在2.2节会给出校验和的实际算法

1.1.2 zlib段 (0x0010-eof)

这一段包含着patch文件的实际压缩数据 它由2字节的zlib头和剩余的压缩部分组成

前2个字节(0x0010-0x0011)是固定的 78 DE 详细含义请参考文档rfc1950
剩余的部分(0x0012-eof)为deflate压缩数据段 可以用标准的zlib算法进行解压缩
注意 patch文件没有包含压缩段的数据长度信息
压缩前的数据结构会在2.3详述

1.2 exe文件结构

标准的exe补丁结构是由exe段 patch段 notice段 文件尾部标识组成
基本上全世界各个服务器的exe补丁 第三方工具生成的补丁 都遵守了这个约定

1.2.1 exe段

这个区块实际上是一个标准的exe可执行文件 又称MZ文件 它是以字节 4D 5A 作为起始标识的
exe段有固定的字节流格式 但是没有解释的意义
它的具体长度对于各个服都不一样 不过执行的功能大同小异
在它和patch段之间会有一段字节填充区段 也可以看做exe段本身的一部分

1.2.2 patch段

这个区块实际上是一个完整的patch文件 它的格式已在1.1描述
它的长度在文件尾部标识有所体现 和exe段之间有一道明显的壕沟用于字节对齐(没有也无所谓)

1.2.3 notice段 

这个区块是一段ansi编码的文本 实际上记录了一些补丁的文字信息 
不过目前大多数补丁文件不显示这个区段了...
它的字节长度在文件尾部标识有所体现 和patch段直接相连 没有首部和尾部的标识

1.2.4 文件尾部标识 (fileLen-12)-eof

这个区块是exe文件中唯一定长的 可区分的一段
前4字节 一个int值 表示patch段的区块长度
中间4字节 一个int值 表示notice段的区块长度
后4字节 固定的 f3 fb f7 f2标识

---------------------------------------------
|-- -- -- --|-- -- -- --|\xf3|\xfb|\xf7|\xf2|
---------------------------------------------
   patchLen   noticeLen


2 编程实现

这个章节将会描述如何读取patch文件并对客户端进行更新的技术

2.1 exe文件的分离

在1.2节已经分析了exe补丁的结构 我们只要简单的解析尾部12字节 就可以从exe中提取出patch段和notice段了
基本步骤如下:
> 打开文件流
> 判断头双字节是否是"MZ"
> 移动读写指针到(-4,SeekEnd)
> 读取4字节 看看是否符合尾部标识
> 移动读写指针到(-12,SeekEnd)
> 连续读取两个int 作为patch段和notice段的长度
> 移动读写指针到(-noticeLen-12,SeekEnd)
> 读取noticeLen个字节 并且解析成ansi字符串 这记录着补丁的更新文字信息
> 移动读写指针到(-patchLen-noticeLen-12,SeekEnd)
> 读取patchLen个字节 作为patch段进行下一步处理

2.2 校验和的算法

patch文件中全部的校验和都使用crc32算法  多项式为0x04C11DB7
在程序中使用查表法就很OK~~
详细的算法实现见程序文件 CheckSum类实现了这个算法

2.3 patch文件预处理

当你输入了一个patch文件 或者从exe中提取出了patch区块
要进行如下步骤的预处理:
> 移动读写指针到(0,SeekBegin)
> 读取8个字节 判断是否是"WzPatch\x1A"
> 读取一个int 作为patch格式版本
> 读取一个uint 作为zlib段的checksum
> 对剩余的字节使用crc32进行hash 并和checksum对比 检查文件完整性
> 移动读写指针到(16,SeekBegin)
> 读取2字节 作为zlib头标识 并且判断压缩类型(也可以不判断)
> 对剩余的字节使用inflate解压缩算法 获得不定长度byte[] 作为patch数据段进行下一步处理
> 创建一个临时文件夹 用于存放更新后的客户端

顺带一提 补丁这玩意压缩率极低... 刚试了下用KMST452to454(65.9Mb)的补丁解压 真实数据段长度也才75.6Mb 通过zlib头还能看出来它使用的是3级压缩的...
果然这种压缩毫无意义啊-△-

另外临时文件夹的选择 一般和冒险文件夹在同一个盘符 在目前的文件系统中 这种操作会使补丁完成后的文件转移更迅速 否则 这会是一个很大规模的I/O操作 并且伴随着风险 两种选择取决于实际需要

2.4 patch数据段的结构

经过解压缩的数据段包含着补丁操作的控制信息 这一区块使用流式读写
基本结构如下:

{
    { fileName }{ patchType }[ { patchData } ]
}
[..n]

fileName: 不定长度的ascii编码字符串 表示要进行操作的文件名或文件夹相对路径
  例:"MapleStoryT.exe" "HShield\\" "HShield\\AhnUpCtl.dll"
  fileName没有‘\0‘结尾标识 需要与patchType一起读出来区分边界

patchType: 1字节的标识位 值域可能为00 01 02
  00:表示文件创建操作 这将创建并替换客户端同名文件
  01:表示文件重构操作 这将从客户端原始文件和patchData中读取段落 生成一个新的文件替换原文件
  02:表示文件删除操作 此时没有也不需要patchData
  patchType不仅标识了更新类型 还可以作为fileName的结束标识 实际读取fileName时可以逐字节读取 当下一字节小于等于02时终止

patchData:可选段 对于patchType=01时一定存在 对于patchType=00时可能存在(当fileName为文件夹时不存在) 对于patchType=02时不存在
  这个区段的结构也取决于对应的patchType 这将在下面章节详述

2.4.1 新增/替换文件指令

当patchType=00时 需要判断fileName是否为文件夹 
判断方式只要简单的判断是否含有后缀名 或者尾字节是否为‘\\‘
如果是文件夹 则在临时文件夹下面创建一个相同名称的文件夹 操作结束
如果是文件 patchData的格式如下:

0           4           8
------------------------------------
|-- -- -- --|-- -- -- --| …… …… 
------------------------------------
   fileLen    checksum     fileData

fileLen:4字节int 表示新文件的长度
checksum:4字节uint 表示新文件的crc校验和
fileData:fileLen长度 表示新文件的字节数据

处理过程如下:
> 依次读取4字节文件长度 以及4字节校验和
> 记录当前读写指针的位置pos
> 对后面(fileLen)字节使用crc32进行hash 并和checksum对比 检查文件完整性
> 移动读写指针回到pos
> 对后面(fileLen)字节转存到文件{fileName}

2.4.2 重构文件指令

当patchType==01时 则使用重构文件的操作 我们需要输入原冒险文件夹中的同名文件 和补丁数据块一同读取
此时patchData的格式如下:

{
    { oldChecksum }{ newChecksum }
    {{ commandBlock }[...n]}
    { commandEnd }
}

oldChecksum:4字节uint 表示原文件的checksum 
newChecksum:4字节uint 表示新文件的checksum
commandBlock:补丁操作命令区块 这个区块长度不定 总的来说有三种命令格式
{
    { commandHeader }[ otherData ]
}
commandHeader:4字节长度的操作命令的头部 包含了丰富的信息

1>
|4bit|  28bit  |  ...
-------------------------
|1000| length  | dataBlock

当高4位的值为0x08时 低28位则作为长度标识 这将进行如下操作:
从补丁中读取length长度字节数据 并写入到新文件中

2>
|4bit|  20bit  | 8bit |
-----------------------
|1100| length  | byte |
当高4位的值为0x0c时 中间20位作为长度标识 低8位作为一个byte的信息 进行如下操作:
向新文件中填充length长度的重复byte字节
此时这个区段不包含otherData

3>
|  32bit  |  32bit  |
---------------------
|  length |  offset |
如果高4位并非以上值 则它的格式为这样 header和otherData各占4字节 分别表示length和offset信息 它将进行如下操作:
从旧文件中offset字节开始 读取length长度数据 然后写入到新文件中

commandEnd:4字节 固定的00 00 00 00 标识patchData的结束

当执行完文件重构指令后 应当对新文件进行crc32检查完整性并再次比较 如果通过验证 则关闭文件流进行下个文件的更新

2.4.3 删除文件指令

它除了文件名没有包含任何额外的信息...当然大多数时候你很少处理它...或者简单处理它即可

当年国际服好像有这样一个故事...制作更新补丁的时候不小心打包进去一个mob1.wz 然后补丁中创建了这个多余的文件...理所当然的...下一个补丁把这个文件移除了
文件夹里出现多余文件的情况很常见 经常在韩服客户端里发现程序猿不小心遗留的服务端脚本...

2.5 patch结束

当你根据patch数据段对原客户端进行处理 生成新客户端临时文件后 只需要按照更新类型对文件剪切 即可以获得一份完整的更新后客户端来 其他的操作 如回收内存 删除临时文件夹等操作也应一并执行如果临时文件夹和冒险客户端文件夹在同一盘符上 这是一个很简单的操作 基本不会出现意外
如果文件覆盖过程中出错 则会破坏整个客户端完整性 这将造成很大的灾难...只能通过手动更新才能实现客户端恢复...否则只能重新下载完整客户端


3 扩展

对整个补丁文件结构和执行过程了解以后 就可以对冒险岛打补丁的过程进行控制和扩展

比较容易想象 为了节省临时文件空间 可以对每个临时文件生成后 直接覆盖原客户端文件
如果补丁过程因为异常中断 下次执行patch的时候可以进行文件hash对比 如果判定旧有文件的hash符合旧文件则执行更新 符合新文件则跳过 这样可以最大限度保证客户端完整

另外比较实用的一种更新模式 即生成临时文件后可以直接与原文件进行对比生成报告 

当了解了patch文件的结构后 你可以很容易预读补丁相关文件的大小 校验和等信息 也可以自由的改变补丁执行顺序

应该还会有其他的对于更新过程可以扩展的方式 暂不列举


4 感谢

拖了整整半个月才成文 代码的部分还是没有整理的太完美 不过还是尝试把自己的客户端更新了 问题不大
特别感谢在我一头雾水的时候发现的Fiel大神的文档...太美好了...
你可以在southperry上找到源代码 是用C编写的 关键字为"NXPatcher"

附件里包含着C#的源代码和一个测试用例
代码写的略乱而且注释很少 请配合上述参考资料一同阅读

[转]Patch文件结构详解