Skip to content

Commit c135e31

Browse files
committed
Implement fs api set_times and set_times_nofollow
1 parent 2cb4e7d commit c135e31

File tree

11 files changed

+504
-37
lines changed

11 files changed

+504
-37
lines changed

library/std/src/fs.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,80 @@ pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result
387387
inner(path.as_ref(), contents.as_ref())
388388
}
389389

390+
/// Changes the timestamps of the file or directory at the specified path.
391+
///
392+
/// This function will attempt to set the access and modification times
393+
/// to the times specified. If the path refers to a symbolic link, this function
394+
/// will follow the link and change the timestamps of the target file.
395+
///
396+
/// # Platform-specific behavior
397+
///
398+
/// This function currently corresponds to the `utimensat` function on Unix platforms
399+
/// and the `SetFileTime` function on Windows.
400+
///
401+
/// # Errors
402+
///
403+
/// This function will return an error if the user lacks permission to change timestamps on the
404+
/// target file or symlink. It may also return an error if the OS does not support it.
405+
///
406+
/// # Examples
407+
///
408+
/// ```no_run
409+
/// #![feature(fs_set_times)]
410+
/// use std::fs::{self, FileTimes};
411+
/// use std::time::SystemTime;
412+
///
413+
/// fn main() -> std::io::Result<()> {
414+
/// let now = SystemTime::now();
415+
/// let times = FileTimes::new()
416+
/// .set_accessed(now)
417+
/// .set_modified(now);
418+
/// fs::set_times("foo.txt", times)?;
419+
/// Ok(())
420+
/// }
421+
/// ```
422+
#[unstable(feature = "fs_set_times", issue = "147455")]
423+
pub fn set_times<P: AsRef<Path>>(path: P, times: FileTimes) -> io::Result<()> {
424+
fs_imp::set_times(path.as_ref(), times.0)
425+
}
426+
427+
/// Changes the timestamps of the file or symlink at the specified path.
428+
///
429+
/// This function will attempt to set the access and modification times
430+
/// to the times specified. Differ from `set_times`, if the path refers to a symbolic link,
431+
/// this function will change the timestamps of the symlink itself, not the target file.
432+
///
433+
/// # Platform-specific behavior
434+
///
435+
/// This function currently corresponds to the `utimensat` function with `AT_SYMLINK_NOFOLLOW`
436+
/// on Unix platforms and the `SetFileTime` function on Windows after opening the symlink.
437+
///
438+
/// # Errors
439+
///
440+
/// This function will return an error if the user lacks permission to change timestamps on the
441+
/// target file or symlink. It may also return an error if the OS does not support it.
442+
///
443+
/// # Examples
444+
///
445+
/// ```no_run
446+
/// #![feature(fs_set_times)]
447+
/// use std::fs::{self, FileTimes};
448+
/// use std::time::SystemTime;
449+
///
450+
/// fn main() -> std::io::Result<()> {
451+
/// let now = SystemTime::now();
452+
/// let times = FileTimes::new()
453+
/// .set_accessed(now)
454+
/// .set_modified(now);
455+
/// fs::set_times_nofollow("symlink.txt", times)?;
456+
/// Ok(())
457+
/// }
458+
/// ```
459+
#[unstable(feature = "fs_set_times", issue = "147455")]
460+
pub fn set_times_nofollow<P: AsRef<Path>>(path: P, times: FileTimes) -> io::Result<()> {
461+
fs_imp::set_times_nofollow(path.as_ref(), times.0)
462+
}
463+
390464
#[stable(feature = "file_lock", since = "1.89.0")]
391465
impl error::Error for TryLockError {}
392466

