99#[ cfg( test) ]
1010use std:: os:: unix:: ffi:: OsStringExt ;
1111
12- use std:: ffi:: { CStr , CString , OsStr , OsString } ;
12+ use std:: ffi:: { CString , OsStr , OsString } ;
1313use std:: io;
1414use std:: os:: unix:: ffi:: OsStrExt ;
15- use std:: os:: unix:: io:: { AsRawFd , RawFd } ;
15+ use std:: os:: unix:: io:: { AsFd , AsRawFd , BorrowedFd , FromRawFd , OwnedFd , RawFd } ;
1616use std:: path:: Path ;
1717
18+ use nix:: dir:: Dir ;
19+ use nix:: fcntl:: { OFlag , openat} ;
20+ use nix:: sys:: stat:: { FileStat , Mode , fstatat} ;
21+ use nix:: unistd:: { UnlinkatFlags , unlinkat} ;
22+
1823// Custom error types for better error reporting
1924#[ derive( thiserror:: Error , Debug ) ]
2025pub enum SafeTraversalError {
@@ -64,88 +69,48 @@ impl From<SafeTraversalError> for io::Error {
6469 }
6570}
6671
67- // RAII wrapper for DIR pointer
68- struct Dir {
69- dirp : * mut libc:: DIR ,
70- }
71-
72- impl Dir {
73- fn from_fd ( fd : RawFd ) -> io:: Result < Self > {
74- let dirp = unsafe { libc:: fdopendir ( fd) } ;
75- if dirp. is_null ( ) {
76- Err ( io:: Error :: last_os_error ( ) )
77- } else {
78- Ok ( Dir { dirp } )
79- }
80- }
72+ // Helper function to read directory entries using nix
73+ fn read_dir_entries ( fd : & OwnedFd ) -> io:: Result < Vec < OsString > > {
74+ let mut entries = Vec :: new ( ) ;
8175
82- fn read_entries ( & self ) -> io :: Result < Vec < OsString > > {
83- let mut entries = Vec :: new ( ) ;
76+ // Duplicate the fd for Dir (it takes ownership)
77+ let dup_fd = nix :: unistd :: dup ( fd ) . map_err ( |e| io :: Error :: from_raw_os_error ( e as i32 ) ) ? ;
8478
85- loop {
86- // Clear errno before readdir as per POSIX requirements
87- unsafe { * libc:: __errno_location ( ) = 0 } ;
79+ let mut dir = Dir :: from_fd ( dup_fd) . map_err ( |e| io:: Error :: from_raw_os_error ( e as i32 ) ) ?;
8880
89- let entry = unsafe { libc:: readdir ( self . dirp ) } ;
90- if entry. is_null ( ) {
91- let errno = unsafe { * libc:: __errno_location ( ) } ;
92- if errno != 0 {
93- return Err ( io:: Error :: from_raw_os_error ( errno) ) ;
94- }
95- break ;
96- }
81+ for entry_result in dir. iter ( ) {
82+ let entry = entry_result. map_err ( |e| io:: Error :: from_raw_os_error ( e as i32 ) ) ?;
9783
98- let name = unsafe { CStr :: from_ptr ( ( * entry) . d_name . as_ptr ( ) ) } ;
99- let name_os = OsStr :: from_bytes ( name. to_bytes ( ) ) ;
84+ let name = entry. file_name ( ) ;
85+ let name_os = OsStr :: from_bytes ( name. to_bytes ( ) ) ;
10086
101- if name_os != "." && name_os != ".." {
102- entries. push ( name_os. to_os_string ( ) ) ;
103- }
87+ if name_os != "." && name_os != ".." {
88+ entries. push ( name_os. to_os_string ( ) ) ;
10489 }
105-
106- Ok ( entries)
10790 }
108- }
10991
110- impl Drop for Dir {
111- fn drop ( & mut self ) {
112- if !self . dirp . is_null ( ) {
113- unsafe {
114- libc:: closedir ( self . dirp ) ;
115- }
116- }
117- }
92+ Ok ( entries)
11893}
11994
12095/// A directory file descriptor that enables safe traversal
12196pub struct DirFd {
122- fd : RawFd ,
123- owned : bool ,
97+ fd : OwnedFd ,
12498}
12599
126100impl DirFd {
127101 /// Open a directory and return a file descriptor
128102 pub fn open ( path : & Path ) -> io:: Result < Self > {
129103 let path_str = path. to_string_lossy ( ) ;
130- let path_cstr = CString :: new ( path. as_os_str ( ) . as_bytes ( ) )
131- . map_err ( |_| SafeTraversalError :: PathContainsNull ) ?;
132-
133- let fd = unsafe {
134- libc:: open (
135- path_cstr. as_ptr ( ) ,
136- libc:: O_RDONLY | libc:: O_DIRECTORY | libc:: O_CLOEXEC ,
137- )
138- } ;
139104
140- if fd < 0 {
141- Err ( SafeTraversalError :: OpenFailed {
105+ let flags = OFlag :: O_RDONLY | OFlag :: O_DIRECTORY | OFlag :: O_CLOEXEC ;
106+ let fd = nix:: fcntl:: open ( path, flags, Mode :: empty ( ) ) . map_err ( |e| {
107+ SafeTraversalError :: OpenFailed {
142108 path : path_str. to_string ( ) ,
143- source : io:: Error :: last_os_error ( ) ,
109+ source : io:: Error :: from_raw_os_error ( e as i32 ) ,
144110 }
145- . into ( ) )
146- } else {
147- Ok ( DirFd { fd, owned : true } )
148- }
111+ } ) ?;
112+
113+ Ok ( DirFd { fd } )
149114 }
150115
151116 /// Open a subdirectory relative to this directory
@@ -154,49 +119,37 @@ impl DirFd {
154119 let name_cstr =
155120 CString :: new ( name. as_bytes ( ) ) . map_err ( |_| SafeTraversalError :: PathContainsNull ) ?;
156121
157- let fd = unsafe {
158- libc:: openat (
159- self . fd ,
160- name_cstr. as_ptr ( ) ,
161- libc:: O_RDONLY | libc:: O_DIRECTORY | libc:: O_CLOEXEC ,
162- )
163- } ;
164-
165- if fd < 0 {
166- Err ( SafeTraversalError :: OpenFailed {
122+ let flags = OFlag :: O_RDONLY | OFlag :: O_DIRECTORY | OFlag :: O_CLOEXEC ;
123+ let fd = openat ( & self . fd , name_cstr. as_c_str ( ) , flags, Mode :: empty ( ) ) . map_err ( |e| {
124+ SafeTraversalError :: OpenFailed {
167125 path : name_str. to_string ( ) ,
168- source : io:: Error :: last_os_error ( ) ,
126+ source : io:: Error :: from_raw_os_error ( e as i32 ) ,
169127 }
170- . into ( ) )
171- } else {
172- Ok ( DirFd { fd, owned : true } )
173- }
128+ } ) ?;
129+
130+ Ok ( DirFd { fd } )
174131 }
175132
176133 /// Get raw stat data for a file relative to this directory
177- pub fn stat_at ( & self , name : & OsStr , follow_symlinks : bool ) -> io:: Result < libc :: stat > {
134+ pub fn stat_at ( & self , name : & OsStr , follow_symlinks : bool ) -> io:: Result < FileStat > {
178135 let name_str = name. to_string_lossy ( ) ;
179136 let name_cstr =
180137 CString :: new ( name. as_bytes ( ) ) . map_err ( |_| SafeTraversalError :: PathContainsNull ) ?;
181138
182- let mut stat: libc:: stat = unsafe { std:: mem:: zeroed ( ) } ;
183139 let flags = if follow_symlinks {
184- 0
140+ nix :: fcntl :: AtFlags :: empty ( )
185141 } else {
186- libc :: AT_SYMLINK_NOFOLLOW
142+ nix :: fcntl :: AtFlags :: AT_SYMLINK_NOFOLLOW
187143 } ;
188144
189- let ret = unsafe { libc:: fstatat ( self . fd , name_cstr. as_ptr ( ) , & mut stat, flags) } ;
190-
191- if ret < 0 {
192- Err ( SafeTraversalError :: StatFailed {
145+ let stat = fstatat ( & self . fd , name_cstr. as_c_str ( ) , flags) . map_err ( |e| {
146+ SafeTraversalError :: StatFailed {
193147 path : name_str. to_string ( ) ,
194- source : io:: Error :: last_os_error ( ) ,
148+ source : io:: Error :: from_raw_os_error ( e as i32 ) ,
195149 }
196- . into ( ) )
197- } else {
198- Ok ( stat)
199- }
150+ } ) ?;
151+
152+ Ok ( stat)
200153 }
201154
202155 /// Get metadata for a file relative to this directory
@@ -210,43 +163,18 @@ impl DirFd {
210163 }
211164
212165 /// Get raw stat data for this directory
213- pub fn fstat ( & self ) -> io:: Result < libc:: stat > {
214- let mut stat: libc:: stat = unsafe { std:: mem:: zeroed ( ) } ;
215-
216- let ret = unsafe { libc:: fstat ( self . fd , & mut stat) } ;
166+ pub fn fstat ( & self ) -> io:: Result < FileStat > {
167+ let stat = nix:: sys:: stat:: fstat ( & self . fd ) . map_err ( |e| SafeTraversalError :: StatFailed {
168+ path : "<current directory>" . to_string ( ) ,
169+ source : io:: Error :: from_raw_os_error ( e as i32 ) ,
170+ } ) ?;
217171
218- if ret < 0 {
219- Err ( SafeTraversalError :: StatFailed {
220- path : "<current directory>" . to_string ( ) ,
221- source : io:: Error :: last_os_error ( ) ,
222- }
223- . into ( ) )
224- } else {
225- Ok ( stat)
226- }
172+ Ok ( stat)
227173 }
228174
229175 /// Read directory entries
230176 pub fn read_dir ( & self ) -> io:: Result < Vec < OsString > > {
231- // Duplicate the fd for fdopendir (it takes ownership)
232- let dup_fd = unsafe { libc:: dup ( self . fd ) } ;
233- if dup_fd < 0 {
234- return Err ( SafeTraversalError :: ReadDirFailed {
235- path : "<directory>" . to_string ( ) ,
236- source : io:: Error :: last_os_error ( ) ,
237- }
238- . into ( ) ) ;
239- }
240-
241- let dir = Dir :: from_fd ( dup_fd) . map_err ( |e| {
242- unsafe { libc:: close ( dup_fd) } ;
243- SafeTraversalError :: ReadDirFailed {
244- path : "<directory>" . to_string ( ) ,
245- source : e,
246- }
247- } ) ?;
248-
249- dir. read_entries ( ) . map_err ( |e| {
177+ read_dir_entries ( & self . fd ) . map_err ( |e| {
250178 SafeTraversalError :: ReadDirFailed {
251179 path : "<directory>" . to_string ( ) ,
252180 source : e,
@@ -260,46 +188,45 @@ impl DirFd {
260188 let name_str = name. to_string_lossy ( ) ;
261189 let name_cstr =
262190 CString :: new ( name. as_bytes ( ) ) . map_err ( |_| SafeTraversalError :: PathContainsNull ) ?;
263- let flags = if is_dir { libc:: AT_REMOVEDIR } else { 0 } ;
264-
265- let ret = unsafe { libc:: unlinkat ( self . fd , name_cstr. as_ptr ( ) , flags) } ;
191+ let flags = if is_dir {
192+ UnlinkatFlags :: RemoveDir
193+ } else {
194+ UnlinkatFlags :: NoRemoveDir
195+ } ;
266196
267- if ret < 0 {
268- Err ( SafeTraversalError :: UnlinkFailed {
197+ unlinkat ( & self . fd , name_cstr . as_c_str ( ) , flags ) . map_err ( |e| {
198+ SafeTraversalError :: UnlinkFailed {
269199 path : name_str. to_string ( ) ,
270- source : io:: Error :: last_os_error ( ) ,
200+ source : io:: Error :: from_raw_os_error ( e as i32 ) ,
271201 }
272- . into ( ) )
273- } else {
274- Ok ( ( ) )
275- }
202+ } ) ?;
203+
204+ Ok ( ( ) )
276205 }
277206
278- /// Create a DirFd from an existing file descriptor (does not take ownership)
207+ /// Create a DirFd from an existing file descriptor (takes ownership)
279208 pub fn from_raw_fd ( fd : RawFd ) -> io:: Result < Self > {
280209 if fd < 0 {
281210 return Err ( io:: Error :: new (
282211 io:: ErrorKind :: InvalidInput ,
283212 "invalid file descriptor" ,
284213 ) ) ;
285214 }
286- Ok ( DirFd { fd, owned : false } )
215+ // SAFETY: We've verified fd >= 0, and the caller is transferring ownership
216+ let owned_fd = unsafe { OwnedFd :: from_raw_fd ( fd) } ;
217+ Ok ( DirFd { fd : owned_fd } )
287218 }
288219}
289220
290- impl Drop for DirFd {
291- fn drop ( & mut self ) {
292- if self . owned && self . fd >= 0 {
293- unsafe {
294- libc:: close ( self . fd ) ;
295- }
296- }
221+ impl AsRawFd for DirFd {
222+ fn as_raw_fd ( & self ) -> RawFd {
223+ self . fd . as_raw_fd ( )
297224 }
298225}
299226
300- impl AsRawFd for DirFd {
301- fn as_raw_fd ( & self ) -> RawFd {
302- self . fd
227+ impl AsFd for DirFd {
228+ fn as_fd ( & self ) -> BorrowedFd < ' _ > {
229+ self . fd . as_fd ( )
303230 }
304231}
305232
@@ -371,11 +298,11 @@ impl FileType {
371298/// Metadata wrapper for safer access to file information
372299#[ derive( Debug , Clone ) ]
373300pub struct Metadata {
374- stat : libc :: stat ,
301+ stat : FileStat ,
375302}
376303
377304impl Metadata {
378- pub fn from_stat ( stat : libc :: stat ) -> Self {
305+ pub fn from_stat ( stat : FileStat ) -> Self {
379306 Self { stat }
380307 }
381308
@@ -407,11 +334,6 @@ impl Metadata {
407334 }
408335 }
409336
410- /// Get the raw libc::stat for compatibility with existing code
411- pub fn as_raw_stat ( & self ) -> & libc:: stat {
412- & self . stat
413- }
414-
415337 /// Compatibility methods to match std::fs::Metadata interface
416338 pub fn is_dir ( & self ) -> bool {
417339 self . file_type ( ) . is_directory ( )
@@ -556,6 +478,7 @@ mod tests {
556478 use super :: * ;
557479 use std:: fs;
558480 use std:: os:: unix:: fs:: symlink;
481+ use std:: os:: unix:: io:: IntoRawFd ;
559482 use tempfile:: TempDir ;
560483
561484 #[ test]
@@ -694,11 +617,16 @@ mod tests {
694617 fn test_from_raw_fd ( ) {
695618 let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
696619 let dir_fd = DirFd :: open ( temp_dir. path ( ) ) . unwrap ( ) ;
697- let raw_fd = dir_fd. as_raw_fd ( ) ;
698620
699- let borrowed_fd = DirFd :: from_raw_fd ( raw_fd) . unwrap ( ) ;
700- assert_eq ! ( borrowed_fd. as_raw_fd( ) , raw_fd) ;
701- assert ! ( !borrowed_fd. owned) ; // Should not own the FD
621+ // Duplicate the fd first so we don't have ownership conflicts
622+ let dup_fd = nix:: unistd:: dup ( & dir_fd) . unwrap ( ) ;
623+ let from_raw_fd = DirFd :: from_raw_fd ( dup_fd. into_raw_fd ( ) ) . unwrap ( ) ;
624+
625+ // Both should refer to the same directory
626+ let stat1 = dir_fd. fstat ( ) . unwrap ( ) ;
627+ let stat2 = from_raw_fd. fstat ( ) . unwrap ( ) ;
628+ assert_eq ! ( stat1. st_ino, stat2. st_ino) ;
629+ assert_eq ! ( stat1. st_dev, stat2. st_dev) ;
702630 }
703631
704632 #[ test]
@@ -770,9 +698,7 @@ mod tests {
770698 assert_eq ! ( metadata. mode( ) & libc:: S_IFMT as u32 , libc:: S_IFREG as u32 ) ;
771699 assert_eq ! ( metadata. nlink( ) , 1 ) ;
772700
773- // Test raw stat access
774- let raw_stat = metadata. as_raw_stat ( ) ;
775- assert_eq ! ( raw_stat. st_size, metadata. size( ) as i64 ) ;
701+ assert ! ( metadata. size( ) > 0 ) ;
776702 }
777703
778704 #[ test]
0 commit comments