866 lines
33 KiB
Haskell
866 lines
33 KiB
Haskell
{-# LANGUAGE DeriveGeneric #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
module Wire
|
|
( Bundle (..)
|
|
, BundleManifest (..)
|
|
, TreeSpec (..)
|
|
, NodeHashSpec (..)
|
|
, RuntimeSpec (..)
|
|
, BundleRoot (..)
|
|
, BundleExport (..)
|
|
, BundleMetadata
|
|
, ClosureMode (..)
|
|
, BundleNode (..)
|
|
, encodeBundle
|
|
, decodeBundle
|
|
, verifyBundle
|
|
, buildBundle
|
|
, reconstructBundleTerms
|
|
, defaultExportNames
|
|
) where
|
|
|
|
import Research hiding (Node)
|
|
|
|
import Control.Monad (foldM, forM_, unless, when)
|
|
import Data.Bits (shiftL, shiftR, (.|.), (.&.))
|
|
import Data.ByteString (ByteString)
|
|
import Data.Foldable (traverse_)
|
|
import qualified Data.Foldable as Foldable
|
|
import Data.List (mapAccumL)
|
|
import Data.Map (Map)
|
|
import qualified Data.Map as Map
|
|
import Data.Sequence (Seq, (|>))
|
|
import qualified Data.Sequence as Seq
|
|
import Data.Set (Set)
|
|
import qualified Data.Set as Set
|
|
import Data.Text (Text, unpack)
|
|
import Data.Text.Encoding (decodeUtf8', encodeUtf8)
|
|
import Data.Vector (Vector)
|
|
import qualified Data.Vector as V
|
|
import qualified Data.Vector.Mutable as MV
|
|
import Data.Word (Word16, Word32, Word64, Word8)
|
|
import GHC.Generics (Generic)
|
|
|
|
import qualified Data.ByteString as BS
|
|
import qualified Data.Text as T
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Container constants
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
bundleMajorVersion :: Word16
|
|
bundleMajorVersion = 1
|
|
|
|
bundleMinorVersion :: Word16
|
|
bundleMinorVersion = 0
|
|
|
|
bundleMagic :: ByteString
|
|
bundleMagic = BS.pack [0x41, 0x52, 0x42, 0x4f, 0x52, 0x49, 0x43, 0x58]
|
|
|
|
headerLength :: Int
|
|
headerLength = 32
|
|
|
|
sectionEntryLength :: Int
|
|
sectionEntryLength = 32
|
|
|
|
sectionManifest, sectionNodes :: Word32
|
|
sectionManifest = 1
|
|
sectionNodes = 2
|
|
|
|
flagCritical :: Word16
|
|
flagCritical = 0x0001
|
|
|
|
compressionNone :: Word16
|
|
compressionNone = 0
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Manifest constants
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
manifestMagic :: ByteString
|
|
manifestMagic = "ARBMNFST"
|
|
|
|
manifestMajorVersion :: Word16
|
|
manifestMajorVersion = 1
|
|
|
|
manifestMinorVersion :: Word16
|
|
manifestMinorVersion = 1
|
|
|
|
closureToByte :: ClosureMode -> Word8
|
|
closureToByte = \case
|
|
ClosureComplete -> 0
|
|
ClosurePartial -> 1
|
|
|
|
closureFromByte :: Word8 -> Either String ClosureMode
|
|
closureFromByte = \case
|
|
0 -> Right ClosureComplete
|
|
1 -> Right ClosurePartial
|
|
n -> Left $ "unsupported closure byte: " ++ show n
|
|
|
|
tagPackage, tagVersion, tagDescription, tagLicense, tagCreatedBy :: Word16
|
|
tagPackage = 1
|
|
tagVersion = 2
|
|
tagDescription = 3
|
|
tagLicense = 4
|
|
tagCreatedBy = 5
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Text encoding helpers
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
encodeLengthPrefixedText :: Text -> ByteString
|
|
encodeLengthPrefixedText t = encode32 (fromIntegral $ BS.length bs) <> bs
|
|
where bs = encodeUtf8 t
|
|
|
|
decodeLengthPrefixedText :: ByteString -> Either String (Text, ByteString)
|
|
decodeLengthPrefixedText bs = do
|
|
(len, rest) <- decode32be "text_length" bs
|
|
let payloadLen = fromIntegral len
|
|
when (BS.length rest < payloadLen) $
|
|
Left "decodeLengthPrefixedText: string extends beyond input"
|
|
let (textBytes, after) = BS.splitAt payloadLen rest
|
|
case decodeUtf8' textBytes of
|
|
Right txt -> Right (txt, after)
|
|
Left _ -> Left "decodeLengthPrefixedText: invalid UTF-8"
|
|
|
|
encodeMetadataTLV :: Word16 -> ByteString -> ByteString
|
|
encodeMetadataTLV tag val = encode16 tag <> encode32 (fromIntegral $ BS.length val) <> val
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Manifest encoders
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
encodeManifest :: BundleManifest -> ByteString
|
|
encodeManifest m =
|
|
manifestMagic
|
|
<> encode16 manifestMajorVersion
|
|
<> encode16 manifestMinorVersion
|
|
<> encodeLengthPrefixedText (manifestSchema m)
|
|
<> encodeLengthPrefixedText (manifestBundleType m)
|
|
<> encodeLengthPrefixedText (treeCalculus (manifestTree m))
|
|
<> encodeLengthPrefixedText (nodeHashAlgorithm (treeNodeHash (manifestTree m)))
|
|
<> encodeLengthPrefixedText (nodeHashDomain (treeNodeHash (manifestTree m)))
|
|
<> encodeLengthPrefixedText (treeNodePayload (manifestTree m))
|
|
<> encodeLengthPrefixedText (runtimeSemantics (manifestRuntime m))
|
|
<> encodeLengthPrefixedText (runtimeEvaluation (manifestRuntime m))
|
|
<> encodeLengthPrefixedText (runtimeAbi (manifestRuntime m))
|
|
<> encode32 (fromIntegral $ length (runtimeCapabilities (manifestRuntime m)))
|
|
<> encodeCapabilities (runtimeCapabilities (manifestRuntime m))
|
|
<> BS.pack [closureToByte (manifestClosure m)]
|
|
<> encode32 (fromIntegral $ length (manifestRoots m))
|
|
<> encodeRoots (manifestRoots m)
|
|
<> encode32 (fromIntegral $ length (manifestExports m))
|
|
<> encodeExports (manifestExports m)
|
|
<> encodeMetadataTLVs (manifestMetadata m)
|
|
<> encode32 0
|
|
|
|
encodeCapabilities :: [Text] -> ByteString
|
|
encodeCapabilities = mconcat . map encodeLengthPrefixedText
|
|
|
|
encodeRoots :: [BundleRoot] -> ByteString
|
|
encodeRoots = mconcat . map encodeRoot
|
|
|
|
encodeRoot :: BundleRoot -> ByteString
|
|
encodeRoot root = encode32 (rootIndex root) <> encodeLengthPrefixedText (rootRole root)
|
|
|
|
encodeExports :: [BundleExport] -> ByteString
|
|
encodeExports = mconcat . map encodeExport
|
|
|
|
encodeExport :: BundleExport -> ByteString
|
|
encodeExport exp =
|
|
encodeLengthPrefixedText (exportName exp)
|
|
<> encode32 (exportRoot exp)
|
|
<> encodeLengthPrefixedText (exportKind exp)
|
|
<> encodeLengthPrefixedText (exportAbi exp)
|
|
|
|
encodeMetadataTLVs :: BundleMetadata -> ByteString
|
|
encodeMetadataTLVs m =
|
|
let entries = metadataTLVEntries m
|
|
in encode32 (fromIntegral $ length entries) <> encodeTLVs entries
|
|
|
|
metadataTLVEntries :: BundleMetadata -> [(Word16, ByteString)]
|
|
metadataTLVEntries m =
|
|
maybeEntry tagPackage (metadataPackage m)
|
|
++ maybeEntry tagVersion (metadataVersion m)
|
|
++ maybeEntry tagDescription (metadataDescription m)
|
|
++ maybeEntry tagLicense (metadataLicense m)
|
|
++ maybeEntry tagCreatedBy (metadataCreatedBy m)
|
|
where
|
|
maybeEntry _ Nothing = []
|
|
maybeEntry tag (Just value) = [(tag, encodeUtf8 value)]
|
|
|
|
encodeTLVs :: [(Word16, ByteString)] -> ByteString
|
|
encodeTLVs = mconcat . map (uncurry encodeMetadataTLV)
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Manifest decoders
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
decodeManifest :: ByteString -> Either String BundleManifest
|
|
decodeManifest bs = do
|
|
when (BS.length bs < 8) $ Left "manifest too short for magic"
|
|
when (BS.take 8 bs /= manifestMagic) $ Left "invalid manifest magic"
|
|
let rest = BS.drop 8 bs
|
|
(major, rest') <- decode16be "major" rest
|
|
(minor, rest'') <- decode16be "minor" rest'
|
|
when (major /= manifestMajorVersion) $
|
|
Left $ "unsupported manifest major version: " ++ show major
|
|
when (minor /= manifestMinorVersion) $
|
|
Left $ "unsupported manifest minor version: " ++ show minor
|
|
|
|
(schema, r1) <- decodeLengthPrefixedText rest''
|
|
(bundleType, r2) <- decodeLengthPrefixedText r1
|
|
(calc, r3) <- decodeLengthPrefixedText r2
|
|
(alg, r4) <- decodeLengthPrefixedText r3
|
|
(domain, r5) <- decodeLengthPrefixedText r4
|
|
(payload, r6) <- decodeLengthPrefixedText r5
|
|
(sem, r7) <- decodeLengthPrefixedText r6
|
|
(eval, r8) <- decodeLengthPrefixedText r7
|
|
(abi, r9) <- decodeLengthPrefixedText r8
|
|
|
|
(capCount, r10) <- decode32be "capability_count" r9
|
|
(caps, r11) <- decodeCapabilities (fromIntegral capCount) r10
|
|
|
|
when (BS.length r11 < 1) $ Left "manifest truncated: missing closure byte"
|
|
let (closureByte, r12) = BS.splitAt 1 r11
|
|
closure <- closureFromByte (head $ BS.unpack closureByte)
|
|
|
|
(rootCount, r13) <- decode32be "root_count" r12
|
|
(roots, r14) <- decodeRoots (fromIntegral rootCount) r13
|
|
|
|
(exportCount, r15) <- decode32be "export_count" r14
|
|
(exports, r16) <- decodeExports (fromIntegral exportCount) r15
|
|
|
|
(metadata, _ext) <- decodeMetadataAndExtensions r16
|
|
|
|
pure BundleManifest
|
|
{ manifestSchema = schema
|
|
, manifestBundleType = bundleType
|
|
, manifestTree = TreeSpec
|
|
{ treeCalculus = calc
|
|
, treeNodeHash = NodeHashSpec
|
|
{ nodeHashAlgorithm = alg
|
|
, nodeHashDomain = domain
|
|
}
|
|
, treeNodePayload = payload
|
|
}
|
|
, manifestRuntime = RuntimeSpec
|
|
{ runtimeSemantics = sem
|
|
, runtimeEvaluation = eval
|
|
, runtimeAbi = abi
|
|
, runtimeCapabilities = caps
|
|
}
|
|
, manifestClosure = closure
|
|
, manifestRoots = roots
|
|
, manifestExports = exports
|
|
, manifestMetadata = metadata
|
|
}
|
|
|
|
decodeCapabilities :: Int -> ByteString -> Either String ([Text], ByteString)
|
|
decodeCapabilities 0 bs = Right ([], bs)
|
|
decodeCapabilities n bs = do
|
|
(txt, rest) <- decodeLengthPrefixedText bs
|
|
(restTxts, restFinal) <- decodeCapabilities (n - 1) rest
|
|
Right (txt : restTxts, restFinal)
|
|
|
|
decodeRoots :: Int -> ByteString -> Either String ([BundleRoot], ByteString)
|
|
decodeRoots 0 bs = Right ([], bs)
|
|
decodeRoots n bs = do
|
|
(idx, rest1) <- decode32be "root_index" bs
|
|
(role, rest2) <- decodeLengthPrefixedText rest1
|
|
(restRoots, restFinal) <- decodeRoots (n - 1) rest2
|
|
Right (BundleRoot idx role : restRoots, restFinal)
|
|
|
|
decodeExports :: Int -> ByteString -> Either String ([BundleExport], ByteString)
|
|
decodeExports 0 bs = Right ([], bs)
|
|
decodeExports n bs = do
|
|
(name, r1) <- decodeLengthPrefixedText bs
|
|
(idx, r2) <- decode32be "export_root" r1
|
|
(kind, r3) <- decodeLengthPrefixedText r2
|
|
(abi, r4) <- decodeLengthPrefixedText r3
|
|
(restExports, restFinal) <- decodeExports (n - 1) r4
|
|
Right (BundleExport name idx kind abi : restExports, restFinal)
|
|
|
|
decodeMetadataAndExtensions :: ByteString -> Either String (BundleMetadata, ByteString)
|
|
decodeMetadataAndExtensions bs = do
|
|
(metadataCount, rest1) <- decode32be "metadata_field_count" bs
|
|
(metadataTlvs, rest2) <- decodeTLVs (fromIntegral metadataCount) rest1
|
|
metadata <- decodeMetadataTLVs metadataTlvs
|
|
(extensionCount, rest3) <- decode32be "extension_field_count" rest2
|
|
(_extensionTlvs, rest4) <- decodeTLVs (fromIntegral extensionCount) rest3
|
|
unless (BS.null rest4) $ Left "trailing bytes after manifest TLV tail"
|
|
Right (metadata, rest4)
|
|
|
|
decodeTLVs :: Int -> ByteString -> Either String ([TLVEntry], ByteString)
|
|
decodeTLVs 0 bs = Right ([], bs)
|
|
decodeTLVs n bs = do
|
|
(tag, r1) <- decode16be "tlv_tag" bs
|
|
(len, r2) <- decode32be "tlv_length" r1
|
|
let payloadLen = fromIntegral len
|
|
when (BS.length r2 < payloadLen) $ Left "TLV value extends beyond input"
|
|
let (value, after) = BS.splitAt payloadLen r2
|
|
(restTlvs, restFinal) <- decodeTLVs (n - 1) after
|
|
Right ((tag, value) : restTlvs, restFinal)
|
|
|
|
decodeMetadataTLVs :: [(Word16, ByteString)] -> Either String BundleMetadata
|
|
decodeMetadataTLVs tlvs = do
|
|
pkg <- lookupText tagPackage
|
|
ver <- lookupText tagVersion
|
|
desc <- lookupText tagDescription
|
|
lic <- lookupText tagLicense
|
|
by <- lookupText tagCreatedBy
|
|
pure BundleMetadata
|
|
{ metadataPackage = pkg
|
|
, metadataVersion = ver
|
|
, metadataDescription = desc
|
|
, metadataLicense = lic
|
|
, metadataCreatedBy = by
|
|
}
|
|
where
|
|
lookupTag t = go t tlvs
|
|
go _ [] = Nothing
|
|
go t ((tag, val):rest)
|
|
| tag == t = Just val
|
|
| otherwise = go t rest
|
|
lookupText tag =
|
|
case lookupTag tag of
|
|
Nothing -> Right Nothing
|
|
Just raw -> case decodeUtf8' raw of
|
|
Right txt -> Right (Just txt)
|
|
Left _ -> Left $ "metadata TLV has invalid UTF-8 for tag " ++ show tag
|
|
|
|
type TLVEntry = (Word16, ByteString)
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Data types
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
data ClosureMode = ClosureComplete | ClosurePartial
|
|
deriving (Show, Eq, Ord, Generic)
|
|
|
|
data NodeHashSpec = NodeHashSpec
|
|
{ nodeHashAlgorithm :: Text
|
|
, nodeHashDomain :: Text
|
|
} deriving (Show, Eq, Ord, Generic)
|
|
|
|
data TreeSpec = TreeSpec
|
|
{ treeCalculus :: Text
|
|
, treeNodeHash :: NodeHashSpec
|
|
, treeNodePayload :: Text
|
|
} deriving (Show, Eq, Ord, Generic)
|
|
|
|
data RuntimeSpec = RuntimeSpec
|
|
{ runtimeSemantics :: Text
|
|
, runtimeEvaluation :: Text
|
|
, runtimeAbi :: Text
|
|
, runtimeCapabilities :: [Text]
|
|
} deriving (Show, Eq, Ord, Generic)
|
|
|
|
data BundleRoot = BundleRoot
|
|
{ rootIndex :: Word32
|
|
, rootRole :: Text
|
|
} deriving (Show, Eq, Ord, Generic)
|
|
|
|
data BundleExport = BundleExport
|
|
{ exportName :: Text
|
|
, exportRoot :: Word32
|
|
, exportKind :: Text
|
|
, exportAbi :: Text
|
|
} deriving (Show, Eq, Ord, Generic)
|
|
|
|
data BundleMetadata = BundleMetadata
|
|
{ metadataPackage :: Maybe Text
|
|
, metadataVersion :: Maybe Text
|
|
, metadataDescription :: Maybe Text
|
|
, metadataLicense :: Maybe Text
|
|
, metadataCreatedBy :: Maybe Text
|
|
} deriving (Show, Eq, Ord, Generic)
|
|
|
|
data BundleManifest = BundleManifest
|
|
{ manifestSchema :: Text
|
|
, manifestBundleType :: Text
|
|
, manifestTree :: TreeSpec
|
|
, manifestRuntime :: RuntimeSpec
|
|
, manifestClosure :: ClosureMode
|
|
, manifestRoots :: [BundleRoot]
|
|
, manifestExports :: [BundleExport]
|
|
, manifestMetadata :: BundleMetadata
|
|
} deriving (Show, Eq, Generic)
|
|
|
|
data BundleNode
|
|
= BNLeaf
|
|
| BNStem !Word32
|
|
| BNFork !Word32 !Word32
|
|
deriving (Show, Eq)
|
|
|
|
data Bundle = Bundle
|
|
{ bundleVersion :: Word16
|
|
, bundleRoots :: [Word32]
|
|
, bundleNodes :: Seq BundleNode
|
|
, bundleManifest :: BundleManifest
|
|
, bundleManifestBytes :: ByteString
|
|
} deriving (Show, Eq)
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Bundle construction
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
data NodeKey = KeyLeaf | KeyStem !Word32 | KeyFork !Word32 !Word32
|
|
deriving (Eq, Ord, Show)
|
|
|
|
buildBundle :: [(Text, T)] -> Bundle
|
|
buildBundle namedTerms =
|
|
let go :: T -> (Seq BundleNode, Map NodeKey Word32) -> (Word32, (Seq BundleNode, Map NodeKey Word32))
|
|
go Leaf (nodes, seen) =
|
|
case Map.lookup KeyLeaf seen of
|
|
Just idx -> (idx, (nodes, seen))
|
|
Nothing ->
|
|
let idx = fromIntegral (Seq.length nodes)
|
|
in (idx, (nodes |> BNLeaf, Map.insert KeyLeaf idx seen))
|
|
go (Stem child) (nodes, seen) =
|
|
let (childIdx, state1) = go child (nodes, seen)
|
|
(nodes1, seen1) = state1
|
|
in case Map.lookup (KeyStem childIdx) seen1 of
|
|
Just idx -> (idx, state1)
|
|
Nothing ->
|
|
let idx = fromIntegral (Seq.length nodes1)
|
|
in (idx, (nodes1 |> BNStem childIdx, Map.insert (KeyStem childIdx) idx seen1))
|
|
go (Fork left right) (nodes, seen) =
|
|
let (leftIdx, state1) = go left (nodes, seen)
|
|
(rightIdx, state2) = go right state1
|
|
(nodes2, seen2) = state2
|
|
in case Map.lookup (KeyFork leftIdx rightIdx) seen2 of
|
|
Just idx -> (idx, state2)
|
|
Nothing ->
|
|
let idx = fromIntegral (Seq.length nodes2)
|
|
in (idx, (nodes2 |> BNFork leftIdx rightIdx, Map.insert (KeyFork leftIdx rightIdx) idx seen2))
|
|
|
|
processExport state (_, t) = let (idx, newState) = go t state in (newState, idx)
|
|
((finalNodes, _), rootIndices) = mapAccumL processExport (Seq.empty, Map.empty) namedTerms
|
|
|
|
roots = zipWith mkRoot [0 :: Int ..] rootIndices
|
|
exports = zipWith mkExport namedTerms rootIndices
|
|
manifest = makeManifest roots exports
|
|
manifestBytes = encodeManifest manifest
|
|
in Bundle
|
|
{ bundleVersion = bundleMajorVersion * 1000 + bundleMinorVersion
|
|
, bundleRoots = rootIndices
|
|
, bundleNodes = finalNodes
|
|
, bundleManifest = manifest
|
|
, bundleManifestBytes = manifestBytes
|
|
}
|
|
where
|
|
mkRoot 0 idx = BundleRoot idx "default"
|
|
mkRoot _ idx = BundleRoot idx "root"
|
|
mkExport (name, _) idx = BundleExport name idx "term" "arboricx.abi.tree.v1"
|
|
|
|
makeManifest :: [BundleRoot] -> [BundleExport] -> BundleManifest
|
|
makeManifest roots exports = BundleManifest
|
|
{ manifestSchema = "arboricx.bundle.manifest.v1"
|
|
, manifestBundleType = "tree-calculus-executable-object"
|
|
, manifestTree = TreeSpec
|
|
{ treeCalculus = "tree-calculus.v1"
|
|
, treeNodeHash = NodeHashSpec
|
|
{ nodeHashAlgorithm = "indexed"
|
|
, nodeHashDomain = "arboricx.indexed.node.v1"
|
|
}
|
|
, treeNodePayload = "arboricx.indexed.payload.v1"
|
|
}
|
|
, manifestRuntime = RuntimeSpec
|
|
{ runtimeSemantics = "tree-calculus.v1"
|
|
, runtimeEvaluation = "normal-order"
|
|
, runtimeAbi = "arboricx.abi.tree.v1"
|
|
, runtimeCapabilities = []
|
|
}
|
|
, manifestClosure = ClosureComplete
|
|
, manifestRoots = roots
|
|
, manifestExports = exports
|
|
, manifestMetadata = BundleMetadata
|
|
{ metadataPackage = Nothing
|
|
, metadataVersion = Nothing
|
|
, metadataDescription = Nothing
|
|
, metadataLicense = Nothing
|
|
, metadataCreatedBy = Just "arboricx"
|
|
}
|
|
}
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Bundle encoding / decoding
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
encodeBundle :: Bundle -> ByteString
|
|
encodeBundle bundle =
|
|
let nodeSection = encodeNodeSection (bundleNodes bundle)
|
|
manifestBytes = bundleManifestBytes bundle
|
|
sectionCount = 2
|
|
dirOffset = fromIntegral headerLength
|
|
sectionDirLength = sectionCount * sectionEntryLength
|
|
manifestOffset = fromIntegral (headerLength + sectionDirLength)
|
|
nodesOffset = manifestOffset + fromIntegral (BS.length manifestBytes)
|
|
manifestEntry = encodeSectionEntry sectionManifest 1 flagCritical compressionNone
|
|
manifestOffset (fromIntegral $ BS.length manifestBytes)
|
|
nodesEntry = encodeSectionEntry sectionNodes 1 flagCritical compressionNone
|
|
nodesOffset (fromIntegral $ BS.length nodeSection)
|
|
header = encodeHeader bundleMajorVersion bundleMinorVersion
|
|
(fromIntegral sectionCount) 0 dirOffset
|
|
in header <> manifestEntry <> nodesEntry <> manifestBytes <> nodeSection
|
|
|
|
decodeBundle :: ByteString -> Either String Bundle
|
|
decodeBundle bs
|
|
| BS.take (BS.length bundleMagic) bs /= bundleMagic = Left "invalid magic"
|
|
| otherwise = do
|
|
(major, minor, sectionCount, _flags, dirOffset) <- decodePortableHeader bs
|
|
when (major /= bundleMajorVersion) $
|
|
Left $ "unsupported bundle major version: " ++ show major
|
|
let dirStart = fromIntegral dirOffset
|
|
dirBytes = fromIntegral sectionCount * sectionEntryLength
|
|
when (BS.length bs < dirStart + dirBytes) $
|
|
Left "bundle truncated in section directory"
|
|
let dirRaw = BS.take dirBytes $ BS.drop dirStart bs
|
|
entries <- decodeSectionEntries sectionCount dirRaw
|
|
traverse_ rejectUnknownCritical entries
|
|
manifestEntry <- requireSection sectionManifest entries
|
|
nodesEntry <- requireSection sectionNodes entries
|
|
manifestBytes <- readAndVerifySection bs manifestEntry
|
|
nodesBytes <- readAndVerifySection bs nodesEntry
|
|
manifest <- decodeManifest manifestBytes
|
|
when (treeNodePayload (manifestTree manifest) /= "arboricx.indexed.payload.v1") $
|
|
Left "manifest does not use indexed payload"
|
|
nodes <- decodeNodeSection nodesBytes
|
|
let rootIndices = map rootIndex (manifestRoots manifest)
|
|
return Bundle
|
|
{ bundleVersion = major * 1000 + minor
|
|
, bundleRoots = rootIndices
|
|
, bundleNodes = nodes
|
|
, bundleManifest = manifest
|
|
, bundleManifestBytes = manifestBytes
|
|
}
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Container encoding / decoding
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
data SectionEntry = SectionEntry
|
|
{ seType :: Word32
|
|
, seVersion :: Word16
|
|
, seFlags :: Word16
|
|
, seCompression :: Word16
|
|
, seOffset :: Word64
|
|
, seLength :: Word64
|
|
} deriving (Show, Eq)
|
|
|
|
encodeHeader :: Word16 -> Word16 -> Word32 -> Word64 -> Word64 -> ByteString
|
|
encodeHeader major minor sectionCount flags dirOffset =
|
|
bundleMagic
|
|
<> encode16 major
|
|
<> encode16 minor
|
|
<> encode32 sectionCount
|
|
<> encode64 flags
|
|
<> encode64 dirOffset
|
|
|
|
encodeSectionEntry :: Word32 -> Word16 -> Word16 -> Word16 -> Word64 -> Word64 -> ByteString
|
|
encodeSectionEntry sectionType sectionVersion sectionFlags compression offset lengthBytes =
|
|
encode32 sectionType
|
|
<> encode16 sectionVersion
|
|
<> encode16 sectionFlags
|
|
<> encode16 compression
|
|
<> encode16 0 -- reserved
|
|
<> encode64 offset
|
|
<> encode64 lengthBytes
|
|
<> encode32 0 -- reserved padding
|
|
|
|
decodePortableHeader :: ByteString -> Either String (Word16, Word16, Word32, Word64, Word64)
|
|
decodePortableHeader bs
|
|
| BS.length bs < headerLength = Left "bundle too short for header"
|
|
| BS.take 8 bs /= bundleMagic = Left "invalid portable bundle magic"
|
|
| otherwise = do
|
|
(major, r1) <- decode16be "major_version" (BS.drop 8 bs)
|
|
(minor, r2) <- decode16be "minor_version" r1
|
|
(sectionCount, r3) <- decode32be "section_count" r2
|
|
(flags, r4) <- decode64be "flags" r3
|
|
(dirOffset, _) <- decode64be "directory_offset" r4
|
|
Right (major, minor, sectionCount, flags, dirOffset)
|
|
|
|
decodeSectionEntries :: Word32 -> ByteString -> Either String [SectionEntry]
|
|
decodeSectionEntries count bytes = reverse <$> go count bytes []
|
|
where
|
|
go 0 _ acc = Right acc
|
|
go n bs acc = do
|
|
when (BS.length bs < sectionEntryLength) $
|
|
Left "section directory truncated"
|
|
(sectionType, r1) <- decode32be "section_type" bs
|
|
(sectionVersion, r2) <- decode16be "section_version" r1
|
|
(sectionFlags, r3) <- decode16be "section_flags" r2
|
|
(compression, r4) <- decode16be "compression_codec" r3
|
|
(_reserved, r5) <- decode16be "reserved" r4
|
|
(offset, r6) <- decode64be "section_offset" r5
|
|
(len, r7) <- decode64be "section_length" r6
|
|
(_reserved2, rest) <- decode32be "reserved" r7
|
|
let entry = SectionEntry sectionType sectionVersion sectionFlags compression offset len
|
|
go (n - 1) rest (entry : acc)
|
|
|
|
rejectUnknownCritical :: SectionEntry -> Either String ()
|
|
rejectUnknownCritical entry =
|
|
let known = seType entry `elem` [sectionManifest, sectionNodes]
|
|
critical = seFlags entry .&. flagCritical /= 0
|
|
in when (critical && not known) $
|
|
Left $ "unknown critical section type: " ++ show (seType entry)
|
|
|
|
requireSection :: Word32 -> [SectionEntry] -> Either String SectionEntry
|
|
requireSection sectionType entries =
|
|
case filter ((== sectionType) . seType) entries of
|
|
[entry] -> Right entry
|
|
[] -> Left $ "missing required section type: " ++ show sectionType
|
|
_ -> Left $ "duplicate section type: " ++ show sectionType
|
|
|
|
readAndVerifySection :: ByteString -> SectionEntry -> Either String ByteString
|
|
readAndVerifySection bs entry = do
|
|
when (seCompression entry /= compressionNone) $
|
|
Left $ "unsupported compression codec in section " ++ show (seType entry)
|
|
let offset = fromIntegral (seOffset entry)
|
|
len = fromIntegral (seLength entry)
|
|
when (offset < 0 || len < 0 || BS.length bs < offset + len) $
|
|
Left $ "section extends beyond bundle end: " ++ show (seType entry)
|
|
Right $ BS.take len $ BS.drop offset bs
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Node section encoding / decoding
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
serializeBundleNode :: BundleNode -> ByteString
|
|
serializeBundleNode BNLeaf = BS.pack [0x00]
|
|
serializeBundleNode (BNStem child) = BS.pack [0x01] <> encode32 child
|
|
serializeBundleNode (BNFork left right) = BS.pack [0x02] <> encode32 left <> encode32 right
|
|
|
|
encodeNodeSection :: Seq BundleNode -> ByteString
|
|
encodeNodeSection nodes =
|
|
encode64 (fromIntegral $ Seq.length nodes)
|
|
<> foldMap encodeNodeEntry nodes
|
|
where
|
|
encodeNodeEntry node =
|
|
let payload = serializeBundleNode node
|
|
in encode32 (fromIntegral $ BS.length payload) <> payload
|
|
|
|
decodeNodeSection :: ByteString -> Either String (Seq BundleNode)
|
|
decodeNodeSection bs = do
|
|
(nodeCount, rest) <- decode64be "node_count" bs
|
|
decodeNodeEntries nodeCount rest
|
|
|
|
decodeNodeEntries :: Word64 -> ByteString -> Either String (Seq BundleNode)
|
|
decodeNodeEntries count bs = go count bs Seq.empty
|
|
where
|
|
go 0 rest acc
|
|
| BS.null rest = Right acc
|
|
| otherwise = Left "trailing bytes after node section"
|
|
go n bytes acc
|
|
| BS.length bytes < 4 =
|
|
Left "not enough bytes for node entry length"
|
|
| otherwise = do
|
|
(plen, rest) <- decode32be "payload_len" bytes
|
|
let payloadLen = fromIntegral plen
|
|
if BS.length rest < payloadLen
|
|
then Left "payload extends beyond node section end"
|
|
else do
|
|
let (payload, after) = BS.splitAt payloadLen rest
|
|
node <- deserializeBundleNode payload
|
|
go (n - 1) after (acc |> node)
|
|
|
|
deserializeBundleNode :: ByteString -> Either String BundleNode
|
|
deserializeBundleNode payload =
|
|
case BS.uncons payload of
|
|
Just (0x00, rest)
|
|
| BS.null rest -> Right BNLeaf
|
|
| otherwise -> Left "invalid leaf payload length"
|
|
Just (0x01, rest)
|
|
| BS.length rest == 4 -> Right $ BNStem (decodeU32 rest)
|
|
| otherwise -> Left "invalid stem payload length"
|
|
Just (0x02, rest)
|
|
| BS.length rest == 8 ->
|
|
let (leftBytes, rightBytes) = BS.splitAt 4 rest
|
|
in Right $ BNFork (decodeU32 leftBytes) (decodeU32 rightBytes)
|
|
| otherwise -> Left "invalid fork payload length"
|
|
_ -> Left "invalid node payload"
|
|
|
|
decodeU32 :: ByteString -> Word32
|
|
decodeU32 bs =
|
|
let b0 = fromIntegral (BS.index bs 0) :: Word32
|
|
b1 = fromIntegral (BS.index bs 1) :: Word32
|
|
b2 = fromIntegral (BS.index bs 2) :: Word32
|
|
b3 = fromIntegral (BS.index bs 3) :: Word32
|
|
in (b0 `shiftL` 24) .|. (b1 `shiftL` 16) .|. (b2 `shiftL` 8) .|. b3
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Bundle verification
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
verifyBundle :: Bundle -> Either String ()
|
|
verifyBundle bundle
|
|
| bundleVersion bundle < 1 = Left $ "unsupported bundle version: " ++ show (bundleVersion bundle)
|
|
| Seq.null (bundleNodes bundle) = Left "bundle has no nodes"
|
|
verifyBundle bundle = do
|
|
verifyManifestConstraints (bundleManifest bundle)
|
|
let nodeCount = fromIntegral $ Seq.length (bundleNodes bundle)
|
|
traverse_ (\idx -> when (idx >= nodeCount) $ Left $ "root index out of bounds: " ++ show idx)
|
|
(bundleRoots bundle)
|
|
traverse_ (\exp -> when (exportRoot exp >= nodeCount) $ Left $ "export index out of bounds: " ++ show (exportRoot exp))
|
|
(manifestExports $ bundleManifest bundle)
|
|
|
|
let verifyNode i node = case node of
|
|
BNLeaf -> Right ()
|
|
BNStem child -> do
|
|
when (child >= i) $ Left $ "stem at index " ++ show i ++ " references child " ++ show child
|
|
when (child >= nodeCount) $ Left $ "stem at index " ++ show i ++ " references child out of bounds"
|
|
Right ()
|
|
BNFork left right -> do
|
|
when (left >= i) $ Left $ "fork at index " ++ show i ++ " references left " ++ show left
|
|
when (right >= i) $ Left $ "fork at index " ++ show i ++ " references right " ++ show right
|
|
when (left >= nodeCount) $ Left $ "fork at index " ++ show i ++ " references left out of bounds"
|
|
when (right >= nodeCount) $ Left $ "fork at index " ++ show i ++ " references right out of bounds"
|
|
Right ()
|
|
|
|
mapM_ (\i -> case Seq.lookup (fromIntegral i) (bundleNodes bundle) of
|
|
Nothing -> Left $ "internal error: node " ++ show i ++ " not found"
|
|
Just node -> verifyNode i node) [0 :: Word32 .. nodeCount - 1]
|
|
|
|
let dupCheck = foldM (\seen (i, node) -> case node of
|
|
BNLeaf -> if Set.member (0 :: Word8, 0 :: Word32, 0 :: Word32) seen
|
|
then Left $ "duplicate leaf at index " ++ show i
|
|
else Right $ Set.insert (0, 0, 0) seen
|
|
BNStem child -> if Set.member (1, child, 0) seen
|
|
then Left $ "duplicate stem at index " ++ show i
|
|
else Right $ Set.insert (1, child, 0) seen
|
|
BNFork left right -> if Set.member (2, left, right) seen
|
|
then Left $ "duplicate fork at index " ++ show i
|
|
else Right $ Set.insert (2, left, right) seen) Set.empty (zip [0 :: Word32 ..] (Foldable.toList $ bundleNodes bundle))
|
|
_ <- dupCheck
|
|
Right ()
|
|
|
|
verifyManifestConstraints :: BundleManifest -> Either String ()
|
|
verifyManifestConstraints manifest = do
|
|
when (manifestSchema manifest /= "arboricx.bundle.manifest.v1") $
|
|
Left $ "unsupported manifest schema: " ++ unpack (manifestSchema manifest)
|
|
when (manifestBundleType manifest /= "tree-calculus-executable-object") $
|
|
Left $ "unsupported bundle type: " ++ unpack (manifestBundleType manifest)
|
|
let treeSpec = manifestTree manifest
|
|
hashSpec = treeNodeHash treeSpec
|
|
runtimeSpec = manifestRuntime manifest
|
|
when (treeCalculus treeSpec /= "tree-calculus.v1") $
|
|
Left $ "unsupported calculus: " ++ unpack (treeCalculus treeSpec)
|
|
when (nodeHashAlgorithm hashSpec /= "indexed") $
|
|
Left $ "unsupported node hash algorithm: " ++ unpack (nodeHashAlgorithm hashSpec)
|
|
when (nodeHashDomain hashSpec /= "arboricx.indexed.node.v1") $
|
|
Left $ "unsupported node hash domain: " ++ unpack (nodeHashDomain hashSpec)
|
|
when (treeNodePayload treeSpec /= "arboricx.indexed.payload.v1") $
|
|
Left $ "unsupported node payload: " ++ unpack (treeNodePayload treeSpec)
|
|
when (runtimeSemantics runtimeSpec /= "tree-calculus.v1") $
|
|
Left $ "unsupported runtime semantics: " ++ unpack (runtimeSemantics runtimeSpec)
|
|
when (runtimeAbi runtimeSpec /= "arboricx.abi.tree.v1") $
|
|
Left $ "unsupported runtime ABI: " ++ unpack (runtimeAbi runtimeSpec)
|
|
when (not (null (runtimeCapabilities runtimeSpec))) $
|
|
Left "unsupported runtime capabilities"
|
|
when (manifestClosure manifest /= ClosureComplete) $
|
|
Left "bundle requires closure = complete"
|
|
when (null $ manifestRoots manifest) $
|
|
Left "manifest has no roots"
|
|
when (null $ manifestExports manifest) $
|
|
Left "manifest has no exports"
|
|
traverse_ verifyExport (manifestExports manifest)
|
|
where
|
|
verifyExport exported = do
|
|
when (T.null $ exportName exported) $
|
|
Left "manifest export has empty name"
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Bundle reconstruction
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
reconstructBundleTerms :: Seq BundleNode -> Vector T
|
|
reconstructBundleTerms nodes = V.create $ do
|
|
let n = Seq.length nodes
|
|
vec <- MV.new n
|
|
forM_ (zip [0 :: Int ..] (Foldable.toList nodes)) $ \(i, node) -> do
|
|
t <- case node of
|
|
BNLeaf -> return Leaf
|
|
BNStem child -> Stem <$> MV.read vec (fromIntegral child)
|
|
BNFork left right -> do
|
|
l <- MV.read vec (fromIntegral left)
|
|
r <- MV.read vec (fromIntegral right)
|
|
return $ Fork l r
|
|
MV.write vec i t
|
|
return vec
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Primitive binary helpers
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
encode16 :: Word16 -> ByteString
|
|
encode16 w = BS.pack
|
|
[ fromIntegral (shiftR w 8)
|
|
, fromIntegral w
|
|
]
|
|
|
|
encode32 :: Word32 -> ByteString
|
|
encode32 w = BS.pack
|
|
[ fromIntegral (shiftR w 24)
|
|
, fromIntegral (shiftR w 16)
|
|
, fromIntegral (shiftR w 8)
|
|
, fromIntegral w
|
|
]
|
|
|
|
encode64 :: Word64 -> ByteString
|
|
encode64 w = BS.pack
|
|
[ fromIntegral (shiftR w 56)
|
|
, fromIntegral (shiftR w 48)
|
|
, fromIntegral (shiftR w 40)
|
|
, fromIntegral (shiftR w 32)
|
|
, fromIntegral (shiftR w 24)
|
|
, fromIntegral (shiftR w 16)
|
|
, fromIntegral (shiftR w 8)
|
|
, fromIntegral w
|
|
]
|
|
|
|
decode16be :: String -> ByteString -> Either String (Word16, ByteString)
|
|
decode16be label bs
|
|
| BS.length bs < 2 = Left (label ++ ": not enough bytes for u16")
|
|
| otherwise =
|
|
let b0 = fromIntegral (BS.index bs 0) :: Word16
|
|
b1 = fromIntegral (BS.index bs 1) :: Word16
|
|
in Right ((b0 `shiftL` 8) .|. b1, BS.drop 2 bs)
|
|
|
|
decode32be :: String -> ByteString -> Either String (Word32, ByteString)
|
|
decode32be label bs
|
|
| BS.length bs < 4 = Left (label ++ ": not enough bytes for u32")
|
|
| otherwise =
|
|
let b0 = fromIntegral (BS.index bs 0) :: Word32
|
|
b1 = fromIntegral (BS.index bs 1) :: Word32
|
|
b2 = fromIntegral (BS.index bs 2) :: Word32
|
|
b3 = fromIntegral (BS.index bs 3) :: Word32
|
|
in Right ((b0 `shiftL` 24) .|. (b1 `shiftL` 16) .|. (b2 `shiftL` 8) .|. b3, BS.drop 4 bs)
|
|
|
|
decode64be :: String -> ByteString -> Either String (Word64, ByteString)
|
|
decode64be label bs
|
|
| BS.length bs < 8 = Left (label ++ ": not enough bytes for u64")
|
|
| otherwise =
|
|
let b0 = fromIntegral (BS.index bs 0) :: Word64
|
|
b1 = fromIntegral (BS.index bs 1) :: Word64
|
|
b2 = fromIntegral (BS.index bs 2) :: Word64
|
|
b3 = fromIntegral (BS.index bs 3) :: Word64
|
|
b4 = fromIntegral (BS.index bs 4) :: Word64
|
|
b5 = fromIntegral (BS.index bs 5) :: Word64
|
|
b6 = fromIntegral (BS.index bs 6) :: Word64
|
|
b7 = fromIntegral (BS.index bs 7) :: Word64
|
|
in Right ((b0 `shiftL` 56) .|. (b1 `shiftL` 48) .|. (b2 `shiftL` 40) .|. (b3 `shiftL` 32)
|
|
.|. (b4 `shiftL` 24) .|. (b5 `shiftL` 16) .|. (b6 `shiftL` 8) .|. b7, BS.drop 8 bs)
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Helpers
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
defaultExportNames :: Int -> [Text]
|
|
defaultExportNames n =
|
|
case n of
|
|
0 -> []
|
|
1 -> ["root"]
|
|
_ -> ["root" <> T.pack (show i) | i <- [0 :: Int .. n - 1]]
|