use std::error::Error; use std::fmt::{Display, Formatter}; use std::io::ErrorKind; use std::path::{Component, Path, PathBuf}; use log::debug; use sha1_smol::{Digest, Sha1}; use tokio::fs::File; use tokio::io::AsyncReadExt; #[derive(Debug)] pub enum IntegrityError { SizeMismatch{ expect: usize, actual: usize }, Sha1Mismatch{ expect: Digest, actual: Digest } } impl Display for IntegrityError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { IntegrityError::SizeMismatch{ expect, actual } => write!(f, "size mismatch (expect {expect} bytes, got {actual} bytes)"), IntegrityError::Sha1Mismatch {expect, actual} => write!(f, "sha1 mismatch (expect {expect}, got {actual})") } } } impl Error for IntegrityError {} pub fn verify_sha1(expect: Digest, s: &str) -> Result<(), Digest> { let dig = Sha1::from(s).digest(); if dig == expect { return Ok(()); } Err(dig) } #[derive(Debug)] pub enum FileVerifyError { Integrity(PathBuf, IntegrityError), Open(PathBuf, tokio::io::Error), Read(PathBuf, tokio::io::Error), } impl Display for FileVerifyError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { FileVerifyError::Integrity(path, e) => write!(f, "file integrity error {}: {}", path.display(), e), FileVerifyError::Open(path, e) => write!(f, "error opening file {}: {}", path.display(), e), FileVerifyError::Read(path, e) => write!(f, "error reading file {}: {}", path.display(), e) } } } impl Error for FileVerifyError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { FileVerifyError::Integrity(_, e) => Some(e), FileVerifyError::Open(_, e) => Some(e), FileVerifyError::Read(_, e) => Some(e) } } } pub async fn verify_file(path: impl AsRef, expect_size: Option, expect_sha1: Option) -> Result<(), FileVerifyError> { let path = path.as_ref(); if expect_size.is_none() && expect_sha1.is_none() { debug!("No size or sha1 for {}, have to assume it's good.", path.display()); return Ok(()); } let mut file = File::open(path).await.map_err(|e| FileVerifyError::Open(path.to_owned(), e))?; let mut tally = 0usize; let mut st = Sha1::new(); let mut buf = [0u8; 4096]; loop { let n = match file.read(&mut buf).await { Ok(n) => n, Err(e) => match e.kind() { ErrorKind::Interrupted => continue, _ => return Err(FileVerifyError::Read(path.to_owned(), e)) } }; if n == 0 { break; } st.update(&buf[..n]); tally += n; } let dig = st.digest(); if expect_size.is_some_and(|sz| sz != tally) { return Err(FileVerifyError::Integrity(path.to_owned(), IntegrityError::SizeMismatch { expect: expect_size.unwrap(), actual: tally })); } else if expect_sha1.is_some_and(|exp_dig| exp_dig != dig) { return Err(FileVerifyError::Integrity(path.to_owned(), IntegrityError::Sha1Mismatch { expect: expect_sha1.unwrap(), actual: dig })); } Ok(()) } pub fn check_path(name: &str) -> Result<&Path, &'static str> { let entry_path: &Path = Path::new(name); let mut depth = 0usize; for component in entry_path.components() { depth = match component { Component::Prefix(_) | Component::RootDir => return Err("root path component in entry"), Component::ParentDir => depth.checked_sub(1) .map_or_else(|| Err("entry path escapes"), |s| Ok(s))?, Component::Normal(_) => depth + 1, _ => depth } } Ok(entry_path) }