実際のバイトを読み、パスを保持する

JavaBeginner
オンラインで実践に進む

はじめに

アーカイブには、名前が違っても同一の画像や、サイズが同じでも内容の違う画像が含まれることがあります。再利用できる重複ファイル検索ツールのバイト読み取り部分を実装し、元のファイルを変更せずに完全な一覧を保存します。

Java のクラス、メソッド、リスト、例外、基本的なファイル読み取りを理解していることを前提とします。このラボではバイナリ入力、リソースの自動クローズ、SHA-256 を紹介します。ディレクトリアダプターと起動スクリプトは用意されており、ダイジェストを自分で実装します。読み取り専用の演習であり、ディスクの削除・整理ツールではありません。読解と通常の修正を含めた所要時間は、著者の見積もりで 50 分です。

ファイル名ではなくバイトを読む

このステップでは、まず通常の画像例を比較し、各バイトをちょうど 1 回ずつ読み取るダイジェストを実装します。WebIDE を開き、Terminal > New Terminal を選択し、以後はこの統合ターミナルを使います。各ラボは独立して開始し、それぞれ専用のファイルがあります。

cd は、このターミナルの作業ディレクトリを変更します。用意された run.sh は、新しい一時ビルドディレクトリで src/*.java をコンパイルしてから、指定した Java クラスを実行します。コンパイラーは -encoding UTF-8 を使うため、ソーステキストがターミナルのロケールに依存しません。C.UTF-8 を使うのはコンパイラーと Java の子プロセスだけです。Java の子プロセスはテキストの既定値にも UTF-8 を使うため、シェルの設定を変えずに、対応範囲内の UTF-8 ファイル名を扱えます。

cd /home/labex/project/duplicates
./run.sh Sample

補助プログラムは本物の PNG ファイルをデコードし、バイトを比較します。red = renamed: true と red = blue: false が表示されます。3 枚とも画像の寸法とバイト長は同じです。名前や長さは検索を絞り込む手掛かりになりますが、同一性の証明にはなりません。任意で、WebIDE Explorer の duplicates/data/gallery を展開し、red.png、blue.png、copies/renamed.png をクリックして画像を表示できます。必須の比較は、上記の Sample によるデコードとバイト比較の結果です。これらは本コース独自の合成画像であり、外部素材は不要です。

公開インターフェイス Digester は 1 つのメソッドを定めます。digest(Path) は 16 進文字列を返し、読み取りに失敗すると IOException を送出します。Path はファイルを指定し、InputStream はデータをテキストと見なさずバイナリのバイトとして読みます。SHA-256 は、それらのバイトを固定長の候補フィンガープリントにまとめます。後の単元でも、重複と判断する前に実際の内容を比較します。

初期実装を置き換える前に、小さなアダプターと初期コードを確認します。

cat src/Digester.java src/Sha256.java

1024 バイトのバッファーを繰り返し使用します。read(buffer) は今回有効なバイト数を返し、末尾では -1 を返します。最後の読み取りではバッファーの一部しか埋まらず、残りに前回の値が残る場合があります。そのため sha.update(buffer, 0, count) には実際に読んだ数を渡す必要があります。空ファイルでは更新を行いませんが、有効なダイジェストが得られます。

try (InputStream ... ) は try-with-resources です。ブロックを抜けるとき、読み取りで例外が起きた場合も Java がストリームを閉じます。クローズはファイルリソースを解放しますが、直ちにガベージコレクションが実行されることは保証しません。MessageDigest.getInstance はアルゴリズムに関する検査例外を送出する可能性があるため、外側の catch で同じファイル操作エラーの契約に変換します。16 進表記では 1 バイトを 2 文字で表します。& 255 は、符号付き Java バイトを符号なしの値として扱います。

次のコマンドで置換内容をすべて書き込みます。> はこのソースファイルを置き換えます。引用符付きの here-document マーカー <<'JAVA' は、その間のテキストをそのまま cat に渡します。最後の JAVA は、インデントせず単独の行に置きます。代わりに、WebIDE で同じファイルを編集して保存してもかまいません。貼り付け後も未完了の here-document プロンプトが残る場合は、Ctrl+C で中止し、最後のマーカーも含めて置換コマンド全体をもう一度貼り付けます。

cat > src/Sha256.java <<'JAVA'
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Sha256 implements Digester {
    public String digest(Path path) throws IOException {
        try {
            MessageDigest sha = MessageDigest.getInstance("SHA-256");
            byte[] buffer = new byte[1024];
            try (InputStream input = Files.newInputStream(path)) {
                int count;
                while ((count = input.read(buffer)) != -1) {
                    sha.update(buffer, 0, count);
                }
            }
            StringBuilder hex = new StringBuilder();
            for (byte value : sha.digest()) {
                hex.append(String.format("%02x", value & 255));
            }
            return hex.toString();
        } catch (NoSuchAlgorithmException error) {
            throw new IOException("SHA-256 unavailable", error);
        }
    }
}
JAVA

同じ例をもう一度コンパイルして実行します。コンパイルで新しいクラスが検査されますが、例の同一性判定は変わりません。

./run.sh Sample

Java が行番号を表示したら、src/Sha256.java を開き直して、波括弧や綴りを置換内容と比較し、保存して再実行します。パスが違う場合は pwd で現在のディレクトリを確認し、上記の絶対パスの cd をもう一度実行します。VM のリセットは不要です。

すべてのパスとフィンガープリントを保存する

このステップでは、自分のダイジェストを使って完全な UTF-8 の一覧を作ります。同じ WebIDE 統合ターミナルで、作業ディレクトリ /home/labex/project/duplicates のまま進めます。

用意された Inputs.files アダプターは指定ディレクトリだけを走査し、ディレクトリストリームを閉じます。シンボリックリンク、特殊ファイル、タブや改行を含むパスは、黙ってたどったり飛ばしたりせず拒否します。対応するのは安定した通常のファイルとディレクトリだけです。走査中に入力を変更しないでください。属性検査は一部の変更を検出できますが、ディレクトリを固定したり、同時更新に対するスナップショットを保証したりするものではありません。

用意された Inventory は、すべての読み取りが成功してから、各相対パス、サイズ、ダイジェストを保存します。TSV 形式では列の区切りにタブを使います。レポートアダプターは新しい一時ファイルに UTF-8 を書き、その後レポートのパスを置き換えます。そのため、既存レポートがハードリンクでも元ファイルのバイトを上書きしません。これはクラッシュ時の永続性を保証するものではありません。出力先は走査ルートの外に置きます。reports は data と並んで既に存在するため、レポートが誤って自分自身の入力になることを防げます。引数の順序は Inventory ROOT REPORT です。最初が走査ディレクトリ、次が出力ファイルのパスなので、逆にしないでください。

./run.sh Inventory data/gallery reports/inventory.tsv

Inventory complete: 11 files が表示されるはずです。cat で保存内容を表示します。

cat reports/inventory.tsv

ヘッダーは PATH、SIZE、SHA256 です。red.png、copies/renamed.png、copies/third.png の行を比較すると、異なるパスが同じダイジェストを持ちます。blue.png はサイズが同じでもダイジェストが違います。2 つの空ファイルはサイズがゼロで、同じ有効なダイジェストを持ちます。tail.bin の最後の読み取りはバッファーの一部だけですが、同一のコピーには同じダイジェストが必要です。同名の north/readme.txt と south/readme.txt は別々の行に残ります。

次は一般的なパスの間違いを試します。次のコマンドは意図的に失敗し、終了ステータス 2 と Scan failed を返します。新しいレポートを生成してはいけません。失敗は、空の一覧を正常に生成したこととは異なります。以前のレポートは、今回失敗した実行の成功証拠になりません。

./run.sh Inventory data/missing reports/missing.tsv

入力パスを直して、新しいレポートを作成します。

./run.sh Inventory data/gallery reports/recovered.tsv

読み取り失敗も同じ規則です。エラーを表示して失敗を返し、読めないファイルを黙って除外してはいけません。検査では、空ファイル、最後の読み取りでバッファーが一部だけ埋まるファイル、読めないファイルの隔離テストデータで現在のソースを実行し、実際の一覧も読みます。すべての原本のパス、バイト、権限、所有者情報、変更時刻を保ってください。削除や置換は行いません。通常の読み取りでアクセス時刻が更新されることはあります。候補となる事実がそろいました。次のラボでは、同じフィンガープリントを持つ候補を実際のバイトで比較します。

まとめ

try-with-resources でバイナリファイルを読み、実際に読み取った数で SHA-256 を更新し、完全な UTF-8 一覧を保存しました。名前、サイズ、ダイジェストは候補となる事実であり、実際のバイト比較によって重複グループを確定します。