packages feed

hnix-store-core-0.3.0.0: src/System/Nix/Internal/Nar/Streamer.hs

-- | Stream out a NAR file from a regular file

{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE RankNTypes          #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies        #-}

module System.Nix.Internal.Nar.Streamer where

import           Control.Monad                   (forM, forM_, when)
import qualified Control.Monad.IO.Class          as IO
import           Data.Bool                       (bool)
import qualified Data.ByteString                 as BS
import qualified Data.ByteString.Char8           as BSC
import qualified Data.ByteString.Lazy            as BSL
import qualified Data.List                       as List
import qualified Data.Serialize                  as Serial
import           GHC.Int                         (Int64)
import qualified System.Directory                as Directory
import           System.FilePath                 ((</>))

import qualified System.Nix.Internal.Nar.Effects as Nar


-- | This implementation of Nar encoding takes an arbitrary @yield@
--   function from any streaming library, and repeatedly calls
--   it while traversing the filesystem object to Nar encode
streamNarIO
  :: forall m.(IO.MonadIO m)
  => (BS.ByteString -> m ())
  -> Nar.NarEffects IO
  -> FilePath
  -> m ()
streamNarIO yield effs basePath = do
  yield (str "nix-archive-1")
  parens (go basePath)
  where

    go :: FilePath -> m ()
    go path = do
      isDir     <- IO.liftIO $ Nar.narIsDir effs path
      isSymLink <- IO.liftIO $ Nar.narIsSymLink effs path
      let isRegular = not (isDir || isSymLink)

      when isSymLink $ do
        target <- IO.liftIO $ Nar.narReadLink effs path
        yield $
          strs ["type", "symlink", "target", BSC.pack target]

      when isRegular $ do
        isExec <- IO.liftIO $ isExecutable effs path
        yield $ strs ["type","regular"]
        when (isExec == Executable) (yield $ strs ["executable", ""])
        fSize <- IO.liftIO $ Nar.narFileSize effs path
        yield $ str "contents"
        yield $ int fSize
        yieldFile path fSize

      when isDir $ do
        fs <- IO.liftIO (Nar.narListDir effs path)
        yield $ strs ["type", "directory"]
        forM_ (List.sort fs) $ \f -> do
          yield $ str "entry"
          parens $ do
            let fullName = path </> f
            yield (strs ["name", BSC.pack f, "node"])
            parens (go fullName)

    str :: BS.ByteString -> BS.ByteString
    str t = let len =  BS.length t
            in  int len <> padBS len t

    padBS :: Int -> BS.ByteString -> BS.ByteString
    padBS strSize bs = bs <> BS.replicate (padLen strSize) 0

    parens act = do
      yield (str "(")
      r <- act
      yield (str ")")
      return r

    -- Read, yield, and pad the file
    yieldFile :: FilePath -> Int64 -> m ()
    yieldFile path fsize = do
      mapM_ yield . BSL.toChunks =<< IO.liftIO (BSL.readFile path)
      yield (BS.replicate (padLen (fromIntegral fsize)) 0)

    strs :: [BS.ByteString] -> BS.ByteString
    strs xs = BS.concat $ str <$> xs

    int :: Integral a => a -> BS.ByteString
    int n = Serial.runPut $ Serial.putInt64le (fromIntegral n)


data IsExecutable = NonExecutable | Executable
    deriving (Eq, Show)

isExecutable :: Functor m => Nar.NarEffects m -> FilePath -> m IsExecutable
isExecutable effs fp =
  bool NonExecutable Executable . Directory.executable <$> Nar.narGetPerms effs fp

-- | Distance to the next multiple of 8
padLen:: Int -> Int
padLen n = (8 - n) `mod` 8