backup_suite/
error.rs

1use std::path::PathBuf;
2use thiserror::Error;
3
4/// backup-suite用のカスタムエラー型
5///
6/// すべてのバックアップ操作で発生する可能性のあるエラーを型安全に表現します。
7/// thiserrorを使用して、エラーメッセージの生成とエラー変換を自動化しています。
8#[derive(Error, Debug)]
9pub enum BackupError {
10    /// ホームディレクトリが見つからない場合
11    #[error("ホームディレクトリが見つかりません")]
12    HomeDirectoryNotFound,
13
14    /// バックアップ対象が存在しない場合
15    #[error("バックアップ対象が存在しません: {path}")]
16    TargetNotFound { path: PathBuf },
17
18    /// 読み取り権限がない場合
19    #[error("読み取り権限がありません: {path}")]
20    PermissionDenied { path: PathBuf },
21
22    /// ディレクトリトラバーサル攻撃を検出した場合
23    #[error("不正なパス(ディレクトリトラバーサル検出): {path}")]
24    PathTraversalDetected { path: PathBuf },
25
26    /// 親ディレクトリが見つからない場合
27    #[error("親ディレクトリが見つかりません: {path}")]
28    ParentDirectoryNotFound { path: PathBuf },
29
30    /// 設定ファイルの読み込みエラー
31    #[error("設定ファイルの読み込みに失敗: {0}")]
32    ConfigLoadError(#[from] toml::de::Error),
33
34    /// 設定ファイルのパースエラー
35    #[error("設定ファイルのパースに失敗: {message}")]
36    ConfigParseError { message: String },
37
38    /// 設定の検証エラー
39    #[error("設定の検証に失敗: {message}")]
40    ConfigValidationError { message: String },
41
42    /// I/Oエラー
43    #[error("I/Oエラー: {0}")]
44    IoError(#[from] std::io::Error),
45
46    /// 正規表現のコンパイルエラー
47    #[error("正規表現のコンパイルに失敗: {pattern}")]
48    RegexError {
49        pattern: String,
50        #[source]
51        source: regex::Error,
52    },
53
54    /// ファイルコピーエラー
55    #[error("ファイルコピーに失敗: {from} → {to}")]
56    FileCopyError { from: PathBuf, to: PathBuf },
57
58    /// バックアップディレクトリ作成エラー
59    #[error("バックアップディレクトリ作成に失敗: {path}")]
60    BackupDirectoryCreationError { path: PathBuf },
61
62    /// 暗号化・復号化エラー
63    #[error("暗号化エラー: {0}")]
64    EncryptionError(String),
65
66    /// 圧縮・展開エラー
67    #[error("圧縮エラー: {0}")]
68    CompressionError(String),
69
70    /// その他のエラー(anyhowからの変換用)
71    #[error("エラー: {0}")]
72    Other(#[from] anyhow::Error),
73}
74
75/// BackupError用のResult型エイリアス
76///
77/// # 使用例
78///
79/// ```rust
80/// use backup_suite::error::Result;
81///
82/// fn some_operation() -> Result<()> {
83///     // 操作
84///     Ok(())
85/// }
86/// ```
87pub type Result<T> = std::result::Result<T, BackupError>;
88
89impl BackupError {
90    /// エラーが回復可能かどうかを判定
91    ///
92    /// # 戻り値
93    ///
94    /// * `true` - リトライで回復可能な一時的エラー
95    /// * `false` - 回復不可能な恒久的エラー
96    #[must_use]
97    pub fn is_recoverable(&self) -> bool {
98        matches!(
99            self,
100            BackupError::IoError(_) | BackupError::FileCopyError { .. }
101        )
102    }
103
104    /// エラーがセキュリティに関連するかどうかを判定
105    ///
106    /// # 戻り値
107    ///
108    /// * `true` - セキュリティに関連するエラー
109    /// * `false` - 通常のエラー
110    #[must_use]
111    pub fn is_security_related(&self) -> bool {
112        matches!(
113            self,
114            BackupError::PathTraversalDetected { .. } | BackupError::PermissionDenied { .. }
115        )
116    }
117
118    /// ユーザーフレンドリーなエラーメッセージを生成
119    ///
120    /// # 戻り値
121    ///
122    /// エラーの詳細と推奨される対処法を含むメッセージ
123    #[must_use]
124    pub fn user_friendly_message(&self) -> String {
125        match self {
126            BackupError::HomeDirectoryNotFound => "ホームディレクトリが見つかりません。\n\
127                 対処法: 環境変数 $HOME が設定されているか確認してください。"
128                .to_string(),
129            BackupError::TargetNotFound { path } => {
130                format!(
131                    "バックアップ対象が存在しません: {}\n\
132                     対処法: パスが正しいか、ファイル/ディレクトリが存在するか確認してください。",
133                    path.display()
134                )
135            }
136            BackupError::PermissionDenied { path } => {
137                format!(
138                    "読み取り権限がありません: {}\n\
139                     対処法: ファイル/ディレクトリの権限を確認するか、sudo で実行してください。",
140                    path.display()
141                )
142            }
143            BackupError::PathTraversalDetected { path } => {
144                format!(
145                    "不正なパスが検出されました: {}\n\
146                     セキュリティ警告: ディレクトリトラバーサル攻撃の可能性があります。",
147                    path.display()
148                )
149            }
150            BackupError::ConfigValidationError { message } => {
151                format!(
152                    "設定に問題があります: {message}\n\
153                     対処法: ~/.config/backup-suite/config.toml を確認してください。"
154                )
155            }
156            BackupError::FileCopyError { from, to } => {
157                format!(
158                    "ファイルコピーに失敗しました:\n\
159                     元: {}\n\
160                     先: {}\n\
161                     対処法: ディスク容量と権限を確認してください。",
162                    from.display(),
163                    to.display()
164                )
165            }
166            _ => self.to_string(),
167        }
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_error_is_recoverable() {
177        let io_error =
178            BackupError::IoError(std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
179        assert!(io_error.is_recoverable());
180
181        let permission_error = BackupError::PermissionDenied {
182            path: PathBuf::from("/test"),
183        };
184        assert!(!permission_error.is_recoverable());
185    }
186
187    #[test]
188    fn test_error_is_security_related() {
189        let path_traversal = BackupError::PathTraversalDetected {
190            path: PathBuf::from("../../../etc/passwd"),
191        };
192        assert!(path_traversal.is_security_related());
193
194        let io_error =
195            BackupError::IoError(std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
196        assert!(!io_error.is_security_related());
197    }
198
199    #[test]
200    fn test_user_friendly_message() {
201        let error = BackupError::TargetNotFound {
202            path: PathBuf::from("/nonexistent"),
203        };
204        let message = error.user_friendly_message();
205        assert!(message.contains("対処法"));
206        assert!(message.contains("/nonexistent"));
207    }
208}