use super::browser_profile_files::physical_path;
use super::browser_profiles::current_platform;
use super::browser_sources::{
browser_sources, current_environment, find_browser_source, resolve_browser_executables,
resolve_browser_roots,
};
use std::path::{Path, PathBuf};
pub(crate) fn assert_cdp_browser(channel: &str) -> Result<(), anyhow::Error> {
if find_browser_source(channel).is_some_and(|source| source.family != "chromium") {
return Err(anyhow::anyhow!(
"{channel} does not support CDP; use its documented WebDriver setup when available"
));
}
Ok(())
}
pub fn default_real_browser_user_data_dir(channel: &str) -> PathBuf {
let directory_name: String = channel
.chars()
.map(|character| {
if character.is_ascii_alphanumeric() || "_.-".contains(character) {
character
} else {
'-'
}
})
.collect();
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".browser-commander")
.join("real-browser")
.join(directory_name)
}
pub fn known_default_user_data_dirs() -> Vec<PathBuf> {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
let environment = current_environment();
browser_sources()
.iter()
.flat_map(|browser| {
resolve_browser_roots(
&browser.id,
current_platform(),
&home.to_string_lossy(),
&environment,
)
.unwrap_or_default()
})
.collect()
}
fn normalize_for_comparison(path: &Path) -> Result<PathBuf, anyhow::Error> {
let normalized = physical_path(path)?;
#[cfg(target_os = "windows")]
{
Ok(PathBuf::from(normalized.to_string_lossy().to_lowercase()))
}
#[cfg(not(target_os = "windows"))]
{
Ok(normalized)
}
}
pub fn assert_dedicated_user_data_dir(user_data_dir: &Path) -> Result<(), anyhow::Error> {
let requested = normalize_for_comparison(user_data_dir)?;
for default in known_default_user_data_dirs() {
if requested.starts_with(normalize_for_comparison(&default)?) {
return Err(anyhow::anyhow!(
"launch_real_browser requires a dedicated user_data_dir, not a browser default profile"
));
}
}
Ok(())
}
fn browser_install_candidates(channel: &str) -> Result<Vec<PathBuf>, anyhow::Error> {
let source = find_browser_source(channel)
.ok_or_else(|| anyhow::anyhow!("unknown browser channel: {channel}"))?;
if source.executable_names.is_empty() {
return Err(anyhow::anyhow!(
"No supported installed executable for {channel}"
));
}
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
resolve_browser_executables(
channel,
current_platform(),
&home.to_string_lossy(),
¤t_environment(),
)
}
fn is_executable(path: &Path) -> bool {
if !path.is_file() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
path.metadata()
.map(|metadata| metadata.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(not(unix))]
{
true
}
}
pub fn resolve_browser_executable(
channel: &str,
executable_path: Option<&Path>,
) -> Result<PathBuf, anyhow::Error> {
let candidates = if let Some(executable_path) = executable_path {
vec![normalize_for_comparison(executable_path)?]
} else {
browser_install_candidates(channel)?
};
for candidate in candidates {
if is_executable(&candidate) {
return Ok(candidate);
}
}
if let Some(executable_path) = executable_path {
Err(anyhow::anyhow!(
"browser executable is not accessible: {}",
executable_path.display()
))
} else {
Err(anyhow::anyhow!(
"could not find an installed {channel} browser; provide executable_path"
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::browser::browser_sources::Environment;
#[cfg(unix)]
#[test]
fn normalizes_new_profiles_beneath_symlinks() {
let root = crate::browser::create_temporary_user_data_dir(None).unwrap();
let actual = root.join("default-profile");
std::fs::create_dir(&actual).unwrap();
let alias = root.join("profile-alias");
std::os::unix::fs::symlink(&actual, &alias).unwrap();
assert_eq!(
normalize_for_comparison(&alias.join("new-profile")).unwrap(),
actual.canonicalize().unwrap().join("new-profile")
);
std::fs::remove_dir_all(root).unwrap();
}
#[test]
fn rejects_the_current_platform_default_profiles() {
for profile in known_default_user_data_dirs() {
let error = assert_dedicated_user_data_dir(&profile).unwrap_err();
assert!(error.to_string().contains("dedicated user_data_dir"));
assert!(assert_dedicated_user_data_dir(&profile.join("Profile 1")).is_err());
}
let dedicated = std::env::temp_dir().join("browser-commander-dedicated");
assert!(assert_dedicated_user_data_dir(&dedicated).is_ok());
}
#[test]
fn all_required_channels_have_discovery_candidates() {
for channel in ["chrome", "chromium", "brave", "msedge"] {
assert!(!browser_install_candidates(channel).unwrap().is_empty());
}
let chrome = browser_install_candidates("chrome").unwrap();
let has_platform_default = if cfg!(target_os = "macos") {
chrome.contains(&PathBuf::from(
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
))
} else if cfg!(target_os = "windows") {
chrome.iter().any(|candidate| {
candidate.ends_with(Path::new("Google/Chrome/Application/chrome.exe"))
})
} else {
chrome.contains(&PathBuf::from("/usr/bin/google-chrome"))
};
assert!(has_platform_default);
}
#[test]
fn reports_unknown_channels_and_missing_executables() {
let error = resolve_browser_executable("netscape", None).unwrap_err();
assert!(
error.to_string().contains("unknown browser channel"),
"{error}"
);
let error = resolve_browser_executable("chrome", Some(Path::new("/nonexistent/chrome")))
.unwrap_err();
assert!(error.to_string().contains("not accessible"), "{error}");
}
#[test]
fn derives_all_platform_executables_and_aliases_from_the_catalogue() {
let env = Environment::from([
("LOCALAPPDATA".into(), "C:\\Local".into()),
("PROGRAMFILES".into(), "C:\\Programs".into()),
]);
for browser in browser_sources()
.iter()
.filter(|entry| entry.family == "chromium")
{
assert!(!browser.executable_names.is_empty(), "{}", browser.id);
for alias in &browser.aliases {
assert_eq!(
find_browser_source(alias).unwrap().executable_names,
browser.executable_names
);
}
for platform in browser.roots.keys() {
assert!(
!resolve_browser_executables(&browser.id, platform, "/test", &env)
.unwrap()
.is_empty(),
"{} {platform}",
browser.id
);
}
}
assert!(
resolve_browser_executables("opera", "win32", "C:\\User", &env)
.unwrap()
.contains(&PathBuf::from("C:\\Local\\Programs\\Opera\\opera.exe"))
);
}
}