bread-ecosystem/bread-utils/src/atomic.rs
Breadway 853ee33415 Add bread-utils and bread-onnx: shared crates for ecosystem-wide duplication
bread-utils extracts genuinely duplicated logic found across breadbox,
breadclip, breadmon, breadcrumbs, bos-settings, and breadhelp:

- hypr: Hyprland socket1 request/response client (breadbox's
  get_active_workspace + breadclip's position.rs hyprctl_json were
  near-identical), socket2 path resolution (breadmon), and a
  version-tolerant `fullscreen` field parser (Hyprland has shipped both
  bool and int representations across versions).
- singleton: correct flock-based single-instance toggle, replacing the
  TOCTOU-prone read-pid/check-proc/kill/write-pid pattern duplicated
  verbatim between breadbox and breadclip (breadclip's own comment says
  "matches breadbox pattern").
- proc: breadcrumbs' timeout-guarded subprocess runner, promoted verbatim
  as the one implementation in the ecosystem that already got this right.
- atomic + xdg: atomic (temp-then-rename) file writes with an optional
  .bak-before-overwrite variant, and XDG path helpers that never fall back
  to a literal "~/..." string (the exact breadclip-core and
  breadpad-shared bug: PathBuf never expands `~`).
- tomlcfg (feature "toml"): the load_doc/save_doc TOML-editing discipline
  bos-settings and breadhelp both implemented byte-for-byte identically in
  the same fix pass that introduced it.
- gtk_popup (feature "gtk"): layer-shell overlay window setup, visible-row
  navigation, and click-outside-close, deduplicated from breadbox and
  breadclip (~150 duplicated lines, per both apps' own "same as breadbox"
  comments).

bread-onnx extracts the embedding pipeline (tokenize -> tensor build ->
mean-pool -> L2-normalize) duplicated near-verbatim between breadarr and
breadsearch, a shared execution-provider session builder with loud EP-
registration logging, and a model download+integrity helper. Defaults AMD
iGPU acceleration to ort::ep::MIGraphX (not ROCm) per this machine's own
breadsearch-gpu-backends lesson: ROCMExecutionProvider silently no-ops to
CPU on distro ROCm onnxruntime builds compiled with --use_migraphx.

Both crates build and pass their own test suites standalone. Consumer
migrations follow in subsequent commits.
2026-07-17 09:15:54 +08:00

185 lines
6.9 KiB
Rust

