Skip to main content

spvirit_codec/
epics_decode.rs

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