umsh_core/
builder.rs

1use core::{marker::PhantomData, ops::Range};
2
3use crate::{
4    BuildError, ChannelId, Fcf, FloodHops, MicSize, NodeHint, OptionNumber, PacketType, PublicKey,
5    Scf, SecInfo, UnsealedPacket, options::OptionEncoder,
6};
7
8/// Typestate markers used by [`PacketBuilder`] and its specialized builders.
9pub mod state {
10    /// Builder state that still requires a source address.
11    pub struct NeedsSource;
12    /// Builder state that still requires a frame counter.
13    pub struct NeedsCounter;
14    /// Builder state where options and payload can be configured.
15    pub struct Configuring;
16    /// Builder state after payload bytes have been staged.
17    pub struct Complete;
18}
19
20/// Entry point for typestate packet construction.
21///
22/// The builder consumes itself as it transitions through required stages so
23/// invalid packet layouts are rejected at compile time where possible.
24pub struct PacketBuilder<'a> {
25    buf: &'a mut [u8],
26}
27
28impl<'a> PacketBuilder<'a> {
29    /// Start building a packet into `buf`.
30    pub fn new(buf: &'a mut [u8]) -> Self {
31        Self { buf }
32    }
33
34    /// Begin a broadcast packet.
35    pub fn broadcast(self) -> BroadcastBuilder<'a, state::NeedsSource> {
36        Builder::new(self.buf, PacketType::Broadcast)
37    }
38
39    /// Build a MAC ACK packet.
40    ///
41    /// The ack carries no destination hint — it is a return-routed token
42    /// correlated by its trailer. `ack_trailer` is the 8-byte
43    /// `ack_mic || ack_tag` value (see the crypto engine's
44    /// `compute_ack_trailer`).
45    pub fn mac_ack(self, ack_trailer: [u8; 8]) -> MacAckBuilder<'a, state::Configuring> {
46        let mut builder = Builder::new(self.buf, PacketType::MacAck);
47        builder.ack_tag = Some(ack_trailer);
48        builder
49    }
50
51    /// Begin a unicast packet to `dst`.
52    pub fn unicast(self, dst: NodeHint) -> UnicastBuilder<'a, state::NeedsSource> {
53        let mut builder = Builder::new(self.buf, PacketType::Unicast);
54        builder.dst = Some(dst);
55        builder
56    }
57
58    /// Begin a multicast packet on `channel`.
59    pub fn multicast(self, channel: ChannelId) -> MulticastBuilder<'a, state::NeedsSource> {
60        let mut builder = Builder::new(self.buf, PacketType::Multicast);
61        builder.channel = Some(channel);
62        builder
63    }
64
65    /// Begin a blind-unicast packet addressed through `channel` to `dst`.
66    pub fn blind_unicast(
67        self,
68        channel: ChannelId,
69        dst: NodeHint,
70    ) -> BlindUnicastBuilder<'a, state::NeedsSource> {
71        let mut builder = Builder::new(self.buf, PacketType::BlindUnicast);
72        builder.channel = Some(channel);
73        builder.dst = Some(dst);
74        builder
75    }
76}
77
78/// Broadcast packet builder alias.
79pub type BroadcastBuilder<'a, S> = Builder<'a, BroadcastKind, S>;
80/// MAC ACK packet builder alias.
81pub type MacAckBuilder<'a, S> = Builder<'a, MacAckKind, S>;
82/// Unicast packet builder alias.
83pub type UnicastBuilder<'a, S> = Builder<'a, UnicastKind, S>;
84/// Multicast packet builder alias.
85pub type MulticastBuilder<'a, S> = Builder<'a, MulticastKind, S>;
86/// Blind-unicast packet builder alias.
87pub type BlindUnicastBuilder<'a, S> = Builder<'a, BlindUnicastKind, S>;
88
89/// Marker type for broadcast builders.
90pub struct BroadcastKind;
91/// Marker type for MAC ACK builders.
92pub struct MacAckKind;
93/// Marker type for unicast builders.
94pub struct UnicastKind;
95/// Marker type for multicast builders.
96pub struct MulticastKind;
97/// Marker type for blind-unicast builders.
98pub struct BlindUnicastKind;
99
100enum SourceValue {
101    Hint(NodeHint),
102    Full(PublicKey),
103}
104
105/// Internal generic builder implementation shared by all packet kinds.
106pub struct Builder<'a, K, S> {
107    buf: &'a mut [u8],
108    packet_type: PacketType,
109    options_len: usize,
110    options_scratch: Option<Range<usize>>,
111    last_option_number: Option<u16>,
112    option_error: Option<BuildError>,
113    source: Option<SourceValue>,
114    dst: Option<NodeHint>,
115    channel: Option<ChannelId>,
116    ack_tag: Option<[u8; 8]>,
117    frame_counter: Option<u32>,
118    encrypted: bool,
119    mic_size: MicSize,
120    salt: Option<u16>,
121    flood_hops: Option<FloodHops>,
122    payload: Option<Range<usize>>,
123    blind_addr: Option<Range<usize>>,
124    _marker: PhantomData<(K, S)>,
125}
126
127impl<'a, K, S> Builder<'a, K, S> {
128    fn new(buf: &'a mut [u8], packet_type: PacketType) -> Self {
129        Self {
130            buf,
131            packet_type,
132            options_len: 0,
133            options_scratch: None,
134            last_option_number: None,
135            option_error: None,
136            source: None,
137            dst: None,
138            channel: None,
139            ack_tag: None,
140            frame_counter: None,
141            encrypted: matches!(
142                packet_type,
143                PacketType::BlindUnicast | PacketType::BlindUnicastAckReq
144            ),
145            mic_size: MicSize::Mic16,
146            salt: None,
147            flood_hops: None,
148            payload: None,
149            blind_addr: None,
150            _marker: PhantomData,
151        }
152    }
153
154    fn with_state<NS>(self) -> Builder<'a, K, NS> {
155        Builder {
156            buf: self.buf,
157            packet_type: self.packet_type,
158            options_len: self.options_len,
159            options_scratch: self.options_scratch,
160            last_option_number: self.last_option_number,
161            option_error: self.option_error,
162            source: self.source,
163            dst: self.dst,
164            channel: self.channel,
165            ack_tag: self.ack_tag,
166            frame_counter: self.frame_counter,
167            encrypted: self.encrypted,
168            mic_size: self.mic_size,
169            salt: self.salt,
170            flood_hops: self.flood_hops,
171            payload: self.payload,
172            blind_addr: self.blind_addr,
173            _marker: PhantomData,
174        }
175    }
176
177    fn push_option(&mut self, number: u16, value: &[u8]) {
178        if self.option_error.is_some() {
179            return;
180        }
181        if let Some(last) = self.last_option_number {
182            if number < last {
183                self.option_error = Some(BuildError::OptionOutOfOrder);
184                return;
185            }
186        }
187        let mut live = match self.last_option_number {
188            Some(last_number) => {
189                OptionEncoder::with_last_number(&mut self.buf[1 + self.options_len..], last_number)
190            }
191            None => OptionEncoder::new(&mut self.buf[1 + self.options_len..]),
192        };
193        match live.put(number, value) {
194            Ok(()) => {
195                self.options_len += live.finish();
196                self.last_option_number = Some(number);
197            }
198            Err(err) => self.option_error = Some(err.into()),
199        }
200    }
201
202    /// Move the options block staged at `buf[1..1 + options_len]` into a tail
203    /// scratch region so the prefix fields can overwrite its original slot.
204    fn stash_options(&mut self) -> Result<(), BuildError> {
205        if self.options_len == 0 {
206            return Ok(());
207        }
208        let payload_len = self.payload.as_ref().map(|p| p.end - p.start).unwrap_or(0);
209        let scratch_end = self
210            .buf
211            .len()
212            .checked_sub(payload_len)
213            .ok_or(BuildError::BufferTooSmall)?;
214        let scratch_start = scratch_end
215            .checked_sub(self.options_len)
216            .ok_or(BuildError::BufferTooSmall)?;
217        if scratch_start < 1 + self.options_len {
218            return Err(BuildError::BufferTooSmall);
219        }
220        self.buf.copy_within(1..1 + self.options_len, scratch_start);
221        self.options_scratch = Some(scratch_start..scratch_end);
222        Ok(())
223    }
224
225    /// Copy the options block from tail scratch to the current cursor. If a
226    /// payload follows, append the `0xFF` end-of-options marker.
227    fn emit_options(
228        &mut self,
229        cursor: &mut usize,
230        has_payload: bool,
231    ) -> Result<Range<usize>, BuildError> {
232        let start = *cursor;
233        if let Some(scratch) = self.options_scratch.clone() {
234            let len = scratch.end - scratch.start;
235            let end = start.checked_add(len).ok_or(BuildError::BufferTooSmall)?;
236            if end > self.buf.len() {
237                return Err(BuildError::BufferTooSmall);
238            }
239            self.buf.copy_within(scratch, start);
240            *cursor = end;
241        }
242        if has_payload {
243            let marker_slot = *cursor;
244            if marker_slot >= self.buf.len() {
245                return Err(BuildError::BufferTooSmall);
246            }
247            self.buf[marker_slot] = 0xFF;
248            *cursor = marker_slot + 1;
249        }
250        Ok(start..*cursor)
251    }
252
253    fn write_common_prefix(&mut self) -> Result<usize, BuildError> {
254        if let Some(err) = self.option_error {
255            return Err(err);
256        }
257        self.stash_options()?;
258        let full_source = matches!(self.source, Some(SourceValue::Full(_)));
259        let fcf = Fcf::new(self.packet_type, full_source, self.flood_hops.is_some());
260        if self.buf.is_empty() {
261            return Err(BuildError::BufferTooSmall);
262        }
263        self.buf[0] = fcf.0;
264        let mut cursor = 1;
265        if let Some(fhops) = self.flood_hops {
266            self.buf
267                .get_mut(cursor)
268                .ok_or(BuildError::BufferTooSmall)
269                .map(|slot| *slot = fhops.0)?;
270            cursor += 1;
271        }
272        Ok(cursor)
273    }
274
275    fn write_source(&mut self, cursor: &mut usize) -> Result<(), BuildError> {
276        match self.source {
277            Some(SourceValue::Hint(hint)) => {
278                let end = *cursor + 3;
279                self.buf
280                    .get_mut(*cursor..end)
281                    .ok_or(BuildError::BufferTooSmall)?
282                    .copy_from_slice(&hint.0);
283                *cursor = end;
284            }
285            Some(SourceValue::Full(key)) => {
286                let end = *cursor + 32;
287                self.buf
288                    .get_mut(*cursor..end)
289                    .ok_or(BuildError::BufferTooSmall)?
290                    .copy_from_slice(&key.0);
291                *cursor = end;
292            }
293            None => return Err(BuildError::MissingSource),
294        }
295        Ok(())
296    }
297
298    fn stage_payload(&mut self, data: &[u8]) {
299        let scratch_start = match self.buf.len().checked_sub(data.len()) {
300            Some(value) => value,
301            None => {
302                self.option_error = Some(BuildError::BufferTooSmall);
303                return;
304            }
305        };
306        if let Some(slot) = self.buf.get_mut(scratch_start..scratch_start + data.len()) {
307            slot.copy_from_slice(data);
308            self.payload = Some(scratch_start..scratch_start + data.len());
309        } else {
310            self.option_error = Some(BuildError::BufferTooSmall);
311        }
312    }
313
314    /// Whether the staged payload actually carries bytes.
315    ///
316    /// An empty payload is no payload: the `0xFF` end-of-options marker is
317    /// only required when data follows the options block, so staging `&[]`
318    /// must not put a marker on the wire. Packet types whose addresses
319    /// follow the options block need the marker regardless and do not
320    /// consult this.
321    fn has_body_bytes(&self) -> bool {
322        self.payload.as_ref().is_some_and(|range| !range.is_empty())
323    }
324
325    fn copy_staged_payload(&mut self, cursor: &mut usize) -> Result<Range<usize>, BuildError> {
326        let payload = self.payload.clone().ok_or(BuildError::MissingPayload)?;
327        let len = payload.end - payload.start;
328        let start = *cursor;
329        let end = start + len;
330        if end > self.buf.len() {
331            return Err(BuildError::BufferTooSmall);
332        }
333        self.buf.copy_within(payload, start);
334        *cursor = end;
335        Ok(start..end)
336    }
337
338    fn stage_blind_addr(&mut self, cursor: &mut usize) -> Result<Range<usize>, BuildError> {
339        let dst = self.dst.ok_or(BuildError::MissingDestination)?;
340        let start = *cursor;
341        let dst_end = start + 3;
342        self.buf
343            .get_mut(start..dst_end)
344            .ok_or(BuildError::BufferTooSmall)?
345            .copy_from_slice(&dst.0);
346        *cursor = dst_end;
347        self.write_source(cursor)?;
348        let end = *cursor;
349        Ok(start..end)
350    }
351}
352
353impl<'a> BroadcastBuilder<'a, state::NeedsSource> {
354    /// Encode the source address as a three-byte hint.
355    pub fn source_hint(mut self, hint: NodeHint) -> BroadcastBuilder<'a, state::Configuring> {
356        self.source = Some(SourceValue::Hint(hint));
357        self.with_state()
358    }
359
360    /// Encode the source address as a full public key.
361    pub fn source_full(mut self, key: &PublicKey) -> BroadcastBuilder<'a, state::Configuring> {
362        self.source = Some(SourceValue::Full(*key));
363        self.with_state()
364    }
365}
366
367impl<'a> UnicastBuilder<'a, state::NeedsSource> {
368    /// Encode the source address as a three-byte hint.
369    pub fn source_hint(mut self, hint: NodeHint) -> UnicastBuilder<'a, state::NeedsCounter> {
370        self.source = Some(SourceValue::Hint(hint));
371        self.with_state()
372    }
373
374    /// Encode the source address as a full public key.
375    pub fn source_full(mut self, key: &PublicKey) -> UnicastBuilder<'a, state::NeedsCounter> {
376        self.source = Some(SourceValue::Full(*key));
377        self.with_state()
378    }
379}
380
381impl<'a> MulticastBuilder<'a, state::NeedsSource> {
382    /// Encode the source address as a three-byte hint.
383    pub fn source_hint(mut self, hint: NodeHint) -> MulticastBuilder<'a, state::NeedsCounter> {
384        self.source = Some(SourceValue::Hint(hint));
385        self.with_state()
386    }
387
388    /// Encode the source address as a full public key.
389    pub fn source_full(mut self, key: &PublicKey) -> MulticastBuilder<'a, state::NeedsCounter> {
390        self.source = Some(SourceValue::Full(*key));
391        self.with_state()
392    }
393}
394
395impl<'a> BlindUnicastBuilder<'a, state::NeedsSource> {
396    /// Encode the source address as a three-byte hint.
397    pub fn source_hint(mut self, hint: NodeHint) -> BlindUnicastBuilder<'a, state::NeedsCounter> {
398        self.source = Some(SourceValue::Hint(hint));
399        self.with_state()
400    }
401
402    /// Encode the source address as a full public key.
403    pub fn source_full(mut self, key: &PublicKey) -> BlindUnicastBuilder<'a, state::NeedsCounter> {
404        self.source = Some(SourceValue::Full(*key));
405        self.with_state()
406    }
407}
408
409impl<'a> UnicastBuilder<'a, state::NeedsCounter> {
410    /// Set the frame counter for the secure packet.
411    pub fn frame_counter(mut self, counter: u32) -> UnicastBuilder<'a, state::Configuring> {
412        self.frame_counter = Some(counter);
413        self.with_state()
414    }
415}
416
417impl<'a> MulticastBuilder<'a, state::NeedsCounter> {
418    /// Set the frame counter for the secure packet.
419    pub fn frame_counter(mut self, counter: u32) -> MulticastBuilder<'a, state::Configuring> {
420        self.frame_counter = Some(counter);
421        self.with_state()
422    }
423}
424
425impl<'a> BlindUnicastBuilder<'a, state::NeedsCounter> {
426    /// Set the frame counter for the secure packet.
427    pub fn frame_counter(mut self, counter: u32) -> BlindUnicastBuilder<'a, state::Configuring> {
428        self.frame_counter = Some(counter);
429        self.with_state()
430    }
431}
432
433macro_rules! impl_configuring_common {
434    ($name:ident<$state:ty>) => {
435        impl<'a> $name<'a, $state> {
436            /// Set the initial flood-hop budget.
437            pub fn flood_hops(mut self, remaining: u8) -> Self {
438                if let Some(value) = FloodHops::new(remaining, 0) {
439                    self.flood_hops = Some(value);
440                }
441                self
442            }
443
444            /// Add the region-code option.
445            pub fn region_code(mut self, code: [u8; 2]) -> Self {
446                self.push_option(OptionNumber::RegionCode.as_u16(), &code);
447                self
448            }
449
450            /// Add an empty trace-route option.
451            pub fn trace_route(mut self) -> Self {
452                self.push_option(OptionNumber::TraceRoute.as_u16(), &[]);
453                self
454            }
455
456            /// Add an empty trace-signal option. Pairs entry-for-entry with
457            /// trace route, so it is only meaningful alongside it.
458            pub fn trace_signal(mut self) -> Self {
459                self.push_option(OptionNumber::TraceSignal.as_u16(), &[]);
460                self
461            }
462
463            /// Add a source-route option from a router-hint slice.
464            pub fn source_route(mut self, hops: &[crate::RouterHint]) -> Self {
465                let mut encoded = [0u8; 30];
466                let needed = hops.len() * 2;
467                if needed > encoded.len() {
468                    self.option_error = Some(BuildError::BufferTooSmall);
469                    return self;
470                }
471                for (index, hop) in hops.iter().enumerate() {
472                    encoded[index * 2..index * 2 + 2].copy_from_slice(&hop.0);
473                }
474                self.push_option(OptionNumber::SourceRoute.as_u16(), &encoded[..needed]);
475                self
476            }
477
478            /// Add an arbitrary option number/value pair.
479            pub fn option(mut self, number: OptionNumber, value: &[u8]) -> Self {
480                self.push_option(number.as_u16(), value);
481                self
482            }
483        }
484    };
485}
486
487impl_configuring_common!(BroadcastBuilder<state::Configuring>);
488impl_configuring_common!(MacAckBuilder<state::Configuring>);
489impl_configuring_common!(UnicastBuilder<state::Configuring>);
490impl_configuring_common!(MulticastBuilder<state::Configuring>);
491impl_configuring_common!(BlindUnicastBuilder<state::Configuring>);
492
493impl<'a> UnicastBuilder<'a, state::Configuring> {
494    /// Upgrade the packet type to ACK-requested unicast.
495    pub fn ack_requested(mut self) -> Self {
496        self.packet_type = PacketType::UnicastAckReq;
497        self
498    }
499
500    /// Mark the packet body for encryption.
501    pub fn encrypted(mut self) -> Self {
502        self.encrypted = true;
503        self
504    }
505
506    /// Select the MIC size reserved in the packet footer.
507    pub fn mic_size(mut self, size: MicSize) -> Self {
508        self.mic_size = size;
509        self
510    }
511
512    /// Attach an explicit salt value to SECINFO.
513    pub fn salt(mut self, salt: u16) -> Self {
514        self.salt = Some(salt);
515        self
516    }
517
518    /// Stage the application payload and advance to the terminal builder state.
519    pub fn payload(mut self, data: &[u8]) -> UnicastBuilder<'a, state::Complete> {
520        self.stage_payload(data);
521        self.with_state()
522    }
523}
524
525impl<'a> MulticastBuilder<'a, state::Configuring> {
526    /// Mark the packet body for encryption.
527    pub fn encrypted(mut self) -> Self {
528        self.encrypted = true;
529        self
530    }
531
532    /// Select the MIC size reserved in the packet footer.
533    pub fn mic_size(mut self, size: MicSize) -> Self {
534        self.mic_size = size;
535        self
536    }
537
538    /// Attach an explicit salt value to SECINFO.
539    pub fn salt(mut self, salt: u16) -> Self {
540        self.salt = Some(salt);
541        self
542    }
543
544    /// Stage the application payload and advance to the terminal builder state.
545    pub fn payload(mut self, data: &[u8]) -> MulticastBuilder<'a, state::Complete> {
546        self.stage_payload(data);
547        self.with_state()
548    }
549}
550
551impl<'a> BlindUnicastBuilder<'a, state::Configuring> {
552    /// Upgrade the packet type to ACK-requested blind unicast.
553    pub fn ack_requested(mut self) -> Self {
554        self.packet_type = PacketType::BlindUnicastAckReq;
555        self
556    }
557
558    /// Mark the body and blinded address block for encryption.
559    pub fn encrypted(mut self) -> Self {
560        self.encrypted = true;
561        self
562    }
563
564    /// Emit a blind-unicast packet without encrypting the payload.
565    ///
566    /// This exists primarily for tests and for explicit policy-rejection
567    /// scenarios where higher layers need to construct a frame they expect to
568    /// reject. Normal blind-unicast traffic should remain encrypted.
569    pub fn unencrypted(mut self) -> Self {
570        self.encrypted = false;
571        self
572    }
573
574    /// Select the MIC size reserved in the packet footer.
575    pub fn mic_size(mut self, size: MicSize) -> Self {
576        self.mic_size = size;
577        self
578    }
579
580    /// Attach an explicit salt value to SECINFO.
581    pub fn salt(mut self, salt: u16) -> Self {
582        self.salt = Some(salt);
583        self
584    }
585
586    /// Stage the application payload and advance to the terminal builder state.
587    pub fn payload(mut self, data: &[u8]) -> BlindUnicastBuilder<'a, state::Complete> {
588        self.stage_payload(data);
589        self.with_state()
590    }
591}
592
593impl<'a> BroadcastBuilder<'a, state::Configuring> {
594    /// Stage a broadcast payload.
595    pub fn payload(mut self, data: &[u8]) -> BroadcastBuilder<'a, state::Complete> {
596        self.stage_payload(data);
597        self.with_state()
598    }
599
600    /// Finalize the broadcast packet and return the written frame bytes.
601    ///
602    /// Layout: `FCF [FHOPS] SRC OPTIONS [0xFF PAYLOAD]`
603    pub fn build(mut self) -> Result<&'a [u8], BuildError> {
604        let has_payload = self.has_body_bytes();
605        let mut cursor = self.write_common_prefix()?;
606        self.write_source(&mut cursor)?;
607        self.emit_options(&mut cursor, has_payload)?;
608        if has_payload {
609            let _ = self.copy_staged_payload(&mut cursor)?;
610        }
611        Ok(&self.buf[..cursor])
612    }
613}
614
615impl<'a> BroadcastBuilder<'a, state::Complete> {
616    /// Finalize the broadcast packet and return the written frame bytes.
617    pub fn build(self) -> Result<&'a [u8], BuildError> {
618        self.with_state::<state::Configuring>().build()
619    }
620}
621
622impl<'a> MacAckBuilder<'a, state::Configuring> {
623    /// Finalize the MAC ACK packet and return the written frame bytes.
624    ///
625    /// Layout: `FCF [FHOPS] OPTIONS ACK_TRAILER(8)` where the trailer is
626    /// `ack_mic(4) || ack_tag(4)`. There is no destination hint. The `0xFF`
627    /// end-marker is omitted — the trailer is at a fixed offset from the end.
628    pub fn build(mut self) -> Result<&'a [u8], BuildError> {
629        let mut cursor = self.write_common_prefix()?;
630        self.emit_options(&mut cursor, false)?;
631        let ack_trailer = self.ack_tag.ok_or(BuildError::MissingAckTag)?;
632        self.buf
633            .get_mut(cursor..cursor + 8)
634            .ok_or(BuildError::BufferTooSmall)?
635            .copy_from_slice(&ack_trailer);
636        cursor += 8;
637        Ok(&self.buf[..cursor])
638    }
639}
640
641impl<'a> UnicastBuilder<'a, state::Complete> {
642    pub fn build(self) -> Result<UnsealedPacket<'a>, BuildError> {
643        self.with_state::<state::Configuring>().build()
644    }
645}
646
647impl<'a> UnicastBuilder<'a, state::Configuring> {
648    /// Layout: `FCF [FHOPS] DST SRC SECINFO OPTIONS [0xFF PAYLOAD] MIC`
649    pub fn build(mut self) -> Result<UnsealedPacket<'a>, BuildError> {
650        let has_payload = self.has_body_bytes();
651        let mut cursor = self.write_common_prefix()?;
652        let dst = self.dst.ok_or(BuildError::MissingDestination)?;
653        self.buf
654            .get_mut(cursor..cursor + 3)
655            .ok_or(BuildError::BufferTooSmall)?
656            .copy_from_slice(&dst.0);
657        cursor += 3;
658        self.write_source(&mut cursor)?;
659        let scf = Scf::new(self.encrypted, self.mic_size, self.salt.is_some());
660        let sec_info = SecInfo {
661            scf,
662            frame_counter: self.frame_counter.ok_or(BuildError::MissingFrameCounter)?,
663            salt: self.salt,
664        };
665        let sec_start = cursor;
666        cursor += sec_info.encode(
667            self.buf
668                .get_mut(cursor..)
669                .ok_or(BuildError::BufferTooSmall)?,
670        )?;
671        let opts_range = self.emit_options(&mut cursor, has_payload)?;
672        let body_start = cursor;
673        let body_range = if has_payload {
674            self.copy_staged_payload(&mut cursor)?
675        } else {
676            body_start..body_start
677        };
678        let mic_start = cursor;
679        let mic_end = mic_start + self.mic_size.byte_len();
680        self.buf
681            .get_mut(mic_start..mic_end)
682            .ok_or(BuildError::BufferTooSmall)?
683            .fill(0);
684        cursor = mic_end;
685        Ok(UnsealedPacket::new(
686            self.buf,
687            cursor,
688            body_range,
689            None,
690            mic_start..mic_end,
691            sec_start..sec_start + sec_info.wire_len(),
692            opts_range,
693        ))
694    }
695}
696
697impl<'a> MulticastBuilder<'a, state::Complete> {
698    pub fn build(self) -> Result<UnsealedPacket<'a>, BuildError> {
699        self.with_state::<state::Configuring>().build()
700    }
701}
702
703impl<'a> MulticastBuilder<'a, state::Configuring> {
704    /// Layout (E=1): `FCF [FHOPS] CHANNEL SECINFO OPTIONS 0xFF ENC(SRC+PAYLOAD) MIC`
705    /// Layout (E=0): `FCF [FHOPS] CHANNEL SECINFO OPTIONS 0xFF SRC [PAYLOAD] MIC`
706    pub fn build(mut self) -> Result<UnsealedPacket<'a>, BuildError> {
707        let mut cursor = self.write_common_prefix()?;
708        let channel = self.channel.ok_or(BuildError::MissingChannel)?;
709        self.buf
710            .get_mut(cursor..cursor + 2)
711            .ok_or(BuildError::BufferTooSmall)?
712            .copy_from_slice(&channel.0);
713        cursor += 2;
714        let scf = Scf::new(self.encrypted, self.mic_size, self.salt.is_some());
715        let sec_info = SecInfo {
716            scf,
717            frame_counter: self.frame_counter.ok_or(BuildError::MissingFrameCounter)?,
718            salt: self.salt,
719        };
720        let sec_start = cursor;
721        cursor += sec_info.encode(
722            self.buf
723                .get_mut(cursor..)
724                .ok_or(BuildError::BufferTooSmall)?,
725        )?;
726        // SRC (and optionally payload) always follow OPTIONS, so 0xFF is always emitted.
727        let opts_range = self.emit_options(&mut cursor, true)?;
728        let body_start = cursor;
729        self.write_source(&mut cursor)?;
730        let payload_range = self.copy_staged_payload(&mut cursor)?;
731        let body_range = if self.encrypted {
732            body_start..payload_range.end
733        } else {
734            payload_range
735        };
736        let mic_start = cursor;
737        let mic_end = mic_start + self.mic_size.byte_len();
738        self.buf
739            .get_mut(mic_start..mic_end)
740            .ok_or(BuildError::BufferTooSmall)?
741            .fill(0);
742        cursor = mic_end;
743        Ok(UnsealedPacket::new(
744            self.buf,
745            cursor,
746            body_range,
747            None,
748            mic_start..mic_end,
749            sec_start..sec_start + sec_info.wire_len(),
750            opts_range,
751        ))
752    }
753}
754
755impl<'a> BlindUnicastBuilder<'a, state::Complete> {
756    pub fn build(self) -> Result<UnsealedPacket<'a>, BuildError> {
757        self.with_state::<state::Configuring>().build()
758    }
759}
760
761impl<'a> BlindUnicastBuilder<'a, state::Configuring> {
762    /// Layout (E=1): `FCF [FHOPS] CHANNEL SECINFO OPTIONS 0xFF ENC_DST_SRC ENC_PAYLOAD MIC`
763    /// Layout (E=0): `FCF [FHOPS] CHANNEL SECINFO OPTIONS 0xFF DST SRC [PAYLOAD] MIC`
764    pub fn build(mut self) -> Result<UnsealedPacket<'a>, BuildError> {
765        let mut cursor = self.write_common_prefix()?;
766        let channel = self.channel.ok_or(BuildError::MissingChannel)?;
767        self.buf
768            .get_mut(cursor..cursor + 2)
769            .ok_or(BuildError::BufferTooSmall)?
770            .copy_from_slice(&channel.0);
771        cursor += 2;
772        let scf = Scf::new(self.encrypted, self.mic_size, self.salt.is_some());
773        let sec_info = SecInfo {
774            scf,
775            frame_counter: self.frame_counter.ok_or(BuildError::MissingFrameCounter)?,
776            salt: self.salt,
777        };
778        let sec_start = cursor;
779        cursor += sec_info.encode(
780            self.buf
781                .get_mut(cursor..)
782                .ok_or(BuildError::BufferTooSmall)?,
783        )?;
784        // DST+SRC (and optionally payload) always follow OPTIONS, so 0xFF is always emitted.
785        let opts_range = self.emit_options(&mut cursor, true)?;
786        let blind_addr_range = self.stage_blind_addr(&mut cursor)?;
787        let body_range = self.copy_staged_payload(&mut cursor)?;
788        let mic_start = cursor;
789        let mic_end = mic_start + self.mic_size.byte_len();
790        self.buf
791            .get_mut(mic_start..mic_end)
792            .ok_or(BuildError::BufferTooSmall)?
793            .fill(0);
794        cursor = mic_end;
795        Ok(UnsealedPacket::new(
796            self.buf,
797            cursor,
798            body_range,
799            Some(blind_addr_range),
800            mic_start..mic_end,
801            sec_start..sec_start + sec_info.wire_len(),
802            opts_range,
803        ))
804    }
805}