-- | Abstract definition of a Repository
--
-- Most clients should only need to import this module if they wish to define
-- their own Repository implementations.
{-# LANGUAGE CPP #-}
module Hackage.Security.Client.Repository (
    -- * Files
    Metadata  -- type index (really a kind)
  , Binary    -- type index (really a kind)
  , RemoteFile(..)
  , CachedFile(..)
  , IndexFile(..)
  , remoteFileDefaultFormat
  , remoteFileDefaultInfo
    -- * Repository proper
  , Repository(..)
  , AttemptNr(..)
  , LogMessage(..)
  , UpdateFailure(..)
  , SomeRemoteError(..)
    -- ** Downloaded files
  , DownloadedFile(..)
    -- ** Helpers
  , mirrorsUnsupported
    -- * Paths
  , remoteRepoPath
  , remoteRepoPath'
    -- * Utility
  , IsCached(..)
  , mustCache
  ) where

import Prelude
import Control.Exception
import Data.Kind (Type)
import Data.Typeable (Typeable)
import qualified Codec.Archive.Tar.Index as Tar
import qualified Data.ByteString.Lazy    as BS.L

import Distribution.Package
import Distribution.Text

import Hackage.Security.Client.Formats
import Hackage.Security.Client.Verify
import Hackage.Security.Trusted
import Hackage.Security.TUF
import Hackage.Security.Util.Checked
import Hackage.Security.Util.Path
import Hackage.Security.Util.Pretty
import Hackage.Security.Util.Some
import Hackage.Security.Util.Stack

{-------------------------------------------------------------------------------
  Files
-------------------------------------------------------------------------------}

data Metadata
data Binary

-- | Abstract definition of files we might have to download
--
-- 'RemoteFile' is parametrized by the type of the formats that we can accept
-- from the remote repository, as well as with information on whether this file
-- is metadata actual binary content.
--
-- NOTE: Haddock lacks GADT support so constructors have only regular comments.
data RemoteFile :: Type -> Type -> Type where
    -- Timestamp metadata (@timestamp.json@)
    --
    -- We never have (explicit) file length available for timestamps.
    RemoteTimestamp :: RemoteFile (FormatUn :- ()) Metadata

    -- Root metadata (@root.json@)
    --
    -- For root information we may or may not have the file info available:
    --
    -- - If during the normal update process the new snapshot tells us the root
    --   information has changed, we can use the file info from the snapshot.
    -- - If however we need to update the root metadata due to a verification
    --   exception we do not know the file info.
    -- - We also do not know the file info during bootstrapping.
    RemoteRoot :: Maybe (Trusted FileInfo) -> RemoteFile (FormatUn :- ()) Metadata

    -- Snapshot metadata (@snapshot.json@)
    --
    -- We get file info of the snapshot from the timestamp.
    RemoteSnapshot :: Trusted FileInfo -> RemoteFile (FormatUn :- ()) Metadata

    -- Mirrors metadata (@mirrors.json@)
    --
    -- We get the file info from the snapshot.
    RemoteMirrors :: Trusted FileInfo -> RemoteFile (FormatUn :- ()) Metadata

    -- Index
    --
    -- The index file length comes from the snapshot.
    --
    -- When we request that the index is downloaded, it is up to the repository
    -- to decide whether to download @00-index.tar@ or @00-index.tar.gz@.
    -- The callback is told which format was requested.
    --
    -- It is a bug to request a file that the repository does not provide
    -- (the snapshot should make it clear which files are available).
    RemoteIndex :: HasFormat fs FormatGz
                -> Formats fs (Trusted FileInfo)
                -> RemoteFile fs Binary

    -- Actual package
    --
    -- Package file length comes from the corresponding @targets.json@.
    RemotePkgTarGz :: PackageIdentifier
                   -> Trusted FileInfo
                   -> RemoteFile (FormatGz :- ()) Binary

deriving instance Show (RemoteFile fs typ)

instance Pretty (RemoteFile fs typ) where
  pretty :: RemoteFile fs typ -> String
pretty RemoteFile fs typ
RemoteTimestamp          = String
"timestamp"
  pretty (RemoteRoot Maybe (Trusted FileInfo)
_)           = String
"root"
  pretty (RemoteSnapshot Trusted FileInfo
_)       = String
"snapshot"
  pretty (RemoteMirrors Trusted FileInfo
_)        = String
"mirrors"
  pretty (RemoteIndex HasFormat fs FormatGz
_ Formats fs (Trusted FileInfo)
_)        = String
"index"
  pretty (RemotePkgTarGz PackageIdentifier
pkgId Trusted FileInfo
_) = String
"package " String -> ShowS
forall a. [a] -> [a] -> [a]
++ PackageIdentifier -> String
forall a. Pretty a => a -> String
display PackageIdentifier
pkgId

-- | Files that we might request from the local cache
data CachedFile =
    -- | Timestamp metadata (@timestamp.json@)
    CachedTimestamp

    -- | Root metadata (@root.json@)
  | CachedRoot

    -- | Snapshot metadata (@snapshot.json@)
  | CachedSnapshot

    -- | Mirrors list (@mirrors.json@)
  | CachedMirrors
  deriving (CachedFile -> CachedFile -> Bool
(CachedFile -> CachedFile -> Bool)
-> (CachedFile -> CachedFile -> Bool) -> Eq CachedFile
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: CachedFile -> CachedFile -> Bool
== :: CachedFile -> CachedFile -> Bool
$c/= :: CachedFile -> CachedFile -> Bool
/= :: CachedFile -> CachedFile -> Bool
Eq, Eq CachedFile
Eq CachedFile =>
(CachedFile -> CachedFile -> Ordering)
-> (CachedFile -> CachedFile -> Bool)
-> (CachedFile -> CachedFile -> Bool)
-> (CachedFile -> CachedFile -> Bool)
-> (CachedFile -> CachedFile -> Bool)
-> (CachedFile -> CachedFile -> CachedFile)
-> (CachedFile -> CachedFile -> CachedFile)
-> Ord CachedFile
CachedFile -> CachedFile -> Bool
CachedFile -> CachedFile -> Ordering
CachedFile -> CachedFile -> CachedFile
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: CachedFile -> CachedFile -> Ordering
compare :: CachedFile -> CachedFile -> Ordering
$c< :: CachedFile -> CachedFile -> Bool
< :: CachedFile -> CachedFile -> Bool
$c<= :: CachedFile -> CachedFile -> Bool
<= :: CachedFile -> CachedFile -> Bool
$c> :: CachedFile -> CachedFile -> Bool
> :: CachedFile -> CachedFile -> Bool
$c>= :: CachedFile -> CachedFile -> Bool
>= :: CachedFile -> CachedFile -> Bool
$cmax :: CachedFile -> CachedFile -> CachedFile
max :: CachedFile -> CachedFile -> CachedFile
$cmin :: CachedFile -> CachedFile -> CachedFile
min :: CachedFile -> CachedFile -> CachedFile
Ord, Int -> CachedFile -> ShowS
[CachedFile] -> ShowS
CachedFile -> String
(Int -> CachedFile -> ShowS)
-> (CachedFile -> String)
-> ([CachedFile] -> ShowS)
-> Show CachedFile
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> CachedFile -> ShowS
showsPrec :: Int -> CachedFile -> ShowS
$cshow :: CachedFile -> String
show :: CachedFile -> String
$cshowList :: [CachedFile] -> ShowS
showList :: [CachedFile] -> ShowS
Show)

instance Pretty CachedFile where
  pretty :: CachedFile -> String
pretty CachedFile
CachedTimestamp = String
"timestamp"
  pretty CachedFile
CachedRoot      = String
"root"
  pretty CachedFile
CachedSnapshot  = String
"snapshot"
  pretty CachedFile
CachedMirrors   = String
"mirrors"

-- | Default format for each file type
--
-- For most file types we don't have a choice; for the index the repository
-- is only required to offer the GZip-compressed format so that is the default.
remoteFileDefaultFormat :: RemoteFile fs typ -> Some (HasFormat fs)
remoteFileDefaultFormat :: forall fs typ. RemoteFile fs typ -> Some (HasFormat fs)
remoteFileDefaultFormat RemoteFile fs typ
RemoteTimestamp      = HasFormat fs FormatUn -> Some (HasFormat fs)
forall (f :: * -> *) a. f a -> Some f
Some (HasFormat fs FormatUn -> Some (HasFormat fs))
-> HasFormat fs FormatUn -> Some (HasFormat fs)
forall a b. (a -> b) -> a -> b
$ Format FormatUn -> HasFormat (FormatUn :- ()) FormatUn
forall b fs. Format b -> HasFormat (b :- fs) b
HFZ Format FormatUn
FUn
remoteFileDefaultFormat (RemoteRoot Maybe (Trusted FileInfo)
_)       = HasFormat fs FormatUn -> Some (HasFormat fs)
forall (f :: * -> *) a. f a -> Some f
Some (HasFormat fs FormatUn -> Some (HasFormat fs))
-> HasFormat fs FormatUn -> Some (HasFormat fs)
forall a b. (a -> b) -> a -> b
$ Format FormatUn -> HasFormat (FormatUn :- ()) FormatUn
forall b fs. Format b -> HasFormat (b :- fs) b
HFZ Format FormatUn
FUn
remoteFileDefaultFormat (RemoteSnapshot Trusted FileInfo
_)   = HasFormat fs FormatUn -> Some (HasFormat fs)
forall (f :: * -> *) a. f a -> Some f
Some (HasFormat fs FormatUn -> Some (HasFormat fs))
-> HasFormat fs FormatUn -> Some (HasFormat fs)
forall a b. (a -> b) -> a -> b
$ Format FormatUn -> HasFormat (FormatUn :- ()) FormatUn
forall b fs. Format b -> HasFormat (b :- fs) b
HFZ Format FormatUn
FUn
remoteFileDefaultFormat (RemoteMirrors Trusted FileInfo
_)    = HasFormat fs FormatUn -> Some (HasFormat fs)
forall (f :: * -> *) a. f a -> Some f
Some (HasFormat fs FormatUn -> Some (HasFormat fs))
-> HasFormat fs FormatUn -> Some (HasFormat fs)
forall a b. (a -> b) -> a -> b
$ Format FormatUn -> HasFormat (FormatUn :- ()) FormatUn
forall b fs. Format b -> HasFormat (b :- fs) b
HFZ Format FormatUn
FUn
remoteFileDefaultFormat (RemotePkgTarGz PackageIdentifier
_ Trusted FileInfo
_) = HasFormat fs FormatGz -> Some (HasFormat fs)
forall (f :: * -> *) a. f a -> Some f
Some (HasFormat fs FormatGz -> Some (HasFormat fs))
-> HasFormat fs FormatGz -> Some (HasFormat fs)
forall a b. (a -> b) -> a -> b
$ Format FormatGz -> HasFormat (FormatGz :- ()) FormatGz
forall b fs. Format b -> HasFormat (b :- fs) b
HFZ Format FormatGz
FGz
remoteFileDefaultFormat (RemoteIndex HasFormat fs FormatGz
pf Formats fs (Trusted FileInfo)
_)   = HasFormat fs FormatGz -> Some (HasFormat fs)
forall (f :: * -> *) a. f a -> Some f
Some HasFormat fs FormatGz
pf

-- | Default file info (see also 'remoteFileDefaultFormat')
remoteFileDefaultInfo :: RemoteFile fs typ -> Maybe (Trusted FileInfo)
remoteFileDefaultInfo :: forall fs typ. RemoteFile fs typ -> Maybe (Trusted FileInfo)
remoteFileDefaultInfo RemoteFile fs typ
RemoteTimestamp         = Maybe (Trusted FileInfo)
forall a. Maybe a
Nothing
remoteFileDefaultInfo (RemoteRoot Maybe (Trusted FileInfo)
info)       = Maybe (Trusted FileInfo)
info
remoteFileDefaultInfo (RemoteSnapshot Trusted FileInfo
info)   = Trusted FileInfo -> Maybe (Trusted FileInfo)
forall a. a -> Maybe a
Just Trusted FileInfo
info
remoteFileDefaultInfo (RemoteMirrors Trusted FileInfo
info)    = Trusted FileInfo -> Maybe (Trusted FileInfo)
forall a. a -> Maybe a
Just Trusted FileInfo
info
remoteFileDefaultInfo (RemotePkgTarGz PackageIdentifier
_ Trusted FileInfo
info) = Trusted FileInfo -> Maybe (Trusted FileInfo)
forall a. a -> Maybe a
Just Trusted FileInfo
info
remoteFileDefaultInfo (RemoteIndex HasFormat fs FormatGz
pf Formats fs (Trusted FileInfo)
info)   = Trusted FileInfo -> Maybe (Trusted FileInfo)
forall a. a -> Maybe a
Just (Trusted FileInfo -> Maybe (Trusted FileInfo))
-> Trusted FileInfo -> Maybe (Trusted FileInfo)
forall a b. (a -> b) -> a -> b
$ HasFormat fs FormatGz
-> Formats fs (Trusted FileInfo) -> Trusted FileInfo
forall fs f a. HasFormat fs f -> Formats fs a -> a
formatsLookup HasFormat fs FormatGz
pf Formats fs (Trusted FileInfo)
info

{-------------------------------------------------------------------------------
  Repository proper
-------------------------------------------------------------------------------}

-- | Repository
--
-- This is an abstract representation of a repository. It simply provides a way
-- to download metafiles and target files, without specifying how this is done.
-- For instance, for a local repository this could just be doing a file read,
-- whereas for remote repositories this could be using any kind of HTTP client.
data Repository down = DownloadedFile down => Repository {
    -- | Get a file from the server
    --
    -- Responsibilies of 'repGetRemote':
    --
    -- * Download the file from the repository and make it available at a
    --   temporary location
    -- * Use the provided file length to protect against endless data attacks.
    --   (Repositories such as local repositories that are not susceptible to
    --   endless data attacks can safely ignore this argument.)
    -- * Move the file from its temporary location to its permanent location
    --   if verification succeeds.
    --
    -- NOTE: Calls to 'repGetRemote' should _always_ be in the scope of
    -- 'repWithMirror'.
    forall (down :: * -> *).
Repository down
-> forall fs typ.
   Throws SomeRemoteError =>
   AttemptNr
   -> RemoteFile fs typ -> Verify (Some (HasFormat fs), down typ)
repGetRemote :: forall fs typ. Throws SomeRemoteError
                 => AttemptNr
                 -> RemoteFile fs typ
                 -> Verify (Some (HasFormat fs), down typ)

    -- | Get a cached file (if available)
  , forall (down :: * -> *).
Repository down -> CachedFile -> IO (Maybe (Path Absolute))
repGetCached :: CachedFile -> IO (Maybe (Path Absolute))

    -- | Get the cached root
    --
    -- This is a separate method only because clients must ALWAYS have root
    -- information available.
  , forall (down :: * -> *). Repository down -> IO (Path Absolute)
repGetCachedRoot :: IO (Path Absolute)

    -- | Clear all cached data
    --
    -- In particular, this should remove the snapshot and the timestamp.
    -- It would also be okay, but not required, to delete the index.
  , forall (down :: * -> *). Repository down -> IO ()
repClearCache :: IO ()

    -- | Open the tarball for reading
    --
    -- This function has this shape so that:
    --
    -- * We can read multiple files from the tarball without having to open
    --   and close the handle each time
    -- * We can close the handle immediately when done.
  , forall (down :: * -> *).
Repository down -> forall a. (Handle -> IO a) -> IO a
repWithIndex :: forall a. (Handle -> IO a) -> IO a

    -- | Read the index index
  , forall (down :: * -> *). Repository down -> IO TarIndex
repGetIndexIdx :: IO Tar.TarIndex

    -- | Lock the cache (during updates)
  , forall (down :: * -> *). Repository down -> IO () -> IO ()
repLockCache :: IO () -> IO ()

    -- | Mirror selection
    --
    -- The purpose of 'repWithMirror' is to scope mirror selection. The idea
    -- is that if we have
    --
    -- > repWithMirror mirrorList $
    -- >   someCallback
    --
    -- then the repository may pick a mirror before calling @someCallback@,
    -- catch exceptions thrown by @someCallback@, and potentially try the
    -- callback again with a different mirror.
    --
    -- The list of mirrors may be @Nothing@ if we haven't yet downloaded the
    -- list of mirrors from the repository, or when our cached list of mirrors
    -- is invalid. Of course, if we did download it, then the list of mirrors
    -- may still be empty. In this case the repository must fall back to its
    -- primary download mechanism.
    --
    -- Mirrors as currently defined (in terms of a "base URL") are inherently a
    -- HTTP (or related) concept, so in repository implementations such as the
    -- local-repo 'repWithMirrors' is probably just an identity operation  (see
    -- 'ignoreMirrors').  Conversely, HTTP implementations of repositories may
    -- have other, out-of-band information (for example, coming from a cabal
    -- config file) that they may use to influence mirror selection.
  , forall (down :: * -> *).
Repository down -> forall a. Maybe [Mirror] -> IO a -> IO a
repWithMirror :: forall a. Maybe [Mirror] -> IO a -> IO a

    -- | Logging
  , forall (down :: * -> *). Repository down -> LogMessage -> IO ()
repLog :: LogMessage -> IO ()

    -- | Layout of this repository
  , forall (down :: * -> *). Repository down -> RepoLayout
repLayout :: RepoLayout

    -- | Layout of the index
    --
    -- Since the repository hosts the index, the layout of the index is
    -- not independent of the layout of the repository.
  , forall (down :: * -> *). Repository down -> IndexLayout
repIndexLayout :: IndexLayout

    -- | Description of the repository (used in the show instance)
  , forall (down :: * -> *). Repository down -> String
repDescription :: String
  }

instance Show (Repository down) where
  show :: Repository down -> String
show = Repository down -> String
forall (down :: * -> *). Repository down -> String
repDescription

-- | Helper function to implement 'repWithMirrors'.
mirrorsUnsupported :: Maybe [Mirror] -> IO a -> IO a
mirrorsUnsupported :: forall a. Maybe [Mirror] -> IO a -> IO a
mirrorsUnsupported Maybe [Mirror]
_ = IO a -> IO a
forall a. a -> a
id

-- | Are we requesting this information because of a previous validation error?
--
-- Clients can take advantage of this to tell caches to revalidate files.
newtype AttemptNr = AttemptNr Int
  deriving (AttemptNr -> AttemptNr -> Bool
(AttemptNr -> AttemptNr -> Bool)
-> (AttemptNr -> AttemptNr -> Bool) -> Eq AttemptNr
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: AttemptNr -> AttemptNr -> Bool
== :: AttemptNr -> AttemptNr -> Bool
$c/= :: AttemptNr -> AttemptNr -> Bool
/= :: AttemptNr -> AttemptNr -> Bool
Eq, Eq AttemptNr
Eq AttemptNr =>
(AttemptNr -> AttemptNr -> Ordering)
-> (AttemptNr -> AttemptNr -> Bool)
-> (AttemptNr -> AttemptNr -> Bool)
-> (AttemptNr -> AttemptNr -> Bool)
-> (AttemptNr -> AttemptNr -> Bool)
-> (AttemptNr -> AttemptNr -> AttemptNr)
-> (AttemptNr -> AttemptNr -> AttemptNr)
-> Ord AttemptNr
AttemptNr -> AttemptNr -> Bool
AttemptNr -> AttemptNr -> Ordering
AttemptNr -> AttemptNr -> AttemptNr
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: AttemptNr -> AttemptNr -> Ordering
compare :: AttemptNr -> AttemptNr -> Ordering
$c< :: AttemptNr -> AttemptNr -> Bool
< :: AttemptNr -> AttemptNr -> Bool
$c<= :: AttemptNr -> AttemptNr -> Bool
<= :: AttemptNr -> AttemptNr -> Bool
$c> :: AttemptNr -> AttemptNr -> Bool
> :: AttemptNr -> AttemptNr -> Bool
$c>= :: AttemptNr -> AttemptNr -> Bool
>= :: AttemptNr -> AttemptNr -> Bool
$cmax :: AttemptNr -> AttemptNr -> AttemptNr
max :: AttemptNr -> AttemptNr -> AttemptNr
$cmin :: AttemptNr -> AttemptNr -> AttemptNr
min :: AttemptNr -> AttemptNr -> AttemptNr
Ord, Integer -> AttemptNr
AttemptNr -> AttemptNr
AttemptNr -> AttemptNr -> AttemptNr
(AttemptNr -> AttemptNr -> AttemptNr)
-> (AttemptNr -> AttemptNr -> AttemptNr)
-> (AttemptNr -> AttemptNr -> AttemptNr)
-> (AttemptNr -> AttemptNr)
-> (AttemptNr -> AttemptNr)
-> (AttemptNr -> AttemptNr)
-> (Integer -> AttemptNr)
-> Num AttemptNr
forall a.
(a -> a -> a)
-> (a -> a -> a)
-> (a -> a -> a)
-> (a -> a)
-> (a -> a)
-> (a -> a)
-> (Integer -> a)
-> Num a
$c+ :: AttemptNr -> AttemptNr -> AttemptNr
+ :: AttemptNr -> AttemptNr -> AttemptNr
$c- :: AttemptNr -> AttemptNr -> AttemptNr
- :: AttemptNr -> AttemptNr -> AttemptNr
$c* :: AttemptNr -> AttemptNr -> AttemptNr
* :: AttemptNr -> AttemptNr -> AttemptNr
$cnegate :: AttemptNr -> AttemptNr
negate :: AttemptNr -> AttemptNr
$cabs :: AttemptNr -> AttemptNr
abs :: AttemptNr -> AttemptNr
$csignum :: AttemptNr -> AttemptNr
signum :: AttemptNr -> AttemptNr
$cfromInteger :: Integer -> AttemptNr
fromInteger :: Integer -> AttemptNr
Num)

-- | Log messages
--
-- We use a 'RemoteFile' rather than a 'RepoPath' here because we might not have
-- a 'RepoPath' for the file that we were trying to download (that is, for
-- example if the server does not provide an uncompressed tarball, it doesn't
-- make much sense to list the path to that non-existing uncompressed tarball).
data LogMessage =
    -- | Root information was updated
    --
    -- This message is issued when the root information is updated as part of
    -- the normal check for updates procedure. If the root information is
    -- updated because of a verification error WarningVerificationError is
    -- issued instead.
    LogRootUpdated

    -- | A verification error
    --
    -- Verification errors can be temporary, and may be resolved later; hence
    -- these are just warnings. (Verification errors that cannot be resolved
    -- are thrown as exceptions.)
  | LogVerificationError VerificationError

    -- | Download a file from a repository
  | forall fs typ. LogDownloading (RemoteFile fs typ)

    -- | Incrementally updating a file from a repository
  | forall fs. LogUpdating (RemoteFile fs Binary)

    -- | Selected a particular mirror
  | LogSelectedMirror MirrorDescription

    -- | Updating a file failed
    -- (we will instead download it whole)
  | forall fs. LogCannotUpdate (RemoteFile fs Binary) UpdateFailure

    -- | We got an exception with a particular mirror
    -- (we will try with a different mirror if any are available)
  | LogMirrorFailed MirrorDescription SomeException

    -- | This log event is triggered before invoking a filesystem lock
    -- operation that may block for a significant amount of time; once
    -- the possibly blocking call completes successfully,
    -- 'LogLockWaitDone' will be emitted.
    --
    -- @since 0.6.0
  | LogLockWait (Path Absolute)

    -- | Denotes completion of the operation that advertised a
    -- 'LogLockWait' event
    --
    -- @since 0.6.0
  | LogLockWaitDone (Path Absolute)

    -- | Denotes the filesystem lock previously acquired (signaled by
    -- 'LogLockWait') has been released.
    --
    -- @since 0.6.0
  | LogUnlock (Path Absolute)


-- | Records why we are downloading a file rather than updating it.
data UpdateFailure =
    -- | Server does not support incremental downloads
    UpdateImpossibleUnsupported

    -- | We don't have a local copy of the file to update
  | UpdateImpossibleNoLocalCopy

    -- | Update failed twice
    --
    -- If we attempt an incremental update the first time, and it fails,  we let
    -- it go round the loop, update local security information, and try again.
    -- But if an incremental update then fails _again_, we  instead attempt a
    -- regular download.
  | UpdateFailedTwice

    -- | Update failed (for example: perhaps the local file got corrupted)
  | UpdateFailed SomeException

{-------------------------------------------------------------------------------
  Downloaded files
-------------------------------------------------------------------------------}

class DownloadedFile (down :: Type -> Type) where
  -- | Verify a download file
  downloadedVerify :: down a -> Trusted FileInfo -> IO Bool

  -- | Read the file we just downloaded into memory
  --
  -- We never read binary data, only metadata.
  downloadedRead :: down Metadata -> IO BS.L.ByteString

  -- | Copy a downloaded file to its destination
  downloadedCopyTo :: down a -> Path Absolute -> IO ()

{-------------------------------------------------------------------------------
  Exceptions thrown by specific Repository implementations
-------------------------------------------------------------------------------}

-- | Repository-specific exceptions
--
-- For instance, for repositories using HTTP this might correspond to a 404;
-- for local repositories this might correspond to file-not-found, etc.
data SomeRemoteError :: Type where
    SomeRemoteError :: Exception e => e -> SomeRemoteError
  deriving (Typeable)

deriving instance Show SomeRemoteError
instance Exception SomeRemoteError where displayException :: SomeRemoteError -> String
displayException = SomeRemoteError -> String
forall a. Pretty a => a -> String
pretty

instance Pretty SomeRemoteError where
    pretty :: SomeRemoteError -> String
pretty (SomeRemoteError e
ex) = e -> String
forall e. Exception e => e -> String
displayException e
ex

{-------------------------------------------------------------------------------
  Paths
-------------------------------------------------------------------------------}

remoteRepoPath :: RepoLayout -> RemoteFile fs typ -> Formats fs RepoPath
remoteRepoPath :: forall fs typ.
RepoLayout -> RemoteFile fs typ -> Formats fs RepoPath
remoteRepoPath RepoLayout{RepoPath
PackageIdentifier -> RepoPath
repoLayoutRoot :: RepoPath
repoLayoutTimestamp :: RepoPath
repoLayoutSnapshot :: RepoPath
repoLayoutMirrors :: RepoPath
repoLayoutIndexTarGz :: RepoPath
repoLayoutIndexTar :: RepoPath
repoLayoutPkgTarGz :: PackageIdentifier -> RepoPath
repoLayoutPkgTarGz :: RepoLayout -> PackageIdentifier -> RepoPath
repoLayoutIndexTar :: RepoLayout -> RepoPath
repoLayoutIndexTarGz :: RepoLayout -> RepoPath
repoLayoutMirrors :: RepoLayout -> RepoPath
repoLayoutSnapshot :: RepoLayout -> RepoPath
repoLayoutTimestamp :: RepoLayout -> RepoPath
repoLayoutRoot :: RepoLayout -> RepoPath
..} = RemoteFile fs typ -> Formats fs RepoPath
forall fs typ. RemoteFile fs typ -> Formats fs RepoPath
go
  where
    go :: RemoteFile fs typ -> Formats fs RepoPath
    go :: forall fs typ. RemoteFile fs typ -> Formats fs RepoPath
go RemoteFile fs typ
RemoteTimestamp        = RepoPath -> Formats (FormatUn :- ()) RepoPath
forall b. b -> Formats (FormatUn :- ()) b
FsUn (RepoPath -> Formats (FormatUn :- ()) RepoPath)
-> RepoPath -> Formats (FormatUn :- ()) RepoPath
forall a b. (a -> b) -> a -> b
$ RepoPath
repoLayoutTimestamp
    go (RemoteRoot Maybe (Trusted FileInfo)
_)         = RepoPath -> Formats (FormatUn :- ()) RepoPath
forall b. b -> Formats (FormatUn :- ()) b
FsUn (RepoPath -> Formats (FormatUn :- ()) RepoPath)
-> RepoPath -> Formats (FormatUn :- ()) RepoPath
forall a b. (a -> b) -> a -> b
$ RepoPath
repoLayoutRoot
    go (RemoteSnapshot Trusted FileInfo
_)     = RepoPath -> Formats (FormatUn :- ()) RepoPath
forall b. b -> Formats (FormatUn :- ()) b
FsUn (RepoPath -> Formats (FormatUn :- ()) RepoPath)
-> RepoPath -> Formats (FormatUn :- ()) RepoPath
forall a b. (a -> b) -> a -> b
$ RepoPath
repoLayoutSnapshot
    go (RemoteMirrors Trusted FileInfo
_)      = RepoPath -> Formats (FormatUn :- ()) RepoPath
forall b. b -> Formats (FormatUn :- ()) b
FsUn (RepoPath -> Formats (FormatUn :- ()) RepoPath)
-> RepoPath -> Formats (FormatUn :- ()) RepoPath
forall a b. (a -> b) -> a -> b
$ RepoPath
repoLayoutMirrors
    go (RemotePkgTarGz PackageIdentifier
pId Trusted FileInfo
_) = RepoPath -> Formats (FormatGz :- ()) RepoPath
forall b. b -> Formats (FormatGz :- ()) b
FsGz (RepoPath -> Formats (FormatGz :- ()) RepoPath)
-> RepoPath -> Formats (FormatGz :- ()) RepoPath
forall a b. (a -> b) -> a -> b
$ PackageIdentifier -> RepoPath
repoLayoutPkgTarGz PackageIdentifier
pId
    go (RemoteIndex HasFormat fs FormatGz
_ Formats fs (Trusted FileInfo)
lens)   = (forall f. Format f -> Trusted FileInfo -> RepoPath)
-> Formats fs (Trusted FileInfo) -> Formats fs RepoPath
forall a b fs.
(forall f. Format f -> a -> b) -> Formats fs a -> Formats fs b
formatsMap Format f -> Trusted FileInfo -> RepoPath
forall f. Format f -> Trusted FileInfo -> RepoPath
forall f a. Format f -> a -> RepoPath
goIndex Formats fs (Trusted FileInfo)
lens

    goIndex :: Format f -> a -> RepoPath
    goIndex :: forall f a. Format f -> a -> RepoPath
goIndex Format f
FUn a
_ = RepoPath
repoLayoutIndexTar
    goIndex Format f
FGz a
_ = RepoPath
repoLayoutIndexTarGz

remoteRepoPath' :: RepoLayout -> RemoteFile fs typ -> HasFormat fs f -> RepoPath
remoteRepoPath' :: forall fs typ f.
RepoLayout -> RemoteFile fs typ -> HasFormat fs f -> RepoPath
remoteRepoPath' RepoLayout
repoLayout RemoteFile fs typ
file HasFormat fs f
format =
    HasFormat fs f -> Formats fs RepoPath -> RepoPath
forall fs f a. HasFormat fs f -> Formats fs a -> a
formatsLookup HasFormat fs f
format (Formats fs RepoPath -> RepoPath)
-> Formats fs RepoPath -> RepoPath
forall a b. (a -> b) -> a -> b
$ RepoLayout -> RemoteFile fs typ -> Formats fs RepoPath
forall fs typ.
RepoLayout -> RemoteFile fs typ -> Formats fs RepoPath
remoteRepoPath RepoLayout
repoLayout RemoteFile fs typ
file

{-------------------------------------------------------------------------------
  Utility
-------------------------------------------------------------------------------}

-- | Is a particular remote file cached?
data IsCached :: Type -> Type where
    -- This remote file should be cached, and we ask for it by name
    CacheAs :: CachedFile -> IsCached Metadata

    -- We don't cache this remote file
    --
    -- This doesn't mean a Repository should not feel free to cache the file
    -- if desired, but it does mean the generic algorithms will never ask for
    -- this file from the cache.
    DontCache :: IsCached Binary

    -- The index is somewhat special: it should be cached, but we never
    -- ask for it directly.
    --
    -- Instead, we will ask the Repository for files _from_ the index, which it
    -- can serve however it likes. For instance, some repositories might keep
    -- the index in uncompressed form, others in compressed form; some might
    -- keep an index tarball index for quick access, others may scan the tarball
    -- linearly, etc.
    CacheIndex :: IsCached Binary
--TODO: ^^ older haddock doesn't support GADT doc comments :-(

deriving instance Eq   (IsCached typ)
deriving instance Show (IsCached typ)

-- | Which remote files should we cache locally?
mustCache :: RemoteFile fs typ -> IsCached typ
mustCache :: forall fs typ. RemoteFile fs typ -> IsCached typ
mustCache RemoteFile fs typ
RemoteTimestamp      = CachedFile -> IsCached Metadata
CacheAs CachedFile
CachedTimestamp
mustCache (RemoteRoot Maybe (Trusted FileInfo)
_)       = CachedFile -> IsCached Metadata
CacheAs CachedFile
CachedRoot
mustCache (RemoteSnapshot Trusted FileInfo
_)   = CachedFile -> IsCached Metadata
CacheAs CachedFile
CachedSnapshot
mustCache (RemoteMirrors Trusted FileInfo
_)    = CachedFile -> IsCached Metadata
CacheAs CachedFile
CachedMirrors
mustCache (RemoteIndex {})     = IsCached typ
IsCached Binary
CacheIndex
mustCache (RemotePkgTarGz PackageIdentifier
_ Trusted FileInfo
_) = IsCached typ
IsCached Binary
DontCache

instance Pretty LogMessage where
  pretty :: LogMessage -> String
pretty LogMessage
LogRootUpdated =
      String
"Root info updated"
  pretty (LogVerificationError VerificationError
err) =
      String
"Verification error: " String -> ShowS
forall a. [a] -> [a] -> [a]
++ VerificationError -> String
forall a. Pretty a => a -> String
pretty VerificationError
err
  pretty (LogDownloading RemoteFile fs typ
file) =
      String
"Downloading " String -> ShowS
forall a. [a] -> [a] -> [a]
++ RemoteFile fs typ -> String
forall a. Pretty a => a -> String
pretty RemoteFile fs typ
file
  pretty (LogUpdating RemoteFile fs Binary
file) =
      String
"Updating " String -> ShowS
forall a. [a] -> [a] -> [a]
++ RemoteFile fs Binary -> String
forall a. Pretty a => a -> String
pretty RemoteFile fs Binary
file
  pretty (LogSelectedMirror String
mirror) =
      String
"Selected mirror " String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
mirror
  pretty (LogCannotUpdate RemoteFile fs Binary
file UpdateFailure
ex) =
      String
"Cannot update " String -> ShowS
forall a. [a] -> [a] -> [a]
++ RemoteFile fs Binary -> String
forall a. Pretty a => a -> String
pretty RemoteFile fs Binary
file String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
" (" String -> ShowS
forall a. [a] -> [a] -> [a]
++ UpdateFailure -> String
forall a. Pretty a => a -> String
pretty UpdateFailure
ex String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
")"
  pretty (LogMirrorFailed String
mirror SomeException
ex) =
      String
"Exception " String -> ShowS
forall a. [a] -> [a] -> [a]
++ SomeException -> String
forall e. Exception e => e -> String
displayException SomeException
ex String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
" when using mirror " String -> ShowS
forall a. [a] -> [a] -> [a]
++ String
mirror
  pretty (LogLockWait Path Absolute
file) =
      String
"Waiting to acquire cache lock on " String -> ShowS
forall a. [a] -> [a] -> [a]
++ Path Absolute -> String
forall a. Pretty a => a -> String
pretty Path Absolute
file
  pretty (LogLockWaitDone Path Absolute
file) =
      String
"Acquired cache lock on " String -> ShowS
forall a. [a] -> [a] -> [a]
++ Path Absolute -> String
forall a. Pretty a => a -> String
pretty Path Absolute
file
  pretty (LogUnlock Path Absolute
file) =
      String
"Released cache lock on " String -> ShowS
forall a. [a] -> [a] -> [a]
++ Path Absolute -> String
forall a. Pretty a => a -> String
pretty Path Absolute
file

instance Pretty UpdateFailure where
  pretty :: UpdateFailure -> String
pretty UpdateFailure
UpdateImpossibleUnsupported =
      String
"server does not provide incremental downloads"
  pretty UpdateFailure
UpdateImpossibleNoLocalCopy =
      String
"no local copy"
  pretty UpdateFailure
UpdateFailedTwice =
      String
"update failed twice"
  pretty (UpdateFailed SomeException
ex) =
      SomeException -> String
forall e. Exception e => e -> String
displayException SomeException
ex