use crate::candidate_object::CandidateMode;
use crate::candidate_private_index::unique_suffix;
use crate::git_metadata::path_from_git_bytes;
use std::path::{Path, PathBuf};
use std::rc::Rc;
pub const INSTALL_SIBLING_PREFIX: &str = ".cli-git-direct-fix-";
const INSTALL_FAILED: &str = "cli-git fix could not install its corrections";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct InstallChange {
pub path: Vec<u8>,
pub mode: CandidateMode,
pub original: Rc<[u8]>,
pub replacement: Rc<[u8]>,
}
#[derive(Debug)]
pub struct Replacement {
destination: PathBuf,
prepared: PathBuf,
backup: PathBuf,
}
fn install_failure(detail: &str) -> String {
return format!("{INSTALL_FAILED}: {detail}");
}
pub fn read_index(path: &Path) -> std::io::Result<Option<Vec<u8>>> {
match std::fs::read(path) {
Ok(bytes) => return Ok(Some(bytes)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error),
}
}
fn cleanup(replacements: &[Replacement]) {
for replacement in replacements {
let _ = std::fs::remove_file(replacement.prepared.as_path());
let _ = std::fs::remove_file(replacement.backup.as_path());
}
}
fn rollback(installed: &[Replacement]) -> Result<(), String> {
for replacement in installed.iter().rev() {
if let Err(error) = std::fs::rename(
replacement.backup.as_path(),
replacement.destination.as_path(),
) {
return Err(format!(
"restoring a corrected file failed ({error}); its original bytes remain beside it in a file whose name starts with {INSTALL_SIBLING_PREFIX}"
));
}
}
return Ok(());
}
fn write_new(path: &Path, bytes: &[u8], mode: CandidateMode) -> std::io::Result<()> {
use std::io::Write;
let mut options: std::fs::OpenOptions = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let permissions: u32 = if mode == CandidateMode::Executable {
0o755
} else {
0o644
};
options.mode(permissions);
}
#[cfg(not(unix))]
{
let _ = mode;
}
return options.open(path)?.write_all(bytes);
}
pub fn prepare_replacements(
root: &Path,
changes: &[InstallChange],
) -> Result<Vec<Replacement>, String> {
let mut replacements: Vec<Replacement> = Vec::new();
for change in changes {
let Some(relative) = path_from_git_bytes(change.path.as_slice()) else {
cleanup(replacements.as_slice());
return Err(install_failure(
"Git named a corrected file this system cannot represent, so no file was changed.",
));
};
let destination: PathBuf = root.join(relative);
let current: Vec<u8> = match std::fs::read(destination.as_path()) {
Ok(bytes) => bytes,
Err(error) => {
cleanup(replacements.as_slice());
return Err(install_failure(
format!("reading a corrected file failed ({error}), so no file was changed.")
.as_str(),
));
}
};
if current.as_slice() != &*change.original {
cleanup(replacements.as_slice());
return Err(install_failure(
"a selected file changed while cli-git fix ran, so no file was changed. Run cli-git fix again.",
));
}
let parent: PathBuf = match destination.parent() {
Some(found) => found.to_path_buf(),
None => root.to_path_buf(),
};
let prefix: String = format!("{INSTALL_SIBLING_PREFIX}{}", unique_suffix());
let replacement: Replacement = Replacement {
prepared: parent.join(format!("{prefix}.new")),
backup: parent.join(format!("{prefix}.old")),
destination,
};
let written: std::io::Result<()> = match std::fs::copy(
replacement.destination.as_path(),
replacement.backup.as_path(),
) {
Ok(_) => write_new(
replacement.prepared.as_path(),
&change.replacement,
change.mode,
),
Err(error) => Err(error),
};
replacements.push(replacement);
if let Err(error) = written {
cleanup(replacements.as_slice());
return Err(install_failure(
format!("writing a corrected file beside its original failed ({error}), so no file was changed.").as_str(),
));
}
}
return Ok(replacements);
}
pub fn install_prepared(
replacements: &[Replacement],
real_index: &Path,
index_before: &Option<Vec<u8>>,
) -> Result<(), String> {
let mut installed: usize = 0;
let mut failure: Option<String> = None;
for replacement in replacements {
if let Err(error) = std::fs::rename(
replacement.prepared.as_path(),
replacement.destination.as_path(),
) {
failure = Some(format!("replacing a corrected file failed ({error})"));
break;
}
installed += 1;
}
if failure.is_none() {
failure = match read_index(real_index) {
Ok(after) if after == *index_before => None,
Ok(_) => Some(String::from("the index changed while cli-git fix ran")),
Err(error) => Some(format!("reading the index again failed ({error})")),
};
}
let Some(cause) = failure else {
cleanup(replacements);
return Ok(());
};
let restored: Result<(), String> = rollback(&replacements[..installed]);
match restored {
Ok(()) => {
cleanup(replacements);
return Err(install_failure(
format!("{cause}, so every corrected file was restored. Run cli-git fix again.")
.as_str(),
));
}
Err(restore_failure) => {
for replacement in replacements {
let _ = std::fs::remove_file(replacement.prepared.as_path());
}
return Err(install_failure(
format!("{cause}, and {restore_failure}.").as_str(),
));
}
}
}
pub fn install_corrections(
root: &Path,
real_index: &Path,
changes: &[InstallChange],
) -> Result<(), String> {
let index_before: Option<Vec<u8>> = match read_index(real_index) {
Ok(found) => found,
Err(error) => {
return Err(install_failure(
format!("reading the index failed ({error}), so no file was changed.").as_str(),
));
}
};
let replacements: Vec<Replacement> = prepare_replacements(root, changes)?;
return install_prepared(replacements.as_slice(), real_index, &index_before);
}
#[cfg(test)]
#[path = "direct_fix_install_tests.rs"]
mod tests;