text-ldap-0.1.0.0: src/Text/LDAP/Parser.hs
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module : Text.LDAP.Parser
-- Copyright : 2014 Kei Hibino
-- License : BSD3
--
-- Maintainer : ex8k.hibino@gmail.com
-- Stability : experimental
-- Portability : unknown
--
module Text.LDAP.Parser
( LdapParser, runLdapParser
, dn, component, attribute
, ldifDN, ldifAttr
, openLdapEntry, openLdapData
, openLdapDataBlocks
, ldifDecodeAttrValue, ldifAttrValue
, ldifDecodeB64Value
) where
import Control.Applicative
((<$>), pure, (<*>), (*>), (<*), (<|>), some, many)
import Numeric (readHex)
import Data.Monoid ((<>))
import Data.Word (Word8)
import Data.ByteString (ByteString, pack)
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as LB
import Data.Attoparsec.ByteString.Char8
(Parser, satisfy, isAlpha_ascii, char, char8, digit)
import qualified Data.Attoparsec.ByteString.Char8 as AP
import qualified Data.Attoparsec.ByteString as APW
import Data.Attoparsec.ByteString.Lazy (parse, eitherResult)
import qualified Data.ByteString.Base64 as Base64
import Text.LDAP.Data
(AttrType (..), AttrValue (..), Attribute, Component, DN, LdifAttrValue (..),
ordW8, exact, inBounds, elem', notElem')
import qualified Text.LDAP.Data as Data
import Text.LDAP.InternalParser (satisfyW8, ldifSafeString)
import qualified Text.LDAP.InternalParser as Internal
-- | Parser context type for LDAP data stream
type LdapParser = Internal.LdapParser
-- | Run 'LdapParser' context.
runLdapParser :: Parser a -> LB.ByteString -> Either String a
runLdapParser p = eitherResult . parse (p <* AP.endOfInput)
spaces :: LdapParser ()
spaces = many (char ' ') *> pure ()
alpha :: LdapParser Char
alpha = satisfy isAlpha_ascii
alphaW8 :: LdapParser Word8
alphaW8 = ordW8 <$> alpha
digitW8 :: LdapParser Word8
digitW8 = ordW8 <$> digit
quotation :: LdapParser Word8
quotation = APW.word8 Data.quotation
digits1' :: LdapParser ByteString
digits1' = pack <$> some digitW8
-- DN
keychar :: LdapParser Word8
keychar = alphaW8 <|> digitW8 <|> char8 '-'
quotechar :: LdapParser Word8
quotechar = APW.satisfy (`notElem'` [ordW8 '\\', Data.quotation])
special :: LdapParser Word8
special = APW.satisfy (`elem'` Data.specialChars)
stringchar :: LdapParser Word8
stringchar = APW.satisfy (`notElem'` map ordW8 ['\r', '\n', '\\'] ++ Data.quotation : Data.specialChars)
hexchar :: LdapParser Char
hexchar = digit <|> satisfy (`inBounds` [('a', 'f'), ('A', 'F')])
hexpair :: LdapParser Word8
hexpair = (rh <$>) $ (:) <$> hexchar <*> ((:) <$> hexchar <*> pure []) where
rh s
| rs == [] = error $ "hexpair: BUG!: fail to read hex: " ++ s
| otherwise = fst $ head rs
where rs = readHex s
pair :: LdapParser Word8
pair = char '\\' *> (
special <|>
char8 '\\' <|>
quotation <|>
hexpair )
hexstring :: LdapParser ByteString
hexstring = pack <$> some hexpair
string :: LdapParser ByteString
string = pack <$> some (stringchar <|> pair) <|>
char '#' *> hexstring <|>
pack <$> (quotation *> many (quotechar <|> pair) <* quotation) {- Only for v2 -} <|>
pure ""
_testString :: Either String ByteString
_testString = runLdapParser string "\",\""
attrOid :: LdapParser AttrType
attrOid = Data.attrOid <$> digits1' <*> many (char '.' *> digits1')
attrTypeStr :: LdapParser AttrType
attrTypeStr = (Data.AttrType . pack <$>) $ (:) <$> alphaW8 <*> many keychar
attrType :: LdapParser AttrType
attrType = attrTypeStr <|> attrOid
_testAT :: Either String AttrType
_testAT = runLdapParser attrType "dc"
attrValueString :: LdapParser AttrValue
attrValueString = AttrValue <$> string
_testAV :: Either String AttrValue
_testAV = runLdapParser attrValueString "com"
-- | Parser of attribute pair string in RDN.
attribute :: LdapParser Attribute
attribute = (,)
<$> (attrType <* char '=')
<*> attrValueString
_testAttr :: Either String Attribute
_testAttr = runLdapParser attribute "dc=com"
-- | Parser of RDN string.
component :: LdapParser Component
component = Data.component <$> attribute <*> many (char '+' *> attribute)
comma :: LdapParser Char
comma = spaces *> (char ',' <|> char ';') <* spaces
-- | Parser of DN string.
dn :: LdapParser DN
dn = Data.consDN <$> component <*> many (comma *> component)
-- LDIF
fill :: LdapParser ()
fill = spaces
base64Bounds :: [(Char, Char)]
base64Bounds = [('A', 'Z'), ('a', 'z'), ('0', '9'), exact '+', exact '/', exact '=']
base64String :: LdapParser ByteString
base64String = pack <$> many (satisfyW8 (`inBounds` base64Bounds))
padDecodeB64 :: ByteString -> Either String ByteString
padDecodeB64 s = Base64.decode (s <> pad) where
pad = BS8.replicate ((- BS8.length s) `mod` 4) '='
eitherParser :: String -> Either String a -> LdapParser a
eitherParser s = either (fail . ((s ++ ": ") ++)) pure
decodeBase64 :: ByteString -> LdapParser ByteString
decodeBase64 = eitherParser "internal decodeBase64" . padDecodeB64
parseDN :: ByteString -> LdapParser DN
parseDN s =
eitherParser "internal parseDN"
. runLdapParser dn $ LB.fromChunks [s]
-- | Parser of LDIF DN line.
ldifDN :: LdapParser DN
ldifDN = AP.string "dn:" *> (
fill *> dn <|>
char ':' *> fill *> (parseDN =<< decodeBase64 =<< base64String)
)
-- | Parser of LDIF attribute value which may be base64 encoded.
-- Available combinator parser to pass 'ldifAttr' or 'openLdapEntry', etc ...
ldifAttrValue :: Parser LdifAttrValue
ldifAttrValue =
fill *> (LAttrValRaw <$> ldifSafeString) <|>
char ':' *> fill *> (LAttrValBase64 <$> base64String) <|>
fill *> pure (LAttrValRaw "")
-- | Decode value string of attribute pair after stream parsing.
ldifDecodeB64Value :: LdifAttrValue -> Either String AttrValue
ldifDecodeB64Value a = case a of
LAttrValRaw s -> Right $ AttrValue s
LAttrValBase64 b -> AttrValue <$> padDecodeB64 b
-- | Parser of LDIF attribute value. This parser decodes base64 string.
-- Available combinator parser to pass 'ldifAttr' or 'openLdapEntry', etc ...
ldifDecodeAttrValue :: LdapParser AttrValue
ldifDecodeAttrValue =
ldifAttrValue >>=
eitherParser "internal ldifDecodeAttrValue" . ldifDecodeB64Value
-- | Parser of LDIF attribute pair line.
-- Use with 'ldifDecodeAttrValue' or 'ldifAttrValue' parser, like @ldifAttr ldifDecodeAttrValue@.
ldifAttr :: LdapParser a -> LdapParser (AttrType, a)
ldifAttr vp =
(,)
<$> (attrType <* char ':')
<*> vp
newline :: LdapParser ByteString
newline = AP.string "\n" <|> AP.string "\r\n"
-- | OpenLDAP data-stream block parser.
-- Use with 'ldifDecodeAttrValue' or 'ldifAttrValue' parser, like @openLdapEntry ldifDecodeAttrValue@.
openLdapEntry :: LdapParser a
-> LdapParser (DN, [(AttrType, a)])
openLdapEntry dp =
(,)
<$> (ldifDN <* newline)
<*> many (ldifAttr dp <* newline)
-- | OpenLDAP data-stream block list parser.
-- Use with 'ldifDecodeAttrValue' or 'ldifAttrValue' parser, like @openLdapData ldifDecodeAttrValue@.
openLdapData :: LdapParser a
-> LdapParser [(DN, [(AttrType, a)])]
openLdapData dp = many (openLdapEntry dp <* newline)
contLines :: [LB.ByteString] -> [LB.ByteString]
contLines = d where
d [] = []
d (x:xs) = rec' x xs where
rec' a [] = [a]
rec' a (y:ys)
| hd == " " = rec' (a <> tl) ys
| otherwise = a : rec' y ys
where (hd, tl) = LB.splitAt 1 y
blocks :: [LB.ByteString] -> [[LB.ByteString]]
blocks = d where
d [] = []
d ls@(_:_) = hd : blocks (drop 1 tl)
where (hd,tl) = break (== "") ls
-- | Chunking lines of OpenLDAP data stream.
openLdapDataBlocks :: [LB.ByteString] -> [[LB.ByteString]]
openLdapDataBlocks = map contLines . blocks
_test0 :: Either String DN
_test0 = runLdapParser ldifDN "dn: cn=Slash\\\\The Post\\,ma\\=ster\\+\\<\\>\\#\\;,dc=example.sk,dc=com"