やっほ-BBS アーカイブ / 技術編

31年前のBBSのバイナリを読んだら、動いたコードが2回とも間違っていた

mmm ホストプログラムのバイナリ解析と、11,411本の書き込みの復元

アーカイブ本体へ

1991年から1994年まで、兵庫県の自宅で草の根BBSを運営していました。「やっほ-BBS」といいます。PC-9801VX21にモデムを繋いで、最初は1回線、最後は3回線。

先日そのディスクが出てきて、ホストマシンのディレクトリがまるごと残っていました。会議室のデータ、会員がアップロードしたファイル、通話ログ。1994年5月で更新が止まったままです。

読み出すためのプログラムをGoで書きました。結果として11,411本の書き込みを取り戻したのですが、その過程で自分が最初に書いたコードが、存在しない本文を出力していたことが分かりました。しかも、同じ種類の間違いをこのあともう一度繰り返します。この記事はその話です。

先に、戻ってきたものから

長い記事なので、結果を先に置きます。

会員がアップロードしたCGが20枚出てきました。PC-9801の640×400・16色が18枚、FM TOWNSの640×480・256色が2枚。全部PNGになって、いまMacのプレビューでそのまま開きます。31年前にモデム越しに何十分もかけて落とされていた絵が、ダブルクリックで出てきます。

会員が自分で作った画像フォーマットも出てきました。MAGでもPiでもない独自形式で、ローダーごと自作されています。仕様書は本人が書き残していましたが、肝心の圧縮方式だけ「ソースを見たほうが速い」と省略されていて、そのソースは配布されていませんでした。実データから解きました。

会員が作ったシューティングゲームが動きました。 1992年12月のものです。PC-9801はもう手元にないので、DOSBox-XのPC-98モードで、当時の起動バッチと同じ順番に流しました。オープニングが流れ、音が鳴りました。

ここまで来るのに、形式を4つ解く必要がありました。掲示板の索引、LZH、MAG、そしてその独自形式です。

そして2回、同じ間違いをしました。 どちらも、動いたので正しいと思っていました。その話をします。

先に断っておきます。この記事で扱うのは、掲示板の書き込みと、FILERにアップロードされたファイルだけです。どちらも当時から会員に開かれていた場所のものです。メール、チャット、プロフィール、入会申請といった私的なものは、暗号化して封印してあります。公開しませんし、この記事でも扱いません。

ホストプログラム「mmm」

会議室のデータは、.IDX と .TXT という対になったバイナリファイルで保存されていました。当然、今のソフトでは開けません。仕様書もありません。

.IDX を16進で眺めるところから始めました。同じ間隔で規則的なパターンが出てくるので固定長レコードです。区切りを数えると114バイト。1レコードが1本の書き込みに対応していました。

0..40    (41 バイト)  タイトル          Shift_JIS、NUL埋め
41..44   ( 4 バイト)  投稿日時          UNIX時間 int32 LE
45..48   ( 4 バイト)  更新日時          UNIX時間 int32 LE
49..57   ( 9 バイト)  ハンドル          ASCII、NUL終端
58..69   (12 バイト)  表示名            Shift_JIS、NUL終端
70..74   ( 5 バイト)  用途不明
75..78   ( 4 バイト)  本文の位置        .TXT内のオフセット int32 LE
79..82   ( 4 バイト)  本文の長さ
83..113  (31 バイト)  用途不明(「!」付き記事で非ゼロ)
タイトル4144912544用途不明310417083114
1レコード114バイトを実際の幅で並べたもの。フィールドの並びは上の表と同じ順で、上の数字が各フィールドのバイト数、下が先頭からの位置。橙の36バイトは、いまも用途が分かっていません。

日時がUNIX時間だったのは助かりました。1991年9月の値を10進で読むと687,000,000あたりになるので、当たりを付けやすい。

.IDX が索引で、本文は .TXT の中にオフセットと長さで指し示されている。ここまでは素直な設計です。

本文が見つからない

問題は、.IDX に対応する .TXT が無い会議室があったことです。

