[译] MoPaQ归档格式

MoPaQ归档格式

© 2006-2010 Justin Olbrantz (Quantam) and Jean-Francois Roy (BahamutZERO). All Rights Reserved.

在不修改本文的前提下对本文的散发和复制没有任何限制;但希望能够提供本文的链接以使每个人都能够看到最新的版本。在其他地方允许自由引用本文的内容,但请附上来源以及原作者。

内容

MoPaQ格式简介

MoPaQ(或MPQ)格式是暴雪娱乐的Mike O'Brien(取名来自Mike O'brien PaCK)所设计的一种档案文件。该格式已被用于了从暗黑破坏神开始到目前为止暴雪所有的游戏。它是一种高度优化了的只读游戏归档格式,并在此方面表现得很出色。

暴雪的MoPaQ读取函数已经被包含在了各种静态的或者动态链接的Storm模块中,而通常暴雪的MoPaQ写入函数仅仅被包含于总是被静态链接的MPQAPI模块中;不过在星际争霸2里,Storm模块也已经包含了MoPaQ写入函数。

StormLib - 由Ladislav Zezula(非暴雪娱乐的员工)编写的一个开源的MoPaQ读写库,在该领域已经研究了很长时间了。由于有段时间没有更新了,它还不支持所有新的MoPaQ特性。但是它包含了MoPaQ所使用的压缩算法的实现代码,诸如PKWare Implode算法、MoPaQ霍夫曼压缩算法、以及MoPaQ使用的IMA ADPCM压缩。

MoPaQ格式

MoPaQ格式中所有的数字都采用小端存储;带符号数采用补码表示形式。数据类型被表示为int(整数,后跟位数)、byte(8位)或者char(存有ASCII字符的字节)。所有的大小和偏移若无特殊说明则都以字节数为单位。结构体成员会以以下形式列出:

相对于结构体开始的偏移: 数据类型(数组大小) 成员名称 : 成员描述

 

一般归档布局

  • 档案头部
  • 文件数据
  • 文件数据 - 特殊文件
  • 散列表
  • 块表
  • 扩展块表
  • 强数字签名

这只是一般性的档案布局,但它并不是绝对的。已经发现有些档案的散列表和块表被放在档案头部之后、文件数据之前。

但是从星际争霸2开始,要求块表必须紧跟在散列表之后。

档案头部

00h: char(4) Magic             标志着该文件是一个MoPaQ档案。必须为ASCII "MPQ" 1Ah。
04h: int32 HeaderSize          档案头部的大小。
08h: int32 ArchiveSize         整个档案的大小,包含其头部,且不包含强数字签名。
                               该大小是用来确定数字签名进行散列计算的区域的。
                               该字段在燃烧远征MoPaQ格式中已被废弃, 
                               而是用从档案的开头到散列表、块表或者扩展块表的末尾(取值最大的那个)来计算。
0Ch: int16 FormatVersion       MoPaQ格式的版本号。如果该值为负MPQAPI会打开失败。已知的版本有:
	0000h                  原始版本。HeaderSize应为20h,且不支持大尺寸档案。
	0001h                  燃烧远征格式。HeaderSize应为2Ch,支持大尺寸档案。
0Eh: int8 SectorSizeShift      该档案中每个逻辑扇区所占用的磁盘扇区(512字节)数,为2的幂指数形式。
                               档案中每个逻辑扇区的大小等于512 * 2^SectorSizeShift. 
                               由于Storm库的Bug,该值只能是3(4096字节的扇区).
10h: int32 HashTableOffset     散列表起始位置的偏移,相对于档案的开始处。
14h: int32 BlockTableOffset    块表起始位置的偏移,相对于档案的开始处。
18h: int32 HashTableEntries    散列表的入口数。必须是2的整数幂,且在原始MoPaQ格式中小于2^16,在燃烧远征格式中小于2^20。
1Ch: int32 BlockTableEntries   块表的入口数。

从燃烧远征起新增的字段:

20h: int64 ExtendedBlockTableOffset   扩展块表起始位置的偏移,相对于档案的开始处。
28h: int16 HashTableOffsetHigh        大尺寸档案的散列表偏移值的高16位。
2Ah: int16 BlockTableOffsetHigh       大尺寸档案的块表偏移值的高16位。

