mj_controller/
hel_desktop.rs1use std::path::{Path, PathBuf};
4
5use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7
8pub const DESKTOP_LAUNCH_PROTOCOL_VERSION: u32 = 1;
9
10#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(deny_unknown_fields)]
17pub struct DesktopLaunch {
18 protocol_version: u32,
19 pub viewer_url: String,
20 pub bootstrap_cookie_value: String,
21}
22
23impl std::fmt::Debug for DesktopLaunch {
24 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 formatter
26 .debug_struct("DesktopLaunch")
27 .field("protocol_version", &self.protocol_version)
28 .field("viewer_url", &self.viewer_url)
29 .field("bootstrap_cookie_value", &"[redacted]")
30 .finish()
31 }
32}
33
34impl DesktopLaunch {
35 pub fn new(viewer_url: String, bootstrap_cookie_value: String) -> Self {
36 Self {
37 protocol_version: DESKTOP_LAUNCH_PROTOCOL_VERSION,
38 viewer_url,
39 bootstrap_cookie_value,
40 }
41 }
42
43 pub fn to_json(&self) -> Result<Vec<u8>> {
44 serde_json::to_vec(self).context("serialize desktop launch")
45 }
46
47 pub fn from_json(bytes: &[u8]) -> Result<Self> {
48 let launch: Self = serde_json::from_slice(bytes).context("parse desktop launch")?;
49 anyhow::ensure!(
50 launch.protocol_version == DESKTOP_LAUNCH_PROTOCOL_VERSION,
51 "desktop launch protocol {} is incompatible with supported version {}",
52 launch.protocol_version,
53 DESKTOP_LAUNCH_PROTOCOL_VERSION
54 );
55 Ok(launch)
56 }
57}
58
59pub fn sibling_executable(current: &Path, basename: &str) -> PathBuf {
61 current.with_file_name(format!("{basename}{}", std::env::consts::EXE_SUFFIX))
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn desktop_launch_round_trips_without_debugging_its_cookie() {
70 let launch = DesktopLaunch::new(
71 "https://localhost:43210/".into(),
72 "signed-secret-cookie".into(),
73 );
74
75 assert_eq!(
76 DesktopLaunch::from_json(&launch.to_json().unwrap()).unwrap(),
77 launch
78 );
79 let debug = format!("{launch:?}");
80 assert!(debug.contains("[redacted]"), "{debug}");
81 assert!(!debug.contains("signed-secret-cookie"), "{debug}");
82 }
83
84 #[test]
85 fn desktop_launch_rejects_unknown_fields() {
86 let error = DesktopLaunch::from_json(
87 br#"{"protocol_version":1,"viewer_url":"https://localhost/","bootstrap_cookie_value":"secret","extra":true}"#,
88 )
89 .unwrap_err()
90 .to_string();
91 assert!(error.contains("parse desktop launch"), "{error}");
92 assert!(!error.contains("secret"), "{error}");
93 }
94
95 #[test]
96 fn desktop_launch_rejects_an_incompatible_protocol() {
97 let error = DesktopLaunch::from_json(
98 br#"{"protocol_version":99,"viewer_url":"https://localhost/","bootstrap_cookie_value":"secret"}"#,
99 )
100 .unwrap_err()
101 .to_string();
102 assert!(error.contains("protocol 99"), "{error}");
103 assert!(!error.contains("secret"), "{error}");
104 }
105
106 #[test]
107 fn companion_path_replaces_only_the_executable_filename() {
108 let current = Path::new("target/profile/mj");
109 let expected =
110 Path::new("target/profile").join(format!("mj-desktop{}", std::env::consts::EXE_SUFFIX));
111 assert_eq!(sibling_executable(current, "mj-desktop"), expected);
112 }
113}