PR

RustでWindowsアプリを作る【ファイル完全削除ソフト編】

Rust便利帳
Sponsored

ファイル完全削除ソフトウェア」という便利なソフトウェアを実装しながら、RustWindowsアプリを作成する方法を解説します。具体的には、ファイルのドラッグ&ドロップ機能プログレスバーWindows標準ダイアログを活用して、ユーザーフレンドリーなアプリを設計することを目指します。

この記事で作成している「ファイル完全削除ソフトウェア」は、「完全削除」の原理を解説するものであり、完全なデータセキュリティを保証するものではありません。もし、あなたが企業のデータ管理担当者なのであれば、専門企業の有料サービスを利用することを強く推奨します。

この記事で使用したコードは、以下のリポジトリに公開されています。

GitHub - doraneko94/complete_deletion
Contribute to doraneko94/complete_deletion development by cr...

このリポジトリはworkspaceとなっており、完全削除のコアの部分であるfile_destroyと、Windowsアプリであるfile_destroyerの2つを含んでいます。

ファイル完全削除ソフトの作り方

Rustを用いて、ファイルを完全に削除するソフトウェアを作る方法を説明します。

ここでは動作OSを限定せず、コマンドラインで動作するソフトウェアを作ることを目指します。

ファイルを完全に削除することの意味

わざわざ「ファイルを完全に削除する」という表現を使うからには、普通の「削除」は不完全なものであるはずです。

ここでは、いわゆる「ゴミ箱」に入れて消去する場合と、ソフトウェアを用いた「完全削除」の違いについて簡単に解説します。すでに原理を理解している方は、読み飛ばしていただいて構いません。

いわゆる普通の「削除」

用語が非常にわかりにくいですが、コンピュータ上でマウスを用いてポチポチとファイルを削除する手順はすべてこちらに含まれます。たとえば

  • ファイルをゴミ箱に入れ、「ゴミ箱を空にする」を選択
  • USBメモリのファイルを右クリックから削除して、「完全に削除しますか?」に「はい」を選択

などの動作でも「完全に削除」という表現を使いますが、これらは情報セキュリティの観点からは不完全な削除に過ぎません。その仕組みは以下の通りです。

(または、以下のサイトを参照してください)

データ復旧はなぜ可能?データ消去はなぜ必要?HDDのデータ保存と削除の仕組み
HDDのデータ復旧ができる理由と、処分の際にデータ消去が必要な理由を、HDDの保存・削除の仕組みとともに解説。HDDのデ...

ファイルの情報は、記憶メディア(HDD、SSDなど)の中に保存されています。記憶メディアには管理領域保存領域があり、ファイルを構成する情報(文書なら文字、画像ならピクセル情報など)は保存領域に記録されています。一方、管理領域は、そのファイルが保存領域のどこに記録されているか、という情報を記録しています。

ここで、ファイルを「削除」しても、保存領域のファイルの情報はしばらく消えません。代わりに、管理領域に「このファイルは削除済みです」という情報が追記されます。

こうしてファイルは消えたことになっていますが、保存領域の情報はしばらく残り続けます。さらに、管理領域の情報も消えたわけではなく、「削除済み」と追記されただけで、保存場所の情報は残っています。

したがって、専用のソフトウェアを使用することで、このファイルを簡単に復元することができてしまいます

ファイル破壊による「完全削除」

ファイルが復元されないようにするためには、情報を「消す」のではなく「破壊する」必要があります。保存領域の情報が壊れていれば、ファイル復元ソフトを使用しても、意味をなさないファイルしか取得することができません。

ファイル削除ソフトウェアでは、ファイルを「削除」する前に、ファイルの内容を何度もランダムな情報で上書きするという方法でファイルを破壊しています。

つまり、公的文書の黒塗りや、モザイク処理と似たような原理です。

Rustでファイルの上書き処理を実装する

コマンドライン上でファイルを完全削除するソフトウェアとして、file_destroyを作成しました。

complete_deletion/file_destroy at master · doraneko94/complete_deletion
Contribute to doraneko94/complete_deletion development by cr...

以下は、そのコアとなる上書き処理部です。

for _ in 0..quality {
        let data: Vec<u8> = (0..file_size).map(|_| rand::random::<u8>()).collect();
        let _ = file.seek(SeekFrom::Start(0));
        match file.write_all(&data) {
            Ok(_) => Ok(()),
            Err(e) => {println!("{}", e); Err(DestroyError::DataNotWritten)},
        }?;
    }

