Skip to content

Commit 5b2ef16

Browse files
Merge pull request #733 from ky438/ky/cbor-view-nav-refactor
Ky/cbor view nav refactor
2 parents 1437577 + d985107 commit 5b2ef16

4 files changed

Lines changed: 1918 additions & 805 deletions

File tree

doc/ref/cbor/cbor_view.md

Lines changed: 169 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,30 @@
88

99
The `cbor::view` namespace reads the *encoded* structure of
1010
[Concise Binary Object Representation](http://cbor.io/) data in place,
11-
without copying it or building a data structure. Its spine is four stages:
12-
13-
> owned bytes -> structural validation -> borrowed checked items -> application semantics
14-
15-
A scan validates that bytes are *structurally* well-formed CBOR once
16-
(framing only — not validity such as UTF-8); everything afterwards
17-
operates on checked `item` views and cannot fail structurally. The last
18-
stage — what tag 2 bytes or a tag 25 string reference *mean* — belongs to
19-
[decode_cbor](decode_cbor.md) and [basic_cbor_cursor](basic_cbor_cursor.md),
20-
not here: tags are exposed, never interpreted.
11+
without copying it or building a data structure. Its checked-item layer
12+
borrows complete encodings; its move-only `navigator` owns reusable traversal
13+
workspace while borrowing the same bytes. The data flow is:
14+
> owned bytes -> structural validation -> checked item or navigator -> application semantics
15+
16+
Validation checks structural well-formedness once (framing only — not content
17+
validity such as UTF-8). Checked items and navigator movement then cannot fail
18+
structurally. The last stage — what tag 2 bytes or a tag 25 string reference
19+
*mean* — belongs to [decode_cbor](decode_cbor.md) and
20+
[basic_cbor_cursor](basic_cbor_cursor.md), not here: tags are exposed, never
21+
interpreted.
2122

2223
This namespace is experimental.
2324

2425
#### Ownership and lifetime
2526

26-
Everything in this namespace borrows. An `item`, the items produced by
27-
iterating it, and every span and string view obtained from them point
28-
into the scanned input bytes, and are invalidated by anything that
29-
invalidates those bytes: destroying the container, mutating it, or any
30-
reallocation. Scanning a temporary container is rejected at compile
31-
time. The copying accessors (`text` into `std::string`, `bytes` into
32-
`std::vector`) are the way to keep content beyond the input's lifetime.
27+
All views borrow the input bytes. An `item`, a `navigator`, and every span or
28+
string view obtained from them are
29+
invalidated by anything that invalidates those bytes: destroying the container,
30+
mutating it, or reallocation. A navigator owns only its mutable navigation and
31+
validation workspace; it does not own the encoded bytes. Factory, reset, and
32+
`wire_cursor` entry points reject temporary owning containers at compile time.
33+
The copying item accessors (`text` into `std::string`, `bytes` into
34+
`std::vector`) retain content independently.
3335

3436
#### Scanning
3537

@@ -62,8 +64,93 @@ anything after it as `cbor_errc::trailing_data`.
6264
6365
Scanning enforces `scan_context::max_nesting_depth` (default
6466
`default_max_nesting_depth`, 1024) using constant call-stack space at
65-
any depth. It allocates only when nesting exceeds 32 open containers;
66-
a reused `scan_context` retains that capacity across scans.
67+
any depth. It allocates only when nesting exceeds 32 open containers; a reused
68+
`scan_context`, `wire_cursor` with a supplied context, or navigator reset retains
69+
that validation capacity.
70+
71+
#### Structural navigation
72+
73+
```cpp
74+
enum class position_role { root, array_element, map_key, map_value };
75+
76+
struct navigation_result
77+
{
78+
navigator first;
79+
span<const uint8_t> remainder;
80+
};
81+
82+
expected<navigation_result, scan_error> navigate_prefix(
83+
span<const uint8_t> input,
84+
int max_nesting_depth = default_max_nesting_depth);
85+
86+
expected<navigator, scan_error> navigate_exact(
87+
span<const uint8_t> input,
88+
int max_nesting_depth = default_max_nesting_depth);
89+
90+
class navigator
91+
{
92+
public:
93+
navigator(navigator&&) noexcept;
94+
navigator& operator=(navigator&&) noexcept;
95+
navigator(const navigator&) = delete;
96+
97+
item_kind kind() const noexcept;
98+
uint64_t argument() const noexcept;
99+
bool indefinite() const noexcept;
100+
tag_range tags() const noexcept;
101+
position_role role() const noexcept;
102+
std::size_t depth() const noexcept;
103+
104+
bool uint64_value(uint64_t&) const noexcept;
105+
bool int64_value(int64_t&) const noexcept;
106+
bool bool_value(bool&) const noexcept;
107+
bool double_value(double&) const noexcept;
108+
bool text(string_view&) const noexcept;
109+
bool bytes(span<const uint8_t>&) const noexcept;
110+
111+
bool enter() noexcept;
112+
bool next() noexcept;
113+
bool leave() noexcept;
114+
void rewind() noexcept;
115+
116+
item finish_item() noexcept;
117+
bool extent_known() const noexcept;
118+
119+
expected<span<const uint8_t>, scan_error> reset_prefix(
120+
span<const uint8_t> input,
121+
int max_nesting_depth = default_max_nesting_depth);
122+
expected<span<const uint8_t>, scan_error> reset_exact(
123+
span<const uint8_t> input,
124+
int max_nesting_depth = default_max_nesting_depth);
125+
};
126+
```
127+
128+
A navigator begins at the checked root. `enter()` moves into a nonempty array
129+
or map; maps expose raw children with alternating key/value roles. `next()`
130+
moves to the next raw sibling, skipping the current unopened container if
131+
necessary. At the end of the siblings it returns `false`; `leave()` restores
132+
the completed parent. Calling `leave()` early skips only the unread remainder.
133+
`rewind()` restores the root.
134+
135+
The current position exposes its already parsed head and scalar/string content.
136+
A container child may not yet have a known end. `finish_item()` establishes and
137+
caches that end, walking the current subtree only when necessary, and returns an
138+
ordinary complete `item`. `extent_known()` makes that cost visible. Descent after
139+
`finish_item()` is legal and revisits the subtree.
140+
141+
Validation records the observed peak open-container depth, prepares that many
142+
navigation frames, and retains the validation scratch. Movement uses indexed
143+
frames; subtree skips reuse the retained scratch. Neither allocates after
144+
successful construction. Resets are transactional and retain both capacities.
145+
Both reset forms return the unconsumed remainder; it is empty after a
146+
successful exact reset.
147+
148+
A known parent boundary propagates to its final definite child. Consequently,
149+
the last payload in a definite transport tuple can be finished in O(1), even
150+
when it is a large container. Non-final arbitrary containers have the honest
151+
O(depth) tradeoff: descend immediately without a pre-walk, or establish/capture
152+
the exact span with one subtree walk.
153+
67154

68155
#### The checked item
69156

@@ -76,9 +163,8 @@ class item
76163
bool indefinite() const noexcept;
77164
tag_range tags() const noexcept; // leading tags, outermost first
78165

79-
element_range elements() const noexcept; // array elements
80-
entry_range entries() const noexcept; // map entries
81166
chunk_range chunks() const noexcept; // string content spans
167+
child_range children(scan_context& context) const noexcept; // raw children as items
82168

83169
bool uint64_value(uint64_t& value) const noexcept;
84170
bool int64_value(int64_t& value) const noexcept;
@@ -96,27 +182,25 @@ enum class item_kind
96182
array, map, simple
97183
};
98184

