cuddle-0.5.0.0: src/Codec/CBOR/Cuddle/CBOR/Validator.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -Wno-incomplete-patterns #-}
module Codec.CBOR.Cuddle.CBOR.Validator where
import Codec.CBOR.Cuddle.CDDL hiding (CDDL, Group, Rule)
import Codec.CBOR.Cuddle.CDDL.CTree
import Codec.CBOR.Cuddle.CDDL.CtlOp
import Codec.CBOR.Cuddle.CDDL.Postlude
import Codec.CBOR.Cuddle.CDDL.Resolve
import Codec.CBOR.Read
import Codec.CBOR.Term
import Control.Exception
import Control.Monad ((>=>))
import Control.Monad.Reader
import Data.Bifunctor
import Data.Bits hiding (And)
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BSL
import Data.Function ((&))
import Data.Functor ((<&>))
import Data.Functor.Identity
import Data.IntSet qualified as IS
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict qualified as Map
import Data.Maybe
import Data.Text qualified as T
import Data.Text.Lazy qualified as TL
import Data.Word
import GHC.Float
import System.Exit
import System.IO
import Text.Regex.TDFA
type CDDL = CTreeRoot' Identity MonoRef
type Rule = Node MonoRef
type ResolvedRule = CTree MonoRef
data CBORTermResult = CBORTermResult Term CDDLResult
deriving (Show)
data CDDLResult
= -- | The rule was valid
Valid Rule
| -- | All alternatives failed
ChoiceFail
-- | Rule we are trying
Rule
-- | The alternatives that arise from said rule
(NE.NonEmpty Rule)
-- | For each alternative, the result
(NE.NonEmpty (Rule, CDDLResult))
| -- | All expansions failed
--
-- An expansion is: Given a CBOR @TList@ of @N@ elements, we will expand the
-- rules in a list spec to match the number of items in the list.
ListExpansionFail
-- | Rule we are trying
Rule
-- | List of expansions of rules
[[Rule]]
-- | For each expansion, for each of the rules in the expansion, the result
[[(Rule, CBORTermResult)]]
| -- | All expansions failed
--
-- An expansion is: Given a CBOR @TMap@ of @N@ elements, we will expand the
-- rules in a map spec to match the number of items in the map.
MapExpansionFail
-- | Rule we are trying
Rule
-- | List of expansions
[[Rule]]
-- | A list of matched items @(key, value, rule)@ and the unmatched item
[([AMatchedItem], ANonMatchedItem)]
| -- | The rule was valid but the control failed
InvalidControl
-- | Control we are trying
Rule
-- | If it is a .cbor, the result of the underlying validation
(Maybe CBORTermResult)
| InvalidRule Rule
| -- | A tagged was invalid
InvalidTagged
-- | Rule we are trying
Rule
-- | Either the tag is wrong, or the contents are wrong
(Either Word64 CBORTermResult)
| -- | The rule we are trying is not applicable to the CBOR term
UnapplicableRule
-- | Rule we are trying
Rule
deriving (Show)
data ANonMatchedItem = ANonMatchedItem
{ anmiKey :: Term
, anmiValue :: Term
, anmiResults :: [Either (Rule, CDDLResult) (Rule, CDDLResult, CDDLResult)]
-- ^ For all the tried rules, either the key failed or the key succeeded and
-- the value failed
}
deriving (Show)
data AMatchedItem = AMatchedItem
{ amiKey :: Term
, amiValue :: Term
, amiRule :: Rule
}
deriving (Show)
--------------------------------------------------------------------------------
-- Main entry point
validateCBOR :: BS.ByteString -> Name -> CDDL -> IO ()
validateCBOR bs rule cddl =
( case validateCBOR' bs rule cddl of
ok@(CBORTermResult _ (Valid _)) -> do
putStrLn $ "Valid " ++ show ok
exitSuccess
err -> do
hPutStrLn stderr $ "Invalid " ++ show err
exitFailure
)
`catch` ( \(e :: PatternMatchFail) ->
putStrLn $
"You uncovered a path we thought was impossible! Please submit your CDDL and CBOR to `https://github.com/input-output-hk/cuddle/issues` for us to investigate\n"
<> displayException e
)
validateCBOR' ::
BS.ByteString -> Name -> CDDL -> CBORTermResult
validateCBOR' bs rule cddl@(CTreeRoot tree) =
case deserialiseFromBytes decodeTerm (BSL.fromStrict bs) of
Left e -> error $ show e
Right (rest, term) ->
if BSL.null rest
then runReader (validateTerm term (runIdentity $ tree Map.! rule)) cddl
else runReader (validateTerm (TBytes bs) (runIdentity $ tree Map.! rule)) cddl
--------------------------------------------------------------------------------
-- Terms
-- | Core function that validates a CBOR term to a particular rule of the CDDL
-- spec
validateTerm ::
MonadReader CDDL m =>
Term -> Rule -> m CBORTermResult
validateTerm term rule =
let f = case term of
TInt i -> validateInteger (fromIntegral i)
TInteger i -> validateInteger i
TBytes b -> validateBytes b
TBytesI b -> validateBytes (BSL.toStrict b)
TString s -> validateText s
TStringI s -> validateText (TL.toStrict s)
TList ts -> validateList ts
TListI ts -> validateList ts
TMap ts -> validateMap ts
TMapI ts -> validateMap ts
TTagged w t -> validateTagged w t
TBool b -> validateBool b
TNull -> validateNull
TSimple s -> validateSimple s
THalf h -> validateHalf h
TFloat h -> validateFloat h
TDouble d -> validateDouble d
in CBORTermResult term <$> f rule
--------------------------------------------------------------------------------
-- Ints and integers
-- | Validation of an Int or Integer. CBOR categorizes every integral in `TInt`
-- or `TInteger` but it can be the case that we are decoding something that is
-- expected to be a `Word64` even if we get a `TInt`.
--
-- > ghci> encodeWord64 15
-- > [TkInt 15]
-- > ghci> encodeWord64 maxBound
-- > [TkInteger 18446744073709551615]
--
-- For this reason, we cannot assume that bounds or literals are going to be
-- Ints, so we convert everything to Integer.
validateInteger ::
MonadReader CDDL m =>
Integer -> Rule -> m CDDLResult
validateInteger i rule =
($ rule) <$> do
getRule rule >>= \case
-- echo "C24101" | xxd -r -p - example.cbor
-- echo "foo = int" > a.cddl
-- cddl a.cddl validate example.cbor
--
-- but
--
-- echo "C249010000000000000000"| xxd -r -p - example.cbor
-- echo "foo = int" > a.cddl
-- cddl a.cddl validate example.cbor
--
-- and they are both bigints?
-- a = any
Postlude PTAny -> pure Valid
-- a = int
Postlude PTInt -> pure Valid
-- a = uint
Postlude PTUInt -> pure $ check (i >= 0)
-- a = nint
Postlude PTNInt -> pure $ check (i <= 0)
-- a = x
Literal (Value (VUInt i') _) -> pure $ check $ i == fromIntegral i'
-- a = -x
Literal (Value (VNInt i') _) -> pure $ check $ -i == fromIntegral i'
-- a = <big number>
Literal (Value (VBignum i') _) -> pure $ check $ i == i'
-- a = foo .ctrl bar
Control op tgt ctrl -> ctrlDispatch (validateInteger i) op tgt ctrl (controlInteger i)
-- a = foo / bar
Choice opts -> validateChoice (validateInteger i) opts
-- a = x..y
Range low high bound ->
((,) <$> getRule low <*> getRule high)
<&> check . \case
(Literal (Value (VUInt (fromIntegral -> n)) _), Literal (Value (VUInt (fromIntegral -> m)) _)) -> n <= i && range bound i m
(Literal (Value (VNInt (fromIntegral -> n)) _), Literal (Value (VUInt (fromIntegral -> m)) _)) -> -n <= i && range bound i m
(Literal (Value (VNInt (fromIntegral -> n)) _), Literal (Value (VNInt (fromIntegral -> m)) _)) -> -n <= i && range bound i (-m)
(Literal (Value VUInt {} _), Literal (Value VNInt {} _)) -> False
(Literal (Value (VBignum n) _), Literal (Value (VUInt (fromIntegral -> m)) _)) -> n <= i && range bound i m
(Literal (Value (VBignum n) _), Literal (Value (VNInt (fromIntegral -> m)) _)) -> n <= i && range bound i (-m)
(Literal (Value (VUInt (fromIntegral -> n)) _), Literal (Value (VBignum m) _)) -> n <= i && range bound i m
(Literal (Value (VNInt (fromIntegral -> n)) _), Literal (Value (VBignum m) _)) -> (-n) <= i && range bound i m
-- a = &(x, y, z)
Enum g ->
getRule g >>= \case
Group g' -> validateInteger i (MIt (Choice (NE.fromList g'))) <&> replaceRule
-- a = x: y
-- Note KV cannot appear on its own, but we will use this when validating
-- lists.
KV _ v _ -> validateInteger i v <&> replaceRule
Tag 2 (MIt (Postlude PTBytes)) -> pure Valid
Tag 3 (MIt (Postlude PTBytes)) -> pure Valid
_ -> pure UnapplicableRule
-- | Controls for an Integer
controlInteger ::
forall m. MonadReader CDDL m => Integer -> CtlOp -> Rule -> m (Either (Maybe CBORTermResult) ())
controlInteger i Size ctrl =
getRule ctrl <&> \case
Literal (Value (VUInt sz) _) ->
boolCtrl $ 0 <= i && i < 256 ^ sz
controlInteger i Bits ctrl = do
indices <-
getRule ctrl >>= \case
Literal (Value (VUInt i') _) -> pure [i']
Choice nodes -> getIndicesOfChoice nodes
Range ff tt incl -> getIndicesOfRange ff tt incl
Enum g -> getIndicesOfEnum g
pure $ boolCtrl $ go (IS.fromList (map fromIntegral indices)) i 0
where
go _ 0 _ = True
go indices n idx =
let bitSet = testBit n 0
allowed = not bitSet || IS.member idx indices
in (allowed && go indices (shiftR n 1) (idx + 1))
controlInteger i Lt ctrl =
getRule ctrl
<&> boolCtrl . \case
Literal (Value (VUInt i') _) -> i < fromIntegral i'
Literal (Value (VNInt i') _) -> i < -fromIntegral i'
Literal (Value (VBignum i') _) -> i < i'
controlInteger i Gt ctrl =
getRule ctrl
<&> boolCtrl . \case
Literal (Value (VUInt i') _) -> i > fromIntegral i'
Literal (Value (VNInt i') _) -> i > -fromIntegral i'
Literal (Value (VBignum i') _) -> i > i'
controlInteger i Le ctrl =
getRule ctrl
<&> boolCtrl . \case
Literal (Value (VUInt i') _) -> i <= fromIntegral i'
Literal (Value (VNInt i') _) -> i <= -fromIntegral i'
Literal (Value (VBignum i') _) -> i <= i'
controlInteger i Ge ctrl =
getRule ctrl
<&> boolCtrl . \case
Literal (Value (VUInt i') _) -> i >= fromIntegral i'
Literal (Value (VNInt i') _) -> i >= -fromIntegral i'
Literal (Value (VBignum i') _) -> i >= i'
controlInteger i Eq ctrl =
getRule ctrl
<&> boolCtrl . \case
Literal (Value (VUInt i') _) -> i == fromIntegral i'
Literal (Value (VNInt i') _) -> i == -fromIntegral i'
Literal (Value (VBignum i') _) -> i == i'
controlInteger i Ne ctrl =
getRule ctrl
<&> boolCtrl . \case
Literal (Value (VUInt i') _) -> i /= fromIntegral i'
Literal (Value (VNInt i') _) -> i /= -fromIntegral i'
Literal (Value (VBignum i') _) -> i /= i'
--------------------------------------------------------------------------------
-- Floating point (Float16, Float32, Float64)
--
-- As opposed to Integral types, there seems to be no ambiguity when encoding
-- and decoding floating-point numbers.
-- | Validating a `Float16`
validateHalf ::
MonadReader CDDL m =>
Float -> Rule -> m CDDLResult
validateHalf f rule =
($ rule) <$> do
getRule rule >>= \case
-- a = any
Postlude PTAny -> pure Valid
-- a = float16
Postlude PTHalf -> pure Valid
-- a = 0.5
Literal (Value (VFloat16 f') _) -> pure $ check $ f == f'
-- a = foo / bar
Choice opts -> validateChoice (validateHalf f) opts
-- a = foo .ctrl bar
Control op tgt ctrl -> ctrlDispatch (validateHalf f) op tgt ctrl (controlHalf f)
-- a = x..y
Range low high bound ->
((,) <$> getRule low <*> getRule high)
<&> check . \case
(Literal (Value (VFloat16 n) _), Literal (Value (VFloat16 m) _)) -> n <= f && range bound f m
_ -> pure UnapplicableRule
-- | Controls for `Float16`
controlHalf :: MonadReader CDDL m => Float -> CtlOp -> Rule -> m (Either (Maybe CBORTermResult) ())
controlHalf f Eq ctrl =
getRule ctrl
<&> boolCtrl . \case
Literal (Value (VFloat16 f') _) -> f == f'
controlHalf f Ne ctrl =
getRule ctrl
<&> boolCtrl . \case
Literal (Value (VFloat16 f') _) -> f /= f'
-- | Validating a `Float32`
validateFloat ::
MonadReader CDDL m =>
Float -> Rule -> m CDDLResult
validateFloat f rule =
($ rule) <$> do
getRule rule >>= \case
-- a = any
Postlude PTAny -> pure Valid
-- a = float32
Postlude PTFloat -> pure Valid
-- a = 0.000000005
-- TODO: it is unclear if smaller floats should also validate
Literal (Value (VFloat32 f') _) -> pure $ check $ f == f'
-- a = foo / bar
Choice opts -> validateChoice (validateFloat f) opts
-- a = foo .ctrl bar
Control op tgt ctrl -> ctrlDispatch (validateFloat f) op tgt ctrl (controlFloat f)
-- a = x..y
-- TODO it is unclear if this should mix floating point types too
Range low high bound ->
((,) <$> getRule low <*> getRule high)
<&> check . \case
(Literal (Value (VFloat16 n) _), Literal (Value (VFloat16 m) _)) -> n <= f && range bound f m
(Literal (Value (VFloat32 n) _), Literal (Value (VFloat32 m) _)) -> n <= f && range bound f m
_ -> pure UnapplicableRule
-- | Controls for `Float32`
controlFloat :: MonadReader CDDL m => Float -> CtlOp -> Rule -> m (Either (Maybe CBORTermResult) ())
controlFloat f Eq ctrl =
getRule ctrl
<&> boolCtrl . \case
Literal (Value (VFloat16 f') _) -> f == f'
Literal (Value (VFloat32 f') _) -> f == f'
controlFloat f Ne ctrl =
getRule ctrl
<&> boolCtrl . \case
Literal (Value (VFloat16 f') _) -> f /= f'
Literal (Value (VFloat32 f') _) -> f /= f'
-- | Validating a `Float64`
validateDouble ::
MonadReader CDDL m =>
Double -> Rule -> m CDDLResult
validateDouble f rule =
($ rule) <$> do
getRule rule >>= \case
-- a = any
Postlude PTAny -> pure Valid
-- a = float64
Postlude PTDouble -> pure Valid
-- a = 0.0000000000000000000000000000000000000000000005
-- TODO: it is unclear if smaller floats should also validate
Literal (Value (VFloat64 f') _) -> pure $ check $ f == f'
-- a = foo / bar
Choice opts -> validateChoice (validateDouble f) opts
-- a = foo .ctrl bar
Control op tgt ctrl -> ctrlDispatch (validateDouble f) op tgt ctrl (controlDouble f)
-- a = x..y
-- TODO it is unclear if this should mix floating point types too
Range low high bound ->
((,) <$> getRule low <*> getRule high)
<&> check . \case
(Literal (Value (VFloat16 (float2Double -> n)) _), Literal (Value (VFloat16 (float2Double -> m)) _)) -> n <= f && range bound f m
(Literal (Value (VFloat32 (float2Double -> n)) _), Literal (Value (VFloat32 (float2Double -> m)) _)) -> n <= f && range bound f m
(Literal (Value (VFloat64 n) _), Literal (Value (VFloat64 m) _)) -> n <= f && range bound f m
_ -> pure UnapplicableRule
-- | Controls for `Float64`
controlDouble ::
MonadReader CDDL m => Double -> CtlOp -> Rule -> m (Either (Maybe CBORTermResult) ())
controlDouble f Eq ctrl =
getRule ctrl
<&> boolCtrl . \case
Literal (Value (VFloat16 f') _) -> f == float2Double f'
Literal (Value (VFloat32 f') _) -> f == float2Double f'
Literal (Value (VFloat64 f') _) -> f == f'
controlDouble f Ne ctrl =
getRule ctrl
<&> boolCtrl . \case
Literal (Value (VFloat16 f') _) -> f /= float2Double f'
Literal (Value (VFloat32 f') _) -> f /= float2Double f'
Literal (Value (VFloat64 f') _) -> f /= f'
--------------------------------------------------------------------------------
-- Bool
-- | Validating a boolean
validateBool ::
MonadReader CDDL m =>
Bool -> Rule -> m CDDLResult
validateBool b rule =
($ rule) <$> do
getRule rule >>= \case
-- a = any
Postlude PTAny -> pure Valid
-- a = bool
Postlude PTBool -> pure Valid
-- a = true
Literal (Value (VBool b') _) -> pure $ check $ b == b'
-- a = foo .ctrl bar
Control op tgt ctrl -> ctrlDispatch (validateBool b) op tgt ctrl (controlBool b)
-- a = foo / bar
Choice opts -> validateChoice (validateBool b) opts
_ -> pure UnapplicableRule
-- | Controls for `Bool`
controlBool :: MonadReader CDDL m => Bool -> CtlOp -> Rule -> m (Either (Maybe CBORTermResult) ())
controlBool b Eq ctrl =
getRule ctrl
<&> boolCtrl . \case
Literal (Value (VBool b') _) -> b == b'
controlBool b Ne ctrl =
getRule ctrl
<&> boolCtrl . \case
Literal (Value (VBool b') _) -> b /= b'
--------------------------------------------------------------------------------
-- Simple
-- | Validating a `TSimple`. It is unclear if this is used for anything else than undefined.
validateSimple ::
MonadReader CDDL m =>
Word8 -> Rule -> m CDDLResult
validateSimple 23 rule =
($ rule) <$> do
getRule rule >>= \case
-- a = any
Postlude PTAny -> pure Valid
-- a = undefined
Postlude PTUndefined -> pure Valid
-- a = foo / bar
Choice opts -> validateChoice (validateSimple 23) opts
_ -> pure UnapplicableRule
validateSimple n _ = error $ "Found simple different to 23! please report this somewhere! Found: " <> show n
--------------------------------------------------------------------------------
-- Null/nil
-- | Validating nil
validateNull ::
MonadReader CDDL m => Rule -> m CDDLResult
validateNull rule =
($ rule) <$> do
getRule rule >>= \case
-- a = any
Postlude PTAny -> pure Valid
-- a = nil
Postlude PTNil -> pure Valid
Choice opts -> validateChoice validateNull opts
_ -> pure UnapplicableRule
--------------------------------------------------------------------------------
-- Bytes
-- | Validating a byte sequence
validateBytes ::
MonadReader CDDL m =>
BS.ByteString -> Rule -> m CDDLResult
validateBytes bs rule =
($ rule) <$> do
getRule rule >>= \case
-- a = any
Postlude PTAny -> pure Valid
-- a = bytes
Postlude PTBytes -> pure Valid
-- a = h'123456'
Literal (Value (VBytes bs') _) -> pure $ check $ bs == bs'
-- a = foo .ctrl bar
Control op tgt ctrl -> ctrlDispatch (validateBytes bs) op tgt ctrl (controlBytes bs)
-- a = foo / bar
Choice opts -> validateChoice (validateBytes bs) opts
_ -> pure UnapplicableRule
-- | Controls for byte strings
controlBytes ::
forall m.
MonadReader CDDL m => BS.ByteString -> CtlOp -> Rule -> m (Either (Maybe CBORTermResult) ())
controlBytes bs Size ctrl =
getRule ctrl >>= \case
Literal (Value (VUInt (fromIntegral -> sz)) _) -> pure $ boolCtrl $ BS.length bs == sz
Range low high bound ->
let i = BS.length bs
in ((,) <$> getRule low <*> getRule high)
<&> boolCtrl . \case
(Literal (Value (VUInt (fromIntegral -> n)) _), Literal (Value (VUInt (fromIntegral -> m)) _)) -> n <= i && range bound i m
(Literal (Value (VNInt (fromIntegral -> n)) _), Literal (Value (VUInt (fromIntegral -> m)) _)) -> -n <= i && range bound i m
(Literal (Value (VNInt (fromIntegral -> n)) _), Literal (Value (VNInt (fromIntegral -> m)) _)) -> -n <= i && range bound i (-m)
(Literal (Value VUInt {} _), Literal (Value VNInt {} _)) -> False
controlBytes bs Bits ctrl = do
indices <-
getRule ctrl >>= \case
Literal (Value (VUInt i') _) -> pure [i']
Choice nodes -> getIndicesOfChoice nodes
Range ff tt incl -> getIndicesOfRange ff tt incl
Enum g -> getIndicesOfEnum g
pure $ boolCtrl $ bitsControlCheck (map fromIntegral indices)
where
bitsControlCheck :: [Int] -> Bool
bitsControlCheck allowedBits =
let allowedSet = IS.fromList allowedBits
totalBits = BS.length bs * 8
isAllowedBit n =
let byteIndex = n `shiftR` 3
bitIndex = n .&. 7
in case BS.indexMaybe bs byteIndex of
Just byte -> not (testBit byte bitIndex) || IS.member n allowedSet
Nothing -> True
in all isAllowedBit [0 .. totalBits - 1]
controlBytes bs Cbor ctrl =
case deserialiseFromBytes decodeTerm (BSL.fromStrict bs) of
Right (BSL.null -> True, term) ->
validateTerm term ctrl >>= \case
CBORTermResult _ (Valid _) -> pure $ Right ()
err -> pure $ Left $ Just err
controlBytes bs Cborseq ctrl =
case deserialiseFromBytes decodeTerm (BSL.fromStrict (BS.snoc (BS.cons 0x9f bs) 0xff)) of
Right (BSL.null -> True, TListI terms) ->
validateTerm (TList terms) (MIt (Array [MIt (Occur ctrl OIZeroOrMore)])) >>= \case
CBORTermResult _ (Valid _) -> pure $ Right ()
CBORTermResult _ err -> error $ show err
--------------------------------------------------------------------------------
-- Text
-- | Validating text strings
validateText ::
MonadReader CDDL m =>
T.Text -> Rule -> m CDDLResult
validateText txt rule =
($ rule) <$> do
getRule rule >>= \case
-- a = any
Postlude PTAny -> pure Valid
-- a = text
Postlude PTText -> pure Valid
-- a = "foo"
Literal (Value (VText txt') _) -> pure $ check $ txt == txt'
-- a = foo .ctrl bar
Control op tgt ctrl -> ctrlDispatch (validateText txt) op tgt ctrl (controlText txt)
-- a = foo / bar
Choice opts -> validateChoice (validateText txt) opts
_ -> pure UnapplicableRule
-- | Controls for text strings
controlText :: MonadReader CDDL m => T.Text -> CtlOp -> Rule -> m (Either (Maybe CBORTermResult) ())
controlText bs Size ctrl =
getRule ctrl >>= \case
Literal (Value (VUInt (fromIntegral -> sz)) _) -> pure $ boolCtrl $ T.length bs == sz
Range ff tt bound ->
((,) <$> getRule ff <*> getRule tt)
<&> boolCtrl . \case
(Literal (Value (VUInt (fromIntegral -> n)) _), Literal (Value (VUInt (fromIntegral -> m)) _)) -> n <= T.length bs && range bound (T.length bs) m
(Literal (Value (VNInt (fromIntegral -> n)) _), Literal (Value (VUInt (fromIntegral -> m)) _)) -> -n <= T.length bs && range bound (T.length bs) m
(Literal (Value (VNInt (fromIntegral -> n)) _), Literal (Value (VNInt (fromIntegral -> m)) _)) -> -n <= T.length bs && range bound (T.length bs) (-m)
controlText s Regexp ctrl =
getRule ctrl
<&> boolCtrl . \case
Literal (Value (VText rxp) _) -> case s =~ rxp :: (T.Text, T.Text, T.Text) of
("", s', "") -> s == s'
--------------------------------------------------------------------------------
-- Tagged values
-- | Validating a `TTagged`
validateTagged ::
MonadReader CDDL m =>
Word64 -> Term -> Rule -> m CDDLResult
validateTagged tag term rule =
($ rule) <$> do
getRule rule >>= \case
Postlude PTAny -> pure Valid
Tag tag' rule' ->
-- If the tag does not match, this is a direct fail
if tag == tag'
then
ask >>= \cddl ->
case runReader (validateTerm term rule') cddl of
CBORTermResult _ (Valid _) -> pure Valid
err -> pure $ \r -> InvalidTagged r (Right err)
else pure $ \r -> InvalidTagged r (Left tag)
Choice opts -> validateChoice (validateTagged tag term) opts
_ -> pure UnapplicableRule
--------------------------------------------------------------------------------
-- Collection helpers
-- | Groups might contain enums, or unwraps inside. This resolves all those to
-- the top level of the group.
flattenGroup :: CDDL -> [Rule] -> [Rule]
flattenGroup cddl nodes =
mconcat
[ case resolveIfRef cddl rule of
Literal {} -> [rule]
Postlude {} -> [rule]
Map {} -> [rule]
Array {} -> [rule]
Choice {} -> [rule]
KV {} -> [rule]
Occur {} -> [rule]
Range {} -> [rule]
Control {} -> [rule]
Enum e -> case resolveIfRef cddl e of
Group g -> flattenGroup cddl g
_ -> error "Malformed cddl"
Unwrap g -> case resolveIfRef cddl g of
Map n -> flattenGroup cddl n
Array n -> flattenGroup cddl n
Tag _ n -> [n]
_ -> error "Malformed cddl"
Tag {} -> [rule]
Group g -> flattenGroup cddl g
| rule <- nodes
]
-- | Expand rules to reach exactly the wanted length, which must be the number
-- of items in the container. For example, if we want to validate 3 elements,
-- and we have the following CDDL:
--
-- > a = [* int, * bool]
--
-- this will be expanded to `[int, int, int], [int, int, bool], [int, bool,
-- bool], [bool, bool, bool]`.
--
-- Essentially the rules we will parse is the choice among the expansions of the
-- original rules.
expandRules :: Int -> [Rule] -> Reader CDDL [[Rule]]
expandRules remainingLen []
| remainingLen /= 0 = pure []
expandRules _ [] = pure [[]]
expandRules remainingLen _
| remainingLen < 0 = pure []
| remainingLen == 0 = pure [[]]
expandRules remainingLen (x : xs) = do
y <- expandRule remainingLen x
concat
<$> mapM
( \y' -> do
suffixes <- expandRules (remainingLen - length y') xs
pure [y' ++ ys' | ys' <- suffixes]
)
y
expandRule :: Int -> Rule -> Reader CDDL [[Rule]]
expandRule maxLen _
| maxLen < 0 = pure []
expandRule maxLen rule =
getRule rule >>= \case
Occur o OIOptional -> pure $ [] : [[o] | maxLen > 0]
Occur o OIZeroOrMore -> ([] :) <$> expandRule maxLen (MIt (Occur o OIOneOrMore))
Occur o OIOneOrMore ->
if maxLen > 0
then ([o] :) . map (o :) <$> expandRule (maxLen - 1) (MIt (Occur o OIOneOrMore))
else pure []
Occur o (OIBounded low high) -> case (low, high) of
(Nothing, Nothing) -> expandRule maxLen (MIt (Occur o OIZeroOrMore))
(Just (fromIntegral -> low'), Nothing) ->
if maxLen >= low'
then map (replicate low' o ++) <$> expandRule (maxLen - low') (MIt (Occur o OIZeroOrMore))
else pure []
(Nothing, Just (fromIntegral -> high')) ->
pure [replicate n o | n <- [0 .. min maxLen high']]
(Just (fromIntegral -> low'), Just (fromIntegral -> high')) ->
if maxLen >= low'
then pure [replicate n o | n <- [low' .. min maxLen high']]
else pure []
_ -> pure [[rule | maxLen > 0]]
-- | Which rules are optional?
isOptional :: MonadReader CDDL m => Rule -> m Bool
isOptional rule =
getRule rule
<&> \case
Occur _ OIOptional -> True
Occur _ OIZeroOrMore -> True
Occur _ (OIBounded Nothing _) -> True
Occur _ (OIBounded (Just 0) _) -> True
_ -> False
-- --------------------------------------------------------------------------------
-- -- Lists
validateListWithExpandedRules ::
forall m.
MonadReader CDDL m =>
[Term] -> [Rule] -> m [(Rule, CBORTermResult)]
validateListWithExpandedRules terms rules =
go (zip terms rules)
where
go ::
[(Term, Rule)] -> m [(Rule, CBORTermResult)]
go [] = pure []
go ((t, r) : ts) =
getRule r >>= \case
-- Should the rule be a KV, then we validate the rule for the value
KV _ v _ ->
-- We need to do this juggling because validateTerm has a different
-- error type
ask >>= \cddl ->
case runReader (validateTerm t v) cddl of
ok@(CBORTermResult _ (Valid _)) -> ((r, ok) :) <$> go ts
err -> pure [(r, err)]
_ ->
ask >>= \cddl ->
case runReader (validateTerm t r) cddl of
ok@(CBORTermResult _ (Valid _)) -> ((r, ok) :) <$> go ts
err -> pure [(r, err)]
validateExpandedList ::
forall m.
MonadReader CDDL m =>
[Term] ->
[[Rule]] ->
m (Rule -> CDDLResult)
validateExpandedList terms rules = go rules
where
go :: [[Rule]] -> m (Rule -> CDDLResult)
go [] = pure $ \r -> ListExpansionFail r rules []
go (choice : choices) = do
res <- validateListWithExpandedRules terms choice
case res of
[] -> pure Valid
_ -> case last res of
(_, CBORTermResult _ (Valid _)) -> pure Valid
_ ->
go choices
>>= ( \case
Valid _ -> pure Valid
ListExpansionFail _ _ errors -> pure $ \r -> ListExpansionFail r rules (res : errors)
)
. ($ dummyRule)
validateList ::
MonadReader CDDL m => [Term] -> Rule -> m CDDLResult
validateList terms rule =
($ rule) <$> do
getRule rule >>= \case
Postlude PTAny -> pure Valid
Array rules ->
case terms of
[] -> ifM (and <$> mapM isOptional rules) (pure Valid) (pure InvalidRule)
_ ->
ask >>= \cddl ->
let sequencesOfRules =
runReader (expandRules (length terms) $ flattenGroup cddl rules) cddl
in validateExpandedList terms sequencesOfRules
Choice opts -> validateChoice (validateList terms) opts
_ -> pure UnapplicableRule
--------------------------------------------------------------------------------
-- Maps
validateMapWithExpandedRules ::
forall m.
MonadReader CDDL m =>
[(Term, Term)] -> [Rule] -> m ([AMatchedItem], Maybe ANonMatchedItem)
validateMapWithExpandedRules =
go
where
go ::
[(Term, Term)] ->
[Rule] ->
m ([AMatchedItem], Maybe ANonMatchedItem)
go [] [] = pure ([], Nothing)
go ((tk, tv) : ts) rs = do
go' tk tv rs >>= \case
Left tt -> pure ([], Just tt)
Right (res, rs') ->
first (res :) <$> go ts rs'
-- For each pair of terms, try to find some rule that can be applied here,
-- and returns the others if there is a succesful match.
go' :: Term -> Term -> [Rule] -> m (Either ANonMatchedItem (AMatchedItem, [Rule]))
go' tk tv [] = pure $ Left $ ANonMatchedItem tk tv []
go' tk tv (r : rs) =
getRule r >>= \case
KV k v _ ->
ask >>= \cddl ->
case runReader (validateTerm tk k) cddl of
CBORTermResult _ r1@(Valid _) -> case runReader (validateTerm tv v) cddl of
CBORTermResult _ (Valid _) -> pure (Right (AMatchedItem tk tv r, rs))
CBORTermResult _ r2 ->
bimap (\anmi -> anmi {anmiResults = Right (r, r1, r2) : anmiResults anmi}) (second (r :))
<$> go' tk tv rs
CBORTermResult _ r1 ->
bimap (\anmi -> anmi {anmiResults = Left (r, r1) : anmiResults anmi}) (second (r :))
<$> go' tk tv rs
validateExpandedMap ::
forall m.
MonadReader CDDL m =>
[(Term, Term)] ->
[[Rule]] ->
m (Rule -> CDDLResult)
validateExpandedMap terms rules = go rules
where
go :: [[Rule]] -> m (Rule -> CDDLResult)
go [] = pure $ \r -> MapExpansionFail r rules []
go (choice : choices) = do
res <- validateMapWithExpandedRules terms choice
case res of
(_, Nothing) -> pure Valid
(matches, Just notMatched) ->
go choices
>>= ( \case
Valid _ -> pure Valid
MapExpansionFail _ _ errors ->
pure $ \r -> MapExpansionFail r rules ((matches, notMatched) : errors)
)
. ($ dummyRule)
validateMap ::
MonadReader CDDL m =>
[(Term, Term)] -> Rule -> m CDDLResult
validateMap terms rule =
($ rule) <$> do
getRule rule >>= \case
Postlude PTAny -> pure Valid
Map rules ->
case terms of
[] -> ifM (and <$> mapM isOptional rules) (pure Valid) (pure InvalidRule)
_ ->
ask >>= \cddl ->
let sequencesOfRules =
runReader (expandRules (length terms) $ flattenGroup cddl rules) cddl
in validateExpandedMap terms sequencesOfRules
Choice opts -> validateChoice (validateMap terms) opts
_ -> pure UnapplicableRule
--------------------------------------------------------------------------------
-- Choices
validateChoice ::
forall m. Monad m => (Rule -> m CDDLResult) -> NE.NonEmpty Rule -> m (Rule -> CDDLResult)
validateChoice v rules = go rules
where
go :: NE.NonEmpty Rule -> m (Rule -> CDDLResult)
go (choice NE.:| xs) = do
v choice >>= \case
Valid _ -> pure Valid
err -> case NE.nonEmpty xs of
Nothing -> pure $ \r -> ChoiceFail r rules ((choice, err) NE.:| [])
Just choices ->
go choices
>>= ( \case
Valid _ -> pure Valid
ChoiceFail _ _ errors -> pure $ \r -> ChoiceFail r rules ((choice, err) NE.<| errors)
)
. ($ dummyRule)
dummyRule :: Rule
dummyRule = MRuleRef (Name "dummy" mempty)
--------------------------------------------------------------------------------
-- Control helpers
-- | Validate both rules
ctrlAnd ::
Monad m =>
(Rule -> m CDDLResult) -> Rule -> Rule -> m (Rule -> CDDLResult)
ctrlAnd v tgt ctrl =
v tgt >>= \case
Valid _ ->
v ctrl <&> \case
Valid _ -> Valid
_ -> flip InvalidControl Nothing
_ -> pure InvalidRule
-- | Dispatch to the appropriate control
ctrlDispatch ::
Monad m =>
(Rule -> m CDDLResult) ->
CtlOp ->
Rule ->
Rule ->
(CtlOp -> Rule -> m (Either (Maybe CBORTermResult) ())) ->
m (Rule -> CDDLResult)
ctrlDispatch v And tgt ctrl _ = ctrlAnd v tgt ctrl
ctrlDispatch v Within tgt ctrl _ = ctrlAnd v tgt ctrl
ctrlDispatch v op tgt ctrl vctrl =
v tgt >>= \case
Valid _ ->
vctrl op ctrl <&> \case
Left err -> flip InvalidControl err
Right () -> Valid
_ -> pure InvalidRule
-- | A boolean control
boolCtrl :: Bool -> Either (Maybe CBORTermResult) ()
boolCtrl c = if c then Right () else Left Nothing
--------------------------------------------------------------------------------
-- Bits control
getIndicesOfChoice :: MonadReader CDDL m => NE.NonEmpty Rule -> m [Word64]
getIndicesOfChoice nodes =
mconcat
. NE.toList
<$> mapM
( getRule
>=> ( \case
Literal (Value (VUInt v) _) -> pure [fromIntegral v]
KV _ v _ ->
getRule v
>>= \case
Literal (Value (VUInt v') _) -> pure [fromIntegral v']
somethingElse ->
error $
"Malformed value in KV in choice in .bits: "
<> show somethingElse
Range ff tt incl -> getIndicesOfRange ff tt incl
Enum g -> getIndicesOfEnum g
somethingElse ->
error $
"Malformed alternative in choice in .bits: "
<> show somethingElse
)
)
nodes
getIndicesOfRange :: MonadReader CDDL m => Rule -> Rule -> RangeBound -> m [Word64]
getIndicesOfRange ff tt incl =
((,) <$> getRule ff <*> getRule tt) >>= \case
(Literal (Value (VUInt ff') _), Literal (Value (VUInt tt') _)) ->
pure $
[ff' .. tt'] & case incl of
ClOpen -> init
Closed -> id
somethingElse -> error $ "Malformed range in .bits: " <> show somethingElse
getIndicesOfEnum :: MonadReader CDDL m => Rule -> m [Word64]
getIndicesOfEnum g =
getRule g >>= \case
Group g' -> getIndicesOfChoice (fromJust $ NE.nonEmpty g')
somethingElse -> error $ "Malformed enum in .bits: " <> show somethingElse
--------------------------------------------------------------------------------
-- Resolving rules from the CDDL spec
resolveIfRef :: CDDL -> Rule -> ResolvedRule
resolveIfRef _ (MIt aa) = aa
resolveIfRef ct@(CTreeRoot cddl) (MRuleRef n) = do
case Map.lookup n cddl of
Nothing -> error $ "Unbound reference: " <> show n
Just val -> resolveIfRef ct $ runIdentity val
getRule :: MonadReader CDDL m => Rule -> m ResolvedRule
getRule rule = asks (`resolveIfRef` rule)
--------------------------------------------------------------------------------
-- Utils
replaceRule :: CDDLResult -> Rule -> CDDLResult
replaceRule (ChoiceFail _ a b) r = ChoiceFail r a b
replaceRule (ListExpansionFail _ a b) r = ListExpansionFail r a b
replaceRule (MapExpansionFail _ a b) r = MapExpansionFail r a b
replaceRule (InvalidTagged _ a) r = InvalidTagged r a
replaceRule InvalidRule {} r = InvalidRule r
replaceRule (InvalidControl _ a) r = InvalidControl r a
replaceRule UnapplicableRule {} r = UnapplicableRule r
replaceRule Valid {} r = Valid r
ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM b t f = do b' <- b; if b' then t else f
check :: Bool -> Rule -> CDDLResult
check c = if c then Valid else InvalidRule
range :: Ord a => RangeBound -> a -> a -> Bool
range Closed = (<=)
range ClOpen = (<)