harn_hostlib/fs/
safe_text_patch_lock.rs1use std::cell::RefCell;
4use std::fs::{self as stdfs, File, OpenOptions};
5use std::path::{Path, PathBuf};
6
7use fs2::FileExt;
8use sha2::{Digest, Sha256};
9
10use crate::error::HostlibError;
11
12use super::{normalize_logical, SAFE_TEXT_PATCH_BUILTIN};
13
14thread_local! {
15 static EXECUTION_LOCK_ROOT: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
16}
17
18#[derive(Debug)]
20#[must_use = "retain this guard for the execution that owns the lock root"]
21pub struct ScopedSafeTextPatchLockRoot {
22 previous: Option<PathBuf>,
23}
24
25pub fn scope_safe_text_patch_lock_root(root: impl AsRef<Path>) -> ScopedSafeTextPatchLockRoot {
32 let root = root.as_ref().to_path_buf();
33 let previous = EXECUTION_LOCK_ROOT.with(|slot| slot.replace(Some(root)));
34 ScopedSafeTextPatchLockRoot { previous }
35}
36
37impl Drop for ScopedSafeTextPatchLockRoot {
38 fn drop(&mut self) {
39 EXECUTION_LOCK_ROOT.with(|slot| {
40 slot.replace(self.previous.take());
41 });
42 }
43}
44
45pub(super) fn safe_text_patch_lock_root() -> PathBuf {
46 if let Some(root) = EXECUTION_LOCK_ROOT.with(|slot| slot.borrow().clone()) {
47 return root;
48 }
49 let runtime_root = harn_vm::stdlib::process::runtime_root_base();
50 harn_vm::runtime_paths::state_root(&runtime_root).join("fs-cas-locks")
51}
52
53pub(super) fn acquire_safe_text_patch_lock(
54 path: &Path,
55 lock_root: &Path,
56) -> Result<SafeTextPatchLock, HostlibError> {
57 let file = open_safe_text_patch_lock(path, lock_root)?;
58 file.lock_exclusive()
59 .map_err(|error| HostlibError::Backend {
60 builtin: SAFE_TEXT_PATCH_BUILTIN,
61 message: format!("acquire CAS lock for `{}`: {error}", path.display()),
62 })?;
63 Ok(SafeTextPatchLock { file })
64}
65
66pub(super) fn open_safe_text_patch_lock(
67 path: &Path,
68 lock_root: &Path,
69) -> Result<File, HostlibError> {
70 stdfs::create_dir_all(lock_root).map_err(|error| HostlibError::Backend {
71 builtin: SAFE_TEXT_PATCH_BUILTIN,
72 message: format!(
73 "create CAS lock directory `{}`: {error}",
74 lock_root.display()
75 ),
76 })?;
77 let identity = canonical_lock_identity(path);
78 let lock_name = format!(
79 "{}.lock",
80 hex::encode(Sha256::digest(lock_identity_bytes(&identity)))
81 );
82 let lock_path = lock_root.join(lock_name);
83 OpenOptions::new()
84 .create(true)
85 .truncate(false)
86 .read(true)
87 .write(true)
88 .open(&lock_path)
89 .map_err(|error| HostlibError::Backend {
90 builtin: SAFE_TEXT_PATCH_BUILTIN,
91 message: format!("open CAS lock `{}`: {error}", lock_path.display()),
92 })
93}
94
95fn lock_identity_bytes(identity: &Path) -> Vec<u8> {
96 #[cfg(any(target_os = "macos", target_os = "windows"))]
97 {
98 identity.to_string_lossy().to_lowercase().into_bytes()
103 }
104 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
105 {
106 identity.as_os_str().as_encoded_bytes().to_vec()
107 }
108}
109
110fn canonical_lock_identity(path: &Path) -> PathBuf {
111 if let Ok(canonical) = stdfs::canonicalize(path) {
112 return canonical;
113 }
114 let absolute = normalize_logical(path);
115 let mut ancestor = absolute.as_path();
116 let mut suffix = Vec::new();
117 while let Some(name) = ancestor.file_name() {
118 suffix.push(name.to_os_string());
119 let Some(parent) = ancestor.parent() else {
120 break;
121 };
122 if let Ok(canonical_parent) = stdfs::canonicalize(parent) {
123 let mut identity = canonical_parent;
124 for component in suffix.iter().rev() {
125 identity.push(component);
126 }
127 return identity;
128 }
129 ancestor = parent;
130 }
131 absolute
132}
133
134pub(super) struct SafeTextPatchLock {
135 file: File,
136}
137
138impl Drop for SafeTextPatchLock {
139 fn drop(&mut self) {
140 let _ = FileExt::unlock(&self.file);
143 }
144}