11use std:: fs:: OpenOptions ;
2+ use std:: io:: Read ;
3+ use std:: io:: Seek ;
4+ use std:: io:: SeekFrom ;
25use std:: io:: Write ;
36use std:: path:: Path ;
47use 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 \n prefix_rule(pattern=[\" echo\" ,\" Hello, world!\" ], decision=\" allow\" )\n "
195+ "prefix_rule(pattern=[\" ls\" ], decision=\" allow\" )\n prefix_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\" )\n prefix_rule(pattern=[\" echo\" ,\" Hello, world!\" ], decision=\" allow\" )\n "
137220 ) ;
138221 }
139222}
0 commit comments