packages feed

nix-serve-ng-1.0.0: src/Nix.hsc

{-# LANGUAGE BlockArguments           #-}
{-# LANGUAGE DeriveAnyClass           #-}
{-# LANGUAGE DerivingStrategies       #-}
{-# LANGUAGE DuplicateRecordFields    #-}
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE MultiWayIf               #-}
{-# LANGUAGE NamedFieldPuns           #-}
{-# LANGUAGE OverloadedStrings        #-}
{-# LANGUAGE TypeApplications         #-}

module Nix where

import Control.Applicative (empty)
import Control.Exception (Exception, SomeException)
import Control.Monad.Managed (Managed)
import Data.ByteString (ByteString)
import Data.ByteString.Builder (Builder)
import Data.Vector (Vector)
import Data.Word (Word64)
import Foreign (FunPtr, Ptr, Storable(..))
import Foreign.C (CChar, CLong, CSize(..), CString)

import qualified Control.Exception       as Exception
import qualified Control.Monad           as Monad
import qualified Control.Monad.Managed   as Managed
import qualified Data.ByteString         as ByteString
import qualified Data.ByteString.Base16  as Base16
import qualified Data.ByteString.Base32  as Base32
import qualified Data.ByteString.Builder as Builder
import qualified Data.IORef              as IORef
import qualified Data.Vector             as Vector
import qualified Data.Vector.Storable    as Vector.Storable
import qualified Foreign

#include "nix.hh"

foreign import ccall "freeString" freeString :: Ptr String_ -> IO ()

data String_ = String_ { data_ :: Ptr CChar, size  :: CSize }

instance Storable String_ where
    sizeOf _ = #{size struct string}

    alignment _ = #{alignment struct string}

    peek pointer = do
        data_ <- #{peek struct string, data} pointer

        size <- #{peek struct string, size} pointer

        return String_{ data_, size }

    poke pointer String_{ data_, size } = do
        #{poke struct string, data} pointer data_

        #{poke struct string, size} pointer size

fromString_ :: String_ -> IO ByteString
fromString_ String_{ data_, size } =
    ByteString.packCStringLen (data_, fromIntegral @CSize @Int size)

toString_ :: ByteString -> Managed String_
toString_ bytes = do
    (data_, size) <- Managed.managed (ByteString.useAsCStringLen bytes)

    return String_{ data_, size = fromIntegral @Int @CSize size }

foreign import ccall "freeStrings" freeStrings :: Ptr Strings -> IO ()

data Strings = Strings
    { data_ :: Ptr String_
    , size  :: CSize
    }

instance Storable Strings where
    sizeOf _ = #{size struct strings}

    alignment _ = #{alignment struct strings}

    peek pointer = do
        data_ <- #{peek struct strings, data} pointer

        size <- #{peek struct strings, size} pointer

        return Strings{ data_, size }

    poke pointer Strings{ data_, size } = do
        #{poke struct strings, data} pointer data_

        #{poke struct strings, size} pointer size

fromStrings :: Strings -> IO (Vector ByteString)
fromStrings Strings{ data_, size} = do
    foreignPointer <- Foreign.newForeignPtr_ data_

    let storableVector =
            Vector.Storable.unsafeFromForeignPtr0 foreignPointer
                (fromIntegral @CSize @Int size)

    traverse fromString_ (Vector.convert storableVector)

toStrings :: Vector ByteString -> Managed Strings
toStrings vector = do
    storableVector <- fmap Vector.convert (traverse toString_ vector)

    data_ <- Managed.managed (Vector.Storable.unsafeWith storableVector)

    let size = fromIntegral @Int @CSize (Vector.Storable.length storableVector)

    return Strings{ data_, size }

foreign import ccall "freePathInfo" freePathInfo :: Ptr CPathInfo -> IO ()

{-| We don't use the original @ValidPathInfo@ Nix type.  Rather, the C FFI
    defines a @pathinfo@ struct that wraps a subset of what we need in a
    C-compatible API
-}
data CPathInfo = CPathInfo
    { deriver    :: String_
    , narHash    :: String_
    , narSize    :: CLong
    , references :: Strings
    , sigs       :: Strings
    }

instance Storable CPathInfo where
    sizeOf _ = #{size struct PathInfo}

    alignment _ = #{alignment struct PathInfo}

    peek pointer = do
        deriver <- #{peek struct PathInfo, deriver} pointer

        narHash <- #{peek struct PathInfo, narHash} pointer

        narSize <- #{peek struct PathInfo, narSize} pointer

        references <- #{peek struct PathInfo, references} pointer

        sigs <- #{peek struct PathInfo, sigs} pointer

        return CPathInfo{ deriver, narHash, narSize, references, sigs }

    poke pointer CPathInfo{ deriver, narHash, narSize, references, sigs } = do
        #{poke struct PathInfo, deriver} pointer deriver

        #{poke struct PathInfo, narHash} pointer narHash

        #{poke struct PathInfo, narSize} pointer narSize

        #{poke struct PathInfo, references} pointer references

        #{poke struct PathInfo, sigs} pointer sigs

data PathInfo = PathInfo
    { deriver    :: Maybe ByteString
    , narHash    :: ByteString
    , narSize    :: Word64
    , references :: Vector ByteString
    , sigs       :: Vector ByteString
    } deriving stock (Show)

fromCPathInfo :: CPathInfo -> IO PathInfo
fromCPathInfo CPathInfo{ deriver, narHash, narSize, references, sigs } = do
    deriver_ <-
        if data_ (deriver :: String_) == Foreign.nullPtr
        then return Nothing
        else fmap Just (fromString_ deriver)

    narHash_ <- fromString_ narHash

    references_ <- fromStrings references

    sigs_ <- fromStrings sigs

    return PathInfo
        { deriver = deriver_
        , narHash = narHash_
        , narSize = fromIntegral @CLong @Word64 narSize
        , references = references_
        , sigs = sigs_
        }

foreign import ccall "getStoreDir" getStoreDir_ :: Ptr String_ -> IO ()

getStoreDir :: IO ByteString
getStoreDir =
    Foreign.alloca \output -> do
        let open = getStoreDir_ output
        let close = freeString output
        Exception.bracket_ open close do
            string_ <- peek output
            fromString_ string_

data NoSuchPath = NoSuchPath
    deriving anyclass (Exception)
    deriving stock (Show)

foreign import ccall "queryPathFromHashPart" queryPathFromHashPart_
    :: CString -> Ptr String_ -> IO ()

queryPathFromHashPart :: ByteString -> IO (Either NoSuchPath ByteString)
queryPathFromHashPart hashPart = do
    ByteString.useAsCString hashPart \cHashPart -> do
        Foreign.alloca \output -> do
            let open = queryPathFromHashPart_ cHashPart output
            let close = freeString output
            Exception.bracket_ open close do
                string_@String_{ data_} <- peek output
                if data_ == Foreign.nullPtr
                    then return (Left NoSuchPath)
                    else fmap Right (fromString_ string_)

foreign import ccall "queryPathInfo" queryPathInfo_
    :: CString -> Ptr CPathInfo -> IO ()

queryPathInfo :: ByteString -> IO PathInfo
queryPathInfo storePath = do
    ByteString.useAsCString storePath \cStorePath -> do
        Foreign.alloca \output -> do
            let open = queryPathInfo_ cStorePath output
            let close = freePathInfo output
            Exception.bracket_ open close do
                cPathInfo <- peek output
                fromCPathInfo cPathInfo

fingerprintPath :: ByteString -> PathInfo -> Maybe Builder
fingerprintPath storePath PathInfo{ narHash, narSize, references } = do
    suffix <- ByteString.stripPrefix "sha256:" narHash

    base32Suffix <- if
        | ByteString.length suffix == 64
        , Right digest <- Base16.decodeBase16 suffix ->
            return (Base32.encodeBase32' digest)
        | ByteString.length suffix == 52 ->
            return suffix
        | otherwise ->
            empty

    return
        (   "1;"
        <>  Builder.byteString storePath
        <>  ";sha256:"
        <>  Builder.byteString base32Suffix
        <>  ";"
        <>  Builder.word64Dec narSize
        <>  ";"
        <>  referencesBuilder
        )
  where
    referencesBuilder =
        case Vector.uncons references of
            Nothing ->
                mempty
            Just (r0, rs) ->
                    Builder.byteString r0
                <>  foldMap (\r -> "," <> Builder.byteString r) rs

foreign import ccall "signString" signString_
    :: CString -> CString -> Ptr String_ -> IO ()

signString :: ByteString -> ByteString -> IO ByteString
signString secretKey fingerprint =
    ByteString.useAsCString secretKey \cSecretKey ->
        ByteString.useAsCString fingerprint \cFingerprint ->
            Foreign.alloca \output -> do
                let open = signString_ cSecretKey cFingerprint output
                let close = freeString output
                Exception.bracket_ open close do
                    string_ <- peek output
                    fromString_ string_

foreign import ccall "dumpPath" dumpPath_
    :: CString -> FunPtr (Ptr CChar -> CSize -> IO Bool) -> IO Bool

dumpPath :: ByteString -> (Builder -> IO ()) -> IO (Either SomeException ())
dumpPath hashPart builderCallback = do
    result <- IORef.newIORef (Right ())

    let cCallback :: Ptr CChar -> CSize -> IO Bool
        cCallback pointer cSize = do
            let handler :: SomeException -> IO Bool
                handler exception = do
                    IORef.writeIORef result (Left exception)
                    return False

            Exception.handle handler do
                -- At the time of this writing Nix uses a maximum chunk size of
                -- 64 kibibytes, which could potentially increase in the future.
                -- However:
                --
                -- • Data.Int.Int guarantees `maxBound :: Int` supports at least
                --   128 mebibytes
                -- • The latest version of GHC sets `maxBound :: Int` to
                --   support at least 2 gibibytes
                -- • On 64-bit systems (almost all systems nowadays)
                --   `maxBound :: Int` supports 8 exbibytes
                --
                -- … so even though this conversion could potentially overflow,
                -- it's unlikely to happen (and it's not even clear what we
                -- would do to fix the overflow anyway since the `bytestring`
                -- package requires an `Int` size).
                let len = fromIntegral @CSize @Int cSize

                byteString <- ByteString.packCStringLen (pointer, len)

                builderCallback (Builder.byteString byteString)

                return True

    wrappedCCallback <- wrapCallback cCallback

    ByteString.useAsCString hashPart \cHashPart -> do
        success <- dumpPath_ cHashPart wrappedCCallback

        Monad.when (not success) do
            IORef.writeIORef result (Left (Exception.toException NoSuchPath))

    IORef.readIORef result

foreign import ccall "dumpLog" dumpLog_
    :: CString -> Ptr String_ -> IO ()

dumpLog :: ByteString -> IO (Maybe ByteString)
dumpLog baseName = do
    ByteString.useAsCString baseName \cBaseName -> do
        Foreign.alloca \output -> do
            let open = dumpLog_ cBaseName output
            let close = freeString output
            Exception.bracket_ open close do
                string_@String_{ data_} <- peek output
                if data_ == Foreign.nullPtr
                    then return Nothing
                    else fmap Just (fromString_ string_)

foreign import ccall "wrapper" wrapCallback
    :: (Ptr CChar -> CSize -> IO Bool) -> IO (FunPtr (Ptr CChar -> CSize -> IO Bool))