99-
struct map_entry
100-
{
101-
item key;
102-
item value;
103-
};
104185
```
105186

106-
An `item` is one complete, well-formed encoded item: its leading
187+
An `item` is one complete, structurally well-formed encoded item: its leading
107188
semantic tags, head, and content. `kind()` classifies the content after
108189
tags (`simple` covers major type 7: simple values and floating point);
109190
`argument()` is the head's argument — an integer's value, a string's
110191
length, a container's count, a simple value's number, or the bit
111192
pattern of a floating-point value.
112193

113-
The ranges iterate with plain range-`for` and cannot fail: validation
114-
already happened. `elements()` and `entries()` yield checked items;
115-
`chunks()` yields the contiguous spans of a string's content, one per
116-
chunk for indefinite-length strings. Kind mismatches yield empty
117-
ranges. Iterating a container walks its encoding, so navigating to
118-
depth *d* of a document rescans the subtrees on that path; iteration
119-
beyond 32 levels of nesting inside one item may allocate.
194+
`chunks()` yields the contiguous spans of a string's content, one per chunk
195+
for indefinite-length strings.
196+
197+
`children(context)` yields a container's raw children in order — array
198+
elements, or a map's keys and values alternating — each a complete checked
199+
item, measured once on the way past. The item's bytes are checked, so
200+
iteration cannot fail; the context, which must outlive the range, supplies
201+
skip workspace for container children and grows only past 32 open
202+
containers. `children` serves sibling iteration over checked bytes;
203+
`navigator` serves stateful traversal with retained parents and extents.
120204

121205
The typed accessors return `false`, leaving `value` untouched, exactly
122206
when the item is not of the requested kind; conversions are strict.
@@ -143,9 +227,6 @@ struct length_first_compare; // RFC 8949 4.2.3 order, three-way
143227
struct bytewise_less; // strict weak orders for sorting
144228
struct length_first_less;
145229
146-
template <typename Compare = bytewise_compare>
147-
bool map_keys_sorted(const item& map_item, Compare compare = Compare());
148-
149230
// Span overloads for raw-span consumers; validate input, then order.
150231
template <typename Order = bytewise_compare>
151232
expected<int, scan_error> compare(span<const uint8_t> a, span<const uint8_t> b,
@@ -156,14 +237,11 @@ expected<bool, scan_error> map_keys_sorted(span<const uint8_t> input,
156237
Order order = Order(), int max_nesting_depth = default_max_nesting_depth);
157238
```
158239

