packages feed

hsec-core-0.1.0.0: src/Security/Advisories/Core/HsecId.hs

module Security.Advisories.Core.HsecId
  (
    HsecId
  , hsecIdYear
  , hsecIdSerial
  , mkHsecId
  , placeholder
  , isPlaceholder
  , parseHsecId
  , printHsecId
  , nextHsecId
  , getNextHsecId
  ) where

import Control.Monad (guard, join)

import Data.Time (getCurrentTime, utctDay)
import Data.Time.Calendar.OrdinalDate (toOrdinalDate)

import Safe (readMay)

data HsecId = HsecId Integer Integer
  deriving (Eq, Ord)

instance Show HsecId where
  show = printHsecId

-- | Make an 'HsecId'.  Year and serial must both be positive, or
-- else both must be zero (the 'placeholder').
mkHsecId
  :: Integer -- ^ Year
  -> Integer -- ^ Serial number within year
  -> Maybe HsecId
mkHsecId y n
  | y > 0 && n > 0 || y == 0 && n == 0 = Just $ HsecId y n
  | otherwise = Nothing

hsecIdYear :: HsecId -> Integer
hsecIdYear (HsecId y _) = y

hsecIdSerial :: HsecId -> Integer
hsecIdSerial (HsecId _ n) = n

-- | The placeholder ID: __HSEC-0000-0000__.
-- See also 'isPlaceholder'.
placeholder :: HsecId
placeholder = HsecId 0 0

-- | Test whether an ID is the 'placeholder'
isPlaceholder :: HsecId -> Bool
isPlaceholder = (==) placeholder

-- | Parse an 'HsecId'.  The 'placeholder' is accepted.
parseHsecId :: String -> Maybe HsecId
parseHsecId s = case s of
  'H':'S':'E':'C':'-':t ->
    let
      (y, t') = break (== '-') t
      n = drop 1 t'
    in do
      guard $ length y >= 4  -- year must have at least 4 digits
      guard $ length n >= 4  -- serial must have at least 4 digits
      join $ mkHsecId <$> readMay y <*> readMay n
  _ -> Nothing

printHsecId :: HsecId -> String
printHsecId (HsecId y n) = "HSEC-" <> pad (show y) <> "-" <> pad (show n)
  where
  pad s = replicate (4 - length s) '0' <> s

-- | Given a year and an HSEC ID, return a larger HSEC ID.  This
-- function, when given the current year and the greatest allocated
-- HSEC ID, returns the next HSEC ID to allocate.
--
nextHsecId
  :: Integer -- ^ Current year
  -> HsecId
  -> HsecId
nextHsecId curYear (HsecId idYear n)
  | curYear > idYear = HsecId curYear 1
  | otherwise = HsecId idYear (n + 1)

-- | Get the current time, and return an HSEC ID greater than the
-- given HSEC ID.  The year of the returned HSEC ID is the current
-- year.
--
getNextHsecId
  :: HsecId
  -> IO HsecId
getNextHsecId oldId = do
  t <- getCurrentTime
  let (year, _dayOfYear) = toOrdinalDate (utctDay t)
  pure $ nextHsecId year oldId