RubyKaigi の後夜祭で、akr さんが「327 種類の Ruby をビルドする方法 〜0.49 から 2.6.0-preview2 まで〜」という発表をされていました。

RubyKaigi 2018 After Party で話したスライドです: 「327 種類の Ruby をビルドする方法 ~0.49 から 2.6.0-preview2 まで ~」 https://t.co/J5MXgM2PNN

その中で、ruby-0.62.tar.gz と ruby-0.63.tar.gz のファイルは「gzip 形式じゃないといわれて展開できない」ということで、ビルド対象から外されていました。

いろいろやって、めでたくこの 2 ファイルを復活させることに成功しました。そのプロセスを書きます。

なお、壊れていたファイルも記念として次の URL に残されています（拡張子が .broken になってます）。

どのように壊れているのかを突き止める

とりあえず当該ファイルをダウンロードし、file してみました。

$ file ruby-0.62.tar.gz ruby-0.63.tar.gz ruby-0.62.tar.gz: data ruby-0.63.tar.gz: data

にべもありません。しょうがないので、バイナリを観察しました。

$ od -c ruby-0.62.tar.gz 0000000 " 253 G 342 302 ; " ; " 243 Y ' 324 270 354 0000020 210 200 003 262 * 373 016 276 \ 304 325 346 276 e 177 212 0000040 327 214 u 021 030 231 X F G 342 ] 231 265 343 216 x 0000060 253 016 ` 222 Y 251 235 255 273 " * " 253 b \v - 0000100 [ C N 217 016 257 \f $ 366 177 361 r Z 360 256 310 0000120 210 016 310 250 " ( < 221 207 . I ? & H 267 004 0000140 021 262 H N 310 210 016 310 253 211 306 p 035 211 226 d 0000160 Y * 324 273 " * " 253 D 332 257 373 336 353 326 313 0000200 F P 266 006 v 6 266 235 } ~ 207 w 337 o } 0000220 366 257 212 305 ; O v t 032 033 333 v 275 w 024 336 0000240 226 034 330 326 322 373 " * 240 226 d Y * 324 273 " 0000260 * " 253 B 336 343 214 y 232 032 i 027 332 2 W 336 0000300 206 206 Z 332 [ Z 333 232 266 { \ \r 0 346 ( 354 0000320 m { N u 274 255 002 \f 217 364 \ 1 005 X 273 016 0000340 { " * 240 226 d Y * 324 273 " * " 253 o 312 0000360 374 256 373 346 371 334 j 177 230 8 026 T = 023 \a 356 0000400 322 177 347 C W ~ 347 201 203 221 330 330 332 334 374 350 0000420 375 ; 332 032 233 234 250 ? 246 366 346 273 " * 240 226 0000440 d Y * 324 273 " * " 253 V 366 226 207 [ j ? 0000460 365 251 267 s 354 q 251 320 S ? | 377 327 177 357 367 0000500 263 224 ; 353 l a 247 C { * 036 210 220 203 ] 0000520 322 035 Q U 354 210 200 354 212 275 317 262 " 252 004 273 0000540 " * " 253 Q g ; 032 025 031 a 367 004 T 005 \ 0000560 315 016 307 K s 261 221 275 222 263 324 y 236 < q w 0000600 b 234 201 354 210 200 354 212 266 242 M 245 r 356 275 226 0000620 030 236 326 257 h 357 262 " 252 004 273 " * " 253 | 0000640 \r 351 J " c ( 254 5 316 356 227 366 302 357 343 8 0000660 O ; c - 250 211 m ] 355 < 326 247 225 305 264 362 0000700 M 023 374 c Q 1 263 205 230 362 352 357 ; " * 240 0000720 357 262 " 252 004 273 " * " 253 ~ 7 200 347 271 002 0000740 366 216 { D 313 A L 304 237 030 ~ L 271 337 0 363 0000760 251 274 377 \t 220 222 O x \r 304 346 z 266 223 227 240 0001000 265 314 226 254 _ c 262 " 003 262 * _ 262 " 252 004 0001020 273 " * " 253 q 363 351 177 341 2 376 026 337 8 302 0001040 3 323 e > C o 244 R 263 374 & D 263 330 i 307 0001060 ` D 024 B ! c 374 223 _ 243 305 303 c 354 210 200

ダブルクォートとアスタリスクが多いな、という印象ですが、さっぱりわかりません。ただ、NUL 文字で埋まっているとかでもないので、完全に無意味なデータとも思えません。

そこでまず、Ruby の歴史を文献調査しました。高橋会長の Ruby 年表によると、Ruby 0.62 は itojun 氏提供のクローズドなメーリングリストで配布されていたらしい。メール配布でデータが壊れる原因と言えば、BASE64 や uuencode などのバイナリ・テキスト変換が頭をよぎります。

そうこうしていると、shinh さんから重要なヒントが。

壊れているというよりはなんらかの圧縮フォーマットな気がしますねえ。0.62と0.63は、e602の地点まで、"22ab"というパターンが118バイトごとに499回出てきて、そこからは特徴が見出せてなくて。圧縮用のなにかの後に本体が入ってるとかなのかなあ…とか。ただそれだと本体部分が短すぎる気もします — shinichiro hamaji (@shinh) 2018年6月6日

周期は118じゃなくて59バイトでした。xxd -c 59とかすると綺麗 — shinichiro hamaji (@shinh) 2018年6月6日

xxd -c 59 の結果はこちら。

00000000: 22ab 47e2 c23b 2220 3b22 a359 27d4 b8ec 8880 03b2 2afb 0ebe 5cc4 d5e6 be65 7f8a d78c 7511 1899 5846 47e2 5d99 b5e3 8e78 ab0e 6092 59a9 9dad bb22 2a ".G..;" ;".Y'.......*...\....e....u...XFG.]....x..`.Y...."* 0000003b: 22ab 620b 2d5b 434e 8f0e af0c 24f6 7ff1 725a f0ae c888 0ec8 a822 283c 9187 2e49 3f26 48b7 0411 b248 4ec8 880e c8ab 89c6 701d 8996 6459 2ad4 bb22 2a ".b.-[CN....$...rZ......."(<...I?&H....HN.......p...dY*.."* 00000076: 22ab 44da affb deeb d6cb 4650 b606 7636 b69d 207d 7e87 77df 6f7d f6af 8ac5 3b4f 7674 1a1b db76 bd77 14de 961c d8d6 d2fb 222a a096 6459 2ad4 bb22 2a ".D.......FP..v6.. }~.w.o}....;Ovt...v.w........"*..dY*.."* 000000b1: 22ab 42de e38c 799a 1a69 17da 3257 de86 865a da5b 5adb 9ab6 7b5c 0d30 e628 ec6d 7b4e 75bc ad02 0c8f f45c 3105 58bb 0e7b 222a a096 6459 2ad4 bb22 2a ".B...y..i..2W...Z.[Z...{\.0.(.m{Nu......\1.X..{"*..dY*.."* 000000ec: 22ab 6fca fcae fbe6 f9dc 6a7f 9838 1654 3d13 07ee d27f e743 577e e781 8391 d8d8 dadc fce8 fd3b da1a 9b9c a83f a6f6 e6bb 222a a096 6459 2ad4 bb22 2a ".o.......j..8.T=......CW~...........;.....?...."*..dY*.."* 00000127: 22ab 56f6 9687 5b6a 3ff5 a9b7 73ec 71a9 d053 3f7c ffd7 7fef f7b3 943b eb20 6c61 a743 7b2a 1e88 9083 5dd2 1d51 55ec 8880 ec8a bdcf b222 aa04 bb22 2a ".V...[j?...s.q..S?|.......;. la.C{*....]..QU........"..."* 00000162: 22ab 5167 3b1a 1519 61f7 0454 055c cd0e c74b 73b1 91bd 92b3 d479 9e3c 7177 629c 81ec 8880 ec8a b6a2 4da5 72ee bd96 189e d6af 68ef b222 aa04 bb22 2a ".Qg;...a..T.\...Ks......y.<qwb.........M.r.......h.."..."* 0000019d: 22ab 7c0d e94a 2263 28ac 35ce ee97 f6c2 efe3 384f 3b63 2da8 896d 5ded 3cd6 a795 c5b4 f24d 13fc 6351 31b3 8598 f2ea ef3b 222a a0ef b222 aa04 bb22 2a ".|..J"c(.5.......8O;c-..m].<......M..cQ1......;"*..."..."* 000001d8: 22ab 7e37 80e7 b902 f68e 7b44 cb41 4cc4 9f18 7e4c b9df 30f3 a9bc ff09 9092 4f78 0dc4 e67a b693 97a0 b5cc 96ac 5f63 b222 03b2 2a5f b222 aa04 bb22 2a ".~7......{D.AL...~L..0.......Ox...z........_c."..*_."..."* 00000213: 22ab 71f3 e97f e132 fe16 df38 c233 d365 3e43 6fa4 52b3 fc26 44b3 d869 c760 4414 4221 63fc 935f a3c5 c363 ec88 80ec 8a82 459f e1b7 b222 aa04 bb22 2a ".q....2...8.3.e>Co.R..&D..i.`D.B!c.._...c......E...."..."* 0000024e: 22ab 57ab 16ef 0d31 4af4 d8df 1879 9388 4e7c 4b4b c885 3ec8 880e c8aa 4d66 6a6a 3bce 49d8 0c63 ea73 1e58 bf1e bfc9 c402 e501 10af b222 aa04 bb22 2a ".W....1J....y..N|KK..>.....Mfjj;.I..c.s.X..........."..."* 00000289: 22ab 52b8 5cb9 aaf8 54dc fae6 6bde c888 0ec8 a812 bc1a f0fa 4812 f8ec 66f2 6716 044f 0aaa be3d 9b80 e89d 2e98 bf7b 9097 b2b2 456f b222 aa04 bb22 2a ".R.\...T...k...........H...f.g..O...=.......{....Eo."..."* 000002c4: 22ab 5a5d b673 b8a6 fe49 683e eda5 b599 ad51 57b9 1967 31ca c4f5 de0a bf8b 7810 fb22 203b 22a9 8f2c 65f8 6be3 6c12 ba01 fc6c 0dfb b222 aa04 bb22 2a ".Z].s...Ih>.....QW..g1.......x.." ;"..,e.k.l....l..."..."* 000002ff: 22ab 52c2 06d5 af81 6c09 ad05 1c93 2c8b fcfb 2220 3b22 ab9f dba8 33ec 8880 ec8a b08a e0a5 8533 669e 3ec8 880e c8ab 2fc5 0bc8 cd04 4636 9fb2 2203 b2 ".R.....l.....,..." ;"....3..........3f.>...../.....F6..".. 0000033a: 22ab 4bbc cbfe 0b2c 2740 2467 44ae 0599 82d2 ef0c 3c73 7525 e085 a127 e738 7acc e7cb 4f08 0548 719e e125 5e27 bf4f 1fbb 222a a004 4636 9fb2 2203 b2 ".K....,'@$gD.......<su%...'.8z...O..Hq..%^'.O.."*..F6..".. 00000375: 22ab 7572 6d1a f013 c5a3 1a06 ec88 80ec 8ab1 a98e ec68 62c8 3db3 9c56 cabb cf68 1963 5888 b79a e142 7c1a a8b7 6c40 1926 425d 47ec 8880 aa02 2203 b2 ".urm................hb.=..V...h.cX....B|...l@.&B]G.....".. 000003b0: 22ab 693b 9d46 c8dd 3950 ac8e 2b27 75bb 8353 fc8f 2b11 376c 28d1 e919 429f 5133 10ad 990c a835 8708 3e28 eda8 7959 e5fb 222a a0ec 8880 aa02 2203 b2 ".i;.F..9P..+'u..S..+.7l(...B.Q3.....5..>(..yY.."*......".. 000003eb: 22ab 6b4c 1cd7 d1dd f3f7 bbf4 c83f 2f69 e961 93da 5af4 3dc6 742e 8c6d cf8f ab5e fb22 203b 22a4 2fd4 7bce a1f7 7aec 8880 ec8a 9eb3 3b5c e170 fb22 2a ".kL.........?/i.a..Z.=.t..m...^." ;"./.{...z.......;\.p."* 00000426: 22ab 45bd cec8 880e c8aa 127a 77b2 2203 b22a 65d7 ed72 4678 7a28 06ac 8482 9a80 8378 eec8 880e c8aa 0301 cbca 1d33 b222 03b2 2abf 9cbc 8974 56d0 45 ".E........zw."..*e..rFxz(.......x...........3."..*....tV.E

各行が必ず 22ab で始まります（shinh さんの言うとおり、この傾向は最初の約 59000 バイトまでで、その後は規則性がなくなります）。

ここで、各行の 3 文字目がだいたいアルファベットに揃っていることに気づきました。これは、3 文字目の上位 2 ビットが固定されている兆候です。つまり、やはり BASE64 ではないか？と思い、59 バイトずつ BASE64 エンコードしてみると

$ ruby -e 'File.binread("ruby-0.62.tar.gz").scan(/.{59}/m) {|s| puts [s].pack("m0") }' IqtH4sI7IiA7IqNZJ9S47IiAA7Iq+w6+XMTV5r5lf4rXjHURGJlYRkfiXZm14454qw5gklmpna27Iio= IqtiCy1bQ06PDq8MJPZ/8XJa8K7IiA7IqCIoPJGHLkk/Jki3BBGySE7IiA7Iq4nGcB2JlmRZKtS7Iio= IqtE2q/73uvWy0ZQtgZ2NradIH1+h3ffb332r4rFO092dBob23a9dxTelhzY1tL7IiqglmRZKtS7Iio= IqtC3uOMeZoaaRfaMlfehoZa2lta25q2e1wNMOYo7G17TnW8rQIMj/RcMQVYuw57IiqglmRZKtS7Iio= Iqtvyvyu++b53Gp/mDgWVD0TB+7Sf+dDV37ngYOR2Nja3Pzo/TvaGpucqD+m9ua7IiqglmRZKtS7Iio= IqtW9paHW2o/9am3c+xxqdBTP3z/13/v97OUO+sgbGGnQ3sqHoiQg13SHVFV7IiA7Iq9z7IiqgS7Iio= IqtRZzsaFRlh9wRUBVzNDsdLc7GRvZKz1HmePHF3YpyB7IiA7Iq2ok2lcu69lhie1q9o77IiqgS7Iio= Iqt8DelKImMorDXO7pf2wu/jOE87Yy2oiW1d7TzWp5XFtPJNE/xjUTGzhZjy6u87Iiqg77IiqgS7Iio= Iqt+N4DnuQL2jntEy0FMxJ8Yfky53zDzqbz/CZCST3gNxOZ6tpOXoLXMlqxfY7IiA7IqX7IiqgS7Iio= Iqtx8+l/4TL+Ft84wjPTZT5Db6RSs/wmRLPYacdgRBRCIWP8k1+jxcNj7IiA7IqCRZ/ht7IiqgS7Iio= IqtXqxbvDTFK9NjfGHmTiE58S0vIhT7IiA7Iqk1mamo7zknYDGPqcx5Yvx6/ycQC5QEQr7IiqgS7Iio= IqtSuFy5qvhU3Prma97IiA7IqBK8GvD6SBL47GbyZxYETwqqvj2bgOidLpi/e5CXsrJFb7IiqgS7Iio= IqtaXbZzuKb+SWg+7aW1ma1RV7kZZzHKxPXeCr+LeBD7IiA7IqmPLGX4a+NsEroB/GwN+7IiqgS7Iio=

いくつか気づくことがあります。

綺麗に先頭が Iqt で揃っている。よくわからないけれど冗長なデータ。

最後の方の 10 文字くらいは、なんだか前の行と同じことが多い。つまり、59 バイト全部に情報が詰まっているわけではなさそう。*1

先頭の Iqt を含め、Iq が頻出している。これがダブルクォート頻出の正体ですね。

さて、冒頭の Iqt が何なのかはともかく、あきらかに冗長なデータなので、展開に必要な情報ではありません。よって、これを取り除いて BASE64 をデコードしてみようと思うのは自然なことです。

$ ruby -e 'print [File.binread("ruby-0.62.tar.gz")[0, 59]].pack("m0")[3, 60].unpack("m").first' | xxd 00000000: 1f8b 08ec 8880 ec8a 8d64 9f52 e3b2 2200 .........d.R..". 00000010: 0ec8 abec 3af9 7313 579a f995 fe2b 5e31 ....:.s.W....+^1 00000020: d444 6265 6119 1f89 7666 d78e 39 .Dbea...vf..9

"1f 8b 08" がでてきました。これは gzip のファイルヘッダです。冒頭 3 文字が偶然一致するとは思えないので、やはりこれは tar.gz のようです。しかし、このままでは gzip 展開できません。

$ ruby -e 'puts [File.binread("ruby-0.62.tar.gz")[0, 59]].pack("m0")[3, 60].unpack("m").first' | gzip -cd gzip: stdin is encrypted -- not supported

正常に展開できる ruby-0.60.tar.gz や ruby-0.64.tar.gz を見ると、ヘッダは "1f 8b 08 00 (タイムスタンプ 4 バイト) 00 03" となるのが正しそうです。しかし、上のように、"1f 8b 08 ec" となっているのでダメなようです。

もう少しヒントを得るために、タイムスタンプを探すことにしました。ruby-0.60.tar.gz のタイムスタンプは 0x2ee6c66d (1994-12-08 17:40:13 +0900) 、ruby-0.64.tar.gz のタイムスタンプは 0x2f1267f7 (1995-01-10 19:56:55 +0900) なので、この間の数値と思われます。0x2e か 0x2f のビットパターン、つまり 00101110 か 00101111 を探します。

$ ruby -e 'print [File.binread("ruby-0.62.tar.gz")[0, 59]].pack("m0")[3, 60].unpack("m").first' | xxd -b 00000000: 00011111 10001011 00001000 11101100 10001000 10000000 ...... 00000006: 11101100 10001010 10001101 01100100 10011111 01010010 ...d.R 0000000c: 11100011 10110010 00100010 00000000 00001110 11001000 .."... 00000012: 10101011 11101100 00111010 11111001 01110011 00010011 ..:.s. 00000018: 01010111 10011010 11111001 10010101 11111110 00101011 W....+ 0000001e: 01011110 00110001 11010100 01000100 01100010 01100101 ^1.Dbe 00000024: 01100001 00011001 00011111 10001001 01110110 01100110 a...vf 0000002a: 11010111 10001110 00111001 ..9

発見。

$ ruby -e 'print [File.binread("ruby-0.62.tar.gz")[0, 59]].pack("m0")[3, 60].unpack("m").first' | xxd -b 00000000: 00011111 10001011 00001000 11101100 10001000 10000000 ...... 00000006: 11101100 10001010 10001101 01100100 10011111 01010010 ...d.R ^~~~ 0000000c: 11100011 10110010 00100010 00000000 00001110 11001000 .."... ~~~~

ここからリトルエンディアンでタイムスタンプの 4 バイトを取り出すと、

$ ruby -e 'print [File.binread("ruby-0.62.tar.gz")[0, 59]].pack("m0")[3, 60].unpack("m").first' | xxd -b 00000000: 00011111 10001011 00001000 11101100 10001000 10000000 ...... 00000006: 11101100 10001010 10001101 01100100 10011111 01010010 ...d.R ^~~~ ~~~~^~~~ ~~~~^~~~ ~~~~^~~~ 0000000c: 11100011 10110010 00100010 00000000 00001110 11001000 .."... ~~~~

0x2ef549d6 (1994-12-19 17:52:38 +0900) になります。夕方なのがそれっぽい *2 。

タイムスタンプっぽいビットパターンを見つけた。



ruby-0.60.tar.gz 1994-12-08 17:40:13 +0900

ruby-0.62.tar.gz 1994-12-19 17:52:38 +0900

ruby-0.63.tar.gz 1994-12-20 14:37:12 +0900

ruby-0.64.tar.gz 1995-01-10 19:56:55 +0900



それっぽい？ — Yusuke Endoh (@mametter) 2018年6月6日

ということで、"1f 8b 08 00 (タイムスタンプ 4 バイト) 00 03" のうち、最初の 3 バイトとタイムスタンプの位置は特定できました。が、間の 00 はどこへ行ったのか。

BASE64 なり uuencode なり、6 ビットごとに区切ったデータにノイズが混ざったのではないか、という確信を得つつあったので、6 ビットごとに区切って観察します。

$ ruby -e 'print File.binread("ruby-0.62.tar.gz").unpack1("B*").scan(/.{6}/)[0, 20].join(" ")' 001000 101010 101101 000111 111000 101100 001000 111011 001000 100010 000000 111011 001000 101010 001101 011001 001001 111101 010010 111000 ^~~~~~ ~~^~~~ ~~~~^~ ~~~~~~ ^~~~~~ ^~~~ ~~~~^~ ~~~~~~ ^~~~~~ ~~^~~~ ~~~~ 1f 8b 08 ?????? timestamp (4 bytes)

いま探しているのは 00 なので、?????? の部分が目につきます。これに着目すると、000000 が "111011 001000 100010" と "111011 001000 101010" で挟まれているんじゃないか？と感じました。他の行も探してみると、どうも "111011 001000 100010" や "111011 001000 101010" は頻出で、その間は 000000 であることに気づきます。まるで、000000 をエスケープするためのモード切替のよう。

と感じた瞬間にピンと来ました。モード切替といえば、ISO-2022-JP のエスケープシーケンスだ！

ISO-2022-JP（いわゆる JIS 文字コード）は、"ESC ( B" という 3 文字で ASCII モードに、"ESC ( J" という 3 文字で日本語モードに切り替わる文字エンコーディングです。これが "111011 001000 100010" や "111011 001000 101010" に対応しているんじゃないか。

BASE64 で 000000 は "A" の文字で、"A" だけエスケープされる理由は謎だったので、uuencode を疑います。uuencode はよく知らなかったので、Wikipedia を調べます。

デコードにおいては、変数 c にエンコードされた文字のASCIIコードが入っているとすると (c XOR 0x20) AND 0x3f でデコードできる（範囲外の文字が入っている可能性は考慮していない）。 uuencode - Wikipedia

早速 "ESC ( B" をこれでデコードしてみます。

$ ruby -e '"\e(B".bytes {|c| p "%06b" % ((c ^ 0x20) & 0x3f) }' "111011" "001000" "100010" $ ruby -e '"\e(J".bytes {|c| p "%06b" % ((c ^ 0x20) & 0x3f) }' "111011" "001000" "101010"

ビンゴ！ みごと、問題の断片がでてきました。

古い uuencode で 000000 は、空白文字になります。よって、何らかのアクシデントで、uuencode されたデータの中の空白文字は ASCII モードに、空白文字以外は日本語モードになるように、エスケープシーケンスが混ざってしまったのでしょう（最初の 59000 バイトだけそうなったのは、この時点では原因不明でしたが、あとでわかります）。

こうして見直すと、各行冒頭の "001000 101010" も、エスケープシーケンスの 2 文字目と 3 文字目なんじゃないかと気づきます。1 文字目はどこに行ったのか。

とあります。「M」ではなく ESC 文字がオクテット数として解釈されたのでは？と思い至り、調べます。すると ESC 文字は 0b111011 、つまり 59 です。これが、shinh さんの指摘した 59 バイト周期の理由。本当は 45 オクテットしかないのに、59 オクテットあるとみなして無理やりデコードされたのでした（ちなみに各行 3 つめの 101101 は、uuencode で「M」で、Wikipedia に書いてある本来のオクテット数です）。

59 バイトの終盤部分が妙に繰り返していたのも、45 オクテット分の情報しかないのに 59 オクテットとしてデコードしたため、バッファの終わりのほうがちゃんと初期化されなかったからでしょう。すべてが腑に落ちていきます。

ということで、

とにかく、ruby-0.62.tar.gz の正体はほぼ明らかになった。本来の tar.gz を uuencode → 不幸にも ISO-2022-JP のエスケープシーケンスが混ざる → 無理やり uudecode 、で得られたバイナリのようです。 — Yusuke Endoh (@mametter) 2018年6月6日

という確信を得ました。これを実証するには、エスケープシーケンスっぽい 3 文字を取り除いて uudecode してみればいいのです。

pic.twitter.com/PrqqCgBlPv — Yusuke Endoh (@mametter) 2018年6月6日

めでたく（冒頭のみですが）gzip 展開できて、tar っぽいデータがでてきました。やった！