From 25b0f118c79f8c1f1e78c5c35094aec3e9166376 Mon Sep 17 00:00:00 2001
From: Julian Ospald <hasufell@posteo.de>
Date: Sat, 16 Sep 2023 16:19:23 +0800
Subject: [PATCH] Add statx

---
 .github/workflows/ci.yml           |   6 +-
 System/Posix/Files.hsc             |  83 ++++
 System/Posix/Files/ByteString.hsc  |  83 ++++
 System/Posix/Files/Common.hsc      | 660 ++++++++++++++++++++++++++++-
 System/Posix/Files/PosixString.hsc |  86 +++-
 changelog.md                       |   5 +
 configure.ac                       |   5 +
 include/HsUnix.h                   |   7 +
 tests/FileExtendedStatus.hs        | 166 ++++++++
 tests/{Test.hs => Test.hsc}        |   9 +
 unix.cabal                         |  17 +-
 11 files changed, 1112 insertions(+), 15 deletions(-)
 create mode 100644 tests/FileExtendedStatus.hs
 rename tests/{Test.hs => Test.hsc} (97%)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 13b485d..400ba3e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,13 +16,9 @@ jobs:
       fail-fast: true
       matrix:
         os: [ubuntu-22.04, macOS-latest]
-        ghc: ['9.6', '9.4', '9.2', '9.0', '8.10', '8.8', '8.6', '8.4', '8.2']
+        ghc: ['9.6', '9.4', '9.2', '9.0', '8.10', '8.8', '8.6']
     steps:
     - uses: actions/checkout@v3
-    - name: Install prerequisites for GHC 8.2 on ubuntu-22.04
-      if: runner.os == 'Linux' && matrix.ghc == '8.2'
-      run: |
-        sudo apt-get install libncurses5 libtinfo5
     - name: Setup toolchain
       run: |
         which ghcup
