crucible-symio-0.1: src/Lang/Crucible/SymIO/Loader.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
-- | This module defines a default loader for initial symbolic filesystem contents
--
-- It uses a simple convention to convert on-disk files and metadata into a
-- 'SymIO.InitialFileSystemContents'. This is not the only way to construct
-- initial filesystem contents, but it is a good default if a tool does not have
-- more specific needs.
--
-- The caller provides a single input: a path to a directory. The directory
-- contains two things:
--
-- 1. A subdirectory named @root@ that contains the concrete files in the symbolic filesystem (i.e., the directory mapped to @/@)
-- 2. (Optional) A file named @symbolic-manifest.json@, which describes symbolic files and overlays
--
-- The symbolic manifest specifies the contents of symbolic files, including
-- constraints on symbolic values. Furthermore, it enables users to specify
-- that concrete files in the provided filesystem have symbolic values overlaid
-- over the concrete values. If an overlay is specified in the symbolic
-- manifest, the referenced concrete file /must/ exist.
--
-- Note: future versions of this interface could support symbolic filesystems
-- stored in zip or tar files.
module Lang.Crucible.SymIO.Loader (
loadInitialFiles
, FileSystemLoadError(..)
) where
import qualified Control.Exception as X
import qualified Data.Aeson as JSON
import qualified Data.ByteString as BS
import qualified Data.Foldable as F
import qualified Data.List as List
import qualified Data.Map.Strict as Map
import Data.Maybe ( fromMaybe )
import qualified Data.Parameterized.NatRepr as PN
import qualified Data.Text as T
import qualified Data.Traversable as T
import Data.Word ( Word64 )
import GHC.Generics ( Generic )
import qualified System.Directory as SD
import System.FilePath ( (</>) )
import qualified System.FilePath.Find as SFF
import qualified What4.BaseTypes as WT
import qualified What4.Interface as WI
import qualified Lang.Crucible.Backend as LCB
import qualified Lang.Crucible.SymIO as SymIO
data FileSystemLoadError = ErrorDecodingJSON String
| forall k . FileSpecifiedAsSymbolicAndConcrete (SymIO.FDTarget k)
deriving instance Show FileSystemLoadError
instance X.Exception FileSystemLoadError
-- | The specification for the symbolic contents of a file in the symbolic
-- filesystem
--
-- There will be multiple specifications including:
--
-- * Complete symbolic file specifications (including concrete regions)
-- * Symbolic overlays on otherwise concrete files
data SymbolicFileContents =
SymbolicContents { symbolicContentSize :: Word64
}
deriving (Show, Generic)
instance JSON.FromJSON SymbolicFileContents
-- | A description of the contents of a symbolic filesystem
--
-- This includes high-level metadata and the specifications for symbolic files.
--
-- Note that the file paths are /absolute/ paths within the symbolic filesystem
data SymbolicManifest =
SymbolicManifest { symbolicFiles :: [(FilePath, SymbolicFileContents)]
, useStdout :: Bool
, useStderr :: Bool
}
deriving (Show, Generic)
instance JSON.FromJSON SymbolicManifest
-- | A file path that is absolute within the symbolic filesystem we are building
newtype AbsolutePath = AbsolutePath FilePath
deriving (Eq, Ord, Show)
-- | Create an absolute path *within the symbolic filesystem* based on the root
-- FS path and the absolute path to a file in the real filesystem
--
-- This effectively strips the real root FS off of the absolute file path,
-- creating an absolute path within the symbolic FS.
toInternalAbsolutePath
:: FilePath
-- ^ The path to the root filesystem in the real (non-symbolic) filesystem
-> FilePath
-- ^ The absolute path to the file in the real (non-symbolic) filesystem
-> AbsolutePath
toInternalAbsolutePath pfx x = AbsolutePath (fromMaybe x (List.stripPrefix pfx x))
createSymbolicFile
:: (LCB.IsSymInterface sym)
=> sym
-> (FilePath, SymbolicFileContents)
-> IO (SymIO.FDTarget SymIO.In, [WI.SymBV sym 8])
createSymbolicFile sym (internalAbsPath, symContent) =
case symContent of
SymbolicContents { symbolicContentSize = numBytes } -> do
bytes <- T.forM [0.. numBytes - 1] $ \byteNum -> do
let symName = WI.safeSymbol (internalAbsPath ++ "_" ++ show byteNum)
WI.freshConstant sym symName (WT.BaseBVRepr (PN.knownNat @8))
return (SymIO.FileTarget internalAbsPath, bytes)
-- | Load the symbolic filesystem at the given file path
--
-- Note that this will throw an exception if:
--
-- * The symbolic manifest declares an overlay for a file that does not exist in the concrete portion of the filesystem
loadInitialFiles
:: (LCB.IsSymInterface sym)
=> sym
-> FilePath
-> IO (SymIO.InitialFileSystemContents sym)
loadInitialFiles sym fsRoot = do
-- FIXME: Use the lower-level fold primitive that enables exception handling;
-- this version just spews errors to stderr, which is inappropriate.
let concreteFilesRoot = fsRoot </> "root"
let isRegular = SFF.fileType SFF.==? SFF.RegularFile
concreteFilePaths <- SFF.find SFF.always isRegular concreteFilesRoot
-- Check if standard input has been specified as a concrete file
let stdinPath = fsRoot </> T.unpack (SymIO.fdTargetToText SymIO.StdinTarget)
hasStdin <- SD.doesFileExist stdinPath
-- Note that all of these paths are absolute *if* @fsRoot@ was absolute.
-- Also, if it has leading .. components, they will be included. We need to
-- normalize these paths so that they have @fsRoot@ stripped off (and thus are
-- absolute in the symbolic filesystem)
let relativePaths = [ (p, toInternalAbsolutePath concreteFilesRoot p)
| p <- concreteFilePaths
]
concFiles <- mapM (\(p, name) -> (name,) <$> BS.readFile p) relativePaths
let concMap0 = Map.fromList [ (SymIO.FileTarget p, bytes) | (AbsolutePath p, bytes) <- concFiles ]
concMap1 <-
if | hasStdin -> do
stdinBytes <- BS.readFile stdinPath
return (Map.insert SymIO.StdinTarget stdinBytes concMap0)
| otherwise -> return concMap0
let manifestFilePath = fsRoot </> "system-manifest.json"
hasManifest <- SD.doesFileExist manifestFilePath
case hasManifest of
False ->
return SymIO.InitialFileSystemContents { SymIO.concreteFiles = concMap1
, SymIO.symbolicFiles = Map.empty
, SymIO.useStdout = False
, SymIO.useStderr = False
}
True -> do
manifestBytes <- BS.readFile manifestFilePath
case JSON.eitherDecodeStrict manifestBytes of
Left msg -> X.throwIO (ErrorDecodingJSON msg)
Right symManifest -> do
symFiles <- mapM (createSymbolicFile sym) (symbolicFiles symManifest)
F.forM_ symFiles $ \(fdTarget, _) -> do
case Map.lookup fdTarget concMap1 of
Nothing -> return ()
Just _ -> X.throwIO (FileSpecifiedAsSymbolicAndConcrete fdTarget)
return SymIO.InitialFileSystemContents { SymIO.concreteFiles = concMap1
, SymIO.symbolicFiles = Map.fromList symFiles
, SymIO.useStdout = useStdout symManifest
, SymIO.useStderr = useStderr symManifest
}