最初に疑ったのは、ファイル番号のズレでした。00000012.IDX に対する 00000012.TXT が無いなら、00000011.TXT が本当の相方なのではないか。ホストが索引だけ振り直した、という仮説です。

これを実装して、動きました。本文が出てきます。

間違いでした。

確かめるために、総当たりで検証するコードを書きました。ディレクトリ内の全 .IDX × 全 .TXT の組み合わせを試して、どれが本当に対応するかを調べる。判定材料はあります。本文の各メッセージの先頭には、日時とハンドルを含むヘッダ行が付いているのです。

0..16    (17 バイト)  投稿日時   "YY/MM/DD HH:MM:SS"
17..20   ( 4 バイト)  空白
21..28   ( 8 バイト)  ハンドル
29..44   (16 バイト)  表示名
45..96   (52 バイト)  タイトル

つまり .IDX のレコードが指す位置を実際に読みに行って、そこにそのレコード自身の日時とハンドルが書かれているかを確認できます。1本でも偶然一致することはあっても、スレッド全部が偶然合うことはありません。

この基準で総当たりした結果、番号のズレによる対応は1件も見つかりませんでした。

仮説が違ったというだけなら、まだよかったのですが。

嘘の本文が出ていた

同じ検証で、もっと悪いことが分かりました。

番号をずらして本文を読む処理は、検証をしていませんでした。 オフセットの位置にたまたま何か文字が入っていれば、それを本文として返してしまう。つまり、あるスレッドのタイトルの下に、まったく別のスレッドの本文が表示されていたわけです。

エラーは出ません。文字化けもしません。日本語として読める文章が出てきます。31年ぶりのアーカイブなので、内容がおかしくても「そんなこともあったかな」で通ってしまう。気づく手がかりがありません。

この処理を削除しました。削除すると、それまで出力されていた132本の本文が消えます。一瞬デグレに見えますが、その132本が全部でたらめだったということです。

代わりに、全レコードが検証を通った組み合わせだけを採用する方式にしました。同名ファイルを最優先で対応付け、残りは検証に通ったものだけを候補にする。

その上で、索引経由で対応が付いた6,991本すべてを、.IDX の値と本文ヘッダの値で突き合わせました。

6,946本が完全一致。残る45本は、.IDX 側の表示名(12バイト)とタイトル(41バイト)が短すぎて途中で切れているものでした。

つまり食い違いは1件もなく、45本については .IDX より本文ヘッダのほうが正しい。以降は、表示名とタイトルは本文ヘッダの値を優先しています。

索引の無い本文ファイル

ここまでで「本文の無い索引」を扱いましたが、逆もありました。索引が無いのに本文ファイルだけ残っている。

本文ヘッダの構造は既に分かっているので、こちらは .IDX 無しで読めます。ヘッダ行の正規表現でメッセージ境界を切り出せばいい。

ただし、ここには罠があります。「1994/05/26 の件ですが」のように日付で始まる本文を、ヘッダ行と誤認してはいけません。日時の後ろに必ず空白4文字が続き、その次が空白以外であることまで要求して切り分けています。

対応の付かない .TXT ファイルは118本ありました。ここから、3,006本の書き込みが追加で読み出せました。

索引を失った会議室が、そのまま蘇ったことになります。

消えていた1,505本

一方で、最後まで本文が見つからなかった書き込みが1,505本あります。

タイトルも、書いた人も、日付も残っている。本文だけがない。当時ディスクの空きを作るために、本文ファイルを消したのだと思います。120MBのハードディスクでやりくりしていた時代です。当時としては妥当な判断でした。31年後に読み返すことになるとは考えていなかった、というだけです。

これらは復元できません。出力にも「本文なし」と明記して、埋めないことにしています。

固定長が2バイト文字を割る

Shift_JISの話も少し。

固定長フィールドは、当然ながら文字の境界を尊重してくれません。離席中/やっほ は13バイトあるので、12バイトの表示名フィールドに入れると ほ の1バイト目で切れます。デコーダに渡すと U+FFFD、つまり やっ� になります。

これは本文ヘッダ側(16バイト)から完全な値が取れるので直せました。

