native_messaging/install/
paths.rs1use 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
7fn 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 pub family: String,
35
36 #[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
101pub 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 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}