use crate::error::Result;
use log::debug;
use std::fs::{self, OpenOptions, TryLockError};
use std::io::{self, ErrorKind};
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::winbase::{MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH, MoveFileExW};
#[cfg(target_os = "windows")]
use winapi::um::winnt::FILE_ATTRIBUTE_READONLY;
#[derive(Debug, PartialEq, Eq)]
pub enum LockStatus {
Available,
InUse,
}
pub fn try_lock_exclusive(path: &Path) -> io::Result<LockStatus> {
let file = OpenOptions::new().read(true).write(true).open(path)?;
match file.try_lock() {
Ok(()) => {
if let Err(err) = file.unlock() {
debug!(
"Failed to unlock {} after availability probe: {}",
path.display(),
err
);
}
Ok(LockStatus::Available)
}
Err(TryLockError::WouldBlock) => Ok(LockStatus::InUse),
Err(TryLockError::Error(err)) => Err(err),
}
}
#[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)
}
}
#[cfg(windows)]
pub fn atomic_rename(from: &Path, to: &Path) -> std::io::Result<()> {
match fs::rename(from, to) {
Ok(()) => return Ok(()),
Err(err) if err.kind() == ErrorKind::AlreadyExists => {}
Err(err) => return Err(err),
}
let to_wide: Vec<u16> = to
.as_os_str()
.encode_wide()
.chain(std::iter::once(0u16))
.collect();
let from_wide: Vec<u16> = from
.as_os_str()
.encode_wide()
.chain(std::iter::once(0u16))
.collect();
let success = unsafe {
MoveFileExW(
from_wide.as_ptr(),
to_wide.as_ptr(),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
) != 0
};
if success {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
#[cfg(not(windows))]
pub fn atomic_rename(from: &Path, to: &Path) -> std::io::Result<()> {
fs::rename(from, to)
}
pub fn check_files_in_use(path: &Path) -> Result<Vec<String>> {
debug!(
"Checking if critical JDK files are in use at {}",
path.display()
);
let mut files_in_use = Vec::new();
for file_name in critical_jdk_files() {
let file_path = path.join(file_name);
if !file_path.exists() {
continue;
}
match try_lock_exclusive(&file_path) {
Ok(LockStatus::Available) => {}
Ok(LockStatus::InUse) => {
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() == 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")]
fn critical_jdk_files() -> &'static [&'static str] {
&[
"bin/java.exe",
"bin/javac.exe",
"bin/javaw.exe",
"bin/jar.exe",
]
}
#[cfg(not(target_os = "windows"))]
fn critical_jdk_files() -> &'static [&'static str] {
&["bin/java", "bin/javac", "bin/javaw", "bin/jar"]
}
#[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 crate::paths::install;
use crate::platform::with_executable_extension;
use std::fs;
use std::sync::{Arc, Barrier, mpsc};
use std::thread;
use tempfile::TempDir;
#[test]
fn std_lock_adapter_allows_relocking() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("lock.txt");
fs::write(&file_path, "probe").unwrap();
let first = try_lock_exclusive(&file_path).unwrap();
assert_eq!(first, LockStatus::Available);
let second = try_lock_exclusive(&file_path).unwrap();
assert_eq!(second, LockStatus::Available);
}
#[test]
fn std_lock_adapter_detects_locked_file() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("locked.bin");
fs::write(&file_path, "data").unwrap();
let (release_tx, release_rx) = mpsc::channel();
let barrier = Arc::new(Barrier::new(2));
let barrier_clone = barrier.clone();
let locked_path = file_path.clone();
let handle = thread::spawn(move || {
let file = std::fs::File::options()
.read(true)
.write(true)
.open(&locked_path)
.unwrap();
file.lock().unwrap();
barrier_clone.wait();
release_rx.recv().unwrap();
file.unlock().unwrap();
});
barrier.wait();
let status = try_lock_exclusive(&file_path).unwrap();
assert_eq!(status, LockStatus::InUse);
release_tx.send(()).unwrap();
handle.join().unwrap();
}
#[test]
fn check_files_in_use_reports_locked_file() {
let temp_dir = TempDir::new().unwrap();
let bin_dir = install::bin_directory(temp_dir.path());
fs::create_dir_all(&bin_dir).unwrap();
let java_name = with_executable_extension("java");
let java_path = bin_dir.join(&java_name);
fs::write(&java_path, "binary").unwrap();
let (release_tx, release_rx) = mpsc::channel();
let barrier = Arc::new(Barrier::new(2));
let barrier_clone = barrier.clone();
let locked_path = java_path.clone();
let handle = thread::spawn(move || {
let file = std::fs::File::options()
.read(true)
.write(true)
.open(&locked_path)
.unwrap();
file.lock().unwrap();
barrier_clone.wait();
release_rx.recv().unwrap();
file.unlock().unwrap();
});
barrier.wait();
let files_in_use = check_files_in_use(temp_dir.path()).unwrap();
assert!(
files_in_use
.iter()
.any(|entry| entry.contains("Critical file may be in use"))
);
release_tx.send(()).unwrap();
handle.join().unwrap();
}
#[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"
);
}
}