browser_commander/browser/migration/
cookies.rs1use std::collections::HashMap;
34use std::path::{Path, PathBuf};
35use std::sync::{Arc, LazyLock};
36
37use anyhow::Result;
38use regex::Regex;
39
40use super::fs_utils::path_exists;
41use super::{ClassOutcome, MigrationEntry};
42use crate::browser::browser_cookies::{
43 read_browser_cookies, BrowserCookie, BrowserCookieReadOptions,
44};
45
46pub const DBSC_BOUND_COOKIE_NAMES: [&str; 3] = ["__Secure-1PSIDTS", "__Secure-3PSIDTS", "SIDTS"];
48
49const DBSC_REGISTRATION_FILES: [&str; 2] = ["Network/DeviceBoundSessions", "DeviceBoundSessions"];
51
52const DBSC_REGISTRATION_DETAIL: &str = "The source profile has a Device Bound Session registration; cookies covered by it are bound to the source device key and will expire in the migrated profile.";
53
54pub type CookieReader =
56 Arc<dyn Fn(BrowserCookieReadOptions) -> Result<Vec<BrowserCookie>> + Send + Sync>;
57
58pub(crate) fn default_cookie_reader() -> CookieReader {
60 Arc::new(read_browser_cookies)
61}
62
63static GOOGLE_COUNTRY_HOST: LazyLock<Regex> = LazyLock::new(|| {
64 Regex::new(r"(^|\.)google\.[a-z.]+$").expect("the Google host pattern is valid")
65});
66
67fn is_google_host(domain: &str) -> bool {
68 let host = domain.strip_prefix('.').unwrap_or(domain).to_lowercase();
69 host == "google.com" || host.ends_with(".google.com") || GOOGLE_COUNTRY_HOST.is_match(&host)
70}
71
72pub(crate) fn is_dbsc_bound_cookie(cookie: &BrowserCookie) -> bool {
74 is_google_host(&cookie.domain) && DBSC_BOUND_COOKIE_NAMES.contains(&cookie.name.as_str())
75}
76
77fn find_dbsc_registration(profile_dir: &Path) -> Option<PathBuf> {
78 DBSC_REGISTRATION_FILES
79 .iter()
80 .map(|relative| profile_dir.join(relative))
81 .find(|candidate| path_exists(candidate))
82}
83
84pub(crate) struct CookieSource<'a> {
86 pub browser: &'a str,
87 pub profile: Option<&'a str>,
88 pub source_profile_dir: Option<&'a Path>,
89 pub domains: &'a [String],
90 pub platform: &'a str,
91 pub home_dir: &'a Path,
92}
93
94pub(crate) fn migrate_cookies(
97 source: &CookieSource<'_>,
98 read_cookies: &CookieReader,
99) -> Result<(Vec<BrowserCookie>, ClassOutcome)> {
100 let domain_filters: Vec<Option<&str>> = if source.domains.is_empty() {
101 vec![None]
102 } else {
103 source
104 .domains
105 .iter()
106 .map(|domain| Some(domain.as_str()))
107 .collect()
108 };
109
110 let mut order: Vec<String> = Vec::new();
112 let mut seen: HashMap<String, BrowserCookie> = HashMap::new();
113 for domain_filter in domain_filters {
114 let mut options = BrowserCookieReadOptions::new(source.browser)
115 .ignore_decryption_errors(true)
116 .platform(source.platform)
117 .home_dir(source.home_dir);
118 if let Some(profile) = source.profile {
119 options = options.profile(profile);
120 }
121 if let Some(profile_dir) = source.source_profile_dir {
124 options = options.profile_dir(profile_dir);
125 }
126 if let Some(domain) = domain_filter {
127 options = options.domain_filter(domain);
128 }
129 for cookie in read_cookies(options)? {
130 if !super::domains::matches_domains(&cookie.domain, source.domains) {
131 continue;
132 }
133 let key = format!("{}\0{}\0{}", cookie.domain, cookie.name, cookie.path);
134 if !seen.contains_key(&key) {
135 order.push(key.clone());
136 }
137 seen.insert(key, cookie);
138 }
139 }
140 let cookies: Vec<BrowserCookie> = order.iter().filter_map(|key| seen.remove(key)).collect();
141
142 let skipped = cookies
143 .iter()
144 .filter(|cookie| is_dbsc_bound_cookie(cookie))
145 .map(|cookie| {
146 MigrationEntry::new(
147 "cookies",
148 format!("{} {}", cookie.domain, cookie.name),
149 "dbsc-bound",
150 )
151 })
152 .collect();
153
154 let mut warnings = Vec::new();
155 if source
156 .source_profile_dir
157 .and_then(find_dbsc_registration)
158 .is_some()
159 {
160 warnings.push(
161 MigrationEntry::new(
162 "cookies",
163 "DeviceBoundSessions",
164 "dbsc-registration-present",
165 )
166 .with_detail(DBSC_REGISTRATION_DETAIL),
167 );
168 }
169
170 let outcome = ClassOutcome {
171 migrated: cookies.len() as u64,
172 skipped,
173 warnings,
174 };
175 Ok((cookies, outcome))
176}