bson-0.3.2: Data/Bson/Binary.hs
-- | Standard binary encoding of BSON documents, version 1.0. See bsonspec.org
module Data.Bson.Binary
( putDocument
, getDocument
, putDouble
, getDouble
, putInt32
, getInt32
, putInt64
, getInt64
, putCString
, getCString
) where
import Prelude hiding (length, concat)
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>), (<*>))
#endif
import Control.Monad (when)
import Data.Binary.Get (Get, runGet, getWord8, getWord32be, getWord64be,
getWord32le, getWord64le, getLazyByteStringNul,
getLazyByteString, getByteString, lookAhead)
import Data.Binary.Put (Put, runPut, putWord8, putWord32le, putWord64le,
putWord32be, putWord64be, putLazyByteString,
putByteString)
import Data.Binary.IEEE754 (getFloat64le, putFloat64le)
import Data.ByteString (ByteString)
import Data.Int (Int32, Int64)
import Data.Time.Clock (UTCTime)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds)
import Data.Word (Word8)
import qualified Data.ByteString.Char8 as SC
import qualified Data.ByteString.Lazy.Char8 as LC
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Bson (Document, Value(..), ObjectId(..), MongoStamp(..), Symbol(..),
Javascript(..), UserDefined(..), Regex(..), MinMaxKey(..),
Binary(..), UUID(..), Field(..), MD5(..), Function(..))
putField :: Field -> Put
-- ^ Write binary representation of element
putField (k := v) = case v of
Float x -> putTL 0x01 >> putDouble x
String x -> putTL 0x02 >> putString x
Doc x -> putTL 0x03 >> putDocument x
Array x -> putTL 0x04 >> putArray x
Bin (Binary x) -> putTL 0x05 >> putBinary 0x00 x
Fun (Function x) -> putTL 0x05 >> putBinary 0x01 x
Uuid (UUID x) -> putTL 0x05 >> putBinary 0x04 x
Md5 (MD5 x) -> putTL 0x05 >> putBinary 0x05 x
UserDef (UserDefined x) -> putTL 0x05 >> putBinary 0x80 x
ObjId x -> putTL 0x07 >> putObjectId x
Bool x -> putTL 0x08 >> putBool x
UTC x -> putTL 0x09 >> putUTC x
Null -> putTL 0x0A
RegEx x -> putTL 0x0B >> putRegex x
JavaScr (Javascript env code) ->
if null env
then putTL 0x0D >> putString code
else putTL 0x0F >> putClosure code env
Sym x -> putTL 0x0E >> putSymbol x
Int32 x -> putTL 0x10 >> putInt32 x
Int64 x -> putTL 0x12 >> putInt64 x
Stamp x -> putTL 0x11 >> putMongoStamp x
MinMax x ->
case x of
MinKey -> putTL 0xFF
MaxKey -> putTL 0x7F
where
putTL t = putTag t >> putLabel k
getField :: Get Field
-- ^ Read binary representation of Element
getField = do
t <- getTag
k <- getLabel
v <- case t of
0x01 -> Float <$> getDouble
0x02 -> String <$> getString
0x03 -> Doc <$> getDocument
0x04 -> Array <$> getArray
0x05 -> getBinary >>= \(s, b) ->
case s of
0x00 -> return $ Bin (Binary b)
0x01 -> return $ Fun (Function b)
0x02 -> return $ Bin (Binary b)
0x03 -> return $ Uuid (UUID b)
0x04 -> return $ Uuid (UUID b)
0x05 -> return $ Md5 (MD5 b)
0x80 -> return $ UserDef (UserDefined b)
_ -> fail $ "unknown Bson binary subtype " ++ show s
0x07 -> ObjId <$> getObjectId
0x08 -> Bool <$> getBool
0x09 -> UTC <$> getUTC
0x0A -> return Null
0x0B -> RegEx <$> getRegex
0x0D -> JavaScr . Javascript [] <$> getString
0x0F -> JavaScr . uncurry (flip Javascript) <$> getClosure
0x0E -> Sym <$> getSymbol
0x10 -> Int32 <$> getInt32
0x12 -> Int64 <$> getInt64
0x11 -> Stamp <$> getMongoStamp
0xFF -> return (MinMax MinKey)
0x7F -> return (MinMax MaxKey)
_ -> fail $ "unknown Bson value type " ++ show t
return (k := v)
putTag = putWord8
getTag = getWord8
putLabel = putCString
getLabel = getCString
putDouble = putFloat64le
getDouble = getFloat64le
putInt32 :: Int32 -> Put
putInt32 = putWord32le . fromIntegral
getInt32 :: Get Int32
getInt32 = fromIntegral <$> getWord32le
putInt64 :: Int64 -> Put
putInt64 = putWord64le . fromIntegral
getInt64 :: Get Int64
getInt64 = fromIntegral <$> getWord64le
putCString :: Text -> Put
putCString x = do
putByteString $ TE.encodeUtf8 x
putWord8 0
getCString :: Get Text
getCString = TE.decodeUtf8 . SC.concat . LC.toChunks <$> getLazyByteStringNul
putString :: Text -> Put
putString x = let b = TE.encodeUtf8 x in do
putInt32 $ toEnum (SC.length b + 1)
putByteString b
putWord8 0
getString :: Get Text
getString = do
len <- subtract 1 <$> getInt32
b <- getByteString (fromIntegral len)
getWord8
return $ TE.decodeUtf8 b
putDocument :: Document -> Put
putDocument es = let b = runPut (mapM_ putField es) in do
putInt32 $ (toEnum . fromEnum) (LC.length b + 5) -- include length and null terminator
putLazyByteString b
putWord8 0
getDocument :: Get Document
getDocument = do
len <- subtract 4 <$> getInt32
b <- getLazyByteString (fromIntegral len)
return (runGet getFields b)
where
getFields = lookAhead getWord8 >>= \done -> if done == 0
then return []
else (:) <$> getField <*> getFields
putArray :: [Value] -> Put
putArray vs = putDocument (zipWith f [0..] vs)
where f i v = (T.pack $! show i) := v
getArray :: Get [Value]
getArray = map value <$> getDocument
type Subtype = Word8
putBinary :: Subtype -> ByteString -> Put
putBinary t x = let len = toEnum (SC.length x) in do
putInt32 len
putTag t
putByteString x
getBinary :: Get (Subtype, ByteString)
getBinary = do
len <- getInt32
t <- getTag
x <- getByteString (fromIntegral len)
return (t, x)
{-putBinary :: Subtype -> ByteString -> Put
-- When Binary subtype (0x02) insert extra length field before bytes
putBinary t x = let len = toEnum (length x) in do
putInt32 $ len + if t == 0x02 then 4 else 0
putTag t
when (t == 0x02) (putInt32 len)
putByteString x-}
{-getBinary :: Get (Subtype, ByteString)
-- When Binary subtype (0x02) there is an extra length field before bytes
getBinary = do
len <- getInt32
t <- getTag
len' <- if t == 0x02 then getInt32 else return len
x <- getByteString (fromIntegral len')
return (t, x)-}
putRegex (Regex x y) = putCString x >> putCString y
getRegex = Regex <$> getCString <*> getCString
putSymbol (Symbol x) = putString x
getSymbol = Symbol <$> getString
putMongoStamp (MongoStamp x) = putInt64 x
getMongoStamp = MongoStamp <$> getInt64
putObjectId (Oid x y) = putWord32be x >> putWord64be y
getObjectId = Oid <$> getWord32be <*> getWord64be
putBool x = putWord8 (if x then 1 else 0)
getBool = (> 0) <$> getWord8
putUTC :: UTCTime -> Put
-- store milliseconds since Unix epoch
putUTC x = putInt64 $ round (utcTimeToPOSIXSeconds x * 1000)
getUTC :: Get UTCTime
-- stored as milliseconds since Unix epoch
getUTC = posixSecondsToUTCTime . (/ 1000) . fromIntegral <$> getInt64
putClosure :: Text -> Document -> Put
putClosure x y = let b = runPut (putString x >> putDocument y) in do
putInt32 $ (toEnum . fromEnum) (LC.length b + 4) -- including this length field
putLazyByteString b
getClosure :: Get (Text, Document)
getClosure = do
getInt32
x <- getString
y <- getDocument
return (x, y)
{- Authors: Tony Hannan <tony@10gen.com>
Copyright 2010 10gen Inc.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -}