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