厄介だったのはもう一方です。あるタイトルが 【Res.54 で終わっていて、】 が来ない。最初、末尾のバイトを落とす処理を書いたのですが、化けが消えませんでした。

原因は順序でした。mmm は本文ヘッダのフィールドを空白で埋めます。つまりデータは ...【Res.54\x81 の後ろに空白が続く形になっている。先にデコードすると、\x81 と直後の空白を1つの壊れた2バイト文字として読んでしまう。

空白を削ってから、末尾に残った中途半端な1バイトを落とす。 順序を入れ替えただけで解決しました。

ただし、この 】 は復元していません。.IDX のフィールド幅の問題ではなく、1993年当時のmmm自身が40バイトで切って書き込んでいたので、閉じ括弧はアーカイブのどこにも存在しないからです。読みやすさのために足すのは、記録を作ることになります。

ついでに解いた形式

会議室以外にも、解かないと読めないものがありました。

LZH — FILERにアップロードされたファイルの多くは書庫でした。方式は -lh4-/-lh5-、LZSS + ブロックハフマンです。参照実装(lhafile パッケージの lzhlib.c)を手元で動かして既知の正解を作り、自分の実装とバイト単位で突き合わせました。ただし -lh4- は参照実装が対応しておらず、「窓が4KBになるだけで他は同一」という文書上の記述からの外挿です。実データでは正しく展開できましたが、別実装との照合はしていません。そう明記してあります。

MAG(MAKI02) — 当時PC-98のBBSでCGをやり取りするのに使われていた画像形式です。これはRustのクレートから移植しました。

ここで一度失敗しています。最初、Webで見つかる解説を元に実装したところ、内部で辻褄の合わないコードができました。ビットフラグ列をLSB-firstとして書いてしまったのですが、実際にそのクレートが依存している bit-vec はMSB-firstと明記されています。要約は、依存先の仕様までは面倒を見てくれません。

cargo fetch でソースを取得して読み直し、行単位で移植し直しました。まずFILERから拾った4枚で、640×400の16色イラストとして破綻なく展開されることを確認しています。

31年前のCGをMacで開く

MAGを解いたのは、それ単体で必要だったからではありません。FILER——会員がファイルをアップロードして共有する、ダウンロードコーナーがあります。ここに何が入っているのか見たかった。

ところが、1枚の絵を表示するまでに関門が3つありました。

1つめ、目録。 アップロードされたファイルは 00001 00002 と番号だけで並んでいて、ファイル名も説明も別の目録ファイルにあります。この目録が2種類ありました。179バイト固定長のバイナリ(FILER.IDX)と、平文のテキスト(FILER.CMP)。mmm のバージョン違いだと思います。どちらが来ても読めるよう、中身を見て自動で判別しています。

2つめ、LZH。 取り出したファイルの多くは書庫でした。ここで自作の -lh5- 展開が要ります。

3つめ、MAG。 書庫から出てくるのが .MAG です。これをPNGに変換して、ようやくプレビューで開けます。

目録・LZH・MAG。3つ全部を解かないと、1枚も見えません。 逆に言えば、3つ解いた時点で全部見えました。

出てきたのは20枚です。うち18枚がPC-9801の 640×400・16色。残る2枚は FM TOWNS の 640×480・256色でした。機種の違うマシンで描かれた絵が、PC-98のBBSに置かれている。当時、CGは機種を越えて流通していたということです。

なお、これらの画像はページには載せていません。描いた人の著作権があります。書き込みの本文と同じ扱いです。

18枚がPNGになって、いまMacのプレビューでそのまま開きます。31年前にモデム越しに何十分もかけてダウンロードされていた絵が、ダブルクリックで出てくる。

4つめの形式は、会員の自作だった

FILERを掘っていて、見慣れない拡張子が出てきました。.ERI です。MAGでもPiでもない。

同梱のドキュメントを読んで分かりました。このBBSの会員が、自分で作った画像フォーマットでした。MAGやPiに対抗して書いた自作のローダーで、開発の経緯まで本人が書き残しています。圧縮率が悪いと部の先輩にけなされ、4回作り直した、と。

