webbrowser/
unix.rs

1use crate::common::run_command;
2use crate::{Browser, BrowserOptions, Error, ErrorKind, Result, TargetType};
3use log::trace;
4use std::io::{BufRead, BufReader};
5use std::os::unix::fs::PermissionsExt;
6use std::path::{Path, PathBuf, MAIN_SEPARATOR};
7use std::process::{Command, Stdio};
8
9macro_rules! try_browser {
10    ( $options: expr, $name:expr, $( $arg:expr ),+ ) => {
11        for_matching_path($name, |pb| {
12            let mut cmd = Command::new(pb);
13            $(
14                cmd.arg($arg);
15            )+
16            run_command(&mut cmd, !is_text_browser(&pb), $options)
17        })
18    }
19}
20
21/// Deal with opening of browsers on Linux and *BSD - currently supports only the default browser
22///
23/// The mechanism of opening the default browser is as follows:
24/// 1. Attempt to use $BROWSER env var if available
25/// 2. Attempt to use xdg-open
26/// 3. Attempt to use window manager specific commands, like gnome-open, kde-open etc. incl. WSL
27/// 4. Fallback to x-www-browser
28pub(super) fn open_browser_internal(
29    browser: Browser,
30    target: &TargetType,
31    options: &BrowserOptions,
32) -> Result<()> {
33    match browser {
34        Browser::Default => open_browser_default(target, options),
35        _ => Err(Error::new(
36            ErrorKind::NotFound,
37            "only default browser supported",
38        )),
39    }
40}
41
42/// Open the default browser.
43///
44/// [BrowserOptions::dry_run] is handled inside [run_command], as all execution paths eventually
45/// rely on it to execute.
46fn open_browser_default(target: &TargetType, options: &BrowserOptions) -> Result<()> {
47    let url: &str = target;
48
49    // we first try with the $BROWSER env
50    try_with_browser_env(url, options)
51        // allow for haiku's open specifically
52        .or_else(|_| try_haiku(options, url))
53        // then we try with xdg configuration
54        .or_else(|_| try_xdg(options, url))
55        // else do desktop specific stuff
56        .or_else(|r| match guess_desktop_env() {
57            "kde" => try_browser!(options, "kde-open", url)
58                .or_else(|_| try_browser!(options, "kde-open5", url))
59                .or_else(|_| try_browser!(options, "kfmclient", "newTab", url)),
60
61            "gnome" => try_browser!(options, "gio", "open", url)
62                .or_else(|_| try_browser!(options, "gvfs-open", url))
63                .or_else(|_| try_browser!(options, "gnome-open", url)),
64
65            "mate" => try_browser!(options, "gio", "open", url)
66                .or_else(|_| try_browser!(options, "gvfs-open", url))
67                .or_else(|_| try_browser!(options, "mate-open", url)),
68
69            "xfce" => try_browser!(options, "exo-open", url)
70                .or_else(|_| try_browser!(options, "gio", "open", url))
71                .or_else(|_| try_browser!(options, "gvfs-open", url)),
72
73            "wsl" => try_wsl(options, target),
74
75            "flatpak" => try_flatpak(options, target),
76
77            _ => Err(r),
78        })
79        // at the end, we'll try x-www-browser and return the result as is
80        .or_else(|_| try_browser!(options, "x-www-browser", url))
81        // if all above failed, map error to not found
82        .map_err(|_| {
83            Error::new(
84                ErrorKind::NotFound,
85                "No valid browsers detected. You can specify one in BROWSER environment variable",
86            )
87        })
88        // and convert a successful result into a ()
89        .map(|_| ())
90}
91
92fn try_with_browser_env(url: &str, options: &BrowserOptions) -> Result<()> {
93    // $BROWSER can contain ':' delimited options, each representing a potential browser command line
94    for browser in std::env::var("BROWSER")
95        .unwrap_or_else(|_| String::from(""))
96        .split(':')
97    {
98        if !browser.is_empty() {
99            // each browser command can have %s to represent URL, while %c needs to be replaced
100            // with ':' and %% with '%'
101            let cmdline = browser
102                .replace("%s", url)
103                .replace("%c", ":")
104                .replace("%%", "%");
105            let cmdarr: Vec<&str> = cmdline.split_ascii_whitespace().collect();
106            let browser_cmd = cmdarr[0];
107            let env_exit = for_matching_path(browser_cmd, |pb| {
108                let mut cmd = Command::new(pb);
109                for arg in cmdarr.iter().skip(1) {
110                    cmd.arg(arg);
111                }
112                if !browser.contains("%s") {
113                    // append the url as an argument only if it was not already set via %s
114                    cmd.arg(url);
115                }
116                run_command(&mut cmd, !is_text_browser(pb), options)
117            });
118            if env_exit.is_ok() {
119                return Ok(());
120            }
121        }
122    }
123    Err(Error::new(
124        ErrorKind::NotFound,
125        "No valid browser configured in BROWSER environment variable",
126    ))
127}
128
129/// Check if we are inside WSL on Windows, and interoperability with Windows tools is
130/// enabled.
131fn is_wsl() -> bool {
132    // we should check in procfs only on linux, as for non-linux it will likely be
133    // a disk hit, which we should avoid.
134    if cfg!(target_os = "linux") {
135        // we check if interop with windows tools is allowed, as if it isn't, we won't
136        // be able to invoke windows commands anyways.
137        // See: https://learn.microsoft.com/en-us/windows/wsl/filesystems#disable-interoperability
138        if let Ok(s) = std::fs::read_to_string("/proc/sys/fs/binfmt_misc/WSLInterop") {
139            s.contains("enabled")
140        } else {
141            false
142        }
143    } else {
144        // we short-circuit and return false on non-linux
145        false
146    }
147}
148
149/// Check if we're running inside Flatpak
150#[inline]
151fn is_flatpak() -> bool {
152    std::env::var("container")
153        .map(|x| x.eq_ignore_ascii_case("flatpak"))
154        .unwrap_or(false)
155}
156
157/// Detect the desktop environment
158fn guess_desktop_env() -> &'static str {
159    let unknown = "unknown";
160    let xcd: String = std::env::var("XDG_CURRENT_DESKTOP")
161        .unwrap_or_else(|_| unknown.into())
162        .to_ascii_lowercase();
163    let dsession: String = std::env::var("DESKTOP_SESSION")
164        .unwrap_or_else(|_| unknown.into())
165        .to_ascii_lowercase();
166
167    if is_flatpak() {
168        "flatpak"
169    } else if xcd.contains("gnome") || xcd.contains("cinnamon") || dsession.contains("gnome") {
170        // GNOME and its derivatives
171        "gnome"
172    } else if xcd.contains("kde")
173        || std::env::var("KDE_FULL_SESSION").is_ok()
174        || std::env::var("KDE_SESSION_VERSION").is_ok()
175    {
176        // KDE: https://userbase.kde.org/KDE_System_Administration/Environment_Variables#Automatically_Set_Variables
177        "kde"
178    } else if xcd.contains("mate") || dsession.contains("mate") {
179        // We'll treat MATE as distinct from GNOME due to mate-open
180        "mate"
181    } else if xcd.contains("xfce") || dsession.contains("xfce") {
182        // XFCE
183        "xfce"
184    } else if is_wsl() {
185        // WSL
186        "wsl"
187    } else {
188        // All others
189        unknown
190    }
191}
192
193/// Open browser in WSL environments
194fn try_wsl(options: &BrowserOptions, target: &TargetType) -> Result<()> {
195    match target.0.scheme() {
196        "http" | "https" => {
197            let url: &str = target;
198            try_browser!(
199                options,
200                "cmd.exe",
201                "/c",
202                "start",
203                url.replace('^', "^^").replace('&', "^&")
204            )
205            .or_else(|_| {
206                try_browser!(
207                    options,
208                    "powershell.exe",
209                    "Start",
210                    url.replace('&', "\"&\"")
211                )
212            })
213            .or_else(|_| try_browser!(options, "wsl-open", url))
214        }
215        #[cfg(all(
216            target_os = "linux",
217            not(feature = "hardened"),
218            not(feature = "disable-wsl")
219        ))]
220        "file" => {
221            // we'll need to detect the default browser and then invoke it
222            // with wsl translated path
223            let wc = wsl::get_wsl_win_config()?;
224            let mut cmd = if wc.powershell_path.is_some() {
225                wsl::get_wsl_windows_browser_ps(&wc, target)
226            } else {
227                wsl::get_wsl_windows_browser_cmd(&wc, target)
228            }?;
229            run_command(&mut cmd, true, options)
230        }
231        _ => Err(Error::new(ErrorKind::NotFound, "invalid browser")),
232    }
233}
234
235/// Open browser in Flatpak environments
236fn try_flatpak(options: &BrowserOptions, target: &TargetType) -> Result<()> {
237    match target.0.scheme() {
238        "http" | "https" => {
239            let url: &str = target;
240            // we assume xdg-open to be present, given that it's a part of standard
241            // runtime & SDK of flatpak
242            try_browser!(options, "xdg-open", url)
243        }
244        // we support only http urls under Flatpak to adhere to the defined
245        // Consistent Behaviour, as effectively DBUS is used interally, and
246        // there doesn't seem to be a way for us to determine actual browser
247        _ => Err(Error::new(ErrorKind::NotFound, "only http urls supported")),
248    }
249}
250
251/// Handle Haiku explicitly, as it uses an "open" command, similar to macos
252/// but on other Unixes, open ends up translating to shell open fd
253fn try_haiku(options: &BrowserOptions, url: &str) -> Result<()> {
254    if cfg!(target_os = "haiku") {
255        try_browser!(options, "open", url).map(|_| ())
256    } else {
257        Err(Error::new(ErrorKind::NotFound, "Not on haiku"))
258    }
259}
260
261/// Dig into XDG settings (if xdg is available) to force it to open the browser, instead of
262/// the default application
263fn try_xdg(options: &BrowserOptions, url: &str) -> Result<()> {
264    // run: xdg-settings get default-web-browser
265    let browser_name_os = for_matching_path("xdg-settings", |pb| {
266        Command::new(pb)
267            .args(["get", "default-web-browser"])
268            .stdin(Stdio::null())
269            .stderr(Stdio::null())
270            .output()
271    })
272    .map_err(|_| Error::new(ErrorKind::NotFound, "unable to determine xdg browser"))?
273    .stdout;
274
275    // convert browser name to a utf-8 string and trim off the trailing newline
276    let browser_name = String::from_utf8(browser_name_os)
277        .map_err(|_| Error::new(ErrorKind::NotFound, "invalid default browser name"))?
278        .trim()
279        .to_owned();
280    if browser_name.is_empty() {
281        return Err(Error::new(ErrorKind::NotFound, "no default xdg browser"));
282    }
283    trace!("found xdg browser: {:?}", &browser_name);
284
285    // search for the config file corresponding to this browser name
286    let mut config_found = false;
287    let app_suffix = "applications";
288    for xdg_dir in get_xdg_dirs().iter_mut() {
289        let mut config_path = xdg_dir.join(app_suffix).join(&browser_name);
290        trace!("checking for xdg config at {config_path:?}");
291        let mut metadata = config_path.metadata();
292        if metadata.is_err() && browser_name.contains('-') {
293            // as per the spec, we need to replace '-' with /
294            let child_path = browser_name.replace('-', "/");
295            config_path = xdg_dir.join(app_suffix).join(child_path);
296            metadata = config_path.metadata();
297        }
298        if metadata.is_ok() {
299            // we've found the config file, so we try running using that
300            config_found = true;
301            match open_using_xdg_config(&config_path, options, url) {
302                Ok(x) => return Ok(x), // return if successful
303                Err(err) => {
304                    // if we got an error other than NotFound, then we short
305                    // circuit, and do not try any more options, else we
306                    // continue to try more
307                    if err.kind() != ErrorKind::NotFound {
308                        return Err(err);
309                    }
310                }
311            }
312        }
313    }
314
315    if config_found {
316        Err(Error::new(ErrorKind::Other, "xdg-open failed"))
317    } else {
318        Err(Error::new(ErrorKind::NotFound, "no valid xdg config found"))
319    }
320}
321
322/// Opens `url` using xdg configuration found in `config_path`
323///
324/// See https://specifications.freedesktop.org/desktop-entry-spec/latest for details
325fn open_using_xdg_config(config_path: &PathBuf, options: &BrowserOptions, url: &str) -> Result<()> {
326    let file = std::fs::File::open(config_path)?;
327    let mut in_desktop_entry = false;
328    let mut hidden = false;
329    let mut cmdline: Option<String> = None;
330    let mut requires_terminal = false;
331
332    // we capture important keys under the [Desktop Entry] section, as defined under:
333    // https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s06.html
334    for line in BufReader::new(file).lines().map_while(Result::ok) {
335        if line == "[Desktop Entry]" {
336            in_desktop_entry = true;
337        } else if line.starts_with('[') {
338            in_desktop_entry = false;
339        } else if in_desktop_entry && !line.starts_with('#') {
340            if let Some(idx) = line.find('=') {
341                let key = &line[..idx];
342                let value = &line[idx + 1..];
343                match key {
344                    "Exec" => cmdline = Some(value.to_owned()),
345                    "Hidden" => hidden = value == "true",
346                    "Terminal" => requires_terminal = value == "true",
347                    _ => (), // ignore
348                }
349            }
350        }
351    }
352
353    if hidden {
354        // we ignore this config if it was marked hidden/deleted
355        return Err(Error::new(ErrorKind::NotFound, "xdg config is hidden"));
356    }
357
358    if let Some(cmdline) = cmdline {
359        // we have a valid configuration
360        let cmdarr: Vec<&str> = cmdline.split_ascii_whitespace().collect();
361        let browser_cmd = cmdarr[0];
362        for_matching_path(browser_cmd, |pb| {
363            let mut cmd = Command::new(pb);
364            let mut url_added = false;
365            for arg in cmdarr.iter().skip(1) {
366                match *arg {
367                    "%u" | "%U" | "%f" | "%F" => {
368                        url_added = true;
369                        cmd.arg(url)
370                    }
371                    _ => cmd.arg(arg),
372                };
373            }
374            if !url_added {
375                // append the url as an argument only if it was not already set
376                cmd.arg(url);
377            }
378            run_command(&mut cmd, !requires_terminal, options)
379        })
380    } else {
381        // we don't have a valid config
382        Err(Error::new(ErrorKind::NotFound, "not a valid xdg config"))
383    }
384}
385
386/// Get the list of directories in which the desktop file needs to be searched
387fn get_xdg_dirs() -> Vec<PathBuf> {
388    let mut xdg_dirs: Vec<PathBuf> = Vec::new();
389
390    #[allow(deprecated)]
391    let data_home = std::env::var("XDG_DATA_HOME")
392        .ok()
393        .map(PathBuf::from)
394        .filter(|path| path.is_absolute())
395        .or_else(|| std::env::home_dir().map(|path| path.join(".local/share")));
396    if let Some(data_home) = data_home {
397        xdg_dirs.push(data_home);
398    }
399
400    if let Ok(data_dirs) = std::env::var("XDG_DATA_DIRS") {
401        for d in data_dirs.split(':') {
402            xdg_dirs.push(PathBuf::from(d));
403        }
404    } else {
405        xdg_dirs.push(PathBuf::from("/usr/local/share"));
406        xdg_dirs.push(PathBuf::from("/usr/share"));
407    }
408
409    xdg_dirs
410}
411
412/// Returns true if specified command refers to a known list of text browsers
413fn is_text_browser(pb: &Path) -> bool {
414    for browser in TEXT_BROWSERS.iter() {
415        if pb.ends_with(browser) {
416            return true;
417        }
418    }
419    false
420}
421
422fn for_matching_path<F, T>(name: &str, op: F) -> Result<T>
423where
424    F: FnOnce(&PathBuf) -> Result<T>,
425{
426    let err = Err(Error::new(ErrorKind::NotFound, "command not found"));
427
428    // if the name already includes path separator, we should not try to do a PATH search on it
429    // as it's likely an absolutely or relative name, so we treat it as such.
430    if name.contains(MAIN_SEPARATOR) {
431        let pb = std::path::PathBuf::from(name);
432        if let Ok(metadata) = pb.metadata() {
433            if metadata.is_file() && metadata.permissions().mode() & 0o111 != 0 {
434                return op(&pb);
435            }
436        } else {
437            return err;
438        }
439    } else {
440        // search for this name inside PATH
441        if let Ok(path) = std::env::var("PATH") {
442            for entry in path.split(':') {
443                let mut pb = std::path::PathBuf::from(entry);
444                pb.push(name);
445                if let Ok(metadata) = pb.metadata() {
446                    if metadata.is_file() && metadata.permissions().mode() & 0o111 != 0 {
447                        return op(&pb);
448                    }
449                }
450            }
451        }
452    }
453    // return the not found err, if we didn't find anything above
454    err
455}
456
457static TEXT_BROWSERS: [&str; 9] = [
458    "lynx", "links", "links2", "elinks", "w3m", "eww", "netrik", "retawq", "curl",
459];
460
461#[cfg(test)]
462mod tests_xdg {
463    use super::*;
464    use std::fs::File;
465    use std::io::Write;
466
467    fn get_temp_path(name: &str, suffix: &str) -> String {
468        let pid = std::process::id();
469        std::env::temp_dir()
470            .join(format!("{name}.{pid}.{suffix}"))
471            .into_os_string()
472            .into_string()
473            .expect("failed to convert into string")
474    }
475
476    #[test]
477    fn test_xdg_open_local_file() {
478        let _ = env_logger::try_init();
479
480        // ensure flag file is not existing
481        let flag_path = get_temp_path("test_xdg", "flag");
482        let _ = std::fs::remove_file(&flag_path);
483
484        // create browser script
485        let txt_path = get_temp_path("test_xdf", "txt");
486        let browser_path = get_temp_path("test_xdg", "browser");
487        {
488            let mut browser_file =
489                File::create(&browser_path).expect("failed to create browser file");
490            let _ = browser_file.write_fmt(format_args!(
491                r#"#!/bin/bash
492                if [ "$1" != "p1" ]; then
493                    echo "1st parameter should've been p1" >&2
494                    exit 1
495                elif [ "$2" != "{}" ]; then
496                    echo "2nd parameter should've been {}" >&2
497                    exit 1
498                elif [ "$3" != "p3" ]; then
499                    echo "3rd parameter should've been p3" >&2
500                    exit 1
501                fi
502
503                echo "$2" > "{}"
504            "#,
505                &txt_path, &txt_path, &flag_path
506            ));
507            let mut perms = browser_file
508                .metadata()
509                .expect("failed to get permissions")
510                .permissions();
511            perms.set_mode(0o755);
512            let _ = browser_file.set_permissions(perms);
513        }
514
515        // create xdg desktop config
516        let config_path = get_temp_path("test_xdg", "desktop");
517        {
518            let mut xdg_file =
519                std::fs::File::create(&config_path).expect("failed to create xdg desktop file");
520            let _ = xdg_file.write_fmt(format_args!(
521                r#"# this line should be ignored
522[Desktop Entry]
523Exec={} p1 %u p3
524[Another Entry]
525Exec=/bin/ls
526# the above Exec line should be getting ignored
527            "#,
528                &browser_path
529            ));
530        }
531
532        // now try opening browser using above desktop config
533        let result = open_using_xdg_config(
534            &PathBuf::from(&config_path),
535            &BrowserOptions::default(),
536            &txt_path,
537        );
538
539        // we need to wait until the flag file shows up due to the async
540        // nature of browser invocation
541        for _ in 0..10 {
542            if std::fs::read_to_string(&flag_path).is_ok() {
543                break;
544            }
545            std::thread::sleep(std::time::Duration::from_millis(500));
546        }
547        std::thread::sleep(std::time::Duration::from_millis(500));
548
549        // validate that the flag file contains the url we passed
550        assert_eq!(
551            std::fs::read_to_string(&flag_path)
552                .expect("flag file not found")
553                .trim(),
554            &txt_path,
555        );
556        assert!(result.is_ok());
557
558        // delete all temp files
559        let _ = std::fs::remove_file(&txt_path);
560        let _ = std::fs::remove_file(&flag_path);
561        let _ = std::fs::remove_file(&browser_path);
562        let _ = std::fs::remove_file(&config_path);
563
564        assert!(result.is_ok());
565    }
566}
567
568/// WSL related browser functionality.
569///
570/// We treat it as a separate submod, to allow for easy logical grouping
571/// and to enable/disable based on some feature easily in future.
572#[cfg(all(
573    target_os = "linux",
574    not(feature = "hardened"),
575    not(feature = "disable-wsl")
576))]
577mod wsl {
578    use crate::common::for_each_token;
579    use crate::{Result, TargetType};
580    use std::io::{Error, ErrorKind};
581    use std::path::{Path, PathBuf};
582    use std::process::{Command, Stdio};
583
584    pub(super) struct WindowsConfig {
585        root: PathBuf,
586        cmd_path: PathBuf,
587        pub(super) powershell_path: Option<PathBuf>,
588    }
589
590    /// Returns a [WindowsConfig] by iterating over PATH entries. This seems to be
591    /// the fastest way to determine this.
592    pub(super) fn get_wsl_win_config() -> Result<WindowsConfig> {
593        let err_fn = || Error::new(ErrorKind::NotFound, "invalid windows config");
594        if let Some(path_env) = std::env::var_os("PATH") {
595            let mut root: Option<PathBuf> = None;
596            let mut cmd_path: Option<PathBuf> = None;
597            let mut powershell_path: Option<PathBuf> = None;
598            for path in std::env::split_paths(&path_env) {
599                let path_s = path.to_string_lossy().to_ascii_lowercase();
600                let path_s = path_s.trim_end_matches('/');
601                if path_s.ends_with("/windows/system32") {
602                    root = Some(std::fs::canonicalize(path.join("../.."))?);
603                    cmd_path = Some(path.join("cmd.exe"));
604                    break;
605                }
606            }
607            if let Some(ref root) = root {
608                for path in std::env::split_paths(&path_env) {
609                    if path.starts_with(root) {
610                        let pb = path.join("powershell.exe");
611                        if pb.is_file() {
612                            powershell_path = Some(pb);
613                        }
614                    }
615                }
616            }
617            if let Some(root) = root {
618                let cmd_path = cmd_path.unwrap_or_else(|| (root).join("windows/system32/cmd.exe"));
619                Ok(WindowsConfig {
620                    root,
621                    cmd_path,
622                    powershell_path,
623                })
624            } else {
625                Err(err_fn())
626            }
627        } else {
628            Err(err_fn())
629        }
630    }
631
632    /// Try to get default browser command from powershell.exe
633    pub(super) fn get_wsl_windows_browser_ps(
634        wc: &WindowsConfig,
635        url: &TargetType,
636    ) -> Result<Command> {
637        let err_fn = || Error::new(ErrorKind::NotFound, "powershell.exe error");
638        let ps_exe = wc.powershell_path.as_ref().ok_or_else(err_fn)?;
639        let mut cmd = Command::new(ps_exe);
640        cmd.arg("-NoLogo")
641            .arg("-NoProfile")
642            .arg("-NonInteractive")
643            .arg("-Command")
644            .arg("-")
645            .stdin(Stdio::piped())
646            .stdout(Stdio::piped())
647            .stderr(Stdio::null());
648        log::debug!("running command: ${:?}", &cmd);
649        let mut child = cmd.spawn()?;
650
651        let mut stdin = child.stdin.take().ok_or_else(err_fn)?;
652        std::io::Write::write_all(&mut stdin, WSL_PS_SCRIPT.as_bytes())?;
653        drop(stdin); // flush to stdin, and close
654        let output_u8 = child.wait_with_output()?;
655        let output = String::from_utf8_lossy(&output_u8.stdout);
656        let output = output.trim();
657        if output.is_empty() {
658            Err(err_fn())
659        } else {
660            parse_wsl_cmdline(wc, output, url)
661        }
662    }
663
664    /// Try to get default browser command from cmd.exe
665    pub(super) fn get_wsl_windows_browser_cmd(
666        wc: &WindowsConfig,
667        url: &TargetType,
668    ) -> Result<Command> {
669        let err_fn = || Error::new(ErrorKind::NotFound, "cmd.exe error");
670        let mut cmd = Command::new(&wc.cmd_path);
671        cmd.arg("/Q")
672            .arg("/C")
673            .arg("ftype http")
674            .stdin(Stdio::null())
675            .stdout(Stdio::piped())
676            .stderr(Stdio::null());
677        log::debug!("running command: ${:?}", &cmd);
678        let output_u8 = cmd.output()?;
679
680        let output = String::from_utf8_lossy(&output_u8.stdout);
681        let output = output.trim();
682        if output.is_empty() {
683            Err(err_fn())
684        } else {
685            parse_wsl_cmdline(wc, output, url)
686        }
687    }
688
689    /// Given the configured command line `cmdline` in registry, and the given `url`,
690    /// return the appropriate `Command` to invoke
691    fn parse_wsl_cmdline(wc: &WindowsConfig, cmdline: &str, url: &TargetType) -> Result<Command> {
692        let mut tokens: Vec<String> = Vec::new();
693        let filepath = wsl_get_filepath_from_url(wc, url)?;
694        let fp = &filepath;
695        for_each_token(cmdline, |token: &str| {
696            if matches!(token, "%0" | "%1") {
697                tokens.push(fp.to_owned());
698            } else {
699                tokens.push(token.to_string());
700            }
701        });
702        if tokens.is_empty() {
703            Err(Error::new(ErrorKind::NotFound, "invalid command"))
704        } else {
705            let progpath = wsl_path_win2lin(wc, &tokens[0])?;
706            let mut cmd = Command::new(progpath);
707            if tokens.len() > 1 {
708                cmd.args(&tokens[1..]);
709            }
710            Ok(cmd)
711        }
712    }
713
714    fn wsl_get_filepath_from_url(wc: &WindowsConfig, target: &TargetType) -> Result<String> {
715        let url = &target.0;
716        if url.scheme() == "file" {
717            if url.host().is_none() {
718                let path = url
719                    .to_file_path()
720                    .map_err(|_| Error::new(ErrorKind::NotFound, "invalid path"))?;
721                wsl_path_lin2win(wc, path)
722            } else {
723                Ok(format!("\\\\wsl${}", url.path().replace('/', "\\")))
724            }
725        } else {
726            Ok(url.as_str().to_string())
727        }
728    }
729
730    /// Converts a windows path to linux `PathBuf`
731    fn wsl_path_win2lin(wc: &WindowsConfig, path: &str) -> Result<PathBuf> {
732        let err_fn = || Error::new(ErrorKind::NotFound, "invalid windows path");
733        if path.len() > 3 {
734            let pfx = &path[..3];
735            if matches!(pfx, "C:\\" | "c:\\") {
736                let win_path = path[3..].replace('\\', "/");
737                Ok(wc.root.join(win_path))
738            } else {
739                Err(err_fn())
740            }
741        } else {
742            Err(err_fn())
743        }
744    }
745
746    /// Converts a linux path to windows. We using `String` instead of `OsString` as
747    /// return type because the `OsString` will be different b/w Windows & Linux.
748    fn wsl_path_lin2win(wc: &WindowsConfig, path: impl AsRef<Path>) -> Result<String> {
749        let path = path.as_ref();
750        if let Ok(path) = path.strip_prefix(&wc.root) {
751            // windows can access this path directly
752            Ok(format!("C:\\{}", path.as_os_str().to_string_lossy()).replace('/', "\\"))
753        } else {
754            // windows needs to access it via network
755            let wsl_hostname = get_wsl_distro_name(wc)?;
756            Ok(format!(
757                "\\\\wsl$\\{}{}",
758                &wsl_hostname,
759                path.as_os_str().to_string_lossy()
760            )
761            .replace('/', "\\"))
762        }
763    }
764
765    /// Gets the WSL distro name
766    fn get_wsl_distro_name(wc: &WindowsConfig) -> Result<String> {
767        let err_fn = || Error::new(ErrorKind::Other, "unable to determine wsl distro name");
768
769        // mostly we should be able to get it from the WSL_DISTRO_NAME env var
770        if let Ok(wsl_hostname) = std::env::var("WSL_DISTRO_NAME") {
771            Ok(wsl_hostname)
772        } else {
773            // but if not (e.g. if we were running as root), we can invoke
774            // powershell.exe to determine pwd and from there infer the distro name
775            let psexe = wc.powershell_path.as_ref().ok_or_else(err_fn)?;
776            let mut cmd = Command::new(psexe);
777            cmd.arg("-NoLogo")
778                .arg("-NoProfile")
779                .arg("-NonInteractive")
780                .arg("-Command")
781                .arg("$loc = Get-Location\nWrite-Output $loc.Path")
782                .current_dir("/")
783                .stdin(Stdio::null())
784                .stderr(Stdio::null());
785            log::debug!("running command: ${:?}", &cmd);
786            let output_u8 = cmd.output()?.stdout;
787            let output = String::from_utf8_lossy(&output_u8);
788            let output = output.trim_end_matches('\\');
789            let idx = output.find("::\\\\").ok_or_else(err_fn)?;
790            Ok((output[idx + 9..]).trim().to_string())
791        }
792    }
793
794    /// Powershell script to get the default browser command.
795    ///
796    /// Adapted from https://stackoverflow.com/a/60972216
797    const WSL_PS_SCRIPT: &str = r#"
798$Signature = @"
799using System;
800using System.Runtime.InteropServices;
801using System.Text;
802public static class Win32Api
803{
804
805    [DllImport("Shlwapi.dll", SetLastError = true, CharSet = CharSet.Auto)]
806    static extern uint AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string pszExtra,[Out] System.Text.StringBuilder pszOut, ref uint pcchOut);
807
808    public static string GetDefaultBrowser()
809    {
810        AssocF assocF = AssocF.IsProtocol;
811        AssocStr association = AssocStr.Command;
812        string assocString = "http";
813
814        uint length = 1024; // we assume 1k is sufficient memory to hold the command
815        var sb = new System.Text.StringBuilder((int) length);
816        uint ret = ret = AssocQueryString(assocF, association, assocString, null, sb, ref length);
817
818        return (ret != 0) ? null : sb.ToString();
819    }
820
821    [Flags]
822    internal enum AssocF : uint
823    {
824        IsProtocol = 0x1000,
825    }
826
827    internal enum AssocStr
828    {
829        Command = 1,
830        Executable,
831    }
832}
833"@
834
835Add-Type -TypeDefinition $Signature
836
837Write-Output $([Win32Api]::GetDefaultBrowser())
838"#;
839
840    /*#[cfg(test)]
841    mod tests {
842        use crate::open;
843
844        #[test]
845        fn test_url() {
846            let _ = env_logger::try_init();
847            assert!(open("https://github.com").is_ok());
848        }
849
850        #[test]
851        fn test_linux_file() {
852            let _ = env_logger::try_init();
853            assert!(open("abc.html").is_ok());
854        }
855
856        #[test]
857        fn test_windows_file() {
858            let _ = env_logger::try_init();
859            assert!(open("/mnt/c/T/abc.html").is_ok());
860        }
861    }*/
862}