档案头部是档案中的第一个结构体,位于档案的偏移0位置处;不过档案本身不一定非要从其所属文件的0偏移处开始。档案相对于所属文件本身的偏移量被存在ArchiveOffset里。如果档案不是从文件开头处开始的话,则必须从一个磁盘的扇区边界处(512字节)起始。早期版本的Storm要求档案必须位于所属文件的尾部(ArchiveOffset + ArchiveSize = 文件大小),不过在较新的版本中该要求已经不复存在(也是由于强数字签名并不被认为是档案的一部分)。

块表

块表包含了档案中每个区域的入口信息。区域可以是单一文件、空闲空间或者不被使用的块表入口。空闲空间可以用来覆盖写入新文件(通常该空间是由删除的文件数据产生的)。空闲空间入口的BlockOffset和BlockSize不能为0,而同时FileSize与Flags必须为0; 块表是被加密了的,可以用"(block table)"的散列值作为密钥解密。每个入口的结构如下:

00h: int32 BlockOffset   块开始位置的偏移,相对于档案的起始位置。
04h: int32 BlockSize     块在档案中占用空间的大小。
08h: int32 FileSize      块中所存储文件数据的大小。仅当块是一个文件的时候才有效,否则无意义,应置0。
                         如果文件是被压缩了的,则该值为解压后的文件大小。
0Ch: int32 Flags         该块的标志掩码。已确定有以下的值:
	80000000h        该块是一个文件,且其他各标志位表示文件数据的格式;否则该块是空闲空间或者未被使用。
                         如果该块不是一个文件,则其他所有标志必须清位,FileSize应为0。
	04000000h	 文件的每个扇区都带有校验码(将在文件数据一节中解释)。如果文件既未被压缩又未被Implode则忽略该标志。
        02000000h        文件是一个删除标记,表示该文件已不再存在。
                         这使得补丁档案可以删除查找链上优先级较低的档案中的文件。
	01000000h        文件被作为一个整体保存,而不是分成若干扇区。
	00020000h        该文件的密钥是经过块偏移和文件大小校正的(将在文件数据一节中详细介绍)。
                         文件必须被加密。
	00010000h        文件被加密。
	00000200h        文件被压缩,不可Implode。
	00000100h        文件被Implode. 不可压缩。

扩展块表

扩展块表是为了让档案能够支持大于4GB(2^32字节)而新增的。该表储存了块表中每个块的档内偏移的高位。它仅仅是一个简单的int16数组,存储着每个块的档内偏移的第32-47位,第48-63将固定被认为是0。档案中每个单独的块仍然有4GB大小的限制。该表仅存于燃烧远征中超过4GB的档案里。

不同于散列表和块表,扩展块表既没有被加密也不带压缩。

散列表

为了能够高速访问MoPaQ,不使用文件名而是采用一个大小为2的整数幂的散列表来索引。一个文件根据其文件路径、语言和平台将得到唯一的标识。

文件在散列表中的起始入口(home entry)为其路径的散列值。当发生冲突时(起始入口已经被另一个文件所使用)采用顺序溢出(progressive overflow)方式,文件将被放在下一个可用的散列表入口里。搜索文件时需从起始入口开始遍历散列表,直到文件被找到,或者已到达散列表的末尾,亦或者遇到一个空的散列表入口(FileBlockIndex为FFFFFFFFh)。

散列表总是加密了的,其密钥为"(hash table)"的散列值。

在星际争霸2之前,散列表是没有被压缩的。但在星际争霸2里该表可能会被压缩。如果块表的偏移不等于散列表的偏移加上其未压缩大小,星际争霸2会认为散列表是被压缩了的(不是被Implode)。该计算方法假设了块表是紧跟在散列表之后的,否则可能会失败或者宕掉。

每个入口都有如下的结构:

00h: int32 FilePathHashA    使用A方法计算的文件路径的散列值。
04h: int32 FilePathHashB    使用B方法计算的文件路径的散列值。
08h: int16 Language         文件的语言。这是一个Windows LANGID数据类型,并使用与其相同的值定义。
                            0表示默认语言(美国英语),或者该文件语言无关。
