module Data.PropertyList.Binary.Get where
import Control.Applicative
import Control.Monad
import Control.Monad.Trans.Error ()
import Data.Bits
import qualified Data.ByteString.Char8 as BSC8
import qualified Data.ByteString.Lazy as BL
import Data.PropertyList.Binary.Float
import Data.PropertyList.Binary.Types
import Data.Serialize.Get
import qualified Data.Sequence as Seq
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Time
import qualified Data.Vector.Unboxed as V
import Data.Word
import GHC.Float
rawBPList bs = do
let headerBS = BL.take 8 bs
header@(BPListHeader version) <- runGetLazy bplistHeader headerBS
when (version .&. 0xff00 /= 0x3000) $
Left "Unsupported bplist version"
let trailerBS = BL.drop (BL.length bs bplistTrailerBytes) bs
trailer <- runGetLazy bplistTrailer trailerBS
let nOffsets :: Num a => a
nOffsets = fromIntegral (numObjects trailer)
bytesPerOffset = fromIntegral (offsetIntSize trailer)
offsetsBS
= BL.take (nOffsets * fromIntegral bytesPerOffset)
. BL.drop (fromIntegral (offsetTableOffset trailer))
$ bs
offsets <- runGetLazy (replicateM nOffsets (sizedInt bytesPerOffset)) offsetsBS
return (RawBPList bs header (V.fromList offsets) trailer)
readBPListRecords :: BL.ByteString -> Either String (BPListRecords Abs)
readBPListRecords bs = do
raw <- rawBPList bs
let tlr = rawTrailer raw
ct = numObjects tlr
root = topObject tlr
recs <- mapM (getBPListRecord raw) [0 .. ct 1]
return (BPListRecords root (Seq.fromList recs))
getBPListRecord (RawBPList bs _hdr offsets tlr) objNum
| objNum >= 0 && fromIntegral objNum < V.length offsets
= runGetLazy (bplistRecord objRef) (BL.drop (fromIntegral (offsets V.! fromIntegral objNum)) bs)
| otherwise = Left "getBPListRecord: index out of range"
where
objRef = sizedInt (fromIntegral (objectRefSize tlr))
asciiString str = do
let bs = BSC8.pack str
bs' <- getByteString (BSC8.length bs)
if (bs == bs')
then return ()
else fail ("Expecting " ++ show str)
bplistHeaderBytes = 8
bplistHeader = do
asciiString "bplist"
BPListHeader <$> getWord16be
bplistTrailerBytes = 32
bplistTrailer =
const BPListTrailer
<$> skip 5
<*> getWord8
<*> getWord8
<*> getWord8
<*> getWord64be
<*> getWord64be
<*> getWord64be
bplistRecord ref = msum
[ const BPLNull <$> bplNull
, BPLBool <$> bplTrue
, BPLBool <$> bplFalse
, const BPLFill <$> bplFill
, BPLInt <$> bplInt
, BPLReal <$> bplFloat32
, BPLReal <$> bplFloat64
, BPLDate <$> bplDate
, BPLData <$> bplData
, BPLString <$> bplASCII
, BPLString <$> bplUTF16
, BPLUID <$> bplUID
, BPLArray <$> bplArray ref
, BPLSet <$> bplSet ref
, uncurry BPLDict <$> bplDict ref
]
word8 b = do
b' <- getWord8
if b == b'
then return b
else fail ("expecting " ++ show b)
bplNull = word8 0x00
bplTrue = word8 0x08 >> return False
bplFalse = word8 0x09 >> return True
bplFill = word8 0x0f
bplInt = do
sz <- shiftL 1 . fromIntegral <$> halfByte 0x1
i <- sizedInt sz
return (interpretBPLInt sz i)
bplFloat32 = do
word8 0x22
float2Double <$> getFloat32be
bplFloat64 = do
word8 0x23
getFloat64be
bplDate = do
word8 0x33
interpretBPLDate . word64ToDouble <$> getWord64be
bplData = do
sz <- markerAndSize 0x4
getByteString sz
bplASCII = do
sz <- markerAndSize 0x5
BSC8.unpack <$> getByteString sz
bplUTF16 = do
sz <- markerAndSize 0x6
Text.unpack . Text.decodeUtf16BE <$> getByteString (2*sz)
bplUID = do
sz <- fmap (1+) (halfByte 0x8)
sizedInt (fromIntegral sz)
bplArray ref = do
sz <- markerAndSize 0xA
replicateM sz ref
bplSet ref = do
sz <- markerAndSize 0xC
replicateM sz ref
bplDict ref = do
sz <- markerAndSize 0xD
ks <- replicateM sz ref
vs <- replicateM sz ref
return (ks, vs)
halfByte x = do
marker <- getWord8
if marker `shiftR` 4 == x
then return (marker .&. 0x0f)
else fail ("expecting marker " ++ show x)
markerAndSize x = do
marker <- halfByte x
case marker of
0xf -> do
intSz <- shiftL 1 . fromIntegral <$> halfByte 0x1
sizedInt intSz
_ -> return (fromIntegral marker)
sizedInt :: (Integral i, Bits i) => Word -> Get i
sizedInt 0 = return 0
sizedInt 1 = fromIntegral <$> getWord8
sizedInt 2 = fromIntegral <$> getWord16be
sizedInt 4 = fromIntegral <$> getWord32be
sizedInt 8 = fromIntegral <$> getWord64be
sizedInt n
| n < 0 = fail ("sizedInt: negative size: " ++ show n)
| otherwise = do
let a = n `shiftR` 1; b = n a
x <- sizedInt a
y <- sizedInt b
return ((x `shiftL` (fromIntegral b * 8)) .|. y)
interpretBPLInt :: Word -> Integer -> Integer
interpretBPLInt sz i
| isSigned && testBit i signBit = i bit nBits
| otherwise = i
where
isSigned = sz >= 8
nBits = fromIntegral sz * 8
signBit = nBits 1
interpretBPLDate :: Double -> UTCTime
interpretBPLDate sec = addUTCTime (realToFrac sec) epoch
where
epoch = UTCTime (fromGregorian 2001 1 1) 0
getFloat32be :: Get Float
getFloat32be = do
d <- getWord32be
return $! word32ToFloat d
getFloat64be :: Get Double
getFloat64be = do
d <- getWord64be
return $! word64ToDouble d