use std::cmp::max;
use std::fs::OpenOptions;
use std::path::Path;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[cfg(test)]
mod tests;
const NANOS_PER_SECOND: u128 = 1_000_000_000;
pub fn saturating_duration_from_nanos(nanos: u128) -> (Duration, bool) {
let seconds = nanos / NANOS_PER_SECOND;
if seconds > u64::MAX as u128 {
return (Duration::MAX, true);
}
let nanos_remainder = (nanos % NANOS_PER_SECOND) as u32;
(Duration::new(seconds as u64, nanos_remainder), false)
}
pub fn saturating_system_time_from_nanos(nanos: u128) -> (SystemTime, bool) {
let (duration, saturated) = saturating_duration_from_nanos(nanos);
(UNIX_EPOCH + duration, saturated)
}
use crate::error::{HoldError, Result};
use crate::state::{FileState, StateMetadata};
pub fn validate_repo_relative_path(path: &Path) -> Result<()> {
if path.is_absolute() {
return Err(HoldError::InvalidPath {
message: format!("absolute path not allowed in metadata: {}", path.display()),
});
}
if path
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return Err(HoldError::InvalidPath {
message: format!("path escapes repository root: {}", path.display()),
});
}
Ok(())
}
pub fn repo_relative_path(repo_root: &Path, relative: &Path) -> Result<std::path::PathBuf> {
validate_repo_relative_path(relative)?;
Ok(repo_root.join(relative))
}
fn nanos_to_system_time(nanos: u128) -> SystemTime {
saturating_system_time_from_nanos(nanos).0
}
fn system_time_to_nanos(time: SystemTime) -> u128 {
time.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_nanos()
}
pub fn generate_monotonic_timestamp(metadata: &StateMetadata) -> SystemTime {
let max_metadata_nanos = metadata.max_mtime_nanos().unwrap_or(0);
let now_nanos = system_time_to_nanos(SystemTime::now());
let monotonic_nanos = max(now_nanos, max_metadata_nanos + 1);
nanos_to_system_time(monotonic_nanos)
}
pub fn set_file_mtime(path: &Path, mtime: SystemTime) -> Result<()> {
let metadata = std::fs::symlink_metadata(path).map_err(|source| HoldError::IoError {
path: path.to_path_buf(),
source,
})?;
if metadata.is_symlink() {
return Err(HoldError::InvalidFileType(
path.to_path_buf(),
"Cannot set timestamp on symbolic links".to_string(),
));
}
if metadata.is_dir() {
return Err(HoldError::InvalidFileType(
path.to_path_buf(),
"Cannot set timestamp on directories".to_string(),
));
}
let file = OpenOptions::new()
.write(true)
.open(path)
.map_err(|source| HoldError::SetTimestampError(path.to_path_buf(), source))?;
file.set_modified(mtime)
.map_err(|source| HoldError::SetTimestampError(path.to_path_buf(), source))?;
Ok(())
}
pub fn restore_timestamps(
repo_root: &Path,
unchanged_files: &[&FileState],
modified_files: &[&Path],
added_files: &[&Path],
new_mtime: SystemTime,
) -> Result<()> {
for file_state in unchanged_files {
validate_repo_relative_path(&file_state.path)?;
let mtime = nanos_to_system_time(file_state.mtime_nanos);
let full_path = repo_root.join(&file_state.path);
set_file_mtime(&full_path, mtime)?;
}
for path in modified_files {
let full_path = repo_relative_path(repo_root, path)?;
set_file_mtime(&full_path, new_mtime)?;
}
for path in added_files {
let full_path = repo_relative_path(repo_root, path)?;
set_file_mtime(&full_path, new_mtime)?;
}
Ok(())
}