native_messaging/install/
manifest.rs

1use 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, // "stdio"
14    allowed_origins: Vec<String>, // chrome-extension://<id>/
15}
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, // "stdio"
24    allowed_extensions: Vec<String>, // Firefox add-on IDs
25}
26
27fn ensure_absolute_path(exe_path: &Path) -> io::Result<()> {
28    // On macOS/Linux the manifest "path" MUST be absolute.
29    #[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
47/// Install manifests for the given browser keys (from browsers.toml).
48///
49/// - `chrome_allowed_origins` is used for `family="chromium"` browsers
50/// - `firefox_allowed_extensions` is used for `family="firefox"` browsers
51pub 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        // On Windows, write registry pointer if configured.
104        #[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
120/// Remove manifests + registry keys for the given browser keys.
121pub fn remove(host_name: &str, browsers: &[&str], scope: paths::Scope) -> io::Result<()> {
122    for browser_key in browsers {
123        // Remove file (best-effort if missing)
124        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        // Remove registry pointer if configured.
130        #[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}
141/// Verify installation for a host across browsers.
142/// - If `browsers` is None, checks all configured browsers in `browsers.toml`.
143/// - On Windows, if `windows_registry=true`, verification is registry-aware.
144pub 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    // Determine manifest path
170    #[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}