Skip to main content

rightkit_db/
host.rs

1//! Per-host database paths for rollback-journal fallback, ported from CodeRight's
2//! `private_fs::journal` (`effective_db_path` / `host_discriminator`).
3//!
4//! A rollback journal on a network share is only safe with one writer host per file, so the
5//! fallback opens `<stem>.h-<host>.<ext>` beside the requested path instead of sharing it.
6
7use std::path::{Path, PathBuf};
8
9/// `name.db` -> `name.h-<host>.db` (extension-less names get the tag appended). Idempotent;
10/// unchanged when no host name is available.
11pub fn per_host_path(db_path: &Path) -> PathBuf {
12    let Some(host) = host_discriminator() else {
13        return db_path.to_path_buf();
14    };
15    tagged(db_path, &host)
16}
17
18pub(crate) fn tagged(db_path: &Path, host: &str) -> PathBuf {
19    let Some(name) = db_path
20        .file_name()
21        .map(|s| s.to_string_lossy().into_owned())
22    else {
23        return db_path.to_path_buf();
24    };
25    let tag = format!(".h-{host}");
26    if name.ends_with(&tag) || name.contains(&format!("{tag}.")) {
27        return db_path.to_path_buf();
28    }
29    // rsplit keeps dotfile names like ".hidden" on the append arm.
30    let new_name = match name.rsplit_once('.') {
31        Some((stem, ext)) if !stem.is_empty() => format!("{stem}{tag}.{ext}"),
32        _ => format!("{name}{tag}"),
33    };
34    db_path.with_file_name(new_name)
35}
36
37/// Lower-case alphanumerics and `-`, at most 24 characters; `None` when unavailable.
38pub(crate) fn host_discriminator() -> Option<String> {
39    let raw = hostname_raw()?;
40    let mut s: String = raw
41        .trim()
42        .chars()
43        .map(|c| {
44            if c.is_ascii_alphanumeric() {
45                c.to_ascii_lowercase()
46            } else {
47                '-'
48            }
49        })
50        .collect();
51    s.truncate(24);
52    let s = s.trim_matches('-');
53    (!s.is_empty()).then(|| s.to_string())
54}
55
56#[cfg(unix)]
57fn hostname_raw() -> Option<String> {
58    let mut buf = [0u8; 256];
59    // SAFETY: valid out-buffer of the given length; position() guards a missing NUL.
60    if unsafe { libc::gethostname(buf.as_mut_ptr().cast(), buf.len()) } != 0 {
61        return None;
62    }
63    let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
64    String::from_utf8(buf[..len].to_vec()).ok()
65}
66
67#[cfg(windows)]
68fn hostname_raw() -> Option<String> {
69    if let Ok(host) = std::env::var("COMPUTERNAME") {
70        if !host.trim().is_empty() {
71            return Some(host);
72        }
73    }
74    #[link(name = "kernel32")]
75    unsafe extern "system" {
76        fn GetComputerNameW(buffer: *mut u16, size: *mut u32) -> i32;
77    }
78    let mut buffer = [0u16; 256];
79    let mut size = buffer.len() as u32;
80    // SAFETY: valid buffer and size out-parameter.
81    if unsafe { GetComputerNameW(buffer.as_mut_ptr(), &mut size) } == 0 || size == 0 {
82        return None;
83    }
84    String::from_utf16(&buffer[..size as usize]).ok()
85}
86
87#[cfg(not(any(unix, windows)))]
88fn hostname_raw() -> Option<String> {
89    None
90}