0Ah: int8 Platform          该文件所用于的平台。0表示默认平台。没有检测到其他值的出现。
0Ch: int32 FileBlockIndex   如果散列表入口有效,则为文件对应的块表的索引。
                            否则为如下的两个值:
	FFFFFFFFh           散列表入口为空,并且总是为空,中止对指定文件的搜索。
	FFFFFFFEh           散列表入口为空,但是在某个时候是有效的(换而言之,该文件被删除了),
                            不中止对指定文件的搜索。

扩展属性

扩展属性可以为块表中的文件提供可选的文件属性。这些属性是在MoPaQ已经完全固化之后加入的,且并非所有的档案都需要有全部的(也可以完全没有)扩展属性。如果一个档案含有某项属性,那么该档案的块表中的每一个块都将含有一个属性实体, 尽管对于非文件的块这并没有意义。块属性的次序和块表中的块的次序是对应的,且个数也一致。这些属性以一维数组的形式保存在档案的"(attributes)"文件中(使用默认语言和平台)。同时此文件对应的属性不需要有效(逻辑上也不能有效)。与MoPaQ格式中所有其他结构不同,扩展属性中的入口可以不必对齐。同时需要注意在某些档案中发现故意设置的全零属性,大概是为了干扰破解而做的吧。该文件具有以下结构:

00h: int32 Version :           指定扩展属性的格式版本。目前只能是100。
04h: int32 AttributesPresent : 档案中的扩展属性标识位掩码:
	00000001h: 文件的CRC32。
	00000002h: 文件时间戳。
	00000004h: 文件的MD5。
08h: int32(BlockTableEntries) CRC32s :   档案中每个块的文件数据(未压缩的)的CRC32值。
                                         如果档案不含CRC32则忽略。
FILETIME(BlockTableEntries) Timestamps : 档案中每个块的时间戳。其格式为Windows FILETIME结构体。
                                         如果档案不含时间戳则忽略。
MD5(BlockTableEntries) MD5s :            档案中每个块的文件数据(未压缩的)的MD5值。
                                         如果档案不含MD5则忽略。

文件数据

每个文件的数据都有以下结构组成:

int32(SectorsInFile* + 1) SectorOffsetTable  每个扇区相对于文件数据开始处的偏移。
                                             最后一个入口包含的是整个文件压缩后的大小,
                                             使得仅仅使用简单的减法运算就能轻易算出任何扇区的大小。
                                             如果文件未被压缩则不存在也不需要该表。
SECTOR Sectors(SectorsInFile)                文件中每个扇区的数据,一个挨一个地放置(具体见下面的描述)。

通常地,文件数据被切割成若干个扇区,以便于简单的流式访问。除了最后一个以外的所有扇区,每个的大小应该与档案头部的SectorSizeShift字段所指定的大小相等;而最后一个扇区则可能比它小,其大小依赖于整个文件的尺寸。但如果文件是被压缩的或者Implode的,扇区会比起实际包含的文件内容要小。当且仅当扇区内容无法被压缩算法压缩时(压缩后的扇区大小大于等于原文件数据大小),保存在压缩或Implode文件中的扇区数据是未经压缩的;此时SectorOffsetTable中该扇区的大小等于扇区中文件数据的大小(可以从FileSize计算得到)。

扇区如果带校验码,校验码会保存在文件末尾的一个附加扇区里,因此这样的文件的扇区表中会有一个附加入口;但是与文件中其他的扇区不同,该扇区总是被压缩(不是被Implode)且不带加密。该扇区由校验码的数组组成,对应于文件的每个扇区,但不包含校验码扇区本身。每个校验码都是从一个压缩的且未加密的的扇区计算得来的;想知道扇区校验的实现细节请参见下面的VerifySectorChecksum函数。

每个扇区的格式取决于其类型。无压缩扇区只是简单地存放了原始的文件数据。Implode扇区则存放了原始压缩数据经过Implode算法压缩后的结果(这些扇区仅存于Implode文件)。压缩扇区(仅限于压缩且非Implode的文件)则被一种或者多种压缩算法所压缩,并带有以下结构:

byte CompressionMask : 该扇区使用的压缩类型的掩码。
byte(SectorSize - 1) SectorData : 该扇区的压缩数据。

