1use std::{
4 fs::OpenOptions,
5 io::{ErrorKind, Write},
6 path::Path,
7 time::{SystemTime, UNIX_EPOCH},
8};
9
10use sim_kernel::{Cx, Error, Expr, Result, Symbol};
11
12use crate::{
13 FsDir,
14 capabilities::{require_table_fs_edit, require_table_fs_read, require_table_fs_write},
15};
16
17impl FsDir {
18 pub fn edit(
22 &self,
23 cx: &mut Cx,
24 key: Symbol,
25 old: &str,
26 new: &str,
27 replace_all: bool,
28 ) -> Result<()> {
29 self.edit_text_leaf(cx, key, |text| apply_edit(text, old, new, replace_all))
30 }
31
32 pub fn edit_lines(
36 &self,
37 cx: &mut Cx,
38 key: Symbol,
39 start: usize,
40 end: usize,
41 new: &str,
42 ) -> Result<()> {
43 self.edit_text_leaf(cx, key, |text| apply_edit_lines(text, start, end, new))
44 }
45
46 fn edit_text_leaf<F>(&self, cx: &mut Cx, key: Symbol, edit: F) -> Result<()>
47 where
48 F: FnOnce(&str) -> Result<String>,
49 {
50 require_table_fs_read(cx)?;
51 require_table_fs_write(cx)?;
52 require_table_fs_edit(cx)?;
53
54 let (path, ext, expr) = self.read_leaf_expr(cx, &key)?;
55 let Expr::String(text) = expr else {
56 return Err(Error::Eval(format!(
57 "table/fs: dir/edit expects string leaf at {key}"
58 )));
59 };
60 let edited = edit(&text)?;
61 let bytes = FsDir::encode_leaf_expr(cx, ext, &Expr::String(edited))?;
62 self.atomic_write_leaf(&path, &bytes)
63 }
64
65 fn atomic_write_leaf(&self, path: &Path, bytes: &[u8]) -> Result<()> {
66 self.ensure_internal_path(path)?;
67 let parent = path
68 .parent()
69 .ok_or_else(|| Error::Eval("table/fs: edit target has no parent".to_owned()))?;
70 let file_name = path
71 .file_name()
72 .and_then(|name| name.to_str())
73 .ok_or_else(|| Error::Eval("table/fs: edit target has invalid name".to_owned()))?;
74 for attempt in 0..32 {
75 let temp_name = format!(
76 ".{file_name}.edit-{}-{}-{attempt}.tmp",
77 std::process::id(),
78 unique_nanos()
79 );
80 let temp_path = parent.join(temp_name);
81 self.ensure_internal_path(&temp_path)?;
82 match OpenOptions::new()
83 .write(true)
84 .create_new(true)
85 .open(&temp_path)
86 {
87 Ok(mut file) => {
88 if let Err(err) = file.write_all(bytes) {
89 let _ = std::fs::remove_file(&temp_path);
90 return Err(Error::Eval(format!("table/fs: edit write {err}")));
91 }
92 if let Err(err) = file.sync_all() {
93 let _ = std::fs::remove_file(&temp_path);
94 return Err(Error::Eval(format!("table/fs: edit sync {err}")));
95 }
96 drop(file);
97 if let Err(err) = std::fs::rename(&temp_path, path) {
98 let _ = std::fs::remove_file(&temp_path);
99 return Err(Error::Eval(format!("table/fs: edit rename {err}")));
100 }
101 return Ok(());
102 }
103 Err(err) if err.kind() == ErrorKind::AlreadyExists => continue,
104 Err(err) => return Err(Error::Eval(format!("table/fs: edit temp {err}"))),
105 }
106 }
107 Err(Error::Eval(
108 "table/fs: edit could not create a temporary file".to_owned(),
109 ))
110 }
111}
112
113fn unique_nanos() -> u128 {
114 SystemTime::now()
115 .duration_since(UNIX_EPOCH)
116 .unwrap_or_default()
117 .as_nanos()
118}
119
120fn apply_edit(text: &str, old: &str, new: &str, replace_all: bool) -> Result<String> {
121 if old.is_empty() {
122 return Err(Error::Eval("edit: old pattern is empty".to_owned()));
123 }
124 let matches = text.matches(old).count();
125 match matches {
126 0 => Err(Error::Eval(format!("edit: pattern not found: {old:?}"))),
127 n if n > 1 && !replace_all => Err(Error::Eval(format!(
128 "edit: pattern is not unique ({n} matches); pass replace_all"
129 ))),
130 _ if replace_all => Ok(text.replace(old, new)),
131 _ => Ok(text.replacen(old, new, 1)),
132 }
133}
134
135fn apply_edit_lines(text: &str, start: usize, end: usize, new: &str) -> Result<String> {
136 if start == 0 {
137 return Err(Error::Eval(
138 "edit-lines: start must be at least 1".to_owned(),
139 ));
140 }
141 if end < start {
142 return Err(Error::Eval(
143 "edit-lines: end must be greater than or equal to start".to_owned(),
144 ));
145 }
146 let lines = text.split_inclusive('\n').collect::<Vec<_>>();
147 if end > lines.len() {
148 return Err(Error::Eval(format!(
149 "edit-lines: range {start}..{end} exceeds {} line(s)",
150 lines.len()
151 )));
152 }
153 let mut edited = String::new();
154 for line in &lines[..start - 1] {
155 edited.push_str(line);
156 }
157 edited.push_str(new);
158 for line in &lines[end..] {
159 edited.push_str(line);
160 }
161 Ok(edited)
162}