v2 / vlib / encoding / cbor / generic.v
781 lines · 754 sloc · 21.08 KB · da7e85cbec7fd73d9d26db033850648c49120c9f
Raw
1module cbor
2
3import math
4import time
5
6const i32_min_i64 = -i64(2_147_483_647) - 1
7const i32_max_i64 = i64(2_147_483_647)
8const u32_max_i64 = i64(4_294_967_295)
9
10// Generic comptime-driven encoder/decoder. The pack[T] / unpack[T]
11// methods below dispatch on T at compile time, so each call site
12// monomorphises into straight-line code with no runtime type tests.
13//
14// Supported targets:
15// * bool, all signed/unsigned integer widths, f32, f64
16// * string (text), []u8 (byte string), enums (encoded as int)
17// * `$array` (any V array) and `$map` (any K with a primitive scalar
18// decoder — string, signed/unsigned ints, bool — plus any V).
19// * `$struct` (encoded as a string-keyed map; honours
20// `@[cbor: 'alt']`, `@[skip]`, `@[cbor: '-']`, optional fields)
21// * `time.Time` — whole seconds use tag 1 (epoch seconds, integer);
22// sub-second values use tag 0 (RFC 3339 string with nanosecond
23// precision). Decode accepts tag 0 (RFC 3339 text) or tag 1
24// (integer or float).
25// * `RawMessage`, `Value`, `Marshaler`/`Unmarshaler` implementers.
26
27// pack encodes `val` into the packer's buffer using compile-time dispatch.
28@[inline]
29pub fn (mut p Packer) pack[T](val T) ! {
30 $if T is RawMessage {
31 p.pack_raw(val)!
32 } $else $if T is Marshaler {
33 bytes := val.to_cbor()
34 if bytes.len == 0 {
35 return error('cbor: ${T.name}.to_cbor() returned empty bytes')
36 }
37 // Validate the user's output is exactly one well-formed CBOR
38 // item before splicing it into the parent stream. A malformed
39 // or truncated Marshaler would otherwise silently corrupt the
40 // surrounding fields (the next struct field would be parsed
41 // from inside the bad item's claimed payload).
42 mut probe := new_unpacker(bytes, DecodeOpts{})
43 probe.skip_value() or {
44 return error('cbor: ${T.name}.to_cbor() returned malformed CBOR: ${err.msg()}')
45 }
46 if !probe.done() {
47 return error('cbor: ${T.name}.to_cbor() returned ${probe.remaining()} trailing byte(s) past one item')
48 }
49 p.reserve(bytes.len)
50 unsafe { p.buf.push_many(bytes.data, bytes.len) }
51 } $else $if T is Value {
52 p.pack_value(val)!
53 } $else $if T is time.Time {
54 // Whole-second values use tag 1 (epoch seconds) + integer — the
55 // most compact and canonical form (RFC 8949 §3.4.2). Sub-second
56 // values fall back to tag 0 (RFC 3339 string) with nanosecond
57 // precision: encoding the seconds.nanoseconds pair as a tag-1
58 // float would lose ~µs of resolution past the year 2001 (f64
59 // can't carry both a 10-digit unix epoch and 9 fractional digits).
60 if val.nanosecond == 0 {
61 p.pack_tag(tag_epoch)
62 p.pack_int(val.unix())
63 } else {
64 p.pack_tag(tag_date_time)
65 p.pack_text(format_rfc3339_nano(val))
66 }
67 } $else $if T is string {
68 if p.opts.validate_utf8 && !utf8_validate_slice(val.bytes(), 0, val.len) {
69 return error('cbor: validate_utf8 set, but string contains invalid UTF-8 (len=${val.len})')
70 }
71 p.pack_text(val)
72 } $else $if T is bool {
73 p.pack_bool(val)
74 } $else $if T is i8 {
75 p.pack_int(i64(val))
76 } $else $if T is i16 {
77 p.pack_int(i64(val))
78 } $else $if T is int {
79 p.pack_int(i64(val))
80 } $else $if T is i32 {
81 p.pack_int(i64(val))
82 } $else $if T is i64 {
83 p.pack_int(val)
84 } $else $if T is u8 {
85 p.pack_uint(u64(val))
86 } $else $if T is u16 {
87 p.pack_uint(u64(val))
88 } $else $if T is u32 {
89 p.pack_uint(u64(val))
90 } $else $if T is u64 {
91 p.pack_uint(val)
92 } $else $if T is f32 {
93 p.pack_float(f64(val))
94 } $else $if T is f64 {
95 p.pack_float(val)
96 } $else $if T is $enum {
97 p.pack_int(i64(val))
98 } $else $if T is []u8 {
99 p.pack_bytes(val)
100 } $else $if T is $array {
101 p.pack_array_header(u64(val.len))
102 for item in val {
103 p.pack(item)!
104 }
105 } $else $if T is $map {
106 p.pack_map_header(u64(val.len))
107 if p.opts.canonical && val.len > 1 {
108 // Sub-encoders inherit `validate_utf8` so the strict-encode
109 // guarantee survives canonical mode. `self_describe` and
110 // `initial_cap` stay local — the wrapper belongs to the top-level
111 // stream only, and 16 B is enough for almost every key/value pair.
112 sub_opts := EncodeOpts{
113 initial_cap: 16
114 canonical: true
115 validate_utf8: p.opts.validate_utf8
116 }
117 mut encoded_keys := [][]u8{cap: val.len}
118 mut encoded_vals := [][]u8{cap: val.len}
119 for k, item in val {
120 mut ksub := new_packer(sub_opts)
121 ksub.pack(k)!
122 encoded_keys << ksub.bytes().clone()
123 mut vsub := new_packer(sub_opts)
124 vsub.pack(item)!
125 encoded_vals << vsub.bytes().clone()
126 }
127 for i in sort_canonical_indices(encoded_keys) {
128 p.reserve(encoded_keys[i].len + encoded_vals[i].len)
129 unsafe {
130 p.buf.push_many(encoded_keys[i].data, encoded_keys[i].len)
131 p.buf.push_many(encoded_vals[i].data, encoded_vals[i].len)
132 }
133 }
134 } else {
135 for k, item in val {
136 p.pack(k)!
137 p.pack(item)!
138 }
139 }
140 } $else $if T is $struct {
141 mut strategy := ''
142 $for attr in T.attributes {
143 if attr.name == 'cbor_rename_all' {
144 strategy = attr.arg
145 }
146 }
147 mut field_count := 0
148 $for field in T.fields {
149 if !cbor_field_skipped(field) {
150 field_count++
151 }
152 }
153 p.pack_map_header(u64(field_count))
154 if p.opts.canonical && field_count > 1 {
155 // RFC 8949 §4.2.1: deterministic encoding requires keys to
156 // be ordered by their encoded byte form, not by struct
157 // declaration. Encode each (key, value) pair to a sub-buffer,
158 // sort, then splice — same shape as the $map branch above.
159 // `validate_utf8` propagates so strict-encode callers don't
160 // silently lose the guarantee in canonical mode.
161 sub_opts := EncodeOpts{
162 initial_cap: 16
163 canonical: true
164 validate_utf8: p.opts.validate_utf8
165 }
166 mut encoded_keys := [][]u8{cap: field_count}
167 mut encoded_vals := [][]u8{cap: field_count}
168 $for field in T.fields {
169 if !cbor_field_skipped(field) {
170 key := cbor_field_explicit_key(field) or {
171 if strategy != '' { cbor_rename(field.name, strategy) } else { field.name }
172 }
173 mut ksub := new_packer(sub_opts)
174 ksub.pack_text(key)
175 encoded_keys << ksub.bytes().clone()
176 mut vsub := new_packer(sub_opts)
177 $if field.typ is $option {
178 if val.$(field.name) == none {
179 vsub.pack_null()
180 } else {
181 vsub.pack(get_value_from_optional(val.$(field.name)))!
182 }
183 } $else {
184 vsub.pack(val.$(field.name))!
185 }
186 encoded_vals << vsub.bytes().clone()
187 }
188 }
189 for i in sort_canonical_indices(encoded_keys) {
190 p.reserve(encoded_keys[i].len + encoded_vals[i].len)
191 unsafe {
192 p.buf.push_many(encoded_keys[i].data, encoded_keys[i].len)
193 p.buf.push_many(encoded_vals[i].data, encoded_vals[i].len)
194 }
195 }
196 } else {
197 $for field in T.fields {
198 if !cbor_field_skipped(field) {
199 key := cbor_field_explicit_key(field) or {
200 if strategy != '' { cbor_rename(field.name, strategy) } else { field.name }
201 }
202 p.pack_text(key)
203 $if field.typ is $option {
204 if val.$(field.name) == none {
205 p.pack_null()
206 } else {
207 p.pack(get_value_from_optional(val.$(field.name)))!
208 }
209 } $else {
210 p.pack(val.$(field.name))!
211 }
212 }
213 }
214 }
215 } $else {
216 p.pack_null()
217 }
218}
219
220// get_value_from_optional unwraps an Option<T> known to be `Some`.
221// Its signature exists solely so V's generic inferrer can pick up the
222// inner T at the comptime call site.
223fn get_value_from_optional[T](val ?T) T {
224 return val or { T{} }
225}
226
227// unpack reads one CBOR value from the buffer and converts it to T.
228@[inline]
229pub fn (mut u Unpacker) unpack[T]() !T {
230 $if T is RawMessage {
231 return u.unpack_raw()!
232 } $else $if T is Unmarshaler {
233 start := u.pos
234 u.skip_value()!
235 mut v := T{}
236 v.from_cbor(u.data[start..u.pos])!
237 return v
238 } $else $if T is Value {
239 return u.unpack_value()!
240 } $else $if T is time.Time {
241 return u.unpack_time()!
242 } $else $if T is string {
243 return u.unpack_text()!
244 } $else $if T is bool {
245 // Accept null as false-equivalent? No — strict by default.
246 return u.unpack_bool()!
247 } $else $if T is i8 {
248 v := u.unpack_int()!
249 if v < -128 || v > 127 {
250 return int_range(u.pos, 'i8', v.str())
251 }
252 return i8(v)
253 } $else $if T is i16 {
254 v := u.unpack_int()!
255 if v < -32_768 || v > 32_767 {
256 return int_range(u.pos, 'i16', v.str())
257 }
258 return i16(v)
259 } $else $if T is int {
260 v := u.unpack_int()!
261 if v < i32_min_i64 || v > i32_max_i64 {
262 return int_range(u.pos, 'int', v.str())
263 }
264 return int(v)
265 } $else $if T is i32 {
266 v := u.unpack_int()!
267 if v < i32_min_i64 || v > i32_max_i64 {
268 return int_range(u.pos, 'i32', v.str())
269 }
270 return i32(v)
271 } $else $if T is i64 {
272 return u.unpack_int()!
273 } $else $if T is u8 {
274 v := u.unpack_int()!
275 if v < 0 || v > 255 {
276 return int_range(u.pos, 'u8', v.str())
277 }
278 return u8(v)
279 } $else $if T is u16 {
280 v := u.unpack_int()!
281 if v < 0 || v > 65_535 {
282 return int_range(u.pos, 'u16', v.str())
283 }
284 return u16(v)
285 } $else $if T is u32 {
286 v := u.unpack_int()!
287 if v < 0 || v > u32_max_i64 {
288 return int_range(u.pos, 'u32', v.str())
289 }
290 return u32(v)
291 } $else $if T is u64 {
292 neg, mag := u.unpack_int_full()!
293 if neg {
294 return int_range(u.pos, 'u64', '-1 - ${mag}')
295 }
296 return mag
297 } $else $if T is f32 {
298 return f32(u.unpack_float()!)
299 } $else $if T is f64 {
300 return u.unpack_float()!
301 } $else $if T is $enum {
302 v := int(u.unpack_int()!)
303 return unsafe { T(v) }
304 } $else $if T is []u8 {
305 return u.unpack_bytes()!
306 } $else $if T is $array {
307 mut out := T{}
308 u.unpack_array_into(mut out)!
309 return out
310 } $else $if T is $map {
311 mut out := T{}
312 read_pairs_into_helper(mut u, mut out)!
313 return out
314 } $else $if T is $struct {
315 mut result := T{}
316 u.unpack_struct_into(mut result)!
317 return result
318 } $else {
319 return error('cbor: unsupported target type')
320 }
321}
322
323fn (mut u Unpacker) unpack_array_into[E](mut out []E) ! {
324 hdr := u.unpack_array_header()!
325 if hdr < 0 {
326 // Indefinite.
327 for {
328 if u.consume_break() {
329 break
330 }
331 out << u.unpack[E]()!
332 }
333 return
334 }
335 for _ in 0 .. hdr {
336 out << u.unpack[E]()!
337 }
338}
339
340// read_pairs_into_helper is a standalone (non-method) generic function;
341// V's generic-method dispatch can drop the second type parameter when
342// invoked from a comptime $map branch, while the standalone form
343// monomorphises correctly.
344fn read_pairs_into_helper[K, V](mut u Unpacker, mut out map[K]V) ! {
345 hdr := u.unpack_map_header()!
346 if hdr < 0 {
347 for {
348 if u.consume_break() {
349 break
350 }
351 key := u.unpack[K]()!
352 val := u.unpack[V]()!
353 if u.opts.deny_duplicate_keys && key in out {
354 return malformed(u.pos, 'duplicate map key')
355 }
356 out[key] = val
357 }
358 return
359 }
360 for _ in 0 .. hdr {
361 key := u.unpack[K]()!
362 val := u.unpack[V]()!
363 if u.opts.deny_duplicate_keys && key in out {
364 return malformed(u.pos, 'duplicate map key')
365 }
366 out[key] = val
367 }
368}
369
370fn (mut u Unpacker) unpack_struct_into[T](mut result T) ! {
371 mut strategy := ''
372 $for attr in T.attributes {
373 if attr.name == 'cbor_rename_all' {
374 strategy = attr.arg
375 }
376 }
377 hdr := u.unpack_map_header()!
378 indef := hdr < 0
379 mut remaining := if indef { i64(-1) } else { hdr }
380 // Tracks keys already seen so deny_duplicate_keys can fire on struct
381 // decode too (the typed-map and Value paths track separately). Built
382 // only when the option is set, so the common case stays allocation-free.
383 // O(1) lookup via V map keeps decode linear even on adversarial inputs
384 // with thousands of distinct keys.
385 mut seen_keys := map[string]bool{}
386 for {
387 if indef {
388 if u.consume_break() {
389 break
390 }
391 } else {
392 if remaining == 0 {
393 break
394 }
395 remaining--
396 }
397 key_ptr, key_len := u.read_text_view()!
398 if u.opts.deny_duplicate_keys {
399 key_str := unsafe { tos(key_ptr, key_len) }.clone()
400 if key_str in seen_keys {
401 return malformed(u.pos, 'duplicate map key "${key_str}"')
402 }
403 seen_keys[key_str] = true
404 }
405 mut matched := false
406 $for field in T.fields {
407 if !cbor_field_skipped(field) {
408 name := cbor_field_explicit_key(field) or {
409 if strategy != '' { cbor_rename(field.name, strategy) } else { field.name }
410 }
411 if !matched && key_len == name.len
412 && unsafe { C.memcmp(key_ptr, name.str, key_len) } == 0 {
413 matched = true
414 $if field.typ is $option {
415 if u.pos < u.data.len && u.data[u.pos] == 0xf6 {
416 u.pos++
417 result.$(field.name) = none
418 } else {
419 mut inner := create_value_from_optional(result.$(field.name))
420 u.unpack_into(mut inner)!
421 result.$(field.name) = inner
422 }
423 } $else {
424 u.unpack_into(mut result.$(field.name))!
425 }
426 }
427 }
428 }
429 if !matched {
430 start := u.pos
431 u.skip_value()!
432 if u.opts.deny_unknown_fields {
433 return UnknownFieldError{
434 pos: start
435 name: unsafe { tos(key_ptr, key_len) }
436 }
437 }
438 }
439 }
440}
441
442// read_text_view returns a (ptr, len) view into the underlying buffer
443// for one definite-length text string. Avoids allocation when matching
444// struct field names. Errors on indefinite-length text since we'd have
445// to copy chunks anyway.
446@[direct_array_access]
447fn (mut u Unpacker) read_text_view() !(&u8, int) {
448 start := u.pos
449 b := u.read_byte()!
450 major := b >> 5
451 if major != 3 {
452 u.pos = start
453 return type_mismatch(start, 'text', b)
454 }
455 info := b & 0x1f
456 if info == 31 {
457 u.pos = start
458 return error('cbor: indefinite-length text not supported as map key (decoder)')
459 }
460 size := u.read_arg(info)!
461 if size > u64(u.data.len - u.pos) {
462 return eof_oversized(u.pos, size, u.data.len - u.pos)
463 }
464 size_int := int(size)
465 if u.opts.validate_utf8 {
466 if !u.is_utf8_at(u.pos, size_int) {
467 return InvalidUtf8Error{
468 pos: u.pos
469 }
470 }
471 }
472 ptr := unsafe { &u8(u.data.data) + u.pos }
473 u.pos += size_int
474 return ptr, size_int
475}
476
477@[direct_array_access; inline]
478fn (u &Unpacker) is_utf8_at(start int, size int) bool {
479 if size == 0 {
480 return true
481 }
482 return utf8_validate_slice(u.data, start, size)
483}
484
485// utf8_validate_slice runs the standard UTF-8 validator on a slice
486// without making an intermediate copy. Mirrors the FSM used by
487// `vlib/encoding/utf8/utf8_util.v`. The 8-byte SWAR pre-scan turns a
488// pure-ASCII payload (the common case: JSON-shaped keys, identifiers)
489// into one load + one mask + one branch per 8 bytes.
490@[direct_array_access]
491fn utf8_validate_slice(data []u8, start int, size int) bool {
492 mut i := start
493 end := start + size
494 for i < end {
495 // 8-byte SWAR ASCII fast path: a pure-ASCII run skips the
496 // per-byte FSM entirely. Triggers on every iteration so a single
497 // non-ASCII rune doesn't disable the fast path for the rest.
498 // `memcpy` into a stack u64 instead of `*(&u64(&data[i]))`: the
499 // latter is undefined behaviour when `i` isn't 8-byte aligned, and
500 // crashes on strict-alignment targets (e.g. some ARMv7, MIPS).
501 // Modern C compilers lower this memcpy to a single unaligned load
502 // on x86 / arm64, so the SWAR speed-up is preserved.
503 for i + 8 <= end {
504 mut chunk := u64(0)
505 unsafe { C.memcpy(&chunk, &data[i], 8) }
506 if chunk & 0x8080808080808080 != 0 {
507 break
508 }
509 i += 8
510 }
511 if i >= end {
512 break
513 }
514 c := data[i]
515 if c < 0x80 {
516 i++
517 continue
518 }
519 mut n := 0
520 if c & 0xe0 == 0xc0 {
521 n = 2
522 } else if c & 0xf0 == 0xe0 {
523 n = 3
524 } else if c & 0xf8 == 0xf0 {
525 n = 4
526 } else {
527 return false
528 }
529 if i + n > end {
530 return false
531 }
532 // Reject overlongs / surrogates / out-of-range.
533 match n {
534 2 {
535 if c < 0xc2 {
536 return false
537 }
538 }
539 3 {
540 b := data[i + 1]
541 if c == 0xe0 && b < 0xa0 {
542 return false
543 }
544 if c == 0xed && b > 0x9f {
545 return false
546 }
547 }
548 4 {
549 b := data[i + 1]
550 if c == 0xf0 && b < 0x90 {
551 return false
552 }
553 if c == 0xf4 && b > 0x8f {
554 return false
555 }
556 if c > 0xf4 {
557 return false
558 }
559 }
560 else {}
561 }
562
563 for k in 1 .. n {
564 if data[i + k] & 0xc0 != 0x80 {
565 return false
566 }
567 }
568 i += n
569 }
570 return true
571}
572
573// create_value_from_optional returns a zero value of an Option's inner T.
574// Exists so the comptime call site can infer T from a struct field.
575fn create_value_from_optional[T](_val ?T) T {
576 return T{}
577}
578
579// unpack_into fills the target through a mutable reference. The mut
580// parameter exists so V's generic inferer picks up T from the
581// `u.unpack_into(mut result.$(field.name))!` call site.
582@[inline]
583fn (mut u Unpacker) unpack_into[T](mut out T) ! {
584 _ = out // vet's "unused parameter" check doesn't track write-only mut args
585 out = u.unpack[T]()!
586}
587
588// format_rfc3339_nano emits a time.Time as RFC 3339 with full nanosecond
589// precision ("YYYY-MM-DDTHH:mm:ss.nnnnnnnnnZ"). vlib's `time` module
590// only goes down to milliseconds (`format_rfc3339`), but tag 0
591// round-trips need 9 digits to preserve `time.Time.nanosecond` exactly.
592// Inputs are normalised to UTC first so a `time.now()` from a local
593// session is encoded as the correct instant rather than as wall-clock
594// digits without an offset.
595fn format_rfc3339_nano(t time.Time) string {
596 utc := if t.is_local { t.local_to_utc() } else { t }
597 return '${utc.year:04d}-${utc.month:02d}-${utc.day:02d}T${utc.hour:02d}:${utc.minute:02d}:${utc.second:02d}.${utc.nanosecond:09d}Z'
598}
599
600// --------------------------------------------------------------------
601// time.Time decoding
602// --------------------------------------------------------------------
603
604fn (mut u Unpacker) unpack_time() !time.Time {
605 start := u.pos
606 b := u.read_byte()!
607 major := b >> 5
608 if major != 6 {
609 u.pos = start
610 return type_mismatch(start, 'time tag', b)
611 }
612 number := u.read_arg(b & 0x1f)!
613 match number {
614 0 {
615 s := u.unpack_text()!
616 return time.parse_iso8601(s) or {
617 return malformed(start, 'invalid RFC 3339 timestamp: ${err}')
618 }
619 }
620 1 {
621 peek := u.peek_byte() or { return error('cbor: missing tag-1 content') }
622 major2 := peek >> 5
623 if major2 == 0 || major2 == 1 {
624 secs := u.unpack_int()!
625 return time.unix(secs)
626 }
627 f := u.unpack_float()!
628 // Reject NaN, ±Inf, and any magnitude that won't fit i64
629 // before casting. Without this, NaN silently saturates to 0
630 // (epoch 1970-01-01) and overflow saturates to i64::max,
631 // either of which could bypass an application-level expiry
632 // or freshness check.
633 if math.is_nan(f) || math.is_inf(f, 0) {
634 return malformed(start, 'tag 1 float must be finite, got ${f}')
635 }
636 if f >= 9_223_372_036_854_775_808.0 || f < -9_223_372_036_854_775_808.0 {
637 return malformed(start, 'tag 1 float ${f} out of range for i64 epoch seconds')
638 }
639 whole := i64(math.floor(f))
640 frac := f - f64(whole)
641 // math.round (not i64-truncate) so 0.999_999_999s doesn't
642 // silently round to 0 ns. Clamp to the valid ns range; the
643 // only way to land on the boundary now is true rounding noise.
644 mut ns := i64(math.round(frac * 1_000_000_000.0))
645 if ns < 0 {
646 ns = 0
647 } else if ns > 999_999_999 {
648 ns = 999_999_999
649 }
650 return time.unix_nanosecond(whole, int(ns))
651 }
652 else {
653 u.pos = start
654 return malformed(start, 'unexpected tag ${number} for time.Time')
655 }
656 }
657}
658
659// --------------------------------------------------------------------
660// Struct attribute helpers
661// --------------------------------------------------------------------
662
663@[inline]
664fn cbor_field_skipped[F](field F) bool {
665 for attr in field.attrs {
666 if attr == 'skip' {
667 return true
668 }
669 if attr.starts_with('cbor:') {
670 if val := parse_cbor_attr(attr) {
671 if val == '-' {
672 return true
673 }
674 }
675 }
676 }
677 return false
678}
679
680// cbor_field_explicit_key returns the rename target from `@[cbor: '...']`
681// when one is set, or `none` if the field has no explicit override.
682// `@[cbor: '-']` and the empty form `@[cbor: '']` are treated as no
683// override (skipping is handled by `cbor_field_skipped`).
684@[inline]
685fn cbor_field_explicit_key[F](field F) ?string {
686 for attr in field.attrs {
687 if attr.starts_with('cbor:') {
688 if val := parse_cbor_attr(attr) {
689 if val != '-' && val != '' {
690 return val
691 }
692 }
693 }
694 }
695 return none
696}
697
698fn cbor_rename(name string, strategy string) string {
699 match strategy {
700 'snake_case' { return cbor_to_snake(name) }
701 'camelCase' { return cbor_to_camel(name) }
702 'PascalCase' { return cbor_to_pascal(name) }
703 'kebab-case' { return cbor_to_kebab(name) }
704 'SCREAMING_SNAKE_CASE' { return cbor_to_snake(name).to_upper() }
705 else { return name }
706 }
707}
708
709fn cbor_to_snake(s string) string {
710 mut out := []u8{cap: s.len + 4}
711 for i, c in s {
712 if c >= `A` && c <= `Z` {
713 if i > 0 {
714 out << `_`
715 }
716 out << u8(c + 32)
717 } else {
718 out << c
719 }
720 }
721 return out.bytestr()
722}
723
724fn cbor_to_camel(s string) string {
725 mut out := []u8{cap: s.len}
726 mut upper_next := false
727 for i, c in s {
728 if c == `_` {
729 upper_next = true
730 continue
731 }
732 if upper_next && c >= `a` && c <= `z` {
733 out << u8(c - 32)
734 upper_next = false
735 } else if i == 0 && c >= `A` && c <= `Z` {
736 out << u8(c + 32)
737 } else {
738 out << c
739 }
740 }
741 return out.bytestr()
742}
743
744fn cbor_to_pascal(s string) string {
745 camel := cbor_to_camel(s)
746 if camel.len == 0 {
747 return camel
748 }
749 first := camel[0]
750 if first >= `a` && first <= `z` {
751 return u8(first - 32).ascii_str() + camel[1..]
752 }
753 return camel
754}
755
756fn cbor_to_kebab(s string) string {
757 mut out := []u8{cap: s.len + 4}
758 for i, c in s {
759 if c >= `A` && c <= `Z` {
760 if i > 0 {
761 out << `-`
762 }
763 out << u8(c + 32)
764 } else if c == `_` {
765 out << `-`
766 } else {
767 out << c
768 }
769 }
770 return out.bytestr()
771}
772
773fn parse_cbor_attr(attr string) ?string {
774 idx := attr.index(':') or { return none }
775 mut v := attr[idx + 1..].trim_space()
776 if v.len >= 2 && ((v.starts_with("'") && v.ends_with("'"))
777 || (v.starts_with('"') && v.ends_with('"'))) {
778 v = v[1..v.len - 1]
779 }
780 return v
781}
782