diff --git a/System/Posix/Files.hsc b/System/Posix/Files.hsc
index 7587718..0810dc7 100644
--- a/System/Posix/Files.hsc
+++ b/System/Posix/Files.hsc
@@ -1,5 +1,6 @@
 {-# LANGUAGE Safe #-}
 {-# LANGUAGE CApiFFI #-}
+{-# LANGUAGE PatternSynonyms #-}
 
 -----------------------------------------------------------------------------
 -- |
@@ -62,6 +63,69 @@ module System.Posix.Files (
     fileBlockSize,
     fileBlocks,
 
+    -- * Extended file status
+    ExtendedFileStatus(..),
+    CAttributes(..),
+    -- ** Obtaining extended file status
+    getExtendedFileStatus,
+    -- ** Flags
+    StatxFlags(..),
+    defaultStatxFlags,
+    pattern EmptyPath,
+    pattern NoAutoMount,
+    pattern SymlinkNoFollow,
+    pattern SyncAsStat,
+    pattern ForceSync,
+    pattern DontSync,
+    -- ** Mask
+    StatxMask(..),
+    defaultStatxMask,
+    pattern StatxType,
+    pattern StatxMode,
+    pattern StatxNlink,
+    pattern StatxUid,
+    pattern StatxGid,
+    pattern StatxAtime,
+    pattern StatxMtime,
+    pattern StatxCtime,
+    pattern StatxIno,
+    pattern StatxSize,
+    pattern StatxBlocks,
+    pattern StatxBasicStats,
+    pattern StatxBtime,
+    pattern StatxMntId,
+    pattern StatxAll,
+    -- ** Querying extended file status
+    fileBlockSizeX,
+    linkCountX,
+    fileOwnerX,
+    fileGroupX,
+    fileModeX,
+    fileIDX,
+    fileSizeX,
+    fileBlocksX,
+    accessTimeHiResX,
+    creationTimeHiResX,
+    statusChangeTimeHiResX,
+    modificationTimeHiResX,
+    deviceIDX,
+    specialDeviceIDX,
+    mountIDX,
+    fileCompressedX,
+    fileImmutableX,
+    fileAppendX,
+    fileNoDumpX,
+    fileEncryptedX,
+    fileVerityX,
+    fileDaxX,
+    isBlockDeviceX,
+    isCharacterDeviceX,
+    isNamedPipeX,
+    isRegularFileX,
+    isDirectoryX,
+    isSymbolicLinkX,
+    isSocketX,
+
     -- * Creation
     createNamedPipe,
     createDevice,
@@ -189,6 +253,25 @@ getFileStatus path = do
       throwErrnoPathIfMinus1Retry_ "getFileStatus" path (c_stat s p)
   return (FileStatus fp)
 
+-- | Gets extended file status information.
+--
+-- The target file to open is identified in one of the following ways:
+--
+-- - If @pathname@ begins with a slash, then it is an absolute pathname that identifies the target file. In this case, @dirfd@ is ignored
+-- - If @pathname@ is a string that begins with a character other than a slash and @dirfd@ is a file descriptor that refers to a
+--   directory, then pathname is a relative pathname that is interpreted relative to the directory referred to by dirfd.
+--   (See @openat(2)@ for an explanation of why this is useful.)
+-- - If @pathname@ is an empty string and the 'EmptyPath' flag is specified in flags (see below), then the target file is
+--   the one referred to by the file descriptor @dirfd@.
+--
+-- Note: calls @statx@.
+getExtendedFileStatus :: Maybe Fd    -- ^ Optional directory file descriptor (@dirfd@)
+                      -> FilePath    -- ^ @pathname@ to open
+                      -> StatxFlags  -- ^ flags
+                      -> StatxMask   -- ^ mask
+                      -> IO ExtendedFileStatus
+getExtendedFileStatus mfd path flags masks = withFilePath path $ \s -> getExtendedFileStatus_ mfd s flags masks
+
 -- | Acts as 'getFileStatus' except when the 'FilePath' refers to a symbolic
 -- link. In that case the @FileStatus@ information of the symbolic link itself
 -- is returned instead of that of the file it points to.
diff --git a/System/Posix/Files/ByteString.hsc b/System/Posix/Files/ByteString.hsc
index 1c7b5a0..25327cc 100644
--- a/System/Posix/Files/ByteString.hsc
+++ b/System/Posix/Files/ByteString.hsc
@@ -1,5 +1,6 @@
 {-# LANGUAGE Safe #-}
 {-# LANGUAGE CApiFFI #-}
+{-# LANGUAGE PatternSynonyms #-}
 
 -----------------------------------------------------------------------------
 -- |
@@ -62,6 +63,69 @@ module System.Posix.Files.ByteString (
     fileBlockSize,
     fileBlocks,
 
+    -- * Extended file status
+    ExtendedFileStatus(..),
+    CAttributes(..),
+    -- ** Obtaining extended file status
+    getExtendedFileStatus,
+    -- ** Flags
+    StatxFlags(..),
+    defaultStatxFlags,
+    pattern EmptyPath,
+    pattern NoAutoMount,
+    pattern SymlinkNoFollow,
+    pattern SyncAsStat,
+    pattern ForceSync,
+    pattern DontSync,
+    -- ** Mask
+    StatxMask(..),
+    defaultStatxMask,
+    pattern StatxType,
+    pattern StatxMode,
+    pattern StatxNlink,
+    pattern StatxUid,
+    pattern StatxGid,
+    pattern StatxAtime,
+    pattern StatxMtime,
+    pattern StatxCtime,
+    pattern StatxIno,
+    pattern StatxSize,
+    pattern StatxBlocks,
+    pattern StatxBasicStats,
+    pattern StatxBtime,
+    pattern StatxMntId,
+    pattern StatxAll,
+    -- ** Querying extended file status
+    fileBlockSizeX,
+    linkCountX,
+    fileOwnerX,
+    fileGroupX,
+    fileModeX,
+    fileIDX,
+    fileSizeX,
+    fileBlocksX,
+    accessTimeHiResX,
+    creationTimeHiResX,
+    statusChangeTimeHiResX,
+    modificationTimeHiResX,
+    deviceIDX,
+    specialDeviceIDX,
+    mountIDX,
+    fileCompressedX,
+    fileImmutableX,
+    fileAppendX,
+    fileNoDumpX,
+    fileEncryptedX,
+    fileVerityX,
+    fileDaxX,
+    isBlockDeviceX,
+    isCharacterDeviceX,
+    isNamedPipeX,
+    isRegularFileX,
+    isDirectoryX,
+    isSymbolicLinkX,
+    isSocketX,
+
     -- * Creation
     createNamedPipe,
     createDevice,
@@ -184,6 +248,25 @@ getFileStatus path = do
       throwErrnoPathIfMinus1Retry_ "getFileStatus" path (c_stat s p)
   return (FileStatus fp)
 
+-- | Gets extended file status information.
+--
+-- The target file to open is identified in one of the following ways:
+--
+-- - If @pathname@ begins with a slash, then it is an absolute pathname that identifies the target file. In this case, @dirfd@ is ignored
+-- - If @pathname@ is a string that begins with a character other than a slash and @dirfd@ is a file descriptor that refers to a
+--   directory, then pathname is a relative pathname that is interpreted relative to the directory referred to by dirfd.
+--   (See @openat(2)@ for an explanation of why this is useful.)
+-- - If @pathname@ is an empty string and the 'EmptyPath' flag is specified in flags (see below), then the target file is
+--   the one referred to by the file descriptor @dirfd@.
+--
+-- Note: calls @statx@.
+getExtendedFileStatus :: Maybe Fd     -- ^ Optional directory file descriptor (@dirfd@)
+                      -> RawFilePath  -- ^ @pathname@ to open
+                      -> StatxFlags   -- ^ flags
+                      -> StatxMask    -- ^ mask
+                      -> IO ExtendedFileStatus
+getExtendedFileStatus mfd path flags masks = withFilePath path $ \s -> getExtendedFileStatus_ mfd s flags masks
+
 -- | Acts as 'getFileStatus' except when the 'RawFilePath' refers to a symbolic
 -- link. In that case the @FileStatus@ information of the symbolic link itself
 -- is returned instead of that of the file it points to.
diff --git a/System/Posix/Files/Common.hsc b/System/Posix/Files/Common.hsc
index b03859a..30ac2b2 100644
--- a/System/Posix/Files/Common.hsc
+++ b/System/Posix/Files/Common.hsc
@@ -1,5 +1,9 @@
 {-# LANGUAGE CApiFFI #-}
+{-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE Trustworthy #-}
+{-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE NumDecimals #-}
 
 -----------------------------------------------------------------------------
 -- |
@@ -60,6 +64,65 @@ module System.Posix.Files.Common (
     fileBlockSize,
     fileBlocks,
 
+    -- * Extended file status
+    StatxFlags(..),
+    pattern EmptyPath,
+    pattern NoAutoMount,
+    pattern SymlinkNoFollow,
+    pattern SyncAsStat,
+    pattern ForceSync,
+    pattern DontSync,
+    defaultStatxFlags,
+    StatxMask(..),
+    pattern StatxType,
+    pattern StatxMode,
+    pattern StatxNlink,
+    pattern StatxUid,
+    pattern StatxGid,
+    pattern StatxAtime,
+    pattern StatxMtime,
+    pattern StatxCtime,
+    pattern StatxBtime,
+    pattern StatxIno,
+    pattern StatxSize,
+    pattern StatxBlocks,
+    pattern StatxMntId,
+    pattern StatxBasicStats,
+    pattern StatxAll,
+    defaultStatxMask,
+    ExtendedFileStatus(..),
+    CAttributes(..),
+    getExtendedFileStatus_,
+    fileBlockSizeX,
+    linkCountX,
+    fileOwnerX,
+    fileGroupX,
+    fileModeX,
+    fileIDX,
+    fileSizeX,
+    fileBlocksX,
+    accessTimeHiResX,
+    creationTimeHiResX,
+    statusChangeTimeHiResX,
+    modificationTimeHiResX,
+    deviceIDX,
+    specialDeviceIDX,
+    mountIDX,
+    fileCompressedX,
+    fileImmutableX,
+    fileAppendX,
+    fileNoDumpX,
+    fileEncryptedX,
+    fileVerityX,
+    fileDaxX,
+    isBlockDeviceX,
+    isCharacterDeviceX,
+    isNamedPipeX,
+    isRegularFileX,
+    isDirectoryX,
+    isSymbolicLinkX,
+    isSocketX,
+
     -- * Setting file sizes
     setFdSize,
 
@@ -93,6 +156,11 @@ import System.IO.Unsafe
 import Data.Bits
 import Data.Int
 import Data.Ratio
+import Data.Word
+#ifdef HAVE_STATX
+import Data.Time.Clock (secondsToNominalDiffTime)
+import Data.Fixed (Fixed(..))
+#endif
 import Data.Time.Clock.POSIX (POSIXTime)
 import System.Posix.Internals
 import Foreign.C
@@ -103,7 +171,7 @@ import Foreign.Marshal (withArray)
 import Foreign.Ptr
 import Foreign.Storable
 
-#if !defined(HAVE_FCHMOD) || !defined(HAVE_CHOWN)
+#if !defined(HAVE_FCHMOD) || !defined(HAVE_CHOWN) || !defined(HAVE_STATX)
 import System.IO.Error ( ioeSetLocation )
 import GHC.IO.Exception ( unsupportedOperation )
 #endif
@@ -688,3 +756,593 @@ getFdPathVar (Fd fd) v =
 
 foreign import ccall unsafe "fpathconf"
   c_fpathconf :: CInt -> CInt -> IO CLong
+
+
+-- -----------------------------------------------------------------------------
+-- statx
+--
+
+newtype {-# CTYPE "__u64" #-} CAttributes = CAttributes Word64
+ deriving (Read, Show, Eq, Ord, Storable, Num, Bits)
+
+-- | Statx flags.
+--
+-- See the pattern synonyms for possible flags. These are combined via `(<>)`.
+-- Flags can be tested via `(.&.)`.
+--
+-- The following flags influence pathname-based lookup:
+--
+-- - 'EmptyPath'
+-- - 'NoAutoMount'
+-- - 'SymlinkNoFollow'
+--
+-- The following flags can be used to control what sort of synchronization the kernel will do when querying a file on a remote filesystem:
+--
+-- - 'SyncAsStat'
+-- - 'ForceSync'
+-- - 'DontSync'
+newtype StatxFlags = StatxFlags CInt deriving (Read, Show, Eq, Ord, Integral, Num, Enum, Bits, Real)
+
+-- | ORs the flags.
+instance Semigroup StatxFlags where
+   a <> b = a .|. b
+
+instance Monoid StatxFlags where
+  mappend = (<>)
+  mempty = 0
+
+-- | If pathname to 'getExtendedFileStatus' is an empty string, operate on the file referred to by
+-- the 'Maybe Fd' argument.
+--
+-- In this case, it can refer to any type of file, not just a directory.
+pattern EmptyPath :: StatxFlags
+#ifdef AT_EMPTY_PATH
+pattern EmptyPath = StatxFlags (#const AT_EMPTY_PATH)
+#else
+pattern EmptyPath = StatxFlags 0
+#endif
+
+-- | Don't automount the terminal ("basename") component of pathname if it is a directory that is an automount point.
+-- This allows the caller to gather attributes of an automount point (rather than the location it would mount).
+-- This flag can be used in tools that scan directories to prevent mass-automounting of a directory of automount points.
+-- This flag has no effect if the mount point has already been mounted over.
+pattern NoAutoMount :: StatxFlags
+#ifdef AT_NO_AUTOMOUNT
+pattern NoAutoMount = StatxFlags (#const AT_NO_AUTOMOUNT)
+#else
+pattern NoAutoMount = StatxFlags 0
+#endif
+
+-- | If pathname is a symbolic link, do not dereference it: instead return information about the link itself, like @lstat(2)@.
+pattern SymlinkNoFollow :: StatxFlags
+#ifdef AT_SYMLINK_NOFOLLOW
+pattern SymlinkNoFollow = StatxFlags (#const AT_SYMLINK_NOFOLLOW)
+#else
+pattern SymlinkNoFollow = StatxFlags 0
+#endif
+
+-- | Do whatever @stat(2)@ does. This is the default and is very much filesystem-specific.
+pattern SyncAsStat :: StatxFlags
+#ifdef AT_STATX_SYNC_AS_STAT
+pattern SyncAsStat = StatxFlags (#const AT_STATX_SYNC_AS_STAT)
+#else
+pattern SyncAsStat = StatxFlags 0
+#endif
+
+-- | Force the attributes to be synchronized with the server.
+-- This may require that a network filesystem perform a data writeback to get the timestamps correct.
+pattern ForceSync :: StatxFlags
+#ifdef AT_STATX_FORCE_SYNC
+pattern ForceSync = StatxFlags (#const AT_STATX_FORCE_SYNC)
+#else
+pattern ForceSync = StatxFlags 0
+#endif
+
+-- | Don't synchronize anything, but rather just take whatever the system has cached if possible.
+-- This may mean that the information returned is approximate, but, on a network filesystem,
+-- it may not involve a round trip to the server - even if no lease is held.
+pattern DontSync :: StatxFlags
+#ifdef AT_STATX_DONT_SYNC
+pattern DontSync = StatxFlags (#const AT_STATX_DONT_SYNC)
+#else
+pattern DontSync = StatxFlags 0
+#endif
+
+defaultStatxFlags :: StatxFlags
+defaultStatxFlags = mempty
+
+-- | Mask argument to 'statx'. It's used to tell the kernel which fields the caller is interested in.
+--
+-- See the pattern synonyms for possible masks. These are combined via @(<>)@.
+-- Masks can be tested via `(.&.)`.
+newtype StatxMask = StatxMask CInt deriving (Read, Show, Eq, Ord, Integral, Num, Enum, Bits, Real)
+
+-- | ORs the masks.
+instance Semigroup StatxMask where
+   a <> b = a .|. b
+
+instance Monoid StatxMask where
+  mappend = (<>)
+  mempty = 0
+
+-- | Want @stx_mode & S_IFMT@.
+pattern StatxType :: StatxMask
+#ifdef STATX_TYPE
+pattern StatxType = StatxMask (#const STATX_TYPE)
+#else
+pattern StatxType = StatxMask 0
+#endif
+
+-- | Want @stx_mode & ~S_IFMT@.
+pattern StatxMode :: StatxMask
+#ifdef STATX_MODE
+pattern StatxMode = StatxMask (#const STATX_MODE)
+#else
+pattern StatxMode = StatxMask 0
+#endif
+
+-- | Want @stx_nlink@.
+pattern StatxNlink :: StatxMask
+#ifdef STATX_NLINK
+pattern StatxNlink = StatxMask (#const STATX_NLINK)
+#else
+pattern StatxNlink = StatxMask 0
+#endif
+
+-- | Want @stx_uid@.
+pattern StatxUid :: StatxMask
+#ifdef STATX_UID
+pattern StatxUid = StatxMask (#const STATX_UID)
+#else
+pattern StatxUid = StatxMask 0
+#endif
+
+-- | Want @stx_gid@.
+pattern StatxGid :: StatxMask
+#ifdef STATX_GID
+pattern StatxGid = StatxMask (#const STATX_GID)
+#else
+pattern StatxGid = StatxMask 0
+#endif
+
+-- | Want @stx_atime@.
+pattern StatxAtime :: StatxMask
+#ifdef STATX_ATIME
+pattern StatxAtime = StatxMask (#const STATX_ATIME)
+#else
+pattern StatxAtime = StatxMask 0
+#endif
+
+-- | Want @stx_mtime@.
+pattern StatxMtime :: StatxMask
+#ifdef STATX_MTIME
+pattern StatxMtime = StatxMask (#const STATX_MTIME)
+#else
+pattern StatxMtime = StatxMask 0
+#endif
+
+-- | Want @stx_ctime@.
+pattern StatxCtime :: StatxMask
+#ifdef STATX_CTIME
+pattern StatxCtime = StatxMask (#const STATX_CTIME)
+#else
+pattern StatxCtime = StatxMask 0
+#endif
+
+-- | Want @stx_btime@.
+pattern StatxBtime :: StatxMask
+#ifdef STATX_BTIME
+pattern StatxBtime = StatxMask (#const STATX_BTIME)
+#else
+pattern StatxBtime = StatxMask 0
+#endif
+
+-- | Want @stx_mnt_id@.
+pattern StatxMntId :: StatxMask
+#ifdef STATX_MNT_ID
+pattern StatxMntId = StatxMask (#const STATX_MNT_ID)
+#else
+pattern StatxMntId = StatxMask 0
+#endif
+
+-- | Want @stx_ino@.
+pattern StatxIno :: StatxMask
+#ifdef STATX_INO
+pattern StatxIno = StatxMask (#const STATX_INO)
+#else
+pattern StatxIno = StatxMask 0
+#endif
+
+-- | Want @stx_size@.
+pattern StatxSize :: StatxMask
+#ifdef STATX_SIZE
+pattern StatxSize = StatxMask (#const STATX_SIZE)
+#else
+pattern StatxSize = StatxMask 0
+#endif
+
+-- | Want @stx_blocks@.
+pattern StatxBlocks :: StatxMask
+#ifdef STATX_BLOCKS
+pattern StatxBlocks = StatxMask (#const STATX_BLOCKS)
+#else
+pattern StatxBlocks = StatxMask 0
+#endif
+
+-- | Want all of the above.
+pattern StatxBasicStats :: StatxMask
+#ifdef STATX_BASIC_STATS
+pattern StatxBasicStats = StatxMask (#const STATX_BASIC_STATS)
+#else
+pattern StatxBasicStats = StatxMask 0
+#endif
+
+-- | Want all currently available fields.
+pattern StatxAll :: StatxMask
+#ifdef STATX_ALL
+pattern StatxAll = StatxMask (#const STATX_ALL)
+#else
+pattern StatxAll = StatxMask 0
+#endif
+
+
+defaultStatxMask :: StatxMask
+defaultStatxMask = mempty
+
+newtype ExtendedFileStatus = ExtendedFileStatus (ForeignPtr CStatx) -- ^ The constructor is considered internal and may change.
+
+-- | The "preferred" block size for efficient filesystem I/O.
+-- (Writing to a file in smaller chunks may cause an inefficient read-mod‐ify-rewrite.)
+fileBlockSizeX             :: ExtendedFileStatus -> CBlkSize
+#if HAVE_STATX
+-- | Further status information about the file.
+fileAttributesX            :: ExtendedFileStatus -> CAttributes
+#endif
+-- | The number of hard links on a file.
+linkCountX                 :: ExtendedFileStatus -> CNlink
+-- | Te user ID of the owner of the file.
+fileOwnerX                 :: ExtendedFileStatus -> UserID
+-- | The ID of the group owner of the file.
+fileGroupX                 :: ExtendedFileStatus -> GroupID
+-- | The file type and mode.  See @inode(7)@ for details.
+fileModeX                  :: ExtendedFileStatus -> FileMode
+-- | The inode number of the file.
+fileIDX                    :: ExtendedFileStatus -> FileID
+-- | The size of the file (if it is a regular file or a symbolic link) in bytes.
+-- The size of a symbolic link is the length of the pathname it contains,
+-- without a terminating null byte.
+fileSizeX                  :: ExtendedFileStatus -> Word64
+-- | The  number of blocks allocated to the file on the medium, in 512-byte units.
+-- (This may be smaller than stx_size/512 when the file has holes.)
+fileBlocksX                :: ExtendedFileStatus -> Word64
+#if HAVE_STATX
+-- | A mask indicating which bits in 'fileAttributesX' are supported by the VFS and the filesystem.
+fileAttributesMaskX        :: ExtendedFileStatus -> CAttributes
+#endif
+-- | The file's last access timestamp.
+accessTimeHiResX           :: ExtendedFileStatus -> POSIXTime
+-- | The file's creation timestamp.
+creationTimeHiResX         :: ExtendedFileStatus -> POSIXTime
+-- | The file's last status change timestamp.
+statusChangeTimeHiResX     :: ExtendedFileStatus -> POSIXTime
+-- | The file's last modification timestamp.
+modificationTimeHiResX     :: ExtendedFileStatus -> POSIXTime
+-- | ID of the device on which this file resides.
+deviceIDX                  :: ExtendedFileStatus -> DeviceID
+-- | Describes the device that this file represents.
+specialDeviceIDX       :: ExtendedFileStatus -> DeviceID
+-- | The mount ID of the mount containing the file. This is the same number
+-- reported by name_to_handle_at(2) and corresponds to the number in the
+-- first field in one of the records in /proc/self/mountinfo.
+mountIDX               :: ExtendedFileStatus -> Word64
+-- | The file is compressed by the filesystem and may take extra resources to access.
+-- This is an extended attribute.
+fileCompressedX            :: ExtendedFileStatus -> Bool
+-- | The file cannot be modified: it cannot be deleted or renamed, no hard links can
+-- be created to this file and no data can be written to it. See @chattr(1)@.
+-- This is an extended attribute.
+fileImmutableX             :: ExtendedFileStatus -> Bool
+-- | The file can only be opened in append mode for writing. Random access writing is not permitted. See @chattr(1)@.
+-- This is an extended attribute.
+fileAppendX                :: ExtendedFileStatus -> Bool
+-- | File is not a candidate for backup when a backup program such as @dump(8)@ is run. See @chattr(1)@.
+-- This is an extended attribute.
+fileNoDumpX                :: ExtendedFileStatus -> Bool
+-- | A key is required for the file to be encrypted by the filesystem.
+-- This is an extended attribute.
+fileEncryptedX             :: ExtendedFileStatus -> Bool
+-- | The file has fs-verity enabled.  It cannot be written to, and all reads from it
+-- will be verified  against a cryptographic hash that covers the entire file (e.g., via a Merkle tree).
+-- This is an extended attribute.
+-- Since Linux 5.5.
+fileVerityX                :: ExtendedFileStatus -> Bool
+-- | The  file is in the DAX (cpu direct access) state.
+-- This is an extended attribute.
+-- Since Linux 5.8.
+fileDaxX                   :: ExtendedFileStatus -> Bool
+
+-- | Checks if this file is a block device.
+isBlockDeviceX     :: ExtendedFileStatus -> Bool
+-- | Checks if this file is a character device.
+isCharacterDeviceX :: ExtendedFileStatus -> Bool
+-- | Checks if this file is a named pipe device.
+isNamedPipeX       :: ExtendedFileStatus -> Bool
+-- | Checks if this file is a regular file device.
+isRegularFileX     :: ExtendedFileStatus -> Bool
+-- | Checks if this file is a directory device.
+isDirectoryX       :: ExtendedFileStatus -> Bool
+-- | Checks if this file is a symbolic link device.
+isSymbolicLinkX    :: ExtendedFileStatus -> Bool
+-- | Checks if this file is a socket device.
+isSocketX          :: ExtendedFileStatus -> Bool
+
+isBlockDeviceX statx =
+  (fileModeX statx `intersectFileModes` fileTypeModes) == blockSpecialMode
+isCharacterDeviceX statx =
+  (fileModeX statx `intersectFileModes` fileTypeModes) == characterSpecialMode
+isNamedPipeX statx =
+  (fileModeX statx `intersectFileModes` fileTypeModes) == namedPipeMode
+isRegularFileX statx =
+  (fileModeX statx `intersectFileModes` fileTypeModes) == regularFileMode
+isDirectoryX statx =
+  (fileModeX statx `intersectFileModes` fileTypeModes) == directoryMode
+isSymbolicLinkX statx =
+  (fileModeX statx `intersectFileModes` fileTypeModes) == symbolicLinkMode
+isSocketX statx =
+  (fileModeX statx `intersectFileModes` fileTypeModes) == socketMode
+
+#if HAVE_STATX
+testFlag :: ExtendedFileStatus -> CAttributes -> Bool
+testFlag ex flag =
+  let attributes = fileAttributesX ex
+      attributes_mask = fileAttributesMaskX ex
+  in (attributes .&. attributes_mask .&. flag) /= 0
+
+#ifdef STATX_ATTR_COMPRESSED
+fileCompressedX ex = testFlag ex (#const STATX_ATTR_COMPRESSED)
+#else
+{-# WARNING fileCompressedX "fileCompressedX: not available on this platform, will default to 'False' (CPP guard: @#if STATX_ATTR_COMPRESSED@)" #-}
+fileCompressedX _ = False
+#endif
+#ifdef STATX_ATTR_IMMUTABLE
+fileImmutableX ex = testFlag ex (#const STATX_ATTR_IMMUTABLE)
+#else
+{-# WARNING fileImmutableX "fileImmutableX: not available on this platform, will default to 'False' (CPP guard: @#if STATX_ATTR_IMMUTABLE@)" #-}
+fileImmutableX _ = False
+#endif
+#ifdef STATX_ATTR_APPEND
+fileAppendX ex = testFlag ex (#const STATX_ATTR_APPEND)
+#else
+{-# WARNING fileAppendX "fileAppendX: not available on this platform, will default to 'False' (CPP guard: @#if STATX_ATTR_APPEND@)" #-}
+fileAppendX _ = False
+#endif
+#ifdef STATX_ATTR_NODUMP
+fileNoDumpX ex = testFlag ex (#const STATX_ATTR_NODUMP)
+#else
+{-# WARNING fileNoDumpX "fileNoDumpX: not available on this platform, will default to 'False' (CPP guard: @#if STATX_ATTR_NODUMP@)" #-}
+fileNoDumpX _ = False
+#endif
+#ifdef STATX_ATTR_ENCRYPTED
+fileEncryptedX ex = testFlag ex (#const STATX_ATTR_ENCRYPTED)
+#else
+{-# WARNING fileEncryptedX "fileEncryptedX: not available on this platform, will default to 'False' (CPP guard: @#if STATX_ATTR_ENCRYPTED@)" #-}
+fileEncryptedX _ = False
+#endif
+#ifdef STATX_ATTR_VERITY
+fileVerityX ex = testFlag ex (#const STATX_ATTR_VERITY)
+#else
+{-# WARNING fileVerityX "fileVerityX: not available on this platform, will default to 'False' (CPP guard: @#if STATX_ATTR_VERITY@)" #-}
+fileVerityX _ = False
+#endif
+#ifdef STATX_ATTR_DAX
+fileDaxX ex = testFlag ex (#const STATX_ATTR_DAX)
+#else
+{-# WARNING fileDaxX "fileDaxX: not available on this platform, will default to 'False' (CPP guard: @#if STATX_ATTR_DAX@)" #-}
+fileDaxX _ = False
+#endif
+
+#ifdef HAVE_SYS_SYSMACROS_H
+deviceIDX (ExtendedFileStatus statx) = unsafePerformIO $ do
+  major <- withForeignPtr statx $ (#peek struct statx, stx_dev_major) :: IO CUInt
+  minor <- withForeignPtr statx $ (#peek struct statx, stx_dev_minor) :: IO CUInt
+  c_makedev major minor
+#else
+{-# WARNING deviceIDX "deviceIDX: not available on this platform, will throw error (CPP guard: @#if HAVE_SYS_SYSMACROS_H@)" #-}
+deviceIDX _ = error "deviceIDX not available on this platform"
+#endif
+#ifdef HAVE_SYS_SYSMACROS_H
+specialDeviceIDX (ExtendedFileStatus statx) = unsafePerformIO $ do
+  major <- withForeignPtr statx $ (#peek struct statx, stx_rdev_major) :: IO CUInt
+  minor <- withForeignPtr statx $ (#peek struct statx, stx_rdev_minor) :: IO CUInt
+  c_makedev major minor
+#else
+{-# WARNING specialDeviceIDX "specialDeviceIDX: not available on this platform, will throw error (CPP guard: @#if HAVE_SYS_SYSMACROS_H@)" #-}
+specialDeviceIDX _ = error "specialDeviceIDX not available on this platform"
+#endif
+#ifdef STATX_MNT_ID
+mountIDX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ (#peek struct statx, stx_mnt_id)
+#else
+{-# WARNING mountIDX "mountIDX: not available on this platform, will throw error (CPP guard: @#if STATX_MNT_ID@)" #-}
+mountIDX _ = error "mountIDX not available on this platform"
+#endif
+fileBlockSizeX (ExtendedFileStatus statx) = unsafePerformIO $ do
+  r <- withForeignPtr statx $ (#peek struct statx, stx_blksize) :: IO Word32
+  return $ CBlkSize (fromIntegral r)
+fileAttributesX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ (#peek struct statx, stx_attributes)
+#ifdef STATX_NLINK
+linkCountX (ExtendedFileStatus statx) = unsafePerformIO $ do
+  links <- withForeignPtr statx $ (#peek struct statx, stx_nlink) :: IO Word32
+  return $ CNlink (fromIntegral links)
+#else
+{-# WARNING linkCountX "linkCountX: not available on this platform, will throw error (CPP guard: @#if STATX_NLINK@)" #-}
+linkCountX _ = error "linkCountX not available on this platform"
+#endif
+#ifdef STATX_UID
+fileOwnerX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ (#peek struct statx, stx_uid)
+#else
+{-# WARNING fileOwnerX "fileOwnerX: not available on this platform, will throw error (CPP guard: @#if STATX_UID@)" #-}
+fileOwnerX _ = error "fileOwnerX not available on this platform"
+#endif
+#ifdef STATX_GID
+fileGroupX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ (#peek struct statx, stx_gid)
+#else
+{-# WARNING fileGroupX "fileGroupX: not available on this platform, will throw error (CPP guard: @#if STATX_GID@)" #-}
+fileGroupX _ = error "fileGroupX not available on this platform"
+#endif
+#ifdef STATX_MODE
+fileModeX (ExtendedFileStatus statx) = unsafePerformIO $ do
+  r <- withForeignPtr statx $ (#peek struct statx, stx_mode) :: IO Word16
+  return $ CMode $ fromIntegral r
+#else
+{-# WARNING fileModeX "fileModeX: not available on this platform, will throw error (CPP guard: @#if STATX_MODE@)" #-}
+fileModeX _ = error "fileModeX not available on this platform"
+#endif
+#ifdef STATX_INO
+fileIDX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ (#peek struct statx, stx_ino)
+#else
+{-# WARNING fileIDX "fileIDX: not available on this platform, will throw error (CPP guard: @#if STATX_INO@)" #-}
+fileIDX _ = error "fileIDX not available on this platform"
+#endif
+#ifdef STATX_SIZE
+fileSizeX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ (#peek struct statx, stx_size) :: Word64
+#else
+{-# WARNING fileSizeX "fileSizeX: not available on this platform, will throw error (CPP guard: @#if STATX_SIZE@)" #-}
+fileSizeX _ = error "fileSizeX not available on this platform"
+#endif
+#ifdef STATX_BLOCKS
+fileBlocksX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ (#peek struct statx, stx_blocks) :: Word64
+#else
+{-# WARNING fileBlocksX "fileBlocksX: not available on this platform, will throw error (CPP guard: @#if STATX_BLOCKS@)" #-}
+fileBlocksX _ = error "fileBlocksX not available on this platform"
+#endif
+fileAttributesMaskX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ (#peek struct statx, stx_attributes_mask)
+#ifdef STATX_ATIME
+accessTimeHiResX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ \statx_ptr -> do
+    sec  <- (#peek struct statx, stx_atime.tv_sec) statx_ptr :: IO EpochTime
+    nsec <- (#peek struct statx, stx_atime.tv_nsec) statx_ptr :: IO (#type int)
+    return $ timeHiResToNominalDiffTime sec nsec
+#else
+{-# WARNING accessTimeHiResX "accessTimeHiResX: not available on this platform, will throw error (CPP guard: @#if STATX_ATIME@)" #-}
+accessTimeHiResX _ = error "accessTimeHiResX not available on this platform"
+#endif
+#ifdef STATX_BTIME
+creationTimeHiResX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ \statx_ptr -> do
+    sec  <- (#peek struct statx, stx_btime.tv_sec) statx_ptr :: IO EpochTime
+    nsec <- (#peek struct statx, stx_btime.tv_nsec) statx_ptr :: IO (#type int)
+    return $ timeHiResToNominalDiffTime sec nsec
+#else
+{-# WARNING creationTimeHiResX "creationTimeHiResX: not available on this platform, will throw error (CPP guard: @#if STATX_BTIME@)" #-}
+creationTimeHiResX _ = error "creationTimeHiResX not available on this platform"
+#endif
+#ifdef STATX_CTIME
+statusChangeTimeHiResX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ \statx_ptr -> do
+    sec  <- (#peek struct statx, stx_ctime.tv_sec) statx_ptr :: IO EpochTime
+    nsec <- (#peek struct statx, stx_ctime.tv_nsec) statx_ptr :: IO (#type int)
+    return $ timeHiResToNominalDiffTime sec nsec
+#else
+{-# WARNING statusChangeTimeHiResX "statusChangeTimeHiResX: not available on this platform, will throw error (CPP guard: @#if STATX_CTIME@)" #-}
+statusChangeTimeHiResX _ = error "statusChangeTimeHiResX not available on this platform"
+#endif
+#ifdef STATX_MTIME
+modificationTimeHiResX (ExtendedFileStatus statx) =
+  unsafePerformIO $ withForeignPtr statx $ \statx_ptr -> do
+    sec  <- (#peek struct statx, stx_mtime.tv_sec) statx_ptr :: IO EpochTime
+    nsec <- (#peek struct statx, stx_mtime.tv_nsec) statx_ptr :: IO (#type int)
+    return $ timeHiResToNominalDiffTime sec nsec
+#else
+{-# WARNING modificationTimeHiResX "modificationTimeHiResX: not available on this platform, will throw error (CPP guard: @#if STATX_MTIME@)" #-}
+modificationTimeHiResX _ = error "modificationTimeHiResX not available on this platform"
+#endif
+
+timeHiResToNominalDiffTime :: EpochTime -> Int32 -> POSIXTime
+timeHiResToNominalDiffTime (CTime sec) nsec = secondsToNominalDiffTime $ MkFixed $ toInteger sec * 1e12 + toInteger nsec * 1e3
+
+#else
+{-# WARNING linkCountX "linkCountX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+linkCountX _ = error "linkCountX not available on this platform"
+{-# WARNING fileBlockSizeX "fileBlockSizeX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+fileBlockSizeX _ = error "fileBlockSizeX not available on this platform"
+{-# WARNING deviceIDX "deviceIDX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+deviceIDX _ = error "deviceIDX not available on this platform"
+{-# WARNING specialDeviceIDX "specialDeviceIDX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+specialDeviceIDX _ = error "specialDeviceIDX not available on this platform"
+{-# WARNING mountIDX "mountIDX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+mountIDX _ = error "mountIDX not available on this platform"
+{-# WARNING fileOwnerX "fileOwnerX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+fileOwnerX _ = error "fileOwnerX not available on this platform"
+{-# WARNING fileGroupX "fileGroupX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+fileGroupX _ = error "fileGroupX not available on this platform"
+{-# WARNING fileModeX "fileModeX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+fileModeX _ = error "fileModeX not available on this platform"
+{-# WARNING fileIDX "fileIDX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+fileIDX _ = error "fileIDX not available on this platform"
+{-# WARNING fileSizeX "fileSizeX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+fileSizeX _ = error "fileSizeX not available on this platform"
+{-# WARNING fileBlocksX "fileBlocksX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+fileBlocksX _ = error "fileBlocksX not available on this platform"
+{-# WARNING accessTimeHiResX "accessTimeHiResX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+accessTimeHiResX _ = error "accessTimeHiResX not available on this platform"
+{-# WARNING creationTimeHiResX "creationTimeHiResX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+creationTimeHiResX _ = error "creationTimeHiResX not available on this platform"
+{-# WARNING statusChangeTimeHiResX "statusChangeTimeHiResX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+statusChangeTimeHiResX _ = error "statusChangeTimeHiResX not available on this platform"
+{-# WARNING modificationTimeHiResX "modificationTimeHiResX: not available on this platform, will throw error (CPP guard: @#if HAVE_STATX@)" #-}
+modificationTimeHiResX _ = error "modificationTimeHiResX not available on this platform"
+{-# WARNING fileCompressedX "fileCompressedX: not available on this platform, will default to 'False' (CPP guard: @#if HAVE_STATX@)" #-}
+fileCompressedX _ = False
+{-# WARNING fileImmutableX "fileImmutableX: not available on this platform, will default to 'False' (CPP guard: @#if HAVE_STATX@)" #-}
+fileImmutableX _ = False
+{-# WARNING fileAppendX "fileAppendX: not available on this platform, will default to 'False' (CPP guard: @#if HAVE_STATX@)" #-}
+fileAppendX _ = False
+{-# WARNING fileNoDumpX "fileNoDumpX: not available on this platform, will default to 'False' (CPP guard: @#if HAVE_STATX@)" #-}
+fileNoDumpX _ = False
+{-# WARNING fileEncryptedX "fileEncryptedX: not available on this platform, will default to 'False' (CPP guard: @#if HAVE_STATX@)" #-}
+fileEncryptedX _ = False
+{-# WARNING fileVerityX "fileVerityX: not available on this platform, will default to 'False' (CPP guard: @#if HAVE_STATX@)" #-}
+fileVerityX _ = False
+{-# WARNING fileDaxX "fileDaxX: not available on this platform, will default to 'False' (CPP guard: @#if HAVE_STATX@)" #-}
+fileDaxX _ = False
+#endif
+
+data {-# CTYPE "struct statx" #-} CStatx
+
+#ifdef HAVE_STATX
+foreign import capi unsafe "sys/stat.h statx"
+   c_statx :: CInt -> CFilePath -> CInt -> CInt -> Ptr CStatx -> IO CInt
+
+#ifdef HAVE_SYS_SYSMACROS_H
+foreign import capi unsafe "sys/sysmacros.h makedev"
+  c_makedev :: CUInt -> CUInt -> IO CDev
+#endif
+#endif
+
+
+getExtendedFileStatus_ :: Maybe Fd  -- ^ Optional directory file descriptor
+                       -> CString   -- ^ Pathname to open
+                       -> StatxFlags
+                       -> StatxMask
+                       -> IO ExtendedFileStatus
+#ifdef HAVE_STATX
+getExtendedFileStatus_ fdMay str (StatxFlags flags) (StatxMask masks) = do
+  fp <- mallocForeignPtrBytes (#const sizeof(struct statx))
+  withForeignPtr fp $ \p ->
+      throwErrnoIfMinus1_ "getExtendedFileStatus_" (c_statx c_fd str flags masks p)
+  return (ExtendedFileStatus fp)
+ where
+  c_fd = maybe (#const AT_FDCWD) (\ (Fd fd) -> fd) fdMay
+#else
+{-# WARNING getExtendedFileStatus_ "operation will throw 'IOError' \"unsupported operation\" (CPP guard: @#if HAVE_STATX@)" #-}
+getExtendedFileStatus_ _ _ _ _ = ioError (ioeSetLocation unsupportedOperation "getExtendedFileStatus")
+#endif
+
diff --git a/System/Posix/Files/PosixString.hsc b/System/Posix/Files/PosixString.hsc
index 2873827..1fdc5d8 100644
--- a/System/Posix/Files/PosixString.hsc
+++ b/System/Posix/Files/PosixString.hsc
@@ -1,4 +1,5 @@
 {-# LANGUAGE CApiFFI #-}
+{-# LANGUAGE PatternSynonyms #-}
 
 -----------------------------------------------------------------------------
 -- |
@@ -58,6 +59,69 @@ module System.Posix.Files.PosixString (
     isBlockDevice, isCharacterDevice, isNamedPipe, isRegularFile,
     isDirectory, isSymbolicLink, isSocket,
 
+    -- * Extended file status
+    ExtendedFileStatus(..),
+    CAttributes(..),
+    -- ** Obtaining extended file status
+    getExtendedFileStatus,
+    -- ** Flags
+    StatxFlags(..),
+    defaultStatxFlags,
+    pattern EmptyPath,
+    pattern NoAutoMount,
+    pattern SymlinkNoFollow,
+    pattern SyncAsStat,
+    pattern ForceSync,
+    pattern DontSync,
+    -- ** Mask
+    StatxMask(..),
+    defaultStatxMask,
+    pattern StatxType,
+    pattern StatxMode,
+    pattern StatxNlink,
+    pattern StatxUid,
+    pattern StatxGid,
+    pattern StatxAtime,
+    pattern StatxMtime,
+    pattern StatxCtime,
+    pattern StatxIno,
+    pattern StatxSize,
+    pattern StatxBlocks,
+    pattern StatxBasicStats,
+    pattern StatxBtime,
+    pattern StatxMntId,
+    pattern StatxAll,
+    -- ** Querying extended file status
+    fileBlockSizeX,
+    linkCountX,
+    fileOwnerX,
+    fileGroupX,
+    fileModeX,
+    fileIDX,
+    fileSizeX,
+    fileBlocksX,
+    accessTimeHiResX,
+    creationTimeHiResX,
+    statusChangeTimeHiResX,
+    modificationTimeHiResX,
+    deviceIDX,
+    specialDeviceIDX,
+    mountIDX,
+    fileCompressedX,
+    fileImmutableX,
+    fileAppendX,
+    fileNoDumpX,
+    fileEncryptedX,
+    fileVerityX,
+    fileDaxX,
+    isBlockDeviceX,
+    isCharacterDeviceX,
+    isNamedPipeX,
+    isRegularFileX,
+    isDirectoryX,
+    isSymbolicLinkX,
+    isSocketX,
+
     -- * Creation
     createNamedPipe,
     createDevice,
@@ -102,7 +166,8 @@ import Foreign.C hiding (
      throwErrnoPathIfMinus1_ )
 
 import System.OsPath.Types
-import System.Posix.Files hiding (getFileStatus, getSymbolicLinkStatus, createNamedPipe, createDevice, createLink, removeLink, createSymbolicLink, readSymbolicLink, rename, setOwnerAndGroup, setSymbolicLinkOwnerAndGroup, setFileTimes, setSymbolicLinkTimesHiRes, touchFile, touchSymbolicLink, setFileSize, getPathVar, setFileMode, fileAccess, fileExist, setFdTimesHiRes, setFileTimesHiRes)
+import System.Posix.Files hiding (getFileStatus, getExtendedFileStatus, getSymbolicLinkStatus, createNamedPipe, createDevice, createLink, removeLink, createSymbolicLink, readSymbolicLink, rename, setOwnerAndGroup, setSymbolicLinkOwnerAndGroup, setFileTimes, setSymbolicLinkTimesHiRes, touchFile, touchSymbolicLink, setFileSize, getPathVar, setFileMode, fileAccess, fileExist, setFdTimesHiRes, setFileTimesHiRes)
+import System.Posix.Files.Common (getExtendedFileStatus_)
 import System.Posix.PosixPath.FilePath
 
 import Data.Time.Clock.POSIX (POSIXTime)
@@ -182,6 +247,25 @@ getFileStatus path = do
       throwErrnoPathIfMinus1Retry_ "getFileStatus" path (c_stat s p)
   return (Common.FileStatus fp)
 
+-- | Gets extended file status information.
+--
+-- The target file to open is identified in one of the following ways:
+--
+-- - If @pathname@ begins with a slash, then it is an absolute pathname that identifies the target file. In this case, @dirfd@ is ignored
+-- - If @pathname@ is a string that begins with a character other than a slash and @dirfd@ is a file descriptor that refers to a
+--   directory, then pathname is a relative pathname that is interpreted relative to the directory referred to by dirfd.
+--   (See @openat(2)@ for an explanation of why this is useful.)
+-- - If @pathname@ is an empty string and the 'EmptyPath' flag is specified in flags (see below), then the target file is
+--   the one referred to by the file descriptor @dirfd@.
+--
+-- Note: calls @statx@.
+getExtendedFileStatus :: Maybe Fd   -- ^ Optional directory file descriptor (@dirfd@)
+                      -> PosixPath  -- ^ @pathname@ to open
+                      -> StatxFlags -- ^ flags
+                      -> StatxMask  -- ^ mask
+                      -> IO ExtendedFileStatus
+getExtendedFileStatus mfd path flags masks = withFilePath path $ \s -> getExtendedFileStatus_ mfd s flags masks
+
 -- | Acts as 'getFileStatus' except when the 'PosixPath' refers to a symbolic
 -- link. In that case the @FileStatus@ information of the symbolic link itself
 -- is returned instead of that of the file it points to.
diff --git a/changelog.md b/changelog.md
index dec5787..d8f2dcb 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,10 @@
 # Changelog for [`unix` package](http://hackage.haskell.org/package/unix)
 
+## 2.8.3.0 *??? 2023*
+
+  * add `getExtendedFileStatus` (based on `statx`) style functions
+  * drop support for GHC < 8.6
+
 ## 2.8.2.1 *Sep 2023*
 
   * Fix UB bug in `withFilePath` that causes it to error out (introduced in 2.8.2.0) wrt [#295](https://github.com/haskell/unix/issues/295)
diff --git a/configure.ac b/configure.ac
index d7d09a5..978b7ad 100644
--- a/configure.ac
+++ b/configure.ac
@@ -23,6 +23,7 @@ AC_SYS_LARGEFILE
 AC_CHECK_HEADERS([dirent.h fcntl.h grp.h limits.h pwd.h signal.h string.h])
 AC_CHECK_HEADERS([sys/resource.h sys/stat.h sys/times.h sys/time.h])
 AC_CHECK_HEADERS([sys/utsname.h sys/wait.h])
+AC_CHECK_HEADERS([sys/sysmacros.h])
 AC_CHECK_HEADERS([bsd/libutil.h libutil.h pty.h utmp.h])
 AC_CHECK_HEADERS([termios.h time.h unistd.h utime.h])
 
@@ -46,6 +47,10 @@ AC_CHECK_FUNCS([chown fchdir fchmod])
 
 AC_CHECK_TYPE([struct rlimit],[AC_DEFINE([HAVE_STRUCT_RLIMIT],[1],[HAVE_STRUCT_RLIMIT])],[],[#include <sys/resource.h>])
 
+# check for statx
+AC_CHECK_FUNC([statx], [AC_DEFINE([HAVE_STATX_FUN],[1],[HAVE_STATX_FUN])],[],[#include <sys/stat.h>])
+AC_CHECK_TYPE([struct statx],[AC_DEFINE([HAVE_STRUCT_STATX],[1],[HAVE_STRUCT_STATX])],[],[#include <sys/stat.h>])
+
 AC_MSG_CHECKING(for F_GETLK from fcntl.h)
 AC_EGREP_CPP(yes,
 [
diff --git a/include/HsUnix.h b/include/HsUnix.h
index 01add31..7639685 100644
--- a/include/HsUnix.h
+++ b/include/HsUnix.h
@@ -90,6 +90,9 @@
 #ifdef HAVE_SIGNAL_H
 #include <signal.h>
 #endif
+#ifdef HAVE_SYS_SYSMACROS_H
+#include <sys/sysmacros.h>
+#endif
 
 /* defined in rts/posix/Signals.c */
 extern HsInt nocldstop;
@@ -123,4 +126,8 @@ int __hsunix_push_module(int fd, const char *module);
 clock_t __hsunix_clocks_per_second (void);
 #endif
 
+#if defined(HAVE_SYS_STAT_H) && defined(HAVE_STATX_FUN) && defined(HAVE_STRUCT_STATX)
+#define HAVE_STATX 1
+#endif
+
 #endif
diff --git a/tests/FileExtendedStatus.hs b/tests/FileExtendedStatus.hs
new file mode 100644
index 0000000..0885593
--- /dev/null
+++ b/tests/FileExtendedStatus.hs
@@ -0,0 +1,166 @@
+{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
+
+module FileExtendedStatus (main) where
+
+import System.Posix.Files
+import System.Posix.Directory
+import System.Posix.IO
+import System.Posix.Types
+import Control.Exception as E
+import Control.Monad
+import Test.Tasty.HUnit
+
+main = do
+  cleanup
+  fs <- testRegular
+  ds <- testDir
+  testSymlink fs ds
+  testLink
+  cleanup
+
+regular       = "regular"
+dir           = "dir"
+slink_regular = "link-regular-symlink"
+hlink_regular = "link-regular-hardlink"
+link_dir      = "link-dir"
+
+testRegular = do
+  _ <- createFile regular ownerReadMode
+  (fs, _) <- getStatus regular
+  let expected = (False,False,False,True,False,False,False)
+      actual   = snd (statusExtendedElements fs)
+  when (actual /= expected) $
+    fail "unexpected file status bits for regular file"
+  return fs
+
+testDir = do
+  createDirectory dir ownerReadMode
+  (ds, _) <- getStatus dir
+  let expected = (False,False,False,False,True,False,False)
+      actual   = snd (statusExtendedElements ds)
+  when (actual /= expected) $
+    fail "unexpected file status bits for directory"
+  return ds
+
+testSymlink fs ds = do
+  createSymbolicLink regular slink_regular
+  createSymbolicLink dir     link_dir
+  (fs', ls)  <- getStatus slink_regular
+  (ds', lds) <- getStatus link_dir
+
+  let expected = (False,False,False,False,False,True,False)
+      actualF  = snd (statusExtendedElements ls)
+      actualD  = snd (statusExtendedElements lds)
+
+  when (actualF /= expected) $
+    fail "unexpected file status bits for symlink to regular file"
+
+  when (actualD /= expected) $
+    fail "unexpected file status bits for symlink to directory"
+
+  when (statusExtendedElements fs /= statusExtendedElements fs') $
+    fail "status for a file does not match when it's accessed via a symlink"
+
+  when (statusExtendedElements ds /= statusExtendedElements ds') $
+    fail "status for a directory does not match when it's accessed via a symlink"
+
+
+testLink = do
+  createLink regular hlink_regular
+  (fs, _)  <- getStatus regular -- we need to retrieve it again as creating the link causes it to change!
+  (fs', ls)  <- getStatus hlink_regular
+  snd (statusExtendedElements ls) @?= (
+                False, -- isBlockDevice
+                False, -- isCharacterDevice
+                False, -- isNamedPipe
+                True,  -- isRegularFile
+                False, -- isDirectory
+                False, -- isSymbolicLink
+                False) -- isSocket
+  linkCountX fs' @?= 2
+  statusExtendedElements fs @?= statusExtendedElements fs' -- status for a file should match when accessed via a link
+
+
+cleanup = do
+  ignoreIOExceptions $ removeDirectory dir
+  mapM_ (ignoreIOExceptions . removeLink)
+        [regular, hlink_regular, slink_regular, link_dir]
+
+ignoreIOExceptions io = io `E.catch`
+                        ((\_ -> return ()) :: IOException -> IO ())
+
+getStatus f = do
+  fs  <- getExtendedFileStatus Nothing f defaultStatxFlags defaultStatxMask
+  ls  <- getExtendedFileStatus Nothing f SymlinkNoFollow defaultStatxMask
+  fs' <- getFileStatus f
+
+  statusExtendedElementsMinimal fs @?= statusElementsMinimal fs'
+
+  return (fs, ls)
+
+-- Yay for 20-element tuples!
+statusExtendedElements fs = (,)
+  (fileBlockSizeX fs
+  ,linkCountX fs
+  ,fileOwnerX fs
+  ,fileGroupX fs
+  ,fileModeX fs
+  ,fileIDX fs
+  ,fileSizeX fs
+  ,accessTimeHiResX fs
+  ,creationTimeHiResX fs
+  ,statusChangeTimeHiResX fs
+  ,modificationTimeHiResX fs
+  )
+  (isBlockDeviceX fs
+  ,isCharacterDeviceX fs
+  ,isNamedPipeX fs
+  ,isRegularFileX fs
+  ,isDirectoryX fs
+  ,isSymbolicLinkX fs
+  ,isSocketX fs
+  )
+
+statusExtendedElementsMinimal fs = (,)
+  (fileModeX fs
+  ,deviceIDX fs
+  ,specialDeviceIDX fs
+  ,linkCountX fs
+  ,fileOwnerX fs
+  ,fileGroupX fs
+  ,COff (fromIntegral (fileSizeX fs))
+  ,Just $ CBlkCnt (fromIntegral (fileBlocksX fs))
+  ,accessTimeHiResX fs
+  ,statusChangeTimeHiResX fs
+  ,modificationTimeHiResX fs
+  )
+  (isBlockDeviceX fs
+  ,isCharacterDeviceX fs
+  ,isNamedPipeX fs
+  ,isRegularFileX fs
+  ,isDirectoryX fs
+  ,isSymbolicLinkX fs
+  ,isSocketX fs
+  )
+
+statusElementsMinimal fs = (,)
+  (fileMode fs
+  ,deviceID fs
+  ,specialDeviceID fs
+  ,linkCount fs
+  ,fileOwner fs
+  ,fileGroup fs
+  ,fileSize fs
+  ,fileBlocks fs
+  ,accessTimeHiRes fs
+  ,statusChangeTimeHiRes fs
+  ,modificationTimeHiRes fs
+  )
+  (isBlockDevice fs
+  ,isCharacterDevice fs
+  ,isNamedPipe fs
+  ,isRegularFile fs
+  ,isDirectory fs
+  ,isSymbolicLink fs
+  ,isSocket fs
+  )
diff --git a/tests/Test.hs b/tests/Test.hsc
similarity index 97%
rename from tests/Test.hs
rename to tests/Test.hsc
index 5ce28ec..f6771cf 100644
--- a/tests/Test.hs
+++ b/tests/Test.hsc
@@ -4,6 +4,8 @@
 {-# OPTIONS_GHC -fno-warn-deprecations #-}
 {-# OPTIONS_GHC -fno-warn-unused-imports #-}
 
+#include "HsUnix.h"
+
 module Main (main) where
 
 import Control.Applicative
@@ -25,6 +27,7 @@ import Test.Tasty.HUnit
 import Test.Tasty.QuickCheck
 
 import qualified FileStatus
+import qualified FileExtendedStatus
 import qualified FileStatusByteString
 import qualified Signals001
 
@@ -37,6 +40,9 @@ main = defaultMain $ testGroup "All"
   , testGroup "Native"
     [ executeFile001               -- JS: missing "pipe"
     , fileStatus                   -- JS: missing "openat"
+#ifdef HAVE_STATX
+    , fileExtendedStatus           -- JS: missing "openat"
+#endif
     , fileStatusByteString         -- JS: missing "openat"
     , getEnvironment01             -- JS: missing "environ"
     , testSystemPosixEnvByteString -- JS: missing "environ"
@@ -74,6 +80,9 @@ fileExist01 = testCase "fileExist01" $ do
 fileStatus :: TestTree
 fileStatus = testCase "fileStatus" FileStatus.main
 
+fileExtendedStatus :: TestTree
+fileExtendedStatus = testCase "fileExtendedStatus" FileExtendedStatus.main
+
 fileStatusByteString :: TestTree
 fileStatusByteString = testCase "fileStatusByteString" FileStatusByteString.main
 
diff --git a/unix.cabal b/unix.cabal
index 94de693..8942c17 100644
--- a/unix.cabal
+++ b/unix.cabal
@@ -1,6 +1,6 @@
 cabal-version:  1.12
 name:           unix
-version:        2.8.2.1
+version:        2.8.3.0
 -- NOTE: Don't forget to update ./changelog.md
 
 license:        BSD3
@@ -11,13 +11,13 @@ bug-reports:    https://github.com/haskell/unix/issues
 synopsis:       POSIX functionality
 category:       System
 build-type:     Configure
-tested-with:    GHC==9.2.4,
+tested-with:    GHC==9.6.2,
+                GHC==9.4.7,
+                GHC==9.2.8,
                 GHC==9.0.2,
                 GHC==8.10.7,
                 GHC==8.8.4,
-                GHC==8.6.5,
-                GHC==8.4.4,
-                GHC==8.2.2
+                GHC==8.6.5
 description:
     This package gives you access to the set of operating system
     services standardised by
@@ -69,10 +69,10 @@ library
         buildable: False
 
     build-depends:
-        base        >= 4.10    && < 4.20,
-        bytestring  >= 0.9.2   && < 0.13,
+        base        >= 4.12.0.0  && < 4.20,
+        bytestring  >= 0.9.2     && < 0.13,
         filepath    >= 1.4.100.0 && < 1.5,
-        time        >= 1.2     && < 1.13
+        time        >= 1.9.1     && < 1.13
 
     exposed-modules:
         System.Posix
@@ -167,6 +167,7 @@ test-suite unix-tests
     main-is: Test.hs
     other-modules:
         FileStatus
+        FileExtendedStatus
         FileStatusByteString
         Signals001
     type: exitcode-stdio-1.0
-- 
GitLab