| Safe Haskell | None |
|---|---|
| Language | GHC2021 |
Ecluse.Core.Json.Selective
Description
A memory-bounded selective decode over a JSON document's token stream: the reusable engine that materialises only the values a caller picks out and skips every other value's tokens unallocated, while depth-bounding every value it walks.
A whole-document decode (aeson's eitherDecodeStrict) builds a Value for every member of a
large object. When a caller needs only a few members out of a multi-megabyte document, that decode
dominates the cost. This engine walks a document's JSON token stream (aeson's
Data.Aeson.Decoding, no new dependency) and materialises a Value only for the picked members,
skipping the rest without allocating them. The win is on the parse, not the fetch: the full bytes
are still read, but they are parsed selectively, O(picked) work and residency rather than O(N).
Faithful to the whole-document decode
Bounded selective decode is a memory-bounding defence on a size-unbounded, attacker-influenced document, so the walk is faithful to the whole-document decode rather than a shortcut past it:
- it consumes the entire token stream, so malformed JSON anywhere surfaces as
SelectiveUndecodable, matchingeitherDecodeStrictfailing the whole body; - every value is depth-bounded at the caller's budget, so a value nested past it anywhere is
a
SelectiveTooDeeplyNestedbreach (withinNestingBudgetis the same bound applied to a builtValue); - within one object a key's first occurrence wins: a later duplicate is walked for the
malformed and over-deep checks but never re-materialised, matching
aeson's duplicate-key resolution.
The engine names aeson's token types and a depth budget only, no registry or package concept, so
each JSON ecosystem layers its own key-selection walk on top. The npm packument selector is
Ecluse.Core.Registry.Npm.SelectiveDecode.
Synopsis
- data SelectiveError
- findInRecord :: Int -> Text -> TkRecord k String -> Either SelectiveError (Maybe Value, Int, k)
- materialiseWithinBudget :: Int -> Tokens k String -> Either SelectiveError (Value, k)
- withRecord :: Int -> Tokens k String -> (TkRecord k String -> Either SelectiveError a) -> Either SelectiveError a
- skipValue :: Int -> Tokens k String -> Either SelectiveError k
- trailingWhitespace :: ByteString -> Bool
Refusal vocabulary
data SelectiveError Source #
Why a selective decode could not yield a value: the two refusal causes a whole-document decode would also raise, so a caller maps them onto its own error vocabulary.
Constructors
| SelectiveUndecodable | The token stream was not well-formed JSON: malformed bytes anywhere, or trailing non-whitespace after the top-level value. |
| SelectiveTooDeeplyNested | Some value nested deeper than the depth budget allowed. |
Instances
| Show SelectiveError Source # | |
Defined in Ecluse.Core.Json.Selective Methods showsPrec :: Int -> SelectiveError -> ShowS # show :: SelectiveError -> String # showList :: [SelectiveError] -> ShowS # | |
| Eq SelectiveError Source # | |
Defined in Ecluse.Core.Json.Selective Methods (==) :: SelectiveError -> SelectiveError -> Bool # (/=) :: SelectiveError -> SelectiveError -> Bool # | |
Bounded token-walk primitives
findInRecord :: Int -> Text -> TkRecord k String -> Either SelectiveError (Maybe Value, Int, k) Source #
Find one key in a record, materialising only the first occurrence of that key's value
(a Value) and skipping every other entry's tokens unallocated. aeson's object decode keeps the
first of duplicate keys, so a later duplicate of the target is walked (for the malformed and
over-deep checks) but not re-materialised. Returns the found value (if any), the raw number of
entries scanned, and the record's continuation. childBudget is the depth budget the record's
values sit at, so a deeply-nested sibling still breaches. The scan runs to the record's end, never
stopping early, so a later duplicate key, a malformed entry, or an over-deep sibling is still seen.
materialiseWithinBudget :: Int -> Tokens k String -> Either SelectiveError (Value, k) Source #
Materialise one value from its tokens, the same Value decode a whole-document path uses,
bounded at budget: malformed tokens are SelectiveUndecodable, a value past the depth budget is
SelectiveTooDeeplyNested. Route every Value a selective walk builds through here, so each
passes the same depth gate.
withRecord :: Int -> Tokens k String -> (TkRecord k String -> Either SelectiveError a) -> Either SelectiveError a Source #
Run k on a record token, refusing a non-record value and refusing the container outright
when the depth budget is already spent (a record is itself one level).
skipValue :: Int -> Tokens k String -> Either SelectiveError k Source #
Consume one value's tokens without allocating a Value, returning the continuation. Bounds
nesting at budget levels exactly as withinNestingBudget does over a built
Value: a value occupies one level (refused at budget < 1) and a container's children are
bounded one level deeper. Malformed tokens are SelectiveUndecodable.
trailingWhitespace :: ByteString -> Bool Source #
Whether the bytes after the top-level value are JSON whitespace only: the end-of-input check
eitherDecodeStrict applies, so a body with trailing non-whitespace is refused identically (space,
tab, newline, carriage return are the four JSON whitespace bytes).