Skip to content

Commit 692e914

Browse files
committed
FileManager.attributesOfItem(atPath:) - use statx() on Linux if available.
- Linux kernel >= 4.11.0 has a statx() call for extended file stat information and this can be used to set .creationDate - statx() is not available on Ubuntu 16.04 so have a fallback if struct statx and the system call number are not declared in the headers.
1 parent 60d2b8f commit 692e914

File tree

3 files changed

+128
-3
lines changed

3 files changed

+128
-3
lines changed

CoreFoundation/Base.subproj/ForSwiftFoundationOnly.h

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@
5252
#include <malloc/malloc.h>
5353
#endif
5454

55+
#if TARGET_OS_LINUX
56+
// required for statx() system call
57+
#include <sys/syscall.h>
58+
#include <sys/types.h>
59+
#include <sys/stat.h>
60+
#include <linux/stat.h>
61+
#define AT_STATX_SYNC_AS_STAT 0x0000 /* - Do whatever stat() does */
62+
#endif
63+
64+
5565
_CF_EXPORT_SCOPE_BEGIN
5666

5767
CF_CROSS_PLATFORM_EXPORT Boolean _CFCalendarGetNextWeekend(CFCalendarRef calendar, _CFCalendarWeekendRange *range);
@@ -482,6 +492,69 @@ static inline unsigned int _dev_major(dev_t rdev) {
482492
static inline unsigned int _dev_minor(dev_t rdev) {
483493
return minor(rdev);
484494
}
495+
496+
static inline dev_t _dev_makedev(unsigned int major, unsigned int minor) {
497+
return makedev(major, minor);
498+
}
499+
#endif
500+
501+
502+
#if TARGET_OS_LINUX
503+
#ifdef __NR_statx
504+
505+
// There is no glibc statx() function, it must be called using syscall().
506+
507+
static inline ssize_t
508+
_statx(int dfd, const char *filename, unsigned int flags, unsigned int mask, struct statx *buffer) {
509+
return syscall(__NR_statx, dfd, filename, flags, mask, buffer);
510+
}
511+
512+
// At the moment the only extra information statx() is used for is to get the btime (file creation time).
513+
// This function is here instead of in FileManager.swift because there is no way of setting a conditional
514+
// define that could be used with a #if in the Swift code.
515+
static inline ssize_t
516+
_stat_with_btime(const char *filename, struct stat *buffer, struct timespec *btime) {
517+
struct statx statx_buffer = {0};
518+
*btime = (struct timespec) {0};
519+
520+
ssize_t ret = _statx(AT_FDCWD, filename, AT_SYMLINK_NOFOLLOW | AT_STATX_SYNC_AS_STAT, STATX_ALL, &statx_buffer);
521+
if (ret == 0) {
522+
*buffer = (struct stat) {
523+
.st_dev = makedev(statx_buffer.stx_dev_major, statx_buffer.stx_dev_minor),
524+
.st_ino = statx_buffer.stx_ino,
525+
.st_mode = statx_buffer.stx_mode,
526+
.st_nlink = statx_buffer.stx_nlink,
527+
.st_uid = statx_buffer.stx_uid,
528+
.st_gid = statx_buffer.stx_gid,
529+
.st_rdev = makedev(statx_buffer.stx_rdev_major, statx_buffer.stx_rdev_minor),
530+
.st_size = statx_buffer.stx_size,
531+
.st_blksize = statx_buffer.stx_blksize,
532+
.st_blocks = statx_buffer.stx_blocks,
533+
.st_atim = { .tv_sec = statx_buffer.stx_atime.tv_sec, .tv_nsec = statx_buffer.stx_atime.tv_nsec },
534+
.st_mtim = { .tv_sec = statx_buffer.stx_mtime.tv_sec, .tv_nsec = statx_buffer.stx_mtime.tv_nsec },
535+
.st_ctim = { .tv_sec = statx_buffer.stx_ctime.tv_sec, .tv_nsec = statx_buffer.stx_ctime.tv_nsec },
536+
};
537+
*btime = (struct timespec) {
538+
.tv_sec = statx_buffer.stx_btime.tv_sec,
539+
.tv_nsec = statx_buffer.stx_btime.tv_nsec
540+
};
541+
}
542+
return ret;
543+
}
544+
#else
545+
546+
// Dummy version when compiled where struct statx is not defined in the headers.
547+
// Just calles lstat() instead.
548+
static inline ssize_t
549+
_stat_with_btime(const char *filename, struct stat *buffer, struct timespec *btime) {
550+
*btime = (struct timespec) {0};
551+
return lstat(filename, buffer);
552+
}
553+
#endif // __NR_statx
554+
#endif // TARGET_OS_LINUX
555+
556+
#if __HAS_STATX
557+
#warning "Enabling statx"
485558
#endif
486559

487560
_CF_EXPORT_SCOPE_END

Foundation/FileManager.swift

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,8 +660,13 @@ open class FileManager : NSObject {
660660
This method replaces fileAttributesAtPath:traverseLink:.
661661
*/
662662
open func attributesOfItem(atPath path: String) throws -> [FileAttributeKey : Any] {
663-
let s = try _lstatFile(atPath: path)
664663
var result = [FileAttributeKey : Any]()
664+
#if os(Linux)
665+
let (s, creationDate) = try _statxFile(atPath: path)
666+
result[.creationDate] = creationDate
667+
#else
668+
let s = try _lstatFile(atPath: path)
669+
#endif
665670
result[.size] = NSNumber(value: UInt64(s.st_size))
666671

667672
#if os(macOS) || os(iOS)
@@ -1397,6 +1402,43 @@ open class FileManager : NSObject {
13971402
return Int(fileInfo.st_mode & 0o777)
13981403
}
13991404

1405+
1406+
#if os(Linux)
1407+
// statx() is only supported by Linux kernels >= 4.11.0
1408+
private lazy var supportsStatx: Bool = {
1409+
let requiredVersion = OperatingSystemVersion(majorVersion: 4, minorVersion: 11, patchVersion: 0)
1410+
return ProcessInfo.processInfo.isOperatingSystemAtLeast(requiredVersion)
1411+
}()
1412+
1413+
// This is only used on Linux and the only extra information it returns in addition
1414+
// to a normal stat() call is the file creation date (stx_btime). It is only
1415+
// used by attributesOfItem(atPath:) which is why the return is a simple stat()
1416+
// structure and optional creation date.
1417+
1418+
private func _statxFile(atPath path: String) throws -> (stat, Date?) {
1419+
let fsRep = fileSystemRepresentation(withPath: path)
1420+
defer { fsRep.deallocate() }
1421+
1422+
if supportsStatx {
1423+
var statInfo = stat()
1424+
var btime = timespec()
1425+
guard _stat_with_btime(fsRep, &statInfo, &btime) == 0 else {
1426+
throw _NSErrorWithErrno(errno, reading: true, path: path)
1427+
}
1428+
1429+
let sec = btime.tv_sec
1430+
let nsec = btime.tv_nsec
1431+
let ti = (TimeInterval(sec) - kCFAbsoluteTimeIntervalSince1970) + (1.0e-9 * TimeInterval(nsec))
1432+
let creationDate = Date(timeIntervalSinceReferenceDate: ti)
1433+
return (statInfo, creationDate)
1434+
} else {
1435+
// fallback if statx() is unavailable or fails
1436+
let statInfo = try _lstatFile(atPath: path, withFileSystemRepresentation: fsRep)
1437+
return (statInfo, nil)
1438+
}
1439+
}
1440+
#endif
1441+
14001442
/* -contentsEqualAtPath:andPath: does not take into account data stored in the resource fork or filesystem extended attributes.
14011443
*/
14021444
open func contentsEqual(atPath path1: String, andPath path2: String) -> Bool {

TestFoundation/TestFileManager.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ class TestFileManager : XCTestCase {
310310
XCTAssertFalse(fm.isDeletableFile(atPath: "/dev/null"))
311311
}
312312

313-
func test_fileAttributes() {
313+
func test_fileAttributes() throws {
314314
let fm = FileManager.default
315315
let path = NSTemporaryDirectory() + "test_fileAttributes\(NSUUID().uuidString)"
316316

@@ -349,7 +349,17 @@ class TestFileManager : XCTestCase {
349349

350350
let fileGroupOwnerAccountID = attrs[.groupOwnerAccountID] as? NSNumber
351351
XCTAssertNotNil(fileGroupOwnerAccountID)
352-
352+
353+
#if os(Linux)
354+
let requiredVersion = OperatingSystemVersion(majorVersion: 4, minorVersion: 11, patchVersion: 0)
355+
let creationDate = attrs[.creationDate] as? Date
356+
if ProcessInfo.processInfo.isOperatingSystemAtLeast(requiredVersion) {
357+
XCTAssertNotNil(creationDate)
358+
XCTAssertGreaterThan(Date().timeIntervalSince1970, try creationDate.unwrapped().timeIntervalSince1970)
359+
} else {
360+
XCTAssertNil(creationDate)
361+
}
362+
#endif
353363
if let fileOwnerAccountName = attrs[.ownerAccountName] {
354364
XCTAssertNotNil(fileOwnerAccountName as? String)
355365
if let fileOwnerAccountNameStr = fileOwnerAccountName as? String {

0 commit comments

Comments
 (0)