古いlibarchive(含cmake)ではapache commons compressで作成された7z書庫を展開できない

## はじめに

私が提出した https://github.com/libarchive/libarchive/pull/1295 について、すでに解決済みではあるが、より詳細な調査をしたくなった。

## 圧縮エンジン

まず、[teamcity](https://www.jetbrains.com/ja-jp/teamcity/)のアーカイブのファイルリストを眺めたところ、`TeamCity/buildAgent/lib/commons-compress.jar`なるファイルが見つかった。これは[apache commons compress](https://commons.apache.org/proper/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](https://github.com/szcnick/p7zip/blob/dev/DOC/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.3[^1]|OK|
|macOS10.14 cmake 3.18.1|OK|
|Debian9 bsdtar 3.4.0[^2]|NG|
|Debian9 bsdtar 3.4.2[^3]|OK|
|Debian9 cmake 3.7.2/libarchive 3.4.0|NG|
|Debian9 cmake 3.7.2/libarchive 3.4.2[^4]|OK|

想定通り再現された。

## py7zlib

7zを展開する[Pure Python実装](https://github.com/fancycode/pylzma/blob/master/py7zlib.py)があるようだ。 これは圧縮ストリームのCRCをkCRCで受けることはあっていたが、中身の読み取りが誤っていた。 https://github.com/fancycode/pylzma/pull/74 にて修正を試みた。

## おわりに

apache commons compressのやつを食わせると死ぬデコーダは他にもありそう。

というか7zは仕様が複雑すぎるのでまともなデコーダを書くのはほぼ不可能ではないだろうか。。

[^1]: Homebrew。システムのlibarchiveは2.8.3であり、そもそも7zを開けない
[^2]: stretch-backports
[^3]: dependencyさえ気をつければDebian11のlibarchiveを導入可能
[^4]: Debian版のCMakeはdynamic binaryなのでlibarchive.soを交換可能