streamly-archive-0.2.0: src/Streamly/External/Archive.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Streamly.External.Archive
( -- ** Read
readArchive,
groupByHeader,
-- ** Header
Header,
FileType (..),
headerFileType,
headerPathName,
headerPathNameUtf8,
headerSize,
)
where
import Control.Exception (mask_)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.Either
import Data.Function
import Data.Int (Int64)
import Data.Void (Void)
import Foreign (Ptr, free, malloc)
import Foreign.C.Types (CChar, CSize)
import Streamly.Data.Fold (Fold)
import qualified Streamly.Data.Parser as P
import Streamly.Data.Stream.Prelude (Stream)
import qualified Streamly.Data.Stream.Prelude as S
import Streamly.Data.Unfold (lmap)
import Streamly.External.Archive.Internal.Foreign
( Entry,
FileType (..),
archive_entry_filetype,
archive_entry_pathname,
archive_entry_pathname_utf8,
archive_entry_size,
archive_read_data_block,
archive_read_free,
archive_read_new,
archive_read_next_header,
archive_read_open_filename,
archive_read_support_filter_all,
archive_read_support_format_all,
)
import Streamly.Internal.Data.IOFinalizer (newIOFinalizer, runIOFinalizer)
import Streamly.Internal.Data.Stream.StreamD.Type (Step (..))
import Streamly.Internal.Data.Unfold.Type (Unfold (..))
-- | Header information for an entry in the archive.
newtype Header = Header Entry
{-# INLINE headerFileType #-}
headerFileType :: Header -> IO (Maybe FileType)
headerFileType (Header e) = archive_entry_filetype e
{-# INLINE headerPathName #-}
headerPathName :: Header -> IO (Maybe ByteString)
headerPathName (Header e) = archive_entry_pathname e
{-# INLINE headerPathNameUtf8 #-}
headerPathNameUtf8 :: Header -> IO (Maybe ByteString)
headerPathNameUtf8 (Header e) = archive_entry_pathname_utf8 e
{-# INLINE headerSize #-}
headerSize :: Header -> IO (Maybe Int)
headerSize (Header e) = archive_entry_size e
-- | A convenience function for grouping @Either Header ByteString@s, usually obtained with
-- 'readArchive', by the headers. The input @Fold@ processes a single entry (a 'Header' followed by
-- zero or more @ByteString@s).
{-# INLINE groupByHeader #-}
groupByHeader ::
(Monad m) =>
Fold m (Either Header ByteString) b ->
Stream m (Either Header ByteString) ->
Stream m b
groupByHeader itemFold str =
str
& S.parseMany (P.groupBy (\_ e -> isRight e) itemFold)
& fmap
( \case
Left _ ->
-- groupBy is documented to never fail.
error "unexpected parseMany/groupBy error"
Right b -> b
)
-- | Creates an unfold with which we can stream data out of the given archive.
{-# INLINE readArchive #-}
readArchive :: (MonadIO m) => FilePath -> Unfold m Void (Either Header ByteString)
readArchive fp =
(lmap . const) () $
Unfold
( \(arch, buf, sz, offs, pos, ref, readHeader) ->
if readHeader
then do
me <- liftIO $ archive_read_next_header arch
case me of
Nothing -> do
liftIO $ runIOFinalizer ref
return Stop
Just e -> do
return $ Yield (Left $ Header e) (arch, buf, sz, offs, 0, ref, False)
else do
(bs, done) <- liftIO $ archive_read_data_block arch buf sz offs pos
return $
if B.length bs > 0
then
Yield
(Right bs)
(arch, buf, sz, offs, pos + fromIntegral (B.length bs), ref, done)
else Skip (arch, buf, sz, offs, pos, ref, done)
)
( \() -> do
(arch, buf, sz, offs, ref) <- liftIO . mask_ $ do
arch <- liftIO archive_read_new
buf :: Ptr (Ptr CChar) <- liftIO malloc
sz :: Ptr CSize <- liftIO malloc
offs :: Ptr Int64 <- liftIO malloc
ref <- newIOFinalizer $ archive_read_free arch >> free buf >> free sz >> free offs
return (arch, buf, sz, offs, ref)
liftIO $ archive_read_support_filter_all arch
liftIO $ archive_read_support_format_all arch
liftIO $ archive_read_open_filename arch fp
return (arch, buf, sz, offs, 0, ref, True)
)