use std::path::Path;
use crate::error::{Error, Result};
use crate::util::atomic_write;
const REPOINT_ATTEMPTS: usize = 3;
pub enum Remap {
Keep,
Rewrite(String),
Skip(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Repointed {
pub from: String,
pub to: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepointSkip {
pub path: String,
pub reason: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RepointReport {
pub rewritten: Vec<Repointed>,
pub skipped: Vec<RepointSkip>,
}
impl RepointReport {
pub fn changed(&self) -> bool {
!self.rewritten.is_empty()
}
}
pub fn repoint_install_paths(registry: &Path, mut remap: impl FnMut(&str) -> Remap) -> Result<RepointReport> {
for _ in 0..REPOINT_ATTEMPTS {
let (report, out) = one_pass(registry, &mut remap)?;
let Some(out) = out else {
return Ok(report);
};
let Some(fresh) = read_registry(registry)? else {
return Err(Error::Io {
context: format!("{}: deleted under the pass; refusing to resurrect it", registry.display()),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "the registry vanished after the pass read it"),
});
};
if fresh == out.read_bytes {
atomic_write(registry, out.text.as_bytes())?;
return Ok(report);
}
}
Err(Error::Io {
context: format!("{}: changed under the pass after {REPOINT_ATTEMPTS} attempts", registry.display()),
source: std::io::Error::new(std::io::ErrorKind::WouldBlock, "the registry keeps changing"),
})
}
fn one_pass(registry: &Path, remap: &mut impl FnMut(&str) -> Remap) -> Result<(RepointReport, Option<PassOut>)> {
let Some(bytes) = read_registry(registry)? else {
return Ok((RepointReport::default(), None));
};
let text = std::str::from_utf8(&bytes).map_err(|source| Error::Io {
context: format!("reading {}: not utf-8", registry.display()),
source: std::io::Error::new(std::io::ErrorKind::InvalidData, source),
})?;
let values = quoted_values(text);
let mut report = RepointReport::default();
let mut decisions = Vec::with_capacity(values.len());
for (open, close) in values {
let value = &text[open + 1..close];
let decision = match remap(value) {
Remap::Keep => Decision::Keep,
Remap::Rewrite(to) if to == value => Decision::Keep,
Remap::Rewrite(to) => {
if !report.rewritten.iter().any(|r| r.from == value) {
report.rewritten.push(Repointed { from: value.to_string(), to: to.clone() });
}
Decision::Rewrite(to)
}
Remap::Skip(reason) => {
if !report.skipped.iter().any(|s| s.path == value) {
report.skipped.push(RepointSkip { path: value.to_string(), reason });
}
Decision::Keep
}
};
decisions.push((open, close, decision));
}
if !report.changed() {
return Ok((report, None));
}
let mut out = String::with_capacity(text.len());
let mut last = 0;
for (open, close, decision) in decisions {
out.push_str(&text[last..open]);
match decision {
Decision::Keep => out.push_str(&text[open..=close]),
Decision::Rewrite(to) => {
out.push('"');
out.push_str(&to);
out.push('"');
}
}
last = close + 1;
}
out.push_str(&text[last..]);
Ok((report, Some(PassOut { text: out, read_bytes: bytes })))
}
struct PassOut {
text: String,
read_bytes: Vec<u8>,
}
enum Decision {
Keep,
Rewrite(String),
}
fn read_registry(registry: &Path) -> Result<Option<Vec<u8>>> {
match std::fs::read(registry) {
Ok(bytes) => Ok(Some(bytes)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(Error::Io { context: format!("reading {}", registry.display()), source: e }),
}
}
fn quoted_values(text: &str) -> Vec<(usize, usize)> {
let b = text.as_bytes();
let mut out = Vec::new();
let mut i = 0;
while i < b.len() {
if b[i] != b'"' {
i += 1;
continue;
}
let open = i;
i += 1;
while i < b.len() && b[i] != b'"' {
i = if b[i] == b'\\' { i + 2 } else { i + 1 };
}
if i < b.len() {
out.push((open, i));
}
i += 1;
}
out
}
#[cfg(test)]
#[path = "../tests/unit/repoint.rs"]
mod repoint_tests;