{-# LANGUAGE OverloadedStrings #-} -- | -- Module: Kesha.NAR -- Copyright: (c) 2020 Jordan Mackie -- License: MIT -- Maintainer: Jordan Mackie -- Stability: experimental -- Portability: portable -- -- An implementation of the (NAR). module Kesha.NAR ( NAR, PackError (..), localPack, dump, ) where {- HLINT ignore "Use lambda-case" -} import Control.Monad (when) import Data.Bifunctor (second) import qualified Data.Binary.Put as Binary import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BSL import Data.Foldable (for_, traverse_) import qualified Data.List as List import qualified Data.Map as Map import Data.Semigroup ((<>)) import Data.Text (Text) import qualified Data.Text as Text import Data.Text.Encoding (encodeUtf8) import Data.Traversable (for) import qualified System.Directory as Directory import System.FilePath (()) import Prelude -- | -- A packed NAR archive. newtype NAR = NAR {getFSO :: FSO} -- | -- Errors that can be raised when attempting to pack a path into a NAR archive. data PackError = -- | -- Attempted to pack a path that doesn't exist. FileDoesNotExist FilePath | -- | -- Heuristic for detecting the /type/ of path failed. Where /type/ is one: -- a regular file, a directory, or a symbolic link. AmbiguousFileType FilePath deriving (Show, Eq) data FSO = Regular !IsExecutable !Size !BS.ByteString | SymLink !UTF8FilePath | Directory !(Map.Map PathSegment FSO) type IsExecutable = Bool type Size = Int type UTF8FilePath = Text type PathSegment = Text -- shouldn't include '/' or 'NUL' ! data PathType = RegularType | SymLinkType | DirectoryType | AmbiguousType -- | -- Create a NAR archive for the given path in a local context. -- -- See figure 5.2 of https://nixos.org/~eelco/pubs/phd-thesis.pdf localPack :: FilePath -> IO (Either PackError NAR) localPack path = second NAR <$> localPackFSO path localPackFSO :: FilePath -> IO (Either PackError FSO) localPackFSO path = guessPathType path >>= \guess -> case guess of Nothing -> pure $ Left (FileDoesNotExist path) Just AmbiguousType -> pure $ Left (AmbiguousFileType path) Just RegularType -> do isExecutable <- Directory.executable <$> Directory.getPermissions path size <- fromIntegral <$> Directory.getFileSize path contents <- BS.readFile path let fso = Regular isExecutable size contents pure $ Right fso Just SymLinkType -> do target <- Directory.getSymbolicLinkTarget path let fso = SymLink (Text.pack target) pure $ Right fso Just DirectoryType -> do fs <- Directory.listDirectory path entries <- for fs $ \path' -> do results <- localPackFSO (path path') pure (Text.pack path', results) pure $ second (Directory . Map.fromList) (traverse sequence entries) -- | -- Serialize a NAR archive. dump :: NAR -> BS.ByteString dump = BSL.toStrict . Binary.runPut . putNAR putNAR :: NAR -> Binary.Put putNAR nar = str "nix-archive-1" <> parens (putFSO (getFSO nar)) where putFSO :: FSO -> Binary.Put putFSO fso = case fso of Regular isExecutable size contents -> do strs ["type", "regular"] when isExecutable $ strs ["executable", ""] str "contents" int size pad size contents SymLink target -> do strs ["type", "symlink"] strs ["target", encodeUtf8 target] Directory entries -> do strs ["type", "directory"] let sortedEntries = List.sortOn fst (Map.toList entries) for_ sortedEntries $ \(name, node) -> do str "entry" parens $ do str "name" str (encodeUtf8 name) str "node" parens (putFSO node) int :: Integral a => a -> Binary.Put int = Binary.putInt64le . fromIntegral parens :: Binary.Put -> Binary.Put parens m = str "(" >> m >> str ")" str :: BS.ByteString -> Binary.Put str bs = let len = BS.length bs in int len <> pad len bs strs :: [BS.ByteString] -> Binary.Put strs = traverse_ str pad :: Int -> BS.ByteString -> Binary.Put pad n bs = do Binary.putByteString bs Binary.putByteString (BS.replicate (padLen n) 0) -- Distance to the next multiple of 8 padLen :: Integral a => a -> a padLen n = (8 - n) `mod` 8 guessPathType :: FilePath -> IO (Maybe PathType) guessPathType path = do pathExists <- Directory.doesPathExist path if not pathExists then pure Nothing else do clues <- (,,) -- returns True if the argument file exists and is not a directory, <$> Directory.doesFileExist path -- returns True if the argument file exists and is either a directory or -- a symbolic link to a directory <*> Directory.doesDirectoryExist path -- Check whether the path refers to a symbolic link <*> Directory.pathIsSymbolicLink path case clues of (True, False, True) -> pure (Just SymLinkType) (True, False, False) -> pure (Just RegularType) (False, True, True) -> pure (Just SymLinkType) (False, True, False) -> pure (Just DirectoryType) _ -> pure (Just AmbiguousType)