Skip to main content

spvirit_codec/
spvd_decode.rs

1//! PVD (pvData) Type Introspection and Value Decoding
2//!
3//! Implements parsing of PVAccess field descriptions and value decoding
4//! according to the pvData serialization specification.
5
6use std::fmt;
7use tracing::debug;
8
9/// Re-export the free-standing `decode_string` from `epics_decode` for
10/// discoverability alongside the other decode helpers in this module.
11pub use crate::epics_decode::decode_string;
12
13/// PVD type codes from the specification
14#[repr(u8)]
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum TypeCode {
17    Null = 0xFF,
18    Boolean = 0x00,
19    Int8 = 0x20,
20    Int16 = 0x21,
21    Int32 = 0x22,
22    Int64 = 0x23,
23    UInt8 = 0x24,
24    UInt16 = 0x25,
25    UInt32 = 0x26,
26    UInt64 = 0x27,
27    Float32 = 0x42,
28    Float64 = 0x43,
29    String = 0x60,
30    // Bounded string has 0x83 prefix followed by size
31    Variant = 0xFE, // Union with no fixed type (0xFF is Null)
32}
33
34impl TypeCode {
35    pub fn from_byte(b: u8) -> Option<Self> {
36        // Clear scalar-array mode bits (variable/bounded/fixed array)
37        let base = b & 0xE7;
38        match base {
39            0x00 => Some(TypeCode::Boolean),
40            0x20 => Some(TypeCode::Int8),
41            0x21 => Some(TypeCode::Int16),
42            0x22 => Some(TypeCode::Int32),
43            0x23 => Some(TypeCode::Int64),
44            0x24 => Some(TypeCode::UInt8),
45            0x25 => Some(TypeCode::UInt16),
46            0x26 => Some(TypeCode::UInt32),
47            0x27 => Some(TypeCode::UInt64),
48            0x42 => Some(TypeCode::Float32),
49            0x43 => Some(TypeCode::Float64),
50            0x60 => Some(TypeCode::String),
51            _ => None,
52        }
53    }
54
55    pub fn size(&self) -> Option<usize> {
56        match self {
57            TypeCode::Boolean | TypeCode::Int8 | TypeCode::UInt8 => Some(1),
58            TypeCode::Int16 | TypeCode::UInt16 => Some(2),
59            TypeCode::Int32 | TypeCode::UInt32 | TypeCode::Float32 => Some(4),
60            TypeCode::Int64 | TypeCode::UInt64 | TypeCode::Float64 => Some(8),
61            TypeCode::String | TypeCode::Null | TypeCode::Variant => None,
62        }
63    }
64}
65
66/// Field type description
67#[derive(Debug, Clone)]
68pub enum FieldType {
69    Scalar(TypeCode),
70    ScalarArray(TypeCode),
71    String,
72    StringArray,
73    Structure(StructureDesc),
74    StructureArray(StructureDesc),
75    Union(Vec<FieldDesc>),
76    UnionArray(Vec<FieldDesc>),
77    Variant,
78    VariantArray,
79    BoundedString(u32),
80}
81
82impl FieldType {
83    pub fn type_name(&self) -> &'static str {
84        match self {
85            FieldType::Scalar(tc) => match tc {
86                TypeCode::Boolean => "boolean",
87                TypeCode::Int8 => "byte",
88                TypeCode::Int16 => "short",
89                TypeCode::Int32 => "int",
90                TypeCode::Int64 => "long",
91                TypeCode::UInt8 => "ubyte",
92                TypeCode::UInt16 => "ushort",
93                TypeCode::UInt32 => "uint",
94                TypeCode::UInt64 => "ulong",
95                TypeCode::Float32 => "float",
96                TypeCode::Float64 => "double",
97                TypeCode::String => "string",
98                _ => "unknown",
99            },
100            FieldType::ScalarArray(tc) => match tc {
101                TypeCode::Float64 => "double[]",
102                TypeCode::Float32 => "float[]",
103                TypeCode::Int64 => "long[]",
104                TypeCode::Int32 => "int[]",
105                _ => "array",
106            },
107            FieldType::String => "string",
108            FieldType::StringArray => "string[]",
109            FieldType::Structure(_) => "structure",
110            FieldType::StructureArray(_) => "structure[]",
111            FieldType::Union(_) => "union",
112            FieldType::UnionArray(_) => "union[]",
113            FieldType::Variant => "any",
114            FieldType::VariantArray => "any[]",
115            FieldType::BoundedString(_) => "string",
116        }
117    }
118}
119
120/// Field description (name + type)
121#[derive(Debug, Clone)]
122pub struct FieldDesc {
123    pub name: String,
124    pub field_type: FieldType,
125}
126
127/// Structure description with optional ID
128#[derive(Debug, Clone)]
129pub struct StructureDesc {
130    pub struct_id: Option<String>,
131    pub fields: Vec<FieldDesc>,
132}
133
134impl StructureDesc {
135    pub fn new() -> Self {
136        Self {
137            struct_id: None,
138            fields: Vec::new(),
139        }
140    }
141
142    /// Look up a field by name.
143    pub fn field(&self, name: &str) -> Option<&FieldDesc> {
144        self.fields.iter().find(|f| f.name == name)
145    }
146}
147
148impl Default for StructureDesc {
149    fn default() -> Self {
150        Self::new()
151    }
152}
153
154impl fmt::Display for StructureDesc {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        fn write_indent(f: &mut fmt::Formatter<'_>, depth: usize) -> fmt::Result {
157            for _ in 0..depth {
158                write!(f, "    ")?;
159            }
160            Ok(())
161        }
162
163        fn write_field_type(
164            f: &mut fmt::Formatter<'_>,
165            ft: &FieldType,
166            depth: usize,
167        ) -> fmt::Result {
168            match ft {
169                FieldType::Structure(desc) => write_structure(f, desc, depth),
170                FieldType::StructureArray(desc) => {
171                    write_structure(f, desc, depth)?;
172                    write!(f, "[]")
173                }
174                FieldType::Union(fields) => {
175                    writeln!(f, "union")?;
176                    for field in fields {
177                        write_indent(f, depth + 1)?;
178                        write!(f, "{} ", field.name)?;
179                        write_field_type(f, &field.field_type, depth + 1)?;
180                        writeln!(f)?;
181                    }
182                    Ok(())
183                }
184                FieldType::UnionArray(fields) => {
185                    writeln!(f, "union[]")?;
186                    for field in fields {
187                        write_indent(f, depth + 1)?;
188                        write!(f, "{} ", field.name)?;
189                        write_field_type(f, &field.field_type, depth + 1)?;
190                        writeln!(f)?;
191                    }
192                    Ok(())
193                }
194                other => write!(f, "{}", other.type_name()),
195            }
196        }
197
198        fn write_structure(
199            f: &mut fmt::Formatter<'_>,
200            desc: &StructureDesc,
201            depth: usize,
202        ) -> fmt::Result {
203            if let Some(id) = &desc.struct_id {
204                write!(f, "structure «{}»", id)?;
205            } else {
206                write!(f, "structure")?;
207            }
208            if desc.fields.is_empty() {
209                return Ok(());
210            }
211            writeln!(f)?;
212            for field in &desc.fields {
213                write_indent(f, depth + 1)?;
214                write!(f, "{} ", field.name)?;
215                write_field_type(f, &field.field_type, depth + 1)?;
216                writeln!(f)?;
217            }
218            Ok(())
219        }
220
221        write_structure(f, self, 0)
222    }
223}
224
225/// Decoded value
226#[derive(Debug, Clone)]
227pub enum DecodedValue {
228    Null,
229    Boolean(bool),
230    Int8(i8),
231    Int16(i16),
232    Int32(i32),
233    Int64(i64),
234    UInt8(u8),
235    UInt16(u16),
236    UInt32(u32),
237    UInt64(u64),
238    Float32(f32),
239    Float64(f64),
240    String(String),
241    Array(Vec<DecodedValue>),
242    Structure(Vec<(String, DecodedValue)>),
243    Raw(Vec<u8>),
244}
245
246impl fmt::Display for DecodedValue {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        match self {
249            DecodedValue::Null => write!(f, "null"),
250            DecodedValue::Boolean(v) => write!(f, "{}", v),
251            DecodedValue::Int8(v) => write!(f, "{}", v),
252            DecodedValue::Int16(v) => write!(f, "{}", v),
253            DecodedValue::Int32(v) => write!(f, "{}", v),
254            DecodedValue::Int64(v) => write!(f, "{}", v),
255            DecodedValue::UInt8(v) => write!(f, "{}", v),
256            DecodedValue::UInt16(v) => write!(f, "{}", v),
257            DecodedValue::UInt32(v) => write!(f, "{}", v),
258            DecodedValue::UInt64(v) => write!(f, "{}", v),
259            DecodedValue::Float32(v) => write!(f, "{:.6}", v),
260            DecodedValue::Float64(v) => write!(f, "{:.6}", v),
261            DecodedValue::String(v) => write!(f, "\"{}\"", v),
262            DecodedValue::Array(arr) => {
263                write!(f, "[")?;
264                for (i, v) in arr.iter().enumerate() {
265                    if i > 0 {
266                        write!(f, ", ")?;
267                    }
268                    if i >= 5 {
269                        write!(f, "... ({} total)", arr.len())?;
270                        break;
271                    }
272                    write!(f, "{}", v)?;
273                }
274                write!(f, "]")
275            }
276            DecodedValue::Structure(fields) => {
277                write!(f, "{{")?;
278                for (i, (name, val)) in fields.iter().enumerate() {
279                    if i > 0 {
280                        write!(f, ", ")?;
281                    }
282                    write!(f, "{}={}", name, val)?;
283                }
284                write!(f, "}}")
285            }
286            DecodedValue::Raw(data) => {
287                if data.len() <= 8 {
288                    write!(f, "<{} bytes: {}>", data.len(), hex::encode(data))
289                } else {
290                    write!(f, "<{} bytes>", data.len())
291                }
292            }
293        }
294    }
295}
296
297/// PVD Decoder state
298pub struct PvdDecoder {
299    is_be: bool,
300}
301
302impl PvdDecoder {
303    pub fn new(is_be: bool) -> Self {
304        Self { is_be }
305    }
306
307    /// Decode a size value (PVA variable-length encoding)
308    pub fn decode_size(&self, data: &[u8]) -> Option<(usize, usize)> {
309        if data.is_empty() {
310            return None;
311        }
312        let first = data[0];
313        if first == 0xFF {
314            // Special: -1 (null)
315            return Some((0, 1)); // Treat as 0 for simplicity
316        }
317        if first < 254 {
318            return Some((first as usize, 1));
319        }
320        if first == 254 {
321            // 4-byte size follows
322            if data.len() < 5 {
323                return None;
324            }
325            let size = if self.is_be {
326                u32::from_be_bytes([data[1], data[2], data[3], data[4]]) as usize
327            } else {
328                u32::from_le_bytes([data[1], data[2], data[3], data[4]]) as usize
329            };
330            return Some((size, 5));
331        }
332        // first == 255 is null marker, handled above.
333        None
334    }
335
336    /// Decode a string
337    pub fn decode_string(&self, data: &[u8]) -> Option<(String, usize)> {
338        let (size, size_bytes) = self.decode_size(data)?;
339        if size == 0 {
340            return Some((String::new(), size_bytes));
341        }
342        if data.len() < size_bytes + size {
343            return None;
344        }
345        let s = std::str::from_utf8(&data[size_bytes..size_bytes + size]).ok()?;
346        Some((s.to_string(), size_bytes + size))
347    }
348
349    /// Parse field description from introspection data
350    pub fn parse_field_desc(&self, data: &[u8]) -> Option<(FieldDesc, usize)> {
351        if data.is_empty() {
352            return None;
353        }
354
355        let mut offset = 0;
356
357        // Parse field name
358        let (name, consumed) = self.decode_string(&data[offset..])?;
359        offset += consumed;
360
361        if offset >= data.len() {
362            return None;
363        }
364
365        // Parse type descriptor
366        let (field_type, type_consumed) = self.parse_type_desc(&data[offset..])?;
367        offset += type_consumed;
368
369        Some((FieldDesc { name, field_type }, offset))
370    }
371
372    /// Parse type descriptor
373    fn parse_type_desc(&self, data: &[u8]) -> Option<(FieldType, usize)> {
374        if data.is_empty() {
375            return None;
376        }
377
378        let type_byte = data[0];
379        let mut offset = 1;
380
381        // Check for NULL type
382        if type_byte == 0xFF {
383            return Some((FieldType::Variant, 1));
384        }
385
386        // Full-with-id from IntrospectionRegistry:
387        // 0xFD + int16 key + type descriptor payload.
388        if type_byte == 0xFD {
389            if data.len() < 4 {
390                return None;
391            }
392            // Preferred parsing path: skip tag + int16 key.
393            if let Some((field_type, consumed)) = self.parse_type_desc(&data[3..]) {
394                return Some((field_type, 3 + consumed));
395            }
396            // Legacy fallback for older non-keyed streams.
397            if let Some((field_type, consumed)) = self.parse_type_desc(&data[1..]) {
398                return Some((field_type, 1 + consumed));
399            }
400            return None;
401        }
402
403        // Only-id from IntrospectionRegistry:
404        // 0xFE + int16 key, requires connection-level registry state.
405        if type_byte == 0xFE {
406            debug!("Type descriptor uses ONLY_ID (0xFE) without registry context");
407            return None;
408        }
409
410        // Check for structure (0x80) or structure array (0x88)
411        if type_byte == 0x80 || type_byte == 0x88 {
412            let is_array = (type_byte & 0x08) != 0;
413            if is_array {
414                // Skip the inner structure element tag (0x80)
415                if offset >= data.len() || data[offset] != 0x80 {
416                    return None;
417                }
418                offset += 1;
419            }
420            let (struct_desc, consumed) = self.parse_structure_desc(&data[offset..])?;
421            offset += consumed;
422            if is_array {
423                return Some((FieldType::StructureArray(struct_desc), offset));
424            } else {
425                return Some((FieldType::Structure(struct_desc), offset));
426            }
427        }
428
429        // Check for union (0x81) or union array (0x89)
430        if type_byte == 0x81 || type_byte == 0x89 {
431            let is_array = (type_byte & 0x08) != 0;
432            if is_array {
433                // Skip the inner union element tag (0x81)
434                if offset >= data.len() || data[offset] != 0x81 {
435                    return None;
436                }
437                offset += 1;
438            }
439            // Parse union fields (same as structure)
440            let (struct_desc, consumed) = self.parse_structure_desc(&data[offset..])?;
441            offset += consumed;
442            if is_array {
443                return Some((FieldType::UnionArray(struct_desc.fields), offset));
444            } else {
445                return Some((FieldType::Union(struct_desc.fields), offset));
446            }
447        }
448
449        // Check for variant/any (0x82) or variant array (0x8A)
450        if type_byte == 0x82 {
451            return Some((FieldType::Variant, 1));
452        }
453        if type_byte == 0x8A {
454            return Some((FieldType::VariantArray, 1));
455        }
456
457        // Check for bounded string (0x83, legacy 0x86 accepted for compatibility)
458        if type_byte == 0x83 || type_byte == 0x86 {
459            let (bound, consumed) = self.decode_size(&data[offset..])?;
460            offset += consumed;
461            return Some((FieldType::BoundedString(bound as u32), offset));
462        }
463
464        // Scalar / scalar-array with mode bits:
465        // 0x00=not-array, 0x08=variable, 0x10=bounded, 0x18=fixed
466        let scalar_or_array = type_byte & 0x18;
467        let is_array = scalar_or_array != 0;
468        if is_array && scalar_or_array != 0x08 {
469            // Consume bounded/fixed max length for alignment, even if we don't model it.
470            let (_bound, consumed) = self.decode_size(&data[offset..])?;
471            offset += consumed;
472        }
473        let base_type = type_byte & 0xE7;
474
475        // String type
476        if base_type == 0x60 {
477            if is_array {
478                return Some((FieldType::StringArray, offset));
479            } else {
480                return Some((FieldType::String, offset));
481            }
482        }
483
484        // Numeric types
485        if let Some(tc) = TypeCode::from_byte(base_type) {
486            if is_array {
487                return Some((FieldType::ScalarArray(tc), offset));
488            } else {
489                return Some((FieldType::Scalar(tc), offset));
490            }
491        }
492
493        debug!("Unknown type byte: 0x{:02x}", type_byte);
494        None
495    }
496
497    /// Parse structure description
498    fn parse_structure_desc(&self, data: &[u8]) -> Option<(StructureDesc, usize)> {
499        let mut offset = 0;
500
501        // Parse optional struct ID
502        let (struct_id, consumed) = self.decode_string(&data[offset..])?;
503        offset += consumed;
504
505        let struct_id = if struct_id.is_empty() {
506            None
507        } else {
508            Some(struct_id)
509        };
510
511        // Parse field count
512        let (field_count, consumed) = self.decode_size(&data[offset..])?;
513        offset += consumed;
514
515        let mut fields = Vec::with_capacity(field_count);
516
517        for _ in 0..field_count {
518            if offset >= data.len() {
519                break;
520            }
521            if let Some((field, consumed)) = self.parse_field_desc(&data[offset..]) {
522                offset += consumed;
523                fields.push(field);
524            } else {
525                break;
526            }
527        }
528
529        Some((StructureDesc { struct_id, fields }, offset))
530    }
531
532    /// Parse the full type introspection from INIT response
533    pub fn parse_introspection(&self, data: &[u8]) -> Option<StructureDesc> {
534        self.parse_introspection_with_len(data)
535            .map(|(desc, _)| desc)
536    }
537
538    /// Parse full type introspection and return consumed bytes.
539    pub fn parse_introspection_with_len(&self, data: &[u8]) -> Option<(StructureDesc, usize)> {
540        if data.is_empty() {
541            return None;
542        }
543
544        // The introspection starts with a type byte
545        let type_byte = data[0];
546
547        // Should be a structure (0x80)
548        if type_byte == 0x80 {
549            let (desc, consumed) = self.parse_structure_desc(&data[1..])?;
550            return Some((desc, 1 + consumed));
551        }
552
553        // Full-with-id from IntrospectionRegistry:
554        // 0xFD + int16 key + field type descriptor payload.
555        if type_byte == 0xFD {
556            if data.len() < 4 {
557                return None;
558            }
559            // Preferred parsing path: skip tag + int16 key.
560            if let Some((desc, consumed)) = self.parse_introspection_with_len(&data[3..]) {
561                return Some((desc, 3 + consumed));
562            }
563            // Legacy fallback for older non-keyed streams.
564            if let Some((desc, consumed)) = self.parse_introspection_with_len(&data[1..]) {
565                return Some((desc, 1 + consumed));
566            }
567            return None;
568        }
569
570        // Only-id from IntrospectionRegistry:
571        // 0xFE + int16 key, requires connection-level registry state.
572        if type_byte == 0xFE {
573            debug!(
574                "Introspection uses ONLY_ID (0xFE), but no registry is available in this decoder"
575            );
576            return None;
577        }
578
579        debug!("Unexpected introspection type byte: 0x{:02x}", type_byte);
580        None
581    }
582
583    /// Decode a scalar value
584    fn decode_scalar(&self, data: &[u8], tc: TypeCode) -> Option<(DecodedValue, usize)> {
585        let size = tc.size()?;
586        if data.len() < size {
587            return None;
588        }
589
590        let value = match tc {
591            TypeCode::Boolean => DecodedValue::Boolean(data[0] != 0),
592            TypeCode::Int8 => DecodedValue::Int8(data[0] as i8),
593            TypeCode::UInt8 => DecodedValue::UInt8(data[0]),
594            TypeCode::Int16 => {
595                let v = if self.is_be {
596                    i16::from_be_bytes([data[0], data[1]])
597                } else {
598                    i16::from_le_bytes([data[0], data[1]])
599                };
600                DecodedValue::Int16(v)
601            }
602            TypeCode::UInt16 => {
603                let v = if self.is_be {
604                    u16::from_be_bytes([data[0], data[1]])
605                } else {
606                    u16::from_le_bytes([data[0], data[1]])
607                };
608                DecodedValue::UInt16(v)
609            }
610            TypeCode::Int32 => {
611                let v = if self.is_be {
612                    i32::from_be_bytes(data[0..4].try_into().unwrap())
613                } else {
614                    i32::from_le_bytes(data[0..4].try_into().unwrap())
615                };
616                DecodedValue::Int32(v)
617            }
618            TypeCode::UInt32 => {
619                let v = if self.is_be {
620                    u32::from_be_bytes(data[0..4].try_into().unwrap())
621                } else {
622                    u32::from_le_bytes(data[0..4].try_into().unwrap())
623                };
624                DecodedValue::UInt32(v)
625            }
626            TypeCode::Int64 => {
627                let v = if self.is_be {
628                    i64::from_be_bytes(data[0..8].try_into().unwrap())
629                } else {
630                    i64::from_le_bytes(data[0..8].try_into().unwrap())
631                };
632                DecodedValue::Int64(v)
633            }
634            TypeCode::UInt64 => {
635                let v = if self.is_be {
636                    u64::from_be_bytes(data[0..8].try_into().unwrap())
637                } else {
638                    u64::from_le_bytes(data[0..8].try_into().unwrap())
639                };
640                DecodedValue::UInt64(v)
641            }
642            TypeCode::Float32 => {
643                let v = if self.is_be {
644                    f32::from_be_bytes(data[0..4].try_into().unwrap())
645                } else {
646                    f32::from_le_bytes(data[0..4].try_into().unwrap())
647                };
648                DecodedValue::Float32(v)
649            }
650            TypeCode::Float64 => {
651                let v = if self.is_be {
652                    f64::from_be_bytes(data[0..8].try_into().unwrap())
653                } else {
654                    f64::from_le_bytes(data[0..8].try_into().unwrap())
655                };
656                DecodedValue::Float64(v)
657            }
658            _ => return None,
659        };
660
661        Some((value, size))
662    }
663
664    /// Decode value according to field type
665    pub fn decode_value(
666        &self,
667        data: &[u8],
668        field_type: &FieldType,
669    ) -> Option<(DecodedValue, usize)> {
670        match field_type {
671            FieldType::Scalar(tc) => self.decode_scalar(data, *tc),
672            FieldType::String | FieldType::BoundedString(_) => {
673                let (s, consumed) = self.decode_string(data)?;
674                Some((DecodedValue::String(s), consumed))
675            }
676            FieldType::ScalarArray(tc) => {
677                let (count, size_consumed) = self.decode_size(data)?;
678                let mut offset = size_consumed;
679                let limit = count.min(4_000_000);
680                let mut values = Vec::with_capacity(limit);
681                let elem_size = tc.size().unwrap_or(1);
682                for _ in 0..limit {
683                    if let Some((val, consumed)) = self.decode_scalar(&data[offset..], *tc) {
684                        values.push(val);
685                        offset += consumed;
686                    } else {
687                        break;
688                    }
689                }
690                // Skip past any remaining elements we didn't store, so the
691                // stream stays aligned for the next field.
692                let remaining = count.saturating_sub(limit);
693                offset += remaining * elem_size;
694                Some((DecodedValue::Array(values), offset))
695            }
696            FieldType::StringArray => {
697                let (count, size_consumed) = self.decode_size(data)?;
698                let mut offset = size_consumed;
699                let max_items = count.min(4096);
700                let mut values = Vec::with_capacity(max_items);
701                for _ in 0..max_items {
702                    if let Some((s, consumed)) = self.decode_string(&data[offset..]) {
703                        values.push(DecodedValue::String(s));
704                        offset += consumed;
705                    } else {
706                        break;
707                    }
708                }
709                Some((DecodedValue::Array(values), offset))
710            }
711            FieldType::Structure(desc) => self.decode_structure(data, desc),
712            FieldType::StructureArray(desc) => {
713                let (count, size_consumed) = self.decode_size(data)?;
714                let mut offset = size_consumed;
715                let mut values = Vec::with_capacity(count.min(256));
716                for _ in 0..count.min(256) {
717                    // Read per-element null indicator (0 = null, non-zero = present)
718                    if offset >= data.len() {
719                        return None;
720                    }
721                    let null_indicator = data[offset];
722                    offset += 1;
723                    if null_indicator == 0 {
724                        // null element – push empty structure placeholder
725                        values.push(DecodedValue::Structure(Vec::new()));
726                        continue;
727                    }
728                    let (item, consumed) = self.decode_structure(&data[offset..], desc)?;
729                    values.push(item);
730                    offset += consumed;
731                }
732                Some((DecodedValue::Array(values), offset))
733            }
734            FieldType::Union(fields) => {
735                let (selector, consumed) = self.decode_size(data)?;
736                let field = fields.get(selector)?;
737                let (value, val_consumed) =
738                    self.decode_value(&data[consumed..], &field.field_type)?;
739                Some((
740                    DecodedValue::Structure(vec![(field.name.clone(), value)]),
741                    consumed + val_consumed,
742                ))
743            }
744            FieldType::UnionArray(fields) => {
745                let (count, size_consumed) = self.decode_size(data)?;
746                let mut offset = size_consumed;
747                let mut values = Vec::with_capacity(count.min(128));
748                for _ in 0..count.min(128) {
749                    let (selector, consumed) = self.decode_size(&data[offset..])?;
750                    offset += consumed;
751                    let field = fields.get(selector)?;
752                    let (value, val_consumed) =
753                        self.decode_value(&data[offset..], &field.field_type)?;
754                    offset += val_consumed;
755                    values.push(DecodedValue::Structure(vec![(field.name.clone(), value)]));
756                }
757                Some((DecodedValue::Array(values), offset))
758            }
759            FieldType::Variant => {
760                if data.is_empty() {
761                    return None;
762                }
763                if data[0] == 0xFF {
764                    return Some((DecodedValue::Null, 1));
765                }
766                let (variant_type, type_consumed) = self.parse_type_desc(data)?;
767                let (variant_value, value_consumed) =
768                    self.decode_value(&data[type_consumed..], &variant_type)?;
769                Some((variant_value, type_consumed + value_consumed))
770            }
771            FieldType::VariantArray => {
772                let (count, size_consumed) = self.decode_size(data)?;
773                let mut offset = size_consumed;
774                let mut values = Vec::with_capacity(count.min(128));
775                for _ in 0..count.min(128) {
776                    let (v, consumed) = self.decode_value(&data[offset..], &FieldType::Variant)?;
777                    values.push(v);
778                    offset += consumed;
779                }
780                Some((DecodedValue::Array(values), offset))
781            }
782        }
783    }
784
785    /// Decode a structure value using the field descriptions
786    pub fn decode_structure(
787        &self,
788        data: &[u8],
789        desc: &StructureDesc,
790    ) -> Option<(DecodedValue, usize)> {
791        let mut offset = 0;
792        let mut fields: Vec<(String, DecodedValue)> = Vec::new();
793
794        for field in &desc.fields {
795            if offset >= data.len() {
796                break;
797            }
798            if let Some((value, consumed)) = self.decode_value(&data[offset..], &field.field_type) {
799                fields.push((field.name.clone(), value));
800                offset += consumed;
801            } else {
802                // Can't decode this field, stop
803                break;
804            }
805        }
806
807        Some((DecodedValue::Structure(fields), offset))
808    }
809
810    /// Decode a structure with a bitset indicating which fields are present
811    /// This is used for delta updates in MONITOR
812    pub fn decode_structure_with_bitset(
813        &self,
814        data: &[u8],
815        desc: &StructureDesc,
816    ) -> Option<(DecodedValue, usize)> {
817        if data.is_empty() {
818            return None;
819        }
820
821        let mut offset = 0;
822
823        // Parse the bitset - PVA uses size-encoded bitset
824        let (bitset_size, size_consumed) = self.decode_size(data)?;
825        offset += size_consumed;
826
827        if bitset_size == 0 || offset + bitset_size > data.len() {
828            return Some((DecodedValue::Structure(vec![]), offset));
829        }
830
831        let bitset = &data[offset..offset + bitset_size];
832        offset += bitset_size;
833
834        let (value, consumed) =
835            self.decode_structure_with_bitset_body(&data[offset..], desc, bitset)?;
836        Some((value, offset + consumed))
837    }
838
839    /// Decode a structure with changed and overrun bitsets (MONITOR updates)
840    pub fn decode_structure_with_bitset_and_overrun(
841        &self,
842        data: &[u8],
843        desc: &StructureDesc,
844    ) -> Option<(DecodedValue, usize)> {
845        if data.is_empty() {
846            return None;
847        }
848        let mut offset = 0usize;
849        let (changed_size, consumed1) = self.decode_size(&data[offset..])?;
850        offset += consumed1;
851        if offset + changed_size > data.len() {
852            return None;
853        }
854        let changed = &data[offset..offset + changed_size];
855        offset += changed_size;
856
857        let (overrun_size, consumed2) = self.decode_size(&data[offset..])?;
858        offset += consumed2;
859        if offset + overrun_size > data.len() {
860            return None;
861        }
862        offset += overrun_size;
863
864        let (value, consumed) =
865            self.decode_structure_with_bitset_body(&data[offset..], desc, changed)?;
866        Some((value, offset + consumed))
867    }
868
869    /// Decode a structure with changed bitset, data, then overrun bitset (spec order)
870    pub fn decode_structure_with_bitset_then_overrun(
871        &self,
872        data: &[u8],
873        desc: &StructureDesc,
874    ) -> Option<(DecodedValue, usize)> {
875        if data.is_empty() {
876            return None;
877        }
878        let mut offset = 0usize;
879        let (changed_size, consumed1) = self.decode_size(&data[offset..])?;
880        offset += consumed1;
881        if offset + changed_size > data.len() {
882            return None;
883        }
884        let changed = &data[offset..offset + changed_size];
885        offset += changed_size;
886
887        let (value, consumed) =
888            self.decode_structure_with_bitset_body(&data[offset..], desc, changed)?;
889        offset += consumed;
890
891        let (overrun_size, consumed2) = self.decode_size(&data[offset..])?;
892        offset += consumed2;
893        if offset + overrun_size > data.len() {
894            return None;
895        }
896        offset += overrun_size;
897
898        Some((value, offset))
899    }
900
901    fn decode_structure_with_bitset_body(
902        &self,
903        data: &[u8],
904        desc: &StructureDesc,
905        bitset: &[u8],
906    ) -> Option<(DecodedValue, usize)> {
907        // Bit 0 is for the whole structure, field bits start at bit 1
908        debug!(
909            "Bitset: {:02x?} (size={}), total_fields={}",
910            bitset,
911            bitset.len(),
912            count_structure_fields(desc)
913        );
914        debug!(
915            "Structure fields: {:?}",
916            desc.fields.iter().map(|f| &f.name).collect::<Vec<_>>()
917        );
918
919        // Special case: bitset contains only bit0 (whole structure) and no field bits.
920        let mut has_field_bits = false;
921        if !bitset.is_empty() {
922            for (i, b) in bitset.iter().enumerate() {
923                let mask = if i == 0 { *b & !0x01 } else { *b };
924                if mask != 0 {
925                    has_field_bits = true;
926                    break;
927                }
928            }
929        }
930        if !has_field_bits && !bitset.is_empty() && (bitset[0] & 0x01) != 0 {
931            if let Some((value, consumed)) = self.decode_structure(data, desc) {
932                return Some((value, consumed));
933            }
934        }
935
936        let mut fields: Vec<(String, DecodedValue)> = Vec::new();
937        let mut offset = 0usize;
938
939        fn decode_with_bitset_recursive(
940            decoder: &PvdDecoder,
941            data: &[u8],
942            offset: &mut usize,
943            desc: &StructureDesc,
944            bitset: &[u8],
945            bit_offset: &mut usize,
946            fields: &mut Vec<(String, DecodedValue)>,
947        ) -> bool {
948            for field in &desc.fields {
949                let byte_idx = *bit_offset / 8;
950                let bit_idx = *bit_offset % 8;
951                let current_bit = *bit_offset;
952                *bit_offset += 1;
953
954                let field_present = if byte_idx < bitset.len() {
955                    (bitset[byte_idx] & (1 << bit_idx)) != 0
956                } else {
957                    false
958                };
959
960                debug!(
961                    "Field '{}' at bit {}: present={}",
962                    field.name, current_bit, field_present
963                );
964
965                if let FieldType::Structure(nested_desc) = &field.field_type {
966                    let child_start_bit = *bit_offset;
967                    let child_field_count = count_structure_fields(nested_desc);
968
969                    let mut any_child_bits_set = false;
970                    for i in 0..child_field_count {
971                        let check_byte = (child_start_bit + i) / 8;
972                        let check_bit = (child_start_bit + i) % 8;
973                        if check_byte < bitset.len() && (bitset[check_byte] & (1 << check_bit)) != 0
974                        {
975                            any_child_bits_set = true;
976                            break;
977                        }
978                    }
979
980                    debug!(
981                        "Nested structure '{}': parent_present={}, child_start_bit={}, child_count={}, any_child_bits_set={}",
982                        field.name,
983                        field_present,
984                        child_start_bit,
985                        child_field_count,
986                        any_child_bits_set
987                    );
988
989                    if field_present && !any_child_bits_set {
990                        *bit_offset += child_field_count;
991                        if *offset < data.len() {
992                            if let Some((value, consumed)) =
993                                decoder.decode_structure(&data[*offset..], nested_desc)
994                            {
995                                debug!(
996                                    "Decoded full nested structure '{}', consumed {} bytes",
997                                    field.name, consumed
998                                );
999                                fields.push((field.name.clone(), value));
1000                                *offset += consumed;
1001                            } else {
1002                                debug!("Failed to decode full nested structure '{}'", field.name);
1003                                return false;
1004                            }
1005                        }
1006                    } else if any_child_bits_set {
1007                        let mut nested_fields: Vec<(String, DecodedValue)> = Vec::new();
1008                        if !decode_with_bitset_recursive(
1009                            decoder,
1010                            data,
1011                            offset,
1012                            nested_desc,
1013                            bitset,
1014                            bit_offset,
1015                            &mut nested_fields,
1016                        ) {
1017                            return false;
1018                        }
1019                        debug!(
1020                            "Nested structure '{}' decoded {} fields",
1021                            field.name,
1022                            nested_fields.len()
1023                        );
1024                        if !nested_fields.is_empty() {
1025                            fields
1026                                .push((field.name.clone(), DecodedValue::Structure(nested_fields)));
1027                        }
1028                    } else {
1029                        *bit_offset += child_field_count;
1030                    }
1031                } else if field_present {
1032                    if *offset >= data.len() {
1033                        debug!(
1034                            "Data exhausted at offset {} for field '{}'",
1035                            *offset, field.name
1036                        );
1037                        return false;
1038                    }
1039                    if let Some((value, consumed)) =
1040                        decoder.decode_value(&data[*offset..], &field.field_type)
1041                    {
1042                        fields.push((field.name.clone(), value));
1043                        *offset += consumed;
1044                    } else {
1045                        return false;
1046                    }
1047                }
1048            }
1049            true
1050        }
1051
1052        let mut bit_offset = 1;
1053        decode_with_bitset_recursive(
1054            self,
1055            data,
1056            &mut offset,
1057            desc,
1058            bitset,
1059            &mut bit_offset,
1060            &mut fields,
1061        );
1062        Some((DecodedValue::Structure(fields), offset))
1063    }
1064}
1065
1066/// Count total fields in a structure (including nested)
1067fn count_structure_fields(desc: &StructureDesc) -> usize {
1068    let mut count = 0;
1069    for field in &desc.fields {
1070        count += 1;
1071        if let FieldType::Structure(nested) = &field.field_type {
1072            count += count_structure_fields(nested);
1073        }
1074    }
1075    count
1076}
1077
1078/// Extract a sub-field from a StructureDesc by dot-separated path.
1079/// Returns the sub-field as an owned StructureDesc. For leaf (non-structure)
1080/// fields, returns a single-field StructureDesc wrapping the matched field.
1081/// Returns the full desc if path is empty.
1082pub fn extract_subfield_desc(desc: &StructureDesc, path: &str) -> Option<StructureDesc> {
1083    if path.is_empty() {
1084        return Some(desc.clone());
1085    }
1086    let mut parts = path.splitn(2, '.');
1087    let head = parts.next()?;
1088    let tail = parts.next().unwrap_or("");
1089    for field in &desc.fields {
1090        if field.name == head {
1091            match &field.field_type {
1092                FieldType::Structure(nested) | FieldType::StructureArray(nested) => {
1093                    return extract_subfield_desc(nested, tail);
1094                }
1095                _ => {
1096                    if tail.is_empty() {
1097                        return Some(StructureDesc {
1098                            struct_id: None,
1099                            fields: vec![field.clone()],
1100                        });
1101                    }
1102                    return None;
1103                }
1104            }
1105        }
1106    }
1107    None
1108}
1109
1110/// Format a structure description for display
1111pub fn format_structure_desc(desc: &StructureDesc) -> String {
1112    let mut parts = Vec::new();
1113    if let Some(ref id) = desc.struct_id {
1114        parts.push(id.clone());
1115    }
1116    for field in &desc.fields {
1117        parts.push(format!("{}:{}", field.name, field.field_type.type_name()));
1118    }
1119    parts.join(", ")
1120}
1121
1122pub fn format_structure_tree(desc: &StructureDesc) -> String {
1123    fn push_fields(out: &mut Vec<String>, fields: &[FieldDesc], indent: usize) {
1124        let prefix = "  ".repeat(indent);
1125        for field in fields {
1126            match &field.field_type {
1127                FieldType::Structure(nested) => {
1128                    out.push(format!("{}{}: structure", prefix, field.name));
1129                    push_fields(out, &nested.fields, indent + 1);
1130                }
1131                FieldType::StructureArray(nested) => {
1132                    out.push(format!("{}{}: structure[]", prefix, field.name));
1133                    push_fields(out, &nested.fields, indent + 1);
1134                }
1135                FieldType::Union(variants) => {
1136                    out.push(format!("{}{}: union", prefix, field.name));
1137                    push_fields(out, variants, indent + 1);
1138                }
1139                FieldType::UnionArray(variants) => {
1140                    out.push(format!("{}{}: union[]", prefix, field.name));
1141                    push_fields(out, variants, indent + 1);
1142                }
1143                FieldType::BoundedString(bound) => {
1144                    out.push(format!("{}{}: string<={}", prefix, field.name, bound));
1145                }
1146                _ => {
1147                    out.push(format!(
1148                        "{}{}: {}",
1149                        prefix,
1150                        field.name,
1151                        field.field_type.type_name()
1152                    ));
1153                }
1154            }
1155        }
1156    }
1157
1158    let mut lines = Vec::new();
1159    if let Some(id) = &desc.struct_id {
1160        lines.push(format!("struct {}", id));
1161    } else {
1162        lines.push("struct <anonymous>".to_string());
1163    }
1164    push_fields(&mut lines, &desc.fields, 0);
1165    lines.join("\n")
1166}
1167
1168/// Extract the "value" field from a decoded NTScalar structure
1169pub fn extract_nt_scalar_value(decoded: &DecodedValue) -> Option<&DecodedValue> {
1170    if let DecodedValue::Structure(fields) = decoded {
1171        for (name, value) in fields {
1172            if name == "value" {
1173                return Some(value);
1174            }
1175        }
1176    }
1177    None
1178}
1179
1180/// Compact display of decoded value for logging - shows only updated fields concisely
1181pub fn format_compact_value(decoded: &DecodedValue) -> String {
1182    match decoded {
1183        DecodedValue::Structure(fields) => {
1184            if fields.is_empty() {
1185                return "{}".to_string();
1186            }
1187
1188            let mut parts = Vec::new();
1189
1190            for (name, val) in fields {
1191                let formatted = format_field_value_compact(name, val);
1192                if !formatted.is_empty() {
1193                    parts.push(formatted);
1194                }
1195            }
1196
1197            parts.join(", ")
1198        }
1199        _ => format!("{}", decoded),
1200    }
1201}
1202
1203/// Format a single field value compactly - shows key info for known structures
1204fn format_field_value_compact(name: &str, val: &DecodedValue) -> String {
1205    match val {
1206        DecodedValue::Structure(fields) => {
1207            // For known EPICS NTScalar structures, show only key fields
1208            match name {
1209                "alarm" => {
1210                    // Show severity and message if non-zero/non-empty
1211                    let severity = fields.iter().find(|(n, _)| n == "severity");
1212                    let message = fields.iter().find(|(n, _)| n == "message");
1213                    let mut parts = Vec::new();
1214                    if let Some((_, DecodedValue::Int32(s))) = severity {
1215                        if *s != 0 {
1216                            parts.push(format!("sev={}", s));
1217                        }
1218                    }
1219                    if let Some((_, DecodedValue::String(m))) = message {
1220                        if !m.is_empty() {
1221                            parts.push(format!("\"{}\"", m));
1222                        }
1223                    }
1224                    if parts.is_empty() {
1225                        String::new() // Don't show alarm if it's OK
1226                    } else {
1227                        format!("alarm={{{}}}", parts.join(", "))
1228                    }
1229                }
1230                "timeStamp" => {
1231                    // Show just seconds or skip entirely for brevity
1232                    let secs = fields.iter().find(|(n, _)| n == "secondsPastEpoch");
1233                    if let Some((_, DecodedValue::Int64(s))) = secs {
1234                        format!("ts={}", s)
1235                    } else {
1236                        String::new()
1237                    }
1238                }
1239                "display" | "control" | "valueAlarm" => {
1240                    // Skip verbose metadata structures in compact view
1241                    String::new()
1242                }
1243                _ => {
1244                    // For other structures, show all fields
1245                    let nested: Vec<String> = fields
1246                        .iter()
1247                        .map(|(n, v)| format!("{}={}", n, format_scalar_value(v)))
1248                        .collect();
1249
1250                    if nested.is_empty() {
1251                        String::new()
1252                    } else {
1253                        format!("{}={{{}}}", name, nested.join(", "))
1254                    }
1255                }
1256            }
1257        }
1258        _ => {
1259            format!("{}={}", name, format_scalar_value(val))
1260        }
1261    }
1262}
1263
1264/// Format a scalar value concisely
1265fn format_scalar_value(val: &DecodedValue) -> String {
1266    match val {
1267        DecodedValue::Null => "null".to_string(),
1268        DecodedValue::Boolean(v) => format!("{}", v),
1269        DecodedValue::Int8(v) => format!("{}", v),
1270        DecodedValue::Int16(v) => format!("{}", v),
1271        DecodedValue::Int32(v) => format!("{}", v),
1272        DecodedValue::Int64(v) => format!("{}", v),
1273        DecodedValue::UInt8(v) => format!("{}", v),
1274        DecodedValue::UInt16(v) => format!("{}", v),
1275        DecodedValue::UInt32(v) => format!("{}", v),
1276        DecodedValue::UInt64(v) => format!("{}", v),
1277        DecodedValue::Float32(v) => format!("{:.4}", v),
1278        DecodedValue::Float64(v) => format!("{:.6}", v),
1279        DecodedValue::String(v) => format!("\"{}\"", v),
1280        DecodedValue::Array(arr) => {
1281            if arr.is_empty() {
1282                "[]".to_string()
1283            } else if arr.len() <= 3 {
1284                let items: Vec<String> = arr.iter().map(|v| format_scalar_value(v)).collect();
1285                format!("[{}]", items.join(", "))
1286            } else {
1287                format!("[{} items]", arr.len())
1288            }
1289        }
1290        DecodedValue::Structure(fields) => {
1291            let nested: Vec<String> = fields
1292                .iter()
1293                .map(|(n, v)| format!("{}={}", n, format_scalar_value(v)))
1294                .collect();
1295            format!("{{{}}}", nested.join(", "))
1296        }
1297        DecodedValue::Raw(data) => {
1298            if data.len() <= 4 {
1299                format!("<{}>", hex::encode(data))
1300            } else {
1301                format!("<{}B>", data.len())
1302            }
1303        }
1304    }
1305}
1306
1307#[cfg(test)]
1308mod tests {
1309    use super::*;
1310
1311    #[test]
1312    fn test_decode_size() {
1313        let decoder = PvdDecoder::new(false);
1314
1315        // Small size (single byte)
1316        assert_eq!(decoder.decode_size(&[5]), Some((5, 1)));
1317        assert_eq!(decoder.decode_size(&[253]), Some((253, 1)));
1318
1319        // Medium/large size (5 bytes, 254 prefix + uint32)
1320        assert_eq!(
1321            decoder.decode_size(&[254, 0x00, 0x01, 0x00, 0x00]),
1322            Some((256, 5))
1323        );
1324    }
1325
1326    #[test]
1327    fn test_parse_introspection_full_with_id() {
1328        let decoder = PvdDecoder::new(false);
1329        let data = vec![
1330            0xFD, // FULL_WITH_ID
1331            0x06, 0x00, // registry key (little-endian)
1332            0x80, // structure type follows
1333            0x00, // empty struct id
1334            0x01, // one field
1335            0x05, b'v', b'a', b'l', b'u', b'e', // field name
1336            0x43, // float64
1337        ];
1338        let desc = decoder
1339            .parse_introspection(&data)
1340            .expect("parsed introspection");
1341        assert_eq!(desc.fields.len(), 1);
1342        assert_eq!(desc.fields[0].name, "value");
1343        match desc.fields[0].field_type {
1344            FieldType::Scalar(TypeCode::Float64) => {}
1345            _ => panic!("expected float64 value field"),
1346        }
1347    }
1348
1349    #[test]
1350    fn test_decode_string() {
1351        let decoder = PvdDecoder::new(false);
1352
1353        // Empty string
1354        assert_eq!(decoder.decode_string(&[0]), Some((String::new(), 1)));
1355
1356        // "hello"
1357        let data = [5, b'h', b'e', b'l', b'l', b'o'];
1358        assert_eq!(decoder.decode_string(&data), Some(("hello".to_string(), 6)));
1359    }
1360
1361    #[test]
1362    fn decode_variant_accepts_full_with_id_type_tag() {
1363        let decoder = PvdDecoder::new(false);
1364        // Variant payload: 0xFD + int16 key + string type + "ok"
1365        let data = [0xFD, 0x02, 0x00, 0x60, 0x02, b'o', b'k'];
1366        let (value, consumed) = decoder
1367            .decode_value(&data, &FieldType::Variant)
1368            .expect("decode variant");
1369        assert_eq!(consumed, data.len());
1370        assert!(matches!(value, DecodedValue::String(ref s) if s == "ok"));
1371    }
1372
1373    #[test]
1374    fn test_decode_bitset_whole_structure() {
1375        let decoder = PvdDecoder::new(false);
1376        let desc = StructureDesc {
1377            struct_id: None,
1378            fields: vec![FieldDesc {
1379                name: "value".to_string(),
1380                field_type: FieldType::Scalar(TypeCode::Float64),
1381            }],
1382        };
1383        // bitset_size=1, bitset=0x01 (whole structure), then float64 value.
1384        let mut data = Vec::new();
1385        data.push(0x01);
1386        data.push(0x01);
1387        data.extend_from_slice(&1.25f64.to_le_bytes());
1388
1389        let (decoded, _consumed) = decoder
1390            .decode_structure_with_bitset(&data, &desc)
1391            .expect("decoded");
1392        if let DecodedValue::Structure(fields) = decoded {
1393            assert_eq!(fields.len(), 1);
1394            assert_eq!(fields[0].0, "value");
1395        } else {
1396            panic!("expected structure");
1397        }
1398    }
1399
1400    #[test]
1401    fn format_structure_tree_includes_nested_fields() {
1402        let desc = StructureDesc {
1403            struct_id: Some("epics:nt/NTScalar:1.0".to_string()),
1404            fields: vec![
1405                FieldDesc {
1406                    name: "value".to_string(),
1407                    field_type: FieldType::Scalar(TypeCode::Float64),
1408                },
1409                FieldDesc {
1410                    name: "alarm".to_string(),
1411                    field_type: FieldType::Structure(StructureDesc {
1412                        struct_id: None,
1413                        fields: vec![
1414                            FieldDesc {
1415                                name: "severity".to_string(),
1416                                field_type: FieldType::Scalar(TypeCode::Int32),
1417                            },
1418                            FieldDesc {
1419                                name: "message".to_string(),
1420                                field_type: FieldType::String,
1421                            },
1422                        ],
1423                    }),
1424                },
1425            ],
1426        };
1427
1428        let rendered = format_structure_tree(&desc);
1429        assert!(rendered.contains("struct epics:nt/NTScalar:1.0"));
1430        assert!(rendered.contains("value: double"));
1431        assert!(rendered.contains("alarm: structure"));
1432        assert!(rendered.contains("severity: int"));
1433        assert!(rendered.contains("message: string"));
1434    }
1435
1436    #[test]
1437    fn decode_string_array_not_capped_at_100_items() {
1438        fn encode_size(size: usize) -> Vec<u8> {
1439            if size == 0 {
1440                return vec![0x00];
1441            }
1442            if size < 254 {
1443                return vec![size as u8];
1444            }
1445            let mut out = vec![0xFE];
1446            out.extend_from_slice(&(size as u32).to_le_bytes());
1447            out
1448        }
1449
1450        let item_count = 150usize;
1451        let mut raw = encode_size(item_count);
1452        for idx in 0..item_count {
1453            let s = format!("PV:{}", idx);
1454            raw.extend_from_slice(&encode_size(s.len()));
1455            raw.extend_from_slice(s.as_bytes());
1456        }
1457
1458        let decoder = PvdDecoder::new(false);
1459        let (decoded, _consumed) = decoder
1460            .decode_value(&raw, &FieldType::StringArray)
1461            .expect("decoded");
1462
1463        let DecodedValue::Array(items) = decoded else {
1464            panic!("expected decoded array");
1465        };
1466        assert_eq!(items.len(), item_count);
1467    }
1468}