{-# LANGUAGE TemplateHaskell , NamedFieldPuns , CPP #-} -- | Provides a quasiquoter for hexadecimal ByteString literals, with -- placeholders that bind variables. module Data.Hex.Quote ( -- * The quasiquoter hex -- * Helper functions , parseHex ) where import Control.Arrow import Control.Applicative import Control.Monad import Data.Char import Data.Word import Data.Maybe import Language.Haskell.TH import Language.Haskell.TH.Syntax import Language.Haskell.TH.Quote import Text.Parsec hiding ( (<|>), many ) import Text.Parsec.Token import Text.Parsec.String import Text.Parsec.Language import qualified Data.ByteString as B import qualified Data.IntMap as IM dropComments :: String -> String dropComments = go where go ('-':'-':xs) = go (dropWhile (/= '\n') xs) go (x:xs) = x : go xs go [] = [] hexMap :: IM.IntMap Word8 hexMap = IM.fromList . map (first ord) $ concat [ zip ['0'..'9'] [0..9] , zip ['A'..'F'] [10..15] , zip ['a'..'f'] [10..15] ] -- | The hexadecimal parser used for @'hex'@ expressions. parseHex :: String -> [Word8] parseHex = pair . catMaybes . map get . dropComments where get v = IM.lookup (ord v) hexMap pair (h:l:xs) = (h*16 + l) : pair xs pair _ = [] -- We can't lift Word8, but Int lifts to a polymorphic literal liftBS :: [Word8] -> Q Exp liftBS xs = lift (map fromIntegral xs :: [Int]) hexExp :: String -> Q Exp hexExp xs = [| B.pack $(liftBS $ parseHex xs) |] data Tok = Lit [Word8] | Take String (Maybe Integer) deriving (Show) parseToks :: Parser [Tok] parseToks = whiteSpace >> lexeme (many parseTok) <* eof where parseTok :: Parser Tok parseTok = (angles (Take <$> identifier <* symbol ":" <*> len)) <|> ((Lit . parseHex) <$> lexeme (many1 hexDigit)) len = lexeme ( (Nothing <$ symbol "rest") <|> (Just <$> decimal)) TokenParser { whiteSpace, identifier, decimal, symbol, angles, lexeme } = makeTokenParser emptyDef { identStart = letter <|> char '_' , identLetter = alphaNum <|> oneOf "_'" , caseSensitive = True } mkExtract :: [Tok] -> Q Exp mkExtract [] = [| \x -> guard (B.null x) >> Just [] |] mkExtract (Lit xs : ts) = let n = length xs in [| \x -> case B.splitAt n x of (y,z) | B.unpack y == $(liftBS xs) -> $(mkExtract ts) z | otherwise -> Nothing |] mkExtract (Take _ (Just n) : ts) = let nn = fromIntegral n in [| \x -> case B.splitAt nn x of (y,z) | B.length y == nn -> (y:) <$> $(mkExtract ts) z | otherwise -> Nothing |] mkExtract (Take _ Nothing : ts) = [| \x -> [x] <$ $(mkExtract ts) B.empty |] mkPat :: [Tok] -> Q Pat mkPat ts = viewP (mkExtract ts) (conP 'Just [listP vars]) where mkV "_" = wildP mkV n = varP (mkName n) vars = [ mkV n | Take n _ <- ts ] hexPat :: String -> Q Pat hexPat xs = case parse parseToks "Data.Hex.Quote pattern" (dropComments xs) of Left e -> error (show e) Right v -> mkPat v {- | As an expression, the @'hex'@ quasiquoter provides hexadecimal @'B.ByteString'@ literals: >import Data.Hex.Quote >import qualified Data.ByteString as B > >main = B.putStr [hex| > 57 65 2c 20 61 6c 6f 6e 65 20 6f 6e 20 65 61 72 > 74 68 2c 20 63 61 6e 20 72 65 62 65 6c 20 61 67 > 61 69 6e 73 74 20 74 68 65 20 74 79 72 61 6e 6e > 79 20 6f 66 20 74 68 65 20 73 65 6c 66 69 73 68 > 20 72 65 70 6c 69 63 61 74 6f 72 73 2e 0a |] All characters other than @0123456789abcdefABCDEF@ are ignored, including whitespace. Comments start with \"@--@\" and continue to end-of-line: >code = [hex| > 7e3a -- jle 0x3c > 4889f5 -- mov rbp, rsi > bb01000000 -- mov ebx, 0x1 > 488b7d08 |] -- mov rdi, [rbp+0x8] When using @'hex'@ as a pattern, you can include placeholders of the form @\@, where * @name@ is a Haskell identifier, or the wildcard pattern \"@_@\" * @size@ is the size of the field in bytes, or the word @rest@ to consume the rest of the @'B.ByteString'@. The named placeholders bind local variables of type @'B.ByteString'@. Here's an example of pattern-matching an IPv4-over-Ethernet-II frame: >import Data.Hex.Quote > >describe [hex| > 08 00 -- ethernet header > 45 <_:1> -- start of IP header > <_:rest> -- discard remaining frame > |] = (src_mac, dst_mac, len) > >describe _ = error "unknown frame" Quasiquotes require the @QuasiQuotes@ extension. In pattern context, @'hex'@ also requires the @ViewPatterns@ extension. -} hex :: QuasiQuoter hex = QuasiQuoter { quoteExp = hexExp , quotePat = hexPat #if MIN_VERSION_template_haskell(2,5,0) , quoteType = const (error "no type quote for Data.Hex.Quote") , quoteDec = const (error "no decl quote for Data.Hex.Quote") #endif }