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