v / vlib / encoding / cbor / decoder.v
934 lines · 886 sloc · 22.18 KB · 468855eef1db0ff73c62be2d1bf176ffa0e1478e
Raw
1module cbor
2
3import math
4
5// DecodeOpts tunes the decoder. Defaults are conservative: UTF-8 is
6// validated, depth is capped to fend off stack-blow-up payloads, and
7// duplicate map keys are tolerated (callers that need detection turn
8// `deny_duplicate_keys` on).
9pub struct DecodeOpts {
10pub:
11 max_depth int = 256
12 max_stream_bytes int // 0 = unbounded for stream readers
13 validate_utf8 bool = true
14 deny_unknown_fields bool // struct decode rejects unmapped keys
15 deny_duplicate_keys bool // Map decode rejects repeated keys
16 allow_trailing_bytes bool // accept extra bytes after the top-level item
17}
18
19// Kind classifies the next item without consuming it. Useful to branch
20// before committing to a typed read.
21pub enum Kind {
22 unsigned // major type 0
23 negative // major type 1
24 bytes // major type 2 (definite or indefinite)
25 text // major type 3 (definite or indefinite)
26 array_val // major type 4 (definite or indefinite)
27 map_val // major type 5 (definite or indefinite)
28 tag_val // major type 6
29 bool_val // simple 20/21
30 null_val // simple 22
31 undefined // simple 23
32 simple_val // other simple values
33 float_val // half/single/double
34 break_code // 0xff outside a definite header
35}
36
37// Unpacker walks a CBOR byte slice. Operates non-allocating where
38// possible; strings and bytes returned by `unpack_text` / `unpack_bytes`
39// always own their storage so they outlive the input buffer.
40pub struct Unpacker {
41pub mut:
42 data []u8
43 pos int
44 opts DecodeOpts
45}
46
47// new_unpacker constructs an Unpacker over the given byte slice.
48pub fn new_unpacker(data []u8, opts DecodeOpts) Unpacker {
49 cap := if opts.max_depth > 0 {
50 opts
51 } else {
52 DecodeOpts{
53 ...opts
54 max_depth: 256
55 }
56 }
57 return Unpacker{
58 data: data
59 pos: 0
60 opts: cap
61 }
62}
63
64// remaining returns the number of unread bytes.
65@[inline]
66pub fn (u &Unpacker) remaining() int {
67 return u.data.len - u.pos
68}
69
70// done reports whether the unpacker has consumed every byte.
71@[inline]
72pub fn (u &Unpacker) done() bool {
73 return u.pos >= u.data.len
74}
75
76// --------------------------------------------------------------------
77// Low-level byte reads
78// --------------------------------------------------------------------
79
80@[direct_array_access; inline]
81fn (mut u Unpacker) read_byte() !u8 {
82 if u.pos >= u.data.len {
83 return eof_at(u.pos)
84 }
85 b := u.data[u.pos]
86 u.pos++
87 return b
88}
89
90@[direct_array_access; inline]
91fn (u &Unpacker) peek_byte() !u8 {
92 if u.pos >= u.data.len {
93 return eof_at(u.pos)
94 }
95 return u.data[u.pos]
96}
97
98@[direct_array_access; inline]
99fn (mut u Unpacker) read_be_u16() !u16 {
100 if u.pos + 2 > u.data.len {
101 return eof_needing(u.pos, 2, u.data.len - u.pos)
102 }
103 v := u16(u.data[u.pos]) << 8 | u16(u.data[u.pos + 1])
104 u.pos += 2
105 return v
106}
107
108@[direct_array_access; inline]
109fn (mut u Unpacker) read_be_u32() !u32 {
110 if u.pos + 4 > u.data.len {
111 return eof_needing(u.pos, 4, u.data.len - u.pos)
112 }
113 v := u32(u.data[u.pos]) << 24 | u32(u.data[u.pos + 1]) << 16 | u32(u.data[u.pos + 2]) << 8 | u32(u.data[
114 u.pos + 3])
115 u.pos += 4
116 return v
117}
118
119@[direct_array_access; inline]
120fn (mut u Unpacker) read_be_u64() !u64 {
121 if u.pos + 8 > u.data.len {
122 return eof_needing(u.pos, 8, u.data.len - u.pos)
123 }
124 v := u64(u.data[u.pos]) << 56 | u64(u.data[u.pos + 1]) << 48 | u64(u.data[u.pos + 2]) << 40 | u64(u.data[
125 u.pos + 3]) << 32 | u64(u.data[u.pos + 4]) << 24 | u64(u.data[u.pos + 5]) << 16 | u64(u.data[
126 u.pos + 6]) << 8 | u64(u.data[u.pos + 7])
127 u.pos += 8
128 return v
129}
130
131// read_arg reads the additional-info argument for the given initial
132// byte. Returns -1 to signal indefinite-length (info == 31) for major
133// types that allow it; the caller decides whether that's legal.
134fn (mut u Unpacker) read_arg(info u8) !u64 {
135 match info {
136 0...23 { return u64(info) }
137 24 { return u64(u.read_byte()!) }
138 25 { return u64(u.read_be_u16()!) }
139 26 { return u64(u.read_be_u32()!) }
140 27 { return u.read_be_u64()! }
141 else { return malformed(u.pos - 1, 'reserved additional info ${info}') }
142 }
143}
144
145// --------------------------------------------------------------------
146// Public peek
147// --------------------------------------------------------------------
148
149// peek_kind classifies the next item without consuming any input.
150pub fn (u &Unpacker) peek_kind() !Kind {
151 if u.pos >= u.data.len {
152 return eof_at(u.pos)
153 }
154 b := u.data[u.pos]
155 major := b >> 5
156 info := b & 0x1f
157 match major {
158 0 {
159 return .unsigned
160 }
161 1 {
162 return .negative
163 }
164 2 {
165 return .bytes
166 }
167 3 {
168 return .text
169 }
170 4 {
171 return .array_val
172 }
173 5 {
174 return .map_val
175 }
176 6 {
177 return .tag_val
178 }
179 else {
180 match info {
181 20, 21 { return .bool_val }
182 22 { return .null_val }
183 23 { return .undefined }
184 25, 26, 27 { return .float_val }
185 31 { return .break_code }
186 else { return .simple_val }
187 }
188 }
189 }
190}
191
192// --------------------------------------------------------------------
193// High-level typed reads
194// --------------------------------------------------------------------
195
196// unpack_uint reads a non-negative integer (major type 0). Errors on
197// negatives, floats, or other major types. Position is rolled back on
198// any error so callers can branch on `peek_kind` and try a different
199// read (same convention as `unpack_bool` / `unpack_text`).
200pub fn (mut u Unpacker) unpack_uint() !u64 {
201 start := u.pos
202 b := u.read_byte()!
203 major := b >> 5
204 if major != 0 {
205 u.pos = start
206 return type_mismatch(start, 'unsigned', b)
207 }
208 return u.read_arg(b & 0x1f) or {
209 u.pos = start
210 return err
211 }
212}
213
214// unpack_int reads any CBOR integer (major type 0 or 1) into i64. Errors
215// when the magnitude exceeds i64 range; use `unpack_int_full` to pull
216// values as u64 with a separate sign flag.
217pub fn (mut u Unpacker) unpack_int() !i64 {
218 start := u.pos
219 b := u.read_byte()!
220 major := b >> 5
221 arg := u.read_arg(b & 0x1f)!
222 if major == 0 {
223 if arg > u64(max_i64) {
224 u.pos = start
225 return int_range(start, 'i64', arg.str())
226 }
227 return i64(arg)
228 }
229 if major == 1 {
230 // Represented integer = -1 - arg.
231 if arg > u64(max_i64) {
232 u.pos = start
233 return int_range(start, 'i64', '-1 - ${arg}')
234 }
235 return -1 - i64(arg)
236 }
237 u.pos = start
238 return type_mismatch(start, 'integer', b)
239}
240
241// unpack_int_full returns (negative, magnitude). For unsigned values
242// negative=false and magnitude is the raw u64. For negative values
243// negative=true and magnitude is the encoded argument (the integer
244// itself is `-1 - magnitude`).
245pub fn (mut u Unpacker) unpack_int_full() !(bool, u64) {
246 start := u.pos
247 b := u.read_byte()!
248 major := b >> 5
249 arg := u.read_arg(b & 0x1f)!
250 if major == 0 {
251 return false, arg
252 }
253 if major == 1 {
254 return true, arg
255 }
256 u.pos = start
257 return type_mismatch(start, 'integer', b)
258}
259
260// unpack_bool reads a CBOR boolean (simple 20/21). Position is rolled
261// back on a type mismatch so callers can branch on `peek_kind` and try
262// a different read.
263pub fn (mut u Unpacker) unpack_bool() !bool {
264 start := u.pos
265 b := u.read_byte()!
266 if b == 0xf4 {
267 return false
268 }
269 if b == 0xf5 {
270 return true
271 }
272 u.pos = start
273 return type_mismatch(start, 'bool', b)
274}
275
276// unpack_null consumes a CBOR null (0xf6) or errors with type mismatch.
277// Position is rolled back on mismatch (same convention as unpack_bool).
278pub fn (mut u Unpacker) unpack_null() ! {
279 start := u.pos
280 b := u.read_byte()!
281 if b != 0xf6 {
282 u.pos = start
283 return type_mismatch(start, 'null', b)
284 }
285}
286
287// unpack_float reads a CBOR float of any width (half/single/double) and
288// returns it as f64.
289pub fn (mut u Unpacker) unpack_float() !f64 {
290 start := u.pos
291 b := u.read_byte()!
292 match b {
293 0xf9 {
294 h := u.read_be_u16()!
295 return half_to_f64(h)
296 }
297 0xfa {
298 bits := u.read_be_u32()!
299 return f64(math.f32_from_bits(bits))
300 }
301 0xfb {
302 bits := u.read_be_u64()!
303 return math.f64_from_bits(bits)
304 }
305 else {
306 u.pos = start
307 return type_mismatch(start, 'float', b)
308 }
309 }
310}
311
312// unpack_simple reads a simple value (0..255). Bool/null/undefined are
313// also simple values; this method returns the raw u8.
314pub fn (mut u Unpacker) unpack_simple() !u8 {
315 start := u.pos
316 b := u.read_byte()!
317 if b >= 0xe0 && b <= 0xf3 {
318 return b & 0x1f
319 }
320 match b {
321 0xf4 {
322 return 20
323 }
324 0xf5 {
325 return 21
326 }
327 0xf6 {
328 return 22
329 }
330 0xf7 {
331 return 23
332 }
333 0xf8 {
334 v := u.read_byte()!
335 if v < 32 {
336 u.pos = start
337 return malformed(start, 'simple value < 32 must use 1-byte form')
338 }
339 return v
340 }
341 else {
342 u.pos = start
343 return type_mismatch(start, 'simple', b)
344 }
345 }
346}
347
348// unpack_text reads a definite or indefinite-length text string. The
349// returned string owns its bytes (it's a clone of the input slice).
350// UTF-8 validation runs unless `DecodeOpts.validate_utf8` is false.
351pub fn (mut u Unpacker) unpack_text() !string {
352 start := u.pos
353 b := u.read_byte()!
354 major := b >> 5
355 if major != 3 {
356 u.pos = start
357 return type_mismatch(start, 'text', b)
358 }
359 info := b & 0x1f
360 if info == 31 {
361 return u.read_indef_text()!
362 }
363 size := u.read_arg(info)!
364 return u.read_text_chunk(size)!
365}
366
367// read_text_chunk consumes `size` bytes as a UTF-8 text fragment. The
368// argument is u64 because CBOR allows lengths up to 2^64-1 in the wire
369// format; the function rejects any length that the host can't represent
370// or that exceeds the available payload, so neither the bounds check
371// nor the slice can panic on adversarial input.
372@[direct_array_access]
373fn (mut u Unpacker) read_text_chunk(size u64) !string {
374 if size > u64(u.data.len - u.pos) {
375 return eof_oversized(u.pos, size, u.data.len - u.pos)
376 }
377 size_int := int(size)
378 bytes_start := u.pos
379 u.pos += size_int
380 if u.opts.validate_utf8 && !utf8_validate_slice(u.data, bytes_start, size_int) {
381 return InvalidUtf8Error{
382 pos: bytes_start
383 }
384 }
385 return u.data[bytes_start..u.pos].bytestr()
386}
387
388fn (mut u Unpacker) read_indef_text() !string {
389 mut acc := strings_builder_new()
390 for {
391 b := u.read_byte()!
392 if b == 0xff {
393 break
394 }
395 major := b >> 5
396 info := b & 0x1f
397 if major != 3 || info == 31 {
398 return malformed(u.pos - 1,
399 'indefinite-length text chunk must be a definite-length text string')
400 }
401 size := u.read_arg(info)!
402 s := u.read_text_chunk(size)!
403 acc.write_string(s)
404 }
405 return acc.str()
406}
407
408// unpack_bytes reads a definite or indefinite-length byte string. The
409// returned slice is a clone, safe to retain after the unpacker is freed.
410pub fn (mut u Unpacker) unpack_bytes() ![]u8 {
411 start := u.pos
412 b := u.read_byte()!
413 major := b >> 5
414 if major != 2 {
415 u.pos = start
416 return type_mismatch(start, 'bytes', b)
417 }
418 info := b & 0x1f
419 if info == 31 {
420 return u.read_indef_bytes()!
421 }
422 size := u.read_arg(info)!
423 return u.read_bytes_chunk(size)!
424}
425
426// read_bytes_chunk consumes `size` bytes as a byte string fragment.
427// See read_text_chunk for why size is u64.
428@[direct_array_access]
429fn (mut u Unpacker) read_bytes_chunk(size u64) ![]u8 {
430 if size > u64(u.data.len - u.pos) {
431 return eof_oversized(u.pos, size, u.data.len - u.pos)
432 }
433 size_int := int(size)
434 out := u.data[u.pos..u.pos + size_int].clone()
435 u.pos += size_int
436 return out
437}
438
439fn (mut u Unpacker) read_indef_bytes() ![]u8 {
440 mut acc := []u8{cap: 64}
441 for {
442 b := u.read_byte()!
443 if b == 0xff {
444 break
445 }
446 major := b >> 5
447 info := b & 0x1f
448 if major != 2 || info == 31 {
449 return malformed(u.pos - 1,
450 'indefinite-length bytes chunk must be a definite-length byte string')
451 }
452 size := u.read_arg(info)!
453 acc << u.read_bytes_chunk(size)!
454 }
455 return acc
456}
457
458// unpack_array_header reads the prefix of an array. Returns the count
459// for definite-length arrays, or -1 for indefinite-length arrays (the
460// caller then loops until peek_kind() == .break_code and consumes the
461// break with `expect_break`).
462pub fn (mut u Unpacker) unpack_array_header() !i64 {
463 start := u.pos
464 b := u.read_byte()!
465 major := b >> 5
466 if major != 4 {
467 u.pos = start
468 return type_mismatch(start, 'array', b)
469 }
470 info := b & 0x1f
471 if info == 31 {
472 return -1
473 }
474 arg := u.read_arg(info)!
475 if arg > u64(max_i64) {
476 u.pos = start
477 return int_range(start, 'i64', arg.str())
478 }
479 return i64(arg)
480}
481
482// unpack_map_header reads the prefix of a map. Returns pair count or -1
483// for indefinite-length maps.
484pub fn (mut u Unpacker) unpack_map_header() !i64 {
485 start := u.pos
486 b := u.read_byte()!
487 major := b >> 5
488 if major != 5 {
489 u.pos = start
490 return type_mismatch(start, 'map', b)
491 }
492 info := b & 0x1f
493 if info == 31 {
494 return -1
495 }
496 arg := u.read_arg(info)!
497 if arg > u64(max_i64) {
498 u.pos = start
499 return int_range(start, 'i64', arg.str())
500 }
501 return i64(arg)
502}
503
504// unpack_tag reads a tag header and returns the tag number. The caller
505// must follow up by reading the tag content. Position is rolled back
506// on any error so callers can branch on `peek_kind` and try a different
507// read.
508pub fn (mut u Unpacker) unpack_tag() !u64 {
509 start := u.pos
510 b := u.read_byte()!
511 major := b >> 5
512 if major != 6 {
513 u.pos = start
514 return type_mismatch(start, 'tag', b)
515 }
516 return u.read_arg(b & 0x1f) or {
517 u.pos = start
518 return err
519 }
520}
521
522// peek_break reports whether the next byte is the break stop code.
523@[inline]
524pub fn (u &Unpacker) peek_break() bool {
525 return u.pos < u.data.len && u.data[u.pos] == 0xff
526}
527
528// consume_break advances past a break stop code if one is at the
529// cursor, returning true. Useful for the indef-length loop pattern:
530// `for { if u.consume_break() { break } ... }`.
531@[inline]
532fn (mut u Unpacker) consume_break() bool {
533 if u.peek_break() {
534 u.pos++
535 return true
536 }
537 return false
538}
539
540// check_container_len rejects a definite-length array or map header
541// whose item count can't fit a host `int` or whose minimum byte cost
542// (1 byte/item for arrays, 2 bytes/pair for maps) already exceeds the
543// remaining payload. Callers use the `int(n)` cast safely after.
544//
545// Comparison uses `remaining / bytes_per_item` rather than
546// `n * bytes_per_item` so the multiplication can't overflow at
547// n ≈ i64::max.
548@[inline]
549fn (u &Unpacker) check_container_len(start int, n u64, bytes_per_item int, kind string) ! {
550 if n > u64(max_i64) || i64(n) > i64(u.data.len - u.pos) / i64(bytes_per_item) {
551 return malformed(start, '${kind} length ${n} exceeds remaining input')
552 }
553}
554
555// expect_break consumes a single 0xff break code; errors otherwise.
556pub fn (mut u Unpacker) expect_break() ! {
557 b := u.read_byte()!
558 if b != 0xff {
559 return malformed(u.pos - 1, 'expected break code, got 0x${b:02x}')
560 }
561}
562
563// --------------------------------------------------------------------
564// Skip
565// --------------------------------------------------------------------
566
567// skip_value advances past one complete CBOR value without allocating.
568// Honours the depth cap so adversarial deeply-nested input cannot blow
569// the stack.
570pub fn (mut u Unpacker) skip_value() ! {
571 u.skip_inner(0)!
572}
573
574fn (mut u Unpacker) skip_inner(depth int) ! {
575 if depth > u.opts.max_depth {
576 return MaxDepthError{
577 pos: u.pos
578 max_depth: u.opts.max_depth
579 }
580 }
581 b := u.read_byte()!
582 major := b >> 5
583 info := b & 0x1f
584 match major {
585 0, 1 {
586 u.read_arg(info)!
587 }
588 2, 3 {
589 if info == 31 {
590 // RFC 8949 §3.2.3: each chunk MUST be a definite-length
591 // string of the same major type — no nested indefinite,
592 // no cross-type chunks. Mirror unpack_text/unpack_bytes.
593 for {
594 if u.consume_break() {
595 break
596 }
597 cb := u.read_byte()!
598 cmajor := cb >> 5
599 cinfo := cb & 0x1f
600 if cmajor != major || cinfo == 31 {
601 return malformed(u.pos - 1,
602 'indefinite-length string chunk must be a definite-length string of the same major type')
603 }
604 csize := u.read_arg(cinfo)!
605 if csize > u64(u.data.len - u.pos) {
606 return eof_oversized(u.pos, csize, u.data.len - u.pos)
607 }
608 u.pos += int(csize)
609 }
610 } else {
611 size := u.read_arg(info)!
612 if size > u64(u.data.len - u.pos) {
613 return eof_oversized(u.pos, size, u.data.len - u.pos)
614 }
615 u.pos += int(size)
616 }
617 }
618 4 {
619 if info == 31 {
620 for {
621 if u.consume_break() {
622 break
623 }
624 u.skip_inner(depth + 1)!
625 }
626 } else {
627 n := u.read_arg(info)!
628 u.check_container_len(u.pos - 1, n, 1, 'array')!
629 for _ in 0 .. n {
630 u.skip_inner(depth + 1)!
631 }
632 }
633 }
634 5 {
635 if info == 31 {
636 for {
637 if u.consume_break() {
638 break
639 }
640 u.skip_inner(depth + 1)! // key
641 u.skip_inner(depth + 1)! // value
642 }
643 } else {
644 n := u.read_arg(info)!
645 u.check_container_len(u.pos - 1, n, 2, 'map')!
646 for _ in 0 .. n {
647 u.skip_inner(depth + 1)!
648 u.skip_inner(depth + 1)!
649 }
650 }
651 }
652 6 {
653 u.read_arg(info)!
654 u.skip_inner(depth + 1)!
655 }
656 else {
657 // Major type 7 (floats / simple).
658 match info {
659 0...23 {} // simple values 0..23 inline
660 24 {
661 // Per RFC 8949 §3.3, a simple value < 32 must use the
662 // inline form (info 0..23) — the 1-byte form is only
663 // well-formed for 32..255. unpack_simple already enforces
664 // this; skip_value must too, otherwise malformed CBOR
665 // slips through RawMessage / Unmarshaler / unknown-field
666 // skipping and lands in downstream consumers.
667 sv := u.read_byte()!
668 if sv < 32 {
669 return malformed(u.pos - 1, 'simple value < 32 must use 1-byte form')
670 }
671 }
672 25 {
673 u.pos += 2
674 } // half
675 26 {
676 u.pos += 4
677 } // single
678 27 {
679 u.pos += 8
680 } // double
681 31 {
682 return malformed(u.pos - 1, 'unexpected break stop code')
683 }
684 else {
685 return malformed(u.pos - 1, 'reserved additional info ${info}')
686 }
687 }
688
689 if u.pos > u.data.len {
690 return eof_at(u.data.len)
691 }
692 }
693 }
694}
695
696// --------------------------------------------------------------------
697// Value tree decoder
698// --------------------------------------------------------------------
699
700// unpack_value materialises one CBOR data item as a Value.
701pub fn (mut u Unpacker) unpack_value() !Value {
702 return u.unpack_value_inner(0)!
703}
704
705fn (mut u Unpacker) unpack_value_inner(depth int) !Value {
706 if depth > u.opts.max_depth {
707 return MaxDepthError{
708 pos: u.pos
709 max_depth: u.opts.max_depth
710 }
711 }
712 start := u.pos
713 b := u.read_byte()!
714 major := b >> 5
715 info := b & 0x1f
716 match major {
717 0 {
718 arg := u.read_arg(info)!
719 return IntNum{
720 negative: false
721 magnitude: arg
722 }
723 }
724 1 {
725 arg := u.read_arg(info)!
726 return IntNum{
727 negative: true
728 magnitude: arg
729 }
730 }
731 2 {
732 u.pos = start
733 data := u.unpack_bytes()!
734 return Bytes{
735 data: data
736 }
737 }
738 3 {
739 u.pos = start
740 s := u.unpack_text()!
741 return Text{
742 value: s
743 }
744 }
745 4 {
746 if info == 31 {
747 mut elements := []Value{cap: 4}
748 for {
749 if u.consume_break() {
750 break
751 }
752 elements << u.unpack_value_inner(depth + 1)!
753 }
754 return Array{
755 elements: elements
756 }
757 }
758 n := u.read_arg(info)!
759 u.check_container_len(start, n, 1, 'array')!
760 mut elements := []Value{cap: int(n)}
761 for _ in 0 .. n {
762 elements << u.unpack_value_inner(depth + 1)!
763 }
764 return Array{
765 elements: elements
766 }
767 }
768 5 {
769 // Dedup tracking hashes the raw on-wire bytes of each key (per
770 // RFC 8949 §5.6 "encoded data items are equal iff their byte
771 // representations match") in a V map → O(1) lookup vs the
772 // previous O(n) linear scan, so adversarial inputs with
773 // thousands of distinct keys decode in linear time. Built
774 // only when the option is set.
775 mut seen := map[string]bool{}
776 if info == 31 {
777 mut pairs := []MapPair{cap: 4}
778 for {
779 if u.consume_break() {
780 break
781 }
782 key_start := u.pos
783 key := u.unpack_value_inner(depth + 1)!
784 if u.opts.deny_duplicate_keys {
785 k := u.data[key_start..u.pos].bytestr()
786 if k in seen {
787 return malformed(key_start, 'duplicate map key')
788 }
789 seen[k] = true
790 }
791 val := u.unpack_value_inner(depth + 1)!
792 pairs << MapPair{
793 key: key
794 value: val
795 }
796 }
797 return Map{
798 pairs: pairs
799 }
800 }
801 n := u.read_arg(info)!
802 u.check_container_len(start, n, 2, 'map')!
803 mut pairs := []MapPair{cap: int(n)}
804 for _ in 0 .. n {
805 key_start := u.pos
806 key := u.unpack_value_inner(depth + 1)!
807 if u.opts.deny_duplicate_keys {
808 k := u.data[key_start..u.pos].bytestr()
809 if k in seen {
810 return malformed(key_start, 'duplicate map key')
811 }
812 seen[k] = true
813 }
814 val := u.unpack_value_inner(depth + 1)!
815 pairs << MapPair{
816 key: key
817 value: val
818 }
819 }
820 return Map{
821 pairs: pairs
822 }
823 }
824 6 {
825 number := u.read_arg(info)!
826 content := u.unpack_value_inner(depth + 1)!
827 // Native validation per RFC 8949 §3.4.1: tag 0 wraps an RFC 3339
828 // text string; tag 1 wraps a numeric value (int or float).
829 // QCBOR does the same — accepting wrong content types here would
830 // allow well-formed-but-invalid payloads through.
831 if number == 0 && content !is Text {
832 return malformed(u.pos, 'tag 0 (date/time) must wrap a text string')
833 }
834 if number == 1 && content !is IntNum && content !is FloatNum {
835 return malformed(u.pos, 'tag 1 (epoch) must wrap a number')
836 }
837 return Tag{
838 number: number
839 content_box: [content]
840 }
841 }
842 else {
843 match info {
844 20 {
845 return Bool{
846 value: false
847 }
848 }
849 21 {
850 return Bool{
851 value: true
852 }
853 }
854 22 {
855 return Null{}
856 }
857 23 {
858 return Undefined{}
859 }
860 24 {
861 v := u.read_byte()!
862 if v < 32 {
863 u.pos = start
864 return malformed(start, 'simple value < 32 must use 1-byte form')
865 }
866 return Simple{
867 value: v
868 }
869 }
870 25 {
871 h := u.read_be_u16()!
872 return FloatNum{
873 value: half_to_f64(h)
874 bits: .half
875 }
876 }
877 26 {
878 bits := u.read_be_u32()!
879 return FloatNum{
880 value: f64(math.f32_from_bits(bits))
881 bits: .single
882 }
883 }
884 27 {
885 bits := u.read_be_u64()!
886 return FloatNum{
887 value: math.f64_from_bits(bits)
888 bits: .double
889 }
890 }
891 31 {
892 u.pos = start
893 return malformed(start, 'unexpected break stop code')
894 }
895 else {
896 if info <= 19 {
897 return Simple{
898 value: info
899 }
900 }
901 u.pos = start
902 return malformed(start, 'reserved additional info ${info}')
903 }
904 }
905 }
906 }
907}
908
909// strings_builder_new is a small alias to keep the import surface tight
910// (we only need the strings module for indefinite-length text accumulation).
911@[inline]
912fn strings_builder_new() StringsBuilder {
913 return StringsBuilder{
914 buf: []u8{cap: 32}
915 }
916}
917
918struct StringsBuilder {
919mut:
920 buf []u8
921}
922
923@[inline]
924fn (mut b StringsBuilder) write_string(s string) {
925 if s == '' {
926 return
927 }
928 unsafe { b.buf.push_many(s.str, s.len) }
929}
930
931@[inline]
932fn (mut b StringsBuilder) str() string {
933 return b.buf.bytestr()
934}
935