native_messaging/install/
manifest.rs1use serde::Serialize;
2use serde_json::Value;
3use std::{fs, io, path::Path};
4
5use crate::install::paths;
6
7#[derive(Serialize)]
8struct ChromiumHostManifest<'a> {
9 name: &'a str,
10 description: &'a str,
11 path: &'a str,
12 #[serde(rename = "type")]
13 ty: &'a str, allowed_origins: Vec<String>, }
16
17#[derive(Serialize)]
18struct FirefoxHostManifest<'a> {
19 name: &'a str,
20 description: &'a str,
21 path: &'a str,
22 #[serde(rename = "type")]
23 ty: &'a str, allowed_extensions: Vec<String>, }
26
27fn ensure_absolute_path(exe_path: &Path) -> io::Result<()> {
28 #[cfg(any(target_os = "macos", target_os = "linux"))]
30 {
31 if !exe_path.is_absolute() {
32 return Err(io::Error::new(
33 io::ErrorKind::InvalidInput,
34 "Manifest `path` must be absolute on macOS/Linux",
35 ));
36 }
37 }
38
39 #[cfg(windows)]
40 {
41 let _ = exe_path;
42 }
43
44 Ok(())
45}
46
47pub fn install(
52 host_name: &str,
53 description: &str,
54 exe_path: &Path,
55 chrome_allowed_origins: &[String],
56 firefox_allowed_extensions: &[String],
57 browsers: &[&str],
58 scope: paths::Scope,
59) -> io::Result<()> {
60 ensure_absolute_path(exe_path)?;
61
62 for browser_key in browsers {
63 let cfg = paths::browser_cfg(browser_key)?;
64 let manifest_path = paths::manifest_path(browser_key, scope, host_name)?;
65
66 if let Some(dir) = manifest_path.parent() {
67 fs::create_dir_all(dir)?;
68 }
69
70 match cfg.family.as_str() {
71 "chromium" => {
72 let m = ChromiumHostManifest {
73 name: host_name,
74 description,
75 path: exe_path.to_str().ok_or_else(|| {
76 io::Error::new(io::ErrorKind::InvalidInput, "exe_path is not valid UTF-8")
77 })?,
78 ty: "stdio",
79 allowed_origins: chrome_allowed_origins.to_vec(),
80 };
81 fs::write(&manifest_path, serde_json::to_vec_pretty(&m)?)?;
82 }
83 "firefox" => {
84 let m = FirefoxHostManifest {
85 name: host_name,
86 description,
87 path: exe_path.to_str().ok_or_else(|| {
88 io::Error::new(io::ErrorKind::InvalidInput, "exe_path is not valid UTF-8")
89 })?,
90 ty: "stdio",
91 allowed_extensions: firefox_allowed_extensions.to_vec(),
92 };
93 fs::write(&manifest_path, serde_json::to_vec_pretty(&m)?)?;
94 }
95 other => {
96 return Err(io::Error::new(
97 io::ErrorKind::InvalidData,
98 format!("unknown browser family '{other}' for browser '{browser_key}'"),
99 ));
100 }
101 }
102
103 #[cfg(windows)]
105 {
106 if cfg.windows_registry {
107 let key_path = paths::winreg_key_path(browser_key, scope, host_name)?;
108 crate::install::winreg::write_manifest_path_to_reg(
109 scope,
110 &key_path,
111 &manifest_path,
112 )?;
113 }
114 }
115 }
116
117 Ok(())
118}
119
120pub fn remove(host_name: &str, browsers: &[&str], scope: paths::Scope) -> io::Result<()> {
122 for browser_key in browsers {
123 let manifest_path = paths::manifest_path(browser_key, scope, host_name)?;
125 if manifest_path.exists() {
126 fs::remove_file(&manifest_path)?;
127 }
128
129 #[cfg(windows)]
131 {
132 let cfg = paths::browser_cfg(browser_key)?;
133 if cfg.windows_registry {
134 let key_path = paths::winreg_key_path(browser_key, scope, host_name)?;
135 crate::install::winreg::remove_manifest_reg(scope, &key_path).ok();
136 }
137 }
138 }
139 Ok(())
140}
141pub fn verify_installed(
145 host_name: &str,
146 browsers: Option<&[&str]>,
147 scope: paths::Scope,
148) -> io::Result<bool> {
149 let keys: Vec<&str> = match browsers {
150 Some(list) => list.to_vec(),
151 None => paths::config()
152 .browsers
153 .keys()
154 .map(|k| k.as_str())
155 .collect(),
156 };
157
158 for browser_key in keys {
159 if verify_one(browser_key, host_name, scope)? {
160 return Ok(true);
161 }
162 }
163 Ok(false)
164}
165
166fn verify_one(browser_key: &str, host_name: &str, scope: paths::Scope) -> io::Result<bool> {
167 let cfg = paths::browser_cfg(browser_key)?;
168
169 #[cfg(windows)]
171 let manifest_path = if cfg.windows_registry {
172 let key_path = paths::winreg_key_path(browser_key, scope, host_name)?;
173 match crate::install::winreg::read_manifest_path_from_reg(scope, &key_path)? {
174 Some(p) => p,
175 None => return Ok(false),
176 }
177 } else {
178 paths::manifest_path(browser_key, scope, host_name)?
179 };
180
181 #[cfg(not(windows))]
182 let manifest_path = paths::manifest_path(browser_key, scope, host_name)?;
183
184 if !manifest_path.exists() {
185 return Ok(false);
186 }
187
188 let data = fs::read_to_string(&manifest_path)?;
189 let v: Value = serde_json::from_str(&data).map_err(|e| {
190 io::Error::new(
191 io::ErrorKind::InvalidData,
192 format!("invalid JSON manifest: {e}"),
193 )
194 })?;
195
196 validate_manifest_json(&v, &cfg.family, host_name)
197}
198
199fn validate_manifest_json(v: &Value, family: &str, expected_name: &str) -> io::Result<bool> {
200 let obj = match v.as_object() {
201 Some(o) => o,
202 None => return Ok(false),
203 };
204
205 if obj.get("name").and_then(|x| x.as_str()) != Some(expected_name) {
206 return Ok(false);
207 }
208 if obj.get("type").and_then(|x| x.as_str()) != Some("stdio") {
209 return Ok(false);
210 }
211 if obj.get("path").and_then(|x| x.as_str()).is_none() {
212 return Ok(false);
213 }
214
215 match family {
216 "chromium" => {
217 if obj
218 .get("allowed_origins")
219 .and_then(|x| x.as_array())
220 .is_none()
221 {
222 return Ok(false);
223 }
224 if obj.contains_key("allowed_extensions") {
225 return Ok(false);
226 }
227 }
228 "firefox" => {
229 if obj
230 .get("allowed_extensions")
231 .and_then(|x| x.as_array())
232 .is_none()
233 {
234 return Ok(false);
235 }
236 if obj.contains_key("allowed_origins") {
237 return Ok(false);
238 }
239 }
240 _ => return Ok(false),
241 }
242
243 #[cfg(any(target_os = "macos", target_os = "linux"))]
244 {
245 let exe = obj.get("path").and_then(|x| x.as_str()).unwrap_or("");
246 if !std::path::Path::new(exe).is_absolute() {
247 return Ok(false);
248 }
249 }
250
251 Ok(true)
252}