packages feed

rpm-nvr-0.1.0: src/Data/RPM/VerCmp.hs

{-# LANGUAGE CPP, MultiWayIf #-}

-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 2 of the License, or
-- (at your option) any later version.

-- String-ified version of vercmp from codec-rpm-0.2.2/Codec/RPM/Version.hs
-- Copyright 2016-2018 Red Hat
-- Copyright 2021 Jens Petersen

-- | Compare versions or releases using rpm's vercmp algorithm
module Data.RPM.VerCmp (rpmVerCompare)
where

import Data.Char (isAsciiLower, isAsciiUpper, isDigit)
import Data.List (isPrefixOf)
#if !MIN_VERSION_base(4,11,0)
import Data.Monoid ((<>))
#endif

-- | Compare two version numbers and return an 'Ordering'.
--
-- Native implementation of rpm's C vercmp
rpmVerCompare :: String -> String -> Ordering
rpmVerCompare a b =
  if a == b then EQ
    else let
    -- strip out all non-version characters
    -- keep in mind the strings may be empty after this
    a' = dropSeparators a
    b' = dropSeparators b

    -- rpm compares strings by digit and non-digit components, so grab the first
    -- component of one type
    fn = if isDigit (head a') then isDigit else isAsciiAlpha
    (prefixA, suffixA) = span fn a'
    (prefixB, suffixB) = span fn b'
 in
    if | a' == b'                                       -> EQ
       -- Nothing left means the versions are equal
       {- null a' && null b'                             -> EQ -}
       -- tilde is less than everything, including an empty string
       | ("~" `isPrefixOf` a') && ("~" `isPrefixOf` b') -> rpmVerCompare (tail a') (tail b')
       | ("~" `isPrefixOf` a')                            -> LT
       | ("~" `isPrefixOf` b')                            -> GT
       -- caret is more than everything, except .
       | ("^" `isPrefixOf` a') && ("^" `isPrefixOf` b') -> rpmVerCompare (tail a') (tail b')
       | ("^" `isPrefixOf` a') && null b'               -> GT
       | null a' && ("^" `isPrefixOf` b')               -> LT
       | ("^" `isPrefixOf` a')                            -> LT
       | ("^" `isPrefixOf` b')                            -> GT
       -- otherwise, if one of the strings is null, the other is greater
       | (null a')                                        -> LT
       | (null b')                                        -> GT
       -- Now we have two non-null strings, starting with a non-tilde version character
       -- If one prefix is a number and the other is a string, the one that is a number
       -- is greater.
       | isDigit (head a') && (not . isDigit) (head b') -> GT
       | (not . isDigit) (head a') && isDigit (head b') -> LT
       | isDigit (head a')                                -> (prefixA `compareAsInts` prefixB) <> (suffixA `rpmVerCompare` suffixB)
       | otherwise                                          -> (prefixA `compare` prefixB) <> (suffixA `rpmVerCompare` suffixB)
 where
    compareAsInts :: String -> String -> Ordering
    -- the version numbers can overflow Int, so strip leading 0's and do a string compare,
    -- longest string wins
    compareAsInts x y =
        let x' = dropWhile (== '0') x
            y' = dropWhile (== '0') y
        in
            if length x' > length y' then GT
            else x' `compare` y'

    -- isAlpha returns any unicode alpha, but we just want ASCII characters
    isAsciiAlpha :: Char -> Bool
    isAsciiAlpha x = isAsciiLower x || isAsciiUpper x

    -- RPM only cares about ascii digits, ascii alpha, and ~ ^
    isVersionChar :: Char -> Bool
    isVersionChar x = isDigit x || isAsciiAlpha x || x == '~' || x == '^'

    dropSeparators :: String -> String
    dropSeparators = dropWhile (not . isVersionChar)