這是7z文件格式及其源碼的分析系列的第四篇. 上一篇講到了7z文件靜態結構的尾header部分.這一篇開始,將從7z實際壓縮流程開始詳細介紹7z文件尾header的詳細結構.
一, 第一個概念: coder.
在7z的壓縮過程中, 一個非常核心的概念就是coder. 一個coder代表一個算法, 通常是指一個壓縮或解壓算法(也包括過濾算法和加密算法等). 例如, 在7z中lzma算法就是一個coder, deflate算法也是一個coder. 7z中用於加密的AES256算法也是一個coder.
所以概念上講, 能處理一個文件流的算法就是一個coder. 這個"處理"的概念可以是壓縮/解壓, 加密等等.

(圖1)
通常來講, 一個coder只能處理一個輸入流, 並且只有一個輸出流. 比如把一個文件流壓縮成一個輸出流. 但是, 7z中有的coder可以把一個輸入流處理成多個輸出流, 反過來也可以把多個流處理成一個流. 比如7z的 BCJ2 coder, 它是一個過濾coder, 可以把一個exe文件過濾成四個輸出流. 這樣的話, 7z的coder概念得到了擴展. 就是可能同時處理多個輸入流, 並且可能輸出多個流:

(圖2)
這里可以先簡單的體驗一下壓縮的過程了:
1. 簡單壓縮過程, 把文件流交給lzma coder壓縮.

(圖3)
2. 多coder串聯, 理論上可以串聯任何兩個coder, 而且串聯的級數也是沒有限制的, 可以串聯任意多級. 當然, 由於熵的存在, 串聯過個壓縮coder是沒有意義的.
這里示例的是最常用的一種方式,就是壓縮並且加密.

(圖4)
注意上圖中的o1, 和 i2. o1是第一個coder的輸出流, i2是第二個coder的輸入流. 在實際操作中, 這兩個流其實是同一個流, 直接把第一個的輸出當做第二個的輸入.
解壓的過程就是上面的逆過程.
上圖就是比較完整的一次壓縮過程了.
二, 第二個概念 Folder, 不是文件夾.
這里的Folder要特別注意, 它不是我們通常指的文件夾. 它也不是任何物理上存在的東西.
7z在開始壓縮之前, 會把文件分類, 大體上是按文件類型以及文件是否需要加密來分類的. 比如說, 把所有的exe文件分成一類(一個Folder), 或者把所有需要加密的文件分在一起. 等等. 具體分類方法以后再說. 這個分類方法並不重要, 7z的實現用的方法比較簡單. 實際上如果要實現7z的壓縮器的話, 這個分類方法你說了算. 你可以給每個文件划分成一個Folder.
我們看一個例子:

(圖5)
在這個例子中, 我們共有5個文件需要壓縮.
1. 首先, 通過一定的分組方法, 我們分成了兩個Folder, 第一個Folder包括: a.exe, b.exe 和 c.dll 三個文件. 第二個Folder包括:a.txt 和 b.txt.
2. 對Folder1來說, 它包含三個文件, Folder1就簡單的把三個文件串聯起來,當做一個大文件, 作為輸入流 i1 給Coder1 用. 后面的過程就就是上面的 圖4 的內容了.
在7z源碼的: \CPP\7zip\Archive\7z\ 這個目錄下,有 7zFolderInStream.h 和7zFolderInStream.cpp 專門處理把多個文件串聯偽裝成一個文件的任務. 從Coder1 的角度看, 它只知道有個文件流 i1, 並不知道這個i1 是一個真實的文件 還是由一個Folder偽裝的.
實際上, 7z概念上最小的壓縮單位不是文件, 而是Folder, 它會先把所有的文件都歸到一個相應的Folder中, 然后 讓這個Folder作為文件流, 流過若干個Coder. 我們再抽象一下上面的壓縮過程:

(圖 6)
上圖中的字母 'i' 表示輸入的意思, 'o' 表示輸出. 后面的數字表示序號. 簡單解釋一下, 這個Folder流最初是作為Coder1 的輸入流i1. Coder1 的輸出流是o1. 這個o1又作為 i2 輸入給Coder2用, 然后又是Coder3.
值得注意的是最后一個coder的輸出流 o3. 它就是壓縮的最終輸出結果了. 它在7z中叫做一個PackedStream. 就是打包的流. 我們叫做p1吧. 如果有多個Folder, 那每個Folder就會有一個或多個PackedStream. 所以所有文件壓縮之后就會有 pn. 這n個packedStream會被按順序存儲在 7z的文件主體, 就是上一篇文章中介紹的第二部分.
每個Folder 包含了哪些文件, 每個文件大小等等這些詳細信息都存貯在7z的尾文件頭中了. 在7zformat.txt中有這一段:
NumFolders
Folders[NumFolders]
{
NumCoders
CodersInfo[NumCoders]
{
ID
NumInStreams; //表示這個coder 所接受的輸入流的個數, 一般是1個
NumOutStreams; //表示這個coder的輸出流的個數, 一般是1個.
PropertiesSize //一個int值, 表示后面Properties的字節長度
Properties[PropertiesSize] // 字節數組, 表示這個coder的一些設置信息, 比如壓縮級別, 或者AES加密的IV等等.
}
NumBindPairs // 表示bindpair 的個數. bindpair表示輸入流和輸出流的綁定關系. 例如上面的圖6中, o1和i2是綁定的, o2和i3是綁定的.
BindPairsInfo[NumBindPairs] //bindpair的數組, 記錄每一個bindpair.
{
InIndex; //這個綁定的輸入index, 就是上圖中對應的 i后面的序號. (不好意思, 畫圖的時候沒注意,圖上下表是從1開始的,但是實際上,你懂的, 都是從0開始的.所有上面圖中的下標都要減一.)
OutIndex; //綁定對應的輸出index, 就是對應上圖中o后面的序號. 同上.
}
PackedIndices //這表示這個folder最終輸出的packstream在所有packstream中的序號.
}
UnPackSize[Folders][Folders.NumOutstreams] // 這是一個二位數組, 記錄每個Folder對應的輸出流的個數.
CRCs[NumFolders] //這是一個Crc的數組, 沒個folder 流的crc, 7z目前沒有使用這一個字段.
稍微解釋一下上面的結構:
1. NumFolders, 顯示一個int32值, 它記錄了7z文件中共有多少個Folder.
2. 后面那是folder數組, 一次排布每個Folder. 每個Folder結構如下:
3. NumCoders, int32值, 記錄了這個Folder總共進過了幾個coder.
4. 后面就是它的所有Coder的數組, 每個coder的結構: 顯示一個coder 的id. 就是coder的唯一標示符. 這個id的定義在: DOC/目錄下的 methods.txt.
5. 更詳細的信息, 請看上面代碼后面的注釋吧.
6. 我再強調一點,我畫圖的時候沒有注意,所以圖中的i和o后面的序號都是從1開始的, 實際上,你懂的, 每個存儲的序號都是從0開始的, 沒有例外. 如果你發現哪里的序號和我說的不一樣, 請檢查這個. 沒有例外, 所有的序號都是從0開始的. 包括以后我可能會畫的圖. 記住都是從0開始的.
7, 再有一點就是, 比如上圖中的folder經過了 coder1, coder2 和coder3 這三個coder. 實際才存儲這三個coder的時候, 是按逆序存儲的, 就是先存Coder3, 然后是coder2, 最后是coder1. 這是為了方便解壓.
上面的圖6就是一次比較完整的壓縮流程, 解壓的流程就是反過來, 先分別構建coder1, coder2 和coder3, 然后逆向流動就最終解壓了.
每個Folder都會經過一次完整的壓縮過程.
好了, 主要的壓縮過程和結構已經介紹完了. 下一篇將給大家介紹剩下的文件詳細信息的存儲方式, 以及最終的Header的生成方式.
最后還是歡迎大家訪問我的獨立博客: http://byNeil.com
寫這么多字, 畫圖都不容易, 幫頂一下吧, 小伙伴們.
