前言
最近在看python自带的zipfile的时候发现有一个函数判断文件是否为zip格式时,采用解析获取文件的魔数并以此来判断文件类型。感觉甚为神奇。
小白的学习之旅开启,下面就幻数、文件类型的幻数以及有什么应用做分享~
Magic Number是什么?
简单来说就是看似毫无意义的数值,却能在程序中完成某种功能,例如:
// 位元反序
// magic number - 0x00082082、0x01122408、255
// input: 0123456
// output: 6543210
// unsigned char 的高两位无意义
unsigned char revert(unsigned char i){
return ((i * 0x00082082) & 0x01122408) % 255;
}
(对幻数有了解,不想看作者瞎比比的可以直接跳到最后看看位反序的算法思路,很有趣哦~~PS||:如果你觉得幻数在算法中的应用很有趣,想了解其他数学思想的幻数实现方式可以阅读知乎的这篇文章进行探索《magic number的由来和推导》)
在编程中有以下三类使用(From Wiki:)
- 无法解释其意义的唯一值或者在程序中多次被引用但又可被命名常数所替代的值。
- 用来识别文本类型的一个常量数值或字符。
- 不易被误解为其他意义的特有值,如全局唯一标识符。
文件类型的Magic number
虽然从刚才的内容中可以看到magic number有三种用法,但在这里我仅对magic number在识别文件类型的应用——即format indicators格式指示器做学习分享。
首先我们要知道文件类型是操作系统为存储信息而使用的对信息的特殊编码,用于识别内部存储的资料。即每一类信息,可以以一种或多种文件格式保存在电脑存储中。在操作系统中,幻数是用于标识文件格式的常量;通过用常量来区分运行时文件类型。接下来了解一下常见的文件幻数。如果对magic number在文件格式应用起源很感兴趣的同学可以去看看wiki。
常见文件头幻数
JPEG (jpg),文件头:FFD8FF
PNG (png),文件头:89504E47
GIF (gif),文件头:47494638
TIFF (tif),文件头:49492A00
Windows Bitmap (bmp),文件头:424D
CAD (dwg),文件头:41433130
Adobe Photoshop (psd),文件头:38425053
Rich Text Format (rtf),文件头:7B5C727466
XML (xml),文件头:3C3F786D6C
HTML (html),文件头:68746D6C3E
Email [thorough only] (eml),文件头:44656C69766572792D646174653A
Outlook Express (dbx),文件头:CFAD12FEC5FD746F
Outlook (pst),文件头:2142444E
MS Word/Excel (xls.or.doc),文件头:D0CF11E0
MS Access (mdb),文件头:5374616E64617264204A
WordPerfect (wpd),文件头:FF575043
Adobe Acrobat (pdf),文件头:255044462D312E
Quicken (qdf),文件头:AC9EBD8F
Windows Password (pwl),文件头:E3828596
ZIP Archive (zip),文件头:504B0304
RAR Archive (rar),文件头:52617221
Wave (wav),文件头:57415645
AVI (avi),文件头:41564920
Real Audio (ram),文件头:2E7261FD
Real Media (rm),文件头:2E524D46
MPEG (mpg),文件头:000001BA
MPEG (mpg),文件头:000001B3
Quicktime (mov),文件头:6D6F6F76
Windows Media (asf),文件头:3026B2758E66CF11
MIDI (mid),文件头:4D546864
幻数在zipfile.py中的应用
python自带的zipfile.py中用幻数来判断文件是否为zip包。但是在讲解代码前,需要再科普一下zip文件格式。
zip文件格式
这里简要的对zip文件格式进行一个简介(zip详细格式内容参照官方文档)。zip文书格式由文件数据区、中央目录结构、中央目录结束节(文件尾)组成。其中中央目录结束节中有一个字段保存了中央目录开始处的偏移。
文件数据区 |
中央目录结构 |
中央目录结束记录 |
中央目录结束记录End of central directory record(EOCD)
- 中央目录结束记录:被用于标识中央目录结束,可以通过读取它来找到中央目录并解析整个文件结构。结构EndLocator的目录结束标记固定值为0x06054b50。
- 结构包含:
signature 目标结束标记:0x06054b50 4bytes elDiskNumber 当前磁盘编号 2bytes elStartDiskNumber 中央目录开始位置的磁盘编号 2bytes elEntriesOnDisk 该磁盘上所记录的核心目录数量 2bytes elEntriesInDirectory 中央目录结构总数 2bytes elDirectorySize 中央目录的大小 4bytes elStartDicOffetInDisk 相对启动磁盘编号的中央目录启动偏移量 4bytes elCommentLen 注释长度 2bytes elComment 注释内容 大小不定
中央目录结构Central directory
-
中央目录结构:位于文件数据区后,用于保存所有文件的路径信息和对应文件数据结构区在文件中的偏移。其中中央文件头标识固定为0x02014b50。
- 结构包括:
signature 中央目录文件header标识:0x02014b50 deVersionMadeBy 压缩所用的pkware版本 deVersionMadeBy 压缩所用的pkware版本 deVersionToExtract 解压所需pkware的最低版本 deFlags 通用位标记 deCompression 压缩方法 deFileTime 文件最后修改时间 deFileDate 文件最后修改日期 deCrc CRC-32校验码 deCompressedSize 压缩后的大小 deUncompressedSize 未压缩的大小 deFileNameLength 文件名长度 deExtraFieldLength 扩展域长度 deFileCommentLength 文件注释长度 deDiskNumberStart 文件开始位置的磁盘编号 deInternalAttributes 内部文件属性 deExternalAttributes 外部文件属性 deHeaderOffset 本地文件头的相对位移 deFileName 目录文件名 deExtraField 扩展域 deFileComment 文件注释内容 - 中央目录结构区由中央目录结构的数组组成(包含目录文件名、扩展域、不定长度的文件注释内容)。
- 可以通过计算中央目录结构偏移量来遍历所有的文件(next_offset 下个中央目录结构偏移,current_offset当前中央目录结构偏移):next_offset = current_offset + sizeof(DirEntry) - 3sizeof(char) + deFileNameLength + deExtraFieldLength + deFileCommentLength;
- CRC32校验码与对应文件的数据压缩数据的CRC32校验码可做比对,若不同则数据损坏。<>
文件数据区file data
- 文件数据区:保存所有压缩文件数据的区,位于文件头,由压缩数据结构的数组构成。其中文件头标识固定为0x04034b50
- 结构如下:
signature 文件头标识:0x04034b50 frVersion 解压文件所需 pkware最低版本 frFlags 通用比特标志位(置比特0位=加密) frCompression 压缩方式 frFileTime 文件最后修改时间 frFileDate 文件最后修改日期 frCrc CRC-32校验码 frCompressedSize 压缩后的大小 frUncompressedSize 未压缩的大小 frFileNameLength 文件名长度 frExtraFieldLength 扩展区长度 frFileName 文件名 frExtraField 扩展区 frData 压缩数据
其中frCompression的取值有0~12,代表的含义各不相同。有兴趣可以wiki看看。
判断是否为zip的py代码
以下是截取的zipfile.py文件中的部分代码:
is_zipfile 判断是否为zip文件
def is_zipfile(filename):
result = False
try:
if hasattr(filename, "read"): #解读:判断文件是否有"read"属性
result = _check_zipfile(fp=filename) #解读:通过_check_zipfile判断文件是否为zip
else:
with open(filename, "rb") as fp: #解读:若文件没有“read"属性,则以二进制格式打开文件
result = _check_zipfile(fp) #解读:通过_check_zipfile判断文件是否为zip
except OSError:
pass
return result
zipfile.py中以is_zipfile方法快速判断文件是否为zip格式。首先判断文件是否有属性“read”,若有则通过_check_zipfile快速判断文件是否为zip;若文件无“read”属性,则以二进制格式打开该文件(文件指针位于文件的开头)再通过_check_zipfile判断文件是否为zip。
那么神奇的_check_zipfile是怎样的呢?
_check_zipfile 检查zip文件格式幻数是否为正确
def _check_zipfile(fp):
try:
if _EndRecData(fp): #解读:若文件有正确的文件格式幻数,则返回真
return True
except OSError:
pass
return False
_check_zipfile通过_EndRecDate方法读取文件的中央目录结束记录,以此检查zip文件格式。至此,追溯到读取文件标志数据的方法_EndRecData,那我们继续往下分析。
_EndRecData 读取中央目录结束记录
def _EndRecData(fpin):
#解读:确定文件大小
fpin.seek(0, 2) #解读:移动文件读取指针到文件末尾
filesize = fpin.tell() #解读:返回文件指针当前的位置,即返回文件大小
#解读:检查是否为无压缩注释内容的zip文件,若无压缩注释内容则按照以下方式处理(此时,中央目录结束记录是文件的最后一个数据项)
try:
fpin.seek(-sizeEndCentDir, 2) #解读:移动件读取指针到中央目录结束记录前,sizeEndCentDir为22位
except OSError:
return None
data = fpin.read() #解读:读取中央目录结束记录EOCD传给data
if (len(data) == sizeEndCentDir and
data[0:4] == stringEndArchive and
data[-2:] == b"\000\000"):
# 解读:如果中央目录结束记录没有注释内容且目录结束标记signature准确,则对data进行解包
# 判断条件:
# 1. 读取的data的大小与zip文件格式既定的中央目录结束记录大小一致(这里的size大小计算在下一个小节进行讲解)
# 2. data前4位是signature,是一个固定值。signature等于stringEndArchive(定义的值为b"PK\x05\x06")
# 3. 由于没有注释内容,故注释长度为0,注释长度有2位,故data最后两位为b"\000\000"
endrec = struct.unpack(structEndArchive, data) #解读:以strucEndArchive模式串进行解包(模式串在下一个小节进行讲解),解包返回一个元组
endrec=list(endrec) #解读: 将解包出来的元组数据转换为列表
#解读:对列表附加一个空白的注释和开始偏移量
endrec.append(b"")
endrec.append(filesize - sizeEndCentDir) #解读: 由于是空白注释,故开始偏移量为文件大小-中央目录文件结束记录(22bytes)大小
#解读: 读取"Zip64 end of central directory" 结构(这个结构在本文没有进行介绍,可阅读zip官方文档进行了解)
return _EndRecData64(fpin, -sizeEndCentDir, endrec)
#解读: 若这是一个带压缩注释内容的zip文件或非zip文件,则需要搜索文件末尾以获取中央目录结束记录的标识(此时注释内容是文件的最后一个数据项,并且长度最长为64k)。假定中央目录结束记录的幻数不会出现在注释内容中。
maxCommentStart = max(filesize - (1 << 16) - sizeEndCentDir, 0) #解读:计算最长注释内容的文件指针位置,文件大小-2^16-中央目录文件结束记录大小(22bytes)
fpin.seek(maxCommentStart, 0) #解读:移动文件读取指针到中央目录结束记录前
data = fpin.read()
start = data.rfind(stringEndArchive) #解读:返回stringEndArchive(b"PK\005\006")最后出现的位置,若无匹配则返回-1
if start >= 0:
#解读:读取中央目录结束记录
recData = data[start:start+sizeEndCentDir]
if len(recData) != sizeEndCentDir:
# 解读: 若读取的中央目录结束记录大小与zip文件定义的大小不符,则该文件有损坏,返回none
return None
endrec = list(struct.unpack(structEndArchive, recData)) #解读::以strucEndArchive模式串进行解包,并将元组结果转为列表
commentSize = endrec[_ECD_COMMENT_SIZE] #解读: zip官方文件声明的注释内容大小,为7
comment = data[start+sizeEndCentDir:start+sizeEndCentDir+commentSize] #解读:读取注释内容
endrec.append(comment) #解读:对列表添加注释内容和开始偏移量
endrec.append(maxCommentStart + start)
#解读: 读取"Zip64 end of central directory"
return _EndRecData64(fpin, maxCommentStart + start - filesize,
endrec)
# 无法找到有效的中央目录结束记录,返回none
return None
此函数返回文件的中央目录结束记录值或返回none。
已知的是end of central directory record值存储在zip的“End of central dir”结构(这个结构是个列表)中的第九项,相对启动磁盘编号的中央目录启动偏移量保存在列表的第十项。最终的值由方法_EndRecData64对Zip64 end of central directory record进行读取并返回中央目录结束值。
_EndRecData64在此不进行具体阐述,有兴趣的同学可以阅读zipfile.py中的def _EndRecData64(fpin, offset, endrec)及zip官方文档中的Zip64 end of central directory record进行学习。
中央目录结束记录的幻数
structEndArchive = b"<4s4H2LH"
stringEndArchive = b"PK\005\006"
sizeEndCentDir = struct.calcsize(structEndArchive)
- structEndArchive 的值b"<4s4H2LH" 是二进制数,代表的意思是:
- “<” - 小端模式
- “4s” - 长度为4的字符串
- “4H” - 长度为4的无符号短整型
- “2L” - 长度为2的无符号长整型
- “H” - 长度为1的无符号整型
- b"<4s4H2LH"正好是中央目录结束记录ECOD的小端结构
(备注:struct支持的格式可见附录。对大端小端有兴趣的同学请wiki。)
- stringEndArchive是中央目录结束记录的前四位值,在zip官方文档中定义的是 b’PK\x05\x06’。
- sizeEndCentDir是中央目录结束记录的大小,zip文件官方定义的是22bytes。
- struct.calcsize 用于计算给定的格式b"<4s4H2LH"占用多少字节内存。
(备注:中央目录结束记录ECOD可见附录。)
补充:文章中引用的位反序的分析
unsigned char revert(unsigned char i) {
return ((i * 0x00082082) & 0x01122408) % 255;
}
位反序的算法通过三个运算就完成了反转。这里记录下其的分析过程(假设输入为“abcdef”)。
i = abcdef
abcdef
* 0x00082082 10000010000010000010
------------------------------------
abcdef
abcdef
abcdef
abcdef
------------------------------------
abcdefabcdefabcdefabcdef0
这里的乘项0x00082082将6位位元向左扩展了3次,并在右侧补一个0。
abcdefabcdefabcdefabcdef0
and 0x01122408 1000100100010010000001000
------------------------------------------
a000e00b000f00c000000d000
^ ^ ^ ^ ^ ^
| | | | | |
25 21 18 14 11 4
上一步运算的结果abcdefabcdefabcdefabcdef0和 0x01122408 进行与运算后,剩下在 4 / 11 / 14 / 18 / 21 / 25 位分别保持输入 6 位二进制的各个位。
a000e00b000f00c000000d000 的 256 进制表示为 axyz
其中 x = e00b0 y = f00c00 z = d000
因 a + x + y + z < 255
则 axyz % 255 = a + x + y + z
e00b0
+ f00c00
+ d000
+ a
---------------------------------------------
fedcba
最后可以看到位元反序算法完成~
附录
struct支持的格式
Format | C Type | Python | 字节数 |
---|---|---|---|
x | pad byte | no value | 1 |
c | char | string of length 1 | 1 |
b | signed char | integer | 1 |
B | unsigned char | integer | 1 |
? | _Bool | bool | 1 |
h | short | integer | 2 |
H | unsigned short | integer | 2 |
i | int | integer | 4 |
I | unsigned int | integer or long | 4 |
l | long | integer | 4 |
L | unsigned long | long | 4 |
q | long long | long | 8 |
Q | unsigned long long | long | 8 |
f | float | float | 4 |
d | double | float | 8 |
s | char[] | string | 1 |
p | char[] | string | 1 |
P | void * | long |
注:
- q和Q只在机器支持64位操作时有意思
- 每个格式前可以有一个数字,表示个数
- s格式表示一定长度的字符串,p表示的是pascal字符串
- P用来转换一个指针,其长度和机器字长相关
- 最后一个可以用来表示指针类型的,占4个字节
End of central directory record
数据 | 字节数 |
---|---|
end of central dir signature | 4 bytes (0x06054b50) |
number of this disk | 2 bytes |
number of the disk with the start of the central directory | 2 bytes |
total number of entries in the central directory on this disk | 2 bytes |
total number of entries in the central directory | 2 bytes |
size of the central directory | 4 bytes |
offset of start of central directory with respect to the starting disk number | 4 bytes |
.ZIP file comment length | 2 bytes |
.ZIP file comment | (variable size) |