From f883f6ece0999e3f1aa39d164b527706e82914f6 Mon Sep 17 00:00:00 2001 From: Cyrus <86699936+cyrusae@users.noreply.github.com> Date: Sat, 23 May 2026 21:14:05 -0700 Subject: [PATCH] fix: atomic config writes; release v0.1.0 --- entangle/.chainlink/.cache/last-edit-time | 2 +- entangle/.chainlink/issues.db | Bin 167936 -> 167936 bytes entangle/Cargo.lock | 2 +- entangle/Cargo.toml | 2 +- entangle/src/config.rs | 59 +++++++++++++++++++--- 5 files changed, 56 insertions(+), 9 deletions(-) diff --git a/entangle/.chainlink/.cache/last-edit-time b/entangle/.chainlink/.cache/last-edit-time index f08f8f5..64425df 100644 --- a/entangle/.chainlink/.cache/last-edit-time +++ b/entangle/.chainlink/.cache/last-edit-time @@ -1 +1 @@ -1779594594.8513 \ No newline at end of file +1779595965.963836 \ No newline at end of file diff --git a/entangle/.chainlink/issues.db b/entangle/.chainlink/issues.db index c77dec1c49ec20a4a85ff785e9cf1db62f4954ee..ad0ce2f4d52bbf6f8ecddc78578657d525d9f43e 100644 GIT binary patch delta 857 zcmZ{iv2W8r6vlmaTQ_Oqq(g>MQ{7MrP;28jX&Vdzs!C-FVn7|bxK473PdK*ay95eI zX+JwUb;B|U~_(QicUf4sl(4w&JAd;-i6nAb{TQp%wqB);1J`cs<* z`Lru%(PdFN9n5FZQ0e}dLz74yz#@a3+3d;&S`jmwP<|=X-MWLuL{%NY`rZJ!)s@3D zxdrl4W_(N%p!R78y*kNGq%N4^i&4fGA+aXG5Emzrj~#{^ltYl$u;bToL_*K0f-&PY z%c?WW8f6X+m4Lf=EzaGt&Ziz1K3I1mP95(eK*y_bvvCo;%Qj)QAJ|?{ePqY|RftRB za84NSe%}yh$~rC>X1r(6DPz=cEaIvYH3A&ADjsDn*0fr*twk*#SHThn;h6Z`@f#k| Tnly^wk+kF6*9ZE(c%1wN7cl;o delta 226 zcmZozz}2vTYl1Z6*@-gFjAu6{wCgL#GB7akaAp=4m!=joa4|7UGD6tWyg&(Qz7vz# z4a_$SDs=H}K4E9W$SlpbZZf+ANTh&o@&Z5B&71uK7Xn4EPiFrB5}nC6`P@H_&G-MQ z3Iav{PG=Wjl-;f-z<7>tVgUE{zjBQ1Kym(r>FfrKAmvK@+Y{^=mly!Ws;09SfW>V1 zw^!9Oc75Pu Result<(), ConfigError> { + use std::io::Write as _; + let dir = path.parent().ok_or_else(|| { + ConfigError::CannotWriteFile(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "config path has no parent directory", + )) + })?; + let mut tmp = tempfile::Builder::new() + .suffix(".lock") + .tempfile_in(dir) + .map_err(ConfigError::CannotWriteFile)?; + tmp.write_all(content.as_bytes()) + .map_err(ConfigError::CannotWriteFile)?; + tmp.persist(path) + .map_err(|e| ConfigError::CannotWriteFile(e.error))?; + Ok(()) +} + // --------------------------------------------------------------------------- // Error types // --------------------------------------------------------------------------- @@ -489,11 +521,10 @@ impl Config { let json = serde_json::to_string_pretty(self).expect("Config serialization should never fail"); - // ── 3. Write atomically-ish via a newline-terminated string ────────── - // We write the complete serialized string in one call; partial writes - // are unlikely on local filesystems but the worst case is a - // re-run of `entangle setup`, not data loss in the repo. - std::fs::write(path, json).map_err(ConfigError::CannotWriteFile)?; + // ── 3. Write atomically ────────────────────────────────────────────── + // Uses a unique temp file in the same directory + rename so a crash + // mid-write never leaves the config in a truncated state. + atomic_write_config(path, &json)?; Ok(()) } @@ -747,6 +778,22 @@ mod tests { // ── Save ───────────────────────────────────────────────────────────────── + #[test] + fn save_leaves_no_lock_file_behind() { + // atomic_write_config uses a temp file with a .lock suffix; it must + // be renamed (not left on disk) after a successful write. + let f = NamedTempFile::new().unwrap(); + let path = f.path(); + let lock_path = path.with_extension("lock"); + + valid_config().save_to_path(path).unwrap(); + + assert!( + !lock_path.exists(), + "no .lock file should remain after a successful save" + ); + } + #[test] fn save_creates_parent_directory_if_missing() { // Create a temp dir, then point save() at a nested path that doesn't exist yet.