use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
pub(crate) struct BrowserProfile {
pub(crate) path: PathBuf,
temporary: bool,
}
impl BrowserProfile {
pub(crate) fn create(base: &Path, app_id: Option<&str>) -> io::Result<Self> {
if let Some(id) = app_id {
let path = identity_path(base, id)?;
std::fs::create_dir_all(&path)?;
return Ok(Self {
path,
temporary: false,
});
}
static NEXT: AtomicU64 = AtomicU64::new(0);
let base = base.join("anonymous");
std::fs::create_dir_all(&base)?;
for _ in 0..128 {
let serial = NEXT.fetch_add(1, Ordering::Relaxed);
let path = base.join(format!("{}-{serial}", std::process::id()));
match std::fs::create_dir(&path) {
Ok(()) => {
return Ok(Self {
path,
temporary: true,
})
}
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
Err(error) => return Err(error),
}
}
Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"cannot allocate an isolated WebView2 profile; set DesktopAppBuilder::app_id",
))
}
}
impl Drop for BrowserProfile {
fn drop(&mut self) {
if self.temporary {
let _ = std::fs::remove_dir_all(&self.path);
}
}
}
fn identity_path(base: &Path, id: &str) -> io::Result<PathBuf> {
if !crate::app_identity::valid_app_id(id) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"app_id must contain 1–255 ASCII letters, digits, dots, underscores or hyphens, with no trailing dot",
));
}
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut path = base.join("identified");
for chunk in id.as_bytes().chunks(64) {
let mut component = String::with_capacity(chunk.len() * 2);
for byte in chunk {
component.push(char::from(HEX[usize::from(byte >> 4)]));
component.push(char::from(HEX[usize::from(byte & 15)]));
}
path.push(component);
}
Ok(path.join("WebView2"))
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
#[test]
fn stable_identity_is_exact_and_independent_of_runner_name() {
let base = tempfile::tempdir().unwrap();
let a = BrowserProfile::create(base.path(), Some("com.example.App")).unwrap();
let same = BrowserProfile::create(base.path(), Some("com.example.App")).unwrap();
let b = BrowserProfile::create(base.path(), Some("com.example.app")).unwrap();
assert_eq!(a.path, same.path);
assert_ne!(a.path, b.path);
assert!(BrowserProfile::create(base.path(), Some(&"a".repeat(255))).is_ok());
for id in [
"", ".", "..", "../app", "app/", "app\\", "C:", "app.", "app?",
] {
assert!(
BrowserProfile::create(base.path(), Some(id)).is_err(),
"{id}"
);
}
assert!(BrowserProfile::create(base.path(), Some(&"a".repeat(256))).is_err());
}
#[test]
fn unidentified_frames_never_share_storage_and_cleanup_is_owned() {
let base = tempfile::tempdir().unwrap();
let a = BrowserProfile::create(base.path(), None).unwrap();
std::fs::write(a.path.join("storage"), "private").unwrap();
let b = BrowserProfile::create(base.path(), None).unwrap();
assert_ne!(a.path, b.path);
assert!(!b.path.join("storage").exists());
let path = a.path.clone();
drop(a);
assert!(!path.exists());
assert!(b.path.exists());
}
}