ありがたいことに、そのドキュメントに仕様が書いてありました。 ヘッダは WRSGRL/GRS_VER5、パレットはG・R・Bの順、画像は1行ずつ4種類の圧縮方式から選んで格納する——というところまで。

ただし4つめの方式(連長圧縮)だけ、こう書かれていました。

> 私は日本語下手なので、汚いあーんどコメントの入ってないソースコードを見たほうが速いと思います。

ソースコードは配布されていません。

仕方がないので、そこだけ実データから解きました。[値][連長-1] の対を、80バイト×4プレーン=320バイトの1行に流し込む方式です。

2度目の、同じ間違い

書けたと思いました。6ファイルすべてが400行ちょうどで終わり、最終バイトまで1バイトも余さず消費します。 枠は完全に合っています。当てずっぽうでこうはなりません。

出てきた絵は、真っ黒な背景に線画が浮かぶものでした。星空、メカらしき輪郭、右下にロゴ。

「線画主体の絵だったのだろう」と思いかけました。

違いました。仕様書が触れていない箇所が2つあり、どちらも外していたのです。

ひとつ、変化バイトのフラグはLSB側から読む。MSBから読んでも行数もバイト数も完全に合うので、数字では絶対に気づけません。もうひとつ、変化しないバイトはゼロではなく、参照行から引き継ぐ。ゼロにすると塗りが全部落ちて、輪郭だけが残ります。

直したら、タイトル画面が出ました。青いメカ、土星、赤い惑星、星空。ロゴ。

会議室で1回、そしてここで1回。どちらも「動いたので正しいと思った」です。

そして、ゲームが動いた

.ERI が入っていたのは、会員が作ったシューティングゲームの体験版パッケージでした。1992年12月。同梱のドキュメントに「BBSアップ用体験版」「転載不可」と明記されています。おまけのグラフィックを削って軽くした、掲示板に上げるための版です。

復元できた6枚は、そのオープニングデモでした。タイトル、機体の三面図、発進、飛行、宇宙、そして「げぇむおーばー」。

ここまで来たら、動かしたくなります。PC-9801はもう手元にありません。

DOSBox-X がPC-98モードを持っているので、Macに入れて、当時の起動バッチと同じ順序で流しました。パレット常駐、カーソル消去、サウンドドライバ、フォント読み込み、そして本体。

PAL /m
C_OFF
NA -O10
FONT
MNM_MAIN

動きました。音も鳴りました。

操作はテンキーとスペースキー。ミサイルは、作中の博士が開発した「何発でも出るミサイルポッド」ということになっていて、実際いくらでも出ます。

1992年に会員のひとりが書いて、この掲示板に上げたゲームです。それが2026年のMacで、音を鳴らして動いています。

最後の日

記録は1994年5月26日で途切れています。

不思議なのは、お別れの書き込みが一つも無いことでした。閉局のお知らせも、最後の挨拶も見つからない。直前まで普通に会話が続いています。

調べていくと、この時期にホストプログラムを別のものへ入れ替えようとした形跡がありました。会員データの変換結果が残っていて、変換元のファイルが消えている。ここは閉局した地点ではなく、記録が途切れた地点でした。

そして最後の日、深夜2時18分。BBS内で開催していたゲーム大会の、6回戦の結果が投稿されていました。ランキングが集計され、まだ次の回戦が残っています。

お別れの挨拶が無いのは当然でした。続きがあるつもりだったのですから。

灯台

読み出したものを、ページにまとめて公開しています。

https://yahho.nao-lab.dev/

書き込みの本文は載せていません。書いた人それぞれに著作権がありますし、内輪の会話をそのまま出すものではないと考えています。載せたのは会議室の名前、スレッドの題、ハンドル、件数だけです。メール、チャット、プロフィール、入会申請は暗号化して封印してあり、公開する予定はありません。

このページを作った理由はひとつで、当時のメンバーに見つけてもらうためです。44人のハンドルが残っています。31年前ですから、今どこで何をしているのか分かりません。

心当たりのある方、あるいは「自分のハンドルがある」という方は、ご連絡いただけると嬉しいです。

やっほ