1mod bsd;
11mod config_file;
12mod macos;
13mod man_db;
14use bsd::mandoc_configured_manual_roots;
15#[cfg(test)]
16use bsd::{BsdManConfig, macos_configuration_roots, parse_bsd_man_config, parse_mandoc_manpaths};
17#[cfg(test)]
18use macos::developer_manual_roots;
19use macos::macos_configured_manual_roots;
20use man_db::linux_configured_manual_roots;
21#[cfg(unix)]
22use man_db::unmapped_man_db_roots;
23#[cfg(test)]
24use man_db::{ManDbConfig, expand_man_db_systems, man_db_manual_roots, parse_man_db_config};
25mod expansion;
26mod windows_config;
27use config_file::read_text as read_config_text;
28#[cfg(test)]
29use expansion::wildcard_matches;
30use expansion::{ExpansionOutcome, ScanBudget, expand_path_pattern_bounded};
31
32use std::{
33 collections::HashMap,
34 env,
35 ffi::{OsStr, OsString},
36 fs,
37 path::{Path, PathBuf},
38};
39
40use crate::source::deduplicate_paths;
41
42#[cfg(unix)]
43const DEFAULT_UNIX_MANUAL_ROOTS: [&str; 4] = [
44 "/usr/local/share/man",
45 "/usr/local/man",
46 "/usr/share/man",
47 "/usr/man",
48];
49const MAX_MANUAL_PATH_CONFIG_BYTES: u64 = 1024 * 1024;
50const MAX_EXPANDED_CONFIG_PATHS: usize = 256;
51const MAX_EXPANDED_CONFIG_CANDIDATES: usize = 4096;
52
53#[derive(Clone, Debug, Eq, PartialEq)]
55pub struct ManualPathDiagnostic {
56 pub config_path: PathBuf,
58 pub line: Option<usize>,
60 pub message: String,
62}
63
64#[derive(Clone, Debug, Default, Eq, PartialEq)]
66pub struct ManualRootDiscovery {
67 pub roots: Vec<PathBuf>,
69 pub diagnostics: Vec<ManualPathDiagnostic>,
71}
72
73#[must_use]
80pub fn discover_manual_roots() -> Vec<PathBuf> {
81 inspect_manual_roots().roots
82}
83
84#[must_use]
91pub fn inspect_manual_roots() -> ManualRootDiscovery {
92 let environment = env::vars_os().collect::<HashMap<_, _>>();
93 if environment_value(&environment, "MANT_MANPATH").is_some() {
94 return ManualRootDiscovery {
95 roots: discover_manual_roots_from(&environment, Vec::new()),
96 diagnostics: Vec::new(),
97 };
98 }
99 if environment_value(&environment, "MANPATH")
100 .is_some_and(|value| env::split_paths(value).all(|path| !path.as_os_str().is_empty()))
101 {
102 return ManualRootDiscovery {
103 roots: discover_manual_roots_from(&environment, Vec::new()),
104 diagnostics: Vec::new(),
105 };
106 }
107
108 let platform = host_platform();
109 let mant_config = (platform == ManualPathPlatform::Windows)
110 .then(|| {
111 mant_sources::document_paths()
112 .ok()
113 .map(|paths| paths.root.join("man.conf"))
114 })
115 .flatten();
116 let context = DiscoveryContext {
117 environment: &environment,
118 platform,
119 mant_config: mant_config.as_deref(),
120 };
121 let defaults = host_default_manual_roots(&context);
122 ManualRootDiscovery {
123 roots: discover_manual_roots_from(&environment, defaults.roots),
124 diagnostics: defaults.diagnostics,
125 }
126}
127
128#[cfg(test)]
129pub(crate) fn discover_manual_roots_with(
130 environment: &HashMap<OsString, OsString>,
131) -> Vec<PathBuf> {
132 discover_manual_roots_from(environment, fallback_manual_roots(environment))
133}
134
135fn discover_manual_roots_from(
136 environment: &HashMap<OsString, OsString>,
137 defaults: Vec<PathBuf>,
138) -> Vec<PathBuf> {
139 discover_manual_roots_from_for(environment, defaults, host_platform())
140}
141
142fn discover_manual_roots_from_for(
143 environment: &HashMap<OsString, OsString>,
144 defaults: Vec<PathBuf>,
145 platform: ManualPathPlatform,
146) -> Vec<PathBuf> {
147 if let Some(explicit) = environment_value_for(environment, "MANT_MANPATH", platform) {
148 return deduplicate_manual_paths(
149 env::split_paths(explicit).filter(|path| !path.as_os_str().is_empty()),
150 platform,
151 );
152 }
153
154 if let Some(manpath) = environment_value_for(environment, "MANPATH", platform) {
155 let mut roots = Vec::new();
156 for path in env::split_paths(manpath) {
157 if path.as_os_str().is_empty() {
158 roots.extend(defaults.iter().cloned());
159 } else {
160 roots.push(path);
161 }
162 }
163 return deduplicate_manual_paths(roots, platform);
164 }
165 defaults
166}
167
168struct DiscoveryContext<'a> {
170 environment: &'a HashMap<OsString, OsString>,
171 platform: ManualPathPlatform,
172 mant_config: Option<&'a Path>,
173}
174
175fn host_default_manual_roots(context: &DiscoveryContext<'_>) -> ManualRootDiscovery {
176 let environment = context.environment;
177 let mut discovery = match context.platform {
178 ManualPathPlatform::Linux => linux_configured_manual_roots(environment),
179 ManualPathPlatform::Macos => macos_configured_manual_roots(environment),
180 ManualPathPlatform::Windows => mant_configured_manual_roots(context),
181 ManualPathPlatform::OtherUnix => mandoc_configured_manual_roots(Path::new("/etc/man.conf")),
182 };
183 if discovery.roots.is_empty() {
184 discovery.roots = if context.platform == ManualPathPlatform::Windows {
185 deduplicate_manual_paths(
186 supplemental_manual_roots_for(environment, context.platform),
187 context.platform,
188 )
189 } else {
190 fallback_manual_roots(environment)
191 };
192 } else {
193 discovery
194 .roots
195 .extend(supplemental_manual_roots_for(environment, context.platform));
196 discovery.roots = deduplicate_manual_paths(discovery.roots, context.platform);
197 }
198 discovery
199}
200
201#[derive(Clone, Copy, Debug, Eq, PartialEq)]
202enum ManualPathPlatform {
203 Linux,
204 Macos,
205 Windows,
206 OtherUnix,
207}
208
209fn environment_value<'a>(
210 environment: &'a HashMap<OsString, OsString>,
211 name: &str,
212) -> Option<&'a OsString> {
213 environment_value_for(environment, name, host_platform())
214}
215
216fn environment_value_for<'a>(
217 environment: &'a HashMap<OsString, OsString>,
218 name: &str,
219 platform: ManualPathPlatform,
220) -> Option<&'a OsString> {
221 environment.get(OsStr::new(name)).or_else(|| {
222 (platform == ManualPathPlatform::Windows).then(|| {
223 environment.iter().find_map(|(candidate, value)| {
224 candidate
225 .to_string_lossy()
226 .eq_ignore_ascii_case(name)
227 .then_some(value)
228 })
229 })?
230 })
231}
232
233fn deduplicate_manual_paths(
234 paths: impl IntoIterator<Item = PathBuf>,
235 platform: ManualPathPlatform,
236) -> Vec<PathBuf> {
237 let paths = paths.into_iter().collect::<Vec<_>>();
238 if platform == ManualPathPlatform::Windows {
239 windows_config::deduplicate_windows_paths(paths)
240 } else {
241 deduplicate_paths(paths)
242 }
243}
244
245const fn host_platform() -> ManualPathPlatform {
246 if cfg!(windows) {
247 ManualPathPlatform::Windows
248 } else if cfg!(target_os = "macos") {
249 ManualPathPlatform::Macos
250 } else if cfg!(target_os = "linux") {
251 ManualPathPlatform::Linux
252 } else {
253 ManualPathPlatform::OtherUnix
254 }
255}
256
257#[cfg(unix)]
258fn fallback_manual_roots(environment: &HashMap<OsString, OsString>) -> Vec<PathBuf> {
259 let mut roots = supplemental_manual_roots(environment);
260 roots.extend(path_derived_manual_roots(environment));
261 roots.extend(DEFAULT_UNIX_MANUAL_ROOTS.map(PathBuf::from));
262 deduplicate_paths(roots)
263}
264
265#[cfg(not(unix))]
266fn fallback_manual_roots(environment: &HashMap<OsString, OsString>) -> Vec<PathBuf> {
267 deduplicate_manual_paths(supplemental_manual_roots(environment), host_platform())
268}
269
270fn supplemental_manual_roots(environment: &HashMap<OsString, OsString>) -> Vec<PathBuf> {
271 supplemental_manual_roots_for(environment, host_platform())
272}
273
274fn supplemental_manual_roots_for(
275 environment: &HashMap<OsString, OsString>,
276 platform: ManualPathPlatform,
277) -> Vec<PathBuf> {
278 let mut roots = Vec::new();
279 if platform == ManualPathPlatform::Windows {
280 if let Some(data_root) =
281 environment_value_for(environment, "APPDATA", platform).map(PathBuf::from)
282 {
283 roots.push(data_root.join("ManT").join("man"));
284 }
285 if let Some(profile) =
286 environment_value_for(environment, "USERPROFILE", platform).map(PathBuf::from)
287 {
288 roots.push(profile.join(".local/share/man"));
289 }
290 return roots;
291 }
292
293 if let Some(home) = environment_value_for(environment, "HOME", platform).map(PathBuf::from) {
294 roots.push(home.join(".local/share/man"));
295 roots.push(home.join(".local/man"));
296 roots.push(home.join("man"));
297 }
298 if let Some(data_home) =
299 environment_value_for(environment, "XDG_DATA_HOME", platform).map(PathBuf::from)
300 {
301 roots.push(data_home.join("man"));
302 }
303 if let Some(data_dirs) = environment_value_for(environment, "XDG_DATA_DIRS", platform) {
304 roots.extend(env::split_paths(data_dirs).map(|root| root.join("man")));
305 }
306 roots
307}
308
309#[cfg(unix)]
310fn path_derived_manual_roots(environment: &HashMap<OsString, OsString>) -> Vec<PathBuf> {
311 let mut roots = Vec::new();
312 if let Some(path) = environment_value(environment, "PATH") {
313 for binary_dir in env::split_paths(path) {
314 roots.extend(unmapped_man_db_roots(&binary_dir));
315 }
316 }
317 roots
318}
319
320fn mant_configured_manual_roots(context: &DiscoveryContext<'_>) -> ManualRootDiscovery {
321 context
322 .mant_config
323 .map(|path| {
324 let executable_paths =
325 environment_value_for(context.environment, "PATH", context.platform)
326 .map(|value| env::split_paths(value).collect::<Vec<_>>())
327 .unwrap_or_default();
328 windows_config::load(path, context.environment, &executable_paths)
329 })
330 .unwrap_or_default()
331}
332
333fn read_config(path: &Path) -> Option<String> {
334 read_config_text(path, MAX_MANUAL_PATH_CONFIG_BYTES).ok()
335}
336
337fn read_path_list(path: &Path) -> Vec<PathBuf> {
338 read_config(path)
339 .map(|text| parse_path_list(&text))
340 .unwrap_or_default()
341}
342
343fn parse_path_list(text: &str) -> Vec<PathBuf> {
344 config_lines(text)
345 .take(MAX_EXPANDED_CONFIG_CANDIDATES)
346 .map(str::trim)
347 .filter(|path| !path.is_empty())
348 .map(PathBuf::from)
349 .collect()
350}
351
352fn config_lines(text: &str) -> impl Iterator<Item = &str> {
353 text.lines()
354 .map(str::trim)
355 .filter(|line| !line.is_empty() && !line.starts_with('#'))
356}
357
358fn config_directive(line: &str) -> Option<(&str, &str)> {
359 let (directive, value) = line.split_once(char::is_whitespace)?;
360 let value = value.trim();
361 (!value.is_empty()).then_some((directive, value))
362}
363
364#[cfg(all(test, windows))]
365fn expand_path_pattern(pattern: &Path) -> Vec<PathBuf> {
366 let mut budget = ScanBudget::new(MAX_EXPANDED_CONFIG_CANDIDATES);
367 expand_path_pattern_bounded(pattern, &mut budget)
368 .paths
369 .into_iter()
370 .take(MAX_EXPANDED_CONFIG_PATHS)
371 .collect()
372}
373
374#[cfg(test)]
375mod tests;