packages feed

streamly-archive-0.2.0: src/Streamly/External/Archive/Internal/Foreign.hs

module Streamly.External.Archive.Internal.Foreign
  ( Archive,
    Entry,
    FileType (..),
    archive_read_new,
    archive_read_support_filter_all,
    archive_read_support_format_all,
    archive_read_support_format_gnutar,
    blockSize,
    archive_read_open_filename,
    archive_read_next_header,
    archive_entry_filetype,
    archive_entry_pathname,
    archive_entry_pathname_utf8,
    archive_entry_size,
    alloc_archive_read_data_buffer,
    archive_read_data,
    archive_read_data_block,
    archive_read_free,
  )
where

import Control.Exception (Exception, mask_, throw)
import Control.Monad (when)
import Data.Bits ((.&.))
import Data.ByteString (ByteString, packCString, packCStringLen)
import qualified Data.ByteString as B
import Data.Int (Int64)
import Foreign (FunPtr, Ptr, nullPtr, peek)
import Foreign.C.String (CString, peekCString, withCString)
import Foreign.C.Types (CChar, CInt (CInt), CSize (CSize))
import Foreign.ForeignPtr (ForeignPtr, newForeignPtr, withForeignPtr)
import Foreign.Marshal.Alloc (mallocBytes)
import System.Posix.Types (CMode (CMode), CSsize (CSsize))

data CArchive

data CEntry

foreign import ccall unsafe "archive.h archive_errno"
  c_archive_errno :: Ptr CArchive -> IO CInt

foreign import ccall unsafe "archive.h archive_error_string"
  c_archive_error_string :: Ptr CArchive -> IO CString

foreign import ccall unsafe "archive.h archive_read_new"
  c_archive_read_new :: IO (Ptr CArchive)

foreign import ccall unsafe "archive.h archive_read_support_filter_all"
  c_archive_read_support_filter_all :: Ptr CArchive -> IO CInt

foreign import ccall unsafe "archive.h archive_read_support_format_all"
  c_archive_read_support_format_all :: Ptr CArchive -> IO CInt

foreign import ccall unsafe "archive.h archive_read_support_format_gnutar"
  c_archive_read_support_format_gnutar :: Ptr CArchive -> IO CInt

foreign import ccall unsafe "archive.h archive_read_open_filename"
  c_archive_read_open_filename :: Ptr CArchive -> CString -> CSize -> IO CInt

foreign import ccall unsafe "archive.h archive_read_next_header2"
  c_archive_read_next_header2 :: Ptr CArchive -> Ptr CEntry -> IO CInt

foreign import ccall unsafe "archive.h archive_read_data"
  -- Todo: Think about la_ssize_t on non-POSIX.
  c_archive_read_data :: Ptr CArchive -> Ptr CChar -> CSize -> IO CSsize

foreign import ccall unsafe "archive.h archive_read_data_block"
  c_archive_read_data_block :: Ptr CArchive -> Ptr (Ptr CChar) -> Ptr CSize -> Ptr Int64 -> IO CInt

foreign import ccall unsafe "archive.h archive_read_free"
  c_archive_read_free :: Ptr CArchive -> IO CInt

foreign import ccall unsafe "archive_entry.h archive_entry_filetype"
  c_archive_entry_filetype :: Ptr CEntry -> IO CMode -- Todo: Think about type on non-POSIX.

foreign import ccall unsafe "archive_entry.h archive_entry_new"
  c_archive_entry_new :: IO (Ptr CEntry)

-- Similar to c_free_finalizer from ByteString.
foreign import ccall unsafe "static archive_entry.h &archive_entry_free"
  c_archive_entry_free_finalizer :: FunPtr (Ptr CEntry -> IO ())

foreign import ccall unsafe "archive_entry.h archive_entry_pathname"
  c_archive_entry_pathname :: Ptr CEntry -> IO CString

foreign import ccall unsafe "archive_entry.h archive_entry_pathname_utf8"
  c_archive_entry_pathname_utf8 :: Ptr CEntry -> IO CString

foreign import ccall unsafe "archive_entry.h archive_entry_size"
  c_archive_entry_size :: Ptr CEntry -> IO Int64

