Skip to main content

kipuka_coap/
block.rs

1//! CoAP block-wise transfer per RFC 7959.
2//!
3//! EST payloads frequently exceed the CoAP datagram size limit, especially
4//! with post-quantum certificates (ML-DSA-87 certificates can exceed 7KB).
5//! Block-wise transfer splits large payloads into numbered blocks that are
6//! individually acknowledged.
7//!
8//! # Block Options
9//!
10//! - **Block1** (option 27): Controls request payload transfer (client to server).
11//!   The client sends the request body in numbered chunks; the server acknowledges
12//!   each chunk before the next is sent.
13//!
14//! - **Block2** (option 23): Controls response payload transfer (server to client).
15//!   The server splits its response into numbered chunks; the client requests
16//!   successive chunks.
17//!
18//! # SZX Encoding
19//!
20//! Block sizes are encoded as `szx` where actual size = 2^(szx + 4):
21//! - szx=0 → 16 bytes
22//! - szx=1 → 32 bytes
23//! - szx=2 → 64 bytes
24//! - szx=3 → 128 bytes
25//! - szx=4 → 256 bytes
26//! - szx=5 → 512 bytes (default)
27//! - szx=6 → 1024 bytes (maximum)
28
29use crate::{CoapError, CoapResult};
30
31/// Default block size exponent (szx=5 → 512 bytes).
32///
33/// RFC 7959 §2.2: 512 bytes is a safe default that fits within most
34/// link-layer MTUs without IP fragmentation.
35pub const DEFAULT_SZX: u8 = 5;
36
37/// Maximum block size exponent (szx=6 → 1024 bytes).
38pub const MAX_SZX: u8 = 6;
39
40/// A decoded Block1 or Block2 option value.
41///
42/// RFC 7959 §2.2: The option value encodes three fields in a variable-length
43/// unsigned integer:
44/// - Bits 0-2: SZX (block size exponent)
45/// - Bit 3: M (more blocks follow)
46/// - Bits 4+: NUM (block number)
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct BlockOption {
49    /// Block number (0-based).
50    pub num: u32,
51    /// Whether more blocks follow this one.
52    pub more: bool,
53    /// Block size exponent: actual size = 2^(szx + 4).
54    pub szx: u8,
55}
56
57impl BlockOption {
58    /// Decodes a Block option from its wire representation.
59    ///
60    /// RFC 7959 §2.2: The option value is a variable-length unsigned integer
61    /// with the three least significant bits encoding SZX, bit 3 encoding M,
62    /// and the remaining upper bits encoding NUM.
63    pub fn decode(value: u32) -> Self {
64        let szx = (value & 0x07) as u8;
65        let more = (value & 0x08) != 0;
66        let num = value >> 4;
67        Self { num, more, szx }
68    }
69
70    /// Encodes this Block option to its wire representation.
71    ///
72    /// RFC 7959 §2.2: Packs NUM, M, and SZX into a single unsigned integer.
73    pub fn encode(&self) -> u32 {
74        let mut value = self.num << 4;
75        if self.more {
76            value |= 0x08;
77        }
78        value |= (self.szx as u32) & 0x07;
79        value
80    }
81
82    /// Returns the block size in bytes for this option's SZX value.
83    pub fn block_size(&self) -> usize {
84        block_size_from_szx(self.szx)
85    }
86
87    /// Returns the byte offset of this block within the full payload.
88    pub fn offset(&self) -> usize {
89        self.num as usize * self.block_size()
90    }
91}
92
93/// Converts an SZX exponent to the actual block size in bytes.
94///
95/// RFC 7959 §2.2: size = 2^(szx + 4). Values of szx above 6 are clamped
96/// to 6 (1024 bytes) since larger sizes are reserved.
97pub fn block_size_from_szx(szx: u8) -> usize {
98    let clamped = szx.min(MAX_SZX);
99    1 << (clamped as usize + 4)
100}
101
102/// Converts a block size in bytes to the corresponding SZX exponent.
103///
104/// Returns `None` if the size is not a valid power of 2 in the range
105/// 16..=1024 (szx 0..=6).
106pub fn szx_from_block_size(size: usize) -> Option<u8> {
107    match size {
108        16 => Some(0),
109        32 => Some(1),
110        64 => Some(2),
111        128 => Some(3),
112        256 => Some(4),
113        512 => Some(5),
114        1024 => Some(6),
115        _ => None,
116    }
117}
118
119/// Reassembles Block1 fragments into a complete request payload.
120///
121/// RFC 7959 §2.5: The server collects Block1 fragments from the client,
122/// validating sequential block numbers and consistent SZX values.
123/// Once the final block (M=0) arrives, the full payload is returned.
124#[derive(Debug)]
125pub struct BlockAssembler {
126    /// Accumulated payload bytes.
127    buffer: Vec<u8>,
128    /// Next expected block number.
129    next_num: u32,
130    /// Negotiated block size exponent.
131    szx: u8,
132    /// Maximum allowed reassembled payload size.
133    max_payload: usize,
134    /// Whether the final block has been received.
135    complete: bool,
136}
137
138impl BlockAssembler {
139    /// Creates a new assembler with default block size and the given payload limit.
140    ///
141    /// # Arguments
142    ///
143    /// * `max_payload` - Maximum total payload size after reassembly. Prevents
144    ///   resource exhaustion from unbounded block sequences.
145    pub fn new(max_payload: usize) -> Self {
146        Self {
147            buffer: Vec::new(),
148            next_num: 0,
149            szx: DEFAULT_SZX,
150            max_payload,
151            complete: false,
152        }
153    }
154
155    /// Processes an incoming Block1 fragment.
156    ///
157    /// Validates that blocks arrive in order and that the reassembled payload
158    /// does not exceed the configured maximum size.
159    ///
160    /// Returns `true` when the final block has been received and the full
161    /// payload is available via [`payload()`](Self::payload).
162    pub fn process_block(&mut self, block: &BlockOption, data: &[u8]) -> CoapResult<bool> {
163        if self.complete {
164            return Err(CoapError::BlockTransferError(
165                "Transfer already complete".to_string(),
166            ));
167        }
168
169        if block.num != self.next_num {
170            return Err(CoapError::BlockTransferError(format!(
171                "Expected block {}, received block {}",
172                self.next_num, block.num
173            )));
174        }
175
176        // On first block, adopt the client's SZX preference.
177        if block.num == 0 {
178            self.szx = block.szx;
179        }
180
181        let new_size = self.buffer.len() + data.len();
182        if new_size > self.max_payload {
183            return Err(CoapError::PayloadTooLarge {
184                size: new_size,
185                max: self.max_payload,
186            });
187        }
188
189        self.buffer.extend_from_slice(data);
190        self.next_num = block.num + 1;
191
192        if !block.more {
193            self.complete = true;
194        }
195
196        Ok(self.complete)
197    }
198
199    /// Returns the reassembled payload, or `None` if transfer is incomplete.
200    pub fn payload(&self) -> Option<&[u8]> {
201        if self.complete {
202            Some(&self.buffer)
203        } else {
204            None
205        }
206    }
207
208    /// Consumes the assembler and returns the reassembled payload.
209    ///
210    /// Returns `Err` if the transfer is not yet complete.
211    pub fn into_payload(self) -> CoapResult<Vec<u8>> {
212        if self.complete {
213            Ok(self.buffer)
214        } else {
215            Err(CoapError::BlockTransferError(format!(
216                "Transfer incomplete: received {} blocks, waiting for more",
217                self.next_num
218            )))
219        }
220    }
221
222    /// Returns the negotiated block size exponent.
223    pub fn szx(&self) -> u8 {
224        self.szx
225    }
226
227    /// Returns whether the final block has been received.
228    pub fn is_complete(&self) -> bool {
229        self.complete
230    }
231
232    /// Resets the assembler for reuse.
233    pub fn reset(&mut self) {
234        self.buffer.clear();
235        self.next_num = 0;
236        self.szx = DEFAULT_SZX;
237        self.complete = false;
238    }
239}
240
241/// Splits a response payload into Block2 chunks for incremental delivery.
242///
243/// RFC 7959 §2.4: The server sends the first block proactively, then the
244/// client requests subsequent blocks by including a Block2 option with the
245/// desired block number.
246#[derive(Debug)]
247pub struct BlockDisassembler {
248    /// Full response payload.
249    payload: Vec<u8>,
250    /// Block size exponent.
251    szx: u8,
252}
253
254impl BlockDisassembler {
255    /// Creates a new disassembler for the given payload and block size.
256    ///
257    /// # Arguments
258    ///
259    /// * `payload` - Complete response payload to split into blocks.
260    /// * `szx` - Block size exponent. Clamped to [`MAX_SZX`].
261    pub fn new(payload: Vec<u8>, szx: u8) -> Self {
262        Self {
263            payload,
264            szx: szx.min(MAX_SZX),
265        }
266    }
267
268    /// Returns the block data and corresponding Block2 option for the given
269    /// block number.
270    ///
271    /// Returns `None` if `block_num` is beyond the last block.
272    pub fn get_block(&self, block_num: u32) -> Option<(Vec<u8>, BlockOption)> {
273        let block_size = block_size_from_szx(self.szx);
274        let offset = block_num as usize * block_size;
275
276        if offset >= self.payload.len() {
277            return None;
278        }
279
280        let end = (offset + block_size).min(self.payload.len());
281        let data = self.payload[offset..end].to_vec();
282        let more = end < self.payload.len();
283
284        let option = BlockOption {
285            num: block_num,
286            more,
287            szx: self.szx,
288        };
289
290        Some((data, option))
291    }
292
293    /// Returns the total number of blocks required for this payload.
294    pub fn total_blocks(&self) -> u32 {
295        let block_size = block_size_from_szx(self.szx);
296        if self.payload.is_empty() {
297            return 1; // Empty payload still requires one (empty) block
298        }
299        ((self.payload.len() + block_size - 1) / block_size) as u32
300    }
301
302    /// Returns the full payload length in bytes.
303    pub fn payload_len(&self) -> usize {
304        self.payload.len()
305    }
306
307    /// Returns the block size exponent.
308    pub fn szx(&self) -> u8 {
309        self.szx
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn test_block_size_from_szx() {
319        assert_eq!(block_size_from_szx(0), 16);
320        assert_eq!(block_size_from_szx(1), 32);
321        assert_eq!(block_size_from_szx(2), 64);
322        assert_eq!(block_size_from_szx(3), 128);
323        assert_eq!(block_size_from_szx(4), 256);
324        assert_eq!(block_size_from_szx(5), 512);
325        assert_eq!(block_size_from_szx(6), 1024);
326    }
327
328    #[test]
329    fn test_block_size_from_szx_clamps() {
330        // Values above 6 should clamp to 1024.
331        assert_eq!(block_size_from_szx(7), 1024);
332        assert_eq!(block_size_from_szx(255), 1024);
333    }
334
335    #[test]
336    fn test_szx_from_block_size() {
337        assert_eq!(szx_from_block_size(16), Some(0));
338        assert_eq!(szx_from_block_size(32), Some(1));
339        assert_eq!(szx_from_block_size(64), Some(2));
340        assert_eq!(szx_from_block_size(128), Some(3));
341        assert_eq!(szx_from_block_size(256), Some(4));
342        assert_eq!(szx_from_block_size(512), Some(5));
343        assert_eq!(szx_from_block_size(1024), Some(6));
344    }
345
346    #[test]
347    fn test_szx_from_block_size_invalid() {
348        assert_eq!(szx_from_block_size(0), None);
349        assert_eq!(szx_from_block_size(8), None);
350        assert_eq!(szx_from_block_size(100), None);
351        assert_eq!(szx_from_block_size(2048), None);
352    }
353
354    #[test]
355    fn test_block_option_decode_encode_roundtrip() {
356        // Block 0, more=true, szx=5 (512 bytes)
357        let opt = BlockOption {
358            num: 0,
359            more: true,
360            szx: 5,
361        };
362        let encoded = opt.encode();
363        let decoded = BlockOption::decode(encoded);
364        assert_eq!(decoded, opt);
365
366        // Block 7, more=false, szx=3 (128 bytes)
367        let opt = BlockOption {
368            num: 7,
369            more: false,
370            szx: 3,
371        };
372        let encoded = opt.encode();
373        let decoded = BlockOption::decode(encoded);
374        assert_eq!(decoded, opt);
375
376        // Large block number
377        let opt = BlockOption {
378            num: 1000,
379            more: true,
380            szx: 6,
381        };
382        let encoded = opt.encode();
383        let decoded = BlockOption::decode(encoded);
384        assert_eq!(decoded, opt);
385    }
386
387    #[test]
388    fn test_block_option_offset() {
389        let opt = BlockOption {
390            num: 3,
391            more: true,
392            szx: 5,
393        };
394        // Block 3 at 512 bytes/block = offset 1536
395        assert_eq!(opt.offset(), 1536);
396    }
397
398    #[test]
399    fn test_block_option_block_size() {
400        let opt = BlockOption {
401            num: 0,
402            more: false,
403            szx: 4,
404        };
405        assert_eq!(opt.block_size(), 256);
406    }
407
408    #[test]
409    fn test_assembler_single_block() {
410        let mut assembler = BlockAssembler::new(4096);
411        let block = BlockOption {
412            num: 0,
413            more: false,
414            szx: 5,
415        };
416        let data = b"hello coap";
417
418        let complete = assembler.process_block(&block, data).unwrap();
419        assert!(complete);
420        assert!(assembler.is_complete());
421        assert_eq!(assembler.payload(), Some(data.as_slice()));
422    }
423
424    #[test]
425    fn test_assembler_multi_block() {
426        let mut assembler = BlockAssembler::new(4096);
427
428        let block0 = BlockOption {
429            num: 0,
430            more: true,
431            szx: 5,
432        };
433        let block1 = BlockOption {
434            num: 1,
435            more: true,
436            szx: 5,
437        };
438        let block2 = BlockOption {
439            num: 2,
440            more: false,
441            szx: 5,
442        };
443
444        assert!(!assembler.process_block(&block0, b"aaa").unwrap());
445        assert!(!assembler.process_block(&block1, b"bbb").unwrap());
446        assert!(assembler.process_block(&block2, b"ccc").unwrap());
447
448        assert_eq!(assembler.payload(), Some(b"aaabbbccc".as_slice()));
449    }
450
451    #[test]
452    fn test_assembler_out_of_order() {
453        let mut assembler = BlockAssembler::new(4096);
454
455        let block0 = BlockOption {
456            num: 0,
457            more: true,
458            szx: 5,
459        };
460        let block2 = BlockOption {
461            num: 2,
462            more: false,
463            szx: 5,
464        };
465
466        assembler.process_block(&block0, b"aaa").unwrap();
467        // Skip block 1 — should fail
468        let err = assembler.process_block(&block2, b"ccc").unwrap_err();
469        assert!(matches!(err, CoapError::BlockTransferError(_)));
470    }
471
472    #[test]
473    fn test_assembler_payload_too_large() {
474        let mut assembler = BlockAssembler::new(5);
475
476        let block = BlockOption {
477            num: 0,
478            more: false,
479            szx: 5,
480        };
481
482        let err = assembler.process_block(&block, b"too large").unwrap_err();
483        assert!(matches!(err, CoapError::PayloadTooLarge { .. }));
484    }
485
486    #[test]
487    fn test_assembler_into_payload_incomplete() {
488        let assembler = BlockAssembler::new(4096);
489        let err = assembler.into_payload().unwrap_err();
490        assert!(matches!(err, CoapError::BlockTransferError(_)));
491    }
492
493    #[test]
494    fn test_assembler_reset() {
495        let mut assembler = BlockAssembler::new(4096);
496        let block = BlockOption {
497            num: 0,
498            more: false,
499            szx: 5,
500        };
501        assembler.process_block(&block, b"data").unwrap();
502        assert!(assembler.is_complete());
503
504        assembler.reset();
505        assert!(!assembler.is_complete());
506        assert_eq!(assembler.payload(), None);
507    }
508
509    #[test]
510    fn test_disassembler_single_block() {
511        let payload = b"small".to_vec();
512        let disasm = BlockDisassembler::new(payload.clone(), DEFAULT_SZX);
513
514        assert_eq!(disasm.total_blocks(), 1);
515
516        let (data, opt) = disasm.get_block(0).unwrap();
517        assert_eq!(data, payload);
518        assert!(!opt.more);
519        assert_eq!(opt.num, 0);
520
521        assert!(disasm.get_block(1).is_none());
522    }
523
524    #[test]
525    fn test_disassembler_multi_block() {
526        // 100 bytes with szx=2 (64-byte blocks) = 2 blocks
527        let payload = vec![0xAB; 100];
528        let disasm = BlockDisassembler::new(payload, 2);
529
530        assert_eq!(disasm.total_blocks(), 2);
531
532        let (data0, opt0) = disasm.get_block(0).unwrap();
533        assert_eq!(data0.len(), 64);
534        assert!(opt0.more);
535        assert_eq!(opt0.num, 0);
536
537        let (data1, opt1) = disasm.get_block(1).unwrap();
538        assert_eq!(data1.len(), 36);
539        assert!(!opt1.more);
540        assert_eq!(opt1.num, 1);
541
542        assert!(disasm.get_block(2).is_none());
543    }
544
545    #[test]
546    fn test_disassembler_exact_boundary() {
547        // Exactly 128 bytes with szx=3 (128-byte blocks) = 1 block
548        let payload = vec![0xCD; 128];
549        let disasm = BlockDisassembler::new(payload, 3);
550
551        assert_eq!(disasm.total_blocks(), 1);
552
553        let (data, opt) = disasm.get_block(0).unwrap();
554        assert_eq!(data.len(), 128);
555        assert!(!opt.more);
556    }
557
558    #[test]
559    fn test_disassembler_empty_payload() {
560        let disasm = BlockDisassembler::new(Vec::new(), DEFAULT_SZX);
561        assert_eq!(disasm.total_blocks(), 1);
562    }
563
564    #[test]
565    fn test_assembler_disassembler_roundtrip() {
566        let original = vec![0x42; 2000]; // ~4 blocks at 512 bytes
567        let disasm = BlockDisassembler::new(original.clone(), DEFAULT_SZX);
568        let mut assembler = BlockAssembler::new(4096);
569
570        for i in 0..disasm.total_blocks() {
571            let (data, opt) = disasm.get_block(i).unwrap();
572            assembler.process_block(&opt, &data).unwrap();
573        }
574
575        assert!(assembler.is_complete());
576        let reassembled = assembler.into_payload().unwrap();
577        assert_eq!(reassembled, original);
578    }
579}