//! Atomic file writes: write to a sibling temp file, then `rename` over the
//! target so a crash, power loss, or disk-full error mid-write never leaves
//! a truncated/corrupt file behind (a same-filesystem rename is atomic).
//!
//! Two flavors, both extracted from real (and identical) duplication:
//!
//! - [`write_atomic`] — temp-then-rename, with an optional Unix `mode` set
//! up front (so secrets never exist world-readable even briefly). This is
//! `breadcrumbs/src/util.rs::write_atomic`, promoted verbatim.
//! - [`write_atomic_backed_up`] — temp-then-rename *plus* a best-effort
//! `<path>.bak` copy of whatever was there before, so a successful-but-wrong
//! write is always recoverable. This is `bos-settings/src/config/mod.rs`'s
//! `atomic_write`, which `breadhelp/src/config.rs` re-implemented
//! byte-for-byte in the same fix pass that introduced it (its own doc
//! comment says "same discipline as bos-settings/src/config/mod.rs") —
//! exactly the kind of fresh duplication this crate exists to remove.
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
/// Write `contents` to `path` atomically. `mode` (Unix only) is applied to
/// the temp file *before* any data is written, so a file that must stay
/// private (secrets, tokens) is never briefly world-readable.
pub fn write_atomic(path: &Path, contents: &str, mode: Option<u32>) -> io::Result<()> {
write_atomic_bytes(path, contents.as_bytes(), mode)
}
/// Byte-oriented sibling of [`write_atomic`], for binary payloads (e.g. a
/// downloaded ONNX model file — see `bread-onnx`'s downloader).
pub fn write_atomic_bytes(path: &Path, contents: &[u8], mode: Option<u32>) -> io::Result<()> {
let dir = path.parent().unwrap_or_else(|| Path::new("."));
fs::create_dir_all(dir)?;
let tmp = tmp_path(path, dir);
let mut open = fs::OpenOptions::new();
open.write(true).create(true).truncate(true);
#[cfg(unix)]
if let Some(mode) = mode {
use std::os::unix::fs::OpenOptionsExt;
open.mode(mode);
}
#[cfg(not(unix))]
let _ = mode;
let res = (|| {
use std::io::Write;
let mut f = open.open(&tmp)?;
f.write_all(contents)?;
f.sync_all()?;
fs::rename(&tmp, path)
})();
if res.is_err() {
let _ = fs::remove_file(&tmp);
}
res
}
/// Like [`write_atomic`] (no `mode`), but first best-effort copies whatever
/// is currently at `path` to `<path>.bak`. The backup is best-effort — a
/// failure to back up (e.g. read-only source, first-ever write) does not
/// block the write itself.
pub fn write_atomic_backed_up(path: &Path, contents: &str) -> io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
if path.exists() {
let backup = backup_path(path);
let _ = fs::copy(path, &backup);
}
write_atomic(path, contents, None)
}
fn tmp_path(path: &Path, dir: &Path) -> PathBuf {
let stem = path.file_name().and_then(|s| s.to_str()).unwrap_or("bread");
dir.join(format!(".{stem}.tmp.{}", std::process::id()))
}
fn backup_path(path: &Path) -> PathBuf {
backup_path_for(path)
}
/// `<path>.bak` — shared with [`crate::tomlcfg`] so its own backup-before-
/// falling-back-to-defaults logging points at the same file this module
/// would have backed up to on a write.
pub(crate) fn backup_path_for(path: &Path) -> PathBuf {
PathBuf::from(format!("{}.bak", path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
fn tmp_dir(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("bread-utils-atomic-test-{name}-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn write_atomic_creates_file_with_contents() {
let dir = tmp_dir("basic");
let path = dir.join("config.toml");
write_atomic(&path, "hello", None).unwrap();
assert_eq!(fs::read_to_string(&path).unwrap(), "hello");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn write_atomic_leaves_no_tmp_file_behind() {
let dir = tmp_dir("no-leftover");
let path = dir.join("config.toml");
write_atomic(&path, "hello", None).unwrap();
let leftover: Vec<_> = fs::read_dir(&dir)
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| n.contains(".tmp."))
.collect();
assert!(leftover.is_empty(), "leftover tmp files: {leftover:?}");
let _ = fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn write_atomic_applies_mode_before_any_data_hits_disk() {
use std::os::unix::fs::PermissionsExt;
let dir = tmp_dir("mode");
let path = dir.join("secret");
write_atomic(&path, "token", Some(0o600)).unwrap();
let perms = fs::metadata(&path).unwrap().permissions();
assert_eq!(perms.mode() & 0o777, 0o600);
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn write_atomic_backed_up_backs_up_previous_contents() {
let dir = tmp_dir("backup");
let path = dir.join("state.toml");
let backup = dir.join("state.toml.bak");
write_atomic_backed_up(&path, "first").unwrap();
assert_eq!(fs::read_to_string(&path).unwrap(), "first");
assert!(!backup.exists(), "no backup should exist before the first overwrite");
write_atomic_backed_up(&path, "second").unwrap();
assert_eq!(fs::read_to_string(&path).unwrap(), "second");
assert_eq!(fs::read_to_string(&backup).unwrap(), "first");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn write_atomic_backed_up_leaves_no_tmp_file_behind() {
let dir = tmp_dir("backup-no-leftover");
let path = dir.join("state.toml");
write_atomic_backed_up(&path, "first").unwrap();
write_atomic_backed_up(&path, "second").unwrap();
let leftover: Vec<_> = fs::read_dir(&dir)
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| n.contains(".tmp."))
.collect();
assert!(leftover.is_empty(), "leftover tmp files: {leftover:?}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn write_atomic_overwrite_never_leaves_partial_contents_visible() {
// Not a true crash-injection test (hard to do portably), but pins
// down the observable contract: after a successful call, the file
// is either fully old or fully new, never truncated.
let dir = tmp_dir("no-partial");
let path = dir.join("f");
write_atomic(&path, "aaaaaaaaaa", None).unwrap();
write_atomic(&path, "b", None).unwrap();
let mut s = String::new();
fs::File::open(&path).unwrap().read_to_string(&mut s).unwrap();
assert_eq!(s, "b");
let _ = fs::remove_dir_all(&dir);
}
}