CompressionMask指出了该压缩扇区采用的是何种压缩算法。该字节也被计算在整个扇区的大小之内,并且如果该扇区的数据被压缩后的大小不足两个字节的话,数据将以未压缩的形式存放;换句话说,也就是说压缩后的结果至少要有一个字节的大小。另外,该字节也会和扇区数据一起被加密。有如下可用的算法被定义(参见StormLib获取具体的算法实现):

20h: Sparse压缩。星际争霸2新增。
40h: 单声道IMA ADPCM
80h: 立体声IMA ADPCM
01h: 霍夫曼编码
02h: Deflate压缩(参见ZLib)。魔兽争霸3新增。
08h: Implode(参见PKWare Data Compression Library)
10h: BZip2压缩(参见BZip2)。魔兽世界:燃烧远征新增。

在星际争霸2之前,该字节为其所使用算法的标志位掩码的组合。可以组合的算法在压缩时会按照上述列表的顺序执行压缩,而在解压时则按照其逆序执行。

而在星际争霸2中,该字节已经被合起来看成是一个大的查找表的索引,根据其对应的值被解释为一个或者两个不同的压缩算法的组合。算法也不再是按位的组合,而必须是某个有效列表指出的组合;尤其是一些压缩类型(比如Deflate和Implode)现在是互斥的,并且必须和另一个数值较小的算法相组合(例如IMA ADPCM),而这些小数值算法之间又是互斥的。这里有一个例外(12h),它并不是两种算法的组合,而是一种单一的新算法(LZMA)。这里是各种有效组合的列表:

12h: LZMA压缩。星际争霸2新增。
22h: Sparse压缩 + Deflate
30h: Sparse压缩 + BZip2
41h: 单声道IMA ADPCM + 霍夫曼编码
48h: 单声道IMA ADPCM + Implode
81h: 立体声IMA ADPCM + 霍夫曼编码
88h: 立体声IMA ADPCM + Implode

如果文件被作为一个整体保存(在其文件标志中指出),则只有一个扇区并保存了整个文件的数据。

如果文件被加密,则每个扇区(压缩/Implode之后)都使用该文件的密钥进行加密。文件的基础密钥(base key)由其路径的文件名部分的散列值(不包含目录部分,例如"directory/file"的密钥是"file"的散列值)来决定的。如果文件标志中指出密钥是经过校正的,则最终的密钥是由((基础密钥 + BlockOffset) XOR FileSize)计算得出的。每个扇区都使用(密钥 + 扇区在文件中从0开始算起的索引)进行加密。SectorOffsetTable使用(密钥 - 1)加密(如果SectorOffsetTable存在的话)。

当文件中所有扇区的大小和偏移都可以从FileSize计算得到时SectorOffsetTable会被省略掉。在一些场合有可能会出现这种情况,如果文件没有被压缩或Implode,那么基于档案的SectorSizeShift,所有扇区的大小和偏移都可以算得出。如果文件被作为一个整体的压缩或Implode单元保存,那么如前所述,该文件唯一的“扇区”大小等同于BlockSize和FileSize,因此SectorOffsetTable被省略。但是,只要文件是被压缩或Implode且文件不被作为一个整体看待的话,即使文件中只含有一个扇区(文件大小小于或等于档案扇区的大小),SectorOffsetTable也会存在。

列表文件

列表文件是一种对MoPaQ格式非常简单的扩充,它包含了该档案的(大多数)文件的路径。列表文件中并不包含文件的语言和平台信息。列表文件包含在一种非常简单的文本文件"(listfile)" 中(使用默认的语言和平台),它是由以';'、0Dh、0Ah或者它们的组合作为分隔符所分隔的文件路径的列表组成。文件"(listfile)"本身可以不在列表文件中。

弱数字签名

弱数字签名为符合RSASSA-PKCS1-v1_5的数字签名,使用MD5散列算法和一个512位(弱的)RSA密钥(请参见RSA实验室PKCS1规范获取该协议的详细信息)。公钥和指数码作为资源被保存在Storm中,而私钥被保存在别的文件里,其文件名被传递给MPQAPI(私钥本身并没有保存在MPQAPI中)。签名保存在档案的"(signature)"文件中(默认语言和平台),且没有被压缩或加密。档案被从档案开始(容器文件的ArchiveOffset)到档案结束(长度取决于ArchiveSize,或者在扩展MoPaQ格式中计算得到的大小)进行散列计算;该数字签名文件在档案被署名之前加入,且档案署名时该签名文件所占据的空间都被当作二进制0来看待。该文件具有以下结构:

00h: int32 Unknown : 必须为0。
04h: int32 Unknown : 必须为0。
08h: int512 Signature : 数字签名。如MoPaQ格式中其他数一样为小端形式。

该数字签名的构造遵循RSASSA-PKCS1-v1_5规范,处理该格式非常之枯燥(该签名的校验程序编写主要是应用MD5函数和大数函数,这很无趣),所以最好交给专门的加密库,比如如下的OpenSSL去处理:

int mpq_verify_weak_signature(RSA *public_key, const unsigned char *signature, const unsigned char *digest) {
    unsigned char reversed_signature[MPQ_WEAK_SIGNATURE_SIZE];
    memcpy(reversed_signature, signature + 8, MPQ_WEAK_SIGNATURE_SIZE);
    memrev(reversed_signature, MPQ_WEAK_SIGNATURE_SIZE);
    
    return RSA_verify(NID_md5, digest, MD5_DIGEST_LENGTH, reversed_signature, MPQ_WEAK_SIGNATURE_SIZE, public_key);
}

MPQKit包含了弱签名的RSA公钥,其格式为PEM的r73版本。

强数字签名 - 概要

强数字签名由SHA-1摘要和一些极其简单的填充组成,直接使用RSA加密。所有已知的暴雪的密钥都是2048位(强)RSA密钥;一个默认的密钥保存在Storm中。显然可以使用任何的RSA密钥,实际上,目前从未发现有人用到过默认密钥。强数字签名在容器文件里紧跟着档案的末尾存储。整个档案(从容器文件的ArchiveOffset位置开始,共ArchiveSize个字节)被作为一个整块计算散列值(该算法有一个例外,后面将会描述)。此外,一个签名在最终成型之前会在SHA-1摘要后跟一段附加数据,它的内容可以是任意的。该签名具有以下格式:

00h: char(4) Magic : 表示这里是一个数字签名。必须为"NGIS"("SIGN"的回文)。
04h: int2048 Signature : 数字签名,以小端格式存储。

当签名域被一个公钥加密时,它的大整数结果被保存为小端字序,其结构如下;

00h: byte Padding : 必须为0Bh。
01h: byte(235) Padding : 必须为BBh。
ECh: byte(20) SHA-1 : 档案的SHA-1摘要,符合标准SHA-1字序。

强数字签名 - 细节

本章主要是希望能够给出一个对暴雪公钥的使用,以及SHA-1摘要末尾的附加数据的细节描述。

魔兽争霸3地图

魔兽争霸3地图(.w3m与.w3x)由一个地图头部、一个头部后位于512偏移处的MPQ档案、以及其后的一个强数字签名所组成。魔兽争霸3地图密钥被用于魔兽争霸3地图的签名;SHA-1摘要由从地图头部起直到档案结束处的整个文件数据(即从容器文件的0偏移开始,共MapHeaderSize + ArchiveSize个字节)生成而来。地图数据计算了摘要之后,SHA-1摘要后将带上附加数据,其内容为地图文件名的全大写字符串。

Mac版魔兽世界补丁

Mac版魔兽世界补丁是由一个补丁程序("patcher")来合并的,该补丁程序内含有一堆standalone.MPQ档案资源,其中存放有补丁的内容。standalone.MPQ档案被Mac版魔兽世界补丁密钥署名;SHA-1摘要采用前面所述的一般场合的方法生成。摘要计算好之后,SHA-1摘要后将带上附加数据,其内容为"ARCHIVE"。

需要注意Windows版魔兽世界补丁采用的是弱数字签名方案。

魔兽世界调查报告

魔兽世界的WDB目录下有一个Survey.MPQ档案。Survey.MPQ档案被魔兽世界调查报告密钥署名,其SHA-1摘要采用前面所述的一般场合的方法生成。摘要计算好之后,SHA-1摘要后将带上附加数据,其内容为"ARCHIVE"。

需要注意在燃烧远征的发行版里并没有发现Survey.MPQ档案的存在。

自定义数据

首次用于燃烧远征的第二版MoPaQ格式, 它是一个能让部分数据存放在档案之外的机制,但目前还不明白该机制的设计目的。它是由位于档案之前的一种叫做分流块的东西实现的。这种块的结构如下:

00h: char(4) Magic                  表示这是一个分流块。为ASCII "MPQ" 1Bh。
04h: int32 UserDataSize             该档案中为自定义数据分配的字节数。
                                    它不需要为数据解压后的大小,只需要等于该档案所存储的数据部分的最大值即可。
08h: int32 ArchiveHeaderOffset      继续搜索档案头部的文件内起始偏移。
0Ch: byte(UserDataSize) UserData    存有自定义数据的数据块。

当Storm在搜索档案头部时遇到了该块时,它将记住该分流块的位置,并且从该分流块中指定的偏移位置起继续搜索档案头部。

暴雪自制的档案将分流块放在了文件的开始处,且档案自身从分流块结束后下一个512字节的边界处开始。

自定义数据文件

某些基于未知的特征的档案可以包含一个分流快。在这种情况下,自定义数据被保存在一个档案中的一个名为"(user data)"的普通文件中。

算法源代码

这里给出的所有源代码都假设机器是采用小端字序的,且short类型为16位,long类型为32位,long long类型为64位。如果所给平台不符合这些假设的话则必须调整这部分代码。除了我在写该文档时所写的以外的代码都没有经过验证。

加密/解密

以下代码基于StormLib。

unsigned long dwCryptTable[0x500];

// 加密与散列函数中用到了一个数字查找表。该表必须在首次调用这些函数之前进行初始化。
void InitializeCryptTable()
{
    unsigned long seed   = 0x00100001;
    unsigned long index1 = 0;
    unsigned long index2 = 0;
    int   i;

    for (index1 = 0; index1 < 0x100; index1++)
    {
        for (index2 = index1, i = 0; i < 5; i++, index2 += 0x100)
        {
            unsigned long temp1, temp2;

            seed  = (seed * 125 + 3) % 0x2AAAAB;
            temp1 = (seed & 0xFFFF) << 0x10;

            seed  = (seed * 125 + 3) % 0x2AAAAB;
            temp2 = (seed & 0xFFFF);

            dwCryptTable[index2] = (temp1 | temp2);
        }
    }
}

void EncryptData(void *lpbyBuffer, unsigned long dwLength, unsigned long dwKey)
{
    assert(lpbyBuffer);

    unsigned long *lpdwBuffer = (unsigned long *)lpbyBuffer;
    unsigned long seed = 0xEEEEEEEE;
    unsigned long ch;

    dwLength /= sizeof(unsigned long);

    while(dwLength-- > 0)
    {
        seed += dwCryptTable[0x400 + (dwKey & 0xFF)];
        ch = *lpdwBuffer ^ (dwKey + seed);

        dwKey = ((~dwKey << 0x15) + 0x11111111) | (dwKey >> 0x0B);
        seed = *lpdwBuffer + seed + (seed << 5) + 3;

		*lpdwBuffer++ = ch;
    }
}

void DecryptData(void *lpbyBuffer, unsigned long dwLength, unsigned long dwKey)
{
    assert(lpbyBuffer);

    unsigned long *lpdwBuffer = (unsigned long *)lpbyBuffer;
    unsigned long seed = 0xEEEEEEEEL;
    unsigned long ch;

    dwLength /= sizeof(unsigned long);

    while(dwLength-- > 0)
    {
        seed += dwCryptTable[0x400 + (dwKey & 0xFF)];
        ch = *lpdwBuffer ^ (dwKey + seed);

        dwKey = ((~dwKey << 0x15) + 0x11111111L) | (dwKey >> 0x0B);
        seed = ch + seed + (seed << 5) + 3;

		*lpdwBuffer++ = ch;
    }
}

散列与文件密钥的计算

这些函数似乎是从某个很古老的版本的StormLib继承而来的。已经过去太久以至于我已经忘了是哪个版本了。

// 所生成的散列值类型
#define MPQ_HASH_TABLE_OFFSET	0
#define MPQ_HASH_NAME_A	1
#define MPQ_HASH_NAME_B	2
#define MPQ_HASH_FILE_KEY	3

