Skip to content

Commit 161abad

Browse files
authored
Merge pull request #18 from garyb/ipv4address-parsing
Fix parsing of IPv4 addresses
2 parents fa818d2 + 4015c10 commit 161abad

File tree

4 files changed

+87
-15
lines changed

4 files changed

+87
-15
lines changed

bower.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"purescript-unfoldable": "^3.0.0"
2626
},
2727
"devDependencies": {
28-
"purescript-test-unit": "11.0.0"
28+
"purescript-test-unit": "11.0.0",
29+
"purescript-strongcheck": "^3.1.0"
2930
}
3031
}

src/Data/URI/Host.purs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@ module Data.URI.Host
66
import Prelude
77

88
import Control.Alt ((<|>))
9-
10-
import Data.String as S
9+
import Data.Int as Int
10+
import Data.Maybe (Maybe(..))
1111
import Data.URI.Common (parseSubDelims, parsePCTEncoded, parseUnreserved, joinWith, rxPat)
1212
import Data.URI.Types (Host(..))
13-
14-
import Text.Parsing.StringParser (Parser, try)
13+
import Text.Parsing.StringParser (Parser, try, fail)
1514
import Text.Parsing.StringParser.Combinators ((<?>), many1)
16-
import Text.Parsing.StringParser.String (string)
15+
import Text.Parsing.StringParser.String (string, char)
1716

1817
parseHost Parser Host
1918
parseHost = parseIPv6Address <|> parseIPv4Address <|> parseRegName
@@ -23,12 +22,24 @@ parseIPv6Address ∷ Parser Host
2322
parseIPv6Address = IPv6Address <$> (string "[" *> rxPat "[a-f0-9\\.:]+" <* string "]") <?> "IPv6 address"
2423

2524
parseIPv4Address Parser Host
26-
parseIPv4Address = IPv4Address <$> rxPat pattern <?> "IPv4 address"
25+
parseIPv4Address = IPv4Address <$> parse <?> "IPv4 address"
2726
where
28-
pattern String
29-
pattern = S.joinWith "" ["(", octet, "\\.", octet, "\\.", octet, "\\.", octet, ")"]
30-
octet String
31-
octet = "(1[0-9]{2}|[1-9][0-9]|[0-9]|2[0-4][0-9]|25[0-5])"
27+
parse Parser String
28+
parse = do
29+
o1 <- octet
30+
_ <- char '.'
31+
o2 <- octet
32+
_ <- char '.'
33+
o3 <- octet
34+
_ <- char '.'
35+
o4 <- octet
36+
pure $ show o1 <> "." <> show o2 <> "." <> show o3 <> "." <> show o4
37+
octet Parser Int
38+
octet = do
39+
s <- rxPat "0|([1-9][0-9]{0,2})"
40+
case Int.fromString s of
41+
Just n | n >= 0 && n <= 255 -> pure n
42+
_ -> fail "Invalid IPv4 address octet"
3243

3344
parseRegName Parser Host
3445
parseRegName =

src/Data/URI/Host/Gen.purs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module Data.URI.Host.Gen where
2+
3+
import Prelude
4+
5+
import Control.Monad.Gen as Gen
6+
import Data.String as S
7+
import Data.URI.Host (Host(..))
8+
9+
genIPv4 :: forall m. Gen.MonadGen m => m Host
10+
genIPv4 = do
11+
a <- Gen.chooseInt 0 255
12+
b <- Gen.chooseInt 0 255
13+
c <- Gen.chooseInt 0 255
14+
d <- Gen.chooseInt 0 255
15+
pure $ IPv4Address $ S.joinWith "." $ show <$> [a, b, c, d]

