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