dstar_gateway_core/codec/dplus/
encode.rs

1//! `DPlus` packet encoders.
2//!
3//! Every encoder writes into a caller-supplied `&mut [u8]` and returns
4//! the number of bytes written. Hot-path encoding is allocation-free.
5
6use crate::error::EncodeError;
7use crate::header::DStarHeader;
8use crate::types::{Callsign, StreamId};
9use crate::voice::{AMBE_SILENCE, VoiceFrame};
10
11use super::consts::{
12    DSVT_MAGIC, DSVT_TYPE, DV_CLIENT_ID, LINK1_ACK_BYTES, LINK1_BYTES, LINK2_ACCEPT_TAG,
13    LINK2_BUSY_TAG, LINK2_HEADER, LINK2_REPLY_PREFIX, POLL_BYTES, POLL_ECHO_BYTES,
14    UNLINK_ACK_BYTES, UNLINK_BYTES, VOICE_DATA_PREFIX, VOICE_EOT_PREFIX, VOICE_EOT_TRAILER,
15    VOICE_HEADER_PREFIX,
16};
17use super::packet::Link2Result;
18
19/// Encode a LINK1 connect request (5 bytes).
20///
21/// # Errors
22///
23/// Returns [`EncodeError::BufferTooSmall`] if `out.len() < 5`.
24///
25/// # See also
26///
27/// `ircDDBGateway/Common/ConnectData.cpp:445-473` (`getDPlusData`
28/// `CT_LINK1` branch) for the reference encoder this function mirrors.
29pub fn encode_link1(out: &mut [u8]) -> Result<usize, EncodeError> {
30    write_fixed(out, &LINK1_BYTES)
31}
32
33/// Encode an UNLINK request (5 bytes).
34///
35/// # Errors
36///
37/// Returns [`EncodeError::BufferTooSmall`] if `out.len() < 5`.
38///
39/// # See also
40///
41/// `ircDDBGateway/Common/ConnectData.cpp:445-473` (`getDPlusData`
42/// `CT_UNLINK` branch).
43pub fn encode_unlink(out: &mut [u8]) -> Result<usize, EncodeError> {
44    write_fixed(out, &UNLINK_BYTES)
45}
46
47/// Encode a 3-byte keepalive poll.
48///
49/// # Errors
50///
51/// Returns [`EncodeError::BufferTooSmall`] if `out.len() < 3`.
52///
53/// # See also
54///
55/// `ircDDBGateway/Common/PollData.cpp:145-160` (`getDPlusData`).
56pub fn encode_poll(out: &mut [u8]) -> Result<usize, EncodeError> {
57    write_fixed(out, &POLL_BYTES)
58}
59
60/// Encode the server's LINK1 ACK echo (5 bytes — same shape as LINK1).
61///
62/// # Errors
63///
64/// Returns [`EncodeError::BufferTooSmall`] if `out.len() < 5`.
65///
66/// # See also
67///
68/// `xlxd/src/cdplusprotocol.cpp:510-540` (`EncodeConnectAckPacket`).
69pub fn encode_link1_ack(out: &mut [u8]) -> Result<usize, EncodeError> {
70    write_fixed(out, &LINK1_ACK_BYTES)
71}
72
73/// Encode the server's UNLINK ACK echo (5 bytes — same shape as UNLINK).
74///
75/// # Errors
76///
77/// Returns [`EncodeError::BufferTooSmall`] if `out.len() < 5`.
78///
79/// # See also
80///
81/// `xlxd/src/cdplusprotocol.cpp:510-540` (`EncodeDisconnectAckPacket`).
82pub fn encode_unlink_ack(out: &mut [u8]) -> Result<usize, EncodeError> {
83    write_fixed(out, &UNLINK_ACK_BYTES)
84}
85
86/// Encode the server's poll echo (3 bytes).
87///
88/// # Errors
89///
90/// Returns [`EncodeError::BufferTooSmall`] if `out.len() < 3`.
91///
92/// # See also
93///
94/// `xlxd/src/cdplusprotocol.cpp` — the reference encoder reuses the
95/// client poll bytes verbatim.
96pub fn encode_poll_echo(out: &mut [u8]) -> Result<usize, EncodeError> {
97    write_fixed(out, &POLL_ECHO_BYTES)
98}
99
100/// Internal helper: copy a fixed-byte literal into the output buffer.
101fn write_fixed(out: &mut [u8], src: &[u8]) -> Result<usize, EncodeError> {
102    if out.len() < src.len() {
103        return Err(EncodeError::BufferTooSmall {
104            need: src.len(),
105            have: out.len(),
106        });
107    }
108    if let Some(dst) = out.get_mut(..src.len()) {
109        dst.copy_from_slice(src);
110    }
111    Ok(src.len())
112}
113
114/// Encode a LINK2 login packet (28 bytes).
115///
116/// Layout per `ircDDBGateway/Common/ConnectData.cpp:449-473`:
117/// - bytes `[0..4]`: `[0x1C, 0xC0, 0x04, 0x00]`
118/// - bytes `[4..20]`: callsign at `[4..]`, zero-padded to offset 20
119/// - bytes `[20..28]`: `b"DV019999"`
120///
121/// # Errors
122///
123/// Returns [`EncodeError::BufferTooSmall`] if `out.len() < 28`.
124///
125/// # See also
126///
127/// `ircDDBGateway/Common/ConnectData.cpp:449-473` (`getDPlusData`
128/// `CT_LINK2`) for the reference encoder this function mirrors.
129pub fn encode_link2(out: &mut [u8], callsign: &Callsign) -> Result<usize, EncodeError> {
130    const LEN: usize = 28;
131    if out.len() < LEN {
132        return Err(EncodeError::BufferTooSmall {
133            need: LEN,
134            have: out.len(),
135        });
136    }
137    // Zero the entire 28-byte region first.
138    if let Some(dst) = out.get_mut(..LEN) {
139        for b in dst.iter_mut() {
140            *b = 0;
141        }
142    }
143    // 4-byte header.
144    if let Some(dst) = out.get_mut(..LINK2_HEADER.len()) {
145        dst.copy_from_slice(&LINK2_HEADER);
146    }
147    // Callsign at offset 4. The Callsign type guarantees 8 bytes
148    // already space-padded — we want the trimmed length followed by
149    // zeros up to offset 20, matching the reference encoder.
150    let bytes = callsign.as_bytes();
151    let trimmed_len = bytes.iter().rposition(|&b| b != b' ').map_or(0, |p| p + 1);
152    if let Some(dst) = out.get_mut(4..4 + trimmed_len)
153        && let Some(src) = bytes.get(..trimmed_len)
154    {
155        dst.copy_from_slice(src);
156    }
157    // DV019999 at offset 20.
158    if let Some(dst) = out.get_mut(20..28) {
159        dst.copy_from_slice(&DV_CLIENT_ID);
160    }
161    Ok(LEN)
162}
163
164/// Encode an 8-byte LINK2 reply.
165///
166/// Layout: 4-byte prefix `[0x08, 0xC0, 0x04, 0x00]` + 4-byte result tag.
167/// - `Link2Result::Accept` → `b"OKRW"`
168/// - `Link2Result::Busy` → `b"BUSY"`
169/// - `Link2Result::Unknown { reply }` → the supplied 4-byte tag
170///
171/// # Errors
172///
173/// Returns [`EncodeError::BufferTooSmall`] if `out.len() < 8`.
174///
175/// # See also
176///
177/// `xlxd/src/cdplusprotocol.cpp:535-544` (`EncodeLoginAckPacket` /
178/// `EncodeLoginNackPacket`) for the reference encoder.
179pub fn encode_link2_reply(out: &mut [u8], result: Link2Result) -> Result<usize, EncodeError> {
180    const LEN: usize = 8;
181    if out.len() < LEN {
182        return Err(EncodeError::BufferTooSmall {
183            need: LEN,
184            have: out.len(),
185        });
186    }
187    if let Some(dst) = out.get_mut(..LINK2_REPLY_PREFIX.len()) {
188        dst.copy_from_slice(&LINK2_REPLY_PREFIX);
189    }
190    let tag: [u8; 4] = match result {
191        Link2Result::Accept => LINK2_ACCEPT_TAG,
192        Link2Result::Busy => LINK2_BUSY_TAG,
193        Link2Result::Unknown { reply } => reply,
194    };
195    if let Some(dst) = out.get_mut(4..8) {
196        dst.copy_from_slice(&tag);
197    }
198    Ok(LEN)
199}
200
201/// Encode a 58-byte voice header.
202///
203/// Layout per `ircDDBGateway/Common/HeaderData.cpp:637-684`
204/// (`getDPlusData`):
205/// - `[0]` 0x3A (length byte)
206/// - `[1]` 0x80 (type byte)
207/// - `[2..6]` "DSVT"
208/// - `[6]` 0x10 (header type)
209/// - `[7..10]` 0x00 0x00 0x00 (reserved)
210/// - `[10]` 0x20 (config)
211/// - `[11..14]` 0x00 0x01 0x02 (band1/2/3)
212/// - `[14..16]` `stream_id` little-endian
213/// - `[16]` 0x80 (header indicator)
214/// - `[17..58]` `DStarHeader::encode_for_dsvt()` (flag bytes zeroed, 41 bytes)
215///
216/// # Errors
217///
218/// Returns [`EncodeError::BufferTooSmall`] if `out.len() < 58`.
219///
220/// # See also
221///
222/// `ircDDBGateway/Common/HeaderData.cpp:637-684` (`getDPlusData`)
223/// for the reference encoder this function mirrors. The
224/// `DStarHeader::encode_for_dsvt` helper mirrors the same file's
225/// CRC logic.
226pub fn encode_voice_header(
227    out: &mut [u8],
228    stream_id: StreamId,
229    header: &DStarHeader,
230) -> Result<usize, EncodeError> {
231    const LEN: usize = 58;
232    if out.len() < LEN {
233        return Err(EncodeError::BufferTooSmall {
234            need: LEN,
235            have: out.len(),
236        });
237    }
238    if let Some(b) = out.get_mut(0) {
239        *b = VOICE_HEADER_PREFIX;
240    }
241    if let Some(b) = out.get_mut(1) {
242        *b = DSVT_TYPE;
243    }
244    if let Some(dst) = out.get_mut(2..6) {
245        dst.copy_from_slice(&DSVT_MAGIC);
246    }
247    if let Some(b) = out.get_mut(6) {
248        *b = 0x10;
249    }
250    if let Some(b) = out.get_mut(7) {
251        *b = 0x00;
252    }
253    if let Some(b) = out.get_mut(8) {
254        *b = 0x00;
255    }
256    if let Some(b) = out.get_mut(9) {
257        *b = 0x00;
258    }
259    if let Some(b) = out.get_mut(10) {
260        *b = 0x20;
261    }
262    if let Some(b) = out.get_mut(11) {
263        *b = 0x00;
264    }
265    if let Some(b) = out.get_mut(12) {
266        *b = 0x01;
267    }
268    if let Some(b) = out.get_mut(13) {
269        *b = 0x02;
270    }
271    let sid = stream_id.get().to_le_bytes();
272    if let Some(b) = out.get_mut(14) {
273        *b = sid[0];
274    }
275    if let Some(b) = out.get_mut(15) {
276        *b = sid[1];
277    }
278    if let Some(b) = out.get_mut(16) {
279        *b = 0x80;
280    }
281    let encoded = header.encode_for_dsvt();
282    if let Some(dst) = out.get_mut(17..58) {
283        dst.copy_from_slice(&encoded);
284    }
285    Ok(LEN)
286}
287
288/// Encode a 29-byte voice data packet.
289///
290/// Layout per `ircDDBGateway/Common/AMBEData.cpp:347-388`
291/// (`getDPlusData` else branch):
292/// - `[0]` 0x1D (length byte)
293/// - `[1]` 0x80 (type byte)
294/// - `[2..6]` "DSVT"
295/// - `[6]` 0x20 (voice type)
296/// - `[7..10]` 0x00 (reserved)
297/// - `[10]` 0x20 (config)
298/// - `[11..14]` 0x00 0x01 0x02 (bands)
299/// - `[14..16]` `stream_id` LE
300/// - `[16]` seq
301/// - `[17..26]` 9 AMBE bytes
302/// - `[26..29]` 3 slow data bytes
303///
304/// # Errors
305///
306/// Returns [`EncodeError::BufferTooSmall`] if `out.len() < 29`.
307///
308/// # See also
309///
310/// `ircDDBGateway/Common/AMBEData.cpp:347-388` (`getDPlusData`)
311/// for the reference encoder, and `xlxd/src/cdplusprotocol.cpp`
312/// for a mirror implementation.
313pub fn encode_voice_data(
314    out: &mut [u8],
315    stream_id: StreamId,
316    seq: u8,
317    frame: &VoiceFrame,
318) -> Result<usize, EncodeError> {
319    const LEN: usize = 29;
320    if out.len() < LEN {
321        return Err(EncodeError::BufferTooSmall {
322            need: LEN,
323            have: out.len(),
324        });
325    }
326    write_voice_prefix(out, VOICE_DATA_PREFIX, stream_id, seq);
327    if let Some(dst) = out.get_mut(17..26) {
328        dst.copy_from_slice(&frame.ambe);
329    }
330    if let Some(dst) = out.get_mut(26..29) {
331        dst.copy_from_slice(&frame.slow_data);
332    }
333    Ok(LEN)
334}
335
336/// Encode a 32-byte voice EOT packet.
337///
338/// Layout per `ircDDBGateway/Common/AMBEData.cpp:380-388`
339/// (`getDPlusData isEnd branch`):
340/// - same as `encode_voice_data` for offsets `[0..17]` except `[0]` is 0x20
341/// - `[16]` seq with 0x40 bit set (EOT marker)
342/// - `[17..26]` `AMBE_SILENCE` (9 bytes)
343/// - `[26..32]` `VOICE_EOT_TRAILER` `[0x55, 0x55, 0x55, 0x55, 0xC8, 0x7A]`
344///
345/// # Errors
346///
347/// Returns [`EncodeError::BufferTooSmall`] if `out.len() < 32`.
348///
349/// # See also
350///
351/// `ircDDBGateway/Common/AMBEData.cpp:380-388` (`getDPlusData`
352/// isEnd branch) for the reference EOT encoder.
353pub fn encode_voice_eot(
354    out: &mut [u8],
355    stream_id: StreamId,
356    seq: u8,
357) -> Result<usize, EncodeError> {
358    const LEN: usize = 32;
359    if out.len() < LEN {
360        return Err(EncodeError::BufferTooSmall {
361            need: LEN,
362            have: out.len(),
363        });
364    }
365    write_voice_prefix(out, VOICE_EOT_PREFIX, stream_id, seq | 0x40);
366    if let Some(dst) = out.get_mut(17..26) {
367        dst.copy_from_slice(&AMBE_SILENCE);
368    }
369    if let Some(dst) = out.get_mut(26..32) {
370        dst.copy_from_slice(&VOICE_EOT_TRAILER);
371    }
372    Ok(LEN)
373}
374
375/// Internal helper: write the 17-byte prefix common to voice data
376/// and voice EOT packets.
377fn write_voice_prefix(out: &mut [u8], prefix: u8, stream_id: StreamId, seq: u8) {
378    if let Some(b) = out.get_mut(0) {
379        *b = prefix;
380    }
381    if let Some(b) = out.get_mut(1) {
382        *b = DSVT_TYPE;
383    }
384    if let Some(dst) = out.get_mut(2..6) {
385        dst.copy_from_slice(&DSVT_MAGIC);
386    }
387    if let Some(b) = out.get_mut(6) {
388        *b = 0x20;
389    }
390    if let Some(b) = out.get_mut(7) {
391        *b = 0x00;
392    }
393    if let Some(b) = out.get_mut(8) {
394        *b = 0x00;
395    }
396    if let Some(b) = out.get_mut(9) {
397        *b = 0x00;
398    }
399    if let Some(b) = out.get_mut(10) {
400        *b = 0x20;
401    }
402    if let Some(b) = out.get_mut(11) {
403        *b = 0x00;
404    }
405    if let Some(b) = out.get_mut(12) {
406        *b = 0x01;
407    }
408    if let Some(b) = out.get_mut(13) {
409        *b = 0x02;
410    }
411    let sid = stream_id.get().to_le_bytes();
412    if let Some(b) = out.get_mut(14) {
413        *b = sid[0];
414    }
415    if let Some(b) = out.get_mut(15) {
416        *b = sid[1];
417    }
418    if let Some(b) = out.get_mut(16) {
419        *b = seq;
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426    use crate::types::Suffix;
427
428    type TestResult = Result<(), Box<dyn std::error::Error>>;
429
430    const fn cs(bytes: [u8; 8]) -> Callsign {
431        Callsign::from_wire_bytes(bytes)
432    }
433
434    #[expect(clippy::unwrap_used, reason = "compile-time validated: n != 0")]
435    const fn sid(n: u16) -> StreamId {
436        StreamId::new(n).unwrap()
437    }
438
439    fn test_header() -> DStarHeader {
440        DStarHeader {
441            flag1: 0,
442            flag2: 0,
443            flag3: 0,
444            rpt2: Callsign::from_wire_bytes(*b"REF030 G"),
445            rpt1: Callsign::from_wire_bytes(*b"REF030 C"),
446            ur_call: Callsign::from_wire_bytes(*b"CQCQCQ  "),
447            my_call: Callsign::from_wire_bytes(*b"W1AW    "),
448            my_suffix: Suffix::EMPTY,
449        }
450    }
451
452    // Fixed-byte encoder tests
453    #[test]
454    fn encode_link1_writes_five_bytes() -> TestResult {
455        let mut buf = [0u8; 16];
456        let n = encode_link1(&mut buf)?;
457        assert_eq!(n, 5);
458        assert_eq!(&buf[..5], &[0x05, 0x00, 0x18, 0x00, 0x01]);
459        Ok(())
460    }
461
462    #[test]
463    fn encode_unlink_writes_five_bytes() -> TestResult {
464        let mut buf = [0u8; 16];
465        let n = encode_unlink(&mut buf)?;
466        assert_eq!(n, 5);
467        assert_eq!(&buf[..5], &[0x05, 0x00, 0x18, 0x00, 0x00]);
468        Ok(())
469    }
470
471    #[test]
472    fn encode_poll_writes_three_bytes() -> TestResult {
473        let mut buf = [0u8; 16];
474        let n = encode_poll(&mut buf)?;
475        assert_eq!(n, 3);
476        assert_eq!(&buf[..3], &[0x03, 0x60, 0x00]);
477        Ok(())
478    }
479
480    #[test]
481    fn encode_link1_ack_matches_link1() -> TestResult {
482        let mut buf = [0u8; 16];
483        let n = encode_link1_ack(&mut buf)?;
484        assert_eq!(
485            buf.get(..n).ok_or("n within buf")?,
486            &[0x05, 0x00, 0x18, 0x00, 0x01]
487        );
488        Ok(())
489    }
490
491    #[test]
492    fn encode_unlink_ack_matches_unlink() -> TestResult {
493        let mut buf = [0u8; 16];
494        let n = encode_unlink_ack(&mut buf)?;
495        assert_eq!(
496            buf.get(..n).ok_or("n within buf")?,
497            &[0x05, 0x00, 0x18, 0x00, 0x00]
498        );
499        Ok(())
500    }
501
502    #[test]
503    fn encode_poll_echo_matches_poll() -> TestResult {
504        let mut buf = [0u8; 16];
505        let n = encode_poll_echo(&mut buf)?;
506        assert_eq!(buf.get(..n).ok_or("n within buf")?, &[0x03, 0x60, 0x00]);
507        Ok(())
508    }
509
510    #[test]
511    fn encode_link1_rejects_small_buffer() -> TestResult {
512        let mut buf = [0u8; 4];
513        let Err(err) = encode_link1(&mut buf) else {
514            return Err("expected BufferTooSmall error".into());
515        };
516        assert_eq!(err, EncodeError::BufferTooSmall { need: 5, have: 4 });
517        Ok(())
518    }
519
520    #[test]
521    fn encode_poll_rejects_two_byte_buffer() -> TestResult {
522        let mut buf = [0u8; 2];
523        let Err(err) = encode_poll(&mut buf) else {
524            return Err("expected BufferTooSmall error".into());
525        };
526        assert_eq!(err, EncodeError::BufferTooSmall { need: 3, have: 2 });
527        Ok(())
528    }
529
530    // LINK2 tests
531    #[test]
532    fn encode_link2_w1aw_writes_28_bytes() -> TestResult {
533        let mut buf = [0u8; 32];
534        let n = encode_link2(&mut buf, &cs(*b"W1AW    "))?;
535        assert_eq!(n, 28);
536        assert_eq!(&buf[..4], &[0x1C, 0xC0, 0x04, 0x00], "header");
537        assert_eq!(
538            &buf[4..12],
539            b"W1AW\0\0\0\0",
540            "callsign followed by zeros to offset 12"
541        );
542        assert_eq!(
543            &buf[12..20],
544            &[0u8; 8],
545            "zero-pad between callsign and DV id"
546        );
547        assert_eq!(&buf[20..28], b"DV019999", "client identifier");
548        Ok(())
549    }
550
551    #[test]
552    fn encode_link2_rejects_buffer_too_small() -> TestResult {
553        let mut buf = [0u8; 16];
554        let Err(err) = encode_link2(&mut buf, &cs(*b"W1AW    ")) else {
555            return Err("expected BufferTooSmall error".into());
556        };
557        assert_eq!(err, EncodeError::BufferTooSmall { need: 28, have: 16 });
558        Ok(())
559    }
560
561    // LINK2 reply tests
562    #[test]
563    fn encode_link2_reply_accept_writes_okrw() -> TestResult {
564        let mut buf = [0u8; 16];
565        let n = encode_link2_reply(&mut buf, Link2Result::Accept)?;
566        assert_eq!(n, 8);
567        assert_eq!(&buf[..4], &[0x08, 0xC0, 0x04, 0x00]);
568        assert_eq!(&buf[4..8], b"OKRW");
569        Ok(())
570    }
571
572    #[test]
573    fn encode_link2_reply_busy_writes_busy() -> TestResult {
574        let mut buf = [0u8; 16];
575        let n = encode_link2_reply(&mut buf, Link2Result::Busy)?;
576        assert_eq!(n, 8);
577        assert_eq!(&buf[4..8], b"BUSY");
578        Ok(())
579    }
580
581    #[test]
582    fn encode_link2_reply_unknown_writes_custom_tag() -> TestResult {
583        let mut buf = [0u8; 16];
584        let n = encode_link2_reply(&mut buf, Link2Result::Unknown { reply: *b"FAIL" })?;
585        assert_eq!(&buf[4..8], b"FAIL");
586        assert_eq!(n, 8);
587        Ok(())
588    }
589
590    // Voice header tests
591    #[test]
592    fn encode_voice_header_writes_58_bytes() -> TestResult {
593        let mut buf = [0u8; 64];
594        let n = encode_voice_header(&mut buf, sid(0xCAFE), &test_header())?;
595        assert_eq!(n, 58);
596        assert_eq!(buf[0], 0x3A, "DPlus prefix");
597        assert_eq!(buf[1], 0x80, "DSVT type");
598        assert_eq!(&buf[2..6], b"DSVT");
599        assert_eq!(buf[6], 0x10, "header type");
600        assert_eq!(buf[14], 0xFE, "stream id LE low byte");
601        assert_eq!(buf[15], 0xCA, "stream id LE high byte");
602        assert_eq!(buf[16], 0x80, "header indicator");
603        assert_eq!(buf[17], 0, "flag1 zeroed by encode_for_dsvt");
604        assert_eq!(buf[18], 0, "flag2 zeroed by encode_for_dsvt");
605        assert_eq!(buf[19], 0, "flag3 zeroed by encode_for_dsvt");
606        Ok(())
607    }
608
609    #[test]
610    fn encode_voice_header_rejects_small_buffer() -> TestResult {
611        let mut buf = [0u8; 32];
612        let Err(err) = encode_voice_header(&mut buf, sid(0x1234), &test_header()) else {
613            return Err("expected BufferTooSmall error".into());
614        };
615        assert_eq!(err, EncodeError::BufferTooSmall { need: 58, have: 32 });
616        Ok(())
617    }
618
619    // Voice data tests
620    #[test]
621    fn encode_voice_data_writes_29_bytes() -> TestResult {
622        let mut buf = [0u8; 64];
623        let frame = VoiceFrame {
624            ambe: [0x11; 9],
625            slow_data: [0x22; 3],
626        };
627        let n = encode_voice_data(&mut buf, sid(0x1234), 5, &frame)?;
628        assert_eq!(n, 29);
629        assert_eq!(buf[0], 0x1D, "DPlus prefix");
630        assert_eq!(buf[1], 0x80, "DSVT type");
631        assert_eq!(&buf[2..6], b"DSVT");
632        assert_eq!(buf[14], 0x34, "stream id LE low byte");
633        assert_eq!(buf[15], 0x12, "stream id LE high byte");
634        assert_eq!(buf[16], 5, "seq");
635        assert_eq!(&buf[17..26], &[0x11; 9]);
636        assert_eq!(&buf[26..29], &[0x22; 3]);
637        Ok(())
638    }
639
640    // Voice EOT tests
641    #[test]
642    fn encode_voice_eot_writes_32_bytes() -> TestResult {
643        let mut buf = [0u8; 64];
644        let n = encode_voice_eot(&mut buf, sid(0x1234), 7)?;
645        assert_eq!(n, 32);
646        assert_eq!(buf[0], 0x20, "EOT prefix");
647        assert_eq!(buf[1], 0x80, "DSVT type");
648        assert_eq!(buf[16] & 0x40, 0x40, "EOT bit set");
649        assert_eq!(buf[16] & 0x3F, 7, "low bits preserve seq");
650        assert_eq!(&buf[17..26], &AMBE_SILENCE);
651        assert_eq!(&buf[26..32], &[0x55, 0x55, 0x55, 0x55, 0xC8, 0x7A]);
652        Ok(())
653    }
654}