// 该代码基于StormLib
unsigned long HashString(const char *lpszString, unsigned long dwHashType)
{
    assert(lpszString);
    assert(dwHashType <= MPQ_HASH_FILE_KEY);
    
    unsigned long  seed1 = 0x7FED7FEDL;
    unsigned long  seed2 = 0xEEEEEEEEL;
    int    ch;

    while (*lpszString != 0)
    {
        ch = toupper(*lpszString++);

        seed1 = dwCryptTable[(dwHashType * 0x100) + ch] ^ (seed1 + seed2);
        seed2 = ch + seed1 + seed2 + (seed2 << 5) + 3;
    }
    return seed1;
}

#define BLOCK_OFFSET_ADJUSTED_KEY 0x00020000L

unsigned long ComputeFileKey(const char *lpszFilePath, const BlockTableEntry &blockEntry, unsigned long nArchiveOffset)
{
	assert(lpszFilePath);

	// 从路径中找出文件名部分
	const char *lpszFileName = strrchr(lpszFilePath, '//');
	if (lpszFileName)
		lpszFileName++;	// 跳过/
	else
		lpszFileName = lpszFilePath;

	// 计算文件名的散列值,得到基础密钥
	unsigned long nFileKey = HashString(lpszFileName, MPQ_HASH_FILE_KEY);

	// 必要时用偏移调整密钥
	if (blockEntry.Flags & BLOCK_OFFSET_ADJUSTED_KEY)
		nFileKey = (nFileKey + blockEntry.BlockOffset) ^ blockEntry.FileSize;

	return nFileKey;
}

FILETIME与time_t的转换

该段代码假设基准时间(零)为01/01/1970。在Windows、Unix System V systems与Mac OS X上也确实如此。但不清楚在其它平台上是否也成立。如果你打算移植到别的平台的话可能就需要自己去研究了。

该段代码未经测试,可能有错误

#define EPOCH_OFFSET 116444736000000000ULL	// 01/01/1601到01/01/1970的时间差,单位100纳秒

bool GetTimeFromFileTime(const FILETIME &fileTime, time_t &time)
{
	// FILETIME作为一个64位整数:从1601年1月1日起的时间,单位为100纳秒
	unsigned long long nTime = ((unsigned long long)fileTime.dwHighDateTime << 32) + fileTime.dwLowDateTime;

	if (nTime < EPOCH_OFFSET)
		return false;

	nTime -= EPOCH_OFFSET;	// 将基准时间从01/01/1601转换到01/01/1970
	nTime /= 10000000ULL;	// 转换100纳秒到秒

	time = (time_t)nTime;

	// 溢出检查(FILETIME为64位,而time_t为32位)
	if ((nTime - (unsigned long long)time) > 0)
		return false;

	return true;
}

void GetFileTimeFromTime(const time_t &time, FILETIME &fileTime)
{
	unsigned long long nTime = (unsigned long long)time;

	nTime *= 10000000ULL;
	nTime += EPOCH_OFFSET;

	fileTime.dwLowDateTime = (DWORD)nTime;
	fileTime.dwHighDateTime = (DWORD)(nTime >> 32);
}

FILETIME与NSDate的转换

MPQKit的[NSDate ]上有一个专栏介绍了NSDate与NTFS FILETIME之间的相互转换,并且经测试无误。

从32位与16位值构建64位大档案相对地址

unsigned long long MakeLargeArchiveOffset(unsigned long nOffsetLow, unsigned short nOffsetHigh)
{
	return ((unsigned long long)nOffsetHigh << 32) + (unsigned long long)nOffsetLow;
}

扇区校验

bool VerifySectorChecksum(const void *buffer, unsigned int length, unsigned long checksum)
{
	if (checksum == 0)
		return true;	// 忽略实际校验

	unsigned long bufferChecksum = adler32(0, buffer, length);
	if (bufferChecksum == 0)
		// 无法处理校验码0
		bufferChecksum = (unsigned long)-1;

	return (bufferChecksum == checksum);
}

查找文件

#define MPQ_HASH_ENTRY_EMPTY 0xFFFFFFFFL
#define MPQ_HASH_ENTRY_DELETED 0xFFFFFFFEL

