use std::fs;
use std::path::{Path, PathBuf};
use color_eyre::eyre::{eyre, WrapErr};
use color_eyre::Result;
#[cfg(unix)]
pub fn symlink_dir_for_virtiofs<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> Result<()> {
_symlink(original, link)
}
#[cfg(unix)]
pub fn symlink_file_for_virtiofs<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> Result<()> {
_symlink(original, link)
}
#[cfg(unix)]
fn _symlink<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> Result<()> {
use std::os::unix::fs::symlink;
let original = original.as_ref();
let link = link.as_ref();
log::trace!("creating symlink: {} -> {}", link.display(), original.display());
symlink(original, link)
.wrap_err_with(|| format!("Failed to create symlink from {} to {}", link.display(), original.display()))
}
#[cfg(unix)]
pub fn symlink_for_virtiofs<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> Result<()> {
_symlink(original, link)
}
#[cfg(unix)]
pub fn symlink_for_native<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> Result<()> {
_symlink(original, link)
}
#[cfg(unix)]
pub fn symlink_file_for_native<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> Result<()> {
_symlink(original, link)
}
#[cfg(unix)]
pub fn symlink_dir_for_native<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> Result<()> {
_symlink(original, link)
}
#[cfg(windows)]
pub fn symlink_dir_for_virtiofs<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> Result<()> {
let original = original.as_ref();
let link = link.as_ref();
debug_assert_no_forward_slash(link);
let original_str = original.to_string_lossy();
let is_unix_path = original_str.starts_with('/');
let normalized_original = if is_unix_path {
original.to_path_buf()
} else {
normalize_symlink_target(original)
};
let posix_target = if is_unix_path {
original_str.to_string()
} else {
let decoded_original = decode_path_from_windows(&normalized_original);
decoded_original.to_string_lossy().to_string()
};
log::trace!(
"symlink_dir_for_virtiofs: {} -> {}",
link.display(),
normalized_original.display()
);
crate::krun_virtiofs_windows::symlink::symlink_dir_for_virtiofs(&normalized_original, link, &posix_target)
.wrap_err_with(|| {
format!(
"Failed symlink_dir_for_virtiofs from {} to {}",
normalized_original.display(),
link.display()
)
})
}
#[cfg(windows)]
pub fn symlink_file_for_virtiofs<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> Result<()> {
let original = original.as_ref();
let link = link.as_ref();
debug_assert_no_forward_slash(link);
let original_str = original.to_string_lossy();
let is_unix_path = original_str.starts_with('/');
let normalized_original = if is_unix_path {
original.to_path_buf()
} else {
normalize_symlink_target(original)
};
let posix_target = if is_unix_path {
original_str.to_string()
} else {
let decoded_original = decode_path_from_windows(&normalized_original);
decoded_original.to_string_lossy().to_string()
};
log::trace!("symlink_file_for_virtiofs: {} -> {}", link.display(), normalized_original.display());
crate::krun_virtiofs_windows::symlink::symlink_file_for_virtiofs(&normalized_original, link, &posix_target)
.wrap_err_with(|| {
format!(
"Failed symlink_file_for_virtiofs from {} to {}",
normalized_original.display(),
link.display()
)
})
}
#[cfg(windows)]
pub fn symlink_file_for_native<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> Result<()> {
let original = original.as_ref();
let link = link.as_ref();
debug_assert_no_forward_slash(link);
let normalized_original = normalize_symlink_target(original);
log::trace!("symlink_file_for_native: {} -> {}", link.display(), normalized_original.display());
crate::krun_virtiofs_windows::symlink::symlink_file_for_native(&normalized_original, link)
.wrap_err_with(|| {
format!(
"Failed symlink_file_for_native from {} to {}",
normalized_original.display(),
link.display()
)
})
}
#[cfg(windows)]
pub fn symlink_dir_for_native<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> Result<()> {
let original = original.as_ref();
let link = link.as_ref();
debug_assert_no_forward_slash(link);
let normalized_original = normalize_symlink_target(original);
log::trace!("symlink_dir_for_native: {} -> {}", link.display(), normalized_original.display());
crate::krun_virtiofs_windows::symlink::symlink_dir_for_native(&normalized_original, link)
.wrap_err_with(|| {
format!(
"Failed symlink_dir_for_native from {} to {}",
normalized_original.display(),
link.display()
)
})
}
#[cfg(windows)]
pub fn symlink_for_native<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> Result<()> {
let original = original.as_ref();
let link = link.as_ref();
debug_assert_no_forward_slash(link);
let normalized_original = normalize_symlink_target(original);
log::trace!("symlink_for_native: {} -> {}", link.display(), normalized_original.display());
crate::krun_virtiofs_windows::symlink::symlink_for_native(&normalized_original, link)
.wrap_err_with(|| {
format!(
"Failed symlink_for_native from {} to {}",
normalized_original.display(),
link.display()
)
})
}
pub fn hard_link<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> Result<()> {
let original = original.as_ref();
let link = link.as_ref();
debug_assert_no_forward_slash(original);
debug_assert_no_forward_slash(link);
log::trace!("creating hard link: {} -> {}", link.display(), original.display());
fs::hard_link(original, link)
.wrap_err_with(|| format!("Failed to create hard link from {} to {}", link.display(), original.display()))
}
pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(source: P, target: Q) -> Result<u64> {
let source = source.as_ref();
let target = target.as_ref();
debug_assert_no_forward_slash(source);
debug_assert_no_forward_slash(target);
log::trace!("copying file: {} -> {}", source.display(), target.display());
fs::copy(source, target)
.wrap_err_with(|| format!("Failed to copy {} to {}", source.display(), target.display()))
}
#[cfg(not(windows))]
pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
let from = from.as_ref();
let to = to.as_ref();
debug_assert_no_forward_slash(from);
debug_assert_no_forward_slash(to);
log::trace!("renaming: {} -> {}", from.display(), to.display());
fs::rename(from, to)
.wrap_err_with(|| format!("Failed to rename {} to {}", from.display(), to.display()))
}
#[cfg(windows)]
pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
use std::os::windows::ffi::OsStrExt;
use windows::Win32::Storage::FileSystem::{
MoveFileExW, MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH,
};
let from = from.as_ref();
let to = to.as_ref();
debug_assert_no_forward_slash(from);
debug_assert_no_forward_slash(to);
log::trace!("renaming: {} -> {}", from.display(), to.display());
let from_wide: Vec<u16> = from.as_os_str().encode_wide().chain(std::iter::once(0)).collect();
let to_wide: Vec<u16> = to.as_os_str().encode_wide().chain(std::iter::once(0)).collect();
unsafe {
let result = MoveFileExW(
windows::core::PCWSTR(from_wide.as_ptr()),
windows::core::PCWSTR(to_wide.as_ptr()),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
);
result.map_err(|e| color_eyre::eyre::eyre!(
"Failed to rename {} to {}: {}",
from.display(),
to.display(),
e
))?;
}
Ok(())
}
#[cfg(not(windows))]
pub fn rename_or_copy_delete<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
let from = from.as_ref();
let to = to.as_ref();
debug_assert_no_forward_slash(from);
debug_assert_no_forward_slash(to);
log::trace!("rename_or_copy_delete: {} -> {}", from.display(), to.display());
if fs::rename(from, to).is_ok() {
return Ok(());
}
log::debug!("rename failed (likely cross-device), falling back to copy+delete: {} -> {}", from.display(), to.display());
fs::copy(from, to)
.wrap_err_with(|| format!("Failed to copy {} to {}", from.display(), to.display()))?;
fs::remove_file(from)
.wrap_err_with(|| format!("Failed to remove source file {} after copy", from.display()))?;
Ok(())
}
#[cfg(windows)]
pub fn rename_or_copy_delete<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
use std::os::windows::ffi::OsStrExt;
use windows::Win32::Storage::FileSystem::{
MoveFileExW, MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH,
};
let from = from.as_ref();
let to = to.as_ref();
debug_assert_no_forward_slash(from);
debug_assert_no_forward_slash(to);
log::trace!("rename_or_copy_delete: {} -> {}", from.display(), to.display());
let from_wide: Vec<u16> = from.as_os_str().encode_wide().chain(std::iter::once(0)).collect();
let to_wide: Vec<u16> = to.as_os_str().encode_wide().chain(std::iter::once(0)).collect();
if unsafe {
MoveFileExW(
windows::core::PCWSTR(from_wide.as_ptr()),
windows::core::PCWSTR(to_wide.as_ptr()),
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
).is_ok()
} {
return Ok(());
}
log::debug!("MoveFileEx failed (likely cross-drive), falling back to copy+delete: {} -> {}", from.display(), to.display());
fs::copy(from, to)
.wrap_err_with(|| format!("Failed to copy {} to {}", from.display(), to.display()))?;
fs::remove_file(from)
.wrap_err_with(|| format!("Failed to remove source file {} after copy", from.display()))?;
Ok(())
}
#[cfg(not(windows))]
pub fn remove_file<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
log::trace!("removing file: {}", path.display());
fs::remove_file(path)
.wrap_err_with(|| format!("Failed to remove file {}", path.display()))
}
#[cfg(windows)]
pub fn remove_file<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
log::trace!("removing file: {}", path.display());
match fs::remove_file(path) {
Ok(()) => Ok(()),
Err(e) if e.raw_os_error() == Some(5) => {
log::trace!("remove_file failed with access denied, trying remove_dir for potential junction: {}", path.display());
fs::remove_dir(path)
.wrap_err_with(|| format!("Failed to remove file/junction {}", path.display()))
}
Err(e) => Err(e).wrap_err_with(|| format!("Failed to remove file {}", path.display())),
}
}
#[cfg(not(windows))]
pub fn remove_dir_all<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
log::trace!("removing directory recursively: {}", path.display());
fs::remove_dir_all(path)
.wrap_err_with(|| format!("Failed to remove directory {}", path.display()))
}
#[cfg(windows)]
pub fn remove_dir_all<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
log::trace!("removing directory recursively: {}", path.display());
remove_dir_all::remove_dir_all(path)
.wrap_err_with(|| format!("Failed to remove directory {}", path.display()))
}
#[cfg(windows)]
pub fn can_create_symlinks() -> bool {
crate::krun_virtiofs_windows::symlink::can_create_symlinks()
}
#[cfg(unix)]
pub fn can_create_symlinks() -> bool {
true
}
pub fn create_dir_all<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
debug_assert_no_forward_slash(path);
log::trace!("creating directory: {}", path.display());
fs::create_dir_all(path)
.wrap_err_with(|| format!("Failed to create directory {}", path.display()))
}
pub fn file_create<P: AsRef<Path>>(path: P) -> Result<fs::File> {
let path = path.as_ref();
log::trace!("creating file: {}", path.display());
fs::File::create(path)
.wrap_err_with(|| format!("Failed to create file {}", path.display()))
}
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, content: C) -> Result<()> {
let path = path.as_ref();
log::trace!("writing file: {}", path.display());
fs::write(path, content)
.wrap_err_with(|| format!("Failed to write file {}", path.display()))
}
pub fn set_permissions<P: AsRef<Path>>(path: P, permissions: fs::Permissions) -> Result<()> {
let path = path.as_ref();
log::trace!("setting permissions for: {} {:?}", path.display(), permissions);
fs::set_permissions(path, permissions)
.wrap_err_with(|| format!("Failed to set permissions for {}", path.display()))
}
pub fn remove_dir<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
log::trace!("removing directory: {}", path.display());
fs::remove_dir(path)
.wrap_err_with(|| format!("Failed to remove directory {}", path.display()))
}
#[allow(dead_code)]
pub fn create_dir<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
log::trace!("creating single directory: {}", path.display());
fs::create_dir(path)
.wrap_err_with(|| format!("Failed to create directory {}", path.display()))
}
#[cfg(windows)]
pub fn set_case_sensitive<P: AsRef<Path>>(path: P) -> Result<()> {
use windows::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
use windows::Win32::Storage::FileSystem::{
CreateFileW, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_SHARE_WRITE,
OPEN_EXISTING, SetFileInformationByHandle, FILE_INFO_BY_HANDLE_CLASS,
};
use windows::core::PCWSTR;
let path = path.as_ref();
log::trace!("setting case sensitivity for: {}", path.display());
let path_wide: Vec<u16> = path
.to_string_lossy()
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let handle = unsafe {
CreateFileW(
PCWSTR(path_wide.as_ptr()),
0x80000000u32 | 0x40000000u32,
FILE_SHARE_READ | FILE_SHARE_WRITE,
None,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
HANDLE::default(),
)
};
match handle {
Ok(h) if h != INVALID_HANDLE_VALUE => {
#[repr(C)]
struct FileCaseSensitiveInformation {
flags: u32,
}
let info = FileCaseSensitiveInformation { flags: 1 };
let result = unsafe {
SetFileInformationByHandle(
h,
FILE_INFO_BY_HANDLE_CLASS(33),
&info as *const _ as *const std::ffi::c_void,
std::mem::size_of::<FileCaseSensitiveInformation>() as u32,
)
};
let _ = unsafe { CloseHandle(h) };
if result.is_ok() {
log::debug!("Enabled case sensitivity for: {}", path.display());
} else {
log::info!(
"Could not enable case sensitivity for {} (requires admin or developer mode)",
path.display()
);
}
}
_ => {
let err = std::io::Error::last_os_error();
log::info!(
"Could not open directory for case sensitivity setting {}: {}",
path.display(),
err
);
}
}
Ok(())
}
#[cfg(not(windows))]
pub fn set_case_sensitive<P: AsRef<Path>>(_path: P) -> Result<()> {
Ok(())
}
#[cfg(windows)]
pub fn create_dir_all_with_case_sensitivity<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
debug_assert_no_forward_slash(path);
create_dir_all(path)?;
set_case_sensitive(path)?;
Ok(())
}
#[cfg(not(windows))]
pub fn create_dir_all_with_case_sensitivity<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
create_dir_all(path)?;
set_case_sensitive(path)?;
Ok(())
}
#[cfg(windows)]
#[allow(dead_code)]
pub fn create_dir_with_case_sensitivity<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
debug_assert_no_forward_slash(path);
create_dir(path)?;
set_case_sensitive(path)?;
Ok(())
}
#[cfg(not(windows))]
#[allow(dead_code)]
pub fn create_dir_with_case_sensitivity<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
create_dir(path)?;
set_case_sensitive(path)?;
Ok(())
}
#[cfg(target_os = "linux")]
pub fn check_reflink_support(env_root: &Path, same_fs: bool) -> bool {
use std::io::Write;
if same_fs {
let test_file = env_root.join(".epkg_reflink_test");
log::trace!("creating test file for reflink check: {}", test_file.display());
if let Ok(mut file) = fs::File::create(&test_file) {
if file.write_all(b"test").is_ok() {
file.sync_all().ok();
let test_target = env_root.join(".epkg_reflink_test_target");
let result = reflink(&test_file, &test_target).is_ok();
let _ = remove_file(&test_file);
let _ = remove_file(&test_target);
return result;
} else {
let _ = remove_file(&test_file);
}
}
}
false
}
#[cfg(not(target_os = "linux"))]
pub fn check_reflink_support(_env_root: &Path, _same_fs: bool) -> bool {
false
}
#[cfg(target_os = "linux")]
pub fn reflink(source: &Path, target: &Path) -> Result<()> {
use std::os::unix::io::AsRawFd;
log::trace!("creating reflink: {} -> {}", source.display(), target.display());
let src_file = fs::File::open(source)
.with_context(|| format!("Failed to open source file {}", source.display()))?;
let dst_file = fs::File::create(target)?;
const FICLONE: libc::Ioctl = 0x4004_9409;
unsafe {
let result = libc::ioctl(dst_file.as_raw_fd(), FICLONE, src_file.as_raw_fd());
if result != 0 {
return Err(eyre!("ioctl FICLONE failed: {}", std::io::Error::last_os_error()));
}
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub fn reflink(_source: &Path, _target: &Path) -> Result<()> {
Err(eyre!("Reflink not supported on this platform"))
}
pub fn reflink_or_copy<P: AsRef<Path>, Q: AsRef<Path>>(source: P, target: Q, can_reflink: bool) -> Result<u64> {
let source = source.as_ref();
let target = target.as_ref();
if !can_reflink {
log::trace!("reflink not supported, copying: {} -> {}", source.display(), target.display());
return copy(source, target);
}
log::trace!("trying reflink: {} -> {}", source.display(), target.display());
match reflink(source, target) {
Ok(()) => {
let metadata = fs::metadata(source)
.wrap_err_with(|| format!("Failed to get metadata for {}", source.display()))?;
log::trace!("created reflink: {} -> {}", source.display(), target.display());
Ok(metadata.len())
}
Err(e) => {
log::debug!("reflink not work for {} -> {}: {}, falling back to copy",
source.display(), target.display(), e);
copy(source, target)
}
}
}
pub fn symlink_metadata<P: AsRef<Path>>(path: P) -> Result<fs::Metadata> {
let path = path.as_ref();
log::trace!("getting metadata (no follow): {}", path.display());
fs::symlink_metadata(path)
.wrap_err_with(|| format!("Failed to get metadata for {}", path.display()))
}
pub fn metadata_on_host<P: AsRef<Path>>(path: P) -> Result<fs::Metadata> {
let path = path.as_ref();
log::trace!("getting metadata (follow symlinks, host path): {}", path.display());
fs::metadata(path)
.wrap_err_with(|| format!("Failed to get metadata for {}", path.display()))
}
#[allow(dead_code)]
pub fn metadata_in_env<P: AsRef<Path>>(path: P, env_root: &Path) -> Result<fs::Metadata> {
let path = path.as_ref();
log::trace!("getting metadata (resolve symlink in env): {}", path.display());
if let Some(link_target) = resolve_symlink_in_env(path, env_root) {
fs::metadata(&link_target)
.wrap_err_with(|| format!("Failed to get metadata for symlink target {}", link_target.display()))
} else {
fs::metadata(path)
.wrap_err_with(|| format!("Failed to get metadata for {}", path.display()))
}
}
pub fn is_symlink(path: &Path) -> bool {
match symlink_metadata(path) {
Ok(metadata) => metadata.file_type().is_symlink(),
Err(_) => false,
}
}
#[cfg(windows)]
pub fn is_directory_symlink(path: &Path) -> bool {
use std::os::windows::fs::MetadataExt;
match symlink_metadata(path) {
Ok(metadata) => {
if !metadata.file_type().is_symlink() {
return false;
}
const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10;
metadata.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0
}
Err(_) => false,
}
}
#[cfg(not(windows))]
pub fn is_directory_symlink(path: &Path) -> bool {
match symlink_metadata(path) {
Ok(metadata) => {
if !metadata.file_type().is_symlink() {
return false;
}
path.metadata().map(|m| m.is_dir()).unwrap_or(false)
}
Err(_) => false,
}
}
#[cfg(windows)]
pub fn is_symlink_or_junction(path: &Path) -> bool {
use std::os::windows::fs::MetadataExt;
match symlink_metadata(path) {
Ok(metadata) => {
if metadata.file_type().is_symlink() {
return true;
}
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
}
Err(_) => false,
}
}
#[cfg(not(windows))]
pub fn is_symlink_or_junction(path: &Path) -> bool {
is_symlink(path)
}
#[cfg(windows)]
fn normalize_symlink_target(target: &Path) -> PathBuf {
let target_str = target.to_string_lossy();
PathBuf::from(target_str.replace('/', "\\"))
}
#[cfg(all(windows, debug_assertions))]
fn debug_assert_no_forward_slash(path: &Path) {
let path_str = path.to_string_lossy();
if path_str.contains('\\') && path_str.contains('/') {
// Check if it's a UNC path prefix (\\?\) which is valid
if !path_str.starts_with("//?/") && !path_str.starts_with("\\\\?\\") {
debug_assert!(
false,
"Mixed path separators detected: {:?}\n\
This will cause Windows error 123 (InvalidFilename).\n\
Path should be normalized to use consistent separators.",
path
);
}
}
}
#[cfg(not(all(windows, debug_assertions)))]
fn debug_assert_no_forward_slash(_path: &Path) {
}
#[cfg(windows)]
pub fn resolve_ancestor_symlink(path: &Path) -> PathBuf {
path.to_path_buf()
}
#[cfg(not(windows))]
pub fn resolve_ancestor_symlink(path: &Path) -> PathBuf {
path.to_path_buf()
}
pub fn exists_no_follow<P: AsRef<Path>>(path: P) -> bool {
symlink_metadata(path.as_ref()).is_ok()
}
pub fn exists_in_env<P: AsRef<Path>>(path: P) -> bool {
let path = path.as_ref();
is_regular_file_on_host(path) || is_symlink(path)
}
pub fn exists_or_any_symlink<P: AsRef<Path>>(path: P) -> bool {
path.as_ref().exists() || is_symlink(path.as_ref())
}
pub fn exists_on_host<P: AsRef<Path>>(path: P) -> bool {
path.as_ref().exists()
}
pub fn is_regular_file_on_host<P: AsRef<Path>>(path: P) -> bool {
match symlink_metadata(path.as_ref()) {
Ok(metadata) => metadata.file_type().is_file(),
Err(_) => false,
}
}
#[cfg(unix)]
pub fn touch(path: &Path) -> Result<()> {
use crate::posix::posix_utime;
posix_utime(path, None, None)
.map_err(|e| color_eyre::eyre::eyre!("Failed to touch file {}: {:?}", path.display(), e))
}
#[cfg(windows)]
pub fn touch(path: &Path) -> Result<()> {
match std::fs::OpenOptions::new().write(true).read(true).open(path) {
Ok(_) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
std::fs::File::create(path)
.wrap_err_with(|| format!("Failed to create file {}", path.display()))?;
Ok(())
}
Err(e) => Err(color_eyre::eyre::eyre!("Failed to touch file {}: {}", path.display(), e)),
}
}
pub fn resolve_symlink_in_env(symlink_path: &std::path::Path, env_root: &std::path::Path) -> Option<std::path::PathBuf> {
resolve_symlink_in_env_recursive(symlink_path, env_root, 0)
}
fn resolve_target_in_env(target_in_env: &Path, env_root: &Path, depth: usize) -> Option<PathBuf> {
if exists_or_any_symlink(target_in_env) {
if is_symlink(target_in_env) {
return resolve_symlink_in_env_recursive(target_in_env, env_root, depth + 1);
}
return Some(target_in_env.to_path_buf());
}
None
}
fn resolve_symlink_in_env_recursive(symlink_path: &std::path::Path, env_root: &std::path::Path, depth: usize) -> Option<std::path::PathBuf> {
log::trace!("resolve_symlink_in_env_recursive: symlink_path={:?}, env_root={:?}, depth={}", symlink_path, env_root, depth);
if depth > 20 {
log::trace!("resolve_symlink_in_env_recursive: depth limit exceeded");
return None;
}
if is_regular_file_on_host(symlink_path) {
log::trace!("resolve_symlink_in_env_recursive: regular file, returning {:?}", symlink_path);
return Some(symlink_path.to_path_buf());
}
if let Ok(link_target) = std::fs::read_link(symlink_path) {
if link_target.is_absolute() {
log::trace!("resolve_symlink_in_env_recursive: absolute symlink target={:?}", link_target);
let is_system_path = link_target.starts_with("/usr") ||
link_target.starts_with("/bin") ||
link_target.starts_with("/sbin") ||
link_target.starts_with("/lib") ||
link_target.starts_with("/lib64") ||
link_target.starts_with("/lib32") ||
link_target.starts_with("/libx32");
log::trace!("resolve_symlink_in_env_recursive: is_system_path={}", is_system_path);
if is_system_path {
let target_rel = link_target.strip_prefix("/").unwrap_or(&link_target);
let target_in_env = normalize_path_separators(&env_root.join(target_rel));
log::trace!("resolve_symlink_in_env_recursive: mapped to target_in_env={:?}", target_in_env);
match resolve_target_in_env(&target_in_env, env_root, depth) {
Some(result) => return Some(result),
None => log::trace!("resolve_symlink_in_env_recursive: target_in_env does not exist"),
}
}
let target_rel = link_target.strip_prefix("/").unwrap_or(&link_target);
let target_in_env = normalize_path_separators(&env_root.join(target_rel));
log::debug!("resolve_symlink_in_env_recursive: checking other path {:?}, target_in_env={:?}", link_target, target_in_env);
match resolve_target_in_env(&target_in_env, env_root, depth) {
Some(result) => {
log::debug!("resolve_symlink_in_env_recursive: target exists in env_root, returning {:?}", target_in_env);
return Some(result);
}
None => {}
}
if exists_on_host(&link_target) {
log::debug!("resolve_symlink_in_env_recursive: absolute path exists on host, returning {:?}", link_target);
return Some(link_target);
} else {
log::debug!("resolve_symlink_in_env_recursive: other absolute path does not exist on host: {:?}", link_target);
}
} else {
let symlink_dir = symlink_path.parent()?;
let resolved_path = symlink_dir.join(&link_target);
if exists_on_host(&resolved_path) {
if is_symlink(&resolved_path) {
return resolve_symlink_in_env_recursive(&resolved_path, env_root, depth + 1);
}
log::trace!("resolve_symlink_in_env_recursive: relative symlink resolved to regular file, returning {:?}", resolved_path);
return Some(resolved_path);
}
}
}
log::trace!("resolve_symlink_in_env_recursive: no resolution found for {:?}", symlink_path);
None
}
#[allow(dead_code)]
mod win32_pua_paths {
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/git/libkrun/src/devices/src/virtio/fs/windows/win32_pua_paths.rs"
));
}
#[allow(unused_imports)]
pub use win32_pua_paths::{
decode_filename_from_windows,
decode_path_from_windows,
has_invalid_windows_chars,
host_path_from_manifest_rel_path,
sanitize_path_for_windows,
};
#[cfg(windows)]
pub fn normalize_path_separators(path: &Path) -> PathBuf {
let path_str = path.to_string_lossy();
PathBuf::from(path_str.replace('/', "\\"))
}
#[cfg(not(windows))]
pub fn normalize_path_separators(path: &Path) -> PathBuf {
let path_str = path.to_string_lossy();
PathBuf::from(path_str.replace('\\', "/"))
}
/// Convert Windows DOS-style path to Linux guest path format.
/// This is used in Linux VM guest to convert env_root paths from env.yaml
/// that were created on Windows host (e.g., "C:\Users\aa\.epkg\envs\alpine").
/// The virtiofs mounts use /mnt/c/Users/... format for Windows drive letter paths.
///
/// Examples:
/// C:\Users\aa\.epkg\envs\alpine -> /mnt/c/Users/aa/.epkg/envs/alpine
/// \\wsl.localhost\Distro\path -> /path (WSL path extraction)
/// /home/user/.epkg/envs/myenv -> unchanged (already Linux path)
#[cfg(not(windows))]
pub fn convert_dos_path_to_linux_guest(path: &Path) -> PathBuf {
let path_str = path.to_string_lossy();
// Handle WSL2 UNC paths: \\wsl.localhost\Distro\linux_path
if path_str.starts_with("\\\\wsl.localhost\\") || path_str.starts_with("\\\\wsl$\\") {
let parts: Vec<&str> = path_str.splitn(5, '\\').collect();
if parts.len() >= 5 {
let linux_path = parts[4].replace('\\', "/");
return PathBuf::from(format!("/{}", linux_path));
}
return PathBuf::from(path_str.replace('\\', "/"));
}
// Handle extended-length path prefix: \\?\C:\...
let path_stripped = if path_str.starts_with("\\\\?\\") {
&path_str[4..]
} else {
&path_str
};
// Handle DOS drive letter paths: C:\Users\...
// Check for pattern: single letter followed by colon
if path_stripped.len() >= 2 && path_stripped.chars().nth(1) == Some(':') {
let drive = path_stripped.chars().next().unwrap().to_ascii_lowercase();
let rest = &path_stripped[2..].replace('\\', "/");
// Strip leading '/' from rest if present to avoid double slash
let rest = rest.strip_prefix('/').unwrap_or(rest);
return PathBuf::from(format!("/mnt/{}/{}", drive, rest));
}
// Handle UNC paths without WSL prefix: \\server\share\...
if path_stripped.starts_with("\\\\") {
let rest = path_stripped[2..].replace('\\', "/");
return PathBuf::from(format!("/mnt/{}", rest));
}
// Not a Windows path - return unchanged
path.to_path_buf()
}
#[cfg(windows)]
pub fn convert_dos_path_to_linux_guest(path: &Path) -> PathBuf {
// On Windows, no conversion needed
path.to_path_buf()
}
/// Normalize path by resolving `.` and `..` components.
/// This is needed when comparing paths that may contain relative components.
/// Unlike `canonicalize()`, this does NOT require the path to exist.
///
/// Example: `/foo/bar/../baz` -> `/foo/baz`
pub fn normalize_path_components(path: &Path) -> PathBuf {
use std::path::Component;
let mut components = Vec::new();
for component in path.components() {
match component {
Component::CurDir => continue,
Component::ParentDir => {
match components.last() {
Some(Component::Normal(_)) => {
components.pop();
}
Some(Component::RootDir) => {
// /.. -> /
continue;
}
_ => components.push(component),
}
}
_ => components.push(component),
}
}
if components.is_empty() {
components.push(Component::CurDir);
}
components.iter().collect()
}