使用 C# 压缩/解压缩类处理标准 Zip 文件






4.89/5 (72投票s)
本项目将提供使用 Deflate 压缩方法进行文件压缩和解压缩的工具,以及读写标准 Zip 文件。
1. 引言
Zip 文件是最常用的文件格式之一。自 1989 年以来,它们一直是计算领域不可或缺的一部分。您是否曾想过它们究竟是如何工作的?我曾经想过。我创建这个项目是为了满足我的好奇心。我想完全理解文件压缩和解压缩编程的精细细节。我几年前购买了 Timothy C. Bell、John G. Cleary 和 Ian H. Witten 合著的《文本压缩》一书。这是一本关于文本压缩主题的优秀入门读物。Ziv 和 Lempel 的自适应字典以及 Huffman 编码算法引起了我的注意。恰巧 Lempel 先生是我在 Technion 求学期间的老师,当时我攻读电气工程学士学位。
当今最常用的压缩方法是“deflate”方法。它内置于 Microsoft Windows Explorer 中。源代码可以从许多网站以多种语言下载。我本项目的目标是尽可能详细地记录代码。我的起点是 Mike Krueger 编写的 C# 代码。我对代码进行了重新组织,重写了其中重要部分,将许多变量重命名为更长、更有意义的名称,并添加了许多注释。此外,我还增加了对读写与 Windows Explorer 的“发送到压缩(zipped)文件夹”选项兼容的 Zip 文件的支持。为了测试代码,我开发了一个应用程序,用于读取现有的 Zip 文件并提取部分或全部文件,创建新的 Zip 文件,或通过添加和删除文件和文件夹来编辑现有的 Zip 文件。
项目由以下逻辑块组成
DeflateMethod
和DeflateTree
类。压缩类。InflateMethod
和InflateTree
类。解压缩类。DeflateNoHeader
、DeflateZLib
和DeflateZipFile
类。DeflateMethod
的派生类。它们提供文件压缩支持。DeflateZIPFile
具有创建 Zip 文件的工具。InflateNoHeader
、InflateZLib
和InflateZipFile
类。InflateMethod
的派生类。它们提供文件解压缩支持。InflateZIPFile
具有读取 Zip 文件的工具。CRC32
、Adler32
和BitReverse
是 Deflate 和 Inflate 类的支持类。UZipDotNet
类。连接所有压缩和解压缩类的应用程序。- 杂项支持类。
其他使用压缩和解压缩类的示例可在 Uzi Granot 在 CodeProject.com 上发布的文章 “PDF 文件分析器(含 C# 解析类)” 和 “PDF 文件写入器 C# 类库” 中找到。在这两个示例中,PDF 流使用 /FlateDecode 过滤器进行压缩和解压缩。两个源模块 InflateMethod.cs 和 DeflateMethod.cs 提供了将一个字节数组压缩和解压缩到另一个字节数组的方法。
2. 参考文献
- "Text Compression" by Timothy C. Bell, John G. Cleary, Ian H. Witten. Prentice Hall Advanced Reference Series. Computer Science 1990.
- DEFLATE Compressed Data Format Specification version 1.3. RFC 1951. By Peter Deutsch. Aladdin Enterprises. 1996.
- GZIP file format specification version 4.3. RFC 1952. By Peter Deutsch. Aladdin Enterprises 1996.
- ZLIB Compressed Data Format Specification version 3.3. RFC 1950. By Peter Deutsch Aladdin Enterprises and Head-Loup Gailly Info-ZIP 1996.
- APPNOTE.TXT - .ZIP File Format Specification. Version: 6.3.2. Revised: September 28, 2007. Copyright (c) 1989 - 2007 PKWARE Inc.
- Algorithm.txt file part of ZLIB 1.2.5 download. Compression algorithm (deflate) and Decompression algorithm (inflate) By Jean-Loup Gailly and Mark Adler.
- Source code download of ICSharpCode.SharpZipLib.Zip Copyright © 2001 Mike Krueger.
3. 压缩
Deflate 压缩方法的基本思想是用到先前出现该字符串的距离和字符串的长度来替换重复的字符串。例如,在本文中,“compression”一词出现了多次。在第一次出现之后,所有后续的出现都将是对先前单词的引用。如果程序在先前文本中找不到至少 3 个字节的匹配字符串,程序会将当前字面量发送到输出流。结果是字面量和距离长度对的混合。字面量和距离长度对被编码为代码和额外位的组合。代码经过 Huffman 编码过程,生成可变长度的位串。高频代码的短位串,低频代码的长位串。
让我们更详细地看看这个过程。程序将输入文件读入读取缓冲区。缓冲区一次扫描一个字节。程序在缓冲区中向前查找当前读取指针处的字符串的最长匹配项。为了加快处理速度,程序使用了一个 16 位(65K)哈希表。读取指针处的当前 3 个字节被转换为哈希索引到哈希表中。哈希表中的每个条目都包含一个指向具有相同哈希索引值的先前 3 字节序列的指针。得到的指针用于将当前字符串与先前的可能匹配项进行比较。程序将基于可能匹配项的哈希链在缓冲区中向后移动。如果找到一个或多个匹配项,则取最长的那个。匹配字符串的距离和匹配长度被放入块缓冲区。如果未找到匹配项,则当前字面量或字节会被放入块缓冲区。块缓冲区中距离为零的条目是字面量,距离非零的条目是匹配字符串。
程序有两种查找字符串匹配的方法:DeflateFast 和 DeflateSlow。它们之间的主要区别在于 DeflateSlow 在将匹配项提交到块缓冲区之前不会尝试下一个字节。如果下一个字节产生了更长的匹配项,则当前指针处的字节将像没有匹配项一样被发送到缓冲区。匹配长度最小为 3 字节,最大为 258 字节。到先前匹配的最大距离为 32768 字节。这是扫描窗口。读取指针在当前指针之前至少有 32768 个字符,在当前指针之后有 262 字节的超前读取,但输入文件的开头和结尾除外。
匹配字符串的一个有趣情况是重复字符的情况。例如,一个源文件包含一行 80 个连字符。当读取指针指向此行的第二个连字符时,匹配过程将产生距离为 1,长度为 79。换句话说,当前字符串和先前匹配的字符串重叠。
块缓冲区长 16K。当块缓冲区满时,编码过程开始。字面量保持不变。编码值与字面量值相同。字面量是字节,范围为 0 到 255。长度范围为 3 到 258。它被编码为代码和额外位的组合。参见下表。例如,如果长度为 7,代码为 261,没有额外位。如果长度为 100,代码为 279,有 4 个额外位“0001”。程序将字面量和长度代码组合成一个称为符号的实体。如果符号值为 0 到 255,则为字面量。如果符号值为 256,则为块结束标记。如果符号为 257 到 285,则为长度。如果符号在 265 到 284 之间,则为长度,后面跟着额外位。
// Length Code (See RFC 1951 3.2.5)
// Extra Extra Extra
// Code Bits Length(s) Code Bits Lengths Code Bits Length(s)
// ---- ---- ------ ---- ---- ------- ---- ---- -------
// 257 0 3 267 1 15,16 277 4 67-82
// 258 0 4 268 1 17,18 278 4 83-98
// 259 0 5 269 2 19-22 279 4 99-114
// 260 0 6 270 2 23-26 280 4 115-130
// 261 0 7 271 2 27-30 281 5 131-162
// 262 0 8 272 2 31-34 282 5 163-194
// 263 0 9 273 3 35-42 283 5 195-226
// 264 0 10 274 3 43-50 284 5 227-257
// 265 1 11,12 275 3 51-58 285 0 258
// 266 1 13,14 276 3 59-66
距离的范围是 1 到 32768。它被编码为代码和额外位的组合。参见下表。例如,距离 3 是代码 2,没有额外位。距离 1000 是代码 19,有 8 个额外位 11100111(231)。
// Distance Codes (See RFC 1951 3.2.5)
// Extra Extra Extra
// Code Bits Dist Code Bits Dist Code Bits Distance
// ---- ---- ---- ---- ---- ------ ---- ---- --------
// 0 0 1 10 4 33-48 20 9 1025-1536
// 1 0 2 11 4 49-64 21 9 1537-2048
// 2 0 3 12 5 65-96 22 10 2049-3072
// 3 0 4 13 5 97-128 23 10 3073-4096
// 4 1 5,6 14 6 129-192 24 11 4097-6144
// 5 1 7,8 15 6 193-256 25 11 6145-8192
// 6 2 9-12 16 7 257-384 26 12 8193-12288
// 7 2 13-16 17 7 385-512 27 12 12289-16384
// 8 3 17-24 18 8 513-768 28 13 16385-24576
// 9 3 25-32 19 8 769-1024 29 13 24577-32768
以上述重复连字符为例,80 个连续连字符的编码结果将是
字面量 | 长度 79 | 距离 1 | ||
连字符 | 代码 | 额外位 | 代码 | 额外位 |
45 | 277 | 1100 (12) | 0 | 无 |
此时我们有两种代码:范围为 0 到 285 的字面量/长度代码,以及范围为 0 到 29 的距离代码。为简单起见,我们将字面量/长度代码称为字面量代码。对于每种代码类型,程序都会计算该代码在块中出现的次数。这就是代码频率。程序构建两个频率数组:286 个元素的字面量频率数组和 30 个元素的距离频率数组。基于每种代码的使用频率,我们构建 Huffman 树,并为每种代码分配不同长度的位串,以便常用代码获得较短的位串,不常用代码获得较长的位串。
代码到 Huffman 代码的转换在 DeflateTree
类中完成。有三个 DeflateTree
类的实例:字面量树、距离树和位长树。到目前为止我们已经讨论了前两者。位长树用于传输其他两棵树的编码信息。这个过程稍后将讨论。将代码转换为 Huffman 代码的第一步是创建一个只有叶节点的树。每个使用过的代码一个节点。每个节点有三个相关值:代码、频率和指向一对子节点的指针。未使用的代码将被忽略。树按频率顺序排序。从低频率到高频率。程序从频率最低的两个节点开始。它创建一个频率组合为这两个叶节点的新父节点。指向两个子节点的指针保存在父节点中。程序将新节点放置在树中,正好在下一个更高频率的节点之前。这个过程对每对节点继续进行,直到所有节点对都被扫描。接下来,程序使用递归方法遍历树,并为每个节点分配到根的距离。这个距离是表示代码所需的位数。它是代码的长度。代码长度保存在数组中。下面您将看到一个六个字母字母表的示例。步骤 1 显示了每个字母的使用频率。步骤 2 是按频率排序的同一表格。步骤 3 显示了添加的父节点以及指向叶节点的指针。位长列显示了从根节点到每个叶节点的步数。
表示符号所需的最多位数限制为字面量树和距离树的 15 位。位长树(稍后讨论)限制为 7 位。如果上述计算结果大于最大允许值,程序将调整最不常用代码的频率,以便创建更对称的树。这将有效地以牺牲下一个较高频率代码为代价来减少最不常用代码的位数。在此过程结束时,我们将得到一个包含 286 个元素的字面量代码位长数组,以及一个包含 30 个元素的距离代码位长数组。此时,我们知道每个代码的位长度,但不知道实际的代码值。代码值将在之后分配。
字面量和距离代码长度数组必须包含在压缩文件中,以便解压缩程序能够读取和解码压缩符号。作为 Deflate 过程的一部分,这两个数组也将被压缩,以便包含在压缩文件中。这里的字面量是位串的长度。如前所述,代码长度的范围是 1 到 15。此外,我们还需要零来表示未使用代码,以及三个其他重复符号来压缩重复代码。总共位长数组是 19 个元素长。程序为字面量和距离树的组合代码构建频率数组。位长树的构建方式与之前构建字面量和距离树的方式相同。一个区别是最大代码长度。在这种情况下,它是 7 位。第二个区别是位长顺序数组。位长数组是 19 个元素长。最后三个是最有可能使用的元素。程序通过使用转换顺序数组重新组织位长数组。此过程的输出是使得可能使用的元素排在前面,而未使用的元素可能排在最后。系统传输到最后一个使用的代码。
Deflate 压缩算法存在一个潜在问题。问题是不可压缩文件压缩后可能比原始文件更长。显然,最好传输原始文件而不是它的更长版本。ZIP 文件头和 ZLIB 文件头都有一个压缩方法标志。在我们的例子中,它可以是 Deflate (8) 或 Stored (0)。如果整个文件不可压缩,它将被存储。整个文件将按原样复制到压缩文件存档中。如果输入文件长度小于 8 字节,则不会尝试压缩,文件将按原样存储。如果文件是可压缩的,我们将使用 Deflate
方法。在可压缩文件内部,有些块可能是不可压缩的。在这种情况下,我们希望将这些块从输入流复制到输出流而不更改。可压缩块有两个选项:动态树选项和静态树选项。动态树选项是程序计算 Huffman 树信息并将其保存在块开头的块。静态树选项是使用固定 Huffman 树。它是由 Deflate 过程的发明者定义的。该树不存储在压缩文件中。解码程序拥有构建它的所有信息。下表描述了这些静态树。
// Static literal codes and length codes (See RFC 1951 3.2.6)
// Lit Value Bits Codes
// --------- ---- -----
// 0 - 143 8 00110000 through 10111111
// 144 - 255 9 110010000 through 111111111
// 256 - 279 7 0000000 through 0010111
// 280 - 287 8 11000000 through 11000111
// Note that literal/length codes 286-287 will never actually
// occur in the compressed data.
//
// Static distance codes (See RFC 1951 3.2.6)
// Distance codes 0-31 are represented by (fixed-length) 5-bit
// codes, with possible additional bits as shown in the table
// shown in Paragraph 3.2.5, above. Note that distance codes 30-
// 31 will never actually occur in the compressed data.
//
静态树的优点是开销小。解压缩程序知道该树,并且它不包含在文件中。动态树的优点是在大多数情况下压缩效果更好。总而言之,整个文件可以被存储或压缩。如果文件被压缩,它被分成块。每个块可以被存储,或使用静态树压缩,或使用动态树压缩。决定使用哪个选项的逻辑如下所述。
如果输入文件非常小,小于 8 字节,则将其存储。所有其他文件都经过我们迄今为止描述的过程。在每个块结束时,我们知道每个符号的频率,并且我们知道每个符号的位长度,并且我们知道文件中将包含多少额外位。根据这些信息,程序会计算如果使用动态树(包括继承的动态树开销)对块进行编码,块的长度将是多少。接下来,程序计算使用静态树的块的长度。程序将比较动态长度、静态长度和未压缩块长度,以确定最短的选项。如果该块是文件的最后一个块,程序将进行一次额外的测试。它将比较压缩文件的预期总长度与未压缩文件的原始大小。如果更大,则整体压缩方法将从 Deflate
更改为 Stored
。程序将倒回输入和输出文件,并将输入文件复制到输出文件。
在选择了动态树选项的情况下,需要代码和位串之间的转换表。我们知道每个位串的长度。因此,下一步是构建一个位串数组。程序扫描代码长度数组,检查 15 种可能的长度。从 1 到 15。假设使用的最小位长度是 3 位。程序将为位长度为 3 的每个代码分配一个 3 位代码。起始代码是零。假设我们总共有 3 个 3 位代码。代码将是 000、001 和 010。下一个使用的位长度是 5 位。第一个代码将是 01100。它是前一个系列的延续,再加上额外的位。如果我们有 5 个 5 位代码,它们将是 01100、01101、01110、01111 和 10000。如果您查看 DeflateTree
类的 BuildCodes
方法,代码首先在 16 位无符号整数中左对齐,然后在 16 位整数中反转位。最终值是反转的代码。这个过程简化了解压缩文件的工作。
压缩程序必须在压缩文件中包含代码和位串之间的转换表,以便解压缩程序能够解码数据。这将通过将每个代码的位串长度包含在文件中来实现。解码程序将按照上一段所述的过程创建转换表。位流长度数组通过检测连续重复的符号来压缩。开始时,每个元素为零到 15。零代表未使用代码,1 到 15 代表所有可能的位长度。程序增加了三个额外的代码来压缩重复的位串长度:RepeatSymbol_3_6(16)、RepeatSymbol_3_10(17)和 RepeatSymbol_11_138(18)。总共有 19 个符号。程序将使用 RepeatSymbol_3_6 来压缩重复使用的代码。程序将使用 RepeatSymbol_3_10 和 RepeatSymbol_11_138 来压缩重复的未使用代码。这 19 个符号会经过进一步的转换。最有可能使用的三个代码是重复符号。程序更改所有 19 个符号的顺序,以便最有可能的符号具有较小的值,最不可能的符号具有较大的值。如果最后几个符号未使用,则不会传输它们。
最后,程序已准备好输出一个压缩块。每个块的开头都有一个 3 位头部。两位用于指定块类型:Stored (00)、Static Tree (01) 和 Dynamic tree (10)。第三位设置为文件的最后一个块。如果是存储块,接下来的两个字节(16 位)是块的长度(最大 65535)。接下来的两个字节是块长度的反码。程序确保块长度是字节对齐的。在这 4 个字节之后,块从输入文件中复制。对于静态树,压缩数据从 3 位头部之后开始。对于动态树,程序将三个树的信息发送到输出文件。首先是三个树的长度,然后是树本身。对于静态树和动态树,现在可以实际将压缩数据写入输出文件。程序逐个条目遍历块缓冲区,将字面量/长度符号转换为位串,加上可选的额外位。如果字面量/长度符号是长度,程序将把匹配的距离转换为位串和可选的额外位。在块结束时,程序写入块结束标记。
在将一个块写入输出文件后,该过程将逐个块地继续到文件末尾。
详细信息包含在 C# 代码本身中。这是一个非常消耗 CPU 的应用程序。查找匹配字符串的代码可能是消耗 CPU 时间最多的部分。
4. 解压缩
解码 Deflate 压缩文件比压缩它们更直接。解压缩程序读取位串符号,将它们转换为代码,字面量代码直接输出到文件,长度和距离代码被转换为字节串,并将这些字符串发送到输出文件。
让我们更详细地看看这个过程。如果压缩文件是 ZIP 或 ZLIB 文件,压缩方法代码是 Stored (0) 或 Deflate (8)。附加程序不支持任何其他压缩方法。如果压缩方法是 Stored,解压缩程序只需将输入文件复制到输出文件。如果压缩方法是 Deflate,解压缩程序将解码文件并重建原始文件。
压缩文件被分成块。每个块的第一个 3 位是块头部。前 2 位决定了块的类型:stored (00)、static tree (01) 和 dynamic tree (10)。第三位是最后一个块标志。
如果一个块是存储块,程序将读取指针对齐到下一个字节边界。它读取接下来的 16 位作为无符号整数。这是块的长度(字节)。接下来的 16 位是相同长度但反码。程序读取该数字,反转它,并与第一个数字进行比较。如果测试失败,则文件无效。接下来,程序将指定数量的字节从输入文件读取到输出文件。
如果一个块是动态树块,程序会读取树信息。有三棵树:位长树、字面量/长度树和距离树。每棵树是一个代码长度数组。数组的索引是代码,每个项目是代码的位长度。这些信息足以重建与代码相关的位串。此过程在上面的压缩部分中进行了描述。读取树信息按以下顺序进行。首先,程序读取三棵树各自的长度。接下来,程序读取位长度数组。使用位长度数组,程序重建此数组所有 19 个项的位串。接下来,程序使用位长度数组的信息读取字面量/长度和距离数组。程序重建字面量/长度树和距离树的位串与代码之间的转换表。在读取和处理完树信息后,读取指针现在位于实际压缩数据的起始位置。
如果一个块是静态树块,程序拥有构建字面量/长度树和距离树所需的所有信息。压缩数据的起始位置就在 3 位头部之后。
压缩文件由可变长度的位串组成。每个代码可以是 1 到 15 位加上可选的额外位。在压缩程序中,将代码转换为位串的任务很简单。我们有一个位串代码数组和一个匹配的位串长度数组。我们索引这两个数组,并将正确数量的位从位串代码数组移动到输出流。反向过程不那么明显。我们有一个位流。代码长度可变。一个代码紧接着另一个代码,没有任何分隔符。为了能够无歧义地读取代码,代码的构造方式是任何代码都不是另一个代码的前缀。例如,如果 0101 是一个有效的 4 位代码,则 0、01 或 010 都不是有效代码。要解码此位流,需要创建一个节点树。每个节点有两个值:child-zero 和 child-one。这些值是指向树的其他节点的指针,或者是转换后的符号。例如,我们有一个六个字母的字母表 A 到 F。每个字母的可变位串是:A-00, B-1110, C-01, D-110, E-10, F-1111。我们创建如图所示的树。输入位是 01001110。如果我们沿着位向下遍历树直到到达一个字母,然后再次从根开始,结果将是 CAB。
这种方法工作正常,但速度很慢。压缩文件将一次处理一位。为了加快处理速度,我们希望一次处理一个位块。程序设置为 9 位。我们创建一个包含 512 个值的数组。程序将从输入文件中读取 9 位块并索引到此数组。如果输入缓冲区中的下一个代码等于或小于 9 位,则数组中的值就是我们的代码。如果输入缓冲区中的下一个代码长于 9 位,则数组中的值是指向从第 10 位开始的子树的指针。为了说明这个过程,基于上面给出的示例,我们将采用一个 3 位块和一个 8 个元素的数组。字母 A 的代码是 00。由于我们的解码器将选择 3 位,因此 A 之后的额外位可以是 1 或 0。我们将 A 放置在表的零元素和一元素中。解码 A 后,我们将位指针移动 2 位。如果一个代码的位数少于表设计的位数,它将填充表中的多个条目。字母 D 的代码是 110。它是三位,正好是解码器将选择的位数。在这种情况下,我们有一个条目。字母 B 和 F 的代码长度超过 3 位。在这种情况下,我们将把一个指针放在位置 7,指向表上方两个元素区域。第一个是 B,第二个是 F。
目录 | 指针 | 代码 | 代码长度 |
0 | A(000) | 2 | |
1 | A(001) | 2 | |
2 | C(010) | 2 | |
3 | C(011) | 2 | |
4 | E(100) | 2 | |
5 | E(101) | 2 | |
6 | D(110) | 3 | |
7 | 8 | ||
8 | B(1110) | 4 | |
9 | F(1111) | 4 |
对于静态树和动态树,解压缩过程是相同的。这个过程的输入是两棵树:字面量/长度树和距离树。过程不知道也不关心树是静态的还是动态的。一个块的解压缩循环很简单。它按上述方式读取文件中的下一个符号。符号值范围是 0 到 285。如果是 0 到 255,则为字面量。字面量被保存在输出文件中。如果是 256,则表示块结束。如果符号是 257 到 285,则为长度。根据上面的长度表,程序获取基础长度以及需要读取的额外位数。长度计算为基础值加上额外位的数值。解码完长度后,程序将下一个符号读取为距离。同样,使用上面的距离表,程序计算距离,即基础值加上额外位的数值。使用距离和长度,程序将字符串从当前位置减去距离复制到写入缓冲区的当前位置。特殊情况是当源字符串和目标字符串重叠时。数组复制例程在这种情况下不起作用。程序将按字节逐个字节地移动重叠区域。如果块头部设置了最后一个块标志,则在接收到块结束标记时,解压缩完成。
5. ZIP 文件
Zip 文件是一个包含单个文件和目录路径的存档。该文件由三部分组成:文件区、中央目录和目录结束记录。下表显示了一个包含三个文件的 Zip 存档的示例。文件区包含压缩文件或目录路径。每个文件条目由头部和压缩文件数据组成。目录文件条目只有头部,没有数据。中央目录包含所有文件的文件头。中央目录中的文件头与文件区中的文件头相似但不完全相同。目录结束记录指向中央目录的开头。要读取 Zip 文件,应该读取文件末尾的目录结束记录,使用指向中央目录开头的指针,将中央目录加载到内存中,并根据目录信息访问单个文件。在每个文件头和目录结束记录的开头都有一个 32 位签名。在处理文件时,程序始终验证签名标记。要读取文件,程序会读取文件最后 512 字节。它向后扫描块以查找 32 位中央目录文件头签名。找到后,程序会读取中央目录条目数量以及中央目录的起始位置。
文件区 | 中央目录 | 目录结束 | |||||||
文件 1 | 文件 2 | 文件 3 | 文件 1 标题 |
文件 2 标题 |
文件 3 标题 |
指向起始位置的指针 中央目录 |
|||
标题 | Data | 标题 | Data | 标题 | Data |
读取 Zip 存档在 InflateZipFile
类中完成。写入 Zip 存档在 DeflateZipFile
类中完成。中央目录信息在内存中的一个数组中维护。每个条目由 ZipDirectory 结构定义。
Zip 支持存在限制
- 不支持多磁盘。
- 压缩方法仅限于 Deflate 或 Stored。
- 版本假定为 20。
- 通用标志假定为 0。
下面将给出文件区文件头、中央目录文件头和中央目录记录结束部分的定义。这些信息来自 PKWARE。APPNOTE.TXT - .ZIP File Format Specification. Version: 6.3.2. Revised: September 28, 2007. Copyright (c) 1989 - 2007 PKWARE Inc. 上述三个类的源代码包含所有详细信息。
// File area file header
//
// Pos Len
// 0 4 Local file header signature = 0x04034b50
// 4 2 Version needed to extract (minimum)
// 6 2 General purpose bit flag
// 8 2 Compression method
// 10 2 File last modification time
// 12 2 File last modification date
// 14 4 CRC-32
// 18 4 Compressed size
// 22 4 Uncompressed size
// 26 2 File name length (n)
// 28 2 Extra field length (m)
// 30 n File name
// 30+n m Extra field (NTFS file date and time)
//
// End of central directory record:
//
// Pos Len
// 0 4 End of central directory signature = 0x06054b50
// 4 2 Number of this disk
// 6 2 Disk where central directory starts
// 8 2 Number of central directory records on this disk
// 10 2 Total number of central directory records
// 12 4 Size of central directory (bytes)
// 16 4 Offset of start of central directory, relative to start of archive
// 20 2 ZIP file comment length (n)
// 22 n ZIP file comment
//
// Central directory file header
//
// Pos Len
// 0 4 Central directory file header signature = 0x02014b50
// 4 2 Version made by
// 6 2 Version needed to extract (minimum)
// 8 2 General purpose bit flag
// 10 2 Compression method
// 12 2 File last modification time
// 14 2 File last modification date
// 16 4 CRC-32
// 20 4 Compressed size
// 24 4 Uncompressed size
// 28 2 File name length (n)
// 30 2 Extra field length (m)
// 32 2 File comment length (k)
// 34 2 Disk number where file starts
// 36 2 Internal file attributes
// 38 4 External file attributes
// 42 4 Offset of local file header
// 46 n File name
// 46+n m Extra field
// 46+n+m k File comment
//
5.1. NTFS 文件日期和时间
Windows 操作系统使用文件区中的文件头额外字段来以本地时间保存文件日期和时间。要获取时间,请使用 FileInfo.LastWriteTime.ToFileTime()
或 File.GetLastWriteTime(FileName).ToFileTime()
。要设置时间,首先将字节转换为 DateTime 格式:FileModifyTime = DateTime.FromFileTime(BitConverter.ToInt64(TimeField, 12))
。接下来,使用 FileInfo.LastWriteTime = FileModifyTime
或 File.SetLastWriteTime(FileName, ModifyTime)
。
// NTFS File date and time
//
// Pos Len
// 0 2 NTFS tag signature = 0x000a
// 2 2 Length 32 bytes = 0x0020
// 4 4 Reserved area = 0x00000000
// 8 2 File date and time signature = 0x0001
// 10 2 Length 24 bytes = 0x0018
// 12 8 File last write time (DateTime format)
// 20 8 File last access time (DateTime format)
// 28 8 File last creation time (DateTime format)//
6. UZipDotNet 应用程序
UZipDotNet 应用程序是为了测试压缩和解压缩类而开发的。如果您想在开发环境之外测试可执行程序,请创建一个名为 UZipDotNet 的目录,并将 UZipDotNet.exe 程序复制到该目录中并运行程序。如果您从 Visual C# 开发环境运行项目,请确保在项目属性的 Debug 选项卡中定义了工作目录。此程序是使用 Microsoft Visual C# 2005 开发的。如果您想使用 Visual C# 2010 运行它,开发环境会毫无错误地进行转换。
启动程序,您将看到一个类似下图的屏幕。
可用选项有
6.1. 打开
“打开”按钮允许您打开一个现有的 Zip 文件。Zip 存档中央目录的内容将显示在屏幕上。“Pos Hex”复选框允许您以十六进制显示文件位置。Zip 存档已打开以供读取。内部 Zip 存档由 InflateZipFile
类打开。如果您想更新存档(添加或删除文件),系统将显示“存档更新”对话框(见下图)。如果您按“确定”,文件将被重新打开以进行更新。内部 Zip 存档将由 DeflateZipFile
类控制。
6.2. 提取
“提取”按钮允许您将文件从 Zip 存档提取到目录。要提取一部分文件,请在单击“提取”按钮之前选择这些文件。“提取文件”对话框定义了提取文件的根文件夹和提取选项。
6.3. 新建
“新建”按钮允许您定义一个新空 Zip 文件的名称。按下按钮后,您将获得一个标准的“.NET”对话框“另存为”。选择一个目录和新文件的名称。“新建”按钮将重命名为“保存”。添加文件后,您需要按“保存”按钮将中央目录和目录结束记录追加到 Zip 存档。
6.4. 添加
定义 Zip 存档名称后,您需要添加要包含在存档中的文件或文件夹。如果压缩级别设置为零,文件将存储在存档中。如果压缩级别为 1 到 3,文件将使用 deflate fast 例程进行压缩。如果压缩级别为 4 到 9,文件将使用 deflate slow 例程进行压缩。默认值为 6。添加文件时,zip 存档的文件区将被更新。中央目录将在保存按钮按下时在过程结束时保存。
6.5. 删除
“删除”按钮允许您从存档中删除文件。首先选择要删除的文件,然后按“删除”按钮。删除过程会删除内存数组中的文件条目。文件本身将在按下“保存”按钮时从存档中删除。
6.6. 测试
“测试”按钮允许您测试压缩和解压缩类。此按钮对打开的 Zip 存档没有影响。它允许开发人员获取任何文件,将其压缩,然后解压缩并比较输入文件和解压缩后的文件。屏幕有三个选项:压缩级别、压缩文件类型以及输入文件和解压缩文件的逐字节比较。如果压缩级别设置为零,文件将存储在存档中。如果压缩级别为 1 到 3,文件将使用 deflate fast 例程进行压缩。如果压缩级别为 4 到 9,文件将使用 deflate slow 例程进行压缩。默认值为 6。Zip 文件类型选项将生成一个标准的 Zip 存档,其中包含一个压缩文件。ZLIB 文件类型将在压缩文件内容之前添加 16 位头部,并在文件末尾添加 32 位 Adler32 校验和。无头部文件类型是没有头部或尾部的压缩文件数据。如果选择了无头部,程序将无法解压缩已存储的文件。换句话说,压缩级别为零或不可压缩的文件。如果您希望程序逐字节比较输入文件和解压缩后的文件,请勾选“比较文件”复选框。要开始测试,请按“打开”按钮。将显示一个标准的 .net 打开文件对话框。选择一个文件然后按“确定”。例如,如果您选择 TestFile.txt 作为输入文件,压缩文件将保存在程序的当前工作目录中。压缩文件名将是 TestFile.zip(或.zlib 或 .def)。程序将解压缩该文件并将结果保存在 TestFileDecomp.txt 中。输入文件和解压缩后的文件应该相同。此过程的结果将显示在屏幕上。
7. 使用压缩/解压缩类源代码
TestForm
类是为了测试压缩/解压缩类而开发的。因此,它是一个如何调用这些类的示例。上一段(测试按钮)描述了如何使用 TestForm
类。该程序支持三种文件类型:ZIP、ZLIB 和无头部。下面将描述如何在代码中为每种文件类型包含压缩和解压缩类。
7.1. 无头部文件类型
使用无头部文件类型的压缩允许您压缩单个文件。结果是一个没有头部或尾部的压缩文件。要压缩文件,请创建 DeflateNoHeader
类的实例。使用压缩级别值(0 到 9)调用构造函数。调用不带参数的构造函数会将级别设置为默认值 6。接下来,调用 Compress
方法,传入输入文件名和输出文件名。请注意:输出文件可以是压缩文件,也可以是存储文件(输入文件的精确副本)。压缩后,您应该检查 CompFunction
属性。
// create compressiom object
// CompLevel is compression level number 0 to 9. Default is 6
// calling DeflateNoHeader with no argument result in CompLevel=6 the default level
DeflateNoHeader Def = new DeflateNoHeader(CompLevel);
// compress file
// return value: false=no error, true=error
if(Def.Compress(InputFileName, OutputFileName))
{
// ExceptionStack is Array of Strings
// ExceptionStatck[0] is the error message
// All other strings are stack entries from UZipDotNet namespace
MessageBox.Show("Compress file Error\n" + Def.ExceptionStack[0] + "\n" + Def.ExceptionStack[1]);
return;
}
// InflateNoHeader cannot decompress stored file
// you must check CompFunction to make sure the file will not be decompressed
if(Def.CompFunction == DeflateMethod.CompFunc.Stored)
{
// set a flag to indicate that the output is the same as the input file.
// in other words you cannot decompress the result
}
要解压缩文件,请创建 InflateNoHeader
类的实例。接下来,调用 Decompress
方法,传入输入文件名和输出文件名。您只能解压缩已压缩的文件。使用存储文件调用 Decompress
方法将导致错误。
// create decompression object
InflateNoHeader Inf = new InflateNoHeader();
// decompress the file
// return value: false=no error, true=error
if(Inf.Decompress(InputFileName, OutputFileName))
{
// ExceptionStack is Array of Strings
// ExceptionStatck[0] is the error message
// All other strings are stack entries from UZipDotNet namespace
MessageBox.Show("Decompress file Error\n" + Inf.ExceptionStack[0] + "\n" + Inf.ExceptionStack[1]);
return;
}
7.2. ZLIB 文件类型
使用 ZLIB 文件类型的压缩允许您压缩单个文件。结果是一个带有头部和校验和尾部的压缩文件。头部包含压缩方法标志。可以是 Deflate
或 Stored
。校验和尾部是 Adler32 校验和。这种文件类型比无头部文件具有两个显著优点。头部包含有关压缩方法的信息,供解压缩程序使用。第二个优点是校验和。解压缩程序可以验证解压缩文件的完整性。要压缩文件,请创建 DeflateZLib 类的实例。使用压缩级别值(0 到 9)调用构造函数。调用不带参数的构造函数会将级别设置为默认值 6。接下来,调用 Compress 方法,传入输入文件名和输出文件名。
// create decompression object
InflateNoHeader Inf = new InflateNoHeader();
// decompress the file
// return value: false=no error, true=error
if(Inf.Decompress(InputFileName, OutputFileName))
{
// ExceptionStack is Array of Strings
// ExceptionStatck[0] is the error message
// All other strings are stack entries from UZipDotNet namespace
MessageBox.Show("Decompress file Error\n" + Inf.ExceptionStack[0] + "\n" + Inf.ExceptionStack[1]);
return;
}
要解压缩文件,请创建 InflateZLib
类的实例。接下来,调用 Decompress
方法,传入输入文件名和输出文件名。
// create decompression object
InflateZLib Inf = new InflateZLib();
// decompress the file
// return value: false=no error, true=error
if(Inf.Decompress(InputFileName, OutputFileName))
{
// ExceptionStack is Array of Strings
// ExceptionStatck[0] is the error message
// All other strings are stack entries from UZipDotNet namespace
MessageBox.Show("Decompress file Error\n" + Inf.ExceptionStack[0] + "\n" + Inf.ExceptionStack[1]);
return;
}
7.3. ZIP 文件类型
使用 ZIP 文件类型的压缩允许您将多个文件压缩到一个存档中。结果是一个由三部分组成的文件:文件区、中央目录和目录结束记录。Zip 文件格式在上面的 5. ZIP 文件部分进行了描述。要压缩文件,请创建 DeflateZipFile
类的实例。使用压缩级别值(0 到 9)调用构造函数。调用不带参数的构造函数会将级别设置为默认值 6。接下来,调用 CreateArchive
方法,传入存档(输出)文件名。文件名应具有 .ZIP 文件扩展名。下一步是向存档添加文件。对要添加的文件调用一次或多次 Compress 方法。Compress 方法有两个参数:FullFileName 和 ArchiveFileName。FullFileName 是本地系统上文件的完整名称。FullFileName 可以是绝对路径或相对路径。ArchiveFileName
是存档目录中文件的名称。ArchiveFileName
可以仅仅是一个文件名,也可以是带路径的文件名。但是,ArchiveFileName
不能以驱动器号、服务器名或反斜杠开头。每次调用 Compress 方法时,程序都会压缩文件并将结果附加到 Zip 存档的文件区。此文件的中央目录信息保存在内存中。所有文件都压缩完成后,调用 SaveArchive 方法。此方法会将中央目录信息和目录结束记录追加到文件中。现在您就有了一个标准的 Zip 存档,可以被 Windows 或任何其他解压程序解压。
DeflateZipFile
类具有其他公共方法
OpenArchive
允许您打开现有的 Zip 存档以进行更新。SaveDirectoryPath
允许您将目录路径添加到中央目录。不涉及压缩。这些条目在解压缩存档时用于重新创建目录结构。如果目录非空,则不需要目录条目。如果需要创建带有空目录的目录结构,则必须存在这些条目。Delete
方法允许您从中央目录中删除条目。文件本身将在调用SaveArchive
方法时从存档中删除。ClearArchive
允许您关闭输出文件并删除它。当用户创建一个新存档并在保存存档之前决定取消操作时,这很有用。- IsOpen 将返回 true,如果
DeflateZipFile
存在并且有一个打开的输出文件。
DeflateZipFile
类公开了以下公共属性
ArchiveName
:打开的 Zip 文件存档的名称。IsEmpty
:如果存档未打开或中央目录为空,则返回 true。ExceptionStack
是一个字符串数组。如果在处理过程中抛出异常,该类会将异常消息保存在元素零中,并将来自UZipDotNet
命名空间的异常堆栈条目。- ZipDir:中央目录条目的列表。
// create compressiom object
// CompLevel is compression level number 0 to 9. Default is 6
// calling DeflateNoHeader with no argument result in CompLevel=6 the default level
DeflateZipFile Def = new DeflateZipFile(CompLevel);
// create empty zip file
if(Def.CreateArchive(CompFileName))
{
// ExceptionStack is Array of Strings
// ExceptionStatck[0] is the error message
// All other strings are stack entries from UZipDotNet namespace
MessageBox.Show("Compress file Error\n" + Def.ExceptionStack[0] + "\n" + Def.ExceptionStack[1]);
return;
}
// compress one file
if(Def.Compress(FullFileName, ArchiveFileName))
{
// ExceptionStack is Array of Strings
// ExceptionStatck[0] is the error message
// All other strings are stack entries from UZipDotNet namespace
MessageBox.Show("Compress file Error\n" + Def.ExceptionStack[0] + "\n" + Def.ExceptionStack[1]);
return;
}
// save archive
if(Def.SaveArchive())
{
// ExceptionStack is Array of Strings
// ExceptionStatck[0] is the error message
// All other strings are stack entries from UZipDotNet namespace
MessageBox.Show("Compress file Error\n" + Def.ExceptionStack[0] + "\n" + Def.ExceptionStack[1]);
return;
}
要解压缩文件,请创建 InflateZipFile
类的实例。调用 OpenZipFile
方法并传入输入文件名。该类将打开 Zip 文件并读取中央目录。多次调用 DecompressZipFile
方法以提取存档中的每个文件。第一个参数是 ZipDir
数组的一个元素。此数组保存存档的目录。
InflateZipFile
类具有其他公共方法
ExtractAll
允许您打开现有的 Zip 存档并将所有文件提取到您选择的目录。- IsOpen 将返回 true,如果 InflateZipFile 存在并且有一个打开的输入文件。
DeflateZipFile
类公开了以下公共属性
ArchiveName
:打开的 Zip 文件存档的名称。IsEmpty
:如果存档未打开或中央目录为空,则返回 true。ExceptionStack
是一个字符串数组。如果在处理过程中抛出异常,该类会将异常消息保存在元素零中,并将来自UZipDotNet
命名空间的异常堆栈条目。- ZipDir:中央目录条目的列表。
- ZipDirPosition:Zip 中央目录在存档中的位置。
- ReadTotal:当前压缩文件的大小。大小不包括文件头部。
- WriteTotal:当前解压缩文件的大小。
// create decompressiom objectt
InflateZipFile Inf = new InflateZipFile();
// open a zip archive
if(Inf.OpenZipFile(ReadFileName))
{
// ExceptionStack is Array of Strings
// ExceptionStatck[0] is the error message
// All other strings are stack entries from UZipDotNet namespace
MessageBox.Show("Decompress file Error\n" + Inf.ExceptionStack[0] + "\n" + Inf.ExceptionStack[1]);
return;
}
// decompress one file
// Inf.ZipDir[n] is one of the file headers loaded by the OpenZipFile menthod
// RootPathName is the path name of the destination folder
// NewFileName is either null or a new name for the extracted file. If null, the original name is used
// CreatePath is a boolean if true the system will create a path if it does not exist
// OverWrite is a boolean if true the system will overwrite existing file withe the same name.
// If false and a file withe the same name exist, the operation will be aborted with application exception
if(Inf.DecompressZipFile(Inf.ZipDir[n], RootPathName, NewFileName, CreatePath, OverWrite))
{
// ExceptionStack is Array of Strings
// ExceptionStatck[0] is the error message
// All other strings are stack entries from UZipDotNet namespace
MessageBox.Show("Compress file Error\n" + Inf.ExceptionStack[0] + "\n" + Inf.ExceptionStack[1]);
return;
}
// close zip file
Inf.CloseZipFile();
8. Visual C# 控件的源代码示例
如果您正在寻找一些 Visual C# 控件的代码示例,这里是此应用程序中使用的一些控件列表
OpenFileDialog
,SaveFileDialog
,SendToRecycleBin
DataGridView
,DataGridViewSortCompareEventHandler
,TreeView
,ListView
,ListBox
,RichTextBox
SplitterPanel
,NumericUpDown
XmlSerializer
,XmlTextWriter
,XmlTextReader
FileSystemWatcher
,FileSystemEventHandler
,RenamedEventHandler
DriveInfo
,FileInfo
,DirectoryInfo
Timer
,EventHandler
Exception
,ApplicationException
9. 值得关注的方面
在我职业生涯的大部分时间里,我使用的计算机语言都直接编译为机器码。在使用 C# 之前,我使用 MASM、C 和 C++ 进行开发。在转向 C# 时,我对 .net 架构的性能表示不确定。由于压缩过程非常消耗 CPU,我决定将 C# 代码转换为 C++ 并查看速度差异。我感到非常惊喜。差异非常小。
10. 作者的其他开源软件
- Android Color Selector for Programmers 是 Google Play 上提供的免费 App。
- Google Play 应用内结算演示 App 本文是 Google Play 应用内结算版本 3 的示例。
- PDF 文件写入器 C# 类库
2013 年 4 月“最佳整体文章”竞赛获奖作品
2013 年 4 月“最佳 C# 文章”竞赛获奖作品
PDF 文件写入器是一个 C# 类库,允许 .NET 应用程序创建 PDF 文件。 - PDF 文件分析器(含 C# 解析类)
PDF 文件分析器旨在读取、解析和显示 PDF 文件的内部结构。
11. 历史记录
- 2012/03/30:版本 1.0 原始修订。
- 2012/05/03:版本 1.1
- InflateTree.cs
- 方法:InflateTree.BuildTree(...)
- 删除程序测试异常。请参见源代码中的注释
- ProcessFilesForm.cs
- 方法 ProcessFilesForm.ExtractFile()
- 修复调用 DecompressZipFile(...) 后的错误报告
- 2012/10/23 版本 1.2
- AddFilesAndFoldersForm.cs
- 方法:OnAddButton(...)
- 修复了在驱动器根目录下添加文件夹时出现的问题