Rust で derive(Debug) を安心して使うために秘匿情報をマスクする

コロナウィルス騒動によって入手不可能になっていますがそのマスクの話ではありません。
~~花粉が激化する３月をどう乗り越えたらいいのか…~~

## マスクってどんな処理？

ここで欲しいのはこんな処理のことです。

```rust
let key = SecretKey::new("wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY");
println("{:?}", key);// SecretKey("****************EKEY")
```



## どうして？

こんなコードを見るとギョッとしますよね。しなければ今からしましょう。

```rust

#[derive(Debug)]
pub struct User {
    name: String,
    credentials: Credentials,
}
#[derive(Debug)]
pub struct Credentials {
    pub access_key: AccessKey,
    pub secret_key: SecretKey,
}
#[derive(Debug)]
pub struct AccessKey(String);

#[derive(Debug)]// ギョッとするのはここ！
pub struct SecretKey(String);
```

なぜギョッとするのかというと :

```rust
let taro = User {
    name: "太郎".to_string(),
    credentials: Credentials {
        access_key: AccessKey("access-key".to_string()),
        secret_key: SecretKey("secret-key".to_string()),
    }
};
// どこかで作られたユーザ情報をログに出力していたら…
println!("log: {:#?}", taro);
/*
log: User {
    name: "太郎",
    credentials: Credentials {
        access_key: AccessKey(
            "access-key",
        ),
        secret_key: SecretKey(
            // そのメンバの構造体に含まれる秘匿情報まで吐かれていた…！
            "secret-key",
        ),
    },
}
*/
```

あるあるですね。

## どうすれば？

* 雑にログに出さなければ済む？
* 秘匿情報をダンプしないように注意する？
* `User` や `Credentials` では `derive(Debug)` を避ける？

いずれの答えも No です。人間はミスをします。
そもそもログに出力されても問題が起きないように作りましょう。

（ログに出力 **できないように** 作るという選択肢もありそうですが、いろいろと大変なので今回はそのアプローチには触れません）

<!--
つまり :

* 秘匿情報 (ここでは `SecretKey` ) の `Debug` 実装を自前で用意すれば解決です。
* その他の `derive(Debug)` に手を加える必要はありません。

この `Debug` の実装にマスキング処理を入れます。
-->

## 具体的な実装例

ということで解答編に入ると :

* 実装すべきは秘匿情報 (ここでは `SecretKey` ) の `fmt::Debug` のみ
* その他の `derive(Debug)` については手を加える必要なし

です。

```rust
use std::fmt;

// これはもう不要
// #[derive(Debug)]
pub struct SecretKey(String);

impl fmt::Debug for SecretKey {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        let visible_length = 4;
        let masked = {
            let start = self.0.len() - visible_length;
            format!("{:*>20}", &self.0[start..])
        };
        f.debug_tuple("SecretKey").field(&masked).finish()
    }
}
```

動作がどのように変わったのか見てみましょう :

```rust
let taro = User {
    name: "太郎".to_string(),
    credentials: Credentials {
        access_key: AccessKey("access-key".to_string()),
        secret_key: SecretKey("secret-key".to_string()),
    }
};
println!("log: {:#?}", taro);
/*
log: User {
    name: "太郎",
    credentials: Credentials {
        access_key: AccessKey(
            "access-key",
        ),
        // 雑にダンプしても読めなくなった！安心！
        secret_key: SecretKey(
            "****************-key",
        ),
    },
}
*/
```

問題なさそうですね。
せっかくなので簡単なテストも残しておきます。

```rust
#[cfg(test)]
mod tests {
    use super::SecretKey;

    #[test]
    fn it_should_be_masked() {
        let key = SecretKey("wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY".to_string());
        let masked = format!("{:?}", key);

        assert_eq!(masked, r#"SecretKey("****************EKEY")"#);
        assert_eq!(masked.len(), 20 + r#"SecretKey("")"#.len());
    }
}
```

ちなみに最後の四文字だけ見せるという挙動は AWS CLI のそれを ~~パクり~~ 参考にしました。

### 追記 : マルチバイト文字が含まれると…

ここまで書いてから、日本語が入ると panic で落ちることに気が付きました :innocent: 

```scala
    #[test]
    fn it_should_be_masked_even_if_multibyte_given() {
        let key = SecretKey("wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAM途中に日本語".to_string());
        let masked = format!("{:?}", key);

        assert_eq!(masked, r#"SecretKey("****************に日本語")"#);
        assert_eq!(masked.chars().count(), 20 + r#"SecretKey("")"#.len());
    }
```

oops!

