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