[轉]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#的源代碼和一個測試用例
代碼寫的略亂而且注釋很少 請配合上述參考資料一同閱讀


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM