use std::fs;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use tokio::io::AsyncWriteExt;
pub const PUB_BASE: &str = "https://pub-e57a7c60f6934eb09a6600bf2fc59cdc.r2.dev";
pub const CHROMIUM_VERSION: &str = "149.0.7827.103";
pub const MANIFEST_URL: &str =
"https://raw.githubusercontent.com/ProxyShard/ShardBrowser/main/runtime.json";
const FINGERPRINTS_KEY: &str = "ShardX-Fingerprints.zip";
const FINGERPRINTS_TOP_DIR: &str = "shardx-fingerprints";
pub type ProgressCb = Arc<dyn Fn(&str, u64, u64) + Send + Sync>;
#[derive(Clone, Debug)]
pub struct Archive {
pub key: String,
pub label: String,
}
#[derive(Clone, Debug)]
pub struct HostSpec {
pub browser: Archive,
pub widevine: Option<Archive>,
pub binary_subpath: Vec<String>,
pub widevine_subpath: Vec<String>,
}
fn arc(key: &str, label: &str) -> Archive {
Archive {
key: key.into(),
label: label.into(),
}
}
#[allow(clippy::needless_return)]
pub fn host_spec() -> Result<HostSpec> {
let p = |parts: &[&str]| parts.iter().map(|s| s.to_string()).collect::<Vec<_>>();
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
{
return Ok(HostSpec {
browser: arc("ShardX-Mac-arm64.zip", "ShardX browser (macOS arm64)"),
widevine: Some(arc("ShardX-Widevine-Mac-arm64.zip", "Widevine CDM")),
binary_subpath: p(&["ShardX-Mac-arm64", "ShardX.app", "Contents", "MacOS", "ShardX"]),
widevine_subpath: p(&[
"ShardX-Mac-arm64",
"ShardX.app",
"Contents",
"Frameworks",
"ShardX Framework.framework",
"Versions",
CHROMIUM_VERSION,
"Libraries",
"WidevineCdm",
]),
});
}
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
{
return Ok(HostSpec {
browser: arc("ShardX-Windows.zip", "ShardX browser (Windows x64)"),
widevine: Some(arc("ShardX-Widevine-Win.zip", "Widevine CDM")),
binary_subpath: p(&["ShardX-Windows", "chrome.exe"]),
widevine_subpath: p(&["ShardX-Windows", "WidevineCdm"]),
});
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
return Ok(HostSpec {
browser: arc("ShardX-Linux.zip", "ShardX browser (Linux x64)"),
widevine: Some(arc("ShardX-Widevine-Linux.zip", "Widevine CDM")),
binary_subpath: p(&["ShardX-Linux", "chrome"]),
widevine_subpath: p(&["ShardX-Linux", "WidevineCdm"]),
});
}
#[allow(unreachable_code)]
Err(anyhow!(
"Unsupported host. ShardX ships mac-arm64, win-x64, linux-x64."
))
}
#[allow(clippy::needless_return)]
pub fn default_cache_dir() -> PathBuf {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
#[cfg(target_os = "macos")]
{
return home.join("Library").join("Application Support").join("shardx-sdk");
}
#[cfg(target_os = "windows")]
{
return std::env::var_os("LOCALAPPDATA")
.map(PathBuf::from)
.unwrap_or_else(|| home.clone())
.join("shardx-sdk");
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
return std::env::var_os("XDG_CACHE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home.join(".cache"))
.join("shardx-sdk");
}
}
#[derive(Default, Serialize, Deserialize)]
struct Manifest {
browser_etag: Option<String>,
widevine_etag: Option<String>,
fingerprints_etag: Option<String>,
#[serde(default)]
installed_chromium_version: Option<String>,
}
pub struct Runtime {
pub root: PathBuf,
pub spec: HostSpec,
profiles_override: Option<PathBuf>,
progress: Option<ProgressCb>,
checked: AtomicBool,
engine_version: std::sync::Mutex<String>,
}
impl Runtime {
pub fn new(
cache_dir: Option<PathBuf>,
profiles_dir: Option<PathBuf>,
progress: Option<ProgressCb>,
) -> Result<Self> {
let root = cache_dir.unwrap_or_else(default_cache_dir);
fs::create_dir_all(&root).with_context(|| format!("create cache dir {root:?}"))?;
Ok(Self {
root,
spec: host_spec()?,
profiles_override: profiles_dir,
progress,
checked: AtomicBool::new(false),
engine_version: std::sync::Mutex::new(CHROMIUM_VERSION.to_string()),
})
}
pub fn chromium_version(&self) -> String {
self.engine_version.lock().unwrap().clone()
}
pub fn manifest_path(&self) -> PathBuf {
self.root.join("manifest.json")
}
pub fn binary_path(&self) -> PathBuf {
let mut p = self.root.clone();
for seg in &self.spec.binary_subpath {
p.push(seg);
}
p
}
pub fn fingerprints_dir(&self) -> PathBuf {
let d = self.root.join("fingerprints");
let _ = fs::create_dir_all(&d);
d
}
pub fn profiles_root(&self) -> PathBuf {
let d = self
.profiles_override
.clone()
.unwrap_or_else(|| self.root.join("profiles"));
let _ = fs::create_dir_all(&d);
d
}
pub fn installed(&self) -> bool {
self.binary_path().exists()
}
fn installed_engine_version(&self) -> Option<String> {
#[cfg(target_os = "macos")]
{
let versions = self
.root
.join("ShardX-Mac-arm64")
.join("ShardX.app")
.join("Contents")
.join("Frameworks")
.join("ShardX Framework.framework")
.join("Versions");
for ent in fs::read_dir(&versions).ok()?.flatten() {
let name = ent.file_name().to_string_lossy().into_owned();
if name != "Current" && name.chars().next().is_some_and(|c| c.is_ascii_digit()) {
return Some(name);
}
}
None
}
#[cfg(target_os = "windows")]
{
for ent in fs::read_dir(self.root.join("ShardX-Windows")).ok()?.flatten() {
let p = ent.path();
if p.extension().and_then(|s| s.to_str()) == Some("manifest") {
return p.file_stem().map(|s| s.to_string_lossy().into_owned());
}
}
None
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
None
}
}
fn effective_installed_version(&self, local: &Manifest) -> Option<String> {
self.installed_engine_version()
.or_else(|| local.installed_chromium_version.clone())
}
fn load_manifest(&self) -> Manifest {
fs::read_to_string(self.manifest_path())
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
fn save_manifest(&self, m: &Manifest) -> Result<()> {
fs::write(self.manifest_path(), serde_json::to_string_pretty(m)?)?;
Ok(())
}
pub async fn install(&self, force: bool) -> Result<()> {
if self.checked.load(Ordering::Relaxed) && !force {
return Ok(());
}
let mut local = self.load_manifest();
let remote = fetch_manifest().await;
*self.engine_version.lock().unwrap() = remote
.chromium_version
.clone()
.unwrap_or_else(|| CHROMIUM_VERSION.to_string());
let mut need_browser = force || !self.installed();
if !need_browser {
if let Some(rv) = remote.chromium_version.as_deref() {
need_browser = self.effective_installed_version(&local).as_deref() != Some(rv);
}
}
if need_browser {
let etag = self
.download_and_extract(&self.spec.browser, &self.root)
.await?;
local.browser_etag = Some(etag);
}
if let Some(wv) = self.spec.widevine.clone() {
if need_browser || local.widevine_etag.is_none() {
let etag = self.download_and_extract(&wv, &self.root).await?;
self.place_widevine();
local.widevine_etag = Some(etag);
}
}
let remote_fp = remote.archives.get(FINGERPRINTS_KEY);
let fp_has_json = fs::read_dir(self.fingerprints_dir())
.map(|it| {
it.flatten()
.any(|e| e.path().extension().and_then(|s| s.to_str()) == Some("json"))
})
.unwrap_or(false);
let need_fp = force
|| !fp_has_json
|| remote_fp
.map(|rf| local.fingerprints_etag.as_deref() != Some(rf.as_str()))
.unwrap_or(false);
if need_fp {
self.install_fingerprints().await?;
if let Some(rf) = remote_fp {
local.fingerprints_etag = Some(rf.clone());
}
}
local.installed_chromium_version = self
.installed_engine_version()
.or_else(|| Some(self.chromium_version()));
self.save_manifest(&local)?;
#[cfg(unix)]
fix_unix_exec_bits(&self.root);
self.checked.store(true, Ordering::Relaxed);
Ok(())
}
async fn download_and_extract(&self, archive: &Archive, dest: &Path) -> Result<String> {
let url = format!("{PUB_BASE}/{}", archive.key);
fs::create_dir_all(dest)?;
let tmp = dest.join(format!(".{}.tmp", archive.key));
let mut resp = reqwest::Client::new()
.get(&url)
.send()
.await?
.error_for_status()
.with_context(|| format!("download {}", archive.key))?;
let total = resp.content_length().unwrap_or(0);
let etag = resp
.headers()
.get("etag")
.and_then(|v| v.to_str().ok())
.map(|s| s.trim_matches('"').to_string())
.unwrap_or_default();
{
let mut out = tokio::fs::File::create(&tmp).await?;
let mut received: u64 = 0;
while let Some(chunk) = resp.chunk().await? {
out.write_all(&chunk).await?;
received += chunk.len() as u64;
if let Some(cb) = &self.progress {
cb(&archive.label, received, total);
}
}
out.flush().await?;
}
let zip_path = tmp.clone();
let dest = dest.to_path_buf();
tokio::task::spawn_blocking(move || extract_zip(&zip_path, &dest)).await??;
let _ = fs::remove_file(&tmp);
Ok(etag)
}
async fn install_fingerprints(&self) -> Result<()> {
let dir = self.fingerprints_dir();
let staging = dir.join(".staging");
let _ = fs::remove_dir_all(&staging);
fs::create_dir_all(&staging)?;
let archive = Archive {
key: FINGERPRINTS_KEY.into(),
label: "Fingerprint library".into(),
};
self.download_and_extract(&archive, &staging).await?;
let src = staging.join(FINGERPRINTS_TOP_DIR);
let walk = if src.exists() { src } else { staging.clone() };
for ent in fs::read_dir(&walk)? {
let ent = ent?;
let p = ent.path();
if p.extension().and_then(|s| s.to_str()) != Some("json") {
continue;
}
let dst = dir.join(p.file_name().unwrap());
fs::copy(&p, &dst)?;
}
let _ = fs::remove_dir_all(&staging);
Ok(())
}
fn place_widevine(&self) {
let Some(wv) = &self.spec.widevine else { return };
let wrapper = wv.key.trim_end_matches(".zip");
let src = self.root.join(wrapper).join("WidevineCdm");
if !src.exists() {
return;
}
let mut dst = self.root.clone();
for seg in &self.spec.widevine_subpath {
dst.push(seg);
}
if dst.exists() {
let _ = fs::remove_dir_all(&dst);
}
if let Some(parent) = dst.parent() {
let _ = fs::create_dir_all(parent);
}
let _ = fs::rename(&src, &dst);
let _ = fs::remove_dir_all(self.root.join(wrapper));
}
}
#[derive(Default)]
struct RemoteManifest {
archives: std::collections::HashMap<String, String>,
chromium_version: Option<String>,
}
async fn fetch_manifest() -> RemoteManifest {
async fn inner() -> Option<RemoteManifest> {
let resp = reqwest::Client::new().get(MANIFEST_URL).send().await.ok()?;
if !resp.status().is_success() {
return None;
}
let v: serde_json::Value = resp.json().await.ok()?;
let archives = v
.get("archives")
.and_then(|a| a.as_object())
.map(|o| {
o.iter()
.filter_map(|(k, val)| val.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default();
let chromium_version = v
.get("chromium_version")
.and_then(|s| s.as_str())
.map(String::from);
Some(RemoteManifest {
archives,
chromium_version,
})
}
inner().await.unwrap_or_default()
}
fn extract_zip(zip_path: &Path, dest: &Path) -> Result<()> {
fs::create_dir_all(dest)?;
#[cfg(unix)]
{
use std::process::Command;
let out = Command::new("unzip")
.arg("-q")
.arg("-o")
.arg(zip_path)
.arg("-d")
.arg(dest)
.output()
.map_err(|e| {
anyhow!("system `unzip` not found ({e}); install via `apt install unzip` / `brew install unzip`")
})?;
let code = out.status.code().unwrap_or(-1);
if code > 1 {
anyhow::bail!(
"unzip failed for {} (exit {}): {}",
zip_path.display(),
code,
String::from_utf8_lossy(&out.stderr).trim()
);
}
Ok(())
}
#[cfg(not(unix))]
{
let f = fs::File::open(zip_path)?;
let mut archive = zip::ZipArchive::new(f)?;
archive.extract(dest)?;
Ok(())
}
}
#[cfg(unix)]
fn fix_unix_exec_bits(root: &Path) {
use std::io::Read;
use std::os::unix::fs::PermissionsExt;
const MAGIC: &[[u8; 4]] = &[
[0x7f, b'E', b'L', b'F'],
[0xfe, 0xed, 0xfa, 0xcf],
[0xcf, 0xfa, 0xed, 0xfe],
[0xfe, 0xed, 0xfa, 0xce],
[0xce, 0xfa, 0xed, 0xfe],
[0xca, 0xfe, 0xba, 0xbe],
[0xbe, 0xba, 0xfe, 0xca],
];
fn walk(dir: &Path) {
let Ok(entries) = fs::read_dir(dir) else { return };
for ent in entries.flatten() {
let p = ent.path();
let Ok(ft) = ent.file_type() else { continue };
if ft.is_symlink() {
continue;
}
if ft.is_dir() {
walk(&p);
continue;
}
if !ft.is_file() {
continue;
}
let mut head = [0u8; 4];
let Ok(mut f) = fs::File::open(&p) else { continue };
if f.read_exact(&mut head).is_err() {
continue;
}
if !MAGIC.contains(&head) {
continue;
}
if let Ok(meta) = fs::metadata(&p) {
let mut perm = meta.permissions();
perm.set_mode(perm.mode() | 0o111);
let _ = fs::set_permissions(&p, perm);
}
}
}
walk(root);
}