normalize
) where
-import Data.List (nubBy)
+import Data.List (nub)
import Data.List.Split (splitOneOf)
import Data.Maybe (catMaybes, mapMaybe)
-import Test.Tasty ( TestTree, testGroup )
+import Test.Tasty ( TestTree, localOption, testGroup )
import Test.Tasty.HUnit ( (@?=), testCase )
import Test.Tasty.QuickCheck (
Arbitrary( arbitrary ),
Gen,
Property,
+ QuickCheckTests( QuickCheckTests ),
(==>),
testProperty )
import Text.Read (readMaybe)
instance Eq Cidr where
- cidr1 == cidr2 = (cidr1 `equivalent` cidr2)
-
-
--- | Two CIDR ranges are equivalent if they have the same network bits
--- and the masks are the same.
-equivalent :: Cidr -> Cidr -> Bool
-equivalent (Cidr addr1 mbits1) (Cidr addr2 mbits2) =
- (mbits1 == mbits2) && ((apply_mask addr1 mbits1 B.Zero) == (apply_mask addr2 mbits2 B.Zero))
+ -- | Two CIDRs are equal if they have the same network bits and if
+ -- their masks are the same. In other words, if they are the same
+ -- after normalization.
+ cidr1 == cidr2 = (cidr1 <= cidr2) && (cidr2 <= cidr1)
+
+instance Ord Cidr where
+ -- | The CIDR order is simply numeric, with the IPv4Address being
+ -- considered first, before the mask. There was an arbitrary
+ -- choice that had to be made here: which CIDR is smaller,
+ -- 127.0.0.1/8, or 127.0.0.1/32?
+ --
+ -- The arguments for 127.0.0.1/8 <= 127.0.0.1/32 are that it
+ -- agrees with the numeric sort order on masks, and that it's
+ -- generally nicer to see the big networks before the small ones.
+ --
+ -- On the other hand, this order disagrees with the containment
+ -- partial order, since 127.0.0.1/32 is contained properly in
+ -- 127.0.0.1/8.
+ --
+ cidr1 <= cidr2 = if addr1 == addr2 then mask1 <= mask2 else addr1 <= addr2
+ where
+ Cidr addr1 mask1 = normalize cidr1
+ Cidr addr2 mask2 = normalize cidr2
-- | Returns the mask portion of a CIDR address. That is, everything
-- after the trailing slash.
| cidrs == (combine_contained unique_cidrs) = cidrs
| otherwise = combine_all (combine_contained unique_cidrs)
where
- unique_cidrs = nubBy equivalent cidr_combinations
+ unique_cidrs = nub cidr_combinations
cidr_combinations =
cidrs ++ (catMaybes [ (combine_adjacent x y) | x <- cidrs, y <- cidrs ])
test_combine_all3,
test_normalize1,
test_normalize2,
- test_normalize3 ]
+ test_normalize3,
+ test_big_networks_come_first ]
cidr_properties :: TestTree
cidr_properties =
testGroup "CIDR Properties" [
prop_all_cidrs_contain_themselves,
prop_contains_proper_antisymmetric,
- prop_normalize_idempotent ]
+ prop_normalize_idempotent,
+ prop_normalize_preserves_equality,
+ prop_ord_instance_antisymmetric,
+ prop_ord_instance_reflexive,
+ prop_ord_instance_transitive,
+ prop_ord_uses_addr_when_masks_equal,
+ prop_ord_uses_mask_when_addrs_equal,
+ prop_ord_and_contains_disagree,
+ prop_ord_minimum,
+ prop_ord_maximum ]
-- HUnit Tests
expected = read "10.10.8.0/22" :: Cidr
actual = normalize (read "10.10.10.10/22" :: Cidr)
+-- | Test a stated property of the Ord instance, namely that the big
+-- network 127.0.0.1/8 comes before the small network 127.0.0.1/32.
+test_big_networks_come_first :: TestTree
+test_big_networks_come_first =
+ testCase desc $ actual @?= expected
+ where
+ desc = "127.0.0.1/8 comes before 127.0.0.1/32"
+ big = read "127.0.0.1/8" :: Cidr
+ small = read "127.0.0.1/32" :: Cidr
+ expected = True
+ actual = big <= small -- not a typo
+
-- QuickCheck Tests
prop_all_cidrs_contain_themselves :: TestTree
prop_all_cidrs_contain_themselves =
-- Running "normalize" a second time shouldn't do anything.
prop_normalize_idempotent :: TestTree
prop_normalize_idempotent =
- testProperty "The CIDR \"normalize\" function is idempotent " prop
+ testProperty "The CIDR \"normalize\" function is idempotent" prop
where
prop :: Cidr -> Bool
prop cidr = (normalize cidr) == (normalize (normalize cidr))
+
+-- Normalization should not affect equality of two CIDRs.
+prop_normalize_preserves_equality :: TestTree
+prop_normalize_preserves_equality =
+ testProperty "The CIDR \"normalize\" function preserves equality" prop
+ where
+ prop :: Cidr -> Cidr -> Bool
+ prop cidr1 cidr2 = (cidr1 == cidr2) == (normalize cidr1 == normalize cidr2)
+
+
+prop_ord_instance_reflexive :: TestTree
+prop_ord_instance_reflexive =
+ testProperty "The CIDR order is reflexive" prop
+ where
+ prop :: Cidr -> Bool
+ prop cidr = cidr <= cidr
+
+
+prop_ord_instance_transitive :: TestTree
+prop_ord_instance_transitive =
+ testProperty "The CIDR order is transitive" prop
+ where
+ prop :: Cidr -> Cidr -> Cidr -> Property
+ prop cidr1 cidr2 cidr3 =
+ (cidr1 <= cidr2 && cidr2 <= cidr3) ==> cidr1 <= cidr3
+
+-- This is how Eq is currently implemented, but it is useful to have
+-- around in case that changes. Try fewer instances of this than usual
+-- because it's a rare condition.
+prop_ord_instance_antisymmetric :: TestTree
+prop_ord_instance_antisymmetric =
+ localOption (QuickCheckTests 500) $
+ testProperty "The CIDR order is antisymmetric" prop
+ where
+ prop :: Cidr -> Cidr -> Property
+ prop cidr1 cidr2 =
+ (cidr1 <= cidr2 && cidr2 <= cidr1) ==> cidr1 == cidr2
+
+
+-- When comparing two CIDRs with the same mask, the comparison
+-- should be numeric (i.e. whatever the IPv4Address does).
+-- Of course, we have to normalize first.
+prop_ord_uses_addr_when_masks_equal :: TestTree
+prop_ord_uses_addr_when_masks_equal =
+ testProperty "The CIDR order is the IPv4Address order for equal masks" prop
+ where
+ prop :: Cidr -> Cidr -> Property
+ prop cidr1 cidr2 =
+ (mask1 == mask2) ==> (cidr1 <= cidr2) == (addr1 <= addr2)
+ where
+ (Cidr addr1 mask1) = normalize cidr1
+ (Cidr addr2 mask2) = normalize cidr2
+
+
+-- If we have two CIDRs whose normalized addresses agree, then we want
+-- to use the mask order, i.e. that big networks come before small
+-- networks. This disagrees with containment order.
+prop_ord_uses_mask_when_addrs_equal :: TestTree
+prop_ord_uses_mask_when_addrs_equal =
+ localOption (QuickCheckTests 500) $
+ testProperty "The CIDR order is by mask when the addresses agree" prop
+ where
+ prop :: Cidr -> Cidr -> Property
+ prop cidr1 cidr2 =
+ (addr1 == addr2) ==> (cidr1 <= cidr2) == (mask1 <= mask2)
+ where
+ (Cidr addr1 mask1) = normalize cidr1
+ (Cidr addr2 mask2) = normalize cidr2
+
+
+-- Big networks come first.
+prop_ord_and_contains_disagree :: TestTree
+prop_ord_and_contains_disagree =
+ testProperty "The CIDR order disagrees with containment" prop
+ where
+ prop :: Cidr -> Cidr -> Property
+ prop cidr1 cidr2 = (cidr1 `contains` cidr2) ==> (cidr1 <= cidr2)
+
+
+-- The biggest network always comes first.
+prop_ord_minimum :: TestTree
+prop_ord_minimum =
+ testProperty "The CIDR order has 0.0.0.0/0 as a minimum" prop
+ where
+ min_cidr = read "0.0.0.0/0" :: Cidr
+ prop :: Cidr -> Bool
+ prop cidr = min_cidr <= cidr
+
+
+-- The CIDR order also has a maximum.
+prop_ord_maximum :: TestTree
+prop_ord_maximum =
+ testProperty "The CIDR order has 255.255.255.255/32 as a maximum" prop
+ where
+ max_cidr = read "255.255.255.255/32" :: Cidr
+ prop :: Cidr -> Bool
+ prop cidr = max_cidr >= cidr