159-
The order function objects compare encoded bytes and accept either two
160-
items or two raw `span<const uint8_t>`. The `*_compare` forms return
161-
negative, zero, or positive; the `*_less` forms are predicates for
162-
`std::sort` and ordered containers. `map_keys_sorted` is true if
163-
`map_item` is a map whose keys are strictly ascending in the given
164-
order — the deterministic-encoding key condition. The span overloads
165-
validate before ordering and, on malformed input, return an error carrying
166-
the code and byte offset; trailing bytes after the first item are tolerated.
240+
The order function objects compare encoded bytes and accept either two items
241+
or two raw spans. The `*_compare` forms return a three-way result; the `*_less`
242+
forms are predicates for sorting and ordered containers. The span-based
243+
`map_keys_sorted` validates once, then walks keys with a navigator; malformed
244+
input returns the code and byte offset, and trailing bytes are tolerated.
167245

168246
#### Low-level tier
169247

@@ -172,16 +250,29 @@ enum class major_type; // CBOR major types 0-7
172250

173251
struct item_head { major_type major_type; uint8_t additional_info; uint64_t value; bool indefinite(); };
174252

175-
bool read_head(const uint8_t*& p, const uint8_t* end, item_head& head, std::error_code& ec);
176-
bool skip_item(const uint8_t*& p, const uint8_t* end, std::error_code& ec,
177-
int max_nesting_depth = default_max_nesting_depth);
253+
class wire_cursor
254+
{
255+
public:
256+
explicit wire_cursor(span<const uint8_t> input) noexcept;
257+
258+
std::size_t position() const noexcept;
259+
span<const uint8_t> remaining() const noexcept;
260+
expected<item_head, scan_error> read_head() noexcept;
261+
expected<item, scan_error> read_item(scan_context& context);
262+
expected<span<const uint8_t>, scan_error> skip_item(scan_context& context);
263+
bool skip(std::size_t count) noexcept;
264+
};
178265
```
179266

180-
The wire-level head decoding the checked item layer is built on, for
181-
consumers that need sub-item head access. Prefer the checked item layer
182-
unless you are walking objects yourself with intention. `read_head`
183-
advances `p` past one head only (tags are returned as their own heads) and
184-
validates just that head; `skip_item` advances past one complete item.
267+
`wire_cursor` is the offset-based low-level tier for consumers that need
268+
sub-item head access. It borrows one input span and exposes its position and
269+
remaining bytes without a mutable pointer/end pair. `read_head` advances past
270+
one head only (tags are returned as their own heads); `read_item` validates and
271+
returns one complete checked item; `skip_item` validates and passes over one
272+
complete item, returning its encoded bytes unparsed; `skip` advances past
273+
`count` bytes of already-measured content, such as a definite string payload
274+
after its head, refusing when fewer bytes remain. Errors report offsets from
275+
the beginning of the cursor's input.
185276

186277
### Examples
187278

@@ -190,63 +281,68 @@ validates just that head; `skip_item` advances past one complete item.
190281
```cpp
191282
#include <jsoncons_ext/cbor/cbor_view.hpp>
192283
#include <iostream>
284+
#include <utility>
285+
#include <vector>
193286

