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