use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::backup::{self, RestoreReport};
use crate::error::{Error, Result};
use crate::registry;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Protocol {
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default = "default_url_protocol")]
pub url_protocol: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
pub command: String,
}
fn default_url_protocol() -> bool {
true
}
impl Protocol {
pub fn new(name: impl Into<String>, command: impl Into<String>) -> Result<Self> {
let name = name.into();
Self::validate_name(&name)?;
Ok(Self {
name,
description: String::new(),
url_protocol: true,
icon: None,
command: command.into(),
})
}
pub fn is_valid_name(name: &str) -> bool {
let mut chars = name.chars();
match chars.next() {
Some(first) if first.is_ascii_alphabetic() => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
}
pub fn validate_name(name: &str) -> Result<()> {
if Self::is_valid_name(name) {
Ok(())
} else {
Err(Error::InvalidProtocolName {
name: name.to_string(),
})
}
}
}
pub struct ProtocolManager;
impl ProtocolManager {
pub fn is_elevated() -> bool {
registry::is_elevated()
}
pub fn query(name: &str) -> Result<Option<Protocol>> {
registry::read_protocol(name)
}
pub fn exists(name: &str) -> Result<bool> {
Ok(registry::read_protocol(name)?.is_some())
}
pub fn add(protocol: &Protocol) -> Result<()> {
Protocol::validate_name(&protocol.name)?;
registry::write_protocol(protocol)
}
pub fn remove(name: &str) -> Result<bool> {
Protocol::validate_name(name)?;
registry::delete_protocol(name)
}
pub fn list() -> Result<Vec<Protocol>> {
let mut protocols = registry::list_url_protocols()?;
protocols.sort_by(|a, b| a.name.cmp(&b.name));
Ok(protocols)
}
pub fn backup_to_json() -> Result<Vec<Protocol>> {
Self::list()
}
pub fn backup_to_file(path: impl AsRef<Path>) -> Result<Vec<Protocol>> {
let protocols = Self::backup_to_json()?;
backup::write_to_file(&protocols, path)?;
Ok(protocols)
}
pub fn backup_protocol_to_json(name: &str) -> Result<Option<Protocol>> {
Protocol::validate_name(name)?;
registry::read_protocol(name)
}
pub fn backup_protocol_to_file(name: &str, path: impl AsRef<Path>) -> Result<Option<Protocol>> {
let Some(protocol) = Self::backup_protocol_to_json(name)? else {
return Ok(None);
};
backup::write_to_file(std::slice::from_ref(&protocol), path)?;
Ok(Some(protocol))
}
pub fn restore_from_json(json: &str) -> Result<RestoreReport> {
let protocols = backup::parse_backup(json)?;
restore_backup(&protocols)
}
pub fn restore_from_file(path: impl AsRef<Path>) -> Result<RestoreReport> {
let protocols = backup::read_from_file(path)?;
restore_backup(&protocols)
}
pub fn restore_protocol_from_file(name: &str, path: impl AsRef<Path>) -> Result<bool> {
Protocol::validate_name(name)?;
let protocols = backup::read_from_file(path)?;
let Some(protocol) = protocols.iter().find(|protocol| protocol.name == name) else {
return Ok(false);
};
registry::write_protocol(protocol)?;
Ok(true)
}
pub fn set_browser_policy(enabled: bool) -> Result<()> {
registry::set_browser_policy(enabled)
}
}
fn restore_backup(protocols: &[Protocol]) -> Result<RestoreReport> {
if !protocols.is_empty() {
registry::ensure_elevated()?;
}
let mut report = RestoreReport::default();
for protocol in protocols {
let outcome = (|| -> Result<()> {
if protocol.command.trim().is_empty() {
return Err(Error::InvalidBackup(format!(
"protocol `{}` has an empty command",
protocol.name
)));
}
Protocol::validate_name(&protocol.name)?;
registry::write_protocol(protocol)
})();
match outcome {
Ok(()) => report.restored += 1,
Err(err) => report.failed.push((protocol.name.clone(), err.to_string())),
}
}
Ok(report)
}