URI.hs 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. {-# LANGUAGE OverloadedStrings #-}
  2. module Data.SMTP.Parser.URI (parseURI) where
  3. import Data.Attoparsec.ByteString.Char8
  4. import qualified Data.Attoparsec.ByteString.Char8 as A
  5. import qualified Data.Attoparsec.ByteString as AA
  6. import Data.Word8 (Word8)
  7. import qualified Data.ByteString as BS
  8. import Data.SMTP.Types.URI
  9. import Data.SMTP.Account
  10. import qualified Data.Char as C
  11. import Control.Applicative ((<|>))
  12. import Text.StringConvert
  13. parseURI :: Parser URI
  14. parseURI = do
  15. stringCI "fCMTP://"
  16. a <- parseAccount
  17. u <- A.choice [
  18. do
  19. string "/"
  20. p <- parsePath
  21. return $ URI a p Nothing,
  22. return $ URI a (Path []) Nothing
  23. ]
  24. r <- parseRevision
  25. return u{revision=r}
  26. parsePath :: Parser Path
  27. parsePath = Path <$>
  28. A.many' parseSegment
  29. parseSegment :: Parser String
  30. parseSegment = do
  31. pp <- A.many' $ A.choice [
  32. escapeURI <$> A.takeWhile isPathChar,
  33. do
  34. A.string "%"
  35. c0 <- AA.anyWord8
  36. c1 <- AA.anyWord8
  37. let n' = do
  38. n0 <- fromHex c0
  39. n1 <- fromHex c1
  40. return $ 16*n0 + n1
  41. case n' of
  42. Nothing -> fail "Invalid URI character escaping"
  43. Just n -> return . BS.pack $ if isUnquoted n
  44. then [n]
  45. else [asWord8 '%', c0, c1]
  46. ]
  47. string "/" <|> return "" -- Segments end on a slash, colon, or end of input
  48. return . s . BS.concat $ pp
  49. where
  50. isPathChar :: Char -> Bool
  51. isPathChar c = (C.isAscii c && C.isAlphaNum c) || elem c ("_-=[]{}()." :: String)
  52. escapeURI = BS.pack . normalizePath . BS.unpack
  53. normalizePath :: [Word8] -> [Word8]
  54. normalizePath [] = []
  55. normalizePath (p:pp)
  56. | isReserved p = p : normalizePath pp
  57. | isUnquoted p = p : normalizePath pp
  58. | otherwise = let
  59. c0 = asWord8 '%'
  60. c1 = div p 16
  61. c2 = mod p 16
  62. in c0:c1:c2: normalizePath pp
  63. fromHex p
  64. | p >= asWord8 '0' && p <= asWord8 '9' = Just $ p - asWord8 '0'
  65. | p >= asWord8 'a' && p <= asWord8 'z' = Just $ 10 + p - asWord8 'a'
  66. | p >= asWord8 'A' && p <= asWord8 'Z' = Just $ 10 + p - asWord8 'A'
  67. | otherwise = Nothing
  68. isReserved :: Word8 -> Bool
  69. isReserved x = elem x $ fmap asWord8
  70. [':', '/', '?', '#', '[', ']', '@', '!', '$', '&',
  71. '\'', '(', ')', '*', '+', ',', ';', '=']
  72. isUnquoted :: Word8 -> Bool
  73. isUnquoted x =
  74. inRange x '=' '9' ||
  75. inRange x 'A' 'Z' ||
  76. (x == asWord8 '_') ||
  77. inRange x 'a' 'z'
  78. inRange x b e = x >= asWord8 b && x <= asWord8 e
  79. asWord8 :: Char -> Word8
  80. asWord8 = fromIntegral . C.ord
  81. parseRevision :: Parser (Maybe Revision)
  82. parseRevision =
  83. A.choice [
  84. do
  85. string ":"
  86. Just . Revision . s <$> A.takeWhile isRevisionChar,
  87. return Nothing
  88. ]
  89. where
  90. isRevisionChar :: Char -> Bool
  91. isRevisionChar c = C.isAlphaNum c || elem c ("+-_=." :: String)