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 && ((subcmd & 0x08) != 0 || (command != 13 && command != 14));
1414
1415        if !body.is_empty() {
1416            if has_status {
1417                let (parsed, consumed) = decode_status(&body, is_be);
1418                status = parsed;
1419                pvd_raw = if body.len() > consumed {
1420                    body[consumed..].to_vec()
1421                } else {
1422                    vec![]
1423                };
1424            } else {
1425                pvd_raw = body.clone();
1426            }
1427        }
1428
1429        let pv_names = extract_pv_names(&pvd_raw);
1430
1431        // Try to parse introspection from INIT response (subcmd & 0x08 and is_server)
1432        let introspection = if is_server && (subcmd & 0x08) != 0 && !pvd_raw.is_empty() {
1433            let decoder = PvdDecoder::new(is_be);
1434            decoder.parse_introspection(&pvd_raw)
1435        } else {
1436            None
1437        };
1438
1439        let result = Some(Self {
1440            sid_or_cid,
1441            ioid,
1442            subcmd,
1443            body: pvd_raw,
1444            command,
1445            is_server,
1446            status: status.clone(),
1447            pv_names,
1448            introspection,
1449            decoded_value: None, // Will be set by packet processor with field_desc
1450        });
1451
1452        result
1453    }
1454
1455    /// Decode the body using provided field description
1456    pub fn decode_with_field_desc(&mut self, field_desc: &StructureDesc, is_be: bool) {
1457        if self.body.is_empty() {
1458            return;
1459        }
1460
1461        let decoder = PvdDecoder::new(is_be);
1462
1463        // For data updates (subcmd == 0x00 or subcmd & 0x40), use bitset decoding
1464        if self.subcmd == 0x00 || (self.subcmd & 0x40) != 0 {
1465            if self.command == 13 {
1466                let cand_overrun_pre =
1467                    decoder.decode_structure_with_bitset_and_overrun(&self.body, field_desc);
1468                let cand_overrun_post =
1469                    decoder.decode_structure_with_bitset_then_overrun(&self.body, field_desc);
1470                let cand_legacy = decoder.decode_structure_with_bitset(&self.body, field_desc);
1471                self.decoded_value =
1472                    choose_best_decoded_multi([cand_overrun_pre, cand_overrun_post, cand_legacy]);
1473            } else if let Some((value, _)) =
1474                decoder.decode_structure_with_bitset(&self.body, field_desc)
1475            {
1476                self.decoded_value = Some(value);
1477            }
1478        } else {
1479            // Full structure decode
1480            if let Some((value, _)) = decoder.decode_structure(&self.body, field_desc) {
1481                self.decoded_value = Some(value);
1482            }
1483        }
1484    }
1485}
1486
1487fn choose_best_decoded_multi(cands: [Option<(DecodedValue, usize)>; 3]) -> Option<DecodedValue> {
1488    let mut best_value: Option<DecodedValue> = None;
1489    let mut best_score = i32::MIN;
1490    let mut best_consumed = 0usize;
1491    let mut best_idx = 0usize;
1492
1493    for (idx, cand) in cands.into_iter().enumerate() {
1494        let Some((value, consumed)) = cand else {
1495            continue;
1496        };
1497        let score = score_decoded(&value);
1498        let better = score > best_score
1499            || (score == best_score && consumed > best_consumed)
1500            || (score == best_score && consumed == best_consumed && idx > best_idx);
1501        if better {
1502            best_score = score;
1503            best_consumed = consumed;
1504            best_idx = idx;
1505            best_value = Some(value);
1506        }
1507    }
1508
1509    best_value
1510}
1511
1512fn score_decoded(value: &DecodedValue) -> i32 {
1513    let DecodedValue::Structure(fields) = value else {
1514        return -1;
1515    };
1516
1517    let mut score = fields.len() as i32;
1518
1519    let mut has_value = false;
1520    let mut has_alarm = false;
1521    let mut has_ts = false;
1522
1523    for (name, val) in fields {
1524        match name.as_str() {
1525            "value" => {
1526                has_value = true;
1527                score += 4;
1528                match val {
1529                    DecodedValue::Array(items) => {
1530                        if items.is_empty() {
1531                            score -= 2;
1532                        } else {
1533                            score += 6 + (items.len().min(8) as i32);
1534                        }
1535                    }
1536                    DecodedValue::Structure(_) => score += 1,
1537                    _ => score += 2,
1538                }
1539            }
1540            "alarm" => {
1541                has_alarm = true;
1542                score += 2;
1543            }
1544            "timeStamp" => {
1545                has_ts = true;
1546                score += 2;
1547                if let DecodedValue::Structure(ts_fields) = val {
1548                    if let Some(secs) = ts_fields.iter().find_map(|(n, v)| {
1549                        if n == "secondsPastEpoch" {
1550                            if let DecodedValue::Int64(s) = v {
1551                                return Some(*s);
1552                            }
1553                        }
1554                        None
1555                    }) {
1556                        if (0..=4_000_000_000i64).contains(&secs) {
1557                            score += 2;
1558                        } else if secs.abs() > 10_000_000_000i64 {
1559                            score -= 2;
1560                        }
1561                    }
1562                }
1563            }
1564            "display" | "control" => {
1565                score += 1;
1566            }
1567            _ => {}
1568        }
1569    }
1570
1571    if !has_value {
1572        score -= 2;
1573    }
1574    if !has_alarm {
1575        score -= 1;
1576    }
1577    if !has_ts {
1578        score -= 1;
1579    }
1580
1581    score
1582}
1583
1584#[derive(Debug, Clone)]
1585pub struct PvaStatus {
1586    pub code: u8,
1587    pub message: Option<String>,
1588    pub stack: Option<String>,
1589}
1590
1591impl PvaStatus {
1592    pub fn is_error(&self) -> bool {
1593        self.code != 0
1594    }
1595}
1596
1597/// Display implementations
1598// beacon payload display
1599impl fmt::Display for PvaBeaconPayload {
1600    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1601        write!(
1602            f,
1603            "Beacon:GUID=[{}],Flags=[{}],SeqId=[{}],ChangeCount=[{}],ServerAddress=[{}],ServerPort=[{}],Protocol=[{}]",
1604            hex::encode(self.guid),
1605            self.flags,
1606            self.beacon_sequence_id,
1607            self.change_count,
1608            format_pva_address(&self.server_address),
1609            self.server_port,
1610            self.protocol
1611        )
1612    }
1613}
1614
1615// search payload display
1616impl fmt::Display for PvaSearchPayload {
1617    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1618        write!(f, "Search:PVs=[{}]", self.pv_names.join(","))
1619    }
1620}
1621
1622impl fmt::Display for PvaControlPayload {
1623    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1624        let name = match self.command {
1625            0 => "MARK_TOTAL_BYTES_SENT",
1626            1 => "ACK_TOTAL_BYTES_RECEIVED",
1627            2 => "SET_BYTE_ORDER",
1628            3 => "ECHO_REQUEST",
1629            4 => "ECHO_RESPONSE",
1630            _ => "CONTROL",
1631        };
1632        write!(f, "{}(data={})", name, self.data)
1633    }
1634}
1635
1636impl fmt::Display for PvaSearchResponsePayload {
1637    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1638        let found_text = if self.found { "true" } else { "false" };
1639        if self.cids.is_empty() {
1640            write!(
1641                f,
1642                "SearchResponse(found={}, proto={})",
1643                found_text, self.protocol
1644            )
1645        } else {
1646            write!(
1647                f,
1648                "SearchResponse(found={}, proto={}, cids=[{}])",
1649                found_text,
1650                self.protocol,
1651                self.cids
1652                    .iter()
1653                    .map(|c| c.to_string())
1654                    .collect::<Vec<String>>()
1655                    .join(",")
1656            )
1657        }
1658    }
1659}
1660
1661impl fmt::Display for PvaConnectionValidationPayload {
1662    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1663        let dir = if self.is_server { "server" } else { "client" };
1664        let authz = self.authz.as_deref().unwrap_or("");
1665        if authz.is_empty() {
1666            write!(
1667                f,
1668                "ConnectionValidation(dir={}, qsize={}, isize={}, qos=0x{:04x})",
1669                dir, self.buffer_size, self.introspection_registry_size, self.qos
1670            )
1671        } else {
1672            write!(
1673                f,
1674                "ConnectionValidation(dir={}, qsize={}, isize={}, qos=0x{:04x}, authz={})",
1675                dir, self.buffer_size, self.introspection_registry_size, self.qos, authz
1676            )
1677        }
1678    }
1679}
1680
1681impl fmt::Display for PvaStatus {
1682    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1683        write!(
1684            f,
1685            "code={} message={} stack={}",
1686            self.code,
1687            self.message.as_deref().unwrap_or(""),
1688            self.stack.as_deref().unwrap_or("")
1689        )
1690    }
1691}
1692
1693impl fmt::Display for PvaConnectionValidatedPayload {
1694    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1695        match &self.status {
1696            Some(s) => write!(f, "ConnectionValidated(status={})", s.code),
1697            None => write!(f, "ConnectionValidated(status=OK)"),
1698        }
1699    }
1700}
1701
1702impl fmt::Display for PvaAuthNzPayload {
1703    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1704        if !self.strings.is_empty() {
1705            write!(f, "AuthNZ(strings=[{}])", self.strings.join(","))
1706        } else {
1707            write!(f, "AuthNZ(raw_len={})", self.raw.len())
1708        }
1709    }
1710}
1711
1712impl fmt::Display for PvaAclChangePayload {
1713    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1714        match &self.status {
1715            Some(s) => write!(f, "ACL_CHANGE(status={})", s.code),
1716            None => write!(f, "ACL_CHANGE(status=OK)"),
1717        }
1718    }
1719}
1720
1721impl fmt::Display for PvaGetFieldPayload {
1722    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1723        if self.is_server {
1724            let status = self.status.as_ref().map(|s| s.code).unwrap_or(0xff);
1725            write!(f, "GET_FIELD(status={})", status)
1726        } else {
1727            let field = self.field_name.as_deref().unwrap_or("");
1728            if field.is_empty() {
1729                write!(f, "GET_FIELD(cid={})", self.cid)
1730            } else {
1731                write!(f, "GET_FIELD(cid={}, field={})", self.cid, field)
1732            }
1733        }
1734    }
1735}
1736
1737impl fmt::Display for PvaMessagePayload {
1738    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1739        match &self.status {
1740            Some(s) => {
1741                if let Some(msg) = &s.message {
1742                    write!(f, "MESSAGE(status={}, msg='{}')", s.code, msg)
1743                } else {
1744                    write!(f, "MESSAGE(status={})", s.code)
1745                }
1746            }
1747            None => write!(f, "MESSAGE(status=OK)"),
1748        }
1749    }
1750}
1751
1752impl fmt::Display for PvaMultipleDataPayload {
1753    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1754        if self.entries.is_empty() {
1755            write!(f, "MULTIPLE_DATA(raw_len={})", self.raw.len())
1756        } else {
1757            write!(f, "MULTIPLE_DATA(entries={})", self.entries.len())
1758        }
1759    }
1760}
1761
1762impl fmt::Display for PvaCancelRequestPayload {
1763    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1764        let status = self.status.as_ref().map(|s| s.code);
1765        match status {
1766            Some(code) => write!(f, "CANCEL_REQUEST(id={}, status={})", self.request_id, code),
1767            None => write!(f, "CANCEL_REQUEST(id={})", self.request_id),
1768        }
1769    }
1770}
1771
1772impl fmt::Display for PvaDestroyRequestPayload {
1773    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1774        let status = self.status.as_ref().map(|s| s.code);
1775        match status {
1776            Some(code) => write!(
1777                f,
1778                "DESTROY_REQUEST(id={}, status={})",
1779                self.request_id, code
1780            ),
1781            None => write!(f, "DESTROY_REQUEST(id={})", self.request_id),
1782        }
1783    }
1784}
1785
1786impl fmt::Display for PvaOriginTagPayload {
1787    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1788        write!(f, "ORIGIN_TAG(addr={})", format_pva_address(&self.address))
1789    }
1790}
1791
1792impl fmt::Display for PvaUnknownPayload {
1793    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1794        let kind = if self.is_control {
1795            "CONTROL"
1796        } else {
1797            "APPLICATION"
1798        };
1799        write!(
1800            f,
1801            "UNKNOWN(cmd={}, type={}, raw_len={})",
1802            self.command, kind, self.raw_len
1803        )
1804    }
1805}
1806
1807// generic display for all payloads
1808impl fmt::Display for PvaPacketCommand {
1809    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1810        match self {
1811            PvaPacketCommand::Control(payload) => write!(f, "{}", payload),
1812            PvaPacketCommand::Search(payload) => write!(f, "{}", payload),
1813            PvaPacketCommand::SearchResponse(payload) => write!(f, "{}", payload),
1814            PvaPacketCommand::Beacon(payload) => write!(f, "{}", payload),
1815            PvaPacketCommand::ConnectionValidation(payload) => write!(f, "{}", payload),
1816            PvaPacketCommand::ConnectionValidated(payload) => write!(f, "{}", payload),
1817            PvaPacketCommand::AuthNZ(payload) => write!(f, "{}", payload),
1818            PvaPacketCommand::AclChange(payload) => write!(f, "{}", payload),
1819            PvaPacketCommand::Op(payload) => write!(f, "{}", payload),
1820            PvaPacketCommand::CreateChannel(payload) => write!(f, "{}", payload),
1821            PvaPacketCommand::DestroyChannel(payload) => write!(f, "{}", payload),
1822            PvaPacketCommand::GetField(payload) => write!(f, "{}", payload),
1823            PvaPacketCommand::Message(payload) => write!(f, "{}", payload),
1824            PvaPacketCommand::MultipleData(payload) => write!(f, "{}", payload),
1825            PvaPacketCommand::CancelRequest(payload) => write!(f, "{}", payload),
1826            PvaPacketCommand::DestroyRequest(payload) => write!(f, "{}", payload),
1827            PvaPacketCommand::OriginTag(payload) => write!(f, "{}", payload),
1828            PvaPacketCommand::Echo(bytes) => write!(f, "ECHO ({} bytes)", bytes.len()),
1829            PvaPacketCommand::Unknown(payload) => write!(f, "{}", payload),
1830        }
1831    }
1832}
1833
1834impl fmt::Display for PvaOpPayload {
1835    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1836        let cmd_name = match self.command {
1837            10 => "GET",
1838            11 => "PUT",
1839            12 => "PUT_GET",
1840            13 => "MONITOR",
1841            14 => "ARRAY",
1842            16 => "PROCESS",
1843            20 => "RPC",
1844            _ => "OP",
1845        };
1846
1847        let status_text = if let Some(s) = &self.status {
1848            match &s.message {
1849                Some(m) if !m.is_empty() => format!(" status={} msg='{}'", s.code, m),
1850                _ => format!(" status={}", s.code),
1851            }
1852        } else {
1853            String::new()
1854        };
1855
1856        // Show decoded value if available, otherwise fall back to heuristic strings
1857        let value_text = if let Some(ref decoded) = self.decoded_value {
1858            let formatted = format_compact_value(decoded);
1859            if formatted.is_empty() || formatted == "{}" {
1860                String::new()
1861            } else {
1862                format!(" [{}]", formatted)
1863            }
1864        } else if !self.pv_names.is_empty() {
1865            format!(" data=[{}]", self.pv_names.join(","))
1866        } else {
1867            String::new()
1868        };
1869
1870        if self.is_server {
1871            write!(
1872                f,
1873                "{}(ioid={}, sub=0x{:02x}{}{})",
1874                cmd_name, self.ioid, self.subcmd, status_text, value_text
1875            )
1876        } else {
1877            write!(
1878                f,
1879                "{}(sid={}, ioid={}, sub=0x{:02x}{}{})",
1880                cmd_name, self.sid_or_cid, self.ioid, self.subcmd, status_text, value_text
1881            )
1882        }
1883    }
1884}
1885
1886#[cfg(test)]
1887mod tests {
1888    use super::*;
1889    use crate::spvd_decode::extract_nt_scalar_value;
1890    use crate::spvd_encode::{
1891        encode_nt_payload_bitset_parts, encode_nt_scalar_bitset_parts, encode_size_pvd,
1892        nt_payload_desc, nt_scalar_desc,
1893    };
1894    use crate::spvirit_encode::encode_header;
1895    use spvirit_types::{NtPayload, NtScalar, NtScalarArray, ScalarArrayValue, ScalarValue};
1896
1897    #[test]
1898    fn test_decode_status_ok() {
1899        let raw = [0xff];
1900        let (status, consumed) = decode_status(&raw, false);
1901        assert!(status.is_none());
1902        assert_eq!(consumed, 1);
1903    }
1904
1905    #[test]
1906    fn test_decode_status_message() {
1907        let raw = [1u8, 2, b'h', b'i', 2, b's', b't'];
1908        let (status, consumed) = decode_status(&raw, false);
1909        assert_eq!(consumed, 7);
1910        let status = status.unwrap();
1911        assert_eq!(status.code, 1);
1912        assert_eq!(status.message.as_deref(), Some("hi"));
1913        assert_eq!(status.stack.as_deref(), Some("st"));
1914    }
1915
1916    #[test]
1917    fn test_search_response_decode() {
1918        let mut raw: Vec<u8> = vec![];
1919        raw.extend_from_slice(&[0u8; 12]); // guid
1920        raw.extend_from_slice(&1u32.to_le_bytes()); // seq
1921        raw.extend_from_slice(&[0u8; 16]); // addr
1922        raw.extend_from_slice(&5076u16.to_le_bytes()); // port
1923        raw.push(3); // protocol size
1924        raw.extend_from_slice(b"tcp");
1925        raw.push(1); // found
1926        raw.extend_from_slice(&1u16.to_le_bytes()); // count
1927        raw.extend_from_slice(&42u32.to_le_bytes()); // cid
1928
1929        let decoded = PvaSearchResponsePayload::new(&raw, false).unwrap();
1930        assert!(decoded.found);
1931        assert_eq!(decoded.protocol, "tcp");
1932        assert_eq!(decoded.cids, vec![42u32]);
1933    }
1934
1935    fn build_monitor_packet(ioid: u32, subcmd: u8, body: &[u8]) -> Vec<u8> {
1936        let mut payload = Vec::new();
1937        payload.extend_from_slice(&ioid.to_le_bytes());
1938        payload.push(subcmd);
1939        payload.extend_from_slice(body);
1940        let mut out = encode_header(true, false, false, 2, 13, payload.len() as u32);
1941        out.extend_from_slice(&payload);
1942        out
1943    }
1944
1945    #[test]
1946    fn test_monitor_decode_overrun_and_legacy() {
1947        let nt = NtScalar::from_value(ScalarValue::F64(3.5));
1948        let desc = nt_scalar_desc(&nt.value);
1949        let (changed_bitset, values) = encode_nt_scalar_bitset_parts(&nt, false);
1950
1951        let mut body_overrun = Vec::new();
1952        body_overrun.extend_from_slice(&changed_bitset);
1953        body_overrun.extend_from_slice(&encode_size_pvd(0, false));
1954        body_overrun.extend_from_slice(&values);
1955
1956        let pkt = build_monitor_packet(1, 0x00, &body_overrun);
1957        let mut pva = PvaPacket::new(&pkt);
1958        let mut cmd = pva.decode_payload().expect("decoded");
1959        if let PvaPacketCommand::Op(ref mut op) = cmd {
1960            op.decode_with_field_desc(&desc, false);
1961            let decoded = op.decoded_value.as_ref().expect("decoded");
1962            let value = extract_nt_scalar_value(decoded).expect("value");
1963            match value {
1964                DecodedValue::Float64(v) => assert!((*v - 3.5).abs() < 1e-6),
1965                other => panic!("unexpected value {:?}", other),
1966            }
1967        } else {
1968            panic!("unexpected cmd");
1969        }
1970
1971        let mut body_legacy = Vec::new();
1972        body_legacy.extend_from_slice(&changed_bitset);
1973        body_legacy.extend_from_slice(&values);
1974
1975        let pkt = build_monitor_packet(1, 0x00, &body_legacy);
1976        let mut pva = PvaPacket::new(&pkt);
1977        let mut cmd = pva.decode_payload().expect("decoded");
1978        if let PvaPacketCommand::Op(ref mut op) = cmd {
1979            op.decode_with_field_desc(&desc, false);
1980            let decoded = op.decoded_value.as_ref().expect("decoded");
1981            let value = extract_nt_scalar_value(decoded).expect("value");
1982            match value {
1983                DecodedValue::Float64(v) => assert!((*v - 3.5).abs() < 1e-6),
1984                other => panic!("unexpected value {:?}", other),
1985            }
1986        } else {
1987            panic!("unexpected cmd");
1988        }
1989
1990        let mut body_spec = Vec::new();
1991        body_spec.extend_from_slice(&changed_bitset);
1992        body_spec.extend_from_slice(&values);
1993        body_spec.extend_from_slice(&encode_size_pvd(0, false));
1994
1995        let pkt = build_monitor_packet(1, 0x00, &body_spec);
1996        let mut pva = PvaPacket::new(&pkt);
1997        let mut cmd = pva.decode_payload().expect("decoded");
1998        if let PvaPacketCommand::Op(ref mut op) = cmd {
1999            op.decode_with_field_desc(&desc, false);
2000            let decoded = op.decoded_value.as_ref().expect("decoded");
2001            let value = extract_nt_scalar_value(decoded).expect("value");
2002            match value {
2003                DecodedValue::Float64(v) => assert!((*v - 3.5).abs() < 1e-6),
2004                other => panic!("unexpected value {:?}", other),
2005            }
2006        } else {
2007            panic!("unexpected cmd");
2008        }
2009    }
2010
2011    #[test]
2012    fn test_monitor_decode_prefers_spec_order_for_array_payload() {
2013        let payload_value =
2014            NtPayload::ScalarArray(NtScalarArray::from_value(ScalarArrayValue::F64(vec![
2015                1.0, 2.0, 3.0, 4.0,
2016            ])));
2017        let desc = nt_payload_desc(&payload_value);
2018        let (changed_bitset, values) = encode_nt_payload_bitset_parts(&payload_value, false);
2019
2020        let mut body_spec = Vec::new();
2021        body_spec.extend_from_slice(&changed_bitset);
2022        body_spec.extend_from_slice(&values);
2023        body_spec.extend_from_slice(&encode_size_pvd(0, false));
2024
2025        let pkt = build_monitor_packet(11, 0x00, &body_spec);
2026        let mut pva = PvaPacket::new(&pkt);
2027        let mut cmd = pva.decode_payload().expect("decoded");
2028        if let PvaPacketCommand::Op(ref mut op) = cmd {
2029            op.decode_with_field_desc(&desc, false);
2030            let decoded = op.decoded_value.as_ref().expect("decoded");
2031            let value = extract_nt_scalar_value(decoded).expect("value");
2032            match value {
2033                DecodedValue::Array(items) => {
2034                    assert_eq!(items.len(), 4);
2035                    assert!(matches!(items[0], DecodedValue::Float64(v) if (v - 1.0).abs() < 1e-6));
2036                    assert!(matches!(items[3], DecodedValue::Float64(v) if (v - 4.0).abs() < 1e-6));
2037                }
2038                other => panic!("unexpected value {:?}", other),
2039            }
2040        } else {
2041            panic!("unexpected cmd");
2042        }
2043    }
2044
2045    #[test]
2046    fn pva_status_reports_error_state() {
2047        let ok = PvaStatus {
2048            code: 0,
2049            message: None,
2050            stack: None,
2051        };
2052        let err = PvaStatus {
2053            code: 2,
2054            message: Some("bad".to_string()),
2055            stack: None,
2056        };
2057        assert!(!ok.is_error());
2058        assert!(err.is_error());
2059    }
2060
2061    #[test]
2062    fn pva_status_display_includes_message_and_stack() {
2063        let status = PvaStatus {
2064            code: 2,
2065            message: Some("bad".to_string()),
2066            stack: Some("trace".to_string()),
2067        };
2068        assert_eq!(status.to_string(), "code=2 message=bad stack=trace");
2069    }
2070
2071    #[test]
2072    fn decode_op_response_status_reads_status_from_packet() {
2073        let raw = vec![
2074            0xCA, 0x02, 0x40, 0x0B, 0x0A, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, 0x00, 0x02,
2075            0x03, b'b', b'a', b'd', 0x00,
2076        ];
2077        let status = decode_op_response_status(&raw, false)
2078            .expect("status parse")
2079            .expect("status");
2080        assert!(status.is_error());
2081        assert_eq!(status.message.as_deref(), Some("bad"));
2082    }
2083}