Patterns for Defensive Programming in Rust

2025/12/06 1:34

Patterns for Defensive Programming in Rust

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

主なメッセージ
一般的な落とし穴(暗黙の不変条件、unsafe インデクシング、隠れたデフォルト値、enum バリアントの無音損失、偶発的変更)を防ぐ具体的な Rust コーディングパターンを採用し、Clippy ラインが自動でそれらを強制できるようにする。

根拠 / 推論

  1. // this should never happen
    のコメントは明示的なチェックに置き換える;そうしないとコンパイラは暗黙の不変条件を検出できない。
  2. is_empty()
    チェック後にインデックスアクセスする代わりに、スライスパターンマッチング(
    match vec.as_slice()
    )を使い、要素がちょうど一つあることを保証し、空の場合も安全に処理する。
  3. 構造体を構築するときは
    ..Default::default()
    を避ける;すべてのフィールドを列挙するか、デフォルトインスタンスを分解して新しいフィールドがコンパイラ警告を発生させるようにする。
  4. PartialEq
    実装では構造体を分解し、すべてのフィールドが考慮されるようにする—そうでなければ、新しく追加されたフィールドが静かに等価性ロジックを壊す可能性がある。
  5. 失敗を明示的に扱うため
    TryFrom
    を優先し、失敗時にデフォルト値の無音化を避ける。
  6. パターンマッチで
    _ => {}
    のキャッチ―オールアームは使わない;すべての enum バリアントを列挙するかグループ化して、コンパイラが未処理ケースを警告できるようにする。
  7. 使用されない変数プレースホルダー(
    _
    )は説明的な名前で置き換え、特にブールフラグの場合には明確さを高める。
  8. 一時的な可変性はシェーディングまたは内部スコープで表現し、初期化後の偶発的変更を防ぐ。
  9. コンストラクタ検証を強制するためにプライベートフィールド(
    _private: ()
    )を追加したり、
    #[non_exhaustive]
    を使用したり、クレート内でプライベートモジュールに型を隠すことで安全性を確保する。
  10. 重要な型には
    #[must_use]
    を付与し、値が無視されたときに警告を発生させることで静かに誤設定されるのを防ぐ。
  11. ブールパラメータは enum またはパラメータ構造体に置き換えて意図を明示的にし、偶発的なスワップを回避する。
  12. indexing_slicing
    fallible_impl_from
    wildcard_enum_match_arm
    unneeded_field_pattern
    fn_params_excessive_bools
    must_use_candidate
    といった Clippy ラインを有効にして、多くのパターンをコンパイル時に検出する。

影響
これらの実践と関連する Clippy ラインを採用すると、ランタイムパニックが減少し、コードの可読性が向上し、将来のリファクタリング時に隠れたバグを早期に検出できるため安全性が高まる。

本文

私は趣味として、コード中に

// this should never happen in code
というコメントを見つけたとき、その「決して起こらないはず」の条件を実際に探し出すことに挑戦しています。90 % のケースでその条件を突き止める方法がわかります。多くの場合、コンパイラが強制しない暗黙の不変条件(インバリアント)が根本原因です。コンパイラはメモリ安全性を確保し、標準ライブラリは業界最高水準ですが、それでも欠点があります。私たちが頼りにできるのは、より防御的な Rust コードを書くために「学習済み」である硬直したパターン―文書化されていないものの、全体品質には不可欠な小さなイディオムです。


コードスメル:ベクタへの直接インデックスアクセス

if !matching_users.is_empty() {
    let existing_user = &matching_users[0];
}

リファクタリング時に

is_empty()
チェックを忘れると、
matching_users[0]
はパニックします。
長さ確認とインデックス付けは別々の操作であり、コンパイラは検出できません。

スライスパターンマッチングで修正

match matching_users.as_slice() {
    [] => todo!("ユーザーが見つからない場合どうする?"),
    [existing_user] => { /* 安全! 要素はちょうど 1 個 */ }
    _ => Err(RepositoryError::DuplicateUsers),
}

コンパイラにすべての状態を考慮させることで、隠れたエッジケースが明らかになります。


コードスメル:
Default
の遅延使用

..Default::default()
を使うと、本来設定したくないフィールドが暗黙的に初期化されます。

let foo = Foo {
    field1: value1,
    field2: value2,
    ..Default::default()
};

すべてのフィールドを明示的に設定

let foo = Foo {
    field1: value1,
    field2: value2,
    field3: value3,
    field4: value4,
};

まだデフォルト値が欲しい場合は:

let Foo { field1, field2, field3, field4 } = Foo::default();

let foo = Foo {
    field1: value1,
    field2: value2,
    field3,
    field4,
};

新しいフィールドが追加されるとコンパイラが警告します。


コードスメル:脆弱なトレイト実装

構造体を分解(デストラクチャリング)すると、忘れたフィールドに気づきやすくなります。

impl PartialEq for PizzaOrder {
    fn eq(&self, other: &Self) -> bool {
        let Self { size, toppings, crust_type, ordered_at: _ } = self;
        let Self { size: o_size, toppings: o_toppings, crust_type: o_crust, ordered_at: _ } = other;

        size == o_size && toppings == o_toppings && crust_type == o_crust
    }
}

