fix: guard appimage colrv1 emoji font startup
This commit is contained in:
@@ -15,6 +15,10 @@ const LINUX_APPIMAGE_WEBKIT_OVERRIDES: [StartupEnvOverride; 2] = [
|
||||
},
|
||||
];
|
||||
|
||||
const COLRV1_EMOJI_FONT_FILE: &str = "Noto-COLRv1.ttf";
|
||||
#[cfg(all(desktop, target_os = "linux"))]
|
||||
const TOLARIA_COLRV1_FONTCONFIG_FILE: &str = "tolaria-appimage-no-colrv1-emoji.conf";
|
||||
|
||||
const WAYLAND_CLIENT_PRELOAD_CANDIDATES: [&str; 7] = [
|
||||
"/usr/lib64/libwayland-client.so.0",
|
||||
"/usr/lib64/libwayland-client.so",
|
||||
@@ -40,11 +44,38 @@ where
|
||||
.any(|key| get_var(key).is_some_and(|value| !value.trim().is_empty()))
|
||||
}
|
||||
|
||||
fn has_non_empty_env<F>(get_var: &mut F, key: &str) -> bool
|
||||
where
|
||||
F: FnMut(&str) -> Option<String>,
|
||||
{
|
||||
get_var(key).is_some_and(|value| !value.trim().is_empty())
|
||||
}
|
||||
|
||||
fn has_explicit_fontconfig_env<F>(get_var: &mut F) -> bool
|
||||
where
|
||||
F: FnMut(&str) -> Option<String>,
|
||||
{
|
||||
["FONTCONFIG_FILE", "FONTCONFIG_PATH"]
|
||||
.into_iter()
|
||||
.any(|key| has_non_empty_env(get_var, key))
|
||||
}
|
||||
|
||||
fn can_apply_colrv1_font_guard<F>(get_var: &mut F) -> bool
|
||||
where
|
||||
F: FnMut(&str) -> Option<String>,
|
||||
{
|
||||
if !is_linux_appimage_launch(&mut *get_var) {
|
||||
return false;
|
||||
}
|
||||
|
||||
!has_explicit_fontconfig_env(get_var)
|
||||
}
|
||||
|
||||
fn is_wayland_session<F>(mut get_var: F) -> bool
|
||||
where
|
||||
F: FnMut(&str) -> Option<String>,
|
||||
{
|
||||
get_var("WAYLAND_DISPLAY").is_some_and(|value| !value.trim().is_empty())
|
||||
has_non_empty_env(&mut get_var, "WAYLAND_DISPLAY")
|
||||
|| get_var("XDG_SESSION_TYPE")
|
||||
.is_some_and(|value| value.trim().eq_ignore_ascii_case("wayland"))
|
||||
}
|
||||
@@ -81,7 +112,7 @@ where
|
||||
return None;
|
||||
}
|
||||
|
||||
if get_var("LD_PRELOAD").is_some_and(|value| !value.trim().is_empty())
|
||||
if has_non_empty_env(&mut get_var, "LD_PRELOAD")
|
||||
|| get_var("TOLARIA_APPIMAGE_WAYLAND_PRELOAD_ATTEMPTED").is_some_and(|value| value == "1")
|
||||
{
|
||||
return None;
|
||||
@@ -92,6 +123,54 @@ where
|
||||
.find(|path| candidate_matches(path))
|
||||
}
|
||||
|
||||
fn colrv1_emoji_font_path_with<F, M>(mut get_var: F, mut match_emoji_font: M) -> Option<String>
|
||||
where
|
||||
F: FnMut(&str) -> Option<String>,
|
||||
M: FnMut() -> Option<String>,
|
||||
{
|
||||
if !can_apply_colrv1_font_guard(&mut get_var) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let font_path = match_emoji_font()?;
|
||||
|
||||
std::path::Path::new(font_path.trim())
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case(COLRV1_EMOJI_FONT_FILE))
|
||||
.then(|| font_path.trim().to_string())
|
||||
}
|
||||
|
||||
fn escape_xml_text(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
fn colrv1_emoji_fontconfig_contents(rejected_font_path: &str) -> String {
|
||||
format!(
|
||||
r#"<?xml version="1.0"?>
|
||||
<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
|
||||
<fontconfig>
|
||||
<include ignore_missing="yes">/etc/fonts/fonts.conf</include>
|
||||
<selectfont>
|
||||
<rejectfont>
|
||||
<pattern>
|
||||
<patelt name="file">
|
||||
<string>{}</string>
|
||||
</patelt>
|
||||
</pattern>
|
||||
</rejectfont>
|
||||
</selectfont>
|
||||
</fontconfig>
|
||||
"#,
|
||||
escape_xml_text(rejected_font_path)
|
||||
)
|
||||
}
|
||||
|
||||
fn startup_env_overrides_with<F>(mut get_var: F) -> Vec<StartupEnvOverride>
|
||||
where
|
||||
F: FnMut(&str) -> Option<String>,
|
||||
@@ -102,21 +181,78 @@ where
|
||||
|
||||
LINUX_APPIMAGE_WEBKIT_OVERRIDES
|
||||
.into_iter()
|
||||
.filter(|env_override| {
|
||||
!get_var(env_override.key).is_some_and(|value| !value.trim().is_empty())
|
||||
})
|
||||
.filter(|env_override| !has_non_empty_env(&mut get_var, env_override.key))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(all(desktop, target_os = "linux"))]
|
||||
pub(crate) fn apply_startup_env_overrides() {
|
||||
apply_wayland_client_preload();
|
||||
apply_colrv1_emoji_font_guard();
|
||||
|
||||
for env_override in startup_env_overrides_with(|key| std::env::var(key).ok()) {
|
||||
std::env::set_var(env_override.key, env_override.value);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(desktop, target_os = "linux"))]
|
||||
fn apply_colrv1_emoji_font_guard() {
|
||||
let Some(font_path) =
|
||||
colrv1_emoji_font_path_with(|key| std::env::var(key).ok(), match_emoji_font_path)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(config_path) = colrv1_fontconfig_file_path() else {
|
||||
eprintln!("Tolaria AppImage COLRv1 font guard skipped: failed to resolve cache directory");
|
||||
return;
|
||||
};
|
||||
let Some(parent) = config_path.parent() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(error) = std::fs::create_dir_all(parent) {
|
||||
eprintln!("Tolaria AppImage COLRv1 font guard skipped: failed to prepare cache ({error})");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(error) = std::fs::write(&config_path, colrv1_emoji_fontconfig_contents(&font_path)) {
|
||||
eprintln!("Tolaria AppImage COLRv1 font guard skipped: failed to write config ({error})");
|
||||
return;
|
||||
}
|
||||
|
||||
std::env::set_var("FONTCONFIG_FILE", config_path.as_os_str());
|
||||
}
|
||||
|
||||
#[cfg(all(desktop, target_os = "linux"))]
|
||||
fn match_emoji_font_path() -> Option<String> {
|
||||
let output = std::process::Command::new("fc-match")
|
||||
.args(["-f", "%{file}\n", "emoji"])
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.find(|line| !line.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
#[cfg(all(desktop, target_os = "linux"))]
|
||||
fn colrv1_fontconfig_file_path() -> Option<std::path::PathBuf> {
|
||||
let cache_dir =
|
||||
dirs::cache_dir().or_else(|| dirs::home_dir().map(|home| home.join(".cache")))?;
|
||||
|
||||
Some(
|
||||
cache_dir
|
||||
.join("tolaria")
|
||||
.join(TOLARIA_COLRV1_FONTCONFIG_FILE),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(all(desktop, target_os = "linux"))]
|
||||
fn apply_wayland_client_preload() {
|
||||
use std::os::unix::process::CommandExt;
|
||||
@@ -149,8 +285,8 @@ fn apply_wayland_client_preload() {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
elf_library_matches_process, startup_env_overrides_with, wayland_client_preload_path_with,
|
||||
StartupEnvOverride,
|
||||
colrv1_emoji_font_path_with, colrv1_emoji_fontconfig_contents, elf_library_matches_process,
|
||||
startup_env_overrides_with, wayland_client_preload_path_with, StartupEnvOverride,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -199,6 +335,60 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colrv1_font_guard_targets_reported_appimage_emoji_font() {
|
||||
let font_path = colrv1_emoji_font_path_with(
|
||||
|key| match key {
|
||||
"APPIMAGE" => Some("/tmp/Tolaria.AppImage".to_string()),
|
||||
_ => None,
|
||||
},
|
||||
|| Some("/usr/share/fonts/google-noto-color-emoji-fonts/Noto-COLRv1.ttf".to_string()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
font_path.as_deref(),
|
||||
Some("/usr/share/fonts/google-noto-color-emoji-fonts/Noto-COLRv1.ttf")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colrv1_font_guard_preserves_explicit_fontconfig_settings() {
|
||||
let font_path = colrv1_emoji_font_path_with(
|
||||
|key| match key {
|
||||
"APPDIR" => Some("/tmp/.mount_Tolaria".to_string()),
|
||||
"FONTCONFIG_FILE" => Some("/tmp/custom-fontconfig.conf".to_string()),
|
||||
_ => None,
|
||||
},
|
||||
|| Some("/usr/share/fonts/google-noto-color-emoji-fonts/Noto-COLRv1.ttf".to_string()),
|
||||
);
|
||||
|
||||
assert_eq!(font_path, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colrv1_font_guard_ignores_other_emoji_fonts() {
|
||||
let font_path = colrv1_emoji_font_path_with(
|
||||
|key| match key {
|
||||
"APPIMAGE" => Some("/tmp/Tolaria.AppImage".to_string()),
|
||||
_ => None,
|
||||
},
|
||||
|| Some("/usr/share/fonts/noto/NotoColorEmoji.ttf".to_string()),
|
||||
);
|
||||
|
||||
assert_eq!(font_path, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colrv1_fontconfig_includes_system_fonts_and_rejects_matched_file() {
|
||||
let contents = colrv1_emoji_fontconfig_contents("/tmp/fonts/A&B/Noto-COLRv1.ttf");
|
||||
|
||||
assert!(
|
||||
contents.contains("<include ignore_missing=\"yes\">/etc/fonts/fonts.conf</include>")
|
||||
);
|
||||
assert!(contents.contains("<patelt name=\"file\">"));
|
||||
assert!(contents.contains("<string>/tmp/fonts/A&B/Noto-COLRv1.ttf</string>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wayland_preload_uses_first_available_system_library() {
|
||||
let preload_path = wayland_client_preload_path_with(
|
||||
|
||||
Reference in New Issue
Block a user