Rustでファイルを上書きするためには、ファイル内容と同じ長さのu8配列を使用します。これをランダムな値で初期化し、削除したいファイルにwrite_allで書き込みます。このとき、seek(SeekFrom::Start(0))で書き込み開始位置をファイルの先頭に持ってくることで、ファイル全体をランダムな情報で置換することができます。

以上の動作を、ユーザーが設定できる変数qualityの回数だけ繰り返します。

Rustでプログレスバーを表示

削除したいファイルのサイズが大きくなると、上書き処理にもそれなりに時間がかかります。ここでは、ユーザーに進行状況を伝えるため、プログレスバーを表示することにします。

Rustでプログレスバーを表示するためには、indicatifクレートを使用します。以下は、このクレートのProgressBarProgressStyleを用いて、上書き処理にプログレスバーを追加したコードです。

let pb = ProgressBar::new(quality as u64);
pb.set_style(ProgressStyle::default_bar()
    .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})")
    .unwrap()
    .progress_chars("#>-"));
for _ in 0..quality {
    let data: Vec<u8> = (0..file_size).map(|_| rand::random::<u8>()).collect();
    let _ = file.seek(SeekFrom::Start(0));
    match file.write_all(&data) {
        Ok(_) => Ok(()),
        Err(e) => {println!("{}", e); Err(DestroyError::DataNotWritten)},
    }?;
    pb.inc(1);
}

ProgressBarを用いて、長さがqualityのプログレスバーを作成し、詳細なスタイルはProgressStyleで指定します。

上書き処理の繰り返しが1回終了するごとに、pb.inc(1)としてプログレスバーを1つ進めることで、進行状況を表示しています。

RustでWindowsアプリを作る

file_destroymain.rsでは、コマンドラインでファイルを完全削除するようにアプリを設計しています。しかし、削除したいファイルの名前を打ち込む必要があったり、ファイルを1つずつしか処理できなかったりするのは面倒です。

ここでは、Windows環境で動作するアプリを作成して使い勝手を向上させます。具体的には、アプリのアイコンにファイルをまとめてドラッグ&ドロップすると、それらのファイルを一括で完全削除するように改良します。このアプリのコードはfile_destroyerとして公開しています。

complete_deletion/file_destroyer at master · doraneko94/complete_deletion
Contribute to doraneko94/complete_deletion development by cr...

Rustの実行ファイルをビルドする

実行ファイル(アプリのこと。Windowsならexeファイル)を作成するためには、以下のコマンドを実行します。

...\\complete_deletion\\file_destroyer> cargo build --release

--releaseを付けるとアプリを高速化することができます。また、--releaseを付けた場合には、実行ファイルは

...\\complete_deletion\\target\\release\\file_destroyer.exe

の場所に作成されます。

実行ファイルのアイコンにファイルをドロップしたときの挙動

使い勝手を考えて、この実行ファイルのショートカットをデスクトップ上に作成します。ショートカットにファイルをドラッグ&ドロップすると、それらのファイルを完全削除します。

Rust製のアプリに限った話ではありませんが、たとえばfile_destroyerのアイコンにA.png, B.jpgというファイルをドロップする操作は、コマンドライン上で

> file_destroyer.exe A.png B.jpg

と入力したのと等しくなります。つまり、ドロップしたファイルの名前は、コマンドライン引数として受け取ることができます。

use std::{env, format};
use file_destroy::destroy;

// ...

let args: Vec<String> = env::args().collect();
let n = args.len();

for i in 1..n {
    let file_path = args[i].as_str();
    match destroy(file_path, 64, true) {
        Ok(_) => { println!("✅{} was successfully deleted.", file_path); }
        Err(e) => {
            eprintln!("❌{} was failed to delete.", file_path);
            eprintln!("Error: {}", e);
        }
    }
}

コマンドライン引数はstd::env::args()で取得します。コマンドライン引数の先頭args[0]には、実行ファイルの名前(今回はfile_destroyer.exe)が入るので、完全削除の対象となるファイル名はargs[1]以降に存在します。

RustでWindowsのダイアログを表示する

上記のコードでは、ファイルをドロップせずにアプリのアイコンをクリックすると、クラッシュしてしまいます。また、ファイルをドロップした瞬間に完全削除が始まるため、間違えても取返しがつかなくなるので危険です。

そこで、不適切な操作が行われた際はアプリの使い方を説明し、ファイルがドロップされた場合は「本当に削除しても良いか?」を確認することにしましょう。このアプリはWindows上で動作させることを想定しているため、これらのメッセージを表示するためにWindowsダイアログを使用します。

