返回 DeepSeek-TUI-2026
amend.rs
根目录 / crates / tui / src / execpolicy / amend.rs
1 use std::fs::OpenOptions;
2 use std::io::Read;
3 use std::io::Seek;
4 use std::io::SeekFrom;
5 use std::io::Write;
6 use std::path::Path;
7 use std::path::PathBuf;
8
9 use fd_lock::RwLock;
10 use serde_json;
11 use thiserror::Error;
12
13 #[derive(Debug, Error)]
14 pub enum AmendError {
15 #[error("prefix rule requires at least one token")]
16 EmptyPrefix,
17 #[error("policy path has no parent: {path}")]
18 MissingParent { path: PathBuf },
19 #[error("failed to create policy directory {dir}: {source}")]
20 CreatePolicyDir {
21 dir: PathBuf,
22 source: std::io::Error,
23 },
24 #[error("failed to format prefix tokens: {source}")]
25 SerializePrefix { source: serde_json::Error },
26 #[error("failed to open policy file {path}: {source}")]
27 OpenPolicyFile {
28 path: PathBuf,
29 source: std::io::Error,
30 },
31 #[error("failed to write to policy file {path}: {source}")]
32 WritePolicyFile {
33 path: PathBuf,
34 source: std::io::Error,
35 },
36 #[error("failed to lock policy file {path}: {source}")]
37 LockPolicyFile {
38 path: PathBuf,
39 source: std::io::Error,
40 },
41 #[error("failed to seek policy file {path}: {source}")]
42 SeekPolicyFile {
43 path: PathBuf,
44 source: std::io::Error,
45 },
46 #[error("failed to read policy file {path}: {source}")]
47 ReadPolicyFile {
48 path: PathBuf,
49 source: std::io::Error,
50 },
51 #[error("failed to read metadata for policy file {path}: {source}")]
52 PolicyMetadata {
53 path: PathBuf,
54 source: std::io::Error,
55 },
56 }
57
58 /// Note this thread uses advisory file locking and performs blocking I/O, so it should be used with
59 /// [`tokio::task::spawn_blocking`] when called from an async context.
60 pub fn blocking_append_allow_prefix_rule(
61 policy_path: &Path,
62 prefix: &[String],
63 ) -> Result<(), AmendError> {
64 if prefix.is_empty() {
65 return Err(AmendError::EmptyPrefix);
66 }
67
68 let tokens = prefix
69 .iter()
70 .map(serde_json::to_string)
71 .collect::<Result<Vec<_>, _>>()
72 .map_err(|source| AmendError::SerializePrefix { source })?;
73 let pattern = format!("[{}]", tokens.join(", "));
74 let rule = format!(r#"prefix_rule(pattern={pattern}, decision="allow")"#);
75
76 let dir = policy_path
77 .parent()
78 .ok_or_else(|| AmendError::MissingParent {
79 path: policy_path.to_path_buf(),
80 })?;
81 match std::fs::create_dir(dir) {
82 Ok(()) => {}
83 Err(ref source) if source.kind() == std::io::ErrorKind::AlreadyExists => {}
84 Err(source) => {
85 return Err(AmendError::CreatePolicyDir {
86 dir: dir.to_path_buf(),
87 source,
88 });
89 }
90 }
91 append_locked_line(policy_path, &rule)
92 }
93
94 fn append_locked_line(policy_path: &Path, line: &str) -> Result<(), AmendError> {
95 let file = OpenOptions::new()
96 .create(true)
97 .read(true)
98 .append(true)
99 .open(policy_path)
100 .map_err(|source| AmendError::OpenPolicyFile {
101 path: policy_path.to_path_buf(),
102 source,
103 })?;
104 let mut file = RwLock::new(file);
105 let mut file = file.write().map_err(|source| AmendError::LockPolicyFile {
106 path: policy_path.to_path_buf(),
107 source,
108 })?;
109
110 let len = file
111 .metadata()
112 .map_err(|source| AmendError::PolicyMetadata {
113 path: policy_path.to_path_buf(),
114 source,
115 })?
116 .len();
117
118 // Ensure file ends in a newline before appending.
119 if len > 0 {
120 file.seek(SeekFrom::End(-1))
121 .map_err(|source| AmendError::SeekPolicyFile {
122 path: policy_path.to_path_buf(),
123 source,
124 })?;
125 let mut last = [0; 1];
126 file.read_exact(&mut last)
127 .map_err(|source| AmendError::ReadPolicyFile {
128 path: policy_path.to_path_buf(),
129 source,
130 })?;
131
132 if last[0] != b'\n' {
133 file.write_all(b"\n")
134 .map_err(|source| AmendError::WritePolicyFile {
135 path: policy_path.to_path_buf(),
136 source,
137 })?;
138 }
139 }
140
141 file.write_all(format!("{line}\n").as_bytes())
142 .map_err(|source| AmendError::WritePolicyFile {
143 path: policy_path.to_path_buf(),
144 source,
145 })?;
146
147 Ok(())
148 }
149
150 #[cfg(test)]
151 mod tests {
152 use super::*;
153 use pretty_assertions::assert_eq;
154 use tempfile::tempdir;
155
156 #[test]
157 fn appends_rule_and_creates_directories() {
158 let tmp = tempdir().expect("create temp dir");
159 let policy_path = tmp.path().join("rules").join("default.rules");
160
161 blocking_append_allow_prefix_rule(
162 &policy_path,
163 &[String::from("echo"), String::from("Hello, world!")],
164 )
165 .expect("append rule");
166
167 let contents = std::fs::read_to_string(&policy_path).expect("default.rules should exist");
168 assert_eq!(
169 contents,
170 r#"prefix_rule(pattern=["echo", "Hello, world!"], decision="allow")
171 "#
172 );
173 }
174
175 #[test]
176 fn appends_rule_without_duplicate_newline() {
177 let tmp = tempdir().expect("create temp dir");
178 let policy_path = tmp.path().join("rules").join("default.rules");
179 std::fs::create_dir_all(policy_path.parent().unwrap()).expect("create policy dir");
180 std::fs::write(
181 &policy_path,
182 r#"prefix_rule(pattern=["ls"], decision="allow")
183 "#,
184 )
185 .expect("write seed rule");
186
187 blocking_append_allow_prefix_rule(
188 &policy_path,
189 &[String::from("echo"), String::from("Hello, world!")],
190 )
191 .expect("append rule");
192
193 let contents = std::fs::read_to_string(&policy_path).expect("read policy");
194 assert_eq!(
195 contents,
196 r#"prefix_rule(pattern=["ls"], decision="allow")
197 prefix_rule(pattern=["echo", "Hello, world!"], decision="allow")
198 "#
199 );
200 }
201
202 #[test]
203 fn inserts_newline_when_missing_before_append() {
204 let tmp = tempdir().expect("create temp dir");
205 let policy_path = tmp.path().join("rules").join("default.rules");
206 std::fs::create_dir_all(policy_path.parent().unwrap()).expect("create policy dir");
207 std::fs::write(
208 &policy_path,
209 r#"prefix_rule(pattern=["ls"], decision="allow")"#,
210 )
211 .expect("write seed rule without newline");
212
213 blocking_append_allow_prefix_rule(
214 &policy_path,
215 &[String::from("echo"), String::from("Hello, world!")],
216 )
217 .expect("append rule");
218
219 let contents = std::fs::read_to_string(&policy_path).expect("read policy");
220 assert_eq!(
221 contents,
222 r#"prefix_rule(pattern=["ls"], decision="allow")
223 prefix_rule(pattern=["echo", "Hello, world!"], decision="allow")
224 "#
225 );
226 }
227 }
228
228 lines RUST