|
| 1 | +#+TITLE: 创建跨平台 ZIP 文件的隐藏陷阱:Extra Field |
| 2 | +#+AUTHOR: lujun9972 |
| 3 | +#+TAGS: linux和它的小伙伴,zip,兼容性 |
| 4 | +#+DATE: [2026-04-15 二 22:00] |
| 5 | +#+LANGUAGE: zh-CN |
| 6 | +#+OPTIONS: H:6 num:nil toc:t \n:nil ::t |:t ^:nil -:nil f:t *:t <:nil |
| 7 | + |
| 8 | +在 Linux 下用 =zip -r something.zip something/= 打包文件分享给朋友,结果对方在 iOS 上怎么也打不开——文件明明没损坏,换个人用 macOS 又能正常解压。 |
| 9 | + |
| 10 | +这背后的原因是 ZIP 格式中的 /Extra Field/ (扩展字段)。 |
| 11 | + |
| 12 | +* 什么是 Extra Field |
| 13 | + |
| 14 | +ZIP 格式规范中,每个文件条目除了基本的文件名、压缩数据之外,还可以携带一组可选的"扩展字段"(Extra Field)。这些字段用来存储标准 ZIP 头部没有覆盖的元数据。 |
| 15 | + |
| 16 | +Info-ZIP(Linux 上最常用的 =zip= 命令)默认会利用这个机制保存 Unix 特有的文件属性,包括: |
| 17 | +- Unix 文件权限(owner/group/other 的读写执行位) |
| 18 | +- UID 和 GID |
| 19 | +- 修改时间、访问时间等时间戳 |
| 20 | + |
| 21 | +按 ZIP 规范的设计,实现应该忽略自己不认识的 Extra Field,只处理能理解的。但现实中,某些平台的 ZIP 实现(特别是 iOS 上的某些解压工具)遇到包含 Extra Field 的 ZIP 文件时会直接报错,而不是优雅地忽略它们。 |
| 22 | + |
| 23 | +* 用 zipinfo 观察差异 |
| 24 | + |
| 25 | +我们可以用 =zipinfo= 命令直观地看到有无 Extra Field 的区别。先创建一个测试目录: |
| 26 | + |
| 27 | +#+BEGIN_SRC shell :exports both :results value code |
| 28 | + mkdir -p /tmp/zip-test/demo |
| 29 | + echo "hello world" > /tmp/zip-test/demo/hello.txt |
| 30 | + chmod 755 /tmp/zip-test/demo/hello.txt |
| 31 | +#+END_SRC |
| 32 | + |
| 33 | +分别用默认方式和 =-X= 选项打包: |
| 34 | + |
| 35 | +#+BEGIN_SRC shell :exports both :results output |
| 36 | + cd /tmp/zip-test |
| 37 | + zip -r with-extra.zip demo/ |
| 38 | + zip -rX without-extra.zip demo/ |
| 39 | +#+END_SRC |
| 40 | + |
| 41 | +#+RESULTS: |
| 42 | +: adding: demo/ (stored 0%) |
| 43 | +: adding: demo/hello.txt (stored 0%) |
| 44 | +: adding: demo/ (stored 0%) |
| 45 | +: adding: demo/hello.txt (stored 0%) |
| 46 | + |
| 47 | +用 =zipinfo= 查看包含 Extra Field 的版本: |
| 48 | + |
| 49 | +#+BEGIN_SRC shell :exports both :results output |
| 50 | + zipinfo /tmp/zip-test/with-extra.zip |
| 51 | +#+END_SRC |
| 52 | + |
| 53 | +#+RESULTS: |
| 54 | +: Archive: /tmp/zip-test/with-extra.zip |
| 55 | +: Zip file size: 328 bytes, number of entries: 2 |
| 56 | +: drwxr-xr-x 3.0 unx 0 bx stor 26-Apr-15 23:01 demo/ |
| 57 | +: -rwxr-xr-x 3.0 unx 12 tx stor 26-Apr-15 23:01 demo/hello.txt |
| 58 | +: 2 files, 12 bytes uncompressed, 12 bytes compressed: 0.0% |
| 59 | + |
| 60 | +注意输出中的 =unx= 和 =bx= / =tx= : =unx= 表示 Unix 格式的条目, =bx= / =tx= 中的 =x= 表示文件保留了可执行权限。 |
| 61 | + |
| 62 | +再看不含 Extra Field 的版本: |
| 63 | + |
| 64 | +#+BEGIN_SRC shell :exports both :results output |
| 65 | + zipinfo /tmp/zip-test/without-extra.zip |
| 66 | +#+END_SRC |
| 67 | + |
| 68 | +#+RESULTS: |
| 69 | +: Archive: /tmp/zip-test/without-extra.zip |
| 70 | +: Zip file size: 224 bytes, number of entries: 2 |
| 71 | +: drwxr-xr-x 3.0 unx 0 b- stor 26-Apr-15 23:01 demo/ |
| 72 | +: -rwxr-xr-x 3.0 unx 12 t- stor 26-Apr-15 23:01 demo/hello.txt |
| 73 | +: 2 files, 12 bytes uncompressed, 12 bytes compressed: 0.0% |
| 74 | + |
| 75 | +用 =zipinfo -v= 可以看到更详细的差异: |
| 76 | + |
| 77 | +#+BEGIN_SRC shell :exports both :results output |
| 78 | + zipinfo -v /tmp/zip-test/with-extra.zip | grep -E "(length of extra field|central-directory extra field|subfield with ID)" |
| 79 | +#+END_SRC |
| 80 | + |
| 81 | +#+RESULTS: |
| 82 | +: length of extra field: 24 bytes |
| 83 | +: The central-directory extra field contains: |
| 84 | +: - A subfield with ID 0x5455 (universal time) and 5 data bytes. |
| 85 | +: - A subfield with ID 0x7875 (Unix UID/GID (any size)) and 11 data bytes: |
| 86 | +: length of extra field: 24 bytes |
| 87 | +: The central-directory extra field contains: |
| 88 | +: - A subfield with ID 0x5455 (universal time) and 5 data bytes. |
| 89 | +: - A subfield with ID 0x7875 (Unix UID/GID (any size)) and 11 data bytes: |
| 90 | + |
| 91 | +对比不含 Extra Field 的版本: |
| 92 | + |
| 93 | +#+BEGIN_SRC shell :exports both :results output |
| 94 | + zipinfo -v /tmp/zip-test/without-extra.zip | grep "extra field" |
| 95 | +#+END_SRC |
| 96 | + |
| 97 | +#+RESULTS: |
| 98 | +: length of extra field: 0 bytes |
| 99 | +: length of extra field: 0 bytes |
| 100 | + |
| 101 | +文件大小的差异也很明显:328 bytes vs 224 bytes,差了 104 bytes,正好是两个条目各 24 bytes 的 Extra Field(包含 Universal Time 和 Unix UID/GID)加上头部开销。 |
| 102 | + |
| 103 | +* 解决方案 |
| 104 | + |
| 105 | +用 =-X= (或其长选项 =--no-extra= )排除 Extra Field: |
| 106 | + |
| 107 | +#+BEGIN_SRC shell |
| 108 | + zip -rX something.zip something/ |
| 109 | +#+END_SRC |
| 110 | + |
| 111 | +这样生成的 ZIP 文件就是纯标准格式,兼容性最好。 |
| 112 | + |
| 113 | +如果你的项目使用 Python 打包, =zipfile= 模块默认 *不会* 写入 Extra Field,所以无需特殊处理。但如果你显式设置了 =external_attr= 来保存 Unix 权限,则需要注意这个问题。 |
| 114 | + |
| 115 | +* 什么时候该保留 Extra Field |
| 116 | + |
| 117 | +=zip -rX= 并不是万能的最佳实践。以下场景 *应该* 保留 Extra Field: |
| 118 | + |
| 119 | +- *系统备份* :需要保留完整的 Unix 权限、所有者信息 |
| 120 | +- *在 Linux 之间传输* :两端都支持 Extra Field,保留元数据有好处 |
| 121 | + |
| 122 | +而以下场景 *应该排除* Extra Field (用 =-X= ): |
| 123 | + |
| 124 | +- *跨平台分享文件* :收件人可能在 macOS、Windows、iOS 等平台 |
| 125 | +- *上传到网站/网盘* :不确定下载方会用什么工具解压 |
| 126 | +- *只需文件内容* :不关心文件权限等元数据 |
| 127 | + |
| 128 | +一句话总结:分享给不确定的接收方时,用 =zip -rX= ;只在 Linux 内部流转时,保留默认行为即可。 |
0 commit comments