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文件结构详解的更多相关文章

  1. PE文件结构详解(六)重定位

    前面两篇 PE文件结构详解(四)PE导入表 和 PE文件结构详解(五)延迟导入表 介绍了PE文件中比较常用的两种导入方式,不知道大家有没有注意到,在调用导入函数时系统生成的代码是像下面这样的: 在这里 ...

  2. PE文件结构详解(五)延迟导入表

    PE文件结构详解(四)PE导入表讲 了一般的PE导入表,这次我们来看一下另外一种导入表:延迟导入(Delay Import).看名字就知道,这种导入机制导入其他DLL的时机比较“迟”,为什么要迟呢?因 ...

  3. PE文件结构详解(四)PE导入表

    PE文件结构详解(二)可执行文件头的最后展示了一个数组,PE文件结构详解(三)PE导出表中解释了其中第一项的格式,本篇文章来揭示这个数组中的第二项:IMAGE_DIRECTORY_ENTRY_IMPO ...

  4. PE文件结构详解(三)PE导出表

    上篇文章 PE文件结构详解(二)可执行文件头 的结尾出现了一个大数组,这个数组中的每一项都是一个特定的结构,通过函数获取数组中的项可以用RtlImageDirectoryEntryToData函数,D ...

  5. PE文件结构详解(二)可执行文件头

    在PE文件结构详解(一)基本概念里,解释了一些PE文件的一些基本概念,从这篇开始,将详细讲解PE文件中的重要结构. 了解一个文件的格式,最应该首先了解的就是这个文件的文件头的含义,因为几乎所有的文件格 ...

  6. Java类文件结构详解

    概述: Class文件结构是了解虚拟机的重要基础之一,如果想深入的了解虚拟机,Class文件结构是不能不了解的.Class文件是一组以8位字节为基础单位的二进制流,各项数据项目严格按照顺序紧凑地排列在 ...

  7. Java Class 字节码文件结构详解

    Class字节码中有两种数据类型: 字节数据直接量:这是基本的数据类型.共细分为u1.u2.u4.u8四种,分别代表连续的1个字节.2个字节.4个字节.8个字节组成的整体数据. 表:表是由多个基本数据 ...

  8. maven pom文件结构详解

    POM文件结构 Project Object Model是Maven2项目的基础所在,简单来说它就是一个XML文件,Maven2用它来描述一个工程的整个生命周期所需要执行的一系列功能和特性. 最小配置 ...

  9. PE文件结构详解(一)基本概念

    PE(Portable Execute) 文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等,事实上,一个文件是否是PE文件与其扩展名无关,PE文件可以是任 何扩展名.那 ...

随机推荐

  1. 最近帮客户实施的基于SQL Server AlwaysOn跨机房切换项目

    最近帮客户实施的基于SQL Server AlwaysOn跨机房切换项目 最近一个来自重庆的客户找到走起君,客户的业务是做移动互联网支付,是微信支付收单渠道合作伙伴,数据库里存储的是支付流水和交易流水 ...

  2. 自定义基于 VLC 的视频播放器

    前言(蛋疼的背景故事) 前段时间,接了一个小项目,有个需求是要在系统待机一段时间以后,循环播放 MV(类似于 Windows 系统的屏幕保护). 听到这个需求,我首先想到的是 MediaPlayer ...

  3. 在 ML2 中配置 OVS flat network - 每天5分钟玩转 OpenStack(133)

    前面讨论了 OVS local network,今天开始学习 flat network. flat network 是不带 tag 的网络,宿主机的物理网卡通过网桥与 flat network 连接, ...

  4. git克隆项目到本地&&全局安装依赖项目&&安装依赖包&&启动服务

     一.安装本地开发环境 1.安装本项目 在需要保存到本地的项目的文件夹,进入到文件夹里点击右键,bash here,出现下图: 2.安装依赖项目  3.安装依赖包(进入到命令行) # 安装依赖包 $ ...

  5. .Net Core上用于代替System.Drawing的类库

    目前.Net Core上没有System.Drawing这个类库,想要在.Net Core上处理图片得另辟蹊径. 微软给出了将来取代System.Drawing的方案,偏向于使用一个单独的服务端进行各 ...

  6. 小兔Java教程 - 三分钟学会Java文件上传

    今天群里正好有人问起了Java文件上传的事情,本来这是Java里面的知识点,而我目前最主要的精力还是放在了JS的部分.不过反正也不麻烦,我就专门开一贴来聊聊Java文件上传的基本实现方法吧. 话不多说 ...

  7. Unable to create the selected property page. An error occurred while automatically activating bundle net.sourceforge.pmd

    解决方案: 在命令行到eclipse目录下使用 eclipse.exe -clean

  8. 图解DevExpress RichEditControl富文本的使用,附源码及官方API

    9点半了,刚写到1.2.   该回家了,明天继续写完. 大家还需要什么操作,留言说一下,没有的我明天继续加. 好久没有玩DevExpress了,今天下载了一个玩玩,发现竟然更新到14.2.5了..我去 ...

  9. 【Update】C# 批量插入数据 SqlBulkCopy

    SqlBulkCopy的原理就是通过在客户端把数据都缓存在table中,然后利用SqlBulkCopy一次性把table中的数据插入到数据库中. SqlConnection sqlConn = new ...

  10. Intelli IDEA 设置项目编码(Mac)

    Intelli IDEA->Editor->File Encodings