```
thread '...secret_key::tests::it_should_be_masked_even_if_multibyte_given'
  panicked at 'byte index 48 is not a char boundary;
    it is inside '本' (bytes 46..49) of `wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAM途中に日本語`',
      src/libcore/str/mod.rs:2068

test ...secret_key::tests::it_should_be_masked_even_if_multibyte_given ... FAILED
```

このテストを通すための修正版はこちら :

```scala
impl fmt::Debug for SecretKey {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        let visible_length = 4;
        let masked = {
            let length = self.0.chars().count();
            let start = length - visible_length;
            let extracted: String = self.0.chars().skip(start).collect();
            format!("{:*>20}", &extracted)
        };
        f.debug_tuple("SecretKey").field(&masked).finish()
    }
}
```

初期バージョンが使えるのはマルチバイト文字が絶対に来ないと言える場面だけになりそうですね… :cry: 

## 余談 : コードの詳細について

上記の実装はわずか数十行程度ですが、
普段あまり使わない & 地味に便利 & おもしろいポイントが何点かあったので書き残しておきます。

### std::fmt::DebugTuple

`debug_tuple` メソッドによっていい感じの pretty-print が手に入ります。
（記事本文で `SecretKey` の `{:#?}` 出力が完璧にインデントされていたことに着目してください！）

```rust
log: User {
    name: "太郎",
    credentials: Credentials {
        access_key: AccessKey(
            "access-key",
        ),
        secret_key: SecretKey(
            "****************-key",
        ),
```

参考リンク

* [std::fmt::DebugTuple - Rust](https://doc.rust-lang.org/std/fmt/struct.DebugTuple.html)
* [rust - Is there a way to customize Debug output without implementing the Debug trait directly? - Stack Overflow](https://stackoverflow.com/questions/46749679/is-there-a-way-to-customize-debug-output-without-implementing-the-debug-trait-di)
* [std::fmt::Formatter - Rust](https://doc.rust-lang.org/std/fmt/struct.Formatter.html)
    * tuple 以外にも struct, list, set, map 用の関数が揃ってます。地味にステキ。

### Fill/Alignment

`"{:*>20}"` で "20 文字に達するまで左側を `*` で埋める" の意です。
文字を `*` 以外に変えるのはもちろん、右側を埋めたり左右を埋めたりもできます。

* 最初はこれを知らずに危うく正規表現 (rf. [regex crate](https://crates.io/crates/regex)) でゴリ押しするところでした
* 参考 : [std::fmt - Rust](https://doc.rust-lang.org/std/fmt/#fillalignment)


### Raw string literals

`r#"..."#` の記法によって `"` や `\` を手作業でエスケープする簡単なお仕事から解放されます。
リテラルの中に `#` を入れたいときは `r##"..."##` です。

* Go のバッククォートや Scala の `"""` にあたるやつです
* 参考 : [Tokens - The Rust Reference](https://doc.rust-lang.org/reference/tokens.html#literals)


### String Slices

`foo[start..]` で "start バイト以降を切り出す" の意です。
添字には範囲を指定できるので `foo[start..end]` や `foo[..end]` も書けます。

* 参考 : [The Slice Type - The Rust Programming Language](https://doc.rust-lang.org/book/ch04-03-slices.html)

ちなみにドキュメントをよく見たらマルチバイト文字の扱いについても明記されていました :joy: 

> Note: String slice range indices must occur at valid UTF-8 character boundaries. If you attempt to create a string slice in the middle of a multibyte character, your program will exit with an error. 

### std::str::Chars

`.chars().count()` によってマルチバイト文字の「文字数」を得られます。
のはずだったんですが、調査の過程で知ったこわおもしろい例を紹介しておきます :

* https://play.rust-lang.org/?version=stable&mode=debug&edition=2015&gist=9480b9d3af4bea29ccadbd25a586dea7

```
String: 'é' has length 3 and character count 2
String: 'é' has length 2 and character count 1
```

この e の頭についている記号は grapheme (書記素) なるもので、別途ライブラリ :

* [unicode-segmentation - crates.io: Rust Package Registry](https://crates.io/crates/unicode-segmentation)

を使えばこの grapheme を考慮して文字数をカウントできるようです。
今回の記事では

* 標準ライブラリの範囲外
* どこまで実装すべきなのかはおそらく要件による
* ~~検証がめんどい~~

という点から対応を見送りました。

参考リンク

* [Get the String length in characters in Rust - Stack Overflow](https://stackoverflow.com/questions/46290655/get-the-string-length-in-characters-in-rust)
* [unicode - How do I count unique grapheme clusters in a string in Rust? - Stack Overflow](https://stackoverflow.com/questions/51818497/how-do-i-count-unique-grapheme-clusters-in-a-string-in-rust)