library/std/src/fs/tests.rs

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2226,3 +2226,225 @@ fn test_open_options_invalid_combinations() {
22262226
assert_eq!(err.kind(), ErrorKind::InvalidInput);
22272227
assert_eq!(err.to_string(), "must specify at least one of read, write, or append access");
22282228
}
2229+
2230+
#[test]
2231+
fn test_fs_set_times() {
2232+
#[cfg(target_vendor = "apple")]
2233+
use crate::os::darwin::fs::FileTimesExt;
2234+
#[cfg(windows)]
2235+
use crate::os::windows::fs::FileTimesExt;
2236+
2237+
let tmp = tmpdir();
2238+
let path = tmp.join("foo");
2239+
File::create(&path).unwrap();
2240+
2241+
let mut times = FileTimes::new();
2242+
let accessed = SystemTime::UNIX_EPOCH + Duration::from_secs(12345);
2243+
let modified = SystemTime::UNIX_EPOCH + Duration::from_secs(54321);
2244+
times = times.set_accessed(accessed).set_modified(modified);
2245+
2246+
#[cfg(any(windows, target_vendor = "apple"))]
2247+
let created = SystemTime::UNIX_EPOCH + Duration::from_secs(32123);
2248+
#[cfg(any(windows, target_vendor = "apple"))]
2249+
{
2250+
times = times.set_created(created);
2251+
}
2252+
2253+
match fs::set_times(&path, times) {
2254+
// Allow unsupported errors on platforms which don't support setting times.
2255+
#[cfg(not(any(
2256+
windows,
2257+
all(
2258+
unix,
2259+
not(any(
2260+
target_os = "android",
2261+
target_os = "redox",
2262+
target_os = "espidf",
2263+
target_os = "horizon"
2264+
))
2265+
)
2266+
)))]
2267+
Err(e) if e.kind() == ErrorKind::Unsupported => return,
2268+
Err(e) => panic!("error setting file times: {e:?}"),
2269+
Ok(_) => {}
2270+
}
2271+
2272+
let metadata = fs::metadata(&path).unwrap();
2273+
assert_eq!(metadata.accessed().unwrap(), accessed);
2274+
assert_eq!(metadata.modified().unwrap(), modified);
2275+
#[cfg(any(windows, target_vendor = "apple"))]
2276+
{
2277+
assert_eq!(metadata.created().unwrap(), created);
2278+
}
2279+
}
2280+
2281+
#[test]
2282+
fn test_fs_set_times_follows_symlink() {
2283+
#[cfg(target_vendor = "apple")]
2284+
use crate::os::darwin::fs::FileTimesExt;
2285+
#[cfg(windows)]
2286+
use crate::os::windows::fs::FileTimesExt;
2287+
2288+
let tmp = tmpdir();
2289+
2290+
// Create a target file
2291+
let target = tmp.join("target");
2292+
File::create(&target).unwrap();
2293+
2294+
// Create a symlink to the target
2295+
#[cfg(unix)]
2296+
let link = tmp.join("link");
2297+
#[cfg(unix)]
2298+
crate::os::unix::fs::symlink(&target, &link).unwrap();
2299+
2300+
#[cfg(windows)]
2301+
let link = tmp.join("link.txt");
2302+
#[cfg(windows)]
2303+
crate::os::windows::fs::symlink_file(&target, &link).unwrap();
2304+
2305+
// Get the symlink's own times BEFORE calling set_times (to compare later)
2306+
let link_metadata_before = fs::symlink_metadata(&link).unwrap();
2307+
let link_accessed_before = link_metadata_before.accessed().unwrap();
2308+
let link_modified_before = link_metadata_before.modified().unwrap();
2309+
2310+
let mut times = FileTimes::new();
2311+
let accessed = SystemTime::UNIX_EPOCH + Duration::from_secs(12345);
2312+
let modified = SystemTime::UNIX_EPOCH + Duration::from_secs(54321);
2313+
times = times.set_accessed(accessed).set_modified(modified);
2314+
2315+
#[cfg(any(windows, target_vendor = "apple"))]
2316+
let created = SystemTime::UNIX_EPOCH + Duration::from_secs(32123);
2317+
#[cfg(any(windows, target_vendor = "apple"))]
2318+
{
2319+
times = times.set_created(created);
2320+
}
2321+
2322+
// Call fs::set_times on the symlink - it should follow the link and modify the target
2323+
match fs::set_times(&link, times) {
2324+
// Allow unsupported errors on platforms which don't support setting times.
2325+
#[cfg(not(any(
2326+
windows,
2327+
all(
2328+
unix,
2329+
not(any(
2330+
target_os = "android",
2331+
target_os = "redox",
2332+
target_os = "espidf",
2333+
target_os = "horizon"
2334+
))
2335+
)
2336+
)))]
2337+
Err(e) if e.kind() == ErrorKind::Unsupported => return,
2338+
Err(e) => panic!("error setting file times through symlink: {e:?}"),
2339+
Ok(_) => {}
2340+
}
2341+
2342+
// Verify that the TARGET file's times were changed (following the symlink)
2343+
let target_metadata = fs::metadata(&target).unwrap();
2344+
assert_eq!(
2345+
target_metadata.accessed().unwrap(),
2346+
accessed,
2347+
"target file accessed time should match"
2348+
);
2349+
assert_eq!(
2350+
target_metadata.modified().unwrap(),
2351+
modified,
2352+
"target file modified time should match"
2353+
);
2354+
#[cfg(any(windows, target_vendor = "apple"))]
2355+
{
2356+
assert_eq!(
2357+
target_metadata.created().unwrap(),
2358+
created,
2359+
"target file created time should match"
2360+
);
2361+
}
2362+
2363+
// Also verify through the symlink (fs::metadata follows symlinks)
2364+
let link_followed_metadata = fs::metadata(&link).unwrap();
2365+
assert_eq!(link_followed_metadata.accessed().unwrap(), accessed);
2366+
assert_eq!(link_followed_metadata.modified().unwrap(), modified);
2367+
2368+
// Verify that the SYMLINK ITSELF was NOT modified
2369+
let link_metadata_after = fs::symlink_metadata(&link).unwrap();
2370+
assert_eq!(
2371+
link_metadata_after.accessed().unwrap(),
2372+
link_accessed_before,
2373+
"symlink's own accessed time should not change"
2374+
);
2375+
assert_eq!(
2376+
link_metadata_after.modified().unwrap(),
2377+
link_modified_before,
2378+
"symlink's own modified time should not change"
2379+
);
2380+
}
2381+
2382+
#[test]
2383+
fn test_fs_set_times_nofollow() {
2384+
#[cfg(target_vendor = "apple")]
2385+
use crate::os::darwin::fs::FileTimesExt;
2386+
#[cfg(windows)]
2387+
use crate::os::windows::fs::FileTimesExt;
2388+
2389+
let tmp = tmpdir();
2390+
2391+
// Create a target file and a symlink to it
2392+
let target = tmp.join("target");
2393+
File::create(&target).unwrap();
2394+
2395+
#[cfg(unix)]
2396+
let link = tmp.join("link");
2397+
#[cfg(unix)]
2398+
crate::os::unix::fs::symlink(&target, &link).unwrap();
2399+
2400+
#[cfg(windows)]
2401+
let link = tmp.join("link.txt");
2402+
#[cfg(windows)]
2403+
crate::os::windows::fs::symlink_file(&target, &link).unwrap();
2404+
2405+
let mut times = FileTimes::new();
2406+
let accessed = SystemTime::UNIX_EPOCH + Duration::from_secs(11111);
2407+
let modified = SystemTime::UNIX_EPOCH + Duration::from_secs(22222);
2408+
times = times.set_accessed(accessed).set_modified(modified);
2409+
2410+
#[cfg(any(windows, target_vendor = "apple"))]
2411+
let created = SystemTime::UNIX_EPOCH + Duration::from_secs(33333);
2412+
#[cfg(any(windows, target_vendor = "apple"))]
2413+
{
2414+
times = times.set_created(created);
2415+
}
2416+
2417+
// Set times on the symlink itself (not following it)
2418+
match fs::set_times_nofollow(&link, times) {
2419+
// Allow unsupported errors on platforms which don't support setting times.
2420+
#[cfg(not(any(
2421+
windows,
2422+
all(
2423+
unix,
2424+
not(any(
2425+
target_os = "android",
2426+
target_os = "redox",
2427+
target_os = "espidf",
2428+
target_os = "horizon"
2429+
))
2430+
)
2431+
)))]
2432+
Err(e) if e.kind() == ErrorKind::Unsupported => return,
2433+
Err(e) => panic!("error setting symlink times: {e:?}"),
2434+
Ok(_) => {}
2435+
}
2436+
2437+
// Read symlink metadata (without following)
2438+
let metadata = fs::symlink_metadata(&link).unwrap();
2439+
assert_eq!(metadata.accessed().unwrap(), accessed);
2440+
assert_eq!(metadata.modified().unwrap(), modified);
2441+
#[cfg(any(windows, target_vendor = "apple"))]
2442+
{
2443+
assert_eq!(metadata.created().unwrap(), created);
2444+
}
2445+
2446+
// Verify that the target file's times were NOT changed
2447+
let target_metadata = fs::metadata(&target).unwrap();
2448+
assert_ne!(target_metadata.accessed().unwrap(), accessed);
2449+
assert_ne!(target_metadata.modified().unwrap(), modified);
2450+
}

