1use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, PartialEq)]
9pub struct Change {
10 pub path: PathBuf,
11 pub before: Option<String>,
12 pub after: Option<String>,
13 pub through_links: bool,
16 pub backup: bool,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum Action {
25 Install,
26 Uninstall,
27}
28
29pub(crate) fn read(p: &Path) -> std::io::Result<Option<String>> {
32 match std::fs::read_to_string(p) {
33 Ok(t) => Ok(Some(t)),
34 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
35 Err(e) => Err(e),
36 }
37}
38
39#[cfg(unix)]
41const NEW_FILE_MODE: u32 = 0o600;
42
43fn create_with_mode(p: &Path, text: &str, mode: Option<u32>) -> std::io::Result<()> {
47 use std::io::Write;
48 let mut o = std::fs::OpenOptions::new();
49 o.write(true).create_new(true);
50 #[cfg(unix)]
51 if let Some(m) = mode {
52 use std::os::unix::fs::OpenOptionsExt;
53 o.mode(m);
54 }
55 let mut f = o.open(p)?;
56 #[cfg(unix)]
57 if let Some(m) = mode {
58 use std::os::unix::fs::PermissionsExt;
59 f.set_permissions(std::fs::Permissions::from_mode(m))?;
60 }
61 #[cfg(not(unix))]
62 let _ = mode;
63 f.write_all(text.as_bytes())?;
64 f.sync_all()
65}
66
67fn write_backup(path: &Path, name: &str, stamp: &str, text: &str, mode: Option<u32>) -> std::io::Result<PathBuf> {
70 for n in 0u32.. {
71 let suffix = if n == 0 { String::new() } else { format!("-{n}") };
72 let p = path.with_file_name(format!("{name}.borg-backup-{stamp}{suffix}"));
73 match create_with_mode(&p, text, mode) {
74 Ok(()) => return Ok(p),
75 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
76 Err(e) => return Err(e),
77 }
78 }
79 unreachable!("u32 backups")
80}
81
82#[cfg(unix)]
83fn mode_of(p: &Path) -> Option<u32> {
84 use std::os::unix::fs::PermissionsExt;
85 std::fs::metadata(p).ok().map(|m| m.permissions().mode() & 0o7777)
86}
87
88#[cfg(not(unix))]
89fn mode_of(_: &Path) -> Option<u32> {
90 None
91}
92
93pub fn apply(c: &Change, stamp: &str) -> std::io::Result<Option<PathBuf>> {
100 let name = c.path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
101 let mode = mode_of(&c.path); let backup = match &c.before {
103 Some(b) if c.backup => Some(write_backup(&c.path, &name, stamp, b, mode)?),
104 _ => None,
105 };
106 match &c.after {
107 None => std::fs::remove_file(&c.path)?,
108 Some(text) => replace(c, &name, text, mode)?,
109 }
110 Ok(backup)
111}
112
113fn replace(c: &Change, name: &str, text: &str, mode: Option<u32>) -> std::io::Result<()> {
116 let is_link = std::fs::symlink_metadata(&c.path).is_ok_and(|m| m.file_type().is_symlink());
117 let target = if is_link && c.through_links { std::fs::canonicalize(&c.path)? } else { c.path.clone() };
118 if let Some(d) = target.parent() {
119 std::fs::create_dir_all(d)?;
120 }
121 #[cfg(unix)]
122 let mode = mode.or(Some(NEW_FILE_MODE));
123 let tmp = target.with_file_name(format!(".{name}.borg-tmp"));
124 let _ = std::fs::remove_file(&tmp); create_with_mode(&tmp, text, mode)?;
126 std::fs::rename(&tmp, &target) }
128
129#[must_use]
131pub fn diff(path: &Path, before: Option<&str>, after: Option<&str>) -> String {
132 let a: Vec<&str> = before.map(|t| t.lines().collect()).unwrap_or_default();
133 let b: Vec<&str> = after.map(|t| t.lines().collect()).unwrap_or_default();
134 let label = |t: Option<&str>| if t.is_some() { path.display().to_string() } else { "/dev/null".into() };
135 format!("--- {}\n+++ {}\n{}", label(before), label(after), hunks(&ops(&a, &b)))
136}
137
138fn ops<'t>(a: &[&'t str], b: &[&'t str]) -> Vec<(char, &'t str)> {
140 let lcs = lcs(a, b);
141 let (mut i, mut j, mut ops) = (0, 0, Vec::new());
142 while i < a.len() || j < b.len() {
143 if i < a.len() && j < b.len() && a[i] == b[j] {
144 ops.push((' ', a[i]));
145 (i, j) = (i + 1, j + 1);
146 } else if j < b.len() && (i == a.len() || lcs[i][j + 1] >= lcs[i + 1][j]) {
147 ops.push(('+', b[j]));
148 j += 1;
149 } else {
150 ops.push(('-', a[i]));
151 i += 1;
152 }
153 }
154 ops
155}
156
157fn lcs(a: &[&str], b: &[&str]) -> Vec<Vec<u32>> {
159 let (n, m) = (a.len(), b.len());
160 let mut lcs = vec![vec![0u32; m + 1]; n + 1];
161 for i in (0..n).rev() {
162 for j in (0..m).rev() {
163 lcs[i][j] = if a[i] == b[j] { lcs[i + 1][j + 1] + 1 } else { lcs[i + 1][j].max(lcs[i][j + 1]) };
164 }
165 }
166 lcs
167}
168
169fn hunks(ops: &[(char, &str)]) -> String {
171 let near = |k: usize| ops[k.saturating_sub(3)..(k + 4).min(ops.len())].iter().any(|(o, _)| *o != ' ');
172 let mut out = String::new();
173 let mut gap = true;
174 for (k, (o, line)) in ops.iter().enumerate() {
175 if near(k) {
176 if gap {
177 out.push_str("@@\n");
178 }
179 out.push(*o);
180 out.push_str(line);
181 out.push('\n');
182 gap = false;
183 } else {
184 gap = true;
185 }
186 }
187 out
188}