194287
int main()
195288
{
196-
// {"id": 42, "name": "ada", "scores": [1, 2]}
289+
// {"id": 42, "scores": [1, 2]}
197290
const std::vector<uint8_t> data = {
198-
0xa3,
291+
0xa2,
199292
0x62,'i','d', 0x18,0x2a,
200-
0x64,'n','a','m','e', 0x63,'a','d','a',
201293
0x66,'s','c','o','r','e','s', 0x82,0x01,0x02
202294
};
203295

204-
auto doc = jsoncons::cbor::view::parse_exact(
205-
jsoncons::span<const uint8_t>(data.data(), data.size()));
206-
if (!doc.has_value())
296+
auto result = jsoncons::cbor::view::navigate_exact(
297+
jsoncons::span<const uint8_t>(data));
298+
if (!result)
207299
{
208-
std::cout << "malformed at offset " << doc.error().offset << "\n";
209300
return 1;
210301
}
211302

212-
for (jsoncons::cbor::view::map_entry entry : doc.value().entries())
303+
auto nav = std::move(result.value());
304+
if (!nav.enter())
305+
{
306+
return 0;
307+
}
308+
for (;;)
213309
{
214310
jsoncons::string_view name;
215-
if (!entry.key.text(name))
311+
if (!nav.text(name) || !nav.next())
216312
{
217-
continue;
313+
break;
218314
}
219315
std::cout << name << ":";
220316

221317
uint64_t number = 0;
222-
jsoncons::string_view text;
223-
if (entry.value.text(text))
224-
{
225-
std::cout << " " << text;
226-
}
227-
else if (entry.value.uint64_value(number))
318+
if (nav.uint64_value(number))
228319
{
229320
std::cout << " " << number;
230321
}
231-
else
322+
else if (nav.enter())
232323
{
233-
for (jsoncons::cbor::view::item element : entry.value.elements())
324+
do
234325
{
235-
if (element.uint64_value(number))
326+
if (nav.uint64_value(number))
236327
{
237328
std::cout << " " << number;
238329
}
239330
}
331+
while (nav.next());
332+
nav.leave();
240333
}
241334
std::cout << "\n";
335+
if (!nav.next())
336+
{
337+
break;
338+
}
242339
}
243340
}
244341
```
245342

246343
Output:
247344
```
248345
id: 42
249-
name: ada
250346
scores: 1 2
251347
```
252348

0 commit comments

Comments
 (0)