library/std/src/sys/fs/hermit.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,14 @@ pub fn set_perm(_p: &Path, _perm: FilePermissions) -> io::Result<()> {
566566
Err(Error::from_raw_os_error(22))
567567
}
568568

569+
pub fn set_times(_p: &Path, _times: FileTimes) -> io::Result<()> {
570+
Err(Error::from_raw_os_error(22))
571+
}
572+
573+
pub fn set_times_nofollow(_p: &Path, _times: FileTimes) -> io::Result<()> {
574+
Err(Error::from_raw_os_error(22))
575+
}
576+
569577
pub fn rmdir(path: &Path) -> io::Result<()> {
570578
run_path_with_cstr(path, &|path| cvt(unsafe { hermit_abi::rmdir(path.as_ptr()) }).map(|_| ()))
571579
}

library/std/src/sys/fs/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,11 @@ pub fn exists(path: &Path) -> io::Result<bool> {
161161
#[cfg(windows)]
162162
with_native_path(path, &imp::exists)
163163
}
164+
165+
pub fn set_times(path: &Path, times: FileTimes) -> io::Result<()> {
166+
with_native_path(path, &|path| imp::set_times(path, times.clone()))
167+
}
168+
169+
pub fn set_times_nofollow(path: &Path, times: FileTimes) -> io::Result<()> {
170+
with_native_path(path, &|path| imp::set_times_nofollow(path, times.clone()))
171+
}

