use crate::checksum::{checksum_file, Checksum};
use crate::error::{Result, SpliceError};
use std::path::Path;
const DISK_SPACE_MULTIPLIER: usize = 3;
const DISK_OVERHEAD_PER_FILE: u64 = 4096;
#[derive(Debug, Clone, PartialEq)]
pub enum PreVerificationResult {
Pass,
Fail {
check: String,
reason: String,
blocking: bool,
},
}
impl PreVerificationResult {
pub fn pass() -> Self {
Self::Pass
}
pub fn blocking(check: impl Into<String>, reason: impl Into<String>) -> Self {
Self::Fail {
check: check.into(),
reason: reason.into(),
blocking: true,
}
}
pub fn warning(check: impl Into<String>, reason: impl Into<String>) -> Self {
Self::Fail {
check: check.into(),
reason: reason.into(),
blocking: false,
}
}
pub fn is_pass(&self) -> bool {
matches!(self, Self::Pass)
}
pub fn is_blocking(&self) -> bool {
matches!(self, Self::Fail { blocking: true, .. })
}
pub fn is_warning(&self) -> bool {
matches!(
self,
Self::Fail {
blocking: false,
..
}
)
}
}
pub fn verify_file_ready(
file_path: &Path,
expected_checksum: Option<&Checksum>,
workspace_root: &Path,
) -> PreVerificationResult {
if !file_path.exists() {
return PreVerificationResult::blocking(
"file_exists",
format!("File does not exist: {}", file_path.display()),
);
}
if let Err(e) = std::fs::metadata(file_path) {
return PreVerificationResult::blocking(
"file_readable",
format!("Cannot read file metadata: {}", e),
);
}
if let Ok(abs_file) = file_path.canonicalize() {
if let Ok(abs_workspace) = workspace_root.canonicalize() {
if !abs_file.starts_with(&abs_workspace) {
return PreVerificationResult::blocking(
"file_in_workspace",
format!(
"File '{}' is outside workspace root '{}'",
file_path.display(),
workspace_root.display()
),
);
}
}
}
if let Err(e) = std::fs::OpenOptions::new()
.write(true)
.create(false)
.open(file_path)
{
return PreVerificationResult::blocking(
"file_writable",
format!("File is not writable: {}", e),
);
}
if let Some(expected) = expected_checksum {
match checksum_file(file_path) {
Ok(actual) => {
if actual != *expected {
return PreVerificationResult::blocking(
"file_checksum",
format!(
"File has been modified externally. Expected checksum {}, got {}",
expected.as_hex(),
actual.as_hex()
),
);
}
}
Err(e) => {
return PreVerificationResult::blocking(
"file_checksum",
format!("Failed to compute checksum: {}", e),
);
}
}
}
PreVerificationResult::pass()
}
pub fn verify_workspace_resources(
workspace_root: &Path,
estimated_size: usize,
) -> PreVerificationResult {
if !workspace_root.exists() {
return PreVerificationResult::blocking(
"workspace_exists",
format!(
"Workspace directory does not exist: {}",
workspace_root.display()
),
);
}
let test_file = workspace_root.join(".splice_write_test");
let _ = std::fs::remove_file(&test_file);
if let Err(e) = std::fs::write(&test_file, b"test") {
return PreVerificationResult::blocking(
"workspace_writable",
format!("Workspace is not writable: {}", e),
);
}
let _ = std::fs::remove_file(&test_file);
match get_disk_space(workspace_root) {
Ok((available, _total)) => {
let needed = (estimated_size * DISK_SPACE_MULTIPLIER) as u64 + DISK_OVERHEAD_PER_FILE;
if available < needed {
return PreVerificationResult::blocking(
"disk_space",
format!(
"Insufficient disk space: need {} bytes ({}x file size + {} overhead), available {} bytes",
needed, DISK_SPACE_MULTIPLIER, DISK_OVERHEAD_PER_FILE, available
),
);
}
}
Err(e) => {
log::warn!("Could not check disk space: {}", e);
return PreVerificationResult::warning(
"disk_space",
format!("Could not verify disk space: {}", e),
);
}
}
let backup_dir = workspace_root.join(".splice/backups");
if !backup_dir.exists() {
if let Err(e) = std::fs::create_dir_all(&backup_dir) {
return PreVerificationResult::blocking(
"backup_directory",
format!("Cannot create backup directory: {}", e),
);
}
}
PreVerificationResult::pass()
}
pub(crate) fn get_disk_space(path: &Path) -> Result<(u64, u64)> {
#[cfg(unix)]
{
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let c_path = CString::new(path.as_os_str().as_bytes()).map_err(|_| SpliceError::Io {
path: path.to_path_buf(),
source: std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"path contains null byte",
),
})?;
unsafe {
let mut stat: libc::statvfs = std::mem::zeroed();
if libc::statvfs(c_path.as_ptr(), &mut stat) != 0 {
return Err(SpliceError::Io {
path: path.to_path_buf(),
source: std::io::Error::last_os_error(),
});
}
let frsize = stat.f_frsize as u64; let total = stat.f_blocks as u64 * frsize;
let available = stat.f_bavail as u64 * frsize;
Ok((available, total))
}
}
#[cfg(windows)]
{
use std::os::windows::ffi::OsStrExt;
use std::ptr;
let path_wide: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
unsafe {
let mut free_bytes: u64 = 0;
let mut total_bytes: u64 = 0;
let mut available_bytes: u64 = 0;
if winapi::um::GetDiskFreeSpaceExW(
path_wide.as_ptr(),
&mut free_bytes,
&mut total_bytes,
ptr::null_mut(),
) != 0
{
Ok((available_bytes, total_bytes))
} else {
Err(std::io::Error::last_os_error().into())
}
}
}
}
pub fn verify_graph_sync(file_path: &Path, db_path: &Path) -> PreVerificationResult {
if !db_path.exists() {
return PreVerificationResult::warning(
"graph_exists",
format!("Graph database does not exist: {}", db_path.display()),
);
}
if let Err(e) = std::fs::metadata(db_path) {
return PreVerificationResult::blocking(
"graph_readable",
format!("Cannot read graph database: {}", e),
);
}
match (std::fs::metadata(file_path), std::fs::metadata(db_path)) {
(Ok(file_meta), Ok(db_meta)) => {
let file_mtime = match file_meta.modified() {
Ok(time) => time,
Err(_) => {
return PreVerificationResult::warning(
"file_mtime",
"Cannot read file modification time",
)
}
};
let db_mtime = match db_meta.modified() {
Ok(time) => time,
Err(_) => {
return PreVerificationResult::warning(
"db_mtime",
"Cannot read database modification time",
)
}
};
if file_mtime > db_mtime {
return PreVerificationResult::blocking(
"graph_sync",
format!(
"File '{}' has been modified since database was last updated (file: {:?}, db: {:?})",
file_path.display(),
file_mtime,
db_mtime
),
);
}
}
(Err(e), Ok(_)) => {
return PreVerificationResult::warning(
"file_metadata",
format!("Cannot read file metadata: {}", e),
)
}
(Ok(_), Err(e)) => {
return PreVerificationResult::warning(
"db_metadata",
format!("Cannot read database metadata: {}", e),
)
}
(Err(e), Err(_)) => {
return PreVerificationResult::warning(
"metadata",
format!("Cannot read metadata: {}", e),
)
}
}
PreVerificationResult::pass()
}
pub fn pre_verify_patch(
file_path: &Path,
expected_checksum: Option<&Checksum>,
workspace_root: &Path,
db_path: &Path,
strict: bool,
skip: bool,
) -> Result<Vec<PreVerificationResult>> {
let mut results = Vec::new();
if skip {
log::warn!("Skipping pre-verification checks (dangerous!)");
results.push(PreVerificationResult::pass());
return Ok(results);
}
let file_size = if file_path.exists() {
std::fs::metadata(file_path)
.map(|m| m.len() as usize)
.unwrap_or(0)
} else {
0
};
let mut file_result = verify_file_ready(file_path, expected_checksum, workspace_root);
let mut workspace_result = verify_workspace_resources(workspace_root, file_size);
let mut graph_result = verify_graph_sync(file_path, db_path);
if strict {
if file_result.is_warning() {
file_result = PreVerificationResult::blocking(
"strict_mode",
format!("Warning treated as error: {:?}", file_result),
);
}
if workspace_result.is_warning() {
workspace_result = PreVerificationResult::blocking(
"strict_mode",
format!("Warning treated as error: {:?}", workspace_result),
);
}
if graph_result.is_warning() {
graph_result = PreVerificationResult::blocking(
"strict_mode",
format!("Warning treated as error: {:?}", graph_result),
);
}
}
results.push(file_result);
results.push(workspace_result);
results.push(graph_result);
Ok(results)
}