capsule_runtime/
records_dir.rs1use std::path::{Component, Path, PathBuf};
27
28use crate::murmur_home::HomeUnusable;
29use crate::protected_paths::canonicalize_existing_prefix;
30use crate::workdir_fs::WorkdirRoot;
31
32pub const RECORDS_DIR_NAME: &str = "records";
34
35pub const RECORDS_DIR_ENV: &str = "MURMUR_RECORDS_DIR";
39
40pub const HOOKS_SUBDIR: &str = "hooks";
43
44const PRIVATE_DIR_MODE: u32 = crate::murmur_home::MURMUR_HOME_DIR_MODE;
46
47pub fn records_dir_root() -> Result<PathBuf, HomeUnusable> {
49 Ok(crate::murmur_home::usable_murmur_home()?.join(RECORDS_DIR_NAME))
50}
51
52pub fn records_group(workdir_root: &Path) -> Result<PathBuf, HomeUnusable> {
60 Ok(records_group_under(
61 &crate::murmur_home::usable_murmur_home()?,
62 workdir_root,
63 ))
64}
65
66pub fn records_group_under(murmur_home: &Path, workdir_root: &Path) -> PathBuf {
69 murmur_home
70 .join(RECORDS_DIR_NAME)
71 .join(mirrored(workdir_root))
72}
73
74fn mirrored(workdir_root: &Path) -> PathBuf {
78 let absolute = std::path::absolute(workdir_root).unwrap_or_else(|_| workdir_root.to_path_buf());
79 let canonical = canonicalize_existing_prefix(&absolute);
80 let mut mirror = PathBuf::new();
81 for component in canonical.components() {
82 match component {
83 Component::Normal(name) => mirror.push(name),
84 Component::ParentDir => {
85 mirror.pop();
86 }
87 Component::RootDir | Component::CurDir | Component::Prefix(_) => {}
88 }
89 }
90 mirror
91}
92
93#[derive(Debug, Clone)]
95pub struct RecordsDir {
96 path: PathBuf,
98 root: WorkdirRoot,
100}
101
102impl RecordsDir {
103 pub fn create(group: &Path, session_id: &str, latch_of: &WorkdirRoot) -> Result<Self, String> {
110 let located = |path: &Path, reason: String| format!("{}: {reason}", path.display());
111 let home = crate::murmur_home::ensure_murmur_home()?;
112 let records = home.join(RECORDS_DIR_NAME);
113 let rel = group
114 .strip_prefix(&records)
115 .map_err(|_| located(group, format!("is not beneath {}", records.display())))?
116 .join(session_id);
117 crate::murmur_home::hold_private_dir(&records)
118 .map_err(|reason| located(&records, reason))?;
119 let records_tree = WorkdirRoot::open(&records).map_err(|error| error.to_string())?;
120 records_tree
121 .create_dir_all(&rel, PRIVATE_DIR_MODE)
122 .map_err(|error| error.to_string())?;
123 let rel_text = rel.to_string_lossy();
124 let walked = records_tree
125 .open_beneath(&rel_text)
126 .map_err(|error| error.to_string())?
127 .ok_or_else(|| located(&records.join(&rel), "vanished after its creation".into()))?;
128 let path = records.join(&rel);
129 let root = WorkdirRoot::open_sharing_latch_absolute(&path, latch_of)
130 .map_err(|error| error.to_string())?;
131 if root.identity().map_err(|error| error.to_string())?
133 != walked.identity().map_err(|error| error.to_string())?
134 {
135 return Err(located(
136 &path,
137 "changed between its creation and its open".to_string(),
138 ));
139 }
140 Ok(Self { path, root })
141 }
142
143 #[must_use]
145 pub fn path(&self) -> &Path {
146 &self.path
147 }
148
149 #[must_use]
151 pub fn root(&self) -> &WorkdirRoot {
152 &self.root
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn records_dir_mirror_drops_the_root_and_every_dot_segment() {
162 let temp = tempfile::tempdir().unwrap();
163 let real = temp.path().canonicalize().unwrap();
164 let mirror = mirrored(&real.join("proj/./workdir/../workdir"));
165 assert_eq!(
166 mirror,
167 real.strip_prefix("/").unwrap().join("proj").join("workdir")
168 );
169 assert!(mirror.is_relative());
170 assert_eq!(mirrored(Path::new("/../../etc")), PathBuf::from("etc"));
171 }
172
173 #[test]
174 fn records_dir_mirror_resolves_a_link_above_the_workdir_root() {
175 let temp = tempfile::tempdir().unwrap();
176 let real = temp.path().join("real");
177 std::fs::create_dir_all(&real).unwrap();
178 let link = temp.path().join("link");
179 std::os::unix::fs::symlink(&real, &link).unwrap();
180 assert_eq!(
181 mirrored(&link.join("workdir")),
182 mirrored(&real.join("workdir"))
183 );
184 }
185
186 #[test]
189 fn records_dir_is_created_owner_only_beneath_the_murmur_home() {
190 let home = tempfile::tempdir().unwrap();
191 crate::murmur_home::run_with_home(
192 "records_dir::tests::records_dir_create_inner",
193 home.path(),
194 );
195 }
196
197 #[test]
198 #[ignore = "run by records_dir_is_created_owner_only_beneath_the_murmur_home"]
199 fn records_dir_create_inner() {
200 if !crate::murmur_home::in_scratch_home() {
201 return;
202 }
203 let home = PathBuf::from(std::env::var_os("HOME").unwrap());
204 let project = home.join("proj");
205 std::fs::create_dir_all(&project).unwrap();
206 let group = records_group(&project.join("workdir")).unwrap();
207 assert_eq!(
208 group,
209 home.join(".murmur/records")
210 .join(mirrored(&project.join("workdir")))
211 );
212 let accessible = WorkdirRoot::open(&project).unwrap();
213 let created = RecordsDir::create(&group, "ses_0001", &accessible).unwrap();
214 assert_eq!(created.path(), group.join("ses_0001"));
215 let mut level = created.path().to_path_buf();
216 while level != home.join(".murmur") {
217 assert_eq!(
218 crate::murmur_home::mode_of(&level),
219 0o700,
220 "{}",
221 level.display()
222 );
223 level.pop();
224 }
225 created
226 .root()
227 .write_file("trace.jsonl", b"{}\n", crate::workdir_fs::DEFAULT_FILE_MODE)
228 .unwrap();
229
230 let elsewhere = home.join("elsewhere");
231 std::fs::create_dir_all(&elsewhere).unwrap();
232 let planted_group = records_group(&home.join("planted/workdir")).unwrap();
233 std::fs::create_dir_all(planted_group.parent().unwrap()).unwrap();
234 std::os::unix::fs::symlink(&elsewhere, &planted_group).unwrap();
235 assert!(RecordsDir::create(&planted_group, "ses_0002", &accessible).is_err());
236 assert!(!elsewhere.join("ses_0002").exists());
237 }
238}