foreign import ccall unsafe "archive_entry.h archive_entry_size_is_set"
  c_archive_entry_size_is_set :: Ptr CEntry -> IO CInt

-- Documented libarchive return codes.
data RetCode
  = RetCodeEOF
  | RetCodeOK
  | RetCodeRETRY
  | RetCodeWARN
  | RetCodeFAILED
  | RetCodeFATAL
  deriving (Show)

retCodes :: [(CInt, RetCode)]
retCodes =
  [ (1, RetCodeEOF),
    (0, RetCodeOK),
    (-10, RetCodeRETRY),
    (-20, RetCodeWARN),
    (-25, RetCodeFAILED),
    (-30, RetCodeFATAL)
  ]

data ArchiveError = ArchiveError
  { err_function :: !String,
    err_retcode :: !(Either CInt RetCode),
    err_number :: !Int,
    err_string :: !String
  }
  deriving (Show)

instance Exception ArchiveError

newtype ErrorString = ErrorString String deriving (Show)

instance Exception ErrorString

archive_error_string :: Ptr CArchive -> IO String
archive_error_string aptr = do
  cstr <- c_archive_error_string aptr
  if cstr == nullPtr
    then return "archive_error_string returned NULL"
    else peekCString cstr

throwArchiveError :: String -> CInt -> Ptr CArchive -> IO noReturn
throwArchiveError fn rc aptr = do
  num <- fromIntegral <$> c_archive_errno aptr
  str <- archive_error_string aptr
  throw $
    ArchiveError
      { err_function = fn,
        err_retcode = maybe (Left rc) Right (lookup rc retCodes),
        err_number = num,
        err_string = str
      }

newtype Archive = Archive (Ptr CArchive)

newtype Entry = Entry (ForeignPtr CEntry)

data FileType
  = FileTypeRegular
  | FileTypeSymlink
  | FileTypeSocket
  | FileTypeCharDevice
  | FileTypeBlockDevice
  | FileTypeDirectory
  | FileTypeNamedPipe
  deriving (Show, Eq)

archive_read_new :: IO Archive
archive_read_new = do
  aptr <- c_archive_read_new
  if aptr == nullPtr
    then throw $ ErrorString "archive_read_new returned NULL"
    else return $ Archive aptr

archive_read_support_filter_all :: Archive -> IO ()
archive_read_support_filter_all (Archive aptr) = do
  rc <- c_archive_read_support_filter_all aptr
  when (rc /= 0) $ throwArchiveError "archive_read_support_filter_all" rc aptr

archive_read_support_format_all :: Archive -> IO ()
archive_read_support_format_all (Archive aptr) = do
  rc <- c_archive_read_support_format_all aptr
  when (rc /= 0) $ throwArchiveError "archive_read_support_format_all" rc aptr

archive_read_support_format_gnutar :: Archive -> IO ()
archive_read_support_format_gnutar (Archive aptr) = do
  rc <- c_archive_read_support_format_gnutar aptr
  when (rc /= 0) $ throwArchiveError "archive_read_support_format_gnutar" rc aptr

