{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE Safe #-} module Data.Dtb.LowLevel (Header(..), MemoryReservation(..), Token(..), PropData, parseHeader, stringsBlock, structBlock, memoryReservations, extractString, deviceTreeTokens) where import Control.Monad (void) import Data.Binary.Get import Data.ByteString as B import Data.ByteString.Lazy as BL import qualified Data.Text as T import qualified Data.Text.Encoding as E import qualified Data.Text.Encoding.Error as E import Data.Word (Word32, Word64) -- |The header of a flattened device tree file. -- -- See Section 5.2 "Header" in the specification. data Header = Header { magic :: !Word32 , totalsize :: !Word32 , off_dt_struct :: !Word32 , off_dt_strings :: !Word32 , off_mem_rsvmap :: !Word32 , version :: !Word32 , last_comp_version :: !Word32 , boot_cpuid_phys :: !Word32 , size_dt_strings :: !Word32 , size_dt_struct :: !Word32 } deriving (Eq, Show) data MemoryReservation = MemoryReservation { address :: !Word64 , size :: !Word64 } deriving (Eq, Show) type RawDtbData = B.ByteString type StringsBlock = B.ByteString type StructureBlock = B.ByteString headerMagic :: Word32 headerMagic = 0xd00dfeed headerSupportedVersions :: [Word32] headerSupportedVersions = [16, 17] headerParser :: Get Header headerParser = Header <$> getWord32be <*> getWord32be <*> getWord32be <*> getWord32be <*> getWord32be <*> getWord32be <*> getWord32be <*> getWord32be <*> getWord32be <*> getWord32be isValidHeader :: Header -> Bool isValidHeader header = magic header == headerMagic && last_comp_version header `Prelude.elem` headerSupportedVersions -- TODO There is also the binary-strict package, which should handle -- strict bytestrings. runParserOrFail :: Get a -> B.ByteString -> Either T.Text a runParserOrFail p dta = case runGetOrFail p (BL.fromStrict dta) of Left (_, _, errorMsg) -> Left $ T.pack errorMsg Right (_, _, parsedVal) -> Right parsedVal -- |Parse a DTB header from a byte string. -- -- Returns `Nothing`, if the header is not recognized as valid. parseHeader :: RawDtbData -> Either T.Text Header parseHeader dta = do header <- runParserOrFail headerParser dta if isValidHeader header then return header else Left "Invalid Header" memoryReservationParser :: Get MemoryReservation memoryReservationParser = MemoryReservation <$> getWord64be <*> getWord64be -- |Get a list of items by repeatedly applying a parser until an exit -- condition is met. getDelimitedList :: Get a -> (a -> Bool) -> Get [a] getDelimitedList p pred = do it <- p if pred it then return [] else do rest <- getDelimitedList p pred return $ it:rest memoryReservationsParser :: Get [MemoryReservation] memoryReservationsParser = getDelimitedList memoryReservationParser (== (MemoryReservation 0 0)) -- |Return a list of memory reservations from a DTB. memoryReservations :: Header -> RawDtbData -> Either T.Text [MemoryReservation] memoryReservations header = runParserOrFail memoryReservationsParser . B.drop (fromIntegral $ off_mem_rsvmap header) -- |Slice a block out of a byte string given by start and length. sliceBlock :: Word32 -> Word32 -> RawDtbData -> Either T.Text B.ByteString sliceBlock start size dta = do if B.length block == (fromIntegral size) then return block else Left "Invalid block slice" where block = B.take (fromIntegral size) $ B.drop (fromIntegral start) dta -- |Extract the strings block from a DTB. -- -- This function may fail with `Nothing` if the DTB is malformed. stringsBlock :: Header -> RawDtbData -> Either T.Text StringsBlock stringsBlock header = sliceBlock (off_dt_strings header) (size_dt_strings header) -- |Extract a string from the strings block. -- -- The specification doesn't specify the encoding, so we assume it's -- UTF-8. Invalid UTF-8 or invalid offsets will result in `Nothing`. extractString :: StringsBlock -> Word32 -> Either T.Text T.Text extractString sb o | fromIntegral o >= B.length sb = Left "Invalid index into strings block" | otherwise = case E.decodeUtf8' $ B.takeWhile (/= 0) $ B.drop (fromIntegral o) sb of Left err -> Left $ T.pack $ show err Right t -> Right t -- |Extract the strings block from a DTB. -- -- This function may fail with `Nothing` if the DTB is malformed. structBlock :: Header -> RawDtbData -> Either T.Text StructureBlock structBlock header = sliceBlock (off_dt_struct header) (size_dt_struct header) type PropData = B.ByteString data Token = BeginNode T.Text | EndNode | Prop T.Text PropData | Nop | End deriving (Eq, Show, Ord) -- |Parse a zero-terminated string. -- -- This parser makes sure that a 32-bit aligned chunk of input is -- consumed. alignedStringParser :: Get T.Text alignedStringParser = do string <- getDelimitedList getWord8 (== 0) void $ getByteString $ 3 - (Prelude.length string `mod` 4) return $ E.decodeUtf8With E.lenientDecode $ B.pack string beginNodeParser :: Get Token beginNodeParser = BeginNode <$> alignedStringParser propParser :: StringsBlock -> Get Token propParser sb = do len <- getWord32be nameoff <- getWord32be payload <- getByteString $ fromIntegral len void $ getByteString $ fromIntegral $ (4 - (len `mod` 4)) `mod` 4 case extractString sb nameoff of Left err -> fail $ T.unpack err Right name -> return $ Prop name payload tokenParser :: StringsBlock -> Get Token tokenParser strs = do tokenType <- getWord32be case tokenType of 0x1 -> beginNodeParser 0x3 -> propParser strs 0x2 -> return $ EndNode 0x4 -> return $ Nop 0x9 -> return $ End _ -> fail "Invalid token type" tokensParser :: StringsBlock -> Get [Token] tokensParser sb = do l <- getDelimitedList (tokenParser sb) (== End) return $ l ++ [End] -- |Parse the structure block of the device tree into individual -- tokens. deviceTreeTokens :: StringsBlock -> StructureBlock -> Either T.Text [Token] deviceTreeTokens = runParserOrFail . tokensParser