灵动游戏研究室

文档-资料-开源

Zip文件结构

1. Zip文件简介

Zip文件可以包含多个使用不同压缩方式甚至不压缩的文件。

Zip文件在文件尾部设置了目录数据,用来描述整个压缩文件包含的文件列表。这样做有好几个好处,首先可以在不遍历所有数据的情况下获取文件列表,其次可以实现分卷压缩,同时还可以保存两个文件信息(还有个在每个文件的头部)用作数据校验

Zip文件使用32CRC算法进行数据校验

2. Zip文件结构

下图中File Entry表示一个文件实体,一个压缩文件中有多个文件实体。

文件实体由一个头部和文件数据组成(压缩后的,压缩算法在头部有说明)

Central Directory由多个File header组成,每个File header都保存一个文件实体的偏移

文件最后由一个叫End of central directory的结构结束

2.1. File Entry结构

2.1.1. File Entry Header

偏移

字节数

类型

描述

0

4

固定值0x04034b50

4

2

解压缩版本

6

2

标志

8

2

压缩方式

10

2

文件最后修改时间

12

2

文件最后修改日期

14

4

CRC-32校验

18

4

压缩后大小

22

4

压缩前大小

26

2

文件名称长度(n)

28

2

扩展字段长度(m)

30

n

文件名称

30+n

m

扩展字段

2.1.2. Data descriptor

当头部标志第3(掩码0×08)置位时,表示CRC-32校验位和压缩后大小在File Entry结构的尾部增加一个Data descriptor来记录,这个时候才会在File Entry结构的尾部出现Data descriptor数据。数据的第一固定值是可选的。

偏移

字节数

类型

描述

0

0/4

固定值0x08074b50

0/4

4

CRC-32校验

4/8

4

压缩后大小

8/12

4

压缩前大小

为什么会有CRC和压缩后大小放到尾部的情况呢?

我猜测是为了让应用程序生成压缩包时更灵活,可以在压缩的同时写入文件,因为在某些情况下可能要文件压缩和写入完成后才能获取具体的大小。

在没有压缩后大小的情况下如何判断文件数据是否加载完毕呢?

我猜测第一是通过固定值0x08074b50来确定,在没有固定值的情况则是比较压缩后大小字段。

2.2. Central Directory

2.2.1. File Header

偏移

字节数

类型

描述

0

4

固定值0x02014b50

4

2

压缩版本

6

2

解压缩版本

8

2

标志

10

2

压缩方式

12

2

文件最后修改时间

14

2

文件最后修改日期

16

4

CRC-32校验

20

4

压缩后大小

24

4

压缩前大小

28

2

文件名称长度(n)

30

2

扩展字段长度(m)

32

2

文件注释长度(k)

34

2

文件开始的分卷号

36

2

文件内部属性

38

4

文件外部属性

42

4

对应文件实体在文件中的偏移

46

n

文件名称

46+n

m

扩展字段

46+n+m

k

文件注释

2.2.2. End of central directory record

所有的File Header结束后就是该数据结构

偏移

字节数

类型

描述

 0

4

固定值0x06054b50

 4

2

当前分卷号

 6

2

Central Directory的开始分卷号

 8

2

当前分卷Central Directory的记录数量

10

2

Central Directory的总记录数量

12

4

Central Directory的大小 (bytes)

16

4

Central Directory的开始位置偏移

20

2

Zip文件注释长度(n)

22

n

Zip文件注释

更详细的信息请参考:http://en.wikipedia.org/wiki/ZIP_%28file_format%29

下面给出一段PHP的代码实现的一个类,这个类可以把一个目录或者一个文件压缩并返回成string,这个string保存成文件就是zip压缩包。
$zip=new PHPZip();
$path=”your target file or dir”;
$data =$zip->zip($path);


