native_messaging/install/
paths.rs

1use once_cell::sync::Lazy;
2use serde::Deserialize;
3use std::{collections::HashMap, fs, io, path::PathBuf};
4
5const DEFAULT_BROWSERS_TOML: &str = include_str!("browsers.toml");
6
7/// Optional override:
8/// - If NATIVE_MESSAGING_BROWSERS_CONFIG is set, load config from that path.
9/// - Otherwise use the embedded browsers.toml.
10fn load_browsers_toml() -> String {
11    if let Ok(p) = std::env::var("NATIVE_MESSAGING_BROWSERS_CONFIG") {
12        if let Ok(s) = fs::read_to_string(&p) {
13            return s;
14        }
15    }
16    DEFAULT_BROWSERS_TOML.to_string()
17}
18
19#[derive(Debug, Clone, Copy)]
20pub enum Scope {
21    User,
22    System,
23}
24
25#[derive(Debug, Deserialize)]
26pub struct Config {
27    pub schema_version: u32,
28    pub browsers: HashMap<String, BrowserCfg>,
29}
30
31#[derive(Debug, Deserialize)]
32pub struct BrowserCfg {
33    /// "chromium" or "firefox"
34    pub family: String,
35
36    /// Whether Windows registry pointers should be written
37    #[serde(default)]
38    pub windows_registry: bool,
39
40    pub paths: PathsByOs,
41
42    #[serde(default)]
43    pub windows: Option<WindowsCfg>,
44}
45
46#[derive(Debug, Deserialize)]
47pub struct WindowsCfg {
48    #[serde(default)]
49    pub registry: Option<RegistryCfg>,
50}
51
52#[derive(Debug, Deserialize)]
53pub struct RegistryCfg {
54    pub hkcu_key_template: Option<String>,
55    pub hklm_key_template: Option<String>,
56}
57
58#[derive(Debug, Deserialize)]
59pub struct PathsByOs {
60    pub macos: Option<Scopes>,
61    pub linux: Option<Scopes>,
62    pub windows: Option<Scopes>,
63}
64
65#[derive(Debug, Deserialize)]
66pub struct Scopes {
67    pub user: Option<PathEntry>,
68    pub system: Option<PathEntry>,
69}
70
71#[derive(Debug, Deserialize)]
72pub struct PathEntry {
73    pub dir: String,
74}
75
76static CONFIG: Lazy<Config> = Lazy::new(|| {
77    let raw = load_browsers_toml();
78    let cfg: Config = toml::from_str(&raw).expect("invalid browsers.toml");
79    if cfg.schema_version != 1 {
80        panic!(
81            "unsupported schema_version {} (expected 1)",
82            cfg.schema_version
83        );
84    }
85    cfg
86});
87
88pub fn config() -> &'static Config {
89    &CONFIG
90}
91
92pub fn browser_cfg(browser_key: &str) -> io::Result<&'static BrowserCfg> {
93    CONFIG.browsers.get(browser_key).ok_or_else(|| {
94        io::Error::new(
95            io::ErrorKind::InvalidInput,
96            format!("unknown browser: {browser_key}"),
97        )
98    })
99}
100
101/// Resolve the full manifest JSON path for this browser+scope+host.
102pub fn manifest_path(browser_key: &str, scope: Scope, host_name: &str) -> io::Result<PathBuf> {
103    let b = browser_cfg(browser_key)?;
104
105    let scopes = current_os_scopes(&b.paths)?.ok_or_else(|| {
106        io::Error::new(
107            io::ErrorKind::NotFound,
108            "browser not configured for this OS",
109        )
110    })?;
111
112    let entry = match scope {
113        Scope::User => scopes.user.as_ref(),
114        Scope::System => scopes.system.as_ref(),
115    }
116    .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "scope not configured for this OS"))?;
117
118    let dir = resolve_dir_template(&entry.dir)?;
119    Ok(dir.join(format!("{host_name}.json")))
120}
121
122fn current_os_scopes(paths: &PathsByOs) -> io::Result<Option<&Scopes>> {
123    #[cfg(target_os = "macos")]
124    {
125        Ok(paths.macos.as_ref())
126    }
127    #[cfg(target_os = "linux")]
128    {
129        Ok(paths.linux.as_ref())
130    }
131    #[cfg(target_os = "windows")]
132    {
133        Ok(paths.windows.as_ref())
134    }
135    #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
136    {
137        Err(io::Error::new(io::ErrorKind::Other, "unsupported OS"))
138    }
139}
140
141fn resolve_dir_template(t: &str) -> io::Result<PathBuf> {
142    let mut s = t.to_string();
143
144    // Only replace if referenced; error if referenced but env missing.
145    replace_var(&mut s, "{HOME}", "HOME")?;
146    replace_var(&mut s, "{LOCALAPPDATA}", "LOCALAPPDATA")?;
147    replace_var(&mut s, "{APPDATA}", "APPDATA")?;
148    replace_var(&mut s, "{PROGRAMDATA}", "PROGRAMDATA")?;
149
150    Ok(PathBuf::from(s))
151}
152
153fn replace_var(s: &mut String, token: &str, env: &str) -> io::Result<()> {
154    if s.contains(token) {
155        let v = std::env::var(env).map_err(|_| {
156            io::Error::new(
157                io::ErrorKind::NotFound,
158                format!("env var {env} not set (needed for {token})"),
159            )
160        })?;
161        *s = s.replace(token, &v);
162    }
163    Ok(())
164}
165
166#[cfg(windows)]
167pub fn winreg_key_path(browser_key: &str, scope: Scope, host_name: &str) -> io::Result<String> {
168    let b = browser_cfg(browser_key)?;
169    if !b.windows_registry {
170        return Err(io::Error::new(
171            io::ErrorKind::InvalidInput,
172            "registry not enabled for this browser",
173        ));
174    }
175
176    let reg = b
177        .windows
178        .as_ref()
179        .and_then(|w| w.registry.as_ref())
180        .ok_or_else(|| {
181            io::Error::new(
182                io::ErrorKind::NotFound,
183                "missing [browsers.<x>.windows.registry] config",
184            )
185        })?;
186
187    let tmpl = match scope {
188        Scope::User => reg.hkcu_key_template.as_ref(),
189        Scope::System => reg.hklm_key_template.as_ref(),
190    }
191    .ok_or_else(|| {
192        io::Error::new(
193            io::ErrorKind::NotFound,
194            "missing registry template for this scope",
195        )
196    })?;
197
198    Ok(tmpl.replace("{name}", host_name))
199}