use crate::diagnostic_log::debug;
use crate::fs_beneath::{ObjectId, entry_metadata_at, identity_at, identity_of, open_directory_at, open_file_at, read_link_at};
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::io::{Read, Write};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
const TAG: &str = "conflictSideSource";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FailureKind {
Io,
Ancestor,
Changed,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SaveFailure {
pub subject: String,
pub reason: String,
pub kind: FailureKind,
}
impl SaveFailure {
pub fn io(subject: String, reason: String) -> SaveFailure {
debug(TAG, &format!("fail closed: {subject}: {reason}"));
return SaveFailure { subject, reason, kind: FailureKind::Io };
}
fn of_kind(kind: FailureKind, subject: String, reason: String) -> SaveFailure {
debug(TAG, &format!("fail closed ({kind:?}): {subject}: {reason}"));
return SaveFailure { subject, reason, kind };
}
}
fn io_failure(subject: &str, error: &std::io::Error) -> SaveFailure {
return SaveFailure::io(String::from(subject), error.to_string());
}
fn joined(names: &[OsString]) -> String {
let mut text: String = String::new();
for (position, name) in names.iter().enumerate() {
if position > 0 {
text.push('/');
}
text.push_str(&name.to_string_lossy());
}
return text;
}
#[derive(Debug)]
pub struct Located {
root: PathBuf,
relative: Vec<u8>,
parent: File,
name: OsString,
}
fn open_parent(root: &Path, relative: &[u8]) -> Result<Option<(File, OsString)>, SaveFailure> {
let shown: String = String::from_utf8_lossy(relative).into_owned();
let names: Vec<OsString> = match crate::fs_beneath::plain_components(Path::new(OsStr::from_bytes(relative))) {
Ok(found) => found,
Err(error) => return Err(io_failure(&shown, &error)),
};
let Some((last, ancestors)) = names.split_last() else {
return Err(SaveFailure::io(shown, String::from("it names no file")));
};
let mut current: File = match File::open(root) {
Ok(opened) => opened,
Err(error) => return Err(io_failure(&root.display().to_string(), &error)),
};
for (depth, name) in ancestors.iter().enumerate() {
match open_directory_at(¤t, name.as_os_str()) {
Ok(next) => current = next,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => {
let subject: String = joined(&names[..=depth]);
let reason: String = match error.raw_os_error() {
Some(libc::ELOOP) => String::from("is a symbolic link"),
Some(libc::ENOTDIR) => String::from("is a file, not a directory"),
Some(libc::EXDEV) => {
return Err(SaveFailure::io(subject, String::from("is on another mount, which the wrapper does not read across")));
}
_ => format!("cannot be opened without following a link ({error})"),
};
return Err(SaveFailure::of_kind(FailureKind::Ancestor, subject, reason));
}
}
}
return Ok(Some((current, last.clone())));
}
pub fn locate(root: &Path, relative: &[u8]) -> Result<Option<Located>, SaveFailure> {
let Some((parent, name)) = open_parent(root, relative)? else {
return Ok(None);
};
return match entry_metadata_at(&parent, name.as_os_str()) {
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(io_failure(&String::from_utf8_lossy(relative), &error)),
Ok(_) => Ok(Some(Located { root: root.to_path_buf(), relative: relative.to_vec(), parent, name })),
};
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct Stamp {
object: (u64, u64),
size: u64,
modified: (i64, i64),
changed: (i64, i64),
mode: u32,
}
impl Stamp {
fn of(metadata: &std::fs::Metadata) -> Stamp {
return Stamp {
object: (metadata.dev(), metadata.ino()),
size: metadata.size(),
modified: (metadata.mtime(), metadata.mtime_nsec()),
changed: (metadata.ctime(), metadata.ctime_nsec()),
mode: metadata.mode(),
};
}
}
fn changed(located: &Located, what: &str) -> SaveFailure {
return SaveFailure::of_kind(FailureKind::Changed, String::from_utf8_lossy(located.relative.as_slice()).into_owned(), what.to_string());
}
fn copy_link(located: &Located, before: &std::fs::Metadata, destination: &Path) -> Result<(), SaveFailure> {
let target: PathBuf = match read_link_at(&located.parent, located.name.as_os_str()) {
Ok(read) => read,
Err(error) => return Err(changed(located, format!("could not be read as a link ({error})").as_str())),
};
if let Err(error) = std::os::unix::fs::symlink(&target, destination) {
return Err(io_failure(&String::from_utf8_lossy(located.relative.as_slice()), &error));
}
if !link_unchanged(located, before, &target) {
let _removed = std::fs::remove_file(destination);
return Err(changed(located, "was replaced while its link target was copied"));
}
return Ok(());
}
fn link_unchanged(located: &Located, before: &std::fs::Metadata, target: &Path) -> bool {
let Ok(now) = entry_metadata_at(&located.parent, located.name.as_os_str()) else {
return false;
};
if Stamp::of(&now) != Stamp::of(before) {
return false;
}
let Ok(read) = read_link_at(&located.parent, located.name.as_os_str()) else {
return false;
};
return read == target;
}
enum CopyError {
System(std::io::Error),
Failed(SaveFailure),
}
impl From<std::io::Error> for CopyError {
fn from(error: std::io::Error) -> CopyError {
return CopyError::System(error);
}
}
impl From<SaveFailure> for CopyError {
fn from(failure: SaveFailure) -> CopyError {
return CopyError::Failed(failure);
}
}
fn copy_stream(located: &Located, from: &mut File, to: &mut File, before: Stamp, between: &mut dyn FnMut()) -> Result<(), CopyError> {
let mut buffer: Vec<u8> = vec![0; 1 << 16];
let mut copied: u64 = 0;
loop {
let count: usize = from.read(buffer.as_mut_slice())?;
if count == 0 {
break;
}
to.write_all(&buffer[..count])?;
copied += u64::try_from(count).unwrap_or(u64::MAX);
between();
}
let after: Stamp = Stamp::of(&from.metadata()?);
if after != before {
debug(TAG, &format!("{}: stamp before the copy {before:?}, after {after:?}", String::from_utf8_lossy(located.relative.as_slice())));
return Err(changed(located, "changed while it was copied").into());
}
if copied != before.size {
return Err(changed(located, format!("gave {copied} bytes where it had {}", before.size).as_str()).into());
}
let named_now: Option<ObjectId> = match open_parent(located.root.as_path(), located.relative.as_slice())? {
Some((parent, name)) => identity_at(&parent, name.as_os_str()).ok(),
None => None,
};
if named_now != identity_of(from).ok() {
return Err(changed(located, "no longer holds the file that was copied").into());
}
to.set_permissions(<std::fs::Permissions as std::os::unix::fs::PermissionsExt>::from_mode(before.mode & 0o777))?;
to.sync_all()?;
return Ok(());
}
fn copy_entry(located: &Located, metadata: &std::fs::Metadata, destination: &Path, between: &mut dyn FnMut()) -> Result<(), CopyError> {
let shown: String = String::from_utf8_lossy(located.relative.as_slice()).into_owned();
let mut from: File = open_file_at(&located.parent, located.name.as_os_str())?;
let opened: std::fs::Metadata = from.metadata()?;
if !opened.file_type().is_file() {
return Err(SaveFailure::io(shown, String::from("it is neither a file nor a symbolic link")).into());
}
let before: Stamp = Stamp::of(&opened);
if before != Stamp::of(metadata) {
return Err(changed(located, "changed between the look at its name and the open").into());
}
let mut to: File = std::fs::OpenOptions::new().write(true).create_new(true).mode_600().open(destination)?;
let streamed: Result<(), CopyError> = copy_stream(located, &mut from, &mut to, before, between);
if streamed.is_err() {
let _removed = std::fs::remove_file(destination);
}
return streamed;
}
pub fn copy_located(located: &Located, destination: &Path, between: &mut dyn FnMut()) -> Result<(), SaveFailure> {
let shown: String = String::from_utf8_lossy(located.relative.as_slice()).into_owned();
let metadata: std::fs::Metadata = match entry_metadata_at(&located.parent, located.name.as_os_str()) {
Ok(found) => found,
Err(error) => return Err(io_failure(&shown, &error)),
};
if metadata.file_type().is_symlink() {
return copy_link(located, &metadata, destination);
}
if !metadata.file_type().is_file() {
return Err(SaveFailure::io(shown, String::from("it is neither a file nor a symbolic link")));
}
return match copy_entry(located, &metadata, destination, between) {
Ok(()) => Ok(()),
Err(CopyError::System(error)) => Err(io_failure(&shown, &error)),
Err(CopyError::Failed(failure)) => Err(failure),
};
}
trait PrivateCreation {
fn mode_600(&mut self) -> &mut Self;
}
impl PrivateCreation for std::fs::OpenOptions {
fn mode_600(&mut self) -> &mut Self {
return std::os::unix::fs::OpenOptionsExt::mode(self, 0o600);
}
}
#[cfg(test)]
#[path = "conflict_side_source_tests.rs"]
mod tests;