{-# LANGUAGE UndecidableInstances #-} module Hackage.Security.TUF.Snapshot ( Snapshot(..) ) where import Control.Monad.Except import Control.Monad.Reader import Hackage.Security.JSON import Hackage.Security.TUF.Header import Hackage.Security.TUF.FileInfo import Hackage.Security.TUF.FileMap import Hackage.Security.TUF.Layout.Repo import Hackage.Security.TUF.Signed import qualified Hackage.Security.TUF.FileMap as FileMap import Hackage.Security.Util.Pretty (pretty) {------------------------------------------------------------------------------- Datatypes -------------------------------------------------------------------------------} data Snapshot = Snapshot { snapshotVersion :: FileVersion , snapshotExpires :: FileExpires -- | File info for the root metadata -- -- We list this explicitly in the snapshot so that we can check if we need -- to update the root metadata without first having to download the entire -- index tarball. , snapshotInfoRoot :: FileInfo -- | File info for the mirror metadata , snapshotInfoMirrors :: FileInfo -- | Compressed index tarball , snapshotInfoTarGz :: FileInfo -- | Uncompressed index tarball -- -- Repositories are not required to provide this. , snapshotInfoTar :: Maybe FileInfo } instance HasHeader Snapshot where fileVersion f x = (\y -> x { snapshotVersion = y }) <$> f (snapshotVersion x) fileExpires f x = (\y -> x { snapshotExpires = y }) <$> f (snapshotExpires x) {------------------------------------------------------------------------------- JSON -------------------------------------------------------------------------------} instance MonadReader RepoLayout m => ToJSON m Snapshot where toJSON Snapshot{..} = do repoLayout <- ask mkObject [ ("_type" , return $ JSString "Snapshot") , ("version" , toJSON snapshotVersion) , ("expires" , toJSON snapshotExpires) , ("meta" , toJSON (snapshotMeta repoLayout)) ] where snapshotMeta repoLayout = FileMap.fromList $ [ (pathRoot repoLayout , snapshotInfoRoot) , (pathMirrors repoLayout , snapshotInfoMirrors) , (pathIndexTarGz repoLayout , snapshotInfoTarGz) ] ++ [ (pathIndexTar repoLayout , infoTar) | Just infoTar <- [snapshotInfoTar] ] instance ( MonadReader RepoLayout m , MonadError DeserializationError m , ReportSchemaErrors m ) => FromJSON m Snapshot where fromJSON enc = do verifyType enc "Snapshot" repoLayout <- ask snapshotVersion <- fromJSField enc "version" snapshotExpires <- fromJSField enc "expires" snapshotMeta <- fromJSField enc "meta" let lookupMeta k = case FileMap.lookup k snapshotMeta of Nothing -> expected ("\"" ++ pretty k ++ "\" entry in .meta object") Nothing Just v -> pure v snapshotInfoRoot <- lookupMeta (pathRoot repoLayout) snapshotInfoMirrors <- lookupMeta (pathMirrors repoLayout) snapshotInfoTarGz <- lookupMeta (pathIndexTarGz repoLayout) let snapshotInfoTar = FileMap.lookup (pathIndexTar repoLayout) snapshotMeta return Snapshot{..} instance (MonadKeys m, MonadReader RepoLayout m) => FromJSON m (Signed Snapshot) where fromJSON = signedFromJSON {------------------------------------------------------------------------------- Paths used in the snapshot NOTE: Since the snapshot lives in the top-level directory of the repository, we can safely reinterpret "relative to the repo root" as "relative to the snapshot"; hence, this use of 'castRoot' is okay. -------------------------------------------------------------------------------} pathRoot, pathMirrors, pathIndexTarGz, pathIndexTar :: RepoLayout -> TargetPath pathRoot = TargetPathRepo . repoLayoutRoot pathMirrors = TargetPathRepo . repoLayoutMirrors pathIndexTarGz = TargetPathRepo . repoLayoutIndexTarGz pathIndexTar = TargetPathRepo . repoLayoutIndexTar