use windows::{core::*, Win32::UI::WindowsAndMessaging::*};

// ...

let args: Vec<String> = env::args().collect();
let n = args.len();

if n <= 1 {
    unsafe { MessageBoxW(None, 
        w!("Please drop the file you want to delete onto the icon."), 
        w!("No file has been specified."), 
        MB_OK); }
    return
} else {
    let message = if n == 2 {
        format!("Do you really want to delete '{}'?", args[1])
    } else {
        format!("Do you really want to delete these {} files?", n - 1)
    } + " Deleted files cannot be restored.";
    unsafe {
        match MessageBoxW(None, 
            &HSTRING::from(message),
            w!("Notice!"), 
            MB_OKCANCEL) {
            MESSAGEBOX_RESULT(1) => {}
            MESSAGEBOX_RESULT(_) => { return; }
        }
    }
}

Windowsの機能をRustから操作するためには、windowsクレートを使用します。

ダイアログの表示にはMessageBoxAMessageBoxWが使えます。これらの違いは、Aがマルチバイト文字(8bit単位)、Wがワイド文字(16bit単位)を表現していることです。ここではワイド文字を使用しています。

表示する文字列の設定

MessageBoxWの定義は次の通りです。

pub unsafe fn MessageBoxW<P0, P1, P2>(
    hwnd: P0,
    lptext: P1,
    lpcaption: P2,
    utype: MESSAGEBOX_STYLE,
) -> MESSAGEBOX_RESULT
where
    P0: Param<HWND>,
    P1: Param<PCWSTR>,
    P2: Param<PCWSTR>,

hwndは「所有者ウインドウへのハンドル」というものですが、とりあえずNoneにしておきます。

ダイアログに表示される文章はlptextlpcaptionに指定します。ここで、実際のダイアログではlpcaptionの方が上に表示されるので注意してください。

これらの引数はString&strではなく、ワイド文字をあらわすParam<PCWSTR>で指定します。この型を取得するためには、w!マクロを使用することができます(MessageBoxAに対応するマルチバイト文字列の場合は、s!マクロを使う)。

ただし、これらのマクロはフォーマッティングに対応していません。そのため、表示内容に変数を用いることができません。

ここで、ファイルが1つだけドロップされた際に、「本当に[ファイル名]を削除してもいいですか?」という警告を発することを考えます。表示する内容はドロップされたファイル名によって変化するため、w!マクロを使用することができません。

そこでfile_destroyerのコード中では、HSTRINGを使って対応しています。

let message = format!("Do you really want to delete '{}'? Deleted files cannot be restored.", args[1]);
unsafe {
    MessageBoxW(
        None, 
        &HSTRING::from(message),
        w!("Notice!"), 
        MB_OKCANCEL
    );
}

HSTRINGFrom<&str>From<String>を実装しています。また、&HSTRINGPCWSTRとして使用することができるため、非常に便利です。

ダイアログボタンの設定と出力

MB_OKCANCELを指定した場合

ダイアログに表示するボタンはutype引数に指定します。

たとえばMB_OKを指定すると「OK」ボタンのみを表示し、MB_OKCANCELの場合は「OK」と「キャンセル」のボタンを表示します。

「本当に削除しますか?」の問いに対し、ユーザーの返答によって完全削除の続行と中止を切り替えるためには、MessageBoxWの出力を利用します。筆者が試したところによると、「OK」が押された場合には1、「キャンセル」が押されるか、ダイアログが「×」で閉じられた場合には2が返されるようです。

「何かキーを押すと終了します」の実装

確認ダイアログで「OK」が押されると、完全削除を開始します。処理中は進行状況を表示します。

その後、処理の完了と同時にアプリが閉じてしまうと、最終的な結果を落ち着いて確認することができません(ファイルが別の場所で開かれているなどの原因で、削除に失敗することもあります)。

file_destroyerでは、以下のコードによって、処理完了後にキー入力を待ち受け、「何かキーを押すと終了します」という機能を実装しています。

println!("Press any key to quit.");
let mut word = String::new();
std::io::stdin().read_line(&mut word).ok();

まとめ

Rustの便利なクレートや、Windowsの標準機能を用いて、ユーザーフレンドリーなアプリを作成する方法を解説しました。RustからWindowsの機能を使用する場合、MessageBoxWなどはunsafeブロックになってしまうのが難点ですが、うまく利用することで操作性の高いアプリを作ることができます。

もっと知りたいこと、感想を教えてください!