#![cfg_attr(not(feature = "resource-assembler"), doc = "`resource_assembler`")]
#![cfg_attr(feature = "resource-assembler", doc = "[`resource_assembler`]")]
#[cfg(feature = "resource-assembler")]
pub mod resource_assembler;
mod resource_storage;
pub(crate) use resource_storage::parse_scriptlet_args;
#[doc(inline)]
pub use resource_storage::{
AddResourceError, InMemoryResourceStorage, ResourceImpl, ResourceStorage,
ResourceStorageBackend, ScriptletResourceError,
};
use memchr::memrchr as find_char_reverse;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Copy, Default)]
#[repr(transparent)]
#[serde(transparent)]
pub struct PermissionMask(u8);
impl std::fmt::Debug for PermissionMask {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "PermissionMask({:b})", self.0)
}
}
impl core::ops::BitOr<PermissionMask> for PermissionMask {
type Output = PermissionMask;
fn bitor(self, rhs: PermissionMask) -> Self::Output {
Self(self.0 | rhs.0)
}
}
impl core::ops::BitOrAssign<PermissionMask> for PermissionMask {
fn bitor_assign(&mut self, rhs: PermissionMask) {
self.0 |= rhs.0;
}
}
impl PermissionMask {
pub const fn from_bits(bits: u8) -> Self {
Self(bits)
}
pub fn to_bits(&self) -> u8 {
self.0
}
pub fn is_injectable_by(&self, filter_mask: PermissionMask) -> bool {
!filter_mask.0 & self.0 == 0
}
fn is_default(&self) -> bool {
self.0 == 0
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct Resource {
pub name: String,
#[serde(default)]
pub aliases: Vec<String>,
pub kind: ResourceType,
pub content: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dependencies: Vec<String>,
#[serde(default, skip_serializing_if = "PermissionMask::is_default")]
pub permission: PermissionMask,
}
impl Resource {
#[cfg(test)]
pub fn simple(name: &str, kind: MimeType, content: &str) -> Self {
use base64::{engine::Engine as _, prelude::BASE64_STANDARD};
Self {
name: name.to_string(),
aliases: vec![],
kind: ResourceType::Mime(kind),
content: BASE64_STANDARD.encode(content),
dependencies: vec![],
permission: Default::default(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ResourceType {
Mime(MimeType),
Template,
}
impl ResourceType {
pub fn supports_redirect(&self) -> bool {
!matches!(
self,
ResourceType::Template | ResourceType::Mime(MimeType::FnJavascript)
)
}
pub fn supports_scriptlet_injection(&self) -> bool {
matches!(
self,
ResourceType::Template | ResourceType::Mime(MimeType::ApplicationJavascript)
)
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(into = "&str")]
#[serde(from = "std::borrow::Cow<'static, str>")]
pub enum MimeType {
TextCss,
ImageGif,
TextHtml,
ApplicationJavascript,
ApplicationJson,
AudioMp3,
VideoMp4,
ImagePng,
TextPlain,
TextXml,
FnJavascript,
Unknown,
}
impl MimeType {
pub fn from_extension(resource_path: &str) -> Self {
if let Some(extension_index) = find_char_reverse(b'.', resource_path.as_bytes()) {
match &resource_path[extension_index + 1..] {
"css" => MimeType::TextCss,
"gif" => MimeType::ImageGif,
"html" => MimeType::TextHtml,
"js" => MimeType::ApplicationJavascript,
"json" => MimeType::ApplicationJson,
"mp3" => MimeType::AudioMp3,
"mp4" => MimeType::VideoMp4,
"png" => MimeType::ImagePng,
"txt" => MimeType::TextPlain,
"xml" => MimeType::TextXml,
_ => {
#[cfg(test)]
eprintln!("Unrecognized file extension on: {resource_path:?}");
MimeType::Unknown
}
}
} else {
MimeType::Unknown
}
}
pub fn is_textual(&self) -> bool {
matches!(
self,
Self::ApplicationJavascript
| Self::FnJavascript
| Self::ApplicationJson
| Self::TextCss
| Self::TextPlain
| Self::TextHtml
| Self::TextXml
)
}
pub fn supports_dependencies(&self) -> bool {
matches!(self, Self::ApplicationJavascript | Self::FnJavascript)
}
}
impl From<&str> for MimeType {
fn from(v: &str) -> Self {
match v {
"text/css" => MimeType::TextCss,
"image/gif" => MimeType::ImageGif,
"text/html" => MimeType::TextHtml,
"application/javascript" => MimeType::ApplicationJavascript,
"application/json" => MimeType::ApplicationJson,
"audio/mp3" => MimeType::AudioMp3,
"video/mp4" => MimeType::VideoMp4,
"image/png" => MimeType::ImagePng,
"text/plain" => MimeType::TextPlain,
"text/xml" => MimeType::TextXml,
"fn/javascript" => MimeType::FnJavascript,
_ => MimeType::Unknown,
}
}
}
impl From<&MimeType> for &str {
fn from(v: &MimeType) -> Self {
match v {
MimeType::TextCss => "text/css",
MimeType::ImageGif => "image/gif",
MimeType::TextHtml => "text/html",
MimeType::ApplicationJavascript => "application/javascript",
MimeType::ApplicationJson => "application/json",
MimeType::AudioMp3 => "audio/mp3",
MimeType::VideoMp4 => "video/mp4",
MimeType::ImagePng => "image/png",
MimeType::TextPlain => "text/plain",
MimeType::TextXml => "text/xml",
MimeType::FnJavascript => "fn/javascript",
MimeType::Unknown => "application/octet-stream",
}
}
}
impl From<std::borrow::Cow<'static, str>> for MimeType {
fn from(v: std::borrow::Cow<'static, str>) -> Self {
v.as_ref().into()
}
}
impl From<MimeType> for &str {
fn from(v: MimeType) -> Self {
(&v).into()
}
}
impl std::fmt::Display for MimeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s: &str = self.into();
write!(f, "{s}")
}
}
#[cfg(test)]
mod permission_tests {
use super::*;
#[test]
fn test_permissions() {
{
let resource = PermissionMask(0b00000000);
assert!(resource.is_injectable_by(PermissionMask(0b00000000)));
assert!(resource.is_injectable_by(PermissionMask(0b00000001)));
assert!(resource.is_injectable_by(PermissionMask(0b00000010)));
assert!(resource.is_injectable_by(PermissionMask(0b00000011)));
assert!(resource.is_injectable_by(PermissionMask(0b10000000)));
assert!(resource.is_injectable_by(PermissionMask(0b11111111)));
}
{
let resource = PermissionMask(0b00000001);
assert!(!resource.is_injectable_by(PermissionMask(0b00000000)));
assert!(resource.is_injectable_by(PermissionMask(0b00000001)));
assert!(!resource.is_injectable_by(PermissionMask(0b00000010)));
assert!(resource.is_injectable_by(PermissionMask(0b00000011)));
assert!(!resource.is_injectable_by(PermissionMask(0b10000000)));
assert!(resource.is_injectable_by(PermissionMask(0b11111111)));
}
{
let resource = PermissionMask(0b00000010);
assert!(!resource.is_injectable_by(PermissionMask(0b00000000)));
assert!(!resource.is_injectable_by(PermissionMask(0b00000001)));
assert!(resource.is_injectable_by(PermissionMask(0b00000010)));
assert!(resource.is_injectable_by(PermissionMask(0b00000011)));
assert!(!resource.is_injectable_by(PermissionMask(0b10000000)));
assert!(resource.is_injectable_by(PermissionMask(0b11111111)));
}
{
let resource = PermissionMask(0b00000011);
assert!(!resource.is_injectable_by(PermissionMask(0b00000000)));
assert!(!resource.is_injectable_by(PermissionMask(0b00000001)));
assert!(!resource.is_injectable_by(PermissionMask(0b00000010)));
assert!(resource.is_injectable_by(PermissionMask(0b00000011)));
assert!(!resource.is_injectable_by(PermissionMask(0b10000000)));
assert!(resource.is_injectable_by(PermissionMask(0b11111111)));
}
{
let resource = PermissionMask(0b10000011);
assert!(!resource.is_injectable_by(PermissionMask(0b00000000)));
assert!(!resource.is_injectable_by(PermissionMask(0b00000001)));
assert!(!resource.is_injectable_by(PermissionMask(0b00000010)));
assert!(!resource.is_injectable_by(PermissionMask(0b00000011)));
assert!(!resource.is_injectable_by(PermissionMask(0b10000000)));
assert!(resource.is_injectable_by(PermissionMask(0b11111111)));
}
}
}