mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-10-18 23:07:27 +02:00
fix(deep-link): preserve XDG registration ownership and report failures (#3585)
* fix(deep-link): honor XDG failures and unregister associations * test(deep-link): isolate other application handlers * chore: satisfy release metadata and test header checks * fix(deep-link): gate XDG helpers to Linux The plugin only supports Linux, so FreeBSD builds warned about dead code. * refactor(deep-link): add run_command helper Share the spawn, status check and logging for XDG commands. * refactor(deep-link): share desktop file list helpers register and unregister now edit MimeType lists the same way. * fix(deep-link): keep desktop file quoting on register Parse raw values so a quoted Exec matches and isn't rewritten each time. * fix(deep-link): clean mimeapps.list first on unregister A failing update-desktop-database no longer leaves the app as the default. * fix(deep-link): keep mimeapps.list comments on unregister Edit it line by line instead of re-serializing it through rust-ini. * fix(deep-link): skip cache refresh when nothing to unregister Only run update-desktop-database if the desktop file changed or the cache still lists the app. * fix(deep-link): treat no default handler as unregistered xdg-mime exits with 4 on KDE and LXQt when no handler is set. * fix(deep-link): tolerate padded xdg-mime query output Compare the trimmed first line instead of the raw output. * test(deep-link): run XDG tests in CI Drop #[ignore], skip when the tools are missing, and install them on Linux CI. --------- Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
1 parent
f83c5d3b0f
commit
2d541bc0d1
7 files changed
+731
-75
No files matched your search
@@ -0,0 +1,259 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use std::{
|
||||
env, fs,
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::PathBuf,
|
||||
process::{Child, Command},
|
||||
thread,
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tauri::test::{mock_builder, mock_context, noop_assets};
|
||||
use tauri_plugin_deep_link::{DeepLinkExt, Error};
|
||||
|
||||
const TEST_NAME: &str = "xdg_command_results";
|
||||
const MODE_ENV: &str = "TAURI_XDG_COMMAND_MODE";
|
||||
|
||||
struct Sandbox(PathBuf);
|
||||
impl Drop for Sandbox {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
struct TestChild(Child);
|
||||
impl Drop for TestChild {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.0.kill();
|
||||
let _ = self.0.wait();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xdg_command_results() {
|
||||
if let Ok(mode) = env::var(MODE_ENV) {
|
||||
exercise_commands(&mode);
|
||||
return;
|
||||
}
|
||||
|
||||
let root = env::temp_dir().join(format!(
|
||||
"tauri-xdg-commands-{}-{}",
|
||||
std::process::id(),
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
fs::create_dir(&root).unwrap();
|
||||
let sandbox = Sandbox(root);
|
||||
for mode in [
|
||||
"real",
|
||||
"update-failure",
|
||||
"register-failure",
|
||||
"query-failure",
|
||||
"query-no-default",
|
||||
"query-padded",
|
||||
"unregister-failure",
|
||||
] {
|
||||
// The stubbed modes run anywhere; only the real one needs the XDG tools.
|
||||
if mode == "real" && !(has_command("xdg-mime") && has_command("update-desktop-database")) {
|
||||
eprintln!(
|
||||
"skipping the real XDG commands: xdg-mime or update-desktop-database is missing"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let home = sandbox.0.join(mode);
|
||||
fs::create_dir(&home).unwrap();
|
||||
for directory in [
|
||||
"config",
|
||||
"data",
|
||||
"runtime",
|
||||
"system-config",
|
||||
"system-data",
|
||||
"bin",
|
||||
] {
|
||||
fs::create_dir(home.join(directory)).unwrap();
|
||||
}
|
||||
for command in ["update-desktop-database", "xdg-mime"] {
|
||||
let path = home.join("bin").join(command);
|
||||
fs::write(
|
||||
&path,
|
||||
format!(
|
||||
"#!/bin/sh\nprintf '%s\\n' '{command}' >> \"$HOME/calls\"\n\
|
||||
if [ '{command}' = update-desktop-database ]; then\n\
|
||||
[ \"$TAURI_XDG_COMMAND_MODE\" = update-failure ] && exit 7\n\
|
||||
[ \"$TAURI_XDG_COMMAND_MODE\" = unregister-failure ] && exit 7\n\
|
||||
else\n\
|
||||
[ \"$TAURI_XDG_COMMAND_MODE\" = register-failure ] && exit 7\n\
|
||||
[ \"$TAURI_XDG_COMMAND_MODE\" = query-no-default ] && exit 4\n\
|
||||
[ \"$TAURI_XDG_COMMAND_MODE\" = query-padded ] && printf ' %s \\r\\nother\\n' \"$TAURI_XDG_DESKTOP_FILE\" && exit 0\n\
|
||||
printf '%s\\n' \"$TAURI_XDG_DESKTOP_FILE\"\n\
|
||||
[ \"$TAURI_XDG_COMMAND_MODE\" = query-failure ] && exit 7\n\
|
||||
fi\nexit 0\n"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
let mut child = TestChild(
|
||||
Command::new(env::current_exe().unwrap())
|
||||
.args(["--exact", TEST_NAME, "--nocapture"])
|
||||
// Isolate real XDG tools from the operator's desktop, bus, and files.
|
||||
.env_clear()
|
||||
.env(
|
||||
"PATH",
|
||||
if mode == "real" {
|
||||
env::var_os("PATH").unwrap()
|
||||
} else {
|
||||
home.join("bin").into_os_string()
|
||||
},
|
||||
)
|
||||
.env("HOME", &home)
|
||||
.env("XDG_CONFIG_HOME", home.join("config"))
|
||||
.env("XDG_DATA_HOME", home.join("data"))
|
||||
.env("XDG_RUNTIME_DIR", home.join("runtime"))
|
||||
.env("XDG_CONFIG_DIRS", home.join("system-config"))
|
||||
.env("XDG_DATA_DIRS", home.join("system-data"))
|
||||
.env("XDG_CURRENT_DESKTOP", "X-Generic")
|
||||
.env(MODE_ENV, mode)
|
||||
.env("TAURI_XDG_DESKTOP_FILE", desktop_file_name())
|
||||
.spawn()
|
||||
.unwrap(),
|
||||
);
|
||||
let deadline = Instant::now() + Duration::from_secs(30);
|
||||
loop {
|
||||
if let Some(status) = child.0.try_wait().unwrap() {
|
||||
assert!(status.success(), "XDG child {mode} failed: {status}");
|
||||
break;
|
||||
}
|
||||
assert!(Instant::now() < deadline, "XDG child {mode} timed out");
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn desktop_file_name() -> String {
|
||||
format!(
|
||||
"{}-handler.desktop",
|
||||
tauri::utils::platform::current_exe()
|
||||
.unwrap()
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
)
|
||||
}
|
||||
|
||||
fn exercise_commands(mode: &str) {
|
||||
let app = mock_builder()
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.build(mock_context(noop_assets()))
|
||||
.unwrap();
|
||||
let deep_link = app.deep_link();
|
||||
let scheme = "tauri-command-test";
|
||||
if mode == "query-no-default" {
|
||||
// Some xdg-mime backends exit with 4 when no handler is set.
|
||||
assert!(!deep_link.is_registered(scheme).unwrap());
|
||||
return;
|
||||
}
|
||||
if mode == "query-padded" {
|
||||
// Surrounding whitespace and extra lines don't hide a match.
|
||||
assert!(deep_link.is_registered(scheme).unwrap());
|
||||
return;
|
||||
}
|
||||
if mode != "real" {
|
||||
let error = if mode == "query-failure" {
|
||||
deep_link.is_registered(scheme).unwrap_err()
|
||||
} else if mode == "unregister-failure" {
|
||||
// Registration leaves its desktop file when the cache update fails.
|
||||
assert!(matches!(deep_link.register(scheme), Err(Error::Io(_))));
|
||||
let mimeapps_path =
|
||||
PathBuf::from(env::var_os("XDG_CONFIG_HOME").unwrap()).join("mimeapps.list");
|
||||
fs::write(
|
||||
&mimeapps_path,
|
||||
format!(
|
||||
"[Default Applications]\nx-scheme-handler/{scheme}={}\n",
|
||||
desktop_file_name()
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
let error = deep_link.unregister(scheme).unwrap_err();
|
||||
// Nothing is left to withdraw, so the failing cache refresh is not run again.
|
||||
deep_link.unregister(scheme).unwrap();
|
||||
// The default is withdrawn even though the cache refresh failed.
|
||||
let mimeapps = ini::Ini::load_from_file(&mimeapps_path).unwrap();
|
||||
assert!(
|
||||
mimeapps
|
||||
.section(Some("Default Applications"))
|
||||
.unwrap()
|
||||
.get(format!("x-scheme-handler/{scheme}"))
|
||||
.is_none()
|
||||
);
|
||||
error
|
||||
} else {
|
||||
deep_link.register(scheme).unwrap_err()
|
||||
};
|
||||
let Error::Io(error) = error else {
|
||||
panic!("command failure must use the existing I/O error variant")
|
||||
};
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::Other);
|
||||
let command = if matches!(mode, "update-failure" | "unregister-failure") {
|
||||
"update-desktop-database"
|
||||
} else {
|
||||
"xdg-mime"
|
||||
};
|
||||
assert!(error.to_string().contains(command));
|
||||
assert!(error.to_string().contains('7'));
|
||||
let calls =
|
||||
fs::read_to_string(PathBuf::from(env::var_os("HOME").unwrap()).join("calls")).unwrap();
|
||||
let expected = match mode {
|
||||
"update-failure" => vec!["update-desktop-database"],
|
||||
"register-failure" => vec!["update-desktop-database", "xdg-mime"],
|
||||
"query-failure" => vec!["xdg-mime"],
|
||||
"unregister-failure" => vec!["update-desktop-database", "update-desktop-database"],
|
||||
_ => unreachable!(),
|
||||
};
|
||||
assert_eq!(calls.lines().collect::<Vec<_>>(), expected);
|
||||
return;
|
||||
}
|
||||
|
||||
deep_link.register(scheme).unwrap();
|
||||
assert!(deep_link.is_registered(scheme).unwrap());
|
||||
let file_name = desktop_file_name();
|
||||
let other_file = format!("other-{file_name}");
|
||||
let applications = PathBuf::from(env::var_os("XDG_DATA_HOME").unwrap()).join("applications");
|
||||
// Use an independent handler: older xdg-utils cannot resolve a quoted Exec token.
|
||||
fs::write(
|
||||
applications.join(&other_file),
|
||||
format!("[Desktop Entry]\nType=Application\nName=Other handler\nExec=/bin/sh\nMimeType=x-scheme-handler/{scheme};\n"),
|
||||
)
|
||||
.unwrap();
|
||||
let mimeapps_path =
|
||||
PathBuf::from(env::var_os("XDG_CONFIG_HOME").unwrap()).join("mimeapps.list");
|
||||
let mut mimeapps = ini::Ini::load_from_file(&mimeapps_path).unwrap();
|
||||
mimeapps
|
||||
.with_section(Some("Default Applications"))
|
||||
.set(format!("x-scheme-handler/{scheme}"), &other_file);
|
||||
mimeapps.write_to_file(mimeapps_path).unwrap();
|
||||
let queried = Command::new("xdg-mime")
|
||||
.args(["query", "default", &format!("x-scheme-handler/{scheme}")])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(queried.status.success());
|
||||
assert_eq!(
|
||||
String::from_utf8(queried.stdout)
|
||||
.unwrap()
|
||||
.trim_end_matches('\n'),
|
||||
other_file
|
||||
);
|
||||
assert!(!deep_link.is_registered(scheme).unwrap());
|
||||
}
|
||||
|
||||
/// Whether `command` is on `PATH`, so the test can be skipped where XDG tools are missing.
|
||||
fn has_command(command: &str) -> bool {
|
||||
std::env::var_os("PATH")
|
||||
.is_some_and(|path| std::env::split_paths(&path).any(|dir| dir.join(command).is_file()))
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
use std::{
|
||||
fs,
|
||||
path::PathBuf,
|
||||
process::{Child, Command},
|
||||
thread,
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tauri::test::{mock_builder, mock_context, noop_assets};
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
|
||||
struct Sandbox(PathBuf);
|
||||
|
||||
impl Drop for Sandbox {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
struct ChildProcess(Child);
|
||||
|
||||
impl Drop for ChildProcess {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.0.kill();
|
||||
let _ = self.0.wait();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xdg_unregister() {
|
||||
if std::env::var_os("TAURI_XDG_TEST_CHILD").is_some() {
|
||||
exercise_registration();
|
||||
return;
|
||||
}
|
||||
if !(has_command("xdg-mime") && has_command("update-desktop-database")) {
|
||||
eprintln!("skipping: xdg-mime or update-desktop-database is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"tauri-deep-link-{}-{}",
|
||||
std::process::id(),
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
fs::create_dir(&root).unwrap();
|
||||
let sandbox = Sandbox(root);
|
||||
for directory in ["config", "data", "runtime", "system-config", "system-data"] {
|
||||
fs::create_dir(sandbox.0.join(directory)).unwrap();
|
||||
}
|
||||
// Exercise quoting with a real executable path containing a space.
|
||||
let executable_dir = sandbox.0.join("app directory");
|
||||
fs::create_dir(&executable_dir).unwrap();
|
||||
let executable = executable_dir.join("test-app");
|
||||
fs::copy(std::env::current_exe().unwrap(), &executable).unwrap();
|
||||
let mut child = ChildProcess(
|
||||
Command::new(&executable)
|
||||
.args(["--exact", "xdg_unregister", "--nocapture"])
|
||||
// Use xdg-utils' generic backend without the operator's desktop or bus.
|
||||
.env_clear()
|
||||
.env("PATH", std::env::var_os("PATH").unwrap())
|
||||
.env("HOME", &sandbox.0)
|
||||
.env("XDG_RUNTIME_DIR", sandbox.0.join("runtime"))
|
||||
.env("XDG_CONFIG_HOME", sandbox.0.join("config"))
|
||||
.env("XDG_DATA_HOME", sandbox.0.join("data"))
|
||||
.env("XDG_CONFIG_DIRS", sandbox.0.join("system-config"))
|
||||
.env("XDG_DATA_DIRS", sandbox.0.join("system-data"))
|
||||
.env("XDG_CURRENT_DESKTOP", "X-Generic")
|
||||
.env("TAURI_XDG_TEST_CHILD", "1")
|
||||
.spawn()
|
||||
.unwrap(),
|
||||
);
|
||||
let deadline = Instant::now() + Duration::from_secs(30);
|
||||
loop {
|
||||
if let Some(status) = child.0.try_wait().unwrap() {
|
||||
assert!(status.success(), "XDG registration child failed: {status}");
|
||||
break;
|
||||
}
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"XDG registration child timed out"
|
||||
);
|
||||
thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
|
||||
fn exercise_registration() {
|
||||
let app = mock_builder()
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.build(mock_context(noop_assets()))
|
||||
.unwrap();
|
||||
let deep_link = app.deep_link();
|
||||
let config = PathBuf::from(std::env::var_os("XDG_CONFIG_HOME").unwrap());
|
||||
let data = PathBuf::from(std::env::var_os("XDG_DATA_HOME").unwrap());
|
||||
let mimeapps_path = config.join("mimeapps.list");
|
||||
fs::write(
|
||||
&mimeapps_path,
|
||||
"# user comment\n[Default Applications]\nx-scheme-handler/unrelated=other.desktop\n\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for scheme in ["tauri-test-first", "tauri-test-second", "tauri-test-first"] {
|
||||
deep_link.register(scheme).unwrap();
|
||||
assert!(deep_link.is_registered(scheme).unwrap());
|
||||
}
|
||||
|
||||
let executable = tauri::utils::platform::current_exe().unwrap();
|
||||
let file_name = format!(
|
||||
"{}-handler.desktop",
|
||||
executable.file_name().unwrap().to_string_lossy()
|
||||
);
|
||||
let desktop_text = fs::read_to_string(data.join("applications").join(&file_name)).unwrap();
|
||||
// INI parsing consumes quotes; verify the desktop command's quoting in the file itself.
|
||||
assert_eq!(
|
||||
desktop_text.lines().find(|line| line.starts_with("Exec=")),
|
||||
Some(format!("Exec=\"{}\" %u", executable.display()).as_str())
|
||||
);
|
||||
let desktop = ini::Ini::load_from_str(&desktop_text).unwrap();
|
||||
let section = desktop.section(Some("Desktop Entry")).unwrap();
|
||||
let mimes: Vec<_> = section
|
||||
.get("MimeType")
|
||||
.unwrap()
|
||||
.split(';')
|
||||
.filter(|mime| !mime.is_empty())
|
||||
.collect();
|
||||
assert_eq!(mimes.len(), 2);
|
||||
assert!(mimes.contains(&"x-scheme-handler/tauri-test-first"));
|
||||
assert!(mimes.contains(&"x-scheme-handler/tauri-test-second"));
|
||||
|
||||
deep_link.unregister("tauri-test-first").unwrap();
|
||||
assert!(!deep_link.is_registered("tauri-test-first").unwrap());
|
||||
assert!(deep_link.is_registered("tauri-test-second").unwrap());
|
||||
// The user's comments and blank lines survive the edit.
|
||||
let mimeapps_text = fs::read_to_string(&mimeapps_path).unwrap();
|
||||
assert!(mimeapps_text.starts_with("# user comment\n"));
|
||||
assert!(mimeapps_text.contains("\n\n"));
|
||||
let after = fs::read_to_string(data.join("applications").join(&file_name)).unwrap();
|
||||
assert_eq!(
|
||||
after.lines().find(|line| line.starts_with("Exec=")),
|
||||
desktop_text.lines().find(|line| line.starts_with("Exec="))
|
||||
);
|
||||
let desktop = ini::Ini::load_from_str(&after).unwrap();
|
||||
assert_eq!(
|
||||
desktop
|
||||
.section(Some("Desktop Entry"))
|
||||
.unwrap()
|
||||
.get("MimeType"),
|
||||
Some("x-scheme-handler/tauri-test-second;")
|
||||
);
|
||||
let mut mimeapps = ini::Ini::load_from_file(&mimeapps_path).unwrap();
|
||||
let defaults = mimeapps.section(Some("Default Applications")).unwrap();
|
||||
assert!(defaults.get("x-scheme-handler/tauri-test-first").is_none());
|
||||
assert_eq!(
|
||||
defaults.get("x-scheme-handler/tauri-test-second"),
|
||||
Some(file_name.as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
defaults.get("x-scheme-handler/unrelated"),
|
||||
Some("other.desktop")
|
||||
);
|
||||
|
||||
// An app must not remove another handler that became the user's default.
|
||||
// Keep the other handler independent of xdg-utils' quoted-Exec lookup bug.
|
||||
fs::write(
|
||||
data.join("applications/other.desktop"),
|
||||
"[Desktop Entry]\nType=Application\nName=Other handler\nExec=/bin/sh\nMimeType=x-scheme-handler/tauri-test-second;\n",
|
||||
)
|
||||
.unwrap();
|
||||
mimeapps.with_section(Some("Default Applications")).set(
|
||||
"x-scheme-handler/tauri-test-second",
|
||||
format!("other.desktop;{file_name};last.desktop;"),
|
||||
);
|
||||
mimeapps
|
||||
.with_section(Some("Added Associations"))
|
||||
.set(
|
||||
"x-scheme-handler/tauri-test-second",
|
||||
format!("{file_name};other.desktop;"),
|
||||
)
|
||||
.set("x-scheme-handler/unrelated", "unrelated.desktop;");
|
||||
mimeapps.write_to_file(&mimeapps_path).unwrap();
|
||||
deep_link.unregister("tauri-test-second").unwrap();
|
||||
let mimeapps = ini::Ini::load_from_file(&mimeapps_path).unwrap();
|
||||
let defaults = mimeapps.section(Some("Default Applications")).unwrap();
|
||||
assert_eq!(
|
||||
defaults.get("x-scheme-handler/tauri-test-second"),
|
||||
Some("other.desktop;last.desktop;")
|
||||
);
|
||||
assert_eq!(
|
||||
defaults.get("x-scheme-handler/unrelated"),
|
||||
Some("other.desktop")
|
||||
);
|
||||
let added = mimeapps.section(Some("Added Associations")).unwrap();
|
||||
assert_eq!(
|
||||
added.get("x-scheme-handler/tauri-test-second"),
|
||||
Some("other.desktop;")
|
||||
);
|
||||
assert_eq!(
|
||||
added.get("x-scheme-handler/unrelated"),
|
||||
Some("unrelated.desktop;")
|
||||
);
|
||||
assert!(!deep_link.is_registered("tauri-test-second").unwrap());
|
||||
let desktop_path = data.join("applications").join(&file_name);
|
||||
let after = fs::read_to_string(&desktop_path).unwrap();
|
||||
assert_eq!(
|
||||
after.lines().find(|line| line.starts_with("Exec=")),
|
||||
desktop_text.lines().find(|line| line.starts_with("Exec="))
|
||||
);
|
||||
let desktop = ini::Ini::load_from_str(&after).unwrap();
|
||||
assert!(
|
||||
desktop
|
||||
.section(Some("Desktop Entry"))
|
||||
.unwrap()
|
||||
.get("MimeType")
|
||||
.is_none()
|
||||
);
|
||||
let other = ini::Ini::load_from_file(data.join("applications/other.desktop")).unwrap();
|
||||
assert_eq!(
|
||||
other
|
||||
.section(Some("Desktop Entry"))
|
||||
.unwrap()
|
||||
.get("MimeType"),
|
||||
Some("x-scheme-handler/tauri-test-second;")
|
||||
);
|
||||
|
||||
// Removing an association must not prevent a later registration.
|
||||
deep_link.register("tauri-test-first").unwrap();
|
||||
assert!(deep_link.is_registered("tauri-test-first").unwrap());
|
||||
deep_link.unregister("tauri-test-first").unwrap();
|
||||
deep_link.unregister("tauri-test-first").unwrap();
|
||||
assert!(!deep_link.is_registered("tauri-test-first").unwrap());
|
||||
|
||||
// A removed desktop file may still have cached associations to withdraw.
|
||||
deep_link.register("tauri-test-first").unwrap();
|
||||
fs::remove_file(&desktop_path).unwrap();
|
||||
deep_link.unregister("tauri-test-first").unwrap();
|
||||
assert!(!deep_link.is_registered("tauri-test-first").unwrap());
|
||||
fs::remove_file(&mimeapps_path).unwrap();
|
||||
deep_link.unregister("tauri-test-first").unwrap();
|
||||
// Nothing to withdraw: the desktop database is not refreshed, so its tool is not needed.
|
||||
let path = std::env::var_os("PATH").unwrap();
|
||||
unsafe { std::env::set_var("PATH", "") };
|
||||
deep_link.unregister("tauri-test-first").unwrap();
|
||||
deep_link.unregister("tauri-never-registered").unwrap();
|
||||
unsafe { std::env::set_var("PATH", path) };
|
||||
|
||||
// Rewrites on register keep the desktop file's quoting and escapes.
|
||||
deep_link.register("tauri-test-first").unwrap();
|
||||
let custom = r#"X-Custom="quoted" a\sb"#;
|
||||
let text = fs::read_to_string(&desktop_path).unwrap();
|
||||
fs::write(&desktop_path, format!("{text}{custom}\n")).unwrap();
|
||||
deep_link.register("tauri-test-first").unwrap();
|
||||
assert_eq!(
|
||||
fs::read_to_string(&desktop_path).unwrap(),
|
||||
format!("{text}{custom}\n")
|
||||
);
|
||||
deep_link.register("tauri-test-second").unwrap();
|
||||
let after = fs::read_to_string(&desktop_path).unwrap();
|
||||
assert!(after.lines().any(|line| line == custom));
|
||||
assert_eq!(
|
||||
after.lines().find(|line| line.starts_with("Exec=")),
|
||||
desktop_text.lines().find(|line| line.starts_with("Exec="))
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether `command` is on `PATH`, so the test can be skipped where XDG tools are missing.
|
||||
fn has_command(command: &str) -> bool {
|
||||
std::env::var_os("PATH")
|
||||
.is_some_and(|path| std::env::split_paths(&path).any(|dir| dir.join(command).is_file()))
|
||||
}
|
||||
Reference in new issue
Block a user