文件格式的幻数File Format and Magic Number

前言

  最近在看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:)

  1. 无法解释其意义的唯一值或者在程序中多次被引用但又可被命名常数所替代的值。
  2. 用来识别文本类型的一个常量数值或字符。
  3. 不易被误解为其他意义的特有值,如全局唯一标识符。

文件类型的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目标结束标记:0x06054b504bytes
    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文件最后修改日期
    deCrcCRC-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文件最后修改日期
    frCrcCRC-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支持的格式

FormatC TypePython字节数
xpad byteno value1
ccharstring of length 11
bsigned charinteger1
Bunsigned charinteger1
?_Boolbool1
hshortinteger2
Hunsigned shortinteger2
iintinteger4
Iunsigned intinteger or long4
llonginteger4
Lunsigned longlong4
qlong longlong8
Qunsigned long longlong8
ffloatfloat4
ddoublefloat8
schar[]string1
pchar[]string1
Pvoid *long

注:

  1. q和Q只在机器支持64位操作时有意思
  2. 每个格式前可以有一个数字,表示个数
  3. s格式表示一定长度的字符串,p表示的是pascal字符串
  4. P用来转换一个指针,其长度和机器字长相关
  5. 最后一个可以用来表示指针类型的,占4个字节

End of central directory record

数据字节数
end of central dir signature4 bytes (0x06054b50)
number of this disk2 bytes
number of the disk with the start of the central directory2 bytes
total number of entries in the central directory on this disk2 bytes
total number of entries in the central directory2 bytes
size of the central directory4 bytes
offset of start of central directory with respect to the starting disk number4 bytes
.ZIP file comment length2 bytes
.ZIP file comment(variable size)
  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值