NimでTarファイル作成を実装する ~続・ちゃぶだい返し(実装編)~

前記事の実装編です。
Nimでtarの作成(アーカイブ)と伸長の処理を実装します。
前記事にも書きましたが、完全な互換性は目指さず、基本的な使い方の範疇でゆるく作成したつもりです。

もくじ:

プログラム

長くなりますが先に今回のプログラムsimpletar.nimを記載しておきます。検証した環境はWindows 10 Pro, Nim 0.17.2です。

エンコードはUTF-8を想定。tarで使うAsciiはUTF-8のサブセットなので、書き込むデータのエンコーディングは特に気にしなくとも大丈夫だと思います。

伸長の処理はNimbleにあるuntarを参考にしました。

使い方

公開プロシージャ(という言い方で正しいのかしら)のpackunpackを使えば、(名前の通り)それぞれアーカイブと伸長ができるようになっています。

アーカイブしたいときはもとのディレクトリへのパスと、出力する予定のtarファイルのパスを指定します。指定されたパスが有効か(存在するか、など)はプロシージャの中で一応チェックしています。

展開したいときは逆に、もとのtarファイルのパスと、出力する先のディレクトリへのパスを指定します。

例外が発生した場合は、その内容を表示してプロシージャを抜けます。
以下に詳細を説明しておきます。

アーカイブ

packプロシージャでアーカイブします。

アーカイブ(あるディレクトリ以下のサブディレクトリ/ファイルからtarファイルを作成する)は、基本的にはディレクトリ階層をたどって、ヘッダとコンテントを作る作業です。

種別(typeflag)

今回はファイルとディレクトリを考えればよいので、scanDir()プロシージャで階層をたどってパスとその種別(ファイル or ディレクトリ)を取得します。

バイナリゼロで512バイトのバイト列を作っておき、種別に応じてtypeflagなどヘッダに詰める値を切り替えます。modeの値などはPythonの実装に寄せてハードコーディングしています。

長いパス

長いパスが入ってきた場合には、Lフラグを使ってパスを格納するためのヘッダとコンテントを別途作る必要があります。

ここではmakeHeader()プロシージャ内でpreheaderという変数を使ってLフラグ用のヘッダと、フルパス本体をコンテントとして作成しています。

チェックサム

チェックサムとするため各文字をint (unsigned)に型変換し総和を取ります。ただし、初期化段階でチェックサム用のフィールドchecksum[8]は全て空白(半角スペース)で埋めておきます。

コンテント

入力ファイルストリーム(元のファイル)から出力ファイルストリーム(tarファイル)へコピーします。一息に開くとおそらくメモリ不足で死ぬので、バッファサイズをconst (COPY_BUFFER_SIZE)で決めておき、順次コピーしていきます。

512バイト単位のブロックで構成する必要がありますので、足りない部分はバイナリゼロでパディングします。
deferを使うと、途中でIOエラーなどの例外が発生した場合でもブロックを抜けるときにファイルストリームを閉じてくれます。

展開(unpack)

unpackプロシージャで展開します。

基本的にはいずれの形式が入ってくるかはわかりませんが、少なくともPOSIX ustarとGNU tarを処理できれば実用上は十分そう。逆に言えば、Pax交換フォーマットはサポートしません。
magicのところにこれら2つの形式のもの以外が入っている場合は、アーカイブの終端か、非サポートの形式として処理を終了します。

基本的に、パス以外のメタデータは修正しません。tarファイルのヘッダ自体には最終更新時刻のタイムスタンプなどが含まれますが、それらのメタデータは反映しません。

長いパス

ヘッダは、tarファイルから512バイトの長さ(1バイト文字列の長さ)を読み込み、ファイル名やサイズなどの各フィールドを取り出します。

GNU tarでLフラグが設定されているものは、長いパスを格納しているブロックになりますので、ファイル名のみコンテントから抽出し、他の内容はその次のヘッダ以降を参照します。

ustarのパス処理

POSIX ustarの場合はprefixフィールドに追加のパスが格納されている可能性があるので、ヌル終端文字列として読み、連結します。

この場合、ファイル名用のフィールドname[100]とプレフィクス用のフィールドprefix[155]をパス区切り文字で結合してフルパスを取得します。

コンテント

アーカイブ時の説明にあるように、コンテントは512バイト単位になるようパディングされています。
ヘッダに格納されている実際のファイルサイズを参照して、その長さ分だけコピーします。

また、コピーが終わった後はファイル読み込み位置を次のブロック先頭へ進めておきます。アーカイブの際も使っていますが、roundup()プロシージャは512単位で整数を切り上げるためのものです。