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
21pub(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
42fn open_browser_default(target: &TargetType, options: &BrowserOptions) -> Result<()> {
47 let url: &str = target;
48
49 try_with_browser_env(url, options)
51 .or_else(|_| try_haiku(options, url))
53 .or_else(|_| try_xdg(options, url))
55 .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 .or_else(|_| try_browser!(options, "x-www-browser", url))
81 .map_err(|_| {
83 Error::new(
84 ErrorKind::NotFound,
85 "No valid browsers detected. You can specify one in BROWSER environment variable",
86 )
87 })
88 .map(|_| ())
90}
91
92fn try_with_browser_env(url: &str, options: &BrowserOptions) -> Result<()> {
93 for browser in std::env::var("BROWSER")
95 .unwrap_or_else(|_| String::from(""))
96 .split(':')
97 {
98 if !browser.is_empty() {
99 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 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
129fn is_wsl() -> bool {
132 if cfg!(target_os = "linux") {
135 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 false
146 }
147}
148
149#[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
157fn 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"
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"
178 } else if xcd.contains("mate") || dsession.contains("mate") {
179 "mate"
181 } else if xcd.contains("xfce") || dsession.contains("xfce") {
182 "xfce"
184 } else if is_wsl() {
185 "wsl"
187 } else {
188 unknown
190 }
191}
192
193fn 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 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
235fn try_flatpak(options: &BrowserOptions, target: &TargetType) -> Result<()> {
237 match target.0.scheme() {
238 "http" | "https" => {
239 let url: &str = target;
240 try_browser!(options, "xdg-open", url)
243 }
244 _ => Err(Error::new(ErrorKind::NotFound, "only http urls supported")),
248 }
249}
250
251fn 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
261fn try_xdg(options: &BrowserOptions, url: &str) -> Result<()> {
264 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 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 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 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 config_found = true;
301 match open_using_xdg_config(&config_path, options, url) {
302 Ok(x) => return Ok(x), Err(err) => {
304 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
322fn 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 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 _ => (), }
349 }
350 }
351 }
352
353 if hidden {
354 return Err(Error::new(ErrorKind::NotFound, "xdg config is hidden"));
356 }
357
358 if let Some(cmdline) = cmdline {
359 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 cmd.arg(url);
377 }
378 run_command(&mut cmd, !requires_terminal, options)
379 })
380 } else {
381 Err(Error::new(ErrorKind::NotFound, "not a valid xdg config"))
383 }
384}
385
386fn 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
412fn 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 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 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 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 let flag_path = get_temp_path("test_xdg", "flag");
482 let _ = std::fs::remove_file(&flag_path);
483
484 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 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 let result = open_using_xdg_config(
534 &PathBuf::from(&config_path),
535 &BrowserOptions::default(),
536 &txt_path,
537 );
538
539 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 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 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#[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 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 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); 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 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 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 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 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 Ok(format!("C:\\{}", path.as_os_str().to_string_lossy()).replace('/', "\\"))
753 } else {
754 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 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 if let Ok(wsl_hostname) = std::env::var("WSL_DISTRO_NAME") {
771 Ok(wsl_hostname)
772 } else {
773 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 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 }