min_octet2,
min_octet3,
min_octet4,
+ 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 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)
-
--- | 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))
+instance Ord Cidr where
+ 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 ])
enumerate :: Cidr -> [IPv4Address]
enumerate cidr = [(min_host cidr)..(max_host cidr)]
+
+-- | Replace any masked bits in this CIDR's IPv4Address with zeros.
+normalize :: Cidr -> Cidr
+normalize (Cidr addr mask) =
+ Cidr nrml_addr mask
+ where
+ nrml_addr = apply_mask addr mask B.Zero
+
-- Test lists.
cidr_tests :: TestTree
cidr_tests =
test_combine_contained2,
test_combine_all1,
test_combine_all2,
- test_combine_all3 ]
+ test_combine_all3,
+ test_normalize1,
+ test_normalize2,
+ test_normalize3 ]
cidr_properties :: TestTree
cidr_properties =
testGroup "CIDR Properties" [
prop_all_cidrs_contain_themselves,
- prop_contains_proper_antisymmetric ]
+ prop_contains_proper_antisymmetric,
+ prop_normalize_idempotent,
+ prop_normalize_preserves_equality,
+ prop_ord_instance_antisymmetric,
+ prop_ord_instance_reflexive,
+ prop_ord_instance_transitive ]
-- HUnit Tests
expected = [read "10.0.0.16/30" :: Cidr]
actual = combine_all test_cidrs
+test_normalize1 :: TestTree
+test_normalize1 =
+ testCase desc $ actual @?= expected
+ where
+ desc = "127.0.0.1/8 normalized is 127.0.0.0/8"
+ expected = read "127.0.0.0/8" :: Cidr
+ actual = normalize (read "127.0.0.1/8" :: Cidr)
+
+
+test_normalize2 :: TestTree
+test_normalize2 =
+ testCase desc $ actual @?= expected
+ where
+ desc = "192.168.1.101/24 normalized is 192.168.1.0/24"
+ expected = read "192.168.1.0/24" :: Cidr
+ actual = normalize (read "192.168.1.101/24" :: Cidr)
+
+test_normalize3 :: TestTree
+test_normalize3 =
+ testCase desc $ actual @?= expected
+ where
+ desc = "10.10.10.10/22 normalized is 10.10.8.0/22"
+ expected = read "10.10.8.0/22" :: Cidr
+ actual = normalize (read "10.10.10.10/22" :: Cidr)
+
-- QuickCheck Tests
prop_all_cidrs_contain_themselves :: TestTree
prop_all_cidrs_contain_themselves =
prop cidr1 cidr2 =
(cidr1 `contains_proper` cidr2) ==>
(not (cidr2 `contains_proper` cidr1))
+
+
+-- Running "normalize" a second time shouldn't do anything.
+prop_normalize_idempotent :: TestTree
+prop_normalize_idempotent =
+ 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