now_proto_pdu/session/
window_rec_event.rs

1use alloc::borrow::Cow;
2
3use bitflags::bitflags;
4use ironrdp_core::{
5    cast_length, other_err, Decode, DecodeResult, Encode, EncodeResult, IntoOwned, ReadCursor, WriteCursor,
6};
7
8use crate::{NowHeader, NowMessage, NowMessageClass, NowSessionMessage, NowSessionMessageKind, NowVarStr};
9
10bitflags! {
11    /// Event kind flags for window recording events (internal).
12    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
13    struct WindowRecEventFlags: u16 {
14        /// NOW-PROTO: NOW_WINDOW_REC_EVENT_ACTIVE_WINDOW
15        const ACTIVE_WINDOW = 0x0001;
16        /// NOW-PROTO: NOW_WINDOW_REC_EVENT_TITLE_CHANGED
17        const TITLE_CHANGED = 0x0002;
18        /// NOW-PROTO: NOW_WINDOW_REC_EVENT_NO_ACTIVE_WINDOW
19        const NO_ACTIVE_WINDOW = 0x0004;
20    }
21}
22
23/// Active window event data.
24///
25/// NOW-PROTO: NOW_WINDOW_REC_EVENT_ACTIVE_WINDOW
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct ActiveWindowEventData<'a> {
28    process_id: u32,
29    title: NowVarStr<'a>,
30    executable_path: NowVarStr<'a>,
31}
32
33impl ActiveWindowEventData<'_> {
34    /// Get the process ID of the active window.
35    pub fn process_id(&self) -> u32 {
36        self.process_id
37    }
38
39    /// Get the window title.
40    pub fn title(&self) -> &str {
41        self.title.as_ref()
42    }
43
44    /// Get the executable path.
45    pub fn executable_path(&self) -> &str {
46        self.executable_path.as_ref()
47    }
48}
49
50/// Title changed event data.
51///
52/// NOW-PROTO: NOW_WINDOW_REC_EVENT_TITLE_CHANGED
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct TitleChangedEventData<'a> {
55    title: NowVarStr<'a>,
56}
57
58impl TitleChangedEventData<'_> {
59    /// Get the new window title.
60    pub fn title(&self) -> &str {
61        self.title.as_ref()
62    }
63}
64
65/// Window recording event kind.
66///
67/// NOW_PROTO: NOW_SESSION_WINDOW_REC_EVENT_MSG msgFlags
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum WindowRecEventKind<'a> {
70    /// Active window changed.
71    ///
72    /// NOW-PROTO: NOW_WINDOW_REC_EVENT_ACTIVE_WINDOW
73    ActiveWindow(ActiveWindowEventData<'a>),
74    /// Window title changed for the current active window.
75    ///
76    /// NOW-PROTO: NOW_WINDOW_REC_EVENT_TITLE_CHANGED
77    TitleChanged(TitleChangedEventData<'a>),
78    /// No active window.
79    ///
80    /// NOW-PROTO: NOW_WINDOW_REC_EVENT_NO_ACTIVE_WINDOW
81    NoActiveWindow,
82}
83
84pub type OwnedActiveWindowEventData = ActiveWindowEventData<'static>;
85
86impl IntoOwned for ActiveWindowEventData<'_> {
87    type Owned = OwnedActiveWindowEventData;
88
89    fn into_owned(self) -> Self::Owned {
90        OwnedActiveWindowEventData {
91            process_id: self.process_id,
92            title: self.title.into_owned(),
93            executable_path: self.executable_path.into_owned(),
94        }
95    }
96}
97
98pub type OwnedTitleChangedEventData = TitleChangedEventData<'static>;
99
100impl IntoOwned for TitleChangedEventData<'_> {
101    type Owned = OwnedTitleChangedEventData;
102
103    fn into_owned(self) -> Self::Owned {
104        OwnedTitleChangedEventData {
105            title: self.title.into_owned(),
106        }
107    }
108}
109
110pub type OwnedWindowRecEventKind = WindowRecEventKind<'static>;
111
112impl IntoOwned for WindowRecEventKind<'_> {
113    type Owned = OwnedWindowRecEventKind;
114
115    fn into_owned(self) -> Self::Owned {
116        match self {
117            Self::ActiveWindow(data) => OwnedWindowRecEventKind::ActiveWindow(data.into_owned()),
118            Self::TitleChanged(data) => OwnedWindowRecEventKind::TitleChanged(data.into_owned()),
119            Self::NoActiveWindow => OwnedWindowRecEventKind::NoActiveWindow,
120        }
121    }
122}
123
124/// The NOW_SESSION_WINDOW_REC_EVENT_MSG message is sent by the server to notify of window recording
125/// events such as active window changes, title changes, or when no window is active.
126///
127/// NOW_PROTO: NOW_SESSION_WINDOW_REC_EVENT_MSG
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct NowSessionWindowRecEventMsg<'a> {
130    timestamp: u64,
131    kind: WindowRecEventKind<'a>,
132}
133
134impl_pdu_borrowing!(NowSessionWindowRecEventMsg<'_>, OwnedNowSessionWindowRecEventMsg);
135
136impl IntoOwned for NowSessionWindowRecEventMsg<'_> {
137    type Owned = OwnedNowSessionWindowRecEventMsg;
138
139    fn into_owned(self) -> Self::Owned {
140        OwnedNowSessionWindowRecEventMsg {
141            timestamp: self.timestamp,
142            kind: self.kind.into_owned(),
143        }
144    }
145}
146
147impl<'a> NowSessionWindowRecEventMsg<'a> {
148    const NAME: &'static str = "NOW_SESSION_WINDOW_REC_EVENT_MSG";
149    const FIXED_PART_SIZE: usize = 8 + 4; // timestamp (8) + process_id (4)
150
151    pub fn active_window(
152        timestamp: u64,
153        process_id: u32,
154        title: impl Into<Cow<'a, str>>,
155        executable_path: impl Into<Cow<'a, str>>,
156    ) -> EncodeResult<Self> {
157        Ok(Self {
158            timestamp,
159            kind: WindowRecEventKind::ActiveWindow(ActiveWindowEventData {
160                process_id,
161                title: NowVarStr::new(title)?,
162                executable_path: NowVarStr::new(executable_path)?,
163            }),
164        })
165    }
166
167    pub fn title_changed(timestamp: u64, title: impl Into<Cow<'a, str>>) -> EncodeResult<Self> {
168        Ok(Self {
169            timestamp,
170            kind: WindowRecEventKind::TitleChanged(TitleChangedEventData {
171                title: NowVarStr::new(title)?,
172            }),
173        })
174    }
175
176    pub fn no_active_window(timestamp: u64) -> Self {
177        Self {
178            timestamp,
179            kind: WindowRecEventKind::NoActiveWindow,
180        }
181    }
182
183    pub fn timestamp(&self) -> u64 {
184        self.timestamp
185    }
186
187    pub fn kind(&self) -> &WindowRecEventKind<'a> {
188        &self.kind
189    }
190}
191
192impl Encode for NowSessionWindowRecEventMsg<'_> {
193    fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
194        let mut title = NowVarStr::default();
195        let mut executable_path = NowVarStr::default();
196
197        let (flags, process_id) = match &self.kind {
198            WindowRecEventKind::ActiveWindow(data) => {
199                title = data.title.clone();
200                executable_path = data.executable_path.clone();
201                (WindowRecEventFlags::ACTIVE_WINDOW, data.process_id)
202            }
203            WindowRecEventKind::TitleChanged(data) => {
204                title = data.title.clone();
205                (WindowRecEventFlags::TITLE_CHANGED, 0)
206            }
207            WindowRecEventKind::NoActiveWindow => (WindowRecEventFlags::NO_ACTIVE_WINDOW, 0),
208        };
209
210        let header = NowHeader {
211            size: cast_length!("size", self.body_size())?,
212            class: NowMessageClass::SESSION,
213            kind: NowSessionMessageKind::WINDOW_REC_EVENT.0,
214            flags: flags.bits(),
215        };
216
217        header.encode(dst)?;
218        dst.write_u64(self.timestamp);
219        dst.write_u32(process_id);
220        title.encode(dst)?;
221        executable_path.encode(dst)?;
222
223        Ok(())
224    }
225
226    fn name(&self) -> &'static str {
227        Self::NAME
228    }
229
230    fn size(&self) -> usize {
231        NowHeader::FIXED_PART_SIZE + self.body_size()
232    }
233}
234
235impl NowSessionWindowRecEventMsg<'_> {
236    fn body_size(&self) -> usize {
237        // Empty string size (VarU32 length encoding + null terminator, minimum 2 bytes)
238        let empty_str_size = NowVarStr::new("").expect("empty string is valid").size();
239
240        let (title_size, exec_path_size) = match &self.kind {
241            WindowRecEventKind::ActiveWindow(data) => (data.title.size(), data.executable_path.size()),
242            WindowRecEventKind::TitleChanged(data) => (data.title.size(), empty_str_size),
243            WindowRecEventKind::NoActiveWindow => (empty_str_size, empty_str_size),
244        };
245
246        Self::FIXED_PART_SIZE + title_size + exec_path_size
247    }
248}
249
250impl<'de> Decode<'de> for NowSessionWindowRecEventMsg<'de> {
251    fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
252        let header = NowHeader::decode(src)?;
253
254        match (header.class, NowSessionMessageKind(header.kind)) {
255            (NowMessageClass::SESSION, NowSessionMessageKind::WINDOW_REC_EVENT) => Self::decode_from_body(header, src),
256            _ => Err(unsupported_message_err!(class: header.class.0, kind: header.kind)),
257        }
258    }
259}
260
261impl<'de> NowSessionWindowRecEventMsg<'de> {
262    pub(crate) fn decode_from_body(header: NowHeader, src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
263        let flags = WindowRecEventFlags::from_bits_retain(header.flags);
264        let timestamp = src.read_u64();
265        let process_id = src.read_u32();
266        let title = NowVarStr::decode(src)?;
267        let executable_path = NowVarStr::decode(src)?;
268
269        let kind = if flags.contains(WindowRecEventFlags::ACTIVE_WINDOW) {
270            WindowRecEventKind::ActiveWindow(ActiveWindowEventData {
271                process_id,
272                title,
273                executable_path,
274            })
275        } else if flags.contains(WindowRecEventFlags::TITLE_CHANGED) {
276            WindowRecEventKind::TitleChanged(TitleChangedEventData { title })
277        } else if flags.contains(WindowRecEventFlags::NO_ACTIVE_WINDOW) {
278            WindowRecEventKind::NoActiveWindow
279        } else {
280            return Err(other_err!(
281                "invalid window recording event flags",
282                "unsupported flags combination"
283            ));
284        };
285
286        Ok(Self { timestamp, kind })
287    }
288}
289
290impl<'a> From<NowSessionWindowRecEventMsg<'a>> for NowMessage<'a> {
291    fn from(value: NowSessionWindowRecEventMsg<'a>) -> Self {
292        Self::Session(NowSessionMessage::WindowRecEvent(value))
293    }
294}