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 sid: u32,
879    pub request_id: u32,
880}
881
882impl PvaDestroyRequestPayload {
883    /// Decode a `destroyRequest` (0x0F) payload.
884    ///
885    /// The PVA spec payload is `serverChannelID (i32)` followed by
886    /// `requestID (i32)`. Older spvirit clients sent only the 4-byte
887    /// requestID; that legacy form is still accepted (with `sid` = 0).
888    pub fn new(raw: &[u8], is_be: bool) -> Option<Self> {
889        let word = |range: std::ops::Range<usize>| -> Option<u32> {
890            let bytes = raw.get(range)?.try_into().ok()?;
891            Some(if is_be {
892                u32::from_be_bytes(bytes)
893            } else {
894                u32::from_le_bytes(bytes)
895            })
896        };
897        if raw.len() >= 8 {
898            Some(Self {
899                sid: word(0..4)?,
900                request_id: word(4..8)?,
901            })
902        } else if raw.len() >= 4 {
903            Some(Self {
904                sid: 0,
905                request_id: word(0..4)?,
906            })
907        } else {
908            debug!("PvaDestroyRequestPayload::new: raw too short {}", raw.len());
909            None
910        }
911    }
912}
913
914#[derive(Debug)]
915pub struct PvaOriginTagPayload {
916    pub address: [u8; 16],
917}
918
919impl PvaOriginTagPayload {
920    pub fn new(raw: &[u8]) -> Option<Self> {
921        if raw.len() < 16 {
922            debug!("PvaOriginTagPayload::new: raw too short {}", raw.len());
923            return None;
924        }
925        let address: [u8; 16] = raw[0..16].try_into().ok()?;
926        Some(Self { address })
927    }
928}
929
930#[derive(Debug)]
931pub struct PvaUnknownPayload {
932    pub command: u8,
933    pub is_control: bool,
934    pub raw_len: usize,
935}
936
937impl PvaUnknownPayload {
938    pub fn new(command: u8, is_control: bool, raw_len: usize) -> Self {
939        Self {
940            command,
941            is_control,
942            raw_len,
943        }
944    }
945}
946
947/// payload decoder
948/// SEARCH
949#[derive(Debug)]
950pub struct PvaSearchPayload {
951    pub seq: u32,
952    pub mask: u8,
953    pub addr: [u8; 16],
954    pub port: u16,
955    pub protocols: Vec<String>,
956    pub pv_requests: Vec<(u32, String)>,
957    pub pv_names: Vec<String>,
958}
959
960impl PvaSearchPayload {
961    pub fn new(raw: &[u8], is_be: bool) -> Option<Self> {
962        if raw.is_empty() {
963            debug!("PvaSearchPayload::new received an empty raw slice.");
964            return None;
965        }
966        const MIN_FIXED_SEARCH_PAYLOAD_SIZE: usize = 26;
967        if raw.len() < MIN_FIXED_SEARCH_PAYLOAD_SIZE {
968            debug!(
969                "PvaSearchPayload::new: raw slice length {} is less than min fixed size {}.",
970                raw.len(),
971                MIN_FIXED_SEARCH_PAYLOAD_SIZE
972            );
973            return None;
974        }
975
976        let seq = if is_be {
977            u32::from_be_bytes(raw[0..4].try_into().unwrap())
978        } else {
979            u32::from_le_bytes(raw[0..4].try_into().unwrap())
980        };
981
982        let mask = raw[4];
983        let addr: [u8; 16] = raw[8..24].try_into().unwrap();
984        let port = if is_be {
985            u16::from_be_bytes(raw[24..26].try_into().unwrap())
986        } else {
987            u16::from_le_bytes(raw[24..26].try_into().unwrap())
988        };
989
990        let mut offset = 26;
991
992        let (protocol_count, consumed) = decode_size(&raw[offset..], is_be)?;
993        offset += consumed;
994
995        let mut protocols = vec![];
996        for _ in 0..protocol_count {
997            let (protocol, len) = decode_string(&raw[offset..], is_be)?;
998            protocols.push(protocol);
999            offset += len;
1000        }
1001
1002        // PV names here
1003        if raw.len() < offset + 2 {
1004            return None;
1005        }
1006        let pv_count = if is_be {
1007            u16::from_be_bytes(raw[offset..offset + 2].try_into().unwrap())
1008        } else {
1009            u16::from_le_bytes(raw[offset..offset + 2].try_into().unwrap())
1010        };
1011        offset += 2;
1012
1013        let mut pv_names = vec![];
1014        let mut pv_requests = vec![];
1015        for _ in 0..pv_count {
1016            if raw.len() < offset + 4 {
1017                debug!(
1018                    "PvaSearchPayload::new: not enough data for PV CID at offset {}. Raw len: {}",
1019                    offset,
1020                    raw.len()
1021                );
1022                return None;
1023            }
1024            let cid = if is_be {
1025                u32::from_be_bytes(raw[offset..offset + 4].try_into().unwrap())
1026            } else {
1027                u32::from_le_bytes(raw[offset..offset + 4].try_into().unwrap())
1028            };
1029            offset += 4;
1030            let (pv_name, len) = decode_string(&raw[offset..], is_be)?;
1031            pv_names.push(pv_name.clone());
1032            pv_requests.push((cid, pv_name));
1033            offset += len;
1034        }
1035
1036        Some(Self {
1037            seq,
1038            mask,
1039            addr,
1040            port,
1041            protocols,
1042            pv_requests,
1043            pv_names,
1044        })
1045    }
1046}
1047
1048/// struct beaconMessage {
1049#[derive(Debug)]
1050pub struct PvaBeaconPayload {
1051    pub guid: [u8; 12],
1052    pub flags: u8,
1053    pub beacon_sequence_id: u8,
1054    pub change_count: u16,
1055    pub server_address: [u8; 16],
1056    pub server_port: u16,
1057    pub protocol: String,
1058    pub server_status_if: String,
1059}
1060
1061impl PvaBeaconPayload {
1062    pub fn new(raw: &[u8], is_be: bool) -> Option<Self> {
1063        // guid(12) + flags(1) + beacon_sequence_id(1) + change_count(2) + server_address(16) + server_port(2)
1064        const MIN_FIXED_BEACON_PAYLOAD_SIZE: usize = 12 + 1 + 1 + 2 + 16 + 2;
1065
1066        if raw.len() < MIN_FIXED_BEACON_PAYLOAD_SIZE {
1067            debug!(
1068                "PvaBeaconPayload::new: raw slice length {} is less than min fixed size {}.",
1069                raw.len(),
1070                MIN_FIXED_BEACON_PAYLOAD_SIZE
1071            );
1072            return None;
1073        }
1074
1075        let guid: [u8; 12] = raw[0..12].try_into().unwrap();
1076        let flags = raw[12];
1077        let beacon_sequence_id = raw[13];
1078        let change_count = if is_be {
1079            u16::from_be_bytes(raw[14..16].try_into().unwrap())
1080        } else {
1081            u16::from_le_bytes(raw[14..16].try_into().unwrap())
1082        };
1083        let server_address: [u8; 16] = raw[16..32].try_into().unwrap();
1084        let server_port = if is_be {
1085            u16::from_be_bytes(raw[32..34].try_into().unwrap())
1086        } else {
1087            u16::from_le_bytes(raw[32..34].try_into().unwrap())
1088        };
1089        let (protocol, len) = decode_string(&raw[34..], is_be)?;
1090        let protocol = protocol;
1091        let server_status_if = if len > 0 {
1092            let (server_status_if, _server_status_len) = decode_string(&raw[34 + len..], is_be)?;
1093            server_status_if
1094        } else {
1095            String::new()
1096        };
1097
1098        Some(Self {
1099            guid,
1100            flags,
1101            beacon_sequence_id,
1102            change_count,
1103            server_address,
1104            server_port,
1105            protocol,
1106            server_status_if,
1107        })
1108    }
1109}
1110
1111/// CREATE_CHANNEL payload (cmd=7)
1112/// Client: count(2), then for each: cid(4), pv_name(string)
1113/// Server: cid(4), sid(4), status
1114#[derive(Debug)]
1115pub struct PvaCreateChannelPayload {
1116    /// Is this from server (response) or client (request)?
1117    pub is_server: bool,
1118    /// For client requests: list of (cid, pv_name) tuples
1119    pub channels: Vec<(u32, String)>,
1120    /// For server response: client channel ID
1121    pub cid: u32,
1122    /// For server response: server channel ID
1123    pub sid: u32,
1124    /// For server response: status
1125    pub status: Option<PvaStatus>,
1126}
1127
1128impl PvaCreateChannelPayload {
1129    pub fn new(raw: &[u8], is_be: bool, is_server: bool) -> Option<Self> {
1130        if raw.is_empty() {
1131            debug!("PvaCreateChannelPayload::new received an empty raw slice.");
1132            return None;
1133        }
1134
1135        if is_server {
1136            // Server response: cid(4), sid(4), status
1137            if raw.len() < 8 {
1138                debug!("CREATE_CHANNEL server response too short: {}", raw.len());
1139                return None;
1140            }
1141
1142            let cid = if is_be {
1143                u32::from_be_bytes(raw[0..4].try_into().unwrap())
1144            } else {
1145                u32::from_le_bytes(raw[0..4].try_into().unwrap())
1146            };
1147
1148            let sid = if is_be {
1149                u32::from_be_bytes(raw[4..8].try_into().unwrap())
1150            } else {
1151                u32::from_le_bytes(raw[4..8].try_into().unwrap())
1152            };
1153
1154            // Decode status if present
1155            let status = if raw.len() > 8 {
1156                let code = raw[8];
1157                if code == 0xff {
1158                    None // OK, no status message
1159                } else {
1160                    let mut idx = 9;
1161                    let message = if idx < raw.len() {
1162                        decode_string(&raw[idx..], is_be).map(|(msg, consumed)| {
1163                            idx += consumed;
1164                            msg
1165                        })
1166                    } else {
1167                        None
1168                    };
1169                    let stack = if idx < raw.len() {
1170                        decode_string(&raw[idx..], is_be).map(|(s, _)| s)
1171                    } else {
1172                        None
1173                    };
1174                    Some(PvaStatus {
1175                        code,
1176                        message,
1177                        stack,
1178                    })
1179                }
1180            } else {
1181                None
1182            };
1183
1184            Some(Self {
1185                is_server: true,
1186                channels: vec![],
1187                cid,
1188                sid,
1189                status,
1190            })
1191        } else {
1192            // Client request: count(2), then for each: cid(4), pv_name(string)
1193            if raw.len() < 2 {
1194                debug!("CREATE_CHANNEL client request too short: {}", raw.len());
1195                return None;
1196            }
1197
1198            let count = if is_be {
1199                u16::from_be_bytes(raw[0..2].try_into().unwrap())
1200            } else {
1201                u16::from_le_bytes(raw[0..2].try_into().unwrap())
1202            };
1203
1204            let mut offset = 2;
1205            let mut channels = Vec::with_capacity(count as usize);
1206
1207            for _ in 0..count {
1208                if raw.len() < offset + 4 {
1209                    debug!(
1210                        "CREATE_CHANNEL: not enough data for CID at offset {}",
1211                        offset
1212                    );
1213                    break;
1214                }
1215
1216                let cid = if is_be {
1217                    u32::from_be_bytes(raw[offset..offset + 4].try_into().unwrap())
1218                } else {
1219                    u32::from_le_bytes(raw[offset..offset + 4].try_into().unwrap())
1220                };
1221                offset += 4;
1222
1223                if let Some((pv_name, consumed)) = decode_string(&raw[offset..], is_be) {
1224                    offset += consumed;
1225                    channels.push((cid, pv_name));
1226                } else {
1227                    debug!(
1228                        "CREATE_CHANNEL: failed to decode PV name at offset {}",
1229                        offset
1230                    );
1231                    break;
1232                }
1233            }
1234
1235            Some(Self {
1236                is_server: false,
1237                channels,
1238                cid: 0,
1239                sid: 0,
1240                status: None,
1241            })
1242        }
1243    }
1244}
1245
1246impl fmt::Display for PvaCreateChannelPayload {
1247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1248        if self.is_server {
1249            let status_text = if let Some(s) = &self.status {
1250                format!(" status={}", s.code)
1251            } else {
1252                String::new()
1253            };
1254            write!(
1255                f,
1256                "CREATE_CHANNEL(cid={}, sid={}{})",
1257                self.cid, self.sid, status_text
1258            )
1259        } else {
1260            let pv_list: Vec<String> = self
1261                .channels
1262                .iter()
1263                .map(|(cid, name)| format!("{}:'{}'", cid, name))
1264                .collect();
1265            write!(f, "CREATE_CHANNEL({})", pv_list.join(", "))
1266        }
1267    }
1268}
1269
1270/// DESTROY_CHANNEL payload (cmd=8)
1271/// Format: sid(4), cid(4)
1272#[derive(Debug)]
1273pub struct PvaDestroyChannelPayload {
1274    /// Server channel ID
1275    pub sid: u32,
1276    /// Client channel ID
1277    pub cid: u32,
1278}
1279
1280impl PvaDestroyChannelPayload {
1281    pub fn new(raw: &[u8], is_be: bool) -> Option<Self> {
1282        if raw.len() < 8 {
1283            debug!("DESTROY_CHANNEL payload too short: {}", raw.len());
1284            return None;
1285        }
1286
1287        let sid = if is_be {
1288            u32::from_be_bytes(raw[0..4].try_into().unwrap())
1289        } else {
1290            u32::from_le_bytes(raw[0..4].try_into().unwrap())
1291        };
1292
1293        let cid = if is_be {
1294            u32::from_be_bytes(raw[4..8].try_into().unwrap())
1295        } else {
1296            u32::from_le_bytes(raw[4..8].try_into().unwrap())
1297        };
1298
1299        Some(Self { sid, cid })
1300    }
1301}
1302
1303impl fmt::Display for PvaDestroyChannelPayload {
1304    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1305        write!(f, "DESTROY_CHANNEL(sid={}, cid={})", self.sid, self.cid)
1306    }
1307}
1308
1309/// Generic operation payload (GET/PUT/PUT_GET/MONITOR/ARRAY/RPC)
1310#[derive(Debug)]
1311pub struct PvaOpPayload {
1312    pub sid_or_cid: u32,
1313    pub ioid: u32,
1314    pub subcmd: u8,
1315    pub body: Vec<u8>,
1316    pub command: u8,
1317    pub is_server: bool,
1318    pub status: Option<PvaStatus>,
1319    pub pv_names: Vec<String>,
1320    /// Parsed introspection data (for INIT responses)
1321    pub introspection: Option<StructureDesc>,
1322    /// Decoded value (when field_desc is available)
1323    pub decoded_value: Option<DecodedValue>,
1324}
1325
1326// Heuristic extraction of PV-like names from a PVD body.
1327fn extract_pv_names(raw: &[u8]) -> Vec<String> {
1328    let mut names: Vec<String> = Vec::new();
1329    let mut i = 0usize;
1330    while i < raw.len() {
1331        // start with an alphanumeric character
1332        if raw[i].is_ascii_alphanumeric() {
1333            let start = i;
1334            i += 1;
1335            while i < raw.len() {
1336                let b = raw[i];
1337                if b.is_ascii_alphanumeric()
1338                    || b == b':'
1339                    || b == b'.'
1340                    || b == b'_'
1341                    || b == b'-'
1342                    || b == b'/'
1343                {
1344                    i += 1;
1345                } else {
1346                    break;
1347                }
1348            }
1349            let len = i - start;
1350            if len >= 3 && len <= 128 {
1351                if let Ok(s) = std::str::from_utf8(&raw[start..start + len]) {
1352                    // validate candidate contains at least one alphabetic char
1353                    if s.chars().any(|c| c.is_ascii_alphabetic()) {
1354                        if !names.contains(&s.to_string()) {
1355                            names.push(s.to_string());
1356                            if names.len() >= 8 {
1357                                break;
1358                            }
1359                        }
1360                    }
1361                }
1362            }
1363        } else {
1364            i += 1;
1365        }
1366    }
1367    names
1368}
1369
1370impl PvaOpPayload {
1371    pub fn new(raw: &[u8], is_be: bool, is_server: bool, command: u8) -> Option<Self> {
1372        // operation payloads have slightly different fixed offsets depending on client/server
1373        if raw.len() < 5 {
1374            debug!("PvaOpPayload::new: raw too short {}", raw.len());
1375            return None;
1376        }
1377
1378        let (sid_or_cid, ioid, subcmd, offset) = if is_server {
1379            // server op: ioid(4), subcmd(1)
1380            if raw.len() < 5 {
1381                return None;
1382            }
1383            let ioid = 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 subcmd = raw[4];
1389            (0, ioid, subcmd, 5)
1390        } else {
1391            // client op: sid(4), ioid(4), subcmd(1)
1392            if raw.len() < 9 {
1393                return None;
1394            }
1395            let sid = if is_be {
1396                u32::from_be_bytes(raw[0..4].try_into().unwrap())
1397            } else {
1398                u32::from_le_bytes(raw[0..4].try_into().unwrap())
1399            };
1400            let ioid = if is_be {
1401                u32::from_be_bytes(raw[4..8].try_into().unwrap())
1402            } else {
1403                u32::from_le_bytes(raw[4..8].try_into().unwrap())
1404            };
1405            let subcmd = raw[8];
1406            (sid, ioid, subcmd, 9)
1407        };
1408
1409        let body = if raw.len() > offset {
1410            raw[offset..].to_vec()
1411        } else {
1412            vec![]
1413        };
1414
1415        // Status is only present in certain subcmd types:
1416        // Status format (per Lua dissector): first byte = code. If code==0xff (255) -> OK
1417        // shorthand (1 byte only). Otherwise follow with two length-prefixed strings:
1418        // message, stack.
1419        // Server responses carry a status prefix for INIT responses (subcmd & 0x08),
1420        // and for non-INIT responses on GET (10), PUT (11), PUT_GET (12).
1421        // Monitor (13) data updates (non-INIT) do NOT have a status prefix.
1422        let mut status: Option<PvaStatus> = None;
1423        let mut pvd_raw: Vec<u8> = vec![];
1424
1425        let has_status = is_server && ((subcmd & 0x08) != 0 || (command != 13 && command != 14));
1426
1427        if !body.is_empty() {
1428            if has_status {
1429                let (parsed, consumed) = decode_status(&body, is_be);
1430                status = parsed;
1431                pvd_raw = if body.len() > consumed {
1432                    body[consumed..].to_vec()
1433                } else {
1434                    vec![]
1435                };
1436            } else {
1437                pvd_raw = body.clone();
1438            }
1439        }
1440
1441        let pv_names = extract_pv_names(&pvd_raw);
1442
1443        // Try to parse introspection from INIT response (subcmd & 0x08 and is_server)
1444        let introspection = if is_server && (subcmd & 0x08) != 0 && !pvd_raw.is_empty() {
1445            let decoder = PvdDecoder::new(is_be);
1446            decoder.parse_introspection(&pvd_raw)
1447        } else {
1448            None
1449        };
1450
1451        let result = Some(Self {
1452            sid_or_cid,
1453            ioid,
1454            subcmd,
1455            body: pvd_raw,
1456            command,
1457            is_server,
1458            status: status.clone(),
1459            pv_names,
1460            introspection,
1461            decoded_value: None, // Will be set by packet processor with field_desc
1462        });
1463
1464        result
1465    }
1466
1467    /// Decode the body using provided field description
1468    pub fn decode_with_field_desc(&mut self, field_desc: &StructureDesc, is_be: bool) {
1469        if self.body.is_empty() {
1470            return;
1471        }
1472
1473        let decoder = PvdDecoder::new(is_be);
1474
1475        // For data updates (subcmd == 0x00 or subcmd & 0x40), use bitset decoding
1476        if self.subcmd == 0x00 || (self.subcmd & 0x40) != 0 {
1477            if self.command == 13 {
1478                let cand_overrun_pre =
1479                    decoder.decode_structure_with_bitset_and_overrun(&self.body, field_desc);
1480                let cand_overrun_post =
1481                    decoder.decode_structure_with_bitset_then_overrun(&self.body, field_desc);
1482                let cand_legacy = decoder.decode_structure_with_bitset(&self.body, field_desc);
1483                self.decoded_value =
1484                    choose_best_decoded_multi([cand_overrun_pre, cand_overrun_post, cand_legacy]);
1485            } else if let Some((value, _)) =
1486                decoder.decode_structure_with_bitset(&self.body, field_desc)
1487            {
1488                self.decoded_value = Some(value);
1489            }
1490        } else {
1491            // Full structure decode
1492            if let Some((value, _)) = decoder.decode_structure(&self.body, field_desc) {
1493                self.decoded_value = Some(value);
1494            }
1495        }
1496    }
1497}
1498
1499fn choose_best_decoded_multi(cands: [Option<(DecodedValue, usize)>; 3]) -> Option<DecodedValue> {
1500    let mut best_value: Option<DecodedValue> = None;
1501    let mut best_score = i32::MIN;
1502    let mut best_consumed = 0usize;
1503    let mut best_idx = 0usize;
1504
1505    for (idx, cand) in cands.into_iter().enumerate() {
1506        let Some((value, consumed)) = cand else {
1507            continue;
1508        };
1509        let score = score_decoded(&value);
1510        let better = score > best_score
1511            || (score == best_score && consumed > best_consumed)
1512            || (score == best_score && consumed == best_consumed && idx > best_idx);
1513        if better {
1514            best_score = score;
1515            best_consumed = consumed;
1516            best_idx = idx;
1517            best_value = Some(value);
1518        }
1519    }
1520
1521    best_value
1522}
1523
1524fn score_decoded(value: &DecodedValue) -> i32 {
1525    let DecodedValue::Structure(fields) = value else {
1526        return -1;
1527    };
1528
1529    let mut score = fields.len() as i32;
1530
1531    let mut has_value = false;
1532    let mut has_alarm = false;
1533    let mut has_ts = false;
1534
1535    for (name, val) in fields {
1536        match name.as_str() {
1537            "value" => {
1538                has_value = true;
1539                score += 4;
1540                match val {
1541                    DecodedValue::Array(items) => {
1542                        if items.is_empty() {
1543                            score -= 2;
1544                        } else {
1545                            score += 6 + (items.len().min(8) as i32);
1546                        }
1547                    }
1548                    DecodedValue::Structure(_) => score += 1,
1549                    _ => score += 2,
1550                }
1551            }
1552            "alarm" => {
1553                has_alarm = true;
1554                score += 2;
1555            }
1556            "timeStamp" => {
1557                has_ts = true;
1558                score += 2;
1559                if let DecodedValue::Structure(ts_fields) = val {
1560                    if let Some(secs) = ts_fields.iter().find_map(|(n, v)| {
1561                        if n == "secondsPastEpoch" {
1562                            if let DecodedValue::Int64(s) = v {
1563                                return Some(*s);
1564                            }
1565                        }
1566                        None
1567                    }) {
1568                        if (0..=4_000_000_000i64).contains(&secs) {
1569                            score += 2;
1570                        } else if secs.abs() > 10_000_000_000i64 {
1571                            score -= 2;
1572                        }
1573                    }
1574                }
1575            }
1576            "display" | "control" => {
1577                score += 1;
1578            }
1579            _ => {}
1580        }
1581    }
1582
1583    if !has_value {
1584        score -= 2;
1585    }
1586    if !has_alarm {
1587        score -= 1;
1588    }
1589    if !has_ts {
1590        score -= 1;
1591    }
1592
1593    score
1594}
1595
1596#[derive(Debug, Clone)]
1597pub struct PvaStatus {
1598    pub code: u8,
1599    pub message: Option<String>,
1600    pub stack: Option<String>,
1601}
1602
1603impl PvaStatus {
1604    pub fn is_error(&self) -> bool {
1605        self.code != 0
1606    }
1607}
1608
1609/// Display implementations
1610// beacon payload display
1611impl fmt::Display for PvaBeaconPayload {
1612    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1613        write!(
1614            f,
1615            "Beacon:GUID=[{}],Flags=[{}],SeqId=[{}],ChangeCount=[{}],ServerAddress=[{}],ServerPort=[{}],Protocol=[{}]",
1616            hex::encode(self.guid),
1617            self.flags,
1618            self.beacon_sequence_id,
1619            self.change_count,
1620            format_pva_address(&self.server_address),
1621            self.server_port,
1622            self.protocol
1623        )
1624    }
1625}
1626
1627// search payload display
1628impl fmt::Display for PvaSearchPayload {
1629    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1630        write!(f, "Search:PVs=[{}]", self.pv_names.join(","))
1631    }
1632}
1633
1634impl fmt::Display for PvaControlPayload {
1635    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1636        let name = match self.command {
1637            0 => "MARK_TOTAL_BYTES_SENT",
1638            1 => "ACK_TOTAL_BYTES_RECEIVED",
1639            2 => "SET_BYTE_ORDER",
1640            3 => "ECHO_REQUEST",
1641            4 => "ECHO_RESPONSE",
1642            _ => "CONTROL",
1643        };
1644        write!(f, "{}(data={})", name, self.data)
1645    }
1646}
1647
1648impl fmt::Display for PvaSearchResponsePayload {
1649    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1650        let found_text = if self.found { "true" } else { "false" };
1651        if self.cids.is_empty() {
1652            write!(
1653                f,
1654                "SearchResponse(found={}, proto={})",
1655                found_text, self.protocol
1656            )
1657        } else {
1658            write!(
1659                f,
1660                "SearchResponse(found={}, proto={}, cids=[{}])",
1661                found_text,
1662                self.protocol,
1663                self.cids
1664                    .iter()
1665                    .map(|c| c.to_string())
1666                    .collect::<Vec<String>>()
1667                    .join(",")
1668            )
1669        }
1670    }
1671}
1672
1673impl fmt::Display for PvaConnectionValidationPayload {
1674    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1675        let dir = if self.is_server { "server" } else { "client" };
1676        let authz = self.authz.as_deref().unwrap_or("");
1677        if authz.is_empty() {
1678            write!(
1679                f,
1680                "ConnectionValidation(dir={}, qsize={}, isize={}, qos=0x{:04x})",
1681                dir, self.buffer_size, self.introspection_registry_size, self.qos
1682            )
1683        } else {
1684            write!(
1685                f,
1686                "ConnectionValidation(dir={}, qsize={}, isize={}, qos=0x{:04x}, authz={})",
1687                dir, self.buffer_size, self.introspection_registry_size, self.qos, authz
1688            )
1689        }
1690    }
1691}
1692
1693impl fmt::Display for PvaStatus {
1694    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1695        write!(
1696            f,
1697            "code={} message={} stack={}",
1698            self.code,
1699            self.message.as_deref().unwrap_or(""),
1700            self.stack.as_deref().unwrap_or("")
1701        )
1702    }
1703}
1704
1705impl fmt::Display for PvaConnectionValidatedPayload {
1706    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1707        match &self.status {
1708            Some(s) => write!(f, "ConnectionValidated(status={})", s.code),
1709            None => write!(f, "ConnectionValidated(status=OK)"),
1710        }
1711    }
1712}
1713
1714impl fmt::Display for PvaAuthNzPayload {
1715    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1716        if !self.strings.is_empty() {
1717            write!(f, "AuthNZ(strings=[{}])", self.strings.join(","))
1718        } else {
1719            write!(f, "AuthNZ(raw_len={})", self.raw.len())
1720        }
1721    }
1722}
1723
1724impl fmt::Display for PvaAclChangePayload {
1725    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1726        match &self.status {
1727            Some(s) => write!(f, "ACL_CHANGE(status={})", s.code),
1728            None => write!(f, "ACL_CHANGE(status=OK)"),
1729        }
1730    }
1731}
1732
1733impl fmt::Display for PvaGetFieldPayload {
1734    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1735        if self.is_server {
1736            let status = self.status.as_ref().map(|s| s.code).unwrap_or(0xff);
1737            write!(f, "GET_FIELD(status={})", status)
1738        } else {
1739            let field = self.field_name.as_deref().unwrap_or("");
1740            if field.is_empty() {
1741                write!(f, "GET_FIELD(cid={})", self.cid)
1742            } else {
1743                write!(f, "GET_FIELD(cid={}, field={})", self.cid, field)
1744            }
1745        }
1746    }
1747}
1748
1749impl fmt::Display for PvaMessagePayload {
1750    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1751        match &self.status {
1752            Some(s) => {
1753                if let Some(msg) = &s.message {
1754                    write!(f, "MESSAGE(status={}, msg='{}')", s.code, msg)
1755                } else {
1756                    write!(f, "MESSAGE(status={})", s.code)
1757                }
1758            }
1759            None => write!(f, "MESSAGE(status=OK)"),
1760        }
1761    }
1762}
1763
1764impl fmt::Display for PvaMultipleDataPayload {
1765    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1766        if self.entries.is_empty() {
1767            write!(f, "MULTIPLE_DATA(raw_len={})", self.raw.len())
1768        } else {
1769            write!(f, "MULTIPLE_DATA(entries={})", self.entries.len())
1770        }
1771    }
1772}
1773
1774impl fmt::Display for PvaCancelRequestPayload {
1775    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1776        let status = self.status.as_ref().map(|s| s.code);
1777        match status {
1778            Some(code) => write!(f, "CANCEL_REQUEST(id={}, status={})", self.request_id, code),
1779            None => write!(f, "CANCEL_REQUEST(id={})", self.request_id),
1780        }
1781    }
1782}
1783
1784impl fmt::Display for PvaDestroyRequestPayload {
1785    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1786        write!(
1787            f,
1788            "DESTROY_REQUEST(sid={}, id={})",
1789            self.sid, self.request_id
1790        )
1791    }
1792}
1793
1794impl fmt::Display for PvaOriginTagPayload {
1795    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1796        write!(f, "ORIGIN_TAG(addr={})", format_pva_address(&self.address))
1797    }
1798}
1799
1800impl fmt::Display for PvaUnknownPayload {
1801    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1802        let kind = if self.is_control {
1803            "CONTROL"
1804        } else {
1805            "APPLICATION"
1806        };
1807        write!(
1808            f,
1809            "UNKNOWN(cmd={}, type={}, raw_len={})",
1810            self.command, kind, self.raw_len
1811        )
1812    }
1813}
1814
1815// generic display for all payloads
1816impl fmt::Display for PvaPacketCommand {
1817    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1818        match self {
1819            PvaPacketCommand::Control(payload) => write!(f, "{}", payload),
1820            PvaPacketCommand::Search(payload) => write!(f, "{}", payload),
1821            PvaPacketCommand::SearchResponse(payload) => write!(f, "{}", payload),
1822            PvaPacketCommand::Beacon(payload) => write!(f, "{}", payload),
1823            PvaPacketCommand::ConnectionValidation(payload) => write!(f, "{}", payload),
1824            PvaPacketCommand::ConnectionValidated(payload) => write!(f, "{}", payload),
1825            PvaPacketCommand::AuthNZ(payload) => write!(f, "{}", payload),
1826            PvaPacketCommand::AclChange(payload) => write!(f, "{}", payload),
1827            PvaPacketCommand::Op(payload) => write!(f, "{}", payload),
1828            PvaPacketCommand::CreateChannel(payload) => write!(f, "{}", payload),
1829            PvaPacketCommand::DestroyChannel(payload) => write!(f, "{}", payload),
1830            PvaPacketCommand::GetField(payload) => write!(f, "{}", payload),
1831            PvaPacketCommand::Message(payload) => write!(f, "{}", payload),
1832            PvaPacketCommand::MultipleData(payload) => write!(f, "{}", payload),
1833            PvaPacketCommand::CancelRequest(payload) => write!(f, "{}", payload),
1834            PvaPacketCommand::DestroyRequest(payload) => write!(f, "{}", payload),
1835            PvaPacketCommand::OriginTag(payload) => write!(f, "{}", payload),
1836            PvaPacketCommand::Echo(bytes) => write!(f, "ECHO ({} bytes)", bytes.len()),
1837            PvaPacketCommand::Unknown(payload) => write!(f, "{}", payload),
1838        }
1839    }
1840}
1841
1842impl fmt::Display for PvaOpPayload {
1843    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1844        let cmd_name = match self.command {
1845            10 => "GET",
1846            11 => "PUT",
1847            12 => "PUT_GET",
1848            13 => "MONITOR",
1849            14 => "ARRAY",
1850            16 => "PROCESS",
1851            20 => "RPC",
1852            _ => "OP",
1853        };
1854
1855        let status_text = if let Some(s) = &self.status {
1856            match &s.message {
1857                Some(m) if !m.is_empty() => format!(" status={} msg='{}'", s.code, m),
1858                _ => format!(" status={}", s.code),
1859            }
1860        } else {
1861            String::new()
1862        };
1863
1864        // Show decoded value if available, otherwise fall back to heuristic strings
1865        let value_text = if let Some(ref decoded) = self.decoded_value {
1866            let formatted = format_compact_value(decoded);
1867            if formatted.is_empty() || formatted == "{}" {
1868                String::new()
1869            } else {
1870                format!(" [{}]", formatted)
1871            }
1872        } else if !self.pv_names.is_empty() {
1873            format!(" data=[{}]", self.pv_names.join(","))
1874        } else {
1875            String::new()
1876        };
1877
1878        if self.is_server {
1879            write!(
1880                f,
1881                "{}(ioid={}, sub=0x{:02x}{}{})",
1882                cmd_name, self.ioid, self.subcmd, status_text, value_text
1883            )
1884        } else {
1885            write!(
1886                f,
1887                "{}(sid={}, ioid={}, sub=0x{:02x}{}{})",
1888                cmd_name, self.sid_or_cid, self.ioid, self.subcmd, status_text, value_text
1889            )
1890        }
1891    }
1892}
1893
1894#[cfg(test)]
1895mod tests {
1896    use super::*;
1897    use crate::spvd_decode::extract_nt_scalar_value;
1898
1899    #[test]
1900    fn destroy_request_decodes_spec_and_legacy_forms() {
1901        // Spec form (pvxs/pvAccessCPP): serverChannelID then requestID.
1902        let mut spec = Vec::new();
1903        spec.extend_from_slice(&7u32.to_le_bytes());
1904        spec.extend_from_slice(&42u32.to_le_bytes());
1905        let p = PvaDestroyRequestPayload::new(&spec, false).unwrap();
1906        assert_eq!((p.sid, p.request_id), (7, 42));
1907
1908        // Legacy 4-byte spvirit form: requestID only.
1909        let legacy = 42u32.to_le_bytes();
1910        let p = PvaDestroyRequestPayload::new(&legacy, false).unwrap();
1911        assert_eq!((p.sid, p.request_id), (0, 42));
1912
1913        assert!(PvaDestroyRequestPayload::new(&[0u8; 3], false).is_none());
1914    }
1915    use crate::spvd_encode::{
1916        encode_nt_payload_bitset_parts, encode_nt_scalar_bitset_parts, encode_size_pvd,
1917        nt_payload_desc, nt_scalar_desc,
1918    };
1919    use crate::spvirit_encode::encode_header;
1920    use spvirit_types::{NtPayload, NtScalar, NtScalarArray, ScalarArrayValue, ScalarValue};
1921
1922    #[test]
1923    fn test_decode_status_ok() {
1924        let raw = [0xff];
1925        let (status, consumed) = decode_status(&raw, false);
1926        assert!(status.is_none());
1927        assert_eq!(consumed, 1);
1928    }
1929
1930    #[test]
1931    fn test_decode_status_message() {
1932        let raw = [1u8, 2, b'h', b'i', 2, b's', b't'];
1933        let (status, consumed) = decode_status(&raw, false);
1934        assert_eq!(consumed, 7);
1935        let status = status.unwrap();
1936        assert_eq!(status.code, 1);
1937        assert_eq!(status.message.as_deref(), Some("hi"));
1938        assert_eq!(status.stack.as_deref(), Some("st"));
1939    }
1940
1941    #[test]
1942    fn test_search_response_decode() {
1943        let mut raw: Vec<u8> = vec![];
1944        raw.extend_from_slice(&[0u8; 12]); // guid
1945        raw.extend_from_slice(&1u32.to_le_bytes()); // seq
1946        raw.extend_from_slice(&[0u8; 16]); // addr
1947        raw.extend_from_slice(&5076u16.to_le_bytes()); // port
1948        raw.push(3); // protocol size
1949        raw.extend_from_slice(b"tcp");
1950        raw.push(1); // found
1951        raw.extend_from_slice(&1u16.to_le_bytes()); // count
1952        raw.extend_from_slice(&42u32.to_le_bytes()); // cid
1953
1954        let decoded = PvaSearchResponsePayload::new(&raw, false).unwrap();
1955        assert!(decoded.found);
1956        assert_eq!(decoded.protocol, "tcp");
1957        assert_eq!(decoded.cids, vec![42u32]);
1958    }
1959
1960    fn build_monitor_packet(ioid: u32, subcmd: u8, body: &[u8]) -> Vec<u8> {
1961        let mut payload = Vec::new();
1962        payload.extend_from_slice(&ioid.to_le_bytes());
1963        payload.push(subcmd);
1964        payload.extend_from_slice(body);
1965        let mut out = encode_header(true, false, false, 2, 13, payload.len() as u32);
1966        out.extend_from_slice(&payload);
1967        out
1968    }
1969
1970    #[test]
1971    fn test_monitor_decode_overrun_and_legacy() {
1972        let nt = NtScalar::from_value(ScalarValue::F64(3.5));
1973        let desc = nt_scalar_desc(&nt.value);
1974        let (changed_bitset, values) = encode_nt_scalar_bitset_parts(&nt, false);
1975
1976        let mut body_overrun = Vec::new();
1977        body_overrun.extend_from_slice(&changed_bitset);
1978        body_overrun.extend_from_slice(&encode_size_pvd(0, false));
1979        body_overrun.extend_from_slice(&values);
1980
1981        let pkt = build_monitor_packet(1, 0x00, &body_overrun);
1982        let mut pva = PvaPacket::new(&pkt);
1983        let mut cmd = pva.decode_payload().expect("decoded");
1984        if let PvaPacketCommand::Op(ref mut op) = cmd {
1985            op.decode_with_field_desc(&desc, false);
1986            let decoded = op.decoded_value.as_ref().expect("decoded");
1987            let value = extract_nt_scalar_value(decoded).expect("value");
1988            match value {
1989                DecodedValue::Float64(v) => assert!((*v - 3.5).abs() < 1e-6),
1990                other => panic!("unexpected value {:?}", other),
1991            }
1992        } else {
1993            panic!("unexpected cmd");
1994        }
1995
1996        let mut body_legacy = Vec::new();
1997        body_legacy.extend_from_slice(&changed_bitset);
1998        body_legacy.extend_from_slice(&values);
1999
2000        let pkt = build_monitor_packet(1, 0x00, &body_legacy);
2001        let mut pva = PvaPacket::new(&pkt);
2002        let mut cmd = pva.decode_payload().expect("decoded");
2003        if let PvaPacketCommand::Op(ref mut op) = cmd {
2004            op.decode_with_field_desc(&desc, false);
2005            let decoded = op.decoded_value.as_ref().expect("decoded");
2006            let value = extract_nt_scalar_value(decoded).expect("value");
2007            match value {
2008                DecodedValue::Float64(v) => assert!((*v - 3.5).abs() < 1e-6),
2009                other => panic!("unexpected value {:?}", other),
2010            }
2011        } else {
2012            panic!("unexpected cmd");
2013        }
2014
2015        let mut body_spec = Vec::new();
2016        body_spec.extend_from_slice(&changed_bitset);
2017        body_spec.extend_from_slice(&values);
2018        body_spec.extend_from_slice(&encode_size_pvd(0, false));
2019
2020        let pkt = build_monitor_packet(1, 0x00, &body_spec);
2021        let mut pva = PvaPacket::new(&pkt);
2022        let mut cmd = pva.decode_payload().expect("decoded");
2023        if let PvaPacketCommand::Op(ref mut op) = cmd {
2024            op.decode_with_field_desc(&desc, false);
2025            let decoded = op.decoded_value.as_ref().expect("decoded");
2026            let value = extract_nt_scalar_value(decoded).expect("value");
2027            match value {
2028                DecodedValue::Float64(v) => assert!((*v - 3.5).abs() < 1e-6),
2029                other => panic!("unexpected value {:?}", other),
2030            }
2031        } else {
2032            panic!("unexpected cmd");
2033        }
2034    }
2035
2036    #[test]
2037    fn test_monitor_decode_prefers_spec_order_for_array_payload() {
2038        let payload_value =
2039            NtPayload::ScalarArray(NtScalarArray::from_value(ScalarArrayValue::F64(vec![
2040                1.0, 2.0, 3.0, 4.0,
2041            ])));
2042        let desc = nt_payload_desc(&payload_value);
2043        let (changed_bitset, values) = encode_nt_payload_bitset_parts(&payload_value, false);
2044
2045        let mut body_spec = Vec::new();
2046        body_spec.extend_from_slice(&changed_bitset);
2047        body_spec.extend_from_slice(&values);
2048        body_spec.extend_from_slice(&encode_size_pvd(0, false));
2049
2050        let pkt = build_monitor_packet(11, 0x00, &body_spec);
2051        let mut pva = PvaPacket::new(&pkt);
2052        let mut cmd = pva.decode_payload().expect("decoded");
2053        if let PvaPacketCommand::Op(ref mut op) = cmd {
2054            op.decode_with_field_desc(&desc, false);
2055            let decoded = op.decoded_value.as_ref().expect("decoded");
2056            let value = extract_nt_scalar_value(decoded).expect("value");
2057            match value {
2058                DecodedValue::Array(items) => {
2059                    assert_eq!(items.len(), 4);
2060                    assert!(matches!(items[0], DecodedValue::Float64(v) if (v - 1.0).abs() < 1e-6));
2061                    assert!(matches!(items[3], DecodedValue::Float64(v) if (v - 4.0).abs() < 1e-6));
2062                }
2063                other => panic!("unexpected value {:?}", other),
2064            }
2065        } else {
2066            panic!("unexpected cmd");
2067        }
2068    }
2069
2070    #[test]
2071    fn pva_status_reports_error_state() {
2072        let ok = PvaStatus {
2073            code: 0,
2074            message: None,
2075            stack: None,
2076        };
2077        let err = PvaStatus {
2078            code: 2,
2079            message: Some("bad".to_string()),
2080            stack: None,
2081        };
2082        assert!(!ok.is_error());
2083        assert!(err.is_error());
2084    }
2085
2086    #[test]
2087    fn pva_status_display_includes_message_and_stack() {
2088        let status = PvaStatus {
2089            code: 2,
2090            message: Some("bad".to_string()),
2091            stack: Some("trace".to_string()),
2092        };
2093        assert_eq!(status.to_string(), "code=2 message=bad stack=trace");
2094    }
2095
2096    #[test]
2097    fn decode_op_response_status_reads_status_from_packet() {
2098        let raw = vec![
2099            0xCA, 0x02, 0x40, 0x0B, 0x0A, 0x00, 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, 0x00, 0x02,
2100            0x03, b'b', b'a', b'd', 0x00,
2101        ];
2102        let status = decode_op_response_status(&raw, false)
2103            .expect("status parse")
2104            .expect("status");
2105        assert!(status.is_error());
2106        assert_eq!(status.message.as_deref(), Some("bad"));
2107    }
2108}