{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
module Codec.RPM.Version(
DepOrdering(..),
DepRequirement(..),
EVR(..),
parseEVR,
parseDepRequirement,
satisfies,
vercmp)
where
import Data.Char(digitToInt, isAsciiLower, isAsciiUpper, isDigit, isSpace)
import Data.Maybe(fromMaybe)
import Data.Monoid((<>))
import qualified Data.Ord as Ord
import qualified Data.Text as T
import Data.Word(Word32)
import Text.Parsec
import Prelude hiding(EQ, GT, LT)
data EVR = EVR {
epoch :: Maybe Word32,
version :: T.Text,
release :: T.Text }
deriving(Show)
instance Eq EVR where
(==) evr1 evr2 = evr1 `compare` evr2 == Ord.EQ
instance Ord EVR where
compare evr1 evr2 = fromMaybe 0 (epoch evr1) `compare` fromMaybe 0 (epoch evr2) <>
version evr1 `vercmp` version evr2 <>
release evr1 `vercmp` release evr2
data DepOrdering = LT | LTE | EQ | GTE | GT
deriving(Eq, Ord, Show)
data DepRequirement = DepRequirement T.Text (Maybe (DepOrdering, EVR))
deriving (Eq, Ord, Show)
vercmp :: T.Text -> T.Text -> Ordering
vercmp a b = let
a' = dropSeparators a
b' = dropSeparators b
fn = if isDigit (T.head a') then isDigit else isAsciiAlpha
(prefixA, suffixA) = T.span fn a'
(prefixB, suffixB) = T.span fn b'
in
if | T.null a' && T.null b' -> Ord.EQ
| ("~" `T.isPrefixOf` a') && ("~" `T.isPrefixOf` b') -> vercmp (T.tail a') (T.tail b')
| ("~" `T.isPrefixOf` a') -> Ord.LT
| ("~" `T.isPrefixOf` b') -> Ord.GT
| (T.null a') -> Ord.LT
| (T.null b') -> Ord.GT
| isDigit (T.head a') && (not . isDigit) (T.head b') -> Ord.GT
| (not . isDigit) (T.head a') && isDigit (T.head b') -> Ord.LT
| isDigit (T.head a') -> (prefixA `compareAsInts` prefixB) <> (suffixA `vercmp` suffixB)
| otherwise -> (prefixA `compare` prefixB) <> (suffixA `vercmp` suffixB)
where
compareAsInts :: T.Text -> T.Text -> Ordering
compareAsInts x y =
let x' = T.dropWhile (== '0') x
y' = T.dropWhile (== '0') y
in
if T.length x' > T.length y' then Ord.GT
else x' `compare` y'
isAsciiAlpha :: Char -> Bool
isAsciiAlpha x = isAsciiLower x || isAsciiUpper x
isVersionChar :: Char -> Bool
isVersionChar x = isDigit x || isAsciiAlpha x || x == '~'
dropSeparators :: T.Text -> T.Text
dropSeparators = T.dropWhile (not . isVersionChar)
{-# ANN satisfies ("HLint: ignore Redundant if" :: String) #-}
satisfies :: DepRequirement
-> DepRequirement
-> Bool
satisfies (DepRequirement name1 ver1) (DepRequirement name2 ver2) =
if name1 /= name2 then False
else satisfiesVersion ver1 ver2
where
satisfiesVersion Nothing _ = True
satisfiesVersion _ Nothing = True
satisfiesVersion (Just (o1, v1)) (Just (o2, v2))
| T.null (release v1) && (not . T.null) (release v2) && compareEV v1 v2 && isEq o1 = True
| T.null (release v2) && (not . T.null) (release v1) && compareEV v1 v2 && isEq o2 = True
| otherwise =
case compare v1 v2 of
Ord.LT -> isGt o1 || isLt o2
Ord.GT -> isLt o1 || isGt o2
Ord.EQ -> (isLt o1 && isLt o2) || (isEq o1 && isEq o2) || (isGt o1 && isGt o2)
isEq EQ = True
isEq GTE = True
isEq LTE = True
isEq _ = False
isLt LT = True
isLt LTE = True
isLt _ = False
isGt GT = True
isGt GTE = True
isGt _ = False
compareEV v1 v2 = fromMaybe 0 (epoch v1) == fromMaybe 0 (epoch v2) && version v1 == version v2
parseEVRParsec :: Parsec T.Text () EVR
parseEVRParsec = do
e <- optionMaybe $ try parseEpoch
v <- many1 versionChar
r <- try parseRelease <|> return ""
eof
return EVR{epoch=e, version=T.pack v, release=T.pack r}
where
parseEpoch :: Parsec T.Text () Word32
parseEpoch = do
e <- many1 digit
_ <- char ':'
parseInteger 0 e
where
maxW32 = toInteger (maxBound :: Word32)
parseInteger :: Integer -> String -> Parsec T.Text () Word32
parseInteger acc [] = return $ fromInteger acc
parseInteger acc (x:xs) = let
newAcc = (acc * (10 :: Integer)) + toInteger (digitToInt x)
in
if newAcc > maxW32 then parserFail ""
else parseInteger newAcc xs
parseRelease = do
_ <- char '-'
many1 versionChar
versionChar = digit <|> upper <|> lower <|> oneOf "._+%{}~"
parseEVR :: T.Text -> Either ParseError EVR
parseEVR = parse parseEVRParsec ""
parseDepRequirement :: T.Text -> Either ParseError DepRequirement
parseDepRequirement input = parse parseDepRequirement' "" input
where
parseDepRequirement' = do
reqname <- many $ satisfy (not . isSpace)
spaces
reqver <- optionMaybe $ try parseDepVersion
case reqver of
Just _ -> return $ DepRequirement (T.pack reqname) reqver
Nothing -> return $ DepRequirement input Nothing
parseOperator :: Parsec T.Text () DepOrdering
parseOperator = lte <|> gte <|> eq <|> lt <|> gt
eq = try (string "=") >> return EQ
lt = try (string "<") >> return LT
gt = try (string ">") >> return GT
lte = try (string "<=") >> return LTE
gte = try (string ">=") >> return GTE
parseDepVersion :: Parsec T.Text () (DepOrdering, EVR)
parseDepVersion = do
oper <- parseOperator
spaces
evr <- parseEVRParsec
eof
return (oper, evr)