use std::env;
use std::fs;
use std::io;
use std::path::PathBuf;
const DEFAULT_FEATURE: &str = "e2e";
const DEFAULT_FILE_NAME: &str = "phyto.json";
const DEFAULT_WINDOW: &str = "main";
const CAPABILITY_IDENTIFIER: &str = "phyto-e2e";
const CAPABILITY_DESCRIPTION: &str =
"Phyto E2E test plugin — enabled only under the e2e feature.";
pub fn sync_capability() {
SyncCapability::new().run();
}
#[derive(Debug, Clone)]
pub struct SyncCapability {
feature: String,
windows: Vec<String>,
file_name: String,
}
impl SyncCapability {
pub fn new() -> Self {
Self {
feature: DEFAULT_FEATURE.to_string(),
windows: vec![DEFAULT_WINDOW.to_string()],
file_name: DEFAULT_FILE_NAME.to_string(),
}
}
pub fn feature(mut self, feature: impl Into<String>) -> Self {
self.feature = feature.into();
self
}
pub fn windows<I, S>(mut self, windows: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.windows = windows.into_iter().map(Into::into).collect();
self
}
pub fn file_name(mut self, name: impl Into<String>) -> Self {
self.file_name = name.into();
self
}
pub fn run(self) {
let cargo_feature_var = format!(
"CARGO_FEATURE_{}",
self.feature.to_uppercase().replace('-', "_")
);
println!("cargo:rerun-if-env-changed={}", cargo_feature_var);
let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect(
"tauri-plugin-phyto::build::sync_capability: CARGO_MANIFEST_DIR not set — \
this helper must be called from a build script",
);
let capabilities_dir = PathBuf::from(&manifest_dir).join("capabilities");
let target = capabilities_dir.join(&self.file_name);
println!("cargo:rerun-if-changed={}", target.display());
let feature_active = env::var(&cargo_feature_var).is_ok();
if feature_active {
if let Err(e) = fs::create_dir_all(&capabilities_dir) {
panic!(
"tauri-plugin-phyto::build::sync_capability: failed to create {}: {}",
capabilities_dir.display(),
e
);
}
let json = render_capability(&self.windows);
if let Err(e) = fs::write(&target, json) {
panic!(
"tauri-plugin-phyto::build::sync_capability: failed to write {}: {}",
target.display(),
e
);
}
} else {
match fs::remove_file(&target) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => panic!(
"tauri-plugin-phyto::build::sync_capability: failed to remove {}: {}",
target.display(),
e
),
}
}
}
}
impl Default for SyncCapability {
fn default() -> Self {
Self::new()
}
}
fn render_capability(windows: &[String]) -> String {
let mut windows_arr = String::new();
for (i, w) in windows.iter().enumerate() {
if i > 0 {
windows_arr.push_str(", ");
}
windows_arr.push('"');
windows_arr.push_str(&escape_json(w));
windows_arr.push('"');
}
format!(
"{{\n \"identifier\": \"{ident}\",\n \"description\": \"{desc}\",\n \"windows\": [{windows}],\n \"permissions\": [\"phyto:default\"]\n}}\n",
ident = CAPABILITY_IDENTIFIER,
desc = escape_json(CAPABILITY_DESCRIPTION),
windows = windows_arr,
)
}
fn escape_json(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn renders_default_capability() {
let json = render_capability(&["main".to_string()]);
assert!(json.contains("\"identifier\": \"phyto-e2e\""));
assert!(json.contains("\"windows\": [\"main\"]"));
assert!(json.contains("\"permissions\": [\"phyto:default\"]"));
}
#[test]
fn renders_multi_window_capability() {
let json = render_capability(&["main".to_string(), "settings".to_string()]);
assert!(json.contains("\"windows\": [\"main\", \"settings\"]"));
}
#[test]
fn escapes_special_chars() {
assert_eq!(escape_json("he said \"hi\""), r#"he said \"hi\""#);
assert_eq!(escape_json("back\\slash"), r"back\\slash");
assert_eq!(escape_json("line1\nline2"), r"line1\nline2");
}
#[test]
fn escape_passes_through_non_ascii() {
assert_eq!(escape_json("a — b"), "a — b");
}
}