Skip to main content

laser_wire/filter/
codecs.rs

1use super::eval::{DecodeLimits, FilterRecord, HeaderValueRef, PathTrie};
2use super::{ConsumerFilter, FaultReason, FilterCodec};
3use crate::control::{SchemaDef, SchemaSource};
4use crate::error::InvalidError;
5use crate::limits::{MAX_FILTER_SCHEMA_BYTES, MAX_FILTER_SCHEMA_DEPTH};
6use apache_avro::Schema;
7use apache_avro::reader::datum::GenericDatumReader;
8use apache_avro::schema::{InnerDecimalSchema, ResolvedSchema, UuidSchema};
9use apache_avro::types::Value as Avro;
10use prost_reflect::{
11    DescriptorPool, DynamicMessage, Kind, MapKey, MessageDescriptor, ReflectMessage, Value as Proto,
12};
13use serde_json::{Map, Number, Value};
14use std::collections::BTreeMap;
15use std::fmt;
16use std::sync::Arc;
17
18/// Schema decoders prepared before records enter the evaluation loop.
19#[derive(Clone, Debug, Default)]
20pub struct PayloadDecoders {
21    schemas: BTreeMap<u32, WriterSchema>,
22}
23
24#[derive(Clone, Debug)]
25enum WriterSchema {
26    Avro(Arc<AvroDecoder>),
27    Protobuf(MessageDescriptor),
28}
29
30#[ouroboros::self_referencing]
31struct AvroDecoder {
32    schema: Schema,
33    #[borrows(schema)]
34    #[covariant]
35    names: ResolvedSchema<'this>,
36    #[borrows(schema)]
37    #[covariant]
38    reader: GenericDatumReader<'this>,
39}
40
41impl fmt::Debug for AvroDecoder {
42    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43        formatter
44            .debug_struct("AvroDecoder")
45            .field("schema", self.borrow_schema())
46            .finish_non_exhaustive()
47    }
48}
49
50impl AvroDecoder {
51    fn compile(source: &str) -> Result<Self, String> {
52        // Both validation and decoding borrow one immutable schema tree. Named
53        // references are resolved once without cloning nested schemas.
54        AvroDecoderTryBuilder {
55            schema: physical_avro_schema(source)?,
56            names_builder: |schema| ResolvedSchema::new(schema),
57            reader_builder: |schema| GenericDatumReader::builder(schema).build(),
58        }
59        .try_build()
60        .map_err(|error| error.to_string())
61    }
62}
63
64impl PayloadDecoders {
65    pub fn compile(filter: &ConsumerFilter, schemas: &[SchemaDef]) -> Result<Self, InvalidError> {
66        let mut prepared = BTreeMap::new();
67        for id in &filter.schema_refs {
68            let schema = schemas
69                .iter()
70                .find(|schema| schema.id == *id)
71                .ok_or_else(|| InvalidError::new(format!("writer schema {id} was not supplied")))?;
72            let decoder = match (&schema.source, filter.codec) {
73                (SchemaSource::Avro { schema }, FilterCodec::Avro) => {
74                    WriterSchema::Avro(Arc::new(AvroDecoder::compile(schema).map_err(|error| {
75                        InvalidError::new(format!("Avro schema {id}: {error}"))
76                    })?))
77                }
78                (
79                    SchemaSource::Protobuf {
80                        descriptor_set,
81                        message_type,
82                    },
83                    FilterCodec::Protobuf,
84                ) => {
85                    if descriptor_set.len() > MAX_FILTER_SCHEMA_BYTES {
86                        return Err(InvalidError::new(format!(
87                            "Protobuf descriptor {id} exceeds {MAX_FILTER_SCHEMA_BYTES} bytes"
88                        )));
89                    }
90                    let pool =
91                        DescriptorPool::decode(descriptor_set.as_slice()).map_err(|error| {
92                            InvalidError::new(format!("Protobuf schema {id}: {error}"))
93                        })?;
94                    WriterSchema::Protobuf(
95                        pool.get_message_by_name(message_type.trim_start_matches('.'))
96                            .ok_or_else(|| {
97                                InvalidError::new(format!(
98                                    "schema {id} has no message {message_type}"
99                                ))
100                            })?,
101                    )
102                }
103                _ => {
104                    return Err(InvalidError::new(format!(
105                        "schema {id} does not match codec {}",
106                        filter.codec
107                    )));
108                }
109            };
110            prepared.insert(*id, decoder);
111        }
112        Ok(Self { schemas: prepared })
113    }
114
115    pub fn decode(
116        &self,
117        codec: FilterCodec,
118        record: &FilterRecord<'_>,
119        limits: &DecodeLimits,
120    ) -> Result<Value, FaultReason> {
121        self.decode_paths(codec, record, limits, &PathTrie::whole())
122    }
123
124    pub(super) fn decode_paths(
125        &self,
126        codec: FilterCodec,
127        record: &FilterRecord<'_>,
128        limits: &DecodeLimits,
129        paths: &PathTrie,
130    ) -> Result<Value, FaultReason> {
131        if record.payload.len() > limits.max_payload_bytes {
132            return Err(FaultReason::TooLarge);
133        }
134        if codec == FilterCodec::Cbor {
135            return decode_cbor(record.payload, limits);
136        }
137        let id = record
138            .headers
139            .iter()
140            .rev()
141            .find(|header| header.key == crate::headers::SCHEMA_ID)
142            .and_then(|header| match header.value {
143                HeaderValueRef::Uint(id) => u32::try_from(id).ok(),
144                _ => None,
145            })
146            .ok_or(FaultReason::MissingSchema)?;
147        match self.schemas.get(&id).ok_or(FaultReason::SchemaNotAllowed)? {
148            WriterSchema::Avro(decoder) if codec == FilterCodec::Avro => {
149                let mut input = Input::new(record.payload, limits);
150                input.avro(
151                    decoder.borrow_schema(),
152                    decoder.borrow_names().get_names(),
153                    0,
154                )?;
155                input.finish()?;
156                let mut bytes = record.payload;
157                let decoded = decoder
158                    .borrow_reader()
159                    .read_value(&mut bytes)
160                    .map_err(|_| FaultReason::Malformed)?;
161                avro_value(decoded, paths)
162            }
163            WriterSchema::Protobuf(descriptor) if codec == FilterCodec::Protobuf => {
164                let mut input = Input::new(record.payload, limits);
165                input.protobuf(descriptor, 0)?;
166                let message = DynamicMessage::decode(descriptor.clone(), record.payload)
167                    .map_err(|_| FaultReason::Malformed)?;
168                protobuf_message(&message)
169            }
170            _ => Err(FaultReason::SchemaMismatch),
171        }
172    }
173}
174
175/// Decode exactly one self-describing CBOR item with bounded recursion.
176pub fn decode_cbor(payload: &[u8], limits: &DecodeLimits) -> Result<Value, FaultReason> {
177    if payload.len() > limits.max_payload_bytes {
178        return Err(FaultReason::TooLarge);
179    }
180    let mut checked = Input::new(payload, limits);
181    checked.cbor(0)?;
182    checked.finish()?;
183    let mut bytes = payload;
184    let value = ciborium::de::from_reader_with_recursion_limit::<super::cbor::CborValue, _>(
185        &mut bytes,
186        limits
187            .max_depth
188            .min(crate::limits::MAX_FILTER_PARSE_DEPTH)
189            .saturating_add(2),
190    )
191    .map_err(|error| match error {
192        ciborium::de::Error::RecursionLimitExceeded => FaultReason::TooDeep,
193        _ => FaultReason::Malformed,
194    })?;
195    if !bytes.is_empty() {
196        return Err(FaultReason::Malformed);
197    }
198    Ok(value.0)
199}
200
201#[cfg(test)]
202fn cbor_value(value: ciborium::Value) -> Result<Value, FaultReason> {
203    Ok(match value {
204        ciborium::Value::Null => Value::Null,
205        ciborium::Value::Bool(value) => Value::Bool(value),
206        ciborium::Value::Integer(value) => {
207            let integer = i128::from(value);
208            if let Ok(value) = i64::try_from(integer) {
209                Value::from(value)
210            } else if let Ok(value) = u64::try_from(integer) {
211                Value::from(value)
212            } else {
213                return Err(FaultReason::Malformed);
214            }
215        }
216        ciborium::Value::Float(value) => number(value)?,
217        ciborium::Value::Text(value) => Value::String(value),
218        ciborium::Value::Bytes(value) => bytes_value(&value),
219        ciborium::Value::Array(items) => Value::Array(
220            items
221                .into_iter()
222                .map(cbor_value)
223                .collect::<Result<_, _>>()?,
224        ),
225        ciborium::Value::Map(items) => {
226            let mut object = Map::new();
227            for (key, value) in items {
228                let ciborium::Value::Text(key) = key else {
229                    return Err(FaultReason::Malformed);
230                };
231                if object.insert(key, cbor_value(value)?).is_some() {
232                    return Err(FaultReason::Malformed);
233                }
234            }
235            Value::Object(object)
236        }
237        // Tags need an explicit semantic profile, never silently unwrap them.
238        _ => return Err(FaultReason::Malformed),
239    })
240}
241
242fn physical_avro_schema(schema: &str) -> Result<Schema, String> {
243    // Logical annotations describe producer interpretation. Comparisons use
244    // physical values and explicit coercions, identically in every SDK.
245    fn strip(value: &mut Value) {
246        match value {
247            Value::Object(map) => {
248                map.remove("logicalType");
249                for value in map.values_mut() {
250                    strip(value);
251                }
252            }
253            Value::Array(items) => {
254                for value in items {
255                    strip(value);
256                }
257            }
258            _ => {}
259        }
260    }
261    let mut value = super::eval::decode_json(
262        schema.as_bytes(),
263        &DecodeLimits {
264            max_payload_bytes: MAX_FILTER_SCHEMA_BYTES,
265            max_depth: MAX_FILTER_SCHEMA_DEPTH,
266        },
267    )
268    .map_err(|reason| format!("schema JSON is {reason}"))?;
269    strip(&mut value);
270    Schema::parse_str(&value.to_string()).map_err(|error| error.to_string())
271}
272
273fn number(value: f64) -> Result<Value, FaultReason> {
274    Number::from_f64(value)
275        .map(Value::Number)
276        .ok_or(FaultReason::Malformed)
277}
278
279fn bytes_value(bytes: &[u8]) -> Value {
280    Value::Array(bytes.iter().copied().map(Value::from).collect())
281}
282
283fn avro_value(value: apache_avro::types::Value, paths: &PathTrie) -> Result<Value, FaultReason> {
284    let whole = PathTrie::whole();
285    Ok(match value {
286        Avro::Null => Value::Null,
287        Avro::Boolean(value) => Value::Bool(value),
288        Avro::Int(value) => Value::from(value),
289        Avro::Long(value) => Value::from(value),
290        Avro::Float(value) => number(f64::from(value))?,
291        Avro::Double(value) => number(value)?,
292        Avro::String(value) | Avro::Enum(_, value) => Value::String(value),
293        Avro::Bytes(value) | Avro::Fixed(_, value) => bytes_value(&value),
294        Avro::Union(_, value) => avro_value(*value, paths)?,
295        Avro::Record(fields) => Value::Object(
296            fields
297                .into_iter()
298                .filter_map(|(key, value)| {
299                    let selected = if paths.whole {
300                        Some(&whole)
301                    } else {
302                        paths.child(&key)
303                    };
304                    selected.map(|child| avro_value(value, child).map(|value| (key, value)))
305                })
306                .collect::<Result<_, _>>()?,
307        ),
308        Avro::Map(fields) => Value::Object(
309            fields
310                .into_iter()
311                .filter_map(|(key, value)| {
312                    let selected = if paths.whole {
313                        Some(&whole)
314                    } else {
315                        paths.child(&key)
316                    };
317                    selected.map(|child| avro_value(value, child).map(|value| (key, value)))
318                })
319                .collect::<Result<_, _>>()?,
320        ),
321        Avro::Array(values) => {
322            let mut selected = Vec::new();
323            for (index, value) in values.into_iter().enumerate() {
324                let child = if paths.whole {
325                    Some(&whole)
326                } else {
327                    u32::try_from(index)
328                        .ok()
329                        .and_then(|index| paths.index(index))
330                };
331                if let Some(child) = child {
332                    selected.resize(index, Value::Null);
333                    selected.push(avro_value(value, child)?);
334                }
335            }
336            Value::Array(selected)
337        }
338        _ => return Err(FaultReason::Malformed),
339    })
340}
341
342// A field without presence tracking (a proto3 scalar or any repeated field)
343// is always part of the message, at its default when the bytes omit it, so
344// `counter == 0` and `present("values")` hold as they do for JSON and Avro.
345fn protobuf_message(message: &DynamicMessage) -> Result<Value, FaultReason> {
346    let mut object = Map::new();
347    for field in message.descriptor().fields() {
348        let set = message.has_field(&field);
349        if field.is_required() && !set {
350            return Err(FaultReason::Malformed);
351        }
352        if set || !field.supports_presence() {
353            object.insert(
354                field.name().to_owned(),
355                protobuf_value(&message.get_field(&field))?,
356            );
357        }
358    }
359    Ok(Value::Object(object))
360}
361
362fn protobuf_value(value: &Proto) -> Result<Value, FaultReason> {
363    Ok(match value {
364        Proto::Bool(value) => Value::Bool(*value),
365        Proto::I32(value) | Proto::EnumNumber(value) => Value::from(*value),
366        Proto::I64(value) => Value::from(*value),
367        Proto::U32(value) => Value::from(*value),
368        Proto::U64(value) => Value::from(*value),
369        Proto::F32(value) => number(f64::from(*value))?,
370        Proto::F64(value) => number(*value)?,
371        Proto::String(value) => Value::String(value.clone()),
372        Proto::Bytes(value) => bytes_value(value),
373        Proto::Message(value) => protobuf_message(value)?,
374        Proto::List(values) => Value::Array(
375            values
376                .iter()
377                .map(protobuf_value)
378                .collect::<Result<_, _>>()?,
379        ),
380        Proto::Map(values) => Value::Object(
381            values
382                .iter()
383                .map(|(key, value)| {
384                    let key = match key {
385                        MapKey::Bool(value) => value.to_string(),
386                        MapKey::I32(value) => value.to_string(),
387                        MapKey::I64(value) => value.to_string(),
388                        MapKey::U32(value) => value.to_string(),
389                        MapKey::U64(value) => value.to_string(),
390                        MapKey::String(value) => value.clone(),
391                    };
392                    Ok((key, protobuf_value(value)?))
393                })
394                .collect::<Result<_, FaultReason>>()?,
395        ),
396    })
397}
398
399// Validate nesting and collection sizes before allocating decoder trees.
400// Avro can represent huge arrays of null with only a few bytes.
401struct Input<'a> {
402    bytes: &'a [u8],
403    limits: &'a DecodeLimits,
404    remaining_values: usize,
405}
406
407impl<'a> Input<'a> {
408    fn new(bytes: &'a [u8], limits: &'a DecodeLimits) -> Self {
409        Self {
410            bytes,
411            limits,
412            remaining_values: limits.max_payload_bytes.max(1),
413        }
414    }
415
416    fn finish(&self) -> Result<(), FaultReason> {
417        if self.bytes.is_empty() {
418            Ok(())
419        } else {
420            Err(FaultReason::Malformed)
421        }
422    }
423
424    fn take(&mut self, count: usize) -> Result<&'a [u8], FaultReason> {
425        let result = self.bytes.get(..count).ok_or(FaultReason::Malformed)?;
426        self.bytes = &self.bytes[count..];
427        Ok(result)
428    }
429
430    fn varint(&mut self) -> Result<u64, FaultReason> {
431        let mut result = 0u64;
432        for shift in (0..70).step_by(7) {
433            let byte = self.take(1)?[0];
434            if shift == 63 && byte > 1 {
435                return Err(FaultReason::Malformed);
436            }
437            result |= u64::from(byte & 127) << shift;
438            if byte < 128 {
439                return Ok(result);
440            }
441        }
442        Err(FaultReason::Malformed)
443    }
444
445    fn long(&mut self) -> Result<i64, FaultReason> {
446        let raw = self.varint()?;
447        Ok(((raw >> 1) as i64) ^ -((raw & 1) as i64))
448    }
449
450    fn sized(&mut self) -> Result<(), FaultReason> {
451        let count = usize::try_from(self.long()?).map_err(|_| FaultReason::Malformed)?;
452        self.take(count)?;
453        Ok(())
454    }
455
456    fn visit(&mut self, depth: usize) -> Result<(), FaultReason> {
457        if depth
458            > self
459                .limits
460                .max_depth
461                .min(crate::limits::MAX_FILTER_PARSE_DEPTH)
462        {
463            return Err(FaultReason::TooDeep);
464        }
465        self.remaining_values = self
466            .remaining_values
467            .checked_sub(1)
468            .ok_or(FaultReason::TooLarge)?;
469        Ok(())
470    }
471
472    fn avro(
473        &mut self,
474        schema: &Schema,
475        names: &apache_avro::schema::NamesRef<'_>,
476        depth: usize,
477    ) -> Result<(), FaultReason> {
478        self.visit(depth)?;
479        match schema {
480            Schema::Null => {}
481            Schema::Boolean => {
482                if self.take(1)?[0] > 1 {
483                    return Err(FaultReason::Malformed);
484                }
485            }
486            Schema::Int
487            | Schema::Long
488            | Schema::Date
489            | Schema::TimeMillis
490            | Schema::TimeMicros
491            | Schema::TimestampMillis
492            | Schema::TimestampMicros
493            | Schema::TimestampNanos
494            | Schema::LocalTimestampMillis
495            | Schema::LocalTimestampMicros
496            | Schema::LocalTimestampNanos
497            | Schema::Enum(_) => {
498                self.long()?;
499            }
500            Schema::Float => {
501                let bytes: [u8; 4] = self
502                    .take(4)?
503                    .try_into()
504                    .map_err(|_| FaultReason::Malformed)?;
505                if !f32::from_le_bytes(bytes).is_finite() {
506                    return Err(FaultReason::Malformed);
507                }
508            }
509            Schema::Double => {
510                let bytes: [u8; 8] = self
511                    .take(8)?
512                    .try_into()
513                    .map_err(|_| FaultReason::Malformed)?;
514                if !f64::from_le_bytes(bytes).is_finite() {
515                    return Err(FaultReason::Malformed);
516                }
517            }
518            Schema::Bytes | Schema::String | Schema::BigDecimal => self.sized()?,
519            Schema::Fixed(fixed) | Schema::Duration(fixed) => {
520                self.take(fixed.size)?;
521            }
522            Schema::Decimal(decimal) => match &decimal.inner {
523                InnerDecimalSchema::Bytes => self.sized()?,
524                InnerDecimalSchema::Fixed(fixed) => {
525                    self.take(fixed.size)?;
526                }
527            },
528            Schema::Uuid(uuid) => match uuid {
529                UuidSchema::Fixed(fixed) => {
530                    self.take(fixed.size)?;
531                }
532                _ => self.sized()?,
533            },
534            Schema::Union(union) => {
535                let index = usize::try_from(self.long()?).map_err(|_| FaultReason::Malformed)?;
536                self.avro(
537                    union.variants().get(index).ok_or(FaultReason::Malformed)?,
538                    names,
539                    depth + 1,
540                )?;
541            }
542            Schema::Record(record) => {
543                for field in &record.fields {
544                    self.avro(&field.schema, names, depth + 1)?;
545                }
546            }
547            Schema::Ref { name } => {
548                self.avro(names.get(name).ok_or(FaultReason::Malformed)?, names, depth)?
549            }
550            Schema::Array(array) => self.blocks(&array.items, names, depth, false)?,
551            Schema::Map(map) => self.blocks(&map.types, names, depth, true)?,
552        }
553        Ok(())
554    }
555
556    fn blocks(
557        &mut self,
558        item: &Schema,
559        names: &apache_avro::schema::NamesRef<'_>,
560        depth: usize,
561        map: bool,
562    ) -> Result<(), FaultReason> {
563        loop {
564            let count = self.long()?;
565            if count == 0 {
566                return Ok(());
567            }
568            let block_end = if count < 0 {
569                let size = usize::try_from(self.long()?).map_err(|_| FaultReason::Malformed)?;
570                Some(
571                    self.bytes
572                        .len()
573                        .checked_sub(size)
574                        .ok_or(FaultReason::Malformed)?,
575                )
576            } else {
577                None
578            };
579            let count = usize::try_from(count.checked_abs().ok_or(FaultReason::TooLarge)?)
580                .map_err(|_| FaultReason::TooLarge)?;
581            if count > self.remaining_values {
582                return Err(FaultReason::TooLarge);
583            }
584            for _ in 0..count {
585                if map {
586                    self.sized()?;
587                }
588                self.avro(item, names, depth + 1)?;
589            }
590            if block_end.is_some_and(|end| self.bytes.len() != end) {
591                return Err(FaultReason::Malformed);
592            }
593        }
594    }
595
596    fn cbor(&mut self, depth: usize) -> Result<(), FaultReason> {
597        self.visit(depth)?;
598        let head = self.take(1)?[0];
599        let major = head >> 5;
600        let info = head & 31;
601        if major == 6 || info == 31 {
602            return Err(FaultReason::Malformed);
603        }
604        let mut value = u64::from(info);
605        if info >= 24 {
606            if info > 27 {
607                return Err(FaultReason::Malformed);
608            }
609            value = 0;
610            for byte in self.take(1 << (info - 24))? {
611                value = (value << 8) | u64::from(*byte);
612            }
613        }
614        if major == 2 || major == 3 {
615            self.take(usize::try_from(value).map_err(|_| FaultReason::Malformed)?)?;
616        }
617        if major == 4 || major == 5 {
618            let count = value
619                .checked_mul(if major == 5 { 2 } else { 1 })
620                .ok_or(FaultReason::Malformed)?;
621            if count > self.bytes.len() as u64 {
622                return Err(FaultReason::Malformed);
623            }
624            for _ in 0..count {
625                self.cbor(depth + 1)?;
626            }
627        }
628        Ok(())
629    }
630
631    fn protobuf(
632        &mut self,
633        descriptor: &MessageDescriptor,
634        depth: usize,
635    ) -> Result<(), FaultReason> {
636        self.visit(depth)?;
637        while !self.bytes.is_empty() {
638            let key = self.varint()?;
639            let field = u32::try_from(key >> 3).map_err(|_| FaultReason::Malformed)?;
640            if field == 0 || field > 0x1fff_ffff {
641                return Err(FaultReason::Malformed);
642            }
643            match key & 7 {
644                0 => {
645                    self.varint()?;
646                }
647                1 => {
648                    self.take(8)?;
649                }
650                2 => {
651                    let count =
652                        usize::try_from(self.varint()?).map_err(|_| FaultReason::TooLarge)?;
653                    let data = self.take(count)?;
654                    if let Some(field) = descriptor.get_field(field)
655                        && let Kind::Message(nested) = field.kind()
656                    {
657                        let mut child = Self {
658                            bytes: data,
659                            limits: self.limits,
660                            remaining_values: self.remaining_values,
661                        };
662                        child.protobuf(&nested, depth + 1)?;
663                        self.remaining_values = child.remaining_values;
664                    }
665                }
666                5 => {
667                    self.take(4)?;
668                }
669                _ => return Err(FaultReason::Malformed),
670            }
671        }
672        Ok(())
673    }
674}
675
676#[cfg(test)]
677mod cbor_tests {
678    use super::*;
679    use ciborium::Value as Cbor;
680
681    #[test]
682    fn given_cbor_scalars_and_collections_when_decoded_directly_then_should_match_the_previous_value_rules()
683     {
684        let values = [
685            Cbor::Integer(i64::MIN.into()),
686            Cbor::Integer(u64::MAX.into()),
687            Cbor::Float(-0.0),
688            Cbor::Float(0.25),
689            Cbor::Float(f64::NAN),
690            Cbor::Float(f64::INFINITY),
691            Cbor::Bytes(vec![0, 128, 255]),
692            Cbor::Text("λ".to_owned()),
693            Cbor::Array(vec![
694                Cbor::Null,
695                Cbor::Bool(true),
696                Cbor::Array(vec![Cbor::Integer(7.into())]),
697            ]),
698            Cbor::Map(vec![(
699                Cbor::Text("key".to_owned()),
700                Cbor::Integer(42.into()),
701            )]),
702            Cbor::Map(vec![(Cbor::Integer(1.into()), Cbor::Null)]),
703            Cbor::Map(vec![
704                (Cbor::Text("key".to_owned()), Cbor::Null),
705                (Cbor::Text("key".to_owned()), Cbor::Bool(true)),
706            ]),
707            Cbor::Tag(1, Box::new(Cbor::Integer(0.into()))),
708        ];
709        for value in values {
710            let expected = cbor_value(value.clone());
711            let mut bytes = Vec::new();
712            ciborium::ser::into_writer(&value, &mut bytes).expect("CBOR fixture");
713            assert_eq!(
714                decode_cbor(&bytes, &DecodeLimits::default()),
715                expected,
716                "{value:?}"
717            );
718        }
719    }
720
721    #[test]
722    fn given_cbor_bytes_when_decoded_then_should_preserve_exact_unsigned_values_and_byte_arrays() {
723        let bytes = [
724            0x82, 0x1b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x42, 0x00, 0xff,
725        ];
726        assert_eq!(
727            decode_cbor(&bytes, &DecodeLimits::default()).expect("bounded CBOR"),
728            serde_json::json!([u64::MAX, [0, 255]])
729        );
730        for malformed in [
731            &[0x61, 0xff][..],
732            &[0x9a, 1, 0, 0, 0][..],
733            &[0x3b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff][..],
734        ] {
735            assert_eq!(
736                decode_cbor(malformed, &DecodeLimits::default()),
737                Err(FaultReason::Malformed)
738            );
739        }
740    }
741}