-- Fixed block size for now.
{-# INLINE blockSize #-}
blockSize :: (Num a) => a
blockSize = 4096

archive_read_open_filename :: Archive -> FilePath -> IO ()
archive_read_open_filename (Archive aptr) fp =
  withCString fp $ \cstr -> do
    rc <- c_archive_read_open_filename aptr cstr blockSize
    when (rc /= 0) $ throwArchiveError "archive_read_open_filename" rc aptr

-- | Returns 'Nothing' if we have reached the end of the archive.
{-# INLINE archive_read_next_header #-}
archive_read_next_header :: Archive -> IO (Maybe Entry)
archive_read_next_header (Archive aptr) = do
  fpe <- mask_ $ c_archive_entry_new >>= newForeignPtr c_archive_entry_free_finalizer
  rc <- withForeignPtr fpe $ c_archive_read_next_header2 aptr
  if rc == 1 -- EOF.
    then return Nothing
    else
      if rc < 0
        then throwArchiveError "archive_read_next_header" rc aptr
        else return . Just . Entry $ fpe

{-# INLINE fileTypeAeIFMT #-}
fileTypeAeIFMT :: CMode
fileTypeAeIFMT = 0o0170000

{-# INLINE fileTypes #-}
fileTypes :: [(CMode, FileType)]
fileTypes =
  [ (0o0100000, FileTypeRegular),
    (0o0120000, FileTypeSymlink),
    (0o0140000, FileTypeSocket),
    (0o0020000, FileTypeCharDevice),
    (0o0060000, FileTypeBlockDevice),
    (0o0040000, FileTypeDirectory),
    (0o0010000, FileTypeNamedPipe)
  ]

{-# INLINE archive_entry_filetype #-}
archive_entry_filetype :: Entry -> IO (Maybe FileType)
archive_entry_filetype (Entry feptr) = withForeignPtr feptr $ \eptr -> do
  i <- c_archive_entry_filetype eptr
  return $ lookup (i .&. fileTypeAeIFMT) fileTypes

{-# INLINE archive_entry_pathname #-}
archive_entry_pathname :: Entry -> IO (Maybe ByteString)
archive_entry_pathname (Entry feptr) = withForeignPtr feptr $ \eptr -> do
  cstr <- c_archive_entry_pathname eptr
  if cstr == nullPtr
    then return Nothing
    else Just <$> packCString cstr

{-# INLINE archive_entry_pathname_utf8 #-}
archive_entry_pathname_utf8 :: Entry -> IO (Maybe ByteString)
archive_entry_pathname_utf8 (Entry feptr) = withForeignPtr feptr $ \eptr -> do
  cstr <- c_archive_entry_pathname_utf8 eptr
  if cstr == nullPtr
    then return Nothing
    else Just <$> packCString cstr

{-# INLINE archive_entry_size #-}
archive_entry_size :: Entry -> IO (Maybe Int)
archive_entry_size (Entry feptr) = withForeignPtr feptr $ \eptr -> do
  size_is_set <- (/= 0) <$> c_archive_entry_size_is_set eptr
  if size_is_set
    then Just . fromIntegral <$> c_archive_entry_size eptr
    else return Nothing

-- | Please free after use.
alloc_archive_read_data_buffer :: IO (Ptr CChar)
alloc_archive_read_data_buffer = mallocBytes blockSize

-- | Returns 'Nothing' if there is no more data for the current entry.
-- Pass in a buffer allocated with 'alloc_archive_read_data_buffer'.
{-# INLINE archive_read_data #-}
archive_read_data :: Archive -> Ptr CChar -> IO (Maybe ByteString)
archive_read_data (Archive aptr) buf = do
  rb <- c_archive_read_data aptr buf blockSize
  if rb == 0
    then return Nothing
    else
      if rb < 0
        then throwArchiveError "archive_read_data" (fromIntegral rb) aptr
        else Just <$> packCStringLen (buf, fromIntegral rb)

{-# INLINE archive_read_data_block #-}
archive_read_data_block ::
  Archive ->
  Ptr (Ptr CChar) ->
  Ptr CSize ->
  Ptr Int64 ->
  Int64 ->
  IO (ByteString, Bool)
archive_read_data_block (Archive aptr) buf sz offs pos = do
  rc <- c_archive_read_data_block aptr buf sz offs
  if rc < 0
    then throwArchiveError "archive_read_data_block" (fromIntegral rc) aptr
    else
      if rc == 0 || rc == 1
        then do
          -- OK or EOF.
          bs <- peek buf >>= \buf' -> peek sz >>= \sz' -> packCStringLen (buf', fromIntegral sz')
          offs' <- peek offs
          -- pos: Where we are currently located and where the data goes normally (for non-sparse
          -- files). offs': Where libarchive is asking us to position the data.
          if offs' == pos
            then return (bs, rc == 1)
            else
              if offs' > pos
                then do
                  -- For a sparse file, we need to prepend zeroes to the normal data.
                  let diff = offs' - pos
                  let bs' = B.replicate (fromIntegral diff) 0 `B.append` bs
                  return (bs', rc == 1)
                else throw $ ErrorString "archive_read_data_block: unexpected offset"
        else throw $ ErrorString "archive_read_data_block: unexpected return code"

archive_read_free :: Archive -> IO ()
archive_read_free (Archive aptr) = do
  rc <- c_archive_read_free aptr
  when (rc /= 0) $ throwArchiveError "archive_read_free" rc aptr