bool FindFileInHashTable(const HashTableEntry *lpHashTable, unsigned long nHashTableSize, const char *lpszFilePath, unsigned short nLang, unsigned char nPlatform, unsigned long &iFileHashEntry)
{
	assert(lpHashTable);
	assert(nHashTableSize);
	assert(lpszFilePath);
	
	// 在散列表中查找文件的起始入口
	unsigned long iInitEntry = HashString(lpszFilePath, MPQ_HASH_TABLE_OFFSET) & (nHashTableSize - 1);
		
	// 是否已经到末尾了?
	if (lpHashTable[iInitEntry].FileBlockIndex == MPQ_HASH_ENTRY_EMPTY)
		return false;

	// 计算散列值,用以比较散列表入口冲突
	unsigned long nNameHashA = HashString(lpszFilePath, MPQ_HASH_NAME_A),
		nNameHashB = HashString(lpszFilePath, MPQ_HASH_NAME_B),
		iCurEntry = iInitEntry;

	// 检查散列表中的每一个入口直到遇到中止条件
	do
	{
		if (lpHashTable[iCurEntry].FileBlockIndex != MPQ_HASH_ENTRY_DELETED)
		{
			if (lpHashTable[iCurEntry].FilePathHashA == nNameHashA 
				&& lpHashTable[iCurEntry].FilePathHashB == nNameHashB
				&& lpHashTable[iCurEntry].Language == nLang
				&& lpHashTable[iCurEntry].Platform == nPlatform)
			{
				iFileHashEntry = iCurEntry;
				
				return true;
			}
		}

		iCurEntry = (iCurEntry + 1) & (nHashTableSize - 1);
	} while (iCurEntry != iInitEntry && lpHashTable[iCurEntry].FileBlockIndex != MPQ_HASH_ENTRY_EMPTY);

	return false;
}

删除

bool DeleteFile(HashTableEntry *lpHashTable, unsigned long nHashTableSize, BlockTableEntry *lpBlockTable, const char *lpszFilePath, unsigned short nLang, unsigned char nPlatform)
{
	assert(lpHashTable);
	assert(nHashTableSize);
	assert(lpBlockTable);

	// 在散列表中查找文件
	unsigned long iFileHashEntry;

	if (!FindFileInHashTable(lpHashTable, nHashTableSize, lpszFilePath, nLang, nPlatform, iFileHashEntry))
		return false;

	// 在删除散列表入口之前,先取到块表索引
	unsigned long iFileBlockEntry = lpHashTable[iFileHashEntry].FileBlockIndex;

	// 删除文件在散列表中对应的入口
	memset(&lpHashTable[iFileHashEntry], 0xFF, sizeof(HashTableEntry));

	// 如果后面的入口为空,把该项标志为空;否则标志为已删除。
	if (lpHashTable[(iFileHashEntry + 1) & (nHashTableSize - 1)].FileBlockIndex == MPQ_HASH_ENTRY_EMPTY)
		lpHashTable[iFileHashEntry].FileBlockIndex = MPQ_HASH_ENTRY_EMPTY;
	else
		lpHashTable[iFileHashEntry].FileBlockIndex = MPQ_HASH_ENTRY_DELETED;
	
	// 如果该块占用了实际空间,标志该块为空闲空间;否则清除块表入口
	if (lpBlockTable[iFileBlockEntry].BlockSize > 0)
	{
		lpBlockTable[iFileBlockEntry].FileSize = 0;
		lpBlockTable[iFileBlockEntry].Flags = 0;
	}
	else
		memset(&lpBlockTable[iFileBlockEntry], 0, sizeof(BlockTableEntry);

	return true;
}

常量

区域编码

    MPQNeutral      = 0,
    MPQChinese      = 0x404,
    MPQCzech        = 0x405,
    MPQGerman       = 0x407,
    MPQEnglish      = 0x409,
    MPQSpanish      = 0x40a,
    MPQFrench       = 0x40c,
    MPQItalian      = 0x410,
    MPQJapanese     = 0x411,
    MPQKorean       = 0x412,
    MPQDutch        = 0x413,
    MPQPolish       = 0x415,
    MPQPortuguese   = 0x416,
    MPQRusssian     = 0x419,
    MPQEnglishUK    = 0x809

 

原文地址:
http://wiki.devklog.net/index.php?title=The_MoPaQ_Archive_Format

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值