はじめに
私が提出した https://github.com/libarchive/libarchive/pull/1295 について、すでに解決済みではあるが、より詳細な調査をしたくなった。
圧縮エンジン
まず、teamcityのアーカイブのファイルリストを眺めたところ、TeamCity/buildAgent/lib/commons-compress.jar
なるファイルが見つかった。これはapache commons compressである。
書庫ファイル
apache commons compressを使って書庫を作成してみた( https://github.com/cielavenir/libarchive_1295_additional_check )。
Main.java/writer.sh/test.sh。writer.shで直接Main.javaを実行しているのはJava11以降の機能だが一旦classにすれば古いJavaでも十分実行可能なはずである。test.7zとして用意した。
このメタデータは以下のようになる。
01 04 06 00 02 09 8a e4 81 a9 0a 01 9e 89 a2 ea
55 5a 93 0e 00 07 0b 02 00 01 21 21 01 16 01 21
21 01 16 0c 9b 88 83 c7 0a 01 3a 27 02 9b 04 42
ad a8 00 08 00 00 05 02 11 2d 00 6c 00 69 00 63
00 65 00 6e 00 73 00 65 00 2e 00 74 00 78 00 74
00 00 00 4d 00 61 00 69 00 6e 00 2e 00 6a 00 61
00 76 00 61 00 00 00 14 12 01 00 80 6f d4 8c 67
01 d6 01 b0 3a 26 fa ed 6f d6 01 00 00
これを7zFormat.txtに従って頑張って解読すると以下のようになる。
0x01(kHeader) {
0x04(kMainStreamsInfo) {
0x06(kPackInfo) {
0x00(PackPos)
0x02(NumPackStreams)
0x09(kSize) [
0x8ae4 -> 2788
0x81a9 -> 425
]
0x0a(kCRC) 0x01(AllAreDefined) [
0xeaa2899e
0x0e935a55
]
0x00(kEnd)
}
0x07(kUnPackInfo) {
0x0b(kFolder) {
0x02(NumFolders)
0x00(External)
[
0x01(NumCoders)
0x21(CodecIdSize=1, Attributes)
0x21(Codec=LZMA2)
0x01(PropertiesSize)
0x16(Properties)
]*2
0x0c(kCodersUnPackSize) [
0x9b88 -> 7048
0x83c7 -> 967
]
0x0a(kCRC) 0x01(AllAreDefined) [
0x9b02273a
0xa8ad4204
]
0x00(kEnd)
}
0x08(kSubStreamsInfo) {
0x00(kEnd)
}
0x00(kEnd)
}
0x05(kFilesInfo) {
0x02(NumFiles)
0x11(kNames)
0x2d(Size)
0x00(External)
[
L"license.txt"
L"Main.java"
]
0x14(kMTime)
0x12(Size)
0x01(AllAreDefined)
0x00(External)
[
0x01d601678cd46f80 -> 2020-03-24 08:05:31
0x01d66fedfa263ab0 -> 2020-08-11 23:44:54
]
0x00(kEnd)
}
0x00(kEnd)
}
filetime変換は次のようになる。
def filetimeToUTC(n)
Time.at n/10000000-11644473600
end
また、可変長整数は、1バイト目の補数のclzバイト分リトルエンディアンが続く。例えば1110xxxxyyyyyyyyzzzzzzzzwwwwwwwwというビット列はxxxxwwwwwwwwzzzzzzzzyyyyyyyyという数を表す(まあ、手動でやるなら切れ目がどこかは目視でだいたい分かる)。
なお、7z.exeで複数ファイルを圧縮すると0x01(kHeader)ではなく0x17(kEncodedHeader)が使われるが、今回は割愛する。
libarchive 3.4.0以下の問題
kPackInfo中にCRCが出てくることがある。展開後ではなく圧縮ストリーム自体のCRCを記録するものである。これはヘッダ中ではkCRC(0x0a)に続くものであるが、libarchiveではkSize(0x09)に続くものとする処理になっていた。まあ、commons compressと違って7z.exeは圧縮ストリーム自体のCRCは記録しないので、気づかなくてもしかたない。
ただ、teamcity/bsdtarだけでなく、apache commons compress全体/cmakeでも再現されるので、実際には影響は当時の想定より大きかったはずである。cmakeは 5d8b3aec0cb8652ae867ff08d2e7bfa2060138dd より古いバージョンすなわり3.18未満が影響を受ける。3.18って(2020年8月時点で)最新版じゃないですか--;;;
古いlibarchiveで実際に確認してみる
bsdtarではtar tf test.7z
、cmakeではcmake -E tar tf test.7z
として確認。開けなければ、前者はtar: Damaged 7-Zip archive
、後者はCMake Error: Problem with archive_read_next_header(): Damaged 7-Zip archive
と表示される。
環境 | 結果 |
---|---|
Windows10 bsdtar 3.3.2 | NG |
Windows10 cmake 3.17.4 | NG |
Windows10 cmake 3.18.1 | OK |
macOS10.14 bsdtar 3.4.31 | OK |
macOS10.14 cmake 3.18.1 | OK |
Debian9 bsdtar 3.4.02 | NG |
Debian9 bsdtar 3.4.23 | OK |
Debian9 cmake 3.7.2/libarchive 3.4.0 | NG |
Debian9 cmake 3.7.2/libarchive 3.4.24 | OK |
想定通り再現された。
py7zlib
7zを展開するPure Python実装があるようだ。 これは圧縮ストリームのCRCをkCRCで受けることはあっていたが、中身の読み取りが誤っていた。 https://github.com/fancycode/pylzma/pull/74 にて修正を試みた。
おわりに
apache commons compressのやつを食わせると死ぬデコーダは他にもありそう。
というか7zは仕様が複雑すぎるのでまともなデコーダを書くのはほぼ不可能ではないだろうか。。