ecluse:ecluse-core
Safe HaskellNone
LanguageGHC2021

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, matching eitherDecodeStrict failing the whole body;
  • every value is depth-bounded at the caller's budget, so a value nested past it anywhere is a SelectiveTooDeeplyNested breach (withinNestingBudget is the same bound applied to a built Value);
  • 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

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.

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).