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