packages feed

streamly-archive-0.3.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
import Control.Monad
import Data.Bits
import Data.ByteString
import qualified Data.ByteString as B
import Data.Int
import Foreign
import Foreign.C.String
import Foreign.C.Types
import System.Posix.Types

data CArchive

data CEntry

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

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

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

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

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

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

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

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

foreign import ccall safe "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 safe "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 safe "archive.h archive_read_free"
  c_archive_read_free :: Ptr CArchive -> IO CInt

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

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

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

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

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

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

foreign import ccall safe "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