extra_cheese
を追加するとコンパイル時に検出できます。


コードスメル:
From
TryFrom
の使い分け

変換が失敗する可能性があるなら

TryFrom
を使用します。

impl TryFrom<&DetectorStartupErrorReport> for DetectorStartupErrorSubject {
    fn try_from(report: &DetectorStartupErrorReport) -> Result<Self, Error> {
        let postfix = report.get_identifier()
            .or_else(get_binary_name)
            .ok_or(Error::MissingIdentifier)?;

        Ok(Self(StreamSubject::from(
            format!("apps.errors.detectors.startup.{postfix}").as_str(),
        )))
    }
}

From
は失敗しない変換にのみ使用します。


コードスメル:非網羅的マッチ

ワイルドカード

_
を避け、すべてのバリアントを列挙します。

match self {
    Self::Variant1 => { /* ... */ }
    Self::Variant2 => { /* ... */ }
    Self::Variant3 | Self::Variant4 => { /* 共通ロジック */ }
}

新しいバリアントが追加されたときにコンパイラが警告します。


コードスメル:未使用変数の
_
プレースホルダー

説明的な名前を付けるだけで意図が明確になります。

match self {
    Self::Rocket { has_fuel: _, has_crew: _, .. } => { /* ... */ }
}

パターン:一時的なミュータビリティ

ミューテーションを明示し、スコープで限定します。

let data = {
    let mut tmp = get_vec();
    tmp.sort();
    tmp  // 戻り値は不変
};

初期化後に偶発的な変更が起きません。


パターン:コンストラクタの防御的取り扱い

外部コード

#[non_exhaustive]
pub struct S { /* フィールド */ }

impl S {
    pub fn new(field1: String, field2: u32) -> Result<Self, Error> { /* 検証 */ }
}

_private
Seal
を使ってクレート内でのみ生成を許可します。

内部コード

mod inner {
    struct Seal;
    pub struct S { field1: String, field2: u32, _seal: Seal }

    impl S {
        pub fn new(field1: String, field2: u32) -> Result<Self, Error> { /* 検証 */ }
    }
}
pub use inner::S;

new
以外からはインスタンスを作れません。


パターン:重要な型に
#[must_use]
を付与

#[must_use = "設定が適用されるには構成を使用する必要があります"]
pub struct Config { /* フィールド */ }

impl Config {
    pub fn new() -> Self { /* ... */ }
    pub fn with_timeout(mut self, timeout: Duration) -> Self { self.timeout = timeout; self }
}

結果を無視するとコンパイラが警告します。


コードスメル:ブールパラメータ

ブール値は列挙型やパラメータ構造体に置き換えます。

enum Compression { Strong, Medium, None }
enum Encryption   { AES, ChaCha20, None }
enum Validation   { Enabled, Disabled }

fn process_data(
    data: &[u8],
    compression: Compression,
    encryption: Encryption,
    validation: Validation,
) {
    /* ... */
}

または:

struct ProcessDataParams {
    compression: Compression,
    encryption: Encryption,
    validation: Validation,
}

impl ProcessDataParams {
    pub fn production() -> Self { /* デフォルト */ }
    pub fn development() -> Self { /* デフォルト */ }
}

防御的プログラミングのための Clippy ライン

ライン目的
clippy::indexing_slicing
スライス/ベクタへの直接インデックスを防止
clippy::fallible_impl_from
失敗し得る
From
実装に警告
clippy::wildcard_enum_match_arm
enum マッチでワイルドカード
_
を禁止
clippy::unneeded_field_pattern
不要なフィールドパターン(
..
)を検出
clippy::fn_params_excessive_bools
余分なブール引数に警告
clippy::must_use_candidate
#[must_use]
の追加を提案

クレートで有効化するには:

#![deny(clippy::indexing_slicing)]
#![deny(clippy::fallible_impl_from)]
#![deny(clippy::wildcard_enum_match_arm)]
#![deny(clippy::unneeded_field_pattern)]
#![deny(clippy::fn_params_excessive_bools)]
#![deny(clippy::must_use_candidate)]

または

Cargo.toml
で:

[lints.clippy]
indexing_slicing = "deny"
fallible_impl_from = "deny"
wildcard_enum_match_arm = "deny"
unneeded_field_pattern = "deny"
fn_params_excessive_bools = "deny"
must_use_candidate = "deny"

結論

Rust における防御的プログラミングは、暗黙の不変条件を明示化しコンパイラで検証することにあります。パターンマッチング、明示的なデフォルト値、分解、制御された生成、

#[must_use]
、名前付きパラメータ、Clippy ラインといったパターンを採用すれば、リファクタリングミスによるバグを事前に防ぎ、実際に起きる前に問題を解決できます。

「// this should never happen」というコメントを書いているときは、一瞬立ち止まり、「コンパイラがその不変条件をどう保証できるか」を考えてみてください。最高のバグとは、コンパイル自体で捕捉されるものです。

