From 7e77e5c9642de0ca933c19c05f98c85921ab351e Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Fri, 24 Jul 2026 19:29:34 +0200 Subject: [PATCH] Preserve modification date when updating access time --- Sources/tart/URL+AccessDate.swift | 16 ++++++++-------- Tests/TartTests/URLAccessDateTests.swift | 20 ++++++++++++++------ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/Sources/tart/URL+AccessDate.swift b/Sources/tart/URL+AccessDate.swift index 3604073d..200e2320 100644 --- a/Sources/tart/URL+AccessDate.swift +++ b/Sources/tart/URL+AccessDate.swift @@ -8,21 +8,21 @@ extension URL { } func updateAccessDate(_ accessDate: Date = Date()) throws { - let attrs = try resourceValues(forKeys: [.contentAccessDateKey]) - let modificationDate = attrs.contentAccessDate! - - let times = [accessDate.asTimeval(), modificationDate.asTimeval()] - let ret = utimes(path, times) + let times = [accessDate.asTimespec(), timespec(tv_sec: 0, tv_nsec: Int(UTIME_OMIT))] + let ret = utimensat(AT_FDCWD, path, times, 0) if ret != 0 { let details = Errno(rawValue: CInt(errno)) - throw RuntimeError.FailedToUpdateAccessDate("utimes(2) failed: \(details)") + throw RuntimeError.FailedToUpdateAccessDate("utimensat(2) failed: \(details)") } } } extension Date { - func asTimeval() -> timeval { - timeval(tv_sec: Int(timeIntervalSince1970), tv_usec: 0) + func asTimespec() -> timespec { + let seconds = floor(timeIntervalSince1970) + let nanoseconds = (timeIntervalSince1970 - seconds) * 1_000_000_000 + + return timespec(tv_sec: Int(seconds), tv_nsec: Int(nanoseconds)) } } diff --git a/Tests/TartTests/URLAccessDateTests.swift b/Tests/TartTests/URLAccessDateTests.swift index 638f9d58..c8a94578 100644 --- a/Tests/TartTests/URLAccessDateTests.swift +++ b/Tests/TartTests/URLAccessDateTests.swift @@ -2,21 +2,29 @@ import XCTest @testable import tart final class URLAccessDateTests: XCTestCase { - func testGetAndSetAccessTime() throws { + func testUpdateAccessDatePreservesModificationDate() throws { // Create a temporary file let tmpDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) var tmpFile = tmpDir.appendingPathComponent(UUID().uuidString) FileManager.default.createFile(atPath: tmpFile.path, contents: nil) + defer { try? FileManager.default.removeItem(at: tmpFile) } - // Ensure it's access date is different than our desired access date - let arbitraryDate = Date.init(year: 2008, month: 09, day: 28, hour: 23, minute: 15) - XCTAssertNotEqual(arbitraryDate, try tmpFile.accessDate()) + // Ensure its access date is different from our desired access date + let accessDate = Date.init(year: 2008, month: 09, day: 28, hour: 23, minute: 15) + let modificationDate = Date(timeIntervalSince1970: 1_577_836_800.125) + try FileManager.default.setAttributes([.modificationDate: modificationDate], ofItemAtPath: tmpFile.path) + XCTAssertNotEqual(accessDate, try tmpFile.accessDate()) // Set our desired access date for a file - try tmpFile.updateAccessDate(arbitraryDate) + try tmpFile.updateAccessDate(accessDate) // Ensure the access date has changed to our value tmpFile.removeCachedResourceValue(forKey: .contentAccessDateKey) - XCTAssertEqual(arbitraryDate, try tmpFile.accessDate()) + XCTAssertEqual(accessDate, try tmpFile.accessDate()) + + // Ensure the modification date has not changed + tmpFile.removeCachedResourceValue(forKey: .contentModificationDateKey) + let attrs = try tmpFile.resourceValues(forKeys: [.contentModificationDateKey]) + XCTAssertEqual(modificationDate, try XCTUnwrap(attrs.contentModificationDate)) } }