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 tempdir = tempfile::tempdir().unwrap();
158     let filename = "foo.txt";
159     let fullpath = tempdir.path().join(filename);
160     File::create(&fullpath).unwrap();
161 
162     let dirfd = fcntl::open(tempdir.path(), fcntl::OFlag::empty(), stat::Mode::empty()).unwrap();
163 
164     let mut mode1 = Mode::empty();
165     mode1.insert(Mode::S_IRUSR);
166     mode1.insert(Mode::S_IWUSR);
167     fchmodat(Some(dirfd), filename, mode1, FchmodatFlags::FollowSymlink).unwrap();
168 
169     let file_stat1 = stat(&fullpath).unwrap();
170     assert_eq!(file_stat1.st_mode & 0o7777, mode1.bits());
171 
172     chdir(tempdir.path()).unwrap();
173 
174     let mut mode2 = Mode::empty();
175     mode2.insert(Mode::S_IROTH);
176     fchmodat(None, filename, mode2, FchmodatFlags::FollowSymlink).unwrap();
177 
178     let file_stat2 = stat(&fullpath).unwrap();
179     assert_eq!(file_stat2.st_mode & 0o7777, mode2.bits());
180 }
181 
182 /// Asserts that the atime and mtime in a file's metadata match expected values.
183 ///
184 /// The atime and mtime are expressed with a resolution of seconds because some file systems
185 /// (like macOS's HFS+) do not have higher granularity.
assert_times_eq(exp_atime_sec: u64, exp_mtime_sec: u64, attr: &fs::Metadata)186 fn assert_times_eq(exp_atime_sec: u64, exp_mtime_sec: u64, attr: &fs::Metadata) {
187     assert_eq!(
188         Duration::new(exp_atime_sec, 0),
189         attr.accessed().unwrap().duration_since(UNIX_EPOCH).unwrap());
190     assert_eq!(
191         Duration::new(exp_mtime_sec, 0),
192         attr.modified().unwrap().duration_since(UNIX_EPOCH).unwrap());
193 }
194 
195 #[test]
test_utimes()196 fn test_utimes() {
197     let tempdir = tempfile::tempdir().unwrap();
198     let fullpath = tempdir.path().join("file");
199     drop(File::create(&fullpath).unwrap());
200 
201     utimes(&fullpath, &TimeVal::seconds(9990), &TimeVal::seconds(5550)).unwrap();
202     assert_times_eq(9990, 5550, &fs::metadata(&fullpath).unwrap());
203 }
204 
205 #[test]
206 #[cfg(any(target_os = "linux",
207           target_os = "haiku",
208           target_os = "ios",
209           target_os = "macos",
210           target_os = "freebsd",
211           target_os = "netbsd"))]
test_lutimes()212 fn test_lutimes() {
213     let tempdir = tempfile::tempdir().unwrap();
214     let target = tempdir.path().join("target");
215     let fullpath = tempdir.path().join("symlink");
216     drop(File::create(&target).unwrap());
217     symlink(&target, &fullpath).unwrap();
218 
219     let exp_target_metadata = fs::symlink_metadata(&target).unwrap();
220     lutimes(&fullpath, &TimeVal::seconds(4560), &TimeVal::seconds(1230)).unwrap();
221     assert_times_eq(4560, 1230, &fs::symlink_metadata(&fullpath).unwrap());
222 
223     let target_metadata = fs::symlink_metadata(&target).unwrap();
224     assert_eq!(exp_target_metadata.accessed().unwrap(), target_metadata.accessed().unwrap(),
225                "atime of symlink target was unexpectedly modified");
226     assert_eq!(exp_target_metadata.modified().unwrap(), target_metadata.modified().unwrap(),
227                "mtime of symlink target was unexpectedly modified");
228 }
229 
230 #[test]
test_futimens()231 fn test_futimens() {
232     let tempdir = tempfile::tempdir().unwrap();
233     let fullpath = tempdir.path().join("file");
234     drop(File::create(&fullpath).unwrap());
235 
236     let fd = fcntl::open(&fullpath, fcntl::OFlag::empty(), stat::Mode::empty()).unwrap();
237 
238     futimens(fd, &TimeSpec::seconds(10), &TimeSpec::seconds(20)).unwrap();
239     assert_times_eq(10, 20, &fs::metadata(&fullpath).unwrap());
240 }
241 
242 #[test]
test_utimensat()243 fn test_utimensat() {
244     let tempdir = tempfile::tempdir().unwrap();
245     let filename = "foo.txt";
246     let fullpath = tempdir.path().join(filename);
247     drop(File::create(&fullpath).unwrap());
248 
249     let dirfd = fcntl::open(tempdir.path(), fcntl::OFlag::empty(), stat::Mode::empty()).unwrap();
250 
251     utimensat(Some(dirfd), filename, &TimeSpec::seconds(12345), &TimeSpec::seconds(678),
252               UtimensatFlags::FollowSymlink).unwrap();
253     assert_times_eq(12345, 678, &fs::metadata(&fullpath).unwrap());
254 
255     chdir(tempdir.path()).unwrap();
256 
257     utimensat(None, filename, &TimeSpec::seconds(500), &TimeSpec::seconds(800),
258               UtimensatFlags::FollowSymlink).unwrap();
259     assert_times_eq(500, 800, &fs::metadata(&fullpath).unwrap());
260 }
261