bevy_webview_wry 0.5.0

Allows you to create a webview based on wry
Documentation
use bevy::asset::io::Reader;
use bevy::asset::{Asset, AssetLoader, Handle, LoadContext};
use bevy::prelude::*;
use bevy_webview_core::prelude::Csp;
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Display, Formatter};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use wry::http::Response;
use wry::http::header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE};

#[derive(Debug, Component, Clone)]
pub struct WryResponseHandle(pub Handle<WryResponseBody>);

#[derive(Debug, Asset, TypePath)]
pub struct WryResponseBody(pub Vec<u8>);

#[derive(
    Serialize, Deserialize, Default, Eq, PartialEq, Hash, Component, Reflect, Clone, Debug,
)]
#[reflect(Component, Serialize, Deserialize)]
pub struct WryRequestArgs {
    pub csp: Option<Csp>,
    pub path: PathBuf,
}

#[derive(Default)]
pub struct WryResponseLoader;

impl AssetLoader for WryResponseLoader {
    type Asset = WryResponseBody;
    type Settings = ();
    type Error = std::io::Error;
    async fn load(
        &self,
        reader: &mut dyn Reader,
        _: &Self::Settings,
        _: &mut LoadContext<'_>,
    ) -> Result<Self::Asset, Self::Error> {
        let mut buf = Vec::new();
        reader.read_to_end(&mut buf).await?;
        Ok(WryResponseBody(buf))
    }

    fn extensions(&self) -> &[&str] {
        &EXTENSIONS
    }
}

pub(crate) fn convert_to_response(content: Vec<u8>, args: &WryRequestArgs) -> Response<Vec<u8>> {
    try_convert_to_response(content, args).unwrap_or_else(|e| {
        Response::builder()
            .header(CONTENT_TYPE, "text/plain")
            .status(500)
            .body(e.to_string().as_bytes().to_vec())
            .unwrap()
    })
}

fn try_convert_to_response(
    content: Vec<u8>,
    args: &WryRequestArgs,
) -> Result<Response<Vec<u8>>, std::io::Error> {
    let Some(mimetype) = get_mime_type(&args.path) else {
        return Err(std::io::Error::other(Box::new(NotImplError(
            args.path.clone(),
        ))));
    };
    let mut response_builder = Response::builder();
    if let Some(csp) = args.csp.as_ref() {
        response_builder = response_builder.header(CONTENT_SECURITY_POLICY, csp.0.as_str());
    }
    response_builder
        .header(CONTENT_TYPE, mimetype)
        .body(content)
        .map_err(std::io::Error::other)
}

struct NotImplError(PathBuf);
impl Display for NotImplError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_fmt(format_args!("not implemented content type {:?}", self.0))
    }
}

impl Debug for NotImplError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_fmt(format_args!("{self}"))
    }
}

impl std::error::Error for NotImplError {}

const EXTENSION_MAP: &[(&[&str], &str)] = &[
    (&["htm", "html"], "text/html"),
    (&["txt"], "text/plain"),
    (&["css"], "text/css"),
    (&["csv"], "text/csv"),
    (&["js"], "text/javascript"),
    (&["jpeg", "jpg"], "image/jpeg"),
    (&["png"], "image/png"),
    (&["gif"], "image/gif"),
    (&["bmp"], "image/bmp"),
    (&["svg"], "image/svg+xml"),
    (&["json"], "application/json"),
    (&["pdf"], "application/pdf"),
    (&["zip"], "application/zip"),
    (&["lzh"], "application/x-lzh"),
    (&["tar"], "application/x-tar"),
    (&["wasm"], "application/wasm"),
    (&["mp3"], "audio/mp3"),
    (&["mp4"], "video/mp4"),
    (&["mpeg"], "video/mpeg"),
    (&["aac"], "audio/aac"),
    (&["abw"], "application/x-abiword"),
    (&["arc"], "application/x-freearc"),
    (&["avi"], "video/m-msvideo"),
    (&["azw"], "application/vnd.amazon.ebook"),
    (&["bin"], "application/octet-stream"),
    (&["bz"], "application/x-bzip"),
    (&["bz2"], "application/x-bzip2"),
    (&["csh"], "application/x-csh"),
    (&["doc"], "application/msword"),
    (
        &["docx"],
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    ),
    (&["eot"], "application/vnd.ms-fontobject"),
    (&["epub"], "application/epub+zip"),
    (&["gz"], "application/gzip"),
    (&["ico"], "image/vnd.microsoft.icon"),
    (&["ics"], "text/calendar"),
    (&["jar"], "application/java-archive"),
    (&["jpeg", "jpg"], "image/jpeg"),
    (&["mid", "midi"], "audio/midi"),
    (&["mpkg"], "application/vnd.apple.installer+xml"),
    (&["odp"], "application/vnd.oasis.opendocument.presentation"),
    (&["ods"], "application/vnd.oasis.opendocument.spreadsheet"),
    (&["odt"], "application/vnd.oasis.opendocument.text"),
    (&["oga"], "audio/ogg"),
    (&["ogv"], "video/ogg"),
    (&["ogx"], "application/ogg"),
    (&["otf"], "font/otf"),
    (&["ppt"], "application/vnd.ms-powerpoint"),
    (
        &["pptx"],
        "application/vnd.openxmlformats-officedocument.presentationml.presentation",
    ),
    (&["rar"], "application/vnd.rar"),
    (&["rtf"], "application/rtf"),
    (&["sh"], "application/x-sh"),
    (&["swf"], "application/x-shockwave-flash"),
    (&["tif", "tiff"], "image/tiff"),
    (&["ttf"], "font/ttf"),
    (&["vsd"], "application/vnd.visio"),
    (&["wav"], "audio/wav"),
    (&["weba"], "audio/webm"),
    (&["webm"], "video/web"),
    (&["woff"], "font/woff"),
    (&["woff2"], "font/woff2"),
    (&["xhtml"], "application/xhtml+xml"),
    (&["xls"], "application/vnd.ms-excel"),
    (
        &["xlsx"],
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    ),
    (&["xml"], "application/xml"),
    (&["xul"], "application/vnd.mozilla.xul+xml"),
    (&["7z"], "application/x-7z-compressed"),
];

static EXTENSIONS: LazyLock<Vec<&str>> = LazyLock::new(|| {
    EXTENSION_MAP
        .iter()
        .flat_map(|(extensions, _)| *extensions)
        .copied()
        .collect::<Vec<&str>>()
});

fn get_mime_type(path: &Path) -> Option<&str> {
    let ext = path.extension()?.to_str()?;
    EXTENSION_MAP
        .iter()
        .find(|(extensions, _)| extensions.iter().any(|e| e == &ext))
        .map(|(_, mime)| *mime)
}