eoka-cli 0.3.7

Browser automation CLI for AI agents and pentesting
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

pub fn default_profile_dir() -> Option<PathBuf> {
    let root_var = if cfg!(target_os = "windows") {
        "LOCALAPPDATA"
    } else {
        "HOME"
    };
    let root = PathBuf::from(std::env::var_os(root_var)?);
    default_profile_dir_in(&root, std::env::consts::OS)
}

fn default_profile_dir_in(root: &Path, platform: &str) -> Option<PathBuf> {
    let candidates: &[&str] = match platform {
        "linux" => &[
            ".config/google-chrome",
            ".config/chromium",
            ".config/microsoft-edge",
            ".var/app/com.google.Chrome/config/google-chrome",
        ],
        "macos" => &[
            "Library/Application Support/Google/Chrome",
            "Library/Application Support/Chromium",
            "Library/Application Support/Microsoft Edge",
        ],
        "windows" => return Some(root.join("Google").join("Chrome").join("User Data")),
        _ => return None,
    };
    candidates
        .iter()
        .map(|candidate| root.join(candidate))
        .find(|path| path.exists())
}

pub fn clone_profile_dir(src: &Path) -> io::Result<PathBuf> {
    static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
    let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
    let dst = std::env::temp_dir().join(format!("eoka-profile-clone-{}-{}", std::process::id(), n));
    fs::create_dir_all(&dst)?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let _ = fs::set_permissions(&dst, fs::Permissions::from_mode(0o700));
    }
    copy_recursive(src, &dst)?;
    Ok(dst)
}

fn should_skip(name: &str) -> bool {
    matches!(
        name,
        "SingletonSocket"
            | "SingletonCookie"
            | "SingletonLock"
            | "Crashpad"
            | "Crash Reports"
            | "GrShaderCache"
            | "ShaderCache"
            | "GraphiteDawnCache"
            | "component_crx_cache"
            | "Service Worker"
            | "Code Cache"
            | "CacheStorage"
            | "Cache"
            | "Extension Rules"
            | "Extension State"
    )
}

fn copy_recursive(src: &Path, dst: &Path) -> io::Result<()> {
    if !src.is_dir() {
        return Ok(());
    }
    for entry in fs::read_dir(src)? {
        let entry = entry?;
        let name = entry.file_name();
        let name_str = name.to_string_lossy();
        if should_skip(&name_str) {
            continue;
        }
        let from = entry.path();
        let to = dst.join(&name);
        let ft = entry.file_type()?;
        if ft.is_dir() {
            fs::create_dir_all(&to)?;
            copy_recursive(&from, &to)?;
        } else if ft.is_file() {
            if let Err(e) = fs::copy(&from, &to) {
                eprintln!("[eoka] skipping {}: {}", from.display(), e);
            }
        }
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    #[cfg(unix)]
    use std::os::unix::fs::PermissionsExt;

    fn temp_root(name: &str) -> PathBuf {
        let dir =
            std::env::temp_dir().join(format!("eoka-profile-test-{}-{}", std::process::id(), name));
        let _ = fs::remove_dir_all(&dir);
        fs::create_dir_all(&dir).unwrap();
        dir
    }

    #[test]
    #[cfg(unix)]
    fn clone_is_owner_only() {
        let src = temp_root("src");
        let profile = src.join("google-chrome");
        fs::create_dir_all(profile.join("Default")).unwrap();
        fs::write(profile.join("Default").join("Cookies"), "cookie-bytes").unwrap();

        let dst = clone_profile_dir(&profile).unwrap();
        let mode = fs::metadata(&dst).unwrap().permissions().mode() & 0o777;
        assert_eq!(
            mode, 0o700,
            "clone dir holds the cookie store: must be 0700"
        );
        assert!(dst.join("Default").join("Cookies").exists());
        fs::remove_dir_all(&dst).unwrap();
        fs::remove_dir_all(&src).unwrap();
    }

    #[test]
    fn default_profile_dir_finds_flatpak_chrome() {
        let root = temp_root("flatpak");
        let flatpak = root.join(".var/app/com.google.Chrome/config/google-chrome");
        fs::create_dir_all(&flatpak).unwrap();
        assert_eq!(default_profile_dir_in(&root, "linux"), Some(flatpak));
        fs::remove_dir_all(&root).unwrap();
    }

    #[test]
    fn default_profile_dir_finds_plain_chrome_before_flatpak() {
        let root = temp_root("both");
        let plain = root.join(".config/google-chrome");
        let flatpak = root.join(".var/app/com.google.Chrome/config/google-chrome");
        fs::create_dir_all(&plain).unwrap();
        fs::create_dir_all(&flatpak).unwrap();
        assert_eq!(default_profile_dir_in(&root, "linux"), Some(plain));
        fs::remove_dir_all(&root).unwrap();
    }

    #[test]
    fn default_profile_dir_preserves_macos_precedence() {
        let root = temp_root("macos");
        for candidate in [
            "Library/Application Support/Microsoft Edge",
            "Library/Application Support/Chromium",
            "Library/Application Support/Google/Chrome",
        ] {
            let profile = root.join(candidate);
            fs::create_dir_all(&profile).unwrap();
            assert_eq!(default_profile_dir_in(&root, "macos"), Some(profile));
        }
        fs::remove_dir_all(&root).unwrap();
    }

    #[test]
    fn default_profile_dir_uses_windows_local_app_data() {
        let root = Path::new("local-app-data");
        assert_eq!(
            default_profile_dir_in(root, "windows"),
            Some(root.join("Google").join("Chrome").join("User Data"))
        );
    }

    #[test]
    fn default_profile_dir_none_without_profiles() {
        let root = temp_root("empty");
        for platform in ["linux", "macos", "unsupported"] {
            assert_eq!(default_profile_dir_in(&root, platform), None);
        }
        fs::remove_dir_all(&root).unwrap();
    }
}