library/std/src/sys/fs/solid.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,17 @@ pub fn set_perm(p: &Path, perm: FilePermissions) -> io::Result<()> {
538538
Ok(())
539539
}
540540

541+
pub fn set_times(_p: &Path, _times: FileTimes) -> io::Result<()> {
542+
Err(io::const_error!(io::ErrorKind::Unsupported, "setting file times not supported",))
543+
}
544+
545+
pub fn set_times_nofollow(_p: &Path, _times: FileTimes) -> io::Result<()> {
546+
Err(io::const_error!(
547+
io::ErrorKind::Unsupported,
548+
"setting file times on symlinks not supported",
549+
))
550+
}
551+
541552
pub fn rmdir(p: &Path) -> io::Result<()> {
542553
if stat(p)?.file_type().is_dir() {
543554
error::SolidError::err_if_negative(unsafe { abi::SOLID_FS_Unlink(cstr(p)?.as_ptr()) })

library/std/src/sys/fs/uefi.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,14 @@ pub fn set_perm(_p: &Path, _perm: FilePermissions) -> io::Result<()> {
333333
unsupported()
334334
}
335335

336+
pub fn set_times(_p: &Path, _times: FileTimes) -> io::Result<()> {
337+
unsupported()
338+
}
339+
340+
pub fn set_times_nofollow(_p: &Path, _times: FileTimes) -> io::Result<()> {
341+
unsupported()
342+
}
343+
336344
pub fn rmdir(_p: &Path) -> io::Result<()> {
337345
unsupported()
338346
}

0 commit comments

Comments
 (0)