use std::io::Read;
use std::path::Path;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WorktreeFileIdentity {
pub device: u64,
pub inode: u64,
pub modified: i128,
pub changed: i128,
pub size: u64,
pub mode: u32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WorktreeFileState {
Original(WorktreeFileIdentity),
Intended,
Conflict,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct InstallResult {
pub rewritten: Vec<Vec<u8>>,
pub conflicted: Vec<Vec<u8>>,
}
pub fn is_conflict_error(error: &std::io::Error) -> bool {
if matches!(
error.kind(),
std::io::ErrorKind::NotFound
| std::io::ErrorKind::NotADirectory
| std::io::ErrorKind::PermissionDenied
) {
return true;
}
#[cfg(unix)]
{
return error.raw_os_error() == Some(libc::ELOOP);
}
#[cfg(not(unix))]
{
return false;
}
}
pub fn identity_of(metadata: &std::fs::Metadata) -> WorktreeFileIdentity {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
return WorktreeFileIdentity {
device: metadata.dev(),
inode: metadata.ino(),
modified: i128::from(metadata.mtime()) * 1_000_000_000
+ i128::from(metadata.mtime_nsec()),
changed: i128::from(metadata.ctime()) * 1_000_000_000
+ i128::from(metadata.ctime_nsec()),
size: metadata.size(),
mode: metadata.mode() & 0o777,
};
}
#[cfg(not(unix))]
{
let modified: i128 = match metadata.modified() {
Ok(time) => match time.duration_since(std::time::UNIX_EPOCH) {
Ok(elapsed) => i128::try_from(elapsed.as_nanos()).unwrap_or(i128::MAX),
Err(_) => 0,
},
Err(_) => 0,
};
return WorktreeFileIdentity {
device: 0,
inode: 0,
modified,
changed: modified,
size: metadata.len(),
mode: if metadata.permissions().readonly() {
0o444
} else {
0o666
},
};
}
}
pub fn link_count(metadata: &std::fs::Metadata) -> u64 {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
return metadata.nlink();
}
#[cfg(not(unix))]
{
let _ = metadata;
return 1;
}
}
pub fn directory_is_canonical(destination: &Path) -> std::io::Result<bool> {
let Some(parent) = destination.parent() else {
return Ok(false);
};
return Ok(std::fs::canonicalize(parent)? == parent);
}
pub fn open_nonblocking(destination: &Path) -> std::io::Result<std::fs::File> {
let mut options: std::fs::OpenOptions = std::fs::OpenOptions::new();
options.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
}
return options.open(destination);
}
pub fn inspect_worktree_file(
destination: &Path,
git_mode: &str,
original: &[u8],
intended: &[u8],
) -> std::io::Result<WorktreeFileState> {
match inspect_or_fail(destination, git_mode, original, intended) {
Ok(state) => return Ok(state),
Err(error) if is_conflict_error(&error) => return Ok(WorktreeFileState::Conflict),
Err(error) => return Err(error),
}
}
pub fn inspect_or_fail(
destination: &Path,
git_mode: &str,
original: &[u8],
intended: &[u8],
) -> std::io::Result<WorktreeFileState> {
if !directory_is_canonical(destination)? {
return Ok(WorktreeFileState::Conflict);
}
let entry: std::fs::Metadata = std::fs::symlink_metadata(destination)?;
if !entry.file_type().is_file() || link_count(&entry) != 1 {
return Ok(WorktreeFileState::Conflict);
}
let mut handle: std::fs::File = open_nonblocking(destination)?;
let opened: std::fs::Metadata = handle.metadata()?;
let opened_identity: WorktreeFileIdentity = identity_of(&opened);
let entry_identity: WorktreeFileIdentity = identity_of(&entry);
if !opened.file_type().is_file()
|| link_count(&opened) != 1
|| entry_identity.device != opened_identity.device
|| entry_identity.inode != opened_identity.inode
{
return Ok(WorktreeFileState::Conflict);
}
if (opened_identity.mode & 0o100 != 0) != (git_mode == "100755") {
return Ok(WorktreeFileState::Conflict);
}
let mut bytes: Vec<u8> = Vec::new();
handle.read_to_end(&mut bytes)?;
let after: WorktreeFileIdentity = identity_of(&std::fs::symlink_metadata(destination)?);
if after.device != opened_identity.device
|| after.inode != opened_identity.inode
|| after.modified != opened_identity.modified
|| after.changed != opened_identity.changed
|| after.size != opened_identity.size
|| !directory_is_canonical(destination)?
{
return Ok(WorktreeFileState::Conflict);
}
if bytes == intended {
return Ok(WorktreeFileState::Intended);
}
if bytes != original {
return Ok(WorktreeFileState::Conflict);
}
return Ok(WorktreeFileState::Original(opened_identity));
}