test/Main.purs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,25 @@ import Prelude
44
import Control.Alternative (empty)
55
import Control.Monad.Aff.AVar (AVAR)
66
import Control.Monad.Eff (Eff)
7+
import Control.Monad.Eff.Class (liftEff)
78
import Control.Monad.Eff.Console (CONSOLE)
9+
import Control.Monad.Eff.Random (RANDOM)
10+
import Data.Array as A
811
import Data.Either (isLeft, Either(..))
9-
import Data.List (List(Nil), singleton, (:))
12+
import Data.Foldable (findMap)
13+
import Data.List (List(..), singleton, (:))
1014
import Data.Maybe (Maybe(Nothing, Just))
1115
import Data.Path.Pathy (currentDir, parentDir', file, dir, rootDir, (</>))
12-
import Data.Tuple (Tuple(Tuple))
16+
import Data.Tuple (Tuple(..), snd)
1317
import Data.URI (Authority(Authority), HierarchicalPart(HierarchicalPart), Host(IPv4Address, NameAddress, IPv6Address), Query(Query), RelativePart(RelativePart), RelativeRef(RelativeRef), URI(URI), URIScheme(URIScheme), runParseURIRef)
18+
import Data.URI.Host as Host
19+
import Data.URI.Host.Gen as Host.Gen
1420
import Data.URI.Query (parseQuery, printQuery)
15-
import Test.Unit (suite, test, TestSuite)
21+
import Test.StrongCheck ((===))
22+
import Test.StrongCheck as SC
23+
import Test.StrongCheck.Gen as SCG
24+
import Test.StrongCheck.LCG as SCL
25+
import Test.Unit (Test, suite, test, TestSuite, success, failure)
1626
import Test.Unit.Assert (assert, equal)
1727
import Test.Unit.Console (TESTOUTPUT)
1828
import Test.Unit.Main (runTest)
@@ -42,8 +52,22 @@ testParseQueryParses uri query =
4252
("parses: \"" <> uri <> "\"")
4353
(equal (Right query) (runParser parseQuery uri))
4454

45-
main :: forall eff. Eff ( console :: CONSOLE , testOutput :: TESTOUTPUT, avar :: AVAR | eff ) Unit
55+
main :: forall eff. Eff ( console :: CONSOLE , testOutput :: TESTOUTPUT, avar :: AVAR, random :: RANDOM | eff ) Unit
4656
main = runTest $ suite "Data.URI" do
57+
58+
suite "parseIPv4Address" do
59+
60+
test "parseIPv4Address / printHost roundtrip" do
61+
forAll do
62+
ipv4 <- Host.Gen.genIPv4
63+
let printed = Host.printHost ipv4
64+
let parsed = runParser Host.parseIPv4Address printed
65+
pure $ pure ipv4 === parsed
66+
67+
test "0-lead octets should not parse" do
68+
assert ("parse should fail for 192.168.001.1") $
69+
isLeft $ runParser Host.parseIPv4Address "192.168.001.1"
70+
4771
suite "runParseURIRef" do
4872
testRunParseURIRefParses
4973
"sql2:///?q=foo&var.bar=baz"
@@ -312,4 +336,25 @@ main = runTest $ suite "Data.URI" do
312336
"key1=&key2="
313337
(Query (Tuple "key1" (Just "") : Tuple "key2" (Just "") : Nil))
314338

339+
forAll :: forall e prop. SC.Testable prop => SCG.Gen prop -> Test (random :: RANDOM | e)
340+
forAll = quickCheck
341+
342+
quickCheck :: forall e prop. SC.Testable prop => prop -> Test (random :: RANDOM | e)
343+
quickCheck = quickCheck' 100
315344

345+
quickCheck' :: forall e prop. SC.Testable prop => Int -> prop -> Test (random :: RANDOM | e)
346+
quickCheck' tries prop = do
347+
seed <- liftEff $ SCL.randomSeed
348+
let
349+
results = SC.quickCheckPure tries seed prop
350+
successes = A.length $ A.filter ((_ == SC.Success) <<< snd) $ results
351+
findErr = findMap case _ of
352+
Tuple seed' (SC.Failed msg) -> Just (Tuple seed' msg)
353+
_ -> Nothing
354+
case findErr results of
355+
Nothing ->
356+
success
357+
Just (Tuple seed' msg) ->
358+
failure $
359+
show (tries - successes) <> "/" <> show tries <> " tests failed: "
360+
<> msg <> " (seed " <> show (SCL.runSeed seed') <> ")"

0 commit comments

Comments
 (0)