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