Critical: fix unsquashfs arg order, quote Exec paths with spaces, fix compare_versions antisymmetry, chunk-based signature detection, bounded ELF header reads. High: handle NULL CVE severity, prevent pipe deadlock in inspector, fix glob_match edge case, fix backup archive path collisions, async crash detection with stderr capture. Medium: gate scan on auto-scan setting, fix window size persistence, fix announce() for Stack containers, claim lightbox gesture, use serde_json for CLI output, remove dead CSS @media blocks, add detail-tab persistence, remove invalid metainfo categories, byte-level fuse signature search. Low: tighten Wayland env var detection, ELF magic validation, timeout for update info extraction, quoted arg parsing, stop watcher timer on window destroy, GSettings choices/range constraints, remove unused CSS classes, define status-ok/status-attention CSS.
497 lines
16 KiB
Rust
497 lines
16 KiB
Rust
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum WaylandStatus {
|
|
/// Native Wayland support detected (GTK4, Qt6+Wayland, Electron 38+)
|
|
Native,
|
|
/// Will run under XWayland compatibility layer
|
|
XWayland,
|
|
/// Toolkit supports Wayland but plugins may be missing or env vars needed
|
|
Possible,
|
|
/// X11-only toolkit with no Wayland path (GTK2, old Electron, Java)
|
|
X11Only,
|
|
/// Could not determine (uncommon toolkit, static binary, etc.)
|
|
Unknown,
|
|
}
|
|
|
|
impl WaylandStatus {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Self::Native => "native",
|
|
Self::XWayland => "xwayland",
|
|
Self::Possible => "possible",
|
|
Self::X11Only => "x11_only",
|
|
Self::Unknown => "unknown",
|
|
}
|
|
}
|
|
|
|
pub fn from_str(s: &str) -> Self {
|
|
match s {
|
|
"native" => Self::Native,
|
|
"xwayland" => Self::XWayland,
|
|
"possible" => Self::Possible,
|
|
"x11_only" => Self::X11Only,
|
|
_ => Self::Unknown,
|
|
}
|
|
}
|
|
|
|
pub fn label(&self) -> &'static str {
|
|
match self {
|
|
Self::Native => "Native Wayland",
|
|
Self::XWayland => "XWayland",
|
|
Self::Possible => "Wayland possible",
|
|
Self::X11Only => "X11 only",
|
|
Self::Unknown => "Unknown",
|
|
}
|
|
}
|
|
|
|
pub fn badge_class(&self) -> &'static str {
|
|
match self {
|
|
Self::Native => "success",
|
|
Self::XWayland | Self::Possible => "warning",
|
|
Self::X11Only => "error",
|
|
Self::Unknown => "neutral",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum DetectedToolkit {
|
|
Gtk4,
|
|
Gtk3Wayland,
|
|
Gtk3X11Only,
|
|
Gtk2,
|
|
Qt6Wayland,
|
|
Qt6X11Only,
|
|
Qt5Wayland,
|
|
Qt5X11Only,
|
|
ElectronNative(u32), // version >= 38
|
|
ElectronFlagged(u32), // version 28-37 with ozone flag
|
|
ElectronLegacy(u32), // version < 28
|
|
JavaSwing,
|
|
Flutter,
|
|
Unknown,
|
|
}
|
|
|
|
impl DetectedToolkit {
|
|
pub fn label(&self) -> String {
|
|
match self {
|
|
Self::Gtk4 => "GTK4".to_string(),
|
|
Self::Gtk3Wayland => "GTK3 (Wayland)".to_string(),
|
|
Self::Gtk3X11Only => "GTK3 (X11)".to_string(),
|
|
Self::Gtk2 => "GTK2".to_string(),
|
|
Self::Qt6Wayland => "Qt6 (Wayland)".to_string(),
|
|
Self::Qt6X11Only => "Qt6 (X11)".to_string(),
|
|
Self::Qt5Wayland => "Qt5 (Wayland)".to_string(),
|
|
Self::Qt5X11Only => "Qt5 (X11)".to_string(),
|
|
Self::ElectronNative(v) => format!("Electron {} (native Wayland)", v),
|
|
Self::ElectronFlagged(v) => format!("Electron {} (Wayland with flags)", v),
|
|
Self::ElectronLegacy(v) => format!("Electron {} (X11)", v),
|
|
Self::JavaSwing => "Java/Swing".to_string(),
|
|
Self::Flutter => "Flutter".to_string(),
|
|
Self::Unknown => "Unknown".to_string(),
|
|
}
|
|
}
|
|
|
|
pub fn wayland_status(&self) -> WaylandStatus {
|
|
match self {
|
|
Self::Gtk4 | Self::Gtk3Wayland | Self::Qt6Wayland | Self::Qt5Wayland
|
|
| Self::ElectronNative(_) | Self::Flutter => WaylandStatus::Native,
|
|
Self::ElectronFlagged(_) => WaylandStatus::Possible,
|
|
Self::Gtk3X11Only | Self::Qt6X11Only | Self::Qt5X11Only => WaylandStatus::XWayland,
|
|
Self::Gtk2 | Self::ElectronLegacy(_) | Self::JavaSwing => WaylandStatus::X11Only,
|
|
Self::Unknown => WaylandStatus::Unknown,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct WaylandAnalysis {
|
|
pub status: WaylandStatus,
|
|
pub toolkit: DetectedToolkit,
|
|
pub libraries_found: Vec<String>,
|
|
}
|
|
|
|
/// Analyze an AppImage's Wayland compatibility by inspecting its bundled libraries.
|
|
/// Uses unsquashfs to list files inside the squashfs.
|
|
pub fn analyze_appimage(appimage_path: &Path) -> WaylandAnalysis {
|
|
let libs = list_bundled_libraries(appimage_path);
|
|
|
|
let toolkit = detect_toolkit(&libs);
|
|
let status = toolkit.wayland_status();
|
|
|
|
WaylandAnalysis {
|
|
status,
|
|
toolkit,
|
|
libraries_found: libs,
|
|
}
|
|
}
|
|
|
|
/// List shared libraries bundled inside the AppImage squashfs.
|
|
fn list_bundled_libraries(appimage_path: &Path) -> Vec<String> {
|
|
// Get the squashfs offset using binary scan (never execute the AppImage -
|
|
// some apps like Affinity have custom AppRun scripts that ignore flags)
|
|
let offset = match crate::core::inspector::find_squashfs_offset_for(appimage_path) {
|
|
Some(o) => o.to_string(),
|
|
None => return Vec::new(),
|
|
};
|
|
|
|
// Use unsquashfs to list files (just filenames, no extraction)
|
|
let output = Command::new("unsquashfs")
|
|
.args(["-o", &offset, "-l", "-no-progress"])
|
|
.arg(appimage_path)
|
|
.output();
|
|
|
|
match output {
|
|
Ok(out) if out.status.success() => {
|
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
stdout
|
|
.lines()
|
|
.filter(|line| line.contains(".so"))
|
|
.map(|line| {
|
|
// unsquashfs -l output format: "squashfs-root/usr/lib/libfoo.so.1"
|
|
// Extract just the filename
|
|
line.rsplit('/').next().unwrap_or(line).to_string()
|
|
})
|
|
.collect()
|
|
}
|
|
_ => Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Detect the UI toolkit from the bundled library list.
|
|
fn detect_toolkit(libs: &[String]) -> DetectedToolkit {
|
|
let has = |pattern: &str| -> bool {
|
|
libs.iter().any(|l| l.contains(pattern))
|
|
};
|
|
|
|
// Check for GTK4 (always Wayland-native)
|
|
if has("libgtk-4") || has("libGdk-4") {
|
|
return DetectedToolkit::Gtk4;
|
|
}
|
|
|
|
// Check for Flutter (GTK backend, Wayland-native)
|
|
if has("libflutter_linux_gtk") {
|
|
return DetectedToolkit::Flutter;
|
|
}
|
|
|
|
// Check for Java/Swing (X11 only)
|
|
if has("libjvm.so") || has("libjava.so") || has("libawt.so") {
|
|
return DetectedToolkit::JavaSwing;
|
|
}
|
|
|
|
// Check for Electron (version-dependent)
|
|
if has("libElectron") || has("electron") || has("libnode.so") || has("libchromium") {
|
|
let version = detect_electron_version(libs);
|
|
if let Some(v) = version {
|
|
if v >= 38 {
|
|
return DetectedToolkit::ElectronNative(v);
|
|
} else if v >= 28 {
|
|
return DetectedToolkit::ElectronFlagged(v);
|
|
} else {
|
|
return DetectedToolkit::ElectronLegacy(v);
|
|
}
|
|
}
|
|
// Can't determine version - assume modern enough for XWayland at minimum
|
|
return DetectedToolkit::ElectronFlagged(0);
|
|
}
|
|
|
|
// Check for Qt6
|
|
if has("libQt6Core") || has("libQt6Gui") {
|
|
if has("libQt6WaylandClient") || has("libqwayland") {
|
|
return DetectedToolkit::Qt6Wayland;
|
|
}
|
|
return DetectedToolkit::Qt6X11Only;
|
|
}
|
|
|
|
// Check for Qt5
|
|
if has("libQt5Core") || has("libQt5Gui") {
|
|
if has("libQt5WaylandClient") || has("libqwayland") {
|
|
return DetectedToolkit::Qt5Wayland;
|
|
}
|
|
return DetectedToolkit::Qt5X11Only;
|
|
}
|
|
|
|
// Check for GTK3
|
|
if has("libgtk-3") || has("libGdk-3") {
|
|
if has("libwayland-client") {
|
|
return DetectedToolkit::Gtk3Wayland;
|
|
}
|
|
return DetectedToolkit::Gtk3X11Only;
|
|
}
|
|
|
|
// Check for GTK2 (X11 only, forever)
|
|
if has("libgtk-x11-2") || has("libgdk-x11-2") {
|
|
return DetectedToolkit::Gtk2;
|
|
}
|
|
|
|
DetectedToolkit::Unknown
|
|
}
|
|
|
|
/// Try to detect Electron version from bundled files.
|
|
fn detect_electron_version(libs: &[String]) -> Option<u32> {
|
|
for lib in libs {
|
|
// Look for version patterns in Electron-related files
|
|
if lib.contains("electron") {
|
|
// Try to extract version number from filenames like "electron-v28.0.0"
|
|
for part in lib.split(&['-', '_', 'v'][..]) {
|
|
if let Some(major) = part.split('.').next() {
|
|
if let Ok(v) = major.parse::<u32>() {
|
|
if v > 0 && v < 200 {
|
|
return Some(v);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Detect the current desktop session type.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum SessionType {
|
|
Wayland,
|
|
X11,
|
|
Unknown,
|
|
}
|
|
|
|
impl SessionType {
|
|
pub fn label(&self) -> &'static str {
|
|
match self {
|
|
Self::Wayland => "Wayland",
|
|
Self::X11 => "X11",
|
|
Self::Unknown => "Unknown",
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn detect_session_type() -> SessionType {
|
|
// Check XDG_SESSION_TYPE first (most reliable)
|
|
if let Ok(session) = std::env::var("XDG_SESSION_TYPE") {
|
|
return match session.as_str() {
|
|
"wayland" => SessionType::Wayland,
|
|
"x11" => SessionType::X11,
|
|
_ => SessionType::Unknown,
|
|
};
|
|
}
|
|
|
|
// Check WAYLAND_DISPLAY
|
|
if std::env::var("WAYLAND_DISPLAY").is_ok() {
|
|
return SessionType::Wayland;
|
|
}
|
|
|
|
// Check DISPLAY (X11 fallback)
|
|
if std::env::var("DISPLAY").is_ok() {
|
|
return SessionType::X11;
|
|
}
|
|
|
|
SessionType::Unknown
|
|
}
|
|
|
|
/// Get desktop environment info string.
|
|
pub fn detect_desktop_environment() -> String {
|
|
let de = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
|
|
let session = std::env::var("DESKTOP_SESSION").unwrap_or_default();
|
|
|
|
if !de.is_empty() {
|
|
de
|
|
} else if !session.is_empty() {
|
|
session
|
|
} else {
|
|
"Unknown".to_string()
|
|
}
|
|
}
|
|
|
|
/// Result of analyzing a running process for Wayland usage.
|
|
#[derive(Debug, Clone)]
|
|
pub struct RuntimeAnalysis {
|
|
pub pid: u32,
|
|
pub has_wayland_socket: bool,
|
|
pub has_x11_connection: bool,
|
|
pub env_vars: Vec<(String, String)>,
|
|
}
|
|
|
|
impl RuntimeAnalysis {
|
|
/// Human-readable status label.
|
|
pub fn status_label(&self) -> &'static str {
|
|
match (self.has_wayland_socket, self.has_x11_connection) {
|
|
(true, false) => "Native Wayland",
|
|
(true, true) => "Wayland + X11 fallback",
|
|
(false, true) => "X11 / XWayland",
|
|
(false, false) => "Unknown",
|
|
}
|
|
}
|
|
|
|
/// Machine-readable status string for database storage.
|
|
pub fn as_status_str(&self) -> &'static str {
|
|
match (self.has_wayland_socket, self.has_x11_connection) {
|
|
(true, false) => "native",
|
|
(true, true) => "native",
|
|
(false, true) => "xwayland",
|
|
(false, false) => "unknown",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Analyze a running process to determine its actual Wayland/X11 usage.
|
|
/// Inspects /proc/<pid>/fd for Wayland and X11 sockets, and reads
|
|
/// relevant environment variables from /proc/<pid>/environ.
|
|
pub fn analyze_running_process(pid: u32) -> Result<RuntimeAnalysis, String> {
|
|
let proc_path = format!("/proc/{}", pid);
|
|
if !std::path::Path::new(&proc_path).exists() {
|
|
return Err(format!("Process {} not found", pid));
|
|
}
|
|
|
|
// Check file descriptors for Wayland and X11 sockets
|
|
let fd_dir = format!("{}/fd", proc_path);
|
|
let mut has_wayland_socket = false;
|
|
let mut has_x11_connection = false;
|
|
|
|
if let Ok(entries) = std::fs::read_dir(&fd_dir) {
|
|
for entry in entries.flatten() {
|
|
if let Ok(target) = std::fs::read_link(entry.path()) {
|
|
let target_str = target.to_string_lossy();
|
|
if target_str.contains("wayland") {
|
|
has_wayland_socket = true;
|
|
}
|
|
if target_str.contains("/tmp/.X11-unix/") || target_str.contains("@/tmp/.X11") {
|
|
has_x11_connection = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Read relevant environment variables
|
|
let environ_path = format!("{}/environ", proc_path);
|
|
let mut env_vars = Vec::new();
|
|
|
|
let relevant_vars = [
|
|
"WAYLAND_DISPLAY", "DISPLAY", "GDK_BACKEND", "QT_QPA_PLATFORM",
|
|
"XDG_SESSION_TYPE", "SDL_VIDEODRIVER", "CLUTTER_BACKEND",
|
|
];
|
|
|
|
if let Ok(data) = std::fs::read(&environ_path) {
|
|
for entry in data.split(|&b| b == 0) {
|
|
if let Ok(s) = std::str::from_utf8(entry) {
|
|
if let Some((key, value)) = s.split_once('=') {
|
|
if relevant_vars.contains(&key) {
|
|
env_vars.push((key.to_string(), value.to_string()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also check env vars for hints if fd inspection was inconclusive
|
|
if !has_wayland_socket {
|
|
has_wayland_socket = env_vars.iter().any(|(k, v)| {
|
|
(k == "GDK_BACKEND" && v.contains("wayland"))
|
|
|| (k == "QT_QPA_PLATFORM" && v.contains("wayland"))
|
|
});
|
|
}
|
|
|
|
Ok(RuntimeAnalysis {
|
|
pid,
|
|
has_wayland_socket,
|
|
has_x11_connection,
|
|
env_vars,
|
|
})
|
|
}
|
|
|
|
/// Check if XWayland is available on the system.
|
|
pub fn has_xwayland() -> bool {
|
|
// Check if Xwayland process is running
|
|
Command::new("pgrep")
|
|
.arg("Xwayland")
|
|
.output()
|
|
.map(|o| o.status.success())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_wayland_status_roundtrip() {
|
|
let statuses = [
|
|
WaylandStatus::Native,
|
|
WaylandStatus::XWayland,
|
|
WaylandStatus::Possible,
|
|
WaylandStatus::X11Only,
|
|
WaylandStatus::Unknown,
|
|
];
|
|
for status in &statuses {
|
|
assert_eq!(&WaylandStatus::from_str(status.as_str()), status);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_detect_toolkit_gtk4() {
|
|
let libs = vec!["libgtk-4.so.1".to_string(), "libglib-2.0.so.0".to_string()];
|
|
let toolkit = detect_toolkit(&libs);
|
|
assert!(matches!(toolkit, DetectedToolkit::Gtk4));
|
|
assert_eq!(toolkit.wayland_status(), WaylandStatus::Native);
|
|
}
|
|
|
|
#[test]
|
|
fn test_detect_toolkit_qt5_wayland() {
|
|
let libs = vec![
|
|
"libQt5Core.so.5".to_string(),
|
|
"libQt5Gui.so.5".to_string(),
|
|
"libQt5WaylandClient.so.5".to_string(),
|
|
];
|
|
let toolkit = detect_toolkit(&libs);
|
|
assert!(matches!(toolkit, DetectedToolkit::Qt5Wayland));
|
|
assert_eq!(toolkit.wayland_status(), WaylandStatus::Native);
|
|
}
|
|
|
|
#[test]
|
|
fn test_detect_toolkit_qt5_x11() {
|
|
let libs = vec![
|
|
"libQt5Core.so.5".to_string(),
|
|
"libQt5Gui.so.5".to_string(),
|
|
];
|
|
let toolkit = detect_toolkit(&libs);
|
|
assert!(matches!(toolkit, DetectedToolkit::Qt5X11Only));
|
|
assert_eq!(toolkit.wayland_status(), WaylandStatus::XWayland);
|
|
}
|
|
|
|
#[test]
|
|
fn test_detect_toolkit_gtk2() {
|
|
let libs = vec!["libgtk-x11-2.0.so.0".to_string()];
|
|
let toolkit = detect_toolkit(&libs);
|
|
assert!(matches!(toolkit, DetectedToolkit::Gtk2));
|
|
assert_eq!(toolkit.wayland_status(), WaylandStatus::X11Only);
|
|
}
|
|
|
|
#[test]
|
|
fn test_detect_toolkit_gtk3_with_wayland() {
|
|
let libs = vec![
|
|
"libgtk-3.so.0".to_string(),
|
|
"libwayland-client.so.0".to_string(),
|
|
];
|
|
let toolkit = detect_toolkit(&libs);
|
|
assert!(matches!(toolkit, DetectedToolkit::Gtk3Wayland));
|
|
assert_eq!(toolkit.wayland_status(), WaylandStatus::Native);
|
|
}
|
|
|
|
#[test]
|
|
fn test_detect_toolkit_unknown() {
|
|
let libs = vec!["libfoo.so.1".to_string()];
|
|
let toolkit = detect_toolkit(&libs);
|
|
assert!(matches!(toolkit, DetectedToolkit::Unknown));
|
|
assert_eq!(toolkit.wayland_status(), WaylandStatus::Unknown);
|
|
}
|
|
|
|
#[test]
|
|
fn test_badge_classes() {
|
|
assert_eq!(WaylandStatus::Native.badge_class(), "success");
|
|
assert_eq!(WaylandStatus::XWayland.badge_class(), "warning");
|
|
assert_eq!(WaylandStatus::X11Only.badge_class(), "error");
|
|
assert_eq!(WaylandStatus::Unknown.badge_class(), "neutral");
|
|
}
|
|
}
|