backup_suite/core/
integrity.rs

1//! # ファイル整合性検証モジュール
2//!
3//! SHA-256ハッシュベースのファイル整合性検証機能を提供します。
4//!
5//! # 機能
6//!
7//! - **ハッシュ計算**: ファイルのSHA-256ハッシュ計算
8//! - **メタデータ管理**: `.integrity` ファイルによるハッシュ保存
9//! - **検証**: 復元時のファイル整合性検証
10//!
11//! # 使用例
12//!
13//! ```no_run
14//! use backup_suite::core::integrity::{IntegrityChecker, BackupMetadata};
15//! use std::path::PathBuf;
16//!
17//! // バックアップ時:ハッシュ計算と保存
18//! let mut checker = IntegrityChecker::new();
19//! let file_path = PathBuf::from("test.txt");
20//! let hash = checker.compute_hash(&file_path).unwrap();
21//! checker.add_file_hash(file_path.clone(), hash);
22//!
23//! // メタデータ保存
24//! let backup_dir = PathBuf::from("/backup/backup_20250107_120000");
25//! checker.save_metadata(&backup_dir).unwrap();
26//!
27//! // 復元時:メタデータ読み込みと検証
28//! let metadata = BackupMetadata::load(&backup_dir).unwrap();
29//! let is_valid = metadata.verify_file(&file_path, &file_path).unwrap();
30//! assert!(is_valid);
31//! ```
32
33use anyhow::{Context, Result};
34use serde::{Deserialize, Serialize};
35use sha2::{Digest, Sha256};
36use std::collections::HashMap;
37use std::fs;
38use std::io::Read;
39use std::path::{Path, PathBuf};
40
41use super::incremental::BackupType;
42
43/// バックアップメタデータ
44///
45/// バックアップディレクトリ内のファイルハッシュ情報を管理します。
46///
47/// # フィールド
48///
49/// * `version` - メタデータ形式のバージョン
50/// * `file_hashes` - ファイルパスとSHA-256ハッシュのマップ
51/// * `timestamp` - バックアップ作成日時
52///
53/// # 使用例
54///
55/// ```no_run
56/// use backup_suite::core::integrity::BackupMetadata;
57/// use std::path::PathBuf;
58///
59/// // メタデータ読み込み
60/// let backup_dir = PathBuf::from("/backup/backup_20250107_120000");
61/// let metadata = BackupMetadata::load(&backup_dir).unwrap();
62///
63/// // ファイル検証
64/// let file_path = PathBuf::from("test.txt");
65/// let is_valid = metadata.verify_file(&file_path, &file_path).unwrap();
66/// ```
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct BackupMetadata {
69    /// メタデータ形式のバージョン
70    pub version: String,
71    /// ファイルパス(相対パス)とSHA-256ハッシュのマップ
72    pub file_hashes: HashMap<PathBuf, String>,
73    /// バックアップ作成日時(ISO 8601形式)
74    pub timestamp: String,
75    /// バックアップタイプ(Full/Incremental)
76    #[serde(default)]
77    pub backup_type: BackupType,
78    /// 親バックアップ名(増分バックアップの場合のみ)
79    #[serde(default)]
80    pub parent_backup: Option<String>,
81    /// 変更ファイルリスト(増分バックアップ時の変更ファイル)
82    #[serde(default)]
83    pub changed_files: Vec<PathBuf>,
84}
85
86impl BackupMetadata {
87    /// 新しいBackupMetadataを作成
88    ///
89    /// # 戻り値
90    ///
91    /// 空のファイルハッシュマップを持つ BackupMetadata インスタンス
92    ///
93    /// # 使用例
94    ///
95    /// ```
96    /// use backup_suite::core::integrity::BackupMetadata;
97    ///
98    /// let metadata = BackupMetadata::new();
99    /// assert_eq!(metadata.version, "1.0");
100    /// ```
101    #[must_use]
102    pub fn new() -> Self {
103        Self {
104            version: "1.0".to_string(),
105            file_hashes: HashMap::new(),
106            timestamp: chrono::Utc::now().to_rfc3339(),
107            backup_type: BackupType::Full,
108            parent_backup: None,
109            changed_files: Vec::new(),
110        }
111    }
112
113    /// バックアップディレクトリからメタデータを読み込み
114    ///
115    /// `.integrity` ファイルから JSON 形式のメタデータを読み込みます。
116    ///
117    /// # 引数
118    ///
119    /// * `backup_dir` - バックアップディレクトリのパス
120    ///
121    /// # 戻り値
122    ///
123    /// 成功時は読み込まれた BackupMetadata、失敗時はエラー
124    ///
125    /// # Errors
126    ///
127    /// 以下の場合にエラーを返します:
128    /// * `.integrity` ファイルが存在しない場合
129    /// * ファイルの読み込みに失敗した場合
130    /// * JSON 解析に失敗した場合
131    ///
132    /// # 使用例
133    ///
134    /// ```no_run
135    /// use backup_suite::core::integrity::BackupMetadata;
136    /// use std::path::PathBuf;
137    ///
138    /// let backup_dir = PathBuf::from("/backup/backup_20250107_120000");
139    /// let metadata = BackupMetadata::load(&backup_dir).unwrap();
140    /// ```
141    pub fn load(backup_dir: &Path) -> Result<Self> {
142        let metadata_path = backup_dir.join(".integrity");
143        if !metadata_path.exists() {
144            return Err(anyhow::anyhow!(
145                "整合性メタデータが見つかりません: metadata_path.display()".to_string()
146            ));
147        }
148
149        let content = fs::read_to_string(&metadata_path)
150            .context("メタデータ読み込み失敗: metadata_path.display()".to_string())?;
151        let metadata: BackupMetadata =
152            serde_json::from_str(&content).context("メタデータJSON解析失敗")?;
153
154        Ok(metadata)
155    }
156
157    /// バックアップディレクトリにメタデータを保存
158    ///
159    /// `.integrity` ファイルに JSON 形式でメタデータを保存します。
160    ///
161    /// # 引数
162    ///
163    /// * `backup_dir` - バックアップディレクトリのパス
164    ///
165    /// # 戻り値
166    ///
167    /// 成功時は `Ok(())`、失敗時はエラー
168    ///
169    /// # Errors
170    ///
171    /// 以下の場合にエラーを返します:
172    /// * JSON 生成に失敗した場合
173    /// * ファイル書き込みに失敗した場合
174    ///
175    /// # 使用例
176    ///
177    /// ```no_run
178    /// use backup_suite::core::integrity::BackupMetadata;
179    /// use std::path::PathBuf;
180    ///
181    /// let mut metadata = BackupMetadata::new();
182    /// metadata.file_hashes.insert(PathBuf::from("test.txt"), "abc123...".to_string());
183    ///
184    /// let backup_dir = PathBuf::from("/backup/backup_20250107_120000");
185    /// metadata.save(&backup_dir).unwrap();
186    /// ```
187    pub fn save(&self, backup_dir: &Path) -> Result<()> {
188        let metadata_path = backup_dir.join(".integrity");
189        let content = serde_json::to_string_pretty(self).context("メタデータJSON生成失敗")?;
190        fs::write(&metadata_path, content)
191            .context("メタデータ保存失敗: metadata_path.display()".to_string())?;
192        Ok(())
193    }
194
195    /// ファイルの整合性を検証
196    ///
197    /// ファイルの現在のSHA-256ハッシュを計算し、保存されたハッシュと比較します。
198    ///
199    /// # 引数
200    ///
201    /// * `relative_path` - バックアップ内の相対パス
202    /// * `actual_file_path` - 検証対象の実際のファイルパス
203    ///
204    /// # 戻り値
205    ///
206    /// ハッシュが一致する場合 `true`、不一致の場合 `false`
207    ///
208    /// # Errors
209    ///
210    /// 以下の場合にエラーを返します:
211    /// * ファイルに対応するハッシュ情報が見つからない場合
212    /// * ファイルの読み込みに失敗した場合
213    /// * ハッシュ計算に失敗した場合
214    ///
215    /// # 使用例
216    ///
217    /// ```no_run
218    /// use backup_suite::core::integrity::BackupMetadata;
219    /// use std::path::PathBuf;
220    ///
221    /// let backup_dir = PathBuf::from("/backup/backup_20250107_120000");
222    /// let metadata = BackupMetadata::load(&backup_dir).unwrap();
223    ///
224    /// let relative = PathBuf::from("test.txt");
225    /// let actual = PathBuf::from("/restore/test.txt");
226    /// let is_valid = metadata.verify_file(&relative, &actual).unwrap();
227    ///
228    /// if is_valid {
229    ///     println!("✓ ファイル整合性確認済み");
230    /// } else {
231    ///     eprintln!("⚠ ファイルが改ざんされています");
232    /// }
233    /// ```
234    pub fn verify_file(&self, relative_path: &Path, actual_file_path: &Path) -> Result<bool> {
235        let expected_hash = match self.file_hashes.get(relative_path) {
236            Some(h) => h,
237            None => {
238                return Err(anyhow::anyhow!(
239                    "ファイルのハッシュ情報が見つかりません: relative_path.display()".to_string()
240                ));
241            }
242        };
243
244        let actual_hash = Self::compute_file_hash(actual_file_path)?;
245        Ok(&actual_hash == expected_hash)
246    }
247
248    /// ファイルのSHA-256ハッシュを計算(公開静的メソッド)
249    ///
250    /// # 引数
251    ///
252    /// * `file_path` - ハッシュ計算対象のファイルパス
253    ///
254    /// # 戻り値
255    ///
256    /// 成功時は16進数文字列形式のSHA-256ハッシュ、失敗時はエラー
257    ///
258    /// # Errors
259    ///
260    /// 以下の場合にエラーを返します:
261    /// * ファイルのオープンに失敗した場合
262    /// * ファイルの読み込みに失敗した場合
263    pub fn compute_file_hash(file_path: &Path) -> Result<String> {
264        let mut file = fs::File::open(file_path)
265            .context("ファイル読み込み失敗: file_path.display()".to_string())?;
266
267        let mut hasher = Sha256::new();
268        let mut buffer = vec![0u8; 8192]; // 8KB バッファ
269
270        loop {
271            let bytes_read = file.read(&mut buffer).context("ファイル読み込みエラー")?;
272            if bytes_read == 0 {
273                break;
274            }
275            hasher.update(&buffer[..bytes_read]);
276        }
277
278        let result = hasher.finalize();
279        Ok(format!("{result:x}"))
280    }
281}
282
283impl Default for BackupMetadata {
284    fn default() -> Self {
285        Self::new()
286    }
287}
288
289/// 整合性検証エンジン
290///
291/// バックアップ時のハッシュ計算と保存を担当します。
292///
293/// # 使用例
294///
295/// ```no_run
296/// use backup_suite::core::integrity::IntegrityChecker;
297/// use std::path::PathBuf;
298///
299/// let mut checker = IntegrityChecker::new();
300///
301/// // ファイルハッシュ計算と追加
302/// let file = PathBuf::from("test.txt");
303/// let hash = checker.compute_hash(&file).unwrap();
304/// checker.add_file_hash(file, hash);
305///
306/// // メタデータ保存
307/// let backup_dir = PathBuf::from("/backup/backup_20250107_120000");
308/// checker.save_metadata(&backup_dir).unwrap();
309/// ```
310pub struct IntegrityChecker {
311    pub metadata: BackupMetadata,
312}
313
314impl IntegrityChecker {
315    /// 新しいIntegrityCheckerを作成
316    ///
317    /// # 戻り値
318    ///
319    /// 空のメタデータを持つ IntegrityChecker インスタンス
320    ///
321    /// # 使用例
322    ///
323    /// ```
324    /// use backup_suite::core::integrity::IntegrityChecker;
325    ///
326    /// let checker = IntegrityChecker::new();
327    /// ```
328    #[must_use]
329    pub fn new() -> Self {
330        Self {
331            metadata: BackupMetadata::new(),
332        }
333    }
334
335    /// ファイルのSHA-256ハッシュを計算
336    ///
337    /// # 引数
338    ///
339    /// * `file_path` - ハッシュ計算対象のファイルパス
340    ///
341    /// # 戻り値
342    ///
343    /// 成功時は16進数文字列形式のSHA-256ハッシュ、失敗時はエラー
344    ///
345    /// # Errors
346    ///
347    /// 以下の場合にエラーを返します:
348    /// * ファイルのオープンに失敗した場合
349    /// * ファイルの読み込みに失敗した場合
350    ///
351    /// # 使用例
352    ///
353    /// ```no_run
354    /// use backup_suite::core::integrity::IntegrityChecker;
355    /// use std::path::PathBuf;
356    ///
357    /// let checker = IntegrityChecker::new();
358    /// let hash = checker.compute_hash(&PathBuf::from("test.txt")).unwrap();
359    /// println!("SHA-256: {}", hash);
360    /// ```
361    pub fn compute_hash(&self, file_path: &Path) -> Result<String> {
362        BackupMetadata::compute_file_hash(file_path)
363    }
364
365    /// ファイルハッシュをメタデータに追加
366    ///
367    /// # 引数
368    ///
369    /// * `relative_path` - バックアップ内の相対パス
370    /// * `hash` - SHA-256ハッシュ(16進数文字列)
371    ///
372    /// # 使用例
373    ///
374    /// ```
375    /// use backup_suite::core::integrity::IntegrityChecker;
376    /// use std::path::PathBuf;
377    ///
378    /// let mut checker = IntegrityChecker::new();
379    /// checker.add_file_hash(PathBuf::from("test.txt"), "abc123...".to_string());
380    /// ```
381    pub fn add_file_hash(&mut self, relative_path: PathBuf, hash: String) {
382        self.metadata.file_hashes.insert(relative_path, hash);
383    }
384
385    /// メタデータをバックアップディレクトリに保存
386    ///
387    /// # 引数
388    ///
389    /// * `backup_dir` - バックアップディレクトリのパス
390    ///
391    /// # 戻り値
392    ///
393    /// 成功時は `Ok(())`、失敗時はエラー
394    ///
395    /// # Errors
396    ///
397    /// 以下の場合にエラーを返します:
398    /// * メタデータの保存に失敗した場合
399    ///
400    /// # 使用例
401    ///
402    /// ```no_run
403    /// use backup_suite::core::integrity::IntegrityChecker;
404    /// use std::path::PathBuf;
405    ///
406    /// let mut checker = IntegrityChecker::new();
407    /// checker.add_file_hash(PathBuf::from("test.txt"), "abc123...".to_string());
408    /// checker.save_metadata(&PathBuf::from("/backup/backup_20250107_120000")).unwrap();
409    /// ```
410    pub fn save_metadata(&self, backup_dir: &Path) -> Result<()> {
411        self.metadata.save(backup_dir)
412    }
413
414    /// 保存予定のファイル数を取得
415    ///
416    /// # 戻り値
417    ///
418    /// 登録されているファイルハッシュの数
419    ///
420    /// # 使用例
421    ///
422    /// ```
423    /// use backup_suite::core::integrity::IntegrityChecker;
424    /// use std::path::PathBuf;
425    ///
426    /// let mut checker = IntegrityChecker::new();
427    /// checker.add_file_hash(PathBuf::from("test.txt"), "abc123...".to_string());
428    /// assert_eq!(checker.file_count(), 1);
429    /// ```
430    #[must_use]
431    pub fn file_count(&self) -> usize {
432        self.metadata.file_hashes.len()
433    }
434}
435
436impl Default for IntegrityChecker {
437    fn default() -> Self {
438        Self::new()
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445    use std::io::Write;
446    use tempfile::TempDir;
447
448    #[test]
449    fn test_compute_hash() {
450        let temp = TempDir::new().unwrap();
451        let file_path = temp.path().join("test.txt");
452        let mut file = fs::File::create(&file_path).unwrap();
453        file.write_all(b"test content").unwrap();
454        drop(file);
455
456        let checker = IntegrityChecker::new();
457        let hash = checker.compute_hash(&file_path).unwrap();
458
459        // SHA-256("test content") の期待値
460        let expected = "6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72";
461        assert_eq!(hash, expected);
462    }
463
464    #[test]
465    fn test_add_file_hash() {
466        let mut checker = IntegrityChecker::new();
467        let path = PathBuf::from("test.txt");
468        let hash = "abc123".to_string();
469
470        checker.add_file_hash(path.clone(), hash.clone());
471        assert_eq!(checker.file_count(), 1);
472        assert_eq!(checker.metadata.file_hashes.get(&path), Some(&hash));
473    }
474
475    #[test]
476    fn test_save_and_load_metadata() {
477        let temp = TempDir::new().unwrap();
478        let backup_dir = temp.path().join("backup");
479        fs::create_dir(&backup_dir).unwrap();
480
481        // メタデータ保存
482        let mut checker = IntegrityChecker::new();
483        checker.add_file_hash(PathBuf::from("test.txt"), "hash1".to_string());
484        checker.add_file_hash(PathBuf::from("data/file.dat"), "hash2".to_string());
485        checker.save_metadata(&backup_dir).unwrap();
486
487        // メタデータ読み込み
488        let loaded = BackupMetadata::load(&backup_dir).unwrap();
489        assert_eq!(loaded.file_hashes.len(), 2);
490        assert_eq!(
491            loaded.file_hashes.get(&PathBuf::from("test.txt")),
492            Some(&"hash1".to_string())
493        );
494        assert_eq!(
495            loaded.file_hashes.get(&PathBuf::from("data/file.dat")),
496            Some(&"hash2".to_string())
497        );
498    }
499
500    #[test]
501    fn test_verify_file() {
502        let temp = TempDir::new().unwrap();
503        let backup_dir = temp.path().join("backup");
504        fs::create_dir(&backup_dir).unwrap();
505
506        // テストファイル作成
507        let test_file = temp.path().join("test.txt");
508        let mut file = fs::File::create(&test_file).unwrap();
509        file.write_all(b"test content").unwrap();
510        drop(file);
511
512        // ハッシュ計算と保存
513        let mut checker = IntegrityChecker::new();
514        let hash = checker.compute_hash(&test_file).unwrap();
515        checker.add_file_hash(PathBuf::from("test.txt"), hash);
516        checker.save_metadata(&backup_dir).unwrap();
517
518        // 検証
519        let metadata = BackupMetadata::load(&backup_dir).unwrap();
520        let is_valid = metadata
521            .verify_file(&PathBuf::from("test.txt"), &test_file)
522            .unwrap();
523        assert!(is_valid);
524    }
525
526    #[test]
527    fn test_verify_file_tampered() {
528        let temp = TempDir::new().unwrap();
529        let backup_dir = temp.path().join("backup");
530        fs::create_dir(&backup_dir).unwrap();
531
532        // テストファイル作成
533        let test_file = temp.path().join("test.txt");
534        let mut file = fs::File::create(&test_file).unwrap();
535        file.write_all(b"test content").unwrap();
536        drop(file);
537
538        // ハッシュ計算と保存
539        let mut checker = IntegrityChecker::new();
540        let hash = checker.compute_hash(&test_file).unwrap();
541        checker.add_file_hash(PathBuf::from("test.txt"), hash);
542        checker.save_metadata(&backup_dir).unwrap();
543
544        // ファイルを改ざん
545        fs::write(&test_file, b"tampered content").unwrap();
546
547        // 検証(失敗するはず)
548        let metadata = BackupMetadata::load(&backup_dir).unwrap();
549        let is_valid = metadata
550            .verify_file(&PathBuf::from("test.txt"), &test_file)
551            .unwrap();
552        assert!(!is_valid);
553    }
554
555    #[test]
556    fn test_metadata_json_format() {
557        let temp = TempDir::new().unwrap();
558        let backup_dir = temp.path().join("backup");
559        fs::create_dir(&backup_dir).unwrap();
560
561        let mut checker = IntegrityChecker::new();
562        checker.add_file_hash(PathBuf::from("test.txt"), "hash123".to_string());
563        checker.save_metadata(&backup_dir).unwrap();
564
565        // JSON ファイルの内容を確認
566        let content = fs::read_to_string(backup_dir.join(".integrity")).unwrap();
567        assert!(content.contains("\"version\""));
568        assert!(content.contains("\"file_hashes\""));
569        assert!(content.contains("\"timestamp\""));
570        assert!(content.contains("test.txt"));
571        assert!(content.contains("hash123"));
572    }
573}