1212use std:: io;
1313use std:: path:: Path ;
1414
15- /// Write `contents` to `path` atomically with `file_mode`, ensuring the
16- /// parent directory exists and is set to `dir_mode`.
15+ /// Unix mode bits for a file (e.g. `0o600`).
16+ ///
17+ /// A distinct newtype from [`DirMode`] so the file- and directory-mode
18+ /// arguments of [`write_atomic_restricted`] cannot be transposed: passing
19+ /// them in the wrong order is a compile error rather than a silent
20+ /// security regression (a secrets file landing at `0o700`, say).
21+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
22+ pub struct FileMode ( pub u32 ) ;
23+
24+ /// Unix mode bits for a directory (e.g. `0o700`).
25+ ///
26+ /// See [`FileMode`] for why this is a distinct newtype.
27+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
28+ pub struct DirMode ( pub u32 ) ;
29+
30+ /// Write `contents` to `path` atomically with `file`, ensuring the
31+ /// parent directory exists and is set to `dir`.
1732///
1833/// # Errors
1934///
2035/// Returns an error if `path` has no parent or no file-name component,
21- /// the parent directory cannot be created or chmod'd to `dir_mode `, the
22- /// tempfile cannot be opened with `file_mode ` or written, or the final
36+ /// the parent directory cannot be created or chmod'd to `dir `, the
37+ /// tempfile cannot be opened with `file ` or written, or the final
2338/// `rename` over `path` fails.
2439pub async fn write_atomic_restricted (
2540 path : impl AsRef < Path > ,
2641 contents : impl AsRef < [ u8 ] > ,
27- file_mode : u32 ,
28- dir_mode : u32 ,
42+ file : FileMode ,
43+ dir : DirMode ,
2944) -> io:: Result < ( ) > {
3045 let path = path. as_ref ( ) . to_owned ( ) ;
3146 let contents = contents. as_ref ( ) . to_vec ( ) ;
@@ -40,7 +55,7 @@ pub async fn write_atomic_restricted(
4055 } ) ?
4156 . to_owned ( ) ;
4257
43- create_dir_with_mode ( & parent, dir_mode ) . await ?;
58+ create_dir_with_mode ( & parent, dir . 0 ) . await ?;
4459
4560 let file_name = path
4661 . file_name ( )
@@ -55,7 +70,7 @@ pub async fn write_atomic_restricted(
5570 tmp_name. push ( format ! ( ".tmp.{}" , std:: process:: id( ) ) ) ;
5671 let tmp_path = parent. join ( & tmp_name) ;
5772
58- write_file_with_mode ( & tmp_path, & contents, file_mode ) . await ?;
73+ write_file_with_mode ( & tmp_path, & contents, file . 0 ) . await ?;
5974
6075 let rename_result = atomic_rename_over ( & tmp_path, & path) . await ;
6176 if rename_result. is_err ( ) {
@@ -189,6 +204,7 @@ async fn write_file_with_mode(path: &Path, contents: &[u8], mode: u32) -> io::Re
189204/// `tokio::task::block_in_place`. Safe to call from sync contexts
190205/// that run inside a tokio runtime (e.g. extism `host_fn` callbacks).
191206pub mod blocking {
207+ use super :: { DirMode , FileMode } ;
192208 use std:: io;
193209 use std:: path:: Path ;
194210
@@ -203,18 +219,16 @@ pub mod blocking {
203219 /// # Errors
204220 ///
205221 /// Returns an error if `path` has no parent or no file-name component,
206- /// the parent directory cannot be created or chmod'd to `dir_mode `, the
207- /// tempfile cannot be opened with `file_mode ` or written, or the final
222+ /// the parent directory cannot be created or chmod'd to `dir `, the
223+ /// tempfile cannot be opened with `file ` or written, or the final
208224 /// `rename` over `path` fails.
209225 pub fn write_atomic_restricted (
210226 path : impl AsRef < Path > ,
211227 contents : impl AsRef < [ u8 ] > ,
212- file_mode : u32 ,
213- dir_mode : u32 ,
228+ file : FileMode ,
229+ dir : DirMode ,
214230 ) -> io:: Result < ( ) > {
215- block_on ( super :: write_atomic_restricted (
216- path, contents, file_mode, dir_mode,
217- ) )
231+ block_on ( super :: write_atomic_restricted ( path, contents, file, dir) )
218232 }
219233
220234 /// Blocking counterpart of [`super::remove_file_if_exists`].
@@ -241,9 +255,14 @@ mod tests {
241255 let dir = tmp. path ( ) . join ( "hm" ) ;
242256 let file = dir. join ( "credentials.toml" ) ;
243257
244- write_atomic_restricted ( & file, b"token = \" hunter2\" \n " , 0o600 , 0o700 )
245- . await
246- . unwrap ( ) ;
258+ write_atomic_restricted (
259+ & file,
260+ b"token = \" hunter2\" \n " ,
261+ FileMode ( 0o600 ) ,
262+ DirMode ( 0o700 ) ,
263+ )
264+ . await
265+ . unwrap ( ) ;
247266
248267 let fmode = std:: fs:: metadata ( & file) . unwrap ( ) . permissions ( ) . mode ( ) & 0o777 ;
249268 assert_eq ! ( fmode, 0o600 , "file mode must be 0o600, got {fmode:o}" ) ;
@@ -257,10 +276,10 @@ mod tests {
257276 async fn rewrite_preserves_0600 ( ) {
258277 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
259278 let file = tmp. path ( ) . join ( "credentials.toml" ) ;
260- write_atomic_restricted ( & file, b"a" , 0o600 , 0o700 )
279+ write_atomic_restricted ( & file, b"a" , FileMode ( 0o600 ) , DirMode ( 0o700 ) )
261280 . await
262281 . unwrap ( ) ;
263- write_atomic_restricted ( & file, b"bb" , 0o600 , 0o700 )
282+ write_atomic_restricted ( & file, b"bb" , FileMode ( 0o600 ) , DirMode ( 0o700 ) )
264283 . await
265284 . unwrap ( ) ;
266285 let fmode = std:: fs:: metadata ( & file) . unwrap ( ) . permissions ( ) . mode ( ) & 0o777 ;
0 commit comments