umsh_node_mgmt/
fragment.rs

1//! Where a response frame can be cut, and which requests may be
2//! continued.
3//!
4//! A read whose answer does not fit one payload is carried across several
5//! exchanges by fragmenting the response frame's **trailing content** —
6//! the value of a `CMD_PROP_IS`, or the entry list of a `CMD_PROP_ARE`.
7//! Every fragment repeats the frame's leading bytes, so each is a
8//! well-formed frame on its own, and the administrator recovers the whole
9//! by concatenating the trailing parts in order.
10//!
11//! Both halves of the binding need the same answer to "where does the
12//! trailing content start", so it lives here rather than in either.
13
14use umsh_ulcp::frame::{Cmd, Frame};
15use umsh_ulcp::pui;
16
17use crate::device::{Dispatch, Produced};
18
19/// Whether a cursor may continue a request bearing this command.
20///
21/// Reads only. A write sequence cannot be continued — resuming one would
22/// mean deciding whether to apply its entries again — so a
23/// `CMD_PROP_MULTI_SET` whose reply does not fit stops instead, and the
24/// administrator reissues the remainder as a new exchange.
25pub const fn continuable(cmd: Cmd) -> bool {
26    matches!(cmd, Cmd::PropGet | Cmd::PropMultiGet)
27}
28
29/// Where `frame`'s trailing content begins, or `None` for a frame that
30/// has none and so cannot be fragmented.
31///
32/// A frame with no trailing content is one that either fits or does not:
33/// a status, an insert or remove acknowledgment. None of them approach a
34/// payload's size.
35pub fn trailing_offset(frame: &[u8]) -> Option<usize> {
36    let parsed = Frame::parse(frame).ok()?;
37    let offset = frame.len().checked_sub(parsed.payload.len())?;
38    match parsed.command()? {
39        // header, command, and the property key.
40        Cmd::PropIs => {
41            let (_, consumed) = pui::decode(parsed.payload).ok()?;
42            Some(offset + consumed)
43        }
44        // header and command; the entry list is the whole payload.
45        Cmd::PropAre => Some(offset),
46        _ => None,
47    }
48}
49
50/// `frame`'s trailing content, empty for a frame that has none.
51pub fn trailing(frame: &[u8]) -> &[u8] {
52    match trailing_offset(frame) {
53        Some(offset) => &frame[offset..],
54        None => &[],
55    }
56}
57
58/// Cut a whole reply down to the fragment one exchange carries.
59///
60/// `reply` is what the local dispatch produced, which for a continuable
61/// read may be larger than a payload holds. The result is the frame's
62/// leading bytes followed by the slice of its trailing content beginning
63/// at `dispatch.resume`, sized to `dispatch.budget` and to `buf`.
64///
65/// A reply the caller cannot cut — one with no trailing content — is
66/// copied through whole. If it does not fit, that is not this function's
67/// to hide: [`DeviceEngine::complete`](crate::device::DeviceEngine::complete)
68/// refuses it.
69pub fn produce<'b>(reply: &[u8], dispatch: &Dispatch<'_>, buf: &'b mut [u8]) -> Produced<'b> {
70    if reply.is_empty() {
71        return Produced::no_response();
72    }
73    let Some(offset) = trailing_offset(reply) else {
74        let len = reply.len().min(buf.len());
75        buf[..len].copy_from_slice(&reply[..len]);
76        return Produced::complete(&buf[..len]);
77    };
78    let (prefix, trailing) = reply.split_at(offset);
79    let prefix_len = prefix.len().min(buf.len());
80    // A cursor pointing past the end yields an empty last fragment, which
81    // ends the read rather than failing it.
82    let resume = (dispatch.resume as usize).min(trailing.len());
83    let available = trailing.len() - resume;
84    let room = dispatch
85        .budget
86        .saturating_sub(prefix_len)
87        .min(buf.len() - prefix_len);
88    let take = available.min(room);
89    let end = prefix_len + take;
90    buf[..prefix_len].copy_from_slice(&prefix[..prefix_len]);
91    buf[prefix_len..end].copy_from_slice(&trailing[resume..resume + take]);
92    Produced::fragment(&buf[..end], take as u32, (available - take) as u32)
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use umsh_ulcp::frame;
99    use umsh_ulcp::ids::prop;
100
101    /// A dispatch of the shape `produce` reads: only the cursor position
102    /// and the budget matter to it.
103    fn dispatch(resume: u32, budget: usize) -> Dispatch<'static> {
104        Dispatch {
105            frame: &[],
106            resume,
107            budget,
108            resets: false,
109        }
110    }
111
112    #[test]
113    fn a_reply_that_fits_is_carried_whole() {
114        let mut reply = [0u8; 64];
115        let len = frame::prop_is(&mut reply, 0, prop::DEV_PEERS, &[7; 32]).unwrap();
116        let mut buf = [0u8; 64];
117        let produced = produce(&reply[..len], &dispatch(0, 64), &mut buf);
118        assert_eq!(produced.frame, &reply[..len]);
119        assert_eq!(produced.remaining, 0);
120    }
121
122    #[test]
123    fn a_reply_that_does_not_fit_is_cut_at_the_budget() {
124        let mut reply = [0u8; 64];
125        let len = frame::prop_is(&mut reply, 0, prop::DEV_PEERS, &[7; 32]).unwrap();
126        let prefix = trailing_offset(&reply[..len]).unwrap();
127
128        let mut buf = [0u8; 64];
129        let first = produce(&reply[..len], &dispatch(0, prefix + 20), &mut buf);
130        assert_eq!(first.produced, 20);
131        assert_eq!(first.remaining, 12);
132        assert_eq!(&first.frame[..prefix], &reply[..prefix]);
133        assert_eq!(&first.frame[prefix..], &[7; 20]);
134
135        // The continuation resumes where the first left off, and this time
136        // the rest fits.
137        let mut buf = [0u8; 64];
138        let rest = produce(&reply[..len], &dispatch(20, prefix + 20), &mut buf);
139        assert_eq!(rest.produced, 12);
140        assert_eq!(rest.remaining, 0);
141        assert_eq!(&rest.frame[prefix..], &[7; 12]);
142    }
143
144    /// The buffer is a second ceiling, and the smaller of the two wins.
145    #[test]
146    fn the_buffer_bounds_the_cut_as_much_as_the_budget_does() {
147        let mut reply = [0u8; 64];
148        let len = frame::prop_is(&mut reply, 0, prop::DEV_PEERS, &[7; 32]).unwrap();
149        let prefix = trailing_offset(&reply[..len]).unwrap();
150        let mut buf = [0u8; 16];
151        let produced = produce(&reply[..len], &dispatch(0, 1024), &mut buf);
152        assert_eq!(produced.frame.len(), 16);
153        assert_eq!(produced.produced as usize, 16 - prefix);
154        assert_eq!(produced.remaining as usize, 32 - (16 - prefix));
155    }
156
157    /// A cursor at or past the end is answered by an empty last fragment,
158    /// which ends the read rather than failing it.
159    #[test]
160    fn a_cursor_past_the_end_ends_the_read() {
161        let mut reply = [0u8; 64];
162        let len = frame::prop_is(&mut reply, 0, prop::DEV_PEERS, &[7; 32]).unwrap();
163        let prefix = trailing_offset(&reply[..len]).unwrap();
164        let mut buf = [0u8; 64];
165        let produced = produce(&reply[..len], &dispatch(99, 64), &mut buf);
166        assert_eq!(produced.frame.len(), prefix);
167        assert_eq!(produced.produced, 0);
168        assert_eq!(produced.remaining, 0);
169    }
170
171    #[test]
172    fn an_empty_reply_is_a_reset() {
173        let mut buf = [0u8; 8];
174        assert_eq!(
175            produce(&[], &dispatch(0, 64), &mut buf),
176            Produced::no_response()
177        );
178    }
179
180    #[test]
181    fn a_prop_is_is_cut_after_its_key() {
182        let mut buf = [0u8; 64];
183        let len = frame::prop_is(&mut buf, 0, prop::DEV_PEERS, &[7; 32]).unwrap();
184        assert_eq!(trailing(&buf[..len]), &[7; 32]);
185    }
186
187    #[test]
188    fn a_prop_are_is_cut_after_its_command() {
189        // Header and command only; the rest is the entry list.
190        let entries = [0x03, 0x05, 0x01, 0x02];
191        let mut buf = [0u8; 16];
192        buf[0] = 0x80;
193        buf[1] = Cmd::PropAre as u8;
194        buf[2..2 + entries.len()].copy_from_slice(&entries);
195        assert_eq!(trailing(&buf[..2 + entries.len()]), &entries);
196    }
197
198    #[test]
199    fn everything_else_has_no_trailing_content() {
200        let mut buf = [0u8; 32];
201        let len = frame::last_status(&mut buf, 0, umsh_ulcp::status::Status::OK).unwrap();
202        // A status is a CMD_PROP_IS, so it does have trailing content —
203        // it is simply always short enough to fit.
204        assert!(!trailing(&buf[..len]).is_empty());
205
206        let len = frame::prop_inserted(&mut buf, 0, prop::DEV_PEERS, &[1, 2]).unwrap();
207        assert!(trailing(&buf[..len]).is_empty());
208        assert_eq!(trailing_offset(&buf[..len]), None);
209    }
210
211    #[test]
212    fn only_reads_may_be_continued() {
213        assert!(continuable(Cmd::PropGet));
214        assert!(continuable(Cmd::PropMultiGet));
215        assert!(!continuable(Cmd::PropMultiSet));
216        assert!(!continuable(Cmd::PropSet));
217        assert!(!continuable(Cmd::Save));
218    }
219}