packages feed

hackage-security 0.5.1.0 → 0.5.2.0

raw patch · 20 files changed

+417/−92 lines, 20 filesdep ~basePVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: base

API changes (from Hackage documentation)

- Hackage.Security.Util.Path: [unPath] :: Path a -> FilePath
+ Hackage.Security.Client: HashFnMD5 :: HashFn
+ Hackage.Security.Server: HashFnMD5 :: HashFn
+ Hackage.Security.Util.Path: splitFragments :: Path Unrooted -> [String]
+ Hackage.Security.Util.Path: takeExtension :: Path a -> String
- Hackage.Security.Client: anchorCachePath :: FsRoot root => Path root -> CachePath -> Path root
+ Hackage.Security.Client: anchorCachePath :: Path root -> CachePath -> Path root
- Hackage.Security.Client: anchorRepoPathLocally :: FsRoot root => Path root -> RepoPath -> Path root
+ Hackage.Security.Client: anchorRepoPathLocally :: Path root -> RepoPath -> Path root
- Hackage.Security.JSON: fromObjectKey :: FromObjectKey m a => String -> m a
+ Hackage.Security.JSON: fromObjectKey :: FromObjectKey m a => String -> m (Maybe a)
- Hackage.Security.Server: anchorCachePath :: FsRoot root => Path root -> CachePath -> Path root
+ Hackage.Security.Server: anchorCachePath :: Path root -> CachePath -> Path root
- Hackage.Security.Server: anchorRepoPathLocally :: FsRoot root => Path root -> RepoPath -> Path root
+ Hackage.Security.Server: anchorRepoPathLocally :: Path root -> RepoPath -> Path root

Files