同じ日のほかのニュース

一覧に戻る →

2025/11/30 18:11

Self-hosting my photos with Immich

## Japanese Translation: 記事では、著者が低電力Ryzen 7ミニPC(ASRock DeskMini X600)に64 GB RAM、1 TBディスクを搭載し、アイドル時の消費電力が10 W未満である環境にImmichをセットアップした手順を説明しています。Proxmox上に「photos」という名前のVMを作成し、500 GBのストレージ、4つのCPUコア、4 GB RAMを割り当てました。NixOS設定ファイルで `services.immich.enable = true` を有効化してImmichサービスを起動します。このサービスは `tailscale serve --bg http://localhost:2283` コマンドと MagicDNS/TLS によりTailscale経由で公開され、`https://photos.example.ts.net` からアクセス可能です。 公式の `immich‑cli` を使用した初期写真インポートでは、バックグラウンドジョブがタイムアウトし、Google Takeout のJSONメタデータが無視されるという問題が発生しました。第三者ツール **immich-go** が両方の問題を解決します。`immich-go upload from-google-photos …` を実行することでバックグラウンドタスクを一時停止し、Google Takeout アーカイブを正しく処理できます。その後、iPhoneアプリはTailscale URL経由でログインし、自動アップロードが有効化され、通知は無効にしてアップロードアラートを防止します。 バックアップについては、著者はsystemdタイマーを使用して `/var/lib/immich`(UPLOAD_LOCATION)ディレクトリ全体を rsync で3‑2‑1戦略で同期する予定です。これはImmichの公式ドキュメントに従った方法です。記事では、Immichには組み込みの写真編集機能がないため、ユーザーはGIMPなど外部ツールを使用しなければならず、共有もまだGoogle Photos経由で行われると指摘しています。Enteと比較して、著者は既存のTailscale VPNとLUKSディスク暗号化が十分なセキュリティを提供するため、エンドツーエンド暗号化を必要としないImmichを好んでいます。 総じて、このセットアップは小型かつ省電力マシン上で高速で信頼性の高いセルフホスト写真保存ソリューションを実現しており、外部編集ワークフローを受け入れられる趣味家や小規模ビジネスに適しています。

2025/12/06 12:32

Nook Browser

## Japanese Translation: > **概要:** > 製品「Browse」はプライバシーを最優先としたオープンソースのウェブブラウザで、ユーザーのデータが販売または追跡されることは決してないと約束しています。WebKit エンジンをベースに構築されており、高速なパフォーマンスと最小限のシステムオーバーヘッドを実現しながら、インターフェイスはクリーンで侵入的なポップアップがありません。チャット支援や要約、最新のウェブ情報などの AI 機能は、ユーザーが明示的に選択した場合のみ利用可能です。コードベース全体が公開されており、パーミッシブ ライセンスでリリースされています。また、コミュニティ主導のロードマップに従い、新しいツールを追加する前に安定性を優先しています。設定はユーザーが理解しやすく、逆行可能(戻せる)ように設計されています。FAQ セクションでは、これらのポイント以外に独自の情報は提供されていません。 このバージョンは主要なポイントをすべて保持し、業界への影響についての推測を削除し、設定の逆行性と FAQ の内容に関する欠落した詳細を追加しています。

2025/12/06 0:35

Cloudflare outage on December 5, 2025

## Japanese Translation: ``` ## Summary Cloudflare の 2025 年 12 月 5 日の障害は約 25 分間続きました。 08:47 UTC にネットワークセグメントが故障を開始し、08:50 UTC に完全な影響に達し、09:12 UTC に問題が解決しました。 全 HTTP トラフィックの約 28 %(古い FL1 プロキシと Managed Rulesets を使用している顧客)がエラーを経験しましたが、中国ネットワークトラフィックは影響を受けませんでした。 障害は、CVE‑2025‑55182(React Server Components の脆弱性)を修正するために意図された二つの急速なコード変更によって引き起こされました。 まず、WAF バッファサイズが 128 KB から 1 MB に増加し、段階的に展開されました。 次に、グローバル設定更新で内部 WAF テストツールが無効化され、FL1 のルールモジュールで Lua エラー(`attempt to index field 'execute' (a nil value)`)を引き起こし、HTTP 500 応答を生成しました。 このバグは何年も存在していましたが、「execute」ルールのキースイッチが execute フィールドが欠落した際に処理するコードを回避したために露呈しました。 同様で大規模なインシデントが 2025 年 11 月 18 日にも発生しました。 Cloudflare はロールアウト手順の強化、バージョン管理制御の追加、ブレイクグラスアクセスの簡素化、およびフェイルオープンエラーハンドリングの実装に取り組んでいます。詳細なレジリエンシー計画は来週公開される予定であり、新しい緩和策が稼働するまでネットワーク変更は停止されたままです。 この出来事は、大規模 CDN 運用における厳格な変更管理の必要性を強調し、迅速展開保護策に関する業界全体での見直しを促す可能性があります。 ```

Patterns for Defensive Programming in Rust | そっか~ニュース