Skip to main content

spvirit_codec/
spvirit_encode.rs

1//! PVA message encoding helpers.
2
3use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
4use spvirit_types::{NtPayload, NtScalar};
5use crate::spvd_decode::StructureDesc;
6use crate::spvd_encode::{
7    encode_nt_payload_bitset, encode_nt_payload_bitset_parts, encode_nt_payload_full,
8    encode_nt_scalar_bitset, encode_nt_scalar_full, encode_structure_desc,
9    nt_payload_desc,
10};
11
12pub fn encode_size_pva(size: usize, is_be: bool) -> Vec<u8> {
13    crate::encode_common::encode_size(size, is_be)
14}
15
16pub fn encode_string_pva(value: &str, is_be: bool) -> Vec<u8> {
17    crate::encode_common::encode_string(value, is_be)
18}
19
20fn encode_status_ok() -> Vec<u8> {
21    vec![0xFF]
22}
23
24fn encode_status_error(message: &str, is_be: bool) -> Vec<u8> {
25    let mut out = Vec::new();
26    out.push(0x02);
27    out.extend_from_slice(&encode_string_pva(message, is_be));
28    out.extend_from_slice(&encode_string_pva("", is_be));
29    out
30}
31
32pub fn encode_message_error(message: &str, version: u8, is_be: bool) -> Vec<u8> {
33    let payload = encode_status_error(message, is_be);
34    let mut out = encode_header(true, is_be, false, version, 18, payload.len() as u32);
35    out.extend_from_slice(&payload);
36    out
37}
38
39pub fn encode_header(
40    is_server: bool,
41    is_be: bool,
42    is_control: bool,
43    version: u8,
44    command: u8,
45    payload_length: u32,
46) -> Vec<u8> {
47    let magic = 0xCA;
48    let mut flags = 0u8;
49    if is_control {
50        flags |= 0x01;
51    }
52    if is_server {
53        flags |= 0x40;
54    }
55    if is_be {
56        flags |= 0x80;
57    }
58    let mut out = vec![magic, version, flags, command];
59    let len_bytes = if is_be {
60        payload_length.to_be_bytes()
61    } else {
62        payload_length.to_le_bytes()
63    };
64    out.extend_from_slice(&len_bytes);
65    out
66}
67
68pub fn encode_search_response(
69    guid: [u8; 12],
70    seq: u32,
71    addr: [u8; 16],
72    port: u16,
73    protocol: &str,
74    found: bool,
75    cids: &[u32],
76    version: u8,
77    is_be: bool,
78) -> Vec<u8> {
79    let mut payload = Vec::new();
80    payload.extend_from_slice(&guid);
81    payload.extend_from_slice(&if is_be {
82        seq.to_be_bytes()
83    } else {
84        seq.to_le_bytes()
85    });
86    payload.extend_from_slice(&addr);
87    payload.extend_from_slice(&if is_be {
88        port.to_be_bytes()
89    } else {
90        port.to_le_bytes()
91    });
92    payload.extend_from_slice(&encode_string_pva(protocol, is_be));
93    payload.push(if found { 1 } else { 0 });
94    let count = cids.len() as u16;
95    payload.extend_from_slice(&if is_be {
96        count.to_be_bytes()
97    } else {
98        count.to_le_bytes()
99    });
100    for cid in cids {
101        payload.extend_from_slice(&if is_be {
102            cid.to_be_bytes()
103        } else {
104            cid.to_le_bytes()
105        });
106    }
107
108    let mut out = encode_header(true, is_be, false, version, 4, payload.len() as u32);
109    out.extend_from_slice(&payload);
110    out
111}
112
113pub fn encode_connection_validated(version: u8, is_be: bool) -> Vec<u8> {
114    let payload = encode_status_ok();
115    let mut out = encode_header(true, is_be, false, version, 9, payload.len() as u32);
116    out.extend_from_slice(&payload);
117    out
118}
119
120pub fn encode_control_message(
121    is_server: bool,
122    is_be: bool,
123    version: u8,
124    command: u8,
125    data: u32,
126) -> Vec<u8> {
127    // Control messages: header only; size field carries data.
128    encode_header(is_server, is_be, true, version, command, data)
129}
130
131pub fn encode_connection_validation(
132    buffer_size: u32,
133    introspection_registry_size: u16,
134    qos: u16,
135    authz_name: &str,
136    version: u8,
137    is_be: bool,
138) -> Vec<u8> {
139    let mut payload = Vec::new();
140    payload.extend_from_slice(&if is_be {
141        buffer_size.to_be_bytes()
142    } else {
143        buffer_size.to_le_bytes()
144    });
145    payload.extend_from_slice(&if is_be {
146        introspection_registry_size.to_be_bytes()
147    } else {
148        introspection_registry_size.to_le_bytes()
149    });
150    payload.extend_from_slice(&if is_be {
151        qos.to_be_bytes()
152    } else {
153        qos.to_le_bytes()
154    });
155    payload.extend_from_slice(&encode_string_pva(authz_name, is_be));
156    let mut out = encode_header(true, is_be, false, version, 1, payload.len() as u32);
157    out.extend_from_slice(&payload);
158    out
159}
160
161pub fn encode_create_channel_response(cid: u32, sid: u32, version: u8, is_be: bool) -> Vec<u8> {
162    let mut payload = Vec::new();
163    payload.extend_from_slice(&if is_be {
164        cid.to_be_bytes()
165    } else {
166        cid.to_le_bytes()
167    });
168    payload.extend_from_slice(&if is_be {
169        sid.to_be_bytes()
170    } else {
171        sid.to_le_bytes()
172    });
173    payload.extend_from_slice(&encode_status_ok());
174    let mut out = encode_header(true, is_be, false, version, 7, payload.len() as u32);
175    out.extend_from_slice(&payload);
176    out
177}
178
179pub fn encode_create_channel_error(cid: u32, message: &str, version: u8, is_be: bool) -> Vec<u8> {
180    let mut payload = Vec::new();
181    payload.extend_from_slice(&if is_be {
182        cid.to_be_bytes()
183    } else {
184        cid.to_le_bytes()
185    });
186    payload.extend_from_slice(&if is_be {
187        0u32.to_be_bytes()
188    } else {
189        0u32.to_le_bytes()
190    });
191    payload.push(0x01);
192    payload.extend_from_slice(&encode_string_pva(message, is_be));
193    payload.extend_from_slice(&encode_string_pva("", is_be));
194    let mut out = encode_header(true, is_be, false, version, 7, payload.len() as u32);
195    out.extend_from_slice(&payload);
196    out
197}
198
199pub fn encode_get_field_response(
200    request_id: u32,
201    desc: &StructureDesc,
202    version: u8,
203    is_be: bool,
204) -> Vec<u8> {
205    let mut payload = Vec::new();
206    payload.extend_from_slice(&if is_be {
207        request_id.to_be_bytes()
208    } else {
209        request_id.to_le_bytes()
210    });
211    payload.extend_from_slice(&encode_status_ok());
212    payload.push(0x80);
213    payload.extend_from_slice(&encode_structure_desc(desc, is_be));
214    let mut out = encode_header(true, is_be, false, version, 17, payload.len() as u32);
215    out.extend_from_slice(&payload);
216    out
217}
218
219pub fn encode_get_field_error(request_id: u32, message: &str, version: u8, is_be: bool) -> Vec<u8> {
220    let mut payload = Vec::new();
221    payload.extend_from_slice(&if is_be {
222        request_id.to_be_bytes()
223    } else {
224        request_id.to_le_bytes()
225    });
226    payload.extend_from_slice(&encode_status_error(message, is_be));
227    let mut out = encode_header(true, is_be, false, version, 17, payload.len() as u32);
228    out.extend_from_slice(&payload);
229    out
230}
231
232pub fn encode_op_init_response(
233    command: u8,
234    ioid: u32,
235    subcmd: u8,
236    desc: &StructureDesc,
237    nt: &NtScalar,
238    version: u8,
239    is_be: bool,
240) -> Vec<u8> {
241    let mut payload = Vec::new();
242    payload.extend_from_slice(&if is_be {
243        ioid.to_be_bytes()
244    } else {
245        ioid.to_le_bytes()
246    });
247    payload.push(subcmd);
248    payload.extend_from_slice(&encode_status_ok());
249    payload.push(0x80); // structure type for introspection
250    payload.extend_from_slice(&encode_structure_desc(desc, is_be));
251    payload.extend_from_slice(&encode_nt_scalar_full(nt, is_be));
252
253    let mut out = encode_header(true, is_be, false, version, command, payload.len() as u32);
254    out.extend_from_slice(&payload);
255    out
256}
257
258pub fn encode_op_init_response_desc(
259    command: u8,
260    ioid: u32,
261    subcmd: u8,
262    desc: &StructureDesc,
263    version: u8,
264    is_be: bool,
265) -> Vec<u8> {
266    let mut payload = Vec::new();
267    payload.extend_from_slice(&if is_be {
268        ioid.to_be_bytes()
269    } else {
270        ioid.to_le_bytes()
271    });
272    payload.push(subcmd);
273    payload.extend_from_slice(&encode_status_ok());
274    payload.push(0x80); // structure type for introspection
275    payload.extend_from_slice(&encode_structure_desc(desc, is_be));
276
277    let mut out = encode_header(true, is_be, false, version, command, payload.len() as u32);
278    out.extend_from_slice(&payload);
279    out
280}
281
282pub fn encode_op_data_response(
283    command: u8,
284    ioid: u32,
285    nt: &NtScalar,
286    version: u8,
287    is_be: bool,
288) -> Vec<u8> {
289    let mut payload = Vec::new();
290    payload.extend_from_slice(&if is_be {
291        ioid.to_be_bytes()
292    } else {
293        ioid.to_le_bytes()
294    });
295    payload.push(0x00);
296    payload.extend_from_slice(&encode_nt_scalar_bitset(nt, is_be));
297    let mut out = encode_header(true, is_be, false, version, command, payload.len() as u32);
298    out.extend_from_slice(&payload);
299    out
300}
301
302pub fn encode_op_get_data_response_payload(
303    ioid: u32,
304    payload_value: &NtPayload,
305    version: u8,
306    is_be: bool,
307) -> Vec<u8> {
308    encode_op_data_response_payload(10, ioid, payload_value, version, is_be)
309}
310
311pub fn encode_op_data_response_payload(
312    command: u8,
313    ioid: u32,
314    payload_value: &NtPayload,
315    version: u8,
316    is_be: bool,
317) -> Vec<u8> {
318    let mut payload = Vec::new();
319    payload.extend_from_slice(&if is_be {
320        ioid.to_be_bytes()
321    } else {
322        ioid.to_le_bytes()
323    });
324    payload.push(0x00);
325    payload.extend_from_slice(&encode_status_ok());
326    payload.extend_from_slice(&encode_nt_payload_bitset(payload_value, is_be));
327    let mut out = encode_header(true, is_be, false, version, command, payload.len() as u32);
328    out.extend_from_slice(&payload);
329    out
330}
331
332pub fn encode_op_status_response(
333    command: u8,
334    ioid: u32,
335    subcmd: u8,
336    version: u8,
337    is_be: bool,
338) -> Vec<u8> {
339    let mut payload = Vec::new();
340    payload.extend_from_slice(&if is_be {
341        ioid.to_be_bytes()
342    } else {
343        ioid.to_le_bytes()
344    });
345    payload.push(subcmd);
346    payload.extend_from_slice(&encode_status_ok());
347    let mut out = encode_header(true, is_be, false, version, command, payload.len() as u32);
348    out.extend_from_slice(&payload);
349    out
350}
351
352pub fn encode_op_status_error_response(
353    command: u8,
354    ioid: u32,
355    subcmd: u8,
356    message: &str,
357    version: u8,
358    is_be: bool,
359) -> Vec<u8> {
360    let mut payload = Vec::new();
361    payload.extend_from_slice(&if is_be {
362        ioid.to_be_bytes()
363    } else {
364        ioid.to_le_bytes()
365    });
366    payload.push(subcmd);
367    payload.extend_from_slice(&encode_status_error(message, is_be));
368    let mut out = encode_header(true, is_be, false, version, command, payload.len() as u32);
369    out.extend_from_slice(&payload);
370    out
371}
372
373pub fn encode_op_rpc_data_response_payload(
374    ioid: u32,
375    subcmd: u8,
376    payload_value: &NtPayload,
377    version: u8,
378    is_be: bool,
379) -> Vec<u8> {
380    let desc = nt_payload_desc(payload_value);
381    let mut payload = Vec::new();
382    payload.extend_from_slice(&if is_be {
383        ioid.to_be_bytes()
384    } else {
385        ioid.to_le_bytes()
386    });
387    payload.push(subcmd);
388    payload.extend_from_slice(&encode_status_ok());
389    payload.push(0x80);
390    payload.extend_from_slice(&encode_structure_desc(&desc, is_be));
391    payload.extend_from_slice(&encode_nt_payload_full(payload_value, is_be));
392    let mut out = encode_header(true, is_be, false, version, 20, payload.len() as u32);
393    out.extend_from_slice(&payload);
394    out
395}
396
397pub fn encode_op_put_get_init_response(
398    ioid: u32,
399    put_desc: &StructureDesc,
400    get_desc: &StructureDesc,
401    version: u8,
402    is_be: bool,
403) -> Vec<u8> {
404    let mut payload = Vec::new();
405    payload.extend_from_slice(&if is_be {
406        ioid.to_be_bytes()
407    } else {
408        ioid.to_le_bytes()
409    });
410    payload.push(0x08);
411    payload.extend_from_slice(&encode_status_ok());
412    payload.push(0x80);
413    payload.extend_from_slice(&encode_structure_desc(put_desc, is_be));
414    payload.push(0x80);
415    payload.extend_from_slice(&encode_structure_desc(get_desc, is_be));
416    let mut out = encode_header(true, is_be, false, version, 12, payload.len() as u32);
417    out.extend_from_slice(&payload);
418    out
419}
420
421pub fn encode_op_put_get_data_response(
422    ioid: u32,
423    nt: &NtScalar,
424    version: u8,
425    is_be: bool,
426) -> Vec<u8> {
427    encode_op_put_get_data_response_payload(ioid, &NtPayload::Scalar(nt.clone()), version, is_be)
428}
429
430pub fn encode_op_put_get_data_response_payload(
431    ioid: u32,
432    payload_value: &NtPayload,
433    version: u8,
434    is_be: bool,
435) -> Vec<u8> {
436    let mut payload = Vec::new();
437    payload.extend_from_slice(&if is_be {
438        ioid.to_be_bytes()
439    } else {
440        ioid.to_le_bytes()
441    });
442    payload.push(0x00);
443    payload.extend_from_slice(&encode_status_ok());
444    payload.extend_from_slice(&encode_nt_payload_bitset(payload_value, is_be));
445    let mut out = encode_header(true, is_be, false, version, 12, payload.len() as u32);
446    out.extend_from_slice(&payload);
447    out
448}
449
450pub fn encode_op_put_response(ioid: u32, version: u8, is_be: bool) -> Vec<u8> {
451    let mut payload = Vec::new();
452    payload.extend_from_slice(&if is_be {
453        ioid.to_be_bytes()
454    } else {
455        ioid.to_le_bytes()
456    });
457    payload.push(0x00);
458    payload.extend_from_slice(&encode_status_ok());
459    let mut out = encode_header(true, is_be, false, version, 11, payload.len() as u32);
460    out.extend_from_slice(&payload);
461    out
462}
463
464pub fn encode_op_put_status_response(
465    ioid: u32,
466    subcmd: u8,
467    message: &str,
468    version: u8,
469    is_be: bool,
470) -> Vec<u8> {
471    let mut payload = Vec::new();
472    payload.extend_from_slice(&if is_be {
473        ioid.to_be_bytes()
474    } else {
475        ioid.to_le_bytes()
476    });
477    payload.push(subcmd);
478    payload.extend_from_slice(&encode_status_error(message, is_be));
479    let mut out = encode_header(true, is_be, false, version, 11, payload.len() as u32);
480    out.extend_from_slice(&payload);
481    out
482}
483
484pub fn encode_op_put_getput_response(
485    ioid: u32,
486    nt: &NtScalar,
487    version: u8,
488    is_be: bool,
489) -> Vec<u8> {
490    encode_op_put_getput_response_payload(ioid, &NtPayload::Scalar(nt.clone()), version, is_be)
491}
492
493pub fn encode_op_put_getput_response_payload(
494    ioid: u32,
495    payload_value: &NtPayload,
496    version: u8,
497    is_be: bool,
498) -> Vec<u8> {
499    let mut payload = Vec::new();
500    payload.extend_from_slice(&if is_be {
501        ioid.to_be_bytes()
502    } else {
503        ioid.to_le_bytes()
504    });
505    payload.push(0x40);
506    payload.extend_from_slice(&encode_status_ok());
507    payload.extend_from_slice(&encode_nt_payload_bitset(payload_value, is_be));
508    let mut out = encode_header(true, is_be, false, version, 11, payload.len() as u32);
509    out.extend_from_slice(&payload);
510    out
511}
512
513pub fn encode_op_put_get_init_error_response(
514    ioid: u32,
515    message: &str,
516    version: u8,
517    is_be: bool,
518) -> Vec<u8> {
519    let mut payload = Vec::new();
520    payload.extend_from_slice(&if is_be {
521        ioid.to_be_bytes()
522    } else {
523        ioid.to_le_bytes()
524    });
525    payload.push(0x08);
526    payload.extend_from_slice(&encode_status_error(message, is_be));
527    let mut out = encode_header(true, is_be, false, version, 12, payload.len() as u32);
528    out.extend_from_slice(&payload);
529    out
530}
531
532pub fn encode_op_put_get_data_error_response(
533    ioid: u32,
534    message: &str,
535    version: u8,
536    is_be: bool,
537) -> Vec<u8> {
538    let mut payload = Vec::new();
539    payload.extend_from_slice(&if is_be {
540        ioid.to_be_bytes()
541    } else {
542        ioid.to_le_bytes()
543    });
544    payload.push(0x00);
545    payload.extend_from_slice(&encode_status_error(message, is_be));
546    let mut out = encode_header(true, is_be, false, version, 12, payload.len() as u32);
547    out.extend_from_slice(&payload);
548    out
549}
550
551pub fn encode_monitor_data_response(
552    ioid: u32,
553    subcmd: u8,
554    nt: &NtScalar,
555    version: u8,
556    is_be: bool,
557) -> Vec<u8> {
558    encode_monitor_data_response_payload(
559        ioid,
560        subcmd,
561        &NtPayload::Scalar(nt.clone()),
562        version,
563        is_be,
564    )
565}
566
567pub fn encode_monitor_data_response_payload(
568    ioid: u32,
569    subcmd: u8,
570    payload_value: &NtPayload,
571    version: u8,
572    is_be: bool,
573) -> Vec<u8> {
574    let (changed_bitset, values) = encode_nt_payload_bitset_parts(payload_value, is_be);
575    let mut payload = Vec::new();
576    payload.extend_from_slice(&if is_be {
577        ioid.to_be_bytes()
578    } else {
579        ioid.to_le_bytes()
580    });
581    payload.push(subcmd);
582    if (subcmd & 0x10) != 0 {
583        payload.extend_from_slice(&encode_status_ok());
584    }
585    payload.extend_from_slice(&changed_bitset);
586    payload.extend_from_slice(&values);
587    // overrun bitset: empty (after data per spec)
588    payload.extend_from_slice(&encode_size_pva(0, is_be));
589    let mut out = encode_header(true, is_be, false, version, 13, payload.len() as u32);
590    out.extend_from_slice(&payload);
591    out
592}
593
594pub fn encode_destroy_channel_response(sid: u32, cid: u32, version: u8, is_be: bool) -> Vec<u8> {
595    let mut payload = Vec::new();
596    payload.extend_from_slice(&if is_be {
597        sid.to_be_bytes()
598    } else {
599        sid.to_le_bytes()
600    });
601    payload.extend_from_slice(&if is_be {
602        cid.to_be_bytes()
603    } else {
604        cid.to_le_bytes()
605    });
606    let mut out = encode_header(true, is_be, false, version, 8, payload.len() as u32);
607    out.extend_from_slice(&payload);
608    out
609}
610
611pub fn encode_op_error(command: u8, ioid: u32, message: &str, version: u8, is_be: bool) -> Vec<u8> {
612    let mut payload = Vec::new();
613    payload.extend_from_slice(&if is_be {
614        ioid.to_be_bytes()
615    } else {
616        ioid.to_le_bytes()
617    });
618    payload.push(0x08);
619    payload.push(0x01); // error
620    payload.extend_from_slice(&encode_string_pva(message, is_be));
621    payload.extend_from_slice(&encode_string_pva("", is_be));
622    let mut out = encode_header(true, is_be, false, version, command, payload.len() as u32);
623    out.extend_from_slice(&payload);
624    out
625}
626
627pub fn encode_beacon(
628    guid: [u8; 12],
629    seq: u8,
630    change_count: u16,
631    addr: [u8; 16],
632    port: u16,
633    protocol: &str,
634    version: u8,
635    is_be: bool,
636) -> Vec<u8> {
637    let mut payload = Vec::new();
638    payload.extend_from_slice(&guid);
639    payload.push(0x00); // flags
640    payload.push(seq);
641    payload.extend_from_slice(&if is_be {
642        change_count.to_be_bytes()
643    } else {
644        change_count.to_le_bytes()
645    });
646    payload.extend_from_slice(&addr);
647    payload.extend_from_slice(&if is_be {
648        port.to_be_bytes()
649    } else {
650        port.to_le_bytes()
651    });
652    payload.extend_from_slice(&encode_string_pva(protocol, is_be));
653    // serverStatus: NULL FieldDesc (0xFF) means "no server status".
654    // Writing a PVA string here instead would be misinterpreted as a TypeCode
655    // by compliant clients (e.g. Phoebus), causing a BufferUnderflowException.
656    payload.push(0xFF);
657    let mut out = encode_header(true, is_be, false, version, 0, payload.len() as u32);
658    out.extend_from_slice(&payload);
659    out
660}
661
662// ---------------------------------------------------------------------------
663// IP address ↔ 16-byte PVA wire-format conversion helpers
664// ---------------------------------------------------------------------------
665
666/// Convert an [`IpAddr`] to the 16-byte PVA wire representation.
667///
668/// IPv4 addresses are stored as IPv4-mapped IPv6 (`::ffff:a.b.c.d`).
669/// Native IPv6 addresses are stored as-is.
670pub fn ip_to_bytes(ip: IpAddr) -> [u8; 16] {
671    match ip {
672        IpAddr::V4(v4) => {
673            let mut out = [0u8; 16];
674            out[10] = 0xFF;
675            out[11] = 0xFF;
676            out[12..16].copy_from_slice(&v4.octets());
677            out
678        }
679        IpAddr::V6(v6) => v6.octets(),
680    }
681}
682
683/// Decode a 16-byte PVA address field to an [`IpAddr`].
684///
685/// Returns `None` for all-zeros (unspecified).
686/// IPv4-mapped addresses (`::ffff:a.b.c.d`) are returned as [`IpAddr::V4`].
687pub fn ip_from_bytes(addr: &[u8; 16]) -> Option<IpAddr> {
688    if addr.iter().all(|&b| b == 0) {
689        return None;
690    }
691    // IPv4-mapped IPv6 address ::ffff:a.b.c.d
692    if addr[0..10].iter().all(|&b| b == 0) && addr[10] == 0xFF && addr[11] == 0xFF {
693        return Some(IpAddr::V4(Ipv4Addr::new(
694            addr[12], addr[13], addr[14], addr[15],
695        )));
696    }
697    Some(IpAddr::V6(Ipv6Addr::from(*addr)))
698}
699
700/// Format a 16-byte PVA address field as a human-readable IP string.
701///
702/// All-zeros → `"0.0.0.0"`, IPv4-mapped → dotted-quad, otherwise IPv6 notation.
703pub fn format_pva_address(addr: &[u8; 16]) -> String {
704    match ip_from_bytes(addr) {
705        Some(ip) => ip.to_string(),
706        None => "0.0.0.0".to_string(),
707    }
708}
709
710#[cfg(test)]
711mod tests {
712    use super::*;
713    use crate::epics_decode::{PvaPacket, PvaPacketCommand};
714
715    #[test]
716    fn encode_decode_connection_validation_roundtrip() {
717        let msg = encode_connection_validation(4096, 2, 0x10, "test", 2, true);
718        let mut pkt = PvaPacket::new(&msg);
719        let cmd = pkt.decode_payload().expect("decoded");
720        match cmd {
721            PvaPacketCommand::ConnectionValidation(payload) => {
722                assert_eq!(payload.buffer_size, 4096);
723                assert_eq!(payload.introspection_registry_size, 2);
724                assert_eq!(payload.qos, 0x10);
725                assert_eq!(payload.authz.as_deref(), Some("test"));
726            }
727            other => panic!("unexpected decode: {:?}", other),
728        }
729    }
730
731    #[test]
732    fn encode_decode_search_response_roundtrip() {
733        let guid = [1u8; 12];
734        let seq = 42;
735        let addr = [0u8; 16];
736        let port = 5075;
737        let cids = vec![100u32, 101u32];
738        let msg = encode_search_response(guid, seq, addr, port, "tcp", true, &cids, 2, false);
739        let mut pkt = PvaPacket::new(&msg);
740        let cmd = pkt.decode_payload().expect("decoded");
741        match cmd {
742            PvaPacketCommand::SearchResponse(payload) => {
743                assert_eq!(payload.guid, guid);
744                assert_eq!(payload.seq, seq);
745                assert_eq!(payload.port, port);
746                assert!(payload.found);
747                assert_eq!(payload.cids, cids);
748            }
749            other => panic!("unexpected decode: {:?}", other),
750        }
751    }
752
753    #[test]
754    fn encode_decode_connection_validated_roundtrip() {
755        let msg = encode_connection_validated(2, false);
756        let mut pkt = PvaPacket::new(&msg);
757        let cmd = pkt.decode_payload().expect("decoded");
758        match cmd {
759            PvaPacketCommand::ConnectionValidated(payload) => {
760                // 0xFF means "OK" which decodes to None in our decoder.
761                assert!(payload.status.is_none());
762            }
763            other => panic!("unexpected decode: {:?}", other),
764        }
765    }
766
767    #[test]
768    fn get_data_response_includes_status() {
769        let nt = NtScalar::from_value(spvirit_types::ScalarValue::F64(1.0));
770        let msg = encode_op_get_data_response_payload(
771            0x11223344,
772            &NtPayload::Scalar(nt),
773            2,
774            false,
775        );
776        assert!(msg.len() > 13);
777        let status_offset = 8 + 4 + 1;
778        assert_eq!(msg[status_offset], 0xFF);
779
780        let mut pkt = PvaPacket::new(&msg);
781        let cmd = pkt.decode_payload().expect("decoded");
782        match cmd {
783            PvaPacketCommand::Op(op) => {
784                assert_eq!(op.command, 10);
785                assert_eq!(op.subcmd, 0x00);
786                assert!(!op.body.is_empty());
787            }
788            other => panic!("unexpected decode: {:?}", other),
789        }
790    }
791
792    #[test]
793    fn put_get_init_includes_two_descriptors() {
794        let nt = NtScalar::from_value(spvirit_types::ScalarValue::F64(1.0));
795        let desc = crate::spvd_encode::nt_scalar_desc(&nt.value);
796        let msg = encode_op_put_get_init_response(0x01020304, &desc, &desc, 2, false);
797
798        let payload = &msg[8..];
799        assert!(payload.len() > 6);
800        // ioid(4) + subcmd(1) + status(1)
801        assert_eq!(payload[5], 0xFF);
802        let rest = &payload[6..];
803        let first = rest.first().copied().unwrap_or(0);
804        assert_eq!(first, 0x80);
805        let second_pos = rest.iter().skip(1).position(|b| *b == 0x80);
806        assert!(second_pos.is_some(), "expected second descriptor marker");
807    }
808
809    #[test]
810    fn put_get_data_includes_status() {
811        let nt = NtScalar::from_value(spvirit_types::ScalarValue::F64(2.0));
812        let msg = encode_op_put_get_data_response(0x55667788, &nt, 2, false);
813        assert!(msg.len() > 13);
814        let status_offset = 8 + 4 + 1;
815        assert_eq!(msg[status_offset], 0xFF);
816    }
817
818    #[test]
819    fn put_getput_response_encodes_subcmd_0x40() {
820        let nt = NtScalar::from_value(spvirit_types::ScalarValue::F64(2.0));
821        let msg = encode_op_put_getput_response(0x01020304, &nt, 2, false);
822        assert!(msg.len() > 13);
823        let status_offset = 8 + 4 + 1;
824        assert_eq!(msg[status_offset], 0xFF);
825        let mut pkt = PvaPacket::new(&msg);
826        let cmd = pkt.decode_payload().expect("decoded");
827        match cmd {
828            PvaPacketCommand::Op(op) => {
829                assert_eq!(op.command, 11);
830                assert_eq!(op.subcmd, 0x40);
831            }
832            other => panic!("unexpected decode: {:?}", other),
833        }
834    }
835
836    #[test]
837    fn encode_get_field_response_roundtrip() {
838        let desc = StructureDesc {
839            struct_id: Some("epics:nt/NTScalar:1.0".to_string()),
840            fields: vec![crate::spvd_decode::FieldDesc {
841                name: "value".to_string(),
842                field_type: crate::spvd_decode::FieldType::Scalar(
843                    crate::spvd_decode::TypeCode::String,
844                ),
845            }],
846        };
847        let msg = encode_get_field_response(11, &desc, 2, false);
848        let mut pkt = PvaPacket::new(&msg);
849        let cmd = pkt.decode_payload().expect("decoded");
850        match cmd {
851            PvaPacketCommand::GetField(payload) => {
852                assert!(payload.is_server);
853                assert_eq!(payload.cid, 11);
854                assert!(payload.status.is_none());
855                let intro = payload.introspection.expect("introspection");
856                assert_eq!(intro.fields.len(), 1);
857                assert_eq!(intro.fields[0].name, "value");
858            }
859            other => panic!("unexpected decode: {:?}", other),
860        }
861    }
862
863    #[test]
864    fn encode_get_field_error_roundtrip() {
865        let msg = encode_get_field_error(7, "listing disabled", 2, false);
866        let mut pkt = PvaPacket::new(&msg);
867        let cmd = pkt.decode_payload().expect("decoded");
868        match cmd {
869            PvaPacketCommand::GetField(payload) => {
870                assert!(payload.is_server);
871                assert_eq!(payload.cid, 7);
872                let status = payload.status.expect("status");
873                assert_eq!(status.code, 0x02);
874                assert_eq!(status.message.as_deref(), Some("listing disabled"));
875            }
876            other => panic!("unexpected decode: {:?}", other),
877        }
878    }
879}