packages feed

hackage-security 0.5.3.0 → 0.6.0.0

raw patch · 14 files changed

+269/−282 lines, 14 filesdep +aesondep +lukkodep +mtl-compatdep ~Cabaldep ~QuickCheckdep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: aeson, lukko, mtl-compat, unordered-containers, vector

Dependency ranges changed: Cabal, QuickCheck, base, mtl, tasty, template-haskell, temporary, transformers

API changes (from Hackage documentation)

- Hackage.Security.TUF.FileMap: lookupM :: Monad m => FileMap -> TargetPath -> m FileInfo
- Hackage.Security.Util.IO: getFileSize :: (Num a, FsRoot root) => Path root -> IO a
- Hackage.Security.Util.IO: handleDoesNotExist :: IO a -> IO (Maybe a)
- Hackage.Security.Util.IO: timedIO :: String -> IO a -> IO a
- Hackage.Security.Util.IO: withDirLock :: Path Absolute -> IO a -> IO a
- Hackage.Security.Util.Lens: get :: Lens' s a -> s -> a
- Hackage.Security.Util.Lens: lookupM :: forall a b. (Eq a, Monoid b) => a -> Lens' [(a, b)] b
- Hackage.Security.Util.Lens: modify :: Lens s t a b -> (a -> b) -> s -> t
- Hackage.Security.Util.Lens: set :: Lens s t a b -> b -> s -> t
- Hackage.Security.Util.Lens: type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
- Hackage.Security.Util.Lens: type Lens' s a = Lens s s a a
+ Hackage.Security.Client: LogLockWait :: Path Absolute -> LogMessage
+ Hackage.Security.Client: LogLockWaitDone :: Path Absolute -> LogMessage
+ Hackage.Security.Client: LogUnlock :: Path Absolute -> LogMessage
+ Hackage.Security.Client.Repository: LogLockWait :: Path Absolute -> LogMessage
+ Hackage.Security.Client.Repository: LogLockWaitDone :: Path Absolute -> LogMessage
+ Hackage.Security.Client.Repository: LogUnlock :: Path Absolute -> LogMessage
+ Hackage.Security.Client.Repository.Cache: lockCacheWithLogger :: (LogMessage -> IO ()) -> Cache -> IO () -> IO ()

Files

