use crate::{Browser, BrowserOptions, Error, ErrorKind, Result, TargetType};
use core_foundation::array::{CFArray, CFArrayRef};
use core_foundation::base::TCFType;
use core_foundation::error::{CFError, CFErrorRef};
use core_foundation::url::{CFURLRef, CFURL};
use std::os::raw::c_void;
pub(super) fn open_browser_internal(
browser: Browser,
target: &TargetType,
options: &BrowserOptions,
) -> Result<()> {
let browser_cf_url = match browser {
Browser::Firefox => create_cf_url("file:///Applications/Firefox.app/"),
Browser::Chrome => create_cf_url("file:///Applications/Google Chrome.app/"),
Browser::Opera => create_cf_url("file:///Applications/Opera.app/"),
Browser::Safari => create_cf_url("file:///Applications/Safari.app/"),
Browser::Default => {
if let Some(dummy_url) = create_cf_url("https://") {
let mut err: CFErrorRef = std::ptr::null_mut();
let result = unsafe {
LSCopyDefaultApplicationURLForURL(
dummy_url.as_concrete_TypeRef(),
LSROLE_VIEWER,
&mut err,
)
};
if result.is_null() {
log::error!("failed to get default browser: {}", unsafe {
CFError::wrap_under_create_rule(err)
});
create_cf_url(DEFAULT_BROWSER_URL)
} else {
let cf_url = unsafe { CFURL::wrap_under_create_rule(result) };
log::trace!("default browser is {:?}", &cf_url);
Some(cf_url)
}
} else {
create_cf_url(DEFAULT_BROWSER_URL)
}
}
_ => {
return Err(Error::new(
ErrorKind::NotFound,
"browser not supported on macos",
))
}
}
.ok_or_else(|| Error::other("failed to create CFURL"))?;
let cf_url =
create_cf_url(target.as_ref()).ok_or_else(|| Error::other("failed to create CFURL"))?;
let urls_v = [cf_url];
let urls_arr = CFArray::<CFURL>::from_CFTypes(&urls_v);
let mut launch_flags = LS_LAUNCH_FLAG_DEFAULTS | LS_LAUNCH_FLAG_ASYNC;
if options.dont_switch {
launch_flags |= LS_LAUNCH_FLAG_DONT_SWITCH;
}
let spec = LSLaunchURLSpec {
app_url: browser_cf_url.as_concrete_TypeRef(),
item_urls: urls_arr.as_concrete_TypeRef(),
pass_thru_params: std::ptr::null(),
launch_flags,
async_ref_con: std::ptr::null(),
};
if options.dry_run {
return if let Some(path) = browser_cf_url.to_path() {
if path.is_dir() {
log::debug!("dry-run: not actually opening the browser {}", &browser);
Ok(())
} else {
log::debug!("dry-run: browser {} not found", &browser);
Err(Error::new(ErrorKind::NotFound, "browser not found"))
}
} else {
Err(Error::other("unable to convert app url to path"))
};
}
log::trace!("about to start browser: {} for {}", &browser, &target);
let mut launched_app: CFURLRef = std::ptr::null_mut();
let status = unsafe { LSOpenFromURLSpec(&spec, &mut launched_app) };
log::trace!("received status: {status}");
if status == 0 {
Ok(())
} else {
Err(Error::from(LSError::from(status)))
}
}
fn create_cf_url(url: &str) -> Option<CFURL> {
let url_u8 = url.as_bytes();
let url_ref = unsafe {
core_foundation::url::CFURLCreateWithBytes(
std::ptr::null(),
url_u8.as_ptr(),
url_u8.len() as isize,
core_foundation::string::kCFStringEncodingUTF8,
std::ptr::null(),
)
};
if url_ref.is_null() {
None
} else {
Some(unsafe { CFURL::wrap_under_create_rule(url_ref) })
}
}
type OSStatus = i32;
enum LSError {
Unknown(OSStatus),
ApplicationNotFound,
NoLaunchPermission,
}
impl From<OSStatus> for LSError {
fn from(status: OSStatus) -> Self {
match status {
-43 | -10814 => Self::ApplicationNotFound,
-10826 => Self::NoLaunchPermission,
_ => Self::Unknown(status),
}
}
}
impl std::fmt::Display for LSError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unknown(code) => write!(f, "ls_error: code {code}"),
Self::ApplicationNotFound => f.write_str("ls_error: application not found"),
Self::NoLaunchPermission => f.write_str("ls_error: no launch permission"),
}
}
}
impl std::fmt::Debug for LSError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
impl From<LSError> for Error {
fn from(err: LSError) -> Self {
let kind = match err {
LSError::Unknown(_) => ErrorKind::Other,
LSError::ApplicationNotFound => ErrorKind::NotFound,
LSError::NoLaunchPermission => ErrorKind::PermissionDenied,
};
Error::new(kind, err.to_string())
}
}
type LSRolesMask = u32;
const LSROLE_VIEWER: LSRolesMask = 0x00000002;
const LS_LAUNCH_FLAG_DEFAULTS: u32 = 0x00000001;
const LS_LAUNCH_FLAG_ASYNC: u32 = 0x00010000;
const LS_LAUNCH_FLAG_DONT_SWITCH: u32 = 0x00000200;
#[repr(C, packed(2))] struct LSLaunchURLSpec {
app_url: CFURLRef,
item_urls: CFArrayRef,
pass_thru_params: *const c_void,
launch_flags: u32,
async_ref_con: *const c_void,
}
#[link(name = "CoreServices", kind = "framework")]
extern "C" {
fn LSCopyDefaultApplicationURLForURL(
inURL: CFURLRef,
inRoleMask: LSRolesMask,
outError: *mut CFErrorRef,
) -> CFURLRef;
fn LSOpenFromURLSpec(
inLaunchSpec: *const LSLaunchURLSpec,
outLaunchedURL: *mut CFURLRef,
) -> OSStatus;
}
const DEFAULT_BROWSER_URL: &str = "file:///Applications/Safari.app/";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_non_existing_browser() {
let _ = env_logger::try_init();
if let Err(err) = open_browser_internal(
Browser::Opera,
&TargetType::try_from("https://github.com").expect("failed to parse url"),
&BrowserOptions::default(),
) {
assert_eq!(err.kind(), ErrorKind::NotFound);
} else {
panic!("expected opening non-existing browser to fail");
}
}
#[test]
fn test_existence() {
let _ = env_logger::try_init();
assert!(Browser::Safari.exists());
assert!(!Browser::Opera.exists());
}
}