Skip to main content

rs_matter/
bdx.rs

1/*
2 *
3 *    Copyright (c) 2026 Project CHIP Authors
4 *
5 *    Licensed under the Apache License, Version 2.0 (the "License");
6 *    you may not use this file except in compliance with the License.
7 *    You may obtain a copy of the License at
8 *
9 *        http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *    Unless required by applicable law or agreed to in writing, software
12 *    distributed under the License is distributed on an "AS IS" BASIS,
13 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *    See the License for the specific language governing permissions and
15 *    limitations under the License.
16 */
17
18//! Bulk Data Exchange (BDX) protocol.
19//!
20//! BDX transfers an opaque "file" (a sequence of bytes plus optional metadata)
21//! between two nodes over a single [`Exchange`](crate::transport::exchange::Exchange),
22//! inside a PASE or CASE session. It is used, among other things, to download
23//! Over-the-Air (OTA) software-update images.
24//!
25//! It provides the wire codec (the protocol id, opcodes, status codes, the
26//! `TransferControl`/`RangeControl` flag fields, and the message types with
27//! binary parse/encode) and a synchronous streaming engine on top of it: the
28//! [`BdxDownloadInitiator`]/[`BdxUploadInitiator`] initiator traits and the
29//! [`BdxDownloadResponder`]/[`BdxUploadResponder`] responders, which yield
30//! [`BdxReader`]/[`BdxWriter`] byte-stream handles.
31//!
32//! All multi-byte integers are little-endian (as per the Matter Core spec).
33
34use num::FromPrimitive;
35use num_derive::FromPrimitive;
36
37use crate::error::{Error, ErrorCode};
38use crate::sc::{self, GeneralCode, StatusReport};
39use crate::transport::exchange::{Exchange, MessageMeta};
40use crate::transport::{MAX_RX_PAYLOAD_SIZE, MAX_TX_PAYLOAD_SIZE};
41use crate::utils::storage::{ReadBuf, WriteBuf};
42
43mod handler;
44mod nego;
45mod read;
46mod write;
47
48pub use handler::*;
49pub use read::*;
50pub use write::*;
51
52/// The buffer a BDX transfer stages each block in. Aliases the central
53/// [`Buffer`](crate::transport::exchange::Buffer) (same size as an Interaction
54/// Model exchange buffer), so a single [`PooledBuffers`] pool can be shared with
55/// the data model if desired.
56///
57/// [`PooledBuffers`]: crate::utils::storage::pooled::PooledBuffers
58pub type BdxBuffer = crate::transport::exchange::Buffer;
59
60/// The Matter protocol id for BDX.
61pub const PROTO_ID_BDX: u16 = 0x0002;
62
63/// The BDX protocol version implemented here. BDX Version 0 is the first (and,
64/// as of Matter 1.5, only) version.
65pub const BDX_VERSION: u8 = 0;
66
67/// The BDX protocol message opcodes.
68#[derive(FromPrimitive, Debug, Copy, Clone, Eq, PartialEq)]
69#[cfg_attr(feature = "defmt", derive(defmt::Format))]
70#[repr(u8)]
71pub enum OpCode {
72    /// Initiator wants to be the Sender (upload).
73    SendInit = 0x01,
74    /// Responder accepts a `SendInit`.
75    SendAccept = 0x02,
76    /// Initiator wants to be the Receiver (download).
77    ReceiveInit = 0x04,
78    /// Responder accepts a `ReceiveInit`.
79    ReceiveAccept = 0x05,
80    /// Driving Receiver requests the next block.
81    BlockQuery = 0x10,
82    /// A block of data.
83    Block = 0x11,
84    /// The final block of a transfer (may be empty).
85    BlockEof = 0x12,
86    /// Acknowledges a received `Block`.
87    BlockAck = 0x13,
88    /// Acknowledges a received `BlockEof`; ends the session.
89    BlockAckEof = 0x14,
90    /// Like `BlockQuery`, but advances the sender's cursor first.
91    BlockQueryWithSkip = 0x15,
92}
93
94impl OpCode {
95    /// The [`MessageMeta`] for this opcode. All BDX messages are reliable: BDX
96    /// runs only over reliable transports and uses MRP over UDP.
97    pub fn meta(self) -> MessageMeta {
98        MessageMeta {
99            proto_id: PROTO_ID_BDX,
100            proto_opcode: self as u8,
101            reliable: true,
102        }
103    }
104}
105
106impl From<OpCode> for MessageMeta {
107    fn from(op: OpCode) -> Self {
108        op.meta()
109    }
110}
111
112/// The BDX status codes carried in a `StatusReport` to fail or reject a transfer.
113#[derive(FromPrimitive, Debug, Copy, Clone, Eq, PartialEq)]
114#[cfg_attr(feature = "defmt", derive(defmt::Format))]
115#[repr(u16)]
116pub enum BdxStatus {
117    LengthTooLarge = 0x0012,
118    LengthTooShort = 0x0013,
119    LengthMismatch = 0x0014,
120    LengthRequired = 0x0015,
121    BadMessageContents = 0x0016,
122    BadBlockCounter = 0x0017,
123    UnexpectedMessage = 0x0018,
124    ResponderBusy = 0x0019,
125    TransferFailedUnknownError = 0x001F,
126    TransferMethodNotSupported = 0x0050,
127    FileDesignatorUnknown = 0x0051,
128    StartOffsetNotSupported = 0x0052,
129    VersionNotSupported = 0x0053,
130    Unknown = 0x005F,
131}
132
133impl BdxStatus {
134    /// Build the BDX failure [`StatusReport`] for this status code (`GeneralCode:
135    /// FAILURE, ProtocolId: BDX`). BDX `StatusReport`s carry no extra data.
136    pub fn as_report(self) -> StatusReport<'static> {
137        StatusReport {
138            general_code: GeneralCode::Failure,
139            proto_id: PROTO_ID_BDX as u32,
140            proto_code: self as u16,
141            proto_data: &[],
142        }
143    }
144}
145
146/// The Proposed Transfer Control (PTC) / Transfer Control (TC) field of the
147/// `*Init`/`*Accept` messages.
148///
149/// Carries the protocol [`version`](Self::version) in the low nibble and the
150/// proposed/selected drive mode(s) in the high bits. In an `*Init` it is a *set*
151/// of proposals; in an `*Accept` exactly one drive mode is selected.
152#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
153#[cfg_attr(feature = "defmt", derive(defmt::Format))]
154pub struct TransferControl {
155    /// Protocol version (bits 0-3).
156    pub version: u8,
157    /// Sender-drive mode (bit 4): the Sender paces the transfer via `Block`.
158    pub sender_drive: bool,
159    /// Receiver-drive mode (bit 5): the Receiver paces it via `BlockQuery`.
160    pub receiver_drive: bool,
161    /// Asynchronous mode (bit 6). Provisional - never selected by a Responder.
162    pub async_mode: bool,
163}
164
165impl TransferControl {
166    const SENDER_DRIVE: u8 = 1 << 4;
167    const RECEIVER_DRIVE: u8 = 1 << 5;
168    const ASYNC: u8 = 1 << 6;
169    const VERSION_MASK: u8 = 0x0f;
170
171    /// The transfer control a Responder echoes back in an `*Accept` to select a
172    /// single (synchronous) drive mode: Sender-drive if `sender_drive`, else
173    /// Receiver-drive, at this protocol version.
174    pub(crate) const fn select(sender_drive: bool) -> Self {
175        Self {
176            version: BDX_VERSION,
177            sender_drive,
178            receiver_drive: !sender_drive,
179            async_mode: false,
180        }
181    }
182
183    fn from_byte(b: u8) -> Self {
184        Self {
185            version: b & Self::VERSION_MASK,
186            sender_drive: b & Self::SENDER_DRIVE != 0,
187            receiver_drive: b & Self::RECEIVER_DRIVE != 0,
188            async_mode: b & Self::ASYNC != 0,
189        }
190    }
191
192    fn to_byte(self) -> u8 {
193        let mut b = self.version & Self::VERSION_MASK;
194
195        if self.sender_drive {
196            b |= Self::SENDER_DRIVE;
197        }
198
199        if self.receiver_drive {
200            b |= Self::RECEIVER_DRIVE;
201        }
202
203        if self.async_mode {
204            b |= Self::ASYNC;
205        }
206
207        b
208    }
209}
210
211/// The Range Control (RC) field of the `*Init`/`ReceiveAccept` messages.
212#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
213#[cfg_attr(feature = "defmt", derive(defmt::Format))]
214pub struct RangeControl {
215    /// A definite length is present (bit 0).
216    pub def_len: bool,
217    /// A start offset is present (bit 1). Not used in `ReceiveAccept`.
218    pub start_offset: bool,
219    /// Offset/length fields are 64-bit rather than 32-bit (bit 4).
220    pub wide_range: bool,
221}
222
223impl RangeControl {
224    const DEF_LEN: u8 = 1 << 0;
225    const START_OFFSET: u8 = 1 << 1;
226    const WIDE_RANGE: u8 = 1 << 4;
227
228    fn from_byte(b: u8) -> Self {
229        Self {
230            def_len: b & Self::DEF_LEN != 0,
231            start_offset: b & Self::START_OFFSET != 0,
232            wide_range: b & Self::WIDE_RANGE != 0,
233        }
234    }
235
236    fn to_byte(self) -> u8 {
237        let mut b = 0;
238
239        if self.def_len {
240            b |= Self::DEF_LEN;
241        }
242
243        if self.start_offset {
244            b |= Self::START_OFFSET;
245        }
246
247        if self.wide_range {
248            b |= Self::WIDE_RANGE;
249        }
250
251        b
252    }
253}
254
255/// A `SendInit` (`OpCode::SendInit`) or `ReceiveInit` (`OpCode::ReceiveInit`)
256/// message - the opening message of a BDX session.
257///
258/// The two share an identical wire format; the opcode distinguishes the
259/// Initiator's intended role (Sender for `SendInit`, Receiver for `ReceiveInit`).
260#[derive(Debug, Clone)]
261pub struct TransferInit<'a> {
262    /// Proposed transfer control (version + supported drive modes).
263    pub transfer_control: TransferControl,
264    /// Range control (length/offset presence + width).
265    pub range_control: RangeControl,
266    /// Proposed maximum block size, exclusive of the block counter.
267    pub max_block_size: u16,
268    /// Start offset within the file. Meaningful only if
269    /// `range_control.start_offset`; `0` otherwise.
270    pub start_offset: u64,
271    /// Proposed/maximum length. Meaningful only if `range_control.def_len`;
272    /// `0` (indefinite) otherwise.
273    pub length: u64,
274    /// The file designator chosen by the Initiator to identify the payload.
275    pub file_designator: &'a [u8],
276    /// Optional application metadata (raw TLV bytes; empty if absent).
277    pub metadata: &'a [u8],
278}
279
280impl<'a> TransferInit<'a> {
281    /// Parse a `SendInit`/`ReceiveInit` payload.
282    pub fn parse(payload: &'a [u8]) -> Result<Self, Error> {
283        let mut rb = ReadBuf::new(payload);
284
285        let transfer_control = TransferControl::from_byte(rb.le_u8()?);
286        let range_control = RangeControl::from_byte(rb.le_u8()?);
287        let max_block_size = rb.le_u16()?;
288
289        let start_offset = if range_control.start_offset {
290            if range_control.wide_range {
291                rb.le_u64()?
292            } else {
293                rb.le_u32()? as u64
294            }
295        } else {
296            0
297        };
298        let length = if range_control.def_len {
299            if range_control.wide_range {
300                rb.le_u64()?
301            } else {
302                rb.le_u32()? as u64
303            }
304        } else {
305            0
306        };
307
308        let fdl = rb.le_u16()? as usize;
309        // The variable-length tail (file designator + metadata) is sliced out of
310        // `payload` directly so it borrows for `'a` rather than for the `ReadBuf`.
311        let off = rb.read_off();
312        let end = off.checked_add(fdl).ok_or(ErrorCode::TruncatedPacket)?;
313        let file_designator = payload.get(off..end).ok_or(ErrorCode::TruncatedPacket)?;
314        let metadata = &payload[end..];
315
316        Ok(Self {
317            transfer_control,
318            range_control,
319            max_block_size,
320            start_offset,
321            length,
322            file_designator,
323            metadata,
324        })
325    }
326
327    /// Encode this message's payload (without the protocol header).
328    pub fn write(&self, wb: &mut WriteBuf) -> Result<(), Error> {
329        wb.le_u8(self.transfer_control.to_byte())?;
330        wb.le_u8(self.range_control.to_byte())?;
331        wb.le_u16(self.max_block_size)?;
332
333        if self.range_control.start_offset {
334            if self.range_control.wide_range {
335                wb.le_u64(self.start_offset)?;
336            } else {
337                wb.le_u32(self.start_offset as u32)?;
338            }
339        }
340
341        if self.range_control.def_len {
342            if self.range_control.wide_range {
343                wb.le_u64(self.length)?;
344            } else {
345                wb.le_u32(self.length as u32)?;
346            }
347        }
348
349        wb.le_u16(self.file_designator.len() as u16)?;
350        wb.append(self.file_designator)?;
351        wb.append(self.metadata)?;
352
353        Ok(())
354    }
355}
356
357/// A `SendAccept` (`OpCode::SendAccept`) or `ReceiveAccept`
358/// (`OpCode::ReceiveAccept`) message.
359///
360/// `SendAccept` carries only the transfer control and max block size;
361/// `ReceiveAccept` additionally carries the range control and (optionally) the
362/// final length. The [`receive`](Self::receive) flag selects the wire format.
363#[derive(Debug, Clone)]
364pub struct TransferAccept<'a> {
365    /// `true` for `ReceiveAccept` (carries range control + optional length),
366    /// `false` for `SendAccept`.
367    pub receive: bool,
368    /// The selected transfer control (exactly one drive mode + version).
369    pub transfer_control: TransferControl,
370    /// Range control. `ReceiveAccept` only; ignored for `SendAccept`.
371    pub range_control: RangeControl,
372    /// The negotiated max block size (`<= max_block_size` of the `*Init`).
373    pub max_block_size: u16,
374    /// The final transfer length (`ReceiveAccept` + `range_control.def_len`);
375    /// `0` (indefinite) otherwise.
376    pub length: u64,
377    /// Optional application metadata (raw TLV bytes; empty if absent).
378    pub metadata: &'a [u8],
379}
380
381impl<'a> TransferAccept<'a> {
382    /// Parse a `SendAccept` (`receive = false`) / `ReceiveAccept`
383    /// (`receive = true`) payload.
384    pub fn parse(receive: bool, payload: &'a [u8]) -> Result<Self, Error> {
385        let mut rb = ReadBuf::new(payload);
386
387        let transfer_control = TransferControl::from_byte(rb.le_u8()?);
388
389        let (range_control, max_block_size, length) = if receive {
390            let range_control = RangeControl::from_byte(rb.le_u8()?);
391            // Max block size comes before the (optional) length.
392            let max_block_size = rb.le_u16()?;
393            let length = if range_control.def_len {
394                if range_control.wide_range {
395                    rb.le_u64()?
396                } else {
397                    rb.le_u32()? as u64
398                }
399            } else {
400                0
401            };
402            (range_control, max_block_size, length)
403        } else {
404            (RangeControl::default(), rb.le_u16()?, 0)
405        };
406
407        // Any trailing bytes are the (optional) metadata; borrow from `payload`.
408        let metadata = &payload[rb.read_off()..];
409
410        Ok(Self {
411            receive,
412            transfer_control,
413            range_control,
414            max_block_size,
415            length,
416            metadata,
417        })
418    }
419
420    /// Encode this message's payload (without the protocol header).
421    pub fn write(&self, wb: &mut WriteBuf) -> Result<(), Error> {
422        wb.le_u8(self.transfer_control.to_byte())?;
423
424        if self.receive {
425            wb.le_u8(self.range_control.to_byte())?;
426            wb.le_u16(self.max_block_size)?;
427
428            if self.range_control.def_len {
429                if self.range_control.wide_range {
430                    wb.le_u64(self.length)?;
431                } else {
432                    wb.le_u32(self.length as u32)?;
433                }
434            }
435        } else {
436            wb.le_u16(self.max_block_size)?;
437        }
438
439        wb.append(self.metadata)?;
440
441        Ok(())
442    }
443}
444
445/// A `Block` (`OpCode::Block`) or `BlockEof` (`OpCode::BlockEof`) message - a
446/// chunk of the transferred data tagged with its block counter.
447#[derive(Debug, Clone)]
448pub struct Block<'a> {
449    /// The block counter (ascending, wrapping `mod 2^32`).
450    pub block_counter: u32,
451    /// The block data. `[0..=max_block_size]` for a `Block` (non-empty in
452    /// practice), and possibly empty for a `BlockEof`.
453    pub data: &'a [u8],
454}
455
456impl<'a> Block<'a> {
457    /// Parse a `Block`/`BlockEof` payload.
458    pub fn parse(payload: &'a [u8]) -> Result<Self, Error> {
459        let mut rb = ReadBuf::new(payload);
460        let block_counter = rb.le_u32()?;
461        // The remaining bytes are the block data; borrow them from `payload`.
462        let data = &payload[rb.read_off()..];
463
464        Ok(Self {
465            block_counter,
466            data,
467        })
468    }
469
470    /// Encode this message's payload (without the protocol header).
471    pub fn write(&self, wb: &mut WriteBuf) -> Result<(), Error> {
472        wb.le_u32(self.block_counter)?;
473        wb.append(self.data)?;
474
475        Ok(())
476    }
477}
478
479/// A `BlockQuery` (`OpCode::BlockQuery`) message - a driving Receiver requesting
480/// the next block.
481///
482/// `BlockAck`/`BlockAckEof` share the same single-`block_counter` wire format,
483/// so this type doubles for them too.
484#[derive(Debug, Clone, Copy, Eq, PartialEq)]
485pub struct BlockQuery {
486    /// The block counter being requested/acknowledged.
487    pub block_counter: u32,
488}
489
490impl BlockQuery {
491    /// Parse a `BlockQuery`/`BlockAck`/`BlockAckEof` payload.
492    pub fn parse(payload: &[u8]) -> Result<Self, Error> {
493        let mut rb = ReadBuf::new(payload);
494
495        Ok(Self {
496            block_counter: rb.le_u32()?,
497        })
498    }
499
500    /// Encode this message's payload (without the protocol header).
501    pub fn write(&self, wb: &mut WriteBuf) -> Result<(), Error> {
502        wb.le_u32(self.block_counter)
503    }
504}
505
506/// A `BlockQueryWithSkip` (`OpCode::BlockQueryWithSkip`) message - a `BlockQuery`
507/// that first advances the Sender's cursor by `bytes_to_skip`.
508#[derive(Debug, Clone, Copy, Eq, PartialEq)]
509pub struct BlockQueryWithSkip {
510    /// The block counter being requested.
511    pub block_counter: u32,
512    /// The number of bytes to skip forward before sending the next block.
513    pub bytes_to_skip: u64,
514}
515
516impl BlockQueryWithSkip {
517    /// Parse a `BlockQueryWithSkip` payload.
518    pub fn parse(payload: &[u8]) -> Result<Self, Error> {
519        let mut rb = ReadBuf::new(payload);
520
521        Ok(Self {
522            block_counter: rb.le_u32()?,
523            bytes_to_skip: rb.le_u64()?,
524        })
525    }
526
527    /// Encode this message's payload (without the protocol header).
528    pub fn write(&self, wb: &mut WriteBuf) -> Result<(), Error> {
529        wb.le_u32(self.block_counter)?;
530        wb.le_u64(self.bytes_to_skip)?;
531
532        Ok(())
533    }
534}
535
536/// Try to interpret a `MessageMeta` as a BDX opcode.
537pub(crate) fn opcode(meta: &MessageMeta) -> Option<OpCode> {
538    (meta.proto_id == PROTO_ID_BDX).then(|| OpCode::from_u8(meta.proto_opcode))?
539}
540
541// ===========================================================================
542// Shared streaming primitives (block size + drive mode) used by both the `read`
543// and `write` submodules. The negotiation/framing helpers live in `nego`, and
544// the byte-stream handles in `read`/`write`:
545// `BdxReader`/`BdxDownloadInitiator`/`BdxUploadResponder` in `read`, and
546// `BdxWriter`/`BdxUploadInitiator`/`BdxDownloadResponder` in `write`.
547// ===========================================================================
548
549/// The number of header bytes preceding a block's data (the 32-bit block counter).
550const BLOCK_HEADER_LEN: usize = 4;
551
552/// The largest block *data* that fits in a `payload`-sized application payload
553/// once the block counter is accounted for, capped to the `u16` of the BDX
554/// max-block-size field.
555const fn max_block_size(payload: usize) -> u16 {
556    let data = payload - BLOCK_HEADER_LEN;
557
558    if data > u16::MAX as usize {
559        u16::MAX
560    } else {
561        data as u16
562    }
563}
564
565/// The largest block the *receiver* (`BdxReader`) can accept: it streams block
566/// data straight out of the exchange RX buffer, so its capacity is the RX
567/// application payload (minus the block counter).
568const MAX_RX_BLOCK_SIZE: u16 = max_block_size(MAX_RX_PAYLOAD_SIZE);
569
570/// The largest block the *sender* (`BdxWriter`) can emit into the exchange TX
571/// buffer. The writer additionally bounds the block size by its caller-provided
572/// staging buffer.
573const MAX_TX_BLOCK_SIZE: u16 = max_block_size(MAX_TX_PAYLOAD_SIZE);
574
575/// How this endpoint participates in a synchronous transfer.
576///
577/// This is the extension point for the (currently unimplemented) asynchronous
578/// mode: adding an `Async` variant here, handled in the `read`/`write` step
579/// helpers, would not change the public `read`/`write` surface.
580#[derive(Debug, Clone, Copy, Eq, PartialEq)]
581enum Drive {
582    /// We control the pace: as a receiver we send `BlockQuery`; as a sender we
583    /// send `Block` and await `BlockAck`.
584    Driver,
585    /// We follow the peer's pace: as a receiver we await `Block` and send
586    /// `BlockAck`; as a sender we await `BlockQuery` before sending `Block`.
587    Follower,
588}
589
590#[cfg(test)]
591mod tests {
592    use crate::utils::storage::WriteBuf;
593
594    use super::*;
595
596    fn roundtrip_init(msg: &TransferInit) {
597        let mut buf = [0u8; 256];
598        let mut wb = WriteBuf::new(&mut buf);
599        msg.write(&mut wb).unwrap();
600        let bytes = wb.as_slice().to_vec();
601
602        let parsed = TransferInit::parse(&bytes).unwrap();
603        assert_eq!(parsed.transfer_control, msg.transfer_control);
604        assert_eq!(parsed.range_control, msg.range_control);
605        assert_eq!(parsed.max_block_size, msg.max_block_size);
606        assert_eq!(parsed.start_offset, msg.start_offset);
607        assert_eq!(parsed.length, msg.length);
608        assert_eq!(parsed.file_designator, msg.file_designator);
609        assert_eq!(parsed.metadata, msg.metadata);
610    }
611
612    #[test]
613    fn transfer_control_flags_roundtrip() {
614        for tc in [
615            TransferControl {
616                version: 0,
617                sender_drive: true,
618                receiver_drive: false,
619                async_mode: false,
620            },
621            TransferControl {
622                version: 0,
623                sender_drive: false,
624                receiver_drive: true,
625                async_mode: false,
626            },
627            TransferControl {
628                version: 0,
629                sender_drive: true,
630                receiver_drive: true,
631                async_mode: true,
632            },
633        ] {
634            assert_eq!(TransferControl::from_byte(tc.to_byte()), tc);
635        }
636        // Wire layout: version low nibble, drive bits high.
637        assert_eq!(
638            TransferControl {
639                version: 0,
640                sender_drive: true,
641                ..Default::default()
642            }
643            .to_byte(),
644            0x10
645        );
646        assert_eq!(
647            TransferControl {
648                version: 0,
649                receiver_drive: true,
650                ..Default::default()
651            }
652            .to_byte(),
653            0x20
654        );
655    }
656
657    #[test]
658    fn transfer_control_select_picks_one_drive() {
659        let sender = TransferControl::select(true);
660        assert!(sender.sender_drive && !sender.receiver_drive && !sender.async_mode);
661        assert_eq!(sender.version, BDX_VERSION);
662
663        let receiver = TransferControl::select(false);
664        assert!(receiver.receiver_drive && !receiver.sender_drive && !receiver.async_mode);
665    }
666
667    #[test]
668    fn range_control_flags_roundtrip() {
669        let rc = RangeControl {
670            def_len: true,
671            start_offset: true,
672            wide_range: true,
673        };
674        assert_eq!(RangeControl::from_byte(rc.to_byte()), rc);
675        assert_eq!(rc.to_byte(), 0x13); // DEFLEN(0) | STARTOFS(1) | WIDERANGE(4)
676    }
677
678    #[test]
679    fn receive_init_minimal_roundtrip() {
680        roundtrip_init(&TransferInit {
681            transfer_control: TransferControl {
682                version: 0,
683                sender_drive: true,
684                receiver_drive: true,
685                async_mode: false,
686            },
687            range_control: RangeControl::default(),
688            max_block_size: 1024,
689            start_offset: 0,
690            length: 0,
691            file_designator: b"firmware.bin",
692            metadata: &[],
693        });
694    }
695
696    #[test]
697    fn receive_init_with_offset_and_length_roundtrip() {
698        roundtrip_init(&TransferInit {
699            transfer_control: TransferControl {
700                version: 0,
701                receiver_drive: true,
702                ..Default::default()
703            },
704            range_control: RangeControl {
705                def_len: true,
706                start_offset: true,
707                wide_range: false,
708            },
709            max_block_size: 1024,
710            start_offset: 0x1234,
711            length: 0x5_6789,
712            file_designator: b"img",
713            metadata: &[0xde, 0xad],
714        });
715    }
716
717    #[test]
718    fn wide_range_uses_8_octets() {
719        let msg = TransferInit {
720            transfer_control: TransferControl {
721                version: 0,
722                sender_drive: true,
723                ..Default::default()
724            },
725            range_control: RangeControl {
726                def_len: true,
727                start_offset: false,
728                wide_range: true,
729            },
730            max_block_size: 512,
731            start_offset: 0,
732            length: 0x1_0000_0000, // > u32, requires wide range
733            file_designator: b"x",
734            metadata: &[],
735        };
736        roundtrip_init(&msg);
737        // 1 (PTC) + 1 (RC) + 2 (PMBS) + 8 (LEN) + 2 (FDL) + 1 (FD) = 15 bytes.
738        let mut buf = [0u8; 64];
739        let mut wb = WriteBuf::new(&mut buf);
740        msg.write(&mut wb).unwrap();
741        assert_eq!(wb.as_slice().len(), 15);
742    }
743
744    #[test]
745    fn send_accept_roundtrip() {
746        let msg = TransferAccept {
747            receive: false,
748            transfer_control: TransferControl {
749                version: 0,
750                sender_drive: true,
751                ..Default::default()
752            },
753            range_control: RangeControl::default(),
754            max_block_size: 1024,
755            length: 0,
756            metadata: &[],
757        };
758        let mut buf = [0u8; 64];
759        let mut wb = WriteBuf::new(&mut buf);
760        msg.write(&mut wb).unwrap();
761        // SendAccept = TC(1) + MBS(2) = 3 bytes (no RC, no LEN).
762        assert_eq!(wb.as_slice().len(), 3);
763        let bytes = wb.as_slice().to_vec();
764        let parsed = TransferAccept::parse(false, &bytes).unwrap();
765        assert!(parsed.transfer_control.sender_drive);
766        assert_eq!(parsed.max_block_size, 1024);
767    }
768
769    #[test]
770    fn receive_accept_roundtrip() {
771        let msg = TransferAccept {
772            receive: true,
773            transfer_control: TransferControl {
774                version: 0,
775                sender_drive: true,
776                ..Default::default()
777            },
778            range_control: RangeControl {
779                def_len: true,
780                start_offset: false,
781                wide_range: false,
782            },
783            max_block_size: 1024,
784            length: 123_456,
785            metadata: &[],
786        };
787        let mut buf = [0u8; 64];
788        let mut wb = WriteBuf::new(&mut buf);
789        msg.write(&mut wb).unwrap();
790        let bytes = wb.as_slice().to_vec();
791        let parsed = TransferAccept::parse(true, &bytes).unwrap();
792        assert!(parsed.transfer_control.sender_drive);
793        assert_eq!(parsed.max_block_size, 1024);
794        assert!(parsed.range_control.def_len);
795        assert_eq!(parsed.length, 123_456);
796    }
797
798    #[test]
799    fn block_roundtrip() {
800        let msg = Block {
801            block_counter: 7,
802            data: b"hello world",
803        };
804        let mut buf = [0u8; 64];
805        let mut wb = WriteBuf::new(&mut buf);
806        msg.write(&mut wb).unwrap();
807        let bytes = wb.as_slice().to_vec();
808        let parsed = Block::parse(&bytes).unwrap();
809        assert_eq!(parsed.block_counter, 7);
810        assert_eq!(parsed.data, b"hello world");
811    }
812
813    #[test]
814    fn block_eof_empty_roundtrip() {
815        let msg = Block {
816            block_counter: 0,
817            data: &[],
818        };
819        let mut buf = [0u8; 8];
820        let mut wb = WriteBuf::new(&mut buf);
821        msg.write(&mut wb).unwrap();
822        assert_eq!(wb.as_slice().len(), 4); // counter only
823        let bytes = wb.as_slice().to_vec();
824        let parsed = Block::parse(&bytes).unwrap();
825        assert_eq!(parsed.block_counter, 0);
826        assert!(parsed.data.is_empty());
827    }
828
829    #[test]
830    fn block_query_and_skip_roundtrip() {
831        let mut buf = [0u8; 32];
832
833        let mut wb = WriteBuf::new(&mut buf);
834        BlockQuery { block_counter: 5 }.write(&mut wb).unwrap();
835        assert_eq!(wb.as_slice().len(), 4);
836        let bytes = wb.as_slice().to_vec();
837        assert_eq!(BlockQuery::parse(&bytes).unwrap().block_counter, 5);
838
839        let mut wb = WriteBuf::new(&mut buf);
840        BlockQueryWithSkip {
841            block_counter: 9,
842            bytes_to_skip: 0x1_0000,
843        }
844        .write(&mut wb)
845        .unwrap();
846        assert_eq!(wb.as_slice().len(), 12); // 4 + 8
847        let bytes = wb.as_slice().to_vec();
848        let parsed = BlockQueryWithSkip::parse(&bytes).unwrap();
849        assert_eq!(parsed.block_counter, 9);
850        assert_eq!(parsed.bytes_to_skip, 0x1_0000);
851    }
852
853    #[test]
854    fn truncated_is_rejected() {
855        assert!(BlockQuery::parse(&[1, 2, 3]).is_err()); // need 4 bytes
856        assert!(TransferInit::parse(&[0x00]).is_err()); // need at least PTC+RC+PMBS+FDL
857    }
858
859    #[test]
860    fn opcode_meta_is_bdx_and_reliable() {
861        let meta = OpCode::ReceiveInit.meta();
862        assert_eq!(meta.proto_id, PROTO_ID_BDX);
863        assert_eq!(meta.proto_opcode, 0x04);
864        assert!(meta.reliable);
865        assert_eq!(opcode(&meta), Some(OpCode::ReceiveInit));
866    }
867}