Skip to content

Commit e896cf5

Browse files
committed
feat: advisory locks in amend
1 parent b544a5b commit e896cf5

File tree

1 file changed

+99
-16
lines changed

1 file changed

+99
-16
lines changed

codex-rs/execpolicy/src/amend.rs

Lines changed: 99 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
use std::fs::OpenOptions;
2+
use std::io::Read;
3+
use std::io::Seek;
4+
use std::io::SeekFrom;
25
use std::io::Write;
36
use std::path::Path;
47
use std::path::PathBuf;
@@ -29,6 +32,26 @@ pub enum AmendError {
2932
path: PathBuf,
3033
source: std::io::Error,
3134
},
35+
#[error("failed to lock policy file {path}: {source}")]
36+
LockPolicyFile {
37+
path: PathBuf,
38+
source: std::io::Error,
39+
},
40+
#[error("failed to unlock policy file {path}: {source}")]
41+
UnlockPolicyFile {
42+
path: PathBuf,
43+
source: std::io::Error,
44+
},
45+
#[error("failed to seek policy file {path}: {source}")]
46+
SeekPolicyFile {
47+
path: PathBuf,
48+
source: std::io::Error,
49+
},
50+
#[error("failed to read policy file {path}: {source}")]
51+
ReadPolicyFile {
52+
path: PathBuf,
53+
source: std::io::Error,
54+
},
3255
#[error("failed to read metadata for policy file {path}: {source}")]
3356
PolicyMetadata {
3457
path: PathBuf,
@@ -43,7 +66,7 @@ pub fn append_allow_prefix_rule(policy_path: &Path, prefix: &[String]) -> Result
4366

4467
let pattern =
4568
serde_json::to_string(prefix).map_err(|source| AmendError::SerializePrefix { source })?;
46-
let rule = format!("prefix_rule(pattern={pattern}, decision=\"allow\")\n");
69+
let rule = format!("prefix_rule(pattern={pattern}, decision=\"allow\")");
4770

4871
let dir = policy_path
4972
.parent()
@@ -60,30 +83,66 @@ pub fn append_allow_prefix_rule(policy_path: &Path, prefix: &[String]) -> Result
6083
});
6184
}
6285
}
86+
append_locked_line(policy_path, &rule)
87+
}
88+
89+
fn append_locked_line(policy_path: &Path, line: &str) -> Result<(), AmendError> {
90+
let policy_path = policy_path.to_path_buf();
6391
let mut file = OpenOptions::new()
6492
.create(true)
93+
.read(true)
6594
.append(true)
66-
.open(policy_path)
95+
.open(&policy_path)
6796
.map_err(|source| AmendError::OpenPolicyFile {
68-
path: policy_path.to_path_buf(),
97+
path: policy_path.clone(),
98+
source,
99+
})?;
100+
file.lock()
101+
.map_err(|source| AmendError::LockPolicyFile {
102+
path: policy_path.clone(),
69103
source,
70104
})?;
71-
let needs_newline = file
105+
106+
let len = file
72107
.metadata()
73-
.map(|metadata| metadata.len() > 0)
74108
.map_err(|source| AmendError::PolicyMetadata {
75-
path: policy_path.to_path_buf(),
109+
path: policy_path.clone(),
76110
source,
77-
})?;
78-
let final_rule = if needs_newline {
79-
format!("\n{rule}")
80-
} else {
81-
rule
82-
};
111+
})?
112+
.len();
113+
114+
if len > 0 {
115+
file.seek(SeekFrom::End(-1))
116+
.map_err(|source| AmendError::SeekPolicyFile {
117+
path: policy_path.clone(),
118+
source,
119+
})?;
120+
let mut last = [0; 1];
121+
file.read_exact(&mut last)
122+
.map_err(|source| AmendError::ReadPolicyFile {
123+
path: policy_path.clone(),
124+
source,
125+
})?;
126+
127+
if last[0] != b'\n' {
128+
file.write_all(b"\n")
129+
.map_err(|source| AmendError::WritePolicyFile {
130+
path: policy_path.clone(),
131+
source,
132+
})?;
133+
}
134+
}
83135

84-
file.write_all(final_rule.as_bytes())
136+
file.write_all(line.as_bytes())
137+
.and_then(|()| file.write_all(b"\n"))
85138
.map_err(|source| AmendError::WritePolicyFile {
86-
path: policy_path.to_path_buf(),
139+
path: policy_path.clone(),
140+
source,
141+
})?;
142+
143+
file.unlock()
144+
.map_err(|source| AmendError::UnlockPolicyFile {
145+
path: policy_path,
87146
source,
88147
})
89148
}
@@ -114,7 +173,7 @@ mod tests {
114173
}
115174

116175
#[test]
117-
fn separates_rules_with_newlines_when_appending() {
176+
fn appends_rule_without_duplicate_newline() {
118177
let tmp = tempdir().expect("create temp dir");
119178
let policy_path = tmp.path().join("policy").join("default.codexpolicy");
120179
std::fs::create_dir_all(policy_path.parent().unwrap()).expect("create policy dir");
@@ -133,7 +192,31 @@ mod tests {
133192
let contents = std::fs::read_to_string(&policy_path).expect("read policy");
134193
assert_eq!(
135194
contents,
136-
"prefix_rule(pattern=[\"ls\"], decision=\"allow\")\n\nprefix_rule(pattern=[\"echo\",\"Hello, world!\"], decision=\"allow\")\n"
195+
"prefix_rule(pattern=[\"ls\"], decision=\"allow\")\nprefix_rule(pattern=[\"echo\",\"Hello, world!\"], decision=\"allow\")\n"
196+
);
197+
}
198+
199+
#[test]
200+
fn inserts_newline_when_missing_before_append() {
201+
let tmp = tempdir().expect("create temp dir");
202+
let policy_path = tmp.path().join("policy").join("default.codexpolicy");
203+
std::fs::create_dir_all(policy_path.parent().unwrap()).expect("create policy dir");
204+
std::fs::write(
205+
&policy_path,
206+
"prefix_rule(pattern=[\"ls\"], decision=\"allow\")",
207+
)
208+
.expect("write seed rule without newline");
209+
210+
append_allow_prefix_rule(
211+
&policy_path,
212+
&[String::from("echo"), String::from("Hello, world!")],
213+
)
214+
.expect("append rule");
215+
216+
let contents = std::fs::read_to_string(&policy_path).expect("read policy");
217+
assert_eq!(
218+
contents,
219+
"prefix_rule(pattern=[\"ls\"], decision=\"allow\")\nprefix_rule(pattern=[\"echo\",\"Hello, world!\"], decision=\"allow\")\n"
137220
);
138221
}
139222
}

0 commit comments

Comments
 (0)