glycin 1.0.0-alpha

Sandboxed image decoding
Documentation
use std::collections::HashMap;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;

use futures_util::StreamExt;
use gio::glib;

use crate::dbus::Error;

pub type MimeType = String;

const CONFIG_FILE_EXT: &str = "conf";
pub const COMPAT_VERSION: u8 = 1;

static CONFIG: OnceLock<Config> = OnceLock::new();

#[derive(Debug, Clone, Default)]
pub struct Config {
    pub image_decoders: HashMap<MimeType, ImageDecoderConfig>,
}

#[derive(Debug, Clone)]
pub struct ImageDecoderConfig {
    pub exec: PathBuf,
    pub expose_base_dir: bool,
}

impl Config {
    pub async fn cached() -> &'static Self {
        if let Some(config) = CONFIG.get() {
            config
        } else {
            let config = Self::load().await;
            CONFIG.get_or_init(|| config)
        }
    }

    pub fn get(&self, mime_type: &MimeType) -> Result<&ImageDecoderConfig, Error> {
        self.image_decoders
            .get(mime_type.as_str())
            .ok_or_else(|| Error::UnknownImageFormat(mime_type.to_string()))
    }

    async fn load() -> Self {
        let mut config = Config::default();

        let mut data_dirs = glib::system_data_dirs();
        data_dirs.push(glib::user_data_dir());

        for mut data_dir in data_dirs {
            data_dir.push("glycin-loaders");
            data_dir.push(format!("{COMPAT_VERSION}+"));
            data_dir.push("conf.d");

            if let Ok(mut config_files) = async_fs::read_dir(data_dir).await {
                while let Some(result) = config_files.next().await {
                    if let Ok(entry) = result {
                        if entry.path().extension() == Some(OsStr::new(CONFIG_FILE_EXT)) {
                            if let Err(err) = Self::load_file(&entry.path(), &mut config).await {
                                eprintln!("Failed to load config file: {err}");
                            }
                        }
                    }
                }
            }
        }

        config
    }

    async fn load_file(path: &Path, config: &mut Config) -> Result<(), Box<dyn std::error::Error>> {
        let data = async_fs::read(path).await?;
        let bytes = glib::Bytes::from_owned(data);

        let keyfile = glib::KeyFile::new();
        keyfile.load_from_bytes(&bytes, glib::KeyFileFlags::NONE)?;

        for group in keyfile.groups() {
            let mut elements = group.split(':');
            let kind = elements.next();
            let mime_type = elements.next();

            if kind == Some("loader") {
                if let Some(mime_type) = mime_type {
                    let group = group.trim();
                    if let Ok(exec) = keyfile.string(group, "Exec") {
                        let expose_base_dir =
                            keyfile.boolean(group, "ExposeBaseDir").unwrap_or_default();

                        let cfg = ImageDecoderConfig {
                            exec: exec.into(),
                            expose_base_dir,
                        };

                        config.image_decoders.insert(mime_type.to_string(), cfg);
                    }
                }
            }
        }

        Ok(())
    }
}