X-Git-Url: http://gitweb.michael.orlitzky.com/?p=dead%2Fharbl.git;a=blobdiff_plain;f=src%2FIPv4Pattern.hs;h=dd95b13be37d4563e064428c0b52ea109d6c49b6;hp=9f8c87fc1cc2cca2ba1d0da98858b7d3e43bdb6b;hb=b374bc7cc9ab43efbe659f681938ae2c54d01fc8;hpb=1a4b97540833ef71bbe1de92dba03023322ba62a diff --git a/src/IPv4Pattern.hs b/src/IPv4Pattern.hs index 9f8c87f..dd95b13 100644 --- a/src/IPv4Pattern.hs +++ b/src/IPv4Pattern.hs @@ -24,32 +24,111 @@ module IPv4Pattern where + import Test.Tasty ( TestTree, testGroup ) import Test.Tasty.HUnit ( (@?=), testCase ) -import Text.Parsec +import Text.Parsec ( + (<|>), + char, + digit, + many1, + parse, + string, + try, + unexpected ) import Text.Parsec.String ( Parser ) +import Text.Read ( readMaybe ) + +import Pretty ( Pretty(..) ) +-- * Octets + +-- | An ipv4 octet; that is, an integer between @0@ and @255@ +-- inclusive. This is the data type corresponding to a \"v4octet\" +-- in the postscreen parser. +-- newtype IPv4Octet = IPv4Octet Int deriving (Eq, Show) + +instance Pretty IPv4Octet where + pretty_show (IPv4Octet x) = show x + + +-- | Parse an IPv4 octet, which should contain a string of digits. +-- Should fail if the parsed integer does not lie between @0@ and +-- @255@ inclusive. +-- +-- ==== _Examples_ +-- +-- >>> import Text.Parsec ( parseTest ) +-- +-- Standard octets are parsed correctly: +-- +-- >>> parseTest v4octet "0" +-- IPv4Octet 0 +-- +-- >>> parseTest v4octet "127" +-- IPv4Octet 127 +-- +-- >>> parseTest v4octet "255" +-- IPv4Octet 255 +-- +-- Non-digit input throws an error: +-- +-- >>> parseTest v4octet "Hello, World!" +-- parse error at (line 1, column 1): +-- unexpected "H" +-- expecting digit +-- +-- If we're given an integer outside the range @0..255@ (i.e. not a +-- valid octet), we fail: +-- +-- >>> parseTest v4octet "9000" +-- parse error at (line 1, column 5): +-- unexpected end of input +-- expecting digit +-- Octet "9000" must be between 0 and 255. +-- +v4octet :: Parser IPv4Octet +v4octet = do + s <- many1 digit + case ( readMaybe s :: Maybe Int ) of + -- If "many1 digit" gives us a list of digits, we should be able + -- to convert that to an Int! It will overflow rather than fail + -- if the input is too big/small, so it should really always + -- succeed. + Nothing -> unexpected "v4octet: readMaybe failed on a sequence of digits!" + + -- If we got an Int, make sure it's actually a representation of + -- an octet. + Just k -> if 0 <= k && k <= 255 + then return (IPv4Octet k) + else fail ("Octet \"" ++ (show k) + ++ "\" must be between 0 and 255.") + + + + +-- * Sequence members + + +-- | An ipv4 \"sequence member\". A sequence member is either an +-- integer (an octet) or a range of integers (contained in an +-- octet). This data type corresponds to \"v4seq_member\" in the +-- postscreen parser. +-- data IPv4SequenceMember = IPv4SequenceMemberOctet IPv4Octet | IPv4SequenceMemberOctetRange IPv4Octet IPv4Octet deriving (Eq, Show) -data IPv4Sequence = - IPv4SequenceSingleMember IPv4SequenceMember - | IPv4SequenceOptions IPv4SequenceMember IPv4Sequence - deriving (Eq, Show) - -data IPv4Field = IPv4FieldOctet IPv4Octet | IPv4FieldSequence IPv4Sequence - deriving (Eq, Show) - -data IPv4Pattern = - IPv4Pattern IPv4Field IPv4Field IPv4Field IPv4Field - deriving (Eq, Show) +instance Pretty IPv4SequenceMember where + pretty_show (IPv4SequenceMemberOctet octet) = pretty_show octet + pretty_show (IPv4SequenceMemberOctetRange octet1 octet2) = + (pretty_show octet1) ++ ".." ++ (pretty_show octet2) -- | Parse an IPv4 \"sequence member\". A sequence member is either an @@ -57,11 +136,13 @@ data IPv4Pattern = -- -- ==== _Examples_ -- --- >>> parse v4seq_member "" "127" --- Right (IPv4SequenceMemberOctet (IPv4Octet 127)) +-- >>> import Text.Parsec ( parseTest ) +-- +-- >>> parseTest v4seq_member "127" +-- IPv4SequenceMemberOctet (IPv4Octet 127) -- --- >>> parse v4seq_member "" "1..5" --- Right (IPv4SequenceMemberOctetRange (IPv4Octet 1) (IPv4Octet 5)) +-- >>> parseTest v4seq_member "1..5" +-- IPv4SequenceMemberOctetRange (IPv4Octet 1) (IPv4Octet 5) -- v4seq_member :: Parser IPv4SequenceMember v4seq_member = try both <|> just_one @@ -75,6 +156,27 @@ v4seq_member = try both <|> just_one just_one = fmap IPv4SequenceMemberOctet v4octet + +-- * Sequences + +-- | An ipv4 \"sequence\". A sequence contains either a single +-- \"sequence member\" (see 'IPv4SequenceMember'), or a sequence +-- member along with another sequence. So, this is a potentially +-- recursive definition. This type corresponds to \"v4sequence\" in +-- the postscreen parser. +-- +data IPv4Sequence = + IPv4SequenceSingleMember IPv4SequenceMember + | IPv4SequenceOptions IPv4SequenceMember IPv4Sequence + deriving (Eq, Show) + + +instance Pretty IPv4Sequence where + pretty_show (IPv4SequenceSingleMember member) = pretty_show member + pretty_show (IPv4SequenceOptions member subsequence) = + (pretty_show member) ++ ";" ++ (pretty_show subsequence) + + -- | Parse an IPv4 \"sequence\". A sequence is whatever is allowed -- within square brackets. Basically it can be three things: -- @@ -85,17 +187,18 @@ v4seq_member = try both <|> just_one -- -- ==== _Examples_ -- --- >>> parse v4sequence "" "1" --- Right (IPv4SequenceSingleMember (IPv4SequenceMemberOctet (IPv4Octet 1))) +-- >>> import Text.Parsec ( parseTest ) +-- >>> parseTest v4sequence "1" +-- IPv4SequenceSingleMember (IPv4SequenceMemberOctet (IPv4Octet 1)) -- --- >>> parse v4sequence "" "1..2" --- Right (IPv4SequenceSingleMember (IPv4SequenceMemberOctetRange (IPv4Octet 1) (IPv4Octet 2))) +-- >>> pretty_print $ parse v4sequence "" "1..2" +-- 1..2 -- --- >>> parse v4sequence "" "1..2;8" --- Right (IPv4SequenceOptions (IPv4SequenceMemberOctetRange (IPv4Octet 1) (IPv4Octet 2)) (IPv4SequenceSingleMember (IPv4SequenceMemberOctet (IPv4Octet 8)))) +-- >>> pretty_print $ parse v4sequence "" "1..2;8" +-- 1..2;8 -- v4sequence :: Parser IPv4Sequence -v4sequence = try both <|> just_one -- Maybe sepBy is appropriate here? +v4sequence = try both <|> just_one where both = do sm <- v4seq_member @@ -106,16 +209,29 @@ v4sequence = try both <|> just_one -- Maybe sepBy is appropriate here? just_one = fmap IPv4SequenceSingleMember v4seq_member + +-- * Fields + +data IPv4Field = IPv4FieldOctet IPv4Octet | IPv4FieldSequence IPv4Sequence + deriving (Eq, Show) + + +instance Pretty IPv4Field where + pretty_show (IPv4FieldOctet octet) = pretty_show octet + pretty_show (IPv4FieldSequence s) = "[" ++ (pretty_show s) ++ "]" + + -- | Parse an IPv4 \"field\", which is either a boring old octet, or a -- 'v4sequence' within square brackets. -- -- ==== _Examples_ -- --- >>> parse v4field "" "127" --- Right (IPv4FieldOctet (IPv4Octet 127)) +-- >>> import Text.Parsec ( parseTest ) +-- >>> parseTest v4field "127" +-- IPv4FieldOctet (IPv4Octet 127) -- --- >>> parse v4field "" "[127]" --- Right (IPv4FieldSequence (IPv4SequenceSingleMember (IPv4SequenceMemberOctet (IPv4Octet 127)))) +-- >>> pretty_print $ parse v4field "" "[127]" +-- [127] -- v4field :: Parser IPv4Field v4field = just_octet <|> brackets @@ -129,16 +245,55 @@ v4field = just_octet <|> brackets return $ IPv4FieldSequence s --- | Parse an IPv4 octet, which should contain a string of digits. + +-- * Patterns + +data IPv4Pattern = + IPv4Pattern IPv4Field IPv4Field IPv4Field IPv4Field + deriving (Eq, Show) + + +instance Pretty IPv4Pattern where + pretty_show (IPv4Pattern f1 f2 f3 f4) = + (pretty_show f1) ++ "." + ++ (pretty_show f2) + ++ "." + ++ (pretty_show f3) + ++ "." + ++ (pretty_show f4) + + +-- | Parse an ipv4 address pattern. This consists of four fields, +-- separated by periods, where a field is either a simple octet or a +-- sequence. +-- +-- See also: 'v4field', 'v4sequence'. -- -- ==== _Examples_ -- --- >>> parse v4octet "" "127" --- Right (IPv4Octet 127) +-- >>> pretty_print $ parse v4pattern "" "127.0.0.1" +-- 127.0.0.1 +-- +-- >>> pretty_print $ parse v4pattern "" "127.0.[1..3].1" +-- 127.0.[1..3].1 +-- +-- >>> pretty_print $ parse v4pattern "" "127.0.[1..3;8].1" +-- 127.0.[1..3;8].1 +-- +-- In the module intro, it is mentioned that this is invalid: +-- +-- >>> import Text.Parsec ( parseTest ) +-- >>> parseTest v4pattern "1.2.[3.4]" +-- parse error at (line 1, column 7): +-- unexpected "." +-- expecting digit or "]" +-- +-- This one is /also/ invalid; however, we'll parse the valid part off +-- the front of it: +-- +-- >>> pretty_print $ parse v4pattern "" "1.2.3.3[6..9]" +-- 1.2.3.3 -- -v4octet :: Parser IPv4Octet -v4octet = fmap (IPv4Octet . read) $ many1 digit - v4pattern :: Parser IPv4Pattern v4pattern = do field1 <- v4field @@ -151,22 +306,104 @@ v4pattern = do return $ IPv4Pattern field1 field2 field3 field4 + +-- * Enumeration + +-- | Enumerate the members of an 'IPv4SequenceMember'. A sequence +-- member is either an octet, which is easy to enumerate -- we just +-- print it -- or an octet range whose members can be enumerated +-- from least to greatest. +-- +-- We enumerate strings instead of integers because the big picture +-- is that we will be listing out patterns of ipv4 addresses, and +-- those are represented as strings (dotted quad format). +-- +-- ==== _Examples_ +-- +-- >>> let (Right r) = parse v4seq_member "" "127" +-- >>> sequence_members r +-- ["127"] +-- +-- >>> let (Right r) = parse v4seq_member "" "127..135" +-- >>> sequence_members r +-- ["127","128","129","130","131","132","133","134","135"] +-- sequence_members :: IPv4SequenceMember -> [String] sequence_members (IPv4SequenceMemberOctet (IPv4Octet i)) = [show i] -sequence_members (IPv4SequenceMemberOctetRange (IPv4Octet start) (IPv4Octet end)) = - [show x | x <- [start..end]] +sequence_members (IPv4SequenceMemberOctetRange (IPv4Octet s) (IPv4Octet t)) = + [show x | x <- [s .. t]] + +-- | Enumerate the members of an ipv4 sequence. These consist of +-- either a single sequence member (in which case we delegate to +-- 'sequence_members'), or an \"option\" which is enumerated +-- recursively. +-- +-- ==== _Examples_ +-- +-- >>> let (Right r) = parse v4sequence "" "1" +-- >>> sequences r +-- ["1"] +-- +-- >>> let (Right r) = parse v4sequence "" "1..2" +-- >>> sequences r +-- ["1","2"] +-- +-- >>> let (Right r) = parse v4sequence "" "1..3;4;5..9" +-- >>> sequences r +-- ["1","2","3","4","5","6","7","8","9"] +-- sequences :: IPv4Sequence -> [String] -sequences (IPv4SequenceSingleMember sm) = sequence_members sm +sequences (IPv4SequenceSingleMember sm) = + sequence_members sm sequences (IPv4SequenceOptions sm s) = (sequence_members sm) ++ (sequences s) +-- | Enumerate the members of an 'IPv4Field'. If the field contains a +-- single 'IPv4Octet', we simply 'show' it. Otherwise it contains an +-- 'IPv4FieldSequence', and we enumerate that recursively using +-- 'sequences'. +-- +-- ==== _Examples_ +-- +-- >>> let (Right r) = parse v4field "" "1" +-- >>> fields r +-- ["1"] +-- +-- >>> let (Right r) = parse v4field "" "[127..135]" +-- >>> fields r +-- ["127","128","129","130","131","132","133","134","135"] +-- fields :: IPv4Field -> [String] fields (IPv4FieldOctet (IPv4Octet i)) = [show i] fields (IPv4FieldSequence s) = sequences s +-- | Enumerate the addresses represented by a given 'IPv4Pattern'. +-- +-- A pattern contains four fields, sepearated by period +-- characters. We want to list all possible combinations of +-- addresses where the first octet comes from the first field, the +-- second octet comes from the second field... and so on. To do +-- this, we take advantage of the List monad and the fact that +-- 'fields' returns a list of 'String's. +-- +-- ==== _Examples_ +-- +-- A single address: +-- +-- >>> let (Right r) = parse v4pattern "" "127.0.0.1" +-- >>> addresses r +-- ["127.0.0.1"] +-- +-- Anything between 127.0.0.2 and 127.0.0.4, and either 127.0.0.10 +-- or 127.0.0.11: +-- +-- >>> let (Right r) = parse v4pattern "" "127.0.0.[2..4;10;11]" +-- >>> addresses r +-- ["127.0.0.2","127.0.0.3","127.0.0.4","127.0.0.10","127.0.0.11"] +-- addresses :: IPv4Pattern -> [String] addresses (IPv4Pattern field1 field2 field3 field4) = do f1 <- fields field1 @@ -176,7 +413,8 @@ addresses (IPv4Pattern field1 field2 field3 field4) = do return $ f1 ++ "." ++ f2 ++ "." ++ f3 ++ "." ++ f4 --- Tests + +-- * Tests v4octet_tests :: TestTree v4octet_tests =