Skip to main content

compose_rust/
protocol.rs

1//! Fixed-layout little-endian boundary protocol.
2
3use crate::schema::{
4    AssetKind, ColorScheme, DesignSystem, Key, MaterialRole, MessageDuration, Modifier, MotionRole,
5    NotificationImportance, NotificationPermission, NotificationPresentation, Paint, PropertyKind,
6    Selection, ShapeRole, SpaceRole, TYPE_ROLE_COUNT, Theme, WidgetKind,
7};
8use core::fmt;
9
10const TAG_ENVELOPE: u16 = 0;
11const TAG_CREATE: u16 = 1;
12const TAG_SET_PROP: u16 = 2;
13const TAG_SET_MODIFIER: u16 = 3;
14const TAG_INSERT: u16 = 4;
15const TAG_MOVE: u16 = 5;
16const TAG_REMOVE: u16 = 6;
17const TAG_SET_TEXT: u16 = 7;
18const TAG_APPEND_TEXT: u16 = 8;
19const TAG_SET_THEME: u16 = 9;
20const TAG_REGISTER_ASSET: u16 = 10;
21const TAG_RELEASE_ASSET: u16 = 11;
22const TAG_SHOW_MESSAGE: u16 = 12;
23const TAG_SET_WINDOW: u16 = 13;
24const TAG_POST_NOTIFICATION: u16 = 14;
25const TAG_WITHDRAW_NOTIFICATION: u16 = 15;
26const TAG_REQUEST_NOTIFICATION_PERMISSION: u16 = 16;
27/// The last mutation tag this decoder knows. A known tag with the wrong length is a length
28/// error rather than an unknown tag.
29const TAG_LAST: u16 = TAG_REQUEST_NOTIFICATION_PERMISSION;
30const ENVELOPE_LEN: usize = 12;
31/// `SetTheme` after its header: four `u16` fields, one font slot per type role, and the
32/// palette's `(offset, len)` reference. 52 bytes, so the record is 56.
33pub const THEME_PAYLOAD_LEN: usize = 8 + 4 * TYPE_ROLE_COUNT + 8;
34/// The shortest mutation record on the wire (`RequestNotificationPermission`: the 4-byte
35/// header and nothing else).
36const MIN_RECORD_LEN: usize = 4;
37
38const VALUE_NONE: u16 = 0;
39const VALUE_STRING: u16 = 1;
40const VALUE_BOOL: u16 = 2;
41const VALUE_I64: u16 = 3;
42const VALUE_F32: u16 = 4;
43/// An opaque byte run in the arena. Same `(offset, length)` layout as a string, with no
44/// UTF-8 check, because a Canvas command list is not text.
45const VALUE_BYTES: u16 = 5;
46
47#[derive(Clone, Debug, PartialEq)]
48pub enum PropertyValue<'a> {
49    None,
50    String(&'a str),
51    Bool(bool),
52    Integer(i64),
53    Float(f32),
54    Bytes(&'a [u8]),
55}
56
57#[derive(Clone, Debug, PartialEq)]
58pub enum Mutation<'a> {
59    Create {
60        node_id: u32,
61        widget: WidgetKind,
62    },
63    SetProp {
64        node_id: u32,
65        property: PropertyKind,
66        value: PropertyValue<'a>,
67    },
68    SetModifier {
69        node_id: u32,
70        index: u16,
71        modifier: Modifier,
72    },
73    Insert {
74        parent_id: u32,
75        node_id: u32,
76        index: u32,
77    },
78    Move {
79        parent_id: u32,
80        node_id: u32,
81        index: u32,
82    },
83    Remove {
84        node_id: u32,
85    },
86    SetText {
87        node_id: u32,
88        text: &'a str,
89        selection: Option<Selection>,
90    },
91    /// Appends the streamed tail to a Text node instead of resending the whole string.
92    AppendText {
93        node_id: u32,
94        text: &'a str,
95    },
96    /// The root theme. Sent once as the first record of the initial batch.
97    SetTheme(Theme),
98    /// What the application asked of its own window.
99    ///
100    /// Written once per rebuild beside the theme, and read by the Renderer before it
101    /// stands the window up: `compose_rust_host_init` answers with the first batch, so
102    /// the setting is in hand before there is a window to apply it to. Platforms where
103    /// the window is not ours ignore it, which is not an error but an absence of anywhere
104    /// to put it.
105    SetWindow(crate::schema::Window),
106    /// Hands the Renderer the bytes of one asset. The Renderer copies them into its own
107    /// cache inside this call, because the batch buffer is only valid for the call that
108    /// carries it and an image has to outlive the frame that draws it.
109    RegisterAsset {
110        asset_id: u32,
111        kind: AssetKind,
112        bytes: &'a [u8],
113    },
114    /// Drops an asset from the Renderer's cache. The Host owns the lifetime.
115    ReleaseAsset {
116        asset_id: u32,
117    },
118    /// Says a sentence to the user once. It is not a node, because the Host would then
119    /// have to hold "showing until four seconds from now" and run a render to take it
120    /// away again. How long it stays, where it sits and what happens when a second one
121    /// arrives while the first is up are all the Renderer's.
122    ///
123    /// `handler_id` is what the action label reports when it is pressed, or 0 when the
124    /// message has no action, in which case `action` is empty too.
125    ShowMessage {
126        handler_id: u64,
127        text: &'a str,
128        action: &'a str,
129        duration: MessageDuration,
130    },
131    /// Asks the platform to show one notification outside the window.
132    ///
133    /// Not a node, for the same reason a message is not: it outlives the component that
134    /// posted it, and when it goes away depends on what the user does in the notification
135    /// centre, which the Host never hears about. Posting again under the same `key`
136    /// replaces it. An empty `key` asks the Renderer to name it, and such a notification
137    /// can be neither replaced nor withdrawn. An empty action label means no button.
138    PostNotification {
139        key: &'a str,
140        title: &'a str,
141        body: &'a str,
142        channel: &'a str,
143        action_1: &'a str,
144        action_2: &'a str,
145        importance: NotificationImportance,
146        presentation: NotificationPresentation,
147    },
148    /// Takes back the notification posted under `key`, if it is still showing.
149    WithdrawNotification {
150        key: &'a str,
151    },
152    /// Asks the platform for permission to show notifications. It carries nothing: the
153    /// answer comes back as a permission event.
154    RequestNotificationPermission,
155}
156
157#[derive(Clone, Debug, Eq, PartialEq)]
158pub enum ProtocolError {
159    Truncated,
160    InvalidEnvelope,
161    InvalidRecordLength,
162    InvalidTag(u16),
163    InvalidWidget(u16),
164    InvalidProperty(u16),
165    InvalidValueKind(u16),
166    InvalidModifier(u16),
167    InvalidTheme(u16),
168    InvalidAssetKind(u16),
169    InvalidMessageDuration(u16),
170    /// A notification importance or presentation this decoder does not know.
171    InvalidNotification(u16),
172    InvalidStringRange,
173    InvalidUtf8,
174    LengthOverflow,
175}
176
177#[derive(Clone, Debug, PartialEq)]
178pub struct HostEvent<'a> {
179    pub node_id: u32,
180    pub handler_id: u64,
181    pub payload: crate::schema::EventPayload<'a>,
182}
183
184const EVENT_CLICK: u16 = 1;
185const EVENT_TEXT_CHANGED: u16 = 2;
186const EVENT_TEXT_SUBMITTED: u16 = 3;
187const EVENT_FOCUS_LOST: u16 = 4;
188const EVENT_PROTOCOL_ERROR: u16 = 5;
189const EVENT_KEY_DOWN: u16 = 6;
190const EVENT_RANGE_REQUESTED: u16 = 7;
191// Tags 8 to 15 are reserved for the pointer gesture events, so this one starts at 16
192// and the window size report continues from 17.
193const EVENT_VALUE_CHANGED: u16 = 16;
194const EVENT_WINDOW_SIZE_CHANGED: u16 = 17;
195const EVENT_RESYNC: u16 = 18;
196const EVENT_LIFECYCLE_START: u16 = 19;
197const EVENT_LIFECYCLE_STOP: u16 = 20;
198const EVENT_DESIGN_SYSTEM_RESOLVED: u16 = 21;
199const EVENT_FILES_ENTERED: u16 = 22;
200const EVENT_FILES_DROPPED: u16 = 23;
201const EVENT_NOTIFICATION_ACTIVATED: u16 = 24;
202const EVENT_NOTIFICATION_PERMISSION_CHANGED: u16 = 25;
203
204const MODIFIER_SHIFT: u8 = 1 << 0;
205const MODIFIER_CTRL: u8 = 1 << 1;
206const MODIFIER_ALT: u8 = 1 << 2;
207const MODIFIER_META: u8 = 1 << 3;
208
209/// Decode one Renderer-to-Host event record.
210pub fn decode_event(bytes: &[u8]) -> Result<HostEvent<'_>, ProtocolError> {
211    if bytes.len() < 16 {
212        return Err(ProtocolError::Truncated);
213    }
214    let tag = read_u16(bytes, 0)?;
215    let record_len = usize::from(read_u16(bytes, 2)?);
216    if record_len < 16 || record_len % 4 != 0 || record_len > bytes.len() {
217        return Err(ProtocolError::InvalidRecordLength);
218    }
219    let node_id = read_u32(bytes, 4)?;
220    let handler_id = read_u64(bytes, 8)?;
221    let payload = match tag {
222        EVENT_CLICK if record_len == 16 => crate::schema::EventPayload::Clicked,
223        EVENT_TEXT_CHANGED if record_len == 24 => {
224            crate::schema::EventPayload::TextChanged(read_string(bytes, 16, record_len)?)
225        }
226        EVENT_TEXT_SUBMITTED if record_len == 24 => {
227            crate::schema::EventPayload::TextSubmitted(read_string(bytes, 16, record_len)?)
228        }
229        EVENT_FOCUS_LOST if record_len == 16 => crate::schema::EventPayload::FocusLost,
230        EVENT_PROTOCOL_ERROR if record_len == 28 => crate::schema::EventPayload::ProtocolError {
231            code: read_u32(bytes, 16)?,
232            message: read_string(bytes, 20, record_len)?,
233        },
234        EVENT_KEY_DOWN if record_len == 20 => {
235            let raw_key = read_u16(bytes, 16)?;
236            let modifiers = *bytes.get(18).ok_or(ProtocolError::Truncated)?;
237            crate::schema::EventPayload::KeyDown {
238                key: Key::try_from(raw_key)
239                    .map_err(|()| ProtocolError::InvalidValueKind(raw_key))?,
240                shift_key: modifiers & MODIFIER_SHIFT != 0,
241                ctrl_key: modifiers & MODIFIER_CTRL != 0,
242                alt_key: modifiers & MODIFIER_ALT != 0,
243                meta_key: modifiers & MODIFIER_META != 0,
244            }
245        }
246        EVENT_RANGE_REQUESTED if record_len == 24 => crate::schema::EventPayload::RangeRequested {
247            start: read_u32(bytes, 16)?,
248            count: read_u32(bytes, 20)?,
249        },
250        EVENT_VALUE_CHANGED if record_len == 24 => {
251            crate::schema::EventPayload::ValueChanged(f64::from_bits(read_u64(bytes, 16)?))
252        }
253        EVENT_WINDOW_SIZE_CHANGED if record_len == 32 => {
254            let raw_class = read_u32(bytes, 24)?;
255            let class = u16::try_from(raw_class)
256                .ok()
257                .and_then(|value| crate::schema::WindowSizeClass::try_from(value).ok())
258                .ok_or(ProtocolError::InvalidValueKind(raw_class as u16))?;
259            let raw_height_class = read_u32(bytes, 28)?;
260            let height_class = u16::try_from(raw_height_class)
261                .ok()
262                .and_then(|value| crate::schema::WindowHeightClass::try_from(value).ok())
263                .ok_or(ProtocolError::InvalidValueKind(raw_height_class as u16))?;
264            crate::schema::EventPayload::WindowSizeChanged {
265                width_dp: f32::from_bits(read_u32(bytes, 16)?),
266                height_dp: f32::from_bits(read_u32(bytes, 20)?),
267                class,
268                height_class,
269            }
270        }
271        EVENT_DESIGN_SYSTEM_RESOLVED if record_len == 20 => {
272            let raw = read_u32(bytes, 16)?;
273            let system = u16::try_from(raw)
274                .ok()
275                .and_then(|tag| crate::schema::DesignSystem::try_from(tag).ok())
276                .ok_or(ProtocolError::InvalidValueKind(raw as u16))?;
277            crate::schema::EventPayload::DesignSystemResolved(system)
278        }
279        EVENT_FILES_ENTERED if record_len == 16 => crate::schema::EventPayload::FilesEntered,
280        EVENT_FILES_DROPPED if record_len == 24 => {
281            crate::schema::EventPayload::FilesDropped(read_string(bytes, 16, record_len)?)
282        }
283        EVENT_NOTIFICATION_ACTIVATED if record_len == 28 => {
284            let action = read_u32(bytes, 16)?;
285            // Zero is the body and one and two are the two buttons a notification can have.
286            // Anything else names a button that cannot exist.
287            if action > 2 {
288                return Err(ProtocolError::InvalidValueKind(
289                    action.min(u32::from(u16::MAX)) as u16,
290                ));
291            }
292            crate::schema::EventPayload::NotificationActivated {
293                action,
294                key: read_string(bytes, 20, record_len)?,
295            }
296        }
297        EVENT_NOTIFICATION_PERMISSION_CHANGED if record_len == 20 => {
298            let raw = read_u32(bytes, 16)?;
299            let state = u16::try_from(raw)
300                .ok()
301                .and_then(|tag| NotificationPermission::try_from(tag).ok())
302                .ok_or(ProtocolError::InvalidValueKind(
303                    raw.min(u32::from(u16::MAX)) as u16,
304                ))?;
305            crate::schema::EventPayload::NotificationPermissionChanged(state)
306        }
307        EVENT_RESYNC if record_len == 16 => crate::schema::EventPayload::Resync,
308        EVENT_LIFECYCLE_START if record_len == 16 => crate::schema::EventPayload::LifecycleStart,
309        EVENT_LIFECYCLE_STOP if record_len == 16 => crate::schema::EventPayload::LifecycleStop,
310        EVENT_CLICK..=EVENT_RANGE_REQUESTED
311        | EVENT_VALUE_CHANGED..=EVENT_NOTIFICATION_PERMISSION_CHANGED => {
312            return Err(ProtocolError::InvalidRecordLength);
313        }
314        other => return Err(ProtocolError::InvalidTag(other)),
315    };
316    Ok(HostEvent {
317        node_id,
318        handler_id,
319        payload,
320    })
321}
322
323/// Test/mock helper for constructing a Renderer-to-Host event.
324pub fn encode_event(event: &HostEvent<'_>, output: &mut Vec<u8>) -> Result<(), ProtocolError> {
325    output.clear();
326    if let crate::schema::EventPayload::KeyDown {
327        key,
328        shift_key,
329        ctrl_key,
330        alt_key,
331        meta_key,
332    } = event.payload
333    {
334        output.extend_from_slice(&EVENT_KEY_DOWN.to_le_bytes());
335        output.extend_from_slice(&20_u16.to_le_bytes());
336        output.extend_from_slice(&event.node_id.to_le_bytes());
337        output.extend_from_slice(&event.handler_id.to_le_bytes());
338        output.extend_from_slice(&(key as u16).to_le_bytes());
339        let modifiers = (u8::from(shift_key) * MODIFIER_SHIFT)
340            | (u8::from(ctrl_key) * MODIFIER_CTRL)
341            | (u8::from(alt_key) * MODIFIER_ALT)
342            | (u8::from(meta_key) * MODIFIER_META);
343        output.push(modifiers);
344        output.push(0);
345        return Ok(());
346    }
347    if let crate::schema::EventPayload::ValueChanged(value) = event.payload {
348        output.extend_from_slice(&EVENT_VALUE_CHANGED.to_le_bytes());
349        output.extend_from_slice(&24_u16.to_le_bytes());
350        output.extend_from_slice(&event.node_id.to_le_bytes());
351        output.extend_from_slice(&event.handler_id.to_le_bytes());
352        output.extend_from_slice(&value.to_bits().to_le_bytes());
353        return Ok(());
354    }
355    if let crate::schema::EventPayload::WindowSizeChanged {
356        width_dp,
357        height_dp,
358        class,
359        height_class,
360    } = event.payload
361    {
362        output.extend_from_slice(&EVENT_WINDOW_SIZE_CHANGED.to_le_bytes());
363        output.extend_from_slice(&32_u16.to_le_bytes());
364        output.extend_from_slice(&event.node_id.to_le_bytes());
365        output.extend_from_slice(&event.handler_id.to_le_bytes());
366        output.extend_from_slice(&width_dp.to_bits().to_le_bytes());
367        output.extend_from_slice(&height_dp.to_bits().to_le_bytes());
368        output.extend_from_slice(&u32::from(u16::from(class)).to_le_bytes());
369        output.extend_from_slice(&u32::from(u16::from(height_class)).to_le_bytes());
370        return Ok(());
371    }
372    if let crate::schema::EventPayload::DesignSystemResolved(system) = event.payload {
373        output.extend_from_slice(&EVENT_DESIGN_SYSTEM_RESOLVED.to_le_bytes());
374        output.extend_from_slice(&20_u16.to_le_bytes());
375        output.extend_from_slice(&event.node_id.to_le_bytes());
376        output.extend_from_slice(&event.handler_id.to_le_bytes());
377        // The tag in a word, which keeps the record a multiple of four the way every
378        // other record here is.
379        output.extend_from_slice(&u32::from(u16::from(system)).to_le_bytes());
380        return Ok(());
381    }
382    if let crate::schema::EventPayload::NotificationPermissionChanged(state) = event.payload {
383        output.extend_from_slice(&EVENT_NOTIFICATION_PERMISSION_CHANGED.to_le_bytes());
384        output.extend_from_slice(&20_u16.to_le_bytes());
385        output.extend_from_slice(&event.node_id.to_le_bytes());
386        output.extend_from_slice(&event.handler_id.to_le_bytes());
387        output.extend_from_slice(&u32::from(u16::from(state)).to_le_bytes());
388        return Ok(());
389    }
390    if let crate::schema::EventPayload::RangeRequested { start, count } = event.payload {
391        output.extend_from_slice(&EVENT_RANGE_REQUESTED.to_le_bytes());
392        output.extend_from_slice(&24_u16.to_le_bytes());
393        output.extend_from_slice(&event.node_id.to_le_bytes());
394        output.extend_from_slice(&event.handler_id.to_le_bytes());
395        output.extend_from_slice(&start.to_le_bytes());
396        output.extend_from_slice(&count.to_le_bytes());
397        return Ok(());
398    }
399    let (tag, record_len, text, error_code) = match &event.payload {
400        crate::schema::EventPayload::Clicked => (EVENT_CLICK, 16_u16, None, None),
401        crate::schema::EventPayload::TextChanged(value) => {
402            (EVENT_TEXT_CHANGED, 24, Some(*value), None)
403        }
404        crate::schema::EventPayload::TextSubmitted(value) => {
405            (EVENT_TEXT_SUBMITTED, 24, Some(*value), None)
406        }
407        crate::schema::EventPayload::FocusLost => (EVENT_FOCUS_LOST, 16, None, None),
408        crate::schema::EventPayload::FilesEntered => (EVENT_FILES_ENTERED, 16, None, None),
409        crate::schema::EventPayload::FilesDropped(paths) => {
410            (EVENT_FILES_DROPPED, 24, Some(*paths), None)
411        }
412        crate::schema::EventPayload::Resync => (EVENT_RESYNC, 16, None, None),
413        crate::schema::EventPayload::LifecycleStart => (EVENT_LIFECYCLE_START, 16, None, None),
414        crate::schema::EventPayload::LifecycleStop => (EVENT_LIFECYCLE_STOP, 16, None, None),
415        crate::schema::EventPayload::ProtocolError { code, message } => {
416            (EVENT_PROTOCOL_ERROR, 28, Some(*message), Some(*code))
417        }
418        // The same shape as a protocol error: one word, then one string.
419        crate::schema::EventPayload::NotificationActivated { action, key } => {
420            (EVENT_NOTIFICATION_ACTIVATED, 28, Some(*key), Some(*action))
421        }
422        crate::schema::EventPayload::KeyDown { .. }
423        | crate::schema::EventPayload::RangeRequested { .. }
424        | crate::schema::EventPayload::ValueChanged(_)
425        | crate::schema::EventPayload::WindowSizeChanged { .. }
426        | crate::schema::EventPayload::DesignSystemResolved(_)
427        | crate::schema::EventPayload::NotificationPermissionChanged(_) => unreachable!(),
428    };
429    output.extend_from_slice(&tag.to_le_bytes());
430    output.extend_from_slice(&record_len.to_le_bytes());
431    output.extend_from_slice(&event.node_id.to_le_bytes());
432    output.extend_from_slice(&event.handler_id.to_le_bytes());
433    if let Some(code) = error_code {
434        output.extend_from_slice(&code.to_le_bytes());
435    }
436    if let Some(text) = text {
437        let offset = u32::from(record_len);
438        let len = u32::try_from(text.len()).map_err(|_| ProtocolError::LengthOverflow)?;
439        output.extend_from_slice(&offset.to_le_bytes());
440        output.extend_from_slice(&len.to_le_bytes());
441        output.extend_from_slice(text.as_bytes());
442    }
443    Ok(())
444}
445
446impl fmt::Display for ProtocolError {
447    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
448        write!(formatter, "protocol error: {self:?}")
449    }
450}
451
452impl std::error::Error for ProtocolError {}
453
454/// Reusable encoder. Once capacities are warmed, `clear` and `finish` allocate no memory.
455pub struct BatchEncoder {
456    records: Vec<u8>,
457    strings: Vec<u8>,
458    string_fixups: Vec<usize>,
459    arena: Vec<u8>,
460    record_count: u32,
461}
462
463impl Default for BatchEncoder {
464    fn default() -> Self {
465        Self::with_capacity(4096, 1024, 64)
466    }
467}
468
469impl BatchEncoder {
470    pub fn with_capacity(record_bytes: usize, string_bytes: usize, string_count: usize) -> Self {
471        let mut encoder = Self {
472            records: Vec::with_capacity(record_bytes),
473            strings: Vec::with_capacity(string_bytes),
474            string_fixups: Vec::with_capacity(string_count),
475            arena: Vec::with_capacity(record_bytes + string_bytes),
476            record_count: 0,
477        };
478        encoder.clear();
479        encoder
480    }
481
482    pub fn clear(&mut self) {
483        self.records.clear();
484        self.strings.clear();
485        self.string_fixups.clear();
486        self.arena.clear();
487        self.record_count = 0;
488        self.begin_record(TAG_ENVELOPE, 8);
489        self.put_u32(0);
490        self.put_u32(0);
491    }
492
493    pub fn encode(&mut self, mutation: &Mutation<'_>) -> Result<(), ProtocolError> {
494        match mutation {
495            Mutation::Create { node_id, widget } => {
496                self.begin_record(TAG_CREATE, 8);
497                self.put_u32(*node_id);
498                self.put_u16(*widget as u16);
499                self.put_u16(0);
500            }
501            Mutation::SetProp {
502                node_id,
503                property,
504                value,
505            } => {
506                self.begin_record(TAG_SET_PROP, 20);
507                self.put_u32(*node_id);
508                self.put_u16(*property as u16);
509                match value {
510                    PropertyValue::None => {
511                        self.put_u16(VALUE_NONE);
512                        self.put_u64(0);
513                        self.put_u32(0);
514                    }
515                    PropertyValue::String(value) => {
516                        self.put_u16(VALUE_STRING);
517                        self.put_string_ref(value)?;
518                        self.put_u32(0);
519                    }
520                    PropertyValue::Bool(value) => {
521                        self.put_u16(VALUE_BOOL);
522                        self.put_u64(u64::from(*value));
523                        self.put_u32(0);
524                    }
525                    PropertyValue::Integer(value) => {
526                        self.put_u16(VALUE_I64);
527                        self.put_u64(*value as u64);
528                        self.put_u32(0);
529                    }
530                    PropertyValue::Float(value) => {
531                        self.put_u16(VALUE_F32);
532                        self.put_u64(u64::from(value.to_bits()));
533                        self.put_u32(0);
534                    }
535                    PropertyValue::Bytes(value) => {
536                        self.put_u16(VALUE_BYTES);
537                        self.put_bytes_ref(value)?;
538                        self.put_u32(0);
539                    }
540                }
541            }
542            Mutation::SetModifier {
543                node_id,
544                index,
545                modifier,
546            } => {
547                self.begin_record(TAG_SET_MODIFIER, 24);
548                self.put_u32(*node_id);
549                self.put_u16(*index);
550                let (tag, first, second) = modifier_fields(modifier);
551                self.put_u16(tag);
552                self.put_u64(first);
553                self.put_u64(second);
554            }
555            Mutation::Insert {
556                parent_id,
557                node_id,
558                index,
559            } => {
560                self.begin_record(TAG_INSERT, 12);
561                self.put_u32(*parent_id);
562                self.put_u32(*node_id);
563                self.put_u32(*index);
564            }
565            Mutation::Move {
566                parent_id,
567                node_id,
568                index,
569            } => {
570                self.begin_record(TAG_MOVE, 12);
571                self.put_u32(*parent_id);
572                self.put_u32(*node_id);
573                self.put_u32(*index);
574            }
575            Mutation::Remove { node_id } => {
576                self.begin_record(TAG_REMOVE, 4);
577                self.put_u32(*node_id);
578            }
579            Mutation::SetText {
580                node_id,
581                text,
582                selection,
583            } => {
584                self.begin_record(TAG_SET_TEXT, 20);
585                self.put_u32(*node_id);
586                self.put_string_ref(text)?;
587                let (start, end) =
588                    selection.map_or((u32::MAX, u32::MAX), |value| (value.start, value.end));
589                self.put_u32(start);
590                self.put_u32(end);
591            }
592            Mutation::AppendText { node_id, text } => {
593                self.begin_record(TAG_APPEND_TEXT, 12);
594                self.put_u32(*node_id);
595                self.put_string_ref(text)?;
596            }
597            Mutation::RegisterAsset {
598                asset_id,
599                kind,
600                bytes,
601            } => {
602                self.begin_record(TAG_REGISTER_ASSET, 16);
603                self.put_u32(*asset_id);
604                self.put_u16(*kind as u16);
605                self.put_u16(0);
606                self.put_bytes_ref(bytes)?;
607            }
608            Mutation::ReleaseAsset { asset_id } => {
609                self.begin_record(TAG_RELEASE_ASSET, 4);
610                self.put_u32(*asset_id);
611            }
612            Mutation::ShowMessage {
613                handler_id,
614                text,
615                action,
616                duration,
617            } => {
618                self.begin_record(TAG_SHOW_MESSAGE, 28);
619                self.put_u64(*handler_id);
620                self.put_string_ref(text)?;
621                self.put_string_ref(action)?;
622                self.put_u16(*duration as u16);
623                self.put_u16(0);
624            }
625            Mutation::SetTheme(theme) => {
626                self.begin_record(TAG_SET_THEME, THEME_PAYLOAD_LEN as u16);
627                self.put_u16(theme.design_system as u16);
628                self.put_u16(theme.fallback as u16);
629                self.put_u16(theme.color_scheme as u16);
630                self.put_u16(u16::from(theme.adaptive));
631                // One slot per type role, zero where the role keeps the system font. Sent
632                // whether or not any of them is filled, because the record is fixed
633                // layout and a theme change is one record rather than a per frame cost.
634                for asset in theme.fonts {
635                    self.put_u32(asset);
636                }
637                // The application's colours, as a run of eight byte entries behind the
638                // records the way a string is. Zero long where there is no palette, so a
639                // theme without one costs the eight bytes of the reference and nothing
640                // else.
641                self.put_palette_ref(theme.palette)?;
642            }
643            Mutation::SetWindow(window) => {
644                self.begin_record(TAG_SET_WINDOW, 28);
645                self.put_u16(window.chrome as u16);
646                self.put_u16(window.title_bar as u16);
647                self.put_u16(window.width);
648                self.put_u16(window.height);
649                self.put_u16(window.min_width);
650                self.put_u16(window.min_height);
651                self.put_u16(u16::from(window.resizable));
652                // The padding that keeps the record a multiple of four, which the envelope
653                // requires. Seven of these words is an odd number of them.
654                self.put_u16(0);
655                self.put_string_ref(window.title)?;
656                self.put_u32(window.icon);
657            }
658            Mutation::PostNotification {
659                key,
660                title,
661                body,
662                channel,
663                action_1,
664                action_2,
665                importance,
666                presentation,
667            } => {
668                self.begin_record(TAG_POST_NOTIFICATION, 52);
669                self.put_string_ref(key)?;
670                self.put_string_ref(title)?;
671                self.put_string_ref(body)?;
672                self.put_string_ref(channel)?;
673                self.put_string_ref(action_1)?;
674                self.put_string_ref(action_2)?;
675                self.put_u16(*importance as u16);
676                self.put_u16(*presentation as u16);
677            }
678            Mutation::WithdrawNotification { key } => {
679                self.begin_record(TAG_WITHDRAW_NOTIFICATION, 8);
680                self.put_string_ref(key)?;
681            }
682            Mutation::RequestNotificationPermission => {
683                self.begin_record(TAG_REQUEST_NOTIFICATION_PERMISSION, 0);
684            }
685        }
686        self.record_count = self
687            .record_count
688            .checked_add(1)
689            .ok_or(ProtocolError::LengthOverflow)?;
690        Ok(())
691    }
692
693    pub fn finish(&mut self) -> Result<&[u8], ProtocolError> {
694        let records_len =
695            u32::try_from(self.records.len()).map_err(|_| ProtocolError::LengthOverflow)?;
696        self.records[4..8].copy_from_slice(&records_len.to_le_bytes());
697        self.records[8..12].copy_from_slice(&self.record_count.to_le_bytes());
698        let total = self
699            .records
700            .len()
701            .checked_add(self.strings.len())
702            .ok_or(ProtocolError::LengthOverflow)?;
703        self.arena.clear();
704        if self.arena.capacity() < total {
705            self.arena.reserve(total);
706        }
707        self.arena.extend_from_slice(&self.records);
708        self.arena.extend_from_slice(&self.strings);
709        for &position in &self.string_fixups {
710            let relative = read_u32(&self.arena, position)?;
711            let absolute = records_len
712                .checked_add(relative)
713                .ok_or(ProtocolError::LengthOverflow)?;
714            self.arena[position..position + 4].copy_from_slice(&absolute.to_le_bytes());
715        }
716        Ok(&self.arena)
717    }
718
719    /// Where the arena a finished batch lives in starts, and how much of it is mapped.
720    ///
721    /// A runtime that cannot read the Host's memory directly wraps this range once and
722    /// keeps the view: every batch is a prefix of the arena, so it is read where it lies
723    /// and no byte is copied out. Growing the arena moves it, which is why the address is
724    /// reported again on every call rather than once at startup.
725    pub fn arena(&self) -> (*const u8, usize) {
726        (self.arena.as_ptr(), self.arena.capacity())
727    }
728
729    pub fn capacity(&self) -> usize {
730        self.records.capacity() + self.strings.capacity() + self.arena.capacity()
731    }
732
733    fn begin_record(&mut self, tag: u16, payload_len: u16) {
734        self.put_u16(tag);
735        self.put_u16(payload_len + 4);
736    }
737
738    /// Writes a palette's entries straight into the trailing region, so encoding a theme
739    /// allocates nothing beyond what that region already has.
740    fn put_palette_ref(
741        &mut self,
742        palette: Option<&crate::palette::Palette>,
743    ) -> Result<(), ProtocolError> {
744        let start = self.strings.len();
745        let offset = u32::try_from(start).map_err(|_| ProtocolError::LengthOverflow)?;
746        if let Some(palette) = palette {
747            palette.write_into(&mut self.strings);
748        }
749        let len =
750            u32::try_from(self.strings.len() - start).map_err(|_| ProtocolError::LengthOverflow)?;
751        self.string_fixups.push(self.records.len());
752        self.put_u32(offset);
753        self.put_u32(len);
754        Ok(())
755    }
756
757    fn put_string_ref(&mut self, value: &str) -> Result<(), ProtocolError> {
758        self.put_bytes_ref(value.as_bytes())
759    }
760
761    /// Asset bytes ride in the same trailing region as strings, with the same
762    /// `(offset, len)` reference and the same fixup, so the record stays fixed layout.
763    fn put_bytes_ref(&mut self, value: &[u8]) -> Result<(), ProtocolError> {
764        let offset =
765            u32::try_from(self.strings.len()).map_err(|_| ProtocolError::LengthOverflow)?;
766        let len = u32::try_from(value.len()).map_err(|_| ProtocolError::LengthOverflow)?;
767        self.string_fixups.push(self.records.len());
768        self.put_u32(offset);
769        self.put_u32(len);
770        self.strings.extend_from_slice(value);
771        Ok(())
772    }
773
774    fn put_u16(&mut self, value: u16) {
775        self.records.extend_from_slice(&value.to_le_bytes());
776    }
777
778    fn put_u32(&mut self, value: u32) {
779        self.records.extend_from_slice(&value.to_le_bytes());
780    }
781
782    fn put_u64(&mut self, value: u64) {
783        self.records.extend_from_slice(&value.to_le_bytes());
784    }
785}
786
787pub fn decode_batch(bytes: &[u8]) -> Result<Vec<Mutation<'_>>, ProtocolError> {
788    if bytes.len() < ENVELOPE_LEN
789        || read_u16(bytes, 0)? != TAG_ENVELOPE
790        || read_u16(bytes, 2)? != 12
791    {
792        return Err(ProtocolError::InvalidEnvelope);
793    }
794    let records_len =
795        usize::try_from(read_u32(bytes, 4)?).map_err(|_| ProtocolError::LengthOverflow)?;
796    let count = usize::try_from(read_u32(bytes, 8)?).map_err(|_| ProtocolError::LengthOverflow)?;
797    if records_len < ENVELOPE_LEN || records_len > bytes.len() || records_len % 4 != 0 {
798        return Err(ProtocolError::InvalidEnvelope);
799    }
800    // `count` is attacker-controlled, so it only sizes the buffer up to what the
801    // record region could actually hold. The shortest mutation record is a bare header at
802    // 4 bytes, so that is the ceiling. Reserving `count` directly let a 12-byte message ask
803    // for a >100 GB allocation.
804    let capacity = count.min((records_len - ENVELOPE_LEN) / MIN_RECORD_LEN);
805    let mut output = Vec::with_capacity(capacity);
806    let mut position = ENVELOPE_LEN;
807    while position < records_len {
808        let tag = read_u16(bytes, position)?;
809        let len = usize::from(read_u16(bytes, position + 2)?);
810        if len < 4 || len % 4 != 0 || position + len > records_len {
811            return Err(ProtocolError::InvalidRecordLength);
812        }
813        let payload = position + 4;
814        let mutation = match tag {
815            TAG_CREATE if len == 12 => Mutation::Create {
816                node_id: read_u32(bytes, payload)?,
817                widget: WidgetKind::try_from(read_u16(bytes, payload + 4)?).map_err(|()| {
818                    ProtocolError::InvalidWidget(read_u16(bytes, payload + 4).unwrap_or(0))
819                })?,
820            },
821            TAG_SET_PROP if len == 24 => {
822                let property_raw = read_u16(bytes, payload + 4)?;
823                let value_kind = read_u16(bytes, payload + 6)?;
824                let value = match value_kind {
825                    VALUE_NONE => PropertyValue::None,
826                    VALUE_STRING => {
827                        PropertyValue::String(read_string(bytes, payload + 8, records_len)?)
828                    }
829                    VALUE_BOOL => PropertyValue::Bool(read_u64(bytes, payload + 8)? != 0),
830                    VALUE_I64 => PropertyValue::Integer(read_u64(bytes, payload + 8)? as i64),
831                    VALUE_F32 => {
832                        PropertyValue::Float(f32::from_bits(read_u64(bytes, payload + 8)? as u32))
833                    }
834                    VALUE_BYTES => {
835                        PropertyValue::Bytes(read_bytes(bytes, payload + 8, records_len)?)
836                    }
837                    other => return Err(ProtocolError::InvalidValueKind(other)),
838                };
839                Mutation::SetProp {
840                    node_id: read_u32(bytes, payload)?,
841                    property: PropertyKind::try_from(property_raw)
842                        .map_err(|()| ProtocolError::InvalidProperty(property_raw))?,
843                    value,
844                }
845            }
846            TAG_SET_MODIFIER if len == 28 => Mutation::SetModifier {
847                node_id: read_u32(bytes, payload)?,
848                index: read_u16(bytes, payload + 4)?,
849                modifier: decode_modifier(
850                    read_u16(bytes, payload + 6)?,
851                    read_u64(bytes, payload + 8)?,
852                    read_u64(bytes, payload + 16)?,
853                )?,
854            },
855            TAG_INSERT if len == 16 => Mutation::Insert {
856                parent_id: read_u32(bytes, payload)?,
857                node_id: read_u32(bytes, payload + 4)?,
858                index: read_u32(bytes, payload + 8)?,
859            },
860            TAG_MOVE if len == 16 => Mutation::Move {
861                parent_id: read_u32(bytes, payload)?,
862                node_id: read_u32(bytes, payload + 4)?,
863                index: read_u32(bytes, payload + 8)?,
864            },
865            TAG_REMOVE if len == 8 => Mutation::Remove {
866                node_id: read_u32(bytes, payload)?,
867            },
868            TAG_SET_TEXT if len == 24 => {
869                let start = read_u32(bytes, payload + 12)?;
870                let end = read_u32(bytes, payload + 16)?;
871                Mutation::SetText {
872                    node_id: read_u32(bytes, payload)?,
873                    text: read_string(bytes, payload + 4, records_len)?,
874                    selection: (start != u32::MAX || end != u32::MAX)
875                        .then_some(Selection { start, end }),
876                }
877            }
878            TAG_APPEND_TEXT if len == 16 => Mutation::AppendText {
879                node_id: read_u32(bytes, payload)?,
880                text: read_string(bytes, payload + 4, records_len)?,
881            },
882            TAG_SET_THEME if len == 4 + THEME_PAYLOAD_LEN => {
883                let design_system = read_u16(bytes, payload)?;
884                let fallback = read_u16(bytes, payload + 2)?;
885                let color_scheme = read_u16(bytes, payload + 4)?;
886                let adaptive = read_u16(bytes, payload + 6)?;
887                if adaptive > 1 {
888                    return Err(ProtocolError::InvalidTheme(adaptive));
889                }
890                let mut fonts = [0u32; TYPE_ROLE_COUNT];
891                for (index, slot) in fonts.iter_mut().enumerate() {
892                    *slot = read_u32(bytes, payload + 8 + 4 * index)?;
893                }
894                // Entries that are wrong are left out and the rest stand, which is what
895                // the Renderer does with them as well. Leaked like the window's title: a
896                // theme holds its palette for the life of the program, and this decoder
897                // is what tests and the mock renderer read a batch back with.
898                let palette_bytes =
899                    read_bytes(bytes, payload + 8 + 4 * TYPE_ROLE_COUNT, records_len)?;
900                let palette = if palette_bytes.is_empty() {
901                    None
902                } else {
903                    let (palette, _problems) = crate::palette::decode_palette(palette_bytes);
904                    Some(&*Box::leak(Box::new(palette)))
905                };
906                Mutation::SetTheme(Theme {
907                    design_system: DesignSystem::try_from(design_system)
908                        .map_err(|()| ProtocolError::InvalidTheme(design_system))?,
909                    fallback: DesignSystem::try_from(fallback)
910                        .map_err(|()| ProtocolError::InvalidTheme(fallback))?,
911                    color_scheme: ColorScheme::try_from(color_scheme)
912                        .map_err(|()| ProtocolError::InvalidTheme(color_scheme))?,
913                    adaptive: adaptive == 1,
914                    fonts,
915                    palette,
916                })
917            }
918            TAG_SET_WINDOW if len == 32 => {
919                let chrome = read_u16(bytes, payload)?;
920                let title_bar = read_u16(bytes, payload + 2)?;
921                let resizable = read_u16(bytes, payload + 12)?;
922                if resizable > 1 {
923                    return Err(ProtocolError::InvalidValueKind(resizable));
924                }
925                Mutation::SetWindow(crate::schema::Window {
926                    chrome: crate::schema::Chrome::try_from(chrome)
927                        .map_err(|()| ProtocolError::InvalidValueKind(chrome))?,
928                    title_bar: crate::schema::TitleBar::try_from(title_bar)
929                        .map_err(|()| ProtocolError::InvalidValueKind(title_bar))?,
930                    width: read_u16(bytes, payload + 4)?,
931                    height: read_u16(bytes, payload + 6)?,
932                    min_width: read_u16(bytes, payload + 8)?,
933                    min_height: read_u16(bytes, payload + 10)?,
934                    resizable: resizable == 1,
935                    // Leaked on purpose, once per window. The record's owner is the
936                    // window, the window outlives the batch it arrived in, and there is
937                    // one of these per process.
938                    icon: read_u32(bytes, payload + 24)?,
939                    title: Box::leak(
940                        read_string(bytes, payload + 16, records_len)?
941                            .to_owned()
942                            .into_boxed_str(),
943                    ),
944                })
945            }
946            TAG_REGISTER_ASSET if len == 20 => {
947                let raw_kind = read_u16(bytes, payload + 4)?;
948                Mutation::RegisterAsset {
949                    asset_id: read_u32(bytes, payload)?,
950                    kind: AssetKind::try_from(raw_kind)
951                        .map_err(|()| ProtocolError::InvalidAssetKind(raw_kind))?,
952                    bytes: read_bytes(bytes, payload + 8, records_len)?,
953                }
954            }
955            TAG_RELEASE_ASSET if len == 8 => Mutation::ReleaseAsset {
956                asset_id: read_u32(bytes, payload)?,
957            },
958            TAG_SHOW_MESSAGE if len == 32 => {
959                let raw_duration = read_u16(bytes, payload + 24)?;
960                Mutation::ShowMessage {
961                    handler_id: read_u64(bytes, payload)?,
962                    text: read_string(bytes, payload + 8, records_len)?,
963                    action: read_string(bytes, payload + 16, records_len)?,
964                    duration: MessageDuration::try_from(raw_duration)
965                        .map_err(|()| ProtocolError::InvalidMessageDuration(raw_duration))?,
966                }
967            }
968            TAG_POST_NOTIFICATION if len == 56 => {
969                let raw_importance = read_u16(bytes, payload + 48)?;
970                let raw_presentation = read_u16(bytes, payload + 50)?;
971                Mutation::PostNotification {
972                    key: read_string(bytes, payload, records_len)?,
973                    title: read_string(bytes, payload + 8, records_len)?,
974                    body: read_string(bytes, payload + 16, records_len)?,
975                    channel: read_string(bytes, payload + 24, records_len)?,
976                    action_1: read_string(bytes, payload + 32, records_len)?,
977                    action_2: read_string(bytes, payload + 40, records_len)?,
978                    importance: NotificationImportance::try_from(raw_importance)
979                        .map_err(|()| ProtocolError::InvalidNotification(raw_importance))?,
980                    presentation: NotificationPresentation::try_from(raw_presentation)
981                        .map_err(|()| ProtocolError::InvalidNotification(raw_presentation))?,
982                }
983            }
984            TAG_WITHDRAW_NOTIFICATION if len == 12 => Mutation::WithdrawNotification {
985                key: read_string(bytes, payload, records_len)?,
986            },
987            TAG_REQUEST_NOTIFICATION_PERMISSION if len == 4 => {
988                Mutation::RequestNotificationPermission
989            }
990            TAG_CREATE..=TAG_LAST => {
991                return Err(ProtocolError::InvalidRecordLength);
992            }
993            other => return Err(ProtocolError::InvalidTag(other)),
994        };
995        output.push(mutation);
996        position += len;
997    }
998    if output.len() != count {
999        return Err(ProtocolError::InvalidEnvelope);
1000    }
1001    Ok(output)
1002}
1003
1004/// Two `f32` share one `u64`, with the first value in the low 32 bits.
1005const fn pack_floats(low: f32, high: f32) -> u64 {
1006    (low.to_bits() as u64) | ((high.to_bits() as u64) << 32)
1007}
1008
1009const fn unpack_low(word: u64) -> f32 {
1010    f32::from_bits(word as u32)
1011}
1012
1013const fn unpack_high(word: u64) -> f32 {
1014    f32::from_bits((word >> 32) as u32)
1015}
1016
1017fn modifier_fields(modifier: &Modifier) -> (u16, u64, u64) {
1018    match modifier {
1019        Modifier::Empty => (0, 0, 0),
1020        Modifier::Padding(value) => (1, u64::from(value.to_bits()), 0),
1021        Modifier::FillMaxWidth => (2, 0, 0),
1022        Modifier::FillMaxHeight => (3, 0, 0),
1023        Modifier::Width(value) => (4, u64::from(value.to_bits()), 0),
1024        Modifier::Height(value) => (5, u64::from(value.to_bits()), 0),
1025        Modifier::Size { width, height } => {
1026            (6, u64::from(width.to_bits()), u64::from(height.to_bits()))
1027        }
1028        Modifier::Background(paint) => (7, paint.to_bits(), 0),
1029        Modifier::Clickable { handler_id } => (8, *handler_id, 0),
1030        Modifier::PaddingRole(role) => (9, *role as u64, 0),
1031        Modifier::PaddingEach {
1032            start,
1033            top,
1034            end,
1035            bottom,
1036        } => (10, pack_floats(*start, *top), pack_floats(*end, *bottom)),
1037        Modifier::Weight(value) => (11, u64::from(value.to_bits()), 0),
1038        Modifier::Shape {
1039            top_start,
1040            top_end,
1041            bottom_end,
1042            bottom_start,
1043        } => (
1044            12,
1045            pack_floats(*top_start, *top_end),
1046            pack_floats(*bottom_end, *bottom_start),
1047        ),
1048        Modifier::ShapeRole(role) => (13, *role as u64, 0),
1049        Modifier::Border { width, paint } => (14, u64::from(width.to_bits()), paint.to_bits()),
1050        Modifier::Elevation(dp) => (15, u64::from(dp.to_bits()), 0),
1051        Modifier::ObserveSize { token } => (16, u64::from(*token), 0),
1052        Modifier::Motion(role) => (17, *role as u64, 0),
1053        Modifier::Material(role) => (18, *role as u64, 0),
1054    }
1055}
1056
1057fn decode_role<T: TryFrom<u16, Error = ()>>(first: u64) -> Result<T, ProtocolError> {
1058    let tag = u16::try_from(first).map_err(|_| ProtocolError::InvalidModifier(0))?;
1059    T::try_from(tag).map_err(|()| ProtocolError::InvalidModifier(tag))
1060}
1061
1062fn decode_paint(bits: u64) -> Result<Paint, ProtocolError> {
1063    Paint::from_bits(bits).ok_or(ProtocolError::InvalidModifier(0))
1064}
1065
1066fn decode_modifier(tag: u16, first: u64, second: u64) -> Result<Modifier, ProtocolError> {
1067    match tag {
1068        0 => Ok(Modifier::Empty),
1069        1 => Ok(Modifier::Padding(f32::from_bits(first as u32))),
1070        2 => Ok(Modifier::FillMaxWidth),
1071        3 => Ok(Modifier::FillMaxHeight),
1072        4 => Ok(Modifier::Width(f32::from_bits(first as u32))),
1073        5 => Ok(Modifier::Height(f32::from_bits(first as u32))),
1074        6 => Ok(Modifier::Size {
1075            width: f32::from_bits(first as u32),
1076            height: f32::from_bits(second as u32),
1077        }),
1078        7 => Ok(Modifier::Background(decode_paint(first)?)),
1079        8 => Ok(Modifier::Clickable { handler_id: first }),
1080        9 => Ok(Modifier::PaddingRole(decode_role::<SpaceRole>(first)?)),
1081        10 => Ok(Modifier::PaddingEach {
1082            start: unpack_low(first),
1083            top: unpack_high(first),
1084            end: unpack_low(second),
1085            bottom: unpack_high(second),
1086        }),
1087        11 => Ok(Modifier::Weight(f32::from_bits(first as u32))),
1088        12 => Ok(Modifier::Shape {
1089            top_start: unpack_low(first),
1090            top_end: unpack_high(first),
1091            bottom_end: unpack_low(second),
1092            bottom_start: unpack_high(second),
1093        }),
1094        13 => Ok(Modifier::ShapeRole(decode_role::<ShapeRole>(first)?)),
1095        14 => Ok(Modifier::Border {
1096            width: f32::from_bits(first as u32),
1097            paint: decode_paint(second)?,
1098        }),
1099        15 => Ok(Modifier::Elevation(f32::from_bits(first as u32))),
1100        16 => Ok(Modifier::ObserveSize {
1101            token: first as u32,
1102        }),
1103        17 => Ok(Modifier::Motion(decode_role::<MotionRole>(first)?)),
1104        18 => Ok(Modifier::Material(decode_role::<MaterialRole>(first)?)),
1105        other => Err(ProtocolError::InvalidModifier(other)),
1106    }
1107}
1108
1109/// Reads a `(offset: u32, len: u32)` string reference at `position`.
1110///
1111/// Strings live in the arena that follows the records, so `arena_start` is the
1112/// first byte a string may legally point at. Without that floor a hostile Renderer can
1113/// aim a string at the record region and have the decoder reinterpret record headers as
1114/// text, garbage decoding into a valid-looking mutation.
1115fn read_string(bytes: &[u8], position: usize, arena_start: usize) -> Result<&str, ProtocolError> {
1116    let value = read_bytes(bytes, position, arena_start)?;
1117    std::str::from_utf8(value).map_err(|_| ProtocolError::InvalidUtf8)
1118}
1119
1120/// The same arena range a string uses, without the UTF-8 requirement.
1121fn read_bytes(bytes: &[u8], position: usize, arena_start: usize) -> Result<&[u8], ProtocolError> {
1122    let offset =
1123        usize::try_from(read_u32(bytes, position)?).map_err(|_| ProtocolError::LengthOverflow)?;
1124    let len = usize::try_from(read_u32(bytes, position + 4)?)
1125        .map_err(|_| ProtocolError::LengthOverflow)?;
1126    let end = offset
1127        .checked_add(len)
1128        .ok_or(ProtocolError::InvalidStringRange)?;
1129    if offset < arena_start {
1130        return Err(ProtocolError::InvalidStringRange);
1131    }
1132    bytes
1133        .get(offset..end)
1134        .ok_or(ProtocolError::InvalidStringRange)
1135}
1136
1137fn read_u16(bytes: &[u8], position: usize) -> Result<u16, ProtocolError> {
1138    let value = bytes
1139        .get(position..position + 2)
1140        .ok_or(ProtocolError::Truncated)?;
1141    Ok(u16::from_le_bytes([value[0], value[1]]))
1142}
1143
1144fn read_u32(bytes: &[u8], position: usize) -> Result<u32, ProtocolError> {
1145    let value = bytes
1146        .get(position..position + 4)
1147        .ok_or(ProtocolError::Truncated)?;
1148    Ok(u32::from_le_bytes([value[0], value[1], value[2], value[3]]))
1149}
1150
1151fn read_u64(bytes: &[u8], position: usize) -> Result<u64, ProtocolError> {
1152    let value = bytes
1153        .get(position..position + 8)
1154        .ok_or(ProtocolError::Truncated)?;
1155    Ok(u64::from_le_bytes(
1156        value.try_into().map_err(|_| ProtocolError::Truncated)?,
1157    ))
1158}
1159
1160#[cfg(test)]
1161mod tests {
1162    use super::*;
1163    use crate::schema::TypeRole;
1164
1165    #[test]
1166    fn round_trips_all_m0_mutations() {
1167        let mutations = [
1168            Mutation::Create {
1169                node_id: 1,
1170                widget: WidgetKind::Column,
1171            },
1172            Mutation::SetProp {
1173                node_id: 2,
1174                property: PropertyKind::Text,
1175                value: PropertyValue::String("안녕"),
1176            },
1177            Mutation::SetProp {
1178                node_id: 3,
1179                property: PropertyKind::Enabled,
1180                value: PropertyValue::Bool(true),
1181            },
1182            Mutation::SetModifier {
1183                node_id: 1,
1184                index: 0,
1185                modifier: Modifier::Padding(16.0),
1186            },
1187            Mutation::Insert {
1188                parent_id: 1,
1189                node_id: 2,
1190                index: 0,
1191            },
1192            Mutation::Move {
1193                parent_id: 1,
1194                node_id: 2,
1195                index: 1,
1196            },
1197            Mutation::Remove { node_id: 3 },
1198            Mutation::SetText {
1199                node_id: 4,
1200                text: "compose",
1201                selection: Some(Selection { start: 1, end: 4 }),
1202            },
1203            Mutation::AppendText {
1204                node_id: 4,
1205                text: " token",
1206            },
1207        ];
1208        let mut encoder = BatchEncoder::default();
1209        for mutation in &mutations {
1210            encoder.encode(mutation).unwrap();
1211        }
1212        let decoded = decode_batch(encoder.finish().unwrap()).unwrap();
1213        assert_eq!(decoded, mutations);
1214    }
1215
1216    /// A role Paint and a literal Paint occupy the same record length and
1217    /// decode back to the value that was encoded.
1218    #[test]
1219    fn fr13_paint_role_and_literal_share_one_record_length() {
1220        let encode_one = |modifier| {
1221            let mut encoder = BatchEncoder::default();
1222            encoder
1223                .encode(&Mutation::SetModifier {
1224                    node_id: 1,
1225                    index: 0,
1226                    modifier,
1227                })
1228                .unwrap();
1229            encoder.finish().unwrap().to_vec()
1230        };
1231        let role = encode_one(Modifier::Background(Paint::Role(
1232            crate::schema::ColorRole::Surface,
1233        )));
1234        let literal = encode_one(Modifier::Background(Paint::Literal(
1235            crate::schema::Color::rgb(0x1b1b1f),
1236        )));
1237        assert_eq!(role.len(), literal.len());
1238        assert_eq!(
1239            decode_batch(&role).unwrap(),
1240            [Mutation::SetModifier {
1241                node_id: 1,
1242                index: 0,
1243                modifier: Modifier::Background(Paint::Role(crate::schema::ColorRole::Surface)),
1244            }]
1245        );
1246        assert_eq!(
1247            decode_batch(&literal).unwrap(),
1248            [Mutation::SetModifier {
1249                node_id: 1,
1250                index: 0,
1251                modifier: Modifier::Background(Paint::Literal(crate::schema::Color::rgb(0x1b1b1f))),
1252            }]
1253        );
1254    }
1255
1256    /// Every design primitive fits in `(tag, u64, u64)` and survives a round trip.
1257    #[test]
1258    fn fr13_design_modifiers_round_trip_in_two_words() {
1259        let modifiers = [
1260            Modifier::PaddingRole(SpaceRole::Md),
1261            Modifier::PaddingEach {
1262                start: 1.0,
1263                top: 2.0,
1264                end: 3.0,
1265                bottom: 4.0,
1266            },
1267            Modifier::Weight(0.25),
1268            Modifier::Shape {
1269                top_start: 4.0,
1270                top_end: 8.0,
1271                bottom_end: 12.0,
1272                bottom_start: 16.0,
1273            },
1274            Modifier::ShapeRole(ShapeRole::Full),
1275            Modifier::Border {
1276                width: 2.0,
1277                paint: Paint::Role(crate::schema::ColorRole::Outline),
1278            },
1279            Modifier::Elevation(6.0),
1280        ];
1281        let mut encoder = BatchEncoder::default();
1282        let expected: Vec<_> = modifiers
1283            .iter()
1284            .enumerate()
1285            .map(|(index, modifier)| Mutation::SetModifier {
1286                node_id: 1,
1287                index: index as u16,
1288                modifier: modifier.clone(),
1289            })
1290            .collect();
1291        for mutation in &expected {
1292            encoder.encode(mutation).unwrap();
1293        }
1294        let bytes = encoder.finish().unwrap();
1295        // Every modifier record is the same fixed 28 bytes.
1296        assert_eq!(bytes.len(), ENVELOPE_LEN + expected.len() * 28);
1297        assert_eq!(decode_batch(bytes).unwrap(), expected);
1298    }
1299
1300    /// `SetTheme` is four `u16` fields and one font slot per type role.
1301    ///
1302    /// Fixed length whether or not any font is named, because the record is fixed layout
1303    /// and a theme is sent once rather than per frame. The slots cost 36 bytes on a
1304    /// message that appears once in a session.
1305    #[test]
1306    fn fr23_set_theme_round_trips_with_a_slot_for_every_type_role() {
1307        let theme = Theme::adaptive(DesignSystem::Cupertino)
1308            .with_color_scheme(ColorScheme::Dark)
1309            .with_font(TypeRole::Display, 7);
1310        let length = 20 + 4 * TYPE_ROLE_COUNT as u16;
1311        let mut encoder = BatchEncoder::default();
1312        encoder.encode(&Mutation::SetTheme(theme)).unwrap();
1313        let bytes = encoder.finish().unwrap();
1314        assert_eq!(bytes.len(), ENVELOPE_LEN + length as usize);
1315        assert_eq!(read_u16(bytes, ENVELOPE_LEN).unwrap(), TAG_SET_THEME);
1316        assert_eq!(read_u16(bytes, ENVELOPE_LEN + 2).unwrap(), length);
1317        assert_eq!(decode_batch(bytes).unwrap(), [Mutation::SetTheme(theme)]);
1318    }
1319
1320    /// A font named for one role leaves every other role on the system font.
1321    #[test]
1322    fn fr23_a_font_is_named_for_one_role_and_no_others() {
1323        let theme = Theme::unified(DesignSystem::Material3).with_font(TypeRole::Display, 3);
1324        assert_eq!(theme.font(TypeRole::Display), Some(3));
1325        for role in [
1326            TypeRole::Body,
1327            TypeRole::Title,
1328            TypeRole::Mono,
1329            TypeRole::Caption,
1330        ] {
1331            assert_eq!(theme.font(role), None);
1332        }
1333    }
1334
1335    /// An out-of-range theme tag is an error, never a panic.
1336    #[test]
1337    fn fr14_rejects_unknown_theme_tags() {
1338        let mut encoder = BatchEncoder::default();
1339        encoder
1340            .encode(&Mutation::SetTheme(Theme::default()))
1341            .unwrap();
1342        let mut bytes = encoder.finish().unwrap().to_vec();
1343        bytes[ENVELOPE_LEN + 4..ENVELOPE_LEN + 6].copy_from_slice(&99_u16.to_le_bytes());
1344        assert_eq!(decode_batch(&bytes), Err(ProtocolError::InvalidTheme(99)));
1345    }
1346
1347    #[test]
1348    fn rejects_unknown_tags_without_panicking() {
1349        let mut bytes = vec![0, 0, 12, 0, 20, 0, 0, 0, 1, 0, 0, 0];
1350        bytes.extend_from_slice(&999_u16.to_le_bytes());
1351        bytes.extend_from_slice(&8_u16.to_le_bytes());
1352        bytes.extend_from_slice(&0_u32.to_le_bytes());
1353        assert_eq!(decode_batch(&bytes), Err(ProtocolError::InvalidTag(999)));
1354    }
1355
1356    #[test]
1357    fn pr4_renderer_events_round_trip_fixed_layout() {
1358        let events = [
1359            HostEvent {
1360                node_id: 7,
1361                handler_id: 11,
1362                payload: crate::EventPayload::Clicked,
1363            },
1364            HostEvent {
1365                node_id: 8,
1366                handler_id: 12,
1367                payload: crate::EventPayload::TextChanged("한글"),
1368            },
1369            HostEvent {
1370                node_id: 8,
1371                handler_id: 13,
1372                payload: crate::EventPayload::TextSubmitted("done"),
1373            },
1374            HostEvent {
1375                node_id: 8,
1376                handler_id: 14,
1377                payload: crate::EventPayload::FocusLost,
1378            },
1379            HostEvent {
1380                node_id: 9,
1381                handler_id: 16,
1382                payload: crate::EventPayload::RangeRequested {
1383                    start: 100,
1384                    count: 20,
1385                },
1386            },
1387            HostEvent {
1388                node_id: 8,
1389                handler_id: 15,
1390                payload: crate::EventPayload::KeyDown {
1391                    key: Key::Enter,
1392                    shift_key: true,
1393                    ctrl_key: false,
1394                    alt_key: true,
1395                    meta_key: false,
1396                },
1397            },
1398        ];
1399        let mut bytes = Vec::new();
1400        for event in events {
1401            encode_event(&event, &mut bytes).unwrap();
1402            assert_eq!(decode_event(&bytes).unwrap(), event);
1403        }
1404    }
1405
1406    #[test]
1407    fn fr20_window_size_changed_round_trips_in_thirty_two_bytes() {
1408        let event = HostEvent {
1409            node_id: 0,
1410            handler_id: 0,
1411            payload: crate::EventPayload::WindowSizeChanged {
1412                width_dp: 841.5,
1413                height_dp: 600.25,
1414                class: crate::WindowSizeClass::Expanded,
1415                height_class: crate::WindowHeightClass::Medium,
1416            },
1417        };
1418        let mut bytes = Vec::new();
1419        encode_event(&event, &mut bytes).unwrap();
1420        assert_eq!(bytes.len(), 32);
1421        assert_eq!(decode_event(&bytes).unwrap(), event);
1422    }
1423
1424    #[test]
1425    fn fr20_unknown_window_size_class_is_a_protocol_error() {
1426        let event = HostEvent {
1427            node_id: 0,
1428            handler_id: 0,
1429            payload: crate::EventPayload::WindowSizeChanged {
1430                width_dp: 320.0,
1431                height_dp: 640.0,
1432                class: crate::WindowSizeClass::Compact,
1433                height_class: crate::WindowHeightClass::Medium,
1434            },
1435        };
1436        let mut bytes = Vec::new();
1437        encode_event(&event, &mut bytes).unwrap();
1438        bytes[24..28].copy_from_slice(&99_u32.to_le_bytes());
1439        assert_eq!(
1440            decode_event(&bytes),
1441            Err(ProtocolError::InvalidValueKind(99))
1442        );
1443    }
1444
1445    #[test]
1446    fn fr20_unknown_window_height_class_is_a_protocol_error() {
1447        let event = HostEvent {
1448            node_id: 0,
1449            handler_id: 0,
1450            payload: crate::EventPayload::WindowSizeChanged {
1451                width_dp: 320.0,
1452                height_dp: 640.0,
1453                class: crate::WindowSizeClass::Compact,
1454                height_class: crate::WindowHeightClass::Medium,
1455            },
1456        };
1457        let mut bytes = Vec::new();
1458        encode_event(&event, &mut bytes).unwrap();
1459        bytes[28..32].copy_from_slice(&99_u32.to_le_bytes());
1460        assert_eq!(
1461            decode_event(&bytes),
1462            Err(ProtocolError::InvalidValueKind(99))
1463        );
1464    }
1465
1466    #[test]
1467    fn fr20_window_size_classes_follow_the_material_boundaries() {
1468        use crate::WindowSizeClass;
1469        assert_eq!(
1470            WindowSizeClass::from_width_dp(599.9),
1471            WindowSizeClass::Compact
1472        );
1473        assert_eq!(
1474            WindowSizeClass::from_width_dp(600.0),
1475            WindowSizeClass::Medium
1476        );
1477        assert_eq!(
1478            WindowSizeClass::from_width_dp(839.9),
1479            WindowSizeClass::Medium
1480        );
1481        assert_eq!(
1482            WindowSizeClass::from_width_dp(840.0),
1483            WindowSizeClass::Expanded
1484        );
1485    }
1486
1487    #[test]
1488    fn fr20_window_height_classes_follow_the_material_boundaries() {
1489        use crate::WindowHeightClass;
1490        assert_eq!(
1491            WindowHeightClass::from_height_dp(479.9),
1492            WindowHeightClass::Compact
1493        );
1494        assert_eq!(
1495            WindowHeightClass::from_height_dp(480.0),
1496            WindowHeightClass::Medium
1497        );
1498        assert_eq!(
1499            WindowHeightClass::from_height_dp(899.9),
1500            WindowHeightClass::Medium
1501        );
1502        assert_eq!(
1503            WindowHeightClass::from_height_dp(900.0),
1504            WindowHeightClass::Expanded
1505        );
1506    }
1507
1508    fn post_notification() -> Mutation<'static> {
1509        Mutation::PostNotification {
1510            key: "session/7",
1511            title: "세션이 끝났습니다",
1512            body: "refactor-parser: 테스트 214개 통과",
1513            channel: "세션",
1514            action_1: "열기",
1515            action_2: "",
1516            importance: NotificationImportance::Urgent,
1517            presentation: NotificationPresentation::WhenInactive,
1518        }
1519    }
1520
1521    /// The three notification commands keep the lengths the wire fixes for them: six string
1522    /// references and two closed enums, one string reference, and a bare header.
1523    #[test]
1524    fn fr36_notification_commands_round_trip_at_their_fixed_lengths() {
1525        let mutations = [
1526            post_notification(),
1527            Mutation::WithdrawNotification { key: "session/7" },
1528            Mutation::RequestNotificationPermission,
1529        ];
1530        let mut lengths = Vec::new();
1531        let mut encoder = BatchEncoder::default();
1532        for mutation in &mutations {
1533            encoder.encode(mutation).unwrap();
1534        }
1535        let bytes = encoder.finish().unwrap();
1536        let mut position = ENVELOPE_LEN;
1537        for _ in &mutations {
1538            let length = read_u16(bytes, position + 2).unwrap();
1539            lengths.push((read_u16(bytes, position).unwrap(), length));
1540            position += usize::from(length);
1541        }
1542        assert_eq!(
1543            lengths,
1544            [
1545                (TAG_POST_NOTIFICATION, 56),
1546                (TAG_WITHDRAW_NOTIFICATION, 12),
1547                (TAG_REQUEST_NOTIFICATION_PERMISSION, 4),
1548            ]
1549        );
1550        assert_eq!(decode_batch(bytes).unwrap(), mutations);
1551    }
1552
1553    /// An importance or a presentation this side does not know is an error, never a guess
1554    /// and never a panic.
1555    #[test]
1556    fn fr36_unknown_importance_and_presentation_are_protocol_errors() {
1557        for (offset, expected) in [(52, 9_u16), (54, 7_u16)] {
1558            let mut encoder = BatchEncoder::default();
1559            encoder.encode(&post_notification()).unwrap();
1560            let mut bytes = encoder.finish().unwrap().to_vec();
1561            bytes[ENVELOPE_LEN + offset..ENVELOPE_LEN + offset + 2]
1562                .copy_from_slice(&expected.to_le_bytes());
1563            assert_eq!(
1564                decode_batch(&bytes),
1565                Err(ProtocolError::InvalidNotification(expected))
1566            );
1567        }
1568    }
1569
1570    /// A known notification tag at the wrong length is a length error, the same as any
1571    /// other known record.
1572    #[test]
1573    fn fr36_a_notification_record_of_the_wrong_length_is_rejected() {
1574        let mut bytes = vec![0, 0, 12, 0, 20, 0, 0, 0, 1, 0, 0, 0];
1575        bytes.extend_from_slice(&TAG_REQUEST_NOTIFICATION_PERMISSION.to_le_bytes());
1576        bytes.extend_from_slice(&8_u16.to_le_bytes());
1577        bytes.extend_from_slice(&0_u32.to_le_bytes());
1578        assert_eq!(
1579            decode_batch(&bytes),
1580            Err(ProtocolError::InvalidRecordLength)
1581        );
1582    }
1583
1584    /// The two notification events: a word and a string after the common head, and a word
1585    /// on its own.
1586    #[test]
1587    fn fr36_notification_events_round_trip_fixed_layout() {
1588        let activated = HostEvent {
1589            node_id: 0,
1590            handler_id: 0,
1591            payload: crate::EventPayload::NotificationActivated {
1592                action: 2,
1593                key: "session/7",
1594            },
1595        };
1596        let mut bytes = Vec::new();
1597        encode_event(&activated, &mut bytes).unwrap();
1598        assert_eq!(read_u16(&bytes, 0).unwrap(), EVENT_NOTIFICATION_ACTIVATED);
1599        assert_eq!(read_u16(&bytes, 2).unwrap(), 28);
1600        assert_eq!(bytes.len(), 28 + "session/7".len());
1601        assert_eq!(decode_event(&bytes).unwrap(), activated);
1602
1603        for state in [
1604            NotificationPermission::NotDetermined,
1605            NotificationPermission::Granted,
1606            NotificationPermission::Denied,
1607            NotificationPermission::Unsupported,
1608        ] {
1609            let changed = HostEvent {
1610                node_id: 0,
1611                handler_id: 0,
1612                payload: crate::EventPayload::NotificationPermissionChanged(state),
1613            };
1614            encode_event(&changed, &mut bytes).unwrap();
1615            assert_eq!(bytes.len(), 20);
1616            assert_eq!(decode_event(&bytes).unwrap(), changed);
1617        }
1618    }
1619
1620    /// A permission state outside the four is reported, not mapped onto one of them.
1621    #[test]
1622    fn fr36_unknown_permission_state_is_a_protocol_error() {
1623        let mut bytes = Vec::new();
1624        encode_event(
1625            &HostEvent {
1626                node_id: 0,
1627                handler_id: 0,
1628                payload: crate::EventPayload::NotificationPermissionChanged(
1629                    NotificationPermission::Granted,
1630                ),
1631            },
1632            &mut bytes,
1633        )
1634        .unwrap();
1635        bytes[16..20].copy_from_slice(&5_u32.to_le_bytes());
1636        assert_eq!(
1637            decode_event(&bytes),
1638            Err(ProtocolError::InvalidValueKind(5))
1639        );
1640    }
1641
1642    /// A notification has a body and at most two buttons, so an activation naming a third
1643    /// is a malformed event.
1644    #[test]
1645    fn fr36_an_activation_naming_a_third_button_is_a_protocol_error() {
1646        let mut bytes = Vec::new();
1647        encode_event(
1648            &HostEvent {
1649                node_id: 0,
1650                handler_id: 0,
1651                payload: crate::EventPayload::NotificationActivated {
1652                    action: 1,
1653                    key: "k",
1654                },
1655            },
1656            &mut bytes,
1657        )
1658        .unwrap();
1659        bytes[16..20].copy_from_slice(&3_u32.to_le_bytes());
1660        assert_eq!(
1661            decode_event(&bytes),
1662            Err(ProtocolError::InvalidValueKind(3))
1663        );
1664    }
1665}