hackage-security.cabal view
@@ -1,5 +1,5 @@ name:                hackage-security-version:             0.5.1.0+version:             0.5.2.0 synopsis:            Hackage security library description:         The hackage security library provides both server and                      client utilities for securing the Hackage package server@@ -18,13 +18,11 @@                      clients (the typical example being @cabal@), and                      "Hackage.Security.Server" is the main entry point for                      servers (the typical example being @hackage-server@).-                     .-                     This is a beta release. license:             BSD3 license-file:        LICENSE author:              Edsko de Vries maintainer:          edsko@well-typed.com-copyright:           Copyright 2015 Well-Typed LLP+copyright:           Copyright 2015-2016 Well-Typed LLP category:            Distribution homepage:            https://github.com/well-typed/hackage-security bug-reports:         https://github.com/well-typed/hackage-security/issues@@ -214,9 +212,11 @@ test-suite TestSuite   type:                exitcode-stdio-1.0   main-is:             TestSuite.hs-  other-modules:       TestSuite.InMemCache+  other-modules:       TestSuite.HttpMem+                       TestSuite.InMemCache                        TestSuite.InMemRepo                        TestSuite.InMemRepository+                       TestSuite.JSON                        TestSuite.PrivateKeys                        TestSuite.Util.StrictMVar   build-depends:       base,
src/Hackage/Security/Client.hs view
@@ -110,8 +110,7 @@     -- root information and start over. However, in order to prevent DoS attacks     -- we limit how often we go round this loop.     -- See als <https://github.com/theupdateframework/tuf/issues/287>.-    limitIterations :: (Throws VerificationError, Throws SomeRemoteError)-                    => VerificationHistory -> IO HasUpdates+    limitIterations :: VerificationHistory -> IO HasUpdates     limitIterations history | length history >= maxNumIterations =         throwChecked $ VerificationErrorLoop (reverse history)     limitIterations history = do@@ -802,7 +801,9 @@         }   where     indexPath :: Tar.Entry -> IndexPath-    indexPath = rootPath . fromUnrootedFilePath . Tar.entryPath+    indexPath = rootPath . fromUnrootedFilePath+              . Tar.fromTarPathToPosixPath+              . Tar.entryTarPath      indexFile :: IndexPath -> Maybe (Some IndexFile)     indexFile = indexFileFromPath repIndexLayout
src/Hackage/Security/Client/Repository.hs view
@@ -403,17 +403,17 @@  -- | Is a particular remote file cached? data IsCached :: * -> * where-    -- | This remote file should be cached, and we ask for it by name+    -- This remote file should be cached, and we ask for it by name     CacheAs :: CachedFile -> IsCached Metadata -    -- | We don't cache this remote file+    -- 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+    -- 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@@ -422,6 +422,7 @@     -- 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)
src/Hackage/Security/Client/Repository/Cache.hs view
@@ -62,7 +62,7 @@     -- update the uncompressed index incrementally (assuming the local files     -- have not been corrupted).     -- NOTE: This assumes we already updated the compressed file.-    unzipIndex :: typ ~ Binary => IO ()+    unzipIndex :: IO ()     unzipIndex = do         createDirectoryIfMissing True (takeDirectory indexUn)         shouldTryIncremenal <- cachedIndexProbablyValid
src/Hackage/Security/Client/Repository/Remote.hs view
@@ -299,24 +299,25 @@  -- | Download method (downloading or updating) data DownloadMethod :: * -> * -> * where-    -- | Download this file (we never attempt to update this type of file)+    -- Download this file (we never attempt to update this type of file)     NeverUpdated :: {         neverUpdatedFormat :: HasFormat fs f       } -> DownloadMethod fs typ -    -- | Download this file (we cannot update this file right now)+    -- Download this file (we cannot update this file right now)     CannotUpdate :: {         cannotUpdateFormat :: HasFormat fs f       , cannotUpdateReason :: UpdateFailure       } -> DownloadMethod fs Binary -    -- | Attempt an (incremental) update of this file+    -- Attempt an (incremental) update of this file     Update :: {         updateFormat :: HasFormat fs f       , updateInfo   :: Trusted FileInfo       , updateLocal  :: Path Absolute       , updateTail   :: Int54       } -> DownloadMethod fs Binary+--TODO: ^^ older haddock doesn't support GADT doc comments :-(  pickDownloadMethod :: forall fs typ. RemoteConfig                    -> AttemptNr@@ -362,8 +363,7 @@ getFile cfg@RemoteConfig{..} attemptNr remoteFile method =     go method   where-    go :: Throws SomeRemoteError-       => DownloadMethod fs typ -> Verify (Some (HasFormat fs), RemoteTemp typ)+    go :: DownloadMethod fs typ -> Verify (Some (HasFormat fs), RemoteTemp typ)     go NeverUpdated{..} = do         cfgLogger $ LogDownloading remoteFile         download neverUpdatedFormat@@ -379,8 +379,7 @@     headers = httpRequestHeaders cfg attemptNr      -- Get any file from the server, without using incremental updates-    download :: Throws SomeRemoteError => HasFormat fs f-             -> Verify (Some (HasFormat fs), RemoteTemp typ)+    download :: HasFormat fs f -> Verify (Some (HasFormat fs), RemoteTemp typ)     download format = do         (tempPath, h) <- openTempFile (Cache.cacheRoot cfgCache) (uriTemplate uri)         liftIO $ do@@ -598,15 +597,16 @@         wholeTemp :: Path Absolute       } -> RemoteTemp a -    -- | If we download only the delta, we record both the path to where the+    -- If we download only the delta, we record both the path to where the     -- "old" file is stored and the path to the temp file containing the delta.     -- Then:     ---    -- * When we verify the file, we need both of these paths if we compute+    --   When we verify the file, we need both of these paths if we compute     --   the hash from scratch, or only the path to the delta if we attempt     --   to compute the hash incrementally (TODO: incremental verification     --   not currently implemented).-    -- * When we copy a file over, we are additionally given a destination+    --+    --   When we copy a file over, we are additionally given a destination     --   path. In this case, we expect that destination path to be equal to     --   the path to the old file (and assert this to be the case).     DownloadedDelta :: {@@ -614,6 +614,8 @@       , deltaExisting :: Path Absolute       , deltaSeek     :: Int54       -- ^ How much of the existing file to keep       } -> RemoteTemp Binary+--TODO: ^^ older haddock doesn't support GADT doc comments :-(+--      and add the '*' bullet points back in  instance Pretty (RemoteTemp typ) where     pretty DownloadedWhole{..} = intercalate " " $ [
src/Hackage/Security/Key.hs view
@@ -153,7 +153,7 @@   toObjectKey = return . keyIdString  instance Monad m => FromObjectKey m KeyId where-  fromObjectKey = return . KeyId+  fromObjectKey = return . Just . KeyId  -- | Compute the key ID of a key class HasKeyId key where
src/Hackage/Security/TUF/FileInfo.hs view
@@ -29,6 +29,7 @@ -------------------------------------------------------------------------------}  data HashFn = HashFnSHA256+            | HashFnMD5   deriving (Show, Eq, Ord)  -- | File information@@ -87,10 +88,12 @@  instance Monad m => ToObjectKey m HashFn where   toObjectKey HashFnSHA256 = return "sha256"+  toObjectKey HashFnMD5    = return "md5"  instance ReportSchemaErrors m => FromObjectKey m HashFn where-  fromObjectKey "sha256" = return HashFnSHA256-  fromObjectKey str      = expected "valid hash function" (Just str)+  fromObjectKey "sha256" = return (Just HashFnSHA256)+  fromObjectKey "md5"    = return (Just HashFnMD5)+  fromObjectKey _        = return Nothing  instance Monad m => ToJSON m FileInfo where   toJSON FileInfo{..} = mkObject [
src/Hackage/Security/TUF/FileMap.hs view
@@ -127,8 +127,7 @@  instance ReportSchemaErrors m => FromObjectKey m TargetPath where   fromObjectKey ('<':'r':'e':'p':'o':'>':'/':path) =-    return . TargetPathRepo  . rootPath . fromUnrootedFilePath $ path+    return . Just . TargetPathRepo  . rootPath . fromUnrootedFilePath $ path   fromObjectKey ('<':'i':'n':'d':'e':'x':'>':'/':path) =-    return . TargetPathIndex . rootPath . fromUnrootedFilePath $ path-  fromObjectKey str =-    expected "target path" (Just str)+    return . Just . TargetPathIndex . rootPath . fromUnrootedFilePath $ path+  fromObjectKey _str = return Nothing
src/Hackage/Security/TUF/Layout/Index.hs view
@@ -9,8 +9,6 @@   , indexLayoutPkgPrefs   ) where -import qualified System.FilePath as FP- import Distribution.Package import Distribution.Text @@ -45,14 +43,15 @@ -- the global preferred-versions file. But supporting legacy Hackage will -- probably require more work anyway.. data IndexFile :: * -> * where-    -- | Package-specific metadata (@targets.json@)+    -- Package-specific metadata (@targets.json@)     IndexPkgMetadata :: PackageIdentifier -> IndexFile (Signed Targets) -    -- | Cabal file for a package+    -- Cabal file for a package     IndexPkgCabal :: PackageIdentifier -> IndexFile () -    -- | Preferred versions a package+    -- Preferred versions a package     IndexPkgPrefs :: PackageName -> IndexFile ()+--TODO: ^^ older haddock doesn't support GADT doc comments :-(  deriving instance Show (IndexFile dec) @@ -68,7 +67,7 @@ hackageIndexLayout :: IndexLayout hackageIndexLayout = IndexLayout {       indexFileToPath   = toPath-    , indexFileFromPath = fromPath . toUnrootedFilePath . unrootPath+    , indexFileFromPath = fromPath     }   where     toPath :: IndexFile dec -> IndexPath@@ -90,16 +89,16 @@     fromFragments :: [String] -> IndexPath     fromFragments = rootPath . joinFragments -    fromPath :: FilePath -> Maybe (Some IndexFile)-    fromPath fp = case FP.splitPath fp of-      [pkg, version, file] -> do-        pkgId <- simpleParse (init pkg ++ "-" ++ init version)-        case FP.takeExtension file of+    fromPath :: IndexPath -> Maybe (Some IndexFile)+    fromPath fp = case splitFragments (unrootPath fp) of+      [pkg, version, _file] -> do+        pkgId <- simpleParse (pkg ++ "-" ++ version)+        case takeExtension fp of           ".cabal"   -> return $ Some $ IndexPkgCabal    pkgId           ".json"    -> return $ Some $ IndexPkgMetadata pkgId           _otherwise -> Nothing       [pkg, "preferred-versions"] ->-        Some . IndexPkgPrefs <$> simpleParse (init pkg)+        Some . IndexPkgPrefs <$> simpleParse pkg       _otherwise -> Nothing  {-------------------------------------------------------------------------------
src/Hackage/Security/TUF/Paths.hs view
@@ -37,7 +37,7 @@ instance Pretty (Path RepoRoot) where   pretty (Path fp) = "<repo>/" ++ fp -anchorRepoPathLocally :: FsRoot root => Path root -> RepoPath -> Path root+anchorRepoPathLocally :: Path root -> RepoPath -> Path root anchorRepoPathLocally localRoot repoPath = localRoot </> unrootPath repoPath  anchorRepoPathRemotely :: Path Web -> RepoPath -> Path Web@@ -68,5 +68,5 @@     pretty (Path fp) = "<cache>/" ++ fp  -- | Anchor a cache path to the location of the cache-anchorCachePath :: FsRoot root => Path root -> CachePath -> Path root+anchorCachePath :: Path root -> CachePath -> Path root anchorCachePath cacheRoot cachePath = cacheRoot </> unrootPath cachePath
src/Hackage/Security/TUF/Patterns.hs view
@@ -27,7 +27,7 @@  import Control.Monad.Except import Language.Haskell.TH (Q, Exp)-import System.FilePath+import System.FilePath.Posix import qualified Language.Haskell.TH.Syntax as TH  import Hackage.Security.JSON
src/Hackage/Security/Util/Checked.hs view
@@ -1,5 +1,8 @@ {-# LANGUAGE CPP #-} {-# OPTIONS_GHC -fno-warn-unused-binds #-}+#if __GLASGOW_HASKELL__ >= 800+{-# OPTIONS_GHC -wredundant-constraints #-}+#endif  #if __GLASGOW_HASKELL__ >= 708 {-# LANGUAGE RoleAnnotations     #-}
src/Hackage/Security/Util/JSON.hs view
@@ -24,6 +24,7 @@   ) where  import Control.Monad (liftM)+import Data.Maybe (catMaybes) import Data.Map (Map) import Data.Time import Text.JSON.Canonical@@ -54,7 +55,7 @@  -- | Used in the 'FromJSON' instance for 'Map' class FromObjectKey m a where-  fromObjectKey :: String -> m a+  fromObjectKey :: String -> m (Maybe a)  -- | Monads in which we can report schema errors class (Applicative m, Monad m) => ReportSchemaErrors m where@@ -85,13 +86,13 @@   toObjectKey = return  instance Monad m => FromObjectKey m String where-  fromObjectKey = return+  fromObjectKey = return . Just  instance Monad m => ToObjectKey m (Path root) where   toObjectKey (Path fp) = return fp  instance Monad m => FromObjectKey m (Path root) where-  fromObjectKey = liftM Path . fromObjectKey+  fromObjectKey = liftM (fmap Path) . fromObjectKey  {-------------------------------------------------------------------------------   ToJSON and FromJSON instances@@ -162,10 +163,13 @@          ) => FromJSON m (Map k a) where   fromJSON enc = do       obj <- fromJSObject enc-      Map.fromList <$> mapM aux obj+      Map.fromList . catMaybes <$> mapM aux obj     where-      aux :: (String, JSValue) -> m (k, a)-      aux (k, a) = (,) <$> fromObjectKey k <*> fromJSON a+      aux :: (String, JSValue) -> m (Maybe (k, a))+      aux (k, a) = knownKeys <$> fromObjectKey k <*> fromJSON a+      knownKeys :: Maybe k -> a -> Maybe (k, a)+      knownKeys Nothing  _ = Nothing+      knownKeys (Just k) a = Just (k, a)  instance Monad m => ToJSON m URI where   toJSON = toJSON . show
src/Hackage/Security/Util/Path.hs view
@@ -17,6 +17,7 @@   , takeFileName   , (<.>)   , splitExtension+  , takeExtension     -- * Unrooted paths   , Unrooted   , (</>)@@ -26,6 +27,7 @@   , fromUnrootedFilePath   , fragment   , joinFragments+  , splitFragments   , isPathPrefixOf     -- * File-system paths   , Relative@@ -92,7 +94,8 @@ #endif import qualified Data.ByteString         as BS import qualified Data.ByteString.Lazy    as BS.L-import qualified System.FilePath         as FP+import qualified System.FilePath         as FP.Native+import qualified System.FilePath.Posix   as FP.Posix import qualified System.IO               as IO import qualified System.Directory        as Dir import qualified Codec.Archive.Tar       as Tar@@ -115,9 +118,21 @@ -- make sense to append two absolute paths together; instead, we can only append -- an unrooted path to another path. It also means we avoid bugs where we use -- one kind of path where we expect another.-newtype Path a = Path { unPath :: FilePath }+newtype Path a = Path FilePath -- always a Posix style path internally   deriving (Show, Eq, Ord) +mkPathNative :: FilePath -> Path a+mkPathNative = Path . FP.Posix.joinPath . FP.Native.splitDirectories++unPathNative :: Path a -> FilePath+unPathNative (Path fp) = FP.Native.joinPath . FP.Posix.splitDirectories $ fp++mkPathPosix :: FilePath -> Path a+mkPathPosix = Path++unPathPosix :: Path a -> FilePath+unPathPosix (Path fp) = fp+ -- | Reinterpret the root of a path -- -- This literally just changes the type-level tag; use with caution!@@ -129,19 +144,22 @@ -------------------------------------------------------------------------------}  takeDirectory :: Path a -> Path a-takeDirectory = liftFP FP.takeDirectory+takeDirectory = liftFP FP.Posix.takeDirectory  takeFileName :: Path a -> String-takeFileName = liftFromFP FP.takeFileName+takeFileName = liftFromFP FP.Posix.takeFileName  (<.>) :: Path a -> String -> Path a-fp <.> ext = liftFP (FP.<.> ext) fp+fp <.> ext = liftFP (FP.Posix.<.> ext) fp  splitExtension :: Path a -> (Path a, String) splitExtension (Path fp) = (Path fp', ext)   where-    (fp', ext) = FP.splitExtension fp+    (fp', ext) = FP.Posix.splitExtension fp +takeExtension :: Path a -> String+takeExtension (Path fp) = FP.Posix.takeExtension fp+ {-------------------------------------------------------------------------------   Unrooted paths -------------------------------------------------------------------------------}@@ -155,7 +173,7 @@   pretty (Path fp) = fp  (</>) :: Path a -> Path Unrooted -> Path a-(</>) = liftFP2 (FP.</>)+(</>) = liftFP2 (FP.Posix.</>)  -- | Reinterpret an unrooted path --@@ -169,19 +187,30 @@ unrootPath :: Path root -> Path Unrooted unrootPath (Path fp) = Path fp +-- | Convert a relative\/unrooted Path to a FilePath (using POSIX style+-- directory separators).+--+-- See also 'toAbsoluteFilePath'+-- toUnrootedFilePath :: Path Unrooted -> FilePath-toUnrootedFilePath = unPath+toUnrootedFilePath = unPathPosix +-- | Convert from a relative\/unrooted FilePath (using POSIX style directory+-- separators).+-- fromUnrootedFilePath :: FilePath -> Path Unrooted-fromUnrootedFilePath = Path+fromUnrootedFilePath = mkPathPosix  -- | A path fragment (like a single directory or filename) fragment :: String -> Path Unrooted-fragment = fromUnrootedFilePath+fragment = Path  joinFragments :: [String] -> Path Unrooted-joinFragments = liftToFP FP.joinPath+joinFragments = liftToFP FP.Posix.joinPath +splitFragments :: Path Unrooted -> [String]+splitFragments (Path fp) = FP.Posix.splitDirectories fp+ isPathPrefixOf :: Path Unrooted -> Path Unrooted -> Bool isPathPrefixOf = liftFromFP2 isPrefixOf @@ -204,30 +233,34 @@  -- | A file system root can be interpreted as an (absolute) FilePath class FsRoot root where+  -- | Convert a Path to an absolute FilePath (using native style directory separators).+  --   toAbsoluteFilePath :: Path root -> IO FilePath  instance FsRoot Relative where-    toAbsoluteFilePath (Path fp) = go fp+    toAbsoluteFilePath p = go (unPathNative p)       where         go :: FilePath -> IO FilePath #if MIN_VERSION_directory(1,2,2)         go = Dir.makeAbsolute #else         -- copied implementation from the directory package-        go = (FP.normalise <$>) . absolutize+        go = (FP.Native.normalise <$>) . absolutize         absolutize path -- avoid the call to `getCurrentDirectory` if we can-          | FP.isRelative path = (FP.</> path) . FP.addTrailingPathSeparator <$>-                                 Dir.getCurrentDirectory-          | otherwise          = return path+          | FP.Native.isRelative path+                      = (FP.Native.</> path)+                      . FP.Native.addTrailingPathSeparator <$>+                        Dir.getCurrentDirectory+          | otherwise = return path #endif  instance FsRoot Absolute where-    toAbsoluteFilePath (Path fp) = return fp+    toAbsoluteFilePath = return . unPathNative  instance FsRoot HomeDir where-    toAbsoluteFilePath (Path fp) = do+    toAbsoluteFilePath p = do       home <- Dir.getHomeDirectory-      return $ home FP.</> fp+      return $ home FP.Native.</> unPathNative p  -- | Abstract over a file system root --@@ -239,29 +272,29 @@ -------------------------------------------------------------------------------}  toFilePath :: Path Absolute -> FilePath-toFilePath (Path fp) = fp+toFilePath = unPathNative  fromFilePath :: FilePath -> FsPath fromFilePath fp-    | FP.isAbsolute      fp = FsPath (Path fp  :: Path Absolute)-    | Just fp' <- atHome fp = FsPath (Path fp' :: Path HomeDir)-    | otherwise             = FsPath (Path fp  :: Path Relative)+    | FP.Native.isAbsolute fp = FsPath (mkPathNative fp  :: Path Absolute)+    | Just fp' <- atHome fp   = FsPath (mkPathNative fp' :: Path HomeDir)+    | otherwise               = FsPath (mkPathNative fp  :: Path Relative)   where     -- TODO: I don't know if there a standard way that Windows users refer to     -- their home directory. For now, we'll only interpret '~'. Everybody else     -- can specify an absolute path if this doesn't work.     atHome :: FilePath -> Maybe FilePath     atHome "~" = Just ""-    atHome ('~':sep:fp') | FP.isPathSeparator sep = Just fp'+    atHome ('~':sep:fp') | FP.Native.isPathSeparator sep = Just fp'     atHome _otherwise = Nothing  makeAbsolute :: FsPath -> IO (Path Absolute)-makeAbsolute (FsPath p) = Path <$> toAbsoluteFilePath p+makeAbsolute (FsPath p) = mkPathNative <$> toAbsoluteFilePath p  fromAbsoluteFilePath :: FilePath -> Path Absolute fromAbsoluteFilePath fp-  | FP.isAbsolute fp = Path fp-  | otherwise        = error "fromAbsoluteFilePath: not an absolute path"+  | FP.Native.isAbsolute fp = mkPathNative fp+  | otherwise               = error "fromAbsoluteFilePath: not an absolute path"  {-------------------------------------------------------------------------------   Wrappers around System.IO@@ -363,7 +396,7 @@     fragments <$> Dir.getDirectoryContents filePath   where     fragments :: [String] -> [Path Unrooted]-    fragments = map fromUnrootedFilePath . filter (not . skip)+    fragments = map fragment . filter (not . skip)      skip :: String -> Bool     skip "."  = True@@ -389,7 +422,7 @@                        else return [path]      emptyPath :: Path Unrooted-    emptyPath = Path (FP.joinPath [])+    emptyPath = joinFragments []  renameFile :: (FsRoot root, FsRoot root')            => Path root  -- ^ Old@@ -431,7 +464,7 @@     Tar.append tarFile' baseDir' contents'   where     contents' :: [FilePath]-    contents' = map (toUnrootedFilePath . unrootPath) contents+    contents' = map (unPathNative . unrootPath) contents  {-------------------------------------------------------------------------------   Wrappers around Network.URI
tests/TestSuite.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE RecordWildCards, GADTs #-} module Main (main) where  -- stdlib@@ -8,15 +9,21 @@ import Network.URI (URI, parseURI) import Test.Tasty import Test.Tasty.HUnit-import Test.Tasty.QuickCheck+import Test.Tasty.QuickCheck hiding (label) import System.IO.Temp (withSystemTempDirectory)+import qualified Codec.Archive.Tar.Entry    as Tar+import qualified Data.ByteString.Lazy.Char8 as BS +-- Cabal+import Distribution.Package (PackageName(..))+ -- hackage-security import Hackage.Security.Client import Hackage.Security.Client.Repository import Hackage.Security.JSON (DeserializationError(..)) import Hackage.Security.Util.Checked import Hackage.Security.Util.Path+import Hackage.Security.Util.Some import Hackage.Security.Util.Pretty import qualified Hackage.Security.Client.Repository.Remote as Remote import qualified Hackage.Security.Client.Repository.Cache  as Cache@@ -45,6 +52,7 @@         , testCase "testInMemUpdatesAfterCron"     testInMemUpdatesAfterCron         , testCase "testInMemKeyRollover"          testInMemKeyRollover         , testCase "testInMemOutdatedTimestamp"    testInMemOutdatedTimestamp+        , testCase "testInMemIndex"                testInMemIndex         ]     , testGroup "HttpMem" [           testCase "testHttpMemInitialHasForUpdates" testHttpMemInitialHasUpdates@@ -52,6 +60,7 @@         , testCase "testHttpMemUpdatesAfterCron"     testHttpMemUpdatesAfterCron         , testCase "testHttpMemKeyRollover"          testHttpMemKeyRollover         , testCase "testHttpMemOutdatedTimestamp"    testHttpMemOutdatedTimestamp+        , testCase "testHttpMemIndex"                testHttpMemIndex         ]     , testGroup "Canonical JSON" [           testProperty "prop_roundtrip_canonical" JSON.prop_roundtrip_canonical@@ -131,6 +140,10 @@       withAssertLog "C" logMsgs [] $ do         assertEqual "C.1" HasUpdates =<< checkForUpdates repo fourDaysLater +testInMemIndex :: Assertion+testInMemIndex = inMemTest $ \inMemRepo _logMsgs repo ->+    testRepoIndex inMemRepo repo+ {-------------------------------------------------------------------------------   Same tests, but going through the "real" Remote repository and Cache, though   still using an in-memory repository (with a HttpLib bridge)@@ -201,6 +214,57 @@     catchVerificationLoop msgs $ do       withAssertLog "C" logMsgs [] $ do         assertEqual "C.1" HasUpdates =<< checkForUpdates repo fourDaysLater++testHttpMemIndex :: Assertion+testHttpMemIndex = httpMemTest $ \inMemRepo _logMsgs repo ->+    testRepoIndex inMemRepo repo++{-------------------------------------------------------------------------------+  Identical tests between the two variants+-------------------------------------------------------------------------------}++testRepoIndex :: (Throws SomeRemoteError, Throws VerificationError)+              => InMemRepo -> Repository down -> IO ()+testRepoIndex inMemRepo repo = do+    assertEqual "A" HasUpdates =<< checkForUpdates repo =<< checkExpiry+    dir1 <- getDirectory repo+    directoryFirst dir1  @?= DirectoryEntry 0+    directoryNext  dir1  @?= DirectoryEntry 0+    length (directoryEntries dir1) @?= 0++    now <- getCurrentTime+    inMemRepoSetIndex inMemRepo now [testEntry1]++    assertEqual "B" HasUpdates =<< checkForUpdates repo =<< checkExpiry+    dir2 <- getDirectory repo+    directoryFirst dir2  @?= DirectoryEntry 0+    directoryNext  dir2  @?= DirectoryEntry 2+    length (directoryEntries dir2) @?= 1+    directoryLookup dir2 testEntryIndexFile @?= Just (DirectoryEntry 0)+    withIndex repo $ \IndexCallbacks{..} -> do+      (sentry, next) <- indexLookupEntry (DirectoryEntry 0)+      next @?= Nothing+      case sentry of Some entry -> checkIndexEntry entry+  where+    checkIndexEntry :: IndexEntry dec -> Assertion+    checkIndexEntry entry = do+       toUnrootedFilePath (unrootPath (indexEntryPath entry))+         @?= "foo/preferred-versions"+       indexEntryContent entry @?= testEntrycontent+       case indexEntryPathParsed entry of+         Just (IndexPkgPrefs pkgname) -> do+           pkgname @?= PackageName "foo"+           case indexEntryContentParsed entry of+             Right () -> return ()+             _        -> fail "unexpected index entry content"+         _ -> fail "unexpected index path"++    testEntry1 = Tar.fileEntry path testEntrycontent+      where+        Right path = Tar.toTarPath False "foo/preferred-versions"+    testEntrycontent   = BS.pack "foo >= 1"+    testEntryIndexFile = IndexPkgPrefs (PackageName "foo")+  {-------------------------------------------------------------------------------   Log messages we expect when using the Remote repository
+ tests/TestSuite/HttpMem.hs view
@@ -0,0 +1,70 @@+-- | HttpLib bridge to the in-memory repository+module TestSuite.HttpMem (+    httpMem+  ) where++-- stdlib+import Network.URI (URI)+import qualified Data.ByteString.Lazy as BS.L++-- hackage-security+import Hackage.Security.Client+import Hackage.Security.Client.Repository.HttpLib+import Hackage.Security.Util.Checked+import Hackage.Security.Util.Path+import Hackage.Security.Util.Some++-- TestSuite+import TestSuite.InMemRepo++httpMem :: InMemRepo -> HttpLib+httpMem inMemRepo = HttpLib {+      httpGet      = get      inMemRepo+    , httpGetRange = getRange inMemRepo+    }++{-------------------------------------------------------------------------------+  Individual methods+-------------------------------------------------------------------------------}++-- | Download a file+--+-- Since we don't (yet?) make any attempt to simulate a cache, we ignore+-- caching headers.+get :: forall a. Throws SomeRemoteError+    => InMemRepo+    -> [HttpRequestHeader]+    -> URI+    -> ([HttpResponseHeader] -> BodyReader -> IO a)+    -> IO a+get InMemRepo{..} _requestHeaders uri callback = do+    Some inMemFile <- inMemRepoGetPath $ castRoot (uriPath uri)+    br <- bodyReaderFromBS $ inMemFileRender inMemFile+    callback [HttpResponseAcceptRangesBytes] br++-- | Download a byte range+--+-- Range is starting and (exclusive) end offset in bytes.+--+-- We ignore requests for compression; different servers deal with compression+-- for byte range requests differently; in particular, Apache returns the range+-- of the _compressed_ file, which is pretty useless for our purposes. For now+-- we ignore this issue completely here.+getRange :: forall a. Throws SomeRemoteError+         => InMemRepo+         -> [HttpRequestHeader]+         -> URI+         -> (Int, Int)+         -> (HttpStatus -> [HttpResponseHeader] -> BodyReader -> IO a)+         -> IO a+getRange InMemRepo{..} _requestHeaders uri (fr, to) callback = do+    Some inMemFile <- inMemRepoGetPath $ castRoot (uriPath uri)+    br <- bodyReaderFromBS $ substr (inMemFileRender inMemFile)++    let responseHeaders = concat [+            [ HttpResponseAcceptRangesBytes ]+          ]+    callback HttpStatus206PartialContent responseHeaders br+  where+    substr :: BS.L.ByteString -> BS.L.ByteString+    substr = BS.L.take (fromIntegral (to - fr)) . BS.L.drop (fromIntegral fr)
tests/TestSuite/InMemCache.hs view
@@ -8,8 +8,13 @@ import qualified Codec.Compression.GZip as GZip import qualified Data.ByteString.Lazy   as BS.L +-- tar+import qualified Codec.Archive.Tar       as Tar+import qualified Codec.Archive.Tar.Index as TarIndex+import           Codec.Archive.Tar.Index   (TarIndex)+ -- hackage-security-import Hackage.Security.Client+import Hackage.Security.Client hiding (withIndex) import Hackage.Security.Client.Formats import Hackage.Security.Client.Repository import Hackage.Security.JSON@@ -20,20 +25,25 @@ import TestSuite.InMemRepo  data InMemCache = InMemCache {-      inMemCacheGet     :: CachedFile -> IO (Maybe (Path Absolute))-    , inMemCacheGetRoot :: IO (Path Absolute)-    , inMemCacheClear   :: IO ()-    , inMemCachePut     :: forall f typ. InMemFile typ -> Format f -> IsCached typ -> IO ()+      inMemCacheGet         :: CachedFile -> IO (Maybe (Path Absolute))+    , inMemCacheGetRoot     :: IO (Path Absolute)+    , inMemCacheWithIndex   :: forall a. (Handle -> IO a) -> IO a+    , inMemCacheGetIndexIdx :: IO TarIndex+    , inMemCacheClear       :: IO ()+    , inMemCachePut         :: forall f typ. InMemFile typ -> Format f+                                          -> IsCached  typ -> IO ()     }  newInMemCache :: Path Absolute -> RepoLayout -> IO InMemCache newInMemCache tempDir layout = do     state <- newMVar $ initLocalState layout     return InMemCache {-        inMemCacheGet     = get     state tempDir-      , inMemCacheGetRoot = getRoot state tempDir-      , inMemCacheClear   = clear   state-      , inMemCachePut     = put     state+        inMemCacheGet         = get         state tempDir+      , inMemCacheGetRoot     = getRoot     state tempDir+      , inMemCacheWithIndex   = withIndex   state tempDir+      , inMemCacheGetIndexIdx = getIndexIdx state+      , inMemCacheClear       = clear       state+      , inMemCachePut         = put         state       }  {-------------------------------------------------------------------------------@@ -105,6 +115,26 @@ getRoot :: MVar LocalState -> Path Absolute -> IO (Path Absolute) getRoot state cacheTempDir =     needRoot `fmap` get state cacheTempDir CachedRoot++withIndex :: MVar LocalState -> Path Absolute -> (Handle -> IO a) -> IO a+withIndex state cacheTempDir action = do+    st <- readMVar state+    case cachedIndex st of+      Nothing -> error "InMemCache.withIndex: Could not read index."+      Just bs -> do+        (_, h) <- openTempFile' cacheTempDir "01-index.tar"+        BS.L.hPut h bs+        hSeek  h AbsoluteSeek 0+        x <- action h+        hClose h+        return x++getIndexIdx :: MVar LocalState -> IO TarIndex+getIndexIdx state = do+    st <- readMVar state+    case cachedIndex st of+      Nothing    -> error "InMemCache.getIndexIdx: Could not read index."+      Just index -> either throwIO return . TarIndex.build . Tar.read $ index  -- | Clear all cached data clear :: MVar LocalState -> IO ()
tests/TestSuite/InMemRepo.hs view
@@ -72,6 +72,9 @@      -- | Rollover the timestamp and snapshot keys   , inMemRepoKeyRollover :: UTCTime -> IO ()++    -- | Set the content of the repo tar index and resign+  , inMemRepoSetIndex :: UTCTime -> [Tar.Entry] -> IO ()   }  newInMemRepo :: RepoLayout@@ -86,6 +89,7 @@       , inMemRepoGetPath     = getPath     state       , inMemRepoCron        = cron        state       , inMemRepoKeyRollover = keyRollover state+      , inMemRepoSetIndex    = setIndex    state       }  {-------------------------------------------------------------------------------@@ -213,6 +217,41 @@     return st {         remoteTimestamp = signedTimestamp       , remoteSnapshot  = signedSnapshot+      }++setIndex :: MVar RemoteState -> UTCTime -> [Tar.Entry] -> IO ()+setIndex state now entries = modifyMVar_ state $ \st@RemoteState{..} -> do+    let snapshot, snapshot' :: Snapshot+        snapshot  = signed remoteSnapshot+        snapshot' = snapshot {+            snapshotVersion   = versionIncrement $ snapshotVersion snapshot+          , snapshotExpires   = expiresInDays now 3+          , snapshotInfoTarGz = fileInfo $ newTarGz+          , snapshotInfoTar   = Just $ fileInfo newTar+          }++        newTar :: BS.L.ByteString+        newTar = Tar.write entries++        newTarGz :: BS.L.ByteString+        newTarGz = GZip.compress newTar++        timestamp, timestamp' :: Timestamp+        timestamp  = signed remoteTimestamp+        timestamp' = Timestamp {+            timestampVersion      = versionIncrement $ timestampVersion timestamp+          , timestampExpires      = expiresInDays now 3+          , timestampInfoSnapshot = fileInfo $ renderJSON remoteLayout signedSnapshot+          }++        signedTimestamp = withSignatures remoteLayout [privateTimestamp remoteKeys] timestamp'+        signedSnapshot  = withSignatures remoteLayout [privateSnapshot  remoteKeys] snapshot'++    return st {+        remoteTimestamp = signedTimestamp+      , remoteSnapshot  = signedSnapshot+      , remoteTar       = newTar+      , remoteTarGz     = newTarGz       }  keyRollover :: MVar RemoteState -> UTCTime -> IO ()
tests/TestSuite/InMemRepository.hs view
@@ -31,8 +31,8 @@       , repGetCachedRoot = inMemCacheGetRoot  cache       , repClearCache    = inMemCacheClear    cache       , repLockCache     = withMVar cacheLock . const-      , repWithIndex     = error "newInMemRepository: repWithIndex TODO"-      , repGetIndexIdx   = error "newInMemRepository: repGetIndexIdx TODO"+      , repWithIndex     = inMemCacheWithIndex   cache+      , repGetIndexIdx   = inMemCacheGetIndexIdx cache       , repWithMirror    = withMirror       , repLog           = logger       , repLayout        = layout
+ tests/TestSuite/JSON.hs view
@@ -0,0 +1,77 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+module TestSuite.JSON (+    prop_roundtrip_canonical,+    prop_roundtrip_pretty,+    prop_canonical_pretty,+  ) where++-- stdlib+import Data.Int+import Data.List (sortBy, nubBy)+import Data.Function (on)+import Control.Applicative+import qualified Data.ByteString.Lazy.Char8 as BS+import Test.QuickCheck++-- hackage-security+import Text.JSON.Canonical+++prop_roundtrip_canonical, prop_roundtrip_pretty, prop_canonical_pretty+  :: JSValue -> Bool++prop_roundtrip_canonical jsval =+    parseCanonicalJSON (renderCanonicalJSON jsval) == Right (canonicalise jsval)++prop_roundtrip_pretty jsval =+    parseCanonicalJSON (BS.pack (prettyCanonicalJSON jsval)) == Right jsval++prop_canonical_pretty jsval =+    parseCanonicalJSON (renderCanonicalJSON jsval) ==+    fmap canonicalise (parseCanonicalJSON (BS.pack (prettyCanonicalJSON jsval)))++canonicalise :: JSValue -> JSValue+canonicalise v@JSNull        = v+canonicalise v@(JSBool    _) = v+canonicalise v@(JSNum     _) = v+canonicalise v@(JSString  _) = v+canonicalise   (JSArray  vs) = JSArray  [ canonicalise v | v <- vs]+canonicalise   (JSObject vs) = JSObject [ (k, canonicalise v)+                                        | (k,v) <- sortBy (compare `on` fst) vs ]++instance Arbitrary JSValue where+  arbitrary =+    sized $ \sz ->+    frequency+      [ (1, pure JSNull)+      , (1, JSBool   <$> arbitrary)+      , (2, JSNum    <$> arbitrary)+      , (2, JSString <$> arbitrary)+      , (3, JSArray                <$> resize (sz `div` 2) arbitrary)+      , (3, JSObject . noDupFields <$> resize (sz `div` 2) arbitrary)+      ]+    where+      noDupFields = nubBy (\(x,_) (y,_) -> x==y)++  shrink JSNull        = []+  shrink (JSBool    _) = []+  shrink (JSNum     n) = [ JSNum    n' | n' <- shrink n ]+  shrink (JSString  s) = [ JSString s' | s' <- shrink s ]+  shrink (JSArray  vs) = [ JSArray vs' | vs' <- shrink vs ]+  shrink (JSObject vs) = [ JSObject vs' | vs' <- shrinkList shrinkSnd vs ]+    where+      shrinkSnd (a,b) = [ (a,b') | b' <- shrink b ]+++instance Arbitrary Int54 where+  arbitrary = fromIntegral <$>+              frequency [ (1, pure lowerbound)+                        , (1, pure upperbound)+                        , (8, choose (lowerbound, upperbound))+                        ]+    where+      upperbound, lowerbound :: Int64+      upperbound =   999999999999999  -- 15 decimal digits+      lowerbound = (-999999999999999)+  shrink = shrinkIntegral+