diff --git a/locale/en-US.ftl b/locale/en-US.ftl index 353afe4..c360a1a 100644 --- a/locale/en-US.ftl +++ b/locale/en-US.ftl @@ -4,6 +4,9 @@ view_main_open_dir = Open a directory view_main_open_receipt = Open a notice of receipt view_main_calc_fingerprints = Checksum calculation view_main_check_fingerprints = Data integrity check +view_main_check_result_title = Data integrity check result +view_main_check_result_ok_text = Data integrity check passed. +view_main_check_result_err_text = Data integrity check failed. cpn_file_list_delete = Reset diff --git a/locale/fr-BE.ftl b/locale/fr-BE.ftl index 1c5d2f3..c554686 100644 --- a/locale/fr-BE.ftl +++ b/locale/fr-BE.ftl @@ -4,6 +4,9 @@ view_main_open_dir = Ouvrir un dossier view_main_open_receipt = Ouvrir un AR view_main_calc_fingerprints = Calculer les empreintes view_main_check_fingerprints = Vérifier les empreintes +view_main_check_result_title = Vérification des empreintes +view_main_check_result_ok_text = Les empreintes correspondent. +view_main_check_result_err_text = Échec de la vérification des empreintes. cpn_file_list_delete = Réinitialiser diff --git a/locale/fr-FR.ftl b/locale/fr-FR.ftl index 845eaa6..83869a9 100644 --- a/locale/fr-FR.ftl +++ b/locale/fr-FR.ftl @@ -4,6 +4,9 @@ view_main_open_dir = Ouvrir un dossier view_main_open_receipt = Ouvrir un AR view_main_calc_fingerprints = Calculer les empreintes view_main_check_fingerprints = Vérifier les empreintes +view_main_check_result_title = Vérification des empreintes +view_main_check_result_ok_text = Les empreintes correspondent. +view_main_check_result_err_text = Échec de la vérification des empreintes. cpn_file_list_delete = Réinitialiser diff --git a/src/check.rs b/src/check.rs new file mode 100644 index 0000000..94afe38 --- /dev/null +++ b/src/check.rs @@ -0,0 +1,133 @@ +use crate::files::HashedFile; +use dioxus_logger::tracing::warn; +use std::collections::HashSet; +use std::fmt; +use std::path::PathBuf; + +#[derive(Debug, Clone, Copy)] +pub enum CheckType { + ContentFile, + Receipt, +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub enum CheckResultError { + ContentFileParseError, + ContentFileMissingFile(PathBuf), + ContentFileNonMatchingFile(PathBuf), + ReceiptMissingFile(PathBuf), + ReceiptNonMatchingFile(PathBuf), +} + +impl fmt::Display for CheckResultError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let ctn_file_fmt = match &self { + Self::ContentFileParseError => "content file: parse error".to_string(), + Self::ContentFileMissingFile(p) => { + format!("content file: missing file: {}", p.display()) + } + Self::ContentFileNonMatchingFile(p) => { + format!("content file: non matching file: {}", p.display()) + } + Self::ReceiptMissingFile(p) => format!("receipt: missing file: {}", p.display()), + Self::ReceiptNonMatchingFile(p) => { + format!("receipt: non matching file: {}", p.display()) + } + }; + write!(f, "{ctn_file_fmt}") + } +} + +#[derive(Debug, Clone)] +pub enum CheckResult { + Error(Vec), + Ok, + None, +} + +impl CheckResult { + pub fn is_err(&self) -> bool { + matches!(self, Self::Error(_)) + } + + pub fn is_ok(&self) -> bool { + matches!(self, Self::Ok) + } +} + +pub fn check( + calculated_set: &Vec, + reference_set: &Vec, + t: CheckType, +) -> CheckResult { + let mut errors = HashSet::new(); + + for ref_file in reference_set { + // Get the canonical absolute path of the reference file. + match ref_file.get_absolute_path() { + Ok(ref_file_abs_path) => { + // We have the canonical absolute path of the reference file. + // Now, let's check if we can find it in the calculated set. + match get_calc_file(calculated_set, ref_file_abs_path) { + Ok(calc_file) => { + if ref_file.get_hash() != calc_file.get_hash() { + // The hashes from both files does not match. + add_non_matching_file(&mut errors, ref_file, t); + } + } + Err(_) => { + // No matching file found in the calculated set. + add_missing_file(&mut errors, ref_file, t); + } + }; + } + Err(_) => { + // Unable to get the canonical path: the file does not exists on disk. + add_missing_file(&mut errors, ref_file, t); + } + }; + } + + // Return the result + if errors.is_empty() { + CheckResult::Ok + } else { + CheckResult::Error(errors.into_iter().collect()) + } +} + +fn get_calc_file( + calculated_set: &Vec, + ref_file_abs_path: PathBuf, +) -> Result<&HashedFile, ()> { + for calc_file in calculated_set { + if let Ok(calc_file_abs_path) = calc_file.get_absolute_path() { + if calc_file_abs_path == ref_file_abs_path { + return Ok(calc_file); + } + } + } + Err(()) +} + +#[inline] +fn add_missing_file(errors: &mut HashSet, file: &HashedFile, t: CheckType) { + let path = file.get_relative_path().to_path_buf(); + let e = match t { + CheckType::ContentFile => CheckResultError::ContentFileMissingFile(path), + CheckType::Receipt => CheckResultError::ReceiptMissingFile(path), + }; + warn!("{e}"); + errors.insert(e); +} + +#[inline] +fn add_non_matching_file(errors: &mut HashSet, file: &HashedFile, t: CheckType) { + let path = file.get_relative_path().to_path_buf(); + let e = match t { + CheckType::ContentFile => CheckResultError::ContentFileNonMatchingFile(path), + CheckType::Receipt => CheckResultError::ReceiptNonMatchingFile(path), + }; + warn!("{e}"); + errors.insert(e); +} diff --git a/src/files.rs b/src/files.rs index 6996096..4396e76 100644 --- a/src/files.rs +++ b/src/files.rs @@ -1,3 +1,4 @@ +use crate::check::{CheckResult, CheckResultError}; use crate::config::Config; use crate::content_file::ContentFileFormat; use crate::events::ExternalEventSender; @@ -103,8 +104,10 @@ macro_rules! common_lst_impl { self.base_dir.as_path() } - pub fn get_files(&self) -> Vec<$file_type> { - self.files.iter().map(|(_, v)| v.clone()).collect() + pub fn get_content_file_absolute_path(&self, config: &Config) -> io::Result { + let mut path = self.base_dir.clone().canonicalize()?; + path.push(config.get_content_file_name()); + Ok(path) } } }; @@ -122,12 +125,6 @@ pub struct NonHashedFileList { common_lst_impl!(NonHashedFileList, NonHashedFile); impl NonHashedFileList { - fn get_content_file_absolute_path(&self, config: &Config) -> io::Result { - let mut path = self.base_dir.clone().canonicalize()?; - path.push(config.get_content_file_name()); - Ok(path) - } - pub fn len(&self) -> usize { self.files.len() } @@ -136,6 +133,13 @@ impl NonHashedFileList { self.files.values().fold(0, |acc, f| acc + f.size) } + pub fn content_file_exists(&self, config: &Config) -> bool { + if let Ok(ctn_file_path) = self.get_content_file_absolute_path(config) { + return ctn_file_path.is_file(); + } + false + } + pub async fn from_dir>( dir_path: P, include_hidden_files: bool, @@ -257,6 +261,7 @@ impl NonHashedFileList { files, empty_files: self.empty_files.clone(), duplicated_files, + result: CheckResult::None, }; hashed_lst .write_content_file_opt(ctn_file_absolute_path.as_path(), config.content_file_format)?; @@ -274,6 +279,7 @@ pub struct HashedFileList { files: HashMap, empty_files: HashSet, duplicated_files: HashMap>, + result: CheckResult, } common_lst_impl!(HashedFileList, HashedFile); @@ -286,17 +292,54 @@ impl HashedFileList { files: HashMap::new(), empty_files: HashSet::new(), duplicated_files: HashMap::new(), + result: CheckResult::None, } } + pub fn get_files(&self, base_dir: &Path) -> Vec { + self.files + .values() + .map(|v| { + let mut f = v.clone(); + f.base_dir = base_dir.to_path_buf(); + f + }) + .collect() + } + + pub fn get_files_no_base_dir(&self) -> Vec { + self.get_files(PathBuf::new().as_path()) + } + pub fn insert_file(&mut self, file: HashedFile) { self.files.insert(file.get_id(), file); } + pub fn set_result_ok(&mut self) { + self.result = CheckResult::Ok; + } + + pub fn push_result_error(&mut self, error: CheckResultError) { + match &self.result { + CheckResult::Error(v) => { + let mut v = v.clone(); + v.push(error); + self.result = CheckResult::Error(v); + } + _ => { + self.result = CheckResult::Error(vec![error]); + } + } + } + pub fn is_empty(&self) -> bool { self.files.is_empty() } + pub fn get_result(&self) -> CheckResult { + self.result.clone() + } + pub fn get_main_hashing_function(&self) -> HashFunc { let mut occurrences = HashMap::with_capacity(self.files.len()); for file in self.files.values() { @@ -345,23 +388,11 @@ macro_rules! common_file_impl { ) } - pub fn get_base_dir(&self) -> &Path { - self.base_dir.as_path() - } - - pub fn get_relative_path(&self) -> &Path { - self.relative_path.as_path() - } - pub fn get_absolute_path(&self) -> io::Result { let mut path = self.base_dir.clone(); path.push(self.relative_path.clone()); path.canonicalize() } - - pub fn is_empty(&self) -> bool { - self.size == 0 - } } }; } @@ -378,6 +409,10 @@ pub struct NonHashedFile { common_file_impl!(NonHashedFile); impl NonHashedFile { + pub fn is_empty(&self) -> bool { + self.size == 0 + } + pub fn new>(base_dir: P, path: P) -> io::Result { let base_dir = base_dir.as_ref(); let path = path.as_ref(); @@ -406,7 +441,7 @@ impl NonHashedFile { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct HashedFile { base_dir: PathBuf, relative_path: PathBuf, @@ -443,6 +478,10 @@ impl HashedFile { pub fn get_hash_func(&self) -> HashFunc { self.hash_func } + + pub fn get_relative_path(&self) -> &Path { + self.relative_path.as_path() + } } #[inline] diff --git a/src/main.rs b/src/main.rs index 5213edb..10a7cab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod analyse_hash; mod app; mod assets; +mod check; mod components; mod config; mod content_file; diff --git a/src/receipt.rs b/src/receipt.rs index 90ed61c..608089d 100644 --- a/src/receipt.rs +++ b/src/receipt.rs @@ -23,8 +23,8 @@ impl Receipt { }) } - pub fn get_files(&self) -> Vec { - self.files.get_files() + pub fn get_files(&self, base_dir: &Path) -> Vec { + self.files.get_files(base_dir) } pub fn get_main_hashing_function(&self) -> HashFunc { diff --git a/src/serializers/ctn_file_cksum_bsd.rs b/src/serializers/ctn_file_cksum_bsd.rs index 7524de4..5872386 100644 --- a/src/serializers/ctn_file_cksum_bsd.rs +++ b/src/serializers/ctn_file_cksum_bsd.rs @@ -3,7 +3,7 @@ use std::fs::File; use std::io::{self, Write}; pub fn ctn_file_cksum_bsd(ctn_file: &mut File, hashed_list: &HashedFileList) -> io::Result<()> { - for file in hashed_list.get_files() { + for file in hashed_list.get_files_no_base_dir() { let line = format_line(&file); ctn_file.write_all(line.as_bytes())?; } diff --git a/src/serializers/ctn_file_cksum_gnu.rs b/src/serializers/ctn_file_cksum_gnu.rs index ef434b5..de14802 100644 --- a/src/serializers/ctn_file_cksum_gnu.rs +++ b/src/serializers/ctn_file_cksum_gnu.rs @@ -3,7 +3,7 @@ use std::fs::File; use std::io::{self, Write}; pub fn ctn_file_cksum_gnu(ctn_file: &mut File, hashed_list: &HashedFileList) -> io::Result<()> { - for file in hashed_list.get_files() { + for file in hashed_list.get_files_no_base_dir() { let line = format_line(&file); ctn_file.write_all(line.as_bytes())?; } diff --git a/src/serializers/ctn_file_cnil.rs b/src/serializers/ctn_file_cnil.rs index 5c9a814..e66d5d5 100644 --- a/src/serializers/ctn_file_cnil.rs +++ b/src/serializers/ctn_file_cnil.rs @@ -16,7 +16,7 @@ pub fn ctn_file_cnil(ctn_file: &mut File, hashed_list: &HashedFileList) -> io::R "Taille (octets)", hashed_list.get_main_hashing_function() ); - for file in hashed_list.get_files() { + for file in hashed_list.get_files_no_base_dir() { write_line!( ctn_file, file.get_relative_path().display(), diff --git a/src/views/main.rs b/src/views/main.rs index fcbb82a..8b5ef80 100644 --- a/src/views/main.rs +++ b/src/views/main.rs @@ -1,12 +1,14 @@ #![allow(non_snake_case)] +use crate::check::{check, CheckResult, CheckResultError, CheckType}; use crate::components::{ Button, DropZone, FileButton, FileListIndicator, FileListReceipt, Header, LoadingBar, - NotificationList, ProgressBar, + Notification, NotificationList, ProgressBar, }; use crate::config::Config; use crate::events::{send_event, send_event_sync, ExternalEvent, ExternalEventSender}; use crate::files::{FileList, NonHashedFileList}; +use crate::notifications::NotificationLevel; use crate::progress::ProgressBarStatus; use crate::receipt::Receipt; use dioxus::html::{FileEngine, HasFileData}; @@ -76,24 +78,43 @@ pub fn Main() -> Element { if pg_status_opt.is_none() { div { - if let FileList::NonHashed(_) = file_list_sig() { - Button { - onclick: move |_event| { - spawn(async move { - calc_fingerprints(&config_sig(), tx_sig(), receipt_opt_sig(), file_list_sig()).await; - }); - }, - { t!("view_main_calc_fingerprints") } + if let FileList::NonHashed(file_lst) = file_list_sig() { + if file_lst.content_file_exists(&config_sig()) { + Button { + onclick: move |_event| { + spawn(async move { + calc_fingerprints(&config_sig(), tx_sig(), receipt_opt_sig(), file_list_sig()).await; + }); + }, + { t!("view_main_check_fingerprints") } + } + } else { + Button { + onclick: move |_event| { + spawn(async move { + calc_fingerprints(&config_sig(), tx_sig(), receipt_opt_sig(), file_list_sig()).await; + }); + }, + { t!("view_main_calc_fingerprints") } + } } } - if let FileList::Hashed(_) = file_list_sig() { - Button { - onclick: move |_event| { - spawn(async move { - check_fingerprints().await; - }); - }, - { t!("view_main_check_fingerprints") } + if let FileList::Hashed(lst) = file_list_sig() { + if let CheckResult::Ok = lst.get_result() { + Notification { + id: "view-main-file-check-ok", + level: NotificationLevel::Success, + title: t!("view_main_check_result_title"), + p { { t!("view_main_check_result_ok_text") } } + } + } + if let CheckResult::Error(_) = lst.get_result() { + Notification { + id: "view-main-file-check-err", + level: NotificationLevel::Error, + title: t!("view_main_check_result_title"), + p { { t!("view_main_check_result_err_text") } } + } } } } @@ -189,22 +210,77 @@ async fn calc_fingerprints( thread::spawn(move || { info!("File hashing thread started"); + let base_dir = file_list.get_base_dir(); let total_size = file_list.total_size(); send_event_sync(&tx, ExternalEvent::ProgressBarCreate(total_size)); info!("Total size to hash: {total_size} bytes"); + // Calculating fingerprints match file_list.hash(&config, hash_func, tx.clone()) { - Ok(hashed_file_list) => { + Ok(mut hashed_file_list) => { + send_event_sync(&tx, ExternalEvent::ProgressBarDelete); + send_event_sync(&tx, ExternalEvent::LoadingBarAdd); + + // Checking fingerprints against the content file + info!("Checking fingerprints against the content file"); + let hashed_file_lst = hashed_file_list.get_files(base_dir); + if let Ok(ctn_file_path) = + hashed_file_list.get_content_file_absolute_path(&config) + { + let default_hash = match crate::analyse_hash::from_path(&ctn_file_path) { + Some(h) => h, + None => config.hash_function, + }; + match Receipt::new(&ctn_file_path, default_hash) { + Ok(ctn_file) => { + match check( + &hashed_file_lst, + &ctn_file.get_files(base_dir), + CheckType::ContentFile, + ) { + CheckResult::Ok => hashed_file_list.set_result_ok(), + CheckResult::Error(err_lst) => { + for e in err_lst { + hashed_file_list.push_result_error(e); + } + } + CheckResult::None => {} + } + } + Err(_) => { + hashed_file_list + .push_result_error(CheckResultError::ContentFileParseError); + } + }; + } + + // Checking fingerprints against the receipt + if let Some(rcpt) = receipt_opt { + info!("Checking fingerprints against the receipt"); + match check( + &hashed_file_lst, + &rcpt.get_files(base_dir), + CheckType::Receipt, + ) { + CheckResult::Ok => { + if !hashed_file_list.get_result().is_err() { + hashed_file_list.set_result_ok() + } + } + CheckResult::Error(err_lst) => { + for e in err_lst { + hashed_file_list.push_result_error(e); + } + } + CheckResult::None => {} + } + } + send_event_sync(&tx, ExternalEvent::HashedFileListSet(hashed_file_list)); + send_event_sync(&tx, ExternalEvent::LoadingBarDelete); } Err(e) => error!("Unable to hash files: {e}"), }; - send_event_sync(&tx, ExternalEvent::ProgressBarDelete); - - if let Some(rcpt) = receipt_opt { - info!("Checking fingerprints against the receipt"); - // TODO - } info!("File hashing thread done"); }); @@ -212,9 +288,3 @@ async fn calc_fingerprints( info!("File hashing async function done"); } - -async fn check_fingerprints() { - info!("Data integrity check async function started"); - // TODO - info!("Data integrity check async function done"); -}