/BEncoding.hs

http://github.com/astro/hashvortex · Haskell · 143 lines · 126 code · 14 blank · 3 comment · 13 complexity · db9de46334c191410bd458ff440c527d MD5 · raw file

  1. module BEncoding (BValue(..), encode, decode, parseFile, bdict, bdictLookup, infoHash) where
  2. import qualified Data.ByteString.Lazy.Char8 as B8
  3. import qualified Data.ByteString.Lazy as W8
  4. import qualified Data.ByteString.Char8 as SB8
  5. import qualified Data.ByteString as SW8
  6. import Data.Binary.Strict.Get
  7. import Data.Char (isDigit, chr)
  8. import Control.Monad
  9. import Test.QuickCheck
  10. import Data.List (intercalate)
  11. import Control.DeepSeq
  12. import OpenSSL.Digest (MessageDigest(SHA1))
  13. import OpenSSL.Digest.ByteString.Lazy (digest)
  14. import Prelude hiding (getChar, takeWhile)
  15. data BValue = BInteger Integer
  16. | BString B8.ByteString
  17. | BList [BValue]
  18. | BDict [(BValue, BValue)]
  19. deriving (Eq, Ord)
  20. instance NFData BValue where
  21. rnf (BInteger i) = rnf i
  22. rnf (BString bs) = rnf $ B8.unpack bs
  23. rnf (BList []) = ()
  24. rnf (BList (x:xs)) = rnf x `seq`
  25. rnf (BList xs)
  26. rnf (BDict []) = ()
  27. rnf (BDict ((x, x'):xs)) = rnf x `seq`
  28. rnf x' `seq`
  29. rnf (BDict xs)
  30. encode :: BValue -> B8.ByteString
  31. encode (BInteger i) = B8.singleton 'i' `B8.append`
  32. B8.pack (show i) `B8.append`
  33. B8.singleton 'e'
  34. encode (BString s) = B8.pack (show $ B8.length s) `B8.append`
  35. B8.singleton ':' `B8.append`
  36. s
  37. encode (BList xs) = B8.singleton 'l' `B8.append`
  38. B8.concat (map encode xs) `B8.append`
  39. B8.singleton 'e'
  40. encode (BDict xs) = B8.singleton 'd' `B8.append`
  41. B8.concat (map (\(k, v) ->
  42. encode k `B8.append` encode v
  43. ) xs) `B8.append`
  44. B8.singleton 'e'
  45. bdict :: [(String, BValue)] -> BValue
  46. bdict = BDict . map (\(s, v) ->
  47. (BString $ B8.pack s, v)
  48. )
  49. instance Show BValue where
  50. show (BInteger i) = show i
  51. show (BString s) = show $ B8.unpack s
  52. show (BList l) = show l
  53. show (BDict d) = "{ " ++ (intercalate ", " $
  54. map (\(k, v) -> show k ++ ": " ++ show v) d
  55. ) ++
  56. " }"
  57. decode :: SB8.ByteString -> Either String BValue
  58. decode bs = case runGet decoder bs of
  59. (Right a, _) -> Right a
  60. (Left e, _) -> Left $ "Parse: " ++ e
  61. decoder :: Get BValue
  62. decoder = do c1 <- getChar
  63. case c1 of
  64. 'i' ->
  65. do iS <- takeWhile (\c -> c == '-' || isDigit c)
  66. char 'e'
  67. let Just (i, _) = SB8.readInteger iS
  68. return $ BInteger i
  69. d | isDigit d ->
  70. do lS <- SB8.cons d `liftM` takeWhile isDigit
  71. char ':'
  72. let Just (l, _) = SB8.readInteger lS
  73. s <- getByteString $ fromIntegral l
  74. return $ BString $ B8.fromChunks [s]
  75. 'l' ->
  76. BList `liftM` manyTill decoder 'e'
  77. 'd' ->
  78. BDict `liftM` manyTill (do k <- decoder
  79. v <- decoder
  80. return (k, v)
  81. ) 'e'
  82. _ -> fail $ "unexpected type: " ++ show c1
  83. where getChar :: Get Char
  84. getChar = (chr . fromIntegral) `liftM` getWord8
  85. char :: Char -> Get ()
  86. char c = getChar >>= \c' ->
  87. if c == c'
  88. then return ()
  89. else fail $ "expected " ++ show c
  90. takeWhile :: (Char -> Bool) -> Get SB8.ByteString
  91. takeWhile p = do buf <- lookAhead $ remaining >>= getByteString
  92. let bufLen = SB8.length buf
  93. len = run 0
  94. run i | i >= bufLen = bufLen
  95. | p (SB8.index buf i) = run $ i + 1
  96. | otherwise = i
  97. getByteString len
  98. manyTill :: Get a -> Char -> Get [a]
  99. manyTill e c = lookAhead getChar >>= \c' ->
  100. if c == c'
  101. then getChar >> return []
  102. else do el <- e
  103. (el:) `liftM` manyTill e c
  104. parseFile :: FilePath -> IO (Maybe BValue)
  105. parseFile = return . result . decode <=< SB8.readFile
  106. where result (Left _) = Nothing
  107. result (Right a) = Just a
  108. bdictLookup :: BValue -> String -> Maybe BValue
  109. bdictLookup (BDict dict) key = lookup (BString $ B8.pack key) dict
  110. bdictLookup _ _ = Nothing
  111. infoHash :: BValue -> IO (Maybe SW8.ByteString)
  112. infoHash metaInfo
  113. = maybe (return Nothing) (return . Just <=< sha1) $
  114. encode `liftM` (metaInfo `bdictLookup` "info")
  115. where sha1 :: B8.ByteString -> IO SW8.ByteString
  116. sha1 bs = SW8.pack `liftM` digest SHA1 bs
  117. {-
  118. instance Arbitrary Char where
  119. arbitrary = chr `fmap` oneof [choose (0,127), choose (0,255)]-}
  120. instance Arbitrary B8.ByteString where
  121. arbitrary = B8.pack `fmap` arbitrary
  122. instance Arbitrary SB8.ByteString where
  123. arbitrary = SB8.pack `fmap` arbitrary
  124. instance Arbitrary BValue where
  125. arbitrary = frequency [(10, BInteger `liftM` arbitrary),
  126. (5, resize 150 $ BString `liftM` arbitrary),
  127. (2, resize 5 $ BList `liftM` arbitrary),
  128. (2, resize 5 $ BDict `liftM` arbitrary)]
  129. propEncodeDecode val = decode (SB8.concat $ B8.toChunks $ encode val) == Right val