1 use std::fs::{self, File};
2 use std::os::unix::fs::symlink;
3 use std::os::unix::prelude::AsRawFd;
4 use std::time::{Duration, UNIX_EPOCH};
5
6 #[cfg(not(any(target_os = "netbsd")))]
7 use libc::{S_IFMT, S_IFLNK};
8
9 use nix::fcntl;
10 use nix::sys::stat::{self, fchmod, fchmodat, futimens, stat, utimes, utimensat};
11 #[cfg(any(target_os = "linux",
12 target_os = "haiku",
13 target_os = "ios",
14 target_os = "macos",
15 target_os = "freebsd",
16 target_os = "netbsd"))]
17 use nix::sys::stat::lutimes;
18 use nix::sys::stat::{Mode, FchmodatFlags, UtimensatFlags};
19
20 #[cfg(not(any(target_os = "netbsd")))]
21 use nix::sys::stat::FileStat;
22
23 use nix::sys::time::{TimeSpec, TimeVal, TimeValLike};
24 use nix::unistd::chdir;
25
26 #[cfg(not(any(target_os = "netbsd")))]
27 use nix::Result;
28 use tempfile;
29
30 #[allow(unused_comparisons)]
31 // uid and gid are signed on Windows, but not on other platforms. This function
32 // allows warning free compiles on all platforms, and can be removed when
33 // expression-level #[allow] is available.
34 #[cfg(not(any(target_os = "netbsd")))]
valid_uid_gid(stat: FileStat) -> bool35 fn valid_uid_gid(stat: FileStat) -> bool {
36 // uid could be 0 for the `root` user. This quite possible when
37 // the tests are being run on a rooted Android device.
38 stat.st_uid >= 0 && stat.st_gid >= 0
39 }
40
41 #[cfg(not(any(target_os = "netbsd")))]
assert_stat_results(stat_result: Result<FileStat>)42 fn assert_stat_results(stat_result: Result<FileStat>) {
43 let stats = stat_result.expect("stat call failed");
44 assert!(stats.st_dev > 0); // must be positive integer, exact number machine dependent
45 assert!(stats.st_ino > 0); // inode is positive integer, exact number machine dependent
46 assert!(stats.st_mode > 0); // must be positive integer
47 assert!(stats.st_nlink == 1); // there links created, must be 1
48 assert!(valid_uid_gid(stats)); // must be positive integers
49 assert!(stats.st_size == 0); // size is 0 because we did not write anything to the file
50 assert!(stats.st_blksize > 0); // must be positive integer, exact number machine dependent
51 assert!(stats.st_blocks <= 16); // Up to 16 blocks can be allocated for a blank file
52 }
53
54 #[cfg(not(any(target_os = "netbsd")))]
assert_lstat_results(stat_result: Result<FileStat>)55 fn assert_lstat_results(stat_result: Result<FileStat>) {
56 let stats = stat_result.expect("stat call failed");
57 assert!(stats.st_dev > 0); // must be positive integer, exact number machine dependent
58 assert!(stats.st_ino > 0); // inode is positive integer, exact number machine dependent
59 assert!(stats.st_mode > 0); // must be positive integer
60
61 // st_mode is c_uint (u32 on Android) while S_IFMT is mode_t
62 // (u16 on Android), and that will be a compile error.
63 // On other platforms they are the same (either both are u16 or u32).
64 assert!((stats.st_mode as usize) & (S_IFMT as usize) == S_IFLNK as usize); // should be a link
65 assert!(stats.st_nlink == 1); // there links created, must be 1
66 assert!(valid_uid_gid(stats)); // must be positive integers
67 assert!(stats.st_size > 0); // size is > 0 because it points to another file
68 assert!(stats.st_blksize > 0); // must be positive integer, exact number machine dependent
69
70 // st_blocks depends on whether the machine's file system uses fast
71 // or slow symlinks, so just make sure it's not negative
72 // (Android's st_blocks is ulonglong which is always non-negative.)
73 assert!(stats.st_blocks >= 0);
74 }
75
76 #[test]
77 #[cfg(not(any(target_os = "netbsd")))]
test_stat_and_fstat()78 fn test_stat_and_fstat() {
79 use nix::sys::stat::fstat;
80
81 let tempdir = tempfile::tempdir().unwrap();
82 let filename = tempdir.path().join("foo.txt");
83 let file = File::create(&filename).unwrap();
84
85 let stat_result = stat(&filename);
86 assert_stat_results(stat_result);
87
88 let fstat_result = fstat(file.as_raw_fd());
89 assert_stat_results(fstat_result);
90 }
91
92 #[test]
93 #[cfg(not(any(target_os = "netbsd")))]
test_fstatat()94 fn test_fstatat() {
95 let tempdir = tempfile::tempdir().unwrap();
96 let filename = tempdir.path().join("foo.txt");
97 File::create(&filename).unwrap();
98 let dirfd = fcntl::open(tempdir.path(),
99 fcntl::OFlag::empty(),
100 stat::Mode::empty());
101
102 let result = stat::fstatat(dirfd.unwrap(),
103 &filename,
104 fcntl::AtFlags::empty());
105 assert_stat_results(result);
106 }
107
108 #[test]
109 #[cfg(not(any(target_os = "netbsd")))]
test_stat_fstat_lstat()110 fn test_stat_fstat_lstat() {
111 use nix::sys::stat::{fstat, lstat};
112
113 let tempdir = tempfile::tempdir().unwrap();
114 let filename = tempdir.path().join("bar.txt");
115 let linkname = tempdir.path().join("barlink");
116
117 File::create(&filename).unwrap();
118 symlink("bar.txt", &linkname).unwrap();
119 let link = File::open(&linkname).unwrap();
120
121 // should be the same result as calling stat,
122 // since it's a regular file
123 let stat_result = stat(&filename);
124 assert_stat_results(stat_result);
125
126 let lstat_result = lstat(&linkname);
127 assert_lstat_results(lstat_result);
128
129 let fstat_result = fstat(link.as_raw_fd());
130 assert_stat_results(fstat_result);
131 }
132
133 #[test]
test_fchmod()134 fn test_fchmod() {
135 let tempdir = tempfile::tempdir().unwrap();
136 let filename = tempdir.path().join("foo.txt");
137 let file = File::create(&filename).unwrap();
138
139 let mut mode1 = Mode::empty();
140 mode1.insert(Mode::S_IRUSR);
141 mode1.insert(Mode::S_IWUSR);
142 fchmod(file.as_raw_fd(), mode1).unwrap();
143
144 let file_stat1 = stat(&filename).unwrap();
145 assert_eq!(file_stat1.st_mode & 0o7777, mode1.bits());
146
147 let mut mode2 = Mode::empty();
148 mode2.insert(Mode::S_IROTH);
149 fchmod(file.as_raw_fd(), mode2).unwrap();
150
151 let file_stat2 = stat(&filename).unwrap();
152 assert_eq!(file_stat2.st_mode & 0o7777, mode2.bits());
153 }
154
155 #[test]
test_fchmodat()156 fn test_fchmodat() {
157 let _dr = ::DirRestore::new();
158 let tempdir = tempfile::tempdir().unwrap();
159 let filename = "foo.txt";
160 let fullpath = tempdir.path().join(filename);
161 File::create(&fullpath).unwrap();
162
163 let dirfd = fcntl::open(tempdir.path(), fcntl::OFlag::empty(), stat::Mode::empty()).unwrap();
164
165 let mut mode1 = Mode::empty();
166 mode1.insert(Mode::S_IRUSR);
167 mode1.insert(Mode::S_IWUSR);
168 fchmodat(Some(dirfd), filename, mode1, FchmodatFlags::FollowSymlink).unwrap();
169
170 let file_stat1 = stat(&fullpath).unwrap();
171 assert_eq!(file_stat1.st_mode & 0o7777, mode1.bits());
172
173 chdir(tempdir.path()).unwrap();
174
175 let mut mode2 = Mode::empty();
176 mode2.insert(Mode::S_IROTH);
177 fchmodat(None, filename, mode2, FchmodatFlags::FollowSymlink).unwrap();
178
179 let file_stat2 = stat(&fullpath).unwrap();
180 assert_eq!(file_stat2.st_mode & 0o7777, mode2.bits());
181 }
182
183 /// Asserts that the atime and mtime in a file's metadata match expected values.
184 ///
185 /// The atime and mtime are expressed with a resolution of seconds because some file systems
186 /// (like macOS's HFS+) do not have higher granularity.
assert_times_eq(exp_atime_sec: u64, exp_mtime_sec: u64, attr: &fs::Metadata)187 fn assert_times_eq(exp_atime_sec: u64, exp_mtime_sec: u64, attr: &fs::Metadata) {
188 assert_eq!(
189 Duration::new(exp_atime_sec, 0),
190 attr.accessed().unwrap().duration_since(UNIX_EPOCH).unwrap());
191 assert_eq!(
192 Duration::new(exp_mtime_sec, 0),
193 attr.modified().unwrap().duration_since(UNIX_EPOCH).unwrap());
194 }
195
196 #[test]
test_utimes()197 fn test_utimes() {
198 let tempdir = tempfile::tempdir().unwrap();
199 let fullpath = tempdir.path().join("file");
200 drop(File::create(&fullpath).unwrap());
201
202 utimes(&fullpath, &TimeVal::seconds(9990), &TimeVal::seconds(5550)).unwrap();
203 assert_times_eq(9990, 5550, &fs::metadata(&fullpath).unwrap());
204 }
205
206 #[test]
207 #[cfg(any(target_os = "linux",
208 target_os = "haiku",
209 target_os = "ios",
210 target_os = "macos",
211 target_os = "freebsd",
212 target_os = "netbsd"))]
test_lutimes()213 fn test_lutimes() {
214 let tempdir = tempfile::tempdir().unwrap();
215 let target = tempdir.path().join("target");
216 let fullpath = tempdir.path().join("symlink");
217 drop(File::create(&target).unwrap());
218 symlink(&target, &fullpath).unwrap();
219
220 let exp_target_metadata = fs::symlink_metadata(&target).unwrap();
221 lutimes(&fullpath, &TimeVal::seconds(4560), &TimeVal::seconds(1230)).unwrap();
222 assert_times_eq(4560, 1230, &fs::symlink_metadata(&fullpath).unwrap());
223
224 let target_metadata = fs::symlink_metadata(&target).unwrap();
225 assert_eq!(exp_target_metadata.accessed().unwrap(), target_metadata.accessed().unwrap(),
226 "atime of symlink target was unexpectedly modified");
227 assert_eq!(exp_target_metadata.modified().unwrap(), target_metadata.modified().unwrap(),
228 "mtime of symlink target was unexpectedly modified");
229 }
230
231 #[test]
test_futimens()232 fn test_futimens() {
233 let tempdir = tempfile::tempdir().unwrap();
234 let fullpath = tempdir.path().join("file");
235 drop(File::create(&fullpath).unwrap());
236
237 let fd = fcntl::open(&fullpath, fcntl::OFlag::empty(), stat::Mode::empty()).unwrap();
238
239 futimens(fd, &TimeSpec::seconds(10), &TimeSpec::seconds(20)).unwrap();
240 assert_times_eq(10, 20, &fs::metadata(&fullpath).unwrap());
241 }
242
243 #[test]
test_utimensat()244 fn test_utimensat() {
245 let _dr = ::DirRestore::new();
246 let tempdir = tempfile::tempdir().unwrap();
247 let filename = "foo.txt";
248 let fullpath = tempdir.path().join(filename);
249 drop(File::create(&fullpath).unwrap());
250
251 let dirfd = fcntl::open(tempdir.path(), fcntl::OFlag::empty(), stat::Mode::empty()).unwrap();
252
253 utimensat(Some(dirfd), filename, &TimeSpec::seconds(12345), &TimeSpec::seconds(678),
254 UtimensatFlags::FollowSymlink).unwrap();
255 assert_times_eq(12345, 678, &fs::metadata(&fullpath).unwrap());
256
257 chdir(tempdir.path()).unwrap();
258
259 utimensat(None, filename, &TimeSpec::seconds(500), &TimeSpec::seconds(800),
260 UtimensatFlags::FollowSymlink).unwrap();
261 assert_times_eq(500, 800, &fs::metadata(&fullpath).unwrap());
262 }
263