ChangeLog.md view
@@ -1,3 +1,18 @@+See also http://pvp.haskell.org/faq++0.6.0.0+-------++* Remove `Hackage.Security.TUF.FileMap.lookupM`+* Don't expose `Hackage.Security.Util.IO` module+* Don't expose `Hackage.Security.Util.Lens` module+* Report missing keys in `.meta` objects more appropriately as+  `ReportSchemaErrors(expected)` instead of via `Monad(fail)`+* Add support for GHC 8.8 / base-4.13+* Use `lukko` for file-locking+* Extend `LogMessage` to signal events for cache lock acquiring and release+* New `lockCacheWithLogger` operation+ 0.5.3.0 ------- 
hackage-security.cabal view
@@ -1,6 +1,6 @@ cabal-version:       1.12 name:                hackage-security-version:             0.5.3.0+version:             0.6.0.0  synopsis:            Hackage security library description:         The hackage security library provides both server and@@ -29,7 +29,10 @@ homepage:            https://github.com/haskell/hackage-security bug-reports:         https://github.com/haskell/hackage-security/issues build-type:          Simple+tested-with:         GHC==8.8.1, GHC==8.6.5, GHC==8.4.4, GHC==8.2.2, GHC==8.0.2,+                     GHC==7.10.3, GHC==7.8.4, GHC==7.6.3, GHC==7.4.2 + extra-source-files:   ChangeLog.md @@ -50,6 +53,16 @@   manual:      False   default:     False +flag mtl21+  description: Use @mtl@ < 2.2 and @mtl-compat@+  manual:      False+  default:     False++flag lukko+  description: Use @lukko@ for file-locking, otherwise use @GHC.IO.Handle.Lock@+  manual:      True+  default:     True+ library   -- Most functionality is exported through the top-level entry points .Client   -- and .Server; the other exported modules are intended for qualified imports.@@ -67,8 +80,6 @@                        Hackage.Security.Trusted                        Hackage.Security.TUF.FileMap                        Hackage.Security.Util.Checked-                       Hackage.Security.Util.IO-                       Hackage.Security.Util.Lens                        Hackage.Security.Util.Path                        Hackage.Security.Util.Pretty                        Hackage.Security.Util.Some@@ -92,40 +103,52 @@                        Hackage.Security.TUF.Timestamp                        Hackage.Security.Util.Base64                        Hackage.Security.Util.Exit-                       Hackage.Security.Util.FileLock+                       Hackage.Security.Util.IO                        Hackage.Security.Util.JSON+                       Hackage.Security.Util.Lens                        Hackage.Security.Util.Stack                        Hackage.Security.Util.TypedEmbedded                        Prelude   -- We support ghc 7.4 (bundled with Cabal 1.14) and up-  build-depends:       base              >= 4.5     && < 4.12,+  build-depends:       base              >= 4.5     && < 4.14,                        base16-bytestring >= 0.1.1   && < 0.2,                        base64-bytestring >= 1.0     && < 1.1,                        bytestring        >= 0.9     && < 0.11,-                       Cabal             >= 1.14    && < 2.4,-                       containers        >= 0.4     && < 0.6,+                       Cabal             >= 1.14    && < 1.26+                                      || >= 2.0     && < 2.6+                                      || >= 3.0     && < 3.2,+                       containers        >= 0.4     && < 0.7,                        ed25519           >= 0.0     && < 0.1,                        filepath          >= 1.2     && < 1.5,-                       mtl               >= 2.2     && < 2.3,                        parsec            >= 3.1     && < 3.2,                        pretty            >= 1.0     && < 1.2,                        cryptohash-sha256 >= 0.11    && < 0.12,                        -- 0.4.2 introduces TarIndex, 0.4.4 introduces more                        -- functionality, 0.5.0 changes type of serialise                        tar               >= 0.5     && < 0.6,-                       time              >= 1.2     && < 1.9,-                       transformers      >= 0.4     && < 0.6,+                       time              >= 1.2     && < 1.10,+                       transformers      >= 0.3     && < 0.6,                        zlib              >= 0.5     && < 0.7,                        -- whatever versions are bundled with ghc:                        template-haskell,                        ghc-prim   if flag(old-directory)-    build-depends:     directory >= 1.1.0.2 && < 1.2,-                       old-time  >= 1 &&       < 1.2+    build-depends:     directory  >= 1.1.0.2 && < 1.2,+                       old-time   >= 1 &&       < 1.2   else-    build-depends:     directory >= 1.2 && < 1.4-  build-tool-depends:  hsc2hs:hsc2hs >= 0.67 && <0.69+    build-depends:     directory  >= 1.2 && < 1.4 +  if flag(mtl21)+    build-depends:     mtl        >= 2.1     && < 2.2,+                       mtl-compat >= 0.2     && < 0.3+  else+    build-depends:     mtl        >= 2.2     && < 2.3++  if flag(lukko)+    build-depends:     lukko      >= 0.1     && < 0.2+  else+    build-depends:     base       >= 4.10+   hs-source-dirs:      src   default-language:    Haskell2010   default-extensions:  DefaultSignatures@@ -200,7 +223,8 @@   -- dependency in network is not redundant.)   if flag(use-network-uri)     build-depends: network-uri >= 2.6 && < 2.7,-                   network     >= 2.6 && < 2.7+                   network     >= 2.6 && < 2.9+                            || >= 3.0 && < 3.2   else     build-depends: network     >= 2.5 && < 2.6 @@ -234,11 +258,14 @@                        zlib    -- dependencies exclusive to test-suite-  build-depends:       tasty            == 1.0.*,+  build-depends:       tasty            == 1.2.*,                        tasty-hunit      == 0.10.*,                        tasty-quickcheck == 0.10.*,-                       QuickCheck       == 2.9.*,-                       temporary        == 1.2.*+                       QuickCheck       >= 2.11 && <2.14,+                       aeson            == 1.4.*,+                       vector           == 0.12.*,+                       unordered-containers >=0.2.8.0 && <0.3,+                       temporary        >= 1.2 && < 1.4    hs-source-dirs:      tests   default-language:    Haskell2010
src/Hackage/Security/Client/Repository.hs view
@@ -316,6 +316,27 @@     -- (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@@ -451,6 +472,12 @@       "Cannot update " ++ pretty file ++ " (" ++ pretty ex ++ ")"   pretty (LogMirrorFailed mirror ex) =       "Exception " ++ displayException ex ++ " when using mirror " ++ mirror+  pretty (LogLockWait file) =+      "Waiting to acquire cache lock on " ++ pretty file+  pretty (LogLockWaitDone file) =+      "Acquired cache lock on " ++ pretty file+  pretty (LogUnlock file) =+      "Released cache lock on " ++ pretty file  instance Pretty UpdateFailure where   pretty UpdateImpossibleUnsupported =
src/Hackage/Security/Client/Repository/Cache.hs view
@@ -12,6 +12,7 @@   , getIndexIdx   , cacheRemoteFile   , lockCache+  , lockCacheWithLogger   ) where  import Control.Exception@@ -214,7 +215,18 @@ -- This avoids two concurrent processes updating the cache at the same time, -- provided they both take the lock. lockCache :: Cache -> IO () -> IO ()-lockCache Cache{..} = withDirLock cacheRoot+lockCache Cache{..} = withDirLock (\_ -> return ()) cacheRoot++-- | Variant of 'lockCache' which emits 'LogMessage's before and after+-- a possibly blocking file-locking system call+--+-- @since 0.6.0+lockCacheWithLogger :: (LogMessage -> IO ()) -> Cache -> IO () -> IO ()+lockCacheWithLogger logger Cache{..} = withDirLock logger' cacheRoot+  where+    logger' (WithDirLockEventPre    fn) = logger (LogLockWait     fn)+    logger' (WithDirLockEventPost   fn) = logger (LogLockWaitDone fn)+    logger' (WithDirLockEventUnlock fn) = logger (LogUnlock       fn)  {-------------------------------------------------------------------------------   Auxiliary: tar
src/Hackage/Security/Client/Repository/Local.hs view
@@ -51,7 +51,7 @@     , repClearCache    = clearCache    cache     , repWithIndex     = withIndex     cache     , repGetIndexIdx   = getIndexIdx   cache-    , repLockCache     = lockCache     cache+    , repLockCache     = lockCacheWithLogger logger cache     , repWithMirror    = mirrorsUnsupported     , repLog           = logger     , repLayout        = repLayout
src/Hackage/Security/Client/Repository/Remote.hs view
@@ -178,7 +178,7 @@       , repClearCache    = Cache.clearCache    cache       , repWithIndex     = Cache.withIndex     cache       , repGetIndexIdx   = Cache.getIndexIdx   cache-      , repLockCache     = Cache.lockCache     cache+      , repLockCache     = Cache.lockCacheWithLogger logger cache       , repWithMirror    = withMirror httpLib                                       selectedMirror                                       logger
src/Hackage/Security/TUF/FileMap.hs view
@@ -13,8 +13,6 @@   , (!)   , insert   , fromList-    -- * Convenience accessors-  , lookupM     -- * Comparing file maps   , FileChange(..)   , fileMapChanges@@ -70,16 +68,6 @@  fromList :: [(TargetPath, FileInfo)] -> FileMap fromList = FileMap . Map.fromList--{--------------------------------------------------------------------------------  Convenience accessors--------------------------------------------------------------------------------}--lookupM :: Monad m => FileMap -> TargetPath -> m FileInfo-lookupM m fp =-    case lookup fp m of-      Nothing  -> fail $ "No entry for " ++ pretty fp ++ " in filemap"-      Just nfo -> return nfo  {-------------------------------------------------------------------------------   Comparing filemaps
src/Hackage/Security/TUF/Snapshot.hs view
@@ -13,6 +13,7 @@ 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@@ -76,9 +77,12 @@     snapshotVersion     <- fromJSField enc "version"     snapshotExpires     <- fromJSField enc "expires"     snapshotMeta        <- fromJSField enc "meta"-    snapshotInfoRoot    <- FileMap.lookupM snapshotMeta (pathRoot       repoLayout)-    snapshotInfoMirrors <- FileMap.lookupM snapshotMeta (pathMirrors    repoLayout)-    snapshotInfoTarGz   <- FileMap.lookupM snapshotMeta (pathIndexTarGz repoLayout)+    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{..} 
src/Hackage/Security/TUF/Timestamp.hs view
@@ -13,6 +13,7 @@ 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@@ -56,7 +57,10 @@     timestampVersion      <- fromJSField enc "version"     timestampExpires      <- fromJSField enc "expires"     timestampMeta         <- fromJSField enc "meta"-    timestampInfoSnapshot <- FileMap.lookupM timestampMeta (pathSnapshot repoLayout)+    let lookupMeta k = case FileMap.lookup k timestampMeta of+          Nothing -> expected ("\"" ++ pretty k ++ "\" entry in .meta object") Nothing+          Just v  -> pure v+    timestampInfoSnapshot <- lookupMeta (pathSnapshot repoLayout)     return Timestamp{..}  instance (MonadKeys m, MonadReader RepoLayout m) => FromJSON m (Signed Timestamp) where
− src/Hackage/Security/Util/FileLock.hsc
@@ -1,202 +0,0 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE InterruptibleFFI #-}-{-# LANGUAGE DeriveDataTypeable #-}---- | This compat module can be removed once base-4.10 (ghc-8.2) is the minimum--- required version. Though note that the locking functionality is not in--- public modules in base-4.10, just in the "GHC.IO.Handle.Lock" module.------ Copied from @cabal-install@ codebase "Distribution.Client.Compat.FileLock".-module Hackage.Security.Util.FileLock (-    FileLockingNotSupported(..)-  , LockMode(..)-  , hLock-  , hTryLock-  ) where--#if MIN_VERSION_base(4,10,0)--import GHC.IO.Handle.Lock--#else---- The remainder of this file is a modified copy--- of GHC.IO.Handle.Lock from ghc-8.2.x------ The modifications were just to the imports and the CPP, since we do not have--- access to the HAVE_FLOCK from the ./configure script. We approximate the--- lack of HAVE_FLOCK with @defined(solaris2_HOST_OS) || defined(aix_HOST_OS)@--- instead since those are known major Unix platforms lacking @flock()@ or--- having broken one.--import Control.Exception (Exception)-import Data.Typeable--#if defined(solaris2_HOST_OS) || defined(aix_HOST_OS)--import Control.Exception (throwIO)-import System.IO (Handle)--#else--import Data.Bits-import Data.Function-import Control.Concurrent.MVar--import Foreign.C.Error-import Foreign.C.Types--import GHC.IO.Handle.Types-import GHC.IO.FD-import GHC.IO.Exception--#if defined(mingw32_HOST_OS)--#if defined(i386_HOST_ARCH)-## define WINDOWS_CCONV stdcall-#elif defined(x86_64_HOST_ARCH)-## define WINDOWS_CCONV ccall-#else-# error Unknown mingw32 arch-#endif--#include <windows.h>--import Foreign.Marshal.Alloc-import Foreign.Marshal.Utils-import Foreign.Ptr-import GHC.Windows--#else /* !defined(mingw32_HOST_OS), so assume unix with flock() */--#include <sys/file.h>--#endif /* !defined(mingw32_HOST_OS) */--#endif /* !(defined(solaris2_HOST_OS) || defined(aix_HOST_OS)) */----- | Exception thrown by 'hLock' on non-Windows platforms that don't support--- 'flock'.-data FileLockingNotSupported = FileLockingNotSupported-  deriving (Typeable, Show)--instance Exception FileLockingNotSupported----- | Indicates a mode in which a file should be locked.-data LockMode = SharedLock | ExclusiveLock---- | If a 'Handle' references a file descriptor, attempt to lock contents of the--- underlying file in appropriate mode. If the file is already locked in--- incompatible mode, this function blocks until the lock is established. The--- lock is automatically released upon closing a 'Handle'.------ Things to be aware of:------ 1) This function may block inside a C call. If it does, in order to be able--- to interrupt it with asynchronous exceptions and/or for other threads to--- continue working, you MUST use threaded version of the runtime system.------ 2) The implementation uses 'LockFileEx' on Windows and 'flock' otherwise,--- hence all of their caveats also apply here.------ 3) On non-Windows plaftorms that don't support 'flock' (e.g. Solaris) this--- function throws 'FileLockingNotImplemented'. We deliberately choose to not--- provide fcntl based locking instead because of its broken semantics.------ @since 4.10.0.0-hLock :: Handle -> LockMode -> IO ()-hLock h mode = lockImpl h "hLock" mode True >> return ()---- | Non-blocking version of 'hLock'.------ @since 4.10.0.0-hTryLock :: Handle -> LockMode -> IO Bool-hTryLock h mode = lockImpl h "hTryLock" mode False--------------------------------------------#if defined(solaris2_HOST_OS) || defined(aix_HOST_OS)---- | No-op implementation.-lockImpl :: Handle -> String -> LockMode -> Bool -> IO Bool-lockImpl _ _ _ _ = throwIO FileLockingNotSupported--#else /* !(defined(solaris2_HOST_OS) || defined(aix_HOST_OS)) */--#if defined(mingw32_HOST_OS)--lockImpl :: Handle -> String -> LockMode -> Bool -> IO Bool-lockImpl h ctx mode block = do-  FD{fdFD = fd} <- handleToFd h-  wh <- throwErrnoIf (== iNVALID_HANDLE_VALUE) ctx $ c_get_osfhandle fd-  allocaBytes sizeof_OVERLAPPED $ \ovrlpd -> do-    fillBytes ovrlpd (fromIntegral sizeof_OVERLAPPED) 0-    let flags = cmode .|. (if block then 0 else #{const LOCKFILE_FAIL_IMMEDIATELY})-    -- We want to lock the whole file without looking up its size to be-    -- consistent with what flock does. According to documentation of LockFileEx-    -- "locking a region that goes beyond the current end-of-file position is-    -- not an error", however e.g. Windows 10 doesn't accept maximum possible-    -- value (a pair of MAXDWORDs) for mysterious reasons. Work around that by-    -- trying 2^32-1.-    fix $ \retry -> c_LockFileEx wh flags 0 0xffffffff 0x0 ovrlpd >>= \b -> case b of-      True  -> return True-      False -> getLastError >>= \err -> case () of-        () | not block && err == #{const ERROR_LOCK_VIOLATION} -> return False-           | err == #{const ERROR_OPERATION_ABORTED} -> retry-           | otherwise -> failWith ctx err-  where-    sizeof_OVERLAPPED = #{size OVERLAPPED}--    cmode = case mode of-      SharedLock    -> 0-      ExclusiveLock -> #{const LOCKFILE_EXCLUSIVE_LOCK}---- https://msdn.microsoft.com/en-us/library/aa297958.aspx-foreign import ccall unsafe "_get_osfhandle"-  c_get_osfhandle :: CInt -> IO HANDLE---- https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203.aspx-foreign import WINDOWS_CCONV interruptible "LockFileEx"-  c_LockFileEx :: HANDLE -> DWORD -> DWORD -> DWORD -> DWORD -> Ptr () -> IO BOOL--#else /* !defined(mingw32_HOST_OS), so assume unix with flock() */--lockImpl :: Handle -> String -> LockMode -> Bool -> IO Bool-lockImpl h ctx mode block = do-  FD{fdFD = fd} <- handleToFd h-  let flags = cmode .|. (if block then 0 else #{const LOCK_NB})-  fix $ \retry -> c_flock fd flags >>= \n -> case n of-    0 -> return True-    _ -> getErrno >>= \errno -> case () of-      () | not block && errno == eWOULDBLOCK -> return False-         | errno == eINTR -> retry-         | otherwise -> ioException $ errnoToIOError ctx errno (Just h) Nothing-  where-    cmode = case mode of-      SharedLock    -> #{const LOCK_SH}-      ExclusiveLock -> #{const LOCK_EX}--foreign import ccall interruptible "flock"-  c_flock :: CInt -> CInt -> IO CInt--#endif /* !defined(mingw32_HOST_OS) */---- | Turn an existing Handle into a file descriptor. This function throws an--- IOError if the Handle does not reference a file descriptor.-handleToFd :: Handle -> IO FD-handleToFd h = case h of-  FileHandle _ mv -> do-    Handle__{haDevice = dev} <- readMVar mv-    case cast dev of-      Just fd -> return fd-      Nothing -> throwErr "not a file descriptor"-  DuplexHandle{} -> throwErr "not a file handle"-  where-    throwErr msg = ioException $ IOError (Just h)-      InappropriateType "handleToFd" msg Nothing Nothing--#endif /* defined(solaris2_HOST_OS) || defined(aix_HOST_OS) */--#endif /* MIN_VERSION_base */
src/Hackage/Security/Util/IO.hs view
@@ -1,21 +1,31 @@+{-# LANGUAGE CPP #-} module Hackage.Security.Util.IO (     -- * Miscelleneous     getFileSize   , handleDoesNotExist+  , WithDirLockEvent(..)   , withDirLock     -- * Debugging   , timedIO   ) where -import Control.Monad (unless)+import Control.Concurrent (threadDelay) import Control.Exception import Data.Time import System.IO hiding (openTempFile, withFile) import System.IO.Error  import Hackage.Security.Util.Path-import Hackage.Security.Util.FileLock (hTryLock, LockMode(ExclusiveLock), FileLockingNotSupported) +#ifdef MIN_VERSION_lukko+import Lukko (FD, fileLockingSupported, fdOpen, fdClose, fdLock, fdUnlock, LockMode(ExclusiveLock))+#else+import GHC.IO.Handle.Lock (hLock, LockMode(ExclusiveLock), FileLockingNotSupported)+#if MIN_VERSION_base(4,11,0)+import GHC.IO.Handle.Lock (hUnlock)+#endif+#endif+ {-------------------------------------------------------------------------------   Miscelleneous -------------------------------------------------------------------------------}@@ -32,21 +42,32 @@         then return Nothing         else throwIO e ++data WithDirLockEvent+  = WithDirLockEventPre    (Path Absolute)+  | WithDirLockEventPost   (Path Absolute)+  | WithDirLockEventUnlock (Path Absolute)+ -- | Attempt to create a filesystem lock in the specified directory. -- -- This will use OS-specific file locking primitives: "GHC.IO.Handle.Lock" with -- @base-4.10" and later or a shim for @base@ versions. ----- Throws an exception if the lock is already present.+-- Blocks if the lock is already present. --+-- The logger callback passed as first argument is invoked before and+-- after acquiring a lock, and after unlocking.+-- -- May fallback to locking via creating a directory: -- Given a file @/path/to@, we do this by attempting to create the directory -- @//path/to/hackage-security-lock@, and deleting the directory again -- afterwards. Creating a directory that already exists will throw an exception -- on most OSs (certainly Linux, OSX and Windows) and is a reasonably common way -- to implement a lock file.-withDirLock :: Path Absolute -> IO a -> IO a-withDirLock dir = bracket takeLock releaseLock . const+withDirLock :: (WithDirLockEvent -> IO ()) -> Path Absolute -> IO a -> IO a+withDirLock logger dir+  = bracket takeLock (\h -> releaseLock h >> logger (WithDirLockEventUnlock lock))+            . const   where     lock :: Path Absolute     lock = dir </> fragment "hackage-security-lock"@@ -54,29 +75,78 @@     lock' :: FilePath     lock' = toFilePath lock +    me = "Hackage.Security.Util.IO.withDirLock: "++    wrapLog :: IO a -> IO a+    wrapLog op = do+      logger (WithDirLockEventPre lock)+      h <- op+      logger (WithDirLockEventPost lock)+      return h++#ifdef MIN_VERSION_lukko+    takeLock :: IO FD+    takeLock+        | fileLockingSupported = do+            h <- fdOpen lock'+            wrapLog (fdLock h ExclusiveLock `onException` fdClose h)+            return h+        | otherwise = wrapLog takeDirLock+      where+        takeDirLock :: IO FD+        takeDirLock = handle onCreateDirError $ do+            createDirectory lock+            return (undefined :: FD)++        onCreateDirError :: IOError -> IO FD+        onCreateDirError ioe+          | isAlreadyExistsError ioe = threadDelay (1*1000*1000) >> takeDirLock+          | otherwise = fail (me++"error creating directory lock: "++show ioe)++    releaseLock h+        | fileLockingSupported = do+            fdUnlock h+            fdClose h+        | otherwise =+            removeDirectory lock++#else     takeLock = do         h <- openFile lock' ReadWriteMode-        handle (takeDirLock h) $ do-            gotlock <- hTryLock h ExclusiveLock-            unless gotlock $-                fail $ "hTryLock: lock already exists: " ++ lock'+        wrapLog $ handle (fallbackToDirLock h) $ do+            hLock h ExclusiveLock             return (Just h) -    takeDirLock :: Handle -> FileLockingNotSupported -> IO (Maybe Handle)-    takeDirLock h _ = do-        -- We fallback to directory locking-        -- so we need to cleanup lock file first: close and remove-        hClose h-        handle onIOError (removeFile lock)-        createDirectory lock-        return Nothing -    onIOError :: IOError -> IO ()-    onIOError _ = hPutStrLn stderr-        "withDirLock: cannot remove lock file before directory lock fallback"+    -- If file locking isn't supported then we fallback to directory locking,+    -- polling if necessary.+    fallbackToDirLock :: Handle -> FileLockingNotSupported -> IO (Maybe Handle)+    fallbackToDirLock h _ = takeDirLock >> return Nothing+      where+        takeDirLock :: IO ()+        takeDirLock = do+            -- We fallback to directory locking+            -- so we need to cleanup lock file first: close and remove+            hClose h+            handle onIOError (removeFile lock)+            handle onCreateDirError (createDirectory lock) -    releaseLock (Just h) = hClose h+        onCreateDirError :: IOError -> IO ()+        onCreateDirError ioe+          | isAlreadyExistsError ioe = threadDelay (1*1000*1000) >> takeDirLock+          | otherwise = fail (me++"error creating directory lock: "++show ioe)++        onIOError :: IOError -> IO ()+        onIOError _ = hPutStrLn stderr+            (me++"cannot remove lock file before directory lock fallback")++    releaseLock (Just h) =+        hClose h+#if MIN_VERSION_base(4,11,0)+        >> hUnlock h+#endif     releaseLock Nothing  = removeDirectory lock+#endif  {-------------------------------------------------------------------------------   Debugging
src/Hackage/Security/Util/Lens.hs view
@@ -7,11 +7,11 @@     -- * Generic definitions     Lens   , Lens'+  , Traversal+  , Traversal'   , get-  , modify+  , over   , set-    -- * Specific lenses-  , lookupM   ) where  import Control.Applicative@@ -22,27 +22,25 @@ -------------------------------------------------------------------------------}  -- | Polymorphic lens-type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t+type Lens s t a b = forall f. Functor f => LensLike f s t a b  -- | Monomorphic lens type Lens' s a = Lens s s a a -get :: Lens' s a -> s -> a-get l = getConst . l Const+-- | Polymorphic traversal+type Traversal s t a b = forall f. Applicative f => LensLike f s t a b -modify :: Lens s t a b -> (a -> b) -> s -> t-modify l f = runIdentity . l (Identity . f)+-- | Monomorphic traversal+type Traversal' s a = Traversal s s a a -set :: Lens s t a b -> b -> s -> t-set l = modify l . const+type LensLike f s t a b = (a -> f b) -> s -> f t+type LensLike' f s a = LensLike f s s a a -{--------------------------------------------------------------------------------  Specific lenses--------------------------------------------------------------------------------}+get :: LensLike' (Const a) s a -> s -> a+get l = getConst . l Const -lookupM :: forall a b. (Eq a, Monoid b) => a -> Lens' [(a, b)] b-lookupM a f = go-  where-    go []                       = (\b'  -> [(a, b')]  ) <$> f mempty-    go ((a', b):xs) | a == a'   = (\b'  -> (a, b'):xs ) <$> f b-                    | otherwise = (\xs' -> (a', b):xs') <$> go xs+over :: LensLike Identity s t a b -> (a -> b) -> s -> t+over l f = runIdentity . l (Identity . f)++set :: LensLike Identity s t a b -> b -> s -> t+set l = over l . const
tests/TestSuite.hs view
@@ -70,6 +70,7 @@           testProperty "prop_roundtrip_canonical" JSON.prop_roundtrip_canonical         , testProperty "prop_roundtrip_pretty"    JSON.prop_roundtrip_pretty         , testProperty "prop_canonical_pretty"    JSON.prop_canonical_pretty+        , testProperty "prop_aeson_canonical"     JSON.prop_aeson_canonical         ]   ] @@ -283,6 +284,9 @@     , downloading isMirrors     , noLocalCopy     , downloading isIndex+    , lockingWait+    , lockingWaitDone+    , lockingRelease     ]  -- | Log messages when we do a check for updates and there are no changes@@ -290,6 +294,9 @@ msgsNoUpdates = [       selectedMirror inMemURI     , downloading isTimestamp+    , lockingWait+    , lockingWaitDone+    , lockingRelease     ]  -- | Log messages we expect when the timestamp and snapshot have been resigned@@ -298,6 +305,9 @@       selectedMirror inMemURI     , downloading isTimestamp     , downloading isSnapshot+    , lockingWait+    , lockingWaitDone+    , lockingRelease     ]  -- | Log messages we expect when the timestamp key has been rolled over@@ -307,12 +317,18 @@     , downloading isTimestamp     , verificationError $ unknownKeyError timestampPath     , downloading isRoot+    , lockingWait+    , lockingWaitDone+    , lockingRelease     , downloading isTimestamp     , downloading isSnapshot     -- Since we delete the timestamp and snapshot on a root info change,     -- we will then conclude that we need to update the mirrors and the index.     , downloading isMirrors     , updating isIndex+    , lockingWait+    , lockingWaitDone+    , lockingRelease     ]  {-------------------------------------------------------------------------------@@ -335,6 +351,14 @@ updating isFile (LogUpdating file) = isFile file updating _ _ = False +lockingWait, lockingWaitDone, lockingRelease :: LogMessage -> Bool+lockingWait (LogLockWait _) = True+lockingWait _ = False+lockingWaitDone (LogLockWaitDone _) = True+lockingWaitDone _ = False+lockingRelease (LogUnlock _) = True+lockingRelease _ = False+ expired :: TargetPath -> VerificationError -> Bool expired f (VerificationErrorExpired f') = f == f' expired _ _ = False@@ -493,6 +517,9 @@     bootstrapMsgs :: [LogMessage -> Bool]     bootstrapMsgs = [ selectedMirror inMemURI                     , downloading isRoot+                    , lockingWait+                    , lockingWaitDone+                    , lockingRelease                     ]      layout :: RepoLayout
tests/TestSuite/JSON.hs view
@@ -3,6 +3,7 @@     prop_roundtrip_canonical,     prop_roundtrip_pretty,     prop_canonical_pretty,+    prop_aeson_canonical,   ) where  -- stdlib@@ -16,8 +17,13 @@ -- hackage-security import Text.JSON.Canonical +-- aeson+import Data.Aeson (Value (..), eitherDecode)+import Data.String (fromString)+import qualified Data.Vector as V+import qualified Data.HashMap.Strict as HM -prop_roundtrip_canonical, prop_roundtrip_pretty, prop_canonical_pretty+prop_aeson_canonical, prop_roundtrip_canonical, prop_roundtrip_pretty, prop_canonical_pretty   :: JSValue -> Bool  prop_roundtrip_canonical jsval =@@ -30,6 +36,9 @@     parseCanonicalJSON (renderCanonicalJSON jsval) ==     fmap canonicalise (parseCanonicalJSON (BS.pack (prettyCanonicalJSON jsval))) +prop_aeson_canonical jsval =+    eitherDecode (renderCanonicalJSON jsval) == Right (toAeson (canonicalise jsval))+ canonicalise :: JSValue -> JSValue canonicalise v@JSNull        = v canonicalise v@(JSBool    _) = v@@ -46,12 +55,13 @@       [ (1, pure JSNull)       , (1, JSBool   <$> arbitrary)       , (2, JSNum    <$> arbitrary)-      , (2, JSString <$> arbitrary)+      , (2, JSString . getASCIIString <$> arbitrary)       , (3, JSArray                <$> resize (sz `div` 2) arbitrary)-      , (3, JSObject . noDupFields <$> resize (sz `div` 2) arbitrary)+      , (3, JSObject . mapFirst getASCIIString .  noDupFields <$> resize (sz `div` 2) arbitrary)       ]     where       noDupFields = nubBy (\(x,_) (y,_) -> x==y)+      mapFirst f = map (\(x, y) -> (f x, y))    shrink JSNull        = []   shrink (JSBool    _) = []@@ -62,6 +72,13 @@     where       shrinkSnd (a,b) = [ (a,b') | b' <- shrink b ] +toAeson :: JSValue -> Value+toAeson JSNull        = Null+toAeson (JSBool b)    = Bool b+toAeson (JSNum n)     = Number (fromIntegral n)+toAeson (JSString s)  = String (fromString s)+toAeson (JSArray xs)  = Array $ V.fromList [ toAeson x | x <- xs ]+toAeson (JSObject xs) = Object $ HM.fromList [ (fromString k, toAeson v) | (k, v) <- xs ]  instance Arbitrary Int54 where   arbitrary = fromIntegral <$>