use crate::error::Result;
use log::debug;
use std::fs;
use std::path::Path;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[cfg(target_os = "windows")]
use std::ffi::OsStr;
#[cfg(target_os = "windows")]
use std::os::windows::ffi::OsStrExt;
#[cfg(target_os = "windows")]
use winapi::um::fileapi::{GetFileAttributesW, INVALID_FILE_ATTRIBUTES, SetFileAttributesW};
#[cfg(target_os = "windows")]
use winapi::um::winnt::FILE_ATTRIBUTE_READONLY;
#[cfg(unix)]
pub fn make_executable(path: &Path) -> std::io::Result<()> {
let metadata = fs::metadata(path)?;
let mut permissions = metadata.permissions();
let mode = permissions.mode() | 0o755;
permissions.set_mode(mode);
fs::set_permissions(path, permissions)?;
Ok(())
}
#[cfg(windows)]
pub fn make_executable(_path: &Path) -> std::io::Result<()> {
Ok(())
}
#[cfg(unix)]
pub fn is_executable(path: &Path) -> std::io::Result<bool> {
let metadata = fs::metadata(path)?;
let permissions = metadata.permissions();
Ok(permissions.mode() & 0o111 != 0)
}
#[cfg(windows)]
pub fn is_executable(path: &Path) -> std::io::Result<bool> {
Ok(path.extension().map(|ext| ext == "exe").unwrap_or(false))
}
#[cfg(unix)]
pub fn set_permissions_from_mode(path: &Path, mode: u32) -> std::io::Result<()> {
use std::fs::Permissions;
fs::set_permissions(path, Permissions::from_mode(mode))
}
#[cfg(windows)]
pub fn set_permissions_from_mode(_path: &Path, _mode: u32) -> std::io::Result<()> {
Ok(())
}
pub fn make_writable(path: &Path) -> std::io::Result<()> {
#[cfg(unix)]
{
let metadata = fs::metadata(path)?;
let mut permissions = metadata.permissions();
let mode = permissions.mode() | 0o200; permissions.set_mode(mode);
fs::set_permissions(path, permissions)
}
#[cfg(windows)]
{
let metadata = fs::metadata(path)?;
let mut permissions = metadata.permissions();
#[allow(clippy::permissions_set_readonly_false)]
permissions.set_readonly(false);
fs::set_permissions(path, permissions)
}
}
pub fn atomic_rename(from: &Path, to: &Path) -> std::io::Result<()> {
#[cfg(windows)]
{
if to.exists() {
fs::remove_file(to)?;
}
}
fs::rename(from, to)
}
#[cfg(target_os = "windows")]
pub fn check_files_in_use(path: &Path) -> Result<Vec<String>> {
use fs2::FileExt;
debug!(
"Checking if critical JDK files are in use at {}",
path.display()
);
let mut files_in_use = Vec::new();
let critical_files = [
"bin/java.exe",
"bin/javac.exe",
"bin/javaw.exe",
"bin/jar.exe",
];
for file_name in &critical_files {
let file_path = path.join(file_name);
if file_path.exists() {
match fs::File::open(&file_path) {
Ok(file) => {
match file.try_lock_exclusive() {
Ok(_) => {
let _ = FileExt::unlock(&file);
}
Err(_) => {
files_in_use.push(format!(
"Critical file may be in use: {}",
file_path.display()
));
}
}
}
Err(e) => {
debug!("Cannot open {}: {}", file_path.display(), e);
if e.kind() == std::io::ErrorKind::PermissionDenied {
files_in_use.push(format!(
"Critical file may be in use (access denied): {}",
file_path.display()
));
}
}
}
}
}
Ok(files_in_use)
}
#[cfg(not(target_os = "windows"))]
pub fn check_files_in_use(path: &Path) -> Result<Vec<String>> {
use fs2::FileExt;
debug!(
"Checking if critical JDK files are in use at {}",
path.display()
);
let mut files_in_use = Vec::new();
let critical_files = ["bin/java", "bin/javac", "bin/javaw", "bin/jar"];
for file_name in &critical_files {
let file_path = path.join(file_name);
if file_path.exists() {
match fs::File::open(&file_path) {
Ok(file) => {
match file.try_lock_exclusive() {
Ok(_) => {
let _ = FileExt::unlock(&file);
}
Err(_) => {
files_in_use.push(format!(
"Critical file may be in use: {}",
file_path.display()
));
}
}
}
Err(e) => {
debug!("Cannot open {}: {}", file_path.display(), e);
if e.kind() == std::io::ErrorKind::PermissionDenied {
files_in_use.push(format!(
"Critical file may be in use (access denied): {}",
file_path.display()
));
}
}
}
}
}
Ok(files_in_use)
}
#[cfg(target_os = "windows")]
pub fn prepare_for_removal(path: &Path) -> Result<()> {
debug!("Preparing {} for removal", path.display());
use walkdir::WalkDir;
for entry in WalkDir::new(path) {
match entry {
Ok(entry) => {
if let Err(e) = remove_readonly_attribute(entry.path()) {
debug!(
"Failed to remove read-only attribute from {}: {}",
entry.path().display(),
e
);
}
}
Err(e) => {
debug!("Failed to access directory entry: {e}");
}
}
}
Ok(())
}
#[cfg(not(target_os = "windows"))]
pub fn prepare_for_removal(path: &Path) -> Result<()> {
debug!("Preparing {} for removal", path.display());
use walkdir::WalkDir;
for entry in WalkDir::new(path).contents_first(true) {
match entry {
Ok(entry) => {
let path = entry.path();
if let Err(e) = make_writable(path) {
debug!("Failed to make {} writable: {}", path.display(), e);
}
}
Err(e) => {
debug!("Failed to access directory entry: {e}");
}
}
}
Ok(())
}
#[cfg(target_os = "windows")]
pub fn post_removal_cleanup(path: &Path) -> Result<()> {
debug!("Performing post-removal cleanup for {}", path.display());
Ok(())
}
#[cfg(not(target_os = "windows"))]
pub fn post_removal_cleanup(path: &Path) -> Result<()> {
debug!("Performing post-removal cleanup for {}", path.display());
if let Some(parent) = path.parent() {
super::symlink::cleanup_orphaned_symlinks(parent)?;
}
Ok(())
}
#[cfg(target_os = "windows")]
fn remove_readonly_attribute(path: &Path) -> std::io::Result<()> {
let path_wide: Vec<u16> = OsStr::new(path)
.encode_wide()
.chain(std::iter::once(0))
.collect();
unsafe {
let current_attrs = GetFileAttributesW(path_wide.as_ptr());
if current_attrs == INVALID_FILE_ATTRIBUTES {
return Err(std::io::Error::last_os_error());
}
let new_attrs = current_attrs & !FILE_ATTRIBUTE_READONLY;
if new_attrs != current_attrs && SetFileAttributesW(path_wide.as_ptr(), new_attrs) == 0 {
return Err(std::io::Error::last_os_error());
}
}
Ok(())
}
#[cfg(unix)]
pub fn check_file_permissions(path: &Path) -> Result<bool> {
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::metadata(path)?;
let permissions = metadata.permissions();
let mode = permissions.mode();
if mode & 0o002 != 0 {
return Ok(false);
}
Ok(true)
}
#[cfg(windows)]
pub fn check_file_permissions(path: &Path) -> Result<bool> {
use crate::error::KopiError;
let metadata = fs::metadata(path)?;
if !metadata.is_file() {
return Err(KopiError::SecurityError(format!(
"Path {path:?} is not a regular file"
)));
}
if metadata.permissions().readonly() {
debug!("File {path:?} is read-only (secure)");
Ok(true)
} else {
log::warn!("File {path:?} is writable - consider setting read-only for security");
Ok(true) }
}
#[cfg(unix)]
pub fn set_secure_permissions(path: &Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::metadata(path)?;
let mut permissions = metadata.permissions();
permissions.set_mode(0o644);
std::fs::set_permissions(path, permissions)?;
Ok(())
}
#[cfg(windows)]
pub fn set_secure_permissions(path: &Path) -> Result<()> {
let metadata = std::fs::metadata(path)?;
let mut permissions = metadata.permissions();
permissions.set_readonly(true);
std::fs::set_permissions(path, permissions)?;
Ok(())
}
#[cfg(unix)]
pub fn check_executable_permissions(path: &Path) -> Result<()> {
use crate::error::KopiError;
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::metadata(path)?;
if !metadata.is_file() {
return Err(KopiError::SecurityError(format!(
"Path '{}' is not a regular file",
path.display()
)));
}
if !is_executable(path)? {
return Err(KopiError::SecurityError(format!(
"File '{}' is not executable",
path.display()
)));
}
let permissions = metadata.permissions();
let mode = permissions.mode();
if mode & 0o002 != 0 {
return Err(KopiError::SecurityError(format!(
"File '{}' is world-writable, which is a security risk",
path.display()
)));
}
Ok(())
}
#[cfg(windows)]
pub fn check_executable_permissions(path: &Path) -> Result<()> {
use crate::error::KopiError;
let metadata = std::fs::metadata(path)?;
if !metadata.is_file() {
return Err(KopiError::SecurityError(format!(
"Path '{}' is not a regular file",
path.display()
)));
}
if !is_executable(path)? {
return Err(KopiError::SecurityError(format!(
"File '{}' does not have .exe extension",
path.display()
)));
}
Ok(())
}
#[cfg(unix)]
pub fn check_file_readable(path: &Path) -> std::io::Result<bool> {
use std::os::unix::fs::PermissionsExt;
let metadata = fs::metadata(path)?;
let permissions = metadata.permissions();
let mode = permissions.mode();
Ok(mode & 0o400 != 0)
}
#[cfg(windows)]
pub fn check_file_readable(path: &Path) -> std::io::Result<bool> {
match fs::File::open(path) {
Ok(_) => Ok(true),
Err(e) => {
if e.kind() == std::io::ErrorKind::PermissionDenied {
Ok(false)
} else {
Err(e)
}
}
}
}
#[cfg(unix)]
pub fn get_file_permissions_string(path: &Path) -> std::io::Result<String> {
use std::os::unix::fs::PermissionsExt;
let metadata = fs::metadata(path)?;
let permissions = metadata.permissions();
let mode = permissions.mode();
Ok(format!("{:o}", mode & 0o777))
}
#[cfg(windows)]
pub fn get_file_permissions_string(path: &Path) -> std::io::Result<String> {
let metadata = fs::metadata(path)?;
let permissions = metadata.permissions();
if permissions.readonly() {
Ok("read-only".to_string())
} else {
Ok("read-write".to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_check_files_in_use_empty_dir() {
let temp_dir = TempDir::new().unwrap();
let files_in_use = check_files_in_use(temp_dir.path()).unwrap();
assert!(files_in_use.is_empty());
}
#[test]
fn test_check_files_in_use_with_file() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.txt");
fs::write(&test_file, "test content").unwrap();
let files_in_use = check_files_in_use(temp_dir.path()).unwrap();
assert!(files_in_use.is_empty());
}
#[test]
fn test_prepare_for_removal() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.txt");
fs::write(&test_file, "test content").unwrap();
let result = prepare_for_removal(temp_dir.path());
assert!(result.is_ok());
}
#[test]
fn test_post_removal_cleanup() {
let temp_dir = TempDir::new().unwrap();
let result = post_removal_cleanup(temp_dir.path());
assert!(result.is_ok());
}
#[test]
#[cfg(unix)]
fn test_check_file_permissions_unix() {
use std::os::unix::fs::PermissionsExt;
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
let mut perms = temp_file.as_file().metadata().unwrap().permissions();
perms.set_mode(0o644);
temp_file.as_file().set_permissions(perms.clone()).unwrap();
assert!(check_file_permissions(temp_file.path()).unwrap());
perms.set_mode(0o666);
temp_file.as_file().set_permissions(perms).unwrap();
assert!(!check_file_permissions(temp_file.path()).unwrap());
}
#[test]
#[cfg(windows)]
fn test_check_file_permissions_windows() {
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
assert!(check_file_permissions(temp_file.path()).unwrap());
let mut perms = temp_file.as_file().metadata().unwrap().permissions();
perms.set_readonly(true);
temp_file.as_file().set_permissions(perms.clone()).unwrap();
assert!(check_file_permissions(temp_file.path()).unwrap());
let temp_dir = tempfile::tempdir().unwrap();
assert!(check_file_permissions(temp_dir.path()).is_err());
}
#[test]
#[cfg(unix)]
fn test_set_secure_permissions_unix() {
use std::os::unix::fs::PermissionsExt;
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
let mut perms = temp_file.as_file().metadata().unwrap().permissions();
perms.set_mode(0o777);
temp_file.as_file().set_permissions(perms).unwrap();
set_secure_permissions(temp_file.path()).unwrap();
let new_perms = temp_file.as_file().metadata().unwrap().permissions();
assert_eq!(new_perms.mode() & 0o777, 0o644);
}
#[test]
#[cfg(windows)]
fn test_set_secure_permissions_windows() {
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
let initial_perms = temp_file.as_file().metadata().unwrap().permissions();
assert!(!initial_perms.readonly());
set_secure_permissions(temp_file.path()).unwrap();
let new_perms = temp_file.as_file().metadata().unwrap().permissions();
assert!(new_perms.readonly());
}
#[test]
#[cfg(unix)]
fn test_check_executable_permissions_unix() {
use std::os::unix::fs::PermissionsExt;
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
let mut perms = temp_file.as_file().metadata().unwrap().permissions();
perms.set_mode(0o777);
temp_file.as_file().set_permissions(perms).unwrap();
assert!(check_executable_permissions(temp_file.path()).is_err());
let mut perms = temp_file.as_file().metadata().unwrap().permissions();
perms.set_mode(0o755);
temp_file.as_file().set_permissions(perms).unwrap();
assert!(check_executable_permissions(temp_file.path()).is_ok());
let mut perms = temp_file.as_file().metadata().unwrap().permissions();
perms.set_mode(0o644);
temp_file.as_file().set_permissions(perms).unwrap();
assert!(check_executable_permissions(temp_file.path()).is_err());
}
#[test]
#[cfg(windows)]
fn test_check_executable_permissions_windows() {
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
assert!(check_executable_permissions(temp_file.path()).is_err());
let temp_dir = TempDir::new().unwrap();
let exe_path = temp_dir.path().join("test.exe");
fs::write(&exe_path, b"fake exe").unwrap();
assert!(check_executable_permissions(&exe_path).is_ok());
assert!(check_executable_permissions(temp_dir.path()).is_err());
}
#[test]
#[cfg(unix)]
fn test_check_file_readable_unix() {
use std::os::unix::fs::PermissionsExt;
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
let mut perms = temp_file.as_file().metadata().unwrap().permissions();
perms.set_mode(0o644);
temp_file.as_file().set_permissions(perms).unwrap();
assert!(check_file_readable(temp_file.path()).unwrap());
let mut perms = temp_file.as_file().metadata().unwrap().permissions();
perms.set_mode(0o000);
temp_file.as_file().set_permissions(perms).unwrap();
assert!(!check_file_readable(temp_file.path()).unwrap());
}
#[test]
#[cfg(windows)]
fn test_check_file_readable_windows() {
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
assert!(check_file_readable(temp_file.path()).unwrap());
let non_existent = temp_file.path().with_extension("nonexistent");
assert!(check_file_readable(&non_existent).is_err());
}
#[test]
#[cfg(unix)]
fn test_get_file_permissions_string_unix() {
use std::os::unix::fs::PermissionsExt;
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
let mut perms = temp_file.as_file().metadata().unwrap().permissions();
perms.set_mode(0o644);
temp_file.as_file().set_permissions(perms).unwrap();
assert_eq!(
get_file_permissions_string(temp_file.path()).unwrap(),
"644"
);
let mut perms = temp_file.as_file().metadata().unwrap().permissions();
perms.set_mode(0o755);
temp_file.as_file().set_permissions(perms).unwrap();
assert_eq!(
get_file_permissions_string(temp_file.path()).unwrap(),
"755"
);
}
#[test]
#[cfg(windows)]
fn test_get_file_permissions_string_windows() {
use tempfile::NamedTempFile;
let temp_file = NamedTempFile::new().unwrap();
assert_eq!(
get_file_permissions_string(temp_file.path()).unwrap(),
"read-write"
);
let mut perms = temp_file.as_file().metadata().unwrap().permissions();
perms.set_readonly(true);
temp_file.as_file().set_permissions(perms).unwrap();
assert_eq!(
get_file_permissions_string(temp_file.path()).unwrap(),
"read-only"
);
}
}