class PHPZip {
function zip($path) {
if (@function_exists('gzcompress')) {
$filelist = $this->getFileList($path);
//print_r($filelist);
if (count($filelist) > 0) {
foreach ($filelist as $filename) {
if (is_file($filename)) { //判断给定文件名是否为一个正常的文件
$fd = fopen($filename, "r");
$content = fread($fd, filesize($filename));
fclose($fd);
if (is_dir($path)) {
$filename = substr($filename, strlen($path) + 1);
}
$this->addFile($content, $filename);
}
}
$out = $this->file();
return $out;
}
return 1;
}
else
return 0;
}

function getFileList($path) {//取文件列表
if (file_exists($path)) {
if (is_dir($path)) {
$result = array();
$dh = opendir($path); //打开目录句柄
while (false !== ($file = readdir($dh))) {
if ($file == "." || $file == "..")
continue;
$result = array_merge($result, $this->getFileList($path . "/" . $file));
}
closedir($dh);
return $result;
} else {
return array($path);
}
}
return array();
}

var $datasec = array();
var $ctrl_dir = array();
var $eof_ctrl_dir = "\x50\x4b\x05\x06";
var $old_offset = 0;

/**
* Converts an Unix timestamp to a four byte DOS date and time format (date
* in high two bytes, time in low two bytes allowing magnitude comparison).
*
* @param integer the current Unix timestamp
*
* @return integer the current date in a four byte DOS format
*
* @access private
*/
private function unix2DosTime($unixtime = 0) {
$timearray = ($unixtime == 0) ? getdate() : getdate($unixtime);
if ($timearray['year'] < 1980) {
$timearray['year'] = 1980;
$timearray['mon'] = 1;
$timearray['mday'] = 1;
$timearray['hours'] = 0;
$timearray['minutes'] = 0;
$timearray['seconds'] = 0;
} // end if
return (($timearray['year'] - 1980) << 25) | ($timearray['mon'] << 21) | ($timearray['mday'] << 16) |
($timearray['hours'] << 11) | ($timearray['minutes'] << 5) | ($timearray['seconds'] >> 1);
}

/**
* 将文件写入压缩包
*
* @param string 文件内容
* @param string 文件名称,可以包含路径
* @param integer 当前时间
*
* @access public
*/
function addFile($data, $name, $time = 0) {
$name = str_replace('\\', '/', $name);
$dtime = dechex($this->unix2DosTime($time)); //十进制转换为16进制
$hexdtime = '\x' . $dtime[6] . $dtime[7]
. '\x' . $dtime[4] . $dtime[5]
. '\x' . $dtime[2] . $dtime[3]
. '\x' . $dtime[0] . $dtime[1];
eval('$hexdtime ="' . $hexdtime . '";'); //等价为php语句
$unc_len = strlen($data); //获取文件未压缩的长度
$crc = crc32($data); //计算32位校验码
$zdata = gzcompress($data); //进行压缩
$c_len = strlen($zdata); //获取压缩后的长度
$zdata = substr(substr($zdata, 0, strlen($zdata) - 4), 2); // fix crc bug

$fr = "\x50\x4b\x03\x04"; //固定File Entry的类型标识
$fr .= "\x14\x00"; //解压缩版本
$fr .= "\x00\x00"; //标志
$fr .= "\x08\x00"; //压缩方式
$fr .= $hexdtime; //最后修改时间和日期
$fr .= pack('V', $crc); //校验码
$fr .= pack('V', $c_len); //压缩后大小
$fr .= pack('V', $unc_len); //压缩前大小
$fr .= pack('v', strlen($name)); //文件名称长度
$fr .= pack('v', 0); //扩展字段长度
$fr .= $name; //文件名
$fr .= $zdata; //文件的压缩数据
// 后置的校验码和相关长度,当数据不是按照文件方式送达的时候是必要的
$fr .= pack('V', $crc); //校验码
$fr .= pack('V', $c_len); //压缩后大小
$fr .= pack('V', $unc_len); //压缩前大小
//添加到FileEntry的数组中
$this->datasec[] = $fr;
$new_offset = strlen(implode('', $this->datasec));

//写FileHeader
$cdrec = "\x50\x4b\x01\x02"; //固定的FileHeader标识
$cdrec .= "\x00\x00"; //压缩版本
$cdrec .= "\x14\x00"; //解压缩版本
$cdrec .= "\x00\x00"; //标志
$cdrec .= "\x08\x00"; //压缩方式
$cdrec .= $hexdtime; //最后修改时间和日期
$cdrec .= pack('V', $crc); //校验码
$cdrec .= pack('V', $c_len); //压缩后大小
$cdrec .= pack('V', $unc_len); //压缩前大小
$cdrec .= pack('v', strlen($name)); //文件名称长度
$cdrec .= pack('v', 0); //扩展字段长度
$cdrec .= pack('v', 0); //文件注释长度
$cdrec .= pack('v', 0); //文件开始的分卷号
$cdrec .= pack('v', 0); //文件内部属性
$cdrec .= pack('V', 32); //文件外部属性 - 'archive ' 被置位

$cdrec .= pack('V', $this->old_offset); //指向FileEntry的偏移位置
$this->old_offset = $new_offset;
$cdrec .= $name;
//这里开可以继续写入扩展字段和文件注释,上面长度都为0则直接跳过
//将FileHeader保存进CentralDirectory
$this->ctrl_dir[] = $cdrec;
}

/**
* 输出文件
*
* @return string the zipped file
*
* @access public
*/
function file() {
$data = implode('', $this->datasec);
$ctrldir = implode('', $this->ctrl_dir);
return
$data .
$ctrldir .
//End of central directory record
$this->eof_ctrl_dir . //固定0x06054b50 (需要使用小端\x50\x4b\x05\x06)
"\x00\x00" . //当前分卷号
"\x00\x00" . //CentralDirectory开始的分卷号
pack('v', sizeof($this->ctrl_dir)) . //当前分卷的CentralDirectory记录数
pack('v', sizeof($this->ctrl_dir)) . //CentralDirectory的总记录数
pack('V', strlen($ctrldir)) . //CentralDirectory区的大小
pack('V', strlen($data)) . //指向CentralDirectory的开始
"\x00\x00"; //Zip文件的注释长度和注释,因为长度为0所以如此表达
}
}

,

发表评论