-- | This module provides functionality for quantum random data operations involving -- the local data store and/or the settings file. -- -- It also provides a way to coordinate access to these local files. -- See 'AccessControl' for details. Any IO operation that uses these files can used in a -- coordinated way by wrapping them in a 'withAccess'. -- -- Some of these functions already come in special access-controlled variants because they only -- require access in particular branches or phases of execution. -- In particular we have 'addSafely', 'extractSafely' and 'observeSafely'. -- -- Finally, there is functionality to ensure that a forked thread is allowed to finish, in case -- main would otherwise return too soon. This is primarily needed to provide 'addConcurrently', -- but it can be re-used by forking a thread with 'forkSafely' and exiting the program with -- 'exitSafely'. -- -- Usually to be imported via the "Quantum.Random" module. module Quantum.Random.Store ( -- * Data store operations -- ** Basic store access getStoreFile, getStore, getStoreBytes, storeSize, save, status, -- ** Store update putStore, putStoreBytes, appendToStore, addToStore, addSafely, addConcurrently, fill, refill, load, clearStore, -- ** Primary store access extract, extractSafely, -- ** Store data display observe, observeSafely, peek, peekAll, display_, -- * Settings file operations -- ** Settings access getMinStoreSize, getTargetStoreSize, getDefaultStyle, -- ** Settings update setMinStoreSize, setTargetStoreSize, setDefaultStyle, restoreDefaults, reinitialize, -- * Store access control AccessControl, initAccessControl, withAccess, forkSafely, exitSafely ) where import Paths_quantum_random import Quantum.Random.Codec import Quantum.Random.ANU import Quantum.Random.Display import Quantum.Random.Exceptions import Quantum.Random.Mutex import System.IO (openBinaryFile, IOMode (..), hClose) import System.Directory (doesFileExist) import Data.Aeson (encode) import Data.Word (Word8) import Data.ByteString (ByteString, readFile, writeFile, pack, unpack, hPut, length) import qualified Data.ByteString.Lazy as Lazy (fromStrict, toStrict) import Control.Concurrent (ThreadId) import Prelude hiding (readFile, writeFile, length) ---- Data/Settings file locations ---- -- | Get path of local store file set up by cabal on installation. getStoreFile :: IO FilePath getStoreFile = getDataFileName "qr_data/qr_store.bin" -- | Get path of local settings file set up by cabal on installation. getSettingsFile :: IO FilePath getSettingsFile = getDataFileName "qr_data/qr_settings.json" ---- Settings access and update ---- getSettings :: IO QRSettings getSettings = throwLeft . fmap (parseSettings . Lazy.fromStrict) $ readFile =<< getSettingsFile putSettings :: QRSettings -> IO () putSettings qs = do file <- getSettingsFile writeFile file . Lazy.toStrict . encode $ qs -- | Query the settings file for the minimum store size setting. getMinStoreSize :: IO Int getMinStoreSize = minStoreSize <$> getSettings -- | Query the settings file for the target store size setting. getTargetStoreSize :: IO Int getTargetStoreSize = targetStoreSize <$> getSettings -- | Query the settings file for the default display style. getDefaultStyle :: IO DisplayStyle getDefaultStyle = defaultDisplayStyle <$> getSettings -- | Update the minimum store size setting in the settings file. setMinStoreSize :: Int -> IO () setMinStoreSize n = updateMinSize n <$> getSettings >>= putSettings -- | Update the target store size setting in the settings file. setTargetStoreSize :: Int -> IO () setTargetStoreSize n = updateTarSize n <$> getSettings >>= putSettings -- | Update the default 'DisplayStyle' setting in the settings file. setDefaultStyle :: DisplayStyle -> IO () setDefaultStyle sty = updateDefaultStyle sty <$> getSettings >>= putSettings -- | Restore default settings. restoreDefaults :: IO () restoreDefaults = putSettings defaults -- | Restore default settings and 'refill' the store. reinitialize :: IO () reinitialize = restoreDefaults *> refill ---- Basic store access ---- -- | Retrieve quantum random data from local store as a raw bytestring. getStore :: IO ByteString getStore = getStoreFile >>= readFile -- | Insert data into local store as a raw bytestring, overwriting any current contents. putStore :: ByteString -> IO () putStore bs = getStoreFile >>= flip writeFile bs -- | Retrieve quantum random data from local store as a list of bytes. getStoreBytes :: IO [Word8] getStoreBytes = unpack <$> getStore -- | Insert data into local store as a list of bytes, overwriting any current contents. putStoreBytes :: [Word8] -> IO () putStoreBytes = putStore . pack -- | Compute the size of the current data store. storeSize :: IO Int storeSize = length <$> getStore -- | Save the data store to another file, specified by the provided path. -- Asks for overwrite confirmation if the file already exists. save :: String -> IO () save path = do exists <- doesFileExist path qs <- getStore case exists of False -> writeFile path qs True -> do putStrLn "File already exists. Enter 'yes' to overwrite." i <- getLine case i of "yes" -> writeFile path qs *> putStrLn "Data saved." _ -> putStrLn "Save aborted." -- | Display status information: Current store size, minimum size setting, target -- size setting, default display style and data file path. status :: IO () status = do smin <- getMinStoreSize star <- getTargetStoreSize sty <- getDefaultStyle siz <- storeSize sto <- getStoreFile mapM_ putStrLn [ "Store contains " ++ bitsNBytes siz ++ " of quantum random data." , "" , "Minimum store size set to " ++ bitsNBytes smin ++ "." , "Target store size set to " ++ bitsNBytes star ++ "." , "" , "Default display style: " ++ show sty ++ "." , "" , "Local data store location:" , sto , "" ] bitsNBytes :: Int -> String bitsNBytes n = show n ++ bytes ++ show (n*8) ++ " bits)" where bytes = if n /= 1 then " bytes (" else " byte (" ---- Store update ---- -- | Remove all data from the store. clearStore :: IO () clearStore = putStore mempty -- | Append the supplied bytestring to the store file. appendToStore :: ByteString -> IO () appendToStore bs = do storefile <- getStoreFile h <- openBinaryFile storefile AppendMode hPut h bs hClose h -- | Retrieve the specified number of QRN bytes and add them to the store. addToStore :: Int -> IO () addToStore n = do bs <- pack <$> fetchQR n appendToStore bs -- | Like `addToStore`, but uses 'AccessControl' to ensure that file writing doesn't interfere with -- other operations. addSafely :: AccessControl -> Int -> IO () addSafely acc n = do bs <- pack <$> fetchQR n withAccess acc (appendToStore bs) -- | Fork a thread to add data to the store concurrently. addConcurrently :: AccessControl -> Int -> IO ThreadId addConcurrently acc n = forkSafely acc $ addSafely acc n -- | Load binary data from specified file path, append it to the data store. load :: String -> IO () load path = do exists <- doesFileExist path case exists of False -> putStrLn "Load failed. File does not exist." True -> readFile path >>= appendToStore -- | Calculate the amount of data needed to reach target store size and retrieve it from ANU. fill :: IO () fill = do targ <- targetStoreSize <$> getSettings qs <- getStoreBytes size <- storeSize case (compare size targ) of LT -> do anu <- fetchQR (targ - size) putStoreBytes $ qs ++ anu _ -> pure () -- | Refill data store to target size, discarding data already present. refill :: IO () refill = getTargetStoreSize >>= fetchQR >>= putStoreBytes ---- Primary store access, including update/display ---- -- | Get the specified number of QRN bytes, either from the store and/or by -- obtaining more from ANU as needed. As the name implies, the obtained bytes -- are removed from the store. If the store is left with fewer than the -- minimum number of QRN bytes it is filled back to the target size. extract :: Int -> IO [Word8] extract n = do size <- storeSize qs <- getStoreBytes st <- getSettings let delta = size - minStoreSize st let needed = targetStoreSize st + n - size case (compare n delta) of GT -> do anu <- fetchQR needed let (xs,ys) = splitAt n $ qs ++ anu putStoreBytes ys pure xs _ -> do let (xs,ys) = splitAt n qs putStoreBytes ys pure xs -- | Access-controlled version of 'extract'. extractSafely :: AccessControl -> Int -> IO [Word8] extractSafely acc n = do size <- storeSize qs <- getStoreBytes st <- getSettings let delta = size - minStoreSize st -- "current margin" let needed = targetStoreSize st + n - size -- "what you want minus what you have" case (compare n delta, compare n size) of (GT,GT) -> do anu <- fetchQR needed -- if n > margin then must req ANU let (xs,ys) = splitAt n $ qs ++ anu withAccess acc $ putStoreBytes ys pure xs (GT,_) -> do addConcurrently acc needed let (xs,ys) = splitAt n qs withAccess acc $ putStoreBytes ys pure xs _ -> do let (xs,ys) = splitAt n qs withAccess acc $ putStoreBytes ys pure xs -- | Like 'display' only taking a `Maybe` 'DisplayStyle' as the first argument, where `Nothing` -- signifies using the default display style. display_ :: Maybe DisplayStyle -> [Word8] -> IO () display_ (Just style) ws = display style ws display_ Nothing ws = getDefaultStyle >>= flip display ws -- | Destructively view the specified number of bytes, via 'extract'. -- The name connotes the irreversibility of quantum measurement. -- Measuring quantum data (analogously, viewing or using) expends them as a randomness resource. -- Thus they are discarded. Use 'peek' if instead you wish the data to be kept. observe :: Maybe DisplayStyle -> Int -> IO () observe ms n = extract n >>= display_ ms -- | Destructively view the specified number of bytes, via 'extractSafely'. -- Access-controlled version of 'observe'. observeSafely :: AccessControl -> Maybe DisplayStyle -> Int -> IO () observeSafely a ms n = extractSafely a n >>= display_ ms -- | Non-destructively view the specified number of bytes. peek :: Maybe DisplayStyle -> Int -> IO () peek ms n = take n <$> getStoreBytes >>= display_ ms -- | Non-destructively view all data in the store. peekAll :: Maybe DisplayStyle -> IO () peekAll ms = getStoreBytes >>= display_ ms