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