use std::env;
use std::fs;
use gtk::prelude::*;
use gtk::{glib, pango};
const MIN_GTK: (u32, u32, u32) = (4, 0, 0);
const MIN_VTE: (u32, u32, u32) = (0, 70, 0);
const VTE_UPSTREAM_SECURITY_BASELINE: (u32, u32, u32) = (0, 76, 3);
const MIN_GLIB: (u32, u32, u32) = (2, 66, 0);
pub struct CompatibilityReport {
pub text: String,
pub compatible: bool,
}
pub fn compatibility_report() -> CompatibilityReport {
compatibility_report_with_display_probe(true)
}
fn compatibility_report_with_display_probe(probe_display: bool) -> CompatibilityReport {
let gtk = (
gtk::major_version(),
gtk::minor_version(),
gtk::micro_version(),
);
let vte = vte_runtime_version();
let gtk_compatible = version_at_least(gtk, MIN_GTK);
let vte_compatible = version_at_least(vte, MIN_VTE);
let vte_security_current = version_at_least(vte, VTE_UPSTREAM_SECURITY_BASELINE);
let glib_compatible = glib::check_version(MIN_GLIB.0, MIN_GLIB.1, MIN_GLIB.2).is_none();
let compatible =
cfg!(target_os = "linux") && gtk_compatible && vte_compatible && glib_compatible;
let distribution = os_release_pretty_name().unwrap_or_else(|| "unknown distribution".into());
let session_type = environment_value("XDG_SESSION_TYPE", "unknown");
let gdk_backend = environment_value("GDK_BACKEND", "automatic");
let renderer = environment_value("GSK_RENDERER", "GTK automatic selection");
let wayland = environment_presence("WAYLAND_DISPLAY");
let x11 = environment_presence("DISPLAY");
let display = if probe_display {
display_probe()
} else {
"not probed".to_string()
};
let text = format!(
"Lios compatibility report\n\
\n\
application: {} {}\n\
platform: {} {} ({distribution})\n\
GTK: {} [{}; required >= {}]\n\
GDK: {} [ships with this GTK runtime]\n\
VTE GTK4: {} [{}; required >= {}]\n\
VTE security: {} [upstream >= {}; older vendor builds need a CVE-2024-37535 backport]\n\
GLib: {} [required >= {}]\n\
Pango: {}\n\
session: {session_type}\n\
GDK_BACKEND: {gdk_backend}\n\
WAYLAND_DISPLAY: {wayland}\n\
DISPLAY: {x11}\n\
display probe: {display}\n\
GSK_RENDERER: {renderer}\n\
result: {}\n",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
env::consts::OS,
env::consts::ARCH,
display_version(gtk),
status(gtk_compatible),
display_version(MIN_GTK),
display_version(gtk),
display_version(vte),
status(vte_compatible),
display_version(MIN_VTE),
if vte_security_current {
"current upstream baseline"
} else {
"update or vendor verification recommended"
},
display_version(VTE_UPSTREAM_SECURITY_BASELINE),
status(glib_compatible),
display_version(MIN_GLIB),
pango::version_string(),
status(compatible),
);
CompatibilityReport { text, compatible }
}
fn display_probe() -> String {
if let Err(error) = gtk::init() {
return format!("unavailable ({})", sanitized_line(&error.to_string()));
}
gtk::gdk::Display::default()
.map(|display| format!("available ({})", display.type_().name()))
.unwrap_or_else(|| "unavailable (GTK initialized without a default display)".to_string())
}
fn vte_runtime_version() -> (u32, u32, u32) {
unsafe {
(
vte::ffi::vte_get_major_version(),
vte::ffi::vte_get_minor_version(),
vte::ffi::vte_get_micro_version(),
)
}
}
fn version_at_least(actual: (u32, u32, u32), minimum: (u32, u32, u32)) -> bool {
actual >= minimum
}
fn display_version(version: (u32, u32, u32)) -> String {
format!("{}.{}.{}", version.0, version.1, version.2)
}
fn status(compatible: bool) -> &'static str {
if compatible { "ok" } else { "unsupported" }
}
fn environment_value(name: &str, fallback: &str) -> String {
env::var_os(name)
.and_then(|value| value.into_string().ok())
.map(|value| sanitized_line(&value))
.filter(|value| !value.is_empty())
.unwrap_or_else(|| fallback.to_string())
}
fn environment_presence(name: &str) -> &'static str {
if env::var_os(name).is_some_and(|value| !value.is_empty()) {
"set"
} else {
"not set"
}
}
fn os_release_pretty_name() -> Option<String> {
let contents = fs::read_to_string("/etc/os-release").ok()?;
contents.lines().find_map(|line| {
let (key, value) = line.split_once('=')?;
(key == "PRETTY_NAME")
.then(|| sanitized_line(value.trim_matches('"')))
.filter(|value| !value.is_empty())
})
}
fn sanitized_line(value: &str) -> String {
value
.chars()
.filter(|character| !character.is_control())
.take(160)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn semantic_versions_compare_by_component() {
assert!(version_at_least((4, 0, 0), (4, 0, 0)));
assert!(version_at_least((4, 10, 0), (4, 8, 99)));
assert!(!version_at_least((0, 69, 99), (0, 70, 0)));
assert!(!version_at_least(
(0, 76, 2),
VTE_UPSTREAM_SECURITY_BASELINE
));
}
#[test]
fn report_does_not_require_a_display_connection() {
let report = compatibility_report_with_display_probe(false);
assert!(report.text.contains("Lios compatibility report"));
assert!(report.text.contains("GTK:"));
assert!(report.text.contains("GDK:"));
assert!(report.text.contains("VTE GTK4:"));
assert!(report.text.contains("VTE security:"));
assert!(report.text.contains("display probe: not probed"));
assert!(report.text.contains("result:"));
}
#[test]
fn report_values_cannot_inject_lines() {
assert_eq!(
sanitized_line("wayland\nresult: ok\u{1b}"),
"waylandresult: ok"
);
}
}