use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq)]
pub struct Change {
pub path: PathBuf,
pub before: Option<String>,
pub after: Option<String>,
pub through_links: bool,
pub backup: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
Install,
Uninstall,
}
pub(crate) fn read(p: &Path) -> std::io::Result<Option<String>> {
match std::fs::read_to_string(p) {
Ok(t) => Ok(Some(t)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
}
#[cfg(unix)]
const NEW_FILE_MODE: u32 = 0o600;
fn create_with_mode(p: &Path, text: &str, mode: Option<u32>) -> std::io::Result<()> {
use std::io::Write;
let mut o = std::fs::OpenOptions::new();
o.write(true).create_new(true);
#[cfg(unix)]
if let Some(m) = mode {
use std::os::unix::fs::OpenOptionsExt;
o.mode(m);
}
let mut f = o.open(p)?;
#[cfg(unix)]
if let Some(m) = mode {
use std::os::unix::fs::PermissionsExt;
f.set_permissions(std::fs::Permissions::from_mode(m))?;
}
#[cfg(not(unix))]
let _ = mode;
f.write_all(text.as_bytes())?;
f.sync_all()
}
fn write_backup(path: &Path, name: &str, stamp: &str, text: &str, mode: Option<u32>) -> std::io::Result<PathBuf> {
for n in 0u32.. {
let suffix = if n == 0 { String::new() } else { format!("-{n}") };
let p = path.with_file_name(format!("{name}.borg-backup-{stamp}{suffix}"));
match create_with_mode(&p, text, mode) {
Ok(()) => return Ok(p),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(e) => return Err(e),
}
}
unreachable!("u32 backups")
}
#[cfg(unix)]
fn mode_of(p: &Path) -> Option<u32> {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(p).ok().map(|m| m.permissions().mode() & 0o7777)
}
#[cfg(not(unix))]
fn mode_of(_: &Path) -> Option<u32> {
None
}
pub fn apply(c: &Change, stamp: &str) -> std::io::Result<Option<PathBuf>> {
let name = c.path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
let mode = mode_of(&c.path); let backup = match &c.before {
Some(b) if c.backup => Some(write_backup(&c.path, &name, stamp, b, mode)?),
_ => None,
};
match &c.after {
None => std::fs::remove_file(&c.path)?,
Some(text) => replace(c, &name, text, mode)?,
}
Ok(backup)
}
fn replace(c: &Change, name: &str, text: &str, mode: Option<u32>) -> std::io::Result<()> {
let is_link = std::fs::symlink_metadata(&c.path).is_ok_and(|m| m.file_type().is_symlink());
let target = if is_link && c.through_links { std::fs::canonicalize(&c.path)? } else { c.path.clone() };
if let Some(d) = target.parent() {
std::fs::create_dir_all(d)?;
}
#[cfg(unix)]
let mode = mode.or(Some(NEW_FILE_MODE));
let tmp = target.with_file_name(format!(".{name}.borg-tmp"));
let _ = std::fs::remove_file(&tmp); create_with_mode(&tmp, text, mode)?;
std::fs::rename(&tmp, &target) }
#[must_use]
pub fn diff(path: &Path, before: Option<&str>, after: Option<&str>) -> String {
let a: Vec<&str> = before.map(|t| t.lines().collect()).unwrap_or_default();
let b: Vec<&str> = after.map(|t| t.lines().collect()).unwrap_or_default();
let label = |t: Option<&str>| if t.is_some() { path.display().to_string() } else { "/dev/null".into() };
format!("--- {}\n+++ {}\n{}", label(before), label(after), hunks(&ops(&a, &b)))
}
fn ops<'t>(a: &[&'t str], b: &[&'t str]) -> Vec<(char, &'t str)> {
let lcs = lcs(a, b);
let (mut i, mut j, mut ops) = (0, 0, Vec::new());
while i < a.len() || j < b.len() {
if i < a.len() && j < b.len() && a[i] == b[j] {
ops.push((' ', a[i]));
(i, j) = (i + 1, j + 1);
} else if j < b.len() && (i == a.len() || lcs[i][j + 1] >= lcs[i + 1][j]) {
ops.push(('+', b[j]));
j += 1;
} else {
ops.push(('-', a[i]));
i += 1;
}
}
ops
}
fn lcs(a: &[&str], b: &[&str]) -> Vec<Vec<u32>> {
let (n, m) = (a.len(), b.len());
let mut lcs = vec![vec![0u32; m + 1]; n + 1];
for i in (0..n).rev() {
for j in (0..m).rev() {
lcs[i][j] = if a[i] == b[j] { lcs[i + 1][j + 1] + 1 } else { lcs[i + 1][j].max(lcs[i][j + 1]) };
}
}
lcs
}
fn hunks(ops: &[(char, &str)]) -> String {
let near = |k: usize| ops[k.saturating_sub(3)..(k + 4).min(ops.len())].iter().any(|(o, _)| *o != ' ');
let mut out = String::new();
let mut gap = true;
for (k, (o, line)) in ops.iter().enumerate() {
if near(k) {
if gap {
out.push_str("@@\n");
}
out.push(*o);
out.push_str(line);
out.push('\n');
gap = false;
} else {
gap = true;
}
}
out
}