nimbus_cli/output/
deeplink.rs
1use anyhow::{anyhow, Result};
6use percent_encoding::{AsciiSet, CONTROLS};
7
8use crate::protocol::StartAppProtocol;
9use crate::{AppOpenArgs, LaunchableApp};
10
11impl LaunchableApp {
12 pub(crate) fn copy_to_clipboard(
13 &self,
14 app_protocol: &StartAppProtocol,
15 open: &AppOpenArgs,
16 ) -> Result<usize> {
17 let url = self.longform_url(app_protocol, open)?;
18 let len = url.len();
19 if let Err(e) = set_clipboard(url) {
20 anyhow::bail!("Can't copy URL to clipboard: {}", e)
21 };
22
23 Ok(len)
24 }
25
26 pub(crate) fn longform_url(
27 &self,
28 app_protocol: &StartAppProtocol,
29 open: &AppOpenArgs,
30 ) -> Result<String> {
31 let deeplink = match (&open.deeplink, self.app_opening_deeplink()) {
32 (Some(deeplink), _) => deeplink.to_owned(),
33 (_, Some(deeplink)) => join_query(deeplink, "--nimbus-cli&--is-launcher"),
34 _ => anyhow::bail!("A deeplink must be provided"),
35 };
36
37 let url = longform_deeplink_url(deeplink.as_str(), app_protocol)?;
38
39 self.prepend_scheme(url.as_str())
40 }
41
42 fn app_opening_deeplink(&self) -> Option<&str> {
43 match self {
44 Self::Android { open_deeplink, .. } => open_deeplink.as_deref(),
45 Self::Ios { .. } => Some("noop"),
46 }
47 }
48
49 pub(crate) fn deeplink(&self, open: &AppOpenArgs) -> Result<Option<String>> {
50 let deeplink = &open.deeplink;
51 if deeplink.is_none() {
52 return Ok(None);
53 }
54 let deeplink = self.prepend_scheme(deeplink.as_ref().unwrap())?;
55 Ok(Some(deeplink))
56 }
57
58 fn prepend_scheme(&self, deeplink: &str) -> Result<String> {
59 Ok(if deeplink.contains("://") {
60 deeplink.to_string()
61 } else {
62 let scheme = self.mandatory_scheme()?;
63 format!("{scheme}://{deeplink}")
64 })
65 }
66
67 fn mandatory_scheme(&self) -> Result<&str> {
68 match self {
69 Self::Android { scheme, .. } | Self::Ios { scheme, .. } => scheme
70 .as_deref()
71 .ok_or_else(|| anyhow!("A scheme is not defined for this app")),
72 }
73 }
74}
75
76const QUERY: &AsciiSet = &CONTROLS
79 .add(b' ')
80 .add(b'"')
81 .add(b'<')
82 .add(b'>')
83 .add(b'#')
84 .add(b'\'')
85 .add(b'{')
87 .add(b'}')
88 .add(b':')
90 .add(b'/')
91 .add(b'?')
92 .add(b'&');
93
94pub(crate) fn longform_deeplink_url(
96 deeplink: &str,
97 app_protocol: &StartAppProtocol,
98) -> Result<String> {
99 let StartAppProtocol {
100 reset_db,
101 experiments,
102 log_state,
103 } = app_protocol;
104 if !reset_db && experiments.is_none() && !log_state {
105 return Ok(deeplink.to_string());
106 }
107
108 let mut parts: Vec<_> = Default::default();
109 if !deeplink.contains("--nimbus-cli") {
110 parts.push("--nimbus-cli".to_string());
111 }
112 if let Some(v) = experiments {
113 let json = serde_json::to_string(v)?;
114 let string = percent_encoding::utf8_percent_encode(&json, QUERY).to_string();
115 parts.push(format!("--experiments={string}"));
116 }
117
118 if *reset_db {
119 parts.push("--reset-db".to_string());
120 }
121 if *log_state {
122 parts.push("--log-state".to_string());
123 }
124
125 Ok(join_query(deeplink, &parts.join("&")))
126}
127
128fn join_query(url: &str, item: &str) -> String {
129 let suffix = if url.contains('?') { '&' } else { '?' };
130 format!("{url}{suffix}{item}")
131}
132
133fn set_clipboard(contents: String) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
134 use copypasta::{ClipboardContext, ClipboardProvider};
135 let mut ctx = ClipboardContext::new()?;
136 ctx.set_contents(contents)?;
137 Ok(())
138}
139
140#[cfg(test)]
141mod unit_tests {
142
143 use super::*;
144 use serde_json::json;
145
146 #[test]
147 fn test_url_noop() -> Result<()> {
148 let p = StartAppProtocol {
149 reset_db: false,
150 experiments: None,
151 log_state: false,
152 };
153 assert_eq!("host".to_string(), longform_deeplink_url("host", &p)?);
154 assert_eq!(
155 "host?query=1".to_string(),
156 longform_deeplink_url("host?query=1", &p)?
157 );
158 Ok(())
159 }
160
161 #[test]
162 fn test_url_reset_db() -> Result<()> {
163 let p = StartAppProtocol {
164 reset_db: true,
165 experiments: None,
166 log_state: false,
167 };
168 assert_eq!(
169 "host?--nimbus-cli&--reset-db".to_string(),
170 longform_deeplink_url("host", &p)?
171 );
172 assert_eq!(
173 "host?query=1&--nimbus-cli&--reset-db".to_string(),
174 longform_deeplink_url("host?query=1", &p)?
175 );
176
177 Ok(())
178 }
179
180 #[test]
181 fn test_url_log_state() -> Result<()> {
182 let p = StartAppProtocol {
183 reset_db: false,
184 experiments: None,
185 log_state: true,
186 };
187 assert_eq!(
188 "host?--nimbus-cli&--log-state".to_string(),
189 longform_deeplink_url("host", &p)?
190 );
191 assert_eq!(
192 "host?query=1&--nimbus-cli&--log-state".to_string(),
193 longform_deeplink_url("host?query=1", &p)?
194 );
195
196 Ok(())
197 }
198
199 #[test]
200 fn test_url_experiments() -> Result<()> {
201 let v = json!({"data": []});
202 let p = StartAppProtocol {
203 reset_db: false,
204 experiments: Some(&v),
205 log_state: false,
206 };
207 assert_eq!(
208 "host?--nimbus-cli&--experiments=%7B%22data%22%3A[]%7D".to_string(),
209 longform_deeplink_url("host", &p)?
210 );
211 assert_eq!(
212 "host?query=1&--nimbus-cli&--experiments=%7B%22data%22%3A[]%7D".to_string(),
213 longform_deeplink_url("host?query=1", &p)?
214 );
215
216 Ok(())
217 }
218
219 #[test]
220 fn test_deeplink_has_is_launcher_param_if_no_deeplink_is_specified() -> Result<()> {
221 let app =
222 LaunchableApp::try_from_app_channel_device(Some("fenix"), Some("developer"), None)?;
223
224 let payload: StartAppProtocol = Default::default();
226 let open: AppOpenArgs = Default::default();
227 assert_eq!(
228 "fenix-dev://open?--nimbus-cli&--is-launcher".to_string(),
229 app.longform_url(&payload, &open)?
230 );
231
232 let open = AppOpenArgs {
234 deeplink: Some("deeplink".to_string()),
235 ..Default::default()
236 };
237 assert_eq!(
238 "fenix-dev://deeplink".to_string(),
239 app.longform_url(&payload, &open)?
240 );
241
242 let payload = StartAppProtocol {
244 log_state: true,
245 ..Default::default()
246 };
247 assert_eq!(
248 "fenix-dev://open?--nimbus-cli&--is-launcher&--log-state".to_string(),
249 app.longform_url(&payload, &Default::default())?
250 );
251
252 let open = AppOpenArgs {
254 deeplink: Some("deeplink".to_string()),
255 ..Default::default()
256 };
257 assert_eq!(
258 "fenix-dev://deeplink?--nimbus-cli&--log-state".to_string(),
259 app.longform_url(&payload, &open)?
260 );
261
262 Ok(())
263 }
264}