packages feed

halfs-2.0: Halfs/CoreAPI.hs

{-# LANGUAGE FlexibleContexts, ScopedTypeVariables, RankNTypes #-}
module Halfs.CoreAPI where

import Control.Exception (assert)
import Data.ByteString   (ByteString)
import Data.Serialize hiding (isEmpty)
import Data.Word
import Foreign.C.Error hiding (throwErrno)
import System.FilePath

import qualified Data.ByteString  as BS
import qualified Data.List        as L
import qualified Data.Map         as M
import qualified Data.Traversable as T

import Halfs.BlockMap
import Halfs.Classes
import Halfs.Directory
import Halfs.Errors
import Halfs.File
import Halfs.HalfsState
import Halfs.Inode
import Halfs.Monad
import Halfs.MonadUtils
import Halfs.Protection
import Halfs.SuperBlock
import Halfs.Types
import Halfs.Utils

import qualified Halfs.File as F

import System.Device.BlockDevice

-- import Debug.Trace

type HalfsM b r l m a = HalfsT HalfsError (Maybe (HalfsState b r l m)) m a

data SyncType = Data | Everything

data FileSystemStats = FSS
  { fssBlockSize       :: Integer -- ^ fundamental file system block size
  , fssBlockCount      :: Integer -- ^ #data blocks in filesystem
  , fssBlocksFree      :: Integer -- ^ #free blocks in filesystem
  , fssBlocksAvailable :: Integer -- ^ #free blocks avail to non-root
  , fssFileCount       :: Integer -- ^ #num inodes in filesystem
  , fssFilesFree       :: Integer -- ^ #free inodes in filesystem
  }
  deriving (Show)


--------------------------------------------------------------------------------
-- Filesystem init, teardown, and check functions

-- | Create a new file system on the given device. The new file system
-- will use the block size reported by bdBlockSize on the device; if you
-- want to increase the file system's block size, you should use
-- 'newRescaledBlockDevice' from System.Device.BlockDevice.
--
-- Block size has no bearing on the maximum size of a file or directory, but
-- will impact the size of certain internal data structures that keep track of
-- free blocks. Larger block sizes will decrease the cost of these structures
-- per underlying disk megabyte, but may also lead to more wasted space for
-- small files and directories.

newfs :: (HalfsCapable b t r l m) =>
         BlockDevice m
      -> UserID
      -> GroupID
      -> FileMode
      -> HalfsM b r l m SuperBlock
newfs dev uid gid rdirPerms = do
  when (superBlockSize > bdBlockSize dev) $
    fail "The device's block size is insufficiently large!"

  blockMap <- lift $ newBlockMap dev
  initFree <- readRef (bmNumFree blockMap)
  rdirAddr <- lift (alloc1 blockMap) >>=
                maybe (fail "Unable to allocate block for rdir inode") return

  -- Build the root directory inode and persist it; note that we can't use
  -- Directory.makeDirectory here because this is a special case where we have
  -- no parent directory (and, furthermore, no filesystem state).
  let rdirIR = blockAddrToInodeRef rdirAddr
  dirInode <- lift $
    buildEmptyInodeEnc
      dev
      Directory
      rdirPerms
      rdirIR
      nilIR
      uid
      gid
  assert (BS.length dirInode == fromIntegral (bdBlockSize dev)) $ do
  lift $ bdWriteBlock dev rdirAddr dirInode

  finalFree <- readRef (bmNumFree blockMap)
  -- Persist the remaining data structures
  lift $ writeBlockMap dev blockMap
  lift $ writeSB dev $ SuperBlock
    { version        = 1
    , devBlockSize   = bdBlockSize dev
    , devBlockCount  = bdNumBlocks dev
    , unmountClean   = True
    , freeBlocks     = finalFree
    , usedBlocks     = initFree - finalFree
    , fileCount      = 0
    , rootDir        = rdirIR
    , blockMapStart  = blockAddrToInodeRef 1
    }

-- | Mounts a filesystem from a given block device.  After this operation
-- completes, the superblock will have its unmountClean flag set to False.
mount :: (HalfsCapable b t r l m) =>
         BlockDevice m
      -> UserID
      -> GroupID
      -> FileMode
      -> HalfsM b r l m (HalfsState b r l m)
mount dev usr grp rdirPerms = do
  -- fsck needs read access some aspects of the HalfsState (e.g. the dev), even
  -- though there isn't yet a valid state from a mount/successful fsck.  Being
  -- able to log here via logMsg is nice here, too, so we wrap everything in a
  -- dummy environment with some bits filled in.  We'll raise internal errors if
  -- any of the nonexistent bits are pulled on.

  let lgr = Nothing -- Just $ \s -> trace s (return ())
  dummy <- newHalfsState dev usr grp lgr
             (error "Internal (mount): dummy state's superblock is invalid!")
             (error "Internal (mount): dummy state's blockmap is invalid!")
  either throwError return =<< (lift $ runHalfs dummy $ mount')
  where
    newfs' d u g p = newfs d u g p >> mount d u g p
    --
    mount' = do
    esb <- decode `fmap` lift (bdReadBlock dev 0)
    case esb of
      Left _msg -> do
        -- Unable to read the superblock: for now, we just give up on the mount
        -- and provide a new filesystem.
        logMsg "mount: unable to read superblock, creating new filesystem."
        newfs' dev usr grp rdirPerms
      Right sb -> do
        if unmountClean sb
         then do
           bm  <- lift $ readBlockMap dev
           sb' <- lift $ writeSB dev sb{ unmountClean = False }
           newHalfsState dev usr grp Nothing sb' bm
         else do
           mfs <- fsck dev sb usr grp rdirPerms
           case mfs of
             Just fs -> return fs
             Nothing -> do
               logMsg "mount: fsck failed. Creating new filesystem."
               newfs' dev usr grp rdirPerms

-- | Unmounts the given filesystem.  After this operation completes, the
-- superblock will have its unmountClean flag set to True.
unmount :: (HalfsCapable b t r l m) =>
           HalfsM b r l m ()
unmount = do
  dhMap <- hasks hsDHMap
  lk    <- hasks hsLock
  dev   <- hasks hsBlockDev
  sbRef <- hasks hsSuperBlock

  withLock lk $ withLockedRscRef dhMap $ \dhMapRef -> do
  -- Grab everything; we do not want to permit other filesystem actions to
  -- occur in other threads during or after teardown. Needs testing. TODO

  sb <- readRef sbRef
  if (unmountClean sb)
   then throwError HE_UnmountFailed
   else do
     -- TODO:
     -- - Persist all dirty data structures (dirents, files w/ buffered IO, etc.)

     -- Sync all directories; clean directory state is a no-op
     mapM_ syncDirectory =<< M.elems `fmap` readRef dhMapRef

     lift $ bdFlush dev

     -- Finalize the superblock
     let sb' = sb{ unmountClean = True }
     writeRef sbRef sb'
     _ <- lift $ writeSB dev sb'
     return ()

--------------------------------------------------------------------------------
-- FileSystem ChecK (fsck)

fsck :: HalfsCapable b t r l m =>
        BlockDevice m
     -> SuperBlock
     -> UserID
     -> GroupID
     -> FileMode
     -> HalfsM b r l m (Maybe (HalfsState b r l m))
fsck dev sb usr grp rdirPerms = do
  used <- lift $ newUsedBitmap dev
  mbad <- fsck' dev sb used (rootDir sb) (rootDir sb)
  case mbad of
    Nothing        -> return Nothing
    Just (bad, fc) -> do
      bm        <- lift $ writeUsedBitmap dev used >> readBlockMap dev
      finalFree <- readRef (bmNumFree bm)
      _ <- lift $ writeSB dev $ sb
        { unmountClean = True
        , freeBlocks   = finalFree
        , usedBlocks   = initFree - finalFree
        , fileCount    = fc
        }
      logMsg $ "fsck: OK. Bad inodes found: " ++ show (map unIR bad)
      Just `fmap` mount dev usr grp rdirPerms
  where
    blks      = blockMapSizeBlks (bdNumBlocks dev) (bdBlockSize dev)
    initFree  = bdNumBlocks dev - blks - 1 {- -1 for superblock -}

fsck' :: HalfsCapable b t r l m =>
         BlockDevice m
      -> SuperBlock
      -> b
      -> InodeRef
      -> InodeRef
      -> HalfsM b r l m (Maybe ([InodeRef], Word64))
fsck' dev sb used pinr inr = do
  whenValid (drefInode inr) val
  where
    -- Failed operations result in this inode being marked invalid
    whenValid act onValid = do
      eea <- Right `fmap` act `catchError` \e -> Left `fmap` inv e
      either return onValid eea
    --
    inv err = do
      logMsg $ "fsck: corrupt inode " ++ show (unIR inr) ++ ".\n\tReason: "
               ++ show err
      if pinr /= inr
       then do
         -- We have a valid parent directory, so remove the dirent for this inr
         pdh      <- newDirHandle pinr
         contents <- readRef (dhContents pdh)
         case L.find (\de -> deInode de == inr) (M.elems contents) of
           Nothing -> do
             logMsg "fsck internal: failed to find expected inode"
             error  "fsck internal: failed to find expected inode"
           Just de -> do
             rmDirEnt_lckd pdh (deName de)
             syncDirectory_lckd pdh
         return $ Just ([inr], 0)
       else do
         logMsg "fsck: Critical failure: couldn't decode root directory"
         return Nothing
    --
    val nd@Inode{ inoExt = ext, inoFileType = ftype } = do
      assert (inoAddress nd == inr) $ return ()
      whenValid (drop 1 `fmap` expandExts Nothing ext) $ \exts -> do
        let markUsed = mapM_ (setBit used) $
                         unIR inr : map (unER . address) exts
        case ftype of
          Directory -> do
            whenValid (newDirHandle inr) $ \thisDH -> do
              markUsed
              kids <- M.elems `fmap` readRef (dhContents thisDH)
              foldM (\(Just (bads, fc)) kid -> do
                         Just (bs, k) <- fsck' dev sb used inr (deInode kid)
                         return $ Just (bads ++ bs, fc + k)
                    )
                    (Just ([], 0)) kids

          RegularFile -> do
            markUsed
            return $ Just ([], 1)

          _ -> error "fsck' internal error: filetype not supported"


--------------------------------------------------------------------------------
-- Directory manipulation

-- | Makes a directory given an absolute path.
--
-- * Yields HalfsPathComponentNotFound if any path component on the way down to
--   the directory to create does not exist.
--
-- * Yields HalfsAbsolutePathExpected if an absolute path is not provided.
--
mkdir :: (HalfsCapable b t r l m) =>
         FilePath
      -> FileMode
      -> HalfsM b r l m ()
mkdir fp fm = do
  parentIR <- fst `fmap` absPathIR path Directory
  usr <- getUser
  grp <- getGroup
  _ <- makeDirectory parentIR dirName usr grp fm
  return ()
  where
    (path, dirName) = splitFileName fp

rmdir :: (HalfsCapable b t r l m) =>
         FilePath -> HalfsM b r l m ()
rmdir fp = removeDirectory (Just $ takeFileName fp)
             =<< fst `fmap` absPathIR fp Directory

openDir :: (HalfsCapable b t r l m) =>
           FilePath -> HalfsM b r l m (DirHandle r l)
openDir fp = openDirectory =<< fst `fmap` absPathIR fp Directory

closeDir :: (HalfsCapable b t r l m) =>
            DirHandle r l -> HalfsM b r l m ()
closeDir dh = closeDirectory dh

readDir :: (HalfsCapable b t r l m) =>
           DirHandle r l
        -> HalfsM b r l m [(FilePath, FileStat t)]
readDir dh = do
  withDHLock dh $ do
    inr      <- getDHINR_lckd dh
    contents <- liftM M.toList $
                  T.mapM (fileStat)
                    =<< fmap deInode `fmap` readRef (dhContents dh)
    thisStat   <- fileStat inr
    parentStat <- fileStat =<< do
      withLockedInode inr $ do
        p <- inoParent `fmap` drefInode inr
        if isNilIR p
         then fmap rootDir . readRef =<< hasks hsSuperBlock
         else return p
    return $ (dotPath, thisStat) : (dotdotPath, parentStat) : contents

-- | Synchronize the given directory to disk.
syncDir :: (HalfsCapable b t r l m) =>
           FilePath -> SyncType -> HalfsM b r l m ()
syncDir = undefined

--------------------------------------------------------------------------------
-- File manipulation

-- | Creates a file given an absolute path. Raises HE_ObjectExists if the file
-- already exists.
createFile :: (HalfsCapable b t r l m ) =>
              FilePath
           -> FileMode
           -> HalfsM b r l m ()
createFile fp mode = do
  -- TODO: permissions
  withDir ppath $ \pdh -> do
    findInDir pdh fname RegularFile >>= \rslt ->
      case rslt of
        DF_Found _          -> throwError $ HE_ObjectExists fp
        DF_WrongFileType ft -> throwError $ HE_UnexpectedFileType ft fp
        _                   -> return ()
    usr  <- getUser
    grp  <- getGroup
    _inr <- F.createFile pdh fname usr grp mode
    return ()
  where
    (ppath, fname) = splitFileName fp

-- | Opens a file given an absolute path. Raises HE_FileNotFound if the named
-- file does not exist.  Raises HE_UnexpectedFileType if the given path is not a
-- file. Otherwise, provides a FileHandle to the requested file

-- TODO: modes and flags for open: append, r/w, ronly, truncate, etc., and
-- enforcement of the same
openFile :: (HalfsCapable b t r l m) =>
            FilePath            -- ^ The absolute path of the file
         -> FileOpenFlags       -- ^ open flags / open mode (ronly, wonly, wr)
         -> HalfsM b r l m (FileHandle r l)
openFile fp oflags = do
  -- TODO: check perms
  pdh <- openDir ppath
  fh  <- findInDir pdh fname RegularFile >>= \rslt ->
           case rslt of
             DF_NotFound         -> throwError $ HE_FileNotFound
             DF_WrongFileType ft -> throwError $ HE_UnexpectedFileType ft fp
             DF_Found (fir, _ft) -> foundFile fir
  closeDir pdh
  return fh
  where
    (ppath, fname) = splitFileName fp
    foundFile      = openFilePrim oflags

read :: (HalfsCapable b t r l m) =>
        FileHandle r l            -- ^ the handle for the open file to read
     -> Word64                    -- ^ the byte offset into the file
     -> Word64                    -- ^ the number of bytes to read
     -> HalfsM b r l m ByteString -- ^ the data read
read fh byteOff len = do
  -- TODO: Check perms
  unless (fhReadable fh) $ HE_BadFileHandleForRead `annErrno` eBADF
  withLock (fhLock fh) $ do
    inr <- getFHINR_lckd fh
    readStream inr byteOff (Just len)

write :: (HalfsCapable b t r l m) =>
         FileHandle r l     -- ^ the handle for the open file to write
      -> Word64             -- ^ the byte offset into the file
      -> ByteString         -- ^ the data to write
      -> HalfsM b r l m ()
write fh byteOff bytes = do
  -- TODO: Check perms
  unless (fhWritable fh) $ HE_BadFileHandleForWrite `annErrno` eBADF
  withLock (fhLock fh) $ do
    inr <- getFHINR_lckd fh
    writeStream inr byteOff False bytes

flush :: (HalfsCapable b t r l m) =>
         FileHandle r l -> HalfsM b r l m ()
flush _fh = lift . bdFlush =<< hasks hsBlockDev

syncFile :: (HalfsCapable b t r l m) =>
            FilePath -> SyncType -> HalfsM b r l m ()
syncFile _fp _st = lift . bdFlush =<< hasks hsBlockDev

closeFile :: (HalfsCapable b t r l m) =>
             FileHandle r l -- ^ the handle to the open file to close
          -> HalfsM b r l m ()
closeFile fh = closeFilePrim fh

setFileSize :: (HalfsCapable b t r l m) =>
               FilePath -> Word64 -> HalfsM b r l m ()
setFileSize fp len =
  withFile fp (fofWriteOnly True) $ \fh -> do
    withLock (fhLock fh) $ do
      inr <- getFHINR_lckd fh
      let wr  = writeStream_lckd inr
      withLockedInode inr $ do
        sz <- fsSize `fmap` fileStat_lckd inr
        if sz > len
          then wr len True BS.empty                   -- truncate at len
          else wr sz False $ bsReplicate (len - sz) 0 -- pad up to len

setFileTimes :: (HalfsCapable b t r l m) =>
                FilePath
             -> t                  -- ^ access time
             -> t                  -- ^ modification time
             -> HalfsM b r l m ()
setFileTimes fp accTm modTm = do
  -- TODO: Check permissions
  modifyInode fp $ \nd -> nd{ inoModifyTime = modTm, inoAccessTime = accTm }

rename :: (HalfsCapable b t r l m) =>
          FilePath -> FilePath -> HalfsM b r l m ()
rename oldFP newFP = do
  -- TODO: perms checks, more errno wrapping

  {- Currently status of unsupported POSIX error behaviors:

     TODO
     ----
     [EACCES] A component of either path prefix denies search permission.

     [EACCES] The requested operation requires writing in a directory (e.g.,
              new, new/.., or old/..) whose modes disallow this.

     [EIO] An I/O error occurs while making or updating a directory entry.

     [ELOOP] Too many symbolic links are encountered in translating either
             pathname.  This is taken to be indicative of a looping symbolic
             link.

     [EPERM] The directory containing old is marked sticky, and neither the
             containing directory nor old are owned by the effective user ID.

     [EPERM] The new file exists, the directory containing new is marked sticky,
             and neither the containing directory nor new are owned by the
             effective user ID.

     DEFERRED
     --------

     [EXDEV] The link named by new and the file named by old are on different
             logical devices (file systems).  Note that this error code will not
             be returned if the implementation permits cross-device links.

     [EROFS] The requested link requires writing in a directory on a read-only
             file system.

     NOT APPLICABLE (but may be reconsidered later)
     ----------------------------------------------
     [EFAULT] Path points outside the process's allocated address space.

     [EDQUOT] The directory in which the entry for the new name is being placed
              cannot be extended because the user's quota of disk blocks on the
              file system containing the directory has been exhausted.

     [ENAMETOOLONG] A component of a pathname exceeds {NAME_MAX} characters, or
                    an entire path name exceeds {PATH_MAX} characters.
  -}

  -- [EINVAL]: An attempt is made to rename `.' or `..'.
  when (oldFP `equalFilePath` dotPath || oldFP `equalFilePath` dotdotPath) $
    HE_RenameFailed `annErrno` eINVAL

  withDirResources $ \oldParentDH newParentDH -> do
    -- Begin critical section over old and new parent directory handles

    mnewDE <- lookupDE newName newParentDH
    oldDE  <- lookupDE oldName oldParentDH
                >>= (`maybe` return)
                      (HE_PathComponentNotFound oldName `annErrno` eNOENT)
                      -- [ENOENT]: A component of the old path DNE

    handleErrors oldDE mnewDE

    let updateContentMaps = do
          addDirEnt_lckd' True newParentDH oldDE{ deName = newName }
          rmDirEnt_lckd oldParentDH oldName
          syncDirectory_lckd newParentDH `catchError` \e -> do
            case e of
              -- [ENOSPC]: The directory in which the entry for the new name is
              -- being placed cannot be extended because there is no space left
              -- on the file system containing the directory.
              HE_AllocFailed{} -> e `annErrno` eNOSPC
              _                -> throwError e

    -- We try to respect rename(2)'s behavior requirement that "an instance of
    -- new will always exist, even if the system should crash in the middle of
    -- the operation."  Although there's still a risk here if the crash occurs
    -- in the middle of the syncDirectory_lckd operation below (we're not
    -- journaled), the content maps for the parent directories are updated
    -- completely before any existing files/directories are removed.
    --
    -- The thinking is as follows: if we crash immediately before the newName ->
    -- oldDE sync'ing is initiated, 'new' is still valid and refers to the old
    -- file/dir.  If we crash immediately after the newName -> oldDE sync'ing,
    -- 'new' is valid and since the old file/dir won't be a referent of any
    -- content maps, fsck should be able to identify its resources as available.
    --
    -- Finally, when 'new' is an existing empty directory that we'll replace
    -- with 'old', it's important that we hold its dh lock for the duration of
    -- the content map updates so that it doesn't become non-empty underneath
    -- us.

    case mnewDE of
      Nothing    -> updateContentMaps
      Just newDE -> case deType newDE of
        RegularFile -> updateContentMaps >> removeFile Nothing (deInode newDE)
        Directory   -> do
          -- New is a directory: ensure that it's empty before updating the
          -- parent's content map
          hbracket (openDirectory $ deInode newDE) closeDirectory $ \ndh -> do
            withDHLock ndh $ do
              -- begin dirhandle critical section
              isEmpty <- M.null `fmap` readRef (dhContents ndh)
              unless isEmpty $ HE_DirectoryNotEmpty `annErrno` eNOTEMPTY
              updateContentMaps
              -- end dirhandle critical section

            -- NB: We remove the replaced new directory's inode outside of its
            -- locked DH context or we'll deadlock when removeDirectory
            -- acquires.  The unlocked window should be safe here, and it lets
            -- us avoid writing "removeDirectory_dhlckd".
            removeDirectory Nothing (deInode newDE)
        _ -> throwError $ HE_InternalError "rename target type is unsupported"
    -- End critical section over old and new parent directory handles
  where
    [oldName, newName] = map takeFileName [oldFP, newFP]
    [oldPP, newPP]     = map takeDirectory [oldFP, newFP]
    --
    withDirResources f =
      -- bracket & lock the old and new parent directories, being careful not to
      -- double-lock when they're the same.
      withDir' openDir' oldPP $ \opdh ->
        withDir' openDir' newPP $ \npdh ->
          (if oldPP == newPP
            then withDHLock opdh
            else withDHLock opdh . withDHLock npdh
          ) $ f opdh npdh
    --
    openDir' dp = openDir dp `catchError` \e ->
      case e of
        -- [ENOTDIR]: A component of path prefix is not a directory
        HE_UnexpectedFileType{}    -> e `annErrno` eNOTDIR
        -- [ENOENT] A component of path does not exist
        HE_PathComponentNotFound{} -> e `annErrno` eNOENT
        _                          -> throwError e
    --
    handleErrors oldDE mnewDE = do
      -- [EINVAL]: Old is a parent directory of new
      when (deType oldDE == Directory && oldFP `isParentOf` newFP) $
        HE_RenameFailed `annErrno` eINVAL

      case mnewDE of
        Nothing    -> return ()
        Just newDE -> do
          -- [EISDIR]: new is a directory, but old is not a directory.
          when (nft == Directory && oft /= Directory) $
            HE_RenameFailed `annErrno` eISDIR
          -- [ENOTDIR]: old is a directory, but new is not a directory.
          when (oft == Directory && nft /= Directory) $
            HE_RenameFailed `annErrno` eNOTDIR
          where
            nft = deType newDE
            oft = deType oldDE
    --
    p1 `isParentOf` p2 = length l1 < length l2 && and (zipWith (==) l1 l2)
                         where l1 = splitDirectories p1
                               l2 = splitDirectories p2


--------------------------------------------------------------------------------
-- Access control

chmod :: (HalfsCapable b t r l m) =>
         FilePath -> FileMode -> HalfsM b r l m ()
chmod fp mode = do
  -- TODO: Check perms
  modifyInode fp $ \nd -> nd{ inoMode = mode }

chown :: (HalfsCapable b t r l m) =>
         FilePath
      -> Maybe UserID  -- ^ Nothing indicates no change to user
      -> Maybe GroupID -- ^ Nothing indicates no change to group
      -> HalfsM b r l m ()
chown fp musr mgrp = do
  -- TODO: Check perms
  modifyInode fp $ \nd ->
    nd{ inoUser  = maybe (inoUser nd) id musr
      , inoGroup = maybe (inoGroup nd) id mgrp
      }

access :: (HalfsCapable b t r l m) =>
          FilePath -> [AccessRight] -> HalfsM b r l m ()
access = undefined


--------------------------------------------------------------------------------
-- Link manipulation

-- | A POSIX link(2)-like operation: makes a hard file link.
mklink :: (HalfsCapable b t r l m) =>
          FilePath -> FilePath -> HalfsM b r l m ()
mklink path1 {-src-} path2 {-dst-} = do
  {- Currently status of unsupported POSIX error behaviors:

     TODO
     ----

     [EACCES] A component of either path prefix denies search permission.
     [EACCES] The requested link requires writing in a directory with a mode
              that denies write permission.
     [EACCES] The current process cannot access the existing file.

     [EIO]    An I/O error occurs while reading from or writing to the file
              system to make the directory entry.

     [ELOOP] Too many symbolic links are encountered in translating one of the
             pathnames.  This is taken to be indicative of a looping symbolic
             link.

     DEFERRED
     --------

     [EROFS] The requested link requires writing in a directory on a read-only
             file system.

     [EXDEV] The link named by path2 and the file named by path1 are on
             different file systems.

     NOT APPLICABLE (but may be reconsidered later)
     ----------------------------------------------

     [EFAULT]       One of the pathnames specified is outside the process's
                    allocated address space.

     [EDQUOT]       The directory in which the entry for the new link is being
                    placed cannot be extended because the user's quota of disk
                    blocks on the file system containing the directory has been
                    exhausted.

     [EMLINK]       The file already has {LINK_MAX} links.

     [ENAMETOOLONG] A component of a pathname exceeds {NAME_MAX} characters, or
                    an entire path name exceeded {PATH_MAX} characters.
  -}

  hbracket openSrcFile closeFile $ \p1fh -> do
    hbracket openDstDir closeDir $ \p2dh -> do
      withLock (fhLock p1fh) $ do
        srcINR <- getFHINR_lckd p1fh
        addLink p2dh srcINR
        incLinkCount srcINR
  where
    addLink p2dh inr = do
      let fname     = takeFileName path2
          linkPerms = FileMode [Read,Write] [Read] [Read]
          -- TODO: Obtain the proper permissions for the link
      usr <- getUser
      grp <- getGroup
      withDHLock p2dh $ do
        addDirEnt_lckd p2dh fname inr usr grp linkPerms RegularFile
          `catchError` \e -> do
            case e of
              -- [EEXIST]: The link named by path2 already exists.
              HE_ObjectExists{} -> e `annErrno` eEXIST
              _                 -> throwError e
        -- Manual directory sync here in order to catch and wrap errors
        syncDirectory_lckd p2dh `catchError` \e -> do
          rmDirEnt_lckd p2dh fname
          case e of
            -- [ENOSPC]: The directory in which the entry for the new link is
            -- being placed cannot be extended because there is no space left
            -- on the file system containing the directory.
            HE_AllocFailed{} -> e `annErrno` eNOSPC
            _                -> throwError e
    --
    openDstDir = openDir (takeDirectory path2) `catchError` \e ->
      case e of
        -- [ENOTDIR]: A component of path2's pfx is not a directory
        HE_UnexpectedFileType{}    -> e `annErrno` eNOTDIR
        -- [ENOENT] A component of path2's pfx does not exist
        HE_PathComponentNotFound{} -> e `annErrno` eNOENT
        _                       -> throwError e
    --
    openSrcFile = openFile path1 fofReadOnly `catchError` \e ->
      case e of
        -- [EPERM]: The file named by path1 is a directory
        HE_UnexpectedFileType Directory _ -> e `annErrno` ePERM
        -- [ENOTDIR]: A component of path1's pfx is not a directory
        HE_UnexpectedFileType _ _         -> e `annErrno` eNOTDIR
        -- [ENOENT] A component of path1's pfx does not exist
        HE_PathComponentNotFound _        -> e `annErrno` eNOENT
        -- [ENOENT] The file named by path1 does not exist
        HE_FileNotFound                   -> e `annErrno` eNOENT
        _                                 -> throwError e

rmlink :: (HalfsCapable b t r l m) =>
          FilePath -> HalfsM b r l m ()
rmlink fp = removeFile (Just $ takeFileName fp)
              =<< fst `fmap` absPathIR fp RegularFile

createSymLink :: (HalfsCapable b t r l m) =>
                 FilePath -> FilePath -> HalfsM b r l m ()
createSymLink = undefined

readSymLink :: (HalfsCapable b t r l m) =>
               FilePath -> HalfsM b r l m FilePath
readSymLink = undefined


--------------------------------------------------------------------------------
-- Filesystem stats

fstat :: (HalfsCapable b t r l m) =>
         FilePath -> HalfsM b r l m (FileStat t)
fstat fp = fileStat =<< fst `fmap` absPathIR fp AnyFileType

fsstat :: (HalfsCapable b t r l m) =>
          HalfsM b r l m FileSystemStats
fsstat = do
  dev         <- hasks hsBlockDev
  numNodesRsc <- hasks hsNumFileNodes
  bm          <- hasks hsBlockMap
  fileCnt     <- fromIntegral `fmap` withLockedRscRef numNodesRsc readRef
  freeCnt     <- fromIntegral `fmap` numFreeBlocks bm
  return FSS
    { fssBlockSize       = fromIntegral $ bdBlockSize dev
    , fssBlockCount      = fromIntegral $ bdNumBlocks dev
    , fssBlocksFree      = freeCnt
    , fssBlocksAvailable = freeCnt -- TODO: blocks avail to non-root
    , fssFileCount       = fileCnt
    , fssFilesFree       = 0       -- TODO: need to supply free inode count
    }


--------------------------------------------------------------------------------
-- Utility functions & consts

newHalfsState :: HalfsCapable b t r l m =>
                 BlockDevice m
              -> UserID
              -> GroupID
              -> Maybe (String -> m ())
              -> SuperBlock
              -> BlockMap b r l
              -> HalfsM b r l m (HalfsState b r l m)
newHalfsState dev usr grp lgr sb bm =
  HalfsState dev usr grp lgr
    `fmap` computeSizes (bdBlockSize dev) -- memoized since it won't change
    `ap`   return bm                      -- blockmap
    `ap`   newRef sb                      -- superblock
    `ap`   newLock                        -- filesystem lock
    `ap`   newLockedRscRef 0              -- Locked file node count
    `ap`   newLockedRscRef M.empty        -- Locked map: inr -> DH
    `ap`   newLockedRscRef M.empty        -- Locked map: inr -> (l, refcnt)
    `ap`   newLockedRscRef M.empty        -- Locked map: inr -> (FH, opencnt)

-- | Atomic inode modification wrapper
modifyInode :: HalfsCapable b t r l m =>
               FilePath
            -> (Inode t -> Inode t)
            -> HalfsM b r l m ()
modifyInode fp f =
  withFile fp (fofReadWrite True) $ \fh ->
    withLock (fhLock fh) $
      getFHINR_lckd fh >>= flip atomicModifyInode (return . f)

-- | Find the InodeRef corresponding to the given path.  On error, raises
-- exceptions HE_PathComponentNotFound, HE_AbsolutePathExpected, or
-- HE_UnexpectedFileType.
absPathIR :: HalfsCapable b t r l m =>
             FilePath
          -> FileType
          -> HalfsM b r l m (InodeRef, FileType)
absPathIR fp ftype = do
  if isAbsolute fp
   then do
     sbRef  <- hasks hsSuperBlock
     rdirIR <- rootDir `fmap` readRef sbRef
     mir    <- find rdirIR ftype (drop 1 $ splitDirectories fp)
     case mir of
       DF_NotFound         -> throwError $ HE_PathComponentNotFound fp
       DF_WrongFileType ft -> throwError $ HE_UnexpectedFileType ft fp
       DF_Found (ir, ft)   -> return (ir, ft)
   else
     throwError HE_AbsolutePathExpected

-- | Bracketed directory open
withDir :: HalfsCapable b t r l m =>
           FilePath
        -> (DirHandle r l -> HalfsM b r l m a)
        -> HalfsM b r l m a
withDir = withDir' openDir

-- | Bracketed directory open, parameterized by a function for opening a dir
withDir' :: HalfsCapable b t r l m =>
            (FilePath -> HalfsM b r l m (DirHandle r l))
         -> FilePath
         -> (DirHandle r l -> HalfsM b r l m a)
         -> HalfsM b r l m a
withDir' openF = (`hbracket` closeDir) . openF

withFile :: (HalfsCapable b t r l m) =>
            FilePath
         -> FileOpenFlags
         -> (FileHandle r l -> HalfsM b r l m a)
         -> HalfsM b r l m a
withFile fp oflags =
  hbracket (openFile fp oflags) closeFile

writeSB :: (HalfsCapable b t r l m) =>
           BlockDevice m -> SuperBlock -> m SuperBlock
writeSB dev sb = do
  let sbdata = encode sb
  assert (BS.length sbdata <= fromIntegral (bdBlockSize dev)) $ return ()
  bdWriteBlock dev 0 sbdata
  bdFlush dev
  return sb

getUser :: HalfsCapable b t r l m =>
           HalfsM b r l m UserID
getUser = hasks hsUserID

getGroup :: HalfsCapable b t r l m =>
            HalfsM b r l m GroupID
getGroup = hasks hsGroupID