Skip to main content

spvirit_codec/
epics_decode.rs

1// Refer to https://github.com/mdavidsaver/cashark/blob/master/pva.lua
2
3// Lookup table for PVA commands
4// -- application messages
5
6use hex;
7use std::fmt;
8use tracing::debug;
9
10use crate::spvd_decode::{DecodedValue, PvdDecoder, StructureDesc, format_compact_value};
11use crate::spvirit_encode::format_pva_address;
12
13/// Single source of truth for PVA application command codes.
14///
15/// Index == command code.  Any code beyond the table returns `"Unknown"`.
16const PVA_COMMAND_NAMES: &[&str] = &[
17    "BEACON",                // 0
18    "CONNECTION_VALIDATION", // 1
19    "ECHO",                  // 2
20    "SEARCH",                // 3
21    "SEARCH_RESPONSE",       // 4
22    "AUTHNZ",                // 5
23    "ACL_CHANGE",            // 6
24    "CREATE_CHANNEL",        // 7
25    "DESTROY_CHANNEL",       // 8
26    "CONNECTION_VALIDATED",  // 9
27    "GET",                   // 10
28    "PUT",                   // 11
29    "PUT_GET",               // 12
30    "MONITOR",               // 13
31    "ARRAY",                 // 14
32    "DESTROY_REQUEST",       // 15
33    "PROCESS",               // 16
34    "GET_FIELD",             // 17
35    "MESSAGE",               // 18
36    "MULTIPLE_DATA",         // 19
37    "RPC",                   // 20
38    "CANCEL_REQUEST",        // 21
39    "ORIGIN_TAG",            // 22
40];
41
42/// Look up a PVA command name by its numeric code.
43pub fn command_name(code: u8) -> &'static str {
44    PVA_COMMAND_NAMES
45        .get(code as usize)
46        .copied()
47        .unwrap_or("Unknown")
48}
49
50/// Look up a PVA command code by its name.  Returns 255 for unknown names.
51pub fn command_to_integer(command: &str) -> u8 {
52    PVA_COMMAND_NAMES
53        .iter()
54        .position(|&name| name == command)
55        .map(|i| i as u8)
56        .unwrap_or(255)
57}
58
59/// Convenience wrapper that matches the pre-existing `PvaCommands` API.
60/// Prefer calling [`command_name`] directly for new code.
61#[derive(Debug)]
62pub struct PvaCommands;
63
64impl PvaCommands {
65    pub fn new() -> Self {
66        Self
67    }
68
69    pub fn get_command(&self, code: u8) -> &'static str {
70        command_name(code)
71    }
72}
73#[derive(Debug)]
74pub struct PvaControlFlags {
75    pub raw: u8,
76    // bits 0 is specifies application or control message (0 or 1 resprectively)
77    // bits 1,2,3, must always be zero
78    // bits 5 and 4 specify if the message is segmented 00 = not segmented, 01 = first segment, 10 = last segment, 11 = in-the-middle segment
79    // bit 6 specifies the direction of the message (0 = client, 1 = server)
80    // bit 7 specifies the byte order (0 = LSB, 1 = MSB)
81    pub is_application: bool,
82    pub is_control: bool,
83    pub is_segmented: u8,
84    pub is_first_segment: bool,
85    pub is_last_segment: bool,
86    pub is_middle_segment: bool,
87    pub is_client: bool,
88    pub is_server: bool,
89    pub is_lsb: bool,
90    pub is_msb: bool,
91    pub is_valid: bool,
92}
93
94impl PvaControlFlags {
95    pub fn new(raw: u8) -> Self {
96        let is_application = (raw & 0x01) == 0; // Bit 0: 0 for application, 1 for control
97        let is_control = (raw & 0x01) != 0; // Bit 0: 1 for control
98        let is_segmented = (raw & 0x30) >> 4; // Bits 5 and 4
99        let is_first_segment = is_segmented == 0x01; // 01
100        let is_last_segment = is_segmented == 0x02; // 10
101        let is_middle_segment = is_segmented == 0x03; // 11
102        let is_client = (raw & 0x40) == 0; // Bit 6: 0 for client, 1 for server
103        let is_server = (raw & 0x40) != 0; // Bit 6: 1 for server
104        let is_lsb = (raw & 0x80) == 0; // Bit 7: 0 for LSB, 1 for MSB
105        let is_msb = (raw & 0x80) != 0; // Bit 7: 1 for MSB
106        let is_valid = (raw & 0x0E) == 0; // Bits 1,2,3 must be zero
107
108        Self {
109            raw,
110            is_application,
111            is_control,
112            is_segmented,
113            is_first_segment,
114            is_last_segment,
115            is_middle_segment,
116            is_client,
117            is_server,
118            is_lsb,
119            is_msb,
120            is_valid,
121        }
122    }
123    fn is_valid(&self) -> bool {
124        self.is_valid
125    }
126}
127#[derive(Debug)]
128pub struct PvaHeader {
129    pub magic: u8,
130    pub version: u8,
131    pub flags: PvaControlFlags,
132    pub command: u8,
133    pub payload_length: u32,
134}
135
136impl PvaHeader {
137    pub fn new(raw: &[u8]) -> Self {
138        Self::try_new(raw).expect("PVA header requires at least 8 bytes")
139    }
140
141    pub fn try_new(raw: &[u8]) -> Option<Self> {
142        if raw.len() < 8 {
143            return None;
144        }
145        let magic = raw[0];
146        let version = raw[1];
147        let flags = PvaControlFlags::new(raw[2]);
148        let command: u8 = raw[3];
149        let payload_length_bytes: [u8; 4] = raw[4..8]
150            .try_into()
151            .expect("Slice for payload_length has incorrect length");
152        let payload_length = if flags.is_msb {
153            u32::from_be_bytes(payload_length_bytes)
154        } else {
155            u32::from_le_bytes(payload_length_bytes)
156        };
157
158        Some(Self {
159            magic,
160            version,
161            flags,
162            command,
163            payload_length,
164        })
165    }
166    pub fn is_valid(&self) -> bool {
167        self.magic == 0xCA && self.flags.is_valid()
168    }
169}
170
171#[derive(Debug)]
172pub enum PvaPacketCommand {
173    Control(PvaControlPayload),
174    Search(PvaSearchPayload),
175    SearchResponse(PvaSearchResponsePayload),
176    Beacon(PvaBeaconPayload),
177    ConnectionValidation(PvaConnectionValidationPayload),
178    ConnectionValidated(PvaConnectionValidatedPayload),
179    AuthNZ(PvaAuthNzPayload),
180    AclChange(PvaAclChangePayload),
181    Op(PvaOpPayload),
182    CreateChannel(PvaCreateChannelPayload),
183    DestroyChannel(PvaDestroyChannelPayload),
184    GetField(PvaGetFieldPayload),
185    Message(PvaMessagePayload),
186    MultipleData(PvaMultipleDataPayload),
187    CancelRequest(PvaCancelRequestPayload),
188    DestroyRequest(PvaDestroyRequestPayload),
189    OriginTag(PvaOriginTagPayload),
190    Echo(Vec<u8>),
191    Unknown(PvaUnknownPayload),
192}
193#[derive(Debug)]
194pub struct PvaPacket {
195    pub header: PvaHeader,
196    pub payload: Vec<u8>,
197}
198
199impl PvaPacket {
200    pub fn new(raw: &[u8]) -> Self {
201        let header = PvaHeader::new(raw);
202        let payload = raw.to_vec();
203        Self { header, payload }
204    }
205    pub fn decode_payload(&mut self) -> Option<PvaPacketCommand> {
206        let pva_header_size = 8;
207        if self.payload.len() < pva_header_size {
208            debug!("Packet too short to contain a PVA payload beyond the header.");
209            return None;
210        }
211
212        let expected_total_len = if self.header.flags.is_control {
213            pva_header_size
214        } else {
215            pva_header_size + self.header.payload_length as usize
216        };
217        if self.payload.len() < expected_total_len {
218            debug!(
219                "Packet data length {} is less than expected total length {} (header {} + payload_length {})",
220                self.payload.len(),
221                expected_total_len,
222                pva_header_size,
223                self.header.payload_length
224            );
225            return None;
226        }
227
228        let command_payload_slice = &self.payload[pva_header_size..expected_total_len];
229
230        if self.header.flags.is_control {
231            return Some(PvaPacketCommand::Control(PvaControlPayload::new(
232                self.header.command,
233                self.header.payload_length,
234            )));
235        }
236
237        let decoded = match self.header.command {
238            0 => PvaBeaconPayload::new(command_payload_slice, self.header.flags.is_msb)
239                .map(PvaPacketCommand::Beacon),
240            2 => Some(PvaPacketCommand::Echo(command_payload_slice.to_vec())),
241            1 => PvaConnectionValidationPayload::new(
242                command_payload_slice,
243                self.header.flags.is_msb,
244                self.header.flags.is_server,
245            )
246            .map(PvaPacketCommand::ConnectionValidation),
247            3 => PvaSearchPayload::new(command_payload_slice, self.header.flags.is_msb)
248                .map(PvaPacketCommand::Search),
249            4 => PvaSearchResponsePayload::new(command_payload_slice, self.header.flags.is_msb)
250                .map(PvaPacketCommand::SearchResponse),
251            5 => PvaAuthNzPayload::new(command_payload_slice, self.header.flags.is_msb)
252                .map(PvaPacketCommand::AuthNZ),
253            6 => PvaAclChangePayload::new(command_payload_slice, self.header.flags.is_msb)
254                .map(PvaPacketCommand::AclChange),
255            7 => PvaCreateChannelPayload::new(
256                command_payload_slice,
257                self.header.flags.is_msb,
258                self.header.flags.is_server,
259            )
260            .map(PvaPacketCommand::CreateChannel),
261            8 => PvaDestroyChannelPayload::new(command_payload_slice, self.header.flags.is_msb)
262                .map(PvaPacketCommand::DestroyChannel),
263            9 => {
264                PvaConnectionValidatedPayload::new(command_payload_slice, self.header.flags.is_msb)
265                    .map(PvaPacketCommand::ConnectionValidated)
266            }
267            10 | 11 | 12 | 13 | 14 | 16 | 20 => PvaOpPayload::new(
268                command_payload_slice,
269                self.header.flags.is_msb,
270                self.header.flags.is_server,
271                self.header.command,
272            )
273            .map(PvaPacketCommand::Op),
274            15 => PvaDestroyRequestPayload::new(command_payload_slice, self.header.flags.is_msb)
275                .map(PvaPacketCommand::DestroyRequest),
276            17 => PvaGetFieldPayload::new(
277                command_payload_slice,
278                self.header.flags.is_msb,
279                self.header.flags.is_server,
280            )
281            .map(PvaPacketCommand::GetField),
282            18 => PvaMessagePayload::new(command_payload_slice, self.header.flags.is_msb)
283                .map(PvaPacketCommand::Message),
284            19 => PvaMultipleDataPayload::new(command_payload_slice, self.header.flags.is_msb)
285                .map(PvaPacketCommand::MultipleData),
286            21 => PvaCancelRequestPayload::new(command_payload_slice, self.header.flags.is_msb)
287                .map(PvaPacketCommand::CancelRequest),
288            22 => PvaOriginTagPayload::new(command_payload_slice).map(PvaPacketCommand::OriginTag),
289            _ => None,
290        };
291
292        if let Some(cmd) = decoded {
293            Some(cmd)
294        } else {
295            debug!(
296                "Decoding not implemented or unknown command: {}",
297                self.header.command
298            );
299            Some(PvaPacketCommand::Unknown(PvaUnknownPayload::new(
300                self.header.command,
301                false,
302                command_payload_slice.len(),
303            )))
304        }
305    }
306
307    pub fn is_valid(&self) -> bool {
308        self.header.is_valid()
309    }
310}
311
312/// helpers
313pub fn decode_size(raw: &[u8], is_be: bool) -> Option<(usize, usize)> {
314    if raw.is_empty() {
315        return None;
316    }
317
318    match raw[0] {
319        255 => Some((0, 1)),
320        254 => {
321            if raw.len() < 5 {
322                return None;
323            }
324            let size_bytes = &raw[1..5];
325            let size = if is_be {
326                u32::from_be_bytes(size_bytes.try_into().unwrap())
327            } else {
328                u32::from_le_bytes(size_bytes.try_into().unwrap())
329            };
330            Some((size as usize, 5))
331        }
332        short_len => Some((short_len as usize, 1)),
333    }
334}
335
336// decoding string using the above helper
337pub fn decode_string(raw: &[u8], is_be: bool) -> Option<(String, usize)> {
338    let (size, offset) = decode_size(raw, is_be)?;
339    let total_len = offset + size;
340    if raw.len() < total_len {
341        return None;
342    }
343
344    let string_bytes = &raw[offset..total_len];
345    let s = String::from_utf8_lossy(string_bytes).to_string();
346    Some((s, total_len))
347}
348
349pub fn decode_status(raw: &[u8], is_be: bool) -> (Option<PvaStatus>, usize) {
350    if raw.is_empty() {
351        return (None, 0);
352    }
353    let code = raw[0];
354    if code == 0xff {
355        return (None, 1);
356    }
357    let mut idx = 1usize;
358    let mut message: Option<String> = None;
359    let mut stack: Option<String> = None;
360    if let Some((msg, consumed)) = decode_string(&raw[idx..], is_be) {
361        message = Some(msg);
362        idx += consumed;
363        if let Some((st, consumed2)) = decode_string(&raw[idx..], is_be) {
364            stack = Some(st);
365            idx += consumed2;
366        }
367    }
368    (
369        Some(PvaStatus {
370            code,
371            message,
372            stack,
373        }),
374        idx,
375    )
376}
377
378pub fn decode_op_response_status(raw: &[u8], is_be: bool) -> Result<Option<PvaStatus>, String> {
379    let pkt = PvaPacket::new(raw);
380    let payload_len = pkt.header.payload_length as usize;
381    if raw.len() < 8 + payload_len {
382        return Err("op response truncated".to_string());
383    }
384    let payload = &raw[8..8 + payload_len];
385    if payload.len() < 5 {
386        return Err("op response payload too short".to_string());
387    }
388    Ok(decode_status(&payload[5..], is_be).0)
389}
390
391#[derive(Debug)]
392pub struct PvaControlPayload {
393    pub command: u8,
394    pub data: u32,
395}
396
397impl PvaControlPayload {
398    pub fn new(command: u8, data: u32) -> Self {
399        Self { command, data }
400    }
401}
402
403#[derive(Debug)]
404pub struct PvaSearchResponsePayload {
405    pub guid: [u8; 12],
406    pub seq: u32,
407    pub addr: [u8; 16],
408    pub port: u16,
409    pub protocol: String,
410    pub found: bool,
411    pub cids: Vec<u32>,
412}
413
414impl PvaSearchResponsePayload {
415    pub fn new(raw: &[u8], is_be: bool) -> Option<Self> {
416        if raw.len() < 34 {
417            debug!("PvaSearchResponsePayload::new: raw too short {}", raw.len());
418            return None;
419        }
420        let guid: [u8; 12] = raw[0..12].try_into().ok()?;
421        let seq = if is_be {
422            u32::from_be_bytes(raw[12..16].try_into().ok()?)
423        } else {
424            u32::from_le_bytes(raw[12..16].try_into().ok()?)
425        };
426        let addr: [u8; 16] = raw[16..32].try_into().ok()?;
427        let port = if is_be {
428            u16::from_be_bytes(raw[32..34].try_into().ok()?)
429        } else {
430            u16::from_le_bytes(raw[32..34].try_into().ok()?)
431        };
432
433        let mut offset = 34;
434        let (protocol, consumed) = decode_string(&raw[offset..], is_be)?;
435        offset += consumed;
436
437        if raw.len() <= offset {
438            return Some(Self {
439                guid,
440                seq,
441                addr,
442                port,
443                protocol,
444                found: false,
445                cids: vec![],
446            });
447        }
448
449        let found = raw[offset] != 0;
450        offset += 1;
451        let mut cids: Vec<u32> = vec![];
452        if raw.len() >= offset + 2 {
453            let count = if is_be {
454                u16::from_be_bytes(raw[offset..offset + 2].try_into().ok()?)
455            } else {
456                u16::from_le_bytes(raw[offset..offset + 2].try_into().ok()?)
457            };
458            offset += 2;
459            for _ in 0..count {
460                if raw.len() < offset + 4 {
461                    break;
462                }
463                let cid = if is_be {
464                    u32::from_be_bytes(raw[offset..offset + 4].try_into().ok()?)
465                } else {
466                    u32::from_le_bytes(raw[offset..offset + 4].try_into().ok()?)
467                };
468                cids.push(cid);
469                offset += 4;
470            }
471        }
472
473        Some(Self {
474            guid,
475            seq,
476            addr,
477            port,
478            protocol,
479            found,
480            cids,
481        })
482    }
483}
484
485#[derive(Debug)]
486pub struct PvaConnectionValidationPayload {
487    pub is_server: bool,
488    pub buffer_size: u32,
489    pub introspection_registry_size: u16,
490    pub qos: u16,
491    pub authz: Option<String>,
492}
493
494impl PvaConnectionValidationPayload {
495    pub fn new(raw: &[u8], is_be: bool, is_server: bool) -> Option<Self> {
496        if raw.len() < 6 {
497            debug!(
498                "PvaConnectionValidationPayload::new: raw too short {}",
499                raw.len()
500            );
501            return None;
502        }
503        let buffer_size = if is_be {
504            u32::from_be_bytes(raw[0..4].try_into().ok()?)
505        } else {
506            u32::from_le_bytes(raw[0..4].try_into().ok()?)
507        };
508        let introspection_registry_size = if is_be {
509            u16::from_be_bytes(raw[4..6].try_into().ok()?)
510        } else {
511            u16::from_le_bytes(raw[4..6].try_into().ok()?)
512        };
513
514        if is_server {
515            // Server→client: buffer_size(u32) + isize(u16) + Size(nauth) + nauth × string
516            // No QoS field.
517            let mut offset = 6;
518            let authz = if offset < raw.len() {
519                if let Some((count, consumed)) = decode_size(&raw[offset..], is_be) {
520                    offset += consumed;
521                    let mut first_method = None;
522                    for _ in 0..count {
523                        if let Some((s, c)) = decode_string(&raw[offset..], is_be) {
524                            if first_method.is_none() && !s.is_empty() {
525                                first_method = Some(s);
526                            }
527                            offset += c;
528                        }
529                    }
530                    first_method
531                } else {
532                    // Fallback: try single string (legacy spvirit servers).
533                    decode_string(&raw[offset..], is_be).map(|(s, _)| s)
534                }
535            } else {
536                None
537            };
538
539            Some(Self {
540                is_server,
541                buffer_size,
542                introspection_registry_size,
543                qos: 0,
544                authz,
545            })
546        } else {
547            // Client→server: buffer_size(u32) + isize(u16) + qos(u16) + auth_method(string) [+ FieldDesc cred]
548            if raw.len() < 8 {
549                return None;
550            }
551            let qos = if is_be {
552                u16::from_be_bytes(raw[6..8].try_into().ok()?)
553            } else {
554                u16::from_le_bytes(raw[6..8].try_into().ok()?)
555            };
556            let authz = if raw.len() > 8 {
557                if let Some((s, consumed)) = decode_string(&raw[8..], is_be) {
558                    if 8 + consumed == raw.len() {
559                        Some(s)
560                    } else {
561                        // Has trailing FieldDesc for credentials; auth name is the string.
562                        Some(s)
563                    }
564                } else {
565                    None
566                }
567            } else {
568                None
569            };
570
571            Some(Self {
572                is_server,
573                buffer_size,
574                introspection_registry_size,
575                qos,
576                authz,
577            })
578        }
579    }
580}
581
582#[derive(Debug)]
583pub struct PvaConnectionValidatedPayload {
584    pub status: Option<PvaStatus>,
585}
586
587impl PvaConnectionValidatedPayload {
588    pub fn new(raw: &[u8], is_be: bool) -> Option<Self> {
589        let (status, _consumed) = decode_status(raw, is_be);
590        Some(Self { status })
591    }
592}
593
594#[derive(Debug)]
595pub struct PvaAuthNzPayload {
596    pub raw: Vec<u8>,
597    pub strings: Vec<String>,
598}
599
600impl PvaAuthNzPayload {
601    pub fn new(raw: &[u8], is_be: bool) -> Option<Self> {
602        let mut strings = vec![];
603        if let Some((count, consumed)) = decode_size(raw, is_be) {
604            let mut offset = consumed;
605            for _ in 0..count {
606                if let Some((s, len)) = decode_string(&raw[offset..], is_be) {
607                    strings.push(s);
608                    offset += len;
609                } else {
610                    break;
611                }
612            }
613        }
614        Some(Self {
615            raw: raw.to_vec(),
616            strings,
617        })
618    }
619}
620
621#[derive(Debug)]
622pub struct PvaAclChangePayload {
623    pub status: Option<PvaStatus>,
624    pub raw: Vec<u8>,
625}
626
627impl PvaAclChangePayload {
628    pub fn new(raw: &[u8], is_be: bool) -> Option<Self> {
629        let (status, consumed) = decode_status(raw, is_be);
630        let raw_rem = if raw.len() > consumed {
631            raw[consumed..].to_vec()
632        } else {
633            vec![]
634        };
635        Some(Self {
636            status,
637            raw: raw_rem,
638        })
639    }
640}
641
642#[derive(Debug)]
643pub struct PvaGetFieldPayload {
644    pub is_server: bool,
645    pub cid: u32,
646    pub sid: Option<u32>,
647    pub ioid: Option<u32>,
648    pub field_name: Option<String>,
649    pub status: Option<PvaStatus>,
650    pub introspection: Option<StructureDesc>,
651    pub raw: Vec<u8>,
652}
653
654impl PvaGetFieldPayload {
655    pub fn new(raw: &[u8], is_be: bool, is_server: bool) -> Option<Self> {
656        if !is_server {
657            if raw.len() < 4 {
658                debug!(
659                    "PvaGetFieldPayload::new (client): raw too short {}",
660                    raw.len()
661                );
662                return None;
663            }
664            let cid = if is_be {
665                u32::from_be_bytes(raw[0..4].try_into().ok()?)
666            } else {
667                u32::from_le_bytes(raw[0..4].try_into().ok()?)
668            };
669
670            // Two client-side wire variants are observed for GET_FIELD:
671            // 1) legacy: [cid][field_name]
672            // 2) EPICS pvAccess: [sid][ioid][field_name]
673            let legacy_field = if raw.len() > 4 {
674                decode_string(&raw[4..], is_be)
675                    .and_then(|(s, consumed)| (4 + consumed == raw.len()).then_some(s))
676            } else {
677                None
678            };
679
680            let epics_variant = if raw.len() >= 9 {
681                let ioid = if is_be {
682                    u32::from_be_bytes(raw[4..8].try_into().ok()?)
683                } else {
684                    u32::from_le_bytes(raw[4..8].try_into().ok()?)
685                };
686                decode_string(&raw[8..], is_be)
687                    .and_then(|(s, consumed)| (8 + consumed == raw.len()).then_some((ioid, s)))
688            } else {
689                None
690            };
691
692            let (sid, ioid, field_name) = if let Some((ioid, field)) = epics_variant {
693                (Some(cid), Some(ioid), Some(field))
694            } else {
695                (None, None, legacy_field)
696            };
697
698            return Some(Self {
699                is_server,
700                cid,
701                sid,
702                ioid,
703                field_name,
704                status: None,
705                introspection: None,
706                raw: vec![],
707            });
708        }
709
710        let parse_status_then_intro = |bytes: &[u8]| {
711            let (status, consumed) = decode_status(bytes, is_be);
712            let pvd_raw = if bytes.len() > consumed {
713                bytes[consumed..].to_vec()
714            } else {
715                vec![]
716            };
717            let introspection = if !pvd_raw.is_empty() {
718                let decoder = PvdDecoder::new(is_be);
719                decoder.parse_introspection(&pvd_raw)
720            } else {
721                None
722            };
723            (status, pvd_raw, introspection)
724        };
725
726        // Server GET_FIELD responses are encoded as:
727        // [request_id/cid][status][optional introspection]
728        // Keep cid present for both success and error responses.
729        let (cid, status, pvd_raw, introspection) = if raw.len() >= 4 {
730            let parsed_cid = if is_be {
731                u32::from_be_bytes(raw[0..4].try_into().ok()?)
732            } else {
733                u32::from_le_bytes(raw[0..4].try_into().ok()?)
734            };
735            let (status, pvd_raw, introspection) = parse_status_then_intro(&raw[4..]);
736            (parsed_cid, status, pvd_raw, introspection)
737        } else {
738            let (status, pvd_raw, introspection) = parse_status_then_intro(raw);
739            (0, status, pvd_raw, introspection)
740        };
741
742        Some(Self {
743            is_server,
744            cid,
745            sid: None,
746            ioid: None,
747            field_name: None,
748            status,
749            introspection,
750            raw: pvd_raw,
751        })
752    }
753}
754
755#[derive(Debug)]
756pub struct PvaMessagePayload {
757    pub ioid: u32,
758    pub message_type: u8,
759    pub message: Option<String>,
760    /// Legacy compat: if the payload looks like old Status format, decode that.
761    pub status: Option<PvaStatus>,
762    pub raw: Vec<u8>,
763}
764
765impl PvaMessagePayload {
766    pub fn new(raw: &[u8], is_be: bool) -> Option<Self> {
767        // PVA spec MESSAGE format: ioid(u32) + message_type(u8) + message(string)
768        if raw.len() >= 5 {
769            let ioid = if is_be {
770                u32::from_be_bytes(raw[0..4].try_into().ok()?)
771            } else {
772                u32::from_le_bytes(raw[0..4].try_into().ok()?)
773            };
774            let message_type = raw[4];
775            let message = if raw.len() > 5 {
776                decode_string(&raw[5..], is_be).map(|(s, _)| s)
777            } else {
778                None
779            };
780            // Build a synthetic PvaStatus so existing tests/code that inspect .status still work.
781            let code = match message_type {
782                0 => 0xFF, // info → OK
783                1 => 0x01, // warning
784                2 => 0x02, // error
785                _ => 0x03, // fatal
786            };
787            let status = Some(PvaStatus {
788                code,
789                message: message.clone(),
790                stack: None,
791            });
792            Some(Self {
793                ioid,
794                message_type,
795                message,
796                status,
797                raw: raw.to_vec(),
798            })
799        } else {
800            // Fallback for very short payloads
801            Some(Self {
802                ioid: 0,
803                message_type: 0,
804                message: None,
805                status: None,
806                raw: raw.to_vec(),
807            })
808        }
809    }
810}
811
812#[derive(Debug)]
813pub struct PvaMultipleDataEntry {
814    pub ioid: u32,
815    pub subcmd: u8,
816}
817
818#[derive(Debug)]
819pub struct PvaMultipleDataPayload {
820    pub entries: Vec<PvaMultipleDataEntry>,
821    pub raw: Vec<u8>,
822}
823
824impl PvaMultipleDataPayload {
825    pub fn new(raw: &[u8], is_be: bool) -> Option<Self> {
826        let mut entries: Vec<PvaMultipleDataEntry> = vec![];
827        if let Some((count, consumed)) = decode_size(raw, is_be) {
828            let mut offset = consumed;
829            for _ in 0..count {
830                if raw.len() < offset + 5 {
831                    break;
832                }
833                let ioid = if is_be {
834                    u32::from_be_bytes(raw[offset..offset + 4].try_into().ok()?)
835                } else {
836                    u32::from_le_bytes(raw[offset..offset + 4].try_into().ok()?)
837                };
838                let subcmd = raw[offset + 4];
839                entries.push(PvaMultipleDataEntry { ioid, subcmd });
840                offset += 5;
841            }
842        }
843        Some(Self {
844            entries,
845            raw: raw.to_vec(),
846        })
847    }
848}
849
850#[derive(Debug)]
851pub struct PvaCancelRequestPayload {
852    pub request_id: u32,
853    pub status: Option<PvaStatus>,
854}
855
856impl PvaCancelRequestPayload {
857    pub fn new(raw: &[u8], is_be: bool) -> Option<Self> {
858        if raw.len() < 4 {
859            debug!("PvaCancelRequestPayload::new: raw too short {}", raw.len());
860            return None;
861        }
862        let request_id = if is_be {
863            u32::from_be_bytes(raw[0..4].try_into().ok()?)
864        } else {
865            u32::from_le_bytes(raw[0..4].try_into().ok()?)
866        };
867        let (status, _) = if raw.len() > 4 {
868            decode_status(&raw[4..], is_be)
869        } else {
870            (None, 0)
871        };
872        Some(Self { request_id, status })
873    }
874}
875
876#[derive(Debug)]
877pub struct PvaDestroyRequestPayload {
878    pub request_id: u32,
879    pub status: Option<PvaStatus>,
880}
881
882impl PvaDestroyRequestPayload {
883    pub fn new(raw: &[u8], is_be: bool) -> Option<Self> {
884        if raw.len() < 4 {
885            debug!("PvaDestroyRequestPayload::new: raw too short {}", raw.len());
886            return None;
887        }
888        let request_id = if is_be {
889            u32::from_be_bytes(raw[0..4].try_into().ok()?)
890        } else {
891            u32::from_le_bytes(raw[0..4].try_into().ok()?)
892        };
893        let (status, _) = if raw.len() > 4 {
894            decode_status(&raw[4..], is_be)
895        } else {
896            (None, 0)
897        };
898        Some(Self { request_id, status })
899    }
900}
901
902#[derive(Debug)]
903pub struct PvaOriginTagPayload {
904    pub address: [u8; 16],
905}
906
907impl PvaOriginTagPayload {
908    pub fn new(raw: &[u8]) -> Option<Self> {
909        if raw.len() < 16 {
910            debug!("PvaOriginTagPayload::new: raw too short {}", raw.len());
911            return None;
912        }
913        let address: [u8; 16] = raw[0..16].try_into().ok()?;
914        Some(Self { address })
915    }
916}
917
918#[derive(Debug)]
919pub struct PvaUnknownPayload {
920    pub command: u8,
921    pub is_control: bool,
922    pub raw_len: usize,
923}
924
925impl PvaUnknownPayload {
926    pub fn new(command: u8, is_control: bool, raw_len: usize) -> Self {
927        Self {
928            command,
929            is_control,
930            raw_len,
931        }
932    }
933}
934
935/// payload decoder
936/// SEARCH
937#[derive(Debug)]
938pub struct PvaSearchPayload {
939    pub seq: u32,
940    pub mask: u8,
941    pub addr: [u8; 16],
942    pub port: u16,
943    pub protocols: Vec<String>,
944    pub pv_requests: Vec<(u32, String)>,
945    pub pv_names: Vec<String>,
946}
947
948impl PvaSearchPayload {
949    pub fn new(raw: &[u8], is_be: bool) -> Option<Self> {
950        if raw.is_empty() {
951            debug!("PvaSearchPayload::new received an empty raw slice.");
952            return None;
953        }
954        const MIN_FIXED_SEARCH_PAYLOAD_SIZE: usize = 26;
955        if raw.len() < MIN_FIXED_SEARCH_PAYLOAD_SIZE {
956            debug!(
957                "PvaSearchPayload::new: raw slice length {} is less than min fixed size {}.",
958                raw.len(),
959                MIN_FIXED_SEARCH_PAYLOAD_SIZE
960            );
961            return None;
962        }
963
964        let seq = if is_be {
965            u32::from_be_bytes(raw[0..4].try_into().unwrap())
966        } else {
967            u32::from_le_bytes(raw[0..4].try_into().unwrap())
968        };
969
970        let mask = raw[4];
971        let addr: [u8; 16] = raw[8..24].try_into().unwrap();
972        let port = if is_be {
973            u16::from_be_bytes(raw[24..26].try_into().unwrap())
974        } else {
975            u16::from_le_bytes(raw[24..26].try_into().unwrap())
976        };
977
978        let mut offset = 26;
979
980        let (protocol_count, consumed) = decode_size(&raw[offset..], is_be)?;
981        offset += consumed;
982
983        let mut protocols = vec![];
984        for _ in 0..protocol_count {
985            let (protocol, len) = decode_string(&raw[offset..], is_be)?;
986            protocols.push(protocol);
987            offset += len;
988        }
989
990        // PV names here
991        if raw.len() < offset + 2 {
992            return None;
993        }
994        let pv_count = if is_be {
995            u16::from_be_bytes(raw[offset..offset + 2].try_into().unwrap())
996        } else {
997            u16::from_le_bytes(raw[offset..offset + 2].try_into().unwrap())
998        };
999        offset += 2;
1000
1001        let mut pv_names = vec![];
1002        let mut pv_requests = vec![];
1003        for _ in 0..pv_count {
1004            if raw.len() < offset + 4 {
1005                debug!(
1006                    "PvaSearchPayload::new: not enough data for PV CID at offset {}. Raw len: {}",
1007                    offset,
1008                    raw.len()
1009                );
1010                return None;
1011            }
1012            let cid = if is_be {
1013                u32::from_be_bytes(raw[offset..offset + 4].try_into().unwrap())
1014            } else {
1015                u32::from_le_bytes(raw[offset..offset + 4].try_into().unwrap())
1016            };
1017            offset += 4;
1018            let (pv_name, len) = decode_string(&raw[offset..], is_be)?;
1019            pv_names.push(pv_name.clone());
1020            pv_requests.push((cid, pv_name));
1021            offset += len;
1022        }
1023
1024        Some(Self {
1025            seq,
1026            mask,
1027            addr,
1028            port,
1029            protocols,
1030            pv_requests,
1031            pv_names,
1032        })
1033    }
1034}
1035
1036/// struct beaconMessage {
1037#[derive(Debug)]
1038pub struct PvaBeaconPayload {
1039    pub guid: [u8; 12],
1040    pub flags: u8,
1041    pub beacon_sequence_id: u8,
1042    pub change_count: u16,
1043    pub server_address: [u8; 16],
1044    pub server_port: u16,
1045    pub protocol: String,
1046    pub server_status_if: String,
1047}
1048
1049impl PvaBeaconPayload {
1050    pub fn new(raw: &[u8], is_be: bool) -> Option<Self> {
1051        // guid(12) + flags(1) + beacon_sequence_id(1) + change_count(2) + server_address(16) + server_port(2)
1052        const MIN_FIXED_BEACON_PAYLOAD_SIZE: usize = 12 + 1 + 1 + 2 + 16 + 2;
1053
1054        if raw.len() < MIN_FIXED_BEACON_PAYLOAD_SIZE {
1055            debug!(
1056                "PvaBeaconPayload::new: raw slice length {} is less than min fixed size {}.",
1057                raw.len(),
1058                MIN_FIXED_BEACON_PAYLOAD_SIZE
1059            );
1060            return None;
1061        }
1062
1063        let guid: [u8; 12] = raw[0..12].try_into().unwrap();
1064        let flags = raw[12];
1065        let beacon_sequence_id = raw[13];
1066        let change_count = if is_be {
1067            u16::from_be_bytes(raw[14..16].try_into().unwrap())
1068        } else {
1069            u16::from_le_bytes(raw[14..16].try_into().unwrap())
1070        };
1071        let server_address: [u8; 16] = raw[16..32].try_into().unwrap();
1072        let server_port = if is_be {
1073            u16::from_be_bytes(raw[32..34].try_into().unwrap())
1074        } else {
1075            u16::from_le_bytes(raw[32..34].try_into().unwrap())
1076        };
1077        let (protocol, len) = decode_string(&raw[34..], is_be)?;
1078        let protocol = protocol;
1079        let server_status_if = if len > 0 {
1080            let (server_status_if, _server_status_len) = decode_string(&raw[34 + len..], is_be)?;
1081            server_status_if
1082        } else {
1083            String::new()
1084        };
1085
1086        Some(Self {
1087            guid,
1088            flags,
1089            beacon_sequence_id,
1090            change_count,
1091            server_address,
1092            server_port,
1093            protocol,
1094            server_status_if,
1095        })
1096    }
1097}
1098
1099/// CREATE_CHANNEL payload (cmd=7)
1100/// Client: count(2), then for each: cid(4), pv_name(string)
1101/// Server: cid(4), sid(4), status
1102#[derive(Debug)]
1103pub struct PvaCreateChannelPayload {
1104    /// Is this from server (response) or client (request)?
1105    pub is_server: bool,
1106    /// For client requests: list of (cid, pv_name) tuples
1107    pub channels: Vec<(u32, String)>,
1108    /// For server response: client channel ID
1109    pub cid: u32,
1110    /// For server response: server channel ID
1111    pub sid: u32,
1112    /// For server response: status
1113    pub status: Option<PvaStatus>,
1114}
1115
1116impl PvaCreateChannelPayload {
1117    pub fn new(raw: &[u8], is_be: bool, is_server: bool) -> Option<Self> {
1118        if raw.is_empty() {
1119            debug!("PvaCreateChannelPayload::new received an empty raw slice.");
1120            return None;
1121        }
1122
1123        if is_server {
1124            // Server response: cid(4), sid(4), status
1125            if raw.len() < 8 {
1126                debug!("CREATE_CHANNEL server response too short: {}", raw.len());
1127                return None;
1128            }
1129
1130            let cid = if is_be {
1131                u32::from_be_bytes(raw[0..4].try_into().unwrap())
1132            } else {
1133                u32::from_le_bytes(raw[0..4].try_into().unwrap())
1134            };
1135
1136            let sid = if is_be {
1137                u32::from_be_bytes(raw[4..8].try_into().unwrap())
1138            } else {
1139                u32::from_le_bytes(raw[4..8].try_into().unwrap())
1140            };
1141
1142            // Decode status if present
1143            let status = if raw.len() > 8 {
1144                let code = raw[8];
1145                if code == 0xff {
1146                    None // OK, no status message
1147                } else {
1148                    let mut idx = 9;
1149                    let message = if idx < raw.len() {
1150                        decode_string(&raw[idx..], is_be).map(|(msg, consumed)| {
1151                            idx += consumed;
1152                            msg
1153                        })
1154                    } else {
1155                        None
1156                    };
1157                    let stack = if idx < raw.len() {
1158                        decode_string(&raw[idx..], is_be).map(|(s, _)| s)
1159                    } else {
1160                        None
1161                    };
1162                    Some(PvaStatus {
1163                        code,
1164                        message,
1165                        stack,
1166                    })
1167                }
1168            } else {
1169                None
1170            };
1171
1172            Some(Self {
1173                is_server: true,
1174                channels: vec![],
1175                cid,
1176                sid,
1177                status,
1178            })
1179        } else {
1180            // Client request: count(2), then for each: cid(4), pv_name(string)
1181            if raw.len() < 2 {
1182                debug!("CREATE_CHANNEL client request too short: {}", raw.len());
1183                return None;
1184            }
1185
1186            let count = if is_be {
1187                u16::from_be_bytes(raw[0..2].try_into().unwrap())
1188            } else {
1189                u16::from_le_bytes(raw[0..2].try_into().unwrap())
1190            };
1191
1192            let mut offset = 2;
1193            let mut channels = Vec::with_capacity(count as usize);
1194
1195            for _ in 0..count {
1196                if raw.len() < offset + 4 {
1197                    debug!(
1198                        "CREATE_CHANNEL: not enough data for CID at offset {}",
1199                        offset
1200                    );
1201                    break;
1202                }
1203
1204                let cid = if is_be {
1205                    u32::from_be_bytes(raw[offset..offset + 4].try_into().unwrap())
1206                } else {
1207                    u32::from_le_bytes(raw[offset..offset + 4].try_into().unwrap())
1208                };
1209                offset += 4;
1210
1211                if let Some((pv_name, consumed)) = decode_string(&raw[offset..], is_be) {
1212                    offset += consumed;
1213                    channels.push((cid, pv_name));
1214                } else {
1215                    debug!(
1216                        "CREATE_CHANNEL: failed to decode PV name at offset {}",
1217                        offset
1218                    );
1219                    break;
1220                }
1221            }
1222
1223            Some(Self {
1224                is_server: false,
1225                channels,
1226                cid: 0,
1227                sid: 0,
1228                status: None,
1229            })
1230        }
1231    }
1232}
1233
1234impl fmt::Display for PvaCreateChannelPayload {
1235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1236        if self.is_server {
1237            let status_text = if let Some(s) = &self.status {
1238                format!(" status={}", s.code)
1239            } else {
1240                String::new()
1241            };
1242            write!(
1243                f,
1244                "CREATE_CHANNEL(cid={}, sid={}{})",
1245                self.cid, self.sid, status_text
1246            )
1247        } else {
1248            let pv_list: Vec<String> = self
1249                .channels
1250                .iter()
1251                .map(|(cid, name)| format!("{}:'{}'", cid, name))
1252                .collect();
1253            write!(f, "CREATE_CHANNEL({})", pv_list.join(", "))
1254        }
1255    }
1256}
1257
1258/// DESTROY_CHANNEL payload (cmd=8)
1259/// Format: sid(4), cid(4)
1260#[derive(Debug)]
1261pub struct PvaDestroyChannelPayload {
1262    /// Server channel ID
1263    pub sid: u32,
1264    /// Client channel ID
1265    pub cid: u32,
1266}
1267
1268impl PvaDestroyChannelPayload {
1269    pub fn new(raw: &[u8], is_be: bool) -> Option<Self> {
1270        if raw.len() < 8 {
1271            debug!("DESTROY_CHANNEL payload too short: {}", raw.len());
1272            return None;
1273        }
1274
1275        let sid = if is_be {
1276            u32::from_be_bytes(raw[0..4].try_into().unwrap())
1277        } else {
1278            u32::from_le_bytes(raw[0..4].try_into().unwrap())
1279        };
1280
1281        let cid = if is_be {
1282            u32::from_be_bytes(raw[4..8].try_into().unwrap())
1283        } else {
1284            u32::from_le_bytes(raw[4..8].try_into().unwrap())
1285        };
1286
1287        Some(Self { sid, cid })
1288    }
1289}
1290
1291impl fmt::Display for PvaDestroyChannelPayload {
1292    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1293        write!(f, "DESTROY_CHANNEL(sid={}, cid={})", self.sid, self.cid)
1294    }
1295}
1296
1297/// Generic operation payload (GET/PUT/PUT_GET/MONITOR/ARRAY/RPC)
1298#[derive(Debug)]
1299pub struct PvaOpPayload {
1300    pub sid_or_cid: u32,
1301    pub ioid: u32,
1302    pub subcmd: u8,
1303    pub body: Vec<u8>,
1304    pub command: u8,
1305    pub is_server: bool,
1306    pub status: Option<PvaStatus>,
1307    pub pv_names: Vec<String>,
1308    /// Parsed introspection data (for INIT responses)
1309    pub introspection: Option<StructureDesc>,
1310    /// Decoded value (when field_desc is available)
1311    pub decoded_value: Option<DecodedValue>,
1312}
1313
1314// Heuristic extraction of PV-like names from a PVD body.
1315fn extract_pv_names(raw: &[u8]) -> Vec<String> {
1316    let mut names: Vec<String> = Vec::new();
1317    let mut i = 0usize;
1318    while i < raw.len() {
1319        // start with an alphanumeric character
1320        if raw[i].is_ascii_alphanumeric() {
1321            let start = i;
1322            i += 1;
1323            while i < raw.len() {
1324                let b = raw[i];
1325                if b.is_ascii_alphanumeric()
1326                    || b == b':'
1327                    || b == b'.'
1328                    || b == b'_'
1329                    || b == b'-'
1330                    || b == b'/'
1331                {
1332                    i += 1;
1333                } else {
1334                    break;
1335                }
1336            }
1337            let len = i - start;
1338            if len >= 3 && len <= 128 {
1339                if let Ok(s) = std::str::from_utf8(&raw[start..start + len]) {
1340                    // validate candidate contains at least one alphabetic char
1341                    if s.chars().any(|c| c.is_ascii_alphabetic()) {
1342                        if !names.contains(&s.to_string()) {
1343                            names.push(s.to_string());
1344                            if names.len() >= 8 {
1345                                break;
1346                            }
1347                        }
1348                    }
1349                }
1350            }
1351        } else {
1352            i += 1;
1353        }
1354    }
1355    names
1356}
1357
1358impl PvaOpPayload {
1359    pub fn new(raw: &[u8], is_be: bool, is_server: bool, command: u8) -> Option<Self> {
1360        // operation payloads have slightly different fixed offsets depending on client/server
1361        if raw.len() < 5 {
1362            debug!("PvaOpPayload::new: raw too short {}", raw.len());
1363            return None;
1364        }
1365
1366        let (sid_or_cid, ioid, subcmd, offset) = if is_server {
1367            // server op: ioid(4), subcmd(1)
1368            if raw.len() < 5 {
1369                return None;
1370            }
1371            let ioid = if is_be {
1372                u32::from_be_bytes(raw[0..4].try_into().unwrap())
1373            } else {
1374                u32::from_le_bytes(raw[0..4].try_into().unwrap())
1375            };
1376            let subcmd = raw[4];
1377            (0, ioid, subcmd, 5)
1378        } else {
1379            // client op: sid(4), ioid(4), subcmd(1)
1380            if raw.len() < 9 {
1381                return None;
1382            }
1383            let sid = if is_be {
1384                u32::from_be_bytes(raw[0..4].try_into().unwrap())
1385            } else {
1386                u32::from_le_bytes(raw[0..4].try_into().unwrap())
1387            };
1388            let ioid = if is_be {
1389                u32::from_be_bytes(raw[4..8].try_into().unwrap())
1390            } else {
1391                u32::from_le_bytes(raw[4..8].try_into().unwrap())
1392            };
1393            let subcmd = raw[8];
1394            (sid, ioid, subcmd, 9)
1395        };
1396
1397        let body = if raw.len() > offset {
1398            raw[offset..].to_vec()
1399        } else {
1400            vec![]
1401        };
1402
1403        // Status is only present in certain subcmd types:
1404        // Status format (per Lua dissector): first byte = code. If code==0xff (255) -> OK
1405        // shorthand (1 byte only). Otherwise follow with two length-prefixed strings:
1406        // message, stack.
1407        // Server responses carry a status prefix for INIT responses (subcmd & 0x08),
1408        // and for non-INIT responses on GET (10), PUT (11), PUT_GET (12).
1409        // Monitor (13) data updates (non-INIT) do NOT have a status prefix.
1410        let mut status: Option<PvaStatus> = None;
1411        let mut pvd_raw: Vec<u8> = vec![];
1412
1413        let has_status = is_server
1414            && ((subcmd & 0x08) != 0 || (command != 13 && command != 14));
1415
1416        if !body.is_empty() {
1417            if has_status {
1418                let (parsed, consumed) = decode_status(&body, is_be);
1419                status = parsed;
1420                pvd_raw = if body.len() > consumed {
1421                    body[consumed..].to_vec()
1422                } else {
1423                    vec![]
1424                };
1425            } else {
1426                pvd_raw = body.clone();
1427            }
1428        }
1429
1430        let pv_names = extract_pv_names(&pvd_raw);
1431
1432        // Try to parse introspection from INIT response (subcmd & 0x08 and is_server)
1433        let introspection = if is_server && (subcmd & 0x08) != 0 && !pvd_raw.is_empty() {
1434            let decoder = PvdDecoder::new(is_be);
1435            decoder.parse_introspection(&pvd_raw)
1436        } else {
1437            None
1438        };
1439
1440        let result = Some(Self {
1441            sid_or_cid,
1442            ioid,
1443            subcmd,
1444            body: pvd_raw,
1445            command,
1446            is_server,
1447            status: status.clone(),
1448            pv_names,
1449            introspection,
1450            decoded_value: None, // Will be set by packet processor with field_desc
1451        });
1452
1453        result
1454    }
1455
1456    /// Decode the body using provided field description
1457    pub fn decode_with_field_desc(&mut self, field_desc: &StructureDesc, is_be: bool) {
1458        if self.body.is_empty() {
1459            return;
1460        }
1461
1462        let decoder = PvdDecoder::new(is_be);
1463
1464        // For data updates (subcmd == 0x00 or subcmd & 0x40), use bitset decoding
1465        if self.subcmd == 0x00 || (self.subcmd & 0x40) != 0 {
1466            if self.command == 13 {
1467                let cand_overrun_pre =
1468                    decoder.decode_structure_with_bitset_and_overrun(&self.body, field_desc);
1469                let cand_overrun_post =
1470                    decoder.decode_structure_with_bitset_then_overrun(&self.body, field_desc);
1471                let cand_legacy = decoder.decode_structure_with_bitset(&self.body, field_desc);
1472                self.decoded_value =
1473                    choose_best_decoded_multi([cand_overrun_pre, cand_overrun_post, cand_legacy]);
1474            } else if let Some((value, _)) =
1475                decoder.decode_structure_with_bitset(&self.body, field_desc)
1476            {
1477                self.decoded_value = Some(value);
1478            }
1479        } else {
1480            // Full structure decode
1481            if let Some((value, _)) = decoder.decode_structure(&self.body, field_desc) {
1482                self.decoded_value = Some(value);
1483            }
1484        }
1485    }
1486}
1487
1488fn choose_best_decoded_multi(cands: [Option<(DecodedValue, usize)>; 3]) -> Option<DecodedValue> {
1489    let mut best_value: Option<DecodedValue> = None;
1490    let mut best_score = i32::MIN;
1491    let mut best_consumed = 0usize;
1492    let mut best_idx = 0usize;
1493
1494    for (idx, cand) in cands.into_iter().enumerate() {
1495        let Some((value, consumed)) = cand else {
1496            continue;
1497        };
1498        let score = score_decoded(&value);
1499        let better = score > best_score
1500            || (score == best_score && consumed > best_consumed)
1501            || (score == best_score && consumed == best_consumed && idx > best_idx);
1502        if better {
1503            best_score = score;
1504            best_consumed = consumed;
1505            best_idx = idx;
1506            best_value = Some(value);
1507        }
1508    }
1509
1510    best_value
1511}
1512
1513fn score_decoded(value: &DecodedValue) -> i32 {
1514    let DecodedValue::Structure(fields) = value else {
1515        return -1;
1516    };
1517
1518    let mut score = fields.len() as i32;
1519
1520    let mut has_value = false;
1521    let mut has_alarm = false;
1522    let mut has_ts = false;
1523
1524    for (name, val) in fields {
1525        match name.as_str() {
1526            "value" => {
1527                has_value = true;
1528                score += 4;
1529                match val {
1530                    DecodedValue::Array(items) => {
1531                        if items.is_empty() {
1532                            score -= 2;
1533                        } else {
1534                            score += 6 + (items.len().min(8) as i32);
1535                        }
1536                    }
1537                    DecodedValue::Structure(_) => score += 1,
1538                    _ => score += 2,
1539                }
1540            }
1541            "alarm" => {
1542                has_alarm = true;
1543                score += 2;
1544            }
1545            "timeStamp" => {
1546                has_ts = true;
1547                score += 2;
1548                if let DecodedValue::Structure(ts_fields) = val {
1549                    if let Some(secs) = ts_fields.iter().find_map(|(n, v)| {
1550                        if n == "secondsPastEpoch" {
1551                            if let DecodedValue::Int64(s) = v {
1552                                return Some(*s);
1553                            }
1554                        }
1555                        None
1556                    }) {
1557                        if (0..=4_000_000_000i64).contains(&secs) {
1558                            score += 2;
1559                        } else if secs.abs() > 10_000_000_000i64 {
1560                            score -= 2;
1561                        }
1562                    }
1563                }
1564            }
1565            "display" | "control" => {
1566                score += 1;
1567            }
1568            _ => {}
1569        }
1570    }
1571
1572    if !has_value {
1573        score -= 2;
1574    }
1575    if !has_alarm {
1576        score -= 1;
1577    }
1578    if !has_ts {
1579        score -= 1;
1580    }
1581
1582    score
1583}
1584
1585#[derive(Debug, Clone)]
1586pub struct PvaStatus {
1587    pub code: u8,
1588    pub message: Option<String>,
1589    pub stack: Option<String>,
1590}
1591
1592impl PvaStatus {
1593    pub fn is_error(&self) -> bool {
1594        self.code != 0
1595    }
1596}
1597
1598/// Display implementations
1599// beacon payload display
1600impl fmt::Display for PvaBeaconPayload {
1601    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1602        write!(
1603            f,
1604            "Beacon:GUID=[{}],Flags=[{}],SeqId=[{}],ChangeCount=[{}],ServerAddress=[{}],ServerPort=[{}],Protocol=[{}]",
1605            hex::encode(self.guid),
1606            self.flags,
1607            self.beacon_sequence_id,
1608            self.change_count,
1609            format_pva_address(&self.server_address),
1610            self.server_port,
1611            self.protocol
1612        )
1613    }
1614}
1615
1616// search payload display
1617impl fmt::Display for PvaSearchPayload {
1618    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1619        write!(f, "Search:PVs=[{}]", self.pv_names.join(","))
1620    }
1621}
1622
1623impl fmt::Display for PvaControlPayload {
1624    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1625        let name = match self.command {
1626            0 => "MARK_TOTAL_BYTES_SENT",
1627            1 => "ACK_TOTAL_BYTES_RECEIVED",
1628            2 => "SET_BYTE_ORDER",
1629            3 => "ECHO_REQUEST",
1630            4 => "ECHO_RESPONSE",
1631            _ => "CONTROL",
1632        };
1633        write!(f, "{}(data={})", name, self.data)
1634    }
1635}
1636
1637impl fmt::Display for PvaSearchResponsePayload {
1638    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1639        let found_text = if self.found { "true" } else { "false" };
1640        if self.cids.is_empty() {
1641            write!(
1642                f,
1643                "SearchResponse(found={}, proto={})",
1644                found_text, self.protocol
1645            )
1646        } else {
1647            write!(
1648                f,
1649                "SearchResponse(found={}, proto={}, cids=[{}])",
1650                found_text,
1651                self.protocol,
1652                self.cids
1653                    .iter()
1654                    .map(|c| c.to_string())
1655                    .collect::<Vec<String>>()
1656                    .join(",")
1657            )
1658        }
1659    }
1660}
1661
1662impl fmt::Display for PvaConnectionValidationPayload {
1663    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1664        let dir = if self.is_server { "server" } else { "client" };
1665        let authz = self.authz.as_deref().unwrap_or("");
1666        if authz.is_empty() {
1667            write!(
1668                f,
1669                "ConnectionValidation(dir={}, qsize={}, isize={}, qos=0x{:04x})",
1670                dir, self.buffer_size, self.introspection_registry_size, self.qos
1671            )
1672        } else {
1673            write!(
1674                f,
1675                "ConnectionValidation(dir={}, qsize={}, isize={}, qos=0x{:04x}, authz={})",
1676                dir, self.buffer_size, self.introspection_registry_size, self.qos, authz
1677            )
1678        }
1679    }
1680}
1681
1682impl fmt::Display for PvaStatus {
1683    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1684        write!(
1685            f,
1686            "code={} message={} stack={}",
1687            self.code,
1688            self.message.as_deref().unwrap_or(""),
1689            self.stack.as_deref().unwrap_or("")
1690        )
1691    }
1692}
1693
1694impl fmt::Display for PvaConnectionValidatedPayload {
1695    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1696        match &self.status {
1697            Some(s) => write!(f, "ConnectionValidated(status={})", s.code),
1698            None => write!(f, "ConnectionValidated(status=OK)"),
1699        }
1700    }
1701}
1702
1703impl fmt::Display for PvaAuthNzPayload {
1704    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1705        if !self.strings.is_empty() {
1706            write!(f, "AuthNZ(strings=[{}])", self.strings.join(","))
1707        } else {
1708            write!(f, "AuthNZ(raw_len={})", self.raw.len())
1709        }
1710    }
1711}
1712
1713impl fmt::Display for PvaAclChangePayload {
1714    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1715        match &self.status {
1716            Some(s) => write!(f, "ACL_CHANGE(status={})", s.code),
1717            None => write!(f, "ACL_CHANGE(status=OK)"),
1718        }
1719    }
1720}
1721
1722impl fmt::Display for PvaGetFieldPayload {
1723    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1724        if self.is_server {
1725            let status = self.status.as_ref().map(|s| s.code).unwrap_or(0xff);
1726            write!(f, "GET_FIELD(status={})", status)
1727        } else {
1728            let field = self.field_name.as_deref().unwrap_or("");
1729            if field.is_empty() {
1730                write!(f, "GET_FIELD(cid={})", self.cid)
1731            } else {
1732                write!(f, "GET_FIELD(cid={}, field={})", self.cid, field)
1733            }
1734        }
1735    }
1736}
1737
1738impl fmt::Display for PvaMessagePayload {
1739    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1740        match &self.status {
1741            Some(s) => {
1742                if let Some(msg) = &s.message {
1743                    write!(f, "MESSAGE(status={}, msg='{}')", s.code, msg)
1744                } else {
1745                    write!(f, "MESSAGE(status={})", s.code)
1746                }
1747            }
1748            None => write!(f, "MESSAGE(status=OK)"),
1749        }
1750    }
1751}
1752
1753impl fmt::Display for PvaMultipleDataPayload {
1754    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1755        if self.entries.is_empty() {
1756            write!(f, "MULTIPLE_DATA(raw_len={})", self.raw.len())
1757        } else {
1758            write!(f, "MULTIPLE_DATA(entries={})", self.entries.len())
1759        }
1760    }
1761}
1762
1763impl fmt::Display for PvaCancelRequestPayload {
1764    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1765        let status = self.status.as_ref().map(|s| s.code);
1766        match status {
1767            Some(code) => write!(f, "CANCEL_REQUEST(id={}, status={})", self.request_id, code),
1768            None => write!(f, "CANCEL_REQUEST(id={})", self.request_id),
1769        }
1770    }
1771}
1772
1773impl fmt::Display for PvaDestroyRequestPayload {
1774    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1775        let status = self.status.as_ref().map(|s| s.code);
1776        match status {
1777            Some(code) => write!(
1778                f,
1779                "DESTROY_REQUEST(id={}, status={})",
1780                self.request_id, code
1781            ),
1782            None => write!(f, "DESTROY_REQUEST(id={})", self.request_id),
1783        }
1784    }
1785}
1786
1787impl fmt::Display for PvaOriginTagPayload {
1788    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1789        write!(f, "ORIGIN_TAG(addr={})", format_pva_address(&self.address))
1790    }
1791}
1792
1793impl fmt::Display for PvaUnknownPayload {
1794    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1795        let kind = if self.is_control {
1796            "CONTROL"
1797        } else {
1798            "APPLICATION"
1799        };
1800        write!(
1801            f,
1802            "UNKNOWN(cmd={}, type={}, raw_len={})",
1803            self.command, kind, self.raw_len
1804        )
1805    }
1806}
1807
1808// generic display for all payloads
1809impl fmt::Display for PvaPacketCommand {
1810    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1811        match self {
1812            PvaPacketCommand::Control(payload) => write!(f, "{}", payload),
1813            PvaPacketCommand::Search(payload) => write!(f, "{}", payload),
1814            PvaPacketCommand::SearchResponse(payload) => write!(f, "{}", payload),
1815            PvaPacketCommand::Beacon(payload) => write!(f, "{}", payload),
1816            PvaPacketCommand::ConnectionValidation(payload) => write!(f, "{}", payload),
1817            PvaPacketCommand::ConnectionValidated(payload) => write!(f, "{}", payload),
1818            PvaPacketCommand::AuthNZ(payload) => write!(f, "{}", payload),
1819            PvaPacketCommand::AclChange(payload) => write!(f, "{}", payload),
1820            PvaPacketCommand::Op(payload) => write!(f, "{}", payload),
1821            PvaPacketCommand::CreateChannel(payload) => write!(f, "{}", payload),
1822            PvaPacketCommand::DestroyChannel(payload) => write!(f, "{}", payload),
1823            PvaPacketCommand::GetField(payload) => write!(f, "{}", payload),
1824            PvaPacketCommand::Message(payload) => write!(f, "{}", payload),
1825            PvaPacketCommand::MultipleData(payload) => write!(f, "{}", payload),
1826            PvaPacketCommand::CancelRequest(payload) => write!(f, "{}", payload),
1827            PvaPacketCommand::DestroyRequest(payload) => write!(f, "{}", payload),
1828            PvaPacketCommand::OriginTag(payload) => write!(f, "{}", payload),
1829            PvaPacketCommand::Echo(bytes) => write!(f, "ECHO ({} bytes)", bytes.len()),
1830            PvaPacketCommand::Unknown(payload) => write!(f, "{}", payload),
1831        }
1832    }
1833}
1834
1835impl fmt::Display for PvaOpPayload {
1836    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1837        let cmd_name = match self.command {
1838            10 => "GET",
1839            11 => "PUT",
1840            12 => "PUT_GET",
1841            13 => "MONITOR",
1842            14 => "ARRAY",
1843            16 => "PROCESS",
1844            20 => "RPC",
1845            _ => "OP",
1846        };
1847
1848        let status_text = if let Some(s) = &self.status {
1849            match &s.message {
1850                Some(m) if !m.is_empty() => format!(" status={} msg='{}'", s.code, m),
1851                _ => format!(" status={}", s.code),
1852            }
1853        } else {
1854            String::new()
1855        };
1856
1857        // Show decoded value if available, otherwise fall back to heuristic strings
1858        let value_text = if let Some(ref decoded) = self.decoded_value {
1859            let formatted = format_compact_value(decoded);
1860            if formatted.is_empty() || formatted == "{}" {
1861                String::new()
1862            } else {
1863                format!(" [{}]", formatted)
1864            }
1865        } else if !self.pv_names.is_empty() {
1866            format!(" data=[{}]", self.pv_names.join(","))
1867        } else {
1868            String::new()
1869        };
1870
1871        if self.is_server {
1872            write!(
1873                f,
1874                "{}(ioid={}, sub=0x{:02x}{}{})",
1875                cmd_name, self.ioid, self.subcmd, status_text, value_text
1876            )
1877        } else {
1878            write!(
1879                f,
1880                "{}(sid={}, ioid={}, sub=0x{:02x}{}{})",
1881                cmd_name, self.sid_or_cid, self.ioid, self.subcmd, status_text, value_text
1882            )
1883        }
1884    }
1885}
1886
1887#[cfg(test)]
1888mod tests {
1889    use super::*;
1890    use crate::spvd_decode::extract_nt_scalar_value;
1891    use crate::spvd_encode::{
1892        encode_nt_payload_bitset_parts, encode_nt_scalar_bitset_parts, encode_size_pvd,
1893        nt_payload_desc, nt_scalar_desc,
1894    };
1895    use crate::spvirit_encode::encode_header;
1896    use spvirit_types::{NtPayload, NtScalar, NtScalarArray, ScalarArrayValue, ScalarValue};
1897
1898    #[test]
1899    fn test_decode_status_ok() {
1900        let raw = [0xff];
1901        let (status, consumed) = decode_status(&raw, false);
1902        assert!(status.is_none());
1903        assert_eq!(consumed, 1);
1904    }
1905
1906    #[test]
1907    fn test_decode_status_message() {
1908        let raw = [1u8, 2, b'h', b'i', 2, b's', b't'];
1909        let (status, consumed) = decode_status(&raw, false);
1910        assert_eq!(consumed, 7);
1911        let status = status.unwrap();
1912        assert_eq!(status.code, 1);
1913        assert_eq!(status.message.as_deref(), Some("hi"));
1914        assert_eq!(status.stack.as_deref(), Some("st"));
1915    }
1916
1917    #[test]
1918    fn test_search_response_decode() {
1919        let mut raw: Vec<u8> = vec![];
1920        raw.extend_from_slice(&[0u8; 12]); // guid
1921        raw.extend_from_slice(&1u32.to_le_bytes()); // seq
1922        raw.extend_from_slice(&[0u8; 16]); // addr
1923        raw.extend_from_slice(&5076u16.to_le_bytes()); // port
1924        raw.push(3); // protocol size
1925        raw.extend_from_slice(b"tcp");
1926        raw.push(1); // found
1927        raw.extend_from_slice(&1u16.to_le_bytes()); // count
1928        raw.extend_from_slice(&42u32.to_le_bytes()); // cid
1929
1930        let decoded = PvaSearchResponsePayload::new(&raw, false).unwrap();
1931        assert!(decoded.found);
1932        assert_eq!(decoded.protocol, "tcp");
1933        assert_eq!(decoded.cids, vec![42u32]);
1934    }
1935
1936    fn build_monitor_packet(ioid: u32, subcmd: u8, body: &[u8]) -> Vec<u8> {
1937        let mut payload = Vec::new();
1938        payload.extend_from_slice(&ioid.to_le_bytes());
1939        payload.push(subcmd);
1940        payload.extend_from_slice(body);
1941        let mut out = encode_header(true, false, false, 2, 13, payload.len() as u32);
1942        out.extend_from_slice(&payload);
1943        out
1944    }
1945
1946    #[test]
1947    fn test_monitor_decode_overrun_and_legacy() {
1948        let nt = NtScalar::from_value(ScalarValue::F64(3.5));
1949        let desc = nt_scalar_desc(&nt.value);
1950        let (changed_bitset, values) = encode_nt_scalar_bitset_parts(&nt, false);
1951
1952        let mut body_overrun = Vec::new();
1953        body_overrun.extend_from_slice(&changed_bitset);
1954        body_overrun.extend_from_slice(&encode_size_pvd(0, false));
1955        body_overrun.extend_from_slice(&values);
1956
1957        let pkt = build_monitor_packet(1, 0x00, &body_overrun);
1958        let mut pva = PvaPacket::new(&pkt);
1959        let mut cmd = pva.decode_payload().expect("decoded");
1960        if let PvaPacketCommand::Op(ref mut op) = cmd {
1961            op.decode_with_field_desc(&desc, false);
1962            let decoded = op.decoded_value.as_ref().expect("decoded");
1963            let value = extract_nt_scalar_value(decoded).expect("value");
1964            match value {
1965                DecodedValue::Float64(v) => assert!((*v - 3.5).abs() < 1e-6),
1966                other => panic!("unexpected value {:?}", other),
1967            }
1968        } else {
1969            panic!("unexpected cmd");
1970        }
1971
1972        let mut body_legacy = Vec::new();
1973        body_legacy.extend_from_slice(&changed_bitset);
1974        body_legacy.extend_from_slice(&values);
1975
1976        let pkt = build_monitor_packet(1, 0x00, &body_legacy);
1977        let mut pva = PvaPacket::new(&pkt);
1978        let mut cmd = pva.decode_payload().expect("decoded");
1979        if let PvaPacketCommand::Op(ref mut op) = cmd {
1980            op.decode_with_field_desc(&desc, false);
1981            let decoded = op.decoded_value.as_ref().expect("decoded");
1982            let value = extract_nt_scalar_value(decoded).expect("value");
1983            match value {
1984                DecodedValue::Float64(v) => assert!((*v - 3.5).abs() < 1e-6),
1985                other => panic!("unexpected value {:?}", other),
1986            }
1987        } else {
1988            panic!("unexpected cmd");
1989        }
1990
1991        let mut body_spec = Vec::new();
1992        body_spec.extend_from_slice(&changed_bitset);
1993        body_spec.extend_from_slice(&values);
1994        body_spec.extend_from_slice(&encode_size_pvd(0, false));
1995
1996        let pkt = build_monitor_packet(1, 0x00, &body_spec);
1997        let mut pva = PvaPacket::new(&pkt);
1998        let mut cmd = pva.decode_payload().expect("decoded");
1999        if let PvaPacketCommand::Op(ref mut op) = cmd {
2000            op.decode_with_field_desc(&desc, false);
2001            let decoded = op.decoded_value.as_ref().expect("decoded");
2002            let value = extract_nt_scalar_value(decoded).expect("value");
2003            match value {
2004                DecodedValue::Float64(v) => assert!((*v - 3.5).abs() < 1e-6),
2005                other => panic!("unexpected value {:?}", other),
2006            }
2007        } else {
2008            panic!("unexpected cmd");
2009        }
2010    }
2011
2012    #[test]
2013    fn test_monitor_decode_prefers_spec_order_for_array_payload() {
2014        let payload_value =
2015            NtPayload::ScalarArray(NtScalarArray::from_value(ScalarArrayValue::F64(vec![
2016                1.0, 2.0, 3.0, 4.0,
2017            ])));
2018        let desc = nt_payload_desc(&payload_value);
2019        let (changed_bitset, values) = encode_nt_payload_bitset_parts(&payload_value, false);
2020
2021        let mut body_spec = Vec::new();
2022        body_spec.extend_from_slice(&changed_bitset);
2023        body_spec.extend_from_slice(&values);
2024        body_spec.extend_from_slice(&encode_size_pvd(0, false));
2025
2026        let pkt = build_monitor_packet(11, 0x00, &body_spec);
2027        let mut pva = PvaPacket::new(&pkt);
2028        let mut cmd = pva.decode_payload().expect("decoded");
2029        if let PvaPacketCommand::Op(ref mut op) = cmd {
2030            op.decode_with_field_desc(&desc, false);
2031            let decoded = op.decoded_value.as_ref().expect("decoded");
2032            let value = extract_nt_scalar_value(decoded).expect("value");
2033            match value {
2034                DecodedValue::Array(items) => {
2035                    assert_eq!(items.len(), 4);
2036                    assert!(matches!(items[0], DecodedValue::Float64(v) if (v - 1.0).abs() < 1e-6));
2037                    assert!(matches!(items[3], DecodedValue::Float64(v) if (v - 4.0).abs() < 1e-6));
2038                }
2039                other => panic!("unexpected value {:?}", other),
2040            }
2041        } else {
2042            panic!("unexpected cmd");
2043        }
2044    }
2045
2046    #[test]
2047    fn pva_status_reports_error_state() {
2048        let ok = PvaStatus {
2049            code: 0,
2050            message: None,
2051            stack: None,
2052        };
2053        let err = PvaStatus {
2054            code: 2,
2055            message: Some("bad".to_string()),
2056            stack: None,
2057        };
2058        assert!(!ok.is_error());
2059        assert!(err.is_error());
2060    }
2061
2062    #[test]
2063    fn pva_status_display_includes_message_and_stack() {
2064        let status = PvaStatus {
2065            code: 2,
2066            message: Some("bad".to_string()),
2067            stack: Some("trace".to_string()),
2068        };
2069        assert_eq!(status.to_string(), "code=2 message=bad stack=trace");
2070    }
2071
2072    #[test]
2073    fn decode_op_response_status_reads_status_from_packet() {
2074        let raw = vec![
2075            0xCA, 0x02, 0x40, 0x0B, 0x0A, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, 0x00, 0x02,
2076            0x03, b'b', b'a', b'd', 0x00,
2077        ];
2078        let status = decode_op_response_status(&raw, false)
2079            .expect("status parse")
2080            .expect("status");
2081        assert!(status.is_error());
2082        assert_eq!(status.message.as_deref(), Some("bad"));
2083    }
2084}