diff --git a/Cabal/Cabal.cabal b/Cabal/Cabal.cabal index eee1b029b687700b6cfd1c4aa87872eac5f86a8d..598a3d677dad4589c337487ee45404fd0d59a2e6 100644 --- a/Cabal/Cabal.cabal +++ b/Cabal/Cabal.cabal @@ -681,3 +681,26 @@ test-suite hackage-tests ghc-options: -Wall -rtsopts -threaded default-extensions: CPP default-language: Haskell2010 + +test-suite rpmvercmp + type: exitcode-stdio-1.0 + main-is: RPMVerCmp.hs + + hs-source-dirs: tests + build-depends: + base, + Cabal, + bytestring + + build-depends: + tasty >= 1.1.0.3 && < 1.2, + tasty-hunit, + tasty-quickcheck, + QuickCheck + + c-sources: tests/cbits/rpmvercmp.c + cc-options: -Wall + pkgconfig-depends: glib-2.0 + + ghc-options: -Wall + default-language: Haskell2010 diff --git a/Cabal/tests/RPMVerCmp.hs b/Cabal/tests/RPMVerCmp.hs new file mode 100644 index 0000000000000000000000000000000000000000..8be48056d71d8acdf75d60a332bc51a709c83079 --- /dev/null +++ b/Cabal/tests/RPMVerCmp.hs @@ -0,0 +1,158 @@ +{-# LANGUAGE ForeignFunctionInterface #-} +{-# LANGUAGE OverloadedStrings #-} +module Main where + +import Data.Monoid ((<>)) +import Data.Word (Word8) +import Foreign.C.String (CString) +import Foreign.C.Types (CInt (..)) +import System.IO.Unsafe (unsafePerformIO) + +import Test.QuickCheck (Arbitrary (..), (===)) +import Test.Tasty (defaultMain, testGroup) +import Test.Tasty.HUnit (assertEqual, testCase) +import Test.Tasty.QuickCheck (testProperty) + +import Distribution.Pretty (prettyShow) +import Distribution.Types.Version + +import qualified Data.ByteString as BS +import qualified Data.ByteString.Char8 as BS8 + +------------------------------------------------------------------------------- +-- C reference implementation +------------------------------------------------------------------------------- + +foreign import ccall unsafe "rpmvercmp" c_rmpvercmp + :: CString -> CString -> CInt + +rpmvercmpRef :: BS.ByteString -> BS.ByteString -> Ordering +rpmvercmpRef a b = unsafePerformIO $ + BS.useAsCString a $ \a' -> + BS.useAsCString b $ \b' -> + return $ fromInt $ c_rmpvercmp a' b' + where + fromInt = flip compare 0 + +------------------------------------------------------------------------------- +-- Pure implementation +------------------------------------------------------------------------------- + +rpmvercmpPure :: BS.ByteString -> BS.ByteString -> Ordering +rpmvercmpPure a b = go0 (BS.unpack a) (BS.unpack b) + where + go0 :: [Word8] -> [Word8] -> Ordering + go0 xs ys = go1 (dropNonAlnum8 xs) (dropNonAlnum8 ys) + + go1 :: [Word8] -> [Word8] -> Ordering + go1 [] [] = EQ + go1 [] _ = LT + go1 _ [] = GT + go1 xs@(x:_) ys + | isDigit8 x = + let (xs1, xs2) = span isDigit8 xs + (ys1, ys2) = span isDigit8 ys + -- numeric segments are always newer than alpha segments + in if null ys1 + then GT + else compareInt xs1 ys1 <> go0 xs2 ys2 + + -- isAlpha + | otherwise = + let (xs1, xs2) = span isAlpha8 xs + (ys1, ys2) = span isAlpha8 ys + in if null ys1 + then LT + else compareStr xs1 ys1 <> go0 xs2 ys2 + +-- compare as numbers +compareInt :: [Word8] -> [Word8] -> Ordering +compareInt xs ys = + -- whichever number has more digits wins + compare (length xs') (length ys') <> + -- equal length: use per character compare, "strcmp" + compare xs' ys' + where + -- drop leading zeros + xs' = dropWhile (== 0x30) xs + ys' = dropWhile (== 0x30) ys + +-- strcmp +compareStr :: [Word8] -> [Word8] -> Ordering +compareStr = compare + +dropNonAlnum8 :: [Word8] -> [Word8] +dropNonAlnum8 = dropWhile (\w -> not (isDigit8 w || isAlpha8 w)) + +isDigit8 :: Word8 -> Bool +isDigit8 w = 0x30 <= w && w <= 0x39 + +isAlpha8 :: Word8 -> Bool +isAlpha8 w = (0x41 <= w && w <= 0x5A) || (0x61 <= w && w <= 0x7A) + +------------------------------------------------------------------------------- +-- Tests +------------------------------------------------------------------------------- + +main :: IO () +main = defaultMain $ testGroup "rpmvercmp" + [ testGroup "examples" + [ example "openssl" "1.1.0g" "1.1.0i" LT + , example "openssl" "1.0.2h" "1.1.0" LT + + , example "simple" "1.2.3" "1.2.4" LT + , example "word" "apple" "banana" LT + + , example "corner case" "r" "" GT + , example "corner case" "0" "1" LT + , example "corner case" "1" "0.0" GT + ] + , testGroup "Properties" + [ testProperty "ref reflexive" $ \a -> + rpmvercmpRef (BS.pack a) (BS.pack a) === EQ + , testProperty "pure reflexive" $ \a -> + rpmvercmpPure (BS.pack a) (BS.pack a) === EQ + , testProperty "ref agrees with Version" $ \a b -> + compare a b === rpmvercmpRef (v2bs a) (v2bs b) + , testProperty "pure agrees with Version" $ \a b -> + compare a b === rpmvercmpPure (v2bs a) (v2bs b) + ] + , testGroup "Random inputs" + [ testProperty "random" $ \xs ys -> + let xs' = BS.pack $ unnull $ filter (/= 0) xs + ys' = BS.pack $ unnull $ filter (/= 0) ys + + -- ref doesn't really work with empty inputs reliably. + unnull [] = [1] + unnull zs = zs + in rpmvercmpRef xs' ys' === rpmvercmpPure xs' ys' + ] + ] + where + example n a b c = testCase (n ++ " " ++ BS8.unpack a ++ " <=> " ++ BS8.unpack b) $ do + let ref = rpmvercmpRef a b + let pur = rpmvercmpPure a b + assertEqual "ref" c ref + assertEqual "pure" c pur + +------------------------------------------------------------------------------- +-- Version arbitrary +------------------------------------------------------------------------------- + +newtype V = V Version + deriving (Show, Eq, Ord) + +unV :: V -> Version +unV (V x) = x + +instance Arbitrary V where + arbitrary = V . mkVersion_ <$> arbitrary + + shrink = map V . filter (/= version0) . map mkVersion_ . shrink . versionNumbers . unV + +mkVersion_ :: [Int] -> Version +mkVersion_ [] = version0 +mkVersion_ xs = mkVersion (map abs xs) + +v2bs :: V -> BS.ByteString +v2bs (V x) = BS8.pack (prettyShow x) diff --git a/Cabal/tests/cbits/rpmvercmp.c b/Cabal/tests/cbits/rpmvercmp.c new file mode 100644 index 0000000000000000000000000000000000000000..09efd7c98e72144aa0ee2d3c4970f209ce22c220 --- /dev/null +++ b/Cabal/tests/cbits/rpmvercmp.c @@ -0,0 +1,130 @@ +/* + * This code is taken from the RPM package manager. + * + * RPM is Copyright (c) 1998 by Red Hat Software, Inc., + * and may be distributed under the terms of the GPL and LGPL. + * See http://rpm.org/gitweb?p=rpm.git;a=blob_plain;f=COPYING;hb=HEAD + * + * The code should follow upstream as closely as possible. + * See http://rpm.org/gitweb?p=rpm.git;a=blob_plain;f=lib/rpmvercmp.c;hb=HEAD + * + * Currently the only difference as a policy is that upstream uses C99 + * features and pkg-config does not require a C99 compiler yet. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <glib.h> +#include <string.h> +#include <ctype.h> +#include <stdio.h> + +/* macros to help code look more like upstream */ +#define rstreq(a, b) (strcmp(a, b) == 0) +#define risalnum(c) isalnum((guchar)(c)) +#define risdigit(c) isdigit((guchar)(c)) +#define risalpha(c) isalpha((guchar)(c)) + +/* compare alpha and numeric segments of two versions */ +/* return 1: a is newer than b */ +/* 0: a and b are the same version */ +/* -1: b is newer than a */ +int rpmvercmp(const char * a, const char * b) +{ + char oldch1, oldch2; + char * str1, * str2; + char * one, * two; + int rc; + int isnum; + + /* easy comparison to see if versions are identical */ + if (rstreq(a, b)) return 0; + + str1 = g_alloca(strlen(a) + 1); + str2 = g_alloca(strlen(b) + 1); + + strcpy(str1, a); + strcpy(str2, b); + + one = str1; + two = str2; + + /* loop through each version segment of str1 and str2 and compare them */ + while (*one && *two) { + while (*one && !risalnum(*one)) one++; + while (*two && !risalnum(*two)) two++; + + /* If we ran to the end of either, we are finished with the loop */ + if (!(*one && *two)) break; + + str1 = one; + str2 = two; + + /* grab first completely alpha or completely numeric segment */ + /* leave one and two pointing to the start of the alpha or numeric */ + /* segment and walk str1 and str2 to end of segment */ + if (risdigit(*str1)) { + while (*str1 && risdigit(*str1)) str1++; + while (*str2 && risdigit(*str2)) str2++; + isnum = 1; + } else { + while (*str1 && risalpha(*str1)) str1++; + while (*str2 && risalpha(*str2)) str2++; + isnum = 0; + } + + /* save character at the end of the alpha or numeric segment */ + /* so that they can be restored after the comparison */ + oldch1 = *str1; + *str1 = '\0'; + oldch2 = *str2; + *str2 = '\0'; + + /* this cannot happen, as we previously tested to make sure that */ + /* the first string has a non-null segment */ + if (one == str1) return -1; /* arbitrary */ + + /* take care of the case where the two version segments are */ + /* different types: one numeric, the other alpha (i.e. empty) */ + /* numeric segments are always newer than alpha segments */ + /* XXX See patch #60884 (and details) from bugzilla #50977. */ + if (two == str2) return (isnum ? 1 : -1); + + if (isnum) { + /* this used to be done by converting the digit segments */ + /* to ints using atoi() - it's changed because long */ + /* digit segments can overflow an int - this should fix that. */ + + /* throw away any leading zeros - it's a number, right? */ + while (*one == '0') one++; + while (*two == '0') two++; + + /* whichever number has more digits wins */ + if (strlen(one) > strlen(two)) return 1; + if (strlen(two) > strlen(one)) return -1; + } + + /* strcmp will return which one is greater - even if the two */ + /* segments are alpha or if they are numeric. don't return */ + /* if they are equal because there might be more segments to */ + /* compare */ + rc = strcmp(one, two); + if (rc) return (rc < 1 ? -1 : 1); + + /* restore character that was replaced by null above */ + *str1 = oldch1; + one = str1; + *str2 = oldch2; + two = str2; + } + + /* this catches the case where all numeric and alpha segments have */ + /* compared identically but the segment sepparating characters were */ + /* different */ + if ((!*one) && (!*two)) return 0; + + /* whichever version still has characters left over wins */ + if (!*one) return -1; else return 1; +}