use std::ffi::OsStr;
use std::fs::File;
use std::io;
use std::io::Read;
use std::path::Path;
use crate::CryptoError;
pub(crate) const INCOMPLETE_SUFFIX: &str = ".incomplete";
pub(crate) fn read_file_capped(
path: &Path,
cap: usize,
over_cap_error: impl FnOnce() -> CryptoError,
) -> Result<Vec<u8>, CryptoError> {
let mut file = File::open(path).map_err(map_user_path_io_error)?;
let mut buf = Vec::with_capacity(cap.saturating_add(1).min(64 * 1024));
let read = file
.by_ref()
.take(cap as u64 + 1)
.read_to_end(&mut buf)
.map_err(CryptoError::Io)?;
if read > cap {
return Err(over_cap_error());
}
Ok(buf)
}
pub(crate) fn file_stem(filename: &Path) -> Result<&OsStr, CryptoError> {
filename
.file_stem()
.ok_or_else(|| CryptoError::InvalidInput("Cannot get file stem".to_string()))
}
pub(crate) fn encryption_base_name(path: impl AsRef<Path>) -> Result<String, CryptoError> {
let path = path.as_ref();
let is_real_dir = match std::fs::symlink_metadata(path) {
Ok(m) => m.file_type().is_dir(),
Err(e) if e.kind() == io::ErrorKind::NotFound => false,
Err(e) => return Err(CryptoError::Io(e)),
};
if is_real_dir {
Ok(path
.file_name()
.ok_or_else(|| CryptoError::InvalidInput("Cannot get directory name".to_string()))?
.to_string_lossy()
.into_owned())
} else {
Ok(file_stem(path)?.to_string_lossy().into_owned())
}
}
pub(crate) fn path_occupied(path: &Path) -> Result<bool, CryptoError> {
match std::fs::symlink_metadata(path) {
Ok(_) => Ok(true),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(CryptoError::Io(e)),
}
}
pub(crate) fn reject_occupied(path: &Path, label: &str) -> Result<(), CryptoError> {
if path_occupied(path)? {
return Err(CryptoError::InvalidInput(format!(
"{label} already exists: {}",
path.display()
)));
}
Ok(())
}
pub(crate) fn parent_or_cwd(path: &Path) -> &Path {
path.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."))
}
pub(crate) fn map_user_path_io_error(e: io::Error) -> CryptoError {
if e.kind() == io::ErrorKind::NotFound {
CryptoError::InputPath
} else {
CryptoError::Io(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encryption_base_name_file() {
let stem = encryption_base_name("path/to/file.txt").unwrap();
assert_eq!(stem, "file");
}
#[test]
fn test_encryption_base_name_no_extension() {
let stem = encryption_base_name("path/to/file").unwrap();
assert_eq!(stem, "file");
}
#[test]
fn test_encryption_base_name_dotted_directory() {
let tmp = tempfile::TempDir::new().unwrap();
let dotted_dir = tmp.path().join("photos.v1");
std::fs::create_dir(&dotted_dir).unwrap();
let name = encryption_base_name(&dotted_dir).unwrap();
assert_eq!(name, "photos.v1");
}
#[test]
fn parent_or_cwd_returns_parent_when_present() {
assert_eq!(parent_or_cwd(Path::new("dir/file.txt")), Path::new("dir"));
assert_eq!(parent_or_cwd(Path::new("/abs/file.txt")), Path::new("/abs"));
}
#[test]
fn parent_or_cwd_falls_back_to_cwd() {
assert_eq!(parent_or_cwd(Path::new("file.txt")), Path::new("."));
assert_eq!(parent_or_cwd(Path::new("")), Path::new("."));
}
#[cfg(unix)]
#[test]
fn parent_or_cwd_root_path_falls_back_to_cwd() {
assert_eq!(parent_or_cwd(Path::new("/")), Path::new("."));
}
#[test]
fn path_occupied_returns_false_for_missing_path() {
let tmp = tempfile::TempDir::new().unwrap();
let absent = tmp.path().join("does-not-exist");
assert!(!path_occupied(&absent).unwrap());
}
#[test]
fn path_occupied_returns_true_for_real_file() {
let tmp = tempfile::TempDir::new().unwrap();
let file = tmp.path().join("real");
std::fs::write(&file, b"x").unwrap();
assert!(path_occupied(&file).unwrap());
}
#[cfg(unix)]
#[test]
fn path_occupied_returns_true_for_dangling_symlink() {
use std::os::unix::fs::symlink;
let tmp = tempfile::TempDir::new().unwrap();
let link = tmp.path().join("dangling");
symlink(tmp.path().join("absent-target"), &link).unwrap();
assert!(!link.exists(), "sanity: target really is missing");
assert!(path_occupied(&link).unwrap());
}
}