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