From 68eb4d408805232c066af08a1a5b91d536f3ff3e Mon Sep 17 00:00:00 2001 From: ciltocruz Date: Thu, 24 Sep 2026 14:49:26 +0200 Subject: [PATCH 1/4] feat(browser): support additional Chrome channels and Chromium --- README.md | 2 +- .../src-tauri/src/commands/browser_import.rs | 4 + docs/PROVIDERS.md | 2 +- rust/src/browser/detection.rs | 119 ++++++++++++++++-- rust/src/browser/wsl_paths.rs | 100 ++++++++++----- 5 files changed, 179 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 2e052ad2f9..653e7f21ad 100755 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Win-CodexBar is a Windows system-tray app for keeping AI coding-tool usage visib - **Tray-first workflow** with a compact provider grid, usage cards, refresh action, settings shortcut, and quit control. - **Provider settings** for source selection, credentials, cookie import, token accounts, API keys, regions, and tray-display preferences. - **Windows credential protection** for app-managed API keys, manual cookies, and token accounts, using user-scoped DPAPI where available. -- **Browser cookie import** for Chrome, Edge, Brave, and Firefox, kept opt-in per provider. +- **Browser cookie import** for Chrome Stable, Beta, Dev, Canary, Chrome for Testing, Chromium, Edge, Brave, and Firefox, kept opt-in per provider. - **Installed local CLI** for scripting usage, cost, config, diagnostics, and loopback integrations. - **Installer + portable builds** with WebView2 runtime bootstrap, VC++ runtime bootstrap, and SHA-256 checksum files. diff --git a/apps/desktop-tauri/src-tauri/src/commands/browser_import.rs b/apps/desktop-tauri/src-tauri/src/commands/browser_import.rs index 40d8ff79a8..6e70a0eb32 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/browser_import.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/browser_import.rs @@ -92,6 +92,10 @@ fn browser_type_key(bt: codexbar::browser::detection::BrowserType) -> &'static s use codexbar::browser::detection::BrowserType; match bt { BrowserType::Chrome => "chrome", + BrowserType::ChromeBeta => "chrome-beta", + BrowserType::ChromeDev => "chrome-dev", + BrowserType::ChromeCanary => "chrome-canary", + BrowserType::ChromeForTesting => "chrome-for-testing", BrowserType::Edge => "edge", BrowserType::Brave => "brave", BrowserType::Arc => "arc", diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index 237f7dab3d..3f5758d5e2 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -40,7 +40,7 @@ Auth resolution helpers in `rust/src/providers/` commonly try: explicit settings ## Cookie-backed providers -Windows browser import: Chrome, Edge, Brave (DPAPI + AES-GCM), Firefox (SQLite). +Windows browser import: Chrome Stable, Beta, Dev, Canary, Chrome for Testing, Chromium, Edge, Brave (DPAPI + AES-GCM), and Firefox (SQLite). Chrome channels are detected separately and keep distinct import choices. Settings → **Providers** → provider detail → choose browser → Import. Manual cookie header paste is the fallback (required under WSL for Chromium DPAPI). Details: [COOKIES.md](./COOKIES.md). diff --git a/rust/src/browser/detection.rs b/rust/src/browser/detection.rs index 0dc39f27d7..3b90c4a633 100755 --- a/rust/src/browser/detection.rs +++ b/rust/src/browser/detection.rs @@ -16,6 +16,10 @@ use crate::wsl; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum BrowserType { Chrome, + ChromeBeta, + ChromeDev, + ChromeCanary, + ChromeForTesting, Edge, Brave, Arc, @@ -28,6 +32,10 @@ impl BrowserType { pub fn all() -> &'static [BrowserType] { &[ BrowserType::Chrome, + BrowserType::ChromeBeta, + BrowserType::ChromeDev, + BrowserType::ChromeCanary, + BrowserType::ChromeForTesting, BrowserType::Edge, BrowserType::Brave, BrowserType::Arc, @@ -45,6 +53,10 @@ impl BrowserType { pub fn display_name(&self) -> &'static str { match self { BrowserType::Chrome => "Google Chrome", + BrowserType::ChromeBeta => "Google Chrome Beta", + BrowserType::ChromeDev => "Google Chrome Dev", + BrowserType::ChromeCanary => "Google Chrome Canary", + BrowserType::ChromeForTesting => "Chrome for Testing", BrowserType::Edge => "Microsoft Edge", BrowserType::Brave => "Brave", BrowserType::Arc => "Arc", @@ -52,6 +64,44 @@ impl BrowserType { BrowserType::Chromium => "Chromium", } } + + /// Resolve a browser's Windows AppData/Local profile root. + pub fn user_data_dir_under(&self, appdata_local: &Path) -> Option { + match self { + BrowserType::Chrome => Some( + appdata_local + .join("Google") + .join("Chrome") + .join("User Data"), + ), + BrowserType::ChromeBeta => Some( + appdata_local + .join("Google") + .join("Chrome Beta") + .join("User Data"), + ), + BrowserType::ChromeDev => Some( + appdata_local + .join("Google") + .join("Chrome Dev") + .join("User Data"), + ), + BrowserType::ChromeCanary => Some( + appdata_local + .join("Google") + .join("Chrome SxS") + .join("User Data"), + ), + BrowserType::ChromeForTesting => Some( + appdata_local + .join("Google") + .join("Chrome for Testing") + .join("User Data"), + ), + BrowserType::Chromium => Some(appdata_local.join("Chromium").join("User Data")), + _ => None, + } + } } /// A detected browser installation @@ -142,12 +192,12 @@ impl BrowserDetector { && let Some(appdata_local) = wsl::windows_appdata_local() { let path = match browser_type { - BrowserType::Chrome => Some( - appdata_local - .join("Google") - .join("Chrome") - .join("User Data"), - ), + BrowserType::Chrome + | BrowserType::ChromeBeta + | BrowserType::ChromeDev + | BrowserType::ChromeCanary + | BrowserType::ChromeForTesting + | BrowserType::Chromium => browser_type.user_data_dir_under(&appdata_local), BrowserType::Edge => Some( appdata_local .join("Microsoft") @@ -161,7 +211,6 @@ impl BrowserDetector { .join("User Data"), ), BrowserType::Arc => Some(appdata_local.join("Arc").join("User Data")), - BrowserType::Chromium => Some(appdata_local.join("Chromium").join("User Data")), BrowserType::Firefox => wsl::windows_appdata_roaming() .map(|roaming| roaming.join("Mozilla").join("Firefox").join("Profiles")), }; @@ -176,10 +225,14 @@ impl BrowserDetector { let app_data = dirs::data_dir()?; let path = match browser_type { - BrowserType::Chrome => local_app_data - .join("Google") - .join("Chrome") - .join("User Data"), + BrowserType::Chrome + | BrowserType::ChromeBeta + | BrowserType::ChromeDev + | BrowserType::ChromeCanary + | BrowserType::ChromeForTesting + | BrowserType::Chromium => browser_type + .user_data_dir_under(&local_app_data) + .expect("matched a Chromium browser"), BrowserType::Edge => local_app_data .join("Microsoft") .join("Edge") @@ -189,7 +242,6 @@ impl BrowserDetector { .join("Brave-Browser") .join("User Data"), BrowserType::Arc => local_app_data.join("Arc").join("User Data"), - BrowserType::Chromium => local_app_data.join("Chromium").join("User Data"), BrowserType::Firefox => app_data.join("Mozilla").join("Firefox").join("Profiles"), }; @@ -268,6 +320,49 @@ impl BrowserDetector { mod tests { use super::*; + #[test] + fn chrome_channels_resolve_to_distinct_windows_profile_roots() { + let appdata = Path::new("C:/Users/test/AppData/Local"); + let channels = [ + ( + BrowserType::Chrome, + "Google/Chrome/User Data", + "Google Chrome", + ), + ( + BrowserType::ChromeBeta, + "Google/Chrome Beta/User Data", + "Google Chrome Beta", + ), + ( + BrowserType::ChromeDev, + "Google/Chrome Dev/User Data", + "Google Chrome Dev", + ), + ( + BrowserType::ChromeCanary, + "Google/Chrome SxS/User Data", + "Google Chrome Canary", + ), + ( + BrowserType::ChromeForTesting, + "Google/Chrome for Testing/User Data", + "Chrome for Testing", + ), + (BrowserType::Chromium, "Chromium/User Data", "Chromium"), + ]; + + for (browser, relative_path, display_name) in channels { + assert_eq!( + browser.user_data_dir_under(appdata).unwrap(), + appdata.join(relative_path), + "wrong profile root for {display_name}" + ); + assert_eq!(browser.display_name(), display_name); + assert!(BrowserType::all().contains(&browser)); + } + } + #[test] fn test_browser_detection() { let browsers = BrowserDetector::detect_all(); diff --git a/rust/src/browser/wsl_paths.rs b/rust/src/browser/wsl_paths.rs index 42d93f17d4..316da4c394 100644 --- a/rust/src/browser/wsl_paths.rs +++ b/rust/src/browser/wsl_paths.rs @@ -29,45 +29,38 @@ impl WslBrowserDetector { let mut browsers = Vec::new(); - let candidates: &[(BrowserType, PathBuf)] = &[ - ( - BrowserType::Chrome, - appdata_local - .join("Google") - .join("Chrome") - .join("User Data"), - ), - ( - BrowserType::Edge, - appdata_local - .join("Microsoft") - .join("Edge") - .join("User Data"), - ), - ( - BrowserType::Brave, - appdata_local - .join("BraveSoftware") - .join("Brave-Browser") - .join("User Data"), - ), - ( - BrowserType::Arc, - appdata_local.join("Arc").join("User Data"), - ), - ( - BrowserType::Chromium, - appdata_local.join("Chromium").join("User Data"), - ), - ]; + let candidates = windows_browser_candidates(&appdata_local); + let candidates: Vec<(BrowserType, PathBuf)> = candidates + .into_iter() + .chain([ + ( + BrowserType::Edge, + appdata_local + .join("Microsoft") + .join("Edge") + .join("User Data"), + ), + ( + BrowserType::Brave, + appdata_local + .join("BraveSoftware") + .join("Brave-Browser") + .join("User Data"), + ), + ( + BrowserType::Arc, + appdata_local.join("Arc").join("User Data"), + ), + ]) + .collect(); for (browser_type, user_data_dir) in candidates { if user_data_dir.exists() { - let profiles = detect_chromium_profiles(user_data_dir); + let profiles = detect_chromium_profiles(&user_data_dir); if !profiles.is_empty() { browsers.push(DetectedBrowser { - browser_type: *browser_type, - user_data_dir: user_data_dir.clone(), + browser_type, + user_data_dir, profiles, }); } @@ -95,6 +88,24 @@ impl WslBrowserDetector { } } +fn windows_browser_candidates(appdata_local: &std::path::Path) -> Vec<(BrowserType, PathBuf)> { + [ + BrowserType::Chrome, + BrowserType::ChromeBeta, + BrowserType::ChromeDev, + BrowserType::ChromeCanary, + BrowserType::ChromeForTesting, + BrowserType::Chromium, + ] + .into_iter() + .filter_map(|browser_type| { + browser_type + .user_data_dir_under(appdata_local) + .map(|path| (browser_type, path)) + }) + .collect() +} + fn detect_chromium_profiles(user_data_dir: &PathBuf) -> Vec { let mut profiles = Vec::new(); @@ -151,6 +162,27 @@ fn detect_firefox_profiles(profiles_dir: &PathBuf) -> Vec { mod tests { use super::*; + #[test] + fn windows_profile_roots_include_every_chrome_channel_and_chromium() { + let appdata = PathBuf::from("/mnt/c/Users/test/AppData/Local"); + let expected = [ + (BrowserType::Chrome, "Google/Chrome/User Data"), + (BrowserType::ChromeBeta, "Google/Chrome Beta/User Data"), + (BrowserType::ChromeDev, "Google/Chrome Dev/User Data"), + (BrowserType::ChromeCanary, "Google/Chrome SxS/User Data"), + ( + BrowserType::ChromeForTesting, + "Google/Chrome for Testing/User Data", + ), + (BrowserType::Chromium, "Chromium/User Data"), + ]; + + let actual = windows_browser_candidates(&appdata); + for (browser_type, relative_path) in expected { + assert!(actual.contains(&(browser_type, appdata.join(relative_path)))); + } + } + #[test] fn test_wsl_browser_detection() { let browsers = WslBrowserDetector::detect_all(); From eec2c4747c9794c9580a69c966c481b696a10b61 Mon Sep 17 00:00:00 2001 From: ciltocruz Date: Thu, 24 Sep 2026 14:59:55 +0200 Subject: [PATCH 2/4] refactor(browser): derive WSL browser candidates from shared types --- rust/src/browser/wsl_paths.rs | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/rust/src/browser/wsl_paths.rs b/rust/src/browser/wsl_paths.rs index 316da4c394..fef31d4781 100644 --- a/rust/src/browser/wsl_paths.rs +++ b/rust/src/browser/wsl_paths.rs @@ -89,21 +89,15 @@ impl WslBrowserDetector { } fn windows_browser_candidates(appdata_local: &std::path::Path) -> Vec<(BrowserType, PathBuf)> { - [ - BrowserType::Chrome, - BrowserType::ChromeBeta, - BrowserType::ChromeDev, - BrowserType::ChromeCanary, - BrowserType::ChromeForTesting, - BrowserType::Chromium, - ] - .into_iter() - .filter_map(|browser_type| { - browser_type - .user_data_dir_under(appdata_local) - .map(|path| (browser_type, path)) - }) - .collect() + BrowserType::all() + .iter() + .copied() + .filter_map(|browser_type| { + browser_type + .user_data_dir_under(appdata_local) + .map(|path| (browser_type, path)) + }) + .collect() } fn detect_chromium_profiles(user_data_dir: &PathBuf) -> Vec { From cb68c7db3acaa75b325bcc9add0f6c5d55124095 Mon Sep 17 00:00:00 2001 From: ciltocruz Date: Thu, 24 Sep 2026 17:21:33 +0200 Subject: [PATCH 3/4] docs(browser): document WSL profile candidate helpers --- rust/src/browser/wsl_paths.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rust/src/browser/wsl_paths.rs b/rust/src/browser/wsl_paths.rs index fef31d4781..6c70fa2234 100644 --- a/rust/src/browser/wsl_paths.rs +++ b/rust/src/browser/wsl_paths.rs @@ -17,6 +17,7 @@ use super::detection::{BrowserProfile, BrowserType, DetectedBrowser}; pub struct WslBrowserDetector; impl WslBrowserDetector { + /// Detect Windows browsers visible from WSL via /mnt/c/ paths. pub fn detect_all() -> Vec { if !wsl::is_wsl() { return Vec::new(); @@ -88,6 +89,9 @@ impl WslBrowserDetector { } } +/// Collect profile roots for every browser that resolves one under the given +/// AppData/Local directory. Browsers without a shared-root mapping (Edge, +/// Brave, Arc, Firefox) are skipped here and added separately by the caller. fn windows_browser_candidates(appdata_local: &std::path::Path) -> Vec<(BrowserType, PathBuf)> { BrowserType::all() .iter() @@ -100,6 +104,7 @@ fn windows_browser_candidates(appdata_local: &std::path::Path) -> Vec<(BrowserTy .collect() } +/// Detect Chromium-based browser profiles fn detect_chromium_profiles(user_data_dir: &PathBuf) -> Vec { let mut profiles = Vec::new(); From 4fd971bf8c15744d5d80dced11dd9a87d85a3bea Mon Sep 17 00:00:00 2001 From: ciltocruz Date: Mon, 28 Sep 2026 19:45:34 +0200 Subject: [PATCH 4/4] refactor(browser): share profile paths and detection across Windows and WSL --- .../src-tauri/src/commands/browser_import.rs | 21 +- rust/src/browser/detection.rs | 194 ++++++++-------- rust/src/browser/wsl_paths.rs | 212 ++++++------------ 3 files changed, 171 insertions(+), 256 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/commands/browser_import.rs b/apps/desktop-tauri/src-tauri/src/commands/browser_import.rs index 6e70a0eb32..8ea322b7c7 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/browser_import.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/browser_import.rs @@ -23,7 +23,7 @@ pub fn list_detected_browsers() -> Vec { BrowserDetector::detect_all() .into_iter() .map(|b| DetectedBrowserBridge { - browser_type: browser_type_key(b.browser_type).to_string(), + browser_type: b.browser_type.key().to_string(), display_name: b.browser_type.display_name().to_string(), profile_count: b.profiles.len(), }) @@ -62,7 +62,7 @@ pub fn import_browser_cookies( let browsers = BrowserDetector::detect_all(); let browser = browsers .into_iter() - .find(|b| browser_type_key(b.browser_type) == browser_type.as_str()) + .find(|b| b.browser_type.key() == browser_type.as_str()) .ok_or_else(|| format!("Browser '{browser_type}' not found or not installed"))?; // Extract the cookie header. @@ -86,20 +86,3 @@ pub fn import_browser_cookies( Ok(get_manual_cookies()) } - -/// Map `BrowserType` to a stable lowercase string key used in the IPC bridge. -fn browser_type_key(bt: codexbar::browser::detection::BrowserType) -> &'static str { - use codexbar::browser::detection::BrowserType; - match bt { - BrowserType::Chrome => "chrome", - BrowserType::ChromeBeta => "chrome-beta", - BrowserType::ChromeDev => "chrome-dev", - BrowserType::ChromeCanary => "chrome-canary", - BrowserType::ChromeForTesting => "chrome-for-testing", - BrowserType::Edge => "edge", - BrowserType::Brave => "brave", - BrowserType::Arc => "arc", - BrowserType::Firefox => "firefox", - BrowserType::Chromium => "chromium", - } -} diff --git a/rust/src/browser/detection.rs b/rust/src/browser/detection.rs index 3b90c4a633..2a4167e8e0 100755 --- a/rust/src/browser/detection.rs +++ b/rust/src/browser/detection.rs @@ -65,41 +65,38 @@ impl BrowserType { } } - /// Resolve a browser's Windows AppData/Local profile root. - pub fn user_data_dir_under(&self, appdata_local: &Path) -> Option { + /// Stable browser identifier shared by the core and desktop IPC bridge. + pub fn key(&self) -> &'static str { match self { - BrowserType::Chrome => Some( - appdata_local - .join("Google") - .join("Chrome") - .join("User Data"), - ), - BrowserType::ChromeBeta => Some( - appdata_local - .join("Google") - .join("Chrome Beta") - .join("User Data"), - ), - BrowserType::ChromeDev => Some( - appdata_local - .join("Google") - .join("Chrome Dev") - .join("User Data"), - ), - BrowserType::ChromeCanary => Some( - appdata_local - .join("Google") - .join("Chrome SxS") - .join("User Data"), - ), - BrowserType::ChromeForTesting => Some( - appdata_local - .join("Google") - .join("Chrome for Testing") - .join("User Data"), - ), - BrowserType::Chromium => Some(appdata_local.join("Chromium").join("User Data")), - _ => None, + BrowserType::Chrome => "chrome", + BrowserType::ChromeBeta => "chrome-beta", + BrowserType::ChromeDev => "chrome-dev", + BrowserType::ChromeCanary => "chrome-canary", + BrowserType::ChromeForTesting => "chrome-for-testing", + BrowserType::Edge => "edge", + BrowserType::Brave => "brave", + BrowserType::Arc => "arc", + BrowserType::Firefox => "firefox", + BrowserType::Chromium => "chromium", + } + } + + /// Resolve a profile root under Windows AppData. Firefox requires Roaming; + /// local-root browsers remain resolvable when Roaming is unavailable. + pub fn user_data_dir(&self, local: &Path, roaming: Option<&Path>) -> Option { + match self { + BrowserType::Chrome => Some(local.join("Google/Chrome/User Data")), + BrowserType::ChromeBeta => Some(local.join("Google/Chrome Beta/User Data")), + BrowserType::ChromeDev => Some(local.join("Google/Chrome Dev/User Data")), + BrowserType::ChromeCanary => Some(local.join("Google/Chrome SxS/User Data")), + BrowserType::ChromeForTesting => { + Some(local.join("Google/Chrome for Testing/User Data")) + } + BrowserType::Edge => Some(local.join("Microsoft/Edge/User Data")), + BrowserType::Brave => Some(local.join("BraveSoftware/Brave-Browser/User Data")), + BrowserType::Arc => Some(local.join("Arc/User Data")), + BrowserType::Firefox => roaming.map(|root| root.join("Mozilla/Firefox/Profiles")), + BrowserType::Chromium => Some(local.join("Chromium/User Data")), } } } @@ -167,7 +164,23 @@ impl BrowserDetector { /// Detect a specific browser pub fn detect(browser_type: BrowserType) -> Option { let user_data_dir = Self::get_user_data_dir(browser_type)?; + Self::detect_at_path(browser_type, user_data_dir) + } + + /// Detect from explicit AppData roots, including WSL-mounted Windows roots. + pub(super) fn detect_in_roots( + browser_type: BrowserType, + local: &Path, + roaming: Option<&Path>, + ) -> Option { + let user_data_dir = browser_type.user_data_dir(local, roaming)?; + Self::detect_at_path(browser_type, user_data_dir) + } + fn detect_at_path( + browser_type: BrowserType, + user_data_dir: PathBuf, + ) -> Option { if !user_data_dir.exists() { return None; } @@ -191,61 +204,17 @@ impl BrowserDetector { if wsl::is_wsl() && let Some(appdata_local) = wsl::windows_appdata_local() { - let path = match browser_type { - BrowserType::Chrome - | BrowserType::ChromeBeta - | BrowserType::ChromeDev - | BrowserType::ChromeCanary - | BrowserType::ChromeForTesting - | BrowserType::Chromium => browser_type.user_data_dir_under(&appdata_local), - BrowserType::Edge => Some( - appdata_local - .join("Microsoft") - .join("Edge") - .join("User Data"), - ), - BrowserType::Brave => Some( - appdata_local - .join("BraveSoftware") - .join("Brave-Browser") - .join("User Data"), - ), - BrowserType::Arc => Some(appdata_local.join("Arc").join("User Data")), - BrowserType::Firefox => wsl::windows_appdata_roaming() - .map(|roaming| roaming.join("Mozilla").join("Firefox").join("Profiles")), - }; - if let Some(ref p) = path - && p.exists() + let roaming = wsl::windows_appdata_roaming(); + if let Some(path) = browser_type.user_data_dir(&appdata_local, roaming.as_deref()) + && path.exists() { - return path; + return Some(path); } } let local_app_data = dirs::data_local_dir()?; - let app_data = dirs::data_dir()?; - - let path = match browser_type { - BrowserType::Chrome - | BrowserType::ChromeBeta - | BrowserType::ChromeDev - | BrowserType::ChromeCanary - | BrowserType::ChromeForTesting - | BrowserType::Chromium => browser_type - .user_data_dir_under(&local_app_data) - .expect("matched a Chromium browser"), - BrowserType::Edge => local_app_data - .join("Microsoft") - .join("Edge") - .join("User Data"), - BrowserType::Brave => local_app_data - .join("BraveSoftware") - .join("Brave-Browser") - .join("User Data"), - BrowserType::Arc => local_app_data.join("Arc").join("User Data"), - BrowserType::Firefox => app_data.join("Mozilla").join("Firefox").join("Profiles"), - }; - - Some(path) + let app_data = dirs::data_dir(); + browser_type.user_data_dir(&local_app_data, app_data.as_deref()) } /// Detect profiles within a browser's user data directory @@ -321,45 +290,88 @@ mod tests { use super::*; #[test] - fn chrome_channels_resolve_to_distinct_windows_profile_roots() { - let appdata = Path::new("C:/Users/test/AppData/Local"); - let channels = [ + fn browser_paths_and_keys_cover_every_type() { + let local = Path::new("C:/Users/test/AppData/Local"); + let roaming = Path::new("C:/Users/test/AppData/Roaming"); + let expected = [ ( BrowserType::Chrome, "Google/Chrome/User Data", "Google Chrome", + "chrome", ), ( BrowserType::ChromeBeta, "Google/Chrome Beta/User Data", "Google Chrome Beta", + "chrome-beta", ), ( BrowserType::ChromeDev, "Google/Chrome Dev/User Data", "Google Chrome Dev", + "chrome-dev", ), ( BrowserType::ChromeCanary, "Google/Chrome SxS/User Data", "Google Chrome Canary", + "chrome-canary", ), ( BrowserType::ChromeForTesting, "Google/Chrome for Testing/User Data", "Chrome for Testing", + "chrome-for-testing", + ), + ( + BrowserType::Edge, + "Microsoft/Edge/User Data", + "Microsoft Edge", + "edge", + ), + ( + BrowserType::Brave, + "BraveSoftware/Brave-Browser/User Data", + "Brave", + "brave", + ), + (BrowserType::Arc, "Arc/User Data", "Arc", "arc"), + ( + BrowserType::Firefox, + "Mozilla/Firefox/Profiles", + "Firefox", + "firefox", + ), + ( + BrowserType::Chromium, + "Chromium/User Data", + "Chromium", + "chromium", ), - (BrowserType::Chromium, "Chromium/User Data", "Chromium"), ]; - for (browser, relative_path, display_name) in channels { + assert_eq!(BrowserType::all().len(), expected.len()); + let mut keys = std::collections::HashSet::new(); + for (browser, relative_path, display_name, key) in expected { + let root = if browser == BrowserType::Firefox { + roaming + } else { + local + }; assert_eq!( - browser.user_data_dir_under(appdata).unwrap(), - appdata.join(relative_path), + browser.user_data_dir(local, Some(roaming)), + Some(root.join(relative_path)), "wrong profile root for {display_name}" ); assert_eq!(browser.display_name(), display_name); + assert_eq!(browser.key(), key); + assert!(keys.insert(browser.key()), "duplicate IPC key: {key}"); assert!(BrowserType::all().contains(&browser)); + assert_eq!( + browser.user_data_dir(local, None), + (browser != BrowserType::Firefox).then(|| local.join(relative_path)) + ); } } diff --git a/rust/src/browser/wsl_paths.rs b/rust/src/browser/wsl_paths.rs index 6c70fa2234..df59249d48 100644 --- a/rust/src/browser/wsl_paths.rs +++ b/rust/src/browser/wsl_paths.rs @@ -4,11 +4,9 @@ //! This module provides path resolvers that detect WSL and map //! browser profile paths to their Windows host equivalents. -use std::path::PathBuf; - use crate::wsl; -use super::detection::{BrowserProfile, BrowserType, DetectedBrowser}; +use super::detection::{BrowserDetector, BrowserType, DetectedBrowser}; /// WSL-aware browser detector. /// @@ -28,133 +26,19 @@ impl WslBrowserDetector { None => return Vec::new(), }; - let mut browsers = Vec::new(); - - let candidates = windows_browser_candidates(&appdata_local); - let candidates: Vec<(BrowserType, PathBuf)> = candidates - .into_iter() - .chain([ - ( - BrowserType::Edge, - appdata_local - .join("Microsoft") - .join("Edge") - .join("User Data"), - ), - ( - BrowserType::Brave, - appdata_local - .join("BraveSoftware") - .join("Brave-Browser") - .join("User Data"), - ), - ( - BrowserType::Arc, - appdata_local.join("Arc").join("User Data"), - ), - ]) - .collect(); - - for (browser_type, user_data_dir) in candidates { - if user_data_dir.exists() { - let profiles = detect_chromium_profiles(&user_data_dir); - if !profiles.is_empty() { - browsers.push(DetectedBrowser { - browser_type, - user_data_dir, - profiles, - }); - } - } - } - - if let Some(appdata_roaming) = wsl::windows_appdata_roaming() { - let ff_dir = appdata_roaming - .join("Mozilla") - .join("Firefox") - .join("Profiles"); - if ff_dir.exists() { - let profiles = detect_firefox_profiles(&ff_dir); - if !profiles.is_empty() { - browsers.push(DetectedBrowser { - browser_type: BrowserType::Firefox, - user_data_dir: ff_dir, - profiles, - }); - } - } - } - - browsers - } -} - -/// Collect profile roots for every browser that resolves one under the given -/// AppData/Local directory. Browsers without a shared-root mapping (Edge, -/// Brave, Arc, Firefox) are skipped here and added separately by the caller. -fn windows_browser_candidates(appdata_local: &std::path::Path) -> Vec<(BrowserType, PathBuf)> { - BrowserType::all() - .iter() - .copied() - .filter_map(|browser_type| { - browser_type - .user_data_dir_under(appdata_local) - .map(|path| (browser_type, path)) - }) - .collect() -} - -/// Detect Chromium-based browser profiles -fn detect_chromium_profiles(user_data_dir: &PathBuf) -> Vec { - let mut profiles = Vec::new(); - - let default_path = user_data_dir.join("Default"); - if default_path.exists() { - profiles.push(BrowserProfile { - name: "Default".to_string(), - path: default_path, - is_default: true, - }); + let appdata_roaming = wsl::windows_appdata_roaming(); + BrowserType::all() + .iter() + .copied() + .filter_map(|browser_type| { + BrowserDetector::detect_in_roots( + browser_type, + &appdata_local, + appdata_roaming.as_deref(), + ) + }) + .collect() } - - if let Ok(entries) = std::fs::read_dir(user_data_dir) { - for entry in entries.flatten() { - let name = entry.file_name().to_string_lossy().to_string(); - if name.starts_with("Profile ") { - let path = entry.path(); - if path.is_dir() { - profiles.push(BrowserProfile { - name, - path, - is_default: false, - }); - } - } - } - } - - profiles -} - -fn detect_firefox_profiles(profiles_dir: &PathBuf) -> Vec { - let mut profiles = Vec::new(); - - if let Ok(entries) = std::fs::read_dir(profiles_dir) { - for entry in entries.flatten() { - let name = entry.file_name().to_string_lossy().to_string(); - let path = entry.path(); - if path.is_dir() && name.contains('.') { - let is_default = name.contains("default"); - profiles.push(BrowserProfile { - name, - path, - is_default, - }); - } - } - } - - profiles } #[cfg(test)] @@ -162,24 +46,60 @@ mod tests { use super::*; #[test] - fn windows_profile_roots_include_every_chrome_channel_and_chromium() { - let appdata = PathBuf::from("/mnt/c/Users/test/AppData/Local"); - let expected = [ - (BrowserType::Chrome, "Google/Chrome/User Data"), - (BrowserType::ChromeBeta, "Google/Chrome Beta/User Data"), - (BrowserType::ChromeDev, "Google/Chrome Dev/User Data"), - (BrowserType::ChromeCanary, "Google/Chrome SxS/User Data"), - ( - BrowserType::ChromeForTesting, - "Google/Chrome for Testing/User Data", - ), - (BrowserType::Chromium, "Chromium/User Data"), - ]; + fn shared_detector_reads_wsl_shaped_roots_without_roaming() { + let temp = tempfile::tempdir().unwrap(); + let local = temp.path().join("mnt/c/Users/test/AppData/Local"); + let chrome = local.join("Google/Chrome Beta/User Data"); + std::fs::create_dir_all(chrome.join("Default")).unwrap(); + std::fs::create_dir_all(chrome.join("Profile 2")).unwrap(); + std::fs::create_dir_all(chrome.join("Profile 1")).unwrap(); + std::fs::create_dir_all(chrome.join("Other")).unwrap(); + + let detected = + BrowserDetector::detect_in_roots(BrowserType::ChromeBeta, &local, None).unwrap(); + assert_eq!(detected.user_data_dir, chrome); + assert_eq!(detected.profiles[0].name, "Default"); + assert!(detected.profiles[0].is_default); + let mut other_names: Vec<_> = detected.profiles[1..] + .iter() + .map(|profile| profile.name.as_str()) + .collect(); + other_names.sort_unstable(); + assert_eq!(other_names, ["Profile 1", "Profile 2"]); + assert!( + detected.profiles[1..] + .iter() + .all(|profile| !profile.is_default) + ); + assert!(BrowserDetector::detect_in_roots(BrowserType::Firefox, &local, None).is_none()); + } - let actual = windows_browser_candidates(&appdata); - for (browser_type, relative_path) in expected { - assert!(actual.contains(&(browser_type, appdata.join(relative_path)))); - } + #[test] + fn shared_detector_reads_firefox_from_wsl_roaming_root() { + let temp = tempfile::tempdir().unwrap(); + let local = temp.path().join("mnt/c/Users/test/AppData/Local"); + let roaming = temp.path().join("mnt/c/Users/test/AppData/Roaming"); + let firefox = roaming.join("Mozilla/Firefox/Profiles"); + std::fs::create_dir_all(firefox.join("abc.default-release")).unwrap(); + std::fs::create_dir_all(firefox.join("def.other")).unwrap(); + std::fs::create_dir_all(local.join("Mozilla/Firefox/Profiles/fake.default")).unwrap(); + + let detected = + BrowserDetector::detect_in_roots(BrowserType::Firefox, &local, Some(&roaming)).unwrap(); + assert_eq!(detected.user_data_dir, firefox); + assert_eq!(detected.profiles.len(), 2); + assert!( + detected + .profiles + .iter() + .any(|profile| profile.name == "abc.default-release" && profile.is_default) + ); + assert!( + detected + .profiles + .iter() + .any(|profile| profile.name == "def.other" && !profile.is_default) + ); } #[test]