use std::fmt;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use sha2::{Digest, Sha256};
use crate::error::Error;
pub fn sha256_file(path: &Path) -> Result<String, Error> {
let bytes = std::fs::read(path)
.map_err(|e| Error::io(e, "reading file for sha256", Some(path.to_path_buf())))?;
Ok(sha256_bytes(&bytes))
}
pub fn sha256_bytes(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
let digest = hasher.finalize();
let mut out = String::with_capacity(digest.len() * 2);
for byte in digest {
out.push(nibble_to_hex((byte >> 4) & 0xF));
out.push(nibble_to_hex(byte & 0xF));
}
out
}
#[inline]
fn nibble_to_hex(n: u8) -> char {
match n {
0..=9 => (b'0' + n) as char,
10..=15 => (b'a' + n - 10) as char,
_ => unreachable!(),
}
}
pub fn write_file_atomic(path: &Path, contents: &[u8]) -> Result<(), Error> {
let parent = path.parent().ok_or_else(|| {
Error::io(
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"path has no parent directory",
),
"atomic write",
Some(path.to_path_buf()),
)
})?;
std::fs::create_dir_all(parent).map_err(|e| {
Error::io(
e,
"creating parent directory for atomic write",
Some(parent.to_path_buf()),
)
})?;
let mut tmp_path: PathBuf = path.to_path_buf();
let mut tmp_name = tmp_path
.file_name()
.map(|n| n.to_os_string())
.unwrap_or_default();
tmp_name.push(".lihaaf.tmp");
tmp_path.set_file_name(tmp_name);
{
let mut f = File::create(&tmp_path)
.map_err(|e| Error::io(e, "creating tmp file", Some(tmp_path.clone())))?;
f.write_all(contents)
.map_err(|e| Error::io(e, "writing tmp file", Some(tmp_path.clone())))?;
f.sync_all()
.map_err(|e| Error::io(e, "syncing tmp file", Some(tmp_path.clone())))?;
}
std::fs::rename(&tmp_path, path)
.map_err(|e| Error::io(e, "renaming tmp file into place", Some(path.to_path_buf())))?;
Ok(())
}
pub fn to_forward_slash(s: &str) -> String {
if !s.contains('\\') {
return s.to_string();
}
s.chars().map(|c| if c == '\\' { '/' } else { c }).collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelativePathError {
path: PathBuf,
base: PathBuf,
}
impl RelativePathError {
pub fn non_absolute_path(&self) -> String {
non_absolute_outside_base_path(&self.path)
}
}
impl fmt::Display for RelativePathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"path `{}` is not under base `{}`",
self.path.display(),
self.base.display()
)
}
}
pub fn non_absolute_outside_base_path(path: &Path) -> String {
let rendered = to_forward_slash(&path.to_string_lossy());
let trimmed = rendered.trim_start_matches('/');
if trimmed.is_empty() {
"outside-base".to_string()
} else {
format!("outside-base/{trimmed}")
}
}
pub fn relative_to(path: &Path, base: &Path) -> Result<String, RelativePathError> {
path.strip_prefix(base)
.map(|rel| to_forward_slash(&rel.to_string_lossy()))
.map_err(|_| RelativePathError {
path: path.to_path_buf(),
base: base.to_path_buf(),
})
}
pub(crate) fn remove_path_race_free(path: &Path, context_prefix: &str) -> Result<(), Error> {
use std::io::ErrorKind;
match std::fs::remove_file(path) {
Ok(()) => return Ok(()),
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()),
Err(e) if e.kind() == ErrorKind::IsADirectory => {}
#[cfg(windows)]
Err(e) if e.kind() == ErrorKind::PermissionDenied => {}
Err(e) => {
return Err(Error::io(
e,
format!("removing {context_prefix} file/symlink"),
Some(path.to_path_buf()),
));
}
}
match std::fs::remove_dir(path) {
Ok(()) => return Ok(()),
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(()),
Err(e) if e.kind() == ErrorKind::DirectoryNotEmpty => {}
#[cfg(windows)]
Err(e) if e.kind() == ErrorKind::PermissionDenied => {}
Err(e) => {
return Err(Error::io(
e,
format!("removing {context_prefix} empty dir / dir-symlink"),
Some(path.to_path_buf()),
));
}
}
match std::fs::remove_dir_all(path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
Err(e) => Err(Error::io(
e,
format!("recursively removing {context_prefix} directory"),
Some(path.to_path_buf()),
)),
}
}
pub fn total_ram_mb() -> Option<u64> {
#[cfg(target_os = "linux")]
{
let text = std::fs::read_to_string("/proc/meminfo").ok()?;
for line in text.lines() {
if let Some(rest) = line.strip_prefix("MemTotal:") {
let kib: u64 = rest
.trim()
.strip_suffix("kB")
.or_else(|| Some(rest.trim()))?
.trim()
.parse()
.ok()?;
return Some(kib / 1024);
}
}
None
}
#[cfg(target_os = "macos")]
{
let out = std::process::Command::new("sysctl")
.args(["-n", "hw.memsize"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let bytes: u64 = String::from_utf8_lossy(&out.stdout).trim().parse().ok()?;
Some(bytes / (1024 * 1024))
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn sha256_bytes_matches_known_vector() {
assert_eq!(
sha256_bytes(b""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
assert_eq!(
sha256_bytes(b"abc"),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
#[test]
fn write_file_atomic_creates_parents_and_writes() {
let tmp = tempdir().unwrap();
let target = tmp.path().join("nested/dirs/out.txt");
write_file_atomic(&target, b"hello world").unwrap();
let read = std::fs::read(&target).unwrap();
assert_eq!(read, b"hello world");
let mut tmp_name = target.file_name().unwrap().to_os_string();
tmp_name.push(".lihaaf.tmp");
let tmp_sidecar = target.with_file_name(tmp_name);
assert!(!tmp_sidecar.exists());
}
#[test]
fn forward_slash_passes_through_when_clean() {
assert_eq!(to_forward_slash("foo/bar/baz.rs"), "foo/bar/baz.rs");
}
#[test]
fn forward_slash_rewrites_backslashes() {
assert_eq!(
to_forward_slash(r"C:\Users\me\code\fixture.rs"),
"C:/Users/me/code/fixture.rs"
);
}
#[test]
fn relative_to_strips_common_prefix() {
let base = std::path::PathBuf::from("/a/b");
let p = std::path::PathBuf::from("/a/b/c/d.rs");
assert_eq!(relative_to(&p, &base).unwrap(), "c/d.rs");
}
#[test]
fn relative_to_errors_when_no_common_prefix() {
let base = std::path::PathBuf::from("/x");
let p = std::path::PathBuf::from("/y/z.rs");
let err = relative_to(&p, &base).unwrap_err();
assert_eq!(err.to_string(), "path `/y/z.rs` is not under base `/x`");
}
#[test]
fn relative_to_error_fallback_is_not_absolute() {
let base = std::path::PathBuf::from("/x");
let p = std::path::PathBuf::from("/y/z.rs");
let err = relative_to(&p, &base).unwrap_err();
assert_eq!(err.non_absolute_path(), "outside-base/y/z.rs");
}
}