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}